feat(ivy): ngcc - add typings to `ModuleWithProviders` functions (#27326)

Exported functions or static method that return a `ModuleWithProviders`
compatible structure need to provide information about the referenced
`NgModule` type in their return type.

This allows ngtsc to be able to understand the type of `NgModule` that is
being returned from calls to the function, without having to dig into the
internals of the compiled library.

There are two ways to provide this information:

* Add a type parameter to the `ModuleWithProviders` return type. E.g.

```
static forRoot(): ModuleWithProviders<SomeNgModule>;
```

* Convert the return type to a union that includes a literal type. E.g.

```
static forRoot(): (SomeOtherType)&{ngModule:SomeNgModule};
```

This commit updates the rendering of typings files to include this type
information on all matching functions/methods.

PR Close #27326
This commit is contained in:
Pete Bacon Darwin 2018-12-07 13:10:52 +00:00 committed by Matias Niemelä
parent cfb8c17511
commit f2a1c66031
10 changed files with 1059 additions and 73 deletions

View File

@ -0,0 +1,118 @@
/**
* @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 {ReferencesRegistry} from '../../../ngtsc/annotations';
import {Declaration} from '../../../ngtsc/host';
import {ResolvedReference} from '../../../ngtsc/metadata';
import {NgccReflectionHost} from '../host/ngcc_host';
import {isDefined} from '../utils';
export interface ModuleWithProvidersInfo {
/**
* The declaration (in the .d.ts file) of the function that returns
* a `ModuleWithProviders object, but has a signature that needs
* a type parameter adding.
*/
declaration: ts.MethodDeclaration|ts.FunctionDeclaration;
/**
* The NgModule class declaration (in the .d.ts file) to add as a type parameter.
*/
ngModule: Declaration;
}
export type ModuleWithProvidersAnalyses = Map<ts.SourceFile, ModuleWithProvidersInfo[]>;
export const ModuleWithProvidersAnalyses = Map;
export class ModuleWithProvidersAnalyzer {
constructor(private host: NgccReflectionHost, private referencesRegistry: ReferencesRegistry) {}
analyzeProgram(program: ts.Program): ModuleWithProvidersAnalyses {
const analyses = new ModuleWithProvidersAnalyses();
const rootFiles = this.getRootFiles(program);
rootFiles.forEach(f => {
const fns = this.host.getModuleWithProvidersFunctions(f);
fns && fns.forEach(fn => {
const dtsFn = this.getDtsDeclaration(fn.declaration);
const typeParam = dtsFn.type && ts.isTypeReferenceNode(dtsFn.type) &&
dtsFn.type.typeArguments && dtsFn.type.typeArguments[0] ||
null;
if (!typeParam || isAnyKeyword(typeParam)) {
// Either we do not have a parameterized type or the type is `any`.
let ngModule = this.host.getDeclarationOfIdentifier(fn.ngModule);
if (!ngModule) {
throw new Error(
`Cannot find a declaration for NgModule ${fn.ngModule.text} referenced in ${fn.declaration.getText()}`);
}
// For internal (non-library) module references, redirect the module's value declaration
// to its type declaration.
if (ngModule.viaModule === null) {
const dtsNgModule = this.host.getDtsDeclaration(ngModule.node);
if (!dtsNgModule) {
throw new Error(
`No typings declaration can be found for the referenced NgModule class in ${fn.declaration.getText()}.`);
}
if (!ts.isClassDeclaration(dtsNgModule)) {
throw new Error(
`The referenced NgModule in ${fn.declaration.getText()} is not a class declaration in the typings program; instead we get ${dtsNgModule.getText()}`);
}
// Record the usage of the internal module as it needs to become an exported symbol
this.referencesRegistry.add(new ResolvedReference(ngModule.node, fn.ngModule));
ngModule = {node: dtsNgModule, viaModule: null};
}
const dtsFile = dtsFn.getSourceFile();
const analysis = analyses.get(dtsFile) || [];
analysis.push({declaration: dtsFn, ngModule});
analyses.set(dtsFile, analysis);
}
});
});
return analyses;
}
private getRootFiles(program: ts.Program): ts.SourceFile[] {
return program.getRootFileNames().map(f => program.getSourceFile(f)).filter(isDefined);
}
private getDtsDeclaration(fn: ts.SignatureDeclaration) {
let dtsFn: ts.Declaration|null = null;
const containerClass = this.host.getClassSymbol(fn.parent);
const fnName = fn.name && ts.isIdentifier(fn.name) && fn.name.text;
if (containerClass && fnName) {
const dtsClass = this.host.getDtsDeclaration(containerClass.valueDeclaration);
// Get the declaration of the matching static method
dtsFn = dtsClass && ts.isClassDeclaration(dtsClass) ?
dtsClass.members
.find(
member => ts.isMethodDeclaration(member) && ts.isIdentifier(member.name) &&
member.name.text === fnName) as ts.Declaration :
null;
} else {
dtsFn = this.host.getDtsDeclaration(fn);
}
if (!dtsFn) {
throw new Error(`Matching type declaration for ${fn.getText()} is missing`);
}
if (!isFunctionOrMethod(dtsFn)) {
throw new Error(
`Matching type declaration for ${fn.getText()} is not a function: ${dtsFn.getText()}`);
}
return dtsFn;
}
}
function isFunctionOrMethod(declaration: ts.Declaration): declaration is ts.FunctionDeclaration|
ts.MethodDeclaration {
return ts.isFunctionDeclaration(declaration) || ts.isMethodDeclaration(declaration);
}
function isAnyKeyword(typeParam: ts.TypeNode): typeParam is ts.KeywordTypeNode {
return typeParam.kind === ts.SyntaxKind.AnyKeyword;
}

View File

@ -15,7 +15,7 @@ import {hasNameIdentifier, isDefined} from '../utils';
export interface ExportInfo { export interface ExportInfo {
identifier: string; identifier: string;
from: string; from: string;
dtsFrom: string|null; dtsFrom?: string|null;
} }
export type PrivateDeclarationsAnalyses = ExportInfo[]; export type PrivateDeclarationsAnalyses = ExportInfo[];

