diff --git a/packages/compiler-cli/linker/index.ts b/packages/compiler-cli/linker/index.ts index 96256b7b43..ae0962151a 100644 --- a/packages/compiler-cli/linker/index.ts +++ b/packages/compiler-cli/linker/index.ts @@ -12,3 +12,4 @@ export {DeclarationScope} from './src/file_linker/declaration_scope'; export {FileLinker} from './src/file_linker/file_linker'; export {LinkerEnvironment} from './src/file_linker/linker_environment'; export {LinkerOptions} from './src/file_linker/linker_options'; +export {needsLinking} from './src/file_linker/needs_linking'; diff --git a/packages/compiler-cli/linker/src/file_linker/needs_linking.ts b/packages/compiler-cli/linker/src/file_linker/needs_linking.ts new file mode 100644 index 0000000000..1a0901e519 --- /dev/null +++ b/packages/compiler-cli/linker/src/file_linker/needs_linking.ts @@ -0,0 +1,28 @@ +/** + * @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 {declarationFunctions} from './partial_linkers/partial_linker_selector'; + +/** + * Determines if the provided source file may need to be processed by the linker, i.e. whether it + * potentially contains any declarations. If true is returned, then the source file should be + * processed by the linker as it may contain declarations that need to be fully compiled. If false + * is returned, parsing and processing of the source file can safely be skipped to improve + * performance. + * + * This function may return true even for source files that don't actually contain any declarations + * that need to be compiled. + * + * @param path the absolute path of the source file for which to determine whether linking may be + * needed. + * @param source the source file content as a string. + * @returns whether the source file may contain declarations that need to be linked. + */ +export function needsLinking(path: string, source: string): boolean { + return declarationFunctions.some(fn => source.includes(fn)); +} 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 12cce3a994..6d24c3d127 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 @@ -12,6 +12,10 @@ import {PartialComponentLinkerVersion1} from './partial_component_linker_1'; import {PartialDirectiveLinkerVersion1} from './partial_directive_linker_1'; import {PartialLinker} from './partial_linker'; +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 @@ -27,11 +31,11 @@ export class PartialLinkerSelector { * allows the linker to work on local builds effectively. */ private linkers: Record}[]> = { - 'ɵɵngDeclareDirective': [ + [ɵɵngDeclareDirective]: [ {range: '0.0.0-PLACEHOLDER', linker: new PartialDirectiveLinkerVersion1()}, {range: '>=11.1.0-next.1', linker: new PartialDirectiveLinkerVersion1()}, ], - 'ɵɵngDeclareComponent': + [ɵɵngDeclareComponent]: [ {range: '0.0.0-PLACEHOLDER', linker: new PartialComponentLinkerVersion1(this.options)}, {range: '>=11.1.0-next.1', linker: new PartialComponentLinkerVersion1(this.options)}, diff --git a/packages/compiler-cli/linker/test/file_linker/needs_linking_spec.ts b/packages/compiler-cli/linker/test/file_linker/needs_linking_spec.ts new file mode 100644 index 0000000000..b83842939e --- /dev/null +++ b/packages/compiler-cli/linker/test/file_linker/needs_linking_spec.ts @@ -0,0 +1,67 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {needsLinking} from '../../src/file_linker/needs_linking'; + +describe('needsLinking', () => { + it('should return true for directive declarations', () => { + expect(needsLinking('file.js', ` + export class Dir { + ɵdir = ɵɵngDeclareDirective({type: Dir}); + } + `)).toBeTrue(); + }); + + it('should return true for namespaced directive declarations', () => { + expect(needsLinking('file.js', ` + export class Dir { + ɵdir = ng.ɵɵngDeclareDirective({type: Dir}); + } + `)).toBeTrue(); + }); + + it('should return true for unrelated usages of ɵɵngDeclareDirective', () => { + expect(needsLinking('file.js', ` + const fnName = 'ɵɵngDeclareDirective'; + `)).toBeTrue(); + }); + + it('should return false when the file does not contain ɵɵngDeclareDirective', () => { + expect(needsLinking('file.js', ` + const foo = ngDeclareDirective; + `)).toBeFalse(); + }); + + it('should return true for component declarations', () => { + expect(needsLinking('file.js', ` + export class Cmp { + ɵdir = ɵɵngDeclareComponent({type: Cmp}); + } + `)).toBeTrue(); + }); + + it('should return true for namespaced component declarations', () => { + expect(needsLinking('file.js', ` + export class Cmp { + ɵdir = ng.ɵɵngDeclareComponent({type: Cmp}); + } + `)).toBeTrue(); + }); + + it('should return true for unrelated usages of ɵɵngDeclareComponent', () => { + expect(needsLinking('file.js', ` + const fnName = 'ɵɵngDeclareComponent'; + `)).toBeTrue(); + }); + + it('should return false when the file does not contain ɵɵngDeclareComponent', () => { + expect(needsLinking('file.js', ` + const foo = ngDeclareComponent; + `)).toBeFalse(); + }); +}); diff --git a/packages/compiler-cli/test/compliance/linked/BUILD.bazel b/packages/compiler-cli/test/compliance/linked/BUILD.bazel index ea2c513b98..7f2f0b16fe 100644 --- a/packages/compiler-cli/test/compliance/linked/BUILD.bazel +++ b/packages/compiler-cli/test/compliance/linked/BUILD.bazel @@ -5,6 +5,7 @@ ts_library( testonly = True, srcs = ["linked_compile_spec.ts"], deps = [ + "//packages/compiler-cli/linker", "//packages/compiler-cli/linker/babel", "//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler-cli/src/ngtsc/logging", 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 2994347cdc..4e09b1397a 100644 --- a/packages/compiler-cli/test/compliance/linked/linked_compile_spec.ts +++ b/packages/compiler-cli/test/compliance/linked/linked_compile_spec.ts @@ -7,6 +7,7 @@ */ import {PluginObj, transformSync} from '@babel/core'; +import {needsLinking} from '../../../linker'; import {createEs2015LinkerPlugin} from '../../../linker/babel'; import {AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system'; import {ConsoleLogger, LogLevel} from '../../../src/ngtsc/logging'; @@ -57,7 +58,7 @@ function linkPartials(fs: FileSystem, test: ComplianceTest): CompileResult { const sourceMap = fs.exists(sourceMapPath) ? JSON.parse(fs.readFile(sourceMapPath)) : undefined; const {linkedSource, linkedSourceMap} = - applyLinker({fileName, source, sourceMap}, linkerPlugin); + applyLinker({path: partialPath, source, sourceMap}, linkerPlugin); if (linkedSourceMap !== undefined) { const mapAndPath: MapAndPath = {map: linkedSourceMap, mapPath: sourceMapPath}; @@ -75,18 +76,18 @@ function linkPartials(fs: FileSystem, test: ComplianceTest): CompileResult { * * It will ignore files that do not have a `.js` extension. * - * @param file The file name and its source to be transformed using the linker. + * @param file The absolute file path and its source to be transformed using the linker. * @param linkerPlugin The linker plugin to apply. * @returns The file's source content, which has been transformed using the linker if necessary. */ function applyLinker( - file: {fileName: string; source: string, sourceMap: RawSourceMap | undefined}, + file: {path: string; source: string, sourceMap: RawSourceMap | undefined}, linkerPlugin: PluginObj): {linkedSource: string, linkedSourceMap: RawSourceMap|undefined} { - if (!file.fileName.endsWith('.js')) { + if (!file.path.endsWith('.js') || !needsLinking(file.path, file.source)) { return {linkedSource: file.source, linkedSourceMap: file.sourceMap}; } const result = transformSync(file.source, { - filename: file.fileName, + filename: file.path, sourceMaps: !!file.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 f0d5f7bb8d..99873782b4 100644 --- a/packages/compiler-cli/test/compliance_old/prelink/BUILD.bazel +++ b/packages/compiler-cli/test/compliance_old/prelink/BUILD.bazel @@ -6,6 +6,7 @@ ts_library( srcs = ["bootstrap.ts"], deps = [ "//packages:types", + "//packages/compiler-cli/linker", "//packages/compiler-cli/linker/babel", "//packages/compiler-cli/test/compliance_old/mock_compile", "@npm//@babel/core", diff --git a/packages/compiler-cli/test/compliance_old/prelink/bootstrap.ts b/packages/compiler-cli/test/compliance_old/prelink/bootstrap.ts index 1682850877..262df85489 100644 --- a/packages/compiler-cli/test/compliance_old/prelink/bootstrap.ts +++ b/packages/compiler-cli/test/compliance_old/prelink/bootstrap.ts @@ -8,6 +8,7 @@ import {PluginObj, transformSync} from '@babel/core'; import * as ts from 'typescript'; +import {needsLinking} from '../../../linker'; import {createEs2015LinkerPlugin} from '../../../linker/babel'; import {compileFiles, CompileFn, setCompileFn} from '../mock_compile'; @@ -34,7 +35,10 @@ const linkedCompile: CompileFn = (data, angularFiles, options) => { ...options, }); - const source = compiledFiles.map(file => applyLinker(file, linkerPlugin)).join('\n'); + const source = + compiledFiles + .map(file => applyLinker({path: file.fileName, source: file.source}, linkerPlugin)) + .join('\n'); return {source}; }; @@ -42,16 +46,16 @@ const linkedCompile: CompileFn = (data, angularFiles, options) => { /** * Runs the provided code through the Babel linker plugin, if the file has the .js extension. * - * @param file The file name and its source to be transformed using the linker. + * @param file The absolute file path and its source to be transformed using the linker. * @param linkerPlugin The linker plugin to apply. * @returns The file's source content, which has been transformed using the linker if necessary. */ -function applyLinker(file: {fileName: string; source: string}, linkerPlugin: PluginObj): string { - if (!file.fileName.endsWith('.js')) { +function applyLinker(file: {path: string; source: string}, linkerPlugin: PluginObj): string { + if (!file.path.endsWith('.js') || !needsLinking(file.path, file.source)) { return file.source; } const result = transformSync(file.source, { - filename: file.fileName, + filename: file.path, plugins: [linkerPlugin], parserOpts: {sourceType: 'unambiguous'}, });