fix(ivy): ngcc - separate typings rendering from src rendering (#25445)

Previously the same `Renderer` was used to render typings (.d.ts)
files. But the new `UmdRenderer` is not able to render typings files
correctly.

This commit splits out the typings rendering from the src rendering.
To achieve this the previous renderers have been refactored from
sub-classes of the abstract `Renderer` class to  classes that implement
the `RenderingFormatter` interface, which are then passed to the
`Renderer` and `DtsRenderer` to modify its rendering behaviour.

Along the way a few utility interfaces and classes have been moved
around and renamed for clarity.

PR Close #25445
This commit is contained in:
Pete Bacon Darwin
2019-04-28 20:48:35 +01:00
committed by Jason Aden
parent f4655ea98a
commit c613596658
18 changed files with 1307 additions and 1080 deletions

View File

@ -0,0 +1,161 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import MagicString from 'magic-string';
import * as ts from 'typescript';
import {translateType, ImportManager} from '../../../src/ngtsc/translator';
import {DecorationAnalyses} from '../analysis/decoration_analyzer';
import {ModuleWithProvidersInfo, ModuleWithProvidersAnalyses} from '../analysis/module_with_providers_analyzer';
import {PrivateDeclarationsAnalyses, ExportInfo} from '../analysis/private_declarations_analyzer';
import {IMPORT_PREFIX} from '../constants';
import {FileSystem} from '../file_system/file_system';
import {NgccReflectionHost} from '../host/ngcc_host';
import {EntryPointBundle} from '../packages/entry_point_bundle';
import {Logger} from '../logging/logger';
import {FileToWrite, getImportRewriter} from './utils';
import {RenderingFormatter} from './rendering_formatter';
import {extractSourceMap, renderSourceAndMap} from './source_maps';
import {CompileResult} from '@angular/compiler-cli/src/ngtsc/transform';
/**
* A structure that captures information about what needs to be rendered
* in a typings file.
*
* It is created as a result of processing the analysis passed to the renderer.
*
* The `renderDtsFile()` method consumes it when rendering a typings file.
*/
class DtsRenderInfo {
classInfo: DtsClassInfo[] = [];
moduleWithProviders: ModuleWithProvidersInfo[] = [];
privateExports: ExportInfo[] = [];
}
/**
* Information about a class in a typings file.
*/
export interface DtsClassInfo {
dtsDeclaration: ts.Declaration;
compilation: CompileResult[];
}
/**
* A base-class for rendering an `AnalyzedFile`.
*
* Package formats have output files that must be rendered differently. Concrete sub-classes must
* implement the `addImports`, `addDefinitions` and `removeDecorators` abstract methods.
*/
export class DtsRenderer {
constructor(
private dtsFormatter: RenderingFormatter, private fs: FileSystem, private logger: Logger,
private host: NgccReflectionHost, private isCore: boolean, private bundle: EntryPointBundle) {
}
renderProgram(
decorationAnalyses: DecorationAnalyses,
privateDeclarationsAnalyses: PrivateDeclarationsAnalyses,
moduleWithProvidersAnalyses: ModuleWithProvidersAnalyses|null): FileToWrite[] {
const renderedFiles: FileToWrite[] = [];
// Transform the .d.ts files
if (this.bundle.dts) {
const dtsFiles = this.getTypingsFilesToRender(
decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses);
// If the dts entry-point is not already there (it did not have compiled classes)
// then add it now, to ensure it gets its extra exports rendered.
if (!dtsFiles.has(this.bundle.dts.file)) {
dtsFiles.set(this.bundle.dts.file, new DtsRenderInfo());
}
dtsFiles.forEach(
(renderInfo, file) => renderedFiles.push(...this.renderDtsFile(file, renderInfo)));
}
return renderedFiles;
}
renderDtsFile(dtsFile: ts.SourceFile, renderInfo: DtsRenderInfo): FileToWrite[] {
const input = extractSourceMap(this.fs, this.logger, dtsFile);
const outputText = new MagicString(input.source);
const printer = ts.createPrinter();
const importManager = new ImportManager(
getImportRewriter(this.bundle.dts !.r3SymbolsFile, this.isCore, false), IMPORT_PREFIX);
renderInfo.classInfo.forEach(dtsClass => {
const endOfClass = dtsClass.dtsDeclaration.getEnd();
dtsClass.compilation.forEach(declaration => {
const type = translateType(declaration.type, importManager);
const typeStr = printer.printNode(ts.EmitHint.Unspecified, type, dtsFile);
const newStatement = ` static ${declaration.name}: ${typeStr};\n`;
outputText.appendRight(endOfClass - 1, newStatement);
});
});
this.dtsFormatter.addModuleWithProvidersParams(
outputText, renderInfo.moduleWithProviders, importManager);
this.dtsFormatter.addExports(
outputText, dtsFile.fileName, renderInfo.privateExports, importManager, dtsFile);
this.dtsFormatter.addImports(
outputText, importManager.getAllImports(dtsFile.fileName), dtsFile);
return renderSourceAndMap(dtsFile, input, outputText);
}
private getTypingsFilesToRender(
decorationAnalyses: DecorationAnalyses,
privateDeclarationsAnalyses: PrivateDeclarationsAnalyses,
moduleWithProvidersAnalyses: ModuleWithProvidersAnalyses|
null): Map<ts.SourceFile, DtsRenderInfo> {
const dtsMap = new Map<ts.SourceFile, DtsRenderInfo>();
// Capture the rendering info from the decoration analyses
decorationAnalyses.forEach(compiledFile => {
compiledFile.compiledClasses.forEach(compiledClass => {
const dtsDeclaration = this.host.getDtsDeclaration(compiledClass.declaration);
if (dtsDeclaration) {
const dtsFile = dtsDeclaration.getSourceFile();
const renderInfo = dtsMap.has(dtsFile) ? dtsMap.get(dtsFile) ! : new DtsRenderInfo();
renderInfo.classInfo.push({dtsDeclaration, compilation: compiledClass.compilation});
dtsMap.set(dtsFile, renderInfo);
}
});
});
// Capture the ModuleWithProviders functions/methods that need updating
if (moduleWithProvidersAnalyses !== null) {
moduleWithProvidersAnalyses.forEach((moduleWithProvidersToFix, dtsFile) => {
const renderInfo = dtsMap.has(dtsFile) ? dtsMap.get(dtsFile) ! : new DtsRenderInfo();
renderInfo.moduleWithProviders = moduleWithProvidersToFix;
dtsMap.set(dtsFile, renderInfo);
});
}
// Capture the private declarations that need to be re-exported
if (privateDeclarationsAnalyses.length) {
privateDeclarationsAnalyses.forEach(e => {
if (!e.dtsFrom && !e.alias) {
throw new Error(
`There is no typings path for ${e.identifier} in ${e.from}.\n` +
`We need to add an export for this class to a .d.ts typings file because ` +
`Angular compiler needs to be able to reference this class in compiled code, such as templates.\n` +
`The simplest fix for this is to ensure that this class is exported from the package's entry-point.`);
}
});
const dtsEntryPoint = this.bundle.dts !.file;
const renderInfo =
dtsMap.has(dtsEntryPoint) ? dtsMap.get(dtsEntryPoint) ! : new DtsRenderInfo();
renderInfo.privateExports = privateDeclarationsAnalyses;
dtsMap.set(dtsEntryPoint, renderInfo);
}
return dtsMap;
}
}

View File

@ -8,22 +8,16 @@
import MagicString from 'magic-string';
import * as ts from 'typescript';
import {CompiledClass} from '../analysis/decoration_analyzer';
import {FileSystem} from '../file_system/file_system';
import {getIifeBody} from '../host/esm5_host';
import {NgccReflectionHost} from '../host/ngcc_host';
import {Logger} from '../logging/logger';
import {EntryPointBundle} from '../packages/entry_point_bundle';
import {EsmRenderer} from './esm_renderer';
export class Esm5Renderer extends EsmRenderer {
constructor(
fs: FileSystem, logger: Logger, host: NgccReflectionHost, isCore: boolean,
bundle: EntryPointBundle) {
super(fs, logger, host, isCore, bundle);
}
import {EsmRenderingFormatter} from './esm_rendering_formatter';
/**
* A RenderingFormatter that works with files that use ECMAScript Module `import` and `export`
* statements, but instead of `class` declarations it uses ES5 `function` wrappers for classes.
*/
export class Esm5RenderingFormatter extends EsmRenderingFormatter {
/**
* Add the definitions to each decorated class
* Add the definitions inside the IIFE of each decorated class
*/
addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void {
const iifeBody = getIifeBody(compiledClass.declaration);

View File

@ -1,140 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import MagicString from 'magic-string';
import * as ts from 'typescript';
import {PathSegment, AbsoluteFsPath} from '../../../src/ngtsc/path';
import {isDtsPath} from '../../../src/ngtsc/util/src/typescript';
import {Import, ImportManager} from '../../../src/ngtsc/translator';
import {CompiledClass} from '../analysis/decoration_analyzer';
import {ExportInfo} from '../analysis/private_declarations_analyzer';
import {FileSystem} from '../file_system/file_system';
import {NgccReflectionHost, POST_R3_MARKER, PRE_R3_MARKER, SwitchableVariableDeclaration} from '../host/ngcc_host';
import {Logger} from '../logging/logger';
import {EntryPointBundle} from '../packages/entry_point_bundle';
import {RedundantDecoratorMap, Renderer, stripExtension} from './renderer';
export class EsmRenderer extends Renderer {
constructor(
fs: FileSystem, logger: Logger, host: NgccReflectionHost, isCore: boolean,
bundle: EntryPointBundle) {
super(fs, logger, host, isCore, bundle);
}
/**
* Add the imports at the top of the file
*/
addImports(output: MagicString, imports: Import[], sf: ts.SourceFile): void {
const insertionPoint = findEndOfImports(sf);
const renderedImports =
imports.map(i => `import * as ${i.qualifier} from '${i.specifier}';\n`).join('');
output.appendLeft(insertionPoint, renderedImports);
}
addExports(
output: MagicString, entryPointBasePath: AbsoluteFsPath, exports: ExportInfo[],
importManager: ImportManager, file: ts.SourceFile): void {
exports.forEach(e => {
let exportFrom = '';
const isDtsFile = isDtsPath(entryPointBasePath);
const from = isDtsFile ? e.dtsFrom : e.from;
if (from) {
const basePath = stripExtension(from);
const relativePath =
'./' + PathSegment.relative(AbsoluteFsPath.dirname(entryPointBasePath), basePath);
exportFrom = entryPointBasePath !== basePath ? ` from '${relativePath}'` : '';
}
// aliases should only be added in dts files as these are lost when rolling up dts file.
const exportStatement = e.alias && isDtsFile ? `${e.alias} as ${e.identifier}` : e.identifier;
const exportStr = `\nexport {${exportStatement}}${exportFrom};`;
output.append(exportStr);
});
}
addConstants(output: MagicString, constants: string, file: ts.SourceFile): void {
if (constants === '') {
return;
}
const insertionPoint = findEndOfImports(file);
// Append the constants to the right of the insertion point, to ensure they get ordered after
// added imports (those are appended left to the insertion point).
output.appendRight(insertionPoint, '\n' + constants + '\n');
}
/**
* Add the definitions to each decorated class
*/
addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void {
const classSymbol = this.host.getClassSymbol(compiledClass.declaration);
if (!classSymbol) {
throw new Error(`Compiled class does not have a valid symbol: ${compiledClass.name}`);
}
const insertionPoint = classSymbol.valueDeclaration !.getEnd();
output.appendLeft(insertionPoint, '\n' + definitions);
}
/**
* Remove static decorator properties from classes
*/
removeDecorators(output: MagicString, decoratorsToRemove: RedundantDecoratorMap): void {
decoratorsToRemove.forEach((nodesToRemove, containerNode) => {
if (ts.isArrayLiteralExpression(containerNode)) {
const items = containerNode.elements;
if (items.length === nodesToRemove.length) {
// Remove the entire statement
const statement = findStatement(containerNode);
if (statement) {
output.remove(statement.getFullStart(), statement.getEnd());
}
} else {
nodesToRemove.forEach(node => {
// remove any trailing comma
const end = (output.slice(node.getEnd(), node.getEnd() + 1) === ',') ?
node.getEnd() + 1 :
node.getEnd();
output.remove(node.getFullStart(), end);
});
}
}
});
}
rewriteSwitchableDeclarations(
outputText: MagicString, sourceFile: ts.SourceFile,
declarations: SwitchableVariableDeclaration[]): void {
declarations.forEach(declaration => {
const start = declaration.initializer.getStart();
const end = declaration.initializer.getEnd();
const replacement = declaration.initializer.text.replace(PRE_R3_MARKER, POST_R3_MARKER);
outputText.overwrite(start, end, replacement);
});
}
}
function findEndOfImports(sf: ts.SourceFile): number {
for (const stmt of sf.statements) {
if (!ts.isImportDeclaration(stmt) && !ts.isImportEqualsDeclaration(stmt) &&
!ts.isNamespaceImport(stmt)) {
return stmt.getStart();
}
}
return 0;
}
function findStatement(node: ts.Node) {
while (node) {
if (ts.isExpressionStatement(node)) {
return node;
}
node = node.parent;
}
return undefined;
}

View File

@ -0,0 +1,218 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import MagicString from 'magic-string';
import * as ts from 'typescript';
import {PathSegment, AbsoluteFsPath} from '../../../src/ngtsc/path';
import {Import, ImportManager} from '../../../src/ngtsc/translator';
import {isDtsPath} from '../../../src/ngtsc/util/src/typescript';
import {CompiledClass} from '../analysis/decoration_analyzer';
import {NgccReflectionHost, POST_R3_MARKER, PRE_R3_MARKER, SwitchableVariableDeclaration} from '../host/ngcc_host';
import {ModuleWithProvidersInfo} from '../analysis/module_with_providers_analyzer';
import {ExportInfo} from '../analysis/private_declarations_analyzer';
import {RenderingFormatter, RedundantDecoratorMap} from './rendering_formatter';
import {stripExtension} from './utils';
/**
* A RenderingFormatter that works with ECMAScript Module import and export statements.
*/
export class EsmRenderingFormatter implements RenderingFormatter {
constructor(protected host: NgccReflectionHost, protected isCore: boolean) {}
/**
* Add the imports at the top of the file, after any imports that are already there.
*/
addImports(output: MagicString, imports: Import[], sf: ts.SourceFile): void {
const insertionPoint = this.findEndOfImports(sf);
const renderedImports =
imports.map(i => `import * as ${i.qualifier} from '${i.specifier}';\n`).join('');
output.appendLeft(insertionPoint, renderedImports);
}
/**
* Add the exports to the end of the file.
*/
addExports(
output: MagicString, entryPointBasePath: AbsoluteFsPath, exports: ExportInfo[],
importManager: ImportManager, file: ts.SourceFile): void {
exports.forEach(e => {
let exportFrom = '';
const isDtsFile = isDtsPath(entryPointBasePath);
const from = isDtsFile ? e.dtsFrom : e.from;
if (from) {
const basePath = stripExtension(from);
const relativePath =
'./' + PathSegment.relative(AbsoluteFsPath.dirname(entryPointBasePath), basePath);
exportFrom = entryPointBasePath !== basePath ? ` from '${relativePath}'` : '';
}
// aliases should only be added in dts files as these are lost when rolling up dts file.
const exportStatement = e.alias && isDtsFile ? `${e.alias} as ${e.identifier}` : e.identifier;
const exportStr = `\nexport {${exportStatement}}${exportFrom};`;
output.append(exportStr);
});
}
/**
* Add the constants directly after the imports.
*/
addConstants(output: MagicString, constants: string, file: ts.SourceFile): void {
if (constants === '') {
return;
}
const insertionPoint = this.findEndOfImports(file);
// Append the constants to the right of the insertion point, to ensure they get ordered after
// added imports (those are appended left to the insertion point).
output.appendRight(insertionPoint, '\n' + constants + '\n');
}
/**
* Add the definitions directly after their decorated class.
*/
addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void {
const classSymbol = this.host.getClassSymbol(compiledClass.declaration);
if (!classSymbol) {
throw new Error(`Compiled class does not have a valid symbol: ${compiledClass.name}`);
}
const insertionPoint = classSymbol.valueDeclaration !.getEnd();
output.appendLeft(insertionPoint, '\n' + definitions);
}
/**
* Remove static decorator properties from classes.
*/
removeDecorators(output: MagicString, decoratorsToRemove: RedundantDecoratorMap): void {
decoratorsToRemove.forEach((nodesToRemove, containerNode) => {
if (ts.isArrayLiteralExpression(containerNode)) {
const items = containerNode.elements;
if (items.length === nodesToRemove.length) {
// Remove the entire statement
const statement = findStatement(containerNode);
if (statement) {
output.remove(statement.getFullStart(), statement.getEnd());
}
} else {
nodesToRemove.forEach(node => {
// remove any trailing comma
const end = (output.slice(node.getEnd(), node.getEnd() + 1) === ',') ?
node.getEnd() + 1 :
node.getEnd();
output.remove(node.getFullStart(), end);
});
}
}
});
}
/**
* Rewrite the the IVY switch markers to indicate we are in IVY mode.
*/
rewriteSwitchableDeclarations(
outputText: MagicString, sourceFile: ts.SourceFile,
declarations: SwitchableVariableDeclaration[]): void {
declarations.forEach(declaration => {
const start = declaration.initializer.getStart();
const end = declaration.initializer.getEnd();
const replacement = declaration.initializer.text.replace(PRE_R3_MARKER, POST_R3_MARKER);
outputText.overwrite(start, end, replacement);
});
}
/**
* Add the type parameters to the appropriate functions that return `ModuleWithProviders`
* structures.
*
* This function will only get called on typings files.
*/
addModuleWithProvidersParams(
outputText: MagicString, moduleWithProviders: ModuleWithProvidersInfo[],
importManager: ImportManager): void {
moduleWithProviders.forEach(info => {
const ngModuleName = info.ngModule.node.name.text;
const declarationFile = AbsoluteFsPath.fromSourceFile(info.declaration.getSourceFile());
const ngModuleFile = AbsoluteFsPath.fromSourceFile(info.ngModule.node.getSourceFile());
const importPath = info.ngModule.viaModule ||
(declarationFile !== ngModuleFile ?
stripExtension(
`./${PathSegment.relative(AbsoluteFsPath.dirname(declarationFile), ngModuleFile)}`) :
null);
const ngModule = generateImportString(importManager, importPath, ngModuleName);
if (info.declaration.type) {
const typeName = info.declaration.type && ts.isTypeReferenceNode(info.declaration.type) ?
info.declaration.type.typeName :
null;
if (this.isCoreModuleWithProvidersType(typeName)) {
// The declaration already returns `ModuleWithProvider` but it needs the `NgModule` type
// parameter adding.
outputText.overwrite(
info.declaration.type.getStart(), info.declaration.type.getEnd(),
`ModuleWithProviders<${ngModule}>`);
} else {
// The declaration returns an unknown type so we need to convert it to a union that
// includes the ngModule property.
const originalTypeString = info.declaration.type.getText();
outputText.overwrite(
info.declaration.type.getStart(), info.declaration.type.getEnd(),
`(${originalTypeString})&{ngModule:${ngModule}}`);
}
} else {
// The declaration has no return type so provide one.
const lastToken = info.declaration.getLastToken();
const insertPoint = lastToken && lastToken.kind === ts.SyntaxKind.SemicolonToken ?
lastToken.getStart() :
info.declaration.getEnd();
outputText.appendLeft(
insertPoint,
`: ${generateImportString(importManager, '@angular/core', 'ModuleWithProviders')}<${ngModule}>`);
}
});
}
protected findEndOfImports(sf: ts.SourceFile): number {
for (const stmt of sf.statements) {
if (!ts.isImportDeclaration(stmt) && !ts.isImportEqualsDeclaration(stmt) &&
!ts.isNamespaceImport(stmt)) {
return stmt.getStart();
}
}
return 0;
}
/**
* Check whether the given type is the core Angular `ModuleWithProviders` interface.
* @param typeName The type to check.
* @returns true if the type is the core Angular `ModuleWithProviders` interface.
*/
private isCoreModuleWithProvidersType(typeName: ts.EntityName|null) {
const id =
typeName && ts.isIdentifier(typeName) ? this.host.getImportOfIdentifier(typeName) : null;
return (
id && id.name === 'ModuleWithProviders' && (this.isCore || id.from === '@angular/core'));
}
}
function findStatement(node: ts.Node) {
while (node) {
if (ts.isExpressionStatement(node)) {
return node;
}
node = node.parent;
}
return undefined;
}
function generateImportString(
importManager: ImportManager, importPath: string | null, importName: string) {
const importAs = importPath ? importManager.generateNamedImport(importPath, importName) : null;
return importAs ? `${importAs.moduleImport}.${importAs.symbol}` : `${importName}`;
}

View File

@ -6,72 +6,22 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ConstantPool, Expression, Statement, WrappedNodeExpr, WritePropExpr} from '@angular/compiler';
import {SourceMapConverter, commentRegex, fromJSON, fromObject, fromSource, generateMapFileComment, mapFileCommentRegex, removeComments, removeMapFileComments} from 'convert-source-map';
import MagicString from 'magic-string';
import {SourceMapConsumer, SourceMapGenerator, RawSourceMap} from 'source-map';
import * as ts from 'typescript';
import {NoopImportRewriter, ImportRewriter, R3SymbolsImportRewriter, NOOP_DEFAULT_IMPORT_RECORDER} from '../../../src/ngtsc/imports';
import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path';
import {CompileResult} from '../../../src/ngtsc/transform';
import {translateStatement, translateType, Import, ImportManager} from '../../../src/ngtsc/translator';
import {NOOP_DEFAULT_IMPORT_RECORDER} from '@angular/compiler-cli/src/ngtsc/imports';
import {translateStatement, ImportManager} from '../../../src/ngtsc/translator';
import {CompiledClass, CompiledFile, DecorationAnalyses} from '../analysis/decoration_analyzer';
import {ModuleWithProvidersInfo, ModuleWithProvidersAnalyses} from '../analysis/module_with_providers_analyzer';
import {PrivateDeclarationsAnalyses, ExportInfo} from '../analysis/private_declarations_analyzer';
import {PrivateDeclarationsAnalyses} from '../analysis/private_declarations_analyzer';
import {SwitchMarkerAnalyses, SwitchMarkerAnalysis} from '../analysis/switch_marker_analyzer';
import {IMPORT_PREFIX} from '../constants';
import {FileSystem} from '../file_system/file_system';
import {NgccReflectionHost, SwitchableVariableDeclaration} from '../host/ngcc_host';
import {Logger} from '../logging/logger';
import {NgccReflectionHost} from '../host/ngcc_host';
import {EntryPointBundle} from '../packages/entry_point_bundle';
import {NgccFlatImportRewriter} from './ngcc_import_rewriter';
interface SourceMapInfo {
source: string;
map: SourceMapConverter|null;
isInline: boolean;
}
/**
* Information about a file that has been rendered.
*/
export interface FileInfo {
/**
* Path to where the file should be written.
*/
path: AbsoluteFsPath;
/**
* The contents of the file to be be written.
*/
contents: string;
}
interface DtsClassInfo {
dtsDeclaration: ts.Declaration;
compilation: CompileResult[];
}
/**
* A structure that captures information about what needs to be rendered
* in a typings file.
*
* It is created as a result of processing the analysis passed to the renderer.
*
* The `renderDtsFile()` method consumes it when rendering a typings file.
*/
class DtsRenderInfo {
classInfo: DtsClassInfo[] = [];
moduleWithProviders: ModuleWithProvidersInfo[] = [];
privateExports: ExportInfo[] = [];
}
/**
* The collected decorators that have become redundant after the compilation
* of Ivy static fields. The map is keyed by the container node, such that we
* can tell if we should remove the entire decorator property
*/
export type RedundantDecoratorMap = Map<ts.Node, ts.Node[]>;
export const RedundantDecoratorMap = Map;
import {Logger} from '../logging/logger';
import {FileToWrite, getImportRewriter, stripExtension} from './utils';
import {RenderingFormatter, RedundantDecoratorMap} from './rendering_formatter';
import {extractSourceMap, renderSourceAndMap} from './source_maps';
/**
* A base-class for rendering an `AnalyzedFile`.
@ -79,42 +29,28 @@ export const RedundantDecoratorMap = Map;
* Package formats have output files that must be rendered differently. Concrete sub-classes must
* implement the `addImports`, `addDefinitions` and `removeDecorators` abstract methods.
*/
export abstract class Renderer {
export class Renderer {
constructor(
protected fs: FileSystem, protected logger: Logger, protected host: NgccReflectionHost,
protected isCore: boolean, protected bundle: EntryPointBundle) {}
private srcFormatter: RenderingFormatter, private fs: FileSystem, private logger: Logger,
private host: NgccReflectionHost, private isCore: boolean, private bundle: EntryPointBundle) {
}
renderProgram(
decorationAnalyses: DecorationAnalyses, switchMarkerAnalyses: SwitchMarkerAnalyses,
privateDeclarationsAnalyses: PrivateDeclarationsAnalyses,
moduleWithProvidersAnalyses: ModuleWithProvidersAnalyses|null): FileInfo[] {
const renderedFiles: FileInfo[] = [];
privateDeclarationsAnalyses: PrivateDeclarationsAnalyses): FileToWrite[] {
const renderedFiles: FileToWrite[] = [];
// Transform the source files.
this.bundle.src.program.getSourceFiles().forEach(sourceFile => {
const compiledFile = decorationAnalyses.get(sourceFile);
const switchMarkerAnalysis = switchMarkerAnalyses.get(sourceFile);
if (compiledFile || switchMarkerAnalysis || sourceFile === this.bundle.src.file) {
if (decorationAnalyses.has(sourceFile) || switchMarkerAnalyses.has(sourceFile) ||
sourceFile === this.bundle.src.file) {
const compiledFile = decorationAnalyses.get(sourceFile);
const switchMarkerAnalysis = switchMarkerAnalyses.get(sourceFile);
renderedFiles.push(...this.renderFile(
sourceFile, compiledFile, switchMarkerAnalysis, privateDeclarationsAnalyses));
}
});
// Transform the .d.ts files
if (this.bundle.dts) {
const dtsFiles = this.getTypingsFilesToRender(
decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses);
// If the dts entry-point is not already there (it did not have compiled classes)
// then add it now, to ensure it gets its extra exports rendered.
if (!dtsFiles.has(this.bundle.dts.file)) {
dtsFiles.set(this.bundle.dts.file, new DtsRenderInfo());
}
dtsFiles.forEach(
(renderInfo, file) => renderedFiles.push(...this.renderDtsFile(file, renderInfo)));
}
return renderedFiles;
}
@ -126,32 +62,32 @@ export abstract class Renderer {
renderFile(
sourceFile: ts.SourceFile, compiledFile: CompiledFile|undefined,
switchMarkerAnalysis: SwitchMarkerAnalysis|undefined,
privateDeclarationsAnalyses: PrivateDeclarationsAnalyses): FileInfo[] {
privateDeclarationsAnalyses: PrivateDeclarationsAnalyses): FileToWrite[] {
const isEntryPoint = sourceFile === this.bundle.src.file;
const input = this.extractSourceMap(sourceFile);
const input = extractSourceMap(this.fs, this.logger, sourceFile);
const outputText = new MagicString(input.source);
if (switchMarkerAnalysis) {
this.rewriteSwitchableDeclarations(
this.srcFormatter.rewriteSwitchableDeclarations(
outputText, switchMarkerAnalysis.sourceFile, switchMarkerAnalysis.declarations);
}
const importManager = new ImportManager(
this.getImportRewriter(this.bundle.src.r3SymbolsFile, this.bundle.isFlatCore),
getImportRewriter(this.bundle.src.r3SymbolsFile, this.isCore, this.bundle.isFlatCore),
IMPORT_PREFIX);
if (compiledFile) {
// TODO: remove constructor param metadata and property decorators (we need info from the
// handlers to do this)
const decoratorsToRemove = this.computeDecoratorsToRemove(compiledFile.compiledClasses);
this.removeDecorators(outputText, decoratorsToRemove);
this.srcFormatter.removeDecorators(outputText, decoratorsToRemove);
compiledFile.compiledClasses.forEach(clazz => {
const renderedDefinition = renderDefinitions(compiledFile.sourceFile, clazz, importManager);
this.addDefinitions(outputText, clazz, renderedDefinition);
this.srcFormatter.addDefinitions(outputText, clazz, renderedDefinition);
});
this.addConstants(
this.srcFormatter.addConstants(
outputText,
renderConstantPool(compiledFile.sourceFile, compiledFile.constantPool, importManager),
compiledFile.sourceFile);
@ -160,115 +96,22 @@ export abstract class Renderer {
// Add exports to the entry-point file
if (isEntryPoint) {
const entryPointBasePath = stripExtension(this.bundle.src.path);
this.addExports(
this.srcFormatter.addExports(
outputText, entryPointBasePath, privateDeclarationsAnalyses, importManager, sourceFile);
}
if (isEntryPoint || compiledFile) {
this.addImports(outputText, importManager.getAllImports(sourceFile.fileName), sourceFile);
this.srcFormatter.addImports(
outputText, importManager.getAllImports(sourceFile.fileName), sourceFile);
}
if (compiledFile || switchMarkerAnalysis || isEntryPoint) {
return this.renderSourceAndMap(sourceFile, input, outputText);
return renderSourceAndMap(sourceFile, input, outputText);
} else {
return [];
}
}
renderDtsFile(dtsFile: ts.SourceFile, renderInfo: DtsRenderInfo): FileInfo[] {
const input = this.extractSourceMap(dtsFile);
const outputText = new MagicString(input.source);
const printer = createPrinter();
const importManager = new ImportManager(
this.getImportRewriter(this.bundle.dts !.r3SymbolsFile, false), IMPORT_PREFIX);
renderInfo.classInfo.forEach(dtsClass => {
const endOfClass = dtsClass.dtsDeclaration.getEnd();
dtsClass.compilation.forEach(declaration => {
const type = translateType(declaration.type, importManager);
const typeStr = printer.printNode(ts.EmitHint.Unspecified, type, dtsFile);
const newStatement = ` static ${declaration.name}: ${typeStr};\n`;
outputText.appendRight(endOfClass - 1, newStatement);
});
});
this.addModuleWithProvidersParams(outputText, renderInfo.moduleWithProviders, importManager);
this.addImports(outputText, importManager.getAllImports(dtsFile.fileName), dtsFile);
this.addExports(
outputText, AbsoluteFsPath.fromSourceFile(dtsFile), renderInfo.privateExports,
importManager, dtsFile);
return this.renderSourceAndMap(dtsFile, input, outputText);
}
/**
* Add the type parameters to the appropriate functions that return `ModuleWithProviders`
* structures.
*
* This function only gets called on typings files, so it doesn't need different implementations
* for each bundle format.
*/
protected addModuleWithProvidersParams(
outputText: MagicString, moduleWithProviders: ModuleWithProvidersInfo[],
importManager: ImportManager): void {
moduleWithProviders.forEach(info => {
const ngModuleName = info.ngModule.node.name.text;
const declarationFile = AbsoluteFsPath.fromSourceFile(info.declaration.getSourceFile());
const ngModuleFile = AbsoluteFsPath.fromSourceFile(info.ngModule.node.getSourceFile());
const importPath = info.ngModule.viaModule ||
(declarationFile !== ngModuleFile ?
stripExtension(
`./${PathSegment.relative(AbsoluteFsPath.dirname(declarationFile), ngModuleFile)}`) :
null);
const ngModule = getImportString(importManager, importPath, ngModuleName);
if (info.declaration.type) {
const typeName = info.declaration.type && ts.isTypeReferenceNode(info.declaration.type) ?
info.declaration.type.typeName :
null;
if (this.isCoreModuleWithProvidersType(typeName)) {
// The declaration already returns `ModuleWithProvider` but it needs the `NgModule` type
// parameter adding.
outputText.overwrite(
info.declaration.type.getStart(), info.declaration.type.getEnd(),
`ModuleWithProviders<${ngModule}>`);
} else {
// The declaration returns an unknown type so we need to convert it to a union that
// includes the ngModule property.
const originalTypeString = info.declaration.type.getText();
outputText.overwrite(
info.declaration.type.getStart(), info.declaration.type.getEnd(),
`(${originalTypeString})&{ngModule:${ngModule}}`);
}
} else {
// The declaration has no return type so provide one.
const lastToken = info.declaration.getLastToken();
const insertPoint = lastToken && lastToken.kind === ts.SyntaxKind.SemicolonToken ?
lastToken.getStart() :
info.declaration.getEnd();
outputText.appendLeft(
insertPoint,
`: ${getImportString(importManager, '@angular/core', 'ModuleWithProviders')}<${ngModule}>`);
}
});
}
protected abstract addConstants(output: MagicString, constants: string, file: ts.SourceFile):
void;
protected abstract addImports(output: MagicString, imports: Import[], sf: ts.SourceFile): void;
protected abstract addExports(
output: MagicString, entryPointBasePath: AbsoluteFsPath, exports: ExportInfo[],
importManager: ImportManager, file: ts.SourceFile): void;
protected abstract addDefinitions(
output: MagicString, compiledClass: CompiledClass, definitions: string): void;
protected abstract removeDecorators(
output: MagicString, decoratorsToRemove: RedundantDecoratorMap): void;
protected abstract rewriteSwitchableDeclarations(
outputText: MagicString, sourceFile: ts.SourceFile,
declarations: SwitchableVariableDeclaration[]): void;
/**
* From the given list of classes, computes a map of decorators that should be removed.
* The decorators to remove are keyed by their container node, such that we can tell if
@ -276,7 +119,7 @@ export abstract class Renderer {
* @param classes The list of classes that may have decorators to remove.
* @returns A map of decorators to remove, keyed by their container node.
*/
protected computeDecoratorsToRemove(classes: CompiledClass[]): RedundantDecoratorMap {
private computeDecoratorsToRemove(classes: CompiledClass[]): RedundantDecoratorMap {
const decoratorsToRemove = new RedundantDecoratorMap();
classes.forEach(clazz => {
clazz.decorators.forEach(dec => {
@ -290,191 +133,6 @@ export abstract class Renderer {
});
return decoratorsToRemove;
}
/**
* Get the map from the source (note whether it is inline or external)
*/
protected extractSourceMap(file: ts.SourceFile): SourceMapInfo {
const inline = commentRegex.test(file.text);
const external = mapFileCommentRegex.exec(file.text);
if (inline) {
const inlineSourceMap = fromSource(file.text);
return {
source: removeComments(file.text).replace(/\n\n$/, '\n'),
map: inlineSourceMap,
isInline: true,
};
} else if (external) {
let externalSourceMap: SourceMapConverter|null = null;
try {
const fileName = external[1] || external[2];
const filePath = AbsoluteFsPath.resolve(
AbsoluteFsPath.dirname(AbsoluteFsPath.fromSourceFile(file)), fileName);
const mappingFile = this.fs.readFile(filePath);
externalSourceMap = fromJSON(mappingFile);
} catch (e) {
if (e.code === 'ENOENT') {
this.logger.warn(
`The external map file specified in the source code comment "${e.path}" was not found on the file system.`);
const mapPath = AbsoluteFsPath.fromUnchecked(file.fileName + '.map');
if (PathSegment.basename(e.path) !== PathSegment.basename(mapPath) &&
this.fs.stat(mapPath).isFile()) {
this.logger.warn(
`Guessing the map file name from the source file name: "${PathSegment.basename(mapPath)}"`);
try {
externalSourceMap = fromObject(JSON.parse(this.fs.readFile(mapPath)));
} catch (e) {
this.logger.error(e);
}
}
}
}
return {
source: removeMapFileComments(file.text).replace(/\n\n$/, '\n'),
map: externalSourceMap,
isInline: false,
};
} else {
return {source: file.text, map: null, isInline: false};
}
}
/**
* Merge the input and output source-maps, replacing the source-map comment in the output file
* with an appropriate source-map comment pointing to the merged source-map.
*/
protected renderSourceAndMap(
sourceFile: ts.SourceFile, input: SourceMapInfo, output: MagicString): FileInfo[] {
const outputPath = AbsoluteFsPath.fromSourceFile(sourceFile);
const outputMapPath = AbsoluteFsPath.fromUnchecked(`${outputPath}.map`);
const relativeSourcePath = PathSegment.basename(outputPath);
const relativeMapPath = `${relativeSourcePath}.map`;
const outputMap = output.generateMap({
source: outputPath,
includeContent: true,
// hires: true // TODO: This results in accurate but huge sourcemaps. Instead we should fix
// the merge algorithm.
});
// we must set this after generation as magic string does "manipulation" on the path
outputMap.file = relativeSourcePath;
const mergedMap =
mergeSourceMaps(input.map && input.map.toObject(), JSON.parse(outputMap.toString()));
const result: FileInfo[] = [];
if (input.isInline) {
result.push({path: outputPath, contents: `${output.toString()}\n${mergedMap.toComment()}`});
} else {
result.push({
path: outputPath,
contents: `${output.toString()}\n${generateMapFileComment(relativeMapPath)}`
});
result.push({path: outputMapPath, contents: mergedMap.toJSON()});
}
return result;
}
protected getTypingsFilesToRender(
decorationAnalyses: DecorationAnalyses,
privateDeclarationsAnalyses: PrivateDeclarationsAnalyses,
moduleWithProvidersAnalyses: ModuleWithProvidersAnalyses|
null): Map<ts.SourceFile, DtsRenderInfo> {
const dtsMap = new Map<ts.SourceFile, DtsRenderInfo>();
// Capture the rendering info from the decoration analyses
decorationAnalyses.forEach(compiledFile => {
compiledFile.compiledClasses.forEach(compiledClass => {
const dtsDeclaration = this.host.getDtsDeclaration(compiledClass.declaration);
if (dtsDeclaration) {
const dtsFile = dtsDeclaration.getSourceFile();
const renderInfo = dtsMap.get(dtsFile) || new DtsRenderInfo();
renderInfo.classInfo.push({dtsDeclaration, compilation: compiledClass.compilation});
dtsMap.set(dtsFile, renderInfo);
}
});
});
// Capture the ModuleWithProviders functions/methods that need updating
if (moduleWithProvidersAnalyses !== null) {
moduleWithProvidersAnalyses.forEach((moduleWithProvidersToFix, dtsFile) => {
const renderInfo = dtsMap.get(dtsFile) || new DtsRenderInfo();
renderInfo.moduleWithProviders = moduleWithProvidersToFix;
dtsMap.set(dtsFile, renderInfo);
});
}
// Capture the private declarations that need to be re-exported
if (privateDeclarationsAnalyses.length) {
privateDeclarationsAnalyses.forEach(e => {
if (!e.dtsFrom && !e.alias) {
throw new Error(
`There is no typings path for ${e.identifier} in ${e.from}.\n` +
`We need to add an export for this class to a .d.ts typings file because ` +
`Angular compiler needs to be able to reference this class in compiled code, such as templates.\n` +
`The simplest fix for this is to ensure that this class is exported from the package's entry-point.`);
}
});
const dtsEntryPoint = this.bundle.dts !.file;
const renderInfo = dtsMap.get(dtsEntryPoint) || new DtsRenderInfo();
renderInfo.privateExports = privateDeclarationsAnalyses;
dtsMap.set(dtsEntryPoint, renderInfo);
}
return dtsMap;
}
/**
* Check whether the given type is the core Angular `ModuleWithProviders` interface.
* @param typeName The type to check.
* @returns true if the type is the core Angular `ModuleWithProviders` interface.
*/
private isCoreModuleWithProvidersType(typeName: ts.EntityName|null) {
const id =
typeName && ts.isIdentifier(typeName) ? this.host.getImportOfIdentifier(typeName) : null;
return (
id && id.name === 'ModuleWithProviders' && (this.isCore || id.from === '@angular/core'));
}
private getImportRewriter(r3SymbolsFile: ts.SourceFile|null, isFlat: boolean): ImportRewriter {
if (this.isCore && isFlat) {
return new NgccFlatImportRewriter();
} else if (this.isCore) {
return new R3SymbolsImportRewriter(r3SymbolsFile !.fileName);
} else {
return new NoopImportRewriter();
}
}
}
/**
* Merge the two specified source-maps into a single source-map that hides the intermediate
* source-map.
* E.g. Consider these mappings:
*
* ```
* OLD_SRC -> OLD_MAP -> INTERMEDIATE_SRC -> NEW_MAP -> NEW_SRC
* ```
*
* this will be replaced with:
*
* ```
* OLD_SRC -> MERGED_MAP -> NEW_SRC
* ```
*/
export function mergeSourceMaps(
oldMap: RawSourceMap | null, newMap: RawSourceMap): SourceMapConverter {
if (!oldMap) {
return fromObject(newMap);
}
const oldMapConsumer = new SourceMapConsumer(oldMap);
const newMapConsumer = new SourceMapConsumer(newMap);
const mergedMapGenerator = SourceMapGenerator.fromSourceMap(newMapConsumer);
mergedMapGenerator.applySourceMap(oldMapConsumer);
const merged = fromJSON(mergedMapGenerator.toString());
return merged;
}
/**
@ -515,10 +173,6 @@ export function renderDefinitions(
return definitions;
}
export function stripExtension<T extends string>(filePath: T): T {
return filePath.replace(/\.(js|d\.ts)$/, '') as T;
}
/**
* Create an Angular AST statement node that contains the assignment of the
* compiled decorator to be applied to the class.
@ -530,12 +184,6 @@ function createAssignmentStatement(
return new WritePropExpr(receiver, propName, initializer).toStmt();
}
function getImportString(
importManager: ImportManager, importPath: string | null, importName: string) {
const importAs = importPath ? importManager.generateNamedImport(importPath, importName) : null;
return importAs ? `${importAs.moduleImport}.${importAs.symbol}` : `${importName}`;
}
function createPrinter(): ts.Printer {
return ts.createPrinter({newLine: ts.NewLineKind.LineFeed});
}
}

View File

@ -0,0 +1,42 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import MagicString from 'magic-string';
import * as ts from 'typescript';
import {Import, ImportManager} from '../../../src/ngtsc/translator';
import {ExportInfo} from '../analysis/private_declarations_analyzer';
import {CompiledClass} from '../analysis/decoration_analyzer';
import {SwitchableVariableDeclaration} from '../host/ngcc_host';
import {ModuleWithProvidersInfo} from '../analysis/module_with_providers_analyzer';
/**
* The collected decorators that have become redundant after the compilation
* of Ivy static fields. The map is keyed by the container node, such that we
* can tell if we should remove the entire decorator property
*/
export type RedundantDecoratorMap = Map<ts.Node, ts.Node[]>;
export const RedundantDecoratorMap = Map;
/**
* Implement this interface with methods that know how to render a specific format,
* such as ESM5 or UMD.
*/
export interface RenderingFormatter {
addConstants(output: MagicString, constants: string, file: ts.SourceFile): void;
addImports(output: MagicString, imports: Import[], sf: ts.SourceFile): void;
addExports(
output: MagicString, entryPointBasePath: string, exports: ExportInfo[],
importManager: ImportManager, file: ts.SourceFile): void;
addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void;
removeDecorators(output: MagicString, decoratorsToRemove: RedundantDecoratorMap): void;
rewriteSwitchableDeclarations(
outputText: MagicString, sourceFile: ts.SourceFile,
declarations: SwitchableVariableDeclaration[]): void;
addModuleWithProvidersParams(
outputText: MagicString, moduleWithProviders: ModuleWithProvidersInfo[],
importManager: ImportManager): void;
}

View File

@ -0,0 +1,137 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {SourceMapConverter, commentRegex, fromJSON, fromObject, fromSource, generateMapFileComment, mapFileCommentRegex, removeComments, removeMapFileComments} from 'convert-source-map';
import MagicString from 'magic-string';
import {RawSourceMap, SourceMapConsumer, SourceMapGenerator} from 'source-map';
import * as ts from 'typescript';
import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path';
import {FileSystem} from '../file_system/file_system';
import {Logger} from '../logging/logger';
import {FileToWrite} from './utils';
export interface SourceMapInfo {
source: string;
map: SourceMapConverter|null;
isInline: boolean;
}
/**
* Get the map from the source (note whether it is inline or external)
*/
export function extractSourceMap(
fs: FileSystem, logger: Logger, file: ts.SourceFile): SourceMapInfo {
const inline = commentRegex.test(file.text);
const external = mapFileCommentRegex.exec(file.text);
if (inline) {
const inlineSourceMap = fromSource(file.text);
return {
source: removeComments(file.text).replace(/\n\n$/, '\n'),
map: inlineSourceMap,
isInline: true,
};
} else if (external) {
let externalSourceMap: SourceMapConverter|null = null;
try {
const fileName = external[1] || external[2];
const filePath = AbsoluteFsPath.resolve(
AbsoluteFsPath.dirname(AbsoluteFsPath.fromSourceFile(file)), fileName);
const mappingFile = fs.readFile(filePath);
externalSourceMap = fromJSON(mappingFile);
} catch (e) {
if (e.code === 'ENOENT') {
logger.warn(
`The external map file specified in the source code comment "${e.path}" was not found on the file system.`);
const mapPath = AbsoluteFsPath.fromUnchecked(file.fileName + '.map');
if (PathSegment.basename(e.path) !== PathSegment.basename(mapPath) && fs.exists(mapPath) &&
fs.stat(mapPath).isFile()) {
logger.warn(
`Guessing the map file name from the source file name: "${PathSegment.basename(mapPath)}"`);
try {
externalSourceMap = fromObject(JSON.parse(fs.readFile(mapPath)));
} catch (e) {
logger.error(e);
}
}
}
}
return {
source: removeMapFileComments(file.text).replace(/\n\n$/, '\n'),
map: externalSourceMap,
isInline: false,
};
} else {
return {source: file.text, map: null, isInline: false};
}
}
/**
* Merge the input and output source-maps, replacing the source-map comment in the output file
* with an appropriate source-map comment pointing to the merged source-map.
*/
export function renderSourceAndMap(
sourceFile: ts.SourceFile, input: SourceMapInfo, output: MagicString): FileToWrite[] {
const outputPath = AbsoluteFsPath.fromSourceFile(sourceFile);
const outputMapPath = AbsoluteFsPath.fromUnchecked(`${outputPath}.map`);
const relativeSourcePath = PathSegment.basename(outputPath);
const relativeMapPath = `${relativeSourcePath}.map`;
const outputMap = output.generateMap({
source: outputPath,
includeContent: true,
// hires: true // TODO: This results in accurate but huge sourcemaps. Instead we should fix
// the merge algorithm.
});
// we must set this after generation as magic string does "manipulation" on the path
outputMap.file = relativeSourcePath;
const mergedMap =
mergeSourceMaps(input.map && input.map.toObject(), JSON.parse(outputMap.toString()));
const result: FileToWrite[] = [];
if (input.isInline) {
result.push({path: outputPath, contents: `${output.toString()}\n${mergedMap.toComment()}`});
} else {
result.push({
path: outputPath,
contents: `${output.toString()}\n${generateMapFileComment(relativeMapPath)}`
});
result.push({path: outputMapPath, contents: mergedMap.toJSON()});
}
return result;
}
/**
* Merge the two specified source-maps into a single source-map that hides the intermediate
* source-map.
* E.g. Consider these mappings:
*
* ```
* OLD_SRC -> OLD_MAP -> INTERMEDIATE_SRC -> NEW_MAP -> NEW_SRC
* ```
*
* this will be replaced with:
*
* ```
* OLD_SRC -> MERGED_MAP -> NEW_SRC
* ```
*/
export function mergeSourceMaps(
oldMap: RawSourceMap | null, newMap: RawSourceMap): SourceMapConverter {
if (!oldMap) {
return fromObject(newMap);
}
const oldMapConsumer = new SourceMapConsumer(oldMap);
const newMapConsumer = new SourceMapConsumer(newMap);
const mergedMapGenerator = SourceMapGenerator.fromSourceMap(newMapConsumer);
mergedMapGenerator.applySourceMap(oldMapConsumer);
const merged = fromJSON(mergedMapGenerator.toString());
return merged;
}

View File

@ -10,25 +10,23 @@ import * as ts from 'typescript';
import MagicString from 'magic-string';
import {Import, ImportManager} from '../../../src/ngtsc/translator';
import {ExportInfo} from '../analysis/private_declarations_analyzer';
import {FileSystem} from '../file_system/file_system';
import {UmdReflectionHost} from '../host/umd_host';
import {Logger} from '../logging/logger';
import {EntryPointBundle} from '../packages/entry_point_bundle';
import {Esm5Renderer} from './esm5_renderer';
import {stripExtension} from './renderer';
import {Esm5RenderingFormatter} from './esm5_rendering_formatter';
import {stripExtension} from './utils';
type CommonJsConditional = ts.ConditionalExpression & {whenTrue: ts.CallExpression};
type AmdConditional = ts.ConditionalExpression & {whenTrue: ts.CallExpression};
export class UmdRenderer extends Esm5Renderer {
constructor(
fs: FileSystem, logger: Logger, protected umdHost: UmdReflectionHost, isCore: boolean,
bundle: EntryPointBundle) {
super(fs, logger, umdHost, isCore, bundle);
}
/**
* A RenderingFormatter that works with UMD files, instead of `import` and `export` statements
* the module is an IIFE with a factory function call with dependencies, which are defined in a
* wrapper function for AMD, CommonJS and global module formats.
*/
export class UmdRenderingFormatter extends Esm5RenderingFormatter {
constructor(protected umdHost: UmdReflectionHost, isCore: boolean) { super(umdHost, isCore); }
/**
* Add the imports at the top of the file
* Add the imports to the UMD module IIFE.
*/
addImports(output: MagicString, imports: Import[], file: ts.SourceFile): void {
// Assume there is only one UMD module in the file
@ -46,6 +44,9 @@ export class UmdRenderer extends Esm5Renderer {
renderFactoryParameters(output, wrapperFunction, imports);
}
/**
* Add the exports to the bottom of the UMD module factory function.
*/
addExports(
output: MagicString, entryPointBasePath: string, exports: ExportInfo[],
importManager: ImportManager, file: ts.SourceFile): void {
@ -70,6 +71,9 @@ export class UmdRenderer extends Esm5Renderer {
});
}
/**
* Add the constants to the top of the UMD factory function.
*/
addConstants(output: MagicString, constants: string, file: ts.SourceFile): void {
if (constants === '') {
return;
@ -86,6 +90,9 @@ export class UmdRenderer extends Esm5Renderer {
}
}
/**
* Add dependencies to the CommonJS part of the UMD wrapper function.
*/
function renderCommonJsDependencies(
output: MagicString, wrapperFunction: ts.FunctionExpression, imports: Import[]) {
const conditional = find(wrapperFunction.body.statements[0], isCommonJSConditional);
@ -98,6 +105,9 @@ function renderCommonJsDependencies(
imports.forEach(i => output.appendLeft(injectionPoint, `,require('${i.specifier}')`));
}
/**
* Add dependencies to the AMD part of the UMD wrapper function.
*/
function renderAmdDependencies(
output: MagicString, wrapperFunction: ts.FunctionExpression, imports: Import[]) {
const conditional = find(wrapperFunction.body.statements[0], isAmdConditional);
@ -113,17 +123,23 @@ function renderAmdDependencies(
imports.forEach(i => output.appendLeft(injectionPoint, `,'${i.specifier}'`));
}
/**
* Add dependencies to the global part of the UMD wrapper function.
*/
function renderGlobalDependencies(
output: MagicString, wrapperFunction: ts.FunctionExpression, imports: Import[]) {
const globalFactoryCall = find(wrapperFunction.body.statements[0], isGlobalFactoryCall);
if (!globalFactoryCall) {
return;
}
const injectionPoint = globalFactoryCall.getEnd() -
1; // Backup one char to account for the closing parenthesis on the call
// Backup one char to account for the closing parenthesis after the argument list of the call.
const injectionPoint = globalFactoryCall.getEnd() - 1;
imports.forEach(i => output.appendLeft(injectionPoint, `,global.${getGlobalIdentifier(i)}`));
}
/**
* Add dependency parameters to the UMD factory function.
*/
function renderFactoryParameters(
output: MagicString, wrapperFunction: ts.FunctionExpression, imports: Import[]) {
const wrapperCall = wrapperFunction.parent as ts.CallExpression;
@ -143,6 +159,9 @@ function renderFactoryParameters(
imports.forEach(i => output.appendLeft(injectionPoint, `,${i.qualifier}`));
}
/**
* Is this node the CommonJS conditional expression in the UMD wrapper?
*/
function isCommonJSConditional(value: ts.Node): value is CommonJsConditional {
if (!ts.isConditionalExpression(value)) {
return false;
@ -160,6 +179,9 @@ function isCommonJSConditional(value: ts.Node): value is CommonJsConditional {
return value.whenTrue.expression.text === 'factory';
}
/**
* Is this node the AMD conditional expression in the UMD wrapper?
*/
function isAmdConditional(value: ts.Node): value is AmdConditional {
if (!ts.isConditionalExpression(value)) {
return false;
@ -177,6 +199,9 @@ function isAmdConditional(value: ts.Node): value is AmdConditional {
return value.whenTrue.expression.text === 'define';
}
/**
* Is this node the call to setup the global dependencies in the UMD wrapper?
*/
function isGlobalFactoryCall(value: ts.Node): value is ts.CallExpression {
if (ts.isCallExpression(value) && !!value.parent) {
// Be resilient to the value being inside parentheses

View File

@ -0,0 +1,39 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter} from '../../../src/ngtsc/imports';
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
import {NgccFlatImportRewriter} from './ngcc_import_rewriter';
/**
* Information about a file that has been rendered.
*/
export interface FileToWrite {
/** Path to where the file should be written. */
path: AbsoluteFsPath;
/** The contents of the file to be be written. */
contents: string;
}
/**
* Create an appropriate ImportRewriter given the parameters.
*/
export function getImportRewriter(
r3SymbolsFile: ts.SourceFile | null, isCore: boolean, isFlat: boolean): ImportRewriter {
if (isCore && isFlat) {
return new NgccFlatImportRewriter();
} else if (isCore) {
return new R3SymbolsImportRewriter(r3SymbolsFile !.fileName);
} else {
return new NoopImportRewriter();
}
}
export function stripExtension<T extends string>(filePath: T): T {
return filePath.replace(/\.(js|d\.ts)$/, '') as T;
}