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