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:
parent
b304bd0535
commit
7e742aea7c
|
@ -8,11 +8,8 @@ ts_library(
|
|||
"src/**/*.ts",
|
||||
]),
|
||||
deps = [
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli/src/ngtsc/translator",
|
||||
"@npm//@babel/core",
|
||||
"@npm//@babel/types",
|
||||
"@npm//@types/babel__core",
|
||||
"@npm//@types/babel__traverse",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# Angular Linker
|
||||
|
||||
This package contains a `FileLinker` and supporting code to be able to "link" partial declarations of components, directives, etc in libraries to produce the full definitions.
|
||||
|
||||
The partial declaration format allows library packages to be published to npm without exposing the underlying Ivy instructions.
|
||||
|
||||
The tooling here allows application build tools (e.g. CLI) to produce fully compiled components, directives, etc at the point when the application is bundled.
|
||||
These linked files can be cached outside `node_modules` so it does not suffer from problems of mutating packages in `node_modules`.
|
||||
|
||||
Generally this tooling will be wrapped in a transpiler specific plugin, such as the provided [Babel plugin](./babel).
|
||||
|
||||
## Unit Testing
|
||||
|
||||
The unit tests are built and run using Bazel:
|
||||
|
||||
```bash
|
||||
yarn bazel test //packages/compiler-cli/linker/test
|
||||
```
|
|
@ -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",
|
||||
],
|
||||
)
|
|
@ -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
|
||||
```
|
|
@ -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';
|
|
@ -7,8 +7,8 @@
|
|||
*/
|
||||
import * as t from '@babel/types';
|
||||
|
||||
import {assert} from '../../../../linker';
|
||||
import {AstFactory, BinaryOperator, LeadingComment, ObjectLiteralProperty, SourceMapRange, TemplateLiteral, VariableDeclarationType} from '../../../../src/ngtsc/translator';
|
||||
import {assert} from '../utils';
|
||||
|
||||
/**
|
||||
* A Babel flavored implementation of the AstFactory.
|
|
@ -8,9 +8,7 @@
|
|||
|
||||
import * as t from '@babel/types';
|
||||
|
||||
import {FatalLinkerError} from '../../fatal_linker_error';
|
||||
import {AstHost, Range} from '../ast_host';
|
||||
import {assert} from '../utils';
|
||||
import {assert, AstHost, FatalLinkerError, Range} from '../../../../linker';
|
||||
|
||||
/**
|
||||
* This implementation of `AstHost` is able to get information from Babel AST nodes.
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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",
|
||||
],
|
||||
)
|
|
@ -10,7 +10,7 @@ import generate from '@babel/generator';
|
|||
import {expression, statement} from '@babel/template';
|
||||
import * as t from '@babel/types';
|
||||
|
||||
import {BabelAstFactory} from '../../../src/ast/babel/babel_ast_factory';
|
||||
import {BabelAstFactory} from '../../src/ast/babel_ast_factory';
|
||||
|
||||
describe('BabelAstFactory', () => {
|
||||
let factory: BabelAstFactory;
|
|
@ -8,7 +8,7 @@
|
|||
import * as t from '@babel/types';
|
||||
import template from '@babel/template';
|
||||
import {parse} from '@babel/parser';
|
||||
import {BabelAstHost} from '../../../src/ast/babel/babel_ast_host';
|
||||
import {BabelAstHost} from '../../src/ast/babel_ast_host';
|
||||
|
||||
describe('BabelAstHost', () => {
|
||||
let host: BabelAstHost;
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
|
@ -5,3 +5,10 @@
|
|||
* 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 {AstHost, Range} from './src/ast/ast_host';
|
||||
export {assert} from './src/ast/utils';
|
||||
export {FatalLinkerError, isFatalLinkerError} from './src/fatal_linker_error';
|
||||
export {DeclarationScope} from './src/file_linker/declaration_scope';
|
||||
export {FileLinker} from './src/file_linker/file_linker';
|
||||
export {LinkerEnvironment} from './src/file_linker/linker_environment';
|
||||
export {LinkerOptions} from './src/file_linker/linker_options';
|
||||
|
|
|
@ -0,0 +1,240 @@
|
|||
/**
|
||||
* @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';
|
||||
import {FatalLinkerError} from '../fatal_linker_error';
|
||||
import {AstHost, Range} from './ast_host';
|
||||
|
||||
/**
|
||||
* This helper class wraps an object expression along with an `AstHost` object, exposing helper
|
||||
* methods that make it easier to extract the properties of the object.
|
||||
*/
|
||||
export class AstObject<TExpression> {
|
||||
/**
|
||||
* Create a new `AstObject` from the given `expression` and `host`.
|
||||
*/
|
||||
static parse<TExpression>(expression: TExpression, host: AstHost<TExpression>):
|
||||
AstObject<TExpression> {
|
||||
const obj = host.parseObjectLiteral(expression);
|
||||
return new AstObject<TExpression>(expression, obj, host);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
readonly expression: TExpression, private obj: Map<string, TExpression>,
|
||||
private host: AstHost<TExpression>) {}
|
||||
|
||||
/**
|
||||
* Returns true if the object has a property called `propertyName`.
|
||||
*/
|
||||
has(propertyName: string): boolean {
|
||||
return this.obj.has(propertyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number value of the property called `propertyName`.
|
||||
*
|
||||
* Throws an error if there is no such property or the property is not a number.
|
||||
*/
|
||||
getNumber(propertyName: string): number {
|
||||
return this.host.parseNumericLiteral(this.getRequiredProperty(propertyName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the string value of the property called `propertyName`.
|
||||
*
|
||||
* Throws an error if there is no such property or the property is not a string.
|
||||
*/
|
||||
getString(propertyName: string): string {
|
||||
return this.host.parseStringLiteral(this.getRequiredProperty(propertyName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the boolean value of the property called `propertyName`.
|
||||
*
|
||||
* Throws an error if there is no such property or the property is not a boolean.
|
||||
*/
|
||||
getBoolean(propertyName: string): boolean {
|
||||
return this.host.parseBooleanLiteral(this.getRequiredProperty(propertyName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the nested `AstObject` parsed from the property called `propertyName`.
|
||||
*
|
||||
* Throws an error if there is no such property or the property is not an object.
|
||||
*/
|
||||
getObject(propertyName: string): AstObject<TExpression> {
|
||||
const expr = this.getRequiredProperty(propertyName);
|
||||
const obj = this.host.parseObjectLiteral(expr);
|
||||
return new AstObject(expr, obj, this.host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of `AstValue` objects parsed from the property called `propertyName`.
|
||||
*
|
||||
* Throws an error if there is no such property or the property is not an array.
|
||||
*/
|
||||
getArray(propertyName: string): AstValue<TExpression>[] {
|
||||
const arr = this.host.parseArrayLiteral(this.getRequiredProperty(propertyName));
|
||||
return arr.map(entry => new AstValue(entry, this.host));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a `WrappedNodeExpr` object that wraps the expression at the property called
|
||||
* `propertyName`.
|
||||
*
|
||||
* Throws an error if there is no such property.
|
||||
*/
|
||||
getOpaque(propertyName: string): o.WrappedNodeExpr<TExpression> {
|
||||
return new o.WrappedNodeExpr(this.getRequiredProperty(propertyName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw `TExpression` value of the property called `propertyName`.
|
||||
*
|
||||
* Throws an error if there is no such property.
|
||||
*/
|
||||
getNode(propertyName: string): TExpression {
|
||||
return this.getRequiredProperty(propertyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an `AstValue` that wraps the value of the property called `propertyName`.
|
||||
*
|
||||
* Throws an error if there is no such property.
|
||||
*/
|
||||
getValue(propertyName: string): AstValue<TExpression> {
|
||||
return new AstValue(this.getRequiredProperty(propertyName), this.host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the AstObject to a raw JavaScript object, mapping each property value (as an
|
||||
* `AstValue`) to the generic type (`T`) via the `mapper` function.
|
||||
*/
|
||||
toLiteral<T>(mapper: (value: AstValue<TExpression>) => T): {[key: string]: T} {
|
||||
const result: {[key: string]: T} = {};
|
||||
for (const [key, expression] of this.obj) {
|
||||
result[key] = mapper(new AstValue(expression, this.host));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private getRequiredProperty(propertyName: string): TExpression {
|
||||
if (!this.obj.has(propertyName)) {
|
||||
throw new FatalLinkerError(
|
||||
this.expression, `Expected property '${propertyName}' to be present.`);
|
||||
}
|
||||
return this.obj.get(propertyName)!;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper class wraps an `expression`, exposing methods that use the `host` to give
|
||||
* access to the underlying value of the wrapped expression.
|
||||
*/
|
||||
export class AstValue<TExpression> {
|
||||
constructor(private expression: TExpression, private host: AstHost<TExpression>) {}
|
||||
|
||||
/**
|
||||
* Is this value a number?
|
||||
*/
|
||||
isNumber(): boolean {
|
||||
return this.host.isNumericLiteral(this.expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the number from this value, or error if it is not a number.
|
||||
*/
|
||||
getNumber(): number {
|
||||
return this.host.parseNumericLiteral(this.expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this value a string?
|
||||
*/
|
||||
isString(): boolean {
|
||||
return this.host.isStringLiteral(this.expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the string from this value, or error if it is not a string.
|
||||
*/
|
||||
getString(): string {
|
||||
return this.host.parseStringLiteral(this.expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this value a boolean?
|
||||
*/
|
||||
isBoolean(): boolean {
|
||||
return this.host.isBooleanLiteral(this.expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the boolean from this value, or error if it is not a boolean.
|
||||
*/
|
||||
getBoolean(): boolean {
|
||||
return this.host.parseBooleanLiteral(this.expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this value an object literal?
|
||||
*/
|
||||
isObject(): boolean {
|
||||
return this.host.isObjectLiteral(this.expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse this value into an `AstObject`, or error if it is not an object literal.
|
||||
*/
|
||||
getObject(): AstObject<TExpression> {
|
||||
return AstObject.parse(this.expression, this.host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this value an array literal?
|
||||
*/
|
||||
isArray(): boolean {
|
||||
return this.host.isArrayLiteral(this.expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse this value into an array of `AstValue` objects, or error if it is not an array literal.
|
||||
*/
|
||||
getArray(): AstValue<TExpression>[] {
|
||||
const arr = this.host.parseArrayLiteral(this.expression);
|
||||
return arr.map(entry => new AstValue(entry, this.host));
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this value a function expression?
|
||||
*/
|
||||
isFunction(): boolean {
|
||||
return this.host.isFunctionExpression(this.expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the return value as an `AstValue` from this value as a function expression, or error if
|
||||
* it is not a function expression.
|
||||
*/
|
||||
getFunctionReturnValue(): AstValue<TExpression> {
|
||||
return new AstValue(this.host.parseReturnValue(this.expression), this.host);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the `TExpression` of this value wrapped in a `WrappedNodeExpr`.
|
||||
*/
|
||||
getOpaque(): o.WrappedNodeExpr<TExpression> {
|
||||
return new o.WrappedNodeExpr(this.expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the range of the location of this value in the original source.
|
||||
*/
|
||||
getRange(): Range {
|
||||
return this.host.getRange(this.expression);
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@
|
|||
* An unrecoverable error during linking.
|
||||
*/
|
||||
export class FatalLinkerError extends Error {
|
||||
private readonly type = 'FatalLinkerError';
|
||||
readonly type = 'FatalLinkerError';
|
||||
|
||||
/**
|
||||
* Create a new FatalLinkerError.
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
/**
|
||||
* This interface represents the lexical scope of a partial declaration in the source code.
|
||||
*
|
||||
* For example, if you had the following code:
|
||||
*
|
||||
* ```
|
||||
* function foo() {
|
||||
* function bar () {
|
||||
* $ngDeclareDirective({...});
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* The `DeclarationScope` of the `$ngDeclareDirective()` call is the body of the `bar()` function.
|
||||
*
|
||||
* The `FileLinker` uses this object to identify the lexical scope of any constant statements that
|
||||
* might be generated by the linking process (i.e. where the `ConstantPool` lives for a set of
|
||||
* partial linkers).
|
||||
*/
|
||||
export interface DeclarationScope<TSharedConstantScope, TExpression> {
|
||||
/**
|
||||
* Get a `TSharedConstantScope` object that can be used to reference the lexical scope where any
|
||||
* shared constant statements would be inserted.
|
||||
*
|
||||
* This object is generic because different AST implementations will need different
|
||||
* `TConstantScope` types to be able to insert shared constant statements. For example in Babel
|
||||
* this would be a `NodePath` object; in TS it would just be a `Node` object.
|
||||
*
|
||||
* If it is not possible to find such a shared scope, then constant statements will be wrapped up
|
||||
* with their generated linked definition expression, in the form of an IIFE.
|
||||
*
|
||||
* @param expression the expression that points to the Angular core framework import.
|
||||
* @returns a reference to a reference object for where the shared constant statements will be
|
||||
* inserted, or `null` if it is not possible to have a shared scope.
|
||||
*/
|
||||
getConstantScopeRef(expression: TExpression): TSharedConstantScope|null;
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* @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 {ConstantPool} from '@angular/compiler';
|
||||
import * as o from '@angular/compiler/src/output/output_ast';
|
||||
import {LinkerImportGenerator} from '../../linker_import_generator';
|
||||
import {LinkerEnvironment} from '../linker_environment';
|
||||
|
||||
/**
|
||||
* This class represents (from the point of view of the `FileLinker`) the scope in which
|
||||
* statements and expressions related to a linked partial declaration will be emitted.
|
||||
*
|
||||
* It holds a copy of a `ConstantPool` that is used to capture any constant statements that need to
|
||||
* be emitted in this context.
|
||||
*
|
||||
* This implementation will emit the definition and the constant statements separately.
|
||||
*/
|
||||
export class EmitScope<TStatement, TExpression> {
|
||||
readonly constantPool = new ConstantPool();
|
||||
|
||||
constructor(
|
||||
protected readonly ngImport: TExpression,
|
||||
protected readonly linkerEnvironment: LinkerEnvironment<TStatement, TExpression>) {}
|
||||
|
||||
/**
|
||||
* Translate the given Output AST definition expression into a generic `TExpression`.
|
||||
*
|
||||
* Use a `LinkerImportGenerator` to handle any imports in the definition.
|
||||
*/
|
||||
translateDefinition(definition: o.Expression): TExpression {
|
||||
return this.linkerEnvironment.translator.translateExpression(
|
||||
definition, new LinkerImportGenerator(this.ngImport));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return any constant statements that are shared between all uses of this `EmitScope`.
|
||||
*/
|
||||
getConstantStatements(): TStatement[] {
|
||||
const {translator} = this.linkerEnvironment;
|
||||
const importGenerator = new LinkerImportGenerator(this.ngImport);
|
||||
return this.constantPool.statements.map(
|
||||
statement => translator.translateStatement(statement, importGenerator));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* @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 {EmitScope} from './emit_scope';
|
||||
|
||||
/**
|
||||
* This class is a specialization of the `EmitScope` class that is designed for the situation where
|
||||
* there is no clear shared scope for constant statements. In this case they are bundled with the
|
||||
* translated definition inside an IIFE.
|
||||
*/
|
||||
export class IifeEmitScope<TStatement, TExpression> extends EmitScope<TStatement, TExpression> {
|
||||
/**
|
||||
* Translate the given Output AST definition expression into a generic `TExpression`.
|
||||
*
|
||||
* Wraps the output from `EmitScope.translateDefinition()` and `EmitScope.getConstantStatements()`
|
||||
* in an IIFE.
|
||||
*/
|
||||
translateDefinition(definition: o.Expression): TExpression {
|
||||
const {factory} = this.linkerEnvironment;
|
||||
const constantStatements = super.getConstantStatements();
|
||||
|
||||
const returnStatement = factory.createReturnStatement(super.translateDefinition(definition));
|
||||
const body = factory.createBlock([...constantStatements, returnStatement]);
|
||||
const fn = factory.createFunctionExpression(/* name */ null, /* args */[], body);
|
||||
return factory.createCallExpression(fn, /* args */[], /* pure */ false);
|
||||
}
|
||||
|
||||
/**
|
||||
* It is not valid to call this method, since there will be no shared constant statements - they
|
||||
* are already emitted in the IIFE alongside the translated definition.
|
||||
*/
|
||||
getConstantStatements(): TStatement[] {
|
||||
throw new Error('BUG - IifeEmitScope should not expose any constant statements');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
/**
|
||||
* @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 {AstObject} from '../ast/ast_value';
|
||||
import {DeclarationScope} from './declaration_scope';
|
||||
import {EmitScope} from './emit_scopes/emit_scope';
|
||||
import {IifeEmitScope} from './emit_scopes/iife_emit_scope';
|
||||
import {LinkerEnvironment} from './linker_environment';
|
||||
import {PartialLinkerSelector} from './partial_linkers/partial_linker_selector';
|
||||
|
||||
export const NO_STATEMENTS: Readonly<any[]> = [] as const;
|
||||
|
||||
/**
|
||||
* This class is responsible for linking all the partial declarations found in a single file.
|
||||
*/
|
||||
export class FileLinker<TConstantScope, TStatement, TExpression> {
|
||||
private linkerSelector = new PartialLinkerSelector<TStatement, TExpression>();
|
||||
private emitScopes = new Map<TConstantScope, EmitScope<TStatement, TExpression>>();
|
||||
|
||||
constructor(
|
||||
private linkerEnvironment: LinkerEnvironment<TStatement, TExpression>,
|
||||
private sourceUrl: string, readonly code: string) {}
|
||||
|
||||
/**
|
||||
* Return true if the given callee name matches a partial declaration that can be linked.
|
||||
*/
|
||||
isPartialDeclaration(calleeName: string): boolean {
|
||||
return this.linkerSelector.supportsDeclaration(calleeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Link the metadata extracted from the args of a call to a partial declaration function.
|
||||
*
|
||||
* The `declarationScope` is used to determine the scope and strategy of emission of the linked
|
||||
* definition and any shared constant statements.
|
||||
*
|
||||
* @param declarationFn the name of the function used to declare the partial declaration - e.g.
|
||||
* `$ngDeclareDirective`.
|
||||
* @param args the arguments passed to the declaration function.
|
||||
* @param declarationScope the scope that contains this call to the declaration function.
|
||||
*/
|
||||
linkPartialDeclaration(
|
||||
declarationFn: string, args: TExpression[],
|
||||
declarationScope: DeclarationScope<TConstantScope, TExpression>): TExpression {
|
||||
if (args.length !== 1) {
|
||||
throw new Error(
|
||||
`Invalid function call: It should have only a single object literal argument, but contained ${
|
||||
args.length}.`);
|
||||
}
|
||||
|
||||
const metaObj = AstObject.parse(args[0], this.linkerEnvironment.host);
|
||||
const ngImport = metaObj.getNode('ngImport');
|
||||
const emitScope = this.getEmitScope(ngImport, declarationScope);
|
||||
|
||||
const version = metaObj.getNumber('version');
|
||||
const linker = this.linkerSelector.getLinker(declarationFn, version);
|
||||
const definition =
|
||||
linker.linkPartialDeclaration(this.sourceUrl, this.code, emitScope.constantPool, metaObj);
|
||||
|
||||
return emitScope.translateDefinition(definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the shared constant statements and their associated constant scope references, so
|
||||
* that they can be inserted into the source code.
|
||||
*/
|
||||
getConstantStatements(): {constantScope: TConstantScope, statements: TStatement[]}[] {
|
||||
const results: {constantScope: TConstantScope, statements: TStatement[]}[] = [];
|
||||
for (const [constantScope, emitScope] of this.emitScopes.entries()) {
|
||||
const statements = emitScope.getConstantStatements();
|
||||
results.push({constantScope, statements});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private getEmitScope(
|
||||
ngImport: TExpression, declarationScope: DeclarationScope<TConstantScope, TExpression>):
|
||||
EmitScope<TStatement, TExpression> {
|
||||
const constantScope = declarationScope.getConstantScopeRef(ngImport);
|
||||
if (constantScope === null) {
|
||||
// There is no constant scope so we will emit extra statements into the definition IIFE.
|
||||
return new IifeEmitScope(ngImport, this.linkerEnvironment);
|
||||
}
|
||||
|
||||
if (!this.emitScopes.has(constantScope)) {
|
||||
this.emitScopes.set(constantScope, new EmitScope(ngImport, this.linkerEnvironment));
|
||||
}
|
||||
return this.emitScopes.get(constantScope)!;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* @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 {AstFactory} from '@angular/compiler-cli/src/ngtsc/translator';
|
||||
|
||||
import {AstHost} from '../ast/ast_host';
|
||||
import {DEFAULT_LINKER_OPTIONS, LinkerOptions} from './linker_options';
|
||||
import {Translator} from './translator';
|
||||
|
||||
export class LinkerEnvironment<TStatement, TExpression> {
|
||||
readonly translator = new Translator<TStatement, TExpression>(this.factory);
|
||||
private constructor(
|
||||
readonly host: AstHost<TExpression>, readonly factory: AstFactory<TStatement, TExpression>,
|
||||
readonly options: LinkerOptions) {}
|
||||
|
||||
static create<TStatement, TExpression>(
|
||||
host: AstHost<TExpression>, factory: AstFactory<TStatement, TExpression>,
|
||||
options: Partial<LinkerOptions>): LinkerEnvironment<TStatement, TExpression> {
|
||||
return new LinkerEnvironment(host, factory, {...DEFAULT_LINKER_OPTIONS, ...options});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
/**
|
||||
* Options to configure the linking behavior.
|
||||
*/
|
||||
export interface LinkerOptions {
|
||||
/**
|
||||
* Whether to generate legacy i18n message ids.
|
||||
* The default is `true`.
|
||||
*/
|
||||
enableI18nLegacyMessageIdFormat: boolean;
|
||||
/**
|
||||
* Whether to convert all line-endings in ICU expressions to `\n` characters.
|
||||
* The default is `false`.
|
||||
*/
|
||||
i18nNormalizeLineEndingsInICUs: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default linker options to use if properties are not provided.
|
||||
*/
|
||||
export const DEFAULT_LINKER_OPTIONS: LinkerOptions = {
|
||||
enableI18nLegacyMessageIdFormat: true,
|
||||
i18nNormalizeLineEndingsInICUs: false,
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* @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 {ConstantPool} from '@angular/compiler';
|
||||
import * as o from '@angular/compiler/src/output/output_ast';
|
||||
|
||||
import {AstObject} from '../../ast/ast_value';
|
||||
|
||||
import {PartialLinker} from './partial_linker';
|
||||
|
||||
/**
|
||||
* A `PartialLinker` that is designed to process `$ngDeclareComponent()` call expressions.
|
||||
*/
|
||||
export class PartialComponentLinkerVersion1<TStatement, TExpression> implements
|
||||
PartialLinker<TStatement, TExpression> {
|
||||
linkPartialDeclaration(
|
||||
sourceUrl: string, code: string, constantPool: ConstantPool,
|
||||
metaObj: AstObject<TExpression>): o.Expression {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* @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 {ConstantPool} from '@angular/compiler';
|
||||
import * as o from '@angular/compiler/src/output/output_ast';
|
||||
|
||||
import {AstObject} from '../../ast/ast_value';
|
||||
|
||||
import {PartialLinker} from './partial_linker';
|
||||
|
||||
/**
|
||||
* A `PartialLinker` that is designed to process `$ngDeclareDirective()` call expressions.
|
||||
*/
|
||||
export class PartialDirectiveLinkerVersion1<TStatement, TExpression> implements
|
||||
PartialLinker<TStatement, TExpression> {
|
||||
linkPartialDeclaration(
|
||||
sourceUrl: string, code: string, constantPool: ConstantPool,
|
||||
metaObj: AstObject<TExpression>): o.Expression {
|
||||
throw new Error('Not implemented.');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* @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 {ConstantPool} from '@angular/compiler';
|
||||
import * as o from '@angular/compiler/src/output/output_ast';
|
||||
import {AstObject} from '../../ast/ast_value';
|
||||
|
||||
/**
|
||||
* An interface for classes that can link partial declarations into full definitions.
|
||||
*/
|
||||
export interface PartialLinker<TStatement, TExpression> {
|
||||
/**
|
||||
* Link the partial declaration `metaObj` information to generate a full definition expression.
|
||||
*/
|
||||
linkPartialDeclaration(
|
||||
sourceUrl: string, code: string, constantPool: ConstantPool,
|
||||
metaObj: AstObject<TExpression>): o.Expression;
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/**
|
||||
* @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 {PartialComponentLinkerVersion1} from './partial_component_linker_1';
|
||||
import {PartialDirectiveLinkerVersion1} from './partial_directive_linker_1';
|
||||
import {PartialLinker} from './partial_linker';
|
||||
|
||||
export class PartialLinkerSelector<TStatement, TExpression> {
|
||||
private linkers: Record<string, Record<number, PartialLinker<TStatement, TExpression>>> = {
|
||||
'$ngDeclareDirective': {
|
||||
1: new PartialDirectiveLinkerVersion1(),
|
||||
},
|
||||
'$ngDeclareComponent': {
|
||||
1: new PartialComponentLinkerVersion1(),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if there are `PartialLinker` classes that can handle functions with this name.
|
||||
*/
|
||||
supportsDeclaration(functionName: string): boolean {
|
||||
return this.linkers[functionName] !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the `PartialLinker` that can handle functions with the given name and version.
|
||||
* Throws an error if there is none.
|
||||
*/
|
||||
getLinker(functionName: string, version: number): PartialLinker<TStatement, TExpression> {
|
||||
const versions = this.linkers[functionName];
|
||||
if (versions === undefined) {
|
||||
throw new Error(`Unknown partial declaration function ${functionName}.`);
|
||||
}
|
||||
const linker = versions[version];
|
||||
if (linker === undefined) {
|
||||
throw new Error(`Unsupported partial declaration version ${version} for ${functionName}.`);
|
||||
}
|
||||
return linker;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* @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';
|
||||
import {AstFactory, Context, ExpressionTranslatorVisitor, ImportGenerator, TranslatorOptions} from '@angular/compiler-cli/src/ngtsc/translator';
|
||||
|
||||
/**
|
||||
* Generic translator helper class, which exposes methods for translating expressions and
|
||||
* statements.
|
||||
*/
|
||||
export class Translator<TStatement, TExpression> {
|
||||
constructor(private factory: AstFactory<TStatement, TExpression>) {}
|
||||
|
||||
/**
|
||||
* Translate the given output AST in the context of an expression.
|
||||
*/
|
||||
translateExpression(
|
||||
expression: o.Expression, imports: ImportGenerator<TExpression>,
|
||||
options: TranslatorOptions<TExpression> = {}): TExpression {
|
||||
return expression.visitExpression(
|
||||
new ExpressionTranslatorVisitor<TStatement, TExpression>(this.factory, imports, options),
|
||||
new Context(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate the given output AST in the context of a statement.
|
||||
*/
|
||||
translateStatement(
|
||||
statement: o.Statement, imports: ImportGenerator<TExpression>,
|
||||
options: TranslatorOptions<TExpression> = {}): TStatement {
|
||||
return statement.visitStatement(
|
||||
new ExpressionTranslatorVisitor<TStatement, TExpression>(this.factory, imports, options),
|
||||
new Context(true));
|
||||
}
|
||||
}
|
|
@ -5,8 +5,8 @@
|
|||
* 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 {ImportGenerator, NamedImport} from '../../src/ngtsc/translator';
|
||||
import {FatalLinkerError} from './fatal_linker_error';
|
||||
|
||||
/**
|
||||
* A class that is used to generate imports when translating from Angular Output AST to an AST to
|
||||
|
@ -31,7 +31,8 @@ export class LinkerImportGenerator<TExpression> implements ImportGenerator<TExpr
|
|||
|
||||
private assertModuleName(moduleName: string): void {
|
||||
if (moduleName !== '@angular/core') {
|
||||
throw new Error(`Unable to import from anything other than '@angular/core'`);
|
||||
throw new FatalLinkerError(
|
||||
this.ngImport, `Unable to import from anything other than '@angular/core'`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,14 +12,7 @@ ts_library(
|
|||
"//packages:types",
|
||||
"//packages/compiler",
|
||||
"//packages/compiler-cli/linker",
|
||||
"@npm//@babel/core",
|
||||
"@npm//@babel/generator",
|
||||
"@npm//@babel/parser",
|
||||
"@npm//@babel/template",
|
||||
"@npm//@babel/types",
|
||||
"@npm//@types/babel__core",
|
||||
"@npm//@types/babel__generator",
|
||||
"@npm//@types/babel__template",
|
||||
"//packages/compiler-cli/src/ngtsc/translator",
|
||||
"@npm//typescript",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -0,0 +1,318 @@
|
|||
/**
|
||||
* @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 {WrappedNodeExpr} from '@angular/compiler';
|
||||
import {TypeScriptAstFactory} from '@angular/compiler-cli/src/ngtsc/translator';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {AstObject, AstValue} from '../../src/ast/ast_value';
|
||||
import {TypeScriptAstHost} from '../../src/ast/typescript/typescript_ast_host';
|
||||
|
||||
const host = new TypeScriptAstHost();
|
||||
const factory = new TypeScriptAstFactory();
|
||||
const nestedObj = factory.createObjectLiteral([
|
||||
{propertyName: 'x', quoted: false, value: factory.createLiteral(42)},
|
||||
{propertyName: 'y', quoted: false, value: factory.createLiteral('X')},
|
||||
]);
|
||||
const nestedArray =
|
||||
factory.createArrayLiteral([factory.createLiteral(1), factory.createLiteral(2)]);
|
||||
const obj = AstObject.parse(
|
||||
factory.createObjectLiteral([
|
||||
{propertyName: 'a', quoted: false, value: factory.createLiteral(42)},
|
||||
{propertyName: 'b', quoted: false, value: factory.createLiteral('X')},
|
||||
{propertyName: 'c', quoted: false, value: factory.createLiteral(true)},
|
||||
{propertyName: 'd', quoted: false, value: nestedObj},
|
||||
{propertyName: 'e', quoted: false, value: nestedArray},
|
||||
]),
|
||||
host);
|
||||
|
||||
describe('AstObject', () => {
|
||||
describe('has()', () => {
|
||||
it('should return true if the property exists on the object', () => {
|
||||
expect(obj.has('a')).toBe(true);
|
||||
expect(obj.has('b')).toBe(true);
|
||||
expect(obj.has('z')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNumber()', () => {
|
||||
it('should return the number value of the property', () => {
|
||||
expect(obj.getNumber('a')).toEqual(42);
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not a number', () => {
|
||||
expect(() => obj.getNumber('b'))
|
||||
.toThrowError('Unsupported syntax, expected a numeric literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getString()', () => {
|
||||
it('should return the string value of the property', () => {
|
||||
expect(obj.getString('b')).toEqual('X');
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not a string', () => {
|
||||
expect(() => obj.getString('a'))
|
||||
.toThrowError('Unsupported syntax, expected a string literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBoolean()', () => {
|
||||
it('should return the boolean value of the property', () => {
|
||||
expect(obj.getBoolean('c')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not a boolean', () => {
|
||||
expect(() => obj.getBoolean('b'))
|
||||
.toThrowError('Unsupported syntax, expected a boolean literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getObject()', () => {
|
||||
it('should return an AstObject instance parsed from the value of the property', () => {
|
||||
expect(obj.getObject('d')).toEqual(AstObject.parse(nestedObj, host));
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not an object expression', () => {
|
||||
expect(() => obj.getObject('b'))
|
||||
.toThrowError('Unsupported syntax, expected an object literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getArray()', () => {
|
||||
it('should return an array of AstValue instances of parsed from the value of the property',
|
||||
() => {
|
||||
expect(obj.getArray('e')).toEqual([
|
||||
new AstValue(factory.createLiteral(1), host),
|
||||
new AstValue(factory.createLiteral(2), host)
|
||||
]);
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not an array of expressions', () => {
|
||||
expect(() => obj.getArray('b'))
|
||||
.toThrowError('Unsupported syntax, expected an array literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOpaque()', () => {
|
||||
it('should return the expression value of the property wrapped in a `WrappedNodeExpr`', () => {
|
||||
expect(obj.getOpaque('d')).toEqual(jasmine.any(WrappedNodeExpr));
|
||||
expect(obj.getOpaque('d').node).toEqual(obj.getNode('d'));
|
||||
});
|
||||
|
||||
it('should throw an error if the property does not exist', () => {
|
||||
expect(() => obj.getOpaque('x')).toThrowError('Expected property \'x\' to be present.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNode()', () => {
|
||||
it('should return the original expression value of the property', () => {
|
||||
expect(obj.getNode('a')).toEqual(factory.createLiteral(42));
|
||||
});
|
||||
|
||||
it('should throw an error if the property does not exist', () => {
|
||||
expect(() => obj.getNode('x')).toThrowError('Expected property \'x\' to be present.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValue()', () => {
|
||||
it('should return the expression value of the property wrapped in an `AstValue`', () => {
|
||||
expect(obj.getValue('a')).toEqual(jasmine.any(AstValue));
|
||||
expect(obj.getValue('a').getNumber()).toEqual(42);
|
||||
});
|
||||
|
||||
it('should throw an error if the property does not exist', () => {
|
||||
expect(() => obj.getValue('x')).toThrowError('Expected property \'x\' to be present.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toLiteral()', () => {
|
||||
it('should convert the AstObject to a raw object with each property mapped', () => {
|
||||
expect(obj.toLiteral(value => value.getOpaque())).toEqual({
|
||||
a: obj.getOpaque('a'),
|
||||
b: obj.getOpaque('b'),
|
||||
c: obj.getOpaque('c'),
|
||||
d: obj.getOpaque('d'),
|
||||
e: obj.getOpaque('e'),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AstValue', () => {
|
||||
describe('isNumber', () => {
|
||||
it('should return true if the value is a number', () => {
|
||||
expect(new AstValue(factory.createLiteral(42), host).isNumber()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false if the value is not a number', () => {
|
||||
expect(new AstValue(factory.createLiteral('a'), host).isNumber()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNumber', () => {
|
||||
it('should return the number value of the AstValue', () => {
|
||||
expect(new AstValue(factory.createLiteral(42), host).getNumber()).toEqual(42);
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not a number', () => {
|
||||
expect(() => new AstValue(factory.createLiteral('a'), host).getNumber())
|
||||
.toThrowError('Unsupported syntax, expected a numeric literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isString', () => {
|
||||
it('should return true if the value is a string', () => {
|
||||
expect(new AstValue(factory.createLiteral('a'), host).isString()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false if the value is not a string', () => {
|
||||
expect(new AstValue(factory.createLiteral(42), host).isString()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getString', () => {
|
||||
it('should return the string value of the AstValue', () => {
|
||||
expect(new AstValue(factory.createLiteral('X'), host).getString()).toEqual('X');
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not a string', () => {
|
||||
expect(() => new AstValue(factory.createLiteral(42), host).getString())
|
||||
.toThrowError('Unsupported syntax, expected a string literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isBoolean', () => {
|
||||
it('should return true if the value is a boolean', () => {
|
||||
expect(new AstValue(factory.createLiteral(true), host).isBoolean()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false if the value is not a boolean', () => {
|
||||
expect(new AstValue(factory.createLiteral(42), host).isBoolean()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBoolean', () => {
|
||||
it('should return the boolean value of the AstValue', () => {
|
||||
expect(new AstValue(factory.createLiteral(true), host).getBoolean()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not a boolean', () => {
|
||||
expect(() => new AstValue(factory.createLiteral(42), host).getBoolean())
|
||||
.toThrowError('Unsupported syntax, expected a boolean literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isObject', () => {
|
||||
it('should return true if the value is an object literal', () => {
|
||||
expect(new AstValue(nestedObj, host).isObject()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false if the value is not an object literal', () => {
|
||||
expect(new AstValue(factory.createLiteral(42), host).isObject()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getObject', () => {
|
||||
it('should return the AstObject value of the AstValue', () => {
|
||||
expect(new AstValue(nestedObj, host).getObject()).toEqual(AstObject.parse(nestedObj, host));
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not an object literal', () => {
|
||||
expect(() => new AstValue(factory.createLiteral(42), host).getObject())
|
||||
.toThrowError('Unsupported syntax, expected an object literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isArray', () => {
|
||||
it('should return true if the value is an array literal', () => {
|
||||
expect(new AstValue(nestedArray, host).isArray()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false if the value is not an object literal', () => {
|
||||
expect(new AstValue(factory.createLiteral(42), host).isArray()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getArray', () => {
|
||||
it('should return an array of AstValue objects from the AstValue', () => {
|
||||
expect(new AstValue(nestedArray, host).getArray()).toEqual([
|
||||
new AstValue(factory.createLiteral(1), host),
|
||||
new AstValue(factory.createLiteral(2), host),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not an array', () => {
|
||||
expect(() => new AstValue(factory.createLiteral(42), host).getArray())
|
||||
.toThrowError('Unsupported syntax, expected an array literal.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFunction', () => {
|
||||
it('should return true if the value is a function expression', () => {
|
||||
const funcExpr = factory.createFunctionExpression(
|
||||
'foo', [],
|
||||
factory.createBlock([factory.createReturnStatement(factory.createLiteral(42))]));
|
||||
expect(new AstValue(funcExpr, host).isFunction()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should return false if the value is not a function expression', () => {
|
||||
expect(new AstValue(factory.createLiteral(42), host).isFunction()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFunctionReturnValue', () => {
|
||||
it('should return the "return value" of the function expression', () => {
|
||||
const funcExpr = factory.createFunctionExpression(
|
||||
'foo', [],
|
||||
factory.createBlock([factory.createReturnStatement(factory.createLiteral(42))]));
|
||||
expect(new AstValue(funcExpr, host).getFunctionReturnValue())
|
||||
.toEqual(new AstValue(factory.createLiteral(42), host));
|
||||
});
|
||||
|
||||
it('should throw an error if the property is not a function expression', () => {
|
||||
expect(() => new AstValue(factory.createLiteral(42), host).getFunctionReturnValue())
|
||||
.toThrowError('Unsupported syntax, expected a function.');
|
||||
});
|
||||
|
||||
it('should throw an error if the property is a function expression with no return value',
|
||||
() => {
|
||||
const funcExpr = factory.createFunctionExpression(
|
||||
'foo', [], factory.createBlock([factory.createExpressionStatement(
|
||||
factory.createLiteral('do nothing'))]));
|
||||
expect(() => new AstValue(funcExpr, host).getFunctionReturnValue())
|
||||
.toThrowError(
|
||||
'Unsupported syntax, expected a function body with a single return statement.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOpaque()', () => {
|
||||
it('should return the value wrapped in a `WrappedNodeExpr`', () => {
|
||||
expect(new AstValue(factory.createLiteral(42), host).getOpaque())
|
||||
.toEqual(jasmine.any(WrappedNodeExpr));
|
||||
expect(new AstValue(factory.createLiteral(42), host).getOpaque().node)
|
||||
.toEqual(factory.createLiteral(42));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRange()', () => {
|
||||
it('should return the source range of the AST node', () => {
|
||||
const file = ts.createSourceFile(
|
||||
'test.ts', '// preamble\nx = \'moo\';', ts.ScriptTarget.ES2015,
|
||||
/* setParentNodes */ true);
|
||||
|
||||
// Grab the `'moo'` string literal from the generated AST
|
||||
const stmt = file.statements[0] as ts.ExpressionStatement;
|
||||
const mooString =
|
||||
(stmt.expression as ts.AssignmentExpression<ts.Token<ts.SyntaxKind.EqualsToken>>).right;
|
||||
|
||||
// Check that this string literal has the expected range.
|
||||
expect(new AstValue(mooString, host).getRange())
|
||||
.toEqual({startLine: 1, startCol: 4, startPos: 16, endPos: 21});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* @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 * as ts from 'typescript';
|
||||
|
||||
import {TypeScriptAstFactory} from '../../../../src/ngtsc/translator';
|
||||
import {TypeScriptAstHost} from '../../../src/ast/typescript/typescript_ast_host';
|
||||
import {EmitScope} from '../../../src/file_linker/emit_scopes/emit_scope';
|
||||
import {LinkerEnvironment} from '../../../src/file_linker/linker_environment';
|
||||
import {DEFAULT_LINKER_OPTIONS} from '../../../src/file_linker/linker_options';
|
||||
import {generate} from '../helpers';
|
||||
|
||||
describe('EmitScope', () => {
|
||||
describe('translateDefinition()', () => {
|
||||
it('should translate the given output AST into a TExpression', () => {
|
||||
const factory = new TypeScriptAstFactory();
|
||||
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
|
||||
new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS);
|
||||
const ngImport = factory.createIdentifier('core');
|
||||
const emitScope = new EmitScope<ts.Statement, ts.Expression>(ngImport, linkerEnvironment);
|
||||
|
||||
const def = emitScope.translateDefinition(o.fn([], [], null, null, 'foo'));
|
||||
expect(generate(def)).toEqual('function foo() { }');
|
||||
});
|
||||
|
||||
it('should use the `ngImport` idenfifier for imports when translating', () => {
|
||||
const factory = new TypeScriptAstFactory();
|
||||
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
|
||||
new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS);
|
||||
const ngImport = factory.createIdentifier('core');
|
||||
const emitScope = new EmitScope<ts.Statement, ts.Expression>(ngImport, linkerEnvironment);
|
||||
|
||||
const coreImportRef = new o.ExternalReference('@angular/core', 'foo');
|
||||
const def = emitScope.translateDefinition(o.importExpr(coreImportRef).callMethod('bar', []));
|
||||
expect(generate(def)).toEqual('core.foo.bar()');
|
||||
});
|
||||
|
||||
it('should not emit any shared constants in the replacement expression', () => {
|
||||
const factory = new TypeScriptAstFactory();
|
||||
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
|
||||
new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS);
|
||||
const ngImport = factory.createIdentifier('core');
|
||||
const emitScope = new EmitScope<ts.Statement, ts.Expression>(ngImport, linkerEnvironment);
|
||||
|
||||
const constArray = o.literalArr([o.literal('CONST')]);
|
||||
// We have to add the constant twice or it will not create a shared statement
|
||||
emitScope.constantPool.getConstLiteral(constArray);
|
||||
emitScope.constantPool.getConstLiteral(constArray);
|
||||
|
||||
const def = emitScope.translateDefinition(o.fn([], [], null, null, 'foo'));
|
||||
expect(generate(def)).toEqual('function foo() { }');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConstantStatements()', () => {
|
||||
it('should return any constant statements that were added to the `constantPool`', () => {
|
||||
const factory = new TypeScriptAstFactory();
|
||||
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
|
||||
new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS);
|
||||
const ngImport = factory.createIdentifier('core');
|
||||
const emitScope = new EmitScope<ts.Statement, ts.Expression>(ngImport, linkerEnvironment);
|
||||
|
||||
const constArray = o.literalArr([o.literal('CONST')]);
|
||||
// We have to add the constant twice or it will not create a shared statement
|
||||
emitScope.constantPool.getConstLiteral(constArray);
|
||||
emitScope.constantPool.getConstLiteral(constArray);
|
||||
|
||||
const statements = emitScope.getConstantStatements();
|
||||
expect(statements.map(generate)).toEqual(['const _c0 = ["CONST"];']);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* @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 * as ts from 'typescript';
|
||||
|
||||
import {TypeScriptAstFactory} from '../../../../src/ngtsc/translator';
|
||||
import {TypeScriptAstHost} from '../../../src/ast/typescript/typescript_ast_host';
|
||||
import {IifeEmitScope} from '../../../src/file_linker/emit_scopes/iife_emit_scope';
|
||||
import {LinkerEnvironment} from '../../../src/file_linker/linker_environment';
|
||||
import {DEFAULT_LINKER_OPTIONS} from '../../../src/file_linker/linker_options';
|
||||
import {generate} from '../helpers';
|
||||
|
||||
describe('IifeEmitScope', () => {
|
||||
describe('translateDefinition()', () => {
|
||||
it('should translate the given output AST into a TExpression, wrapped in an IIFE', () => {
|
||||
const factory = new TypeScriptAstFactory();
|
||||
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
|
||||
new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS);
|
||||
const ngImport = factory.createIdentifier('core');
|
||||
const emitScope = new IifeEmitScope<ts.Statement, ts.Expression>(ngImport, linkerEnvironment);
|
||||
|
||||
const def = emitScope.translateDefinition(o.fn([], [], null, null, 'foo'));
|
||||
expect(generate(def)).toEqual('function () { return function foo() { }; }()');
|
||||
});
|
||||
|
||||
it('should use the `ngImport` idenfifier for imports when translating', () => {
|
||||
const factory = new TypeScriptAstFactory();
|
||||
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
|
||||
new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS);
|
||||
const ngImport = factory.createIdentifier('core');
|
||||
const emitScope = new IifeEmitScope<ts.Statement, ts.Expression>(ngImport, linkerEnvironment);
|
||||
|
||||
const coreImportRef = new o.ExternalReference('@angular/core', 'foo');
|
||||
const def = emitScope.translateDefinition(o.importExpr(coreImportRef).callMethod('bar', []));
|
||||
expect(generate(def)).toEqual('function () { return core.foo.bar(); }()');
|
||||
});
|
||||
|
||||
it('should emit any shared constants in the replacement expression IIFE', () => {
|
||||
const factory = new TypeScriptAstFactory();
|
||||
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
|
||||
new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS);
|
||||
const ngImport = factory.createIdentifier('core');
|
||||
const emitScope = new IifeEmitScope<ts.Statement, ts.Expression>(ngImport, linkerEnvironment);
|
||||
|
||||
const constArray = o.literalArr([o.literal('CONST')]);
|
||||
// We have to add the constant twice or it will not create a shared statement
|
||||
emitScope.constantPool.getConstLiteral(constArray);
|
||||
emitScope.constantPool.getConstLiteral(constArray);
|
||||
|
||||
const def = emitScope.translateDefinition(o.fn([], [], null, null, 'foo'));
|
||||
expect(generate(def))
|
||||
.toEqual('function () { const _c0 = ["CONST"]; return function foo() { }; }()');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConstantStatements()', () => {
|
||||
it('should throw an error', () => {
|
||||
const factory = new TypeScriptAstFactory();
|
||||
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
|
||||
new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS);
|
||||
const ngImport = factory.createIdentifier('core');
|
||||
const emitScope = new IifeEmitScope<ts.Statement, ts.Expression>(ngImport, linkerEnvironment);
|
||||
expect(() => emitScope.getConstantStatements()).toThrowError();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,191 @@
|
|||
/**
|
||||
* @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 * as ts from 'typescript';
|
||||
|
||||
import {TypeScriptAstFactory} from '../../../src/ngtsc/translator';
|
||||
import {AstHost} from '../../src/ast/ast_host';
|
||||
import {TypeScriptAstHost} from '../../src/ast/typescript/typescript_ast_host';
|
||||
import {DeclarationScope} from '../../src/file_linker/declaration_scope';
|
||||
import {FileLinker} from '../../src/file_linker/file_linker';
|
||||
import {LinkerEnvironment} from '../../src/file_linker/linker_environment';
|
||||
import {DEFAULT_LINKER_OPTIONS} from '../../src/file_linker/linker_options';
|
||||
import {PartialDirectiveLinkerVersion1} from '../../src/file_linker/partial_linkers/partial_directive_linker_1';
|
||||
import {generate} from './helpers';
|
||||
|
||||
describe('FileLinker', () => {
|
||||
let factory: TypeScriptAstFactory;
|
||||
beforeEach(() => factory = new TypeScriptAstFactory());
|
||||
|
||||
describe('isPartialDeclaration()', () => {
|
||||
it('should return true if the callee is recognized', () => {
|
||||
const {fileLinker} = createFileLinker();
|
||||
expect(fileLinker.isPartialDeclaration('$ngDeclareDirective')).toBe(true);
|
||||
expect(fileLinker.isPartialDeclaration('$ngDeclareComponent')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the callee is not recognized', () => {
|
||||
const {fileLinker} = createFileLinker();
|
||||
expect(fileLinker.isPartialDeclaration('$foo')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('linkPartialDeclaration()', () => {
|
||||
it('should throw an error if the function name is not recognised', () => {
|
||||
const {fileLinker} = createFileLinker();
|
||||
const version = factory.createLiteral(1);
|
||||
const ngImport = factory.createIdentifier('core');
|
||||
const declarationArg = factory.createObjectLiteral([
|
||||
{propertyName: 'version', quoted: false, value: version},
|
||||
{propertyName: 'ngImport', quoted: false, value: ngImport},
|
||||
]);
|
||||
expect(
|
||||
() => fileLinker.linkPartialDeclaration(
|
||||
'foo', [declarationArg], new MockDeclarationScope()))
|
||||
.toThrowError('Unknown partial declaration function foo.');
|
||||
});
|
||||
|
||||
it('should throw an error if the metadata object does not have a `version` property', () => {
|
||||
const {fileLinker} = createFileLinker();
|
||||
const ngImport = factory.createIdentifier('core');
|
||||
const declarationArg = factory.createObjectLiteral([
|
||||
{propertyName: 'ngImport', quoted: false, value: ngImport},
|
||||
]);
|
||||
expect(
|
||||
() => fileLinker.linkPartialDeclaration(
|
||||
'$ngDeclareDirective', [declarationArg], new MockDeclarationScope()))
|
||||
.toThrowError(`Expected property 'version' to be present.`);
|
||||
});
|
||||
|
||||
it('should throw an error if the metadata object does not have a `ngImport` property', () => {
|
||||
const {fileLinker} = createFileLinker();
|
||||
const ngImport = factory.createIdentifier('core');
|
||||
const declarationArg = factory.createObjectLiteral([
|
||||
{propertyName: 'version', quoted: false, value: ngImport},
|
||||
]);
|
||||
expect(
|
||||
() => fileLinker.linkPartialDeclaration(
|
||||
'$ngDeclareDirective', [declarationArg], new MockDeclarationScope()))
|
||||
.toThrowError(`Expected property 'ngImport' to be present.`);
|
||||
});
|
||||
|
||||
it('should call `linkPartialDeclaration()` on the appropriate partial compiler', () => {
|
||||
const {fileLinker} = createFileLinker();
|
||||
const compileSpy = spyOn(PartialDirectiveLinkerVersion1.prototype, 'linkPartialDeclaration')
|
||||
.and.returnValue(o.literal('compilation result'));
|
||||
|
||||
const ngImport = factory.createIdentifier('core');
|
||||
const version = factory.createLiteral(1);
|
||||
const declarationArg = factory.createObjectLiteral([
|
||||
{propertyName: 'ngImport', quoted: false, value: ngImport},
|
||||
{propertyName: 'version', quoted: false, value: version},
|
||||
]);
|
||||
|
||||
const compilationResult = fileLinker.linkPartialDeclaration(
|
||||
'$ngDeclareDirective', [declarationArg], new MockDeclarationScope());
|
||||
|
||||
expect(compilationResult).toEqual(factory.createLiteral('compilation result'));
|
||||
expect(compileSpy).toHaveBeenCalled();
|
||||
expect(compileSpy.calls.mostRecent().args[3].getNode('ngImport')).toBe(ngImport);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConstantStatements()', () => {
|
||||
it('should capture shared constant values', () => {
|
||||
const {fileLinker} = createFileLinker();
|
||||
spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT'));
|
||||
|
||||
// Here we use the `core` idenfifier for `ngImport` to trigger the use of a shared scope for
|
||||
// constant statements.
|
||||
const declarationArg = factory.createObjectLiteral([
|
||||
{propertyName: 'ngImport', quoted: false, value: factory.createIdentifier('core')},
|
||||
{propertyName: 'version', quoted: false, value: factory.createLiteral(1)},
|
||||
]);
|
||||
|
||||
const replacement = fileLinker.linkPartialDeclaration(
|
||||
'$ngDeclareDirective', [declarationArg], new MockDeclarationScope());
|
||||
expect(generate(replacement)).toEqual('"REPLACEMENT"');
|
||||
|
||||
const results = fileLinker.getConstantStatements();
|
||||
expect(results.length).toEqual(1);
|
||||
const {constantScope, statements} = results[0];
|
||||
expect(constantScope).toBe(MockConstantScopeRef.singleton);
|
||||
expect(statements.map(generate)).toEqual(['const _c0 = [1];']);
|
||||
});
|
||||
|
||||
it('should be no shared constant statements to capture when they are emitted into the replacement IIFE',
|
||||
() => {
|
||||
const {fileLinker} = createFileLinker();
|
||||
spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT'));
|
||||
|
||||
// Here we use a string literal `"not-a-module"` for `ngImport` to cause constant
|
||||
// statements to be emitted in an IIFE rather than added to the shared constant scope.
|
||||
const declarationArg = factory.createObjectLiteral([
|
||||
{propertyName: 'ngImport', quoted: false, value: factory.createLiteral('not-a-module')},
|
||||
{propertyName: 'version', quoted: false, value: factory.createLiteral(1)},
|
||||
]);
|
||||
|
||||
const replacement = fileLinker.linkPartialDeclaration(
|
||||
'$ngDeclareDirective', [declarationArg], new MockDeclarationScope());
|
||||
expect(generate(replacement))
|
||||
.toEqual('function () { const _c0 = [1]; return "REPLACEMENT"; }()');
|
||||
|
||||
const results = fileLinker.getConstantStatements();
|
||||
expect(results.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
function createFileLinker(): {
|
||||
host: AstHost<ts.Expression>,
|
||||
fileLinker: FileLinker<MockConstantScopeRef, ts.Statement, ts.Expression>
|
||||
} {
|
||||
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
|
||||
new TypeScriptAstHost(), new TypeScriptAstFactory(), DEFAULT_LINKER_OPTIONS);
|
||||
const fileLinker = new FileLinker<MockConstantScopeRef, ts.Statement, ts.Expression>(
|
||||
linkerEnvironment, 'test.js', '// test code');
|
||||
return {host: linkerEnvironment.host, fileLinker};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* This mock implementation of `DeclarationScope` will return a singleton instance of
|
||||
* `MockConstantScopeRef` if the expression is an identifier, or `null` otherwise.
|
||||
*
|
||||
* This way we can simulate whether the constants will be shared or inlined into an IIFE.
|
||||
*/
|
||||
class MockDeclarationScope implements DeclarationScope<MockConstantScopeRef, ts.Expression> {
|
||||
getConstantScopeRef(expression: ts.Expression): MockConstantScopeRef|null {
|
||||
if (ts.isIdentifier(expression)) {
|
||||
return MockConstantScopeRef.singleton;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MockConstantScopeRef {
|
||||
private constructor() {}
|
||||
static singleton = new MockDeclarationScope();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
/**
|
||||
* @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 ts from 'typescript';
|
||||
|
||||
/**
|
||||
* A simple helper to render a TS Node as a string.
|
||||
*/
|
||||
export function generate(node: ts.Node): string {
|
||||
const printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed});
|
||||
const sf = ts.createSourceFile('test.ts', '', ts.ScriptTarget.ES2015, true);
|
||||
return printer.printNode(ts.EmitHint.Unspecified, node, sf);
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* @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 {PartialComponentLinkerVersion1} from '../../../src/file_linker/partial_linkers/partial_component_linker_1';
|
||||
import {PartialDirectiveLinkerVersion1} from '../../../src/file_linker/partial_linkers/partial_directive_linker_1';
|
||||
import {PartialLinkerSelector} from '../../../src/file_linker/partial_linkers/partial_linker_selector';
|
||||
|
||||
describe('PartialLinkerSelector', () => {
|
||||
describe('supportsDeclaration()', () => {
|
||||
it('should return true if there is at least one linker that matches the given function name',
|
||||
() => {
|
||||
const selector = new PartialLinkerSelector();
|
||||
expect(selector.supportsDeclaration('$ngDeclareDirective')).toBe(true);
|
||||
expect(selector.supportsDeclaration('$ngDeclareComponent')).toBe(true);
|
||||
expect(selector.supportsDeclaration('$foo')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLinker()', () => {
|
||||
it('should return the linker that matches the name and version number', () => {
|
||||
const selector = new PartialLinkerSelector();
|
||||
expect(selector.getLinker('$ngDeclareDirective', 1))
|
||||
.toBeInstanceOf(PartialDirectiveLinkerVersion1);
|
||||
expect(selector.getLinker('$ngDeclareComponent', 1))
|
||||
.toBeInstanceOf(PartialComponentLinkerVersion1);
|
||||
});
|
||||
|
||||
it('should throw an error if there is no linker that matches the given name or version', () => {
|
||||
const selector = new PartialLinkerSelector();
|
||||
expect(() => selector.getLinker('$foo', 1))
|
||||
.toThrowError('Unknown partial declaration function $foo.');
|
||||
expect(() => selector.getLinker('$ngDeclareDirective', 2))
|
||||
.toThrowError('Unsupported partial declaration version 2 for $ngDeclareDirective.');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* @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';
|
||||
import {ImportGenerator, NamedImport, TypeScriptAstFactory} from '@angular/compiler-cli/src/ngtsc/translator';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {Translator} from '../../src/file_linker/translator';
|
||||
import {generate} from './helpers';
|
||||
|
||||
describe('Translator', () => {
|
||||
let factory: TypeScriptAstFactory;
|
||||
beforeEach(() => factory = new TypeScriptAstFactory());
|
||||
|
||||
describe('translateExpression()', () => {
|
||||
it('should generate expression specific output', () => {
|
||||
const translator = new Translator<ts.Statement, ts.Expression>(factory);
|
||||
const outputAst = new o.WriteVarExpr('foo', new o.LiteralExpr(42));
|
||||
const translated = translator.translateExpression(outputAst, new MockImportGenerator());
|
||||
expect(generate(translated)).toEqual('(foo = 42)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('translateStatement()', () => {
|
||||
it('should generate statement specific output', () => {
|
||||
const translator = new Translator<ts.Statement, ts.Expression>(factory);
|
||||
const outputAst = new o.ExpressionStatement(new o.WriteVarExpr('foo', new o.LiteralExpr(42)));
|
||||
const translated = translator.translateStatement(outputAst, new MockImportGenerator());
|
||||
expect(generate(translated)).toEqual('foo = 42;');
|
||||
});
|
||||
});
|
||||
class MockImportGenerator implements ImportGenerator<ts.Expression> {
|
||||
generateNamespaceImport(moduleName: string): ts.Expression {
|
||||
return factory.createLiteral(moduleName);
|
||||
}
|
||||
generateNamedImport(moduleName: string, originalSymbol: string): NamedImport<ts.Expression> {
|
||||
return {
|
||||
moduleImport: factory.createLiteral(moduleName),
|
||||
symbol: originalSymbol,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
|
@ -8,7 +8,7 @@
|
|||
import {LinkerImportGenerator} from '../src/linker_import_generator';
|
||||
|
||||
const ngImport = {
|
||||
type: 'ngImport'
|
||||
ngImport: true
|
||||
};
|
||||
|
||||
describe('LinkerImportGenerator<TExpression>', () => {
|
||||
|
|
|
@ -8,8 +8,9 @@
|
|||
|
||||
export {AstFactory, BinaryOperator, LeadingComment, ObjectLiteralProperty, SourceMapLocation, SourceMapRange, TemplateElement, TemplateLiteral, UnaryOperator, VariableDeclarationType} from './src/api/ast_factory';
|
||||
export {Import, ImportGenerator, NamedImport} from './src/api/import_generator';
|
||||
export {Context} from './src/context';
|
||||
export {ImportManager} from './src/import_manager';
|
||||
export {RecordWrappedNodeExprFn} from './src/translator';
|
||||
export {ExpressionTranslatorVisitor, RecordWrappedNodeExprFn, TranslatorOptions} from './src/translator';
|
||||
export {translateType} from './src/type_translator';
|
||||
export {attachComments, TypeScriptAstFactory} from './src/typescript_ast_factory';
|
||||
export {translateExpression, translateStatement} from './src/typescript_translator';
|
||||
|
|
Loading…
Reference in New Issue