This commit is contained in:
CHEVALLIER Abel
2025-11-13 16:23:22 +01:00
parent de9c515a47
commit cb235644dc
34924 changed files with 3811102 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
export interface RequireInfo {
module: string;
export?: string;
isCall?: boolean;
arguments?: KarmaConfigValue[];
}
export type KarmaConfigValue = string | boolean | number | KarmaConfigValue[] | {
[key: string]: KarmaConfigValue;
} | RequireInfo | undefined;
export interface KarmaConfigAnalysis {
settings: Map<string, KarmaConfigValue>;
hasUnsupportedValues: boolean;
}
/**
* Analyzes the content of a Karma configuration file to extract its settings.
*
* @param content The string content of the `karma.conf.js` file.
* @returns An object containing the configuration settings and a flag indicating if unsupported values were found.
*/
export declare function analyzeKarmaConfig(content: string): KarmaConfigAnalysis;

View File

@@ -0,0 +1,131 @@
"use strict";
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.analyzeKarmaConfig = analyzeKarmaConfig;
const typescript_1 = __importDefault(require("../../third_party/github.com/Microsoft/TypeScript/lib/typescript"));
function isRequireInfo(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value) && 'module' in value;
}
function isSupportedPropertyAssignment(prop) {
return (typescript_1.default.isPropertyAssignment(prop) && (typescript_1.default.isIdentifier(prop.name) || typescript_1.default.isStringLiteral(prop.name)));
}
/**
* Analyzes the content of a Karma configuration file to extract its settings.
*
* @param content The string content of the `karma.conf.js` file.
* @returns An object containing the configuration settings and a flag indicating if unsupported values were found.
*/
function analyzeKarmaConfig(content) {
const sourceFile = typescript_1.default.createSourceFile('karma.conf.js', content, typescript_1.default.ScriptTarget.Latest, true);
const settings = new Map();
let hasUnsupportedValues = false;
function visit(node) {
// The Karma configuration is defined within a `config.set({ ... })` call.
if (typescript_1.default.isCallExpression(node) &&
typescript_1.default.isPropertyAccessExpression(node.expression) &&
node.expression.expression.getText(sourceFile) === 'config' &&
node.expression.name.text === 'set' &&
node.arguments.length === 1 &&
typescript_1.default.isObjectLiteralExpression(node.arguments[0])) {
// We found `config.set`, now we extract the properties from the object literal.
for (const prop of node.arguments[0].properties) {
if (isSupportedPropertyAssignment(prop)) {
const key = prop.name.text;
const value = extractValue(prop.initializer);
settings.set(key, value);
}
else {
hasUnsupportedValues = true;
}
}
}
else {
typescript_1.default.forEachChild(node, visit);
}
}
function extractValue(node) {
switch (node.kind) {
case typescript_1.default.SyntaxKind.StringLiteral:
return node.text;
case typescript_1.default.SyntaxKind.NumericLiteral:
return Number(node.text);
case typescript_1.default.SyntaxKind.TrueKeyword:
return true;
case typescript_1.default.SyntaxKind.FalseKeyword:
return false;
case typescript_1.default.SyntaxKind.Identifier: {
const identifier = node.text;
if (identifier === '__dirname' || identifier === '__filename') {
return identifier;
}
break;
}
case typescript_1.default.SyntaxKind.CallExpression: {
const callExpr = node;
// Handle require('...')
if (typescript_1.default.isIdentifier(callExpr.expression) &&
callExpr.expression.text === 'require' &&
callExpr.arguments.length === 1 &&
typescript_1.default.isStringLiteral(callExpr.arguments[0])) {
return { module: callExpr.arguments[0].text };
}
// Handle calls on a require, e.g. require('path').join()
const calleeValue = extractValue(callExpr.expression);
if (isRequireInfo(calleeValue)) {
return {
...calleeValue,
isCall: true,
arguments: callExpr.arguments.map(extractValue),
};
}
break;
}
case typescript_1.default.SyntaxKind.PropertyAccessExpression: {
const propAccessExpr = node;
// Handle config constants like `config.LOG_INFO`
if (typescript_1.default.isIdentifier(propAccessExpr.expression) &&
propAccessExpr.expression.text === 'config') {
return `config.${propAccessExpr.name.text}`;
}
const value = extractValue(propAccessExpr.expression);
if (isRequireInfo(value)) {
const currentExport = value.export
? `${value.export}.${propAccessExpr.name.text}`
: propAccessExpr.name.text;
return { ...value, export: currentExport };
}
break;
}
case typescript_1.default.SyntaxKind.ArrayLiteralExpression:
return node.elements.map(extractValue);
case typescript_1.default.SyntaxKind.ObjectLiteralExpression: {
const obj = {};
for (const prop of node.properties) {
if (isSupportedPropertyAssignment(prop)) {
// Recursively extract values for nested objects.
obj[prop.name.text] = extractValue(prop.initializer);
}
else {
hasUnsupportedValues = true;
}
}
return obj;
}
}
// For complex expressions (like variables) that we don't need to resolve,
// we mark the analysis as potentially incomplete.
hasUnsupportedValues = true;
return undefined;
}
visit(sourceFile);
return { settings, hasUnsupportedValues };
}

