diff --git a/packages/core/schematics/BUILD.bazel b/packages/core/schematics/BUILD.bazel index 29546e0172..8b3f9de64d 100644 --- a/packages/core/schematics/BUILD.bazel +++ b/packages/core/schematics/BUILD.bazel @@ -12,6 +12,7 @@ npm_package( deps = [ "//packages/core/schematics/migrations/injectable-pipe", "//packages/core/schematics/migrations/move-document", + "//packages/core/schematics/migrations/renderer-to-renderer2", "//packages/core/schematics/migrations/static-queries", "//packages/core/schematics/migrations/template-var-assignment", ], diff --git a/packages/core/schematics/migrations.json b/packages/core/schematics/migrations.json index 6fb3a71b9d..9bf020f9db 100644 --- a/packages/core/schematics/migrations.json +++ b/packages/core/schematics/migrations.json @@ -14,6 +14,11 @@ "version": "8-beta", "description": "Warns developers if values are assigned to template variables", "factory": "./migrations/template-var-assignment/index" + }, + "migration-v9-renderer-to-renderer2": { + "version": "9-beta", + "description": "Migrates usages of Renderer to Renderer2", + "factory": "./migrations/renderer-to-renderer2/index" } } } diff --git a/packages/core/schematics/migrations/renderer-to-renderer2/BUILD.bazel b/packages/core/schematics/migrations/renderer-to-renderer2/BUILD.bazel new file mode 100644 index 0000000000..ab755c17f6 --- /dev/null +++ b/packages/core/schematics/migrations/renderer-to-renderer2/BUILD.bazel @@ -0,0 +1,18 @@ +load("//tools:defaults.bzl", "ts_library") + +ts_library( + name = "renderer-to-renderer2", + srcs = glob(["**/*.ts"]), + tsconfig = "//packages/core/schematics:tsconfig.json", + visibility = [ + "//packages/core/schematics:__pkg__", + "//packages/core/schematics/migrations/renderer-to-renderer2/google3:__pkg__", + "//packages/core/schematics/test:__pkg__", + ], + deps = [ + "//packages/core/schematics/utils", + "@npm//@angular-devkit/schematics", + "@npm//@types/node", + "@npm//typescript", + ], +) diff --git a/packages/core/schematics/migrations/renderer-to-renderer2/README.md b/packages/core/schematics/migrations/renderer-to-renderer2/README.md new file mode 100644 index 0000000000..b816cdd091 --- /dev/null +++ b/packages/core/schematics/migrations/renderer-to-renderer2/README.md @@ -0,0 +1,33 @@ +## Renderer -> Renderer2 migration + +Automatically migrates from `Renderer` to `Renderer2` by changing method calls, renaming imports +and renaming types. Tries to either map method calls directly from one renderer to the other, or +if that's not possible, inserts custom helper functions at the bottom of the file. + +#### Before +```ts +import { Renderer, ElementRef } from '@angular/core'; + +@Component({}) +export class MyComponent { + constructor(private _renderer: Renderer, private _elementRef: ElementRef) {} + + changeColor() { + this._renderer.setElementStyle(this._element.nativeElement, 'color', 'purple'); + } +} +``` + +#### After +```ts +import { Renderer2, ElementRef } from '@angular/core'; + +@Component({}) +export class MyComponent { + constructor(private _renderer: Renderer2, private _elementRef: ElementRef) {} + + changeColor() { + this._renderer.setStyle(this._element.nativeElement, 'color', 'purple'); + } +} +``` diff --git a/packages/core/schematics/migrations/renderer-to-renderer2/google3/BUILD.bazel b/packages/core/schematics/migrations/renderer-to-renderer2/google3/BUILD.bazel new file mode 100644 index 0000000000..d1772f9b2b --- /dev/null +++ b/packages/core/schematics/migrations/renderer-to-renderer2/google3/BUILD.bazel @@ -0,0 +1,13 @@ +load("//tools:defaults.bzl", "ts_library") + +ts_library( + name = "google3", + srcs = glob(["**/*.ts"]), + tsconfig = "//packages/core/schematics:tsconfig.json", + visibility = ["//packages/core/schematics/test:__pkg__"], + deps = [ + "//packages/core/schematics/migrations/renderer-to-renderer2", + "@npm//tslint", + "@npm//typescript", + ], +) diff --git a/packages/core/schematics/migrations/renderer-to-renderer2/google3/rendererToRenderer2Rule.ts b/packages/core/schematics/migrations/renderer-to-renderer2/google3/rendererToRenderer2Rule.ts new file mode 100644 index 0000000000..a6eaed901c --- /dev/null +++ b/packages/core/schematics/migrations/renderer-to-renderer2/google3/rendererToRenderer2Rule.ts @@ -0,0 +1,139 @@ +/** + * @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 {Replacement, RuleFailure, Rules} from 'tslint'; +import * as ts from 'typescript'; + +import {HelperFunction, getHelper} from '../helpers'; +import {migrateExpression, replaceImport} from '../migration'; +import {findCoreImport, findRendererReferences} from '../util'; + +/** + * TSLint rule that migrates from `Renderer` to `Renderer2`. More information on how it works: + * https://hackmd.angular.io/UTzUZTnPRA-cSa_4mHyfYw + */ +export class Rule extends Rules.TypedRule { + applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { + const typeChecker = program.getTypeChecker(); + const printer = ts.createPrinter(); + const failures: RuleFailure[] = []; + const rendererImport = findCoreImport(sourceFile, 'Renderer'); + + // If there are no imports for the `Renderer`, we can exit early. + if (!rendererImport) { + return failures; + } + + const {typedNodes, methodCalls, forwardRefs} = + findRendererReferences(sourceFile, typeChecker, rendererImport); + const helpersToAdd = new Set(); + + failures.push(this._getNamedImportsFailure(rendererImport, sourceFile, printer)); + typedNodes.forEach(node => failures.push(this._getTypedNodeFailure(node, sourceFile))); + forwardRefs.forEach(node => failures.push(this._getIdentifierNodeFailure(node, sourceFile))); + + methodCalls.forEach(call => { + const {failure, requiredHelpers} = + this._getMethodCallFailure(call, sourceFile, typeChecker, printer); + + failures.push(failure); + + if (requiredHelpers) { + requiredHelpers.forEach(helperName => helpersToAdd.add(helperName)); + } + }); + + // Some of the methods can't be mapped directly to `Renderer2` and need extra logic around them. + // The safest way to do so is to declare helper functions similar to the ones emitted by TS + // which encapsulate the extra "glue" logic. We should only emit these functions once per + // file and only if they're needed. + if (helpersToAdd.size) { + failures.push(this._getHelpersFailure(helpersToAdd, sourceFile, printer)); + } + + return failures; + } + + /** Gets a failure for an import of the Renderer. */ + private _getNamedImportsFailure( + node: ts.NamedImports, sourceFile: ts.SourceFile, printer: ts.Printer): RuleFailure { + const replacementText = printer.printNode( + ts.EmitHint.Unspecified, replaceImport(node, 'Renderer', 'Renderer2'), sourceFile); + + return new RuleFailure( + sourceFile, node.getStart(), node.getEnd(), + 'Imports of deprecated Renderer are not allowed. Please use Renderer2 instead.', + this.ruleName, new Replacement(node.getStart(), node.getWidth(), replacementText)); + } + + /** Gets a failure for a typed node (e.g. function parameter or property). */ + private _getTypedNodeFailure( + node: ts.ParameterDeclaration|ts.PropertyDeclaration|ts.AsExpression, + sourceFile: ts.SourceFile): RuleFailure { + const type = node.type !; + + return new RuleFailure( + sourceFile, type.getStart(), type.getEnd(), + 'References to deprecated Renderer are not allowed. Please use Renderer2 instead.', + this.ruleName, new Replacement(type.getStart(), type.getWidth(), 'Renderer2')); + } + + /** Gets a failure for an identifier node. */ + private _getIdentifierNodeFailure(node: ts.Identifier, sourceFile: ts.SourceFile): RuleFailure { + return new RuleFailure( + sourceFile, node.getStart(), node.getEnd(), + 'References to deprecated Renderer are not allowed. Please use Renderer2 instead.', + this.ruleName, new Replacement(node.getStart(), node.getWidth(), 'Renderer2')); + } + + /** Gets a failure for a Renderer method call. */ + private _getMethodCallFailure( + call: ts.CallExpression, sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker, + printer: ts.Printer): {failure: RuleFailure, requiredHelpers?: HelperFunction[]} { + const {node, requiredHelpers} = migrateExpression(call, typeChecker); + let fix: Replacement|undefined; + + if (node) { + // If we migrated the node to a new expression, replace only the call expression. + fix = new Replacement( + call.getStart(), call.getWidth(), + printer.printNode(ts.EmitHint.Unspecified, node, sourceFile)); + } else if (call.parent && ts.isExpressionStatement(call.parent)) { + // Otherwise if the call is inside an expression statement, drop the entire statement. + // This takes care of any trailing semicolons. We only need to drop nodes for cases like + // `setBindingDebugInfo` which have been noop for a while so they can be removed safely. + fix = new Replacement(call.parent.getStart(), call.parent.getWidth(), ''); + } + + return { + failure: new RuleFailure( + sourceFile, call.getStart(), call.getEnd(), 'Calls to Renderer methods are not allowed', + this.ruleName, fix), + requiredHelpers + }; + } + + /** Gets a failure that inserts the required helper functions at the bottom of the file. */ + private _getHelpersFailure( + helpersToAdd: Set, sourceFile: ts.SourceFile, + printer: ts.Printer): RuleFailure { + const helpers: Replacement[] = []; + const endOfFile = sourceFile.endOfFileToken; + + helpersToAdd.forEach(helperName => { + helpers.push(new Replacement( + endOfFile.getStart(), endOfFile.getWidth(), getHelper(helperName, sourceFile, printer))); + }); + + // Add a failure at the end of the file which we can use as an anchor to insert the helpers. + return new RuleFailure( + sourceFile, endOfFile.getStart(), endOfFile.getStart() + 1, + 'File should contain Renderer helper functions. Run tslint with --fix to generate them.', + this.ruleName, helpers); + } +} diff --git a/packages/core/schematics/migrations/renderer-to-renderer2/helpers.ts b/packages/core/schematics/migrations/renderer-to-renderer2/helpers.ts new file mode 100644 index 0000000000..b41a5c2cee --- /dev/null +++ b/packages/core/schematics/migrations/renderer-to-renderer2/helpers.ts @@ -0,0 +1,403 @@ +/** + * @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'; + +/** Names of the helper functions that are supported for this migration. */ +export const enum HelperFunction { + any = 'AnyDuringRendererMigration', + createElement = '__ngRendererCreateElementHelper', + createText = '__ngRendererCreateTextHelper', + createTemplateAnchor = '__ngRendererCreateTemplateAnchorHelper', + projectNodes = '__ngRendererProjectNodesHelper', + animate = '__ngRendererAnimateHelper', + destroyView = '__ngRendererDestroyViewHelper', + detachView = '__ngRendererDetachViewHelper', + attachViewAfter = '__ngRendererAttachViewAfterHelper', + splitNamespace = '__ngRendererSplitNamespaceHelper', + setElementAttribute = '__ngRendererSetElementAttributeHelper' +} + +/** Gets the string representation of a helper function. */ +export function getHelper( + name: HelperFunction, sourceFile: ts.SourceFile, printer: ts.Printer): string { + const helperDeclaration = getHelperDeclaration(name); + return '\n' + printer.printNode(ts.EmitHint.Unspecified, helperDeclaration, sourceFile) + '\n'; +} + +/** Creates a function declaration for the specified helper name. */ +function getHelperDeclaration(name: HelperFunction): ts.Node { + switch (name) { + case HelperFunction.any: + return createAnyTypeHelper(); + case HelperFunction.createElement: + return getCreateElementHelper(); + case HelperFunction.createText: + return getCreateTextHelper(); + case HelperFunction.createTemplateAnchor: + return getCreateTemplateAnchorHelper(); + case HelperFunction.projectNodes: + return getProjectNodesHelper(); + case HelperFunction.animate: + return getAnimateHelper(); + case HelperFunction.destroyView: + return getDestroyViewHelper(); + case HelperFunction.detachView: + return getDetachViewHelper(); + case HelperFunction.attachViewAfter: + return getAttachViewAfterHelper(); + case HelperFunction.setElementAttribute: + return getSetElementAttributeHelper(); + case HelperFunction.splitNamespace: + return getSplitNamespaceHelper(); + } + + throw new Error(`Unsupported helper called "${name}".`); +} + +/** Creates a helper for a custom `any` type during the migration. */ +function createAnyTypeHelper(): ts.TypeAliasDeclaration { + // type AnyDuringRendererMigration = any; + return ts.createTypeAliasDeclaration( + [], [], HelperFunction.any, [], ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); +} + +/** Creates a function parameter that is typed as `any`. */ +function getAnyTypedParameter( + parameterName: string | ts.Identifier, isRequired = true): ts.ParameterDeclaration { + // Declare the parameter as `any` so we don't have to add extra logic to ensure that the + // generated code will pass type checking. Use our custom `any` type so people have an incentive + // to clean it up afterwards and to avoid potentially introducing lint warnings in G3. + const type = ts.createTypeReferenceNode(HelperFunction.any, []); + return ts.createParameter( + [], [], undefined, parameterName, + isRequired ? undefined : ts.createToken(ts.SyntaxKind.QuestionToken), type); +} + +/** Creates a helper for `createElement`. */ +function getCreateElementHelper(): ts.FunctionDeclaration { + const renderer = ts.createIdentifier('renderer'); + const parent = ts.createIdentifier('parent'); + const namespaceAndName = ts.createIdentifier('namespaceAndName'); + const name = ts.createIdentifier('name'); + const namespace = ts.createIdentifier('namespace'); + + // [namespace, name] = splitNamespace(namespaceAndName); + const namespaceAndNameVariable = ts.createVariableDeclaration( + ts.createArrayBindingPattern( + [namespace, name].map(id => ts.createBindingElement(undefined, undefined, id))), + undefined, + ts.createCall(ts.createIdentifier(HelperFunction.splitNamespace), [], [namespaceAndName])); + + // `renderer.createElement(name, namespace)`. + const creationCall = + ts.createCall(ts.createPropertyAccess(renderer, 'createElement'), [], [name, namespace]); + + return getCreationHelper( + HelperFunction.createElement, creationCall, renderer, parent, [namespaceAndName], + [ts.createVariableStatement( + undefined, + ts.createVariableDeclarationList([namespaceAndNameVariable], ts.NodeFlags.Const))]); +} + +/** Creates a helper for `createText`. */ +function getCreateTextHelper(): ts.FunctionDeclaration { + const renderer = ts.createIdentifier('renderer'); + const parent = ts.createIdentifier('parent'); + const value = ts.createIdentifier('value'); + + // `renderer.createText(value)`. + const creationCall = ts.createCall(ts.createPropertyAccess(renderer, 'createText'), [], [value]); + + return getCreationHelper(HelperFunction.createText, creationCall, renderer, parent, [value]); +} + +/** Creates a helper for `createTemplateAnchor`. */ +function getCreateTemplateAnchorHelper(): ts.FunctionDeclaration { + const renderer = ts.createIdentifier('renderer'); + const parent = ts.createIdentifier('parent'); + + // `renderer.createComment('')`. + const creationCall = ts.createCall( + ts.createPropertyAccess(renderer, 'createComment'), [], [ts.createStringLiteral('')]); + + return getCreationHelper(HelperFunction.createTemplateAnchor, creationCall, renderer, parent); +} + +/** + * Gets the function declaration for a creation helper. This is reused between `createElement`, + * `createText` and `createTemplateAnchor` which follow a very similar pattern. + * @param functionName Function that the helper should have. + * @param creationCall Expression that is used to create a node inside the function. + * @param rendererParameter Parameter for the `renderer`. + * @param parentParameter Parameter for the `parent` inside the function. + * @param extraParameters Extra parameters to be added to the end. + * @param precedingVariables Extra variables to be added before the one that creates the `node`. + */ +function getCreationHelper( + functionName: HelperFunction, creationCall: ts.CallExpression, renderer: ts.Identifier, + parent: ts.Identifier, extraParameters: ts.Identifier[] = [], + precedingVariables: ts.VariableStatement[] = []): ts.FunctionDeclaration { + const node = ts.createIdentifier('node'); + + // `const node = {{creationCall}}`. + const nodeVariableStatement = ts.createVariableStatement( + undefined, + ts.createVariableDeclarationList( + [ts.createVariableDeclaration(node, undefined, creationCall)], ts.NodeFlags.Const)); + + // `if (parent) { renderer.appendChild(parent, node) }`. + const guardedAppendChildCall = ts.createIf( + parent, ts.createBlock( + [ts.createExpressionStatement(ts.createCall( + ts.createPropertyAccess(renderer, 'appendChild'), [], [parent, node]))], + true)); + + return ts.createFunctionDeclaration( + [], [], undefined, functionName, [], + [renderer, parent, ...extraParameters].map(name => getAnyTypedParameter(name)), undefined, + ts.createBlock( + [ + ...precedingVariables, nodeVariableStatement, guardedAppendChildCall, + ts.createReturn(node) + ], + true)); +} + +/** Creates a helper for `projectNodes`. */ +function getProjectNodesHelper(): ts.FunctionDeclaration { + const renderer = ts.createIdentifier('renderer'); + const parent = ts.createIdentifier('parent'); + const nodes = ts.createIdentifier('nodes'); + const incrementor = ts.createIdentifier('i'); + + // for (let i = 0; i < nodes.length; i++) { + // renderer.appendChild(parent, nodes[i]); + // } + const loopInitializer = ts.createVariableDeclarationList( + [ts.createVariableDeclaration(incrementor, undefined, ts.createNumericLiteral('0'))], + ts.NodeFlags.Let); + const loopCondition = ts.createBinary( + incrementor, ts.SyntaxKind.LessThanToken, + ts.createPropertyAccess(nodes, ts.createIdentifier('length'))); + const appendStatement = ts.createExpressionStatement(ts.createCall( + ts.createPropertyAccess(renderer, 'appendChild'), [], + [parent, ts.createElementAccess(nodes, incrementor)])); + const loop = ts.createFor( + loopInitializer, loopCondition, ts.createPostfix(incrementor, ts.SyntaxKind.PlusPlusToken), + ts.createBlock([appendStatement])); + + return ts.createFunctionDeclaration( + [], [], undefined, HelperFunction.projectNodes, [], + [renderer, parent, nodes].map(name => getAnyTypedParameter(name)), undefined, + ts.createBlock([loop], true)); +} + +/** Creates a helper for `animate`. */ +function getAnimateHelper(): ts.FunctionDeclaration { + // throw new Error('...'); + const throwStatement = ts.createThrow(ts.createNew( + ts.createIdentifier('Error'), [], + [ts.createStringLiteral('Renderer.animate is no longer supported!')])); + + return ts.createFunctionDeclaration( + [], [], undefined, HelperFunction.animate, [], [], undefined, + ts.createBlock([throwStatement], true)); +} + +/** Creates a helper for `destroyView`. */ +function getDestroyViewHelper(): ts.FunctionDeclaration { + const renderer = ts.createIdentifier('renderer'); + const allNodes = ts.createIdentifier('allNodes'); + const incrementor = ts.createIdentifier('i'); + + // for (let i = 0; i < allNodes.length; i++) { + // renderer.destroyNode(allNodes[i]); + // } + const loopInitializer = ts.createVariableDeclarationList( + [ts.createVariableDeclaration(incrementor, undefined, ts.createNumericLiteral('0'))], + ts.NodeFlags.Let); + const loopCondition = ts.createBinary( + incrementor, ts.SyntaxKind.LessThanToken, + ts.createPropertyAccess(allNodes, ts.createIdentifier('length'))); + const destroyStatement = ts.createExpressionStatement(ts.createCall( + ts.createPropertyAccess(renderer, 'destroyNode'), [], + [ts.createElementAccess(allNodes, incrementor)])); + const loop = ts.createFor( + loopInitializer, loopCondition, ts.createPostfix(incrementor, ts.SyntaxKind.PlusPlusToken), + ts.createBlock([destroyStatement])); + + return ts.createFunctionDeclaration( + [], [], undefined, HelperFunction.destroyView, [], + [renderer, allNodes].map(name => getAnyTypedParameter(name)), undefined, + ts.createBlock([loop], true)); +} + +/** Creates a helper for `detachView`. */ +function getDetachViewHelper(): ts.FunctionDeclaration { + const renderer = ts.createIdentifier('renderer'); + const rootNodes = ts.createIdentifier('rootNodes'); + const incrementor = ts.createIdentifier('i'); + const node = ts.createIdentifier('node'); + + // for (let i = 0; i < rootNodes.length; i++) { + // const node = rootNodes[i]; + // renderer.removeChild(renderer.parentNode(node), node); + // } + const loopInitializer = ts.createVariableDeclarationList( + [ts.createVariableDeclaration(incrementor, undefined, ts.createNumericLiteral('0'))], + ts.NodeFlags.Let); + const loopCondition = ts.createBinary( + incrementor, ts.SyntaxKind.LessThanToken, + ts.createPropertyAccess(rootNodes, ts.createIdentifier('length'))); + + // const node = rootNodes[i]; + const nodeVariableStatement = ts.createVariableStatement( + undefined, ts.createVariableDeclarationList( + [ts.createVariableDeclaration( + node, undefined, ts.createElementAccess(rootNodes, incrementor))], + ts.NodeFlags.Const)); + // renderer.removeChild(renderer.parentNode(node), node); + const removeCall = ts.createCall( + ts.createPropertyAccess(renderer, 'removeChild'), [], + [ts.createCall(ts.createPropertyAccess(renderer, 'parentNode'), [], [node]), node]); + + const loop = ts.createFor( + loopInitializer, loopCondition, ts.createPostfix(incrementor, ts.SyntaxKind.PlusPlusToken), + ts.createBlock([nodeVariableStatement, ts.createExpressionStatement(removeCall)])); + + return ts.createFunctionDeclaration( + [], [], undefined, HelperFunction.detachView, [], + [renderer, rootNodes].map(name => getAnyTypedParameter(name)), undefined, + ts.createBlock([loop], true)); +} + +/** Creates a helper for `attachViewAfter` */ +function getAttachViewAfterHelper(): ts.FunctionDeclaration { + const renderer = ts.createIdentifier('renderer'); + const node = ts.createIdentifier('node'); + const rootNodes = ts.createIdentifier('rootNodes'); + const parent = ts.createIdentifier('parent'); + const nextSibling = ts.createIdentifier('nextSibling'); + const incrementor = ts.createIdentifier('i'); + const createConstWithMethodCallInitializer = (constName: ts.Identifier, methodToCall: string) => { + return ts.createVariableStatement( + undefined, + ts.createVariableDeclarationList( + [ts.createVariableDeclaration( + constName, undefined, + ts.createCall(ts.createPropertyAccess(renderer, methodToCall), [], [node]))], + ts.NodeFlags.Const)); + }; + + // const parent = renderer.parentNode(node); + const parentVariableStatement = createConstWithMethodCallInitializer(parent, 'parentNode'); + + // const nextSibling = renderer.nextSibling(node); + const nextSiblingVariableStatement = + createConstWithMethodCallInitializer(nextSibling, 'nextSibling'); + + // for (let i = 0; i < rootNodes.length; i++) { + // renderer.insertBefore(parentElement, rootNodes[i], nextSibling); + // } + const loopInitializer = ts.createVariableDeclarationList( + [ts.createVariableDeclaration(incrementor, undefined, ts.createNumericLiteral('0'))], + ts.NodeFlags.Let); + const loopCondition = ts.createBinary( + incrementor, ts.SyntaxKind.LessThanToken, + ts.createPropertyAccess(rootNodes, ts.createIdentifier('length'))); + const insertBeforeCall = ts.createCall( + ts.createPropertyAccess(renderer, 'insertBefore'), [], + [parent, ts.createElementAccess(rootNodes, incrementor), nextSibling]); + const loop = ts.createFor( + loopInitializer, loopCondition, ts.createPostfix(incrementor, ts.SyntaxKind.PlusPlusToken), + ts.createBlock([ts.createExpressionStatement(insertBeforeCall)])); + + return ts.createFunctionDeclaration( + [], [], undefined, HelperFunction.attachViewAfter, [], + [renderer, node, rootNodes].map(name => getAnyTypedParameter(name)), undefined, + ts.createBlock([parentVariableStatement, nextSiblingVariableStatement, loop], true)); +} + +/** Creates a helper for `setElementAttribute` */ +function getSetElementAttributeHelper(): ts.FunctionDeclaration { + const renderer = ts.createIdentifier('renderer'); + const element = ts.createIdentifier('element'); + const namespaceAndName = ts.createIdentifier('namespaceAndName'); + const value = ts.createIdentifier('value'); + const name = ts.createIdentifier('name'); + const namespace = ts.createIdentifier('namespace'); + + // [namespace, name] = splitNamespace(namespaceAndName); + const namespaceAndNameVariable = ts.createVariableDeclaration( + ts.createArrayBindingPattern( + [namespace, name].map(id => ts.createBindingElement(undefined, undefined, id))), + undefined, + ts.createCall(ts.createIdentifier(HelperFunction.splitNamespace), [], [namespaceAndName])); + + // renderer.setAttribute(element, name, value, namespace); + const setCall = ts.createCall( + ts.createPropertyAccess(renderer, 'setAttribute'), [], [element, name, value, namespace]); + + // renderer.removeAttribute(element, name, namespace); + const removeCall = ts.createCall( + ts.createPropertyAccess(renderer, 'removeAttribute'), [], [element, name, namespace]); + + // if (value != null) { setCall() } else { removeCall } + const ifStatement = ts.createIf( + ts.createBinary(value, ts.SyntaxKind.ExclamationEqualsToken, ts.createNull()), + ts.createBlock([ts.createExpressionStatement(setCall)], true), + ts.createBlock([ts.createExpressionStatement(removeCall)], true)); + + const functionBody = ts.createBlock( + [ + ts.createVariableStatement( + undefined, + ts.createVariableDeclarationList([namespaceAndNameVariable], ts.NodeFlags.Const)), + ifStatement + ], + true); + + return ts.createFunctionDeclaration( + [], [], undefined, HelperFunction.setElementAttribute, [], + [ + getAnyTypedParameter(renderer), getAnyTypedParameter(element), + getAnyTypedParameter(namespaceAndName), getAnyTypedParameter(value, false) + ], + undefined, functionBody); +} + +/** Creates a helper for splitting a name that might contain a namespace. */ +function getSplitNamespaceHelper(): ts.FunctionDeclaration { + const name = ts.createIdentifier('name'); + const match = ts.createIdentifier('match'); + const regex = ts.createRegularExpressionLiteral('/^:([^:]+):(.+)$/'); + const matchCall = ts.createCall(ts.createPropertyAccess(name, 'match'), [], [regex]); + + // const match = name.split(regex); + const matchVariable = ts.createVariableDeclarationList( + [ts.createVariableDeclaration(match, undefined, matchCall)], ts.NodeFlags.Const); + + // return [match[1], match[2]]; + const matchReturn = ts.createReturn( + ts.createArrayLiteral([ts.createElementAccess(match, 1), ts.createElementAccess(match, 2)])); + + // if (name[0] === ':') { const match = ...; return ...; } + const ifStatement = ts.createIf( + ts.createBinary( + ts.createElementAccess(name, 0), ts.SyntaxKind.EqualsEqualsEqualsToken, + ts.createStringLiteral(':')), + ts.createBlock([ts.createVariableStatement([], matchVariable), matchReturn], true)); + + // return ['', name]; + const elseReturn = ts.createReturn(ts.createArrayLiteral([ts.createStringLiteral(''), name])); + + return ts.createFunctionDeclaration( + [], [], undefined, HelperFunction.splitNamespace, [], [getAnyTypedParameter(name)], undefined, + ts.createBlock([ifStatement, elseReturn], true)); +} diff --git a/packages/core/schematics/migrations/renderer-to-renderer2/index.ts b/packages/core/schematics/migrations/renderer-to-renderer2/index.ts new file mode 100644 index 0000000000..57eda170e7 --- /dev/null +++ b/packages/core/schematics/migrations/renderer-to-renderer2/index.ts @@ -0,0 +1,134 @@ +/** + * @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, SchematicContext, 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 {HelperFunction, getHelper} from './helpers'; +import {migrateExpression, replaceImport} from './migration'; +import {findCoreImport, findRendererReferences} from './util'; + + +/** + * Migration that switches from `Renderer` to `Renderer2`. More information on how it works: + * https://hackmd.angular.io/UTzUZTnPRA-cSa_4mHyfYw + */ +export default function(): Rule { + return (tree: Tree, context: SchematicContext) => { + const {buildPaths, testPaths} = getProjectTsConfigPaths(tree); + const basePath = process.cwd(); + const allPaths = [...buildPaths, ...testPaths]; + const logger = context.logger; + + logger.info('------ Renderer to Renderer2 Migration ------'); + logger.info('As of Angular 9, the Renderer class is no longer available.'); + logger.info('Renderer2 should be used instead.'); + + if (!allPaths.length) { + throw new SchematicsException( + 'Could not find any tsconfig file. Cannot migrate Renderer usages to Renderer2.'); + } + + for (const tsconfigPath of allPaths) { + runRendererToRenderer2Migration(tree, tsconfigPath, basePath); + } + }; +} + +function runRendererToRenderer2Migration(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 printer = ts.createPrinter(); + const sourceFiles = program.getSourceFiles().filter( + f => !f.isDeclarationFile && !program.isSourceFileFromExternalLibrary(f)); + + sourceFiles.forEach(sourceFile => { + const rendererImport = findCoreImport(sourceFile, 'Renderer'); + + // If there are no imports for the `Renderer`, we can exit early. + if (!rendererImport) { + return; + } + + const {typedNodes, methodCalls, forwardRefs} = + findRendererReferences(sourceFile, typeChecker, rendererImport); + const update = tree.beginUpdate(relative(basePath, sourceFile.fileName)); + const helpersToAdd = new Set(); + + // Change the `Renderer` import to `Renderer2`. + update.remove(rendererImport.getStart(), rendererImport.getWidth()); + update.insertRight( + rendererImport.getStart(), + printer.printNode( + ts.EmitHint.Unspecified, replaceImport(rendererImport, 'Renderer', 'Renderer2'), + sourceFile)); + + // Change the method parameter and property types to `Renderer2`. + typedNodes.forEach(node => { + const type = node.type; + + if (type) { + update.remove(type.getStart(), type.getWidth()); + update.insertRight(type.getStart(), 'Renderer2'); + } + }); + + // Change all identifiers inside `forwardRef` referring to the `Renderer`. + forwardRefs.forEach(identifier => { + update.remove(identifier.getStart(), identifier.getWidth()); + update.insertRight(identifier.getStart(), 'Renderer2'); + }); + + // Migrate all of the method calls. + methodCalls.forEach(call => { + const {node, requiredHelpers} = migrateExpression(call, typeChecker); + + if (node) { + // If we migrated the node to a new expression, replace only the call expression. + update.remove(call.getStart(), call.getWidth()); + update.insertRight( + call.getStart(), printer.printNode(ts.EmitHint.Unspecified, node, sourceFile)); + } else if (call.parent && ts.isExpressionStatement(call.parent)) { + // Otherwise if the call is inside an expression statement, drop the entire statement. + // This takes care of any trailing semicolons. We only need to drop nodes for cases like + // `setBindingDebugInfo` which have been noop for a while so they can be removed safely. + update.remove(call.parent.getStart(), call.parent.getWidth()); + } + + if (requiredHelpers) { + requiredHelpers.forEach(helperName => helpersToAdd.add(helperName)); + } + }); + + // Some of the methods can't be mapped directly to `Renderer2` and need extra logic around them. + // The safest way to do so is to declare helper functions similar to the ones emitted by TS + // which encapsulate the extra "glue" logic. We should only emit these functions once per file. + helpersToAdd.forEach(helperName => { + update.insertLeft( + sourceFile.endOfFileToken.getStart(), getHelper(helperName, sourceFile, printer)); + }); + + tree.commitUpdate(update); + }); +} diff --git a/packages/core/schematics/migrations/renderer-to-renderer2/migration.ts b/packages/core/schematics/migrations/renderer-to-renderer2/migration.ts new file mode 100644 index 0000000000..88f29fd3e3 --- /dev/null +++ b/packages/core/schematics/migrations/renderer-to-renderer2/migration.ts @@ -0,0 +1,268 @@ +/** + * @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'; + +import {HelperFunction} from './helpers'; +import {findImportSpecifier} from './util'; + +/** A call expression that is based on a property access. */ +type PropertyAccessCallExpression = ts.CallExpression & {expression: ts.PropertyAccessExpression}; + +/** Replaces an import inside an import statement with a different one. */ +export function replaceImport(node: ts.NamedImports, oldImport: string, newImport: string) { + const isAlreadyImported = findImportSpecifier(node.elements, newImport); + + if (isAlreadyImported) { + return node; + } + + const existingImport = findImportSpecifier(node.elements, oldImport); + + if (!existingImport) { + throw new Error(`Could not find an import to replace using ${oldImport}.`); + } + + return ts.updateNamedImports(node, [ + ...node.elements.filter(current => current !== existingImport), + // Create a new import while trying to preserve the alias of the old one. + ts.createImportSpecifier( + existingImport.propertyName ? ts.createIdentifier(newImport) : undefined, + existingImport.propertyName ? existingImport.name : ts.createIdentifier(newImport)) + ]); +} + +/** + * Migrates a function call expression from `Renderer` to `Renderer2`. + * Returns null if the expression should be dropped. + */ +export function migrateExpression(node: ts.CallExpression, typeChecker: ts.TypeChecker): + {node: ts.Node | null, requiredHelpers?: HelperFunction[]} { + if (isPropertyAccessCallExpression(node)) { + switch (node.expression.name.getText()) { + case 'setElementProperty': + return {node: renameMethodCall(node, 'setProperty')}; + case 'setText': + return {node: renameMethodCall(node, 'setValue')}; + case 'listenGlobal': + return {node: renameMethodCall(node, 'listen')}; + case 'selectRootElement': + return {node: migrateSelectRootElement(node)}; + case 'setElementClass': + return {node: migrateSetElementClass(node)}; + case 'setElementStyle': + return {node: migrateSetElementStyle(node, typeChecker)}; + case 'invokeElementMethod': + return {node: migrateInvokeElementMethod(node)}; + case 'setBindingDebugInfo': + return {node: null}; + case 'createViewRoot': + return {node: migrateCreateViewRoot(node)}; + case 'setElementAttribute': + return { + node: switchToHelperCall(node, HelperFunction.setElementAttribute, node.arguments), + requiredHelpers: [ + HelperFunction.any, HelperFunction.splitNamespace, HelperFunction.setElementAttribute + ] + }; + case 'createElement': + return { + node: switchToHelperCall(node, HelperFunction.createElement, node.arguments.slice(0, 2)), + requiredHelpers: + [HelperFunction.any, HelperFunction.splitNamespace, HelperFunction.createElement] + }; + case 'createText': + return { + node: switchToHelperCall(node, HelperFunction.createText, node.arguments.slice(0, 2)), + requiredHelpers: [HelperFunction.any, HelperFunction.createText] + }; + case 'createTemplateAnchor': + return { + node: switchToHelperCall( + node, HelperFunction.createTemplateAnchor, node.arguments.slice(0, 1)), + requiredHelpers: [HelperFunction.any, HelperFunction.createTemplateAnchor] + }; + case 'projectNodes': + return { + node: switchToHelperCall(node, HelperFunction.projectNodes, node.arguments), + requiredHelpers: [HelperFunction.any, HelperFunction.projectNodes] + }; + case 'animate': + return { + node: migrateAnimateCall(), + requiredHelpers: [HelperFunction.any, HelperFunction.animate] + }; + case 'destroyView': + return { + node: switchToHelperCall(node, HelperFunction.destroyView, [node.arguments[1]]), + requiredHelpers: [HelperFunction.any, HelperFunction.destroyView] + }; + case 'detachView': + return { + node: switchToHelperCall(node, HelperFunction.detachView, [node.arguments[0]]), + requiredHelpers: [HelperFunction.any, HelperFunction.detachView] + }; + case 'attachViewAfter': + return { + node: switchToHelperCall(node, HelperFunction.attachViewAfter, node.arguments), + requiredHelpers: [HelperFunction.any, HelperFunction.attachViewAfter] + }; + } + } + + return {node}; +} + +/** Checks whether a node is a PropertyAccessExpression. */ +function isPropertyAccessCallExpression(node: ts.Node): node is PropertyAccessCallExpression { + return ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression); +} + +/** Renames a method call while keeping all of the parameters in place. */ +function renameMethodCall(node: PropertyAccessCallExpression, newName: string): ts.CallExpression { + const newExpression = ts.updatePropertyAccess( + node.expression, node.expression.expression, ts.createIdentifier(newName)); + + return ts.updateCall(node, newExpression, node.typeArguments, node.arguments); +} + +/** + * Migrates a `selectRootElement` call by removing the last argument which is no longer supported. + */ +function migrateSelectRootElement(node: ts.CallExpression): ts.Node { + // The only thing we need to do is to drop the last argument + // (`debugInfo`), if the consumer was passing it in. + if (node.arguments.length > 1) { + return ts.updateCall(node, node.expression, node.typeArguments, [node.arguments[0]]); + } + + return node; +} + +/** + * Migrates a call to `setElementClass` either to a call to `addClass` or `removeClass`, or + * to an expression like `isAdd ? addClass(el, className) : removeClass(el, className)`. + */ +function migrateSetElementClass(node: PropertyAccessCallExpression): ts.Node { + // Clone so we don't mutate by accident. Note that we assume that + // the user's code is providing all three required arguments. + const outputMethodArgs = node.arguments.slice(); + const isAddArgument = outputMethodArgs.pop() !; + const createRendererCall = (isAdd: boolean) => { + const innerExpression = node.expression.expression; + const topExpression = + ts.createPropertyAccess(innerExpression, isAdd ? 'addClass' : 'removeClass'); + return ts.createCall(topExpression, [], node.arguments.slice(0, 2)); + }; + + // If the call has the `isAdd` argument as a literal boolean, we can map it directly to + // `addClass` or `removeClass`. Note that we can't use the type checker here, because it + // won't tell us whether the value resolves to true or false. + if (isAddArgument.kind === ts.SyntaxKind.TrueKeyword || + isAddArgument.kind === ts.SyntaxKind.FalseKeyword) { + return createRendererCall(isAddArgument.kind === ts.SyntaxKind.TrueKeyword); + } + + // Otherwise create a ternary on the variable. + return ts.createConditional(isAddArgument, createRendererCall(true), createRendererCall(false)); +} + +/** + * Migrates a call to `setElementStyle` call either to a call to + * `setStyle` or `removeStyle`. or to an expression like + * `value == null ? removeStyle(el, key) : setStyle(el, key, value)`. + */ +function migrateSetElementStyle( + node: PropertyAccessCallExpression, typeChecker: ts.TypeChecker): ts.Node { + const args = node.arguments; + const addMethodName = 'setStyle'; + const removeMethodName = 'removeStyle'; + const lastArgType = args[2] ? + typeChecker.typeToString( + typeChecker.getTypeAtLocation(args[2]), node, ts.TypeFormatFlags.AddUndefined) : + null; + + // Note that for a literal null, TS considers it a `NullKeyword`, + // whereas a literal `undefined` is just an Identifier. + if (args.length === 2 || lastArgType === 'null' || lastArgType === 'undefined') { + // If we've got a call with two arguments, or one with three arguments where the last one is + // `undefined` or `null`, we can safely switch to a `removeStyle` call. + const innerExpression = node.expression.expression; + const topExpression = ts.createPropertyAccess(innerExpression, removeMethodName); + return ts.createCall(topExpression, [], args.slice(0, 2)); + } else if (args.length === 3) { + // We need the checks for string literals, because the type of something + // like `"blue"` is the literal `blue`, not `string`. + if (lastArgType === 'string' || lastArgType === 'number' || ts.isStringLiteral(args[2]) || + ts.isNoSubstitutionTemplateLiteral(args[2]) || ts.isNumericLiteral(args[2])) { + // If we've got three arguments and the last one is a string literal or a number, we + // can safely rename to `setStyle`. + return renameMethodCall(node, addMethodName); + } else { + // Otherwise migrate to a ternary that looks like: + // `value == null ? removeStyle(el, key) : setStyle(el, key, value)` + const condition = ts.createBinary(args[2], ts.SyntaxKind.EqualsEqualsToken, ts.createNull()); + const whenNullCall = renameMethodCall( + ts.createCall(node.expression, [], args.slice(0, 2)) as PropertyAccessCallExpression, + removeMethodName); + return ts.createConditional(condition, whenNullCall, renameMethodCall(node, addMethodName)); + } + } + + return node; +} + +/** + * Migrates a call to `invokeElementMethod(target, method, [arg1, arg2])` either to + * `target.method(arg1, arg2)` or `(target as any)[method].apply(target, [arg1, arg2])`. + */ +function migrateInvokeElementMethod(node: ts.CallExpression): ts.Node { + const [target, name, args] = node.arguments; + const isNameStatic = ts.isStringLiteral(name) || ts.isNoSubstitutionTemplateLiteral(name); + const isArgsStatic = !args || ts.isArrayLiteralExpression(args); + + if (isNameStatic && isArgsStatic) { + // If the name is a static string and the arguments are an array literal, + // we can safely convert the node into a call expression. + const expression = ts.createPropertyAccess( + target, (name as ts.StringLiteral | ts.NoSubstitutionTemplateLiteral).text); + const callArguments = args ? (args as ts.ArrayLiteralExpression).elements : []; + return ts.createCall(expression, [], callArguments); + } else { + // Otherwise create an expression in the form of `(target as any)[name].apply(target, args)`. + const asExpression = ts.createParen( + ts.createAsExpression(target, ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword))); + const elementAccess = ts.createElementAccess(asExpression, name); + const applyExpression = ts.createPropertyAccess(elementAccess, 'apply'); + return ts.createCall(applyExpression, [], args ? [target, args] : [target]); + } +} + +/** Migrates a call to `createViewRoot` to whatever node was passed in as the first argument. */ +function migrateCreateViewRoot(node: ts.CallExpression): ts.Node { + return node.arguments[0]; +} + +/** Migrates a call to `migrate` a direct call to the helper. */ +function migrateAnimateCall() { + return ts.createCall(ts.createIdentifier(HelperFunction.animate), [], []); +} + +/** + * Switches out a call to the `Renderer` to a call to one of our helper functions. + * Most of the helpers accept an instance of `Renderer2` as the first argument and all + * subsequent arguments differ. + * @param node Node of the original method call. + * @param helper Name of the helper with which to replace the original call. + * @param args Arguments that should be passed into the helper after the renderer argument. + */ +function switchToHelperCall( + node: PropertyAccessCallExpression, helper: HelperFunction, + args: ts.Expression[] | ts.NodeArray): ts.Node { + return ts.createCall(ts.createIdentifier(helper), [], [node.expression.expression, ...args]); +} diff --git a/packages/core/schematics/migrations/renderer-to-renderer2/util.ts b/packages/core/schematics/migrations/renderer-to-renderer2/util.ts new file mode 100644 index 0000000000..e2dc21df8a --- /dev/null +++ b/packages/core/schematics/migrations/renderer-to-renderer2/util.ts @@ -0,0 +1,125 @@ +/** + * @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'; + +/** + * Finds typed nodes (e.g. function parameters or class properties) that are referencing the old + * `Renderer`, as well as calls to the `Renderer` methods. + */ +export function findRendererReferences( + sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker, rendererImport: ts.NamedImports) { + const typedNodes = new Set(); + const methodCalls = new Set(); + const forwardRefs = new Set(); + const importSpecifier = findImportSpecifier(rendererImport.elements, 'Renderer'); + const forwardRefImport = findCoreImport(sourceFile, 'forwardRef'); + const forwardRefSpecifier = + forwardRefImport ? findImportSpecifier(forwardRefImport.elements, 'forwardRef') : null; + + ts.forEachChild(sourceFile, function visitNode(node: ts.Node) { + if ((ts.isParameter(node) || ts.isPropertyDeclaration(node)) && + isReferenceToImport(typeChecker, node.name, importSpecifier)) { + typedNodes.add(node); + } else if ( + ts.isAsExpression(node) && isReferenceToImport(typeChecker, node.type, importSpecifier)) { + typedNodes.add(node); + } else if (ts.isCallExpression(node)) { + if (ts.isPropertyAccessExpression(node.expression) && + isReferenceToImport(typeChecker, node.expression.expression, importSpecifier)) { + methodCalls.add(node); + } else if ( + // If we're dealing with a forwardRef that's returning a Renderer. + forwardRefSpecifier && ts.isIdentifier(node.expression) && + isReferenceToImport(typeChecker, node.expression, forwardRefSpecifier) && + node.arguments.length) { + const rendererIdentifier = + findRendererIdentifierInForwardRef(typeChecker, node, importSpecifier); + if (rendererIdentifier) { + forwardRefs.add(rendererIdentifier); + } + } + } + + ts.forEachChild(node, visitNode); + }); + + return {typedNodes, methodCalls, forwardRefs}; +} + +/** Finds the import from @angular/core that has a symbol with a particular name. */ +export function findCoreImport(sourceFile: ts.SourceFile, symbolName: string): ts.NamedImports| + null { + // Only look through the top-level imports. + for (const node of sourceFile.statements) { + if (!ts.isImportDeclaration(node) || !ts.isStringLiteral(node.moduleSpecifier) || + node.moduleSpecifier.text !== '@angular/core') { + continue; + } + + const namedBindings = node.importClause && node.importClause.namedBindings; + + if (!namedBindings || !ts.isNamedImports(namedBindings)) { + continue; + } + + if (findImportSpecifier(namedBindings.elements, symbolName)) { + return namedBindings; + } + } + + return null; +} + +/** Finds an import specifier with a particular name, accounting for aliases. */ +export function findImportSpecifier( + elements: ts.NodeArray, importName: string) { + return elements.find(element => { + const {name, propertyName} = element; + return propertyName ? propertyName.text === importName : name.text === importName; + }) || + null; +} + +/** Checks whether a node is referring to an import spcifier. */ +function isReferenceToImport( + typeChecker: ts.TypeChecker, node: ts.Node, + importSpecifier: ts.ImportSpecifier | null): boolean { + if (importSpecifier) { + const nodeSymbol = typeChecker.getTypeAtLocation(node).getSymbol(); + const importSymbol = typeChecker.getTypeAtLocation(importSpecifier).getSymbol(); + return !!(nodeSymbol && importSymbol) && + nodeSymbol.valueDeclaration === importSymbol.valueDeclaration; + } + return false; +} + +/** Finds the identifier referring to the `Renderer` inside a `forwardRef` call expression. */ +function findRendererIdentifierInForwardRef( + typeChecker: ts.TypeChecker, node: ts.CallExpression, + rendererImport: ts.ImportSpecifier | null): ts.Identifier|null { + const firstArg = node.arguments[0]; + + if (ts.isArrowFunction(firstArg)) { + // Check if the function is `forwardRef(() => Renderer)`. + if (ts.isIdentifier(firstArg.body) && + isReferenceToImport(typeChecker, firstArg.body, rendererImport)) { + return firstArg.body; + } else if (ts.isBlock(firstArg.body) && ts.isReturnStatement(firstArg.body.statements[0])) { + // Otherwise check if the expression is `forwardRef(() => { return Renderer })`. + const returnStatement = firstArg.body.statements[0] as ts.ReturnStatement; + + if (returnStatement.expression && ts.isIdentifier(returnStatement.expression) && + isReferenceToImport(typeChecker, returnStatement.expression, rendererImport)) { + return returnStatement.expression; + } + } + } + + return null; +} diff --git a/packages/core/schematics/test/BUILD.bazel b/packages/core/schematics/test/BUILD.bazel index b064f3a3af..e63051f3a1 100644 --- a/packages/core/schematics/test/BUILD.bazel +++ b/packages/core/schematics/test/BUILD.bazel @@ -12,6 +12,8 @@ ts_library( "//packages/core/schematics/migrations/injectable-pipe", "//packages/core/schematics/migrations/injectable-pipe/google3", "//packages/core/schematics/migrations/move-document", + "//packages/core/schematics/migrations/renderer-to-renderer2", + "//packages/core/schematics/migrations/renderer-to-renderer2/google3", "//packages/core/schematics/migrations/static-queries", "//packages/core/schematics/migrations/static-queries/google3", "//packages/core/schematics/migrations/template-var-assignment", diff --git a/packages/core/schematics/test/google3/renderer_to_renderer2_spec.ts b/packages/core/schematics/test/google3/renderer_to_renderer2_spec.ts new file mode 100644 index 0000000000..6fb9961738 --- /dev/null +++ b/packages/core/schematics/test/google3/renderer_to_renderer2_spec.ts @@ -0,0 +1,415 @@ +/** + * @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 {readFileSync, writeFileSync} from 'fs'; +import {dirname, join} from 'path'; +import * as shx from 'shelljs'; +import {Configuration, Linter} from 'tslint'; + +describe('Google3 Renderer to Renderer2 TSLint rule', () => { + const rulesDirectory = dirname( + require.resolve('../../migrations/renderer-to-renderer2/google3/rendererToRenderer2Rule')); + + let tmpDir: string; + + beforeEach(() => { + tmpDir = join(process.env['TEST_TMPDIR'] !, 'google3-test'); + shx.mkdir('-p', tmpDir); + + // We need to declare the Angular symbols we're testing for, otherwise type checking won't work. + writeFile('angular.d.ts', ` + export declare abstract class Renderer {} + export declare function forwardRef(fn: () => any): any {} + `); + + writeFile('tsconfig.json', JSON.stringify({ + compilerOptions: { + module: 'es2015', + baseUrl: './', + paths: { + '@angular/core': ['angular.d.ts'], + } + } + })); + }); + + afterEach(() => shx.rm('-r', tmpDir)); + + function runTSLint(fix: boolean) { + const program = Linter.createProgram(join(tmpDir, 'tsconfig.json')); + const linter = new Linter({fix, rulesDirectory: [rulesDirectory]}, program); + const config = Configuration.parseConfigFile( + {rules: {'renderer-to-renderer2': true}, linterOptions: {typeCheck: true}}); + + program.getRootFileNames().forEach(fileName => { + linter.lint(fileName, program.getSourceFile(fileName) !.getFullText(), config); + }); + + return linter; + } + + function writeFile(fileName: string, content: string) { + writeFileSync(join(tmpDir, fileName), content); + } + + function getFile(fileName: string) { return readFileSync(join(tmpDir, fileName), 'utf8'); } + + it('should flag Renderer imports and typed nodes', () => { + writeFile('/index.ts', ` + import { Renderer, Component } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + public renderer: Renderer; + + constructor(renderer: Renderer) { + this.renderer = renderer; + } + } + `); + + const linter = runTSLint(false); + const failures = linter.getResult().failures.map(failure => failure.getFailure()); + + expect(failures.length).toBe(3); + expect(failures[0]).toMatch(/Imports of deprecated Renderer are not allowed/); + expect(failures[1]).toMatch(/References to deprecated Renderer are not allowed/); + expect(failures[2]).toMatch(/References to deprecated Renderer are not allowed/); + }); + + it('should change Renderer imports and typed nodes to Renderer2', () => { + writeFile('/index.ts', ` + import { Renderer, Component } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + public renderer: Renderer; + + constructor(renderer: Renderer) { + this.renderer = renderer; + } + } + `); + + runTSLint(true); + const content = getFile('index.ts'); + + expect(content).toContain(`import { Component, Renderer2 } from '@angular/core';`); + expect(content).toContain('public renderer: Renderer2;'); + expect(content).toContain('(renderer: Renderer2)'); + }); + + it('should change Renderer inside single-line forwardRefs to Renderer2', () => { + writeFile('/index.ts', ` + import { Renderer, Component, forwardRef, Inject } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(@Inject(forwardRef(() => Renderer)) private _renderer: Renderer) {} + } + `); + + runTSLint(true); + const content = getFile('index.ts'); + + expect(content).toContain( + `constructor(@Inject(forwardRef(() => Renderer2)) private _renderer: Renderer2) {}`); + }); + + it('should change Renderer inside multi-line forwardRefs to Renderer2', () => { + writeFile('/index.ts', ` + import { Renderer, Component, forwardRef, Inject } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(@Inject(forwardRef(() => { return Renderer; })) private _renderer: Renderer) {} + } + `); + + runTSLint(true); + const content = getFile('index.ts'); + + expect(content).toContain( + `constructor(@Inject(forwardRef(() => { return Renderer2; })) private _renderer: Renderer2) {}`); + }); + + it('should flag something that was cast to Renderer', () => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + setColor(maybeRenderer: any, element: ElementRef) { + const renderer = maybeRenderer as Renderer; + renderer.setElementStyle(element.nativeElement, 'color', 'red'); + } + } + `); + + const linter = runTSLint(false); + const failures = linter.getResult().failures.map(failure => failure.getFailure()); + + expect(failures.length).toBe(3); + expect(failures[0]).toMatch(/Imports of deprecated Renderer are not allowed/); + expect(failures[1]).toMatch(/References to deprecated Renderer are not allowed/); + expect(failures[2]).toMatch(/Calls to Renderer methods are not allowed/); + }); + + it('should change the type of something that was cast to Renderer', () => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + setColor(maybeRenderer: any, element: ElementRef) { + const renderer = maybeRenderer as Renderer; + renderer.setElementStyle(element.nativeElement, 'color', 'red'); + } + } + `); + + runTSLint(true); + const content = getFile('index.ts'); + + expect(content).toContain(`const renderer = maybeRenderer as Renderer2;`); + expect(content).toContain(`renderer.setStyle(element.nativeElement, 'color', 'red');`); + }); + + it('should be able to insert helper functions', () => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(renderer: Renderer, element: ElementRef) { + const el = renderer.createElement(element.nativeElement, 'div'); + renderer.setElementAttribute(el, 'title', 'hello'); + renderer.projectNodes(element.nativeElement, [el]); + } + } + `); + + runTSLint(true); + const content = getFile('index.ts'); + + expect(content).toContain(`function __ngRendererCreateElementHelper(`); + expect(content).toContain(`function __ngRendererSetElementAttributeHelper(`); + expect(content).toContain(`function __ngRendererProjectNodesHelper(`); + }); + + it('should only insert each helper only once per file', () => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(renderer: Renderer, element: ElementRef) { + const el = renderer.createElement(element.nativeElement, 'div'); + renderer.setElementAttribute(el, 'title', 'hello'); + + const el1 = renderer.createElement(element.nativeElement, 'div'); + renderer.setElementAttribute(el2, 'title', 'hello'); + + const el2 = renderer.createElement(element.nativeElement, 'div'); + renderer.setElementAttribute(el2, 'title', 'hello'); + } + } + `); + + runTSLint(true); + const content = getFile('index.ts'); + + expect(content.match(/function __ngRendererCreateElementHelper\(/g) !.length).toBe(1); + expect(content.match(/function __ngRendererSetElementAttributeHelper\(/g) !.length).toBe(1); + }); + + it('should insert helpers after the user\'s code', () => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(renderer: Renderer, element: ElementRef) { + const el = renderer.createElement(element.nativeElement, 'div'); + renderer.setElementAttribute(el, 'title', 'hello'); + } + } + + //--- + `); + + runTSLint(true); + const content = getFile('index.ts'); + const [contentBeforeSeparator, contentAfterSeparator] = content.split('//---'); + + expect(contentBeforeSeparator).not.toContain('function __ngRendererCreateElementHelper('); + expect(contentAfterSeparator).toContain('function __ngRendererCreateElementHelper('); + }); + + // Note that this is intended primarily as a sanity test. All of the replacement logic is the + // same between the lint rule and the CLI migration so there's not much value in repeating and + // maintaining the same tests twice. The migration's tests are more exhaustive. + it('should flag calls to Renderer methods', () => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _renderer: Renderer, private _element: ElementRef) { + const span = _renderer.createElement(_element.nativeElement, 'span'); + const greeting = _renderer.createText(_element.nativeElement, 'hello'); + const color = 'red'; + + _renderer.setElementProperty(_element.nativeElement, 'disabled', true); + _renderer.listenGlobal('window', 'resize', () => console.log('resized')); + _renderer.setElementAttribute(_element.nativeElement, 'title', 'hello'); + _renderer.createViewRoot(_element.nativeElement); + _renderer.animate(_element.nativeElement); + _renderer.detachView([]); + _renderer.destroyView(_element.nativeElement, []); + _renderer.invokeElementMethod(_element.nativeElement, 'focus', []); + _renderer.setElementStyle(_element.nativeElement, 'color', color); + _renderer.setText(_element.nativeElement.querySelector('span'), 'Hello'); + } + + getRootElement() { + return this._renderer.selectRootElement(this._element.nativeElement, {}); + } + + toggleClass(className: string, shouldAdd: boolean) { + this._renderer.setElementClass(this._element.nativeElement, className, shouldAdd); + } + + setInfo() { + this._renderer.setBindingDebugInfo(this._element.nativeElement, 'prop', 'value'); + } + + createAndAppendAnchor() { + return this._renderer.createTemplateAnchor(this._element.nativeElement); + } + + attachViewAfter(rootNodes) { + this._renderer.attachViewAfter(this._element.nativeElement, rootNodes); + } + + projectNodes(nodesToProject: Node[]) { + this._renderer.projectNodes(this._element.nativeElement, nodesToProject); + } + } + `); + + const linter = runTSLint(false); + const failures = linter.getResult().failures.map(failure => failure.getFailure()); + + // One failure for the import, one for the constructor param, one at the end that is used as + // an anchor for inserting helper functions and the rest are for method calls. + expect(failures.length).toBe(21); + expect(failures[0]).toMatch(/Imports of deprecated Renderer are not allowed/); + expect(failures[1]).toMatch(/References to deprecated Renderer are not allowed/); + expect(failures[failures.length - 1]).toMatch(/File should contain Renderer helper functions/); + expect(failures.slice(2, -1).every(message => { + return /Calls to Renderer methods are not allowed/.test(message); + })).toBe(true); + }); + + // Note that this is intended primarily as a sanity test. All of the replacement logic is the + // same between the lint rule and the CLI migration so there's not much value in repeating and + // maintaining the same tests twice. The migration's tests are more exhaustive. + it('should fix calls to Renderer methods', () => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _renderer: Renderer, private _element: ElementRef) { + const span = _renderer.createElement(_element.nativeElement, 'span'); + const greeting = _renderer.createText(_element.nativeElement, 'hello'); + const color = 'red'; + + _renderer.setElementProperty(_element.nativeElement, 'disabled', true); + _renderer.listenGlobal('window', 'resize', () => console.log('resized')); + _renderer.setElementAttribute(_element.nativeElement, 'title', 'hello'); + _renderer.animate(_element.nativeElement); + _renderer.detachView([]); + _renderer.destroyView(_element.nativeElement, []); + _renderer.invokeElementMethod(_element.nativeElement, 'focus', []); + _renderer.setElementStyle(_element.nativeElement, 'color', color); + _renderer.setText(_element.nativeElement.querySelector('span'), 'Hello'); + } + + createRoot() { + return this._renderer.createViewRoot(this._element.nativeElement); + } + + getRootElement() { + return this._renderer.selectRootElement(this._element.nativeElement, {}); + } + + toggleClass(className: string, shouldAdd: boolean) { + this._renderer.setElementClass(this._element.nativeElement, className, shouldAdd); + } + + setInfo() { + this._renderer.setBindingDebugInfo(this._element.nativeElement, 'prop', 'value'); + } + + createAndAppendAnchor() { + return this._renderer.createTemplateAnchor(this._element.nativeElement); + } + + attachViewAfter(rootNodes: Node[]) { + this._renderer.attachViewAfter(this._element.nativeElement, rootNodes); + } + + projectNodes(nodesToProject: Node[]) { + this._renderer.projectNodes(this._element.nativeElement, nodesToProject); + } + } + `); + + runTSLint(true); + const content = getFile('index.ts'); + + expect(content).toContain( + `const span = __ngRendererCreateElementHelper(_renderer, _element.nativeElement, 'span');`); + expect(content).toContain( + `const greeting = __ngRendererCreateTextHelper(_renderer, _element.nativeElement, 'hello');`); + expect(content).toContain(`_renderer.setProperty(_element.nativeElement, 'disabled', true);`); + expect(content).toContain( + `_renderer.listen('window', 'resize', () => console.log('resized'));`); + expect(content).toContain( + `__ngRendererSetElementAttributeHelper(_renderer, _element.nativeElement, 'title', 'hello');`); + expect(content).toContain('__ngRendererAnimateHelper();'); + expect(content).toContain('__ngRendererDetachViewHelper(_renderer, []);'); + expect(content).toContain('__ngRendererDestroyViewHelper(_renderer, []);'); + expect(content).toContain(`_element.nativeElement.focus()`); + expect(content).toContain( + `color == null ? _renderer.removeStyle(_element.nativeElement, 'color') : ` + + `_renderer.setStyle(_element.nativeElement, 'color', color);`); + expect(content).toContain( + `_renderer.setValue(_element.nativeElement.querySelector('span'), 'Hello')`); + expect(content).toContain( + `return this._renderer.selectRootElement(this._element.nativeElement);`); + expect(content).toContain( + `shouldAdd ? this._renderer.addClass(this._element.nativeElement, className) : ` + + `this._renderer.removeClass(this._element.nativeElement, className);`); + expect(content).toContain( + `return __ngRendererCreateTemplateAnchorHelper(this._renderer, this._element.nativeElement);`); + expect(content).toContain( + `__ngRendererAttachViewAfterHelper(this._renderer, this._element.nativeElement, rootNodes);`); + expect(content).toContain( + `__ngRendererProjectNodesHelper(this._renderer, this._element.nativeElement, nodesToProject);`); + + // Expect the `createRoot` only to return `this._element.nativeElement`. + expect(content).toMatch(/createRoot\(\) \{\s+return this\._element\.nativeElement;\s+\}/); + + // Expect the `setInfo` method to only contain whitespace. + expect(content).toMatch(/setInfo\(\) \{\s+\}/); + }); + +}); diff --git a/packages/core/schematics/test/renderer_to_renderer2_migration_spec.ts b/packages/core/schematics/test/renderer_to_renderer2_migration_spec.ts new file mode 100644 index 0000000000..8fddee0a40 --- /dev/null +++ b/packages/core/schematics/test/renderer_to_renderer2_migration_spec.ts @@ -0,0 +1,1230 @@ +/** + * @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('Renderer to Renderer2 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'], + strictNullChecks: true, + } + })); + writeFile('/angular.json', JSON.stringify({ + projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}} + })); + // We need to declare the Angular symbols we're testing for, otherwise type checking won't work. + writeFile('/node_modules/@angular/core/index.d.ts', ` + export declare abstract class Renderer {} + export declare function forwardRef(fn: () => any): any {} + `); + + 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('import renaming', () => { + it('should change Renderer imports to Renderer2', async() => { + writeFile('/index.ts', ` + import { Renderer, Component } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(renderer: Renderer) {} + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain(`import { Component, Renderer2 } from '@angular/core';`); + }); + + it('should change aliased Renderer imports to Renderer2', async() => { + writeFile('/index.ts', ` + import { Renderer as RenamedRenderer, Component } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(renderer: RenamedRenderer) {} + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain(`import { Component, Renderer2 as RenamedRenderer } from '@angular/core';`); + }); + + it('should not change Renderer imports if they are not from @angular/core', async() => { + writeFile('/index.ts', ` + import { Component } from '@angular/core'; + import { Renderer } from './my-renderer'; + + @Component({template: ''}) + export class MyComp { + constructor(renderer: Renderer) {} + } + `); + + await runMigration(); + const content = tree.readContent('/index.ts'); + + expect(content).toContain(`import { Component } from '@angular/core';`); + expect(content).toContain(`import { Renderer } from './my-renderer';`); + }); + }); + + describe('type renaming', () => { + it('should change type of constructor parameter from Renderer to Renderer2', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(element: ElementRef, renderer: Renderer) {} + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain('constructor(element: ElementRef, renderer: Renderer2)'); + }); + + it('should change type of method parameter from Renderer to Renderer2', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + disable(renderer: Renderer, element: HTMLElement, isDisabled: boolean) { + renderer.setElementProperty(element, 'disabled', isDisabled); + } + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain('disable(renderer: Renderer2, element: HTMLElement, isDisabled: boolean)'); + }); + + it('should change type of property declarations', async() => { + writeFile('/index.ts', ` + import { Renderer, Component } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + public renderer: Renderer; + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')).toContain('public renderer: Renderer2;'); + }); + + it('should change type of properties initialized via the constructor', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(element: ElementRef, private _renderer: Renderer) {} + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain('constructor(element: ElementRef, private _renderer: Renderer2)'); + }); + + it('should change the type of something that was cast to Renderer', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + setColor(maybeRenderer: any, element: ElementRef) { + const renderer = maybeRenderer as Renderer; + renderer.setElementStyle(element.nativeElement, 'color', 'red'); + } + } + `); + + await runMigration(); + + const content = tree.readContent('/index.ts'); + + expect(content).toContain(`const renderer = maybeRenderer as Renderer2;`); + expect(content).toContain(`renderer.setStyle(element.nativeElement, 'color', 'red');`); + }); + + it('should not rename types called Renderer that do not come from Angular', async() => { + // Write a dummy renderer file so type checking picks it up. + writeFile('/my-renderer.ts', `export abstract class Renderer {}`); + + writeFile('/index.ts', ` + import { Renderer as ActualAngularRenderer, Component, ElementRef } from '@angular/core'; + import { Renderer } from './my-renderer'; + + @Component({template: ''}) + export class MyComp { + constructor(element: ElementRef, renderer: Renderer) {} + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain('constructor(element: ElementRef, renderer: Renderer)'); + }); + + it('should rename inside single-line forwardRef', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef, forwardRef, Inject } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(@Inject(forwardRef(() => Renderer)) private _renderer: Renderer) {} + } + `); + + await runMigration(); + + const content = tree.readContent('/index.ts'); + + expect(content).toContain( + `constructor(@Inject(forwardRef(() => Renderer2)) private _renderer: Renderer2)`); + }); + + it('should rename inside multi-line forwardRef', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef, forwardRef, Inject } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(@Inject(forwardRef(() => { return Renderer; })) private _renderer: Renderer) {} + } + `); + + await runMigration(); + + const content = tree.readContent('/index.ts'); + + expect(content).toContain( + `constructor(@Inject(forwardRef(() => { return Renderer2; })) private _renderer: Renderer2) {}`); + }); + + }); + + describe('helper insertion', () => { + it('should only declare each helper once per file', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) { + _renderer.createElement(_element.nativeElement, 'span'); + _renderer.createElement(_element.nativeElement, 'button'); + _renderer.createElement(_element.nativeElement, 'div'); + } + } + `); + + await runMigration(); + + const content = tree.readContent('/index.ts'); + + expect(content.match(/function __ngRendererCreateElementHelper\(/g) !.length) + .toBe(1, 'Expected exactly one helper for createElement.'); + }); + + it('should insert helpers after the user\'s code', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) { + _renderer.createElement(_element.nativeElement, 'span'); + } + } + + //--- + `); + + await runMigration(); + + const content = tree.readContent('/index.ts'); + const [contentBeforeSeparator, contentAfterSeparator] = content.split('//---'); + + expect(contentBeforeSeparator).not.toContain('function __ngRendererCreateElementHelper('); + expect(contentAfterSeparator).toContain('function __ngRendererCreateElementHelper('); + }); + + it('should be able to handle multiple helpers per file', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) { + _renderer.createText(_element.nativeElement, 'hello'); + _renderer.createText(_element.nativeElement, 'there'); + _renderer.createText(_element.nativeElement, '!'); + } + + createElements(parent: HTMLElement) { + this._renderer.createElement(parent, 'span'); + this._renderer.createElement(parent, 'button'); + } + + createAnchor() { + this._renderer.createTemplateAnchor(this._element.nativeElement); + } + } + `); + + await runMigration(); + + const content = tree.readContent('/index.ts'); + + expect(content.match(/function __ngRendererCreateTextHelper\(/g) !.length) + .toBe(1, 'Expected exactly one helper for createElement.'); + expect(content.match(/function __ngRendererCreateElementHelper\(/g) !.length) + .toBe(1, 'Expected exactly one helper for createText.'); + expect(content.match(/function __ngRendererCreateTemplateAnchorHelper\(/g) !.length) + .toBe(1, 'Expected exactly one helper for createTemplateAnchor.'); + }); + + it('should create the __ngRendererSplitNamespaceHelper', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(element: ElementRef, renderer: Renderer) { + renderer.createElement(element.nativeElement, 'span'); + } + } + `); + + await runMigration(); + + expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(` + function __ngRendererSplitNamespaceHelper(name: AnyDuringRendererMigration) { + if (name[0] === ":") { + const match = name.match(/^:([^:]+):(.+)$/); + return [match[1], match[2]]; + } + return ["", name]; + } + `)); + }); + + it('should declare our custom any type', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(element: ElementRef, renderer: Renderer) { + renderer.createElement(element.nativeElement, 'span'); + } + } + `); + + await runMigration(); + + expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(` + type AnyDuringRendererMigration = any; + `)); + }); + + }); + + describe('setElementProperty migration', () => { + it('should migrate setElementProperty calls', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) {} + + disable() { + this._renderer.setElementProperty(this._element.nativeElement, 'disabled', true); + } + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain(`this._renderer.setProperty(this._element.nativeElement, 'disabled', true);`); + }); + }); + + describe('setText migration', () => { + it('should migrate setText calls', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) {} + + sayHello() { + this._renderer.setText(this._element.nativeElement.querySelector('span'), 'Hello'); + } + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain( + `this._renderer.setValue(this._element.nativeElement.querySelector('span'), 'Hello');`); + }); + }); + + describe('listenGlobal migration', () => { + it('should migrate listenGlobal calls', async() => { + writeFile('/index.ts', ` + import { Renderer, Component } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _renderer: Renderer) {} + + listenToResize() { + this._renderer.listenGlobal('window', 'resize', () => console.log('resized')); + } + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain(`this._renderer.listen('window', 'resize', () => console.log('resized'));`); + }); + }); + + describe('selectRootElement migration', () => { + it('should migrate selectRootElement calls', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) {} + + getRootElement() { + return this._renderer.selectRootElement(this._element.nativeElement, {}); + } + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain(`return this._renderer.selectRootElement(this._element.nativeElement);`); + }); + }); + + describe('setElementClass migration', () => { + it('should migrate calls with inline isAdd value', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) {} + + addClass(className: string) { + this._renderer.setElementClass(this._element.nativeElement, className, true); + } + + removeClass(className: string) { + this._renderer.setElementClass(this._element.nativeElement, className, false); + } + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain(`this._renderer.addClass(this._element.nativeElement, className);`); + expect(tree.readContent('/index.ts')) + .toContain(`this._renderer.removeClass(this._element.nativeElement, className);`); + }); + + it('should migrate calls with variable isAdd value', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) {} + + toggleClass(className: string, shouldAdd: any) { + this._renderer.setElementClass(this._element.nativeElement, className, shouldAdd); + } + } + `); + + await runMigration(); + + expect(tree.readContent('/index.ts')) + .toContain( + `shouldAdd ? this._renderer.addClass(this._element.nativeElement, className) : ` + + `this._renderer.removeClass(this._element.nativeElement, className);`); + }); + }); + + describe('setElementStyle migration', () => { + it('should migrate calls with two arguments to a removeStyle call', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) {} + + removeColor() { + this._renderer.setElementStyle(this._element.nativeElement, 'color'); + } + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain(`this._renderer.removeStyle(this._element.nativeElement, 'color');`); + }); + + it('should migrate calls with static third arguments to a setStyle call', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) {} + + setStyles(height: number) { + this._renderer.setElementStyle(this._element.nativeElement, 'color', 'red'); + this._renderer.setElementStyle(this._element.nativeElement, 'background-color', \`blue\`); + this._renderer.setElementStyle(this._element.nativeElement, 'width', 3); + this._renderer.setElementStyle(this._element.nativeElement, 'height', \`\${height\}px\`); + } + } + `); + + await runMigration(); + const content = tree.readContent('/index.ts'); + expect(content).toContain( + `this._renderer.setStyle(this._element.nativeElement, 'color', 'red');`); + expect(content).toContain( + `this._renderer.setStyle(this._element.nativeElement, 'background-color', \`blue\`);`); + expect(content).toContain( + `this._renderer.setStyle(this._element.nativeElement, 'width', 3);`); + expect(content).toContain( + `this._renderer.setStyle(this._element.nativeElement, 'height', \`\${height\}px\`);`); + }); + + it('should migrate calls with null or undefined value for last argument to a removeStyle call', + async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) {} + + removeColors() { + this._renderer.setElementStyle(this._element.nativeElement, 'color', null); + this._renderer.setElementStyle(this._element.nativeElement, 'background-color', undefined); + } + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain(`this._renderer.removeStyle(this._element.nativeElement, 'color');`); + expect(tree.readContent('/index.ts')) + .toContain( + `this._renderer.removeStyle(this._element.nativeElement, 'background-color');`); + }); + + it('should migrate calls with a variable third argument', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) {} + + setColor(value: string | null) { + this._renderer.setElementStyle(this._element.nativeElement, 'color', value); + } + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain( + `value == null ? this._renderer.removeStyle(this._element.nativeElement, 'color') : ` + + `this._renderer.setStyle(this._element.nativeElement, 'color', value);`); + }); + + it('should migrate calls with a variable third argument whose value can be inferred', + async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) {} + + setColor(color: string, backgroundColor: null, width: number) { + this._renderer.setElementStyle(this._element.nativeElement, 'color', color); + this._renderer.setElementStyle(this._element.nativeElement, 'background-color', backgroundColor); + this._renderer.setElementStyle(this._element.nativeElement, 'width', width + 'px'); + } + } + `); + + await runMigration(); + const content = tree.readContent('/index.ts'); + expect(content).toContain( + `this._renderer.setStyle(this._element.nativeElement, 'color', color);`); + expect(content).toContain( + `this._renderer.removeStyle(this._element.nativeElement, 'background-color');`); + expect(content).toContain( + `this._renderer.setStyle(this._element.nativeElement, 'width', width + 'px');`); + }); + }); + + describe('setElementAttribute migration', () => { + it('should migrate to calls to the __ngRendererSetElementAttributeHelper', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) { + _renderer.setElementAttribute(_element.nativeElement, 'title', 'hello'); + } + + removeAttribute(name: string) { + this._renderer.setElementAttribute(this._element.nativeElement, name); + } + } + `); + + await runMigration(); + + const content = tree.readContent('/index.ts'); + + expect(content).toContain( + `__ngRendererSetElementAttributeHelper(_renderer, _element.nativeElement, 'title', 'hello');`); + expect(content).toContain( + '__ngRendererSetElementAttributeHelper(this._renderer, this._element.nativeElement, name);'); + }); + + it('should declare the __ngRendererSetElementAttributeHelper', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(element: ElementRef, renderer: Renderer) { + renderer.setElementAttribute(element.nativeElement, 'foo', 'bar'); + } + } + `); + + await runMigration(); + + const content = stripWhitespace(tree.readContent('/index.ts')); + + expect(content).toContain(stripWhitespace(` + function __ngRendererSetElementAttributeHelper(renderer: AnyDuringRendererMigration, element: AnyDuringRendererMigration, namespaceAndName: AnyDuringRendererMigration, value?: AnyDuringRendererMigration) { + const [namespace, name] = __ngRendererSplitNamespaceHelper(namespaceAndName); + if (value != null) { + renderer.setAttribute(element, name, value, namespace); + } else { + renderer.removeAttribute(element, name, namespace); + } + } + `)); + + expect(content).toContain(stripWhitespace('function __ngRendererSplitNamespaceHelper(')); + }); + + }); + + describe('invokeElementMethod migration', () => { + it('should migrate calls to a direct method call if the method name and arguments are static', + async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) {} + + focus() { + this._renderer.invokeElementMethod(this._element.nativeElement, 'focus', []); + this._renderer.invokeElementMethod(this._element.nativeElement, 'focusEvenMore'); + this._renderer.invokeElementMethod(this._element.nativeElement, 'doSomething', [1, true, 'three']); + } + } + `); + + await runMigration(); + const content = tree.readContent('/index.ts'); + expect(content).not.toContain('this._renderer'); + expect(content).toContain(`this._element.nativeElement.focus()`); + expect(content).toContain(`this._element.nativeElement.focusEvenMore()`); + expect(content).toContain(`this._element.nativeElement.doSomething(1, true, 'three')`); + }); + + it('should migrate calls to a property access if the method name or arguments are dynamic', + async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) {} + + callMethod(name: string, args: any[]) { + this._renderer.invokeElementMethod(this._element.nativeElement, name, [1, true]); + } + + callOtherMethod(args: any[]) { + this._renderer.invokeElementMethod(this._element.nativeElement, 'otherMethod', args); + } + } + `); + + await runMigration(); + const content = tree.readContent('/index.ts'); + expect(content).not.toContain('this._renderer'); + expect(content).toContain( + `(this._element.nativeElement as any)[name].apply(this._element.nativeElement, [1, true]);`); + + expect(content).toContain( + `(this._element.nativeElement as any)['otherMethod'].apply(this._element.nativeElement, args);`); + }); + + it('should handle calls without an `args` array', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) {} + + callMethod(name: string) { + this._renderer.invokeElementMethod(this._element.nativeElement, name); + } + } + `); + + await runMigration(); + const content = tree.readContent('/index.ts'); + expect(content).not.toContain('this._renderer'); + expect(content).toContain( + `(this._element.nativeElement as any)[name].apply(this._element.nativeElement);`); + }); + }); + + describe('setBindingDebugInfo migration', () => { + it('should drop calls to setBindingDebugInfo', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) {} + + setInfo() { + this._renderer.setBindingDebugInfo(this._element.nativeElement, 'prop', 'value'); + } + } + `); + + await runMigration(); + + // Expect the `setInfo` method to only contain whitespace. + expect(tree.readContent('/index.ts')).toMatch(/setInfo\(\) \{\s+\}/); + }); + }); + + describe('createViewRoot migration', () => { + it('should replace createViewRoot calls with a reference to the first argument', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) {} + + createRoot() { + return this._renderer.createViewRoot(this._element.nativeElement); + } + } + `); + + await runMigration(); + + // Expect the `createRoot` only to return `this._element.nativeElement`. + expect(tree.readContent('/index.ts')) + .toMatch(/createRoot\(\) \{\s+return this\._element\.nativeElement;\s+\}/); + }); + }); + + describe('createElement migration', () => { + it('should migrate to calls to the __ngRendererCreateElementHelper', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) { + const message = _renderer.createElement(_element.nativeElement, 'span', {}); + message.textContent = 'hello'; + } + + createAndAppendElement(nodeName: string) { + return this._renderer.createElement(this._element.nativeElement, nodeName); + } + } + `); + + await runMigration(); + + const content = tree.readContent('/index.ts'); + + expect(content).toContain( + `const message = __ngRendererCreateElementHelper(_renderer, _element.nativeElement, 'span');`); + expect(content).toContain( + 'return __ngRendererCreateElementHelper(this._renderer, this._element.nativeElement, nodeName);'); + }); + + it('should declare the __ngRendererCreateElementHelper', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(element: ElementRef, renderer: Renderer) { + renderer.createElement(element.nativeElement, 'span'); + } + } + `); + + await runMigration(); + + const content = stripWhitespace(tree.readContent('/index.ts')); + + expect(content).toContain(stripWhitespace(` + function __ngRendererCreateElementHelper(renderer: AnyDuringRendererMigration, parent: AnyDuringRendererMigration, namespaceAndName: AnyDuringRendererMigration) { + const [namespace, name] = __ngRendererSplitNamespaceHelper(namespaceAndName); + const node = renderer.createElement(name, namespace); + if (parent) { + renderer.appendChild(parent, node); + } + return node; + } + `)); + + expect(content).toContain(stripWhitespace('function __ngRendererSplitNamespaceHelper(')); + }); + + }); + + describe('createText migration', () => { + it('should migrate to calls to the __ngRendererCreateTextHelper', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) { + const message = _renderer.createText(_element.nativeElement, 'hello', {}); + message.textContent += ' world'; + } + + createAndAppendText(value: string) { + return this._renderer.createText(this._element.nativeElement, value); + } + } + `); + + await runMigration(); + + const content = tree.readContent('/index.ts'); + + expect(content).toContain( + `const message = __ngRendererCreateTextHelper(_renderer, _element.nativeElement, 'hello');`); + expect(content).toContain( + 'return __ngRendererCreateTextHelper(this._renderer, this._element.nativeElement, value);'); + }); + + it('should declare the __ngRendererCreateTextHelper', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(element: ElementRef, renderer: Renderer) { + renderer.createText(element.nativeElement, 'hi'); + } + } + `); + + await runMigration(); + + expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(` + function __ngRendererCreateTextHelper(renderer: AnyDuringRendererMigration, parent: AnyDuringRendererMigration, value: AnyDuringRendererMigration) { + const node = renderer.createText(value); + if (parent) { + renderer.appendChild(parent, node); + } + return node; + } + `)); + }); + + }); + + describe('createTemplateAnchor migration', () => { + it('should migrate to calls to the __ngRendererCreateTemplateAnchorHelper', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) { + console.log(_renderer.createTemplateAnchor(_element.nativeElement)); + } + + createAndAppendAnchor() { + return this._renderer.createTemplateAnchor(this._element.nativeElement); + } + } + `); + + await runMigration(); + + const content = tree.readContent('/index.ts'); + + expect(content).toContain( + `console.log(__ngRendererCreateTemplateAnchorHelper(_renderer, _element.nativeElement));`); + expect(content).toContain( + 'return __ngRendererCreateTemplateAnchorHelper(this._renderer, this._element.nativeElement);'); + }); + + it('should declare the __ngRendererCreateTemplateAnchorHelper', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(element: ElementRef, renderer: Renderer) { + renderer.createTemplateAnchor(element.nativeElement); + } + } + `); + + await runMigration(); + + expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(` + function __ngRendererCreateTemplateAnchorHelper(renderer: AnyDuringRendererMigration, parent: AnyDuringRendererMigration) { + const node = renderer.createComment(""); + if (parent) { + renderer.appendChild(parent, node); + } + return node; + } + `)); + }); + + }); + + describe('projectNodes migration', () => { + it('should migrate to calls to the __ngRendererProjectNodesHelper', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) { + } + + projectNodes(nodesToProject: Node[]) { + this._renderer.projectNodes(this._element.nativeElement, nodesToProject); + } + } + `); + + await runMigration(); + + expect(tree.readContent('/index.ts')) + .toContain( + '__ngRendererProjectNodesHelper(this._renderer, this._element.nativeElement, nodesToProject);'); + }); + + it('should declare the __ngRendererProjectNodesHelper', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(element: ElementRef, renderer: Renderer) { + renderer.projectNodes(element.nativeElement, []); + } + } + `); + + await runMigration(); + + expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(` + function __ngRendererProjectNodesHelper(renderer: AnyDuringRendererMigration, parent: AnyDuringRendererMigration, nodes: AnyDuringRendererMigration) { + for (let i = 0; i < nodes.length; i++) { + renderer.appendChild(parent, nodes[i]); + } + } + `)); + }); + + }); + + describe('animate migration', () => { + it('should migrate to calls to the __ngRendererAnimateHelper', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) { + } + + animate() { + this._renderer.animate(this._element.nativeElement); + } + } + `); + + await runMigration(); + + expect(tree.readContent('/index.ts')).toContain('__ngRendererAnimateHelper();'); + }); + + it('should declare the __ngRendererAnimateHelper', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(element: ElementRef, renderer: Renderer) { + renderer.animate(element.nativeElement); + } + } + `); + + await runMigration(); + + expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(` + function __ngRendererAnimateHelper() { + throw new Error("Renderer.animate is no longer supported!"); + } + `)); + }); + + }); + + describe('destroyView migration', () => { + it('should migrate to calls to the __ngRendererDestroyViewHelper', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) { + } + + destroyView(allNodes: Node[]) { + this._renderer.destroyView(this._element.nativeElement, allNodes); + } + } + `); + + await runMigration(); + + expect(tree.readContent('/index.ts')) + .toContain('__ngRendererDestroyViewHelper(this._renderer, allNodes);'); + }); + + it('should declare the __ngRendererDestroyViewHelper', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) { + } + + destroyView(allNodes: Node[]) { + this._renderer.destroyView(this._element.nativeElement, allNodes); + } + } + `); + + await runMigration(); + + expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(` + function __ngRendererDestroyViewHelper(renderer: AnyDuringRendererMigration, allNodes: AnyDuringRendererMigration) { + for (let i = 0; i < allNodes.length; i++) { + renderer.destroyNode(allNodes[i]); + } + } + `)); + }); + }); + + describe('detachView migration', () => { + it('should migrate to calls to the __ngRendererDetachViewHelper', async() => { + writeFile('/index.ts', ` + import { Renderer, Component } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _renderer: Renderer) { + } + + detachView(rootNodes: Node[]) { + this._renderer.detachView(rootNodes); + } + } + `); + + await runMigration(); + + expect(tree.readContent('/index.ts')) + .toContain('__ngRendererDetachViewHelper(this._renderer, rootNodes);'); + }); + + it('should declare the __ngRendererDetachViewHelper', async() => { + writeFile('/index.ts', ` + import { Renderer, Component } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _renderer: Renderer) { + } + + detachView(rootNodes: Node[]) { + this._renderer.detachView(rootNodes); + } + } + `); + + await runMigration(); + + expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(` + function __ngRendererDetachViewHelper(renderer: AnyDuringRendererMigration, rootNodes: AnyDuringRendererMigration) { + for (let i = 0; i < rootNodes.length; i++) { + const node = rootNodes[i]; + renderer.removeChild(renderer.parentNode(node), node); + } + } + `)); + }); + }); + + describe('attachViewAfter migration', () => { + it('should migrate to calls to the __ngRendererAttachViewAfterHelper', async() => { + writeFile('/index.ts', ` + import { Renderer, Component, ElementRef } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _element: ElementRef, private _renderer: Renderer) { + } + + attachViewAfter(rootNodes: Node[]) { + this._renderer.attachViewAfter(this._element.nativeElement, rootNodes); + } + } + `); + + await runMigration(); + + expect(tree.readContent('/index.ts')) + .toContain( + '__ngRendererAttachViewAfterHelper(this._renderer, this._element.nativeElement, rootNodes);'); + }); + + it('should declare the __ngRendererAttachViewAfterHelper', async() => { + writeFile('/index.ts', ` + import { Renderer, Component } from '@angular/core'; + + @Component({template: ''}) + export class MyComp { + constructor(private _renderer: Renderer) { + } + + attachViewAfter(node: Node, rootNodes: Node[]) { + this._renderer.attachViewAfter(node, rootNodes); + } + } + `); + + await runMigration(); + + expect(stripWhitespace(tree.readContent('/index.ts'))).toContain(stripWhitespace(` + function __ngRendererAttachViewAfterHelper(renderer: AnyDuringRendererMigration, node: AnyDuringRendererMigration, rootNodes: AnyDuringRendererMigration) { + const parent = renderer.parentNode(node); + const nextSibling = renderer.nextSibling(node); + for (let i = 0; i < rootNodes.length; i++) { + renderer.insertBefore(parent, rootNodes[i], nextSibling); + } + } + `)); + }); + }); + + function writeFile(filePath: string, contents: string) { + host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); + } + + function runMigration() { + return runner.runSchematicAsync('migration-v9-renderer-to-renderer2', {}, tree).toPromise(); + } + + function stripWhitespace(contents: string) { return contents.replace(/\s/g, ''); } +});