refactor(compiler-cli): support external template source-mapping when linking (#40237)

This commit changes the `PartialComponentLinker` to use the original source
of an external template when compiling, if available, to ensure that the
source-mapping of the final linked code is accurate.

If the linker is given a file-system and logger, then it will attempt
to compute the original source of external templates so that the final
linked code references the correct template source.

PR Close #40237
This commit is contained in:
Pete Bacon Darwin 2020-12-21 10:06:23 +00:00 committed by atscott
parent 0fd06e5ab8
commit 266cc9b162
24 changed files with 532 additions and 265 deletions

View File

@ -9,6 +9,9 @@ ts_library(
]),
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/logging",
"//packages/compiler-cli/src/ngtsc/sourcemaps",
"//packages/compiler-cli/src/ngtsc/translator",
"@npm//@types/semver",
"@npm//semver",

View File

@ -10,6 +10,8 @@ ts_library(
deps = [
"//packages/compiler",
"//packages/compiler-cli/linker",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/logging",
"//packages/compiler-cli/src/ngtsc/translator",
"@npm//@babel/core",
"@npm//@babel/types",

View File

@ -9,11 +9,13 @@ import {PluginObj} from '@babel/core';
import {NodePath} from '@babel/traverse';
import * as t from '@babel/types';
import {FileLinker, isFatalLinkerError, LinkerEnvironment, LinkerOptions} from '../../../linker';
import {FileLinker, isFatalLinkerError, LinkerEnvironment} from '../../../linker';
import {BabelAstFactory} from './ast/babel_ast_factory';
import {BabelAstHost} from './ast/babel_ast_host';
import {BabelDeclarationScope, ConstantScopePath} from './babel_declaration_scope';
import {LinkerPluginOptions} from './linker_plugin_options';
/**
* Create a Babel plugin that visits the program, identifying and linking partial declarations.
@ -21,12 +23,10 @@ import {BabelDeclarationScope, ConstantScopePath} from './babel_declaration_scop
* 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 {
export function createEs2015LinkerPlugin({fileSystem, logger, ...options}: LinkerPluginOptions):
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: {
@ -36,8 +36,19 @@ export function createEs2015LinkerPlugin(options: Partial<LinkerOptions> = {}):
*/
enter(path: NodePath<t.Program>): void {
assertNull(fileLinker);
// Babel can be configured with a `filename` or `relativeFilename` (or both, or neither) -
// possibly relative to the optional `cwd` path.
const file: BabelFile = path.hub.file;
fileLinker = new FileLinker(linkerEnvironment, file.opts.filename ?? '', file.code);
const filename = file.opts.filename ?? file.opts.filenameRelative;
if (!filename) {
throw new Error(
'No filename (nor filenameRelative) provided by Babel. This is required for the linking of partially compiled directives and components.');
}
const sourceUrl = fileSystem.resolve(file.opts.cwd ?? '.', filename);
const linkerEnvironment = LinkerEnvironment.create<t.Statement, t.Expression>(
fileSystem, logger, new BabelAstHost(), new BabelAstFactory(sourceUrl), options);
fileLinker = new FileLinker(linkerEnvironment, sourceUrl, file.code);
},
/**
@ -66,11 +77,7 @@ export function createEs2015LinkerPlugin(options: Partial<LinkerOptions> = {}):
}
try {
const callee = call.node.callee;
if (!t.isExpression(callee)) {
return;
}
const calleeName = linkerEnvironment.host.getSymbolName(callee);
const calleeName = getCalleeName(call);
if (calleeName === null) {
return;
}
@ -126,6 +133,17 @@ function insertIntoProgram(program: NodePath<t.Program>, statements: t.Statement
}
}
function getCalleeName(call: NodePath<t.CallExpression>): string|null {
const callee = call.node.callee;
if (t.isIdentifier(callee)) {
return callee.name;
} else if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
return callee.property.name;
} else {
return null;
}
}
/**
* Return true if all the `nodes` are Babel expressions.
*/
@ -166,7 +184,11 @@ function buildCodeFrameError(file: BabelFile, message: string, node: t.Node): st
*/
interface BabelFile {
code: string;
opts: {filename?: string;};
opts: {
filename?: string,
filenameRelative?: string,
cwd?: string,
};
buildCodeFrameError(node: t.Node, message: string): Error;
}

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 {LinkerOptions} from '../..';
import {FileSystem} from '../../../src/ngtsc/file_system';
import {Logger} from '../../../src/ngtsc/logging';
export interface LinkerPluginOptions extends Partial<LinkerOptions> {
/**
* File-system, used to load up the input source-map and content.
*/
fileSystem: FileSystem;
/**
* Logger used by the linker.
*/
logger: Logger;
}

View File

@ -13,6 +13,8 @@ ts_library(
"//packages/compiler",
"//packages/compiler-cli/linker",
"//packages/compiler-cli/linker/babel",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/logging/testing",
"//packages/compiler-cli/src/ngtsc/translator",
"@npm//@babel/core",
"@npm//@babel/generator",

View File

@ -11,13 +11,17 @@ import generate from '@babel/generator';
import * as t from '@babel/types';
import {FileLinker} from '../../../linker';
import {MockFileSystemNative} from '../../../src/ngtsc/file_system/testing';
import {MockLogger} from '../../../src/ngtsc/logging/testing';
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();
const fileSystem = new MockFileSystemNative();
const logger = new MockLogger();
const plugin = createEs2015LinkerPlugin({fileSystem, logger});
expect(plugin.visitor).toEqual({
Program: {
enter: jasmine.any(Function),
@ -31,13 +35,16 @@ describe('createEs2015LinkerPlugin()', () => {
() => {
const isPartialDeclarationSpy = spyOn(FileLinker.prototype, 'isPartialDeclaration');
const fileSystem = new MockFileSystemNative();
const logger = new MockLogger();
const plugin = createEs2015LinkerPlugin({fileSystem, logger});
transformSync(
[
'var core;', `fn1()`, 'fn2({prop: () => fn3({})});', `x.method(() => fn4());`,
'spread(...x);'
].join('\n'),
{
plugins: [createEs2015LinkerPlugin()],
plugins: [plugin],
filename: '/test.js',
parserOpts: {sourceType: 'unambiguous'},
});
@ -55,6 +62,9 @@ describe('createEs2015LinkerPlugin()', () => {
() => {
const linkSpy = spyOn(FileLinker.prototype, 'linkPartialDeclaration')
.and.returnValue(t.identifier('REPLACEMENT'));
const fileSystem = new MockFileSystemNative();
const logger = new MockLogger();
const plugin = createEs2015LinkerPlugin({fileSystem, logger});
transformSync(
[
@ -65,7 +75,7 @@ describe('createEs2015LinkerPlugin()', () => {
'spread(...x);',
].join('\n'),
{
plugins: [createEs2015LinkerPlugin()],
plugins: [createEs2015LinkerPlugin({fileSystem, logger})],
filename: '/test.js',
parserOpts: {sourceType: 'unambiguous'},
});
@ -86,6 +96,9 @@ describe('createEs2015LinkerPlugin()', () => {
let replaceCount = 0;
spyOn(FileLinker.prototype, 'linkPartialDeclaration')
.and.callFake(() => t.identifier('REPLACEMENT_' + ++replaceCount));
const fileSystem = new MockFileSystemNative();
const logger = new MockLogger();
const plugin = createEs2015LinkerPlugin({fileSystem, logger});
const result = transformSync(
[
'var core;',
@ -95,7 +108,7 @@ describe('createEs2015LinkerPlugin()', () => {
'spread(...x);',
].join('\n'),
{
plugins: [createEs2015LinkerPlugin()],
plugins: [createEs2015LinkerPlugin({fileSystem, logger})],
filename: '/test.js',
parserOpts: {sourceType: 'unambiguous'},
generatorOpts: {compact: true},
@ -105,6 +118,9 @@ describe('createEs2015LinkerPlugin()', () => {
it('should return a Babel plugin that adds shared statements after any imports', () => {
spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT'));
const fileSystem = new MockFileSystemNative();
const logger = new MockLogger();
const plugin = createEs2015LinkerPlugin({fileSystem, logger});
const result = transformSync(
[
'import * as core from \'some-module\';',
@ -114,7 +130,7 @@ describe('createEs2015LinkerPlugin()', () => {
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
].join('\n'),
{
plugins: [createEs2015LinkerPlugin()],
plugins: [createEs2015LinkerPlugin({fileSystem, logger})],
filename: '/test.js',
parserOpts: {sourceType: 'unambiguous'},
generatorOpts: {compact: true},
@ -127,6 +143,9 @@ describe('createEs2015LinkerPlugin()', () => {
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 fileSystem = new MockFileSystemNative();
const logger = new MockLogger();
const plugin = createEs2015LinkerPlugin({fileSystem, logger});
const result = transformSync(
[
'var core;',
@ -135,7 +154,7 @@ describe('createEs2015LinkerPlugin()', () => {
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
].join('\n'),
{
plugins: [createEs2015LinkerPlugin()],
plugins: [createEs2015LinkerPlugin({fileSystem, logger})],
filename: '/test.js',
// We declare the file as a module because this cannot be inferred from the source
parserOpts: {sourceType: 'module'},
@ -149,6 +168,9 @@ describe('createEs2015LinkerPlugin()', () => {
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 fileSystem = new MockFileSystemNative();
const logger = new MockLogger();
const plugin = createEs2015LinkerPlugin({fileSystem, logger});
const result = transformSync(
[
'function run(core) {',
@ -157,7 +179,7 @@ describe('createEs2015LinkerPlugin()', () => {
` ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`, '}'
].join('\n'),
{
plugins: [createEs2015LinkerPlugin()],
plugins: [createEs2015LinkerPlugin({fileSystem, logger})],
filename: '/test.js',
parserOpts: {sourceType: 'unambiguous'},
generatorOpts: {compact: true},
@ -170,6 +192,9 @@ describe('createEs2015LinkerPlugin()', () => {
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 fileSystem = new MockFileSystemNative();
const logger = new MockLogger();
const plugin = createEs2015LinkerPlugin({fileSystem, logger});
const result = transformSync(
[
'function run() {',
@ -179,7 +204,7 @@ describe('createEs2015LinkerPlugin()', () => {
'}',
].join('\n'),
{
plugins: [createEs2015LinkerPlugin()],
plugins: [createEs2015LinkerPlugin({fileSystem, logger})],
filename: '/test.js',
parserOpts: {sourceType: 'unambiguous'},
generatorOpts: {compact: true},
@ -196,13 +221,16 @@ describe('createEs2015LinkerPlugin()', () => {
it('should still execute other plugins that match AST nodes inside the result of the replacement',
() => {
spyOnLinkPartialDeclarationWithConstants(o.fn([], [], null, null, 'FOO'));
const fileSystem = new MockFileSystemNative();
const logger = new MockLogger();
const plugin = createEs2015LinkerPlugin({fileSystem, logger});
const result = transformSync(
[
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core}); FOO;`,
].join('\n'),
{
plugins: [
createEs2015LinkerPlugin(),
createEs2015LinkerPlugin({fileSystem, logger}),
createIdentifierMapperPlugin('FOO', 'BAR'),
createIdentifierMapperPlugin('_c0', 'x1'),
],
@ -217,7 +245,7 @@ describe('createEs2015LinkerPlugin()', () => {
it('should not process call expressions within inserted functions', () => {
spyOn(PartialDirectiveLinkerVersion1.prototype, 'linkPartialDeclaration')
.and.callFake(((sourceUrl, code, constantPool) => {
.and.callFake((constantPool => {
// Insert a call expression into the constant pool. This is inserted into
// Babel's AST upon program exit, and will therefore be visited by Babel
// outside of an active linker context.
@ -233,13 +261,16 @@ describe('createEs2015LinkerPlugin()', () => {
const isPartialDeclarationSpy =
spyOn(FileLinker.prototype, 'isPartialDeclaration').and.callThrough();
const fileSystem = new MockFileSystemNative();
const logger = new MockLogger();
const plugin = createEs2015LinkerPlugin({fileSystem, logger});
const result = transformSync(
[
'import * as core from \'some-module\';',
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
].join('\n'),
{
plugins: [createEs2015LinkerPlugin()],
plugins: [createEs2015LinkerPlugin({fileSystem, logger})],
filename: '/test.js',
parserOpts: {sourceType: 'unambiguous'},
generatorOpts: {compact: true},
@ -266,7 +297,7 @@ function humanizeLinkerCalls(
function spyOnLinkPartialDeclarationWithConstants(replacement: o.Expression) {
let callCount = 0;
spyOn(PartialDirectiveLinkerVersion1.prototype, 'linkPartialDeclaration')
.and.callFake(((sourceUrl, code, constantPool) => {
.and.callFake((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);

View File

@ -8,7 +8,7 @@
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';
import {Translator} from '../translator';
/**
* This class represents (from the point of view of the `FileLinker`) the scope in which
@ -24,7 +24,7 @@ export class EmitScope<TStatement, TExpression> {
constructor(
protected readonly ngImport: TExpression,
protected readonly linkerEnvironment: LinkerEnvironment<TStatement, TExpression>) {}
protected readonly translator: Translator<TStatement, TExpression>) {}
/**
* Translate the given Output AST definition expression into a generic `TExpression`.
@ -32,7 +32,7 @@ export class EmitScope<TStatement, TExpression> {
* Use a `LinkerImportGenerator` to handle any imports in the definition.
*/
translateDefinition(definition: o.Expression): TExpression {
return this.linkerEnvironment.translator.translateExpression(
return this.translator.translateExpression(
definition, new LinkerImportGenerator(this.ngImport));
}
@ -40,9 +40,8 @@ export class EmitScope<TStatement, TExpression> {
* 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));
statement => this.translator.translateStatement(statement, importGenerator));
}
}

View File

@ -6,6 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as o from '@angular/compiler/src/output/output_ast';
import {AstFactory} from '../../../../src/ngtsc/translator';
import {Translator} from '../translator';
import {EmitScope} from './emit_scope';
/**
@ -14,6 +18,12 @@ import {EmitScope} from './emit_scope';
* translated definition inside an IIFE.
*/
export class IifeEmitScope<TStatement, TExpression> extends EmitScope<TStatement, TExpression> {
constructor(
ngImport: TExpression, translator: Translator<TStatement, TExpression>,
private readonly factory: AstFactory<TStatement, TExpression>) {
super(ngImport, translator);
}
/**
* Translate the given Output AST definition expression into a generic `TExpression`.
*
@ -21,13 +31,13 @@ export class IifeEmitScope<TStatement, TExpression> extends EmitScope<TStatement
* 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);
const returnStatement =
this.factory.createReturnStatement(super.translateDefinition(definition));
const body = this.factory.createBlock([...constantStatements, returnStatement]);
const fn = this.factory.createFunctionExpression(/* name */ null, /* args */[], body);
return this.factory.createCallExpression(fn, /* args */[], /* pure */ false);
}
/**

View File

@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {R3PartialDeclaration} from '@angular/compiler';
import {AbsoluteFsPath} from '../../../src/ngtsc/file_system';
import {AstObject} from '../ast/ast_value';
import {DeclarationScope} from './declaration_scope';
import {EmitScope} from './emit_scopes/emit_scope';
@ -19,12 +20,15 @@ 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<TExpression>(this.linkerEnvironment.options);
private linkerSelector: PartialLinkerSelector<TStatement, TExpression>;
private emitScopes = new Map<TConstantScope, EmitScope<TStatement, TExpression>>();
constructor(
private linkerEnvironment: LinkerEnvironment<TStatement, TExpression>,
private sourceUrl: string, readonly code: string) {}
sourceUrl: AbsoluteFsPath, code: string) {
this.linkerSelector =
new PartialLinkerSelector<TStatement, TExpression>(this.linkerEnvironment, sourceUrl, code);
}
/**
* Return true if the given callee name matches a partial declaration that can be linked.
@ -61,8 +65,7 @@ export class FileLinker<TConstantScope, TStatement, TExpression> {
const version = metaObj.getString('version');
const linker = this.linkerSelector.getLinker(declarationFn, version);
const definition =
linker.linkPartialDeclaration(this.sourceUrl, this.code, emitScope.constantPool, metaObj);
const definition = linker.linkPartialDeclaration(emitScope.constantPool, metaObj);
return emitScope.translateDefinition(definition);
}
@ -86,11 +89,13 @@ export class FileLinker<TConstantScope, 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);
return new IifeEmitScope(
ngImport, this.linkerEnvironment.translator, this.linkerEnvironment.factory);
}
if (!this.emitScopes.has(constantScope)) {
this.emitScopes.set(constantScope, new EmitScope(ngImport, this.linkerEnvironment));
this.emitScopes.set(
constantScope, new EmitScope(ngImport, this.linkerEnvironment.translator));
}
return this.emitScopes.get(constantScope)!;
}

View File

@ -0,0 +1,36 @@
/**
* @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 {AbsoluteFsPath} from '../../../src/ngtsc/file_system';
import {SourceFile, SourceFileLoader} from '../../../src/ngtsc/sourcemaps';
/**
* A function that will return a `SourceFile` object (or null) for the current file being linked.
*/
export type GetSourceFileFn = () => SourceFile|null;
/**
* Create a `GetSourceFileFn` that will return the `SourceFile` being linked or `null`, if not
* available.
*/
export function createGetSourceFile(
sourceUrl: AbsoluteFsPath, code: string, loader: SourceFileLoader|null): GetSourceFileFn {
if (loader === null) {
// No source-mapping so just return a function that always returns `null`.
return () => null;
} else {
// Source-mapping is available so return a function that will load (and cache) the `SourceFile`.
let sourceFile: SourceFile|null|undefined = undefined;
return () => {
if (sourceFile === undefined) {
sourceFile = loader.loadSourceFile(sourceUrl, code);
}
return sourceFile;
};
}
}

View File

@ -5,7 +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
*/
import {AstFactory} from '@angular/compiler-cli/src/ngtsc/translator';
import {FileSystem} from '../../../src/ngtsc/file_system';
import {Logger} from '../../../src/ngtsc/logging';
import {SourceFileLoader} from '../../../src/ngtsc/sourcemaps';
import {AstFactory} from '../../../src/ngtsc/translator';
import {AstHost} from '../ast/ast_host';
import {DEFAULT_LINKER_OPTIONS, LinkerOptions} from './linker_options';
@ -13,19 +16,24 @@ import {Translator} from './translator';
export class LinkerEnvironment<TStatement, TExpression> {
readonly translator = new Translator<TStatement, TExpression>(this.factory);
readonly sourceFileLoader =
this.options.sourceMapping ? new SourceFileLoader(this.fileSystem, this.logger, {}) : null;
private constructor(
readonly host: AstHost<TExpression>, readonly factory: AstFactory<TStatement, TExpression>,
readonly options: LinkerOptions) {}
readonly fileSystem: FileSystem, readonly logger: Logger, readonly host: AstHost<TExpression>,
readonly factory: AstFactory<TStatement, TExpression>, readonly options: LinkerOptions) {}
static create<TStatement, TExpression>(
host: AstHost<TExpression>, factory: AstFactory<TStatement, TExpression>,
fileSystem: FileSystem, logger: Logger, host: AstHost<TExpression>,
factory: AstFactory<TStatement, TExpression>,
options: Partial<LinkerOptions>): LinkerEnvironment<TStatement, TExpression> {
return new LinkerEnvironment(host, factory, {
return new LinkerEnvironment(fileSystem, logger, host, factory, {
enableI18nLegacyMessageIdFormat: options.enableI18nLegacyMessageIdFormat ??
DEFAULT_LINKER_OPTIONS.enableI18nLegacyMessageIdFormat,
i18nNormalizeLineEndingsInICUs: options.i18nNormalizeLineEndingsInICUs ??
DEFAULT_LINKER_OPTIONS.i18nNormalizeLineEndingsInICUs,
i18nUseExternalIds: options.i18nUseExternalIds ?? DEFAULT_LINKER_OPTIONS.i18nUseExternalIds,
sourceMapping: options.sourceMapping ?? DEFAULT_LINKER_OPTIONS.sourceMapping
});
}
}

View File

@ -27,6 +27,12 @@ export interface LinkerOptions {
* The default is `false`.
*/
i18nUseExternalIds: boolean;
/**
* Whether to use source-mapping to compute the original source for external templates.
* The default is `true`.
*/
sourceMapping: boolean;
}
/**
@ -36,4 +42,5 @@ export const DEFAULT_LINKER_OPTIONS: LinkerOptions = {
enableI18nLegacyMessageIdFormat: true,
i18nNormalizeLineEndingsInICUs: false,
i18nUseExternalIds: false,
sourceMapping: true,
};

View File

@ -9,10 +9,12 @@ import {compileComponentFromMetadata, ConstantPool, DeclarationListEmitMode, DEF
import {ChangeDetectionStrategy, ViewEncapsulation} from '@angular/compiler/src/core';
import * as o from '@angular/compiler/src/output/output_ast';
import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system';
import {Range} from '../../ast/ast_host';
import {AstObject, AstValue} from '../../ast/ast_value';
import {FatalLinkerError} from '../../fatal_linker_error';
import {LinkerOptions} from '../linker_options';
import {GetSourceFileFn} from '../get_source_file';
import {LinkerEnvironment} from '../linker_environment';
import {toR3DirectiveMeta} from './partial_directive_linker_1';
import {PartialLinker} from './partial_linker';
@ -20,116 +22,197 @@ import {PartialLinker} from './partial_linker';
/**
* A `PartialLinker` that is designed to process `ɵɵngDeclareComponent()` call expressions.
*/
export class PartialComponentLinkerVersion1<TExpression> implements PartialLinker<TExpression> {
constructor(private readonly options: LinkerOptions) {}
export class PartialComponentLinkerVersion1<TStatement, TExpression> implements
PartialLinker<TExpression> {
private readonly i18nNormalizeLineEndingsInICUs =
this.environment.options.i18nNormalizeLineEndingsInICUs;
private readonly enableI18nLegacyMessageIdFormat =
this.environment.options.enableI18nLegacyMessageIdFormat;
private readonly i18nUseExternalIds = this.environment.options.i18nUseExternalIds;
constructor(
private readonly environment: LinkerEnvironment<TStatement, TExpression>,
private readonly getSourceFile: GetSourceFileFn, private sourceUrl: AbsoluteFsPath,
private code: string) {}
linkPartialDeclaration(
sourceUrl: string, code: string, constantPool: ConstantPool,
constantPool: ConstantPool,
metaObj: AstObject<R3PartialDeclaration, TExpression>): o.Expression {
const meta = toR3ComponentMeta(metaObj, code, sourceUrl, this.options);
const meta = this.toR3ComponentMeta(metaObj);
const def = compileComponentFromMetadata(meta, constantPool, makeBindingParser());
return def.expression;
}
/**
* This function derives the `R3ComponentMetadata` from the provided AST object.
*/
private toR3ComponentMeta(metaObj: AstObject<R3DeclareComponentMetadata, TExpression>):
R3ComponentMetadata {
const interpolation = parseInterpolationConfig(metaObj);
const templateObj = metaObj.getObject('template');
const templateSource = templateObj.getValue('source');
const isInline = templateObj.getBoolean('isInline');
const templateInfo = this.getTemplateInfo(templateSource, isInline);
// We always normalize line endings if the template is inline.
const i18nNormalizeLineEndingsInICUs = isInline || this.i18nNormalizeLineEndingsInICUs;
const template = parseTemplate(templateInfo.code, templateInfo.sourceUrl, {
escapedString: templateInfo.isEscaped,
interpolationConfig: interpolation,
range: templateInfo.range,
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
preserveWhitespaces:
metaObj.has('preserveWhitespaces') ? metaObj.getBoolean('preserveWhitespaces') : false,
i18nNormalizeLineEndingsInICUs,
isInline,
});
if (template.errors !== null) {
const errors = template.errors.map(err => err.toString()).join('\n');
throw new FatalLinkerError(
templateSource.expression, `Errors found in the template:\n${errors}`);
}
let declarationListEmitMode = DeclarationListEmitMode.Direct;
let directives: R3UsedDirectiveMetadata[] = [];
if (metaObj.has('directives')) {
directives = metaObj.getArray('directives').map(directive => {
const directiveExpr = directive.getObject();
const type = directiveExpr.getValue('type');
const selector = directiveExpr.getString('selector');
let typeExpr = type.getOpaque();
const forwardRefType = extractForwardRef(type);
if (forwardRefType !== null) {
typeExpr = forwardRefType;
declarationListEmitMode = DeclarationListEmitMode.Closure;
}
return {
type: typeExpr,
selector: selector,
inputs: directiveExpr.has('inputs') ?
directiveExpr.getArray('inputs').map(input => input.getString()) :
[],
outputs: directiveExpr.has('outputs') ?
directiveExpr.getArray('outputs').map(input => input.getString()) :
[],
exportAs: directiveExpr.has('exportAs') ?
directiveExpr.getArray('exportAs').map(exportAs => exportAs.getString()) :
null,
};
});
}
let pipes = new Map<string, o.Expression>();
if (metaObj.has('pipes')) {
pipes = metaObj.getObject('pipes').toMap(pipe => {
const forwardRefType = extractForwardRef(pipe);
if (forwardRefType !== null) {
declarationListEmitMode = DeclarationListEmitMode.Closure;
return forwardRefType;
} else {
return pipe.getOpaque();
}
});
}
return {
...toR3DirectiveMeta(metaObj, this.code, this.sourceUrl),
viewProviders: metaObj.has('viewProviders') ? metaObj.getOpaque('viewProviders') : null,
template: {
nodes: template.nodes,
ngContentSelectors: template.ngContentSelectors,
},
declarationListEmitMode,
styles: metaObj.has('styles') ? metaObj.getArray('styles').map(entry => entry.getString()) :
[],
encapsulation: metaObj.has('encapsulation') ?
parseEncapsulation(metaObj.getValue('encapsulation')) :
ViewEncapsulation.Emulated,
interpolation,
changeDetection: metaObj.has('changeDetection') ?
parseChangeDetectionStrategy(metaObj.getValue('changeDetection')) :
ChangeDetectionStrategy.Default,
animations: metaObj.has('animations') ? metaObj.getOpaque('animations') : null,
relativeContextFilePath: this.sourceUrl,
i18nUseExternalIds: this.i18nUseExternalIds,
pipes,
directives,
};
}
/**
* Update the range to remove the start and end chars, which should be quotes around the template.
*/
private getTemplateInfo(templateNode: AstValue<unknown, TExpression>, isInline: boolean):
TemplateInfo {
const range = templateNode.getRange();
if (!isInline) {
// If not marked as inline, then we try to get the template info from the original external
// template file, via source-mapping.
const externalTemplate = this.tryExternalTemplate(range);
if (externalTemplate !== null) {
return externalTemplate;
}
}
// Either the template is marked inline or we failed to find the original external template.
// So just use the literal string from the partially compiled component declaration.
return this.templateFromPartialCode(templateNode, range);
}
private tryExternalTemplate(range: Range): TemplateInfo|null {
const sourceFile = this.getSourceFile();
if (sourceFile === null) {
return null;
}
const pos = sourceFile.getOriginalLocation(range.startLine, range.startCol);
// Only interested if the original location is in an "external" template file:
// * the file is different to the current file
// * the file does not end in `.js` or `.ts` (we expect it to be something like `.html`).
// * the range starts at the beginning of the file
if (pos === null || pos.file === this.sourceUrl || /\.[jt]s$/.test(pos.file) ||
pos.line !== 0 || pos.column !== 0) {
return null;
}
const templateContents = sourceFile.sources.find(src => src?.sourcePath === pos.file)!.contents;
return {
code: templateContents,
sourceUrl: pos.file,
range: {startPos: 0, startLine: 0, startCol: 0, endPos: templateContents.length},
isEscaped: false,
};
}
private templateFromPartialCode(
templateNode: AstValue<unknown, TExpression>,
{startPos, endPos, startLine, startCol}: Range): TemplateInfo {
if (!/["'`]/.test(this.code[startPos]) || this.code[startPos] !== this.code[endPos - 1]) {
throw new FatalLinkerError(
templateNode.expression,
`Expected the template string to be wrapped in quotes but got: ${
this.code.substring(startPos, endPos)}`);
}
return {
code: this.code,
sourceUrl: this.sourceUrl,
range: {startPos: startPos + 1, endPos: endPos - 1, startLine, startCol: startCol + 1},
isEscaped: true,
};
}
}
/**
* This function derives the `R3ComponentMetadata` from the provided AST object.
*/
export function toR3ComponentMeta<TExpression>(
metaObj: AstObject<R3DeclareComponentMetadata, TExpression>, code: string, sourceUrl: string,
options: LinkerOptions): R3ComponentMetadata {
const interpolation = parseInterpolationConfig(metaObj);
const templateObj = metaObj.getObject('template');
const templateSource = templateObj.getValue('source');
const range = getTemplateRange(templateSource, code);
const isInline = templateObj.getBoolean('isInline');
// We always normalize line endings if the template is inline.
const i18nNormalizeLineEndingsInICUs = isInline || options.i18nNormalizeLineEndingsInICUs;
const template = parseTemplate(code, sourceUrl, {
escapedString: true,
interpolationConfig: interpolation,
range,
enableI18nLegacyMessageIdFormat: options.enableI18nLegacyMessageIdFormat,
preserveWhitespaces:
metaObj.has('preserveWhitespaces') ? metaObj.getBoolean('preserveWhitespaces') : false,
i18nNormalizeLineEndingsInICUs,
isInline,
});
if (template.errors !== null) {
const errors = template.errors.map(err => err.toString()).join('\n');
throw new FatalLinkerError(
templateSource.expression, `Errors found in the template:\n${errors}`);
}
let declarationListEmitMode = DeclarationListEmitMode.Direct;
let directives: R3UsedDirectiveMetadata[] = [];
if (metaObj.has('directives')) {
directives = metaObj.getArray('directives').map(directive => {
const directiveExpr = directive.getObject();
const type = directiveExpr.getValue('type');
const selector = directiveExpr.getString('selector');
let typeExpr = type.getOpaque();
const forwardRefType = extractForwardRef(type);
if (forwardRefType !== null) {
typeExpr = forwardRefType;
declarationListEmitMode = DeclarationListEmitMode.Closure;
}
return {
type: typeExpr,
selector: selector,
inputs: directiveExpr.has('inputs') ?
directiveExpr.getArray('inputs').map(input => input.getString()) :
[],
outputs: directiveExpr.has('outputs') ?
directiveExpr.getArray('outputs').map(input => input.getString()) :
[],
exportAs: directiveExpr.has('exportAs') ?
directiveExpr.getArray('exportAs').map(exportAs => exportAs.getString()) :
null,
};
});
}
let pipes = new Map<string, o.Expression>();
if (metaObj.has('pipes')) {
pipes = metaObj.getObject('pipes').toMap(pipe => {
const forwardRefType = extractForwardRef(pipe);
if (forwardRefType !== null) {
declarationListEmitMode = DeclarationListEmitMode.Closure;
return forwardRefType;
} else {
return pipe.getOpaque();
}
});
}
return {
...toR3DirectiveMeta(metaObj, code, sourceUrl),
viewProviders: metaObj.has('viewProviders') ? metaObj.getOpaque('viewProviders') : null,
template: {
nodes: template.nodes,
ngContentSelectors: template.ngContentSelectors,
},
declarationListEmitMode,
styles: metaObj.has('styles') ? metaObj.getArray('styles').map(entry => entry.getString()) : [],
encapsulation: metaObj.has('encapsulation') ?
parseEncapsulation(metaObj.getValue('encapsulation')) :
ViewEncapsulation.Emulated,
interpolation,
changeDetection: metaObj.has('changeDetection') ?
parseChangeDetectionStrategy(metaObj.getValue('changeDetection')) :
ChangeDetectionStrategy.Default,
animations: metaObj.has('animations') ? metaObj.getOpaque('animations') : null,
relativeContextFilePath: sourceUrl,
i18nUseExternalIds: options.i18nUseExternalIds,
pipes,
directives,
};
interface TemplateInfo {
code: string;
sourceUrl: string;
range: Range;
isEscaped: boolean;
}
/**
@ -188,27 +271,6 @@ function parseChangeDetectionStrategy<TExpression>(
return enumValue;
}
/**
* Update the range to remove the start and end chars, which should be quotes around the template.
*/
function getTemplateRange<TExpression>(
templateNode: AstValue<unknown, TExpression>, code: string): Range {
const {startPos, endPos, startLine, startCol} = templateNode.getRange();
if (!/["'`]/.test(code[startPos]) || code[startPos] !== code[endPos - 1]) {
throw new FatalLinkerError(
templateNode.expression,
`Expected the template string to be wrapped in quotes but got: ${
code.substring(startPos, endPos)}`);
}
return {
startPos: startPos + 1,
endPos: endPos - 1,
startLine,
startCol: startCol + 1,
};
}
/**
* Extract the type reference expression from a `forwardRef` function call. For example, the
* expression `forwardRef(function() { return FooDir; })` returns `FooDir`. Note that this

View File

@ -8,6 +8,7 @@
import {compileDirectiveFromMetadata, ConstantPool, makeBindingParser, ParseLocation, ParseSourceFile, ParseSourceSpan, R3DeclareDirectiveMetadata, R3DeclareQueryMetadata, R3DirectiveMetadata, R3HostMetadata, R3PartialDeclaration, R3QueryMetadata, R3Reference} from '@angular/compiler';
import * as o from '@angular/compiler/src/output/output_ast';
import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system';
import {Range} from '../../ast/ast_host';
import {AstObject, AstValue} from '../../ast/ast_value';
import {FatalLinkerError} from '../../fatal_linker_error';
@ -18,10 +19,12 @@ import {PartialLinker} from './partial_linker';
* A `PartialLinker` that is designed to process `ɵɵngDeclareDirective()` call expressions.
*/
export class PartialDirectiveLinkerVersion1<TExpression> implements PartialLinker<TExpression> {
constructor(private sourceUrl: AbsoluteFsPath, private code: string) {}
linkPartialDeclaration(
sourceUrl: string, code: string, constantPool: ConstantPool,
constantPool: ConstantPool,
metaObj: AstObject<R3PartialDeclaration, TExpression>): o.Expression {
const meta = toR3DirectiveMeta(metaObj, code, sourceUrl);
const meta = toR3DirectiveMeta(metaObj, this.code, this.sourceUrl);
const def = compileDirectiveFromMetadata(meta, constantPool, makeBindingParser());
return def.expression;
}
@ -32,7 +35,7 @@ export class PartialDirectiveLinkerVersion1<TExpression> implements PartialLinke
*/
export function toR3DirectiveMeta<TExpression>(
metaObj: AstObject<R3DeclareDirectiveMetadata, TExpression>, code: string,
sourceUrl: string): R3DirectiveMetadata {
sourceUrl: AbsoluteFsPath): R3DirectiveMetadata {
const typeExpr = metaObj.getValue('type');
const typeName = typeExpr.getSymbolName();
if (typeName === null) {

View File

@ -7,6 +7,7 @@
*/
import {ConstantPool, R3PartialDeclaration} from '@angular/compiler';
import * as o from '@angular/compiler/src/output/output_ast';
import {AstObject} from '../../ast/ast_value';
/**
@ -20,6 +21,6 @@ export interface PartialLinker<TExpression> {
* `R3DeclareComponentMetadata` interfaces.
*/
linkPartialDeclaration(
sourceUrl: string, code: string, constantPool: ConstantPool,
constantPool: ConstantPool,
metaObj: AstObject<R3PartialDeclaration, TExpression>): o.Expression;
}

View File

@ -6,7 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/
import {satisfies} from 'semver';
import {LinkerOptions} from '../linker_options';
import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system';
import {createGetSourceFile} from '../get_source_file';
import {LinkerEnvironment} from '../linker_environment';
import {PartialComponentLinkerVersion1} from './partial_component_linker_1';
import {PartialDirectiveLinkerVersion1} from './partial_directive_linker_1';
@ -16,33 +19,29 @@ export const ɵɵngDeclareDirective = 'ɵɵngDeclareDirective';
export const ɵɵngDeclareComponent = 'ɵɵngDeclareComponent';
export const declarationFunctions = [ɵɵngDeclareDirective, ɵɵngDeclareComponent];
export class PartialLinkerSelector<TExpression> {
/**
* A database of linker instances that should be used if their given semver range satisfies the
* version found in the code to be linked.
*
* Note that the ranges are checked in order, and the first matching range will be selected, so
* ranges should be most restrictive first.
*
* Also, ranges are matched to include "pre-releases", therefore if the range is `>=11.1.0-next.1`
* then this includes `11.1.0-next.2` and also `12.0.0-next.1`.
*
* Finally, note that we always start with the current version (i.e. `0.0.0-PLACEHOLDER`). This
* allows the linker to work on local builds effectively.
*/
private linkers: Record<string, {range: string, linker: PartialLinker<TExpression>}[]> = {
[ɵɵngDeclareDirective]: [
{range: '0.0.0-PLACEHOLDER', linker: new PartialDirectiveLinkerVersion1()},
{range: '>=11.1.0-next.1', linker: new PartialDirectiveLinkerVersion1()},
],
[ɵɵngDeclareComponent]:
[
{range: '0.0.0-PLACEHOLDER', linker: new PartialComponentLinkerVersion1(this.options)},
{range: '>=11.1.0-next.1', linker: new PartialComponentLinkerVersion1(this.options)},
],
};
/**
* A helper that selects the appropriate `PartialLinker` for a given declaration.
*
* The selection is made from a database of linker instances, chosen if their given semver range
* satisfies the version found in the code to be linked.
*
* Note that the ranges are checked in order, and the first matching range will be selected, so
* ranges should be most restrictive first.
*
* Also, ranges are matched to include "pre-releases", therefore if the range is `>=11.1.0-next.1`
* then this includes `11.1.0-next.2` and also `12.0.0-next.1`.
*
* Finally, note that we always start with the current version (i.e. `0.0.0-PLACEHOLDER`). This
* allows the linker to work on local builds effectively.
*/
export class PartialLinkerSelector<TStatement, TExpression> {
private readonly linkers: Record<string, {range: string, linker: PartialLinker<TExpression>}[]>;
constructor(private options: LinkerOptions) {}
constructor(
environment: LinkerEnvironment<TStatement, TExpression>, sourceUrl: AbsoluteFsPath,
code: string) {
this.linkers = this.createLinkerMap(environment, sourceUrl, code);
}
/**
* Returns true if there are `PartialLinker` classes that can handle functions with this name.
@ -69,4 +68,24 @@ export class PartialLinkerSelector<TExpression> {
`Unsupported partial declaration version ${version} for ${functionName}.\n` +
'Valid version ranges are:\n' + versions.map(v => ` - ${v.range}`).join('\n'));
}
private createLinkerMap(
environment: LinkerEnvironment<TStatement, TExpression>, sourceUrl: AbsoluteFsPath,
code: string) {
const partialDirectiveLinkerVersion1 = new PartialDirectiveLinkerVersion1(sourceUrl, code);
const partialComponentLinkerVersion1 = new PartialComponentLinkerVersion1(
environment, createGetSourceFile(sourceUrl, code, environment.sourceFileLoader), sourceUrl,
code);
return {
[ɵɵngDeclareDirective]: [
{range: '0.0.0-PLACEHOLDER', linker: partialDirectiveLinkerVersion1},
{range: '>=11.1.0-next.1', linker: partialDirectiveLinkerVersion1},
],
[ɵɵngDeclareComponent]: [
{range: '0.0.0-PLACEHOLDER', linker: partialComponentLinkerVersion1},
{range: '>=11.1.0-next.1', linker: partialComponentLinkerVersion1},
],
};
}
}

View File

@ -12,6 +12,9 @@ ts_library(
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/linker",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/logging/testing",
"//packages/compiler-cli/src/ngtsc/translator",
"@npm//typescript",
],

View File

@ -9,20 +9,17 @@ 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 {Translator} from '../../../src/file_linker/translator';
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 translator = new Translator<ts.Statement, ts.Expression>(factory);
const ngImport = factory.createIdentifier('core');
const emitScope = new EmitScope<ts.Statement, ts.Expression>(ngImport, linkerEnvironment);
const emitScope = new EmitScope<ts.Statement, ts.Expression>(ngImport, translator);
const def = emitScope.translateDefinition(o.fn([], [], null, null, 'foo'));
expect(generate(def)).toEqual('function foo() { }');
@ -30,10 +27,9 @@ describe('EmitScope', () => {
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 translator = new Translator<ts.Statement, ts.Expression>(factory);
const ngImport = factory.createIdentifier('core');
const emitScope = new EmitScope<ts.Statement, ts.Expression>(ngImport, linkerEnvironment);
const emitScope = new EmitScope<ts.Statement, ts.Expression>(ngImport, translator);
const coreImportRef = new o.ExternalReference('@angular/core', 'foo');
const def = emitScope.translateDefinition(o.importExpr(coreImportRef).callMethod('bar', []));
@ -42,10 +38,9 @@ describe('EmitScope', () => {
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 translator = new Translator<ts.Statement, ts.Expression>(factory);
const ngImport = factory.createIdentifier('core');
const emitScope = new EmitScope<ts.Statement, ts.Expression>(ngImport, linkerEnvironment);
const emitScope = new EmitScope<ts.Statement, ts.Expression>(ngImport, translator);
const constArray = o.literalArr([o.literal('CONST')]);
// We have to add the constant twice or it will not create a shared statement
@ -60,10 +55,9 @@ describe('EmitScope', () => {
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 translator = new Translator<ts.Statement, ts.Expression>(factory);
const ngImport = factory.createIdentifier('core');
const emitScope = new EmitScope<ts.Statement, ts.Expression>(ngImport, linkerEnvironment);
const emitScope = new EmitScope<ts.Statement, ts.Expression>(ngImport, translator);
const constArray = o.literalArr([o.literal('CONST')]);
// We have to add the constant twice or it will not create a shared statement

View File

@ -9,20 +9,18 @@ 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 {Translator} from '../../../src/file_linker/translator';
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 translator = new Translator<ts.Statement, ts.Expression>(factory);
const ngImport = factory.createIdentifier('core');
const emitScope = new IifeEmitScope<ts.Statement, ts.Expression>(ngImport, linkerEnvironment);
const emitScope =
new IifeEmitScope<ts.Statement, ts.Expression>(ngImport, translator, factory);
const def = emitScope.translateDefinition(o.fn([], [], null, null, 'foo'));
expect(generate(def)).toEqual('function () { return function foo() { }; }()');
@ -30,10 +28,10 @@ describe('IifeEmitScope', () => {
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 translator = new Translator<ts.Statement, ts.Expression>(factory);
const ngImport = factory.createIdentifier('core');
const emitScope = new IifeEmitScope<ts.Statement, ts.Expression>(ngImport, linkerEnvironment);
const emitScope =
new IifeEmitScope<ts.Statement, ts.Expression>(ngImport, translator, factory);
const coreImportRef = new o.ExternalReference('@angular/core', 'foo');
const def = emitScope.translateDefinition(o.importExpr(coreImportRef).callMethod('bar', []));
@ -42,10 +40,10 @@ describe('IifeEmitScope', () => {
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 translator = new Translator<ts.Statement, ts.Expression>(factory);
const ngImport = factory.createIdentifier('core');
const emitScope = new IifeEmitScope<ts.Statement, ts.Expression>(ngImport, linkerEnvironment);
const emitScope =
new IifeEmitScope<ts.Statement, ts.Expression>(ngImport, translator, factory);
const constArray = o.literalArr([o.literal('CONST')]);
// We have to add the constant twice or it will not create a shared statement
@ -61,10 +59,10 @@ describe('IifeEmitScope', () => {
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 translator = new Translator<ts.Statement, ts.Expression>(factory);
const ngImport = factory.createIdentifier('core');
const emitScope = new IifeEmitScope<ts.Statement, ts.Expression>(ngImport, linkerEnvironment);
const emitScope =
new IifeEmitScope<ts.Statement, ts.Expression>(ngImport, translator, factory);
expect(() => emitScope.getConstantStatements()).toThrowError();
});
});

View File

@ -8,6 +8,8 @@
import * as o from '@angular/compiler/src/output/output_ast';
import * as ts from 'typescript';
import {MockFileSystemNative} from '../../../src/ngtsc/file_system/testing';
import {MockLogger} from '../../../src/ngtsc/logging/testing';
import {TypeScriptAstFactory} from '../../../src/ngtsc/translator';
import {AstHost} from '../../src/ast/ast_host';
import {TypeScriptAstHost} from '../../src/ast/typescript/typescript_ast_host';
@ -16,6 +18,7 @@ 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', () => {
@ -91,7 +94,7 @@ describe('FileLinker', () => {
expect(compilationResult).toEqual(factory.createLiteral('compilation result'));
expect(compileSpy).toHaveBeenCalled();
expect(compileSpy.calls.mostRecent().args[3].getNode('ngImport')).toBe(ngImport);
expect(compileSpy.calls.mostRecent().args[1].getNode('ngImport')).toBe(ngImport);
});
});
@ -148,10 +151,12 @@ describe('FileLinker', () => {
host: AstHost<ts.Expression>,
fileLinker: FileLinker<MockConstantScopeRef, ts.Statement, ts.Expression>
} {
const fs = new MockFileSystemNative();
const logger = new MockLogger();
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
new TypeScriptAstHost(), new TypeScriptAstFactory(), DEFAULT_LINKER_OPTIONS);
fs, logger, new TypeScriptAstHost(), new TypeScriptAstFactory(), DEFAULT_LINKER_OPTIONS);
const fileLinker = new FileLinker<MockConstantScopeRef, ts.Statement, ts.Expression>(
linkerEnvironment, 'test.js', '// test code');
linkerEnvironment, fs.resolve('/test.js'), '// test code');
return {host: linkerEnvironment.host, fileLinker};
}
});
@ -185,7 +190,7 @@ class MockConstantScopeRef {
function spyOnLinkPartialDeclarationWithConstants(replacement: o.Expression) {
let callCount = 0;
spyOn(PartialDirectiveLinkerVersion1.prototype, 'linkPartialDeclaration')
.and.callFake(((sourceUrl, code, constantPool) => {
.and.callFake((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);

View File

@ -5,8 +5,15 @@
* 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';
import {LinkerOptions} from '../../..';
import {FileSystem} from '../../../../src/ngtsc/file_system';
import {MockFileSystemNative} from '../../../../src/ngtsc/file_system/testing';
import {MockLogger} from '../../../../src/ngtsc/logging/testing';
import {TypeScriptAstFactory} from '../../../../src/ngtsc/translator';
import {TypeScriptAstHost} from '../../../src/ast/typescript/typescript_ast_host';
import {LinkerEnvironment} from '../../../src/file_linker/linker_environment';
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';
@ -16,12 +23,23 @@ describe('PartialLinkerSelector', () => {
i18nNormalizeLineEndingsInICUs: true,
enableI18nLegacyMessageIdFormat: false,
i18nUseExternalIds: false,
sourceMapping: false,
};
let environment: LinkerEnvironment<ts.Statement, ts.Expression>;
let fs: FileSystem;
beforeEach(() => {
fs = new MockFileSystemNative();
const logger = new MockLogger();
environment = LinkerEnvironment.create<ts.Statement, ts.Expression>(
fs, logger, new TypeScriptAstHost(), new TypeScriptAstFactory(), options);
});
describe('supportsDeclaration()', () => {
it('should return true if there is at least one linker that matches the given function name',
() => {
const selector = new PartialLinkerSelector(options);
const selector = new PartialLinkerSelector(
environment, fs.resolve('/some/path/to/file.js'), 'some file contents');
expect(selector.supportsDeclaration('ɵɵngDeclareDirective')).toBe(true);
expect(selector.supportsDeclaration('ɵɵngDeclareComponent')).toBe(true);
expect(selector.supportsDeclaration('$foo')).toBe(false);
@ -30,7 +48,8 @@ describe('PartialLinkerSelector', () => {
describe('getLinker()', () => {
it('should return the latest linker if the version is "0.0.0-PLACEHOLDER"', () => {
const selector = new PartialLinkerSelector(options);
const selector = new PartialLinkerSelector(
environment, fs.resolve('/some/path/to/file.js'), 'some file contents');
expect(selector.getLinker('ɵɵngDeclareDirective', '0.0.0-PLACEHOLDER'))
.toBeInstanceOf(PartialDirectiveLinkerVersion1);
expect(selector.getLinker('ɵɵngDeclareComponent', '0.0.0-PLACEHOLDER'))
@ -38,7 +57,8 @@ describe('PartialLinkerSelector', () => {
});
it('should return the linker that matches the name and valid full version', () => {
const selector = new PartialLinkerSelector(options);
const selector = new PartialLinkerSelector(
environment, fs.resolve('/some/path/to/file.js'), 'some file contents');
expect(selector.getLinker('ɵɵngDeclareDirective', '11.1.2'))
.toBeInstanceOf(PartialDirectiveLinkerVersion1);
expect(selector.getLinker('ɵɵngDeclareDirective', '11.2.5'))
@ -48,7 +68,8 @@ describe('PartialLinkerSelector', () => {
});
it('should return the linker that matches the name and valid pre-release versions', () => {
const selector = new PartialLinkerSelector(options);
const selector = new PartialLinkerSelector(
environment, fs.resolve('/some/path/to/file.js'), 'some file contents');
expect(selector.getLinker('ɵɵngDeclareDirective', '11.1.0-next.1'))
.toBeInstanceOf(PartialDirectiveLinkerVersion1);
expect(selector.getLinker('ɵɵngDeclareDirective', '11.1.0-next.7'))
@ -58,7 +79,8 @@ describe('PartialLinkerSelector', () => {
});
it('should throw an error if there is no linker that matches the given name or version', () => {
const selector = new PartialLinkerSelector(options);
const selector = new PartialLinkerSelector(
environment, fs.resolve('/some/path/to/file.js'), 'some file contents');
// `$foo` is not a valid name, even though `0.0.0-PLACEHOLDER` is a valid version
expect(() => selector.getLinker('$foo', '0.0.0-PLACEHOLDER'))
.toThrowError('Unknown partial declaration function $foo.');

View File

@ -22,50 +22,55 @@ runTests('linked compile', linkPartials);
/**
* Link all the partials specified in the given `test`.
*
* @param fs The mock file-system to use for linking the partials.
* @param fileSystem The mock file-system to use for linking the partials.
* @param test The compliance test whose partials will be linked.
*/
function linkPartials(fs: FileSystem, test: ComplianceTest): CompileResult {
function linkPartials(fileSystem: FileSystem, test: ComplianceTest): CompileResult {
const logger = new ConsoleLogger(LogLevel.debug);
const loader = new SourceFileLoader(fs, logger, {});
const builtDirectory = getBuildOutputDirectory(fs);
const loader = new SourceFileLoader(fileSystem, logger, {});
const builtDirectory = getBuildOutputDirectory(fileSystem);
const linkerPlugin = createEs2015LinkerPlugin({
fileSystem,
logger,
// By default we don't render legacy message ids in compliance tests.
enableI18nLegacyMessageIdFormat: false,
sourceMapping: test.compilerOptions?.sourceMap === true,
...test.angularCompilerOptions
});
const goldenPartialPath = fs.resolve('/GOLDEN_PARTIAL.js');
if (!fs.exists(goldenPartialPath)) {
const goldenPartialPath = fileSystem.resolve('/GOLDEN_PARTIAL.js');
if (!fileSystem.exists(goldenPartialPath)) {
throw new Error(
'Golden partial does not exist for this test\n' +
'Try generating it by running:\n' +
`bazel run //packages/compiler-cli/test/compliance/test_cases:${
test.relativePath}.golden.update`);
}
const partialFile = fs.readFile(goldenPartialPath);
const partialFile = fileSystem.readFile(goldenPartialPath);
const partialFiles = parseGoldenPartial(partialFile);
partialFiles.forEach(f => safeWrite(fs, fs.resolve(builtDirectory, f.path), f.content));
partialFiles.forEach(
f => safeWrite(fileSystem, fileSystem.resolve(builtDirectory, f.path), f.content));
for (const expectation of test.expectations) {
for (const {generated: fileName} of expectation.files) {
const partialPath = fs.resolve(builtDirectory, fileName);
if (!fs.exists(partialPath)) {
for (const {generated} of expectation.files) {
const fileName = fileSystem.resolve(builtDirectory, generated);
if (!fileSystem.exists(fileName)) {
continue;
}
const source = fs.readFile(partialPath);
const sourceMapPath = fs.resolve(partialPath + '.map');
const sourceMap =
fs.exists(sourceMapPath) ? JSON.parse(fs.readFile(sourceMapPath)) : undefined;
const source = fileSystem.readFile(fileName);
const sourceMapPath = fileSystem.resolve(fileName + '.map');
const sourceMap = fileSystem.exists(sourceMapPath) ?
JSON.parse(fileSystem.readFile(sourceMapPath)) :
undefined;
const {linkedSource, linkedSourceMap} =
applyLinker({path: partialPath, source, sourceMap}, linkerPlugin);
applyLinker(builtDirectory, fileName, source, sourceMap, linkerPlugin);
if (linkedSourceMap !== undefined) {
const mapAndPath: MapAndPath = {map: linkedSourceMap, mapPath: sourceMapPath};
const sourceFile = loader.loadSourceFile(partialPath, linkedSource, mapAndPath);
safeWrite(fs, sourceMapPath, JSON.stringify(sourceFile.renderFlattenedSourceMap()));
const sourceFile = loader.loadSourceFile(fileName, linkedSource, mapAndPath);
safeWrite(fileSystem, sourceMapPath, JSON.stringify(sourceFile.renderFlattenedSourceMap()));
}
safeWrite(fs, partialPath, linkedSource);
safeWrite(fileSystem, fileName, linkedSource);
}
}
return {emittedFiles: [], errors: []};
@ -81,14 +86,15 @@ function linkPartials(fs: FileSystem, test: ComplianceTest): CompileResult {
* @returns The file's source content, which has been transformed using the linker if necessary.
*/
function applyLinker(
file: {path: string; source: string, sourceMap: RawSourceMap | undefined},
cwd: string, filename: string, source: string, sourceMap: RawSourceMap|undefined,
linkerPlugin: PluginObj): {linkedSource: string, linkedSourceMap: RawSourceMap|undefined} {
if (!file.path.endsWith('.js') || !needsLinking(file.path, file.source)) {
return {linkedSource: file.source, linkedSourceMap: file.sourceMap};
if (!filename.endsWith('.js') || !needsLinking(filename, source)) {
return {linkedSource: source, linkedSourceMap: sourceMap};
}
const result = transformSync(file.source, {
filename: file.path,
sourceMaps: !!file.sourceMap,
const result = transformSync(source, {
cwd,
filename,
sourceMaps: !!sourceMap,
plugins: [linkerPlugin],
parserOpts: {sourceType: 'unambiguous'},
});

View File

@ -8,6 +8,8 @@ ts_library(
"//packages:types",
"//packages/compiler-cli/linker",
"//packages/compiler-cli/linker/babel",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/logging/testing",
"//packages/compiler-cli/test/compliance_old/mock_compile",
"@npm//@babel/core",
"@npm//@babel/generator",

View File

@ -10,6 +10,8 @@ import * as ts from 'typescript';
import {needsLinking} from '../../../linker';
import {createEs2015LinkerPlugin} from '../../../linker/babel';
import {MockFileSystemNative} from '../../../src/ngtsc/file_system/testing';
import {MockLogger} from '../../../src/ngtsc/logging/testing';
import {compileFiles, CompileFn, setCompileFn} from '../mock_compile';
/**
@ -28,8 +30,11 @@ const linkedCompile: CompileFn = (data, angularFiles, options) => {
}
const compiledFiles = compileFiles(data, angularFiles, {...options, compilationMode: 'partial'});
const fileSystem = new MockFileSystemNative();
const logger = new MockLogger();
const linkerPlugin = createEs2015LinkerPlugin({
fileSystem,
logger,
// enableI18nLegacyMessageIdFormat defaults to false in `compileFiles`.
enableI18nLegacyMessageIdFormat: false,
...options,