From c61359665809beb8600182666745a0fbc6c2ad6e Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:48:35 +0100 Subject: [PATCH] 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 --- .../ngcc/src/packages/transformer.ts | 41 +- .../ngcc/src/rendering/dts_renderer.ts | 161 +++++ ...enderer.ts => esm5_rendering_formatter.ts} | 20 +- .../ngcc/src/rendering/esm_renderer.ts | 140 ----- .../src/rendering/esm_rendering_formatter.ts | 218 +++++++ .../ngcc/src/rendering/renderer.ts | 414 +------------ .../ngcc/src/rendering/rendering_formatter.ts | 42 ++ .../ngcc/src/rendering/source_maps.ts | 137 +++++ ...renderer.ts => umd_rendering_formatter.ts} | 53 +- .../compiler-cli/ngcc/src/rendering/utils.ts | 39 ++ .../ngcc/src/writing/file_writer.ts | 5 +- .../ngcc/src/writing/in_place_file_writer.ts | 6 +- .../writing/new_entry_point_file_writer.ts | 6 +- .../ngcc/test/rendering/dts_renderer_spec.ts | 181 ++++++ ...ec.ts => esm5_rendering_formatter_spec.ts} | 6 +- ...pec.ts => esm_rendering_formatter_spec.ts} | 559 +++++++++++------- .../ngcc/test/rendering/renderer_spec.ts | 353 ++--------- ...pec.ts => umd_rendering_formatter_spec.ts} | 6 +- 18 files changed, 1307 insertions(+), 1080 deletions(-) create mode 100644 packages/compiler-cli/ngcc/src/rendering/dts_renderer.ts rename packages/compiler-cli/ngcc/src/rendering/{esm5_renderer.ts => esm5_rendering_formatter.ts} (68%) delete mode 100644 packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts create mode 100644 packages/compiler-cli/ngcc/src/rendering/esm_rendering_formatter.ts create mode 100644 packages/compiler-cli/ngcc/src/rendering/rendering_formatter.ts create mode 100644 packages/compiler-cli/ngcc/src/rendering/source_maps.ts rename packages/compiler-cli/ngcc/src/rendering/{umd_renderer.ts => umd_rendering_formatter.ts} (83%) create mode 100644 packages/compiler-cli/ngcc/src/rendering/utils.ts create mode 100644 packages/compiler-cli/ngcc/test/rendering/dts_renderer_spec.ts rename packages/compiler-cli/ngcc/test/rendering/{esm5_renderer_spec.ts => esm5_rendering_formatter_spec.ts} (98%) rename packages/compiler-cli/ngcc/test/rendering/{esm2015_renderer_spec.ts => esm_rendering_formatter_spec.ts} (65%) rename packages/compiler-cli/ngcc/test/rendering/{umd_renderer_spec.ts => umd_rendering_formatter_spec.ts} (99%) diff --git a/packages/compiler-cli/ngcc/src/packages/transformer.ts b/packages/compiler-cli/ngcc/src/packages/transformer.ts index a17f017294..4b87430c40 100644 --- a/packages/compiler-cli/ngcc/src/packages/transformer.ts +++ b/packages/compiler-cli/ngcc/src/packages/transformer.ts @@ -18,10 +18,13 @@ import {Esm5ReflectionHost} from '../host/esm5_host'; import {NgccReflectionHost} from '../host/ngcc_host'; import {UmdReflectionHost} from '../host/umd_host'; import {Logger} from '../logging/logger'; -import {Esm5Renderer} from '../rendering/esm5_renderer'; -import {EsmRenderer} from '../rendering/esm_renderer'; -import {FileInfo, Renderer} from '../rendering/renderer'; -import {UmdRenderer} from '../rendering/umd_renderer'; +import {DtsRenderer} from '../rendering/dts_renderer'; +import {Esm5RenderingFormatter} from '../rendering/esm5_rendering_formatter'; +import {EsmRenderingFormatter} from '../rendering/esm_rendering_formatter'; +import {Renderer} from '../rendering/renderer'; +import {RenderingFormatter} from '../rendering/rendering_formatter'; +import {UmdRenderingFormatter} from '../rendering/umd_rendering_formatter'; +import {FileToWrite} from '../rendering/utils'; import {EntryPointBundle} from './entry_point_bundle'; @@ -56,7 +59,7 @@ export class Transformer { * @param bundle the bundle to transform. * @returns information about the files that were transformed. */ - transform(bundle: EntryPointBundle): FileInfo[] { + transform(bundle: EntryPointBundle): FileToWrite[] { const isCore = bundle.isCore; const reflectionHost = this.getHost(isCore, bundle); @@ -65,10 +68,21 @@ export class Transformer { moduleWithProvidersAnalyses} = this.analyzeProgram(reflectionHost, isCore, bundle); // Transform the source files and source maps. - const renderer = this.getRenderer(reflectionHost, isCore, bundle); - const renderedFiles = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); + const srcFormatter = this.getRenderingFormatter(reflectionHost, isCore, bundle); + + const renderer = + new Renderer(srcFormatter, this.fs, this.logger, reflectionHost, isCore, bundle); + let renderedFiles = renderer.renderProgram( + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + + if (bundle.dts) { + const dtsFormatter = new EsmRenderingFormatter(reflectionHost, isCore); + const dtsRenderer = + new DtsRenderer(dtsFormatter, this.fs, this.logger, reflectionHost, isCore, bundle); + const renderedDtsFiles = dtsRenderer.renderProgram( + decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); + renderedFiles = renderedFiles.concat(renderedDtsFiles); + } return renderedFiles; } @@ -88,17 +102,18 @@ export class Transformer { } } - getRenderer(host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle): Renderer { + getRenderingFormatter(host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle): + RenderingFormatter { switch (bundle.format) { case 'esm2015': - return new EsmRenderer(this.fs, this.logger, host, isCore, bundle); + return new EsmRenderingFormatter(host, isCore); case 'esm5': - return new Esm5Renderer(this.fs, this.logger, host, isCore, bundle); + return new Esm5RenderingFormatter(host, isCore); case 'umd': if (!(host instanceof UmdReflectionHost)) { throw new Error('UmdRenderer requires a UmdReflectionHost'); } - return new UmdRenderer(this.fs, this.logger, host, isCore, bundle); + return new UmdRenderingFormatter(host, isCore); default: throw new Error(`Renderer for "${bundle.format}" not yet implemented.`); } diff --git a/packages/compiler-cli/ngcc/src/rendering/dts_renderer.ts b/packages/compiler-cli/ngcc/src/rendering/dts_renderer.ts new file mode 100644 index 0000000000..6329506136 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/rendering/dts_renderer.ts @@ -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 { + const dtsMap = new Map(); + + // 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; + } +} diff --git a/packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts b/packages/compiler-cli/ngcc/src/rendering/esm5_rendering_formatter.ts similarity index 68% rename from packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts rename to packages/compiler-cli/ngcc/src/rendering/esm5_rendering_formatter.ts index 368c93b44f..8b22f7519f 100644 --- a/packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/esm5_rendering_formatter.ts @@ -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); diff --git a/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts b/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts deleted file mode 100644 index 3b2be815d3..0000000000 --- a/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts +++ /dev/null @@ -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; -} diff --git a/packages/compiler-cli/ngcc/src/rendering/esm_rendering_formatter.ts b/packages/compiler-cli/ngcc/src/rendering/esm_rendering_formatter.ts new file mode 100644 index 0000000000..579fde535b --- /dev/null +++ b/packages/compiler-cli/ngcc/src/rendering/esm_rendering_formatter.ts @@ -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}`; +} diff --git a/packages/compiler-cli/ngcc/src/rendering/renderer.ts b/packages/compiler-cli/ngcc/src/rendering/renderer.ts index f2dbf5b276..136c401fee 100644 --- a/packages/compiler-cli/ngcc/src/rendering/renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/renderer.ts @@ -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; -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 { - const dtsMap = new Map(); - - // 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(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}); -} \ No newline at end of file +} diff --git a/packages/compiler-cli/ngcc/src/rendering/rendering_formatter.ts b/packages/compiler-cli/ngcc/src/rendering/rendering_formatter.ts new file mode 100644 index 0000000000..b2a0ad2ca3 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/rendering/rendering_formatter.ts @@ -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; +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; +} diff --git a/packages/compiler-cli/ngcc/src/rendering/source_maps.ts b/packages/compiler-cli/ngcc/src/rendering/source_maps.ts new file mode 100644 index 0000000000..23be4392c8 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/rendering/source_maps.ts @@ -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; +} diff --git a/packages/compiler-cli/ngcc/src/rendering/umd_renderer.ts b/packages/compiler-cli/ngcc/src/rendering/umd_rendering_formatter.ts similarity index 83% rename from packages/compiler-cli/ngcc/src/rendering/umd_renderer.ts rename to packages/compiler-cli/ngcc/src/rendering/umd_rendering_formatter.ts index e3ccfff46f..f15e23b581 100644 --- a/packages/compiler-cli/ngcc/src/rendering/umd_renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/umd_rendering_formatter.ts @@ -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 diff --git a/packages/compiler-cli/ngcc/src/rendering/utils.ts b/packages/compiler-cli/ngcc/src/rendering/utils.ts new file mode 100644 index 0000000000..8392a09425 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/rendering/utils.ts @@ -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(filePath: T): T { + return filePath.replace(/\.(js|d\.ts)$/, '') as T; +} diff --git a/packages/compiler-cli/ngcc/src/writing/file_writer.ts b/packages/compiler-cli/ngcc/src/writing/file_writer.ts index 0d7ee1e8a6..5b178e5355 100644 --- a/packages/compiler-cli/ngcc/src/writing/file_writer.ts +++ b/packages/compiler-cli/ngcc/src/writing/file_writer.ts @@ -8,12 +8,13 @@ */ import {EntryPoint} from '../packages/entry_point'; import {EntryPointBundle} from '../packages/entry_point_bundle'; -import {FileInfo} from '../rendering/renderer'; +import {FileToWrite} from '../rendering/utils'; /** * Responsible for writing out the transformed files to disk. */ export interface FileWriter { - writeBundle(entryPoint: EntryPoint, bundle: EntryPointBundle, transformedFiles: FileInfo[]): void; + writeBundle(entryPoint: EntryPoint, bundle: EntryPointBundle, transformedFiles: FileToWrite[]): + void; } diff --git a/packages/compiler-cli/ngcc/src/writing/in_place_file_writer.ts b/packages/compiler-cli/ngcc/src/writing/in_place_file_writer.ts index 6bf43e94ca..adc7f396c5 100644 --- a/packages/compiler-cli/ngcc/src/writing/in_place_file_writer.ts +++ b/packages/compiler-cli/ngcc/src/writing/in_place_file_writer.ts @@ -10,7 +10,7 @@ import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {FileSystem} from '../file_system/file_system'; import {EntryPoint} from '../packages/entry_point'; import {EntryPointBundle} from '../packages/entry_point_bundle'; -import {FileInfo} from '../rendering/renderer'; +import {FileToWrite} from '../rendering/utils'; import {FileWriter} from './file_writer'; /** @@ -20,11 +20,11 @@ import {FileWriter} from './file_writer'; export class InPlaceFileWriter implements FileWriter { constructor(protected fs: FileSystem) {} - writeBundle(_entryPoint: EntryPoint, _bundle: EntryPointBundle, transformedFiles: FileInfo[]) { + writeBundle(_entryPoint: EntryPoint, _bundle: EntryPointBundle, transformedFiles: FileToWrite[]) { transformedFiles.forEach(file => this.writeFileAndBackup(file)); } - protected writeFileAndBackup(file: FileInfo): void { + protected writeFileAndBackup(file: FileToWrite): void { this.fs.ensureDir(AbsoluteFsPath.dirname(file.path)); const backPath = AbsoluteFsPath.fromUnchecked(`${file.path}.__ivy_ngcc_bak`); if (this.fs.exists(backPath)) { diff --git a/packages/compiler-cli/ngcc/src/writing/new_entry_point_file_writer.ts b/packages/compiler-cli/ngcc/src/writing/new_entry_point_file_writer.ts index 10414ebdfc..59ff4e67c1 100644 --- a/packages/compiler-cli/ngcc/src/writing/new_entry_point_file_writer.ts +++ b/packages/compiler-cli/ngcc/src/writing/new_entry_point_file_writer.ts @@ -10,7 +10,7 @@ import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path'; import {isDtsPath} from '../../../src/ngtsc/util/src/typescript'; import {EntryPoint, EntryPointJsonProperty} from '../packages/entry_point'; import {EntryPointBundle} from '../packages/entry_point_bundle'; -import {FileInfo} from '../rendering/renderer'; +import {FileToWrite} from '../rendering/utils'; import {InPlaceFileWriter} from './in_place_file_writer'; @@ -25,7 +25,7 @@ const NGCC_DIRECTORY = '__ivy_ngcc__'; * `InPlaceFileWriter`). */ export class NewEntryPointFileWriter extends InPlaceFileWriter { - writeBundle(entryPoint: EntryPoint, bundle: EntryPointBundle, transformedFiles: FileInfo[]) { + writeBundle(entryPoint: EntryPoint, bundle: EntryPointBundle, transformedFiles: FileToWrite[]) { // The new folder is at the root of the overall package const ngccFolder = AbsoluteFsPath.join(entryPoint.package, NGCC_DIRECTORY); this.copyBundle(bundle, entryPoint.package, ngccFolder); @@ -47,7 +47,7 @@ export class NewEntryPointFileWriter extends InPlaceFileWriter { }); } - protected writeFile(file: FileInfo, packagePath: AbsoluteFsPath, ngccFolder: AbsoluteFsPath): + protected writeFile(file: FileToWrite, packagePath: AbsoluteFsPath, ngccFolder: AbsoluteFsPath): void { if (isDtsPath(file.path.replace(/\.map$/, ''))) { // This is either `.d.ts` or `.d.ts.map` file diff --git a/packages/compiler-cli/ngcc/test/rendering/dts_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/dts_renderer_spec.ts new file mode 100644 index 0000000000..3fea099f44 --- /dev/null +++ b/packages/compiler-cli/ngcc/test/rendering/dts_renderer_spec.ts @@ -0,0 +1,181 @@ +/** + * @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 {fromObject} from 'convert-source-map'; +import {Import, ImportManager} from '../../../src/ngtsc/translator'; +import {CompiledClass, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; +import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; +import {ModuleWithProvidersAnalyzer, ModuleWithProvidersInfo} from '../../src/analysis/module_with_providers_analyzer'; +import {PrivateDeclarationsAnalyzer, ExportInfo} from '../../src/analysis/private_declarations_analyzer'; +import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; +import {DtsRenderer} from '../../src/rendering/dts_renderer'; +import {makeTestEntryPointBundle, createFileSystemFromProgramFiles} from '../helpers/utils'; +import {MockLogger} from '../helpers/mock_logger'; +import {RenderingFormatter, RedundantDecoratorMap} from '../../src/rendering/rendering_formatter'; +import {MockFileSystem} from '../helpers/mock_file_system'; +import {AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/path'; + +const _ = AbsoluteFsPath.fromUnchecked; + +class TestRenderingFormatter implements RenderingFormatter { + addImports(output: MagicString, imports: Import[], sf: ts.SourceFile) { + output.prepend('\n// ADD IMPORTS\n'); + } + addExports(output: MagicString, baseEntryPointPath: string, exports: ExportInfo[]) { + output.prepend('\n// ADD EXPORTS\n'); + } + addConstants(output: MagicString, constants: string, file: ts.SourceFile): void { + output.prepend('\n// ADD CONSTANTS\n'); + } + addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string) { + output.prepend('\n// ADD DEFINITIONS\n'); + } + removeDecorators(output: MagicString, decoratorsToRemove: RedundantDecoratorMap) { + output.prepend('\n// REMOVE DECORATORS\n'); + } + rewriteSwitchableDeclarations(output: MagicString, sourceFile: ts.SourceFile): void { + output.prepend('\n// REWRITTEN DECLARATIONS\n'); + } + addModuleWithProvidersParams( + output: MagicString, moduleWithProviders: ModuleWithProvidersInfo[], + importManager: ImportManager): void { + output.prepend('\n// ADD MODUlE WITH PROVIDERS PARAMS\n'); + } +} + +function createTestRenderer( + packageName: string, files: {name: string, contents: string}[], + dtsFiles?: {name: string, contents: string}[], + mappingFiles?: {name: string, contents: string}[]) { + const logger = new MockLogger(); + const fs = new MockFileSystem(createFileSystemFromProgramFiles(files, dtsFiles, mappingFiles)); + const isCore = packageName === '@angular/core'; + const bundle = makeTestEntryPointBundle('es2015', 'esm2015', isCore, files, dtsFiles); + const typeChecker = bundle.src.program.getTypeChecker(); + const host = new Esm2015ReflectionHost(logger, isCore, typeChecker, bundle.dts); + const referencesRegistry = new NgccReferencesRegistry(host); + const decorationAnalyses = new DecorationAnalyzer( + fs, bundle.src.program, bundle.src.options, bundle.src.host, + typeChecker, host, referencesRegistry, bundle.rootDirs, isCore) + .analyzeProgram(); + const moduleWithProvidersAnalyses = + new ModuleWithProvidersAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program); + const privateDeclarationsAnalyses = + new PrivateDeclarationsAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program); + const testFormatter = new TestRenderingFormatter(); + spyOn(testFormatter, 'addExports').and.callThrough(); + spyOn(testFormatter, 'addImports').and.callThrough(); + spyOn(testFormatter, 'addDefinitions').and.callThrough(); + spyOn(testFormatter, 'addConstants').and.callThrough(); + spyOn(testFormatter, 'removeDecorators').and.callThrough(); + spyOn(testFormatter, 'rewriteSwitchableDeclarations').and.callThrough(); + spyOn(testFormatter, 'addModuleWithProvidersParams').and.callThrough(); + + const renderer = new DtsRenderer(testFormatter, fs, logger, host, isCore, bundle); + + return {renderer, + testFormatter, + decorationAnalyses, + moduleWithProvidersAnalyses, + privateDeclarationsAnalyses, + bundle}; +} + + +describe('DtsRenderer', () => { + const INPUT_PROGRAM = { + name: '/src/file.js', + contents: + `import { Directive } from '@angular/core';\nexport class A {\n foo(x) {\n return x;\n }\n}\nA.decorators = [\n { type: Directive, args: [{ selector: '[a]' }] }\n];\n` + }; + const INPUT_DTS_PROGRAM = { + name: '/typings/file.d.ts', + contents: `export declare class A {\nfoo(x: number): number;\n}\n` + }; + + const INPUT_PROGRAM_MAP = fromObject({ + 'version': 3, + 'file': '/src/file.js', + 'sourceRoot': '', + 'sources': ['/src/file.ts'], + 'names': [], + 'mappings': + 'AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,MAAM;IACF,GAAG,CAAC,CAAS;QACT,OAAO,CAAC,CAAC;IACb,CAAC;;AACM,YAAU,GAAG;IAChB,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE;CACnD,CAAC', + 'sourcesContent': [INPUT_PROGRAM.contents] + }); + + const RENDERED_CONTENTS = ` +// ADD IMPORTS + +// ADD EXPORTS + +// ADD CONSTANTS + +// ADD DEFINITIONS + +// REMOVE DECORATORS +` + INPUT_PROGRAM.contents; + + const MERGED_OUTPUT_PROGRAM_MAP = fromObject({ + 'version': 3, + 'sources': ['/src/file.ts'], + 'names': [], + 'mappings': ';;;;;;;;;;AAAA', + 'file': 'file.js', + 'sourcesContent': [INPUT_PROGRAM.contents] + }); + + it('should render extract types into typings files', () => { + const {renderer, decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses} = + createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); + const result = renderer.renderProgram( + decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); + + const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; + expect(typingsFile.contents) + .toContain( + 'foo(x: number): number;\n static ngDirectiveDef: ɵngcc0.ΔDirectiveDefWithMeta'); + }); + + it('should render imports into typings files', () => { + const {renderer, decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses} = + createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); + const result = renderer.renderProgram( + decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); + + const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; + expect(typingsFile.contents).toContain(`\n// ADD IMPORTS\n`); + }); + + it('should render exports into typings files', () => { + const {renderer, decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses} = + createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); + + // Add a mock export to trigger export rendering + privateDeclarationsAnalyses.push( + {identifier: 'ComponentB', from: _('/src/file.js'), dtsFrom: _('/typings/b.d.ts')}); + + const result = renderer.renderProgram( + decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); + + const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; + expect(typingsFile.contents).toContain(`\n// ADD EXPORTS\n`); + }); + + it('should render ModuleWithProviders type params', () => { + const {renderer, decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses} = + createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); + + const result = renderer.renderProgram( + decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses); + + const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; + expect(typingsFile.contents).toContain(`\n// ADD MODUlE WITH PROVIDERS PARAMS\n`); + }); +}); diff --git a/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/esm5_rendering_formatter_spec.ts similarity index 98% rename from packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts rename to packages/compiler-cli/ngcc/test/rendering/esm5_rendering_formatter_spec.ts index fc57b1e653..4f8e69e5ca 100644 --- a/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/esm5_rendering_formatter_spec.ts @@ -15,7 +15,7 @@ import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registr import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {IMPORT_PREFIX} from '../../src/constants'; import {Esm5ReflectionHost} from '../../src/host/esm5_host'; -import {Esm5Renderer} from '../../src/rendering/esm5_renderer'; +import {Esm5RenderingFormatter} from '../../src/rendering/esm5_rendering_formatter'; import {makeTestEntryPointBundle, getDeclaration} from '../helpers/utils'; import {MockFileSystem} from '../helpers/mock_file_system'; import {MockLogger} from '../helpers/mock_logger'; @@ -35,7 +35,7 @@ function setup(file: {name: AbsoluteFsPath, contents: string}) { referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false) .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); - const renderer = new Esm5Renderer(fs, logger, host, false, bundle); + const renderer = new Esm5RenderingFormatter(host, false); const importManager = new ImportManager(new NoopImportRewriter(), IMPORT_PREFIX); return { host, @@ -155,7 +155,7 @@ export { D }; // Some other content` }; -describe('Esm5Renderer', () => { +describe('Esm5RenderingFormatter', () => { describe('addImports', () => { it('should insert the given imports after existing imports of the source file', () => { diff --git a/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/esm_rendering_formatter_spec.ts similarity index 65% rename from packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts rename to packages/compiler-cli/ngcc/test/rendering/esm_rendering_formatter_spec.ts index df75d709b1..fc68fee711 100644 --- a/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/esm_rendering_formatter_spec.ts @@ -15,29 +15,33 @@ import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registr import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {IMPORT_PREFIX} from '../../src/constants'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; -import {EsmRenderer} from '../../src/rendering/esm_renderer'; +import {EsmRenderingFormatter} from '../../src/rendering/esm_rendering_formatter'; import {makeTestEntryPointBundle} from '../helpers/utils'; import {MockFileSystem} from '../helpers/mock_file_system'; import {MockLogger} from '../helpers/mock_logger'; +import {ModuleWithProvidersAnalyzer} from '../../src/analysis/module_with_providers_analyzer'; const _ = AbsoluteFsPath.fromUnchecked; -function setup(file: {name: AbsoluteFsPath, contents: string}) { +function setup( + files: {name: string, contents: string}[], + dtsFiles?: {name: string, contents: string, isRoot?: boolean}[]) { const fs = new MockFileSystem(); const logger = new MockLogger(); - const bundle = makeTestEntryPointBundle('es2015', 'esm2015', false, [file]) !; + const bundle = makeTestEntryPointBundle('es2015', 'esm2015', false, files, dtsFiles) !; const typeChecker = bundle.src.program.getTypeChecker(); - const host = new Esm2015ReflectionHost(logger, false, typeChecker); + const host = new Esm2015ReflectionHost(logger, false, typeChecker, bundle.dts); const referencesRegistry = new NgccReferencesRegistry(host); const decorationAnalyses = new DecorationAnalyzer( fs, bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host, referencesRegistry, [_('/')], false) .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); - const renderer = new EsmRenderer(fs, logger, host, false, bundle); + const renderer = new EsmRenderingFormatter(host, false); const importManager = new ImportManager(new NoopImportRewriter(), IMPORT_PREFIX); return { host, + bundle, program: bundle.src.program, sourceFile: bundle.src.file, renderer, decorationAnalyses, switchMarkerAnalyses, importManager, }; @@ -79,9 +83,207 @@ function compileNgModuleFactory__POST_R3__(injector, options, moduleType) { // Some other content` }; -const PROGRAM_DECORATE_HELPER = { - name: _('/some/file.js'), - contents: ` +describe('EsmRenderingFormatter', () => { + + describe('addImports', () => { + it('should insert the given imports after existing imports of the source file', () => { + const {renderer, sourceFile} = setup([PROGRAM]); + const output = new MagicString(PROGRAM.contents); + renderer.addImports( + output, + [ + {specifier: '@angular/core', qualifier: 'i0'}, + {specifier: '@angular/common', qualifier: 'i1'} + ], + sourceFile); + expect(output.toString()).toContain(`/* A copyright notice */ +import 'some-side-effect'; +import {Directive} from '@angular/core'; +import * as i0 from '@angular/core'; +import * as i1 from '@angular/common';`); + }); + }); + + describe('addExports', () => { + it('should insert the given exports at the end of the source file', () => { + const {importManager, renderer, sourceFile} = setup([PROGRAM]); + const output = new MagicString(PROGRAM.contents); + renderer.addExports( + output, _(PROGRAM.name.replace(/\.js$/, '')), + [ + {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA1'}, + {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), dtsFrom: _('/some/foo/b.d.ts'), identifier: 'ComponentB'}, + {from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'}, + ], + importManager, sourceFile); + expect(output.toString()).toContain(` +// Some other content +export {ComponentA1} from './a'; +export {ComponentA2} from './a'; +export {ComponentB} from './foo/b'; +export {TopLevelComponent};`); + }); + + it('should not insert alias exports in js output', () => { + const {importManager, renderer, sourceFile} = setup([PROGRAM]); + const output = new MagicString(PROGRAM.contents); + renderer.addExports( + output, _(PROGRAM.name.replace(/\.js$/, '')), + [ + {from: _('/some/a.js'), alias: 'eComponentA1', identifier: 'ComponentA1'}, + {from: _('/some/a.js'), alias: 'eComponentA2', identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), alias: 'eComponentB', identifier: 'ComponentB'}, + {from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, + ], + importManager, sourceFile); + const outputString = output.toString(); + expect(outputString).not.toContain(`{eComponentA1 as ComponentA1}`); + expect(outputString).not.toContain(`{eComponentB as ComponentB}`); + expect(outputString).not.toContain(`{eTopLevelComponent as TopLevelComponent}`); + }); + }); + + describe('addConstants', () => { + it('should insert the given constants after imports in the source file', () => { + const {renderer, program} = setup([PROGRAM]); + const file = program.getSourceFile('some/file.js'); + if (file === undefined) { + throw new Error(`Could not find source file`); + } + const output = new MagicString(PROGRAM.contents); + renderer.addConstants(output, 'const x = 3;', file); + expect(output.toString()).toContain(` +import {Directive} from '@angular/core'; + +const x = 3; +export class A {}`); + }); + + it('should insert constants after inserted imports', () => { + const {renderer, program} = setup([PROGRAM]); + const file = program.getSourceFile('some/file.js'); + if (file === undefined) { + throw new Error(`Could not find source file`); + } + const output = new MagicString(PROGRAM.contents); + renderer.addConstants(output, 'const x = 3;', file); + renderer.addImports(output, [{specifier: '@angular/core', qualifier: 'i0'}], file); + expect(output.toString()).toContain(` +import {Directive} from '@angular/core'; +import * as i0 from '@angular/core'; + +const x = 3; +export class A {`); + }); + }); + + describe('rewriteSwitchableDeclarations', () => { + it('should switch marked declaration initializers', () => { + const {renderer, program, switchMarkerAnalyses, sourceFile} = setup([PROGRAM]); + const file = program.getSourceFile('some/file.js'); + if (file === undefined) { + throw new Error(`Could not find source file`); + } + const output = new MagicString(PROGRAM.contents); + renderer.rewriteSwitchableDeclarations( + output, file, switchMarkerAnalyses.get(sourceFile) !.declarations); + expect(output.toString()) + .not.toContain(`let compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;`); + expect(output.toString()) + .toContain(`let badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;`); + expect(output.toString()) + .toContain(`let compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`); + expect(output.toString()) + .toContain(`function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`); + expect(output.toString()) + .toContain(`function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`); + }); + }); + + describe('addDefinitions', () => { + it('should insert the definitions directly after the class declaration', () => { + const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM]); + const output = new MagicString(PROGRAM.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; + renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT'); + expect(output.toString()).toContain(` +export class A {} +SOME DEFINITION TEXT +A.decorators = [ +`); + }); + + }); + + + describe('removeDecorators', () => { + describe('[static property declaration]', () => { + it('should delete the decorator (and following comma) that was matched in the analysis', + () => { + const {decorationAnalyses, sourceFile, renderer} = setup([PROGRAM]); + const output = new MagicString(PROGRAM.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; + const decorator = compiledClass.decorators[0]; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()) + .not.toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); + }); + + + it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', + () => { + const {decorationAnalyses, sourceFile, renderer} = setup([PROGRAM]); + const output = new MagicString(PROGRAM.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; + const decorator = compiledClass.decorators[0]; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()) + .not.toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); + }); + + + it('should delete the decorator (and its container if there are no other decorators left) that was matched in the analysis', + () => { + const {decorationAnalyses, sourceFile, renderer} = setup([PROGRAM]); + const output = new MagicString(PROGRAM.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !; + const decorator = compiledClass.decorators[0]; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()) + .not.toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); + expect(output.toString()).not.toContain(`C.decorators = [`); + }); + }); + }); + + describe('[__decorate declarations]', () => { + + const PROGRAM_DECORATE_HELPER = { + name: '/some/file.js', + contents: ` import * as tslib_1 from "tslib"; var D_1; /* A copyright notice */ @@ -115,207 +317,10 @@ D = D_1 = tslib_1.__decorate([ ], D); export { D }; // Some other content` -}; + }; -describe('Esm2015Renderer', () => { - - describe('addImports', () => { - it('should insert the given imports after existing imports of the source file', () => { - const {renderer, sourceFile} = setup(PROGRAM); - const output = new MagicString(PROGRAM.contents); - renderer.addImports( - output, - [ - {specifier: '@angular/core', qualifier: 'i0'}, - {specifier: '@angular/common', qualifier: 'i1'} - ], - sourceFile); - expect(output.toString()).toContain(`/* A copyright notice */ -import 'some-side-effect'; -import {Directive} from '@angular/core'; -import * as i0 from '@angular/core'; -import * as i1 from '@angular/common';`); - }); - }); - - describe('addExports', () => { - it('should insert the given exports at the end of the source file', () => { - const {importManager, renderer, sourceFile} = setup(PROGRAM); - const output = new MagicString(PROGRAM.contents); - renderer.addExports( - output, _(PROGRAM.name.replace(/\.js$/, '')), - [ - {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA1'}, - {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA2'}, - {from: _('/some/foo/b.js'), dtsFrom: _('/some/foo/b.d.ts'), identifier: 'ComponentB'}, - {from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'}, - ], - importManager, sourceFile); - expect(output.toString()).toContain(` -// Some other content -export {ComponentA1} from './a'; -export {ComponentA2} from './a'; -export {ComponentB} from './foo/b'; -export {TopLevelComponent};`); - }); - - it('should not insert alias exports in js output', () => { - const {importManager, renderer, sourceFile} = setup(PROGRAM); - const output = new MagicString(PROGRAM.contents); - renderer.addExports( - output, _(PROGRAM.name.replace(/\.js$/, '')), - [ - {from: _('/some/a.js'), alias: 'eComponentA1', identifier: 'ComponentA1'}, - {from: _('/some/a.js'), alias: 'eComponentA2', identifier: 'ComponentA2'}, - {from: _('/some/foo/b.js'), alias: 'eComponentB', identifier: 'ComponentB'}, - {from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, - ], - importManager, sourceFile); - const outputString = output.toString(); - expect(outputString).not.toContain(`{eComponentA1 as ComponentA1}`); - expect(outputString).not.toContain(`{eComponentB as ComponentB}`); - expect(outputString).not.toContain(`{eTopLevelComponent as TopLevelComponent}`); - }); - }); - - describe('addConstants', () => { - it('should insert the given constants after imports in the source file', () => { - const {renderer, program} = setup(PROGRAM); - const file = program.getSourceFile('some/file.js'); - if (file === undefined) { - throw new Error(`Could not find source file`); - } - const output = new MagicString(PROGRAM.contents); - renderer.addConstants(output, 'const x = 3;', file); - expect(output.toString()).toContain(` -import {Directive} from '@angular/core'; - -const x = 3; -export class A {}`); - }); - - it('should insert constants after inserted imports', () => { - const {renderer, program} = setup(PROGRAM); - const file = program.getSourceFile('some/file.js'); - if (file === undefined) { - throw new Error(`Could not find source file`); - } - const output = new MagicString(PROGRAM.contents); - renderer.addConstants(output, 'const x = 3;', file); - renderer.addImports(output, [{specifier: '@angular/core', qualifier: 'i0'}], file); - expect(output.toString()).toContain(` -import {Directive} from '@angular/core'; -import * as i0 from '@angular/core'; - -const x = 3; -export class A {`); - }); - }); - - describe('rewriteSwitchableDeclarations', () => { - it('should switch marked declaration initializers', () => { - const {renderer, program, switchMarkerAnalyses, sourceFile} = setup(PROGRAM); - const file = program.getSourceFile('some/file.js'); - if (file === undefined) { - throw new Error(`Could not find source file`); - } - const output = new MagicString(PROGRAM.contents); - renderer.rewriteSwitchableDeclarations( - output, file, switchMarkerAnalyses.get(sourceFile) !.declarations); - expect(output.toString()) - .not.toContain(`let compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;`); - expect(output.toString()) - .toContain(`let badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;`); - expect(output.toString()) - .toContain(`let compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`); - expect(output.toString()) - .toContain(`function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`); - expect(output.toString()) - .toContain(`function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`); - }); - }); - - describe('addDefinitions', () => { - it('should insert the definitions directly after the class declaration', () => { - const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); - const output = new MagicString(PROGRAM.contents); - const compiledClass = - decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; - renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT'); - expect(output.toString()).toContain(` -export class A {} -SOME DEFINITION TEXT -A.decorators = [ -`); - }); - - }); - - - describe('removeDecorators', () => { - describe('[static property declaration]', () => { - it('should delete the decorator (and following comma) that was matched in the analysis', - () => { - const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM); - const output = new MagicString(PROGRAM.contents); - const compiledClass = - decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; - const decorator = compiledClass.decorators[0]; - const decoratorsToRemove = new Map(); - decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); - renderer.removeDecorators(output, decoratorsToRemove); - expect(output.toString()) - .not.toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); - expect(output.toString()).toContain(`{ type: OtherA }`); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); - expect(output.toString()).toContain(`{ type: OtherB }`); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); - }); - - - it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', - () => { - const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM); - const output = new MagicString(PROGRAM.contents); - const compiledClass = - decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; - const decorator = compiledClass.decorators[0]; - const decoratorsToRemove = new Map(); - decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); - renderer.removeDecorators(output, decoratorsToRemove); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); - expect(output.toString()).toContain(`{ type: OtherA }`); - expect(output.toString()) - .not.toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); - expect(output.toString()).toContain(`{ type: OtherB }`); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); - }); - - - it('should delete the decorator (and its container if there are no other decorators left) that was matched in the analysis', - () => { - const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM); - const output = new MagicString(PROGRAM.contents); - const compiledClass = - decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !; - const decorator = compiledClass.decorators[0]; - const decoratorsToRemove = new Map(); - decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); - renderer.removeDecorators(output, decoratorsToRemove); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); - expect(output.toString()).toContain(`{ type: OtherA }`); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); - expect(output.toString()).toContain(`{ type: OtherB }`); - expect(output.toString()) - .not.toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); - expect(output.toString()).not.toContain(`C.decorators = [`); - }); - }); - }); - - describe('[__decorate declarations]', () => { it('should delete the decorator (and following comma) that was matched in the analysis', () => { - const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); + const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM_DECORATE_HELPER]); const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); const compiledClass = decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; @@ -332,7 +337,7 @@ A.decorators = [ it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', () => { - const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); + const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM_DECORATE_HELPER]); const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); const compiledClass = decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; @@ -350,7 +355,7 @@ A.decorators = [ it('should delete the decorator (and its container if there are not other decorators left) that was matched in the analysis', () => { - const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); + const {renderer, decorationAnalyses, sourceFile} = setup([PROGRAM_DECORATE_HELPER]); const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); const compiledClass = decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !; @@ -367,4 +372,140 @@ A.decorators = [ expect(output.toString()).toContain(`let C = class C {\n};\nexport { C };`); }); }); + + describe('addModuleWithProvidersParams', () => { + const MODULE_WITH_PROVIDERS_PROGRAM = [ + { + name: '/src/index.js', + contents: ` + import {ExternalModule} from './module'; + import {LibraryModule} from 'some-library'; + export class SomeClass {} + export class SomeModule { + static withProviders1() { return {ngModule: SomeModule}; } + static withProviders2() { return {ngModule: SomeModule}; } + static withProviders3() { return {ngModule: SomeClass}; } + static withProviders4() { return {ngModule: ExternalModule}; } + static withProviders5() { return {ngModule: ExternalModule}; } + static withProviders6() { return {ngModule: LibraryModule}; } + static withProviders7() { return {ngModule: SomeModule, providers: []}; }; + static withProviders8() { return {ngModule: SomeModule}; } + } + export function withProviders1() { return {ngModule: SomeModule}; } + export function withProviders2() { return {ngModule: SomeModule}; } + export function withProviders3() { return {ngModule: SomeClass}; } + export function withProviders4() { return {ngModule: ExternalModule}; } + export function withProviders5() { return {ngModule: ExternalModule}; } + export function withProviders6() { return {ngModule: LibraryModule}; } + export function withProviders7() { return {ngModule: SomeModule, providers: []}; }; + export function withProviders8() { return {ngModule: SomeModule}; }`, + }, + { + name: '/src/module.js', + contents: ` + export class ExternalModule { + static withProviders1() { return {ngModule: ExternalModule}; } + static withProviders2() { return {ngModule: ExternalModule}; } + }` + }, + { + name: '/node_modules/some-library/index.d.ts', + contents: 'export declare class LibraryModule {}' + }, + ]; + const MODULE_WITH_PROVIDERS_DTS_PROGRAM = [ + { + name: '/typings/index.d.ts', + contents: ` + import {ModuleWithProviders} from '@angular/core'; + export declare class SomeClass {} + export interface MyModuleWithProviders extends ModuleWithProviders {} + export declare class SomeModule { + static withProviders1(): ModuleWithProviders; + static withProviders2(): ModuleWithProviders; + static withProviders3(): ModuleWithProviders; + static withProviders4(): ModuleWithProviders; + static withProviders5(); + static withProviders6(): ModuleWithProviders; + static withProviders7(): {ngModule: SomeModule, providers: any[]}; + static withProviders8(): MyModuleWithProviders; + } + export declare function withProviders1(): ModuleWithProviders; + export declare function withProviders2(): ModuleWithProviders; + export declare function withProviders3(): ModuleWithProviders; + export declare function withProviders4(): ModuleWithProviders; + export declare function withProviders5(); + export declare function withProviders6(): ModuleWithProviders; + export declare function withProviders7(): {ngModule: SomeModule, providers: any[]}; + export declare function withProviders8(): MyModuleWithProviders;` + }, + { + name: '/typings/module.d.ts', + contents: ` + export interface ModuleWithProviders {} + export declare class ExternalModule { + static withProviders1(): ModuleWithProviders; + static withProviders2(): ModuleWithProviders; + }` + }, + { + name: '/node_modules/some-library/index.d.ts', + contents: 'export declare class LibraryModule {}' + }, + ]; + + it('should fixup functions/methods that return ModuleWithProviders structures', () => { + const {bundle, renderer, host} = + setup(MODULE_WITH_PROVIDERS_PROGRAM, MODULE_WITH_PROVIDERS_DTS_PROGRAM); + + const referencesRegistry = new NgccReferencesRegistry(host); + const moduleWithProvidersAnalyses = new ModuleWithProvidersAnalyzer(host, referencesRegistry) + .analyzeProgram(bundle.src.program); + const typingsFile = bundle.dts !.program.getSourceFile('/typings/index.d.ts') !; + const moduleWithProvidersInfo = moduleWithProvidersAnalyses.get(typingsFile) !; + + const output = new MagicString(MODULE_WITH_PROVIDERS_DTS_PROGRAM[0].contents); + const importManager = new ImportManager(new NoopImportRewriter(), 'i'); + renderer.addModuleWithProvidersParams(output, moduleWithProvidersInfo, importManager); + + expect(output.toString()).toContain(` + static withProviders1(): ModuleWithProviders; + static withProviders2(): ModuleWithProviders; + static withProviders3(): ModuleWithProviders; + static withProviders4(): ModuleWithProviders; + static withProviders5(): i1.ModuleWithProviders; + static withProviders6(): ModuleWithProviders; + static withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule}; + static withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`); + expect(output.toString()).toContain(` + export declare function withProviders1(): ModuleWithProviders; + export declare function withProviders2(): ModuleWithProviders; + export declare function withProviders3(): ModuleWithProviders; + export declare function withProviders4(): ModuleWithProviders; + export declare function withProviders5(): i1.ModuleWithProviders; + export declare function withProviders6(): ModuleWithProviders; + export declare function withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule}; + export declare function withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`); + }); + + it('should not mistake `ModuleWithProviders` types that are not imported from `@angular/core', + () => { + const {bundle, renderer, host} = + setup(MODULE_WITH_PROVIDERS_PROGRAM, MODULE_WITH_PROVIDERS_DTS_PROGRAM); + + const referencesRegistry = new NgccReferencesRegistry(host); + const moduleWithProvidersAnalyses = + new ModuleWithProvidersAnalyzer(host, referencesRegistry) + .analyzeProgram(bundle.src.program); + const typingsFile = bundle.dts !.program.getSourceFile('/typings/module.d.ts') !; + const moduleWithProvidersInfo = moduleWithProvidersAnalyses.get(typingsFile) !; + + const output = new MagicString(MODULE_WITH_PROVIDERS_DTS_PROGRAM[1].contents); + const importManager = new ImportManager(new NoopImportRewriter(), 'i'); + renderer.addModuleWithProvidersParams(output, moduleWithProvidersInfo, importManager); + expect(output.toString()).toContain(` + static withProviders1(): (ModuleWithProviders)&{ngModule:ExternalModule}; + static withProviders2(): (ModuleWithProviders)&{ngModule:ExternalModule};`); + }); + }); }); diff --git a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts index 81b5f97575..83ff27c7ac 100644 --- a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts @@ -9,29 +9,22 @@ import MagicString from 'magic-string'; import * as ts from 'typescript'; import {fromObject, generateMapFileComment} from 'convert-source-map'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; -import {Import} from '../../../src/ngtsc/translator'; +import {Import, ImportManager} from '../../../src/ngtsc/translator'; import {CompiledClass, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; -import {ModuleWithProvidersAnalyzer} from '../../src/analysis/module_with_providers_analyzer'; +import {ModuleWithProvidersInfo} from '../../src/analysis/module_with_providers_analyzer'; import {PrivateDeclarationsAnalyzer, ExportInfo} from '../../src/analysis/private_declarations_analyzer'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; -import {RedundantDecoratorMap, Renderer} from '../../src/rendering/renderer'; -import {EntryPointBundle} from '../../src/packages/entry_point_bundle'; -import {makeTestEntryPointBundle, createFileSystemFromProgramFiles} from '../helpers/utils'; -import {Logger} from '../../src/logging/logger'; -import {MockFileSystem} from '../helpers/mock_file_system'; -import {MockLogger} from '../helpers/mock_logger'; -import {FileSystem} from '../../src/file_system/file_system'; - const _ = AbsoluteFsPath.fromUnchecked; -class TestRenderer extends Renderer { - constructor( - fs: FileSystem, logger: Logger, host: Esm2015ReflectionHost, isCore: boolean, - bundle: EntryPointBundle) { - super(fs, logger, host, isCore, bundle); - } +import {Renderer} from '../../src/rendering/renderer'; +import {MockLogger} from '../helpers/mock_logger'; +import {RenderingFormatter, RedundantDecoratorMap} from '../../src/rendering/rendering_formatter'; +import {makeTestEntryPointBundle, createFileSystemFromProgramFiles} from '../helpers/utils'; +import {MockFileSystem} from '../helpers/mock_file_system'; + +class TestRenderingFormatter implements RenderingFormatter { addImports(output: MagicString, imports: Import[], sf: ts.SourceFile) { output.prepend('\n// ADD IMPORTS\n'); } @@ -50,6 +43,11 @@ class TestRenderer extends Renderer { rewriteSwitchableDeclarations(output: MagicString, sourceFile: ts.SourceFile): void { output.prepend('\n// REWRITTEN DECLARATIONS\n'); } + addModuleWithProvidersParams( + output: MagicString, moduleWithProviders: ModuleWithProvidersInfo[], + importManager: ImportManager): void { + output.prepend('\n// ADD MODUlE WITH PROVIDERS PARAMS\n'); + } } function createTestRenderer( @@ -68,21 +66,23 @@ function createTestRenderer( typeChecker, host, referencesRegistry, bundle.rootDirs, isCore) .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); - const moduleWithProvidersAnalyses = - new ModuleWithProvidersAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program); const privateDeclarationsAnalyses = new PrivateDeclarationsAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program); - const renderer = new TestRenderer(fs, logger, host, isCore, bundle); - spyOn(renderer, 'addExports').and.callThrough(); - spyOn(renderer, 'addImports').and.callThrough(); - spyOn(renderer, 'addDefinitions').and.callThrough(); - spyOn(renderer, 'addConstants').and.callThrough(); - spyOn(renderer, 'removeDecorators').and.callThrough(); + const testFormatter = new TestRenderingFormatter(); + spyOn(testFormatter, 'addExports').and.callThrough(); + spyOn(testFormatter, 'addImports').and.callThrough(); + spyOn(testFormatter, 'addDefinitions').and.callThrough(); + spyOn(testFormatter, 'addConstants').and.callThrough(); + spyOn(testFormatter, 'removeDecorators').and.callThrough(); + spyOn(testFormatter, 'rewriteSwitchableDeclarations').and.callThrough(); + spyOn(testFormatter, 'addModuleWithProvidersParams').and.callThrough(); + + const renderer = new Renderer(testFormatter, fs, logger, host, isCore, bundle); return {renderer, + testFormatter, decorationAnalyses, switchMarkerAnalyses, - moduleWithProvidersAnalyses, privateDeclarationsAnalyses, bundle}; } @@ -94,10 +94,6 @@ describe('Renderer', () => { contents: `import { Directive } from '@angular/core';\nexport class A {\n foo(x) {\n return x;\n }\n}\nA.decorators = [\n { type: Directive, args: [{ selector: '[a]' }] }\n];\n` }; - const INPUT_DTS_PROGRAM = { - name: '/typings/file.d.ts', - contents: `export declare class A {\nfoo(x: number): number;\n}\n` - }; const COMPONENT_PROGRAM = { name: '/src/component.js', @@ -149,11 +145,10 @@ describe('Renderer', () => { describe('renderProgram()', () => { it('should render the modified contents; and a new map file, if the original provided no map file.', () => { - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = createTestRenderer('test-package', [INPUT_PROGRAM]); + const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} = + createTestRenderer('test-package', [INPUT_PROGRAM]); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); expect(result[0].path).toEqual('/src/file.js'); expect(result[0].contents) .toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('file.js.map')); @@ -164,11 +159,9 @@ describe('Renderer', () => { it('should render as JavaScript', () => { const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = createTestRenderer('test-package', [COMPONENT_PROGRAM]); - renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; + testFormatter} = createTestRenderer('test-package', [COMPONENT_PROGRAM]); + renderer.renderProgram(decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; expect(addDefinitionsSpy.calls.first().args[2]) .toEqual( `A.ngComponentDef = ɵngcc0.ΔdefineComponent({ type: A, selectors: [["a"]], factory: function A_Factory(t) { return new (t || A)(); }, consts: 1, vars: 1, template: function A_Template(rf, ctx) { if (rf & 1) { @@ -184,16 +177,14 @@ describe('Renderer', () => { }); - describe('calling abstract methods', () => { + describe('calling RenderingFormatter methods', () => { it('should call addImports with the source code and info about the core Angular library.', () => { const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = - createTestRenderer('test-package', [INPUT_PROGRAM]); + testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - const addImportsSpy = renderer.addImports as jasmine.Spy; + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + const addImportsSpy = testFormatter.addImports as jasmine.Spy; expect(addImportsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); expect(addImportsSpy.calls.first().args[1]).toEqual([ {specifier: '@angular/core', qualifier: 'ɵngcc0'} @@ -203,12 +194,10 @@ describe('Renderer', () => { it('should call addDefinitions with the source code, the analyzed class and the rendered definitions.', () => { const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = - createTestRenderer('test-package', [INPUT_PROGRAM]); + testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; expect(addDefinitionsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); expect(addDefinitionsSpy.calls.first().args[1]).toEqual(jasmine.objectContaining({ name: _('A'), @@ -226,12 +215,10 @@ describe('Renderer', () => { it('should call removeDecorators with the source code, a map of class decorators that have been analyzed', () => { const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = - createTestRenderer('test-package', [INPUT_PROGRAM]); + testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - const removeDecoratorsSpy = renderer.removeDecorators as jasmine.Spy; + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + const removeDecoratorsSpy = testFormatter.removeDecorators as jasmine.Spy; expect(removeDecoratorsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); // Each map key is the TS node of the decorator container @@ -251,14 +238,13 @@ describe('Renderer', () => { it('should call renderImports after other abstract methods', () => { // This allows the other methods to add additional imports if necessary const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = createTestRenderer('test-package', [INPUT_PROGRAM]); - const addExportsSpy = renderer.addExports as jasmine.Spy; - const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; - const addConstantsSpy = renderer.addConstants as jasmine.Spy; - const addImportsSpy = renderer.addImports as jasmine.Spy; + testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]); + const addExportsSpy = testFormatter.addExports as jasmine.Spy; + const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; + const addConstantsSpy = testFormatter.addConstants as jasmine.Spy; + const addImportsSpy = testFormatter.addImports as jasmine.Spy; renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); expect(addExportsSpy).toHaveBeenCalledBefore(addImportsSpy); expect(addDefinitionsSpy).toHaveBeenCalledBefore(addImportsSpy); expect(addConstantsSpy).toHaveBeenCalledBefore(addImportsSpy); @@ -268,16 +254,14 @@ describe('Renderer', () => { describe('source map merging', () => { it('should merge any inline source map from the original file and write the output as an inline source map', () => { - const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = + const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses} = createTestRenderer( 'test-package', [{ ...INPUT_PROGRAM, contents: INPUT_PROGRAM.contents + '\n' + INPUT_PROGRAM_MAP.toComment() }]); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); expect(result[0].path).toEqual('/src/file.js'); expect(result[0].contents) .toEqual(RENDERED_CONTENTS + '\n' + MERGED_OUTPUT_PROGRAM_MAP.toComment()); @@ -292,12 +276,10 @@ describe('Renderer', () => { }]; const mappingFiles = [{name: INPUT_PROGRAM.name + '.map', contents: INPUT_PROGRAM_MAP.toJSON()}]; - const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = + const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses} = createTestRenderer('test-package', sourceFiles, undefined, mappingFiles); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); expect(result[0].path).toEqual('/src/file.js'); expect(result[0].contents) .toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('file.js.map')); @@ -320,15 +302,13 @@ describe('Renderer', () => { }; // The package name of `@angular/core` indicates that we are compiling the core library. const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = - createTestRenderer('@angular/core', [CORE_FILE, R3_SYMBOLS_FILE]); + testFormatter} = createTestRenderer('@angular/core', [CORE_FILE, R3_SYMBOLS_FILE]); renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; expect(addDefinitionsSpy.calls.first().args[2]) .toContain(`/*@__PURE__*/ ɵngcc0.setClassMetadata(`); - const addImportsSpy = renderer.addImports as jasmine.Spy; + const addImportsSpy = testFormatter.addImports as jasmine.Spy; expect(addImportsSpy.calls.first().args[1]).toEqual([ {specifier: './r3_symbols', qualifier: 'ɵngcc0'} ]); @@ -342,230 +322,15 @@ describe('Renderer', () => { }; const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = createTestRenderer('@angular/core', [CORE_FILE]); + testFormatter} = createTestRenderer('@angular/core', [CORE_FILE]); renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy; expect(addDefinitionsSpy.calls.first().args[2]) .toContain(`/*@__PURE__*/ setClassMetadata(`); - const addImportsSpy = renderer.addImports as jasmine.Spy; + const addImportsSpy = testFormatter.addImports as jasmine.Spy; expect(addImportsSpy.calls.first().args[1]).toEqual([]); }); }); - - describe('rendering typings', () => { - it('should render extract types into typings files', () => { - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = - createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); - const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - - const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; - expect(typingsFile.contents) - .toContain( - 'foo(x: number): number;\n static ngDirectiveDef: ɵngcc0.ΔDirectiveDefWithMeta'); - }); - - it('should render imports into typings files', () => { - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = - createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); - const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - - const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; - expect(typingsFile.contents).toContain(`\n// ADD IMPORTS\n`); - }); - - it('should render exports into typings files', () => { - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses} = - createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); - - // Add a mock export to trigger export rendering - privateDeclarationsAnalyses.push( - {identifier: 'ComponentB', from: _('/src/file.js'), dtsFrom: _('/typings/b.d.ts')}); - - const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - - const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; - expect(typingsFile.contents).toContain(`\n// ADD EXPORTS\n`); - }); - - it('should fixup functions/methods that return ModuleWithProviders structures', () => { - const MODULE_WITH_PROVIDERS_PROGRAM = [ - { - name: '/src/index.js', - contents: ` - import {ExternalModule} from './module'; - import {LibraryModule} from 'some-library'; - export class SomeClass {} - export class SomeModule { - static withProviders1() { - return {ngModule: SomeModule}; - } - static withProviders2() { - return {ngModule: SomeModule}; - } - static withProviders3() { - return {ngModule: SomeClass}; - } - static withProviders4() { - return {ngModule: ExternalModule}; - } - static withProviders5() { - return {ngModule: ExternalModule}; - } - static withProviders6() { - return {ngModule: LibraryModule}; - } - static withProviders7() { - return {ngModule: SomeModule, providers: []}; - }; - static withProviders8() { - return {ngModule: SomeModule}; - } - } - export function withProviders1() { - return {ngModule: SomeModule}; - } - export function withProviders2() { - return {ngModule: SomeModule}; - } - export function withProviders3() { - return {ngModule: SomeClass}; - } - export function withProviders4() { - return {ngModule: ExternalModule}; - } - export function withProviders5() { - return {ngModule: ExternalModule}; - } - export function withProviders6() { - return {ngModule: LibraryModule}; - } - export function withProviders7() { - return {ngModule: SomeModule, providers: []}; - }; - export function withProviders8() { - return {ngModule: SomeModule}; - }`, - }, - { - name: '/src/module.js', - contents: ` - export class ExternalModule { - static withProviders1() { - return {ngModule: ExternalModule}; - } - static withProviders2() { - return {ngModule: ExternalModule}; - } - }` - }, - { - name: '/node_modules/some-library/index.d.ts', - contents: 'export declare class LibraryModule {}' - }, - ]; - const MODULE_WITH_PROVIDERS_DTS_PROGRAM = [ - { - name: '/typings/index.d.ts', - contents: ` - import {ModuleWithProviders} from '@angular/core'; - export declare class SomeClass {} - export interface MyModuleWithProviders extends ModuleWithProviders {} - export declare class SomeModule { - static withProviders1(): ModuleWithProviders; - static withProviders2(): ModuleWithProviders; - static withProviders3(): ModuleWithProviders; - static withProviders4(): ModuleWithProviders; - static withProviders5(); - static withProviders6(): ModuleWithProviders; - static withProviders7(): {ngModule: SomeModule, providers: any[]}; - static withProviders8(): MyModuleWithProviders; - } - export declare function withProviders1(): ModuleWithProviders; - export declare function withProviders2(): ModuleWithProviders; - export declare function withProviders3(): ModuleWithProviders; - export declare function withProviders4(): ModuleWithProviders; - export declare function withProviders5(); - export declare function withProviders6(): ModuleWithProviders; - export declare function withProviders7(): {ngModule: SomeModule, providers: any[]}; - export declare function withProviders8(): MyModuleWithProviders;` - }, - { - name: '/typings/module.d.ts', - contents: ` - export interface ModuleWithProviders {} - export declare class ExternalModule { - static withProviders1(): ModuleWithProviders; - static withProviders2(): ModuleWithProviders; - }` - }, - { - name: '/node_modules/some-library/index.d.ts', - contents: 'export declare class LibraryModule {}' - }, - ]; - const {renderer, - decorationAnalyses, - switchMarkerAnalyses, - privateDeclarationsAnalyses, - moduleWithProvidersAnalyses, - bundle} = - createTestRenderer( - 'test-package', MODULE_WITH_PROVIDERS_PROGRAM, MODULE_WITH_PROVIDERS_DTS_PROGRAM); - - const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, - moduleWithProvidersAnalyses); - - const typingsFile = result.find(f => f.path === '/typings/index.d.ts') !; - - expect(typingsFile.contents).toContain(` - static withProviders1(): ModuleWithProviders; - static withProviders2(): ModuleWithProviders; - static withProviders3(): ModuleWithProviders; - static withProviders4(): ModuleWithProviders<ɵngcc0.ExternalModule>; - static withProviders5(): ɵngcc1.ModuleWithProviders<ɵngcc0.ExternalModule>; - static withProviders6(): ModuleWithProviders<ɵngcc2.LibraryModule>; - static withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule}; - static withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`); - expect(typingsFile.contents).toContain(` - export declare function withProviders1(): ModuleWithProviders; - export declare function withProviders2(): ModuleWithProviders; - export declare function withProviders3(): ModuleWithProviders; - export declare function withProviders4(): ModuleWithProviders<ɵngcc0.ExternalModule>; - export declare function withProviders5(): ɵngcc1.ModuleWithProviders<ɵngcc0.ExternalModule>; - export declare function withProviders6(): ModuleWithProviders<ɵngcc2.LibraryModule>; - export declare function withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule}; - export declare function withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`); - - expect(renderer.addImports) - .toHaveBeenCalledWith( - jasmine.any(MagicString), - [ - {specifier: './module', qualifier: 'ɵngcc0'}, - {specifier: '@angular/core', qualifier: 'ɵngcc1'}, - {specifier: 'some-library', qualifier: 'ɵngcc2'}, - ], - bundle.dts !.file); - - - // The following expectation checks that we do not mistake `ModuleWithProviders` types - // that are not imported from `@angular/core`. - const typingsFile2 = result.find(f => f.path === '/typings/module.d.ts') !; - expect(typingsFile2.contents).toContain(` - static withProviders1(): (ModuleWithProviders)&{ngModule:ExternalModule}; - static withProviders2(): (ModuleWithProviders)&{ngModule:ExternalModule};`); - }); - }); }); }); diff --git a/packages/compiler-cli/ngcc/test/rendering/umd_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/umd_rendering_formatter_spec.ts similarity index 99% rename from packages/compiler-cli/ngcc/test/rendering/umd_renderer_spec.ts rename to packages/compiler-cli/ngcc/test/rendering/umd_rendering_formatter_spec.ts index e3dd4df9a6..8d6e09d6b1 100644 --- a/packages/compiler-cli/ngcc/test/rendering/umd_renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/umd_rendering_formatter_spec.ts @@ -14,8 +14,8 @@ import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registr import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {UmdReflectionHost} from '../../src/host/umd_host'; import {ImportManager} from '../../../src/ngtsc/translator'; -import {UmdRenderer} from '../../src/rendering/umd_renderer'; import {MockFileSystem} from '../helpers/mock_file_system'; +import {UmdRenderingFormatter} from '../../src/rendering/umd_rendering_formatter'; import {MockLogger} from '../helpers/mock_logger'; import {getDeclaration, makeTestEntryPointBundle, createFileSystemFromProgramFiles} from '../helpers/utils'; @@ -34,7 +34,7 @@ function setup(file: {name: string, contents: string}) { referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false) .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(src.program); - const renderer = new UmdRenderer(fs, logger, host, false, bundle); + const renderer = new UmdRenderingFormatter(host, false); const importManager = new ImportManager(new NoopImportRewriter(), 'i'); return { decorationAnalyses, @@ -169,7 +169,7 @@ typeof define === 'function' && define.amd ? define('file', ['exports','/tslib', })));` }; -describe('UmdRenderer', () => { +describe('UmdRenderingFormatter', () => { describe('addImports', () => { it('should append the given imports into the CommonJS factory call', () => {