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

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

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

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

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

View File

@ -18,10 +18,13 @@ import {Esm5ReflectionHost} from '../host/esm5_host';
import {NgccReflectionHost} from '../host/ngcc_host'; import {NgccReflectionHost} from '../host/ngcc_host';
import {UmdReflectionHost} from '../host/umd_host'; import {UmdReflectionHost} from '../host/umd_host';
import {Logger} from '../logging/logger'; import {Logger} from '../logging/logger';
import {Esm5Renderer} from '../rendering/esm5_renderer'; import {DtsRenderer} from '../rendering/dts_renderer';
import {EsmRenderer} from '../rendering/esm_renderer'; import {Esm5RenderingFormatter} from '../rendering/esm5_rendering_formatter';
import {FileInfo, Renderer} from '../rendering/renderer'; import {EsmRenderingFormatter} from '../rendering/esm_rendering_formatter';
import {UmdRenderer} from '../rendering/umd_renderer'; 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'; import {EntryPointBundle} from './entry_point_bundle';
@ -56,7 +59,7 @@ export class Transformer {
* @param bundle the bundle to transform. * @param bundle the bundle to transform.
* @returns information about the files that were transformed. * @returns information about the files that were transformed.
*/ */
transform(bundle: EntryPointBundle): FileInfo[] { transform(bundle: EntryPointBundle): FileToWrite[] {
const isCore = bundle.isCore; const isCore = bundle.isCore;
const reflectionHost = this.getHost(isCore, bundle); const reflectionHost = this.getHost(isCore, bundle);
@ -65,10 +68,21 @@ export class Transformer {
moduleWithProvidersAnalyses} = this.analyzeProgram(reflectionHost, isCore, bundle); moduleWithProvidersAnalyses} = this.analyzeProgram(reflectionHost, isCore, bundle);
// Transform the source files and source maps. // Transform the source files and source maps.
const renderer = this.getRenderer(reflectionHost, isCore, bundle); const srcFormatter = this.getRenderingFormatter(reflectionHost, isCore, bundle);
const renderedFiles = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, const renderer =
moduleWithProvidersAnalyses); 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; 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) { switch (bundle.format) {
case 'esm2015': case 'esm2015':
return new EsmRenderer(this.fs, this.logger, host, isCore, bundle); return new EsmRenderingFormatter(host, isCore);
case 'esm5': case 'esm5':
return new Esm5Renderer(this.fs, this.logger, host, isCore, bundle); return new Esm5RenderingFormatter(host, isCore);
case 'umd': case 'umd':
if (!(host instanceof UmdReflectionHost)) { if (!(host instanceof UmdReflectionHost)) {
throw new Error('UmdRenderer requires a UmdReflectionHost'); throw new Error('UmdRenderer requires a UmdReflectionHost');
} }
return new UmdRenderer(this.fs, this.logger, host, isCore, bundle); return new UmdRenderingFormatter(host, isCore);
default: default:
throw new Error(`Renderer for "${bundle.format}" not yet implemented.`); throw new Error(`Renderer for "${bundle.format}" not yet implemented.`);
} }

View File

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

View File

@ -8,22 +8,16 @@
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {CompiledClass} from '../analysis/decoration_analyzer'; import {CompiledClass} from '../analysis/decoration_analyzer';
import {FileSystem} from '../file_system/file_system';
import {getIifeBody} from '../host/esm5_host'; import {getIifeBody} from '../host/esm5_host';
import {NgccReflectionHost} from '../host/ngcc_host'; import {EsmRenderingFormatter} from './esm_rendering_formatter';
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);
}
/**
* 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 { addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void {
const iifeBody = getIifeBody(compiledClass.declaration); const iifeBody = getIifeBody(compiledClass.declaration);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,12 +8,13 @@
*/ */
import {EntryPoint} from '../packages/entry_point'; import {EntryPoint} from '../packages/entry_point';
import {EntryPointBundle} from '../packages/entry_point_bundle'; 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. * Responsible for writing out the transformed files to disk.
*/ */
export interface FileWriter { export interface FileWriter {
writeBundle(entryPoint: EntryPoint, bundle: EntryPointBundle, transformedFiles: FileInfo[]): void; writeBundle(entryPoint: EntryPoint, bundle: EntryPointBundle, transformedFiles: FileToWrite[]):
void;
} }

View File

