Skip to content

Commit 645e305

Browse files
CaerusKarubenlesh
authored andcommittedApr 22, 2019
feat(core): add schematics to move deprecated DOCUMENT import (#29950)
PR Close #29950
1 parent f53d0fd commit 645e305

File tree

8 files changed

+410
-0
lines changed

8 files changed

+410
-0
lines changed
 

‎packages/core/schematics/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ npm_package(
1010
srcs = ["migrations.json"],
1111
visibility = ["//packages/core:__pkg__"],
1212
deps = [
13+
"//packages/core/schematics/migrations/move-document",
1314
"//packages/core/schematics/migrations/static-queries",
1415
"//packages/core/schematics/migrations/template-var-assignment",
1516
],

‎packages/core/schematics/migrations.json

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
{
22
"schematics": {
3+
"migration-v8-move-document": {
4+
"version": "8-beta",
5+
"description": "Migrates DOCUMENT Injection token from platform-browser imports to common import",
6+
"factory": "./migrations/move-document/index"
7+
},
38
"migration-v8-static-queries": {
49
"version": "8-beta",
510
"description": "Migrates ViewChild and ContentChild to explicit query timing",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
load("//tools:defaults.bzl", "ts_library")
2+
3+
ts_library(
4+
name = "move-document",
5+
srcs = glob(["**/*.ts"]),
6+
tsconfig = "//packages/core/schematics:tsconfig.json",
7+
visibility = [
8+
"//packages/core/schematics:__pkg__",
9+
"//packages/core/schematics/migrations/move-document/google3:__pkg__",
10+
"//packages/core/schematics/test:__pkg__",
11+
],
12+
deps = [
13+
"//packages/core/schematics/utils",
14+
"@npm//@angular-devkit/schematics",
15+
"@npm//@types/node",
16+
"@npm//typescript",
17+
],
18+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. 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 * as ts from 'typescript';
10+
11+
export const COMMON_IMPORT = '@angular/common';
12+
export const PLATFORM_BROWSER_IMPORT = '@angular/platform-browser';
13+
export const DOCUMENT_TOKEN_NAME = 'DOCUMENT';
14+
15+
/** This contains the metadata necessary to move items from one import to another */
16+
export interface ResolvedDocumentImport {
17+
platformBrowserImport: ts.NamedImports|null;
18+
commonImport: ts.NamedImports|null;
19+
documentElement: ts.ImportSpecifier|null;
20+
}
21+
22+
/** Visitor that can be used to find a set of imports in a TypeScript file. */
23+
export class DocumentImportVisitor {
24+
importsMap: Map<ts.SourceFile, ResolvedDocumentImport> = new Map();
25+
26+
constructor(public typeChecker: ts.TypeChecker) {}
27+
28+
visitNode(node: ts.Node) {
29+
if (ts.isNamedImports(node)) {
30+
this.visitNamedImport(node);
31+
}
32+
33+
ts.forEachChild(node, node => this.visitNode(node));
34+
}
35+
36+
private visitNamedImport(node: ts.NamedImports) {
37+
if (!node.elements || !node.elements.length) {
38+
return;
39+
}
40+
41+
const importDeclaration = node.parent.parent;
42+
// If this is not a StringLiteral it will be a grammar error
43+
const moduleSpecifier = importDeclaration.moduleSpecifier as ts.StringLiteral;
44+
const sourceFile = node.getSourceFile();
45+
let imports = this.importsMap.get(sourceFile);
46+
if (!imports) {
47+
imports = {
48+
platformBrowserImport: null,
49+
commonImport: null,
50+
documentElement: null,
51+
};
52+
}
53+
54+
if (moduleSpecifier.text === PLATFORM_BROWSER_IMPORT) {
55+
const documentElement = this.getDocumentElement(node);
56+
if (documentElement) {
57+
imports.platformBrowserImport = node;
58+
imports.documentElement = documentElement;
59+
}
60+
} else if (moduleSpecifier.text === COMMON_IMPORT) {
61+
imports.commonImport = node;
62+
} else {
63+
return;
64+
}
65+
this.importsMap.set(sourceFile, imports);
66+
}
67+
68+
private getDocumentElement(node: ts.NamedImports): ts.ImportSpecifier|undefined {
69+
const elements = node.elements;
70+
return elements.find(el => (el.propertyName || el.name).escapedText === DOCUMENT_TOKEN_NAME);
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. 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 {dirname, relative} from 'path';
11+
import * as ts from 'typescript';
12+
13+
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
14+
import {parseTsconfigFile} from '../../utils/typescript/parse_tsconfig';
15+
import {COMMON_IMPORT, DOCUMENT_TOKEN_NAME, DocumentImportVisitor, ResolvedDocumentImport} from './document_import_visitor';
16+
import {addToImport, createImport, removeFromImport} from './move-import';
17+
18+
19+
/** Entry point for the V8 move-document migration. */
20+
export default function(): Rule {
21+
return (tree: Tree) => {
22+
const projectTsConfigPaths = getProjectTsConfigPaths(tree);
23+
const basePath = process.cwd();
24+
25+
if (!projectTsConfigPaths.length) {
26+
throw new SchematicsException(`Could not find any tsconfig file. Cannot migrate DOCUMENT
27+
to new import source.`);
28+
}
29+
30+
for (const tsconfigPath of projectTsConfigPaths) {
31+
runMoveDocumentMigration(tree, tsconfigPath, basePath);
32+
}
33+
};
34+
}
35+
36+
/**
37+
* Runs the DOCUMENT InjectionToken import migration for the given TypeScript project. The
38+
* schematic analyzes the imports within the project and moves the deprecated symbol to the
39+
* new import source.
40+
*/
41+
function runMoveDocumentMigration(tree: Tree, tsconfigPath: string, basePath: string) {
42+
const parsed = parseTsconfigFile(tsconfigPath, dirname(tsconfigPath));
43+
const host = ts.createCompilerHost(parsed.options, true);
44+
45+
// We need to overwrite the host "readFile" method, as we want the TypeScript
46+
// program to be based on the file contents in the virtual file tree. Otherwise
47+
// if we run the migration for multiple tsconfig files which have intersecting
48+
// source files, it can end up updating query definitions multiple times.
49+
host.readFile = fileName => {
50+
const buffer = tree.read(relative(basePath, fileName));
51+
return buffer ? buffer.toString() : undefined;
52+
};
53+
54+
const program = ts.createProgram(parsed.fileNames, parsed.options, host);
55+
const typeChecker = program.getTypeChecker();
56+
const visitor = new DocumentImportVisitor(typeChecker);
57+
const rootSourceFiles = program.getRootFileNames().map(f => program.getSourceFile(f) !);
58+
59+
// Analyze source files by finding imports.
60+
rootSourceFiles.forEach(sourceFile => visitor.visitNode(sourceFile));
61+
62+
const {importsMap} = visitor;
63+
64+
// Walk through all source files that contain resolved queries and update
65+
// the source files if needed. Note that we need to update multiple queries
66+
// within a source file within the same recorder in order to not throw off
67+
// the TypeScript node offsets.
68+
importsMap.forEach((resolvedImport: ResolvedDocumentImport, sourceFile: ts.SourceFile) => {
69+
const {platformBrowserImport, commonImport, documentElement} = resolvedImport;
70+
if (!documentElement || !platformBrowserImport) {
71+
return;
72+
}
73+
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
74+
75+
const platformBrowserDeclaration = platformBrowserImport.parent.parent;
76+
const newPlatformBrowserText =
77+
removeFromImport(platformBrowserImport, sourceFile, DOCUMENT_TOKEN_NAME);
78+
const newCommonText = commonImport ?
79+
addToImport(commonImport, sourceFile, documentElement.name, documentElement.propertyName) :
80+
createImport(COMMON_IMPORT, sourceFile, documentElement.name, documentElement.propertyName);
81+
82+
// Replace the existing query decorator call expression with the updated
83+
// call expression node.
84+
update.remove(platformBrowserDeclaration.getStart(), platformBrowserDeclaration.getWidth());
85+
update.insertRight(platformBrowserDeclaration.getStart(), newPlatformBrowserText);
86+
87+
if (commonImport) {
88+
const commonDeclaration = commonImport.parent.parent;
89+
update.remove(commonDeclaration.getStart(), commonDeclaration.getWidth());
90+
update.insertRight(commonDeclaration.getStart(), newCommonText);
91+
} else {
92+
update.insertRight(platformBrowserDeclaration.getStart(), newCommonText);
93+
}
94+
95+
tree.commitUpdate(update);
96+
});
97+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. 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+
import * as ts from 'typescript';
9+
10+
export function removeFromImport(
11+
importNode: ts.NamedImports, sourceFile: ts.SourceFile, importName: string): string {
12+
const printer = ts.createPrinter();
13+
const elements = importNode.elements.filter(
14+
el => String((el.propertyName || el.name).escapedText) !== importName);
15+
16+
if (!elements.length) {
17+
return '';
18+
}
19+
20+
const oldDeclaration = importNode.parent.parent;
21+
const newImport = ts.createNamedImports(elements);
22+
const importClause = ts.createImportClause(undefined, newImport);
23+
const newDeclaration = ts.createImportDeclaration(
24+
undefined, undefined, importClause, oldDeclaration.moduleSpecifier);
25+
26+
return printer.printNode(ts.EmitHint.Unspecified, newDeclaration, sourceFile);
27+
}
28+
29+
export function addToImport(
30+
importNode: ts.NamedImports, sourceFile: ts.SourceFile, name: ts.Identifier,
31+
propertyName?: ts.Identifier): string {
32+
const printer = ts.createPrinter();
33+
const propertyNameIdentifier =
34+
propertyName ? ts.createIdentifier(String(propertyName.escapedText)) : undefined;
35+
const nameIdentifier = ts.createIdentifier(String(name.escapedText));
36+
const newSpecfier = ts.createImportSpecifier(propertyNameIdentifier, nameIdentifier);
37+
const elements = [...importNode.elements];
38+
39+
elements.push(newSpecfier);
40+
41+
const oldDeclaration = importNode.parent.parent;
42+
const newImport = ts.createNamedImports(elements);
43+
const importClause = ts.createImportClause(undefined, newImport);
44+
const newDeclaration = ts.createImportDeclaration(
45+
undefined, undefined, importClause, oldDeclaration.moduleSpecifier);
46+
47+
return printer.printNode(ts.EmitHint.Unspecified, newDeclaration, sourceFile);
48+
}
49+
50+
export function createImport(
51+
importSource: string, sourceFile: ts.SourceFile, name: ts.Identifier,
52+
propertyName?: ts.Identifier) {
53+
const printer = ts.createPrinter();
54+
const propertyNameIdentifier =
55+
propertyName ? ts.createIdentifier(String(propertyName.escapedText)) : undefined;
56+
const nameIdentifier = ts.createIdentifier(String(name.escapedText));
57+
const newSpecfier = ts.createImportSpecifier(propertyNameIdentifier, nameIdentifier);
58+
const newNamedImports = ts.createNamedImports([newSpecfier]);
59+
const importClause = ts.createImportClause(undefined, newNamedImports);
60+
const moduleSpecifier = ts.createStringLiteral(importSource);
61+
const newImport = ts.createImportDeclaration(undefined, undefined, importClause, moduleSpecifier);
62+
63+
return printer.printNode(ts.EmitHint.Unspecified, newImport, sourceFile);
64+
}

‎packages/core/schematics/test/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ ts_library(
88
"//packages/core/schematics:migrations.json",
99
],
1010
deps = [
11+
"//packages/core/schematics/migrations/move-document",
1112
"//packages/core/schematics/migrations/static-queries",
1213
"//packages/core/schematics/migrations/static-queries/google3",
1314
"//packages/core/schematics/migrations/template-var-assignment",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. 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 * as shx from 'shelljs';
14+
15+
describe('move-document migration', () => {
16+
let runner: SchematicTestRunner;
17+
let host: TempScopedNodeJsSyncHost;
18+
let tree: UnitTestTree;
19+
let tmpDirPath: string;
20+
let previousWorkingDir: string;
21+
22+
beforeEach(() => {
23+
runner = new SchematicTestRunner('test', require.resolve('../migrations.json'));
24+
host = new TempScopedNodeJsSyncHost();
25+
tree = new UnitTestTree(new HostTree(host));
26+
27+
writeFile('/tsconfig.json', JSON.stringify({
28+
compilerOptions: {
29+
lib: ['es2015'],
30+
}
31+
}));
32+
33+
previousWorkingDir = shx.pwd();
34+
tmpDirPath = getSystemPath(host.root);
35+
36+
// Switch into the temporary directory path. This allows us to run
37+
// the schematic against our custom unit test tree.
38+
shx.cd(tmpDirPath);
39+
});
40+
41+
afterEach(() => {
42+
shx.cd(previousWorkingDir);
43+
shx.rm('-r', tmpDirPath);
44+
});
45+
46+
describe('move-document', () => {
47+
it('should properly apply import replacement', () => {
48+
writeFile('/index.ts', `
49+
import {DOCUMENT} from '@angular/platform-browser';
50+
`);
51+
52+
runMigration();
53+
54+
const content = tree.readContent('/index.ts');
55+
56+
expect(content).toContain(`import { DOCUMENT } from "@angular/common";`);
57+
expect(content).not.toContain(`import {DOCUMENT} from '@angular/platform-browser';`);
58+
});
59+
60+
it('should properly apply import replacement with existing import', () => {
61+
writeFile('/index.ts', `
62+
import {DOCUMENT} from '@angular/platform-browser';
63+
import {someImport} from '@angular/common';
64+
`);
65+
66+
writeFile('/reverse.ts', `
67+
import {someImport} from '@angular/common';
68+
import {DOCUMENT} from '@angular/platform-browser';
69+
`);
70+
71+
runMigration();
72+
73+
const content = tree.readContent('/index.ts');
74+
const contentReverse = tree.readContent('/reverse.ts');
75+
76+
expect(content).toContain(`import { someImport, DOCUMENT } from '@angular/common';`);
77+
expect(content).not.toContain(`import {DOCUMENT} from '@angular/platform-browser';`);
78+
79+
expect(contentReverse).toContain(`import { someImport, DOCUMENT } from '@angular/common';`);
80+
expect(contentReverse).not.toContain(`import {DOCUMENT} from '@angular/platform-browser';`);
81+
});
82+
83+
it('should properly apply import replacement with existing import w/ comments', () => {
84+
writeFile('/index.ts', `
85+
/**
86+
* this is a comment
87+
*/
88+
import {someImport} from '@angular/common';
89+
import {DOCUMENT} from '@angular/platform-browser';
90+
`);
91+
92+
runMigration();
93+
94+
const content = tree.readContent('/index.ts');
95+
96+
expect(content).toContain(`import { someImport, DOCUMENT } from '@angular/common';`);
97+
expect(content).not.toContain(`import {DOCUMENT} from '@angular/platform-browser';`);
98+
99+
expect(content).toMatch(/.*this is a comment.*/);
100+
});
101+
102+
it('should properly apply import replacement with existing and redundant imports', () => {
103+
writeFile('/index.ts', `
104+
import {DOCUMENT} from '@angular/platform-browser';
105+
import {anotherImport} from '@angular/platform-browser-dynamic';
106+
import {someImport} from '@angular/common';
107+
`);
108+
109+
runMigration();
110+
111+
const content = tree.readContent('/index.ts');
112+
113+
expect(content).toContain(`import { someImport, DOCUMENT } from '@angular/common';`);
114+
expect(content).not.toContain(`import {DOCUMENT} from '@angular/platform-browser';`);
115+
});
116+
117+
it('should properly apply import replacement with existing import and leave original import',
118+
() => {
119+
writeFile('/index.ts', `
120+
import {DOCUMENT, anotherImport} from '@angular/platform-browser';
121+
import {someImport} from '@angular/common';
122+
`);
123+
124+
runMigration();
125+
126+
const content = tree.readContent('/index.ts');
127+
128+
expect(content).toContain(`import { someImport, DOCUMENT } from '@angular/common';`);
129+
expect(content).toContain(`import { anotherImport } from '@angular/platform-browser';`);
130+
});
131+
132+
it('should properly apply import replacement with existing import and alias', () => {
133+
writeFile('/index.ts', `
134+
import {DOCUMENT as doc, anotherImport} from '@angular/platform-browser';
135+
import {someImport} from '@angular/common';
136+
`);
137+
138+
runMigration();
139+
140+
const content = tree.readContent('/index.ts');
141+
142+
expect(content).toContain(`import { someImport, DOCUMENT as doc } from '@angular/common';`);
143+
expect(content).toContain(`import { anotherImport } from '@angular/platform-browser';`);
144+
});
145+
});
146+
147+
function writeFile(filePath: string, contents: string) {
148+
host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents));
149+
}
150+
151+
function runMigration() { runner.runSchematic('migration-v8-move-document', {}, tree); }
152+
});

0 commit comments

Comments
 (0)
Please sign in to comment.