Skip to content

Commit 605c536

Browse files
devversionatscott
authored andcommitted
feat(core): add migration to remove moduleId references (#49496)
Removes all `moduleId:` property references in `@Directive` and `@Component`. PR Close #49496
1 parent 316c91b commit 605c536

File tree

9 files changed

+367
-28
lines changed

9 files changed

+367
-28
lines changed

packages/core/schematics/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pkg_npm(
1818
visibility = ["//packages/core:__pkg__"],
1919
deps = [
2020
"//packages/core/schematics/migrations/relative-link-resolution:bundle",
21+
"//packages/core/schematics/migrations/remove-module-id:bundle",
2122
"//packages/core/schematics/migrations/router-link-with-href:bundle",
2223
"//packages/core/schematics/ng-generate/standalone-migration:bundle",
2324
],

packages/core/schematics/migrations.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
"version": "15.0.0",
1010
"description": "In Angular version 15, the deprecated `relativeLinkResolution` config parameter of the Router is removed. This migration removes all `relativeLinkResolution` fields from the Router config objects.",
1111
"factory": "./migrations/relative-link-resolution/bundle"
12+
},
13+
"migration-v16-remove-module-id": {
14+
"version": "16.0.0",
15+
"description": "As of Angular v16, the `moduleId` property of `@Component` is deprecated as it no longer has any effect.",
16+
"factory": "./migrations/remove-module-id/bundle"
1217
}
1318
}
1419
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
load("//tools:defaults.bzl", "esbuild", "ts_library")
2+
3+
package(
4+
default_visibility = [
5+
"//packages/core/schematics:__pkg__",
6+
"//packages/core/schematics/migrations/google3:__pkg__",
7+
"//packages/core/schematics/test:__pkg__",
8+
],
9+
)
10+
11+
ts_library(
12+
name = "remove-module-id",
13+
srcs = glob(["**/*.ts"]),
14+
tsconfig = "//packages/core/schematics:tsconfig.json",
15+
deps = [
16+
"//packages/core/schematics/utils",
17+
"@npm//@angular-devkit/schematics",
18+
"@npm//@types/node",
19+
"@npm//typescript",
20+
],
21+
)
22+
23+
esbuild(
24+
name = "bundle",
25+
entry_point = ":index.ts",
26+
external = [
27+
"@angular-devkit/*",
28+
"typescript",
29+
],
30+
format = "cjs",
31+
platform = "node",
32+
deps = [":remove-module-id"],
33+
)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
## RemoveModuleId migration
2+
3+
As of Angular version 9, the `moduleId` property has no effect. This migration
4+
removes the field from all `@Directive` or `@Component` decorators.
5+
6+
#### Before
7+
```ts
8+
@Component({
9+
moduleId: <..>,
10+
template: 'Works',
11+
})
12+
export class MyComponent {}
13+
```
14+
15+
#### After
16+
```ts
17+
@Component({
18+
template: 'Works',
19+
})
20+
export class MyComponent {}
21+
```
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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, SchematicsException, Tree} from '@angular-devkit/schematics';
10+
import {relative} from 'path';
11+
import ts from 'typescript';
12+
13+
import {extractAngularClassMetadata} from '../../utils/extract_metadata';
14+
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
15+
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
16+
import {getPropertyNameText} from '../../utils/typescript/property_name';
17+
18+
export default function(): Rule {
19+
return async (tree: Tree) => {
20+
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
21+
const basePath = process.cwd();
22+
const allPaths = [...buildPaths, ...testPaths];
23+
24+
if (!allPaths.length) {
25+
throw new SchematicsException(
26+
'Could not find any tsconfig file. Cannot run the `RemoveModuleId` migration.');
27+
}
28+
29+
for (const tsconfigPath of allPaths) {
30+
runRemoveModuleIdMigration(tree, tsconfigPath, basePath);
31+
}
32+
};
33+
}
34+
35+
function runRemoveModuleIdMigration(tree: Tree, tsconfigPath: string, basePath: string) {
36+
const program = createMigrationProgram(tree, tsconfigPath, basePath);
37+
const typeChecker = program.getTypeChecker();
38+
const sourceFiles =
39+
program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program));
40+
41+
for (const sourceFile of sourceFiles) {
42+
const nodesToRemove = collectUpdatesForFile(typeChecker, sourceFile);
43+
if (nodesToRemove.length !== 0) {
44+
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
45+
for (const node of nodesToRemove) {
46+
update.remove(node.getFullStart(), node.getFullWidth());
47+
}
48+
tree.commitUpdate(update);
49+
}
50+
}
51+
}
52+
53+
function collectUpdatesForFile(typeChecker: ts.TypeChecker, file: ts.SourceFile): ts.Node[] {
54+
const removeNodes: ts.Node[] = [];
55+
const attemptMigrateClass = (node: ts.ClassDeclaration) => {
56+
const metadata = extractAngularClassMetadata(typeChecker, node);
57+
if (metadata === null) {
58+
return;
59+
}
60+
61+
const syntaxList = metadata.node.getChildren().find(
62+
((n): n is ts.SyntaxList => n.kind === ts.SyntaxKind.SyntaxList));
63+
const tokens = syntaxList?.getChildren();
64+
65+
if (!tokens) {
66+
return;
67+
}
68+
69+
let removeNextComma = false;
70+
for (const token of tokens) {
71+
// Track the comma token if it's requested to be removed.
72+
if (token.kind === ts.SyntaxKind.CommaToken) {
73+
if (removeNextComma) {
74+
removeNodes.push(token);
75+
}
76+
removeNextComma = false;
77+
}
78+
79+
// Track the `moduleId` property assignment. Note that the AST node does not include a
80+
// potential followed comma token.
81+
if (ts.isPropertyAssignment(token) && getPropertyNameText(token.name) === 'moduleId') {
82+
removeNodes.push(token);
83+
removeNextComma = true;
84+
}
85+
}
86+
};
87+
88+
ts.forEachChild(file, function visitNode(node: ts.Node) {
89+
if (ts.isClassDeclaration(node)) {
90+
attemptMigrateClass(node);
91+
}
92+
ts.forEachChild(node, visitNode);
93+
});
94+
95+
return removeNodes;
96+
}

