feat(ivy): ngcc - handle top-level helper calls in CommonJS (#31335)
Some formats of CommonJS put the decorator helper calls outside the class IIFE as statements on the top level of the source file. This commit adds support to the `CommonJSReflectionHost` for this format. PR Close #31335
This commit is contained in:
		
							parent
							
								
									0d6fd134d4
								
							
						
					
					
						commit
						dd36f3ac99
					
				| @ -8,13 +8,16 @@ | |||||||
| 
 | 
 | ||||||
| import * as ts from 'typescript'; | import * as ts from 'typescript'; | ||||||
| import {absoluteFrom} from '../../../src/ngtsc/file_system'; | import {absoluteFrom} from '../../../src/ngtsc/file_system'; | ||||||
| import {Declaration, Import} from '../../../src/ngtsc/reflection'; | import {ClassSymbol, Declaration, Import} from '../../../src/ngtsc/reflection'; | ||||||
| import {Logger} from '../logging/logger'; | import {Logger} from '../logging/logger'; | ||||||
| import {BundleProgram} from '../packages/bundle_program'; | import {BundleProgram} from '../packages/bundle_program'; | ||||||
|  | import {isDefined} from '../utils'; | ||||||
|  | 
 | ||||||
| import {Esm5ReflectionHost} from './esm5_host'; | import {Esm5ReflectionHost} from './esm5_host'; | ||||||
| 
 | 
 | ||||||
| export class CommonJsReflectionHost extends Esm5ReflectionHost { | export class CommonJsReflectionHost extends Esm5ReflectionHost { | ||||||
|   protected commonJsExports = new Map<ts.SourceFile, Map<string, Declaration>|null>(); |   protected commonJsExports = new Map<ts.SourceFile, Map<string, Declaration>|null>(); | ||||||
|  |   protected topLevelHelperCalls = new Map<string, Map<ts.SourceFile, ts.CallExpression[]>>(); | ||||||
|   constructor( |   constructor( | ||||||
|       logger: Logger, isCore: boolean, protected program: ts.Program, |       logger: Logger, isCore: boolean, protected program: ts.Program, | ||||||
|       protected compilerHost: ts.CompilerHost, dts?: BundleProgram|null) { |       protected compilerHost: ts.CompilerHost, dts?: BundleProgram|null) { | ||||||
| @ -38,11 +41,50 @@ export class CommonJsReflectionHost extends Esm5ReflectionHost { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   getCommonJsExports(sourceFile: ts.SourceFile): Map<string, Declaration>|null { |   getCommonJsExports(sourceFile: ts.SourceFile): Map<string, Declaration>|null { | ||||||
|     if (!this.commonJsExports.has(sourceFile)) { |     return getOrDefault( | ||||||
|       const moduleExports = this.computeExportsOfCommonJsModule(sourceFile); |         this.commonJsExports, sourceFile, () => this.computeExportsOfCommonJsModule(sourceFile)); | ||||||
|       this.commonJsExports.set(sourceFile, moduleExports); |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Search statements related to the given class for calls to the specified helper. | ||||||
|  |    * | ||||||
|  |    * In CommonJS these helper calls can be outside the class's IIFE at the top level of the | ||||||
|  |    * source file. Searching the top level statements for helpers can be expensive, so we | ||||||
|  |    * try to get helpers from the IIFE first and only fall back on searching the top level if | ||||||
|  |    * no helpers are found. | ||||||
|  |    * | ||||||
|  |    * @param classSymbol the class whose helper calls we are interested in. | ||||||
|  |    * @param helperName the name of the helper (e.g. `__decorate`) whose calls we are interested in. | ||||||
|  |    * @returns an array of nodes of calls to the helper with the given name. | ||||||
|  |    */ | ||||||
|  |   protected getHelperCallsForClass(classSymbol: ClassSymbol, helperName: string): | ||||||
|  |       ts.CallExpression[] { | ||||||
|  |     const esm5HelperCalls = super.getHelperCallsForClass(classSymbol, helperName); | ||||||
|  |     if (esm5HelperCalls.length > 0) { | ||||||
|  |       return esm5HelperCalls; | ||||||
|  |     } else { | ||||||
|  |       const sourceFile = classSymbol.valueDeclaration.getSourceFile(); | ||||||
|  |       return this.getTopLevelHelperCalls(sourceFile, helperName); | ||||||
|     } |     } | ||||||
|     return this.commonJsExports.get(sourceFile) !; |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Find all the helper calls at the top level of a source file. | ||||||
|  |    * | ||||||
|  |    * We cache the helper calls per source file so that we don't have to keep parsing the code for | ||||||
|  |    * each class in a file. | ||||||
|  |    * | ||||||
|  |    * @param sourceFile the source who may contain helper calls. | ||||||
|  |    * @param helperName the name of the helper (e.g. `__decorate`) whose calls we are interested in. | ||||||
|  |    * @returns an array of nodes of calls to the helper with the given name. | ||||||
|  |    */ | ||||||
|  |   private getTopLevelHelperCalls(sourceFile: ts.SourceFile, helperName: string): | ||||||
|  |       ts.CallExpression[] { | ||||||
|  |     const helperCallsMap = getOrDefault(this.topLevelHelperCalls, helperName, () => new Map()); | ||||||
|  |     return getOrDefault( | ||||||
|  |         helperCallsMap, sourceFile, | ||||||
|  |         () => sourceFile.statements.map(statement => this.getHelperCall(statement, helperName)) | ||||||
|  |                   .filter(isDefined)); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private computeExportsOfCommonJsModule(sourceFile: ts.SourceFile): Map<string, Declaration> { |   private computeExportsOfCommonJsModule(sourceFile: ts.SourceFile): Map<string, Declaration> { | ||||||
| @ -184,3 +226,10 @@ function isReexportStatement(statement: ts.Statement): statement is ReexportStat | |||||||
| function stripExtension(fileName: string): string { | function stripExtension(fileName: string): string { | ||||||
|   return fileName.replace(/\..+$/, ''); |   return fileName.replace(/\..+$/, ''); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | function getOrDefault<K, V>(map: Map<K, V>, key: K, factory: (key: K) => V): V { | ||||||
|  |   if (!map.has(key)) { | ||||||
|  |     map.set(key, factory(key)); | ||||||
|  |   } | ||||||
|  |   return map.get(key) !; | ||||||
|  | } | ||||||
| @ -0,0 +1,96 @@ | |||||||
|  | /** | ||||||
|  |  * @license | ||||||
|  |  * Copyright Google Inc. 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 {absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system'; | ||||||
|  | import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; | ||||||
|  | import {isNamedVariableDeclaration} from '../../../src/ngtsc/reflection'; | ||||||
|  | import {getDeclaration} from '../../../src/ngtsc/testing'; | ||||||
|  | import {loadFakeCore, loadTestFiles} from '../../../test/helpers'; | ||||||
|  | import {CommonJsReflectionHost} from '../../src/host/commonjs_host'; | ||||||
|  | import {MockLogger} from '../helpers/mock_logger'; | ||||||
|  | import {makeTestBundleProgram} from '../helpers/utils'; | ||||||
|  | 
 | ||||||
|  | runInEachFileSystem(() => { | ||||||
|  |   describe('CommonJsReflectionHost [import helper style]', () => { | ||||||
|  |     let _: typeof absoluteFrom; | ||||||
|  |     let TOPLEVEL_DECORATORS_FILE: TestFile; | ||||||
|  | 
 | ||||||
|  |     beforeEach(() => { | ||||||
|  |       _ = absoluteFrom; | ||||||
|  | 
 | ||||||
|  |       TOPLEVEL_DECORATORS_FILE = { | ||||||
|  |         name: _('/toplevel_decorators.cjs.js'), | ||||||
|  |         contents: ` | ||||||
|  | var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { | ||||||
|  |   var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; | ||||||
|  |   if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); | ||||||
|  |   else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; | ||||||
|  |   return c > 3 && r && Object.defineProperty(target, key, r), r; | ||||||
|  | }; | ||||||
|  | var core = require('@angular/core'); | ||||||
|  | 
 | ||||||
|  | var INJECTED_TOKEN = new InjectionToken('injected'); | ||||||
|  | var ViewContainerRef = {}; | ||||||
|  | var TemplateRef = {}; | ||||||
|  | 
 | ||||||
|  | var SomeDirective = (function() { | ||||||
|  |   function SomeDirective(_viewContainer, _template, injected) {} | ||||||
|  |   return SomeDirective; | ||||||
|  | }()); | ||||||
|  | SomeDirective = __decorate([ | ||||||
|  |   core.Directive({ selector: '[someDirective]' }), | ||||||
|  |   __metadata("design:paramtypes", [core.ViewContainerRef, core.TemplateRef]) | ||||||
|  | ], SomeDirective); | ||||||
|  | __decorate([ | ||||||
|  |   core.Input(), | ||||||
|  | ], SomeDirective.prototype, "input1", void 0); | ||||||
|  | __decorate([ | ||||||
|  |   core.Input(), | ||||||
|  | ], SomeDirective.prototype, "input2", void 0); | ||||||
|  | exports.SomeDirective = SomeDirective; | ||||||
|  | 
 | ||||||
|  | var OtherDirective = (function() { | ||||||
|  |   function OtherDirective(_viewContainer, _template, injected) {} | ||||||
|  |   return OtherDirective; | ||||||
|  | }()); | ||||||
|  | OtherDirective = __decorate([ | ||||||
|  |   core.Directive({ selector: '[OtherDirective]' }), | ||||||
|  |   __metadata("design:paramtypes", [core.ViewContainerRef, core.TemplateRef]) | ||||||
|  | ], OtherDirective); | ||||||
|  | __decorate([ | ||||||
|  |   core.Input(), | ||||||
|  | ], OtherDirective.prototype, "input1", void 0); | ||||||
|  | __decorate([ | ||||||
|  |   core.Input(), | ||||||
|  | ], OtherDirective.prototype, "input2", void 0); | ||||||
|  | exports.OtherDirective = OtherDirective; | ||||||
|  | ` | ||||||
|  |       }; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     describe('getDecoratorsOfDeclaration()', () => { | ||||||
|  |       it('should find the decorators on a class at the top level', () => { | ||||||
|  |         loadFakeCore(getFileSystem()); | ||||||
|  |         loadTestFiles([TOPLEVEL_DECORATORS_FILE]); | ||||||
|  |         const {program, host: compilerHost} = makeTestBundleProgram(TOPLEVEL_DECORATORS_FILE.name); | ||||||
|  |         const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); | ||||||
|  |         const classNode = getDeclaration( | ||||||
|  |             program, TOPLEVEL_DECORATORS_FILE.name, 'SomeDirective', isNamedVariableDeclaration); | ||||||
|  |         const decorators = host.getDecoratorsOfDeclaration(classNode) !; | ||||||
|  | 
 | ||||||
|  |         expect(decorators.length).toEqual(1); | ||||||
|  | 
 | ||||||
|  |         const decorator = decorators[0]; | ||||||
|  |         expect(decorator.name).toEqual('Directive'); | ||||||
|  |         expect(decorator.import).toEqual({name: 'Directive', from: '@angular/core'}); | ||||||
|  |         expect(decorator.args !.map(arg => arg.getText())).toEqual([ | ||||||
|  |           '{ selector: \'[someDirective]\' }', | ||||||
|  |         ]); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user