View File

@ -14,7 +14,7 @@ import {BundleProgram} from '../packages/bundle_program';
import {findAll, getNameText, isDefined} from '../utils'; import {findAll, getNameText, isDefined} from '../utils';
import {DecoratedClass} from './decorated_class'; import {DecoratedClass} from './decorated_class';
import {NgccReflectionHost, PRE_R3_MARKER, SwitchableVariableDeclaration, isSwitchableVariableDeclaration} from './ngcc_host'; import {ModuleWithProvidersFunction, NgccReflectionHost, PRE_R3_MARKER, SwitchableVariableDeclaration, isSwitchableVariableDeclaration} from './ngcc_host';
export const DECORATORS = 'decorators' as ts.__String; export const DECORATORS = 'decorators' as ts.__String;
export const PROP_DECORATORS = 'propDecorators' as ts.__String; export const PROP_DECORATORS = 'propDecorators' as ts.__String;
@ -357,6 +357,37 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
return this.dtsDeclarationMap.get(declaration.name.text) || null; return this.dtsDeclarationMap.get(declaration.name.text) || null;
} }
/**
* Search the given source file for exported functions and static class methods that return
* ModuleWithProviders objects.
* @param f The source file to search for these functions
* @returns An array of function declarations that look like they return ModuleWithProviders
* objects.
*/
getModuleWithProvidersFunctions(f: ts.SourceFile): ModuleWithProvidersFunction[] {
const exports = this.getExportsOfModule(f);
if (!exports) return [];
const infos: ModuleWithProvidersFunction[] = [];
exports.forEach((declaration, name) => {
if (this.isClass(declaration.node)) {
this.getMembersOfClass(declaration.node).forEach(member => {
if (member.isStatic) {
const info = this.parseForModuleWithProviders(member.node);
if (info) {
infos.push(info);
}
}
});
} else {
const info = this.parseForModuleWithProviders(declaration.node);
if (info) {
infos.push(info);
}
}
});
return infos;
}
///////////// Protected Helpers ///////////// ///////////// Protected Helpers /////////////
protected getDecoratorsOfSymbol(symbol: ts.Symbol): Decorator[]|null { protected getDecoratorsOfSymbol(symbol: ts.Symbol): Decorator[]|null {
@ -1017,6 +1048,31 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
sourceFile => { collectExportedDeclarations(checker, dtsDeclarationMap, sourceFile); }); sourceFile => { collectExportedDeclarations(checker, dtsDeclarationMap, sourceFile); });
return dtsDeclarationMap; return dtsDeclarationMap;
} }
/**
* Parse the given node, to see if it is a function that returns a `ModuleWithProviders` object.
* @param node a node to check to see if it is a function that returns a `ModuleWithProviders`
* object.
* @returns info about the function if it does return a `ModuleWithProviders` object; `null`
* otherwise.
*/
protected parseForModuleWithProviders(node: ts.Node|null): ModuleWithProvidersFunction|null {
const declaration =
node && (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) ? node : null;
const body = declaration ? this.getDefinitionOfFunction(declaration).body : null;
const lastStatement = body && body[body.length - 1];
const returnExpression =
lastStatement && ts.isReturnStatement(lastStatement) && lastStatement.expression || null;
const ngModuleProperty = returnExpression && ts.isObjectLiteralExpression(returnExpression) &&
returnExpression.properties.find(
prop =>
!!prop.name && ts.isIdentifier(prop.name) && prop.name.text === 'ngModule') ||
null;
const ngModule = ngModuleProperty && ts.isPropertyAssignment(ngModuleProperty) &&
ts.isIdentifier(ngModuleProperty.initializer) && ngModuleProperty.initializer ||
null;
return ngModule && declaration && {ngModule, declaration};
}
} }
///////////// Exported Helpers ///////////// ///////////// Exported Helpers /////////////

View File

@ -19,6 +19,21 @@ export function isSwitchableVariableDeclaration(node: ts.Node):
ts.isIdentifier(node.initializer) && node.initializer.text.endsWith(PRE_R3_MARKER); ts.isIdentifier(node.initializer) && node.initializer.text.endsWith(PRE_R3_MARKER);
} }
/**
* A structure returned from `getModuleWithProviderInfo` that describes functions
* that return ModuleWithProviders objects.
*/
export interface ModuleWithProvidersFunction {
/**
* The declaration of the function that returns the `ModuleWithProviders` object.
*/
declaration: ts.SignatureDeclaration;
/**
* The identifier of the `ngModule` property on the `ModuleWithProviders` object.
*/
ngModule: ts.Identifier;
}
/** /**
* A reflection host that has extra methods for looking at non-Typescript package formats * A reflection host that has extra methods for looking at non-Typescript package formats
*/ */
@ -45,4 +60,13 @@ export interface NgccReflectionHost extends ReflectionHost {
* @returns An array of decorated classes. * @returns An array of decorated classes.
*/ */
findDecoratedClasses(sourceFile: ts.SourceFile): DecoratedClass[]; findDecoratedClasses(sourceFile: ts.SourceFile): DecoratedClass[];
/**
* Search the given source file for exported functions and static class methods that return
* ModuleWithProviders objects.
* @param f The source file to search for these functions
* @returns An array of info items about each of the functions that return ModuleWithProviders
* objects.
*/
getModuleWithProvidersFunctions(f: ts.SourceFile): ModuleWithProvidersFunction[];
} }

View File