packages/core/schematics/test/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ jasmine_node_test(
2121
"//packages/core/schematics:migrations.json",
2222
"//packages/core/schematics/migrations/relative-link-resolution",
2323
"//packages/core/schematics/migrations/relative-link-resolution:bundle",
24+
"//packages/core/schematics/migrations/remove-module-id",
25+
"//packages/core/schematics/migrations/remove-module-id:bundle",
2426
"//packages/core/schematics/migrations/router-link-with-href",
2527
"//packages/core/schematics/migrations/router-link-with-href:bundle",
2628
"//packages/core/schematics/ng-generate/standalone-migration",
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
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 {getSystemPath, normalize, virtualFs} from '@angular-devkit/core';
10+
import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing';
11+
import {HostTree} from '@angular-devkit/schematics';
12+
import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing';
13+
import {runfiles} from '@bazel/runfiles';
14+
import shx from 'shelljs';
15+
16+
import {dedent} from './helpers';
17+
18+
describe('Remove `moduleId` migration', () => {
19+
let runner: SchematicTestRunner;
20+
let host: TempScopedNodeJsSyncHost;
21+
let tree: UnitTestTree;
22+
let tmpDirPath: string;
23+
let previousWorkingDir: string;
24+
25+
function writeFile(filePath: string, contents: string) {
26+
host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents));
27+
}
28+
29+
function runMigration() {
30+
return runner.runSchematic('migration-v16-remove-module-id', {}, tree);
31+
}
32+
33+
beforeEach(() => {
34+
runner = new SchematicTestRunner('test', runfiles.resolvePackageRelative('../migrations.json'));
35+
host = new TempScopedNodeJsSyncHost();
36+
tree = new UnitTestTree(new HostTree(host));
37+
38+
writeFile('/tsconfig.json', JSON.stringify({
39+
compilerOptions: {
40+
lib: ['es2022'],
41+
strict: true,
42+
},
43+
}));
44+
45+
writeFile('/angular.json', JSON.stringify({
46+
version: 1,
47+
projects: {t: {root: '', architect: {build: {options: {tsConfig: './tsconfig.json'}}}}}
48+
}));
49+
50+
previousWorkingDir = shx.pwd();
51+
tmpDirPath = getSystemPath(host.root);
52+
53+
// Switch into the temporary directory path. This allows us to run
54+
// the schematic against our custom unit test tree.
55+
shx.cd(tmpDirPath);
56+
});
57+
58+
afterEach(() => {
59+
shx.cd(previousWorkingDir);
60+
shx.rm('-r', tmpDirPath);
61+
});
62+
63+
it('should remove `moduleId` from `@Directive`', async () => {
64+
writeFile('/index.ts', dedent`
65+
import {Directive} from '@angular/core';
66+
67+
@Directive({
68+
selector: 'my-dir',
69+
moduleId: module.id,
70+
standalone: true,
71+
})
72+
export class MyDir {}
73+
`);
74+
75+
await runMigration();
76+
77+
expect(tree.readContent('/index.ts')).toEqual(dedent`
78+
import {Directive} from '@angular/core';
79+
80+
@Directive({
81+
selector: 'my-dir',
82+
standalone: true,
83+
})
84+
export class MyDir {}
85+
`);
86+
});
87+
88+
it('should be able to remove `moduleId` from multiple classes in the same file', async () => {
89+
writeFile('/index.ts', dedent`
90+
import {Directive} from '@angular/core';
91+
92+
@Directive({
93+
selector: 'my-dir-a',
94+
moduleId: module.id,
95+
standalone: true,
96+
})
97+
export class MyDirA {}
98+
99+
@Directive({
100+
selector: 'my-dir-b',
101+
moduleId: module.id,
102+
standalone: true,
103+
})
104+
export class MyDirB {}
105+
`);
106+
107+
await runMigration();
108+
109+
expect(tree.readContent('/index.ts')).toEqual(dedent`
110+
import {Directive} from '@angular/core';
111+
112+
@Directive({
113+
selector: 'my-dir-a',
114+
standalone: true,
115+
})
116+
export class MyDirA {}
117+
118+
@Directive({
119+
selector: 'my-dir-b',
120+
standalone: true,
121+
})
122+
export class MyDirB {}
123+
`);
124+
});
125+
126+
it('should not fail if `moduleId` is last property of decorator', async () => {
127+
writeFile('/index.ts', dedent`
128+
import {Directive} from '@angular/core';
129+
130+
@Directive({
131+
selector: 'my-dir',
132+
moduleId: module.id,
133+
})
134+
export class MyDir {}
135+
`);
136+
137+
await runMigration();
138+
139+
expect(tree.readContent('/index.ts')).toEqual(dedent`
140+
import {Directive} from '@angular/core';
141+
142+
@Directive({
143+
selector: 'my-dir',
144+
})
145+
export class MyDir {}
146+
`);
147+
});
148+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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 ts from 'typescript';
10+
11+
import {getAngularDecorators} from './ng_decorators';
12+
import {unwrapExpression} from './typescript/functions';
13+
14+
/** Interface describing metadata of an Angular class. */
15+
export interface AngularClassMetadata {
16+
type: 'component'|'directive';
17+
node: ts.ObjectLiteralExpression;
18+
}
19+
20+
/** Extracts `@Directive` or `@Component` metadata from the given class. */
21+
export function extractAngularClassMetadata(
22+
typeChecker: ts.TypeChecker, node: ts.ClassDeclaration): AngularClassMetadata|null {
23+
const decorators = ts.getDecorators(node);
24+
25+
if (!decorators || !decorators.length) {
26+
return null;
27+
}
28+
29+
const ngDecorators = getAngularDecorators(typeChecker, decorators);
30+
const componentDecorator = ngDecorators.find(dec => dec.name === 'Component');
31+
const directiveDecorator = ngDecorators.find(dec => dec.name === 'Directive');
32+
const decorator = componentDecorator ?? directiveDecorator;
33+
34+
// In case no decorator could be found on the current class, skip.
35+
if (!decorator) {
36+
return null;
37+
}
38+
39+
const decoratorCall = decorator.node.expression;
40+
41+
// In case the decorator call is not valid, skip this class declaration.
42+
if (decoratorCall.arguments.length !== 1) {
43+
return null;
44+
}
45+
46+
const metadata = unwrapExpression(decoratorCall.arguments[0]);
47+
48+
// Ensure that the metadata is an object literal expression.
49+
if (!ts.isObjectLiteralExpression(metadata)) {
50+
return null;
51+
}
52+
53+
return {
54+
type: componentDecorator ? 'component' : 'directive',
55+
node: metadata,
56+
};
57+
}

0 commit comments

Comments
 (0)