feat(ivy): generate ngInjectorDef for @NgModule in AOT mode (#24632)
This change generates ngInjectorDef as well as ngModuleDef for @NgModule annotated types, reflecting the dual nature of @NgModules as both compilation scopes and as DI configuration containers. This required implementing ngInjectorDef compilation in @angular/compiler as well as allowing for multiple generated definitions for a single decorator in the core of ngtsc. PR Close #24632
This commit is contained in:
		
							parent
							
								
									166d90d2a9
								
							
						
					
					
						commit
						ae9418c7de
					
				| @ -116,7 +116,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe | ||||
| 
 | ||||
|     const res = compileComponentFromMetadata(analysis, pool, makeBindingParser()); | ||||
|     return { | ||||
|       field: 'ngComponentDef', | ||||
|       name: 'ngComponentDef', | ||||
|       initializer: res.expression, | ||||
|       statements: pool.statements, | ||||
|       type: res.type, | ||||
|  | ||||
| @ -44,7 +44,7 @@ export class DirectiveDecoratorHandler implements DecoratorHandler<R3DirectiveMe | ||||
|     const pool = new ConstantPool(); | ||||
|     const res = compileDirectiveFromMetadata(analysis, pool, makeBindingParser()); | ||||
|     return { | ||||
|       field: 'ngDirectiveDef', | ||||
|       name: 'ngDirectiveDef', | ||||
|       initializer: res.expression, | ||||
|       statements: pool.statements, | ||||
|       type: res.type, | ||||
|  | ||||
| @ -35,7 +35,7 @@ export class InjectableDecoratorHandler implements DecoratorHandler<R3Injectable | ||||
|   compile(node: ts.ClassDeclaration, analysis: R3InjectableMetadata): CompileResult { | ||||
|     const res = compileIvyInjectable(analysis); | ||||
|     return { | ||||
|       field: 'ngInjectableDef', | ||||
|       name: 'ngInjectableDef', | ||||
|       initializer: res.expression, | ||||
|       statements: [], | ||||
|       type: res.type, | ||||
|  | ||||
| @ -6,29 +6,36 @@ | ||||
|  * found in the LICENSE file at https://angular.io/license
 | ||||
|  */ | ||||
| 
 | ||||
| import {ConstantPool, Expression, R3DirectiveMetadata, R3NgModuleMetadata, WrappedNodeExpr, compileNgModule, makeBindingParser, parseTemplate} from '@angular/compiler'; | ||||
| import {ConstantPool, Expression, LiteralArrayExpr, R3DirectiveMetadata, R3InjectorMetadata, R3NgModuleMetadata, WrappedNodeExpr, compileInjector, compileNgModule, makeBindingParser, parseTemplate} from '@angular/compiler'; | ||||
| import * as ts from 'typescript'; | ||||
| 
 | ||||
| import {Decorator} from '../../host'; | ||||
| import {Decorator, ReflectionHost} from '../../host'; | ||||
| import {Reference, ResolvedValue, reflectObjectLiteral, staticallyResolve} from '../../metadata'; | ||||
| import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform'; | ||||
| 
 | ||||
| import {SelectorScopeRegistry} from './selector_scope'; | ||||
| import {isAngularCore, referenceToExpression} from './util'; | ||||
| import {getConstructorDependencies, isAngularCore, referenceToExpression} from './util'; | ||||
| 
 | ||||
| export interface NgModuleAnalysis { | ||||
|   ngModuleDef: R3NgModuleMetadata; | ||||
|   ngInjectorDef: R3InjectorMetadata; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Compiles @NgModule annotations to ngModuleDef fields. | ||||
|  * | ||||
|  * TODO(alxhub): handle injector side of things as well. | ||||
|  */ | ||||
| export class NgModuleDecoratorHandler implements DecoratorHandler<R3NgModuleMetadata> { | ||||
|   constructor(private checker: ts.TypeChecker, private scopeRegistry: SelectorScopeRegistry) {} | ||||
| export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalysis> { | ||||
|   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<R3NgModuleMetadata> { | ||||
|   analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<NgModuleAnalysis> { | ||||
|     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<R3NgModuleMeta | ||||
| 
 | ||||
|     const context = node.getSourceFile(); | ||||
| 
 | ||||
|     const ngModuleDef: R3NgModuleMetadata = { | ||||
|       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, | ||||
|     }; | ||||
| 
 | ||||
|     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, | ||||
|       }, | ||||
|     ]; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -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); | ||||
| 
 | ||||
|  | ||||
| @ -37,7 +37,7 @@ export interface DecoratorHandler<A> { | ||||
|    * 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<A> { | ||||
|  * and a type for the .d.ts file. | ||||
|  */ | ||||
| export interface CompileResult { | ||||
|   field: string; | ||||
|   name: string; | ||||
|   initializer: Expression; | ||||
|   statements: Statement[]; | ||||
|   type: Type; | ||||
|  | ||||
| @ -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.
 | ||||
|  | ||||
| @ -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<string, CompileResult>(); | ||||
|   private ivyFields = new Map<string, CompileResult[]>(); | ||||
|   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; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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}; | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -180,4 +180,46 @@ describe('ngtsc behavioral tests', () => { | ||||
|         .toContain('static ngModuleDef: i0.NgModuleDef<TestModule, [TestCmp], [], []>'); | ||||
|     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<TestModule, [TestCmp], [OtherModule], []>'); | ||||
|     expect(dtsContents).toContain('static ngInjectorDef: i0.InjectorDef'); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| @ -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.
 | ||||
| @ -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, | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user