diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts b/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts index 365d1ce33a..06a208291e 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts @@ -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 { - constructor(private reflector: ReflectionHost, private isCore: boolean) {} +export class PipeDecoratorHandler implements DecoratorHandler { + 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 { + analyze(clazz: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput { + 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, }; } } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/selector_scope.ts b/packages/compiler-cli/src/ngtsc/annotations/src/selector_scope.ts index 0c067fb5f3..06778fbc10 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/selector_scope.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/selector_scope.ts @@ -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. diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index c1990f974b..d2e5731ab3 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -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); diff --git a/packages/compiler-cli/test/ngtsc/fake_core/index.ts b/packages/compiler-cli/test/ngtsc/fake_core/index.ts index a87052c3a1..beceb52459 100644 --- a/packages/compiler-cli/test/ngtsc/fake_core/index.ts +++ b/packages/compiler-cli/test/ngtsc/fake_core/index.ts @@ -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(); diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index e280dd90b2..48e230e17e 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -243,4 +243,80 @@ describe('ngtsc behavioral tests', () => { .toContain('static ngModuleDef: i0.NgModuleDef'); 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;'); + }); + + 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'); + }); }); diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 2a2c92253b..e2b3bb064a 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -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, []); }