diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index e539ef1dc1..ca4bdb788e 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -116,7 +116,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler { - constructor(private checker: ts.TypeChecker, private scopeRegistry: SelectorScopeRegistry) {} +export class NgModuleDecoratorHandler implements DecoratorHandler { + constructor( + private checker: ts.TypeChecker, private reflector: ReflectionHost, + private scopeRegistry: SelectorScopeRegistry) {} detect(decorators: Decorator[]): Decorator|undefined { return decorators.find(decorator => decorator.name === 'NgModule' && isAngularCore(decorator)); } - analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput { + analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput { if (decorator.args === null || decorator.args.length !== 1) { throw new Error(`Incorrect number of arguments to @NgModule decorator`); } @@ -66,26 +73,51 @@ export class NgModuleDecoratorHandler implements DecoratorHandler referenceToExpression(decl, context)), + exports: exports.map(exp => referenceToExpression(exp, context)), + imports: imports.map(imp => referenceToExpression(imp, context)), + emitInline: false, + }; + + const providers: Expression = ngModule.has('providers') ? + new WrappedNodeExpr(ngModule.get('providers') !) : + new LiteralArrayExpr([]); + + const ngInjectorDef: R3InjectorMetadata = { + name: node.name !.text, + type: new WrappedNodeExpr(node.name !), + deps: getConstructorDependencies(node, this.reflector), providers, + imports: new LiteralArrayExpr( + [...imports, ...exports].map(imp => referenceToExpression(imp, context))), + }; + return { analysis: { - type: new WrappedNodeExpr(node.name !), - bootstrap: [], - declarations: declarations.map(decl => referenceToExpression(decl, context)), - exports: exports.map(exp => referenceToExpression(exp, context)), - imports: imports.map(imp => referenceToExpression(imp, context)), - emitInline: false, + ngModuleDef, ngInjectorDef, }, }; } - compile(node: ts.ClassDeclaration, analysis: R3NgModuleMetadata): CompileResult { - const res = compileNgModule(analysis); - return { - field: 'ngModuleDef', - initializer: res.expression, - statements: [], - type: res.type, - }; + compile(node: ts.ClassDeclaration, analysis: NgModuleAnalysis): CompileResult[] { + const ngInjectorDef = compileInjector(analysis.ngInjectorDef); + const ngModuleDef = compileNgModule(analysis.ngModuleDef); + return [ + { + name: 'ngModuleDef', + initializer: ngModuleDef.expression, + statements: [], + type: ngModuleDef.type, + }, + { + name: 'ngInjectorDef', + initializer: ngInjectorDef.expression, + statements: [], + type: ngInjectorDef.type, + }, + ]; } } diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 95ef841abe..7d1bff4e35 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -99,7 +99,7 @@ export class NgtscProgram implements api.Program { new ComponentDecoratorHandler(checker, reflector, scopeRegistry), new DirectiveDecoratorHandler(checker, reflector, scopeRegistry), new InjectableDecoratorHandler(reflector), - new NgModuleDecoratorHandler(checker, scopeRegistry), + new NgModuleDecoratorHandler(checker, reflector, scopeRegistry), ]; const compilation = new IvyCompilation(handlers, checker, reflector); diff --git a/packages/compiler-cli/src/ngtsc/transform/src/api.ts b/packages/compiler-cli/src/ngtsc/transform/src/api.ts index 6dac390d0b..445216bc96 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/api.ts @@ -37,7 +37,7 @@ export interface DecoratorHandler { * Generate a description of the field which should be added to the class, including any * initialization code to be generated. */ - compile(node: ts.Declaration, analysis: A): CompileResult; + compile(node: ts.Declaration, analysis: A): CompileResult|CompileResult[]; } /** @@ -55,7 +55,7 @@ export interface AnalysisOutput { * and a type for the .d.ts file. */ export interface CompileResult { - field: string; + name: string; initializer: Expression; statements: Statement[]; type: Type; diff --git a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts index bebda19a99..d58a0db402 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -107,7 +107,7 @@ export class IvyCompilation { * Perform a compilation operation on the given class declaration and return instructions to an * AST transformer if any are available. */ - compileIvyFieldFor(node: ts.Declaration): CompileResult|undefined { + compileIvyFieldFor(node: ts.Declaration): CompileResult[]|undefined { // Look to see whether the original node was analyzed. If not, there's nothing to do. const original = ts.getOriginalNode(node) as ts.Declaration; if (!this.analysis.has(original)) { @@ -116,7 +116,10 @@ export class IvyCompilation { const op = this.analysis.get(original) !; // Run the actual compilation, which generates an Expression for the Ivy field. - const res = op.adapter.compile(node, op.analysis); + let res: CompileResult|CompileResult[] = op.adapter.compile(node, op.analysis); + if (!Array.isArray(res)) { + res = [res]; + } // Look up the .d.ts transformer for the input file and record that a field was generated, // which will allow the .d.ts to be transformed later. diff --git a/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts b/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts index 3038806287..499e6406e7 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts @@ -17,13 +17,13 @@ import {ImportManager, translateType} from './translator'; * Processes .d.ts file text and adds static field declarations, with types. */ export class DtsFileTransformer { - private ivyFields = new Map(); + private ivyFields = new Map(); private imports = new ImportManager(); /** * Track that a static field was added to the code for a class. */ - recordStaticField(name: string, decl: CompileResult): void { this.ivyFields.set(name, decl); } + recordStaticField(name: string, decls: CompileResult[]): void { this.ivyFields.set(name, decls); } /** * Process the .d.ts text for a file and add any declarations which were recorded. @@ -36,11 +36,18 @@ export class DtsFileTransformer { const stmt = dtsFile.statements[i]; if (ts.isClassDeclaration(stmt) && stmt.name !== undefined && this.ivyFields.has(stmt.name.text)) { - const desc = this.ivyFields.get(stmt.name.text) !; + const decls = this.ivyFields.get(stmt.name.text) !; const before = dts.substring(0, stmt.end - 1); const after = dts.substring(stmt.end - 1); - const type = translateType(desc.type, this.imports); - dts = before + ` static ${desc.field}: ${type};\n` + after; + + dts = before + + decls + .map(decl => { + const type = translateType(decl.type, this.imports); + return ` static ${decl.name}: ${type};\n`; + }) + .join('') + + after; } } diff --git a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts index 6878b2278c..47fc8c05b2 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts @@ -11,6 +11,7 @@ import * as ts from 'typescript'; import {VisitListEntryResult, Visitor, visit} from '../../util/src/visitor'; +import {CompileResult} from './api'; import {IvyCompilation} from './compilation'; import {ImportManager, translateExpression, translateStatement} from './translator'; @@ -33,14 +34,26 @@ class IvyVisitor extends Visitor { // Determine if this class has an Ivy field that needs to be added, and compile the field // to an expression if so. const res = this.compilation.compileIvyFieldFor(node); - if (res !== undefined) { - // There is a field to add. Translate the initializer for the field into TS nodes. - const exprNode = translateExpression(res.initializer, this.importManager); - // Create a static property declaration for the new field. - const property = ts.createProperty( - undefined, [ts.createToken(ts.SyntaxKind.StaticKeyword)], res.field, undefined, undefined, - exprNode); + if (res !== undefined) { + // There is at least one field to add. + const statements: ts.Statement[] = []; + const members = [...node.members]; + + res.forEach(field => { + // Translate the initializer for the field into TS nodes. + const exprNode = translateExpression(field.initializer, this.importManager); + + // Create a static property declaration for the new field. + const property = ts.createProperty( + undefined, [ts.createToken(ts.SyntaxKind.StaticKeyword)], field.name, undefined, + undefined, exprNode); + + field.statements.map(stmt => translateStatement(stmt, this.importManager)) + .forEach(stmt => statements.push(stmt)); + + members.push(property); + }); // Replace the class declaration with an updated version. node = ts.updateClassDeclaration( @@ -48,9 +61,7 @@ class IvyVisitor extends Visitor { // Remove the decorator which triggered this compilation, leaving the others alone. maybeFilterDecorator( node.decorators, this.compilation.ivyDecoratorFor(node) !.node as ts.Decorator), - node.modifiers, node.name, node.typeParameters, node.heritageClauses || [], - [...node.members, property]); - const statements = res.statements.map(stmt => translateStatement(stmt, this.importManager)); + node.modifiers, node.name, node.typeParameters, node.heritageClauses || [], members); return {node, before: statements}; } diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index b4869e1728..af0867849c 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -180,4 +180,46 @@ describe('ngtsc behavioral tests', () => { .toContain('static ngModuleDef: i0.NgModuleDef'); expect(dtsContents).not.toContain('__decorate'); }); + + it('should compile NgModules with services without errors', () => { + writeConfig(); + write('test.ts', ` + import {Component, NgModule} from '@angular/core'; + + export class Token {} + + @NgModule({}) + export class OtherModule {} + + @Component({ + selector: 'test-cmp', + template: 'this is a test', + }) + export class TestCmp {} + + @NgModule({ + declarations: [TestCmp], + providers: [{provide: Token, useValue: 'test'}], + imports: [OtherModule], + }) + export class TestModule {} + `); + + const exitCode = main(['-p', basePath], errorSpy); + expect(errorSpy).not.toHaveBeenCalled(); + expect(exitCode).toBe(0); + + const jsContents = getContents('test.js'); + expect(jsContents).toContain('i0.ɵdefineNgModule({ type: TestModule,'); + expect(jsContents) + .toContain( + `TestModule.ngInjectorDef = i0.defineInjector({ factory: ` + + `function TestModule_Factory() { return new TestModule(); }, providers: [{ provide: ` + + `Token, useValue: 'test' }], imports: [OtherModule] });`); + + const dtsContents = getContents('test.d.ts'); + expect(dtsContents) + .toContain('static ngModuleDef: i0.NgModuleDef'); + expect(dtsContents).toContain('static ngInjectorDef: i0.InjectorDef'); + }); }); diff --git a/packages/compiler/src/compiler.ts b/packages/compiler/src/compiler.ts index f3067a6c0e..22ca91c682 100644 --- a/packages/compiler/src/compiler.ts +++ b/packages/compiler/src/compiler.ts @@ -83,7 +83,7 @@ export * from './injectable_compiler_2'; export * from './render3/view/api'; export {jitExpression} from './render3/r3_jit'; export {R3DependencyMetadata, R3FactoryMetadata, R3ResolvedDependencyType} from './render3/r3_factory'; -export {compileNgModule, R3NgModuleMetadata} from './render3/r3_module_compiler'; +export {compileInjector, compileNgModule, R3InjectorMetadata, R3NgModuleMetadata} from './render3/r3_module_compiler'; export {makeBindingParser, parseTemplate} from './render3/view/template'; export {compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings} from './render3/view/compiler'; // This file only reexports content of the `src` folder. Keep it that way. \ No newline at end of file diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 03f6f8e5fe..2d5475c6eb 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -114,6 +114,11 @@ export class Identifiers { moduleName: CORE, }; + static InjectorDef: o.ExternalReference = { + name: 'InjectorDef', + moduleName: CORE, + }; + static defineInjector: o.ExternalReference = { name: 'defineInjector', moduleName: CORE, diff --git a/packages/compiler/src/render3/r3_module_compiler.ts b/packages/compiler/src/render3/r3_module_compiler.ts index 78f3e0db84..8b83f25931 100644 --- a/packages/compiler/src/render3/r3_module_compiler.ts +++ b/packages/compiler/src/render3/r3_module_compiler.ts @@ -13,6 +13,7 @@ import {mapLiteral} from '../output/map_util'; import * as o from '../output/output_ast'; import {OutputContext} from '../util'; +import {R3DependencyMetadata, compileFactoryFunction} from './r3_factory'; import {Identifiers as R3} from './r3_identifiers'; import {convertMetaToOutput, mapToMapExpression} from './util'; @@ -81,6 +82,35 @@ export function compileNgModule(meta: R3NgModuleMetadata): R3NgModuleDef { return {expression, type, additionalStatements}; } +export interface R3InjectorDef { + expression: o.Expression; + type: o.Type; +} + +export interface R3InjectorMetadata { + name: string; + type: o.Expression; + deps: R3DependencyMetadata[]; + providers: o.Expression; + imports: o.Expression; +} + +export function compileInjector(meta: R3InjectorMetadata): R3InjectorDef { + const expression = o.importExpr(R3.defineInjector).callFn([mapToMapExpression({ + factory: compileFactoryFunction({ + name: meta.name, + fnOrClass: meta.type, + deps: meta.deps, + useNew: true, + injectFn: R3.inject, + }), + providers: meta.providers, + imports: meta.imports, + })]); + const type = new o.ExpressionType(o.importExpr(R3.InjectorDef)); + return {expression, type}; +} + // TODO(alxhub): integrate this with `compileNgModule`. Currently the two are separate operations. export function compileNgModuleFromRender2( ctx: OutputContext, ngModule: CompileShallowModuleMetadata,