angular-docs-cn/packages/compiler-cli/linker/babel/test/es2015_linker_plugin_spec.ts
JoostK 306a1307c7 refactor(compiler-cli): rename $ngDeclareDirective/$ngDeclareComponent to use ɵɵ prefix (#39518)
For consistency with other generated code, the partial declaration
functions are renamed to use the `ɵɵ` prefix which indicates that it is
generated API.

This commit also removes the declaration from the public API golden
file, as it's not yet considered stable at this point. Once the linker
is finalized will these declaration function be included into the golden
file.

PR Close #39518
2020-11-04 10:44:37 -08:00

257 lines
10 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: 1, ngImport: core, x: 1});`,
`ɵɵngDeclareComponent({version: 1, ngImport: core, foo: () => ɵɵngDeclareDirective({version: 1, ngImport: core, x: 2})});`,
`x.qux(() => ɵɵngDeclareDirective({version: 1, ngImport: core, x: 3}));`,
'spread(...x);',
].join('\n'),
{
plugins: [createEs2015LinkerPlugin()],
filename: '/test.js',
parserOpts: {sourceType: 'unambiguous'},
});
expect(humanizeLinkerCalls(linkSpy.calls)).toEqual([
['ɵɵngDeclareDirective', '{version:1,ngImport:core,x:1}'],
[
'ɵɵngDeclareComponent',
'{version:1,ngImport:core,foo:()=>ɵɵngDeclareDirective({version:1,ngImport:core,x:2})}'
],
// Note we do not process `x:2` declaration since it is nested within another declaration
['ɵɵngDeclareDirective', '{version:1,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: 1, ngImport: core});',
'ɵɵngDeclareDirective({version: 1, 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: 1, ngImport: core})`,
`ɵɵngDeclareDirective({version: 1, ngImport: core})`,
`ɵɵngDeclareDirective({version: 1, 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: 1, ngImport: core})`,
`ɵɵngDeclareDirective({version: 1, ngImport: core})`,
`ɵɵngDeclareDirective({version: 1, 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: 1, ngImport: core})`,
` ɵɵngDeclareDirective({version: 1, ngImport: core})`,
` ɵɵngDeclareDirective({version: 1, 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: 1, ngImport: core})`,
` ɵɵngDeclareDirective({version: 1, ngImport: core})`,
` ɵɵngDeclareDirective({version: 1, 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: 1, 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(''));
});
});
/**
* 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));
}
}
},
};
}