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:
parent
0fd06e5ab8
commit
266cc9b162
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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)!;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.');
|
||||
|
|
|
@ -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'},
|
||||
});
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue