Skip to content

Commit 766d4a0

Browse files
committed
feat(@schematics/angular): add migration to remove require calls from karma builder main file
With the recent changes in build-angular the `require.context` calls have become unneeded.
1 parent 2624d89 commit 766d4a0

File tree

3 files changed

+257
-0
lines changed

3 files changed

+257
-0
lines changed

packages/schematics/angular/migrations/migration-collection.json

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
"version": "15.0.0",
2020
"factory": "./update-15/update-workspace-config",
2121
"description": "Remove options from 'angular.json' that are no longer supported by the official builders."
22+
},
23+
"update-karma-main-file": {
24+
"version": "15.0.0",
25+
"factory": "./update-15/update-karma-main-file",
26+
"description": "Remove no longer needed require calls in Karma builder main file."
2227
}
2328
}
2429
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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.io/license
7+
*/
8+
9+
import { Rule, Tree } from '@angular-devkit/schematics';
10+
import * as ts from '../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
11+
import { readWorkspace } from '../../utility';
12+
import { allTargetOptions } from '../../utility/workspace';
13+
import { Builders } from '../../utility/workspace-models';
14+
15+
export default function (): Rule {
16+
return async (host) => {
17+
for (const file of await findTestMainFiles(host)) {
18+
updateTestFile(host, file);
19+
}
20+
};
21+
}
22+
23+
async function findTestMainFiles(host: Tree): Promise<Set<string>> {
24+
const testFiles = new Set<string>();
25+
const workspace = await readWorkspace(host);
26+
27+
// find all test.ts files.
28+
for (const project of workspace.projects.values()) {
29+
for (const target of project.targets.values()) {
30+
if (target.builder !== Builders.Karma) {
31+
continue;
32+
}
33+
34+
for (const [, options] of allTargetOptions(target)) {
35+
if (typeof options.main === 'string') {
36+
testFiles.add(options.main);
37+
}
38+
}
39+
}
40+
}
41+
42+
return testFiles;
43+
}
44+
45+
function updateTestFile(host: Tree, file: string): void {
46+
const content = host.readText(file);
47+
if (!content.includes('require.context')) {
48+
return;
49+
}
50+
51+
const sourceFile = ts.createSourceFile(
52+
file,
53+
content.replace(/^\uFEFF/, ''),
54+
ts.ScriptTarget.Latest,
55+
true,
56+
);
57+
58+
const usedVariableNames = new Set<string>();
59+
const recorder = host.beginUpdate(sourceFile.fileName);
60+
61+
ts.forEachChild(sourceFile, (node) => {
62+
if (ts.isVariableStatement(node)) {
63+
const variableDeclaration = node.declarationList.declarations[0];
64+
65+
if (ts.getModifiers(node)?.some((m) => m.kind === ts.SyntaxKind.DeclareKeyword)) {
66+
// `declare const require`
67+
if (variableDeclaration.name.getText() !== 'require') {
68+
return;
69+
}
70+
} else {
71+
// `const context = require.context('./', true, /\.spec\.ts$/);`
72+
if (!variableDeclaration.initializer?.getText().startsWith('require.context')) {
73+
return;
74+
}
75+
76+
// add variable name as used.
77+
usedVariableNames.add(variableDeclaration.name.getText());
78+
}
79+
80+
// Delete node.
81+
recorder.remove(node.getFullStart(), node.getFullWidth());
82+
}
83+
84+
if (
85+
usedVariableNames.size &&
86+
ts.isExpressionStatement(node) && // context.keys().map(context);
87+
ts.isCallExpression(node.expression) && // context.keys().map(context);
88+
ts.isPropertyAccessExpression(node.expression.expression) && // context.keys().map
89+
ts.isCallExpression(node.expression.expression.expression) && // context.keys()
90+
ts.isPropertyAccessExpression(node.expression.expression.expression.expression) && // context.keys
91+
ts.isIdentifier(node.expression.expression.expression.expression.expression) && // context
92+
usedVariableNames.has(node.expression.expression.expression.expression.expression.getText())
93+
) {
94+
// `context.keys().map(context);`
95+
// `context.keys().forEach(context);`
96+
recorder.remove(node.getFullStart(), node.getFullWidth());
97+
}
98+
});
99+
100+
host.commitUpdate(recorder);
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
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.io/license
7+
*/
8+
9+
import { tags } from '@angular-devkit/core';
10+
import { EmptyTree } from '@angular-devkit/schematics';
11+
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
12+
import { Builders, ProjectType, WorkspaceSchema } from '../../utility/workspace-models';
13+
14+
function createWorkspace(tree: UnitTestTree): void {
15+
const angularConfig: WorkspaceSchema = {
16+
version: 1,
17+
projects: {
18+
app: {
19+
root: '',
20+
sourceRoot: 'src',
21+
projectType: ProjectType.Application,
22+
prefix: 'app',
23+
architect: {
24+
test: {
25+
builder: Builders.Karma,
26+
options: {
27+
main: 'test.ts',
28+
karmaConfig: './karma.config.js',
29+
tsConfig: 'test-spec.json',
30+
},
31+
configurations: {
32+
production: {
33+
main: 'test-multiple-context.ts',
34+
},
35+
},
36+
},
37+
},
38+
},
39+
},
40+
};
41+
42+
tree.create('/angular.json', JSON.stringify(angularConfig, undefined, 2));
43+
tree.create(
44+
'test.ts',
45+
tags.stripIndents`
46+
import { getTestBed } from '@angular/core/testing';
47+
import {
48+
BrowserDynamicTestingModule,
49+
platformBrowserDynamicTesting
50+
} from '@angular/platform-browser-dynamic/testing';
51+
52+
declare const require: {
53+
context(path: string, deep?: boolean, filter?: RegExp): {
54+
<T>(id: string): T;
55+
keys(): string[];
56+
};
57+
};
58+
59+
// First, initialize the Angular testing environment.
60+
getTestBed().initTestEnvironment(
61+
BrowserDynamicTestingModule,
62+
platformBrowserDynamicTesting(),
63+
);
64+
65+
// Then we find all the tests.
66+
const context = require.context('./', true, /\.spec\.ts$/);
67+
// And load the modules.
68+
context.keys().map(context);
69+
`,
70+
);
71+
72+
tree.create(
73+
'test-multiple-context.ts',
74+
tags.stripIndents`
75+
import { getTestBed } from '@angular/core/testing';
76+
import {
77+
BrowserDynamicTestingModule,
78+
platformBrowserDynamicTesting
79+
} from '@angular/platform-browser-dynamic/testing';
80+
81+
declare const require: {
82+
context(path: string, deep?: boolean, filter?: RegExp): {
83+
<T>(id: string): T;
84+
keys(): string[];
85+
};
86+
};
87+
88+
// First, initialize the Angular testing environment.
89+
getTestBed().initTestEnvironment(
90+
BrowserDynamicTestingModule,
91+
platformBrowserDynamicTesting(),
92+
);
93+
94+
// Then we find all the tests.
95+
const context1 = require.context('./', true, /\.spec\.ts$/);
96+
const context2 = require.context('./', true, /\.spec\.ts$/);
97+
// And load the modules.
98+
context2.keys().forEach(context2);
99+
context1.keys().map(context1);
100+
`,
101+
);
102+
}
103+
104+
describe(`Migration to karma builder main file (test.ts)`, () => {
105+
const schematicName = 'update-karma-main-file';
106+
107+
const schematicRunner = new SchematicTestRunner(
108+
'migrations',
109+
require.resolve('../migration-collection.json'),
110+
);
111+
112+
let tree: UnitTestTree;
113+
beforeEach(() => {
114+
tree = new UnitTestTree(new EmptyTree());
115+
createWorkspace(tree);
116+
});
117+
118+
it(`should remove 'declare const require' and 'require.context' usages`, async () => {
119+
const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise();
120+
expect(newTree.readText('test.ts')).toBe(tags.stripIndents`
121+
import { getTestBed } from '@angular/core/testing';
122+
import {
123+
BrowserDynamicTestingModule,
124+
platformBrowserDynamicTesting
125+
} from '@angular/platform-browser-dynamic/testing';
126+
127+
// First, initialize the Angular testing environment.
128+
getTestBed().initTestEnvironment(
129+
BrowserDynamicTestingModule,
130+
platformBrowserDynamicTesting(),
131+
);
132+
`);
133+
});
134+
135+
it(`should remove multiple 'require.context' usages`, async () => {
136+
const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise();
137+
expect(newTree.readText('test-multiple-context.ts')).toBe(tags.stripIndents`
138+
import { getTestBed } from '@angular/core/testing';
139+
import {
140+
BrowserDynamicTestingModule,
141+
platformBrowserDynamicTesting
142+
} from '@angular/platform-browser-dynamic/testing';
143+
144+
// First, initialize the Angular testing environment.
145+
getTestBed().initTestEnvironment(
146+
BrowserDynamicTestingModule,
147+
platformBrowserDynamicTesting(),
148+
);
149+
`);
150+
});
151+
});

0 commit comments

Comments
 (0)