diff --git a/packages/compiler-cli/src/ngcc/src/analysis/module_with_providers_analyzer.ts b/packages/compiler-cli/src/ngcc/src/analysis/module_with_providers_analyzer.ts new file mode 100644 index 0000000000..fe774860d9 --- /dev/null +++ b/packages/compiler-cli/src/ngcc/src/analysis/module_with_providers_analyzer.ts @@ -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; +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; +} diff --git a/packages/compiler-cli/src/ngcc/src/analysis/private_declarations_analyzer.ts b/packages/compiler-cli/src/ngcc/src/analysis/private_declarations_analyzer.ts index fb2505dca5..3420b669b6 100644 --- a/packages/compiler-cli/src/ngcc/src/analysis/private_declarations_analyzer.ts +++ b/packages/compiler-cli/src/ngcc/src/analysis/private_declarations_analyzer.ts @@ -15,7 +15,7 @@ import {hasNameIdentifier, isDefined} from '../utils'; export interface ExportInfo { identifier: string; from: string; - dtsFrom: string|null; + dtsFrom?: string|null; } export type PrivateDeclarationsAnalyses = ExportInfo[]; diff --git a/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts b/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts index 6d9f09af89..34f3345369 100644 --- a/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts +++ b/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts @@ -14,7 +14,7 @@ import {BundleProgram} from '../packages/bundle_program'; import {findAll, getNameText, isDefined} from '../utils'; 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 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; } + /** + * 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 getDecoratorsOfSymbol(symbol: ts.Symbol): Decorator[]|null { @@ -1017,6 +1048,31 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N sourceFile => { collectExportedDeclarations(checker, dtsDeclarationMap, sourceFile); }); 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 ///////////// diff --git a/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts b/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts index d8555c9295..1dc7ae8759 100644 --- a/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts +++ b/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts @@ -19,6 +19,21 @@ export function isSwitchableVariableDeclaration(node: ts.Node): 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 */ @@ -45,4 +60,13 @@ export interface NgccReflectionHost extends ReflectionHost { * @returns An array of decorated classes. */ 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[]; } diff --git a/packages/compiler-cli/src/ngcc/src/packages/transformer.ts b/packages/compiler-cli/src/ngcc/src/packages/transformer.ts index 1c4233fe74..e2bf4a57a8 100644 --- a/packages/compiler-cli/src/ngcc/src/packages/transformer.ts +++ b/packages/compiler-cli/src/ngcc/src/packages/transformer.ts @@ -11,6 +11,7 @@ import {mkdir, mv} from 'shelljs'; import * as ts from 'typescript'; import {CompiledFile, DecorationAnalyzer} from '../analysis/decoration_analyzer'; +import {ModuleWithProvidersAnalyses, ModuleWithProvidersAnalyzer} from '../analysis/module_with_providers_analyzer'; import {NgccReferencesRegistry} from '../analysis/ngcc_references_registry'; import {ExportInfo, PrivateDeclarationsAnalyzer} from '../analysis/private_declarations_analyzer'; import {SwitchMarkerAnalyses, SwitchMarkerAnalyzer} from '../analysis/switch_marker_analyzer'; @@ -25,6 +26,7 @@ import {EntryPoint} from './entry_point'; import {EntryPointBundle} from './entry_point_bundle'; + /** * 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). @@ -59,13 +61,14 @@ export class Transformer { const reflectionHost = this.getHost(isCore, bundle); // Parse and analyze the files. - const {decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} = - this.analyzeProgram(reflectionHost, isCore, bundle); + const {decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses} = this.analyzeProgram(reflectionHost, isCore, bundle); // Transform the source files and source maps. const renderer = this.getRenderer(reflectionHost, isCore, bundle); const renderedFiles = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses); // Write out all the transformed files. renderedFiles.forEach(file => this.writeFile(file)); @@ -102,16 +105,26 @@ export class Transformer { ProgramAnalyses { const typeChecker = bundle.src.program.getTypeChecker(); const referencesRegistry = new NgccReferencesRegistry(reflectionHost); + + const switchMarkerAnalyzer = new SwitchMarkerAnalyzer(reflectionHost); + const switchMarkerAnalyses = switchMarkerAnalyzer.analyzeProgram(bundle.src.program); + const decorationAnalyzer = new DecorationAnalyzer( 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 = new PrivateDeclarationsAnalyzer(reflectionHost, referencesRegistry); - const decorationAnalyses = decorationAnalyzer.analyzeProgram(bundle.src.program); - const switchMarkerAnalyses = switchMarkerAnalyzer.analyzeProgram(bundle.src.program); const privateDeclarationsAnalyses = privateDeclarationsAnalyzer.analyzeProgram(bundle.src.program); - return {decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses}; + + return {decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses}; } writeFile(file: FileInfo): void { @@ -129,4 +142,5 @@ interface ProgramAnalyses { decorationAnalyses: Map; switchMarkerAnalyses: SwitchMarkerAnalyses; privateDeclarationsAnalyses: ExportInfo[]; + moduleWithProvidersAnalyses: ModuleWithProvidersAnalyses|null; } diff --git a/packages/compiler-cli/src/ngcc/src/rendering/renderer.ts b/packages/compiler-cli/src/ngcc/src/rendering/renderer.ts index 0c1c39fd97..f88c6775bf 100644 --- a/packages/compiler-cli/src/ngcc/src/rendering/renderer.ts +++ b/packages/compiler-cli/src/ngcc/src/rendering/renderer.ts @@ -15,9 +15,10 @@ import * as ts from 'typescript'; import {Decorator} from '../../../ngtsc/host'; 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 {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 {SwitchMarkerAnalyses, SwitchMarkerAnalysis} from '../analysis/switch_marker_analyzer'; import {IMPORT_PREFIX} from '../constants'; @@ -49,6 +50,20 @@ interface DtsClassInfo { 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 @@ -71,7 +86,8 @@ export abstract class Renderer { renderProgram( decorationAnalyses: DecorationAnalyses, switchMarkerAnalyses: SwitchMarkerAnalyses, - privateDeclarationsAnalyses: PrivateDeclarationsAnalyses): FileInfo[] { + privateDeclarationsAnalyses: PrivateDeclarationsAnalyses, + moduleWithProvidersAnalyses: ModuleWithProvidersAnalyses|null): FileInfo[] { const renderedFiles: FileInfo[] = []; // Transform the source files. @@ -87,16 +103,16 @@ export abstract class Renderer { // Transform the .d.ts files 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) // 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, []); + dtsFiles.set(this.bundle.dts.file, new DtsRenderInfo()); } dtsFiles.forEach( - (classes, file) => renderedFiles.push( - ...this.renderDtsFile(file, classes, privateDeclarationsAnalyses))); + (renderInfo, file) => renderedFiles.push(...this.renderDtsFile(file, renderInfo))); } return renderedFiles; @@ -151,14 +167,12 @@ export abstract class Renderer { return this.renderSourceAndMap(sourceFile, input, outputText); } - renderDtsFile( - dtsFile: ts.SourceFile, dtsClasses: DtsClassInfo[], - privateDeclarationsAnalyses: PrivateDeclarationsAnalyses): FileInfo[] { + renderDtsFile(dtsFile: ts.SourceFile, renderInfo: DtsRenderInfo): FileInfo[] { const input = this.extractSourceMap(dtsFile); const outputText = new MagicString(input.source); const importManager = new NgccImportManager(false, this.isCore, IMPORT_PREFIX); - dtsClasses.forEach(dtsClass => { + renderInfo.classInfo.forEach(dtsClass => { const endOfClass = dtsClass.dtsDeclaration.getEnd(); dtsClass.compilation.forEach(declaration => { const type = translateType(declaration.type, importManager); @@ -167,26 +181,67 @@ export abstract class Renderer { }); }); + this.addModuleWithProvidersParams(outputText, renderInfo.moduleWithProviders, importManager); this.addImports( outputText, importManager.getAllImports(dtsFile.fileName, this.bundle.dts !.r3SymbolsFile)); - if (dtsFile === this.bundle.dts !.file) { - 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); - } + this.addExports(outputText, dtsFile.fileName, renderInfo.privateExports); + 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): void; protected abstract addImports(output: MagicString, imports: {name: string, as: string}[]): void; @@ -302,22 +357,67 @@ export abstract class Renderer { return result; } - protected getTypingsFilesToRender(analyses: DecorationAnalyses): - Map { - const dtsMap = new Map(); - analyses.forEach(compiledFile => { + protected getTypingsFilesToRender( + decorationAnalyses: DecorationAnalyses, + privateDeclarationsAnalyses: PrivateDeclarationsAnalyses, + moduleWithProvidersAnalyses: ModuleWithProvidersAnalyses| + null): Map { + const dtsMap = new Map(); + + // Capture the rendering info from the decoration analyses + decorationAnalyses.forEach(compiledFile => { compiledFile.compiledClasses.forEach(compiledClass => { const dtsDeclaration = this.host.getDtsDeclaration(compiledClass.declaration); if (dtsDeclaration) { const dtsFile = dtsDeclaration.getSourceFile(); - const classes = dtsMap.get(dtsFile) || []; - classes.push({dtsDeclaration, compilation: compiledClass.compilation}); - dtsMap.set(dtsFile, classes); + 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) { + 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; } + + /** + * 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 { - return filePath.replace(/\.(js|d\.ts$)/, ''); + return filePath.replace(/\.(js|d\.ts)$/, ''); } /** @@ -399,3 +499,9 @@ function createAssignmentStatement( const receiver = new WrappedNodeExpr(receiverName); 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}`; +} diff --git a/packages/compiler-cli/src/ngcc/test/analysis/module_with_providers_analyzer_spec.ts b/packages/compiler-cli/src/ngcc/test/analysis/module_with_providers_analyzer_spec.ts new file mode 100644 index 0000000000..f8fa294b14 --- /dev/null +++ b/packages/compiler-cli/src/ngcc/test/analysis/module_with_providers_analyzer_spec.ts @@ -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; + export declare function explicitExternalFunction(): ModuleWithProviders; + export declare function explicitLibraryFunction(): ModuleWithProviders; + export declare class ExplicitClass { + static explicitInternalMethod(): ModuleWithProviders; + static explicitExternalMethod(): ModuleWithProviders; + static explicitLibraryMethod(): ModuleWithProviders; + } + ` + }, + { + name: '/typings/any.d.ts', + contents: ` + import {ModuleWithProviders} from './core'; + export declare class AnyInternalModule {} + export declare function anyInternalFunction(): ModuleWithProviders; + export declare function anyExternalFunction(): ModuleWithProviders; + export declare function anyLibraryFunction(): ModuleWithProviders; + export declare class AnyClass { + static anyInternalMethod(): ModuleWithProviders; + static anyExternalMethod(): ModuleWithProviders; + static anyLibraryMethod(): ModuleWithProviders; + } + ` + }, + { + 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; + export declare function noProvExplicitExternalFunction(): ModuleWithProviders; + export declare function noProvExplicitLibraryFunction(): ModuleWithProviders; + export declare function noProvAnyInternalFunction(): ModuleWithProviders; + export declare function noProvAnyExternalFunction(): ModuleWithProviders; + export declare function noProvAnyLibraryFunction(): ModuleWithProviders; + 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 { + new (...args: any[]): T + } + export declare type Provider = any; + export declare interface ModuleWithProviders { + ngModule: Type + 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]) : + []; + } + }); +}); diff --git a/packages/compiler-cli/src/ngcc/test/helpers/utils.ts b/packages/compiler-cli/src/ngcc/test/helpers/utils.ts index 1d466683ab..3b5656ffab 100644 --- a/packages/compiler-cli/src/ngcc/test/helpers/utils.ts +++ b/packages/compiler-cli/src/ngcc/test/helpers/utils.ts @@ -84,6 +84,8 @@ export function getFakeCore() { export class InjectionToken { constructor(name: string) {} } + + export interface ModuleWithProviders {} ` }; } diff --git a/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts b/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts index 0b8def3fb8..5c40e9f44c 100644 --- a/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts @@ -485,6 +485,54 @@ const TYPINGS_DTS_FILES = [ {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('getDecoratorsOfDeclaration()', () => { @@ -1375,4 +1423,34 @@ describe('Fesm2015ReflectionHost', () => { .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'], + ]); + }); + }); }); diff --git a/packages/compiler-cli/src/ngcc/test/rendering/renderer_spec.ts b/packages/compiler-cli/src/ngcc/test/rendering/renderer_spec.ts index af391eff71..ca4151e524 100644 --- a/packages/compiler-cli/src/ngcc/test/rendering/renderer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/rendering/renderer_spec.ts @@ -11,6 +11,7 @@ import * as ts from 'typescript'; import {fromObject, generateMapFileComment} from 'convert-source-map'; import {CompiledClass, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; +import {ModuleWithProvidersAnalyzer} from '../../src/analysis/module_with_providers_analyzer'; import {PrivateDeclarationsAnalyzer} from '../../src/analysis/private_declarations_analyzer'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; @@ -47,9 +48,9 @@ class TestRenderer extends Renderer { function createTestRenderer( packageName: string, files: {name: string, contents: string}[], - dtsFile?: {name: string, contents: string}) { + dtsFiles?: {name: string, contents: string}[]) { 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 host = new Esm2015ReflectionHost(isCore, typeChecker, bundle.dts); const referencesRegistry = new NgccReferencesRegistry(host); @@ -57,6 +58,8 @@ function createTestRenderer( new DecorationAnalyzer(typeChecker, host, referencesRegistry, bundle.rootDirs, isCore) .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 = new PrivateDeclarationsAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program); const renderer = new TestRenderer(host, isCore, bundle); @@ -64,7 +67,8 @@ function createTestRenderer( spyOn(renderer, 'addDefinitions').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()', () => { it('should render the modified contents; and a new map file, if the original provided no map file.', () => { - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} = - createTestRenderer('test-package', [INPUT_PROGRAM]); + const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses} = createTestRenderer('test-package', [INPUT_PROGRAM]); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses); expect(result[0].path).toEqual('/dist/file.js'); expect(result[0].contents) .toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('/dist/file.js.map')); @@ -134,9 +139,11 @@ describe('Renderer', () => { it('should render as JavaScript', () => { - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} = - createTestRenderer('test-package', [COMPONENT_PROGRAM]); - renderer.renderProgram(decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses} = createTestRenderer('test-package', [COMPONENT_PROGRAM]); + renderer.renderProgram( + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses); const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; expect(addDefinitionsSpy.calls.first().args[2]) .toEqual(`/*@__PURE__*/ ɵngcc0.ɵsetClassMetadata(A, [{ @@ -154,10 +161,12 @@ A.ngComponentDef = ɵngcc0.ɵdefineComponent({ type: A, selectors: [["a"]], fact describe('calling abstract methods', () => { 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]); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses); const addImportsSpy = renderer.addImports as jasmine.Spy; expect(addImportsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); 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.', () => { - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} = + const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses} = createTestRenderer('test-package', [INPUT_PROGRAM]); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses); const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; expect(addDefinitionsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); 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', () => { - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} = + const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses} = createTestRenderer('test-package', [INPUT_PROGRAM]); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses); const removeDecoratorsSpy = renderer.removeDecorators as jasmine.Spy; 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', () => { 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( 'test-package', [{ ...INPUT_PROGRAM, contents: INPUT_PROGRAM.contents + '\n' + INPUT_PROGRAM_MAP.toComment() }]); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses); expect(result[0].path).toEqual('/dist/file.js'); expect(result[0].contents) .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 spyOn(fs, 'readFileSync').and.returnValue(INPUT_PROGRAM_MAP.toJSON()); - const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses} = + const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses} = createTestRenderer( 'test-package', [{ ...INPUT_PROGRAM, contents: INPUT_PROGRAM.contents + '\n//# sourceMappingURL=file.js.map' }]); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses); expect(result[0].path).toEqual('/dist/file.js'); expect(result[0].contents) .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;` }; // 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]); renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses); const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; expect(addDefinitionsSpy.calls.first().args[2]) .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` }; - const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses} = - createTestRenderer('@angular/core', [CORE_FILE]); + const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses} = createTestRenderer('@angular/core', [CORE_FILE]); renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses); const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; expect(addDefinitionsSpy.calls.first().args[2]) .toContain(`/*@__PURE__*/ setClassMetadata(`); @@ -291,10 +311,12 @@ A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", "" describe('rendering typings', () => { it('should render extract types into typings files', () => { - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} = - createTestRenderer('test-package', [INPUT_PROGRAM], INPUT_DTS_PROGRAM); + const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses} = + createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses); const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; expect(typingsFile.contents) @@ -303,30 +325,195 @@ A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", "" }); it('should render imports into typings files', () => { - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} = - createTestRenderer('test-package', [INPUT_PROGRAM], INPUT_DTS_PROGRAM); + const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses} = + createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]); const result = renderer.renderProgram( - decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses); + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses); const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; expect(typingsFile.contents).toContain(`// ADD IMPORTS\nexport declare class A`); }); it('should render exports into typings files', () => { - const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses} = - createTestRenderer('test-package', [INPUT_PROGRAM], INPUT_DTS_PROGRAM); + 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); + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses); const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; expect(typingsFile.contents) .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; + static withProviders3(): ModuleWithProviders; + static withProviders4(): ModuleWithProviders; + static withProviders5(); + static withProviders6(): ModuleWithProviders; + static withProviders7(): {ngModule: SomeModule, providers: any[]}; + static withProviders8(): MyModuleWithProviders; + } + export declare function withProviders1(): ModuleWithProviders; + export declare function withProviders2(): ModuleWithProviders; + export declare function withProviders3(): ModuleWithProviders; + export declare function withProviders4(): ModuleWithProviders; + export declare function withProviders5(); + export declare function withProviders6(): ModuleWithProviders; + export declare function withProviders7(): {ngModule: SomeModule, providers: any[]}; + export declare function withProviders8(): MyModuleWithProviders;` + }, + { + name: '/typings/module.d.ts', + contents: ` + export interface ModuleWithProviders {} + export declare class ExternalModule { + static withProviders1(): ModuleWithProviders; + static withProviders2(): ModuleWithProviders; + }` + }, + { + name: '/node_modules/some-library/index.d.ts', + contents: 'export declare class LibraryModule {}' + }, + ]; + const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses} = + createTestRenderer( + 'test-package', MODULE_WITH_PROVIDERS_PROGRAM, MODULE_WITH_PROVIDERS_DTS_PROGRAM); + + const result = renderer.renderProgram( + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses); + + const typingsFile = result.find(f => f.path === '/typings/index.d.ts') !; + + expect(typingsFile.contents).toContain(` + static withProviders1(): ModuleWithProviders; + static withProviders2(): ModuleWithProviders; + static withProviders3(): ModuleWithProviders; + static withProviders4(): ModuleWithProviders<ɵngcc0.ExternalModule>; + static withProviders5(): ɵngcc1.ModuleWithProviders<ɵngcc0.ExternalModule>; + static withProviders6(): ModuleWithProviders<ɵngcc2.LibraryModule>; + static withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule}; + static withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`); + expect(typingsFile.contents).toContain(` + export declare function withProviders1(): ModuleWithProviders; + export declare function withProviders2(): ModuleWithProviders; + export declare function withProviders3(): ModuleWithProviders; + export declare function withProviders4(): ModuleWithProviders<ɵngcc0.ExternalModule>; + export declare function withProviders5(): ɵngcc1.ModuleWithProviders<ɵngcc0.ExternalModule>; + export declare function withProviders6(): ModuleWithProviders<ɵngcc2.LibraryModule>; + export declare function withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule}; + export declare function withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`); + + expect(renderer.addImports).toHaveBeenCalledWith(jasmine.any(MagicString), [ + {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};`); + }); }); }); });