The partial compiler will add a version number to the objects that are generated so that the linker can select the appropriate partial linker class to process the metadata. Previously this version matching was a simple number check. Now the partial compilation writes the current Angular compiler version into the generated metadata, and semantic version ranges are used to select the appropriate partial linker. PR Close #39847
293 lines
12 KiB
TypeScript
293 lines
12 KiB
TypeScript
/**
|
|
* @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 * as o from '@angular/compiler/src/output/output_ast';
|
|
import {NodePath, PluginObj, transformSync} from '@babel/core';
|
|
import generate from '@babel/generator';
|
|
import * as t from '@babel/types';
|
|
|
|
import {FileLinker} from '../../../linker';
|
|
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();
|
|
expect(plugin.visitor).toEqual({
|
|
Program: {
|
|
enter: jasmine.any(Function),
|
|
exit: jasmine.any(Function),
|
|
},
|
|
CallExpression: jasmine.any(Function),
|
|
});
|
|
});
|
|
|
|
it('should return a Babel plugin that calls FileLinker.isPartialDeclaration() on each call expression',
|
|
() => {
|
|
const isPartialDeclarationSpy = spyOn(FileLinker.prototype, 'isPartialDeclaration');
|
|
|
|
transformSync(
|
|
[
|
|
'var core;', `fn1()`, 'fn2({prop: () => fn3({})});', `x.method(() => fn4());`,
|
|
'spread(...x);'
|
|
].join('\n'),
|
|
{
|
|
plugins: [createEs2015LinkerPlugin()],
|
|
filename: '/test.js',
|
|
parserOpts: {sourceType: 'unambiguous'},
|
|
});
|
|
expect(isPartialDeclarationSpy.calls.allArgs()).toEqual([
|
|
['fn1'],
|
|
['fn2'],
|
|
['fn3'],
|
|
['method'],
|
|
['fn4'],
|
|
['spread'],
|
|
]);
|
|
});
|
|
|
|
it('should return a Babel plugin that calls FileLinker.linkPartialDeclaration() on each matching declaration',
|
|
() => {
|
|
const linkSpy = spyOn(FileLinker.prototype, 'linkPartialDeclaration')
|
|
.and.returnValue(t.identifier('REPLACEMENT'));
|
|
|
|
transformSync(
|
|
[
|
|
'var core;',
|
|
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core, x: 1});`,
|
|
`ɵɵngDeclareComponent({version: '0.0.0-PLACEHOLDER', ngImport: core, foo: () => ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core, x: 2})});`,
|
|
`x.qux(() => ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core, x: 3}));`,
|
|
'spread(...x);',
|
|
].join('\n'),
|
|
{
|
|
plugins: [createEs2015LinkerPlugin()],
|
|
filename: '/test.js',
|
|
parserOpts: {sourceType: 'unambiguous'},
|
|
});
|
|
|
|
expect(humanizeLinkerCalls(linkSpy.calls)).toEqual([
|
|
['ɵɵngDeclareDirective', '{version:\'0.0.0-PLACEHOLDER\',ngImport:core,x:1}'],
|
|
[
|
|
'ɵɵngDeclareComponent',
|
|
'{version:\'0.0.0-PLACEHOLDER\',ngImport:core,foo:()=>ɵɵngDeclareDirective({version:\'0.0.0-PLACEHOLDER\',ngImport:core,x:2})}'
|
|
],
|
|
// Note we do not process `x:2` declaration since it is nested within another declaration
|
|
['ɵɵngDeclareDirective', '{version:\'0.0.0-PLACEHOLDER\',ngImport:core,x:3}']
|
|
]);
|
|
});
|
|
|
|
it('should return a Babel plugin that replaces call expressions with the return value from FileLinker.linkPartialDeclaration()',
|
|
() => {
|
|
let replaceCount = 0;
|
|
spyOn(FileLinker.prototype, 'linkPartialDeclaration')
|
|
.and.callFake(() => t.identifier('REPLACEMENT_' + ++replaceCount));
|
|
const result = transformSync(
|
|
[
|
|
'var core;',
|
|
'ɵɵngDeclareDirective({version: \'0.0.0-PLACEHOLDER\', ngImport: core});',
|
|
'ɵɵngDeclareDirective({version: \'0.0.0-PLACEHOLDER\', ngImport: core, foo: () => bar({})});',
|
|
'x.qux();',
|
|
'spread(...x);',
|
|
].join('\n'),
|
|
{
|
|
plugins: [createEs2015LinkerPlugin()],
|
|
filename: '/test.js',
|
|
parserOpts: {sourceType: 'unambiguous'},
|
|
generatorOpts: {compact: true},
|
|
});
|
|
expect(result!.code).toEqual('var core;REPLACEMENT_1;REPLACEMENT_2;x.qux();spread(...x);');
|
|
});
|
|
|
|
it('should return a Babel plugin that adds shared statements after any imports', () => {
|
|
spyOnLinkPartialDeclarationWithConstants(o.literal('REPLACEMENT'));
|
|
const result = transformSync(
|
|
[
|
|
'import * as core from \'some-module\';',
|
|
'import {id} from \'other-module\';',
|
|
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
|
|
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
|
|
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
|
|
].join('\n'),
|
|
{
|
|
plugins: [createEs2015LinkerPlugin()],
|
|
filename: '/test.js',
|
|
parserOpts: {sourceType: 'unambiguous'},
|
|
generatorOpts: {compact: true},
|
|
});
|
|
expect(result!.code)
|
|
.toEqual(
|
|
'import*as core from\'some-module\';import{id}from\'other-module\';const _c0=[1];const _c1=[2];const _c2=[3];"REPLACEMENT";"REPLACEMENT";"REPLACEMENT";');
|
|
});
|
|
|
|
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 result = transformSync(
|
|
[
|
|
'var core;',
|
|
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
|
|
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
|
|
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
|
|
].join('\n'),
|
|
{
|
|
plugins: [createEs2015LinkerPlugin()],
|
|
filename: '/test.js',
|
|
// We declare the file as a module because this cannot be inferred from the source
|
|
parserOpts: {sourceType: 'module'},
|
|
generatorOpts: {compact: true},
|
|
});
|
|
expect(result!.code)
|
|
.toEqual(
|
|
'const _c0=[1];const _c1=[2];const _c2=[3];var core;"REPLACEMENT";"REPLACEMENT";"REPLACEMENT";');
|
|
});
|
|
|
|
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 result = transformSync(
|
|
[
|
|
'function run(core) {',
|
|
` ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
|
|
` ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
|
|
` ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`, '}'
|
|
].join('\n'),
|
|
{
|
|
plugins: [createEs2015LinkerPlugin()],
|
|
filename: '/test.js',
|
|
parserOpts: {sourceType: 'unambiguous'},
|
|
generatorOpts: {compact: true},
|
|
});
|
|
expect(result!.code)
|
|
.toEqual(
|
|
'function run(core){const _c0=[1];const _c1=[2];const _c2=[3];"REPLACEMENT";"REPLACEMENT";"REPLACEMENT";}');
|
|
});
|
|
|
|
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 result = transformSync(
|
|
[
|
|
'function run() {',
|
|
` ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
|
|
` ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
|
|
` ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
|
|
'}',
|
|
].join('\n'),
|
|
{
|
|
plugins: [createEs2015LinkerPlugin()],
|
|
filename: '/test.js',
|
|
parserOpts: {sourceType: 'unambiguous'},
|
|
generatorOpts: {compact: true},
|
|
});
|
|
expect(result!.code).toEqual([
|
|
`function run(){`,
|
|
`(function(){const _c0=[1];return"REPLACEMENT";})();`,
|
|
`(function(){const _c0=[2];return"REPLACEMENT";})();`,
|
|
`(function(){const _c0=[3];return"REPLACEMENT";})();`,
|
|
`}`,
|
|
].join(''));
|
|
});
|
|
|
|
it('should still execute other plugins that match AST nodes inside the result of the replacement',
|
|
() => {
|
|
spyOnLinkPartialDeclarationWithConstants(o.fn([], [], null, null, 'FOO'));
|
|
const result = transformSync(
|
|
[
|
|
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core}); FOO;`,
|
|
].join('\n'),
|
|
{
|
|
plugins: [
|
|
createEs2015LinkerPlugin(),
|
|
createIdentifierMapperPlugin('FOO', 'BAR'),
|
|
createIdentifierMapperPlugin('_c0', 'x1'),
|
|
],
|
|
filename: '/test.js',
|
|
parserOpts: {sourceType: 'module'},
|
|
generatorOpts: {compact: true},
|
|
});
|
|
expect(result!.code).toEqual([
|
|
`(function(){const x1=[1];return function BAR(){};})();BAR;`,
|
|
].join(''));
|
|
});
|
|
|
|
it('should not process call expressions within inserted functions', () => {
|
|
spyOn(PartialDirectiveLinkerVersion1.prototype, 'linkPartialDeclaration')
|
|
.and.callFake(((sourceUrl, code, 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.
|
|
constantPool.statements.push(
|
|
o.fn(/* params */[], /* body */[], /* type */ undefined,
|
|
/* sourceSpan */ undefined, /* name */ 'inserted')
|
|
.callFn([])
|
|
.toStmt());
|
|
|
|
return o.literal('REPLACEMENT');
|
|
}) as typeof PartialDirectiveLinkerVersion1.prototype.linkPartialDeclaration);
|
|
|
|
const isPartialDeclarationSpy =
|
|
spyOn(FileLinker.prototype, 'isPartialDeclaration').and.callThrough();
|
|
|
|
const result = transformSync(
|
|
[
|
|
'import * as core from \'some-module\';',
|
|
`ɵɵngDeclareDirective({version: '0.0.0-PLACEHOLDER', ngImport: core})`,
|
|
].join('\n'),
|
|
{
|
|
plugins: [createEs2015LinkerPlugin()],
|
|
filename: '/test.js',
|
|
parserOpts: {sourceType: 'unambiguous'},
|
|
generatorOpts: {compact: true},
|
|
});
|
|
expect(result!.code)
|
|
.toEqual('import*as core from\'some-module\';(function inserted(){})();"REPLACEMENT";');
|
|
|
|
expect(isPartialDeclarationSpy.calls.allArgs()).toEqual([['ɵɵngDeclareDirective']]);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Convert the arguments of the spied-on `calls` into a human readable array.
|
|
*/
|
|
function humanizeLinkerCalls(
|
|
calls: jasmine.Calls<typeof FileLinker.prototype.linkPartialDeclaration>) {
|
|
return calls.all().map(({args: [fn, args]}) => [fn, generate(args[0], {compact: true}).code]);
|
|
}
|
|
|
|
/**
|
|
* Spy on the `PartialDirectiveLinkerVersion1.linkPartialDeclaration()` method, triggering
|
|
* shared constants to be created.
|
|
*/
|
|
function spyOnLinkPartialDeclarationWithConstants(replacement: o.Expression) {
|
|
let callCount = 0;
|
|
spyOn(PartialDirectiveLinkerVersion1.prototype, 'linkPartialDeclaration')
|
|
.and.callFake(((sourceUrl, code, 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);
|
|
constantPool.getConstLiteral(constArray);
|
|
return replacement;
|
|
}) as typeof PartialDirectiveLinkerVersion1.prototype.linkPartialDeclaration);
|
|
}
|
|
|
|
/**
|
|
* A simple Babel plugin that will replace all identifiers that match `<src>` with identifiers
|
|
* called `<dest>`.
|
|
*/
|
|
function createIdentifierMapperPlugin(src: string, dest: string): PluginObj {
|
|
return {
|
|
visitor: {
|
|
Identifier(path: NodePath<t.Identifier>) {
|
|
if (path.node.name === src) {
|
|
path.replaceWith(t.identifier(dest));
|
|
}
|
|
}
|
|
},
|
|
};
|
|
}
|