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
|
* 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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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], [], []>');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
[]);
|
[]);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue