Skip to content

Commit

Permalink
fixup! feat(core): add automatic migration from Renderer to Renderer2
Browse files Browse the repository at this point in the history
  • Loading branch information
crisbeto committed Jun 19, 2019
1 parent 2dca1c7 commit 8cbfd01
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 102 deletions.
@@ -1,8 +1,8 @@
## Renderer -> Renderer2 migration

Automatically migrates from `Renderer` to `Renderer2` by changing method calls, renaming imports
and 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.
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
Expand Down
Expand Up @@ -13,6 +13,10 @@ import {HelperFunction, getHelper} from '../helpers';
import {migrateExpression, replaceImport} from '../migration';
import {findRendererImport, 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();
Expand Down Expand Up @@ -59,7 +63,8 @@ export class Rule extends Rules.TypedRule {
ts.EmitHint.Unspecified, replaceImport(node, 'Renderer', 'Renderer2'), sourceFile);

return new RuleFailure(
sourceFile, node.getStart(), node.getEnd(), 'Imports of Renderer are not allowed.',
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));
}

Expand All @@ -70,7 +75,8 @@ export class Rule extends Rules.TypedRule {
const type = node.type !;

return new RuleFailure(
sourceFile, type.getStart(), type.getEnd(), 'References to Renderer are not allowed.',
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'));
}

Expand Down
Expand Up @@ -10,16 +10,17 @@ import * as ts from 'typescript';

/** Names of the helper functions that are supported for this migration. */
export const enum HelperFunction {
createElement = '__rendererCreateElementHelper',
createText = '__rendererCreateTextHelper',
createTemplateAnchor = '__rendererCreateTemplateAnchorHelper',
projectNodes = '__rendererProjectNodesHelper',
animate = '__rendererAnimateHelper',
destroyView = '__rendererDestroyViewHelper',
detachView = '__rendererDetachViewHelper',
attachViewAfter = '__rendererAttachViewAfterHelper',
splitNamespace = '__rendererSplitNamespaceHelper',
setElementAttribute = '__rendererSetElementAttributeHelper'
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. */
Expand All @@ -30,8 +31,10 @@ export function getHelper(
}

/** Creates a function declaration for the specified helper name. */
function getHelperDeclaration(name: HelperFunction): ts.FunctionDeclaration {
function getHelperDeclaration(name: HelperFunction): ts.Node {
switch (name) {
case HelperFunction.any:
return createAnyTypeHelper();
case HelperFunction.createElement:
return getCreateElementHelper();
case HelperFunction.createText:
Expand All @@ -57,12 +60,20 @@ function getHelperDeclaration(name: HelperFunction): ts.FunctionDeclaration {
throw new Error(`Unsupported helper called "${name}".`);
}

/** Creates a helper our 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.
const type = ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
// 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);
Expand Down
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
import {Rule, SchematicContext, SchematicsException, Tree} from '@angular-devkit/schematics';
import {dirname, relative} from 'path';
import * as ts from 'typescript';

Expand All @@ -19,13 +19,19 @@ import {findRendererImport, findRendererReferences} from './util';


/**
* Runs a migration over a TypeScript project that changes `Renderer` usages to `Renderer2`.
* 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) => {
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(
Expand Down
Expand Up @@ -63,45 +63,51 @@ export function migrateExpression(node: ts.CallExpression):
case 'setElementAttribute':
return {
node: switchToHelperCall(node, HelperFunction.setElementAttribute, node.arguments),
requiredHelpers: [HelperFunction.splitNamespace, HelperFunction.setElementAttribute]
requiredHelpers: [
HelperFunction.any, HelperFunction.splitNamespace, HelperFunction.setElementAttribute
]
};
case 'createElement':
return {
node: switchToHelperCall(node, HelperFunction.createElement, node.arguments.slice(0, 2)),
requiredHelpers: [HelperFunction.splitNamespace, HelperFunction.createElement]
requiredHelpers:
[HelperFunction.any, HelperFunction.splitNamespace, HelperFunction.createElement]
};
case 'createText':
return {
node: switchToHelperCall(node, HelperFunction.createText, node.arguments.slice(0, 2)),
requiredHelpers: [HelperFunction.createText]
requiredHelpers: [HelperFunction.any, HelperFunction.createText]
};
case 'createTemplateAnchor':
return {
node: switchToHelperCall(
node, HelperFunction.createTemplateAnchor, node.arguments.slice(0, 1)),
requiredHelpers: [HelperFunction.createTemplateAnchor]
requiredHelpers: [HelperFunction.any, HelperFunction.createTemplateAnchor]
};
case 'projectNodes':
return {
node: switchToHelperCall(node, HelperFunction.projectNodes, node.arguments),
requiredHelpers: [HelperFunction.projectNodes]
requiredHelpers: [HelperFunction.any, HelperFunction.projectNodes]
};
case 'animate':
return {node: migrateAnimateCall(), requiredHelpers: [HelperFunction.animate]};
return {
node: migrateAnimateCall(),
requiredHelpers: [HelperFunction.any, HelperFunction.animate]
};
case 'destroyView':
return {
node: switchToHelperCall(node, HelperFunction.destroyView, [node.arguments[1]]),
requiredHelpers: [HelperFunction.destroyView]
requiredHelpers: [HelperFunction.any, HelperFunction.destroyView]
};
case 'detachView':
return {
node: switchToHelperCall(node, HelperFunction.detachView, [node.arguments[0]]),
requiredHelpers: [HelperFunction.detachView]
requiredHelpers: [HelperFunction.any, HelperFunction.detachView]
};
case 'attachViewAfter':
return {
node: switchToHelperCall(node, HelperFunction.attachViewAfter, node.arguments),
requiredHelpers: [HelperFunction.attachViewAfter]
requiredHelpers: [HelperFunction.any, HelperFunction.attachViewAfter]
};
}
}
Expand Down
65 changes: 44 additions & 21 deletions packages/core/schematics/test/google3/renderer_to_renderer2_spec.ts
Expand Up @@ -74,9 +74,9 @@ describe('Google3 Renderer to Renderer2 TSLint rule', () => {
const failures = linter.getResult().failures.map(failure => failure.getFailure());

expect(failures.length).toBe(3);
expect(failures[0]).toMatch(/Imports of Renderer are not allowed/);
expect(failures[1]).toMatch(/References to Renderer are not allowed/);
expect(failures[2]).toMatch(/References to Renderer are not allowed/);
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', () => {
Expand Down Expand Up @@ -118,8 +118,8 @@ describe('Google3 Renderer to Renderer2 TSLint rule', () => {
const failures = linter.getResult().failures.map(failure => failure.getFailure());

expect(failures.length).toBe(3);
expect(failures[0]).toMatch(/Imports of Renderer are not allowed/);
expect(failures[1]).toMatch(/References to Renderer are not allowed/);
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/);
});

Expand Down Expand Up @@ -160,9 +160,9 @@ describe('Google3 Renderer to Renderer2 TSLint rule', () => {
runTSLint(true);
const content = getFile('index.ts');

expect(content).toContain(`function __rendererCreateElementHelper(`);
expect(content).toContain(`function __rendererSetElementAttributeHelper(`);
expect(content).toContain(`function __rendererProjectNodesHelper(`);
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', () => {
Expand All @@ -187,8 +187,31 @@ describe('Google3 Renderer to Renderer2 TSLint rule', () => {
runTSLint(true);
const content = getFile('index.ts');

expect(content.match(/function __rendererCreateElementHelper\(/g) !.length).toBe(1);
expect(content.match(/function __rendererSetElementAttributeHelper\(/g) !.length).toBe(1);
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
Expand Down Expand Up @@ -249,8 +272,8 @@ describe('Google3 Renderer to Renderer2 TSLint rule', () => {
// 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 Renderer are not allowed/);
expect(failures[1]).toMatch(/References to Renderer are not allowed/);
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);
Expand Down Expand Up @@ -316,17 +339,17 @@ describe('Google3 Renderer to Renderer2 TSLint rule', () => {
const content = getFile('index.ts');

expect(content).toContain(
`const span = __rendererCreateElementHelper(_renderer, _element.nativeElement, 'span');`);
`const span = ngRendererCreateElementHelper(_renderer, _element.nativeElement, 'span');`);
expect(content).toContain(
`const greeting = __rendererCreateTextHelper(_renderer, _element.nativeElement, 'hello');`);
`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(
`__rendererSetElementAttributeHelper(_renderer, _element.nativeElement, 'title', 'hello');`);
expect(content).toContain('__rendererAnimateHelper();');
expect(content).toContain('__rendererDetachViewHelper(_renderer, []);');
expect(content).toContain('__rendererDestroyViewHelper(_renderer, []);');
`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') : ` +
Expand All @@ -339,11 +362,11 @@ describe('Google3 Renderer to Renderer2 TSLint rule', () => {
`shouldAdd ? this._renderer.addClass(this._element.nativeElement, className) : ` +
`this._renderer.removeClass(this._element.nativeElement, className);`);
expect(content).toContain(
`return __rendererCreateTemplateAnchorHelper(this._renderer, this._element.nativeElement);`);
`return ngRendererCreateTemplateAnchorHelper(this._renderer, this._element.nativeElement);`);
expect(content).toContain(
`__rendererAttachViewAfterHelper(this._renderer, this._element.nativeElement, rootNodes);`);
`ngRendererAttachViewAfterHelper(this._renderer, this._element.nativeElement, rootNodes);`);
expect(content).toContain(
`__rendererProjectNodesHelper(this._renderer, this._element.nativeElement, nodesToProject);`);
`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+\}/);
Expand Down

0 comments on commit 8cbfd01

Please sign in to comment.