feat(compiler-cli): expose function to allow short-circuiting of linking (#40137)

The linker is implemented using a Babel transform such that Babel needs
to parse and walk a source file to find the declarations that need to be
compiled. If it can be determined that a source file is known not to
contain any declarations the parsing and walking can be skipped as a
performance improvement. This commit adds an exposed function for tools
that integrate the linker to use to allow short-circuiting of the linker
transform.

PR Close #40137
This commit is contained in:
JoostK 2020-12-15 22:31:44 +01:00 committed by Joey Perrott
parent e4fbab9ec8
commit 7dcf2864a3
8 changed files with 119 additions and 12 deletions

View File

@ -12,3 +12,4 @@ export {DeclarationScope} from './src/file_linker/declaration_scope';
export {FileLinker} from './src/file_linker/file_linker'; export {FileLinker} from './src/file_linker/file_linker';
export {LinkerEnvironment} from './src/file_linker/linker_environment'; export {LinkerEnvironment} from './src/file_linker/linker_environment';
export {LinkerOptions} from './src/file_linker/linker_options'; export {LinkerOptions} from './src/file_linker/linker_options';
export {needsLinking} from './src/file_linker/needs_linking';

View File

@ -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));
}

View File

@ -12,6 +12,10 @@ import {PartialComponentLinkerVersion1} from './partial_component_linker_1';
import {PartialDirectiveLinkerVersion1} from './partial_directive_linker_1'; import {PartialDirectiveLinkerVersion1} from './partial_directive_linker_1';
import {PartialLinker} from './partial_linker'; import {PartialLinker} from './partial_linker';
export const ɵɵngDeclareDirective = 'ɵɵngDeclareDirective';
export const ɵɵngDeclareComponent = 'ɵɵngDeclareComponent';
export const declarationFunctions = [ɵɵngDeclareDirective, ɵɵngDeclareComponent];
export class PartialLinkerSelector<TExpression> { export class PartialLinkerSelector<TExpression> {
/** /**
* A database of linker instances that should be used if their given semver range satisfies the * A database of linker instances that should be used if their given semver range satisfies the
@ -27,11 +31,11 @@ export class PartialLinkerSelector<TExpression> {
* allows the linker to work on local builds effectively. * allows the linker to work on local builds effectively.
*/ */
private linkers: Record<string, {range: string, linker: PartialLinker<TExpression>}[]> = { private linkers: Record<string, {range: string, linker: PartialLinker<TExpression>}[]> = {
'ɵɵngDeclareDirective': [ [ɵɵngDeclareDirective]: [
{range: '0.0.0-PLACEHOLDER', linker: new PartialDirectiveLinkerVersion1()}, {range: '0.0.0-PLACEHOLDER', linker: new PartialDirectiveLinkerVersion1()},
{range: '>=11.1.0-next.1', 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: '0.0.0-PLACEHOLDER', linker: new PartialComponentLinkerVersion1(this.options)},
{range: '>=11.1.0-next.1', linker: new PartialComponentLinkerVersion1(this.options)}, {range: '>=11.1.0-next.1', linker: new PartialComponentLinkerVersion1(this.options)},

View File

@ -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();
});
});

View File

@ -5,6 +5,7 @@ ts_library(
testonly = True, testonly = True,
srcs = ["linked_compile_spec.ts"], srcs = ["linked_compile_spec.ts"],
deps = [ deps = [
"//packages/compiler-cli/linker",
"//packages/compiler-cli/linker/babel", "//packages/compiler-cli/linker/babel",
"//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/logging", "//packages/compiler-cli/src/ngtsc/logging",

View File

@ -7,6 +7,7 @@
*/ */
import {PluginObj, transformSync} from '@babel/core'; import {PluginObj, transformSync} from '@babel/core';
import {needsLinking} from '../../../linker';
import {createEs2015LinkerPlugin} from '../../../linker/babel'; import {createEs2015LinkerPlugin} from '../../../linker/babel';
import {AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system'; import {AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system';
import {ConsoleLogger, LogLevel} from '../../../src/ngtsc/logging'; import {ConsoleLogger, LogLevel} from '../../../src/ngtsc/logging';
@ -57,7 +58,7 @@ function linkPartials(fs: FileSystem, test: ComplianceTest): CompileResult {
const sourceMap = const sourceMap =
fs.exists(sourceMapPath) ? JSON.parse(fs.readFile(sourceMapPath)) : undefined; fs.exists(sourceMapPath) ? JSON.parse(fs.readFile(sourceMapPath)) : undefined;
const {linkedSource, linkedSourceMap} = const {linkedSource, linkedSourceMap} =
applyLinker({fileName, source, sourceMap}, linkerPlugin); applyLinker({path: partialPath, source, sourceMap}, linkerPlugin);
if (linkedSourceMap !== undefined) { if (linkedSourceMap !== undefined) {
const mapAndPath: MapAndPath = {map: linkedSourceMap, mapPath: sourceMapPath}; 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. * 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. * @param linkerPlugin The linker plugin to apply.
* @returns The file's source content, which has been transformed using the linker if necessary. * @returns The file's source content, which has been transformed using the linker if necessary.
*/ */
function applyLinker( function applyLinker(
file: {fileName: string; source: string, sourceMap: RawSourceMap | undefined}, file: {path: string; source: string, sourceMap: RawSourceMap | undefined},
linkerPlugin: PluginObj): {linkedSource: string, linkedSourceMap: 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}; return {linkedSource: file.source, linkedSourceMap: file.sourceMap};
} }
const result = transformSync(file.source, { const result = transformSync(file.source, {
filename: file.fileName, filename: file.path,
sourceMaps: !!file.sourceMap, sourceMaps: !!file.sourceMap,
plugins: [linkerPlugin], plugins: [linkerPlugin],
parserOpts: {sourceType: 'unambiguous'}, parserOpts: {sourceType: 'unambiguous'},

View File

@ -6,6 +6,7 @@ ts_library(
srcs = ["bootstrap.ts"], srcs = ["bootstrap.ts"],
deps = [ deps = [
"//packages:types", "//packages:types",
"//packages/compiler-cli/linker",
"//packages/compiler-cli/linker/babel", "//packages/compiler-cli/linker/babel",
"//packages/compiler-cli/test/compliance_old/mock_compile", "//packages/compiler-cli/test/compliance_old/mock_compile",
"@npm//@babel/core", "@npm//@babel/core",

View File

@ -8,6 +8,7 @@
import {PluginObj, transformSync} from '@babel/core'; import {PluginObj, transformSync} from '@babel/core';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {needsLinking} from '../../../linker';
import {createEs2015LinkerPlugin} from '../../../linker/babel'; import {createEs2015LinkerPlugin} from '../../../linker/babel';
import {compileFiles, CompileFn, setCompileFn} from '../mock_compile'; import {compileFiles, CompileFn, setCompileFn} from '../mock_compile';
@ -34,7 +35,10 @@ const linkedCompile: CompileFn = (data, angularFiles, options) => {
...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}; 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. * 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. * @param linkerPlugin The linker plugin to apply.
* @returns The file's source content, which has been transformed using the linker if necessary. * @returns The file's source content, which has been transformed using the linker if necessary.
*/ */
function applyLinker(file: {fileName: string; source: string}, linkerPlugin: PluginObj): string { function applyLinker(file: {path: string; source: string}, linkerPlugin: PluginObj): string {
if (!file.fileName.endsWith('.js')) { if (!file.path.endsWith('.js') || !needsLinking(file.path, file.source)) {
return file.source; return file.source;
} }
const result = transformSync(file.source, { const result = transformSync(file.source, {
filename: file.fileName, filename: file.path,
plugins: [linkerPlugin], plugins: [linkerPlugin],
parserOpts: {sourceType: 'unambiguous'}, parserOpts: {sourceType: 'unambiguous'},
}); });