Skip to content

Commit fd04756

Browse files
committed
refactor(@schematics/angular): add Karma configuration analyzer and comparer
Introduces a new utility for analyzing and comparing Karma configuration files. This is a foundational step for migrating from Karma to other testing frameworks like Jest and Vitest, as it allows the migration schematic to understand the user's existing setup. The new `analyzeKarmaConfig` function uses TypeScript's AST parser to safely extract settings from a `karma.conf.js` file. It can identify common patterns, including `require` calls, and flags configurations that are too complex for static analysis. The `compareKarmaConfigs` function provides the ability to diff a user's Karma configuration against a default template. This will be used to determine which custom settings need to be migrated. Known limitations of the analyzer: - It does not resolve variables or complex expressions. Any value that is not a literal (string, number, boolean, array, object) or a direct `require` call will be marked as an unsupported value. - It does not support Karma configuration files that use ES Modules (import/export syntax).
1 parent b6b2578 commit fd04756

File tree

4 files changed

+847
-0
lines changed

4 files changed

+847
-0
lines changed
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import ts from '../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
10+
11+
export interface RequireInfo {
12+
module: string;
13+
export?: string;
14+
isCall?: boolean;
15+
arguments?: KarmaConfigValue[];
16+
}
17+
18+
export type KarmaConfigValue =
19+
| string
20+
| boolean
21+
| number
22+
| KarmaConfigValue[]
23+
| { [key: string]: KarmaConfigValue }
24+
| RequireInfo
25+
| undefined;
26+
27+
export interface KarmaConfigAnalysis {
28+
settings: Map<string, KarmaConfigValue>;
29+
hasUnsupportedValues: boolean;
30+
}
31+
32+
function isRequireInfo(value: KarmaConfigValue): value is RequireInfo {
33+
return typeof value === 'object' && value !== null && !Array.isArray(value) && 'module' in value;
34+
}
35+
36+
function isSupportedPropertyAssignment(
37+
prop: ts.ObjectLiteralElementLike,
38+
): prop is ts.PropertyAssignment & { name: ts.Identifier | ts.StringLiteral } {
39+
return (
40+
ts.isPropertyAssignment(prop) && (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name))
41+
);
42+
}
43+
44+
/**
45+
* Analyzes the content of a Karma configuration file to extract its settings.
46+
*
47+
* @param content The string content of the `karma.conf.js` file.
48+
* @returns An object containing the configuration settings and a flag indicating if unsupported values were found.
49+
*/
50+
export function analyzeKarmaConfig(content: string): KarmaConfigAnalysis {
51+
const sourceFile = ts.createSourceFile('karma.conf.js', content, ts.ScriptTarget.Latest, true);
52+
const settings = new Map<string, KarmaConfigValue>();
53+
let hasUnsupportedValues = false;
54+
55+
function visit(node: ts.Node) {
56+
// The Karma configuration is defined within a `config.set({ ... })` call.
57+
if (
58+
ts.isCallExpression(node) &&
59+
ts.isPropertyAccessExpression(node.expression) &&
60+
node.expression.expression.getText(sourceFile) === 'config' &&
61+
node.expression.name.text === 'set' &&
62+
node.arguments.length === 1 &&
63+
ts.isObjectLiteralExpression(node.arguments[0])
64+
) {
65+
// We found `config.set`, now we extract the properties from the object literal.
66+
for (const prop of node.arguments[0].properties) {
67+
if (isSupportedPropertyAssignment(prop)) {
68+
const key = prop.name.text;
69+
const value = extractValue(prop.initializer);
70+
settings.set(key, value);
71+
} else {
72+
hasUnsupportedValues = true;
73+
}
74+
}
75+
} else {
76+
ts.forEachChild(node, visit);
77+
}
78+
}
79+
80+
function extractValue(node: ts.Expression): KarmaConfigValue {
81+
switch (node.kind) {
82+
case ts.SyntaxKind.StringLiteral:
83+
return (node as ts.StringLiteral).text;
84+
case ts.SyntaxKind.NumericLiteral:
85+
return Number((node as ts.NumericLiteral).text);
86+
case ts.SyntaxKind.TrueKeyword:
87+
return true;
88+
case ts.SyntaxKind.FalseKeyword:
89+
return false;
90+
case ts.SyntaxKind.Identifier: {
91+
const identifier = (node as ts.Identifier).text;
92+
if (identifier === '__dirname' || identifier === '__filename') {
93+
return identifier;
94+
}
95+
break;
96+
}
97+
case ts.SyntaxKind.CallExpression: {
98+
const callExpr = node as ts.CallExpression;
99+
// Handle require('...')
100+
if (
101+
ts.isIdentifier(callExpr.expression) &&
102+
callExpr.expression.text === 'require' &&
103+
callExpr.arguments.length === 1 &&
104+
ts.isStringLiteral(callExpr.arguments[0])
105+
) {
106+
return { module: callExpr.arguments[0].text };
107+
}
108+
109+
// Handle calls on a require, e.g. require('path').join()
110+
const calleeValue = extractValue(callExpr.expression);
111+
if (isRequireInfo(calleeValue)) {
112+
return {
113+
...calleeValue,
114+
isCall: true,
115+
arguments: callExpr.arguments.map(extractValue),
116+
};
117+
}
118+
break;
119+
}
120+
case ts.SyntaxKind.PropertyAccessExpression: {
121+
const propAccessExpr = node as ts.PropertyAccessExpression;
122+
const value = extractValue(propAccessExpr.expression);
123+
if (isRequireInfo(value)) {
124+
const currentExport = value.export
125+
? `${value.export}.${propAccessExpr.name.text}`
126+
: propAccessExpr.name.text;
127+
128+
return { ...value, export: currentExport };
129+
}
130+
break;
131+
}
132+
case ts.SyntaxKind.ArrayLiteralExpression:
133+
return (node as ts.ArrayLiteralExpression).elements.map(extractValue);
134+
case ts.SyntaxKind.ObjectLiteralExpression: {
135+
const obj: { [key: string]: KarmaConfigValue } = {};
136+
for (const prop of (node as ts.ObjectLiteralExpression).properties) {
137+
if (isSupportedPropertyAssignment(prop)) {
138+
// Recursively extract values for nested objects.
139+
obj[prop.name.text] = extractValue(prop.initializer);
140+
} else {
141+
hasUnsupportedValues = true;
142+
}
143+
}
144+
145+
return obj;
146+
}
147+
}
148+
149+
// For complex expressions (like variables) that we don't need to resolve,
150+
// we mark the analysis as potentially incomplete.
151+
hasUnsupportedValues = true;
152+
153+
return undefined;
154+
}
155+
156+
visit(sourceFile);
157+
158+
return { settings, hasUnsupportedValues };
159+
}

0 commit comments

Comments
 (0)