fix(ngcc): handle UMD re-exports (#34254)

In TS we can re-export imports using statements of the form:

```
export * from 'some-import';
```

This is downleveled in UMD to:

```
function factory(exports, someImport) {
  function __export(m) {
    for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
  }
  __export(someImport);
}
```

This commit adds support for this.

PR Close #34254
This commit is contained in:
Pete Bacon Darwin
2019-12-18 14:03:05 +00:00
committed by Kara Erickson
parent 47666f548c
commit e9fb5fdb89
4 changed files with 129 additions and 27 deletions

View File

@ -11,7 +11,7 @@ import {absoluteFrom} from '../../../src/ngtsc/file_system';
import {Declaration, Import} from '../../../src/ngtsc/reflection';
import {Logger} from '../logging/logger';
import {BundleProgram} from '../packages/bundle_program';
import {isDefined} from '../utils';
import {isDefined, stripExtension} from '../utils';
import {Esm5ReflectionHost} from './esm5_host';
import {NgccClassSymbol} from './ngcc_host';
@ -152,12 +152,12 @@ export class CommonJsReflectionHost extends Esm5ReflectionHost {
return [];
}
const viaModule = stripExtension(importedFile.fileName);
const importedExports = this.getExportsOfModule(importedFile);
if (importedExports === null) {
return [];
}
const viaModule = stripExtension(importedFile.fileName);
const reexports: CommonJsExportDeclaration[] = [];
importedExports.forEach((decl, name) => {
if (decl.node !== null) {
@ -259,10 +259,6 @@ function isReexportStatement(statement: ts.Statement): statement is ReexportStat
statement.expression.arguments.length === 1;
}
function stripExtension(fileName: string): string {
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));

View File

@ -12,6 +12,7 @@ import {absoluteFrom} from '../../../src/ngtsc/file_system';
import {Declaration, Import} from '../../../src/ngtsc/reflection';
import {Logger} from '../logging/logger';
import {BundleProgram} from '../packages/bundle_program';
import {stripExtension} from '../utils';
import {Esm5ReflectionHost, stripParentheses} from './esm5_host';
export class UmdReflectionHost extends Esm5ReflectionHost {
@ -32,7 +33,10 @@ export class UmdReflectionHost extends Esm5ReflectionHost {
return superImport;
}
const importParameter = this.findUmdImportParameter(id);
// Is `id` a namespaced property access, e.g. `Directive` in `core.Directive`?
// If so capture the symbol of the namespace, e.g. `core`.
const nsIdentifier = findNamespaceOfIdentifier(id);
const importParameter = nsIdentifier && this.findUmdImportParameter(nsIdentifier);
const from = importParameter && this.getUmdImportPath(importParameter);
return from !== null ? {from, name: id.text} : null;
}
@ -107,14 +111,19 @@ export class UmdReflectionHost extends Esm5ReflectionHost {
private computeExportsOfUmdModule(sourceFile: ts.SourceFile): Map<string, Declaration>|null {
const moduleMap = new Map<string, Declaration>();
const exportStatements = this.getModuleStatements(sourceFile).filter(isUmdExportStatement);
const exportDeclarations =
exportStatements.map(statement => this.extractUmdExportDeclaration(statement));
exportDeclarations.forEach(decl => {
if (decl) {
moduleMap.set(decl.name, decl.declaration);
for (const statement of this.getModuleStatements(sourceFile)) {
if (isUmdExportStatement(statement)) {
const declaration = this.extractUmdExportDeclaration(statement);
if (declaration !== null) {
moduleMap.set(declaration.name, declaration.declaration);
}
} else if (isReexportStatement(statement)) {
const reexports = this.extractUmdReexports(statement, sourceFile);
for (const reexport of reexports) {
moduleMap.set(reexport.name, reexport.declaration);
}
}
});
}
return moduleMap;
}
@ -130,16 +139,43 @@ export class UmdReflectionHost extends Esm5ReflectionHost {
return {name, declaration};
}
private findUmdImportParameter(id: ts.Identifier): ts.ParameterDeclaration|null {
// Is `id` a namespaced property access, e.g. `Directive` in `core.Directive`?
// If so capture the symbol of the namespace, e.g. `core`.
const nsIdentifier = findNamespaceOfIdentifier(id);
const nsSymbol = nsIdentifier && this.checker.getSymbolAtLocation(nsIdentifier) || null;
private extractUmdReexports(statement: ReexportStatement, containingFile: ts.SourceFile):
UmdExportDeclaration[] {
const importParameter = this.findUmdImportParameter(statement.expression.arguments[0]);
const importPath = importParameter && this.getUmdImportPath(importParameter);
if (importPath === null) {
return [];
}
const importedFile = this.resolveModuleName(importPath, containingFile);
if (importedFile === undefined) {
return [];
}
// Is the namespace a parameter on a UMD factory function, e.g. `function factory(this, core)`?
// If so then return its declaration.
const nsDeclaration = nsSymbol && nsSymbol.valueDeclaration;
return nsDeclaration && ts.isParameter(nsDeclaration) ? nsDeclaration : null;
const importedExports = this.getExportsOfModule(importedFile);
if (importedExports === null) {
return [];
}
const viaModule = stripExtension(importedFile.fileName);
const reexports: UmdExportDeclaration[] = [];
importedExports.forEach((decl, name) => {
if (decl.node !== null) {
reexports.push({name, declaration: {node: decl.node, viaModule}});
} else {
reexports.push({name, declaration: {node: null, expression: decl.expression, viaModule}});
}
});
return reexports;
}
/**
* Is the identifier a parameter on a UMD factory function, e.g. `function factory(this, core)`?
* If so then return its declaration.
*/
private findUmdImportParameter(id: ts.Identifier): ts.ParameterDeclaration|null {
const symbol = id && this.checker.getSymbolAtLocation(id) || null;
const declaration = symbol && symbol.valueDeclaration;
return declaration && ts.isParameter(declaration) ? declaration : null;
}
private getUmdImportedDeclaration(id: ts.Identifier): Declaration|null {
@ -237,6 +273,15 @@ interface UmdExportDeclaration {
declaration: Declaration;
}
type ReexportStatement = ts.ExpressionStatement & {expression: {arguments: [ts.Identifier]}};
function isReexportStatement(statement: ts.Statement): statement is ReexportStatement {
return ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression) &&
ts.isIdentifier(statement.expression.expression) &&
statement.expression.expression.text === '__export' &&
statement.expression.arguments.length === 1 &&
ts.isIdentifier(statement.expression.arguments[0]);
}
function getRequiredModulePath(wrapperFn: ts.FunctionExpression, paramIndex: number): string {
const statement = wrapperFn.body.statements[0];
if (!ts.isExpressionStatement(statement)) {

View File

@ -115,3 +115,7 @@ export function resolveFileWithPostfixes(
export function stripDollarSuffix(value: string): string {
return value.replace(/\$\d+$/, '');
}
export function stripExtension(fileName: string): string {
return fileName.replace(/\..+$/, '');
}