Skip to content

Commit

Permalink
feat(@schematics/angular): add migration to remove require calls from…
Browse files Browse the repository at this point in the history
… karma builder main file

With the recent changes in build-angular the `require.context` calls have become unneeded.
  • Loading branch information
alan-agius4 committed Sep 26, 2022
1 parent 2624d89 commit 766d4a0
Show file tree
Hide file tree
Showing 3 changed files with 257 additions and 0 deletions.
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(),
);
`);
});
});

0 comments on commit 766d4a0

Please sign in to comment.