@ -10,7 +10,7 @@ import {AbsoluteFsPath} from '../../../src/ngtsc/path';
import {FileSystem} from '../file_system/file_system'; import {FileSystem} from '../file_system/file_system';
import {EntryPoint} from '../packages/entry_point'; import {EntryPoint} from '../packages/entry_point';
import {EntryPointBundle} from '../packages/entry_point_bundle'; import {EntryPointBundle} from '../packages/entry_point_bundle';
import {FileInfo} from '../rendering/renderer'; import {FileToWrite} from '../rendering/utils';
import {FileWriter} from './file_writer'; import {FileWriter} from './file_writer';
/** /**
@ -20,11 +20,11 @@ import {FileWriter} from './file_writer';
export class InPlaceFileWriter implements FileWriter { export class InPlaceFileWriter implements FileWriter {
constructor(protected fs: FileSystem) {} constructor(protected fs: FileSystem) {}
writeBundle(_entryPoint: EntryPoint, _bundle: EntryPointBundle, transformedFiles: FileInfo[]) { writeBundle(_entryPoint: EntryPoint, _bundle: EntryPointBundle, transformedFiles: FileToWrite[]) {
transformedFiles.forEach(file => this.writeFileAndBackup(file)); transformedFiles.forEach(file => this.writeFileAndBackup(file));
} }
protected writeFileAndBackup(file: FileInfo): void { protected writeFileAndBackup(file: FileToWrite): void {
this.fs.ensureDir(AbsoluteFsPath.dirname(file.path)); this.fs.ensureDir(AbsoluteFsPath.dirname(file.path));
const backPath = AbsoluteFsPath.fromUnchecked(`${file.path}.__ivy_ngcc_bak`); const backPath = AbsoluteFsPath.fromUnchecked(`${file.path}.__ivy_ngcc_bak`);
if (this.fs.exists(backPath)) { if (this.fs.exists(backPath)) {

View File

@ -10,7 +10,7 @@ import {AbsoluteFsPath, PathSegment} from '../../../src/ngtsc/path';
import {isDtsPath} from '../../../src/ngtsc/util/src/typescript'; import {isDtsPath} from '../../../src/ngtsc/util/src/typescript';
import {EntryPoint, EntryPointJsonProperty} from '../packages/entry_point'; import {EntryPoint, EntryPointJsonProperty} from '../packages/entry_point';
import {EntryPointBundle} from '../packages/entry_point_bundle'; import {EntryPointBundle} from '../packages/entry_point_bundle';
import {FileInfo} from '../rendering/renderer'; import {FileToWrite} from '../rendering/utils';
import {InPlaceFileWriter} from './in_place_file_writer'; import {InPlaceFileWriter} from './in_place_file_writer';
@ -25,7 +25,7 @@ const NGCC_DIRECTORY = '__ivy_ngcc__';
* `InPlaceFileWriter`). * `InPlaceFileWriter`).
*/ */
export class NewEntryPointFileWriter extends 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 // The new folder is at the root of the overall package
const ngccFolder = AbsoluteFsPath.join(entryPoint.package, NGCC_DIRECTORY); const ngccFolder = AbsoluteFsPath.join(entryPoint.package, NGCC_DIRECTORY);
this.copyBundle(bundle, entryPoint.package, ngccFolder); 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 { void {
if (isDtsPath(file.path.replace(/\.map$/, ''))) { if (isDtsPath(file.path.replace(/\.map$/, ''))) {
// This is either `.d.ts` or `.d.ts.map` file // This is either `.d.ts` or `.d.ts.map` file

View File

@ -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`);
});
});

View File

@ -15,7 +15,7 @@ import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registr
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
import {IMPORT_PREFIX} from '../../src/constants'; import {IMPORT_PREFIX} from '../../src/constants';
import {Esm5ReflectionHost} from '../../src/host/esm5_host'; 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 {makeTestEntryPointBundle, getDeclaration} from '../helpers/utils';
import {MockFileSystem} from '../helpers/mock_file_system'; import {MockFileSystem} from '../helpers/mock_file_system';
import {MockLogger} from '../helpers/mock_logger'; import {MockLogger} from '../helpers/mock_logger';
@ -35,7 +35,7 @@ function setup(file: {name: AbsoluteFsPath, contents: string}) {
referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false) referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false)
.analyzeProgram(); .analyzeProgram();
const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); 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); const importManager = new ImportManager(new NoopImportRewriter(), IMPORT_PREFIX);
return { return {
host, host,
@ -155,7 +155,7 @@ export { D };
// Some other content` // Some other content`
}; };
describe('Esm5Renderer', () => { describe('Esm5RenderingFormatter', () => {
describe('addImports', () => { describe('addImports', () => {
it('should insert the given imports after existing imports of the source file', () => { it('should insert the given imports after existing imports of the source file', () => {

View File

@ -15,29 +15,33 @@ import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registr
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
import {IMPORT_PREFIX} from '../../src/constants'; import {IMPORT_PREFIX} from '../../src/constants';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; 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 {makeTestEntryPointBundle} from '../helpers/utils';
import {MockFileSystem} from '../helpers/mock_file_system'; import {MockFileSystem} from '../helpers/mock_file_system';
import {MockLogger} from '../helpers/mock_logger'; import {MockLogger} from '../helpers/mock_logger';
import {ModuleWithProvidersAnalyzer} from '../../src/analysis/module_with_providers_analyzer';
const _ = AbsoluteFsPath.fromUnchecked; 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 fs = new MockFileSystem();
const logger = new MockLogger(); 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 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 referencesRegistry = new NgccReferencesRegistry(host);
const decorationAnalyses = new DecorationAnalyzer( const decorationAnalyses = new DecorationAnalyzer(
fs, bundle.src.program, bundle.src.options, bundle.src.host, fs, bundle.src.program, bundle.src.options, bundle.src.host,
typeChecker, host, referencesRegistry, [_('/')], false) typeChecker, host, referencesRegistry, [_('/')], false)
.analyzeProgram(); .analyzeProgram();
const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); 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); const importManager = new ImportManager(new NoopImportRewriter(), IMPORT_PREFIX);
return { return {
host, host,
bundle,
program: bundle.src.program, program: bundle.src.program,
sourceFile: bundle.src.file, renderer, decorationAnalyses, switchMarkerAnalyses, importManager, sourceFile: bundle.src.file, renderer, decorationAnalyses, switchMarkerAnalyses, importManager,
}; };
@ -79,9 +83,207 @@ function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {
// Some other content` // Some other content`
}; };
const PROGRAM_DECORATE_HELPER = { describe('EsmRenderingFormatter', () => {
name: _('/some/file.js'),
contents: ` 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<ts.Node, ts.Node[]>();
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<ts.Node, ts.Node[]>();
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<ts.Node, ts.Node[]>();
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"; import * as tslib_1 from "tslib";
var D_1; var D_1;
/* A copyright notice */ /* A copyright notice */
@ -115,207 +317,10 @@ D = D_1 = tslib_1.__decorate([
], D); ], D);
export { D }; export { D };
// Some other content` // 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<ts.Node, ts.Node[]>();
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<ts.Node, ts.Node[]>();
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<ts.Node, ts.Node[]>();
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', () => { 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 output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; 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', 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 output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; 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', 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 output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !; 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 };`); 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<any>;
static withProviders3(): ModuleWithProviders<SomeClass>;
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<any>;
export declare function withProviders3(): ModuleWithProviders<SomeClass>;
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<SomeModule>;
static withProviders2(): ModuleWithProviders<SomeModule>;
static withProviders3(): ModuleWithProviders<SomeClass>;
static withProviders4(): ModuleWithProviders<i0.ExternalModule>;
static withProviders5(): i1.ModuleWithProviders<i0.ExternalModule>;
static withProviders6(): ModuleWithProviders<i2.LibraryModule>;
static withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule};
static withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`);
expect(output.toString()).toContain(`
export declare function withProviders1(): ModuleWithProviders<SomeModule>;
export declare function withProviders2(): ModuleWithProviders<SomeModule>;
export declare function withProviders3(): ModuleWithProviders<SomeClass>;
export declare function withProviders4(): ModuleWithProviders<i0.ExternalModule>;
export declare function withProviders5(): i1.ModuleWithProviders<i0.ExternalModule>;
export declare function withProviders6(): ModuleWithProviders<i2.LibraryModule>;
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};`);
});
});
}); });

View File

@ -9,29 +9,22 @@ import MagicString from 'magic-string';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {fromObject, generateMapFileComment} from 'convert-source-map'; import {fromObject, generateMapFileComment} from 'convert-source-map';
import {AbsoluteFsPath} from '../../../src/ngtsc/path'; 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 {CompiledClass, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; 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 {PrivateDeclarationsAnalyzer, ExportInfo} from '../../src/analysis/private_declarations_analyzer';
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; 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; const _ = AbsoluteFsPath.fromUnchecked;
class TestRenderer extends Renderer { import {Renderer} from '../../src/rendering/renderer';
constructor( import {MockLogger} from '../helpers/mock_logger';
fs: FileSystem, logger: Logger, host: Esm2015ReflectionHost, isCore: boolean, import {RenderingFormatter, RedundantDecoratorMap} from '../../src/rendering/rendering_formatter';
bundle: EntryPointBundle) { import {makeTestEntryPointBundle, createFileSystemFromProgramFiles} from '../helpers/utils';
super(fs, logger, host, isCore, bundle); import {MockFileSystem} from '../helpers/mock_file_system';
}
class TestRenderingFormatter implements RenderingFormatter {
addImports(output: MagicString, imports: Import[], sf: ts.SourceFile) { addImports(output: MagicString, imports: Import[], sf: ts.SourceFile) {
output.prepend('\n// ADD IMPORTS\n'); output.prepend('\n// ADD IMPORTS\n');
} }
@ -50,6 +43,11 @@ class TestRenderer extends Renderer {
rewriteSwitchableDeclarations(output: MagicString, sourceFile: ts.SourceFile): void { rewriteSwitchableDeclarations(output: MagicString, sourceFile: ts.SourceFile): void {
output.prepend('\n// REWRITTEN DECLARATIONS\n'); 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( function createTestRenderer(
@ -68,21 +66,23 @@ function createTestRenderer(
typeChecker, host, referencesRegistry, bundle.rootDirs, isCore) typeChecker, host, referencesRegistry, bundle.rootDirs, isCore)
.analyzeProgram(); .analyzeProgram();
const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program);
const moduleWithProvidersAnalyses =
new ModuleWithProvidersAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program);
const privateDeclarationsAnalyses = const privateDeclarationsAnalyses =
new PrivateDeclarationsAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program); new PrivateDeclarationsAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program);
const renderer = new TestRenderer(fs, logger, host, isCore, bundle); const testFormatter = new TestRenderingFormatter();
spyOn(renderer, 'addExports').and.callThrough(); spyOn(testFormatter, 'addExports').and.callThrough();
spyOn(renderer, 'addImports').and.callThrough(); spyOn(testFormatter, 'addImports').and.callThrough();
spyOn(renderer, 'addDefinitions').and.callThrough(); spyOn(testFormatter, 'addDefinitions').and.callThrough();
spyOn(renderer, 'addConstants').and.callThrough(); spyOn(testFormatter, 'addConstants').and.callThrough();
spyOn(renderer, 'removeDecorators').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, return {renderer,
testFormatter,
decorationAnalyses, decorationAnalyses,
switchMarkerAnalyses, switchMarkerAnalyses,
moduleWithProvidersAnalyses,
privateDeclarationsAnalyses, privateDeclarationsAnalyses,
bundle}; bundle};
} }
@ -94,10 +94,6 @@ describe('Renderer', () => {
contents: 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` `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 = { const COMPONENT_PROGRAM = {
name: '/src/component.js', name: '/src/component.js',
@ -149,11 +145,10 @@ describe('Renderer', () => {
describe('renderProgram()', () => { describe('renderProgram()', () => {
it('should render the modified contents; and a new map file, if the original provided no map file.', it('should render the modified contents; and a new map file, if the original provided no map file.',
() => { () => {
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} =
moduleWithProvidersAnalyses} = createTestRenderer('test-package', [INPUT_PROGRAM]); createTestRenderer('test-package', [INPUT_PROGRAM]);
const result = renderer.renderProgram( const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
moduleWithProvidersAnalyses);
expect(result[0].path).toEqual('/src/file.js'); expect(result[0].path).toEqual('/src/file.js');
expect(result[0].contents) expect(result[0].contents)
.toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('file.js.map')); .toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('file.js.map'));
@ -164,11 +159,9 @@ describe('Renderer', () => {
it('should render as JavaScript', () => { it('should render as JavaScript', () => {
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} = createTestRenderer('test-package', [COMPONENT_PROGRAM]); testFormatter} = createTestRenderer('test-package', [COMPONENT_PROGRAM]);
renderer.renderProgram( renderer.renderProgram(decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy;
moduleWithProvidersAnalyses);
const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy;
expect(addDefinitionsSpy.calls.first().args[2]) expect(addDefinitionsSpy.calls.first().args[2])
.toEqual( .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) { `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.', it('should call addImports with the source code and info about the core Angular library.',
() => { () => {
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} = testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]);
createTestRenderer('test-package', [INPUT_PROGRAM]);
const result = renderer.renderProgram( const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
moduleWithProvidersAnalyses); const addImportsSpy = testFormatter.addImports as jasmine.Spy;
const addImportsSpy = renderer.addImports as jasmine.Spy;
expect(addImportsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); expect(addImportsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS);
expect(addImportsSpy.calls.first().args[1]).toEqual([ expect(addImportsSpy.calls.first().args[1]).toEqual([
{specifier: '@angular/core', qualifier: 'ɵngcc0'} {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.', it('should call addDefinitions with the source code, the analyzed class and the rendered definitions.',
() => { () => {
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} = testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]);
createTestRenderer('test-package', [INPUT_PROGRAM]);
const result = renderer.renderProgram( const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
moduleWithProvidersAnalyses); const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy;
const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy;
expect(addDefinitionsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); expect(addDefinitionsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS);
expect(addDefinitionsSpy.calls.first().args[1]).toEqual(jasmine.objectContaining({ expect(addDefinitionsSpy.calls.first().args[1]).toEqual(jasmine.objectContaining({
name: _('A'), 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', it('should call removeDecorators with the source code, a map of class decorators that have been analyzed',
() => { () => {
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} = testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]);
createTestRenderer('test-package', [INPUT_PROGRAM]);
const result = renderer.renderProgram( const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
moduleWithProvidersAnalyses); const removeDecoratorsSpy = testFormatter.removeDecorators as jasmine.Spy;
const removeDecoratorsSpy = renderer.removeDecorators as jasmine.Spy;
expect(removeDecoratorsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); expect(removeDecoratorsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS);
// Each map key is the TS node of the decorator container // 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', () => { it('should call renderImports after other abstract methods', () => {
// This allows the other methods to add additional imports if necessary // This allows the other methods to add additional imports if necessary
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} = createTestRenderer('test-package', [INPUT_PROGRAM]); testFormatter} = createTestRenderer('test-package', [INPUT_PROGRAM]);
const addExportsSpy = renderer.addExports as jasmine.Spy; const addExportsSpy = testFormatter.addExports as jasmine.Spy;
const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy;
const addConstantsSpy = renderer.addConstants as jasmine.Spy; const addConstantsSpy = testFormatter.addConstants as jasmine.Spy;
const addImportsSpy = renderer.addImports as jasmine.Spy; const addImportsSpy = testFormatter.addImports as jasmine.Spy;
renderer.renderProgram( renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
moduleWithProvidersAnalyses);
expect(addExportsSpy).toHaveBeenCalledBefore(addImportsSpy); expect(addExportsSpy).toHaveBeenCalledBefore(addImportsSpy);
expect(addDefinitionsSpy).toHaveBeenCalledBefore(addImportsSpy); expect(addDefinitionsSpy).toHaveBeenCalledBefore(addImportsSpy);
expect(addConstantsSpy).toHaveBeenCalledBefore(addImportsSpy); expect(addConstantsSpy).toHaveBeenCalledBefore(addImportsSpy);
@ -268,16 +254,14 @@ describe('Renderer', () => {
describe('source map merging', () => { describe('source map merging', () => {
it('should merge any inline source map from the original file and write the output as an inline source map', 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, const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses} =
moduleWithProvidersAnalyses} =
createTestRenderer( createTestRenderer(
'test-package', [{ 'test-package', [{
...INPUT_PROGRAM, ...INPUT_PROGRAM,
contents: INPUT_PROGRAM.contents + '\n' + INPUT_PROGRAM_MAP.toComment() contents: INPUT_PROGRAM.contents + '\n' + INPUT_PROGRAM_MAP.toComment()
}]); }]);
const result = renderer.renderProgram( const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
moduleWithProvidersAnalyses);
expect(result[0].path).toEqual('/src/file.js'); expect(result[0].path).toEqual('/src/file.js');
expect(result[0].contents) expect(result[0].contents)
.toEqual(RENDERED_CONTENTS + '\n' + MERGED_OUTPUT_PROGRAM_MAP.toComment()); .toEqual(RENDERED_CONTENTS + '\n' + MERGED_OUTPUT_PROGRAM_MAP.toComment());
@ -292,12 +276,10 @@ describe('Renderer', () => {
}]; }];
const mappingFiles = const mappingFiles =
[{name: INPUT_PROGRAM.name + '.map', contents: INPUT_PROGRAM_MAP.toJSON()}]; [{name: INPUT_PROGRAM.name + '.map', contents: INPUT_PROGRAM_MAP.toJSON()}];
const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses, const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses} =
moduleWithProvidersAnalyses} =
createTestRenderer('test-package', sourceFiles, undefined, mappingFiles); createTestRenderer('test-package', sourceFiles, undefined, mappingFiles);
const result = renderer.renderProgram( const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
moduleWithProvidersAnalyses);
expect(result[0].path).toEqual('/src/file.js'); expect(result[0].path).toEqual('/src/file.js');
expect(result[0].contents) expect(result[0].contents)
.toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('file.js.map')); .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. // The package name of `@angular/core` indicates that we are compiling the core library.
const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses, const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} = testFormatter} = createTestRenderer('@angular/core', [CORE_FILE, R3_SYMBOLS_FILE]);
createTestRenderer('@angular/core', [CORE_FILE, R3_SYMBOLS_FILE]);
renderer.renderProgram( renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
moduleWithProvidersAnalyses); const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy;
const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy;
expect(addDefinitionsSpy.calls.first().args[2]) expect(addDefinitionsSpy.calls.first().args[2])
.toContain(`/*@__PURE__*/ ɵngcc0.setClassMetadata(`); .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([ expect(addImportsSpy.calls.first().args[1]).toEqual([
{specifier: './r3_symbols', qualifier: 'ɵngcc0'} {specifier: './r3_symbols', qualifier: 'ɵngcc0'}
]); ]);
@ -342,230 +322,15 @@ describe('Renderer', () => {
}; };
const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses, const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} = createTestRenderer('@angular/core', [CORE_FILE]); testFormatter} = createTestRenderer('@angular/core', [CORE_FILE]);
renderer.renderProgram( renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses);
moduleWithProvidersAnalyses); const addDefinitionsSpy = testFormatter.addDefinitions as jasmine.Spy;
const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy;
expect(addDefinitionsSpy.calls.first().args[2]) expect(addDefinitionsSpy.calls.first().args[2])
.toContain(`/*@__PURE__*/ setClassMetadata(`); .toContain(`/*@__PURE__*/ setClassMetadata(`);
const addImportsSpy = renderer.addImports as jasmine.Spy; const addImportsSpy = testFormatter.addImports as jasmine.Spy;
expect(addImportsSpy.calls.first().args[1]).toEqual([]); 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<any>;
static withProviders3(): ModuleWithProviders<SomeClass>;
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<any>;
export declare function withProviders3(): ModuleWithProviders<SomeClass>;
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<SomeModule>;
static withProviders2(): ModuleWithProviders<SomeModule>;
static withProviders3(): ModuleWithProviders<SomeClass>;
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<SomeModule>;
export declare function withProviders2(): ModuleWithProviders<SomeModule>;
export declare function withProviders3(): ModuleWithProviders<SomeClass>;
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};`);
});
});
}); });
}); });

View File

@ -14,8 +14,8 @@ import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registr
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
import {UmdReflectionHost} from '../../src/host/umd_host'; import {UmdReflectionHost} from '../../src/host/umd_host';
import {ImportManager} from '../../../src/ngtsc/translator'; import {ImportManager} from '../../../src/ngtsc/translator';
import {UmdRenderer} from '../../src/rendering/umd_renderer';
import {MockFileSystem} from '../helpers/mock_file_system'; import {MockFileSystem} from '../helpers/mock_file_system';
import {UmdRenderingFormatter} from '../../src/rendering/umd_rendering_formatter';
import {MockLogger} from '../helpers/mock_logger'; import {MockLogger} from '../helpers/mock_logger';
import {getDeclaration, makeTestEntryPointBundle, createFileSystemFromProgramFiles} from '../helpers/utils'; import {getDeclaration, makeTestEntryPointBundle, createFileSystemFromProgramFiles} from '../helpers/utils';
@ -34,7 +34,7 @@ function setup(file: {name: string, contents: string}) {
referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false) referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false)
.analyzeProgram(); .analyzeProgram();
const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(src.program); 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'); const importManager = new ImportManager(new NoopImportRewriter(), 'i');
return { return {
decorationAnalyses, decorationAnalyses,
@ -169,7 +169,7 @@ typeof define === 'function' && define.amd ? define('file', ['exports','/tslib',
})));` })));`
}; };
describe('UmdRenderer', () => { describe('UmdRenderingFormatter', () => {
describe('addImports', () => { describe('addImports', () => {
it('should append the given imports into the CommonJS factory call', () => { it('should append the given imports into the CommonJS factory call', () => {