feat(ngcc): enable private NgModule re-exports in ngcc on request (#33177)

This commit adapts the private NgModule re-export system (using aliasing) to
ngcc. Not all ngcc compilations are compatible with these re-exports, as
they assume a 1:1 correspondence between .js and .d.ts files. The primary
concern here is supporting them for commonjs-only packages.

PR Close #33177
This commit is contained in:
Alex Rickabaugh
2019-10-14 13:04:42 -07:00
committed by Matias Niemelä
parent c4733c15c0
commit e030375d9a
17 changed files with 287 additions and 11 deletions

View File

@ -8,6 +8,7 @@
import {dirname, relative} from 'canonical-path';
import * as ts from 'typescript';
import MagicString from 'magic-string';
import {Reexport} from '../../../src/ngtsc/imports';
import {Import, ImportManager} from '../../../src/ngtsc/translator';
import {ExportInfo} from '../analysis/private_declarations_analyzer';
import {isRequireCall} from '../host/commonjs_host';
@ -53,6 +54,17 @@ export class CommonJsRenderingFormatter extends Esm5RenderingFormatter {
});
}
addDirectExports(
output: MagicString, exports: Reexport[], importManager: ImportManager,
file: ts.SourceFile): void {
for (const e of exports) {
const namedImport = importManager.generateNamedImport(e.fromModule, e.symbolName);
const importNamespace = namedImport.moduleImport ? `${namedImport.moduleImport}.` : '';
const exportStr = `\nexports.${e.asAlias} = ${importNamespace}${namedImport.symbol};`;
output.append(exportStr);
}
}
protected findEndOfImports(sf: ts.SourceFile): number {
for (const statement of sf.statements) {
if (ts.isExpressionStatement(statement) && isRequireCall(statement.expression)) {

View File

@ -8,6 +8,7 @@
import MagicString from 'magic-string';
import * as ts from 'typescript';
import {FileSystem} from '../../../src/ngtsc/file_system';
import {Reexport} from '../../../src/ngtsc/imports';
import {CompileResult} from '../../../src/ngtsc/transform';
import {translateType, ImportManager} from '../../../src/ngtsc/translator';
import {DecorationAnalyses} from '../analysis/types';
@ -33,6 +34,7 @@ class DtsRenderInfo {
classInfo: DtsClassInfo[] = [];
moduleWithProviders: ModuleWithProvidersInfo[] = [];
privateExports: ExportInfo[] = [];
reexports: Reexport[] = [];
}
@ -94,6 +96,13 @@ export class DtsRenderer {
const newStatement = ` static ${declaration.name}: ${typeStr};\n`;
outputText.appendRight(endOfClass - 1, newStatement);
});
if (renderInfo.reexports.length > 0) {
for (const e of renderInfo.reexports) {
const newStatement = `\nexport {${e.symbolName} as ${e.asAlias}} from '${e.fromModule}';`;
outputText.appendRight(endOfClass, newStatement);
}
}
});
this.dtsFormatter.addModuleWithProvidersParams(
@ -103,8 +112,6 @@ export class DtsRenderer {
this.dtsFormatter.addImports(
outputText, importManager.getAllImports(dtsFile.fileName), dtsFile);
return renderSourceAndMap(dtsFile, input, outputText);
}
@ -123,6 +130,15 @@ export class DtsRenderer {
const dtsFile = dtsDeclaration.getSourceFile();
const renderInfo = dtsMap.has(dtsFile) ? dtsMap.get(dtsFile) ! : new DtsRenderInfo();
renderInfo.classInfo.push({dtsDeclaration, compilation: compiledClass.compilation});
// Only add re-exports if the .d.ts tree is overlayed with the .js tree, as re-exports in
// ngcc are only used to support deep imports into e.g. commonjs code. For a deep import
// to work, the typing file and JS file must be in parallel trees. This logic will detect
// the simplest version of this case, which is sufficient to handle most commonjs
// libraries.
if (compiledClass.declaration.getSourceFile().fileName ===
dtsFile.fileName.replace(/\.d\.ts$/, '.js')) {
renderInfo.reexports.push(...compiledClass.reexports);
}
dtsMap.set(dtsFile, renderInfo);
}
});

