Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 51 additions & 9 deletions packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

@cexbrayat cexbrayat Jul 3, 2018 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alxhub Dumb question as I'm probably missing something but shouldn't that be let pure = true by default as it is currently and in Ivy JiT mode?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opened PR #24750 in case the pipes should indeed by pure by default.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you.

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,
};
}
}
49 changes: 45 additions & 4 deletions packages/compiler-cli/src/ngtsc/annotations/src/selector_scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
});

Expand Down Expand Up @@ -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);
}
}

/**
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler-cli/src/ngtsc/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions packages/compiler-cli/test/ngtsc/fake_core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
76 changes: 76 additions & 0 deletions packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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], [], []>');
});
});
2 changes: 1 addition & 1 deletion packages/compiler/src/aot/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {OutputEmitter} from '../output/abstract_emitter';
import * as o from '../output/output_ast';
import {ParseError} from '../parse_util';
import {compileNgModuleFromRender2 as compileR3Module} from '../render3/r3_module_compiler';
import {compilePipe as compileR3Pipe} from '../render3/r3_pipe_compiler';
import {compilePipeFromRender2 as compileR3Pipe} from '../render3/r3_pipe_compiler';
import {htmlAstToRender3Ast} from '../render3/r3_template_transform';
import {compileComponentFromRender2 as compileR3Component, compileDirectiveFromRender2 as compileR3Directive} from '../render3/view/compiler';
import {DomElementSchemaRegistry} from '../schema/dom_element_schema_registry';
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export * from './render3/view/api';
export {jitExpression} from './render3/r3_jit';
export {R3DependencyMetadata, R3FactoryMetadata, R3ResolvedDependencyType} from './render3/r3_factory';
export {compileInjector, compileNgModule, R3InjectorMetadata, R3NgModuleMetadata} from './render3/r3_module_compiler';
export {compilePipeFromMetadata, R3PipeMetadata} from './render3/r3_pipe_compiler';
export {makeBindingParser, parseTemplate} from './render3/view/template';
export {compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings} from './render3/view/compiler';
// This file only reexports content of the `src` folder. Keep it that way.
2 changes: 2 additions & 0 deletions packages/compiler/src/render3/r3_identifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ export class Identifiers {

static defineNgModule: o.ExternalReference = {name: 'ɵdefineNgModule', moduleName: CORE};

static PipeDef: o.ExternalReference = {name: 'ɵPipeDef', moduleName: CORE};

static definePipe: o.ExternalReference = {name: 'ɵdefinePipe', moduleName: CORE};

static query: o.ExternalReference = {name: 'ɵQ', moduleName: CORE};
Expand Down
73 changes: 51 additions & 22 deletions packages/compiler/src/render3/r3_pipe_compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,55 +12,84 @@ import {DefinitionKind} from '../constant_pool';
import * as o from '../output/output_ast';
import {OutputContext, error} from '../util';

import {compileFactoryFunction, dependenciesFromGlobalMetadata} from './r3_factory';
import {R3DependencyMetadata, compileFactoryFunction, dependenciesFromGlobalMetadata} from './r3_factory';
import {Identifiers as R3} from './r3_identifiers';

export interface R3PipeMetadata {
name: string;
type: o.Expression;
pipeName: string;
deps: R3DependencyMetadata[];
pure: boolean;
}

/**
* Write a pipe definition to the output context.
*/
export function compilePipe(
outputCtx: OutputContext, pipe: CompilePipeMetadata, reflector: CompileReflector) {
export interface R3PipeDef {
expression: o.Expression;
type: o.Type;
}

export function compilePipeFromMetadata(metadata: R3PipeMetadata) {
const definitionMapValues: {key: string, quoted: boolean, value: o.Expression}[] = [];

// e.g. `name: 'myPipe'`
definitionMapValues.push({key: 'name', value: o.literal(pipe.name), quoted: false});
definitionMapValues.push({key: 'name', value: o.literal(metadata.pipeName), quoted: false});

// e.g. `type: MyPipe`
definitionMapValues.push(
{key: 'type', value: outputCtx.importExpr(pipe.type.reference), quoted: false});
definitionMapValues.push({key: 'type', value: metadata.type, quoted: false});

// e.g. `factory: function MyPipe_Factory() { return new MyPipe(); }`
const deps = dependenciesFromGlobalMetadata(pipe.type, outputCtx, reflector);
const templateFactory = compileFactoryFunction({
name: identifierName(pipe.type) !,
fnOrClass: outputCtx.importExpr(pipe.type.reference), deps,
name: metadata.name,
fnOrClass: metadata.type,
deps: metadata.deps,
useNew: true,
injectFn: R3.directiveInject,
});
definitionMapValues.push({key: 'factory', value: templateFactory, quoted: false});

// e.g. `pure: true`
if (pipe.pure) {
definitionMapValues.push({key: 'pure', value: o.literal(true), quoted: false});
definitionMapValues.push({key: 'pure', value: o.literal(metadata.pure), quoted: false});

const expression = o.importExpr(R3.definePipe).callFn([o.literalMap(definitionMapValues)]);
const type = new o.ExpressionType(o.importExpr(R3.PipeDef, [
new o.ExpressionType(metadata.type),
new o.ExpressionType(new o.LiteralExpr(metadata.pipeName)),
]));
return {expression, type};
}

/**
* Write a pipe definition to the output context.
*/
export function compilePipeFromRender2(
outputCtx: OutputContext, pipe: CompilePipeMetadata, reflector: CompileReflector) {
const definitionMapValues: {key: string, quoted: boolean, value: o.Expression}[] = [];

const name = identifierName(pipe.type);
if (!name) {
return error(`Cannot resolve the name of ${pipe.type}`);
}

const className = identifierName(pipe.type) !;
className || error(`Cannot resolve the name of ${pipe.type}`);
const metadata: R3PipeMetadata = {
name,
pipeName: pipe.name,
type: outputCtx.importExpr(pipe.type.reference),
deps: dependenciesFromGlobalMetadata(pipe.type, outputCtx, reflector),
pure: pipe.pure,
};

const res = compilePipeFromMetadata(metadata);

const definitionField = outputCtx.constantPool.propertyNameOf(DefinitionKind.Pipe);
const definitionFunction =
o.importExpr(R3.definePipe).callFn([o.literalMap(definitionMapValues)]);

outputCtx.statements.push(new o.ClassStmt(
/* name */ className,
/* name */ name,
/* parent */ null,
/* fields */[new o.ClassField(
/* name */ definitionField,
/* type */ o.INFERRED_TYPE,
/* modifiers */[o.StmtModifier.Static],
/* initializer */ definitionFunction)],
/* initializer */ res.expression)],
/* getters */[],
/* constructorMethod */ new o.ClassMethod(null, [], []),
/* methods */[]));
}
}
Loading