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 = [ deps = [
"//packages/compiler", "//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", "//packages/compiler-cli/src/ngtsc/translator",
"@npm//@types/semver", "@npm//@types/semver",
"@npm//semver", "@npm//semver",

View File

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

View File

@ -9,11 +9,13 @@ import {PluginObj} from '@babel/core';
import {NodePath} from '@babel/traverse'; import {NodePath} from '@babel/traverse';
import * as t from '@babel/types'; 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 {BabelAstFactory} from './ast/babel_ast_factory';
import {BabelAstHost} from './ast/babel_ast_host'; import {BabelAstHost} from './ast/babel_ast_host';
import {BabelDeclarationScope, ConstantScopePath} from './babel_declaration_scope'; 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. * 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 * The plugin delegates most of its work to a generic `FileLinker` for each file (`t.Program` in
* Babel) that is visited. * 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; let fileLinker: FileLinker<ConstantScopePath, t.Statement, t.Expression>|null = null;
const linkerEnvironment = LinkerEnvironment.create<t.Statement, t.Expression>(
new BabelAstHost(), new BabelAstFactory(), options);
return { return {
visitor: { visitor: {
Program: { Program: {
@ -36,8 +36,19 @@ export function createEs2015LinkerPlugin(options: Partial<LinkerOptions> = {}):
*/ */
enter(path: NodePath<t.Program>): void { enter(path: NodePath<t.Program>): void {
assertNull(fileLinker); 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; 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 { try {
const callee = call.node.callee; const calleeName = getCalleeName(call);
if (!t.isExpression(callee)) {
return;
}
const calleeName = linkerEnvironment.host.getSymbolName(callee);
if (calleeName === null) { if (calleeName === null) {
return; 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. * 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 { interface BabelFile {
code: string; code: string;
opts: {filename?: string;}; opts: {
filename?: string,
filenameRelative?: string,
cwd?: string,
};
buildCodeFrameError(node: t.Node, message: string): Error; 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",
"//packages/compiler-cli/linker", "//packages/compiler-cli/linker",
"//packages/compiler-cli/linker/babel", "//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", "//packages/compiler-cli/src/ngtsc/translator",
"@npm//@babel/core", "@npm//@babel/core",
"@npm//@babel/generator", "@npm//@babel/generator",

View File

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

View File

@ -8,7 +8,7 @@
import {ConstantPool} from '@angular/compiler'; import {ConstantPool} from '@angular/compiler';
import * as o from '@angular/compiler/src/output/output_ast'; import * as o from '@angular/compiler/src/output/output_ast';
import {LinkerImportGenerator} from '../../linker_import_generator'; 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 * 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( constructor(
protected readonly ngImport: TExpression, 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`. * 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. * Use a `LinkerImportGenerator` to handle any imports in the definition.
*/ */
translateDefinition(definition: o.Expression): TExpression { translateDefinition(definition: o.Expression): TExpression {
return this.linkerEnvironment.translator.translateExpression( return this.translator.translateExpression(
definition, new LinkerImportGenerator(this.ngImport)); 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`. * Return any constant statements that are shared between all uses of this `EmitScope`.
*/ */
getConstantStatements(): TStatement[] { getConstantStatements(): TStatement[] {
const {translator} = this.linkerEnvironment;
const importGenerator = new LinkerImportGenerator(this.ngImport); const importGenerator = new LinkerImportGenerator(this.ngImport);
return this.constantPool.statements.map( 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 * found in the LICENSE file at https://angular.io/license
*/ */
import * as o from '@angular/compiler/src/output/output_ast'; 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'; import {EmitScope} from './emit_scope';
/** /**
@ -14,6 +18,12 @@ import {EmitScope} from './emit_scope';
* translated definition inside an IIFE. * translated definition inside an IIFE.
*/ */
export class IifeEmitScope<TStatement, TExpression> extends EmitScope<TStatement, TExpression> { 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`. * 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. * in an IIFE.
*/ */
translateDefinition(definition: o.Expression): TExpression { translateDefinition(definition: o.Expression): TExpression {
const {factory} = this.linkerEnvironment;
const constantStatements = super.getConstantStatements(); const constantStatements = super.getConstantStatements();
const returnStatement = factory.createReturnStatement(super.translateDefinition(definition)); const returnStatement =
const body = factory.createBlock([...constantStatements, returnStatement]); this.factory.createReturnStatement(super.translateDefinition(definition));
const fn = factory.createFunctionExpression(/* name */ null, /* args */[], body); const body = this.factory.createBlock([...constantStatements, returnStatement]);
return factory.createCallExpression(fn, /* args */[], /* pure */ false); 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {R3PartialDeclaration} from '@angular/compiler'; import {R3PartialDeclaration} from '@angular/compiler';
import {AbsoluteFsPath} from '../../../src/ngtsc/file_system';
import {AstObject} from '../ast/ast_value'; import {AstObject} from '../ast/ast_value';
import {DeclarationScope} from './declaration_scope'; import {DeclarationScope} from './declaration_scope';
import {EmitScope} from './emit_scopes/emit_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. * This class is responsible for linking all the partial declarations found in a single file.
*/ */
export class FileLinker<TConstantScope, TStatement, TExpression> { 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>>(); private emitScopes = new Map<TConstantScope, EmitScope<TStatement, TExpression>>();
constructor( constructor(
private linkerEnvironment: LinkerEnvironment<TStatement, TExpression>, 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. * 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 version = metaObj.getString('version');
const linker = this.linkerSelector.getLinker(declarationFn, version); const linker = this.linkerSelector.getLinker(declarationFn, version);
const definition = const definition = linker.linkPartialDeclaration(emitScope.constantPool, metaObj);
linker.linkPartialDeclaration(this.sourceUrl, this.code, emitScope.constantPool, metaObj);
return emitScope.translateDefinition(definition); return emitScope.translateDefinition(definition);
} }
@ -86,11 +89,13 @@ export class FileLinker<TConstantScope, TStatement, TExpression> {
const constantScope = declarationScope.getConstantScopeRef(ngImport); const constantScope = declarationScope.getConstantScopeRef(ngImport);
if (constantScope === null) { if (constantScope === null) {
// There is no constant scope so we will emit extra statements into the definition IIFE. // 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)) { 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)!; 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 * 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 * 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 {AstHost} from '../ast/ast_host';
import {DEFAULT_LINKER_OPTIONS, LinkerOptions} from './linker_options'; import {DEFAULT_LINKER_OPTIONS, LinkerOptions} from './linker_options';
@ -13,19 +16,24 @@ import {Translator} from './translator';
export class LinkerEnvironment<TStatement, TExpression> { export class LinkerEnvironment<TStatement, TExpression> {
readonly translator = new Translator<TStatement, TExpression>(this.factory); readonly translator = new Translator<TStatement, TExpression>(this.factory);
readonly sourceFileLoader =
this.options.sourceMapping ? new SourceFileLoader(this.fileSystem, this.logger, {}) : null;
private constructor( private constructor(
readonly host: AstHost<TExpression>, readonly factory: AstFactory<TStatement, TExpression>, readonly fileSystem: FileSystem, readonly logger: Logger, readonly host: AstHost<TExpression>,
readonly options: LinkerOptions) {} readonly factory: AstFactory<TStatement, TExpression>, readonly options: LinkerOptions) {}
static create<TStatement, TExpression>( 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> { options: Partial<LinkerOptions>): LinkerEnvironment<TStatement, TExpression> {
return new LinkerEnvironment(host, factory, { return new LinkerEnvironment(fileSystem, logger, host, factory, {
enableI18nLegacyMessageIdFormat: options.enableI18nLegacyMessageIdFormat ?? enableI18nLegacyMessageIdFormat: options.enableI18nLegacyMessageIdFormat ??
DEFAULT_LINKER_OPTIONS.enableI18nLegacyMessageIdFormat, DEFAULT_LINKER_OPTIONS.enableI18nLegacyMessageIdFormat,
i18nNormalizeLineEndingsInICUs: options.i18nNormalizeLineEndingsInICUs ?? i18nNormalizeLineEndingsInICUs: options.i18nNormalizeLineEndingsInICUs ??
DEFAULT_LINKER_OPTIONS.i18nNormalizeLineEndingsInICUs, DEFAULT_LINKER_OPTIONS.i18nNormalizeLineEndingsInICUs,
i18nUseExternalIds: options.i18nUseExternalIds ?? DEFAULT_LINKER_OPTIONS.i18nUseExternalIds, 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`. * The default is `false`.
*/ */
i18nUseExternalIds: boolean; 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, enableI18nLegacyMessageIdFormat: true,
i18nNormalizeLineEndingsInICUs: false, i18nNormalizeLineEndingsInICUs: false,
i18nUseExternalIds: 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 {ChangeDetectionStrategy, ViewEncapsulation} from '@angular/compiler/src/core';
import * as o from '@angular/compiler/src/output/output_ast'; import * as o from '@angular/compiler/src/output/output_ast';
import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system';
import {Range} from '../../ast/ast_host'; import {Range} from '../../ast/ast_host';
import {AstObject, AstValue} from '../../ast/ast_value'; import {AstObject, AstValue} from '../../ast/ast_value';
import {FatalLinkerError} from '../../fatal_linker_error'; 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 {toR3DirectiveMeta} from './partial_directive_linker_1';
import {PartialLinker} from './partial_linker'; import {PartialLinker} from './partial_linker';
@ -20,38 +22,46 @@ import {PartialLinker} from './partial_linker';
/** /**
* A `PartialLinker` that is designed to process `ɵɵngDeclareComponent()` call expressions. * A `PartialLinker` that is designed to process `ɵɵngDeclareComponent()` call expressions.
*/ */
export class PartialComponentLinkerVersion1<TExpression> implements PartialLinker<TExpression> { export class PartialComponentLinkerVersion1<TStatement, TExpression> implements
constructor(private readonly options: LinkerOptions) {} 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( linkPartialDeclaration(
sourceUrl: string, code: string, constantPool: ConstantPool, constantPool: ConstantPool,
metaObj: AstObject<R3PartialDeclaration, TExpression>): o.Expression { 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()); const def = compileComponentFromMetadata(meta, constantPool, makeBindingParser());
return def.expression; return def.expression;
} }
}
/** /**
* This function derives the `R3ComponentMetadata` from the provided AST object. * This function derives the `R3ComponentMetadata` from the provided AST object.
*/ */
export function toR3ComponentMeta<TExpression>( private toR3ComponentMeta(metaObj: AstObject<R3DeclareComponentMetadata, TExpression>):
metaObj: AstObject<R3DeclareComponentMetadata, TExpression>, code: string, sourceUrl: string, R3ComponentMetadata {
options: LinkerOptions): R3ComponentMetadata {
const interpolation = parseInterpolationConfig(metaObj); const interpolation = parseInterpolationConfig(metaObj);
const templateObj = metaObj.getObject('template'); const templateObj = metaObj.getObject('template');
const templateSource = templateObj.getValue('source'); const templateSource = templateObj.getValue('source');
const range = getTemplateRange(templateSource, code);
const isInline = templateObj.getBoolean('isInline'); const isInline = templateObj.getBoolean('isInline');
const templateInfo = this.getTemplateInfo(templateSource, isInline);
// We always normalize line endings if the template is inline. // We always normalize line endings if the template is inline.
const i18nNormalizeLineEndingsInICUs = isInline || options.i18nNormalizeLineEndingsInICUs; const i18nNormalizeLineEndingsInICUs = isInline || this.i18nNormalizeLineEndingsInICUs;
const template = parseTemplate(code, sourceUrl, { const template = parseTemplate(templateInfo.code, templateInfo.sourceUrl, {
escapedString: true, escapedString: templateInfo.isEscaped,
interpolationConfig: interpolation, interpolationConfig: interpolation,
range, range: templateInfo.range,
enableI18nLegacyMessageIdFormat: options.enableI18nLegacyMessageIdFormat, enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
preserveWhitespaces: preserveWhitespaces:
metaObj.has('preserveWhitespaces') ? metaObj.getBoolean('preserveWhitespaces') : false, metaObj.has('preserveWhitespaces') ? metaObj.getBoolean('preserveWhitespaces') : false,
i18nNormalizeLineEndingsInICUs, i18nNormalizeLineEndingsInICUs,
@ -109,14 +119,15 @@ export function toR3ComponentMeta<TExpression>(
} }
return { return {
...toR3DirectiveMeta(metaObj, code, sourceUrl), ...toR3DirectiveMeta(metaObj, this.code, this.sourceUrl),
viewProviders: metaObj.has('viewProviders') ? metaObj.getOpaque('viewProviders') : null, viewProviders: metaObj.has('viewProviders') ? metaObj.getOpaque('viewProviders') : null,
template: { template: {
nodes: template.nodes, nodes: template.nodes,
ngContentSelectors: template.ngContentSelectors, ngContentSelectors: template.ngContentSelectors,
}, },
declarationListEmitMode, declarationListEmitMode,
styles: metaObj.has('styles') ? metaObj.getArray('styles').map(entry => entry.getString()) : [], styles: metaObj.has('styles') ? metaObj.getArray('styles').map(entry => entry.getString()) :
[],
encapsulation: metaObj.has('encapsulation') ? encapsulation: metaObj.has('encapsulation') ?
parseEncapsulation(metaObj.getValue('encapsulation')) : parseEncapsulation(metaObj.getValue('encapsulation')) :
ViewEncapsulation.Emulated, ViewEncapsulation.Emulated,
@ -125,11 +136,83 @@ export function toR3ComponentMeta<TExpression>(
parseChangeDetectionStrategy(metaObj.getValue('changeDetection')) : parseChangeDetectionStrategy(metaObj.getValue('changeDetection')) :
ChangeDetectionStrategy.Default, ChangeDetectionStrategy.Default,
animations: metaObj.has('animations') ? metaObj.getOpaque('animations') : null, animations: metaObj.has('animations') ? metaObj.getOpaque('animations') : null,
relativeContextFilePath: sourceUrl, relativeContextFilePath: this.sourceUrl,
i18nUseExternalIds: options.i18nUseExternalIds, i18nUseExternalIds: this.i18nUseExternalIds,
pipes, pipes,
directives, 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,
};
}
}
interface TemplateInfo {
code: string;
sourceUrl: string;
range: Range;
isEscaped: boolean;
} }
/** /**
@ -188,27 +271,6 @@ function parseChangeDetectionStrategy<TExpression>(
return enumValue; 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 * Extract the type reference expression from a `forwardRef` function call. For example, the
* expression `forwardRef(function() { return FooDir; })` returns `FooDir`. Note that this * 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 {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 * as o from '@angular/compiler/src/output/output_ast';
import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system';
import {Range} from '../../ast/ast_host'; import {Range} from '../../ast/ast_host';
import {AstObject, AstValue} from '../../ast/ast_value'; import {AstObject, AstValue} from '../../ast/ast_value';
import {FatalLinkerError} from '../../fatal_linker_error'; 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. * A `PartialLinker` that is designed to process `ɵɵngDeclareDirective()` call expressions.
*/ */
export class PartialDirectiveLinkerVersion1<TExpression> implements PartialLinker<TExpression> { export class PartialDirectiveLinkerVersion1<TExpression> implements PartialLinker<TExpression> {
constructor(private sourceUrl: AbsoluteFsPath, private code: string) {}
linkPartialDeclaration( linkPartialDeclaration(
sourceUrl: string, code: string, constantPool: ConstantPool, constantPool: ConstantPool,
metaObj: AstObject<R3PartialDeclaration, TExpression>): o.Expression { 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()); const def = compileDirectiveFromMetadata(meta, constantPool, makeBindingParser());
return def.expression; return def.expression;
} }
@ -32,7 +35,7 @@ export class PartialDirectiveLinkerVersion1<TExpression> implements PartialLinke
*/ */
export function toR3DirectiveMeta<TExpression>( export function toR3DirectiveMeta<TExpression>(
metaObj: AstObject<R3DeclareDirectiveMetadata, TExpression>, code: string, metaObj: AstObject<R3DeclareDirectiveMetadata, TExpression>, code: string,
sourceUrl: string): R3DirectiveMetadata { sourceUrl: AbsoluteFsPath): R3DirectiveMetadata {
const typeExpr = metaObj.getValue('type'); const typeExpr = metaObj.getValue('type');
const typeName = typeExpr.getSymbolName(); const typeName = typeExpr.getSymbolName();
if (typeName === null) { if (typeName === null) {

View File

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

View File

@ -6,7 +6,10 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {satisfies} from 'semver'; 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 {PartialComponentLinkerVersion1} from './partial_component_linker_1';
import {PartialDirectiveLinkerVersion1} from './partial_directive_linker_1'; import {PartialDirectiveLinkerVersion1} from './partial_directive_linker_1';
@ -16,10 +19,11 @@ export const ɵɵngDeclareDirective = 'ɵɵngDeclareDirective';
export const ɵɵngDeclareComponent = 'ɵɵngDeclareComponent'; export const ɵɵngDeclareComponent = 'ɵɵngDeclareComponent';
export const declarationFunctions = [ɵɵngDeclareDirective, ɵɵngDeclareComponent]; export const declarationFunctions = [ɵɵngDeclareDirective, ɵɵngDeclareComponent];
export class PartialLinkerSelector<TExpression> { /**
/** * A helper that selects the appropriate `PartialLinker` for a given declaration.
* A database of linker instances that should be used if their given semver range satisfies the *
* version found in the code to be linked. * 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 * Note that the ranges are checked in order, and the first matching range will be selected, so
* ranges should be most restrictive first. * ranges should be most restrictive first.
@ -30,19 +34,14 @@ export class PartialLinkerSelector<TExpression> {
* Finally, note that we always start with the current version (i.e. `0.0.0-PLACEHOLDER`). This * 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. * allows the linker to work on local builds effectively.
*/ */
private linkers: Record<string, {range: string, linker: PartialLinker<TExpression>}[]> = { export class PartialLinkerSelector<TStatement, TExpression> {
[ɵɵngDeclareDirective]: [ private readonly linkers: Record<string, {range: string, linker: PartialLinker<TExpression>}[]>;
{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)},
],
};
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. * 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` + `Unsupported partial declaration version ${version} for ${functionName}.\n` +
'Valid version ranges are:\n' + versions.map(v => ` - ${v.range}`).join('\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:types",
"//packages/compiler", "//packages/compiler",
"//packages/compiler-cli/linker", "//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", "//packages/compiler-cli/src/ngtsc/translator",
"@npm//typescript", "@npm//typescript",
], ],

View File

@ -9,20 +9,17 @@ import * as o from '@angular/compiler/src/output/output_ast';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {TypeScriptAstFactory} from '../../../../src/ngtsc/translator'; 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 {EmitScope} from '../../../src/file_linker/emit_scopes/emit_scope';
import {LinkerEnvironment} from '../../../src/file_linker/linker_environment'; import {Translator} from '../../../src/file_linker/translator';
import {DEFAULT_LINKER_OPTIONS} from '../../../src/file_linker/linker_options';
import {generate} from '../helpers'; import {generate} from '../helpers';
describe('EmitScope', () => { describe('EmitScope', () => {
describe('translateDefinition()', () => { describe('translateDefinition()', () => {
it('should translate the given output AST into a TExpression', () => { it('should translate the given output AST into a TExpression', () => {
const factory = new TypeScriptAstFactory(); const factory = new TypeScriptAstFactory();
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>( const translator = new Translator<ts.Statement, ts.Expression>(factory);
new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS);
const ngImport = factory.createIdentifier('core'); 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')); const def = emitScope.translateDefinition(o.fn([], [], null, null, 'foo'));
expect(generate(def)).toEqual('function foo() { }'); expect(generate(def)).toEqual('function foo() { }');
@ -30,10 +27,9 @@ describe('EmitScope', () => {
it('should use the `ngImport` idenfifier for imports when translating', () => { it('should use the `ngImport` idenfifier for imports when translating', () => {
const factory = new TypeScriptAstFactory(); const factory = new TypeScriptAstFactory();
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>( const translator = new Translator<ts.Statement, ts.Expression>(factory);
new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS);
const ngImport = factory.createIdentifier('core'); 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 coreImportRef = new o.ExternalReference('@angular/core', 'foo');
const def = emitScope.translateDefinition(o.importExpr(coreImportRef).callMethod('bar', [])); 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', () => { it('should not emit any shared constants in the replacement expression', () => {
const factory = new TypeScriptAstFactory(); const factory = new TypeScriptAstFactory();
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>( const translator = new Translator<ts.Statement, ts.Expression>(factory);
new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS);
const ngImport = factory.createIdentifier('core'); 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')]); const constArray = o.literalArr([o.literal('CONST')]);
// We have to add the constant twice or it will not create a shared statement // We have to add the constant twice or it will not create a shared statement
@ -60,10 +55,9 @@ describe('EmitScope', () => {
describe('getConstantStatements()', () => { describe('getConstantStatements()', () => {
it('should return any constant statements that were added to the `constantPool`', () => { it('should return any constant statements that were added to the `constantPool`', () => {
const factory = new TypeScriptAstFactory(); const factory = new TypeScriptAstFactory();
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>( const translator = new Translator<ts.Statement, ts.Expression>(factory);
new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS);
const ngImport = factory.createIdentifier('core'); 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')]); const constArray = o.literalArr([o.literal('CONST')]);
// We have to add the constant twice or it will not create a shared statement // 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 * as ts from 'typescript';
import {TypeScriptAstFactory} from '../../../../src/ngtsc/translator'; 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 {IifeEmitScope} from '../../../src/file_linker/emit_scopes/iife_emit_scope';
import {LinkerEnvironment} from '../../../src/file_linker/linker_environment'; import {Translator} from '../../../src/file_linker/translator';
import {DEFAULT_LINKER_OPTIONS} from '../../../src/file_linker/linker_options';
import {generate} from '../helpers'; import {generate} from '../helpers';
describe('IifeEmitScope', () => { describe('IifeEmitScope', () => {
describe('translateDefinition()', () => { describe('translateDefinition()', () => {
it('should translate the given output AST into a TExpression, wrapped in an IIFE', () => { it('should translate the given output AST into a TExpression, wrapped in an IIFE', () => {
const factory = new TypeScriptAstFactory(); const factory = new TypeScriptAstFactory();
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>( const translator = new Translator<ts.Statement, ts.Expression>(factory);
new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS);
const ngImport = factory.createIdentifier('core'); 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')); const def = emitScope.translateDefinition(o.fn([], [], null, null, 'foo'));
expect(generate(def)).toEqual('function () { return function 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', () => { it('should use the `ngImport` idenfifier for imports when translating', () => {
const factory = new TypeScriptAstFactory(); const factory = new TypeScriptAstFactory();
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>( const translator = new Translator<ts.Statement, ts.Expression>(factory);
new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS);
const ngImport = factory.createIdentifier('core'); 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 coreImportRef = new o.ExternalReference('@angular/core', 'foo');
const def = emitScope.translateDefinition(o.importExpr(coreImportRef).callMethod('bar', [])); 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', () => { it('should emit any shared constants in the replacement expression IIFE', () => {
const factory = new TypeScriptAstFactory(); const factory = new TypeScriptAstFactory();
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>( const translator = new Translator<ts.Statement, ts.Expression>(factory);
new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS);
const ngImport = factory.createIdentifier('core'); 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')]); const constArray = o.literalArr([o.literal('CONST')]);
// We have to add the constant twice or it will not create a shared statement // We have to add the constant twice or it will not create a shared statement
@ -61,10 +59,10 @@ describe('IifeEmitScope', () => {
describe('getConstantStatements()', () => { describe('getConstantStatements()', () => {
it('should throw an error', () => { it('should throw an error', () => {
const factory = new TypeScriptAstFactory(); const factory = new TypeScriptAstFactory();
const linkerEnvironment = LinkerEnvironment.create<ts.Statement, ts.Expression>( const translator = new Translator<ts.Statement, ts.Expression>(factory);
new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS);
const ngImport = factory.createIdentifier('core'); 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(); expect(() => emitScope.getConstantStatements()).toThrowError();
}); });
}); });

View File

@ -8,6 +8,8 @@
import * as o from '@angular/compiler/src/output/output_ast'; import * as o from '@angular/compiler/src/output/output_ast';
import * as ts from 'typescript'; 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 {TypeScriptAstFactory} from '../../../src/ngtsc/translator';
import {AstHost} from '../../src/ast/ast_host'; import {AstHost} from '../../src/ast/ast_host';
import {TypeScriptAstHost} from '../../src/ast/typescript/typescript_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 {LinkerEnvironment} from '../../src/file_linker/linker_environment';
import {DEFAULT_LINKER_OPTIONS} from '../../src/file_linker/linker_options'; import {DEFAULT_LINKER_OPTIONS} from '../../src/file_linker/linker_options';
import {PartialDirectiveLinkerVersion1} from '../../src/file_linker/partial_linkers/partial_directive_linker_1'; import {PartialDirectiveLinkerVersion1} from '../../src/file_linker/partial_linkers/partial_directive_linker_1';
import {generate} from './helpers'; import {generate} from './helpers';
describe('FileLinker', () => { describe('FileLinker', () => {
@ -91,7 +94,7 @@ describe('FileLinker', () => {
expect(compilationResult).toEqual(factory.createLiteral('compilation result')); expect(compilationResult).toEqual(factory.createLiteral('compilation result'));
expect(compileSpy).toHaveBeenCalled(); 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>, host: AstHost<ts.Expression>,
fileLinker: FileLinker<MockConstantScopeRef, ts.Statement, 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>( 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>( 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}; return {host: linkerEnvironment.host, fileLinker};
} }
}); });
@ -185,7 +190,7 @@ class MockConstantScopeRef {
function spyOnLinkPartialDeclarationWithConstants(replacement: o.Expression) { function spyOnLinkPartialDeclarationWithConstants(replacement: o.Expression) {
let callCount = 0; let callCount = 0;
spyOn(PartialDirectiveLinkerVersion1.prototype, 'linkPartialDeclaration') spyOn(PartialDirectiveLinkerVersion1.prototype, 'linkPartialDeclaration')
.and.callFake(((sourceUrl, code, constantPool) => { .and.callFake((constantPool => {
const constArray = o.literalArr([o.literal(++callCount)]); const constArray = o.literalArr([o.literal(++callCount)]);
// We have to add the constant twice or it will not create a shared statement // We have to add the constant twice or it will not create a shared statement
constantPool.getConstLiteral(constArray); constantPool.getConstLiteral(constArray);

View File

@ -5,8 +5,15 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import * as ts from 'typescript';
import {LinkerOptions} from '../../..'; 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 {PartialComponentLinkerVersion1} from '../../../src/file_linker/partial_linkers/partial_component_linker_1';
import {PartialDirectiveLinkerVersion1} from '../../../src/file_linker/partial_linkers/partial_directive_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'; import {PartialLinkerSelector} from '../../../src/file_linker/partial_linkers/partial_linker_selector';
@ -16,12 +23,23 @@ describe('PartialLinkerSelector', () => {
i18nNormalizeLineEndingsInICUs: true, i18nNormalizeLineEndingsInICUs: true,
enableI18nLegacyMessageIdFormat: false, enableI18nLegacyMessageIdFormat: false,
i18nUseExternalIds: 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()', () => { describe('supportsDeclaration()', () => {
it('should return true if there is at least one linker that matches the given function name', 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('ɵɵngDeclareDirective')).toBe(true);
expect(selector.supportsDeclaration('ɵɵngDeclareComponent')).toBe(true); expect(selector.supportsDeclaration('ɵɵngDeclareComponent')).toBe(true);
expect(selector.supportsDeclaration('$foo')).toBe(false); expect(selector.supportsDeclaration('$foo')).toBe(false);
@ -30,7 +48,8 @@ describe('PartialLinkerSelector', () => {
describe('getLinker()', () => { describe('getLinker()', () => {
it('should return the latest linker if the version is "0.0.0-PLACEHOLDER"', () => { 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')) expect(selector.getLinker('ɵɵngDeclareDirective', '0.0.0-PLACEHOLDER'))
.toBeInstanceOf(PartialDirectiveLinkerVersion1); .toBeInstanceOf(PartialDirectiveLinkerVersion1);
expect(selector.getLinker('ɵɵngDeclareComponent', '0.0.0-PLACEHOLDER')) 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', () => { 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')) expect(selector.getLinker('ɵɵngDeclareDirective', '11.1.2'))
.toBeInstanceOf(PartialDirectiveLinkerVersion1); .toBeInstanceOf(PartialDirectiveLinkerVersion1);
expect(selector.getLinker('ɵɵngDeclareDirective', '11.2.5')) 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', () => { 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')) expect(selector.getLinker('ɵɵngDeclareDirective', '11.1.0-next.1'))
.toBeInstanceOf(PartialDirectiveLinkerVersion1); .toBeInstanceOf(PartialDirectiveLinkerVersion1);
expect(selector.getLinker('ɵɵngDeclareDirective', '11.1.0-next.7')) 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', () => { 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 // `$foo` is not a valid name, even though `0.0.0-PLACEHOLDER` is a valid version
expect(() => selector.getLinker('$foo', '0.0.0-PLACEHOLDER')) expect(() => selector.getLinker('$foo', '0.0.0-PLACEHOLDER'))
.toThrowError('Unknown partial declaration function $foo.'); .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`. * 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. * @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 logger = new ConsoleLogger(LogLevel.debug);
const loader = new SourceFileLoader(fs, logger, {}); const loader = new SourceFileLoader(fileSystem, logger, {});
const builtDirectory = getBuildOutputDirectory(fs); const builtDirectory = getBuildOutputDirectory(fileSystem);
const linkerPlugin = createEs2015LinkerPlugin({ const linkerPlugin = createEs2015LinkerPlugin({
fileSystem,
logger,
// By default we don't render legacy message ids in compliance tests. // By default we don't render legacy message ids in compliance tests.
enableI18nLegacyMessageIdFormat: false, enableI18nLegacyMessageIdFormat: false,
sourceMapping: test.compilerOptions?.sourceMap === true,
...test.angularCompilerOptions ...test.angularCompilerOptions
}); });
const goldenPartialPath = fs.resolve('/GOLDEN_PARTIAL.js'); const goldenPartialPath = fileSystem.resolve('/GOLDEN_PARTIAL.js');
if (!fs.exists(goldenPartialPath)) { if (!fileSystem.exists(goldenPartialPath)) {
throw new Error( throw new Error(
'Golden partial does not exist for this test\n' + 'Golden partial does not exist for this test\n' +
'Try generating it by running:\n' + 'Try generating it by running:\n' +
`bazel run //packages/compiler-cli/test/compliance/test_cases:${ `bazel run //packages/compiler-cli/test/compliance/test_cases:${
test.relativePath}.golden.update`); test.relativePath}.golden.update`);
} }
const partialFile = fs.readFile(goldenPartialPath); const partialFile = fileSystem.readFile(goldenPartialPath);
const partialFiles = parseGoldenPartial(partialFile); 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 expectation of test.expectations) {
for (const {generated: fileName} of expectation.files) { for (const {generated} of expectation.files) {
const partialPath = fs.resolve(builtDirectory, fileName); const fileName = fileSystem.resolve(builtDirectory, generated);
if (!fs.exists(partialPath)) { if (!fileSystem.exists(fileName)) {
continue; continue;
} }
const source = fs.readFile(partialPath); const source = fileSystem.readFile(fileName);
const sourceMapPath = fs.resolve(partialPath + '.map'); const sourceMapPath = fileSystem.resolve(fileName + '.map');
const sourceMap = const sourceMap = fileSystem.exists(sourceMapPath) ?
fs.exists(sourceMapPath) ? JSON.parse(fs.readFile(sourceMapPath)) : undefined; JSON.parse(fileSystem.readFile(sourceMapPath)) :
undefined;
const {linkedSource, linkedSourceMap} = const {linkedSource, linkedSourceMap} =
applyLinker({path: partialPath, source, sourceMap}, linkerPlugin); applyLinker(builtDirectory, fileName, source, sourceMap, linkerPlugin);
if (linkedSourceMap !== undefined) { if (linkedSourceMap !== undefined) {
const mapAndPath: MapAndPath = {map: linkedSourceMap, mapPath: sourceMapPath}; const mapAndPath: MapAndPath = {map: linkedSourceMap, mapPath: sourceMapPath};
const sourceFile = loader.loadSourceFile(partialPath, linkedSource, mapAndPath); const sourceFile = loader.loadSourceFile(fileName, linkedSource, mapAndPath);
safeWrite(fs, sourceMapPath, JSON.stringify(sourceFile.renderFlattenedSourceMap())); safeWrite(fileSystem, sourceMapPath, JSON.stringify(sourceFile.renderFlattenedSourceMap()));
} }
safeWrite(fs, partialPath, linkedSource); safeWrite(fileSystem, fileName, linkedSource);
} }
} }
return {emittedFiles: [], errors: []}; 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. * @returns The file's source content, which has been transformed using the linker if necessary.
*/ */
function applyLinker( 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} { linkerPlugin: PluginObj): {linkedSource: string, linkedSourceMap: RawSourceMap|undefined} {
if (!file.path.endsWith('.js') || !needsLinking(file.path, file.source)) { if (!filename.endsWith('.js') || !needsLinking(filename, source)) {
return {linkedSource: file.source, linkedSourceMap: file.sourceMap}; return {linkedSource: source, linkedSourceMap: sourceMap};
} }
const result = transformSync(file.source, { const result = transformSync(source, {
filename: file.path, cwd,
sourceMaps: !!file.sourceMap, filename,
sourceMaps: !!sourceMap,
plugins: [linkerPlugin], plugins: [linkerPlugin],
parserOpts: {sourceType: 'unambiguous'}, parserOpts: {sourceType: 'unambiguous'},
}); });

View File

@ -8,6 +8,8 @@ ts_library(
"//packages:types", "//packages:types",
"//packages/compiler-cli/linker", "//packages/compiler-cli/linker",
"//packages/compiler-cli/linker/babel", "//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", "//packages/compiler-cli/test/compliance_old/mock_compile",
"@npm//@babel/core", "@npm//@babel/core",
"@npm//@babel/generator", "@npm//@babel/generator",

View File

@ -10,6 +10,8 @@ import * as ts from 'typescript';
import {needsLinking} from '../../../linker'; import {needsLinking} from '../../../linker';
import {createEs2015LinkerPlugin} from '../../../linker/babel'; 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'; 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 compiledFiles = compileFiles(data, angularFiles, {...options, compilationMode: 'partial'});
const fileSystem = new MockFileSystemNative();
const logger = new MockLogger();
const linkerPlugin = createEs2015LinkerPlugin({ const linkerPlugin = createEs2015LinkerPlugin({
fileSystem,
logger,
// enableI18nLegacyMessageIdFormat defaults to false in `compileFiles`. // enableI18nLegacyMessageIdFormat defaults to false in `compileFiles`.
enableI18nLegacyMessageIdFormat: false, enableI18nLegacyMessageIdFormat: false,
...options, ...options,