@ -11,6 +11,7 @@ import {mkdir, mv} from 'shelljs';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {CompiledFile, DecorationAnalyzer} from '../analysis/decoration_analyzer'; import {CompiledFile, DecorationAnalyzer} from '../analysis/decoration_analyzer';
import {ModuleWithProvidersAnalyses, ModuleWithProvidersAnalyzer} from '../analysis/module_with_providers_analyzer';
import {NgccReferencesRegistry} from '../analysis/ngcc_references_registry'; import {NgccReferencesRegistry} from '../analysis/ngcc_references_registry';
import {ExportInfo, PrivateDeclarationsAnalyzer} from '../analysis/private_declarations_analyzer'; import {ExportInfo, PrivateDeclarationsAnalyzer} from '../analysis/private_declarations_analyzer';
import {SwitchMarkerAnalyses, SwitchMarkerAnalyzer} from '../analysis/switch_marker_analyzer'; import {SwitchMarkerAnalyses, SwitchMarkerAnalyzer} from '../analysis/switch_marker_analyzer';
@ -25,6 +26,7 @@ import {EntryPoint} from './entry_point';
import {EntryPointBundle} from './entry_point_bundle'; import {EntryPointBundle} from './entry_point_bundle';
/** /**
* A Package is stored in a directory on disk and that directory can contain one or more package * A Package is stored in a directory on disk and that directory can contain one or more package
* formats - e.g. fesm2015, UMD, etc. Additionally, each package provides typings (`.d.ts` files). * formats - e.g. fesm2015, UMD, etc. Additionally, each package provides typings (`.d.ts` files).
@ -59,13 +61,14 @@ export class Transformer {
const reflectionHost = this.getHost(isCore, bundle); const reflectionHost = this.getHost(isCore, bundle);
// Parse and analyze the files. // Parse and analyze the files.
const {decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} = const {decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
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 renderer = this.getRenderer(reflectionHost, isCore, bundle);
const renderedFiles = renderer.renderProgram( const renderedFiles = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses);
// Write out all the transformed files. // Write out all the transformed files.
renderedFiles.forEach(file => this.writeFile(file)); renderedFiles.forEach(file => this.writeFile(file));
@ -102,16 +105,26 @@ export class Transformer {
ProgramAnalyses { ProgramAnalyses {
const typeChecker = bundle.src.program.getTypeChecker(); const typeChecker = bundle.src.program.getTypeChecker();
const referencesRegistry = new NgccReferencesRegistry(reflectionHost); const referencesRegistry = new NgccReferencesRegistry(reflectionHost);
const switchMarkerAnalyzer = new SwitchMarkerAnalyzer(reflectionHost);
const switchMarkerAnalyses = switchMarkerAnalyzer.analyzeProgram(bundle.src.program);
const decorationAnalyzer = new DecorationAnalyzer( const decorationAnalyzer = new DecorationAnalyzer(
typeChecker, reflectionHost, referencesRegistry, bundle.rootDirs, isCore); typeChecker, reflectionHost, referencesRegistry, bundle.rootDirs, isCore);
const switchMarkerAnalyzer = new SwitchMarkerAnalyzer(reflectionHost); const decorationAnalyses = decorationAnalyzer.analyzeProgram(bundle.src.program);
const moduleWithProvidersAnalyzer =
bundle.dts && new ModuleWithProvidersAnalyzer(reflectionHost, referencesRegistry);
const moduleWithProvidersAnalyses = moduleWithProvidersAnalyzer &&
moduleWithProvidersAnalyzer.analyzeProgram(bundle.src.program);
const privateDeclarationsAnalyzer = const privateDeclarationsAnalyzer =
new PrivateDeclarationsAnalyzer(reflectionHost, referencesRegistry); new PrivateDeclarationsAnalyzer(reflectionHost, referencesRegistry);
const decorationAnalyses = decorationAnalyzer.analyzeProgram(bundle.src.program);
const switchMarkerAnalyses = switchMarkerAnalyzer.analyzeProgram(bundle.src.program);
const privateDeclarationsAnalyses = const privateDeclarationsAnalyses =
privateDeclarationsAnalyzer.analyzeProgram(bundle.src.program); privateDeclarationsAnalyzer.analyzeProgram(bundle.src.program);
return {decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses};
return {decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses};
} }
writeFile(file: FileInfo): void { writeFile(file: FileInfo): void {
@ -129,4 +142,5 @@ interface ProgramAnalyses {
decorationAnalyses: Map<ts.SourceFile, CompiledFile>; decorationAnalyses: Map<ts.SourceFile, CompiledFile>;
switchMarkerAnalyses: SwitchMarkerAnalyses; switchMarkerAnalyses: SwitchMarkerAnalyses;
privateDeclarationsAnalyses: ExportInfo[]; privateDeclarationsAnalyses: ExportInfo[];
moduleWithProvidersAnalyses: ModuleWithProvidersAnalyses|null;
} }

View File

@ -15,9 +15,10 @@ import * as ts from 'typescript';
import {Decorator} from '../../../ngtsc/host'; import {Decorator} from '../../../ngtsc/host';
import {CompileResult} from '@angular/compiler-cli/src/ngtsc/transform'; import {CompileResult} from '@angular/compiler-cli/src/ngtsc/transform';
import {translateStatement, translateType} from '../../../ngtsc/translator'; import {translateStatement, translateType, ImportManager} from '../../../ngtsc/translator';
import {NgccImportManager} from './ngcc_import_manager'; import {NgccImportManager} from './ngcc_import_manager';
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, ExportInfo} 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';
@ -49,6 +50,20 @@ interface DtsClassInfo {
compilation: CompileResult[]; 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 * 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 * of Ivy static fields. The map is keyed by the container node, such that we
@ -71,7 +86,8 @@ export abstract class Renderer {
renderProgram( renderProgram(
decorationAnalyses: DecorationAnalyses, switchMarkerAnalyses: SwitchMarkerAnalyses, decorationAnalyses: DecorationAnalyses, switchMarkerAnalyses: SwitchMarkerAnalyses,
privateDeclarationsAnalyses: PrivateDeclarationsAnalyses): FileInfo[] { privateDeclarationsAnalyses: PrivateDeclarationsAnalyses,
moduleWithProvidersAnalyses: ModuleWithProvidersAnalyses|null): FileInfo[] {
const renderedFiles: FileInfo[] = []; const renderedFiles: FileInfo[] = [];
// Transform the source files. // Transform the source files.
@ -87,16 +103,16 @@ export abstract class Renderer {
// Transform the .d.ts files // Transform the .d.ts files
if (this.bundle.dts) { if (this.bundle.dts) {
const dtsFiles = this.getTypingsFilesToRender(decorationAnalyses); const dtsFiles = this.getTypingsFilesToRender(
decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses);
// If the dts entry-point is not already there (it did not have compiled classes) // 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. // then add it now, to ensure it gets its extra exports rendered.
if (!dtsFiles.has(this.bundle.dts.file)) { if (!dtsFiles.has(this.bundle.dts.file)) {
dtsFiles.set(this.bundle.dts.file, []); dtsFiles.set(this.bundle.dts.file, new DtsRenderInfo());
} }
dtsFiles.forEach( dtsFiles.forEach(
(classes, file) => renderedFiles.push( (renderInfo, file) => renderedFiles.push(...this.renderDtsFile(file, renderInfo)));
...this.renderDtsFile(file, classes, privateDeclarationsAnalyses)));
} }
return renderedFiles; return renderedFiles;
@ -151,14 +167,12 @@ export abstract class Renderer {
return this.renderSourceAndMap(sourceFile, input, outputText); return this.renderSourceAndMap(sourceFile, input, outputText);
} }
renderDtsFile( renderDtsFile(dtsFile: ts.SourceFile, renderInfo: DtsRenderInfo): FileInfo[] {
dtsFile: ts.SourceFile, dtsClasses: DtsClassInfo[],
privateDeclarationsAnalyses: PrivateDeclarationsAnalyses): FileInfo[] {
const input = this.extractSourceMap(dtsFile); const input = this.extractSourceMap(dtsFile);
const outputText = new MagicString(input.source); const outputText = new MagicString(input.source);
const importManager = new NgccImportManager(false, this.isCore, IMPORT_PREFIX); const importManager = new NgccImportManager(false, this.isCore, IMPORT_PREFIX);
dtsClasses.forEach(dtsClass => { renderInfo.classInfo.forEach(dtsClass => {
const endOfClass = dtsClass.dtsDeclaration.getEnd(); const endOfClass = dtsClass.dtsDeclaration.getEnd();
dtsClass.compilation.forEach(declaration => { dtsClass.compilation.forEach(declaration => {
const type = translateType(declaration.type, importManager); const type = translateType(declaration.type, importManager);
@ -167,26 +181,67 @@ export abstract class Renderer {
}); });
}); });
this.addModuleWithProvidersParams(outputText, renderInfo.moduleWithProviders, importManager);
this.addImports( this.addImports(
outputText, importManager.getAllImports(dtsFile.fileName, this.bundle.dts !.r3SymbolsFile)); outputText, importManager.getAllImports(dtsFile.fileName, this.bundle.dts !.r3SymbolsFile));
if (dtsFile === this.bundle.dts !.file) { this.addExports(outputText, dtsFile.fileName, renderInfo.privateExports);
const dtsExports = privateDeclarationsAnalyses.map(e => {
if (!e.dtsFrom) {
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.`);
}
return {identifier: e.identifier, from: e.dtsFrom};
});
this.addExports(outputText, dtsFile.fileName, dtsExports);
}
return this.renderSourceAndMap(dtsFile, input, outputText); 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: NgccImportManager): void {
moduleWithProviders.forEach(info => {
const ngModuleName = (info.ngModule.node as ts.ClassDeclaration).name !.text;
const declarationFile = info.declaration.getSourceFile().fileName;
const ngModuleFile = info.ngModule.node.getSourceFile().fileName;
const importPath = info.ngModule.viaModule ||
(declarationFile !== ngModuleFile ?
stripExtension(`./${relative(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): protected abstract addConstants(output: MagicString, constants: string, file: ts.SourceFile):
void; void;
protected abstract addImports(output: MagicString, imports: {name: string, as: string}[]): void; protected abstract addImports(output: MagicString, imports: {name: string, as: string}[]): void;
@ -302,22 +357,67 @@ export abstract class Renderer {
return result; return result;
} }
protected getTypingsFilesToRender(analyses: DecorationAnalyses): protected getTypingsFilesToRender(
Map<ts.SourceFile, DtsClassInfo[]> { decorationAnalyses: DecorationAnalyses,
const dtsMap = new Map<ts.SourceFile, DtsClassInfo[]>(); privateDeclarationsAnalyses: PrivateDeclarationsAnalyses,
analyses.forEach(compiledFile => { 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 => { compiledFile.compiledClasses.forEach(compiledClass => {
const dtsDeclaration = this.host.getDtsDeclaration(compiledClass.declaration); const dtsDeclaration = this.host.getDtsDeclaration(compiledClass.declaration);
if (dtsDeclaration) { if (dtsDeclaration) {
const dtsFile = dtsDeclaration.getSourceFile(); const dtsFile = dtsDeclaration.getSourceFile();
const classes = dtsMap.get(dtsFile) || []; const renderInfo = dtsMap.get(dtsFile) || new DtsRenderInfo();
classes.push({dtsDeclaration, compilation: compiledClass.compilation}); renderInfo.classInfo.push({dtsDeclaration, compilation: compiledClass.compilation});
dtsMap.set(dtsFile, classes); 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) {
const dtsExports = privateDeclarationsAnalyses.map(e => {
if (!e.dtsFrom) {
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.`);
}
return {identifier: e.identifier, from: e.dtsFrom};
});
const dtsEntryPoint = this.bundle.dts !.file;
const renderInfo = dtsMap.get(dtsEntryPoint) || new DtsRenderInfo();
renderInfo.privateExports = dtsExports;
dtsMap.set(dtsEntryPoint, renderInfo);
}
return dtsMap; 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'));
}
} }
/** /**
@ -386,7 +486,7 @@ export function renderDefinitions(
} }
export function stripExtension(filePath: string): string { export function stripExtension(filePath: string): string {
return filePath.replace(/\.(js|d\.ts$)/, ''); return filePath.replace(/\.(js|d\.ts)$/, '');
} }
/** /**
@ -399,3 +499,9 @@ function createAssignmentStatement(
const receiver = new WrappedNodeExpr(receiverName); const receiver = new WrappedNodeExpr(receiverName);
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}`;
}

View File

@ -0,0 +1,401 @@
/**
* @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 {ModuleWithProvidersAnalyses, ModuleWithProvidersAnalyzer} from '../../src/analysis/module_with_providers_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {BundleProgram} from '../../src/packages/bundle_program';
import {getDeclaration, makeTestBundleProgram, makeTestProgram} from '../helpers/utils';
const TEST_PROGRAM = [
{
name: '/src/entry-point.js',
contents: `
export * from './explicit';
export * from './any';
export * from './implicit';
export * from './no-providers';
export * from './module';
`
},
{
name: '/src/explicit.js',
contents: `
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export class ExplicitInternalModule {}
export function explicitInternalFunction() {
return {
ngModule: ExplicitInternalModule,
providers: []
};
}
export function explicitExternalFunction() {
return {
ngModule: ExternalModule,
providers: []
};
}
export function explicitLibraryFunction() {
return {
ngModule: LibraryModule,
providers: []
};
}
export class ExplicitClass {
static explicitInternalMethod() {
return {
ngModule: ExplicitInternalModule,
providers: []
};
}
static explicitExternalMethod() {
return {
ngModule: ExternalModule,
providers: []
};
}
static explicitLibraryMethod() {
return {
ngModule: LibraryModule,
providers: []
};
}
}
`
},
{
name: '/src/any.js',
contents: `
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export class AnyInternalModule {}
export function anyInternalFunction() {
return {
ngModule: AnyInternalModule,
providers: []
};
}
export function anyExternalFunction() {
return {
ngModule: ExternalModule,
providers: []
};
}
export function anyLibraryFunction() {
return {
ngModule: LibraryModule,
providers: []
};
}
export class AnyClass {
static anyInternalMethod() {
return {
ngModule: AnyInternalModule,
providers: []
};
}
static anyExternalMethod() {
return {
ngModule: ExternalModule,
providers: []
};
}
static anyLibraryMethod() {
return {
ngModule: LibraryModule,
providers: []
};
}
}
`
},
{
name: '/src/implicit.js',
contents: `
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export class ImplicitInternalModule {}
export function implicitInternalFunction() {
return {
ngModule: ImplicitInternalModule,
providers: [],
};
}
export function implicitExternalFunction() {
return {
ngModule: ExternalModule,
providers: [],
};
}
export function implicitLibraryFunction() {
return {
ngModule: LibraryModule,
providers: [],
};
}
export class ImplicitClass {
static implicitInternalMethod() {
return {
ngModule: ImplicitInternalModule,
providers: [],
};
}
static implicitExternalMethod() {
return {
ngModule: ExternalModule,
providers: [],
};
}
static implicitLibraryMethod() {
return {
ngModule: LibraryModule,
providers: [],
};
}
}
`
},
{
name: '/src/no-providers.js',
contents: `
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export class NoProvidersInternalModule {}
export function noProvExplicitInternalFunction() {
return {ngModule: NoProvidersInternalModule};
}
export function noProvExplicitExternalFunction() {
return {ngModule: ExternalModule};
}
export function noProvExplicitLibraryFunction() {
return {ngModule: LibraryModule};
}
export function noProvAnyInternalFunction() {
return {ngModule: NoProvidersInternalModule};
}
export function noProvAnyExternalFunction() {
return {ngModule: ExternalModule};
}
export function noProvAnyLibraryFunction() {
return {ngModule: LibraryModule};
}
export function noProvImplicitInternalFunction() {
return {ngModule: NoProvidersInternalModule};
}
export function noProvImplicitExternalFunction() {
return {ngModule: ExternalModule};
}
export function noProvImplicitLibraryFunction() {
return {ngModule: LibraryModule};
}
`
},
{
name: '/src/module.js',
contents: `
export class ExternalModule {}
`
},
{
name: '/node_modules/some-library/index.d.ts',
contents: 'export declare class LibraryModule {}'
},
];
const TEST_DTS_PROGRAM = [
{
name: '/typings/entry-point.d.ts',
contents: `
export * from './explicit';
export * from './any';
export * from './implicit';
export * from './no-providers';
export * from './module';
`
},
{
name: '/typings/explicit.d.ts',
contents: `
import {ModuleWithProviders} from './core';
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export declare class ExplicitInternalModule {}
export declare function explicitInternalFunction(): ModuleWithProviders<ExplicitInternalModule>;
export declare function explicitExternalFunction(): ModuleWithProviders<ExternalModule>;
export declare function explicitLibraryFunction(): ModuleWithProviders<LibraryModule>;
export declare class ExplicitClass {
static explicitInternalMethod(): ModuleWithProviders<ExplicitInternalModule>;
static explicitExternalMethod(): ModuleWithProviders<ExternalModule>;
static explicitLibraryMethod(): ModuleWithProviders<LibraryModule>;
}
`
},
{
name: '/typings/any.d.ts',
contents: `
import {ModuleWithProviders} from './core';
export declare class AnyInternalModule {}
export declare function anyInternalFunction(): ModuleWithProviders<any>;
export declare function anyExternalFunction(): ModuleWithProviders<any>;
export declare function anyLibraryFunction(): ModuleWithProviders<any>;
export declare class AnyClass {
static anyInternalMethod(): ModuleWithProviders<any>;
static anyExternalMethod(): ModuleWithProviders<any>;
static anyLibraryMethod(): ModuleWithProviders<any>;
}
`
},
{
name: '/typings/implicit.d.ts',
contents: `
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export declare class ImplicitInternalModule {}
export declare function implicitInternalFunction(): { ngModule: typeof ImplicitInternalModule; providers: never[]; };
export declare function implicitExternalFunction(): { ngModule: typeof ExternalModule; providers: never[]; };
export declare function implicitLibraryFunction(): { ngModule: typeof LibraryModule; providers: never[]; };
export declare class ImplicitClass {
static implicitInternalMethod(): { ngModule: typeof ImplicitInternalModule; providers: never[]; };
static implicitExternalMethod(): { ngModule: typeof ExternalModule; providers: never[]; };
static implicitLibraryMethod(): { ngModule: typeof LibraryModule; providers: never[]; };
}
`
},
{
name: '/typings/no-providers.d.ts',
contents: `
import {ModuleWithProviders} from './core';
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export declare class NoProvidersInternalModule {}
export declare function noProvExplicitInternalFunction(): ModuleWithProviders<NoProvidersInternalModule>;
export declare function noProvExplicitExternalFunction(): ModuleWithProviders<ExternalModule>;
export declare function noProvExplicitLibraryFunction(): ModuleWithProviders<LibraryModule>;
export declare function noProvAnyInternalFunction(): ModuleWithProviders<any>;
export declare function noProvAnyExternalFunction(): ModuleWithProviders<any>;
export declare function noProvAnyLibraryFunction(): ModuleWithProviders<any>;
export declare function noProvImplicitInternalFunction(): { ngModule: typeof NoProvidersInternalModule; };
export declare function noProvImplicitExternalFunction(): { ngModule: typeof ExternalModule; };
export declare function noProvImplicitLibraryFunction(): { ngModule: typeof LibraryModule; };
`
},
{
name: '/typings/module.d.ts',
contents: `
export declare class ExternalModule {}
`
},
{
name: '/typings/core.d.ts',
contents: `
export declare interface Type<T> {
new (...args: any[]): T
}
export declare type Provider = any;
export declare interface ModuleWithProviders<T> {
ngModule: Type<T>
providers?: Provider[]
}
`
},
{
name: '/node_modules/some-library/index.d.ts',
contents: 'export declare class LibraryModule {}'
},
];
describe('ModuleWithProvidersAnalyzer', () => {
describe('analyzeProgram()', () => {
let analyses: ModuleWithProvidersAnalyses;
let program: ts.Program;
let dtsProgram: BundleProgram;
let referencesRegistry: NgccReferencesRegistry;
beforeAll(() => {
program = makeTestProgram(...TEST_PROGRAM);
dtsProgram = makeTestBundleProgram(TEST_DTS_PROGRAM);
const host = new Esm2015ReflectionHost(false, program.getTypeChecker(), dtsProgram);
referencesRegistry = new NgccReferencesRegistry(host);
const analyzer = new ModuleWithProvidersAnalyzer(host, referencesRegistry);
analyses = analyzer.analyzeProgram(program);
});
it('should ignore declarations that already have explicit NgModule type params',
() => { expect(getAnalysisDescription(analyses, '/typings/explicit.d.ts')).toEqual([]); });
it('should find declarations that use `any` for the NgModule type param', () => {
const anyAnalysis = getAnalysisDescription(analyses, '/typings/any.d.ts');
expect(anyAnalysis).toContain(['anyInternalFunction', 'AnyInternalModule', null]);
expect(anyAnalysis).toContain(['anyExternalFunction', 'ExternalModule', null]);
expect(anyAnalysis).toContain(['anyLibraryFunction', 'LibraryModule', 'some-library']);
expect(anyAnalysis).toContain(['anyInternalMethod', 'AnyInternalModule', null]);
expect(anyAnalysis).toContain(['anyExternalMethod', 'ExternalModule', null]);
expect(anyAnalysis).toContain(['anyLibraryMethod', 'LibraryModule', 'some-library']);
});
it('should track internal module references in the references registry', () => {
const declarations = referencesRegistry.getDeclarationMap();
const externalModuleDeclaration =
getDeclaration(program, '/src/module.js', 'ExternalModule', ts.isClassDeclaration);
const libraryModuleDeclaration = getDeclaration(
program, '/node_modules/some-library/index.d.ts', 'LibraryModule', ts.isClassDeclaration);
expect(declarations.has(externalModuleDeclaration.name !)).toBe(true);
expect(declarations.has(libraryModuleDeclaration.name !)).toBe(false);
});
it('should find declarations that have implicit return types', () => {
const anyAnalysis = getAnalysisDescription(analyses, '/typings/implicit.d.ts');
expect(anyAnalysis).toContain(['implicitInternalFunction', 'ImplicitInternalModule', null]);
expect(anyAnalysis).toContain(['implicitExternalFunction', 'ExternalModule', null]);
expect(anyAnalysis).toContain(['implicitLibraryFunction', 'LibraryModule', 'some-library']);
expect(anyAnalysis).toContain(['implicitInternalMethod', 'ImplicitInternalModule', null]);
expect(anyAnalysis).toContain(['implicitExternalMethod', 'ExternalModule', null]);
expect(anyAnalysis).toContain(['implicitLibraryMethod', 'LibraryModule', 'some-library']);
});
it('should find declarations that do not specify a `providers` property in the return type',
() => {
const anyAnalysis = getAnalysisDescription(analyses, '/typings/no-providers.d.ts');
expect(anyAnalysis).not.toContain([
'noProvExplicitInternalFunction', 'NoProvidersInternalModule'
]);
expect(anyAnalysis).not.toContain([
'noProvExplicitExternalFunction', 'ExternalModule', null
]);
expect(anyAnalysis).toContain([
'noProvAnyInternalFunction', 'NoProvidersInternalModule', null
]);
expect(anyAnalysis).toContain(['noProvAnyExternalFunction', 'ExternalModule', null]);
expect(anyAnalysis).toContain([
'noProvAnyLibraryFunction', 'LibraryModule', 'some-library'
]);
expect(anyAnalysis).toContain([
'noProvImplicitInternalFunction', 'NoProvidersInternalModule', null
]);
expect(anyAnalysis).toContain(['noProvImplicitExternalFunction', 'ExternalModule', null]);
expect(anyAnalysis).toContain([
'noProvImplicitLibraryFunction', 'LibraryModule', 'some-library'
]);
});
function getAnalysisDescription(analyses: ModuleWithProvidersAnalyses, fileName: string) {
const file = dtsProgram.program.getSourceFile(fileName) !;
const analysis = analyses.get(file);
return analysis ?
analysis.map(
info =>
[info.declaration.name !.getText(),
(info.ngModule.node as ts.ClassDeclaration).name !.getText(),
info.ngModule.viaModule]) :
[];
}
});
});

View File

@ -84,6 +84,8 @@ export function getFakeCore() {
export class InjectionToken { export class InjectionToken {
constructor(name: string) {} constructor(name: string) {}
} }
export interface ModuleWithProviders<T = any> {}
` `
}; };
} }

View File

@ -485,6 +485,54 @@ const TYPINGS_DTS_FILES = [
{name: '/typings/class3.d.ts', contents: `export declare class Class3 {}`}, {name: '/typings/class3.d.ts', contents: `export declare class Class3 {}`},
]; ];
const MODULE_WITH_PROVIDERS_PROGRAM = [
{
name: '/src/functions.js',
contents: `
import {ExternalModule} from './module';
export class SomeService {}
export class InternalModule {}
export function aNumber() { return 42; }
export function aString() { return 'foo'; }
export function emptyObject() { return {}; }
export function ngModuleIdentifier() { return { ngModule: InternalModule }; }
export function ngModuleWithEmptyProviders() { return { ngModule: InternalModule, providers: [] }; }
export function ngModuleWithProviders() { return { ngModule: InternalModule, providers: [SomeService] }; }
export function onlyProviders() { return { providers: [SomeService] }; }
export function ngModuleNumber() { return { ngModule: 42 }; }
export function ngModuleString() { return { ngModule: 'foo' }; }
export function ngModuleObject() { return { ngModule: { foo: 42 } }; }
export function externalNgModule() { return { ngModule: ExternalModule }; }
`
},
{
name: '/src/methods.js',
contents: `
import {ExternalModule} from './module';
export class SomeService {}
export class InternalModule {
static aNumber() { return 42; }
static aString() { return 'foo'; }
static emptyObject() { return {}; }
static ngModuleIdentifier() { return { ngModule: InternalModule }; }
static ngModuleWithEmptyProviders() { return { ngModule: InternalModule, providers: [] }; }
static ngModuleWithProviders() { return { ngModule: InternalModule, providers: [SomeService] }; }
static onlyProviders() { return { providers: [SomeService] }; }
static ngModuleNumber() { return { ngModule: 42 }; }
static ngModuleString() { return { ngModule: 'foo' }; }
static ngModuleObject() { return { ngModule: { foo: 42 } }; }
static externalNgModule() { return { ngModule: ExternalModule }; }
instanceNgModuleIdentifier() { return { ngModule: InternalModule }; }
instanceNgModuleWithEmptyProviders() { return { ngModule: InternalModule, providers: [] }; }
instanceNgModuleWithProviders() { return { ngModule: InternalModule, providers: [SomeService] }; }
instanceExternalNgModule() { return { ngModule: ExternalModule }; }
}
`
},
{name: '/src/module', contents: 'export class ExternalModule {}'},
];
describe('Fesm2015ReflectionHost', () => { describe('Fesm2015ReflectionHost', () => {
describe('getDecoratorsOfDeclaration()', () => { describe('getDecoratorsOfDeclaration()', () => {
@ -1375,4 +1423,34 @@ describe('Fesm2015ReflectionHost', () => {
.toEqual('/typings/class2.d.ts'); .toEqual('/typings/class2.d.ts');
}); });
}); });
describe('getModuleWithProvidersFunctions', () => {
it('should find every exported function that returns an object that looks like a ModuleWithProviders object',
() => {
const srcProgram = makeTestProgram(...MODULE_WITH_PROVIDERS_PROGRAM);
const host = new Esm2015ReflectionHost(false, srcProgram.getTypeChecker());
const file = srcProgram.getSourceFile('/src/functions.js') !;
const fns = host.getModuleWithProvidersFunctions(file);
expect(fns.map(info => [info.declaration.name !.getText(), info.ngModule.text])).toEqual([
['ngModuleIdentifier', 'InternalModule'],
['ngModuleWithEmptyProviders', 'InternalModule'],
['ngModuleWithProviders', 'InternalModule'],
['externalNgModule', 'ExternalModule'],
]);
});
it('should find every static method on exported classes that return an object that looks like a ModuleWithProviders object',
() => {
const srcProgram = makeTestProgram(...MODULE_WITH_PROVIDERS_PROGRAM);
const host = new Esm2015ReflectionHost(false, srcProgram.getTypeChecker());
const file = srcProgram.getSourceFile('/src/methods.js') !;
const fn = host.getModuleWithProvidersFunctions(file);
expect(fn.map(fn => [fn.declaration.name !.getText(), fn.ngModule.text])).toEqual([
['ngModuleIdentifier', 'InternalModule'],
['ngModuleWithEmptyProviders', 'InternalModule'],
['ngModuleWithProviders', 'InternalModule'],
['externalNgModule', 'ExternalModule'],
]);
});
});
}); });

View File

@ -11,6 +11,7 @@ import * as ts from 'typescript';
import {fromObject, generateMapFileComment} from 'convert-source-map'; import {fromObject, generateMapFileComment} from 'convert-source-map';
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 {PrivateDeclarationsAnalyzer} from '../../src/analysis/private_declarations_analyzer'; import {PrivateDeclarationsAnalyzer} 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';
@ -47,9 +48,9 @@ class TestRenderer extends Renderer {
function createTestRenderer( function createTestRenderer(
packageName: string, files: {name: string, contents: string}[], packageName: string, files: {name: string, contents: string}[],
dtsFile?: {name: string, contents: string}) { dtsFiles?: {name: string, contents: string}[]) {
const isCore = packageName === '@angular/core'; const isCore = packageName === '@angular/core';
const bundle = makeTestEntryPointBundle('esm2015', files, dtsFile && [dtsFile]); const bundle = makeTestEntryPointBundle('esm2015', files, dtsFiles);
const typeChecker = bundle.src.program.getTypeChecker(); const typeChecker = bundle.src.program.getTypeChecker();
const host = new Esm2015ReflectionHost(isCore, typeChecker, bundle.dts); const host = new Esm2015ReflectionHost(isCore, typeChecker, bundle.dts);
const referencesRegistry = new NgccReferencesRegistry(host); const referencesRegistry = new NgccReferencesRegistry(host);
@ -57,6 +58,8 @@ function createTestRenderer(
new DecorationAnalyzer(typeChecker, host, referencesRegistry, bundle.rootDirs, isCore) new DecorationAnalyzer(typeChecker, host, referencesRegistry, bundle.rootDirs, isCore)
.analyzeProgram(bundle.src.program); .analyzeProgram(bundle.src.program);
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(host, isCore, bundle); const renderer = new TestRenderer(host, isCore, bundle);
@ -64,7 +67,8 @@ function createTestRenderer(
spyOn(renderer, 'addDefinitions').and.callThrough(); spyOn(renderer, 'addDefinitions').and.callThrough();
spyOn(renderer, 'removeDecorators').and.callThrough(); spyOn(renderer, 'removeDecorators').and.callThrough();
return {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses}; return {renderer, decorationAnalyses, switchMarkerAnalyses, moduleWithProvidersAnalyses,
privateDeclarationsAnalyses};
} }
@ -121,10 +125,11 @@ 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,
createTestRenderer('test-package', [INPUT_PROGRAM]); moduleWithProvidersAnalyses} = 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('/dist/file.js'); expect(result[0].path).toEqual('/dist/file.js');
expect(result[0].contents) expect(result[0].contents)
.toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('/dist/file.js.map')); .toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('/dist/file.js.map'));
@ -134,9 +139,11 @@ describe('Renderer', () => {
it('should render as JavaScript', () => { it('should render as JavaScript', () => {
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} = const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
createTestRenderer('test-package', [COMPONENT_PROGRAM]); moduleWithProvidersAnalyses} = createTestRenderer('test-package', [COMPONENT_PROGRAM]);
renderer.renderProgram(decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses);
const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy;
expect(addDefinitionsSpy.calls.first().args[2]) expect(addDefinitionsSpy.calls.first().args[2])
.toEqual(`/*@__PURE__*/ ɵngcc0.ɵsetClassMetadata(A, [{ .toEqual(`/*@__PURE__*/ ɵngcc0.ɵsetClassMetadata(A, [{
@ -154,10 +161,12 @@ A.ngComponentDef = ɵngcc0.ɵdefineComponent({ type: A, selectors: [["a"]], fact
describe('calling abstract methods', () => { describe('calling abstract 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} =
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 = renderer.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([
@ -167,10 +176,12 @@ A.ngComponentDef = ɵngcc0.ɵdefineComponent({ type: A, selectors: [["a"]], fact
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} =
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 = renderer.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({
@ -187,10 +198,12 @@ A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""
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} =
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 = renderer.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);
@ -212,14 +225,16 @@ A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""
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('/dist/file.js'); expect(result[0].path).toEqual('/dist/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());
@ -230,14 +245,16 @@ A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""
() => { () => {
// Mock out reading the map file from disk // Mock out reading the map file from disk
spyOn(fs, 'readFileSync').and.returnValue(INPUT_PROGRAM_MAP.toJSON()); spyOn(fs, 'readFileSync').and.returnValue(INPUT_PROGRAM_MAP.toJSON());
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//# sourceMappingURL=file.js.map' contents: INPUT_PROGRAM.contents + '\n//# sourceMappingURL=file.js.map'
}]); }]);
const result = renderer.renderProgram( const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses);
expect(result[0].path).toEqual('/dist/file.js'); expect(result[0].path).toEqual('/dist/file.js');
expect(result[0].contents) expect(result[0].contents)
.toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('/dist/file.js.map')); .toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('/dist/file.js.map'));
@ -259,10 +276,12 @@ A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""
contents: `export const NgModule = () => null;` contents: `export const NgModule = () => null;`
}; };
// 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} =
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 = renderer.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(`);
@ -277,10 +296,11 @@ A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""
export class MyModule {}\nMyModule.decorators = [\n { type: NgModule, args: [] }\n];\n` export class MyModule {}\nMyModule.decorators = [\n { type: NgModule, args: [] }\n];\n`
}; };
const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses} = const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses,
createTestRenderer('@angular/core', [CORE_FILE]); moduleWithProvidersAnalyses} = createTestRenderer('@angular/core', [CORE_FILE]);
renderer.renderProgram( renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses);
const addDefinitionsSpy = renderer.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(`);
@ -291,10 +311,12 @@ A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""
describe('rendering typings', () => { describe('rendering typings', () => {
it('should render extract types into typings files', () => { it('should render extract types into typings files', () => {
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} = const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
createTestRenderer('test-package', [INPUT_PROGRAM], INPUT_DTS_PROGRAM); moduleWithProvidersAnalyses} =
createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]);
const result = renderer.renderProgram( const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses);
const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !;
expect(typingsFile.contents) expect(typingsFile.contents)
@ -303,30 +325,195 @@ A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""
}); });
it('should render imports into typings files', () => { it('should render imports into typings files', () => {
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} = const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
createTestRenderer('test-package', [INPUT_PROGRAM], INPUT_DTS_PROGRAM); moduleWithProvidersAnalyses} =
createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]);
const result = renderer.renderProgram( const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses);
const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !;
expect(typingsFile.contents).toContain(`// ADD IMPORTS\nexport declare class A`); expect(typingsFile.contents).toContain(`// ADD IMPORTS\nexport declare class A`);
}); });
it('should render exports into typings files', () => { it('should render exports into typings files', () => {
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} = const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
createTestRenderer('test-package', [INPUT_PROGRAM], INPUT_DTS_PROGRAM); moduleWithProvidersAnalyses} =
createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]);
// Add a mock export to trigger export rendering // Add a mock export to trigger export rendering
privateDeclarationsAnalyses.push( privateDeclarationsAnalyses.push(
{identifier: 'ComponentB', from: '/src/file.js', dtsFrom: '/typings/b.d.ts'}); {identifier: 'ComponentB', from: '/src/file.js', dtsFrom: '/typings/b.d.ts'});
const result = renderer.renderProgram( const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses);
const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !;
expect(typingsFile.contents) expect(typingsFile.contents)
.toContain(`// ADD EXPORTS\n\n// ADD IMPORTS\nexport declare class A`); .toContain(`// ADD EXPORTS\n\n// ADD IMPORTS\nexport declare class A`);
}); });
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} =
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), [
{name: './module', as: 'ɵngcc0'},
{name: '@angular/core', as: 'ɵngcc1'},
{name: 'some-library', as: 'ɵngcc2'},
]);
// 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};`);
});
}); });
}); });
}); });