Skip to content

Commit

Permalink
feat(core): add schematics to move deprecated DOCUMENT import (#29950)
Browse files Browse the repository at this point in the history
PR Close #29950
  • Loading branch information
CaerusKaru authored and benlesh committed Apr 22, 2019
1 parent f53d0fd commit 645e305
Show file tree
Hide file tree
Showing 8 changed files with 410 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/core/schematics/BUILD.bazel
Expand Up @@ -10,6 +10,7 @@ npm_package(
srcs = ["migrations.json"],
visibility = ["//packages/core:__pkg__"],
deps = [
"//packages/core/schematics/migrations/move-document",
"//packages/core/schematics/migrations/static-queries",
"//packages/core/schematics/migrations/template-var-assignment",
],
Expand Down
5 changes: 5 additions & 0 deletions packages/core/schematics/migrations.json
@@ -1,5 +1,10 @@
{
"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": {
"version": "8-beta",
"description": "Migrates ViewChild and ContentChild to explicit query timing",
Expand Down
18 changes: 18 additions & 0 deletions packages/core/schematics/migrations/move-document/BUILD.bazel
@@ -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);
}
}
97 changes: 97 additions & 0 deletions packages/core/schematics/migrations/move-document/index.ts
@@ -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);
});
}
64 changes: 64 additions & 0 deletions packages/core/schematics/migrations/move-document/move-import.ts
@@ -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);
}
1 change: 1 addition & 0 deletions packages/core/schematics/test/BUILD.bazel
Expand Up @@ -8,6 +8,7 @@ ts_library(
"//packages/core/schematics:migrations.json",
],
deps = [
"//packages/core/schematics/migrations/move-document",
"//packages/core/schematics/migrations/static-queries",
"//packages/core/schematics/migrations/static-queries/google3",
"//packages/core/schematics/migrations/template-var-assignment",
Expand Down

0 comments on commit 645e305

Please sign in to comment.