diff --git a/packages/compiler-cli/ngcc/src/host/umd_host.ts b/packages/compiler-cli/ngcc/src/host/umd_host.ts index d91805e0cb..51ae53b130 100644 --- a/packages/compiler-cli/ngcc/src/host/umd_host.ts +++ b/packages/compiler-cli/ngcc/src/host/umd_host.ts @@ -44,8 +44,8 @@ export class UmdReflectionHost extends Esm5ReflectionHost { } getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null { - return this.getUmdModuleDeclaration(id) || this.getUmdDeclaration(id) || - super.getDeclarationOfIdentifier(id); + return this.getExportsDeclaration(id) || this.getUmdModuleDeclaration(id) || + this.getUmdDeclaration(id) || super.getDeclarationOfIdentifier(id); } getExportsOfModule(module: ts.Node): Map|null { @@ -249,6 +249,29 @@ export class UmdReflectionHost extends Esm5ReflectionHost { return {...declaration, viaModule, known: getTsHelperFnFromIdentifier(id)}; } + private getExportsDeclaration(id: ts.Identifier): Declaration|null { + if (!isExportIdentifier(id)) { + return null; + } + + // Sadly, in the case of `exports.foo = bar`, we can't use `this.findUmdImportParameter(id)` to + // check whether this `exports` is from the IIFE body arguments, because + // `this.checker.getSymbolAtLocation(id)` will return the symbol for the `foo` identifier rather + // than the `exports` identifier. + // + // Instead we search the symbols in the current local scope. + const exportsSymbol = this.checker.getSymbolsInScope(id, ts.SymbolFlags.Variable) + .find(symbol => symbol.name === 'exports'); + if (exportsSymbol !== undefined && + !ts.isFunctionExpression(exportsSymbol.valueDeclaration.parent)) { + // There is an `exports` symbol in the local scope that is not a function parameter. + // So this `exports` identifier must be a local variable and does not represent the module. + return {node: exportsSymbol.valueDeclaration, viaModule: null, known: null, identity: null}; + } + + return {node: id.getSourceFile(), viaModule: null, known: null, identity: null}; + } + private getUmdModuleDeclaration(id: ts.Identifier): Declaration|null { const importPath = this.getImportPathFromParameter(id) || this.getImportPathFromRequireCall(id); if (importPath === null) { @@ -370,3 +393,10 @@ function getRequiredModulePath(wrapperFn: ts.FunctionExpression, paramIndex: num } } } + +/** + * Is the `node` an identifier with the name "exports"? + */ +export function isExportIdentifier(node: ts.Node): node is ts.Identifier { + return ts.isIdentifier(node) && node.text === 'exports'; +} diff --git a/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts index 25a40b8626..7f22966d61 100644 --- a/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/umd_host_spec.ts @@ -14,6 +14,7 @@ import {MockLogger} from '../../../src/ngtsc/logging/testing'; import {ClassMemberKind, ConcreteDeclaration, CtorParameter, DownleveledEnum, Import, InlineDeclaration, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, TypeScriptReflectionHost, TypeValueReferenceKind} from '../../../src/ngtsc/reflection'; import {getDeclaration} from '../../../src/ngtsc/testing'; import {loadFakeCore, loadTestFiles} from '../../../test/helpers'; +import {isExportsStatement} from '../../src/host/commonjs_umd_utils'; import {DelegatingReflectionHost} from '../../src/host/delegating_host'; import {getIifeBody} from '../../src/host/esm2015_host'; import {NgccReflectionHost} from '../../src/host/ngcc_host'; @@ -34,6 +35,7 @@ runInEachFileSystem(() => { let SIMPLE_CLASS_FILE: TestFile; let FOO_FUNCTION_FILE: TestFile; let INLINE_EXPORT_FILE: TestFile; + let EXPORTS_IDENTIFIERS_FILE: TestFile; let INVALID_DECORATORS_FILE: TestFile; let INVALID_DECORATOR_ARGS_FILE: TestFile; let INVALID_PROP_DECORATORS_FILE: TestFile; @@ -238,6 +240,33 @@ runInEachFileSystem(() => { `, }; + EXPORTS_IDENTIFIERS_FILE = { + name: _('/exports_identifiers.js'), + contents: ` +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@angular/core')) : + typeof define === 'function' && define.amd ? define('foo_function', ['exports', '@angular/core'], factory) : + (factory(global.inline_export,global.ng.core)); +}(this, (function (exports,core) { 'use strict'; + var x = exports; + exports.foo = 42; + + function simpleFn() { + exports.foo = 42; + } + + function localVar() { + var exports = {}; + exports.foo = 42; + } + + function exportsArg(exports) { + exports.foo = 42; + } +}))); +`, + }; + INVALID_DECORATORS_FILE = { name: _('/invalid_decorators.js'), contents: ` @@ -2078,6 +2107,111 @@ runInEachFileSystem(() => { expect(actualDeclaration!.viaModule).toBe('@angular/core'); }); + it('should return the source file as the declaration of a standalone "exports" identifier', + () => { + loadTestFiles([EXPORTS_IDENTIFIERS_FILE]); + const bundle = makeTestBundleProgram(EXPORTS_IDENTIFIERS_FILE.name); + const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle)); + const xVar = getDeclaration( + bundle.program, EXPORTS_IDENTIFIERS_FILE.name, 'x', ts.isVariableDeclaration); + const exportsIdentifier = xVar.initializer; + if (exportsIdentifier === undefined || !ts.isIdentifier(exportsIdentifier)) { + throw new Error('Bad test - unable to find `var x = exports;` statement'); + } + const exportsDeclaration = host.getDeclarationOfIdentifier(exportsIdentifier); + expect(exportsDeclaration).not.toBeNull(); + expect(exportsDeclaration!.node).toBe(bundle.file); + }); + + it('should return the source file as the declaration of "exports" in the LHS of a property access', + () => { + loadTestFiles([EXPORTS_IDENTIFIERS_FILE]); + const bundle = makeTestBundleProgram(EXPORTS_IDENTIFIERS_FILE.name); + const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle)); + const exportsStatement = walkForNode(bundle.file, isExportsStatement); + if (exportsStatement === undefined) { + throw new Error('Bad test - unable to find `exports.foo = 42;` statement'); + } + const exportsIdentifier = exportsStatement.expression.left.expression; + const exportDeclaration = host.getDeclarationOfIdentifier(exportsIdentifier); + expect(exportDeclaration).not.toBeNull(); + expect(exportDeclaration!.node).toBe(bundle.file); + }); + + it('should return the source file as the declaration of "exports" in the LHS of a property access inside a local function', + () => { + loadTestFiles([EXPORTS_IDENTIFIERS_FILE]); + const bundle = makeTestBundleProgram(EXPORTS_IDENTIFIERS_FILE.name); + const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle)); + const simpleFn = getDeclaration( + bundle.program, EXPORTS_IDENTIFIERS_FILE.name, 'simpleFn', ts.isFunctionDeclaration); + const exportsStatement = walkForNode(simpleFn, isExportsStatement); + if (exportsStatement === undefined) { + throw new Error('Bad test - unable to find `exports.foo = 42;` statement'); + } + const exportsIdentifier = exportsStatement.expression.left.expression; + const exportDeclaration = host.getDeclarationOfIdentifier(exportsIdentifier); + expect(exportDeclaration).not.toBeNull(); + expect(exportDeclaration!.node).toBe(bundle.file); + }); + + it('should return the variable declaration if a standalone "exports" is declared locally', + () => { + loadTestFiles([EXPORTS_IDENTIFIERS_FILE]); + const bundle = makeTestBundleProgram(EXPORTS_IDENTIFIERS_FILE.name); + const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle)); + const localVarFn = getDeclaration( + bundle.program, EXPORTS_IDENTIFIERS_FILE.name, 'localVar', ts.isFunctionDeclaration); + const exportsVar = walkForNode(localVarFn, ts.isVariableDeclaration); + if (exportsVar === undefined) { + throw new Error('Bad test - unable to find `var exports = {}` statement'); + } + const exportsIdentifier = exportsVar.name; + if (exportsIdentifier === undefined || !ts.isIdentifier(exportsIdentifier)) { + throw new Error('Bad test - unable to find `var exports = {};` statement'); + } + const exportsDeclaration = host.getDeclarationOfIdentifier(exportsIdentifier); + expect(exportsDeclaration).not.toBeNull(); + expect(exportsDeclaration!.node).toBe(exportsVar); + }); + + it('should return the variable declaration of "exports" in the LHS of a property access, if it is declared locally', + () => { + loadTestFiles([EXPORTS_IDENTIFIERS_FILE]); + const bundle = makeTestBundleProgram(EXPORTS_IDENTIFIERS_FILE.name); + const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle)); + const localVarFn = getDeclaration( + bundle.program, EXPORTS_IDENTIFIERS_FILE.name, 'localVar', ts.isFunctionDeclaration); + const exportsVar = walkForNode(localVarFn, ts.isVariableDeclaration); + const exportsStatement = walkForNode(localVarFn, isExportsStatement); + if (exportsStatement === undefined) { + throw new Error('Bad test - unable to find `exports.foo = 42;` statement'); + } + const exportsIdentifier = exportsStatement.expression.left.expression; + const exportDeclaration = host.getDeclarationOfIdentifier(exportsIdentifier); + expect(exportDeclaration).not.toBeNull(); + expect(exportDeclaration?.node).toBe(exportsVar); + }); + + it('should return the variable declaration of "exports" in the LHS of a property access, if it is a local function parameter', + () => { + loadTestFiles([EXPORTS_IDENTIFIERS_FILE]); + const bundle = makeTestBundleProgram(EXPORTS_IDENTIFIERS_FILE.name); + const host = createHost(bundle, new UmdReflectionHost(new MockLogger(), false, bundle)); + const exportsArgFn = getDeclaration( + bundle.program, EXPORTS_IDENTIFIERS_FILE.name, 'exportsArg', + ts.isFunctionDeclaration); + const exportsVar = exportsArgFn.parameters[0]; + const exportsStatement = walkForNode(exportsArgFn, isExportsStatement); + if (exportsStatement === undefined) { + throw new Error('Bad test - unable to find `exports.foo = 42;` statement'); + } + const exportsIdentifier = exportsStatement.expression.left.expression; + const exportDeclaration = host.getDeclarationOfIdentifier(exportsIdentifier); + expect(exportDeclaration).not.toBeNull(); + expect(exportDeclaration?.node).toBe(exportsVar); + }); + it('should recognize TypeScript helpers (as function declarations)', () => { const file: TestFile = { name: _('/test.js'), @@ -3232,3 +3366,8 @@ runInEachFileSystem(() => { }); }); }); + +type WalkerPredicate = (node: ts.Node) => node is T; +function walkForNode(node: ts.Node, predicate: WalkerPredicate): T|undefined { + return node.forEachChild(child => predicate(child) ? child : walkForNode(child, predicate)); +}