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:
Pete Bacon Darwin 2020-10-03 19:59:53 +01:00 committed by Andrew Kushnir
parent b304bd0535
commit 7e742aea7c
39 changed files with 2159 additions and 23 deletions

View File

@ -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",
],
)

View File

@ -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
```

View File

@ -0,0 +1,19 @@
load("//tools:defaults.bzl", "ts_library")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "babel",
srcs = ["index.ts"] + glob([
"src/**/*.ts",
]),
deps = [
"//packages/compiler",
"//packages/compiler-cli/linker",
"//packages/compiler-cli/src/ngtsc/translator",
"@npm//@babel/core",
"@npm//@babel/types",
"@npm//@types/babel__core",
"@npm//@types/babel__traverse",
],
)

View File

@ -0,0 +1,12 @@
# Angular linker - Babel plugin
This package contains a Babel plugin that can be used to find and link partially compiled declarations in library source code.
See the [linker package README](../README.md) for more information.
## Unit Testing
The unit tests are built and run using Bazel:
```bash
yarn bazel test //packages/compiler-cli/linker/babel/test
```

View File

@ -0,0 +1,8 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export {createEs2015LinkerPlugin} from './src/es2015_linker_plugin';

View File

@ -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.

View File

@ -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.

View File

@ -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;
}
}

View File

@ -0,0 +1,167 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {PluginObj} from '@babel/core';
import {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {FileLinker, isFatalLinkerError, LinkerEnvironment, LinkerOptions} from '../../../linker';
import {BabelAstFactory} from './ast/babel_ast_factory';
import {BabelAstHost} from './ast/babel_ast_host';
import {BabelDeclarationScope, ConstantScopePath} from './babel_declaration_scope';
/**
* Create a Babel plugin that visits the program, identifying and linking partial declarations.
*
* The plugin delegates most of its work to a generic `FileLinker` for each file (`t.Program` in
* Babel) that is visited.
*/
export function createEs2015LinkerPlugin(options: Partial<LinkerOptions> = {}): PluginObj {
let fileLinker: FileLinker<ConstantScopePath, t.Statement, t.Expression>|null = null;
const linkerEnvironment = LinkerEnvironment.create<t.Statement, t.Expression>(
new BabelAstHost(), new BabelAstFactory(), options);
return {
visitor: {
Program: {
/**
* Create a new `FileLinker` as we enter each file (`t.Program` in Babel).
*/
enter(path: NodePath<t.Program>): void {
assertNull(fileLinker);
const file: BabelFile = path.hub.file;
fileLinker = new FileLinker(linkerEnvironment, file.opts.filename ?? '', file.code);
},
/**
* On exiting the file, insert any shared constant statements that were generated during
* linking of the partial declarations.
*/
exit(): void {
assertNotNull(fileLinker);
for (const {constantScope, statements} of fileLinker.getConstantStatements()) {
insertStatements(constantScope, statements);
}
fileLinker = null;
}
},
/**
* Test each call expression to see if it is a partial declaration; it if is then replace it
* with the results of linking the declaration.
*/
CallExpression(call: NodePath<t.CallExpression>): void {
try {
assertNotNull(fileLinker);
const callee = call.node.callee;
if (!t.isExpression(callee)) {
return;
}
const calleeName = linkerEnvironment.host.getSymbolName(callee);
if (calleeName === null) {
return;
}
const args = call.node.arguments;
if (!fileLinker.isPartialDeclaration(calleeName) || !isExpressionArray(args)) {
return;
}
const declarationScope = new BabelDeclarationScope(call.scope);
const replacement = fileLinker.linkPartialDeclaration(calleeName, args, declarationScope);
call.replaceWith(replacement);
} catch (e) {
const node = isFatalLinkerError(e) ? e.node as t.Node : call.node;
throw buildCodeFrameError(call.hub.file, e.message, node);
}
}
}
};
}
/**
* Insert the `statements` at the location defined by `path`.
*
* The actual insertion strategy depends upon the type of the `path`.
*/
function insertStatements(path: ConstantScopePath, statements: t.Statement[]): void {
if (path.isFunction()) {
insertIntoFunction(path, statements);
} else if (path.isProgram()) {
insertIntoProgram(path, statements);
}
}
/**
* Insert the `statements` at the top of the body of the `fn` function.
*/
function insertIntoFunction(fn: NodePath<t.Function>, statements: t.Statement[]): void {
const body = fn.get('body');
body.unshiftContainer('body', statements);
}
/**
* Insert the `statements` at the top of the `program`, below any import statements.
*/
function insertIntoProgram(program: NodePath<t.Program>, statements: t.Statement[]): void {
const body = program.get('body');
const importStatements = body.filter(statement => statement.isImportDeclaration());
if (importStatements.length === 0) {
program.unshiftContainer('body', statements);
} else {
importStatements[importStatements.length - 1].insertAfter(statements);
}
}
/**
* Return true if all the `nodes` are Babel expressions.
*/
function isExpressionArray(nodes: t.Node[]): nodes is t.Expression[] {
return nodes.every(node => t.isExpression(node));
}
/**
* Assert that the given `obj` is `null`.
*/
function assertNull<T>(obj: T|null): asserts obj is null {
if (obj !== null) {
throw new Error('BUG - expected `obj` to be null');
}
}
/**
* Assert that the given `obj` is not `null`.
*/
function assertNotNull<T>(obj: T|null): asserts obj is T {
if (obj === null) {
throw new Error('BUG - expected `obj` not to be null');
}
}
/**
* Create a string representation of an error that includes the code frame of the `node`.
*/
function buildCodeFrameError(file: BabelFile, message: string, node: t.Node): string {
const filename = file.opts.filename || '(unknown file)';
const error = file.buildCodeFrameError(node, message);
return `${filename}: ${error.message}`;
}
/**
* This interface is making up for the fact that the Babel typings for `NodePath.hub.file` are
* lacking.
*/
interface BabelFile {
code: string;
opts: {filename?: string;};
buildCodeFrameError(node: t.Node, message: string): Error;
}

View File

@ -0,0 +1,36 @@
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "test_lib",
testonly = True,
srcs = glob([
"**/*.ts",
]),
deps = [
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/linker",
"//packages/compiler-cli/linker/babel",
"//packages/compiler-cli/src/ngtsc/translator",
"@npm//@babel/core",
"@npm//@babel/generator",
"@npm//@babel/parser",
"@npm//@babel/template",
"@npm//@babel/traverse",
"@npm//@babel/types",
"@npm//@types/babel__core",
"@npm//@types/babel__generator",
"@npm//@types/babel__template",
"@npm//@types/babel__traverse",
],
)
jasmine_node_test(
name = "test",
bootstrap = ["//tools/testing:node_no_angular_es5"],
deps = [
":test_lib",
],
)

View File

@ -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;

View File

@ -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;

View File

@ -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;
}

View File

@ -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));
}
}
},
};
}

View File

@ -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';

View File

@ -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);
}
}

View File

@ -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.

View File

@ -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;
}

View File

@ -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));
}
}

View File

@ -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');
}
}

View File

@ -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)!;
}
}

View File

@ -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});
}
}

View File

@ -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,
};

View File

@ -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.');
}
}

View File

@ -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.');
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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'`);
}
}
}

View File

@ -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",
],
)

View File

@ -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});
});
});
});

View File

@ -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"];']);
});
});
});

View File

@ -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();
});
});
});

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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.');
});
});
});

View File

@ -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,
};
}
}
});

View File

@ -8,7 +8,7 @@
import {LinkerImportGenerator} from '../src/linker_import_generator';
const ngImport = {
type: 'ngImport'
ngImport: true
};
describe('LinkerImportGenerator<TExpression>', () => {

View File

@ -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';