View File

@@ -0,0 +1,63 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import { KarmaConfigAnalysis, KarmaConfigValue } from './karma-config-analyzer';
/**
* Represents the difference between two Karma configurations.
*/
export interface KarmaConfigDiff {
/** A map of settings that were added in the project's configuration. */
added: Map<string, KarmaConfigValue>;
/** A map of settings that were removed from the project's configuration. */
removed: Map<string, KarmaConfigValue>;
/** A map of settings that were modified between the two configurations. */
modified: Map<string, {
projectValue: KarmaConfigValue;
defaultValue: KarmaConfigValue;
}>;
/** A boolean indicating if the comparison is reliable (i.e., no unsupported values were found). */
isReliable: boolean;
}
/**
* Generates the default Karma configuration file content as a string.
* @param relativePathToWorkspaceRoot The relative path from the Karma config file to the workspace root.
* @param projectName The name of the project.
* @param needDevkitPlugin A boolean indicating if the devkit plugin is needed.
* @returns The content of the default `karma.conf.js` file.
*/
export declare function generateDefaultKarmaConfig(relativePathToWorkspaceRoot: string, projectName: string, needDevkitPlugin: boolean): Promise<string>;
/**
* Compares two Karma configuration analyses and returns the difference.
* @param projectAnalysis The analysis of the project's configuration.
* @param defaultAnalysis The analysis of the default configuration to compare against.
* @returns A diff object representing the changes between the two configurations.
*/
export declare function compareKarmaConfigs(projectAnalysis: KarmaConfigAnalysis, defaultAnalysis: KarmaConfigAnalysis): KarmaConfigDiff;
/**
* Checks if there are any differences in the provided Karma configuration diff.
* @param diff The Karma configuration diff object to check.
* @returns True if there are any differences; false otherwise.
*/
export declare function hasDifferences(diff: KarmaConfigDiff): boolean;
/**
* Compares a project's Karma configuration with the default configuration.
* @param projectConfigContent The content of the project's `karma.conf.js` file.
* @param projectRoot The root directory of the project.
* @param needDevkitPlugin A boolean indicating if the devkit plugin is needed for the default config.
* @param karmaConfigPath The path to the Karma configuration file, used to resolve relative paths.
* @returns A diff object representing the changes.
*/
export declare function compareKarmaConfigToDefault(projectConfigContent: string, projectName: string, karmaConfigPath: string, needDevkitPlugin: boolean): Promise<KarmaConfigDiff>;
/**
* Compares a project's Karma configuration with the default configuration.
* @param projectAnalysis The analysis of the project's configuration.
* @param projectRoot The root directory of the project.
* @param needDevkitPlugin A boolean indicating if the devkit plugin is needed for the default config.
* @param karmaConfigPath The path to the Karma configuration file, used to resolve relative paths.
* @returns A diff object representing the changes.
*/
export declare function compareKarmaConfigToDefault(projectAnalysis: KarmaConfigAnalysis, projectName: string, karmaConfigPath: string, needDevkitPlugin: boolean): Promise<KarmaConfigDiff>;

View File

