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
*/
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,
};
}
}

View File

@ -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.

View File

@ -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);

View File

@ -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();

View File

@ -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], [], []>');
});
});

View File

@ -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,
[]);
}