diff --git a/packages/schematics/angular/migrations/migration-collection.json b/packages/schematics/angular/migrations/migration-collection.json index d402927d5f6b..40ec225245ed 100644 --- a/packages/schematics/angular/migrations/migration-collection.json +++ b/packages/schematics/angular/migrations/migration-collection.json @@ -19,6 +19,11 @@ "version": "15.0.0", "factory": "./update-15/update-workspace-config", "description": "Remove options from 'angular.json' that are no longer supported by the official builders." + }, + "update-karma-main-file": { + "version": "15.0.0", + "factory": "./update-15/update-karma-main-file", + "description": "Remove no longer needed require calls in Karma builder main file." } } } diff --git a/packages/schematics/angular/migrations/update-15/update-karma-main-file.ts b/packages/schematics/angular/migrations/update-15/update-karma-main-file.ts new file mode 100644 index 000000000000..0a7e08ba10f1 --- /dev/null +++ b/packages/schematics/angular/migrations/update-15/update-karma-main-file.ts @@ -0,0 +1,101 @@ +/** + * @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.io/license + */ + +import { Rule, Tree } from '@angular-devkit/schematics'; +import * as ts from '../../third_party/github.com/Microsoft/TypeScript/lib/typescript'; +import { readWorkspace } from '../../utility'; +import { allTargetOptions } from '../../utility/workspace'; +import { Builders } from '../../utility/workspace-models'; + +export default function (): Rule { + return async (host) => { + for (const file of await findTestMainFiles(host)) { + updateTestFile(host, file); + } + }; +} + +async function findTestMainFiles(host: Tree): Promise> { + const testFiles = new Set(); + const workspace = await readWorkspace(host); + + // find all test.ts files. + for (const project of workspace.projects.values()) { + for (const target of project.targets.values()) { + if (target.builder !== Builders.Karma) { + continue; + } + + for (const [, options] of allTargetOptions(target)) { + if (typeof options.main === 'string') { + testFiles.add(options.main); + } + } + } + } + + return testFiles; +} + +function updateTestFile(host: Tree, file: string): void { + const content = host.readText(file); + if (!content.includes('require.context')) { + return; + } + + const sourceFile = ts.createSourceFile( + file, + content.replace(/^\uFEFF/, ''), + ts.ScriptTarget.Latest, + true, + ); + + const usedVariableNames = new Set(); + const recorder = host.beginUpdate(sourceFile.fileName); + + ts.forEachChild(sourceFile, (node) => { + if (ts.isVariableStatement(node)) { + const variableDeclaration = node.declarationList.declarations[0]; + + if (ts.getModifiers(node)?.some((m) => m.kind === ts.SyntaxKind.DeclareKeyword)) { + // `declare const require` + if (variableDeclaration.name.getText() !== 'require') { + return; + } + } else { + // `const context = require.context('./', true, /\.spec\.ts$/);` + if (!variableDeclaration.initializer?.getText().startsWith('require.context')) { + return; + } + + // add variable name as used. + usedVariableNames.add(variableDeclaration.name.getText()); + } + + // Delete node. + recorder.remove(node.getFullStart(), node.getFullWidth()); + } + + if ( + usedVariableNames.size && + ts.isExpressionStatement(node) && // context.keys().map(context); + ts.isCallExpression(node.expression) && // context.keys().map(context); + ts.isPropertyAccessExpression(node.expression.expression) && // context.keys().map + ts.isCallExpression(node.expression.expression.expression) && // context.keys() + ts.isPropertyAccessExpression(node.expression.expression.expression.expression) && // context.keys + ts.isIdentifier(node.expression.expression.expression.expression.expression) && // context + usedVariableNames.has(node.expression.expression.expression.expression.expression.getText()) + ) { + // `context.keys().map(context);` + // `context.keys().forEach(context);` + recorder.remove(node.getFullStart(), node.getFullWidth()); + } + }); + + host.commitUpdate(recorder); +} diff --git a/packages/schematics/angular/migrations/update-15/update-karma-main-file_spec.ts b/packages/schematics/angular/migrations/update-15/update-karma-main-file_spec.ts new file mode 100644 index 000000000000..d8da17b2a1c6 --- /dev/null +++ b/packages/schematics/angular/migrations/update-15/update-karma-main-file_spec.ts @@ -0,0 +1,151 @@ +/** + * @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.io/license + */ + +import { tags } from '@angular-devkit/core'; +import { EmptyTree } from '@angular-devkit/schematics'; +import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing'; +import { Builders, ProjectType, WorkspaceSchema } from '../../utility/workspace-models'; + +function createWorkspace(tree: UnitTestTree): void { + const angularConfig: WorkspaceSchema = { + version: 1, + projects: { + app: { + root: '', + sourceRoot: 'src', + projectType: ProjectType.Application, + prefix: 'app', + architect: { + test: { + builder: Builders.Karma, + options: { + main: 'test.ts', + karmaConfig: './karma.config.js', + tsConfig: 'test-spec.json', + }, + configurations: { + production: { + main: 'test-multiple-context.ts', + }, + }, + }, + }, + }, + }, + }; + + tree.create('/angular.json', JSON.stringify(angularConfig, undefined, 2)); + tree.create( + 'test.ts', + tags.stripIndents` + import { getTestBed } from '@angular/core/testing'; + import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting + } from '@angular/platform-browser-dynamic/testing'; + + declare const require: { + context(path: string, deep?: boolean, filter?: RegExp): { + (id: string): T; + keys(): string[]; + }; + }; + + // First, initialize the Angular testing environment. + getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), + ); + + // Then we find all the tests. + const context = require.context('./', true, /\.spec\.ts$/); + // And load the modules. + context.keys().map(context); + `, + ); + + tree.create( + 'test-multiple-context.ts', + tags.stripIndents` + import { getTestBed } from '@angular/core/testing'; + import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting + } from '@angular/platform-browser-dynamic/testing'; + + declare const require: { + context(path: string, deep?: boolean, filter?: RegExp): { + (id: string): T; + keys(): string[]; + }; + }; + + // First, initialize the Angular testing environment. + getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), + ); + + // Then we find all the tests. + const context1 = require.context('./', true, /\.spec\.ts$/); + const context2 = require.context('./', true, /\.spec\.ts$/); + // And load the modules. + context2.keys().forEach(context2); + context1.keys().map(context1); + `, + ); +} + +describe(`Migration to karma builder main file (test.ts)`, () => { + const schematicName = 'update-karma-main-file'; + + const schematicRunner = new SchematicTestRunner( + 'migrations', + require.resolve('../migration-collection.json'), + ); + + let tree: UnitTestTree; + beforeEach(() => { + tree = new UnitTestTree(new EmptyTree()); + createWorkspace(tree); + }); + + it(`should remove 'declare const require' and 'require.context' usages`, async () => { + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + expect(newTree.readText('test.ts')).toBe(tags.stripIndents` + import { getTestBed } from '@angular/core/testing'; + import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting + } from '@angular/platform-browser-dynamic/testing'; + + // First, initialize the Angular testing environment. + getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), + ); + `); + }); + + it(`should remove multiple 'require.context' usages`, async () => { + const newTree = await schematicRunner.runSchematicAsync(schematicName, {}, tree).toPromise(); + expect(newTree.readText('test-multiple-context.ts')).toBe(tags.stripIndents` + import { getTestBed } from '@angular/core/testing'; + import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting + } from '@angular/platform-browser-dynamic/testing'; + + // First, initialize the Angular testing environment. + getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), + ); + `); + }); +});