Skip to content

feat(@schematics/angular): add migration to remove require calls from karma builder main file #23955

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -342,8 +342,7 @@ export async function getCommonConfig(wco: WebpackConfigOptions): Promise<Config
strictExportPresence: true,
parser: {
javascript: {
// TODO(alanagius): disable the below once we have a migration to remove `require.context` from test.ts file in users projects.
requireContext: true,
requireContext: false,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this is not an officially supported feature, we might want to add a breaking change notice to let users know that this Webpack specific feature will definitely not work anymore. (And maybe reiterate that Webpack specific features are not guaranteed to work in the future.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let’s do that.

// Disable auto URL asset module creation. This doesn't effect `new Worker(new URL(...))`
// https://webpack.js.org/guides/asset-modules/#url-assets
url: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Set<string>> {
const testFiles = new Set<string>();
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<string>();
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);
}
Original file line number Diff line number Diff line change
@@ -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): {
<T>(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): {
<T>(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(),
);
`);
});
});