404 lines
18 KiB
TypeScript
404 lines
18 KiB
TypeScript
|
/**
|
||
|
* @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));
|
||
|
}
|