View File

@ -16,6 +16,7 @@ import {ModuleWithProvidersInfo} from '../analysis/module_with_providers_analyze
import {ExportInfo} from '../analysis/private_declarations_analyzer';
import {RenderingFormatter, RedundantDecoratorMap} from './rendering_formatter';
import {stripExtension} from './utils';
import {Reexport} from '../../../src/ngtsc/imports';
/**
* A RenderingFormatter that works with ECMAScript Module import and export statements.
@ -57,6 +58,22 @@ export class EsmRenderingFormatter implements RenderingFormatter {
});
}
/**
* Add plain exports to the end of the file.
*
* Unlike `addExports`, direct exports go directly in a .js and .d.ts file and don't get added to
* an entrypoint.
*/
addDirectExports(
output: MagicString, exports: Reexport[], importManager: ImportManager,
file: ts.SourceFile): void {
for (const e of exports) {
const exportStatement = `\nexport {${e.symbolName} as ${e.asAlias}} from '${e.fromModule}';`;
output.append(exportStatement);
}
}
/**
* Add the constants directly after the imports.
*/

View File

@ -83,6 +83,11 @@ export class Renderer {
compiledFile.compiledClasses.forEach(clazz => {
const renderedDefinition = renderDefinitions(compiledFile.sourceFile, clazz, importManager);
this.srcFormatter.addDefinitions(outputText, clazz, renderedDefinition);
if (!isEntryPoint && clazz.reexports.length > 0) {
this.srcFormatter.addDirectExports(
outputText, clazz.reexports, importManager, compiledFile.sourceFile);
}
});
this.srcFormatter.addConstants(

View File

@ -7,6 +7,7 @@
*/
import MagicString from 'magic-string';
import * as ts from 'typescript';
import {Reexport} from '../../../src/ngtsc/imports';
import {Import, ImportManager} from '../../../src/ngtsc/translator';
import {ExportInfo} from '../analysis/private_declarations_analyzer';
import {CompiledClass} from '../analysis/types';
@ -31,6 +32,9 @@ export interface RenderingFormatter {
addExports(
output: MagicString, entryPointBasePath: string, exports: ExportInfo[],
importManager: ImportManager, file: ts.SourceFile): void;
addDirectExports(
output: MagicString, exports: Reexport[], importManager: ImportManager,
file: ts.SourceFile): void;
addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void;
removeDecorators(output: MagicString, decoratorsToRemove: RedundantDecoratorMap): void;
rewriteSwitchableDeclarations(

View File

@ -13,6 +13,7 @@ import {ExportInfo} from '../analysis/private_declarations_analyzer';
import {UmdReflectionHost} from '../host/umd_host';
import {Esm5RenderingFormatter} from './esm5_rendering_formatter';
import {stripExtension} from './utils';
import {Reexport} from '../../../src/ngtsc/imports';
type CommonJsConditional = ts.ConditionalExpression & {whenTrue: ts.CallExpression};
type AmdConditional = ts.ConditionalExpression & {whenTrue: ts.CallExpression};
@ -71,6 +72,26 @@ export class UmdRenderingFormatter extends Esm5RenderingFormatter {
});
}
addDirectExports(
output: MagicString, exports: Reexport[], importManager: ImportManager,
file: ts.SourceFile): void {
const umdModule = this.umdHost.getUmdModule(file);
if (!umdModule) {
return;
}
const factoryFunction = umdModule.factoryFn;
const lastStatement =
factoryFunction.body.statements[factoryFunction.body.statements.length - 1];
const insertionPoint =
lastStatement ? lastStatement.getEnd() : factoryFunction.body.getEnd() - 1;
for (const e of exports) {
const namedImport = importManager.generateNamedImport(e.fromModule, e.symbolName);
const importNamespace = namedImport.moduleImport ? `${namedImport.moduleImport}.` : '';
const exportStr = `\nexports.${e.asAlias} = ${importNamespace}${namedImport.symbol};`;
output.appendRight(insertionPoint, exportStr);
}
}
/**
* Add the constants to the top of the UMD factory function.
*/