@@ -0,0 +1,89 @@
"use strict";
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateDefaultKarmaConfig = generateDefaultKarmaConfig;
exports.compareKarmaConfigs = compareKarmaConfigs;
exports.hasDifferences = hasDifferences;
exports.compareKarmaConfigToDefault = compareKarmaConfigToDefault;
const promises_1 = require("node:fs/promises");
const posix_1 = __importDefault(require("node:path/posix"));
const node_util_1 = require("node:util");
const paths_1 = require("../../utility/paths");
const karma_config_analyzer_1 = require("./karma-config-analyzer");
/**
* Generates the default Karma configuration file content as a string.
* @param relativePathToWorkspaceRoot The relative path from the Karma config file to the workspace root.
* @param projectName The name of the project.
* @param needDevkitPlugin A boolean indicating if the devkit plugin is needed.
* @returns The content of the default `karma.conf.js` file.
*/
async function generateDefaultKarmaConfig(relativePathToWorkspaceRoot, projectName, needDevkitPlugin) {
const templatePath = require.resolve('../../config/files/karma.conf.js.template');
let template = await (0, promises_1.readFile)(templatePath, 'utf-8');
// TODO: Replace this with the actual schematic templating logic.
template = template
.replace(/<%= relativePathToWorkspaceRoot %>/g, posix_1.default.normalize(relativePathToWorkspaceRoot).replace(/\\/g, '/'))
.replace(/<%= folderName %>/g, projectName);
const devkitPluginRegex = /<% if \(needDevkitPlugin\) { %>(.*?)<% } %>/gs;
const replacement = needDevkitPlugin ? '$1' : '';
template = template.replace(devkitPluginRegex, replacement);
return template;
}
/**
* Compares two Karma configuration analyses and returns the difference.
* @param projectAnalysis The analysis of the project's configuration.
* @param defaultAnalysis The analysis of the default configuration to compare against.
* @returns A diff object representing the changes between the two configurations.
*/
function compareKarmaConfigs(projectAnalysis, defaultAnalysis) {
const added = new Map();
const removed = new Map();
const modified = new Map();
const allKeys = new Set([...projectAnalysis.settings.keys(), ...defaultAnalysis.settings.keys()]);
for (const key of allKeys) {
const projectValue = projectAnalysis.settings.get(key);
const defaultValue = defaultAnalysis.settings.get(key);
if (projectValue !== undefined && defaultValue === undefined) {
added.set(key, projectValue);
}
else if (projectValue === undefined && defaultValue !== undefined) {
removed.set(key, defaultValue);
}
else if (projectValue !== undefined && defaultValue !== undefined) {
if (!(0, node_util_1.isDeepStrictEqual)(projectValue, defaultValue)) {
modified.set(key, { projectValue, defaultValue });
}
}
}
return {
added,
removed,
modified,
isReliable: !projectAnalysis.hasUnsupportedValues && !defaultAnalysis.hasUnsupportedValues,
};
}
/**
* Checks if there are any differences in the provided Karma configuration diff.
* @param diff The Karma configuration diff object to check.
* @returns True if there are any differences; false otherwise.
*/
function hasDifferences(diff) {
return diff.added.size > 0 || diff.removed.size > 0 || diff.modified.size > 0;
}
async function compareKarmaConfigToDefault(projectConfigOrAnalysis, projectName, karmaConfigPath, needDevkitPlugin) {
const projectAnalysis = typeof projectConfigOrAnalysis === 'string'
? (0, karma_config_analyzer_1.analyzeKarmaConfig)(projectConfigOrAnalysis)
: projectConfigOrAnalysis;
const defaultContent = await generateDefaultKarmaConfig((0, paths_1.relativePathToWorkspaceRoot)(posix_1.default.dirname(karmaConfigPath)), projectName, needDevkitPlugin);
const defaultAnalysis = (0, karma_config_analyzer_1.analyzeKarmaConfig)(defaultContent);
return compareKarmaConfigs(projectAnalysis, defaultAnalysis);
}

View File

@@ -0,0 +1,9 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import type { Rule } from '@angular-devkit/schematics';
export default function (): Rule;

View File

@@ -0,0 +1,62 @@
"use strict";
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = default_1;
const workspace_1 = require("../../utility/workspace");
const workspace_models_1 = require("../../utility/workspace-models");
const karma_config_analyzer_1 = require("./karma-config-analyzer");
const karma_config_comparer_1 = require("./karma-config-comparer");
function updateProjects(tree) {
return (0, workspace_1.updateWorkspace)(async (workspace) => {
const removableKarmaConfigs = new Map();
for (const [projectName, project] of workspace.projects) {
for (const [, target] of project.targets) {
let needDevkitPlugin = false;
switch (target.builder) {
case workspace_models_1.Builders.Karma:
needDevkitPlugin = true;
break;
case workspace_models_1.Builders.BuildKarma:
break;
default:
continue;
}
for (const [, options] of (0, workspace_1.allTargetOptions)(target, false)) {
const karmaConfig = options['karmaConfig'];
if (typeof karmaConfig !== 'string') {
continue;
}
let isRemovable = removableKarmaConfigs.get(karmaConfig);
if (isRemovable === undefined && tree.exists(karmaConfig)) {
const content = tree.readText(karmaConfig);
const analysis = (0, karma_config_analyzer_1.analyzeKarmaConfig)(content);
if (analysis.hasUnsupportedValues) {
// Cannot safely determine if the file is removable.
isRemovable = false;
}
else {
const diff = await (0, karma_config_comparer_1.compareKarmaConfigToDefault)(analysis, projectName, karmaConfig, needDevkitPlugin);
isRemovable = !(0, karma_config_comparer_1.hasDifferences)(diff) && diff.isReliable;
}
removableKarmaConfigs.set(karmaConfig, isRemovable);
if (isRemovable) {
tree.delete(karmaConfig);
}
}
if (isRemovable) {
delete options['karmaConfig'];
}
}
}
}
});
}
function default_1() {
return updateProjects;
}