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:
Alex Rickabaugh 2018-06-26 10:44:22 -07:00 committed by Miško Hevery
parent 3d52174bf1
commit b6af8700ce
6 changed files with 175 additions and 15 deletions

View File

@ -6,34 +6,76 @@
* found in the LICENSE file at https://angular.io/license * 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 * as ts from 'typescript';
import {Decorator, ReflectionHost} from '../../host'; import {Decorator, ReflectionHost} from '../../host';
import {reflectObjectLiteral, staticallyResolve} from '../../metadata';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform'; 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> { export class PipeDecoratorHandler implements DecoratorHandler<R3PipeMetadata> {
constructor(private reflector: ReflectionHost, private isCore: boolean) {} constructor(
private checker: ts.TypeChecker, private reflector: ReflectionHost,
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {}
detect(decorator: Decorator[]): Decorator|undefined { detect(decorator: Decorator[]): Decorator|undefined {
return decorator.find( return decorator.find(
decorator => decorator.name === 'Pipe' && (this.isCore || isAngularCore(decorator))); 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 { 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 { return {
name: 'ngPipeDef', name: 'ngPipeDef',
initializer: new LiteralExpr(null), initializer: res.expression,
statements: [], statements: [],
type: new ExpressionType(new LiteralExpr(null)), type: res.type,
}; };
} }
} }

View File

@ -118,7 +118,11 @@ export class SelectorScopeRegistry {
/** /**
* Register the name of a pipe with the registry. * 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 * 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 // The initial value of ngModuleImportedFrom is 'null' which signifies that the NgModule
// was not imported from a .d.ts source. // was not imported from a .d.ts source.
this.lookupScopes(module !, /* ngModuleImportedFrom */ null).compilation.forEach(ref => { 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. // Only directives/components with selectors get added to the scope.
if (selector != null) { if (selector != null) {
directives.set(selector, ref); 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 { private lookupPipeName(node: ts.Declaration): string|null {
return this._pipeToName.get(node); 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; 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 * Process a `TypeNode` which is a tuple of references to other types, and return `Reference`s to
* them. * them.

View File

@ -154,7 +154,7 @@ export class NgtscProgram implements api.Program {
new DirectiveDecoratorHandler(checker, this.reflector, scopeRegistry, this.isCore), new DirectiveDecoratorHandler(checker, this.reflector, scopeRegistry, this.isCore),
new InjectableDecoratorHandler(this.reflector, this.isCore), new InjectableDecoratorHandler(this.reflector, this.isCore),
new NgModuleDecoratorHandler(checker, this.reflector, scopeRegistry, 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); return new IvyCompilation(handlers, checker, this.reflector, this.coreImportsFrom);

View File

@ -20,6 +20,7 @@ export const Component = callableClassDecorator();
export const Directive = callableClassDecorator(); export const Directive = callableClassDecorator();
export const Injectable = callableClassDecorator(); export const Injectable = callableClassDecorator();
export const NgModule = callableClassDecorator(); export const NgModule = callableClassDecorator();
export const Pipe = callableClassDecorator();
export const Inject = callableParamDecorator(); export const Inject = callableParamDecorator();
export const Self = callableParamDecorator(); export const Self = callableParamDecorator();

View File

@ -243,4 +243,80 @@ describe('ngtsc behavioral tests', () => {
.toContain('static ngModuleDef: i0.NgModuleDef<TestModule, [TestCmp], [OtherModule], []>'); .toContain('static ngModuleDef: i0.NgModuleDef<TestModule, [TestCmp], [OtherModule], []>');
expect(dtsContents).toContain('static ngInjectorDef: i0.InjectorDef'); 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], [], []>');
});
}); });

View File

@ -886,6 +886,6 @@ export function parseTemplate(
*/ */
export function makeBindingParser(): BindingParser { export function makeBindingParser(): BindingParser {
return new BindingParser( return new BindingParser(
new Parser(new Lexer()), DEFAULT_INTERPOLATION_CONFIG, new DomElementSchemaRegistry(), [], new Parser(new Lexer()), DEFAULT_INTERPOLATION_CONFIG, new DomElementSchemaRegistry(), null,
[]); []);
} }