refactor(compiler-cli): linker - add Babel plugin, FileLinker and initial PartialLinkers (#39116)
This commit adds the basic building blocks for linking partial declarations. In particular it provides a generic `FileLinker` class that delegates to a set of (not yet implemented) `PartialLinker` classes. The Babel plugin makes use of this `FileLinker` providing concrete classes for `AstHost` and `AstFactory` that work with Babel AST. It can be created with the following code: ```ts const plugin = createEs2015LinkerPlugin({ /* options */ }); ``` PR Close #39116
This commit is contained in:

committed by
Andrew Kushnir

parent
b304bd0535
commit
7e742aea7c
19
packages/compiler-cli/linker/babel/BUILD.bazel
Normal file
19
packages/compiler-cli/linker/babel/BUILD.bazel
Normal file
@ -0,0 +1,19 @@
|
||||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
ts_library(
|
||||
name = "babel",
|
||||
srcs = ["index.ts"] + glob([
|
||||
"src/**/*.ts",
|
||||
]),
|
||||
deps = [
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli/linker",
|
||||
"//packages/compiler-cli/src/ngtsc/translator",
|
||||
"@npm//@babel/core",
|
||||
"@npm//@babel/types",
|
||||
"@npm//@types/babel__core",
|
||||
"@npm//@types/babel__traverse",
|
||||
],
|
||||
)
|
12
packages/compiler-cli/linker/babel/README.md
Normal file
12
packages/compiler-cli/linker/babel/README.md
Normal file
@ -0,0 +1,12 @@
|
||||
# Angular linker - Babel plugin
|
||||
|
||||
This package contains a Babel plugin that can be used to find and link partially compiled declarations in library source code.
|
||||
See the [linker package README](../README.md) for more information.
|
||||
|
||||
## Unit Testing
|
||||
|
||||
The unit tests are built and run using Bazel:
|
||||
|
||||
```bash
|
||||
yarn bazel test //packages/compiler-cli/linker/babel/test
|
||||
```
|
8
packages/compiler-cli/linker/babel/index.ts
Normal file
8
packages/compiler-cli/linker/babel/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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
|
||||
*/
|
||||
export {createEs2015LinkerPlugin} from './src/es2015_linker_plugin';
|
164
packages/compiler-cli/linker/babel/src/ast/babel_ast_factory.ts
Normal file
164
packages/compiler-cli/linker/babel/src/ast/babel_ast_factory.ts
Normal file
@ -0,0 +1,164 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 t from '@babel/types';
|
||||
|
||||
import {assert} from '../../../../linker';
|
||||
import {AstFactory, BinaryOperator, LeadingComment, ObjectLiteralProperty, SourceMapRange, TemplateLiteral, VariableDeclarationType} from '../../../../src/ngtsc/translator';
|
||||
|
||||
/**
|
||||
* A Babel flavored implementation of the AstFactory.
|
||||
*/
|
||||
export class BabelAstFactory implements AstFactory<t.Statement, t.Expression> {
|
||||
attachComments(statement: t.Statement, leadingComments: LeadingComment[]): void {
|
||||
// We must process the comments in reverse because `t.addComment()` will add new ones in front.
|
||||
for (let i = leadingComments.length - 1; i >= 0; i--) {
|
||||
const comment = leadingComments[i];
|
||||
t.addComment(statement, 'leading', comment.toString(), !comment.multiline);
|
||||
}
|
||||
}
|
||||
|
||||
createArrayLiteral = t.arrayExpression;
|
||||
|
||||
createAssignment(target: t.Expression, value: t.Expression): t.Expression {
|
||||
assert(target, isLExpression, 'must be a left hand side expression');
|
||||
return t.assignmentExpression('=', target, value);
|
||||
}
|
||||
|
||||
createBinaryExpression(
|
||||
leftOperand: t.Expression, operator: BinaryOperator,
|
||||
rightOperand: t.Expression): t.Expression {
|
||||
switch (operator) {
|
||||
case '&&':
|
||||
case '||':
|
||||
return t.logicalExpression(operator, leftOperand, rightOperand);
|
||||
default:
|
||||
return t.binaryExpression(operator, leftOperand, rightOperand);
|
||||
}
|
||||
}
|
||||
|
||||
createBlock = t.blockStatement;
|
||||
|
||||
createCallExpression(callee: t.Expression, args: t.Expression[], pure: boolean): t.Expression {
|
||||
const call = t.callExpression(callee, args);
|
||||
if (pure) {
|
||||
t.addComment(call, 'leading', ' @__PURE__ ', /* line */ false);
|
||||
}
|
||||
return call;
|
||||
}
|
||||
|
||||
createConditional = t.conditionalExpression;
|
||||
|
||||
createElementAccess(expression: t.Expression, element: t.Expression): t.Expression {
|
||||
return t.memberExpression(expression, element, /* computed */ true);
|
||||
}
|
||||
|
||||
createExpressionStatement = t.expressionStatement;
|
||||
|
||||
createFunctionDeclaration(functionName: string, parameters: string[], body: t.Statement):
|
||||
t.Statement {
|
||||
assert(body, t.isBlockStatement, 'a block');
|
||||
return t.functionDeclaration(
|
||||
t.identifier(functionName), parameters.map(param => t.identifier(param)), body);
|
||||
}
|
||||
|
||||
createFunctionExpression(functionName: string|null, parameters: string[], body: t.Statement):
|
||||
t.Expression {
|
||||
assert(body, t.isBlockStatement, 'a block');
|
||||
const name = functionName !== null ? t.identifier(functionName) : null;
|
||||
return t.functionExpression(name, parameters.map(param => t.identifier(param)), body);
|
||||
}
|
||||
|
||||
createIdentifier = t.identifier;
|
||||
|
||||
createIfStatement = t.ifStatement;
|
||||
|
||||
createLiteral(value: string|number|boolean|null|undefined): t.Expression {
|
||||
if (typeof value === 'string') {
|
||||
return t.stringLiteral(value);
|
||||
} else if (typeof value === 'number') {
|
||||
return t.numericLiteral(value);
|
||||
} else if (typeof value === 'boolean') {
|
||||
return t.booleanLiteral(value);
|
||||
} else if (value === undefined) {
|
||||
return t.identifier('undefined');
|
||||
} else if (value === null) {
|
||||
return t.nullLiteral();
|
||||
} else {
|
||||
throw new Error(`Invalid literal: ${value} (${typeof value})`);
|
||||
}
|
||||
}
|
||||
|
||||
createNewExpression = t.newExpression;
|
||||
|
||||
createObjectLiteral(properties: ObjectLiteralProperty<t.Expression>[]): t.Expression {
|
||||
return t.objectExpression(properties.map(prop => {
|
||||
const key =
|
||||
prop.quoted ? t.stringLiteral(prop.propertyName) : t.identifier(prop.propertyName);
|
||||
return t.objectProperty(key, prop.value);
|
||||
}));
|
||||
}
|
||||
|
||||
createParenthesizedExpression = t.parenthesizedExpression;
|
||||
|
||||
createPropertyAccess(expression: t.Expression, propertyName: string): t.Expression {
|
||||
return t.memberExpression(expression, t.identifier(propertyName), /* computed */ false);
|
||||
}
|
||||
|
||||
createReturnStatement = t.returnStatement;
|
||||
|
||||
createTaggedTemplate(tag: t.Expression, template: TemplateLiteral<t.Expression>): t.Expression {
|
||||
const elements = template.elements.map(
|
||||
(element, i) => this.setSourceMapRange(
|
||||
t.templateElement(element, i === template.elements.length - 1), element.range));
|
||||
return t.taggedTemplateExpression(tag, t.templateLiteral(elements, template.expressions));
|
||||
}
|
||||
|
||||
createThrowStatement = t.throwStatement;
|
||||
|
||||
createTypeOfExpression(expression: t.Expression): t.Expression {
|
||||
return t.unaryExpression('typeof', expression);
|
||||
}
|
||||
|
||||
createUnaryExpression = t.unaryExpression;
|
||||
|
||||
createVariableDeclaration(
|
||||
variableName: string, initializer: t.Expression|null,
|
||||
type: VariableDeclarationType): t.Statement {
|
||||
return t.variableDeclaration(
|
||||
type, [t.variableDeclarator(t.identifier(variableName), initializer)]);
|
||||
}
|
||||
|
||||
setSourceMapRange<T extends t.Statement|t.Expression|t.TemplateElement>(
|
||||
node: T, sourceMapRange: SourceMapRange|null): T {
|
||||
if (sourceMapRange === null) {
|
||||
return node;
|
||||
}
|
||||
// Note that the linker only works on a single file at a time, so there is no need to track the
|
||||
// filename. Babel will just use the current filename in the source-map.
|
||||
node.loc = {
|
||||
start: {
|
||||
line: sourceMapRange.start.line + 1, // lines are 1-based in Babel.
|
||||
column: sourceMapRange.start.column,
|
||||
},
|
||||
end: {
|
||||
line: sourceMapRange.end.line + 1, // lines are 1-based in Babel.
|
||||
column: sourceMapRange.end.column,
|
||||
},
|
||||
};
|
||||
node.start = sourceMapRange.start.offset;
|
||||
node.end = sourceMapRange.end.offset;
|
||||
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
function isLExpression(expr: t.Expression): expr is Extract<t.LVal, t.Expression> {
|
||||
// Some LVal types are not expressions, which prevents us from using `t.isLVal()`
|
||||
// directly with `assert()`.
|
||||
return t.isLVal(expr);
|
||||
}
|
140
packages/compiler-cli/linker/babel/src/ast/babel_ast_host.ts
Normal file
140
packages/compiler-cli/linker/babel/src/ast/babel_ast_host.ts
Normal file
@ -0,0 +1,140 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 t from '@babel/types';
|
||||
|
||||
import {assert, AstHost, FatalLinkerError, Range} from '../../../../linker';
|
||||
|
||||
/**
|
||||
* This implementation of `AstHost` is able to get information from Babel AST nodes.
|
||||
*/
|
||||
export class BabelAstHost implements AstHost<t.Expression> {
|
||||
getSymbolName(node: t.Expression): string|null {
|
||||
if (t.isIdentifier(node)) {
|
||||
return node.name;
|
||||
} else if (t.isMemberExpression(node) && t.isIdentifier(node.property)) {
|
||||
return node.property.name;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
isStringLiteral = t.isStringLiteral;
|
||||
|
||||
parseStringLiteral(str: t.Expression): string {
|
||||
assert(str, t.isStringLiteral, 'a string literal');
|
||||
return str.value;
|
||||
}
|
||||
|
||||
isNumericLiteral = t.isNumericLiteral;
|
||||
|
||||
parseNumericLiteral(num: t.Expression): number {
|
||||
assert(num, t.isNumericLiteral, 'a numeric literal');
|
||||
return num.value;
|
||||
}
|
||||
|
||||
isBooleanLiteral = t.isBooleanLiteral;
|
||||
|
||||
parseBooleanLiteral(bool: t.Expression): boolean {
|
||||
assert(bool, t.isBooleanLiteral, 'a boolean literal');
|
||||
return bool.value;
|
||||
}
|
||||
|
||||
isArrayLiteral = t.isArrayExpression;
|
||||
|
||||
parseArrayLiteral(array: t.Expression): t.Expression[] {
|
||||
assert(array, t.isArrayExpression, 'an array literal');
|
||||
return array.elements.map(element => {
|
||||
assert(element, isNotEmptyElement, 'element in array not to be empty');
|
||||
assert(element, isNotSpreadElement, 'element in array not to use spread syntax');
|
||||
return element;
|
||||
});
|
||||
}
|
||||
|
||||
isObjectLiteral = t.isObjectExpression;
|
||||
|
||||
parseObjectLiteral(obj: t.Expression): Map<string, t.Expression> {
|
||||
assert(obj, t.isObjectExpression, 'an object literal');
|
||||
|
||||
const result = new Map<string, t.Expression>();
|
||||
for (const property of obj.properties) {
|
||||
assert(property, t.isObjectProperty, 'a property assignment');
|
||||
assert(property.value, t.isExpression, 'an expression');
|
||||
assert(property.key, isPropertyName, 'a property name');
|
||||
const key = t.isIdentifier(property.key) ? property.key.name : property.key.value;
|
||||
result.set(key, property.value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
isFunctionExpression(node: t.Expression): node is Extract<t.Function, t.Expression> {
|
||||
return t.isFunction(node);
|
||||
}
|
||||
|
||||
parseReturnValue(fn: t.Expression): t.Expression {
|
||||
assert(fn, this.isFunctionExpression, 'a function');
|
||||
if (!t.isBlockStatement(fn.body)) {
|
||||
// it is a simple array function expression: `(...) => expr`
|
||||
return fn.body;
|
||||
}
|
||||
|
||||
// it is a function (arrow or normal) with a body. E.g.:
|
||||
// * `(...) => { stmt; ... }`
|
||||
// * `function(...) { stmt; ... }`
|
||||
|
||||
if (fn.body.body.length !== 1) {
|
||||
throw new FatalLinkerError(
|
||||
fn.body, 'Unsupported syntax, expected a function body with a single return statement.');
|
||||
}
|
||||
const stmt = fn.body.body[0];
|
||||
assert(stmt, t.isReturnStatement, 'a function body with a single return statement');
|
||||
if (stmt.argument === null) {
|
||||
throw new FatalLinkerError(stmt, 'Unsupported syntax, expected function to return a value.');
|
||||
}
|
||||
|
||||
return stmt.argument;
|
||||
}
|
||||
|
||||
getRange(node: t.Expression): Range {
|
||||
if (node.loc == null || node.start === null || node.end === null) {
|
||||
throw new FatalLinkerError(
|
||||
node, 'Unable to read range for node - it is missing location information.');
|
||||
}
|
||||
return {
|
||||
startLine: node.loc.start.line - 1, // Babel lines are 1-based
|
||||
startCol: node.loc.start.column,
|
||||
startPos: node.start,
|
||||
endPos: node.end,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the expression does not represent an empty element in an array literal.
|
||||
* For example in `[,foo]` the first element is "empty".
|
||||
*/
|
||||
function isNotEmptyElement(e: t.Expression|t.SpreadElement|null): e is t.Expression|
|
||||
t.SpreadElement {
|
||||
return e !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the expression is not a spread element of an array literal.
|
||||
* For example in `[x, ...rest]` the `...rest` expression is a spread element.
|
||||
*/
|
||||
function isNotSpreadElement(e: t.Expression|t.SpreadElement): e is t.Expression {
|
||||
return !t.isSpreadElement(e);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return true if the expression can be considered a text based property name.
|
||||
*/
|
||||
function isPropertyName(e: t.Expression): e is t.Identifier|t.StringLiteral|t.NumericLiteral {
|
||||
return t.isIdentifier(e) || t.isStringLiteral(e) || t.isNumericLiteral(e);
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 {NodePath, Scope} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
|
||||
import {DeclarationScope} from '../../../linker';
|
||||
|
||||
export type ConstantScopePath = NodePath<t.Function|t.Program>;
|
||||
|
||||
/**
|
||||
* This class represents the lexical scope of a partial declaration in Babel source code.
|
||||
*
|
||||
* Its only responsibility is to compute a reference object for the scope of shared constant
|
||||
* statements that will be generated during partial linking.
|
||||
*/
|
||||
export class BabelDeclarationScope implements DeclarationScope<ConstantScopePath, t.Expression> {
|
||||
/**
|
||||
* Construct a new `BabelDeclarationScope`.
|
||||
*
|
||||
* @param declarationScope the Babel scope containing the declaration call expression.
|
||||
*/
|
||||
constructor(private declarationScope: Scope) {}
|
||||
|
||||
/**
|
||||
* Compute the Babel `NodePath` that can be used to reference the lexical scope where any
|
||||
* shared constant statements would be inserted.
|
||||
*
|
||||
* There will only be a shared constant scope if the expression is in an ECMAScript module, or a
|
||||
* UMD module. Otherwise `null` is returned to indicate that constant statements must be emitted
|
||||
* locally to the generated linked definition, to avoid polluting the global scope.
|
||||
*
|
||||
* @param expression the expression that points to the Angular core framework import.
|
||||
*/
|
||||
getConstantScopeRef(expression: t.Expression): ConstantScopePath|null {
|
||||
// If the expression is of the form `a.b.c` then we want to get the far LHS (e.g. `a`).
|
||||
let bindingExpression = expression;
|
||||
while (t.isMemberExpression(bindingExpression)) {
|
||||
bindingExpression = bindingExpression.object;
|
||||
}
|
||||
|
||||
if (!t.isIdentifier(bindingExpression)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// The binding of the expression is where this identifier was declared.
|
||||
// This could be a variable declaration, an import namespace or a function parameter.
|
||||
const binding = this.declarationScope.getBinding(bindingExpression.name);
|
||||
if (binding === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We only support shared constant statements if the binding was in a UMD module (i.e. declared
|
||||
// within a `t.Function`) or an ECMASCript module (i.e. declared at the top level of a
|
||||
// `t.Program` that is marked as a module).
|
||||
const path = binding.scope.path;
|
||||
if (!path.isFunctionParent() && !(path.isProgram() && path.node.sourceType === 'module')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
167
packages/compiler-cli/linker/babel/src/es2015_linker_plugin.ts
Normal file
167
packages/compiler-cli/linker/babel/src/es2015_linker_plugin.ts
Normal file
@ -0,0 +1,167 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 {PluginObj} from '@babel/core';
|
||||
import {NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
|
||||
import {FileLinker, isFatalLinkerError, LinkerEnvironment, LinkerOptions} from '../../../linker';
|
||||
|
||||
import {BabelAstFactory} from './ast/babel_ast_factory';
|
||||
import {BabelAstHost} from './ast/babel_ast_host';
|
||||
import {BabelDeclarationScope, ConstantScopePath} from './babel_declaration_scope';
|
||||
|
||||
/**
|
||||
* Create a Babel plugin that visits the program, identifying and linking partial declarations.
|
||||
*
|
||||
* The plugin delegates most of its work to a generic `FileLinker` for each file (`t.Program` in
|
||||
* Babel) that is visited.
|
||||
*/
|
||||
export function createEs2015LinkerPlugin(options: Partial<LinkerOptions> = {}): PluginObj {
|
||||
let fileLinker: FileLinker<ConstantScopePath, t.Statement, t.Expression>|null = null;
|
||||
|
||||
const linkerEnvironment = LinkerEnvironment.create<t.Statement, t.Expression>(
|
||||
new BabelAstHost(), new BabelAstFactory(), options);
|
||||
|
||||
return {
|
||||
visitor: {
|
||||
Program: {
|
||||
|
||||
/**
|
||||
* Create a new `FileLinker` as we enter each file (`t.Program` in Babel).
|
||||
*/
|
||||
enter(path: NodePath<t.Program>): void {
|
||||
assertNull(fileLinker);
|
||||
const file: BabelFile = path.hub.file;
|
||||
fileLinker = new FileLinker(linkerEnvironment, file.opts.filename ?? '', file.code);
|
||||
},
|
||||
|
||||
/**
|
||||
* On exiting the file, insert any shared constant statements that were generated during
|
||||
* linking of the partial declarations.
|
||||
*/
|
||||
exit(): void {
|
||||
assertNotNull(fileLinker);
|
||||
for (const {constantScope, statements} of fileLinker.getConstantStatements()) {
|
||||
insertStatements(constantScope, statements);
|
||||
}
|
||||
fileLinker = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Test each call expression to see if it is a partial declaration; it if is then replace it
|
||||
* with the results of linking the declaration.
|
||||
*/
|
||||
CallExpression(call: NodePath<t.CallExpression>): void {
|
||||
try {
|
||||
assertNotNull(fileLinker);
|
||||
|
||||
const callee = call.node.callee;
|
||||
if (!t.isExpression(callee)) {
|
||||
return;
|
||||
}
|
||||
const calleeName = linkerEnvironment.host.getSymbolName(callee);
|
||||
if (calleeName === null) {
|
||||
return;
|
||||
}
|
||||
const args = call.node.arguments;
|
||||
if (!fileLinker.isPartialDeclaration(calleeName) || !isExpressionArray(args)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const declarationScope = new BabelDeclarationScope(call.scope);
|
||||
const replacement = fileLinker.linkPartialDeclaration(calleeName, args, declarationScope);
|
||||
|
||||
call.replaceWith(replacement);
|
||||
} catch (e) {
|
||||
const node = isFatalLinkerError(e) ? e.node as t.Node : call.node;
|
||||
throw buildCodeFrameError(call.hub.file, e.message, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert the `statements` at the location defined by `path`.
|
||||
*
|
||||
* The actual insertion strategy depends upon the type of the `path`.
|
||||
*/
|
||||
function insertStatements(path: ConstantScopePath, statements: t.Statement[]): void {
|
||||
if (path.isFunction()) {
|
||||
insertIntoFunction(path, statements);
|
||||
} else if (path.isProgram()) {
|
||||
insertIntoProgram(path, statements);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert the `statements` at the top of the body of the `fn` function.
|
||||
*/
|
||||
function insertIntoFunction(fn: NodePath<t.Function>, statements: t.Statement[]): void {
|
||||
const body = fn.get('body');
|
||||
body.unshiftContainer('body', statements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert the `statements` at the top of the `program`, below any import statements.
|
||||
*/
|
||||
function insertIntoProgram(program: NodePath<t.Program>, statements: t.Statement[]): void {
|
||||
const body = program.get('body');
|
||||
const importStatements = body.filter(statement => statement.isImportDeclaration());
|
||||
if (importStatements.length === 0) {
|
||||
program.unshiftContainer('body', statements);
|
||||
} else {
|
||||
importStatements[importStatements.length - 1].insertAfter(statements);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if all the `nodes` are Babel expressions.
|
||||
*/
|
||||
function isExpressionArray(nodes: t.Node[]): nodes is t.Expression[] {
|
||||
return nodes.every(node => t.isExpression(node));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the given `obj` is `null`.
|
||||
*/
|
||||
function assertNull<T>(obj: T|null): asserts obj is null {
|
||||
if (obj !== null) {
|
||||
throw new Error('BUG - expected `obj` to be null');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the given `obj` is not `null`.
|
||||
*/
|
||||
function assertNotNull<T>(obj: T|null): asserts obj is T {
|
||||
if (obj === null) {
|
||||
throw new Error('BUG - expected `obj` not to be null');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a string representation of an error that includes the code frame of the `node`.
|
||||
*/
|
||||
function buildCodeFrameError(file: BabelFile, message: string, node: t.Node): string {
|
||||
const filename = file.opts.filename || '(unknown file)';
|
||||
const error = file.buildCodeFrameError(node, message);
|
||||
return `${filename}: ${error.message}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* This interface is making up for the fact that the Babel typings for `NodePath.hub.file` are
|
||||
* lacking.
|
||||
*/
|
||||
interface BabelFile {
|
||||
code: string;
|
||||
opts: {filename?: string;};
|
||||
|
||||
buildCodeFrameError(node: t.Node, message: string): Error;
|
||||
}
|
36
packages/compiler-cli/linker/babel/test/BUILD.bazel
Normal file
36
packages/compiler-cli/linker/babel/test/BUILD.bazel
Normal file
@ -0,0 +1,36 @@
|
||||
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob([
|
||||
"**/*.ts",
|
||||
]),
|
||||
deps = [
|
||||
"//packages:types",
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli/linker",
|
||||
"//packages/compiler-cli/linker/babel",
|
||||
"//packages/compiler-cli/src/ngtsc/translator",
|
||||
"@npm//@babel/core",
|
||||
"@npm//@babel/generator",
|
||||
"@npm//@babel/parser",
|
||||
"@npm//@babel/template",
|
||||
"@npm//@babel/traverse",
|
||||
"@npm//@babel/types",
|
||||
"@npm//@types/babel__core",
|
||||
"@npm//@types/babel__generator",
|
||||
"@npm//@types/babel__template",
|
||||
"@npm//@types/babel__traverse",
|
||||
],
|
||||
)
|
||||
|
||||
jasmine_node_test(
|
||||
name = "test",
|
||||
bootstrap = ["//tools/testing:node_no_angular_es5"],
|
||||
deps = [
|
||||
":test_lib",
|
||||
],
|
||||
)
|
@ -0,0 +1,382 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 {leadingComment} from '@angular/compiler';
|
||||
import generate from '@babel/generator';
|
||||
import {expression, statement} from '@babel/template';
|
||||
import * as t from '@babel/types';
|
||||
|
||||
import {BabelAstFactory} from '../../src/ast/babel_ast_factory';
|
||||
|
||||
describe('BabelAstFactory', () => {
|
||||
let factory: BabelAstFactory;
|
||||
beforeEach(() => factory = new BabelAstFactory());
|
||||
|
||||
describe('attachComments()', () => {
|
||||
it('should add the comments to the given statement', () => {
|
||||
const stmt = statement.ast`x = 10;`;
|
||||
factory.attachComments(
|
||||
stmt, [leadingComment('comment 1', true), leadingComment('comment 2', false)]);
|
||||
|
||||
expect(generate(stmt).code).toEqual([
|
||||
'/* comment 1 */',
|
||||
'//comment 2',
|
||||
'x = 10;',
|
||||
].join('\n'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('createArrayLiteral()', () => {
|
||||
it('should create an array node containing the provided expressions', () => {
|
||||
const expr1 = expression.ast`42`;
|
||||
const expr2 = expression.ast`"moo"`;
|
||||
|
||||
const array = factory.createArrayLiteral([expr1, expr2]);
|
||||
expect(generate(array).code).toEqual('[42, "moo"]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAssignment()', () => {
|
||||
it('should create an assignment node using the target and value expressions', () => {
|
||||
const target = expression.ast`x`;
|
||||
const value = expression.ast`42`;
|
||||
const assignment = factory.createAssignment(target, value);
|
||||
expect(generate(assignment).code).toEqual('x = 42');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createBinaryExpression()', () => {
|
||||
it('should create a binary operation node using the left and right expressions', () => {
|
||||
const left = expression.ast`17`;
|
||||
const right = expression.ast`42`;
|
||||
const expr = factory.createBinaryExpression(left, '+', right);
|
||||
expect(generate(expr).code).toEqual('17 + 42');
|
||||
});
|
||||
|
||||
it('should create a binary operation node for logical operators', () => {
|
||||
const left = expression.ast`17`;
|
||||
const right = expression.ast`42`;
|
||||
const expr = factory.createBinaryExpression(left, '&&', right);
|
||||
expect(t.isLogicalExpression(expr)).toBe(true);
|
||||
expect(generate(expr).code).toEqual('17 && 42');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createBlock()', () => {
|
||||
it('should create a block statement containing the given statements', () => {
|
||||
const stmt1 = statement.ast`x = 10`;
|
||||
const stmt2 = statement.ast`y = 20`;
|
||||
const block = factory.createBlock([stmt1, stmt2]);
|
||||
expect(generate(block).code).toEqual([
|
||||
'{',
|
||||
' x = 10;',
|
||||
' y = 20;',
|
||||
'}',
|
||||
].join('\n'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('createCallExpression()', () => {
|
||||
it('should create a call on the `callee` with the given `args`', () => {
|
||||
const callee = expression.ast`foo`;
|
||||
const arg1 = expression.ast`42`;
|
||||
const arg2 = expression.ast`"moo"`;
|
||||
const call = factory.createCallExpression(callee, [arg1, arg2], false);
|
||||
expect(generate(call).code).toEqual('foo(42, "moo")');
|
||||
});
|
||||
|
||||
it('should create a call marked with a PURE comment if `pure` is true', () => {
|
||||
const callee = expression.ast`foo`;
|
||||
const arg1 = expression.ast`42`;
|
||||
const arg2 = expression.ast`"moo"`;
|
||||
const call = factory.createCallExpression(callee, [arg1, arg2], true);
|
||||
expect(generate(call).code).toEqual(['/* @__PURE__ */', 'foo(42, "moo")'].join('\n'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('createConditional()', () => {
|
||||
it('should create a condition expression', () => {
|
||||
const test = expression.ast`!test`;
|
||||
const thenExpr = expression.ast`42`;
|
||||
const elseExpr = expression.ast`"moo"`;
|
||||
const conditional = factory.createConditional(test, thenExpr, elseExpr);
|
||||
expect(generate(conditional).code).toEqual('!test ? 42 : "moo"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createElementAccess()', () => {
|
||||
it('should create an expression accessing the element of an array/object', () => {
|
||||
const expr = expression.ast`obj`;
|
||||
const element = expression.ast`"moo"`;
|
||||
const access = factory.createElementAccess(expr, element);
|
||||
expect(generate(access).code).toEqual('obj["moo"]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createExpressionStatement()', () => {
|
||||
it('should create a statement node from the given expression', () => {
|
||||
const expr = expression.ast`x = 10`;
|
||||
const stmt = factory.createExpressionStatement(expr);
|
||||
expect(t.isStatement(stmt)).toBe(true);
|
||||
expect(generate(stmt).code).toEqual('x = 10;');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createFunctionDeclaration()', () => {
|
||||
it('should create a function declaration node with the given name, parameters and body statements',
|
||||
() => {
|
||||
const stmts = statement.ast`{x = 10; y = 20;}`;
|
||||
const fn = factory.createFunctionDeclaration('foo', ['arg1', 'arg2'], stmts);
|
||||
expect(generate(fn).code).toEqual([
|
||||
'function foo(arg1, arg2) {',
|
||||
' x = 10;',
|
||||
' y = 20;',
|
||||
'}',
|
||||
].join('\n'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('createFunctionExpression()', () => {
|
||||
it('should create a function expression node with the given name, parameters and body statements',
|
||||
() => {
|
||||
const stmts = statement.ast`{x = 10; y = 20;}`;
|
||||
const fn = factory.createFunctionExpression('foo', ['arg1', 'arg2'], stmts);
|
||||
expect(t.isStatement(fn)).toBe(false);
|
||||
expect(generate(fn).code).toEqual([
|
||||
'function foo(arg1, arg2) {',
|
||||
' x = 10;',
|
||||
' y = 20;',
|
||||
'}',
|
||||
].join('\n'));
|
||||
});
|
||||
|
||||
it('should create an anonymous function expression node if the name is null', () => {
|
||||
const stmts = statement.ast`{x = 10; y = 20;}`;
|
||||
const fn = factory.createFunctionExpression(null, ['arg1', 'arg2'], stmts);
|
||||
expect(generate(fn).code).toEqual([
|
||||
'function (arg1, arg2) {',
|
||||
' x = 10;',
|
||||
' y = 20;',
|
||||
'}',
|
||||
].join('\n'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('createIdentifier()', () => {
|
||||
it('should create an identifier with the given name', () => {
|
||||
const id = factory.createIdentifier('someId') as t.Identifier;
|
||||
expect(t.isIdentifier(id)).toBe(true);
|
||||
expect(id.name).toEqual('someId');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createIfStatement()', () => {
|
||||
it('should create an if-else statement', () => {
|
||||
const test = expression.ast`!test`;
|
||||
const thenStmt = statement.ast`x = 10;`;
|
||||
const elseStmt = statement.ast`x = 42;`;
|
||||
const ifStmt = factory.createIfStatement(test, thenStmt, elseStmt);
|
||||
expect(generate(ifStmt).code).toEqual('if (!test) x = 10;else x = 42;');
|
||||
});
|
||||
|
||||
it('should create an if statement if the else expression is null', () => {
|
||||
const test = expression.ast`!test`;
|
||||
const thenStmt = statement.ast`x = 10;`;
|
||||
const ifStmt = factory.createIfStatement(test, thenStmt, null);
|
||||
expect(generate(ifStmt).code).toEqual('if (!test) x = 10;');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createLiteral()', () => {
|
||||
it('should create a string literal', () => {
|
||||
const literal = factory.createLiteral('moo');
|
||||
expect(t.isStringLiteral(literal)).toBe(true);
|
||||
expect(generate(literal).code).toEqual('"moo"');
|
||||
});
|
||||
|
||||
it('should create a number literal', () => {
|
||||
const literal = factory.createLiteral(42);
|
||||
expect(t.isNumericLiteral(literal)).toBe(true);
|
||||
expect(generate(literal).code).toEqual('42');
|
||||
});
|
||||
|
||||
it('should create a number literal for `NaN`', () => {
|
||||
const literal = factory.createLiteral(NaN);
|
||||
expect(t.isNumericLiteral(literal)).toBe(true);
|
||||
expect(generate(literal).code).toEqual('NaN');
|
||||
});
|
||||
|
||||
it('should create a boolean literal', () => {
|
||||
const literal = factory.createLiteral(true);
|
||||
expect(t.isBooleanLiteral(literal)).toBe(true);
|
||||
expect(generate(literal).code).toEqual('true');
|
||||
});
|
||||
|
||||
it('should create an `undefined` literal', () => {
|
||||
const literal = factory.createLiteral(undefined);
|
||||
expect(t.isIdentifier(literal)).toBe(true);
|
||||
expect(generate(literal).code).toEqual('undefined');
|
||||
});
|
||||
|
||||
it('should create a null literal', () => {
|
||||
const literal = factory.createLiteral(null);
|
||||
expect(t.isNullLiteral(literal)).toBe(true);
|
||||
expect(generate(literal).code).toEqual('null');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createNewExpression()', () => {
|
||||
it('should create a `new` operation on the constructor `expression` with the given `args`',
|
||||
() => {
|
||||
const expr = expression.ast`Foo`;
|
||||
const arg1 = expression.ast`42`;
|
||||
const arg2 = expression.ast`"moo"`;
|
||||
const call = factory.createNewExpression(expr, [arg1, arg2]);
|
||||
expect(generate(call).code).toEqual('new Foo(42, "moo")');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createObjectLiteral()', () => {
|
||||
it('should create an object literal node, with the given properties', () => {
|
||||
const prop1 = expression.ast`42`;
|
||||
const prop2 = expression.ast`"moo"`;
|
||||
const obj = factory.createObjectLiteral([
|
||||
{propertyName: 'prop1', value: prop1, quoted: false},
|
||||
{propertyName: 'prop2', value: prop2, quoted: true},
|
||||
]);
|
||||
expect(generate(obj).code).toEqual([
|
||||
'{',
|
||||
' prop1: 42,',
|
||||
' "prop2": "moo"',
|
||||
'}',
|
||||
].join('\n'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('createParenthesizedExpression()', () => {
|
||||
it('should add parentheses around the given expression', () => {
|
||||
const expr = expression.ast`a + b`;
|
||||
const paren = factory.createParenthesizedExpression(expr);
|
||||
expect(generate(paren).code).toEqual('(a + b)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPropertyAccess()', () => {
|
||||
it('should create a property access expression node', () => {
|
||||
const expr = expression.ast`obj`;
|
||||
const access = factory.createPropertyAccess(expr, 'moo');
|
||||
expect(generate(access).code).toEqual('obj.moo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createReturnStatement()', () => {
|
||||
it('should create a return statement returning the given expression', () => {
|
||||
const expr = expression.ast`42`;
|
||||
const returnStmt = factory.createReturnStatement(expr);
|
||||
expect(generate(returnStmt).code).toEqual('return 42;');
|
||||
});
|
||||
|
||||
it('should create a void return statement if the expression is null', () => {
|
||||
const returnStmt = factory.createReturnStatement(null);
|
||||
expect(generate(returnStmt).code).toEqual('return;');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTaggedTemplate()', () => {
|
||||
it('should create a tagged template node from the tag, elements and expressions', () => {
|
||||
const elements = [
|
||||
{raw: 'raw1', cooked: 'cooked1', range: null},
|
||||
{raw: 'raw2', cooked: 'cooked2', range: null},
|
||||
{raw: 'raw3', cooked: 'cooked3', range: null},
|
||||
];
|
||||
const expressions = [
|
||||
expression.ast`42`,
|
||||
expression.ast`"moo"`,
|
||||
];
|
||||
const tag = expression.ast`tagFn`;
|
||||
const template = factory.createTaggedTemplate(tag, {elements, expressions});
|
||||
expect(generate(template).code).toEqual('tagFn`raw1${42}raw2${"moo"}raw3`');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createThrowStatement()', () => {
|
||||
it('should create a throw statement, throwing the given expression', () => {
|
||||
const expr = expression.ast`new Error("bad")`;
|
||||
const throwStmt = factory.createThrowStatement(expr);
|
||||
expect(generate(throwStmt).code).toEqual('throw new Error("bad");');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTypeOfExpression()', () => {
|
||||
it('should create a typeof expression node', () => {
|
||||
const expr = expression.ast`42`;
|
||||
const typeofExpr = factory.createTypeOfExpression(expr);
|
||||
expect(generate(typeofExpr).code).toEqual('typeof 42');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createUnaryExpression()', () => {
|
||||
it('should create a unary expression with the operator and operand', () => {
|
||||
const expr = expression.ast`value`;
|
||||
const unaryExpr = factory.createUnaryExpression('!', expr);
|
||||
expect(generate(unaryExpr).code).toEqual('!value');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createVariableDeclaration()', () => {
|
||||
it('should create a variable declaration statement node for the given variable name and initializer',
|
||||
() => {
|
||||
const initializer = expression.ast`42`;
|
||||
const varDecl = factory.createVariableDeclaration('foo', initializer, 'let');
|
||||
expect(generate(varDecl).code).toEqual('let foo = 42;');
|
||||
});
|
||||
|
||||
it('should create a constant declaration statement node for the given variable name and initializer',
|
||||
() => {
|
||||
const initializer = expression.ast`42`;
|
||||
const varDecl = factory.createVariableDeclaration('foo', initializer, 'const');
|
||||
expect(generate(varDecl).code).toEqual('const foo = 42;');
|
||||
});
|
||||
|
||||
it('should create a downleveled variable declaration statement node for the given variable name and initializer',
|
||||
() => {
|
||||
const initializer = expression.ast`42`;
|
||||
const varDecl = factory.createVariableDeclaration('foo', initializer, 'var');
|
||||
expect(generate(varDecl).code).toEqual('var foo = 42;');
|
||||
});
|
||||
|
||||
it('should create an uninitialized variable declaration statement node for the given variable name and a null initializer',
|
||||
() => {
|
||||
const varDecl = factory.createVariableDeclaration('foo', null, 'let');
|
||||
expect(generate(varDecl).code).toEqual('let foo;');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSourceMapRange()', () => {
|
||||
it('should attach the `sourceMapRange` to the given `node`', () => {
|
||||
const expr = expression.ast`42`;
|
||||
expect(expr.loc).toBeUndefined();
|
||||
expect(expr.start).toBeUndefined();
|
||||
expect(expr.end).toBeUndefined();
|
||||
|
||||
factory.setSourceMapRange(expr, {
|
||||
start: {line: 0, column: 1, offset: 1},
|
||||
end: {line: 2, column: 3, offset: 15},
|
||||
content: '-****\n*****\n****',
|
||||
url: 'original.ts'
|
||||
});
|
||||
|
||||
// Lines are 1-based in Babel.
|
||||
expect(expr.loc).toEqual({
|
||||
start: {line: 1, column: 1},
|
||||
end: {line: 3, column: 3},
|
||||
});
|
||||
expect(expr.start).toEqual(1);
|
||||
expect(expr.end).toEqual(15);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,305 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 t from '@babel/types';
|
||||
import template from '@babel/template';
|
||||
import {parse} from '@babel/parser';
|
||||
import {BabelAstHost} from '../../src/ast/babel_ast_host';
|
||||
|
||||
describe('BabelAstHost', () => {
|
||||
let host: BabelAstHost;
|
||||
beforeEach(() => host = new BabelAstHost());
|
||||
|
||||
describe('getSymbolName()', () => {
|
||||
it('should return the name of an identifier', () => {
|
||||
expect(host.getSymbolName(expr('someIdentifier'))).toEqual('someIdentifier');
|
||||
});
|
||||
|
||||
it('should return the name of an identifier at the end of a property access chain', () => {
|
||||
expect(host.getSymbolName(expr('a.b.c.someIdentifier'))).toEqual('someIdentifier');
|
||||
});
|
||||
|
||||
it('should return null if the expression has no identifier', () => {
|
||||
expect(host.getSymbolName(expr('42'))).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isStringLiteral()', () => {
|
||||
it('should return true if the expression is a string literal', () => {
|
||||
expect(host.isStringLiteral(expr('"moo"'))).toBe(true);
|
||||
expect(host.isStringLiteral(expr('\'moo\''))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the expression is not a string literal', () => {
|
||||
expect(host.isStringLiteral(expr('true'))).toBe(false);
|
||||
expect(host.isStringLiteral(expr('someIdentifier'))).toBe(false);
|
||||
expect(host.isStringLiteral(expr('42'))).toBe(false);
|
||||
expect(host.isStringLiteral(expr('{}'))).toBe(false);
|
||||
expect(host.isStringLiteral(expr('[]'))).toBe(false);
|
||||
expect(host.isStringLiteral(expr('null'))).toBe(false);
|
||||
expect(host.isStringLiteral(expr('\'a\' + \'b\''))).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if the expression is a template string', () => {
|
||||
expect(host.isStringLiteral(expr('\`moo\`'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseStringLiteral()', () => {
|
||||
it('should extract the string value', () => {
|
||||
expect(host.parseStringLiteral(expr('"moo"'))).toEqual('moo');
|
||||
expect(host.parseStringLiteral(expr('\'moo\''))).toEqual('moo');
|
||||
});
|
||||
|
||||
it('should error if the value is not a string literal', () => {
|
||||
expect(() => host.parseStringLiteral(expr('42')))
|
||||
.toThrowError('Unsupported syntax, expected a string literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNumericLiteral()', () => {
|
||||
it('should return true if the expression is a number literal', () => {
|
||||
expect(host.isNumericLiteral(expr('42'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the expression is not a number literal', () => {
|
||||
expect(host.isStringLiteral(expr('true'))).toBe(false);
|
||||
expect(host.isNumericLiteral(expr('"moo"'))).toBe(false);
|
||||
expect(host.isNumericLiteral(expr('\'moo\''))).toBe(false);
|
||||
expect(host.isNumericLiteral(expr('someIdentifier'))).toBe(false);
|
||||
expect(host.isNumericLiteral(expr('{}'))).toBe(false);
|
||||
expect(host.isNumericLiteral(expr('[]'))).toBe(false);
|
||||
expect(host.isNumericLiteral(expr('null'))).toBe(false);
|
||||
expect(host.isNumericLiteral(expr('\'a\' + \'b\''))).toBe(false);
|
||||
expect(host.isNumericLiteral(expr('\`moo\`'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseNumericLiteral()', () => {
|
||||
it('should extract the number value', () => {
|
||||
expect(host.parseNumericLiteral(expr('42'))).toEqual(42);
|
||||
});
|
||||
|
||||
it('should error if the value is not a numeric literal', () => {
|
||||
expect(() => host.parseNumericLiteral(expr('"moo"')))
|
||||
.toThrowError('Unsupported syntax, expected a numeric literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isBooleanLiteral()', () => {
|
||||
it('should return true if the expression is a boolean literal', () => {
|
||||
expect(host.isBooleanLiteral(expr('true'))).toBe(true);
|
||||
expect(host.isBooleanLiteral(expr('false'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the expression is not a boolean literal', () => {
|
||||
expect(host.isBooleanLiteral(expr('"moo"'))).toBe(false);
|
||||
expect(host.isBooleanLiteral(expr('\'moo\''))).toBe(false);
|
||||
expect(host.isBooleanLiteral(expr('someIdentifier'))).toBe(false);
|
||||
expect(host.isBooleanLiteral(expr('42'))).toBe(false);
|
||||
expect(host.isBooleanLiteral(expr('{}'))).toBe(false);
|
||||
expect(host.isBooleanLiteral(expr('[]'))).toBe(false);
|
||||
expect(host.isBooleanLiteral(expr('null'))).toBe(false);
|
||||
expect(host.isBooleanLiteral(expr('\'a\' + \'b\''))).toBe(false);
|
||||
expect(host.isBooleanLiteral(expr('\`moo\`'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseBooleanLiteral()', () => {
|
||||
it('should extract the boolean value', () => {
|
||||
expect(host.parseBooleanLiteral(expr('true'))).toEqual(true);
|
||||
expect(host.parseBooleanLiteral(expr('false'))).toEqual(false);
|
||||
});
|
||||
|
||||
it('should error if the value is not a boolean literal', () => {
|
||||
expect(() => host.parseBooleanLiteral(expr('"moo"')))
|
||||
.toThrowError('Unsupported syntax, expected a boolean literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isArrayLiteral()', () => {
|
||||
it('should return true if the expression is an array literal', () => {
|
||||
expect(host.isArrayLiteral(expr('[]'))).toBe(true);
|
||||
expect(host.isArrayLiteral(expr('[1, 2, 3]'))).toBe(true);
|
||||
expect(host.isArrayLiteral(expr('[[], []]'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the expression is not an array literal', () => {
|
||||
expect(host.isArrayLiteral(expr('"moo"'))).toBe(false);
|
||||
expect(host.isArrayLiteral(expr('\'moo\''))).toBe(false);
|
||||
expect(host.isArrayLiteral(expr('someIdentifier'))).toBe(false);
|
||||
expect(host.isArrayLiteral(expr('42'))).toBe(false);
|
||||
expect(host.isArrayLiteral(expr('{}'))).toBe(false);
|
||||
expect(host.isArrayLiteral(expr('null'))).toBe(false);
|
||||
expect(host.isArrayLiteral(expr('\'a\' + \'b\''))).toBe(false);
|
||||
expect(host.isArrayLiteral(expr('\`moo\`'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseArrayLiteral()', () => {
|
||||
it('should extract the expressions in the array', () => {
|
||||
const moo = expr('\'moo\'');
|
||||
expect(host.parseArrayLiteral(expr('[]'))).toEqual([]);
|
||||
expect(host.parseArrayLiteral(expr('[\'moo\']'))).toEqual([moo]);
|
||||
});
|
||||
|
||||
it('should error if there is an empty item', () => {
|
||||
expect(() => host.parseArrayLiteral(expr('[,]')))
|
||||
.toThrowError('Unsupported syntax, expected element in array not to be empty.');
|
||||
});
|
||||
|
||||
it('should error if there is a spread element', () => {
|
||||
expect(() => host.parseArrayLiteral(expr('[...[0,1]]')))
|
||||
.toThrowError('Unsupported syntax, expected element in array not to use spread syntax.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isObjectLiteral()', () => {
|
||||
it('should return true if the expression is an object literal', () => {
|
||||
expect(host.isObjectLiteral(rhs('x = {}'))).toBe(true);
|
||||
expect(host.isObjectLiteral(rhs('x = { foo: \'bar\' }'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the expression is not an object literal', () => {
|
||||
expect(host.isObjectLiteral(rhs('x = "moo"'))).toBe(false);
|
||||
expect(host.isObjectLiteral(rhs('x = \'moo\''))).toBe(false);
|
||||
expect(host.isObjectLiteral(rhs('x = someIdentifier'))).toBe(false);
|
||||
expect(host.isObjectLiteral(rhs('x = 42'))).toBe(false);
|
||||
expect(host.isObjectLiteral(rhs('x = []'))).toBe(false);
|
||||
expect(host.isObjectLiteral(rhs('x = null'))).toBe(false);
|
||||
expect(host.isObjectLiteral(rhs('x = \'a\' + \'b\''))).toBe(false);
|
||||
expect(host.isObjectLiteral(rhs('x = \`moo\`'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseObjectLiteral()', () => {
|
||||
it('should extract the properties from the object', () => {
|
||||
const moo = expr('\'moo\'');
|
||||
expect(host.parseObjectLiteral(rhs('x = {}'))).toEqual(new Map());
|
||||
expect(host.parseObjectLiteral(rhs('x = {a: \'moo\'}'))).toEqual(new Map([['a', moo]]));
|
||||
});
|
||||
|
||||
it('should error if there is a method', () => {
|
||||
expect(() => host.parseObjectLiteral(rhs('x = { foo() {} }')))
|
||||
.toThrowError('Unsupported syntax, expected a property assignment.');
|
||||
});
|
||||
|
||||
it('should error if there is a spread element', () => {
|
||||
expect(() => host.parseObjectLiteral(rhs('x = {...{a:\'moo\'}}')))
|
||||
.toThrowError('Unsupported syntax, expected a property assignment.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFunctionExpression()', () => {
|
||||
it('should return true if the expression is a function', () => {
|
||||
expect(host.isFunctionExpression(rhs('x = function() {}'))).toBe(true);
|
||||
expect(host.isFunctionExpression(rhs('x = function foo() {}'))).toBe(true);
|
||||
expect(host.isFunctionExpression(rhs('x = () => {}'))).toBe(true);
|
||||
expect(host.isFunctionExpression(rhs('x = () => true'))).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the expression is a function declaration', () => {
|
||||
expect(host.isFunctionExpression(expr('function foo() {}'))).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if the expression is not a function expression', () => {
|
||||
expect(host.isFunctionExpression(expr('[]'))).toBe(false);
|
||||
expect(host.isFunctionExpression(expr('"moo"'))).toBe(false);
|
||||
expect(host.isFunctionExpression(expr('\'moo\''))).toBe(false);
|
||||
expect(host.isFunctionExpression(expr('someIdentifier'))).toBe(false);
|
||||
expect(host.isFunctionExpression(expr('42'))).toBe(false);
|
||||
expect(host.isFunctionExpression(expr('{}'))).toBe(false);
|
||||
expect(host.isFunctionExpression(expr('null'))).toBe(false);
|
||||
expect(host.isFunctionExpression(expr('\'a\' + \'b\''))).toBe(false);
|
||||
expect(host.isFunctionExpression(expr('\`moo\`'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseReturnValue()', () => {
|
||||
it('should extract the return value of a function', () => {
|
||||
const moo = expr('\'moo\'');
|
||||
expect(host.parseReturnValue(rhs('x = function() { return \'moo\'; }'))).toEqual(moo);
|
||||
});
|
||||
|
||||
it('should extract the value of a simple arrow function', () => {
|
||||
const moo = expr('\'moo\'');
|
||||
expect(host.parseReturnValue(rhs('x = () => \'moo\''))).toEqual(moo);
|
||||
});
|
||||
|
||||
it('should extract the return value of an arrow function', () => {
|
||||
const moo = expr('\'moo\'');
|
||||
expect(host.parseReturnValue(rhs('x = () => { return \'moo\' }'))).toEqual(moo);
|
||||
});
|
||||
|
||||
it('should error if the body has 0 statements', () => {
|
||||
expect(() => host.parseReturnValue(rhs('x = function () { }')))
|
||||
.toThrowError(
|
||||
'Unsupported syntax, expected a function body with a single return statement.');
|
||||
expect(() => host.parseReturnValue(rhs('x = () => { }')))
|
||||
.toThrowError(
|
||||
'Unsupported syntax, expected a function body with a single return statement.');
|
||||
});
|
||||
|
||||
it('should error if the body has more than 1 statement', () => {
|
||||
expect(() => host.parseReturnValue(rhs('x = function () { const x = 10; return x; }')))
|
||||
.toThrowError(
|
||||
'Unsupported syntax, expected a function body with a single return statement.');
|
||||
expect(() => host.parseReturnValue(rhs('x = () => { const x = 10; return x; }')))
|
||||
.toThrowError(
|
||||
'Unsupported syntax, expected a function body with a single return statement.');
|
||||
});
|
||||
|
||||
it('should error if the single statement is not a return statement', () => {
|
||||
expect(() => host.parseReturnValue(rhs('x = function () { const x = 10; }')))
|
||||
.toThrowError(
|
||||
'Unsupported syntax, expected a function body with a single return statement.');
|
||||
expect(() => host.parseReturnValue(rhs('x = () => { const x = 10; }')))
|
||||
.toThrowError(
|
||||
'Unsupported syntax, expected a function body with a single return statement.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRange()', () => {
|
||||
it('should extract the range from the expression', () => {
|
||||
const file = parse('// preamble\nx = \'moo\';');
|
||||
const stmt = file.program.body[0];
|
||||
assertExpressionStatement(stmt);
|
||||
assertAssignmentExpression(stmt.expression);
|
||||
expect(host.getRange(stmt.expression.right))
|
||||
.toEqual({startLine: 1, startCol: 4, startPos: 16, endPos: 21});
|
||||
});
|
||||
|
||||
it('should error if there is no range information', () => {
|
||||
const moo = rhs('// preamble\nx = \'moo\';');
|
||||
expect(() => host.getRange(moo))
|
||||
.toThrowError('Unable to read range for node - it is missing location information.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function expr(code: string): t.Expression {
|
||||
const stmt = template.ast(code);
|
||||
return (stmt as t.ExpressionStatement).expression;
|
||||
}
|
||||
|
||||
function rhs(code: string): t.Expression {
|
||||
const e = expr(code);
|
||||
assertAssignmentExpression(e);
|
||||
return e.right;
|
||||
}
|
||||
|
||||
function assertExpressionStatement(e: t.Node): asserts e is t.ExpressionStatement {
|
||||
if (!t.isExpressionStatement(e)) {
|
||||
throw new Error('Bad test - expected an expression statement');
|
||||
}
|
||||
}
|
||||
|
||||
function assertAssignmentExpression(e: t.Expression): asserts e is t.AssignmentExpression {
|
||||
if (!t.isAssignmentExpression(e)) {
|
||||
throw new Error('Bad test - expected an assignment expression');
|
||||
}
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 {parse} from '@babel/parser';
|
||||
import traverse, {NodePath} from '@babel/traverse';
|
||||
import * as t from '@babel/types';
|
||||
import {BabelDeclarationScope} from '../src/babel_declaration_scope';
|
||||
|
||||
|
||||
describe('BabelDeclarationScope', () => {
|
||||
describe('getConstantScopeRef()', () => {
|
||||
it('should return a path to the ES module where the expression was imported', () => {
|
||||
const ast = parse(
|
||||
[
|
||||
'import * as core from \'@angular/core\';',
|
||||
'function foo() {',
|
||||
' var TEST = core;',
|
||||
'}',
|
||||
].join('\n'),
|
||||
{sourceType: 'module'});
|
||||
const nodePath = findVarDeclaration(ast, 'TEST');
|
||||
const scope = new BabelDeclarationScope(nodePath.scope);
|
||||
const constantScope = scope.getConstantScopeRef(nodePath.get('init').node);
|
||||
expect(constantScope).not.toBe(null);
|
||||
expect(constantScope!.node).toBe(ast.program);
|
||||
});
|
||||
|
||||
it('should return a path to the ES Module where the expression is declared', () => {
|
||||
const ast = parse(
|
||||
[
|
||||
'var core;',
|
||||
'export function foo() {',
|
||||
' var TEST = core;',
|
||||
'}',
|
||||
].join('\n'),
|
||||
{sourceType: 'module'});
|
||||
const nodePath = findVarDeclaration(ast, 'TEST');
|
||||
const scope = new BabelDeclarationScope(nodePath.scope);
|
||||
const constantScope = scope.getConstantScopeRef(nodePath.get('init').node);
|
||||
expect(constantScope).not.toBe(null);
|
||||
expect(constantScope!.node).toBe(ast.program);
|
||||
});
|
||||
|
||||
it('should return null if the file is not an ES module', () => {
|
||||
const ast = parse(
|
||||
[
|
||||
'var core;',
|
||||
'function foo() {',
|
||||
' var TEST = core;',
|
||||
'}',
|
||||
].join('\n'),
|
||||
{sourceType: 'script'});
|
||||
const nodePath = findVarDeclaration(ast, 'TEST');
|
||||
const scope = new BabelDeclarationScope(nodePath.scope);
|
||||
const constantScope = scope.getConstantScopeRef(nodePath.get('init').node);
|
||||
expect(constantScope).toBe(null);
|
||||
});
|
||||
|
||||
it('should return the IIFE factory function where the expression is a parameter', () => {
|
||||
const ast = parse(
|
||||
[
|
||||
'var core;',
|
||||
'(function(core) {',
|
||||
' var BLOCK = \'block\';',
|
||||
' function foo() {',
|
||||
' var TEST = core;',
|
||||
' }',
|
||||
'})(core);',
|
||||
].join('\n'),
|
||||
{sourceType: 'script'});
|
||||
const nodePath = findVarDeclaration(ast, 'TEST');
|
||||
const fnPath = findFirstFunction(ast);
|
||||
const scope = new BabelDeclarationScope(nodePath.scope);
|
||||
const constantScope = scope.getConstantScopeRef(nodePath.get('init').node);
|
||||
expect(constantScope).not.toBe(null);
|
||||
expect(constantScope!.isFunction()).toBe(true);
|
||||
expect(constantScope!.node).toEqual(fnPath.node);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function findVarDeclaration(
|
||||
file: t.File, varName: string): NodePath<t.VariableDeclarator&{init: t.Expression}> {
|
||||
let varDecl: NodePath<t.VariableDeclarator>|undefined = undefined;
|
||||
traverse(file, {
|
||||
VariableDeclarator: (path) => {
|
||||
const id = path.get('id');
|
||||
if (id.isIdentifier() && id.node.name === varName && path.get('init') !== null) {
|
||||
varDecl = path;
|
||||
path.stop();
|
||||
}
|
||||
}
|
||||
});
|
||||
if (varDecl === undefined) {
|
||||
throw new Error(`TEST BUG: expected to find variable declaration for ${varName}.`);
|
||||
}
|
||||
return varDecl;
|
||||
}
|
||||
|
||||
function findFirstFunction(file: t.File): NodePath<t.Function> {
|
||||
let fn: NodePath<t.Function>|undefined = undefined;
|
||||
traverse(file, {
|
||||
Function: (path) => {
|
||||
fn = path;
|
||||
path.stop();
|
||||
}
|
||||
});
|
||||
if (fn === undefined) {
|
||||
throw new Error(`TEST BUG: expected to find a function.`);
|
||||
}
|
||||
return fn;
|
||||
}
|
@ -0,0 +1,256 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC 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 o from '@angular/compiler/src/output/output_ast';
|
||||
import {NodePath, PluginObj, transformSync} from '@babel/core';
|
||||
import generate from '@babel/generator';
|
||||
import * as t from '@babel/types';
|
||||
|
||||
import {FileLinker} from '../../../linker';
|
||||
import {PartialDirectiveLinkerVersion1} from '../../src/file_linker/partial_linkers/partial_directive_linker_1';
|
||||
import {createEs2015LinkerPlugin} from '../src/es2015_linker_plugin';
|
||||
|
||||
describe('createEs2015LinkerPlugin()', () => {
|
||||
it('should return a Babel plugin visitor that handles Program (enter/exit) and CallExpression nodes',
|
||||
() => {
|
||||
const plugin = createEs2015LinkerPlugin();
|
||||
expect(plugin.visitor).toEqual({
|
||||
Program: {
|
||||
enter: jasmine.any(Function),
|
||||
exit: jasmine.any(Function),
|
||||
},
|
||||
CallExpression: jasmine.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a Babel plugin that calls FileLinker.isPartialDeclaration() on each call expression',
|
||||
() => {
|
||||
const isPartialDeclarationSpy = spyOn(FileLinker.prototype, 'isPartialDeclaration');
|
||||
|
||||
transformSync(
|
||||
[
|
||||
'var core;', `fn1()`, 'fn2({prop: () => fn3({})});', `x.method(() => fn4());`,
|
||||
'spread(...x);'
|
||||
].join('\n'),
|
||||
{
|
||||
plugins: [createEs2015LinkerPlugin()],
|
||||
filename: '/test.js',
|
||||
parserOpts: {sourceType: 'unambiguous'},
|
||||
});
|
||||
expect(isPartialDeclarationSpy.calls.allArgs()).toEqual([
|
||||
['fn1'],
|
||||
['fn2'],
|
||||
['fn3'],
|
||||
['method'],
|
||||
['fn4'],
|
||||
['spread'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return a Babel plugin that calls FileLinker.linkPartialDeclaration() on each matching declaration',
|
||||
() => {
|
||||
const linkSpy = spyOn(FileLinker.prototype, 'linkPartialDeclaration')
|
||||
.and.returnValue(t.identifier('REPLACEMENT'));
|
||||
|
||||
transformSync(
|
||||
[
|
||||
'var core;',
|
||||
`$ngDeclareDirective({version: 1, ngImport: core, x: 1});`,
|
||||
`$ngDeclareComponent({version: 1, ngImport: core, foo: () => $ngDeclareDirective({version: 1, ngImport: core, x: 2})});`,
|
||||
`x.qux(() => $ngDeclareDirective({version: 1, ngImport: core, x: 3}));`,
|
||||
'spread(...x);',
|
||||
].join('\n'),
|
||||
{
|
||||
plugins: [createEs2015LinkerPlugin()],
|
||||
filename: '/test.js',
|
||||
parserOpts: {sourceType: 'unambiguous'},
|
||||
});
|
||||
|
||||
expect(humanizeLinkerCalls(linkSpy.calls)).toEqual([
|
||||
['$ngDeclareDirective', '{version:1,ngImport:core,x:1}'],
|
||||
[
|
||||
'$ngDeclareComponent',
|
||||
'{version:1,ngImport:core,foo:()=>$ngDeclareDirective({version:1,ngImport:core,x:2})}'
|
||||
],
|
||||
// Note we do not process `x:2` declaration since it is nested within another declaration
|
||||
['$ngDeclareDirective', '{version:1,ngImport:core,x:3}']
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return a Babel plugin that replaces call expressions with the return value from FileLinker.linkPartialDeclaration()',
|
||||
() => {
|
||||
let replaceCount = 0;
|
||||
spyOn(FileLinker.prototype, 'linkPartialDeclaration')
|
||||
.and.callFake(() => t.identifier('REPLACEMENT_' + ++replaceCount));
|
||||
const result = transformSync(
|
||||
[
|
||||
'var core;',
|
||||
'$ngDeclareDirective({version: 1, ngImport: core});',
|
||||
'$ngDeclareDirective({version: 1, ngImport: core, foo: () => bar({})});',
|
||||
'x.qux();',
|
||||
'spread(...x);',
|
||||
].join('\n'),
|
||||
{
|
||||
plugins: [createEs2015LinkerPlugin()],
|
||||
filename: '/test.js',
|
||||
parserOpts: {sourceType: 'unambiguous'},
|
||||
generatorOpts: {compact: true},
|
||||
});
|
||||
expect(result!.code).toEqual('var core;REPLACEMENT_1;REPLACEMENT_2;x.qux();spread(...x);');
|
||||
});
|
||||
|
||||
it('should return a Babel plugin that adds shared statements after any imports', () => {
|
||||
spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT'));
|
||||
const result = transformSync(
|
||||
[
|
||||
'import * as core from \'some-module\';',
|
||||
'import {id} from \'other-module\';',
|
||||
`$ngDeclareDirective({version: 1, ngImport: core})`,
|
||||
`$ngDeclareDirective({version: 1, ngImport: core})`,
|
||||
`$ngDeclareDirective({version: 1, ngImport: core})`,
|
||||
].join('\n'),
|
||||
{
|
||||
plugins: [createEs2015LinkerPlugin()],
|
||||
filename: '/test.js',
|
||||
parserOpts: {sourceType: 'unambiguous'},
|
||||
generatorOpts: {compact: true},
|
||||
});
|
||||
expect(result!.code)
|
||||
.toEqual(
|
||||
'import*as core from\'some-module\';import{id}from\'other-module\';const _c0=[1];const _c1=[2];const _c2=[3];"REPLACEMENT";"REPLACEMENT";"REPLACEMENT";');
|
||||
});
|
||||
|
||||
it('should return a Babel plugin that adds shared statements at the start of the program if it is an ECMAScript Module and there are no imports',
|
||||
() => {
|
||||
spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT'));
|
||||
const result = transformSync(
|
||||
[
|
||||
'var core;',
|
||||
`$ngDeclareDirective({version: 1, ngImport: core})`,
|
||||
`$ngDeclareDirective({version: 1, ngImport: core})`,
|
||||
`$ngDeclareDirective({version: 1, ngImport: core})`,
|
||||
].join('\n'),
|
||||
{
|
||||
plugins: [createEs2015LinkerPlugin()],
|
||||
filename: '/test.js',
|
||||
// We declare the file as a module because this cannot be inferred from the source
|
||||
parserOpts: {sourceType: 'module'},
|
||||
generatorOpts: {compact: true},
|
||||
});
|
||||
expect(result!.code)
|
||||
.toEqual(
|
||||
'const _c0=[1];const _c1=[2];const _c2=[3];var core;"REPLACEMENT";"REPLACEMENT";"REPLACEMENT";');
|
||||
});
|
||||
|
||||
it('should return a Babel plugin that adds shared statements at the start of the function body if the ngImport is from a function parameter',
|
||||
() => {
|
||||
spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT'));
|
||||
const result = transformSync(
|
||||
[
|
||||
'function run(core) {', ` $ngDeclareDirective({version: 1, ngImport: core})`,
|
||||
` $ngDeclareDirective({version: 1, ngImport: core})`,
|
||||
` $ngDeclareDirective({version: 1, ngImport: core})`, '}'
|
||||
].join('\n'),
|
||||
{
|
||||
plugins: [createEs2015LinkerPlugin()],
|
||||
filename: '/test.js',
|
||||
parserOpts: {sourceType: 'unambiguous'},
|
||||
generatorOpts: {compact: true},
|
||||
});
|
||||
expect(result!.code)
|
||||
.toEqual(
|
||||
'function run(core){const _c0=[1];const _c1=[2];const _c2=[3];"REPLACEMENT";"REPLACEMENT";"REPLACEMENT";}');
|
||||
});
|
||||
|
||||
it('should return a Babel plugin that adds shared statements into an IIFE if no scope could not be derived for the ngImport',
|
||||
() => {
|
||||
spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT'));
|
||||
const result = transformSync(
|
||||
[
|
||||
'function run() {',
|
||||
` $ngDeclareDirective({version: 1, ngImport: core})`,
|
||||
` $ngDeclareDirective({version: 1, ngImport: core})`,
|
||||
` $ngDeclareDirective({version: 1, ngImport: core})`,
|
||||
'}',
|
||||
].join('\n'),
|
||||
{
|
||||
plugins: [createEs2015LinkerPlugin()],
|
||||
filename: '/test.js',
|
||||
parserOpts: {sourceType: 'unambiguous'},
|
||||
generatorOpts: {compact: true},
|
||||
});
|
||||
expect(result!.code).toEqual([
|
||||
`function run(){`,
|
||||
`(function(){const _c0=[1];return"REPLACEMENT";})();`,
|
||||
`(function(){const _c0=[2];return"REPLACEMENT";})();`,
|
||||
`(function(){const _c0=[3];return"REPLACEMENT";})();`,
|
||||
`}`,
|
||||
].join(''));
|
||||
});
|
||||
|
||||
it('should still execute other plugins that match AST nodes inside the result of the replacement',
|
||||
() => {
|
||||
spyOnLinkPartialDeclarationWithConstants(o.fn([], [], null, null, 'FOO'));
|
||||
const result = transformSync(
|
||||
[
|
||||
`$ngDeclareDirective({version: 1, ngImport: core}); FOO;`,
|
||||
].join('\n'),
|
||||
{
|
||||
plugins: [
|
||||
createEs2015LinkerPlugin(),
|
||||
createIdentifierMapperPlugin('FOO', 'BAR'),
|
||||
createIdentifierMapperPlugin('_c0', 'x1'),
|
||||
],
|
||||
filename: '/test.js',
|
||||
parserOpts: {sourceType: 'module'},
|
||||
generatorOpts: {compact: true},
|
||||
});
|
||||
expect(result!.code).toEqual([
|
||||
`(function(){const x1=[1];return function BAR(){};})();BAR;`,
|
||||
].join(''));
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Convert the arguments of the spied-on `calls` into a human readable array.
|
||||
*/
|
||||
function humanizeLinkerCalls(
|
||||
calls: jasmine.Calls<typeof FileLinker.prototype.linkPartialDeclaration>) {
|
||||
return calls.all().map(({args: [fn, args]}) => [fn, generate(args[0], {compact: true}).code]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Spy on the `PartialDirectiveLinkerVersion1.linkPartialDeclaration()` method, triggering
|
||||
* shared constants to be created.
|
||||
*/
|
||||
function spyOnLinkPartialDeclarationWithConstants(replacement: o.Expression) {
|
||||
let callCount = 0;
|
||||
spyOn(PartialDirectiveLinkerVersion1.prototype, 'linkPartialDeclaration')
|
||||
.and.callFake(((sourceUrl, code, constantPool) => {
|
||||
const constArray = o.literalArr([o.literal(++callCount)]);
|
||||
// We have to add the constant twice or it will not create a shared statement
|
||||
constantPool.getConstLiteral(constArray);
|
||||
constantPool.getConstLiteral(constArray);
|
||||
return replacement;
|
||||
}) as typeof PartialDirectiveLinkerVersion1.prototype.linkPartialDeclaration);
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple Babel plugin that will replace all identifiers that match `<src>` with identifiers
|
||||
* called `<dest>`.
|
||||
*/
|
||||
function createIdentifierMapperPlugin(src: string, dest: string): PluginObj {
|
||||
return {
|
||||
visitor: {
|
||||
Identifier(path: NodePath<t.Identifier>) {
|
||||
if (path.node.name === src) {
|
||||
path.replaceWith(t.identifier(dest));
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user