From 266cc9b162d92f5998de1e574eb66b26c0e6f98f Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Mon, 21 Dec 2020 10:06:23 +0000 Subject: [PATCH] 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 --- packages/compiler-cli/linker/BUILD.bazel | 3 + .../compiler-cli/linker/babel/BUILD.bazel | 2 + .../linker/babel/src/es2015_linker_plugin.ts | 46 ++- .../linker/babel/src/linker_plugin_options.ts | 22 ++ .../linker/babel/test/BUILD.bazel | 2 + .../babel/test/es2015_linker_plugin_spec.ts | 55 +++- .../src/file_linker/emit_scopes/emit_scope.ts | 9 +- .../emit_scopes/iife_emit_scope.ts | 20 +- .../linker/src/file_linker/file_linker.ts | 17 +- .../linker/src/file_linker/get_source_file.ts | 36 ++ .../src/file_linker/linker_environment.ts | 18 +- .../linker/src/file_linker/linker_options.ts | 7 + .../partial_component_linker_1.ts | 310 +++++++++++------- .../partial_directive_linker_1.ts | 9 +- .../partial_linkers/partial_linker.ts | 3 +- .../partial_linker_selector.ts | 73 +++-- packages/compiler-cli/linker/test/BUILD.bazel | 3 + .../emit_scopes/emit_scope_spec.ts | 24 +- .../emit_scopes/iief_emit_scope_spec.ts | 28 +- .../test/file_linker/file_linker_spec.ts | 13 +- .../partial_linker_selector_spec.ts | 32 +- .../compliance/linked/linked_compile_spec.ts | 56 ++-- .../test/compliance_old/prelink/BUILD.bazel | 2 + .../test/compliance_old/prelink/bootstrap.ts | 7 +- 24 files changed, 532 insertions(+), 265 deletions(-) create mode 100644 packages/compiler-cli/linker/babel/src/linker_plugin_options.ts create mode 100644 packages/compiler-cli/linker/src/file_linker/get_source_file.ts diff --git a/packages/compiler-cli/linker/BUILD.bazel b/packages/compiler-cli/linker/BUILD.bazel index 15295b4a20..f114a02c97 100644 --- a/packages/compiler-cli/linker/BUILD.bazel +++ b/packages/compiler-cli/linker/BUILD.bazel @@ -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", diff --git a/packages/compiler-cli/linker/babel/BUILD.bazel b/packages/compiler-cli/linker/babel/BUILD.bazel index b5a15beef3..b8798073d9 100644 --- a/packages/compiler-cli/linker/babel/BUILD.bazel +++ b/packages/compiler-cli/linker/babel/BUILD.bazel @@ -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", diff --git a/packages/compiler-cli/linker/babel/src/es2015_linker_plugin.ts b/packages/compiler-cli/linker/babel/src/es2015_linker_plugin.ts index 0ebe7b5db5..17abf1ad33 100644 --- a/packages/compiler-cli/linker/babel/src/es2015_linker_plugin.ts +++ b/packages/compiler-cli/linker/babel/src/es2015_linker_plugin.ts @@ -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 = {}): PluginObj { +export function createEs2015LinkerPlugin({fileSystem, logger, ...options}: LinkerPluginOptions): + PluginObj { let fileLinker: FileLinker|null = null; - const linkerEnvironment = LinkerEnvironment.create( - new BabelAstHost(), new BabelAstFactory(), options); - return { visitor: { Program: { @@ -36,8 +36,19 @@ export function createEs2015LinkerPlugin(options: Partial = {}): */ enter(path: NodePath): 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( + fileSystem, logger, new BabelAstHost(), new BabelAstFactory(sourceUrl), options); + fileLinker = new FileLinker(linkerEnvironment, sourceUrl, file.code); }, /** @@ -66,11 +77,7 @@ export function createEs2015LinkerPlugin(options: Partial = {}): } 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, statements: t.Statement } } +function getCalleeName(call: NodePath): 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; } diff --git a/packages/compiler-cli/linker/babel/src/linker_plugin_options.ts b/packages/compiler-cli/linker/babel/src/linker_plugin_options.ts new file mode 100644 index 0000000000..49dbd839a3 --- /dev/null +++ b/packages/compiler-cli/linker/babel/src/linker_plugin_options.ts @@ -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 { + /** + * File-system, used to load up the input source-map and content. + */ + fileSystem: FileSystem; + + /** + * Logger used by the linker. + */ + logger: Logger; +} diff --git a/packages/compiler-cli/linker/babel/test/BUILD.bazel b/packages/compiler-cli/linker/babel/test/BUILD.bazel index f2a426de88..f1a48537c3 100644 --- a/packages/compiler-cli/linker/babel/test/BUILD.bazel +++ b/packages/compiler-cli/linker/babel/test/BUILD.bazel @@ -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", diff --git a/packages/compiler-cli/linker/babel/test/es2015_linker_plugin_spec.ts b/packages/compiler-cli/linker/babel/test/es2015_linker_plugin_spec.ts index 0019ada2ba..21a8b63402 100644 --- a/packages/compiler-cli/linker/babel/test/es2015_linker_plugin_spec.ts +++ b/packages/compiler-cli/linker/babel/test/es2015_linker_plugin_spec.ts @@ -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); diff --git a/packages/compiler-cli/linker/src/file_linker/emit_scopes/emit_scope.ts b/packages/compiler-cli/linker/src/file_linker/emit_scopes/emit_scope.ts index 75f47067ce..4402e160df 100644 --- a/packages/compiler-cli/linker/src/file_linker/emit_scopes/emit_scope.ts +++ b/packages/compiler-cli/linker/src/file_linker/emit_scopes/emit_scope.ts @@ -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 { constructor( protected readonly ngImport: TExpression, - protected readonly linkerEnvironment: LinkerEnvironment) {} + protected readonly translator: Translator) {} /** * Translate the given Output AST definition expression into a generic `TExpression`. @@ -32,7 +32,7 @@ export class EmitScope { * 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 { * 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)); } } diff --git a/packages/compiler-cli/linker/src/file_linker/emit_scopes/iife_emit_scope.ts b/packages/compiler-cli/linker/src/file_linker/emit_scopes/iife_emit_scope.ts index 18d0982d2d..d98484453c 100644 --- a/packages/compiler-cli/linker/src/file_linker/emit_scopes/iife_emit_scope.ts +++ b/packages/compiler-cli/linker/src/file_linker/emit_scopes/iife_emit_scope.ts @@ -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 extends EmitScope { + constructor( + ngImport: TExpression, translator: Translator, + private readonly factory: AstFactory) { + super(ngImport, translator); + } + /** * Translate the given Output AST definition expression into a generic `TExpression`. * @@ -21,13 +31,13 @@ export class IifeEmitScope extends EmitScope = [] as const; * This class is responsible for linking all the partial declarations found in a single file. */ export class FileLinker { - private linkerSelector = new PartialLinkerSelector(this.linkerEnvironment.options); + private linkerSelector: PartialLinkerSelector; private emitScopes = new Map>(); constructor( private linkerEnvironment: LinkerEnvironment, - private sourceUrl: string, readonly code: string) {} + sourceUrl: AbsoluteFsPath, code: string) { + this.linkerSelector = + new PartialLinkerSelector(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 { 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 { 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)!; } diff --git a/packages/compiler-cli/linker/src/file_linker/get_source_file.ts b/packages/compiler-cli/linker/src/file_linker/get_source_file.ts new file mode 100644 index 0000000000..f7764e2c86 --- /dev/null +++ b/packages/compiler-cli/linker/src/file_linker/get_source_file.ts @@ -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; + }; + } +} diff --git a/packages/compiler-cli/linker/src/file_linker/linker_environment.ts b/packages/compiler-cli/linker/src/file_linker/linker_environment.ts index ff1401b028..3782ddeec1 100644 --- a/packages/compiler-cli/linker/src/file_linker/linker_environment.ts +++ b/packages/compiler-cli/linker/src/file_linker/linker_environment.ts @@ -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 { readonly translator = new Translator(this.factory); + readonly sourceFileLoader = + this.options.sourceMapping ? new SourceFileLoader(this.fileSystem, this.logger, {}) : null; + private constructor( - readonly host: AstHost, readonly factory: AstFactory, - readonly options: LinkerOptions) {} + readonly fileSystem: FileSystem, readonly logger: Logger, readonly host: AstHost, + readonly factory: AstFactory, readonly options: LinkerOptions) {} static create( - host: AstHost, factory: AstFactory, + fileSystem: FileSystem, logger: Logger, host: AstHost, + factory: AstFactory, options: Partial): LinkerEnvironment { - 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 }); } } diff --git a/packages/compiler-cli/linker/src/file_linker/linker_options.ts b/packages/compiler-cli/linker/src/file_linker/linker_options.ts index 9c4d8feed0..120be4d417 100644 --- a/packages/compiler-cli/linker/src/file_linker/linker_options.ts +++ b/packages/compiler-cli/linker/src/file_linker/linker_options.ts @@ -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, }; diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts index d309596fb4..c453286976 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts @@ -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 implements PartialLinker { - constructor(private readonly options: LinkerOptions) {} +export class PartialComponentLinkerVersion1 implements + PartialLinker { + 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, + private readonly getSourceFile: GetSourceFileFn, private sourceUrl: AbsoluteFsPath, + private code: string) {} linkPartialDeclaration( - sourceUrl: string, code: string, constantPool: ConstantPool, + constantPool: ConstantPool, metaObj: AstObject): 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): + 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(); + 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, 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, + {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( - metaObj: AstObject, 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(); - 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( return enumValue; } -/** - * Update the range to remove the start and end chars, which should be quotes around the template. - */ -function getTemplateRange( - templateNode: AstValue, 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 diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_directive_linker_1.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_directive_linker_1.ts index 7fe5330434..283c1b9474 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_directive_linker_1.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_directive_linker_1.ts @@ -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 implements PartialLinker { + constructor(private sourceUrl: AbsoluteFsPath, private code: string) {} + linkPartialDeclaration( - sourceUrl: string, code: string, constantPool: ConstantPool, + constantPool: ConstantPool, metaObj: AstObject): 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 implements PartialLinke */ export function toR3DirectiveMeta( metaObj: AstObject, code: string, - sourceUrl: string): R3DirectiveMetadata { + sourceUrl: AbsoluteFsPath): R3DirectiveMetadata { const typeExpr = metaObj.getValue('type'); const typeName = typeExpr.getSymbolName(); if (typeName === null) { diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker.ts index 35eb33e595..c75dbc2d93 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker.ts @@ -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 { * `R3DeclareComponentMetadata` interfaces. */ linkPartialDeclaration( - sourceUrl: string, code: string, constantPool: ConstantPool, + constantPool: ConstantPool, metaObj: AstObject): o.Expression; } diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker_selector.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker_selector.ts index 6d24c3d127..81e494ecf5 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker_selector.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_linker_selector.ts @@ -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 { - /** - * 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}[]> = { - [ɵɵ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 { + private readonly linkers: Record}[]>; - constructor(private options: LinkerOptions) {} + constructor( + environment: LinkerEnvironment, 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 { `Unsupported partial declaration version ${version} for ${functionName}.\n` + 'Valid version ranges are:\n' + versions.map(v => ` - ${v.range}`).join('\n')); } + + private createLinkerMap( + environment: LinkerEnvironment, 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}, + ], + }; + } } diff --git a/packages/compiler-cli/linker/test/BUILD.bazel b/packages/compiler-cli/linker/test/BUILD.bazel index 5a3008dcc0..e917f02c74 100644 --- a/packages/compiler-cli/linker/test/BUILD.bazel +++ b/packages/compiler-cli/linker/test/BUILD.bazel @@ -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", ], diff --git a/packages/compiler-cli/linker/test/file_linker/emit_scopes/emit_scope_spec.ts b/packages/compiler-cli/linker/test/file_linker/emit_scopes/emit_scope_spec.ts index 3484f3978d..fe9b82ebfb 100644 --- a/packages/compiler-cli/linker/test/file_linker/emit_scopes/emit_scope_spec.ts +++ b/packages/compiler-cli/linker/test/file_linker/emit_scopes/emit_scope_spec.ts @@ -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( - new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS); + const translator = new Translator(factory); const ngImport = factory.createIdentifier('core'); - const emitScope = new EmitScope(ngImport, linkerEnvironment); + const emitScope = new EmitScope(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( - new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS); + const translator = new Translator(factory); const ngImport = factory.createIdentifier('core'); - const emitScope = new EmitScope(ngImport, linkerEnvironment); + const emitScope = new EmitScope(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( - new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS); + const translator = new Translator(factory); const ngImport = factory.createIdentifier('core'); - const emitScope = new EmitScope(ngImport, linkerEnvironment); + const emitScope = new EmitScope(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( - new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS); + const translator = new Translator(factory); const ngImport = factory.createIdentifier('core'); - const emitScope = new EmitScope(ngImport, linkerEnvironment); + const emitScope = new EmitScope(ngImport, translator); const constArray = o.literalArr([o.literal('CONST')]); // We have to add the constant twice or it will not create a shared statement diff --git a/packages/compiler-cli/linker/test/file_linker/emit_scopes/iief_emit_scope_spec.ts b/packages/compiler-cli/linker/test/file_linker/emit_scopes/iief_emit_scope_spec.ts index b4d12f8269..4873fd97b9 100644 --- a/packages/compiler-cli/linker/test/file_linker/emit_scopes/iief_emit_scope_spec.ts +++ b/packages/compiler-cli/linker/test/file_linker/emit_scopes/iief_emit_scope_spec.ts @@ -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( - new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS); + const translator = new Translator(factory); const ngImport = factory.createIdentifier('core'); - const emitScope = new IifeEmitScope(ngImport, linkerEnvironment); + const emitScope = + new IifeEmitScope(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( - new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS); + const translator = new Translator(factory); const ngImport = factory.createIdentifier('core'); - const emitScope = new IifeEmitScope(ngImport, linkerEnvironment); + const emitScope = + new IifeEmitScope(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( - new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS); + const translator = new Translator(factory); const ngImport = factory.createIdentifier('core'); - const emitScope = new IifeEmitScope(ngImport, linkerEnvironment); + const emitScope = + new IifeEmitScope(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( - new TypeScriptAstHost(), factory, DEFAULT_LINKER_OPTIONS); + const translator = new Translator(factory); const ngImport = factory.createIdentifier('core'); - const emitScope = new IifeEmitScope(ngImport, linkerEnvironment); + const emitScope = + new IifeEmitScope(ngImport, translator, factory); expect(() => emitScope.getConstantStatements()).toThrowError(); }); }); diff --git a/packages/compiler-cli/linker/test/file_linker/file_linker_spec.ts b/packages/compiler-cli/linker/test/file_linker/file_linker_spec.ts index b0cfd9b261..39b0e2f78c 100644 --- a/packages/compiler-cli/linker/test/file_linker/file_linker_spec.ts +++ b/packages/compiler-cli/linker/test/file_linker/file_linker_spec.ts @@ -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, fileLinker: FileLinker } { + const fs = new MockFileSystemNative(); + const logger = new MockLogger(); const linkerEnvironment = LinkerEnvironment.create( - new TypeScriptAstHost(), new TypeScriptAstFactory(), DEFAULT_LINKER_OPTIONS); + fs, logger, new TypeScriptAstHost(), new TypeScriptAstFactory(), DEFAULT_LINKER_OPTIONS); const fileLinker = new FileLinker( - 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); diff --git a/packages/compiler-cli/linker/test/file_linker/partial_linkers/partial_linker_selector_spec.ts b/packages/compiler-cli/linker/test/file_linker/partial_linkers/partial_linker_selector_spec.ts index 8a0426e9cb..f8d524881a 100644 --- a/packages/compiler-cli/linker/test/file_linker/partial_linkers/partial_linker_selector_spec.ts +++ b/packages/compiler-cli/linker/test/file_linker/partial_linkers/partial_linker_selector_spec.ts @@ -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; + let fs: FileSystem; + + beforeEach(() => { + fs = new MockFileSystemNative(); + const logger = new MockLogger(); + environment = LinkerEnvironment.create( + 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.'); diff --git a/packages/compiler-cli/test/compliance/linked/linked_compile_spec.ts b/packages/compiler-cli/test/compliance/linked/linked_compile_spec.ts index 4e09b1397a..bbe1379f7e 100644 --- a/packages/compiler-cli/test/compliance/linked/linked_compile_spec.ts +++ b/packages/compiler-cli/test/compliance/linked/linked_compile_spec.ts @@ -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'}, }); diff --git a/packages/compiler-cli/test/compliance_old/prelink/BUILD.bazel b/packages/compiler-cli/test/compliance_old/prelink/BUILD.bazel index 99873782b4..3953c78dc9 100644 --- a/packages/compiler-cli/test/compliance_old/prelink/BUILD.bazel +++ b/packages/compiler-cli/test/compliance_old/prelink/BUILD.bazel @@ -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", diff --git a/packages/compiler-cli/test/compliance_old/prelink/bootstrap.ts b/packages/compiler-cli/test/compliance_old/prelink/bootstrap.ts index 262df85489..3830156273 100644 --- a/packages/compiler-cli/test/compliance_old/prelink/bootstrap.ts +++ b/packages/compiler-cli/test/compliance_old/prelink/bootstrap.ts @@ -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,