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…
Reference in New Issue