257 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			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)); | ||
|  |         } | ||
|  |       } | ||
|  |     }, | ||
|  |   }; | ||
|  | } |