From bdcbd9ed4b253998b3ad9567dec00636dcbafee5 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Wed, 20 Mar 2019 13:47:59 +0000 Subject: [PATCH] fix(ivy): ngcc - teach Esm5ReflectionHost about aliased variables (#29092) Sometimes, in ESM5 code, aliases to exported variables are used internally to refer to the exported value. This prevented some analysis from being able to match up a reference to an export to the actual export itself. For example in the following code: ``` var HttpClientXsrfModule = /** @class */ (function () { function HttpClientXsrfModule() { } HttpClientXsrfModule_1 = HttpClientXsrfModule; HttpClientXsrfModule.withOptions = function (options) { if (options === void 0) { options = {}; } return { ngModule: HttpClientXsrfModule_1, providers: [], }; }; var HttpClientXsrfModule_1; HttpClientXsrfModule = HttpClientXsrfModule_1 = tslib_1.__decorate([ NgModule({ providers: [], }) ], HttpClientXsrfModule); return HttpClientXsrfModule; }()); ``` We were not able to tell that the `ngModule: HttpClientXsrfModule_1` property assignment was actually meant to refer to the `function HttpClientXrsfModule()` declaration. This caused the `ModuleWithProviders` processing to fail. This commit ensures that we can compile typings files using the ESM5 format, so we can now update the examples boilerplate tool so that it does not need to compile the ESM2015 format at all. PR Close #29092 --- aio/tools/examples/example-boilerplate.js | 7 ++--- .../compiler-cli/ngcc/src/host/esm5_host.ts | 27 +++++++++++++++-- .../test/host/esm5_host_import_helper_spec.ts | 29 +++++++++++++++++++ 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/aio/tools/examples/example-boilerplate.js b/aio/tools/examples/example-boilerplate.js index 6a7c1715a1..6304e05950 100644 --- a/aio/tools/examples/example-boilerplate.js +++ b/aio/tools/examples/example-boilerplate.js @@ -68,11 +68,8 @@ class ExampleBoilerPlate { if (ivy) { // We only need the "fesm5" bundles as the CLI webpack build does not need - // any other formats for building and serving. Ngcc currently only updates - // the module typings if we specified an "es2015" format. This means that - // we also need to build with "fesm2015" in order to get updated typings - // which are needed for compilation. - shelljs.exec(`yarn --cwd ${SHARED_PATH} ivy-ngcc --properties module es2015`); + // any other formats for building and serving. + shelljs.exec(`yarn --cwd ${SHARED_PATH} ivy-ngcc --properties module`); } exampleFolders.forEach(exampleFolder => { diff --git a/packages/compiler-cli/ngcc/src/host/esm5_host.ts b/packages/compiler-cli/ngcc/src/host/esm5_host.ts index 628b6be1c2..846bc45dac 100644 --- a/packages/compiler-cli/ngcc/src/host/esm5_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm5_host.ts @@ -14,6 +14,7 @@ import {getNameText, hasNameIdentifier} from '../utils'; import {Esm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignmentStatement} from './esm2015_host'; + /** * ESM5 packages contain ECMAScript IIFE functions that act like classes. For example: * @@ -117,8 +118,30 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost { id = outerClassNode.name; } - // Resolve the identifier to a Symbol, and return the declaration of that. - return super.getDeclarationOfIdentifier(id); + const declaration = super.getDeclarationOfIdentifier(id); + + if (!declaration || !ts.isVariableDeclaration(declaration.node) || + declaration.node.initializer !== undefined || + // VariableDeclaration => VariableDeclarationList => VariableStatement => IIFE Block + !ts.isBlock(declaration.node.parent.parent.parent)) { + return declaration; + } + + // We might have an alias to another variable declaration. + // Search the containing iife body for it. + const block = declaration.node.parent.parent.parent; + const aliasSymbol = this.checker.getSymbolAtLocation(declaration.node.name); + for (let i = 0; i < block.statements.length; i++) { + const statement = block.statements[i]; + // Looking for statement that looks like: `AliasedVariable = OriginalVariable;` + if (isAssignmentStatement(statement) && ts.isIdentifier(statement.expression.left) && + ts.isIdentifier(statement.expression.right) && + this.checker.getSymbolAtLocation(statement.expression.left) === aliasSymbol) { + return this.getDeclarationOfIdentifier(statement.expression.right); + } + } + + return declaration; } /** diff --git a/packages/compiler-cli/ngcc/test/host/esm5_host_import_helper_spec.ts b/packages/compiler-cli/ngcc/test/host/esm5_host_import_helper_spec.ts index fb4b7d8752..d439f82745 100644 --- a/packages/compiler-cli/ngcc/test/host/esm5_host_import_helper_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm5_host_import_helper_spec.ts @@ -371,6 +371,18 @@ describe('Esm5ReflectionHost [import helper style]', () => { expect(actualDeclaration !.node).toBe(expectedDeclarationNode); expect(actualDeclaration !.viaModule).toBe('@angular/core'); }); + + it('should find the "actual" declaration of an aliased variable identifier', () => { + const program = makeTestProgram(fileSystem.files[2]); + const host = new Esm5ReflectionHost(false, program.getTypeChecker()); + const ngModuleRef = findIdentifier( + program.getSourceFile(fileSystem.files[2].name) !, 'HttpClientXsrfModule_1', + isNgModulePropertyAssignment); + + const declaration = host.getDeclarationOfIdentifier(ngModuleRef !); + expect(declaration).not.toBe(null); + expect(declaration !.node.getText()).toContain('function HttpClientXsrfModule()'); + }); }); }); @@ -422,3 +434,20 @@ describe('Esm5ReflectionHost [import helper style]', () => { return node.forEachChild(node => findVariableDeclaration(node, variableName)); } }); + +function findIdentifier( + node: ts.Node | undefined, identifierName: string, + requireFn: (node: ts.Identifier) => boolean): ts.Identifier|undefined { + if (!node) { + return undefined; + } + if (ts.isIdentifier(node) && node.text === identifierName && requireFn(node)) { + return node; + } + return node.forEachChild(node => findIdentifier(node, identifierName, requireFn)); +} + +function isNgModulePropertyAssignment(identifier: ts.Identifier): boolean { + return ts.isPropertyAssignment(identifier.parent) && + identifier.parent.name.getText() === 'ngModule'; +} \ No newline at end of file