feat(ivy): AOT support for compilation of @Pipes (#24703)
This commit adds support to ngtsc for compilation of the @Pipe annotation, including support for pipes in @NgModule scopes. PR Close #24703
This commit is contained in:
		
							parent
							
								
									3d52174bf1
								
							
						
					
					
						commit
						b6af8700ce
					
				| @ -6,34 +6,76 @@ | ||||
|  * found in the LICENSE file at https://angular.io/license
 | ||||
|  */ | ||||
| 
 | ||||
| import {Expression, ExpressionType, LiteralExpr, R3DependencyMetadata, R3InjectableMetadata, R3ResolvedDependencyType, WrappedNodeExpr, compileInjectable as compileIvyInjectable} from '@angular/compiler'; | ||||
| import {LiteralExpr, R3PipeMetadata, WrappedNodeExpr, compilePipeFromMetadata} from '@angular/compiler'; | ||||
| import * as ts from 'typescript'; | ||||
| 
 | ||||
| import {Decorator, ReflectionHost} from '../../host'; | ||||
| import {reflectObjectLiteral, staticallyResolve} from '../../metadata'; | ||||
| import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform'; | ||||
| 
 | ||||
| import {isAngularCore} from './util'; | ||||
| import {SelectorScopeRegistry} from './selector_scope'; | ||||
| import {getConstructorDependencies, isAngularCore} from './util'; | ||||
| 
 | ||||
| export class PipeDecoratorHandler implements DecoratorHandler<string> { | ||||
|   constructor(private reflector: ReflectionHost, private isCore: boolean) {} | ||||
| export class PipeDecoratorHandler implements DecoratorHandler<R3PipeMetadata> { | ||||
|   constructor( | ||||
|       private checker: ts.TypeChecker, private reflector: ReflectionHost, | ||||
|       private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {} | ||||
| 
 | ||||
|   detect(decorator: Decorator[]): Decorator|undefined { | ||||
|     return decorator.find( | ||||
|         decorator => decorator.name === 'Pipe' && (this.isCore || isAngularCore(decorator))); | ||||
|   } | ||||
| 
 | ||||
|   analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<string> { | ||||
|   analyze(clazz: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<R3PipeMetadata> { | ||||
|     if (clazz.name === undefined) { | ||||
|       throw new Error(`@Pipes must have names`); | ||||
|     } | ||||
|     const name = clazz.name.text; | ||||
|     const type = new WrappedNodeExpr(clazz.name); | ||||
|     if (decorator.args === null) { | ||||
|       throw new Error(`@Pipe must be called`); | ||||
|     } | ||||
|     const meta = decorator.args[0]; | ||||
|     if (!ts.isObjectLiteralExpression(meta)) { | ||||
|       throw new Error(`Decorator argument must be literal.`); | ||||
|     } | ||||
|     const pipe = reflectObjectLiteral(meta); | ||||
| 
 | ||||
|     if (!pipe.has('name')) { | ||||
|       throw new Error(`@Pipe decorator is missing name field`); | ||||
|     } | ||||
|     const pipeName = staticallyResolve(pipe.get('name') !, this.checker); | ||||
|     if (typeof pipeName !== 'string') { | ||||
|       throw new Error(`@Pipe.name must be a string`); | ||||
|     } | ||||
|     this.scopeRegistry.registerPipe(clazz, pipeName); | ||||
| 
 | ||||
|     let pure = false; | ||||
|     if (pipe.has('pure')) { | ||||
|       const pureValue = staticallyResolve(pipe.get('pure') !, this.checker); | ||||
|       if (typeof pureValue !== 'boolean') { | ||||
|         throw new Error(`@Pipe.pure must be a boolean`); | ||||
|       } | ||||
|       pure = pureValue; | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|       analysis: 'test', | ||||
|       analysis: { | ||||
|         name, | ||||
|         type, | ||||
|         pipeName, | ||||
|         deps: getConstructorDependencies(clazz, this.reflector, this.isCore), pure, | ||||
|       } | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   compile(node: ts.ClassDeclaration, analysis: string): CompileResult { | ||||
|   compile(node: ts.ClassDeclaration, analysis: R3PipeMetadata): CompileResult { | ||||
|     const res = compilePipeFromMetadata(analysis); | ||||
|     return { | ||||
|       name: 'ngPipeDef', | ||||
|       initializer: new LiteralExpr(null), | ||||
|       initializer: res.expression, | ||||
|       statements: [], | ||||
|       type: new ExpressionType(new LiteralExpr(null)), | ||||
|       type: res.type, | ||||
|     }; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -118,7 +118,11 @@ export class SelectorScopeRegistry { | ||||
|   /** | ||||
|    * Register the name of a pipe with the registry. | ||||
|    */ | ||||
|   registerPipe(node: ts.Declaration, name: string): void { this._pipeToName.set(node, name); } | ||||
|   registerPipe(node: ts.Declaration, name: string): void { | ||||
|     node = ts.getOriginalNode(node) as ts.Declaration; | ||||
| 
 | ||||
|     this._pipeToName.set(node, name); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Produce the compilation scope of a component, which is determined by the module that declares | ||||
| @ -153,10 +157,19 @@ export class SelectorScopeRegistry { | ||||
|     // The initial value of ngModuleImportedFrom is 'null' which signifies that the NgModule
 | ||||
|     // was not imported from a .d.ts source.
 | ||||
|     this.lookupScopes(module !, /* ngModuleImportedFrom */ null).compilation.forEach(ref => { | ||||
|       const selector = this.lookupDirectiveSelector(ts.getOriginalNode(ref.node) as ts.Declaration); | ||||
|       const node = ts.getOriginalNode(ref.node) as ts.Declaration; | ||||
| 
 | ||||
|       // Either the node represents a directive or a pipe. Look for both.
 | ||||
|       const selector = this.lookupDirectiveSelector(node); | ||||
|       // Only directives/components with selectors get added to the scope.
 | ||||
|       if (selector != null) { | ||||
|         directives.set(selector, ref); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       const name = this.lookupPipeName(node); | ||||
|       if (name != null) { | ||||
|         pipes.set(name, ref); | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
| @ -238,8 +251,12 @@ export class SelectorScopeRegistry { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   private lookupPipeName(node: ts.Declaration): string|undefined { | ||||
|     return this._pipeToName.get(node); | ||||
|   private lookupPipeName(node: ts.Declaration): string|null { | ||||
|     if (this._pipeToName.has(node)) { | ||||
|       return this._pipeToName.get(node) !; | ||||
|     } else { | ||||
|       return this._readNameFromCompiledClass(node); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
| @ -300,6 +317,30 @@ export class SelectorScopeRegistry { | ||||
|     return type.literal.text; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Get the selector from type metadata for a class with a precompiled ngComponentDef or | ||||
|    * ngDirectiveDef. | ||||
|    */ | ||||
|   private _readNameFromCompiledClass(clazz: ts.Declaration): string|null { | ||||
|     const def = this.reflector.getMembersOfClass(clazz).find( | ||||
|         field => field.isStatic && field.name === 'ngPipeDef'); | ||||
|     if (def === undefined) { | ||||
|       // No definition could be found.
 | ||||
|       return null; | ||||
|     } else if ( | ||||
|         def.type === null || !ts.isTypeReferenceNode(def.type) || | ||||
|         def.type.typeArguments === undefined || def.type.typeArguments.length !== 2) { | ||||
|       // The type metadata was the wrong shape.
 | ||||
|       return null; | ||||
|     } | ||||
|     const type = def.type.typeArguments[1]; | ||||
|     if (!ts.isLiteralTypeNode(type) || !ts.isStringLiteral(type.literal)) { | ||||
|       // The type metadata was the wrong type.
 | ||||
|       return null; | ||||
|     } | ||||
|     return type.literal.text; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Process a `TypeNode` which is a tuple of references to other types, and return `Reference`s to | ||||
|    * them. | ||||
|  | ||||
| @ -154,7 +154,7 @@ export class NgtscProgram implements api.Program { | ||||
|       new DirectiveDecoratorHandler(checker, this.reflector, scopeRegistry, this.isCore), | ||||
|       new InjectableDecoratorHandler(this.reflector, this.isCore), | ||||
|       new NgModuleDecoratorHandler(checker, this.reflector, scopeRegistry, this.isCore), | ||||
|       new PipeDecoratorHandler(this.reflector, this.isCore), | ||||
|       new PipeDecoratorHandler(checker, this.reflector, scopeRegistry, this.isCore), | ||||
|     ]; | ||||
| 
 | ||||
|     return new IvyCompilation(handlers, checker, this.reflector, this.coreImportsFrom); | ||||
|  | ||||
| @ -20,6 +20,7 @@ export const Component = callableClassDecorator(); | ||||
| export const Directive = callableClassDecorator(); | ||||
| export const Injectable = callableClassDecorator(); | ||||
| export const NgModule = callableClassDecorator(); | ||||
| export const Pipe = callableClassDecorator(); | ||||
| 
 | ||||
| export const Inject = callableParamDecorator(); | ||||
| export const Self = callableParamDecorator(); | ||||
|  | ||||
| @ -243,4 +243,80 @@ describe('ngtsc behavioral tests', () => { | ||||
|         .toContain('static ngModuleDef: i0.NgModuleDef<TestModule, [TestCmp], [OtherModule], []>'); | ||||
|     expect(dtsContents).toContain('static ngInjectorDef: i0.InjectorDef'); | ||||
|   }); | ||||
| 
 | ||||
|   it('should compile Pipes without errors', () => { | ||||
|     writeConfig(); | ||||
|     write('test.ts', ` | ||||
|         import {Pipe} from '@angular/core'; | ||||
| 
 | ||||
|         @Pipe({ | ||||
|           name: 'test-pipe', | ||||
|           pure: false, | ||||
|         }) | ||||
|         export class TestPipe {} | ||||
|     `);
 | ||||
| 
 | ||||
|     const exitCode = main(['-p', basePath], errorSpy); | ||||
|     expect(errorSpy).not.toHaveBeenCalled(); | ||||
|     expect(exitCode).toBe(0); | ||||
| 
 | ||||
|     const jsContents = getContents('test.js'); | ||||
|     const dtsContents = getContents('test.d.ts'); | ||||
| 
 | ||||
|     expect(jsContents) | ||||
|         .toContain( | ||||
|             'TestPipe.ngPipeDef = i0.ɵdefinePipe({ name: "test-pipe", type: TestPipe, ' + | ||||
|             'factory: function TestPipe_Factory() { return new TestPipe(); }, pure: false })'); | ||||
|     expect(dtsContents).toContain('static ngPipeDef: i0.ɵPipeDef<TestPipe, \'test-pipe\'>;'); | ||||
|   }); | ||||
| 
 | ||||
|   it('should compile Pipes with dependencies', () => { | ||||
|     writeConfig(); | ||||
|     write('test.ts', ` | ||||
|         import {Pipe} from '@angular/core'; | ||||
| 
 | ||||
|         export class Dep {} | ||||
| 
 | ||||
|         @Pipe({ | ||||
|           name: 'test-pipe', | ||||
|           pure: false, | ||||
|         }) | ||||
|         export class TestPipe { | ||||
|           constructor(dep: Dep) {} | ||||
|         } | ||||
|     `);
 | ||||
| 
 | ||||
|     const exitCode = main(['-p', basePath], errorSpy); | ||||
|     expect(errorSpy).not.toHaveBeenCalled(); | ||||
|     expect(exitCode).toBe(0); | ||||
| 
 | ||||
|     const jsContents = getContents('test.js'); | ||||
|     expect(jsContents).toContain('return new TestPipe(i0.ɵdirectiveInject(Dep));'); | ||||
|   }); | ||||
| 
 | ||||
|   it('should include @Pipes in @NgModule scopes', () => { | ||||
|     writeConfig(); | ||||
|     write('test.ts', ` | ||||
|         import {Component, NgModule, Pipe} from '@angular/core'; | ||||
| 
 | ||||
|         @Pipe({name: 'test'}) | ||||
|         export class TestPipe {} | ||||
| 
 | ||||
|         @Component({selector: 'test-cmp', template: '{{value | test}}'}) | ||||
|         export class TestCmp {} | ||||
| 
 | ||||
|         @NgModule({declarations: [TestPipe, TestCmp]}) | ||||
|         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('pipes: [TestPipe]'); | ||||
| 
 | ||||
|     const dtsContents = getContents('test.d.ts'); | ||||
|     expect(dtsContents).toContain('i0.NgModuleDef<TestModule, [TestPipe,TestCmp], [], []>'); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| @ -886,6 +886,6 @@ export function parseTemplate( | ||||
|  */ | ||||
| export function makeBindingParser(): BindingParser { | ||||
|   return new BindingParser( | ||||
|       new Parser(new Lexer()), DEFAULT_INTERPOLATION_CONFIG, new DomElementSchemaRegistry(), [], | ||||
|       new Parser(new Lexer()), DEFAULT_INTERPOLATION_CONFIG, new DomElementSchemaRegistry(), null, | ||||
|       []); | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user