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:
parent
e4fbab9ec8
commit
7dcf2864a3
|
@ -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';
|
||||
|
|
|
@ -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));
|
||||
}
|
|
@ -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<TExpression> {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private linkers: Record<string, {range: string, linker: PartialLinker<TExpression>}[]> = {
|
||||
'ɵɵ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)},
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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",
|
||||
|
|
|
@ -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'},
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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'},
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue