Skip to content

Commit accc65c

Browse files
crisbetothePunderWoman
authored andcommitted
refactor(migrations): add a migration to move DOCUMENT imports (#60663)
Adds a migration that moves imports of `DOCUMENT` from `common` to `core`. PR Close #60663
1 parent 7ccec14 commit accc65c

File tree

7 files changed

+308
-0
lines changed

7 files changed

+308
-0
lines changed

packages/core/schematics/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ rollup_bundle(
4545
"//packages/core/schematics/ng-generate/self-closing-tags-migration:index.ts": "self-closing-tags-migration",
4646
"//packages/core/schematics/migrations/inject-flags:index.ts": "inject-flags",
4747
"//packages/core/schematics/migrations/test-bed-get:index.ts": "test-bed-get",
48+
"//packages/core/schematics/migrations/document-core:index.ts": "document-core",
4849
"//packages/core/schematics/migrations/control-flow-migration:index.ts": "control-flow-migration",
4950
},
5051
format = "cjs",
@@ -56,6 +57,7 @@ rollup_bundle(
5657
],
5758
deps = [
5859
"//packages/core/schematics/migrations/control-flow-migration",
60+
"//packages/core/schematics/migrations/document-core",
5961
"//packages/core/schematics/migrations/inject-flags",
6062
"//packages/core/schematics/migrations/test-bed-get",
6163
"//packages/core/schematics/ng-generate/cleanup-unused-imports",

packages/core/schematics/migrations.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
"description": "Converts the entire application to block control flow syntax",
1616
"factory": "./bundles/control-flow-migration#migrate",
1717
"optional": true
18+
},
19+
"document-core": {
20+
"version": "20.0.0",
21+
"description": "Moves imports of `DOCUMENT` from `@angular/common` to `@angular/core`",
22+
"factory": "./bundles/document-core#migrate"
1823
}
1924
}
2025
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
load("//tools:defaults.bzl", "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 = "document-core",
13+
srcs = glob(["**/*.ts"]),
14+
tsconfig = "//packages/core/schematics:tsconfig.json",
15+
deps = [
16+
"//packages/compiler-cli/private",
17+
"//packages/compiler-cli/src/ngtsc/file_system",
18+
"//packages/core/schematics/utils",
19+
"//packages/core/schematics/utils/tsurge",
20+
"//packages/core/schematics/utils/tsurge/helpers/angular_devkit",
21+
"@npm//@angular-devkit/schematics",
22+
"@npm//@types/node",
23+
"@npm//typescript",
24+
],
25+
)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
## Move `DOCUMENT` migration
2+
Replaces imports of `DOCUMENT` from `@angular/core` to `@angular/common`:
3+
4+
### Before
5+
```typescript
6+
import { Component, inject } from '@angular/core';
7+
import { DOCUMENT } from '@angular/common';
8+
9+
@Component()
10+
export class MyComp {
11+
document = inject(DOCUMENT);
12+
}
13+
```
14+
15+
### After
16+
```typescript
17+
import { Component, inject, DOCUMENT } from '@angular/core';
18+
19+
@Component()
20+
export class MyComp {
21+
document = inject(DOCUMENT);
22+
}
23+
```
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.dev/license
7+
*/
8+
9+
import {ImportManager} from '@angular/compiler-cli/private/migrations';
10+
import {applyImportManagerChanges} from '../../utils/tsurge/helpers/apply_import_manager';
11+
import {
12+
confirmAsSerializable,
13+
ProgramInfo,
14+
Replacement,
15+
Serializable,
16+
TsurgeFunnelMigration,
17+
} from '../../utils/tsurge';
18+
import {getImportSpecifier} from '../../utils/typescript/imports';
19+
20+
export interface CompilationUnitData {
21+
replacements: Replacement[];
22+
}
23+
24+
/** Migration that moves the import of `DOCUMENT` from `core` to `common`. */
25+
export class DocumentCoreMigration extends TsurgeFunnelMigration<
26+
CompilationUnitData,
27+
CompilationUnitData
28+
> {
29+
override async analyze(info: ProgramInfo): Promise<Serializable<CompilationUnitData>> {
30+
const replacements: Replacement[] = [];
31+
let importManager: ImportManager | null = null;
32+
33+
for (const sourceFile of info.sourceFiles) {
34+
const specifier = getImportSpecifier(sourceFile, '@angular/common', 'DOCUMENT');
35+
36+
if (specifier === null) {
37+
continue;
38+
}
39+
40+
importManager ??= new ImportManager({
41+
// Prevent the manager from trying to generate a non-conflicting import.
42+
generateUniqueIdentifier: () => null,
43+
shouldUseSingleQuotes: () => true,
44+
});
45+
importManager.removeImport(sourceFile, 'DOCUMENT', '@angular/common');
46+
importManager.addImport({
47+
exportSymbolName: 'DOCUMENT',
48+
exportModuleSpecifier: '@angular/core',
49+
requestedFile: sourceFile,
50+
unsafeAliasOverride: specifier.propertyName ? specifier.name.text : undefined,
51+
});
52+
}
53+
54+
if (importManager !== null) {
55+
applyImportManagerChanges(importManager, replacements, info.sourceFiles, info);
56+
}
57+
58+
return confirmAsSerializable({replacements});
59+
}
60+
61+
override async migrate(globalData: CompilationUnitData) {
62+
return confirmAsSerializable(globalData);
63+
}
64+
65+
override async combine(
66+
unitA: CompilationUnitData,
67+
unitB: CompilationUnitData,
68+
): Promise<Serializable<CompilationUnitData>> {
69+
const seen = new Set<string>();
70+
const combined: Replacement[] = [];
71+
72+
[unitA.replacements, unitB.replacements].forEach((replacements) => {
73+
replacements.forEach((current) => {
74+
const {position, end, toInsert} = current.update.data;
75+
const key = current.projectFile.id + '/' + position + '/' + end + '/' + toInsert;
76+
77+
if (!seen.has(key)) {
78+
seen.add(key);
79+
combined.push(current);
80+
}
81+
});
82+
});
83+
84+
return confirmAsSerializable({replacements: combined});
85+
}
86+
87+
override async globalMeta(
88+
combinedData: CompilationUnitData,
89+
): Promise<Serializable<CompilationUnitData>> {
90+
return confirmAsSerializable(combinedData);
91+
}
92+
93+
override async stats() {
94+
return {counters: {}};
95+
}
96+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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.dev/license
7+
*/
8+
9+
import {Rule} from '@angular-devkit/schematics';
10+
import {DocumentCoreMigration} from './document_core_migration';
11+
import {runMigrationInDevkit} from '../../utils/tsurge/helpers/angular_devkit';
12+
13+
export function migrate(): Rule {
14+
return async (tree) => {
15+
await runMigrationInDevkit({
16+
tree,
17+
getMigration: () => new DocumentCoreMigration(),
18+
});
19+
};
20+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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.dev/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+
describe('document-core migration', () => {
17+
let runner: SchematicTestRunner;
18+
let host: TempScopedNodeJsSyncHost;
19+
let tree: UnitTestTree;
20+
let tmpDirPath: string;
21+
22+
function writeFile(filePath: string, contents: string) {
23+
host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents));
24+
}
25+
26+
function runMigration() {
27+
return runner.runSchematic('document-core', {}, tree);
28+
}
29+
30+
beforeEach(() => {
31+
runner = new SchematicTestRunner('test', runfiles.resolvePackageRelative('../migrations.json'));
32+
host = new TempScopedNodeJsSyncHost();
33+
tree = new UnitTestTree(new HostTree(host));
34+
tmpDirPath = getSystemPath(host.root);
35+
36+
writeFile('/tsconfig.json', '{}');
37+
writeFile(
38+
'/angular.json',
39+
JSON.stringify({
40+
version: 1,
41+
projects: {t: {root: '', architect: {build: {options: {tsConfig: './tsconfig.json'}}}}},
42+
}),
43+
);
44+
45+
shx.cd(tmpDirPath);
46+
});
47+
48+
it('should migrate an import of DOCUMENT', async () => {
49+
writeFile(
50+
'/dir.ts',
51+
`
52+
import { Directive, inject } from '@angular/core';
53+
import { DOCUMENT } from '@angular/common';
54+
55+
@Directive()
56+
export class Dir {
57+
protected doc = inject(DOCUMENT);
58+
}
59+
`,
60+
);
61+
62+
await runMigration();
63+
const content = tree.readContent('/dir.ts');
64+
expect(content).toContain(`import { Directive, inject, DOCUMENT } from '@angular/core';`);
65+
expect(content).not.toContain(`@angular/common`);
66+
});
67+
68+
it('should migrate an aliased import of DOCUMENT', async () => {
69+
writeFile(
70+
'/dir.ts',
71+
`
72+
import { Directive, inject } from '@angular/core';
73+
import { DOCUMENT as MY_DOC } from '@angular/common';
74+
75+
@Directive()
76+
export class Dir {
77+
protected doc = inject(MY_DOC);
78+
}
79+
`,
80+
);
81+
82+
await runMigration();
83+
const content = tree.readContent('/dir.ts');
84+
expect(content).toContain(
85+
`import { Directive, inject, DOCUMENT as MY_DOC } from '@angular/core';`,
86+
);
87+
expect(content).not.toContain(`@angular/common`);
88+
});
89+
90+
it('should migrate a file that does not import @angular/core', async () => {
91+
writeFile(
92+
'/dir.ts',
93+
`
94+
import { DOCUMENT } from '@angular/common';
95+
96+
console.log(DOCUMENT);
97+
`,
98+
);
99+
100+
await runMigration();
101+
const content = tree.readContent('/dir.ts');
102+
expect(content).toContain(`import { DOCUMENT } from '@angular/core';`);
103+
expect(content).not.toContain(`@angular/common`);
104+
});
105+
106+
it('should handle a file that is present in multiple projects', async () => {
107+
writeFile('/tsconfig-2.json', '{}');
108+
writeFile(
109+
'/angular.json',
110+
JSON.stringify({
111+
version: 1,
112+
projects: {
113+
a: {root: '', architect: {build: {options: {tsConfig: './tsconfig.json'}}}},
114+
b: {root: '', architect: {build: {options: {tsConfig: './tsconfig-2.json'}}}},
115+
},
116+
}),
117+
);
118+
119+
writeFile(
120+
'dir.ts',
121+
`
122+
import { Directive, inject } from '@angular/core';
123+
import { DOCUMENT } from '@angular/common';
124+
125+
@Directive()
126+
export class Dir {
127+
protected doc = inject(DOCUMENT);
128+
}
129+
`,
130+
);
131+
132+
await runMigration();
133+
const content = tree.readContent('/dir.ts');
134+
expect(content).toContain(`import { Directive, inject, DOCUMENT } from '@angular/core';`);
135+
expect(content).not.toContain(`@angular/common`);
136+
});
137+
});

0 commit comments

Comments
 (0)