fix(tsc-wrapped): emit exports metadata in flat modules (#17893)

Fixes: #17888
This commit is contained in:
Chuck Jazdzewski 2017-07-07 09:46:42 -06:00 committed by Jason Aden
parent 3b2d2c467a
commit cb16e9c747
2 changed files with 92 additions and 6 deletions

View File

@ -9,7 +9,8 @@ import * as path from 'path';
import * as ts from 'typescript';
import {MetadataCollector} from './collector';
import {ClassMetadata, ConstructorMetadata, FunctionMetadata, MemberMetadata, MetadataEntry, MetadataError, MetadataImportedSymbolReferenceExpression, MetadataMap, MetadataObject, MetadataSymbolicExpression, MetadataSymbolicReferenceExpression, MetadataValue, MethodMetadata, ModuleMetadata, VERSION, isClassMetadata, isConstructorMetadata, isFunctionMetadata, isInterfaceMetadata, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataImportedSymbolReferenceExpression, isMetadataModuleReferenceExpression, isMetadataSymbolicExpression, isMethodMetadata} from './schema';
import {ClassMetadata, ConstructorMetadata, FunctionMetadata, MemberMetadata, MetadataEntry, MetadataError, MetadataImportedSymbolReferenceExpression, MetadataMap, MetadataObject, MetadataSymbolicExpression, MetadataSymbolicReferenceExpression, MetadataValue, MethodMetadata, ModuleExportMetadata, ModuleMetadata, VERSION, isClassMetadata, isConstructorMetadata, isFunctionMetadata, isInterfaceMetadata, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataImportedSymbolReferenceExpression, isMetadataModuleReferenceExpression, isMetadataSymbolicExpression, isMethodMetadata} from './schema';
// The character set used to produce private names.
@ -44,6 +45,10 @@ interface Symbol {
// a referenced symbol's value.
referenced?: boolean;
// A symbol is marked as a re-export the symbol was rexported from a module that is
// not part of the flat module bundle.
reexport?: boolean;
// Only valid for referenced canonical symbols. Produces by convertSymbols().
value?: MetadataEntry;
@ -98,14 +103,19 @@ export class MetadataBundler {
module: s.declaration.module
}));
const origins = Array.from(this.symbolMap.values())
.filter(s => s.referenced)
.filter(s => s.referenced && !s.reexport)
.reduce<{[name: string]: string}>((p, s) => {
p[s.isPrivate ? s.privateName : s.name] = s.declaration.module;
return p;
}, {});
const exports = this.getReExports(exportedSymbols);
return {
metadata:
{__symbolic: 'module', version: VERSION, metadata, origins, importAs: this.importAs},
metadata: {
__symbolic: 'module',
version: VERSION,
exports: exports.length ? exports : undefined, metadata, origins,
importAs: this.importAs
},
privates
};
}
@ -165,12 +175,19 @@ export class MetadataBundler {
for (const exportDeclaration of module.exports) {
const exportFrom = resolveModule(exportDeclaration.from, moduleName);
// Record all the exports from the module even if we don't use it directly.
this.exportAll(exportFrom);
const exportedSymbols = this.exportAll(exportFrom);
if (exportDeclaration.export) {
// Re-export all the named exports from a module.
for (const exportItem of exportDeclaration.export) {
const name = typeof exportItem == 'string' ? exportItem : exportItem.name;
const exportAs = typeof exportItem == 'string' ? exportItem : exportItem.as;
const symbol = this.symbolOf(exportFrom, name);
if (exportedSymbols && exportedSymbols.length == 1 && exportedSymbols[0].reexport &&
exportedSymbols[0].name == '*') {
// This is a named export from a module we have no metadata about. Record the named
// export as a re-export.
symbol.reexport = true;
}
exportSymbol(this.symbolOf(exportFrom, name), exportAs);
}
} else {
@ -183,6 +200,15 @@ export class MetadataBundler {
}
}
}
if (!module) {
// If no metadata is found for this import then it is considered external to the
// library and should be recorded as a re-export in the final metadata if it is
// eventually re-exported.
const symbol = this.symbolOf(moduleName, '*');
symbol.reexport = true;
result.push(symbol);
}
this.exports.set(moduleName, result);
return result;
@ -207,6 +233,7 @@ export class MetadataBundler {
symbol.isPrivate = isPrivate;
symbol.declaration = declaration;
symbol.canonicalSymbol = canonicalSymbol;
symbol.reexport = declaration.reexport;
}
private getEntries(exportedSymbols: Symbol[]): BundleEntries {
@ -233,7 +260,7 @@ export class MetadataBundler {
exportedSymbols.forEach(symbol => this.convertSymbol(symbol));
Array.from(this.symbolMap.values()).forEach(symbol => {
if (symbol.referenced) {
if (symbol.referenced && !symbol.reexport) {
let name = symbol.name;
if (symbol.isPrivate && !symbol.privateName) {
name = newPrivateName();
@ -246,6 +273,36 @@ export class MetadataBundler {
return result;
}
private getReExports(exportedSymbols: Symbol[]): ModuleExportMetadata[] {
type ExportClause = {name: string, as: string}[];
const modules = new Map<string, ExportClause>();
const exportAlls = new Set<string>();
for (const symbol of exportedSymbols) {
if (symbol.reexport) {
const declaration = symbol.declaration;
const module = declaration.module;
if (declaration.name == '*') {
// Reexport all the symbols.
exportAlls.add(declaration.module);
} else {
// Re-export the symbol as the exported name.
let entry = modules.get(module);
if (!entry) {
entry = [];
modules.set(module, entry);
}
const as = symbol.name;
const name = declaration.name;
entry.push({name, as});
}
}
}
return [
...Array.from(exportAlls.values()).map(from => ({from})),
...Array.from(modules.entries()).map(([from, exports]) => ({export: exports, from}))
];
}
private convertSymbol(symbol: Symbol) {
const canonicalSymbol = symbol.canonicalSymbol;

View File

@ -163,6 +163,35 @@ describe('metadata bundler', () => {
expect(Object.keys(result.metadata.metadata).sort()).toEqual(['Foo', 'ɵa']);
expect(result.privates).toEqual([{privateName: 'ɵa', name: 'Bar', module: './bar'}]);
});
it('should be able to bundle a library with re-exported symbols', () => {
const host = new MockStringBundlerHost('/', {
'public-api.ts': `
export * from './src/core';
export * from './src/externals';
`,
'src': {
'core.ts': `
export class A {}
export class B extends A {}
`,
'externals.ts': `
export {E, F, G} from 'external_one';
export * from 'external_two';
`
}
});
const bundler = new MetadataBundler('/public-api', undefined, host);
const result = bundler.getMetadataBundle();
expect(result.metadata.exports).toEqual([
{from: 'external_two'}, {
export: [{name: 'E', as: 'E'}, {name: 'F', as: 'F'}, {name: 'G', as: 'G'}],
from: 'external_one'
}
]);
expect(result.metadata.origins['E']).toBeUndefined();
});
});
export class MockStringBundlerHost implements MetadataBundlerHost {