feat(core): add schematics to move deprecated DOCUMENT import (#29950)
PR Close #29950
This commit is contained in:
parent
f53d0fd2d0
commit
645e305733
|
@ -10,6 +10,7 @@ npm_package(
|
||||||
srcs = ["migrations.json"],
|
srcs = ["migrations.json"],
|
||||||
visibility = ["//packages/core:__pkg__"],
|
visibility = ["//packages/core:__pkg__"],
|
||||||
deps = [
|
deps = [
|
||||||
|
"//packages/core/schematics/migrations/move-document",
|
||||||
"//packages/core/schematics/migrations/static-queries",
|
"//packages/core/schematics/migrations/static-queries",
|
||||||
"//packages/core/schematics/migrations/template-var-assignment",
|
"//packages/core/schematics/migrations/template-var-assignment",
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
{
|
{
|
||||||
"schematics": {
|
"schematics": {
|
||||||
|
"migration-v8-move-document": {
|
||||||
|
"version": "8-beta",
|
||||||
|
"description": "Migrates DOCUMENT Injection token from platform-browser imports to common import",
|
||||||
|
"factory": "./migrations/move-document/index"
|
||||||
|
},
|
||||||
"migration-v8-static-queries": {
|
"migration-v8-static-queries": {
|
||||||
"version": "8-beta",
|
"version": "8-beta",
|
||||||
"description": "Migrates ViewChild and ContentChild to explicit query timing",
|
"description": "Migrates ViewChild and ContentChild to explicit query timing",
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
load("//tools:defaults.bzl", "ts_library")
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "move-document",
|
||||||
|
srcs = glob(["**/*.ts"]),
|
||||||
|
tsconfig = "//packages/core/schematics:tsconfig.json",
|
||||||
|
visibility = [
|
||||||
|
"//packages/core/schematics:__pkg__",
|
||||||
|
"//packages/core/schematics/migrations/move-document/google3:__pkg__",
|
||||||
|
"//packages/core/schematics/test:__pkg__",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"//packages/core/schematics/utils",
|
||||||
|
"@npm//@angular-devkit/schematics",
|
||||||
|
"@npm//@types/node",
|
||||||
|
"@npm//typescript",
|
||||||
|
],
|
||||||
|
)
|
|
@ -0,0 +1,72 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. 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 * as ts from 'typescript';
|
||||||
|
|
||||||
|
export const COMMON_IMPORT = '@angular/common';
|
||||||
|
export const PLATFORM_BROWSER_IMPORT = '@angular/platform-browser';
|
||||||
|
export const DOCUMENT_TOKEN_NAME = 'DOCUMENT';
|
||||||
|
|
||||||
|
/** This contains the metadata necessary to move items from one import to another */
|
||||||
|
export interface ResolvedDocumentImport {
|
||||||
|
platformBrowserImport: ts.NamedImports|null;
|
||||||
|
commonImport: ts.NamedImports|null;
|
||||||
|
documentElement: ts.ImportSpecifier|null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Visitor that can be used to find a set of imports in a TypeScript file. */
|
||||||
|
export class DocumentImportVisitor {
|
||||||
|
importsMap: Map<ts.SourceFile, ResolvedDocumentImport> = new Map();
|
||||||
|
|
||||||
|
constructor(public typeChecker: ts.TypeChecker) {}
|
||||||
|
|
||||||
|
visitNode(node: ts.Node) {
|
||||||
|
if (ts.isNamedImports(node)) {
|
||||||
|
this.visitNamedImport(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
ts.forEachChild(node, node => this.visitNode(node));
|
||||||
|
}
|
||||||
|
|
||||||
|
private visitNamedImport(node: ts.NamedImports) {
|
||||||
|
if (!node.elements || !node.elements.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const importDeclaration = node.parent.parent;
|
||||||
|
// If this is not a StringLiteral it will be a grammar error
|
||||||
|
const moduleSpecifier = importDeclaration.moduleSpecifier as ts.StringLiteral;
|
||||||
|
const sourceFile = node.getSourceFile();
|
||||||
|
let imports = this.importsMap.get(sourceFile);
|
||||||
|
if (!imports) {
|
||||||
|
imports = {
|
||||||
|
platformBrowserImport: null,
|
||||||
|
commonImport: null,
|
||||||
|
documentElement: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moduleSpecifier.text === PLATFORM_BROWSER_IMPORT) {
|
||||||
|
const documentElement = this.getDocumentElement(node);
|
||||||
|
if (documentElement) {
|
||||||
|
imports.platformBrowserImport = node;
|
||||||
|
imports.documentElement = documentElement;
|
||||||
|
}
|
||||||
|
} else if (moduleSpecifier.text === COMMON_IMPORT) {
|
||||||
|
imports.commonImport = node;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.importsMap.set(sourceFile, imports);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getDocumentElement(node: ts.NamedImports): ts.ImportSpecifier|undefined {
|
||||||
|
const elements = node.elements;
|
||||||
|
return elements.find(el => (el.propertyName || el.name).escapedText === DOCUMENT_TOKEN_NAME);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. 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, SchematicsException, Tree} from '@angular-devkit/schematics';
|
||||||
|
import {dirname, relative} from 'path';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
|
||||||
|
import {parseTsconfigFile} from '../../utils/typescript/parse_tsconfig';
|
||||||
|
import {COMMON_IMPORT, DOCUMENT_TOKEN_NAME, DocumentImportVisitor, ResolvedDocumentImport} from './document_import_visitor';
|
||||||
|
import {addToImport, createImport, removeFromImport} from './move-import';
|
||||||
|
|
||||||
|
|
||||||
|
/** Entry point for the V8 move-document migration. */
|
||||||
|
export default function(): Rule {
|
||||||
|
return (tree: Tree) => {
|
||||||
|
const projectTsConfigPaths = getProjectTsConfigPaths(tree);
|
||||||
|
const basePath = process.cwd();
|
||||||
|
|
||||||
|
if (!projectTsConfigPaths.length) {
|
||||||
|
throw new SchematicsException(`Could not find any tsconfig file. Cannot migrate DOCUMENT
|
||||||
|
to new import source.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tsconfigPath of projectTsConfigPaths) {
|
||||||
|
runMoveDocumentMigration(tree, tsconfigPath, basePath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the DOCUMENT InjectionToken import migration for the given TypeScript project. The
|
||||||
|
* schematic analyzes the imports within the project and moves the deprecated symbol to the
|
||||||
|
* new import source.
|
||||||
|
*/
|
||||||
|
function runMoveDocumentMigration(tree: Tree, tsconfigPath: string, basePath: string) {
|
||||||
|
const parsed = parseTsconfigFile(tsconfigPath, dirname(tsconfigPath));
|
||||||
|
const host = ts.createCompilerHost(parsed.options, true);
|
||||||
|
|
||||||
|
// We need to overwrite the host "readFile" method, as we want the TypeScript
|
||||||
|
// program to be based on the file contents in the virtual file tree. Otherwise
|
||||||
|
// if we run the migration for multiple tsconfig files which have intersecting
|
||||||
|
// source files, it can end up updating query definitions multiple times.
|
||||||
|
host.readFile = fileName => {
|
||||||
|
const buffer = tree.read(relative(basePath, fileName));
|
||||||
|
return buffer ? buffer.toString() : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const program = ts.createProgram(parsed.fileNames, parsed.options, host);
|
||||||
|
const typeChecker = program.getTypeChecker();
|
||||||
|
const visitor = new DocumentImportVisitor(typeChecker);
|
||||||
|
const rootSourceFiles = program.getRootFileNames().map(f => program.getSourceFile(f) !);
|
||||||
|
|
||||||
|
// Analyze source files by finding imports.
|
||||||
|
rootSourceFiles.forEach(sourceFile => visitor.visitNode(sourceFile));
|
||||||
|
|
||||||
|
const {importsMap} = visitor;
|
||||||
|
|
||||||
|
// Walk through all source files that contain resolved queries and update
|
||||||
|
// the source files if needed. Note that we need to update multiple queries
|
||||||
|
// within a source file within the same recorder in order to not throw off
|
||||||
|
// the TypeScript node offsets.
|
||||||
|
importsMap.forEach((resolvedImport: ResolvedDocumentImport, sourceFile: ts.SourceFile) => {
|
||||||
|
const {platformBrowserImport, commonImport, documentElement} = resolvedImport;
|
||||||
|
if (!documentElement || !platformBrowserImport) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
|
||||||
|
|
||||||
|
const platformBrowserDeclaration = platformBrowserImport.parent.parent;
|
||||||
|
const newPlatformBrowserText =
|
||||||
|
removeFromImport(platformBrowserImport, sourceFile, DOCUMENT_TOKEN_NAME);
|
||||||
|
const newCommonText = commonImport ?
|
||||||
|
addToImport(commonImport, sourceFile, documentElement.name, documentElement.propertyName) :
|
||||||
|
createImport(COMMON_IMPORT, sourceFile, documentElement.name, documentElement.propertyName);
|
||||||
|
|
||||||
|
// Replace the existing query decorator call expression with the updated
|
||||||
|
// call expression node.
|
||||||
|
update.remove(platformBrowserDeclaration.getStart(), platformBrowserDeclaration.getWidth());
|
||||||
|
update.insertRight(platformBrowserDeclaration.getStart(), newPlatformBrowserText);
|
||||||
|
|
||||||
|
if (commonImport) {
|
||||||
|
const commonDeclaration = commonImport.parent.parent;
|
||||||
|
update.remove(commonDeclaration.getStart(), commonDeclaration.getWidth());
|
||||||
|
update.insertRight(commonDeclaration.getStart(), newCommonText);
|
||||||
|
} else {
|
||||||
|
update.insertRight(platformBrowserDeclaration.getStart(), newCommonText);
|
||||||
|
}
|
||||||
|
|
||||||
|
tree.commitUpdate(update);
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. 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 * as ts from 'typescript';
|
||||||
|
|
||||||
|
export function removeFromImport(
|
||||||
|
importNode: ts.NamedImports, sourceFile: ts.SourceFile, importName: string): string {
|
||||||
|
const printer = ts.createPrinter();
|
||||||
|
const elements = importNode.elements.filter(
|
||||||
|
el => String((el.propertyName || el.name).escapedText) !== importName);
|
||||||
|
|
||||||
|
if (!elements.length) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldDeclaration = importNode.parent.parent;
|
||||||
|
const newImport = ts.createNamedImports(elements);
|
||||||
|
const importClause = ts.createImportClause(undefined, newImport);
|
||||||
|
const newDeclaration = ts.createImportDeclaration(
|
||||||
|
undefined, undefined, importClause, oldDeclaration.moduleSpecifier);
|
||||||
|
|
||||||
|
return printer.printNode(ts.EmitHint.Unspecified, newDeclaration, sourceFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addToImport(
|
||||||
|
importNode: ts.NamedImports, sourceFile: ts.SourceFile, name: ts.Identifier,
|
||||||
|
propertyName?: ts.Identifier): string {
|
||||||
|
const printer = ts.createPrinter();
|
||||||
|
const propertyNameIdentifier =
|
||||||
|
propertyName ? ts.createIdentifier(String(propertyName.escapedText)) : undefined;
|
||||||
|
const nameIdentifier = ts.createIdentifier(String(name.escapedText));
|
||||||
|
const newSpecfier = ts.createImportSpecifier(propertyNameIdentifier, nameIdentifier);
|
||||||
|
const elements = [...importNode.elements];
|
||||||
|
|
||||||
|
elements.push(newSpecfier);
|
||||||
|
|
||||||
|
const oldDeclaration = importNode.parent.parent;
|
||||||
|
const newImport = ts.createNamedImports(elements);
|
||||||
|
const importClause = ts.createImportClause(undefined, newImport);
|
||||||
|
const newDeclaration = ts.createImportDeclaration(
|
||||||
|
undefined, undefined, importClause, oldDeclaration.moduleSpecifier);
|
||||||
|
|
||||||
|
return printer.printNode(ts.EmitHint.Unspecified, newDeclaration, sourceFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createImport(
|
||||||
|
importSource: string, sourceFile: ts.SourceFile, name: ts.Identifier,
|
||||||
|
propertyName?: ts.Identifier) {
|
||||||
|
const printer = ts.createPrinter();
|
||||||
|
const propertyNameIdentifier =
|
||||||
|
propertyName ? ts.createIdentifier(String(propertyName.escapedText)) : undefined;
|
||||||
|
const nameIdentifier = ts.createIdentifier(String(name.escapedText));
|
||||||
|
const newSpecfier = ts.createImportSpecifier(propertyNameIdentifier, nameIdentifier);
|
||||||
|
const newNamedImports = ts.createNamedImports([newSpecfier]);
|
||||||
|
const importClause = ts.createImportClause(undefined, newNamedImports);
|
||||||
|
const moduleSpecifier = ts.createStringLiteral(importSource);
|
||||||
|
const newImport = ts.createImportDeclaration(undefined, undefined, importClause, moduleSpecifier);
|
||||||
|
|
||||||
|
return printer.printNode(ts.EmitHint.Unspecified, newImport, sourceFile);
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ ts_library(
|
||||||
"//packages/core/schematics:migrations.json",
|
"//packages/core/schematics:migrations.json",
|
||||||
],
|
],
|
||||||
deps = [
|
deps = [
|
||||||
|
"//packages/core/schematics/migrations/move-document",
|
||||||
"//packages/core/schematics/migrations/static-queries",
|
"//packages/core/schematics/migrations/static-queries",
|
||||||
"//packages/core/schematics/migrations/static-queries/google3",
|
"//packages/core/schematics/migrations/static-queries/google3",
|
||||||
"//packages/core/schematics/migrations/template-var-assignment",
|
"//packages/core/schematics/migrations/template-var-assignment",
|
||||||
|
|
|
@ -0,0 +1,152 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google Inc. 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 {getSystemPath, normalize, virtualFs} from '@angular-devkit/core';
|
||||||
|
import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing';
|
||||||
|
import {HostTree} from '@angular-devkit/schematics';
|
||||||
|
import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing';
|
||||||
|
import * as shx from 'shelljs';
|
||||||
|
|
||||||
|
describe('move-document migration', () => {
|
||||||
|
let runner: SchematicTestRunner;
|
||||||
|
let host: TempScopedNodeJsSyncHost;
|
||||||
|
let tree: UnitTestTree;
|
||||||
|
let tmpDirPath: string;
|
||||||
|
let previousWorkingDir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
runner = new SchematicTestRunner('test', require.resolve('../migrations.json'));
|
||||||
|
host = new TempScopedNodeJsSyncHost();
|
||||||
|
tree = new UnitTestTree(new HostTree(host));
|
||||||
|
|
||||||
|
writeFile('/tsconfig.json', JSON.stringify({
|
||||||
|
compilerOptions: {
|
||||||
|
lib: ['es2015'],
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
previousWorkingDir = shx.pwd();
|
||||||
|
tmpDirPath = getSystemPath(host.root);
|
||||||
|
|
||||||
|
// Switch into the temporary directory path. This allows us to run
|
||||||
|
// the schematic against our custom unit test tree.
|
||||||
|
shx.cd(tmpDirPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
shx.cd(previousWorkingDir);
|
||||||
|
shx.rm('-r', tmpDirPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('move-document', () => {
|
||||||
|
it('should properly apply import replacement', () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import {DOCUMENT} from '@angular/platform-browser';
|
||||||
|
`);
|
||||||
|
|
||||||
|
runMigration();
|
||||||
|
|
||||||
|
const content = tree.readContent('/index.ts');
|
||||||
|
|
||||||
|
expect(content).toContain(`import { DOCUMENT } from "@angular/common";`);
|
||||||
|
expect(content).not.toContain(`import {DOCUMENT} from '@angular/platform-browser';`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly apply import replacement with existing import', () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import {DOCUMENT} from '@angular/platform-browser';
|
||||||
|
import {someImport} from '@angular/common';
|
||||||
|
`);
|
||||||
|
|
||||||
|
writeFile('/reverse.ts', `
|
||||||
|
import {someImport} from '@angular/common';
|
||||||
|
import {DOCUMENT} from '@angular/platform-browser';
|
||||||
|
`);
|
||||||
|
|
||||||
|
runMigration();
|
||||||
|
|
||||||
|
const content = tree.readContent('/index.ts');
|
||||||
|
const contentReverse = tree.readContent('/reverse.ts');
|
||||||
|
|
||||||
|
expect(content).toContain(`import { someImport, DOCUMENT } from '@angular/common';`);
|
||||||
|
expect(content).not.toContain(`import {DOCUMENT} from '@angular/platform-browser';`);
|
||||||
|
|
||||||
|
expect(contentReverse).toContain(`import { someImport, DOCUMENT } from '@angular/common';`);
|
||||||
|
expect(contentReverse).not.toContain(`import {DOCUMENT} from '@angular/platform-browser';`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly apply import replacement with existing import w/ comments', () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
/**
|
||||||
|
* this is a comment
|
||||||
|
*/
|
||||||
|
import {someImport} from '@angular/common';
|
||||||
|
import {DOCUMENT} from '@angular/platform-browser';
|
||||||
|
`);
|
||||||
|
|
||||||
|
runMigration();
|
||||||
|
|
||||||
|
const content = tree.readContent('/index.ts');
|
||||||
|
|
||||||
|
expect(content).toContain(`import { someImport, DOCUMENT } from '@angular/common';`);
|
||||||
|
expect(content).not.toContain(`import {DOCUMENT} from '@angular/platform-browser';`);
|
||||||
|
|
||||||
|
expect(content).toMatch(/.*this is a comment.*/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly apply import replacement with existing and redundant imports', () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import {DOCUMENT} from '@angular/platform-browser';
|
||||||
|
import {anotherImport} from '@angular/platform-browser-dynamic';
|
||||||
|
import {someImport} from '@angular/common';
|
||||||
|
`);
|
||||||
|
|
||||||
|
runMigration();
|
||||||
|
|
||||||
|
const content = tree.readContent('/index.ts');
|
||||||
|
|
||||||
|
expect(content).toContain(`import { someImport, DOCUMENT } from '@angular/common';`);
|
||||||
|
expect(content).not.toContain(`import {DOCUMENT} from '@angular/platform-browser';`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly apply import replacement with existing import and leave original import',
|
||||||
|
() => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import {DOCUMENT, anotherImport} from '@angular/platform-browser';
|
||||||
|
import {someImport} from '@angular/common';
|
||||||
|
`);
|
||||||
|
|
||||||
|
runMigration();
|
||||||
|
|
||||||
|
const content = tree.readContent('/index.ts');
|
||||||
|
|
||||||
|
expect(content).toContain(`import { someImport, DOCUMENT } from '@angular/common';`);
|
||||||
|
expect(content).toContain(`import { anotherImport } from '@angular/platform-browser';`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly apply import replacement with existing import and alias', () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import {DOCUMENT as doc, anotherImport} from '@angular/platform-browser';
|
||||||
|
import {someImport} from '@angular/common';
|
||||||
|
`);
|
||||||
|
|
||||||
|
runMigration();
|
||||||
|
|
||||||
|
const content = tree.readContent('/index.ts');
|
||||||
|
|
||||||
|
expect(content).toContain(`import { someImport, DOCUMENT as doc } from '@angular/common';`);
|
||||||
|
expect(content).toContain(`import { anotherImport } from '@angular/platform-browser';`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function writeFile(filePath: string, contents: string) {
|
||||||
|
host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents));
|
||||||
|
}
|
||||||
|
|
||||||
|
function runMigration() { runner.runSchematic('migration-v8-move-document', {}, tree); }
|
||||||
|
});
|
Loading…
Reference in New Issue