Skip to content

Commit

Permalink
feat(compiler-cli): partial compilation of directives (#39518)
Browse files Browse the repository at this point in the history
This commit implements partial code generation for directives, which
will be transformed by the linker plugin to fully AOT compiled code in
follow-up work.

PR Close #39518
  • Loading branch information
JoostK authored and josephperrott committed Nov 4, 2020
1 parent ded7bee commit 8c0a92b
Show file tree
Hide file tree
Showing 13 changed files with 363 additions and 15 deletions.
3 changes: 3 additions & 0 deletions goldens/public-api/core/core.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/** @codeGenApi */
export declare function $ngDeclareDirective(decl: unknown): unknown;

export declare interface AbstractType<T> extends Function {
prototype: T;
}
Expand Down
29 changes: 22 additions & 7 deletions packages/compiler-cli/src/ngtsc/annotations/src/directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {compileDirectiveFromMetadata, ConstantPool, Expression, Identifiers, makeBindingParser, ParsedHostBindings, ParseError, parseHostBindings, R3DependencyMetadata, R3DirectiveMetadata, R3FactoryTarget, R3QueryMetadata, Statement, verifyHostBindings, WrappedNodeExpr} from '@angular/compiler';
import {compileDeclareDirectiveFromMetadata, compileDirectiveFromMetadata, ConstantPool, Expression, Identifiers, makeBindingParser, ParsedHostBindings, ParseError, parseHostBindings, R3DependencyMetadata, R3DirectiveDef, R3DirectiveMetadata, R3FactoryTarget, R3QueryMetadata, Statement, verifyHostBindings, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript';

import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
Expand Down Expand Up @@ -154,19 +154,34 @@ export class DirectiveDecoratorHandler implements
compileFull(
node: ClassDeclaration, analysis: Readonly<DirectiveHandlerData>,
resolution: Readonly<unknown>, pool: ConstantPool): CompileResult[] {
const meta = analysis.meta;
const res = compileDirectiveFromMetadata(meta, pool, makeBindingParser());
const factoryRes = compileNgFactoryDefField(
{...meta, injectFn: Identifiers.directiveInject, target: R3FactoryTarget.Directive});
const def = compileDirectiveFromMetadata(analysis.meta, pool, makeBindingParser());
return this.compileDirective(analysis, def);
}

compilePartial(
node: ClassDeclaration, analysis: Readonly<DirectiveHandlerData>,
resolution: Readonly<unknown>): CompileResult[] {
const def = compileDeclareDirectiveFromMetadata(analysis.meta);
return this.compileDirective(analysis, def);
}

private compileDirective(
analysis: Readonly<DirectiveHandlerData>,
{expression: initializer, type}: R3DirectiveDef): CompileResult[] {
const factoryRes = compileNgFactoryDefField({
...analysis.meta,
injectFn: Identifiers.directiveInject,
target: R3FactoryTarget.Directive,
});
if (analysis.metadataStmt !== null) {
factoryRes.statements.push(analysis.metadataStmt);
}
return [
factoryRes, {
name: 'ɵdir',
initializer: res.expression,
initializer,
statements: [],
type: res.type,
type,
}
];
}
Expand Down
5 changes: 4 additions & 1 deletion packages/compiler-cli/src/ngtsc/translator/src/translator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,10 @@ export class ExpressionTranslatorVisitor<TStatement, TExpression> implements o.E

visitExternalExpr(ast: o.ExternalExpr, _context: Context): TExpression {
if (ast.value.name === null) {
throw new Error(`Import unknown module or symbol ${ast.value}`);
if (ast.value.moduleName === null) {
throw new Error('Invalid import without name nor moduleName');
}
return this.imports.generateNamespaceImport(ast.value.moduleName);
}
// If a moduleName is specified, this is a normal import. If there's no module name, it's a
// reference to a global/ambient symbol.
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 @@ -102,6 +102,7 @@ export {compilePipeFromMetadata, R3PipeMetadata} from './render3/r3_pipe_compile
export {makeBindingParser, ParsedTemplate, parseTemplate, ParseTemplateOptions} from './render3/view/template';
export {R3Reference} from './render3/util';
export {compileComponentFromMetadata, compileDirectiveFromMetadata, parseHostBindings, ParsedHostBindings, verifyHostBindings} from './render3/view/compiler';
export {compileDeclareDirectiveFromMetadata} from './render3/partial/directive';
export {publishFacade} from './jit_compiler_facade';
// This file only reexports content of the `src` folder. Keep it that way.

Expand Down
157 changes: 157 additions & 0 deletions packages/compiler/src/render3/partial/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as o from '../../output/output_ast';

/**
* This interface describes the shape of the object that partial directive declarations are compiled
* into. This serves only as documentation, as conformance of this interface is not enforced during
* the generation of the partial declaration, nor when the linker applies full compilation from the
* partial declaration.
*/
export interface R3DeclareDirectiveMetadata {
/**
* Version number of the metadata format. This is used to evolve the metadata
* interface later - the linker will be able to detect which version a library
* is using and interpret its metadata accordingly.
*/
version: 1;

/**
* Unparsed selector of the directive.
*/
selector?: string;

/**
* Reference to the directive class itself.
*/
type: o.Expression;

/**
* A mapping of inputs from class property names to binding property names, or to a tuple of
* binding property name and class property name if the names are different.
*/
inputs?: {[classPropertyName: string]: string|[string, string]};

/**
* A mapping of outputs from class property names to binding property names, or to a tuple of
* binding property name and class property name if the names are different.
*/
outputs?: {[classPropertyName: string]: string};

/**
* Information about host bindings present on the component.
*/
host?: {
/**
* A mapping of attribute names to their value expression.
*/
attributes?: {[key: string]: o.Expression};

/**
* A mapping of event names to their unparsed event handler expression.
*/
listeners: {[key: string]: string};

/**
* A mapping of bound properties to their unparsed binding expression.
*/
properties?: {[key: string]: string};

/**
* The value of the class attribute, if present. This is stored outside of `attributes` as its
* string value must be known statically.
*/
classAttribute?: string;

/**
* The value of the style attribute, if present. This is stored outside of `attributes` as its
* string value must be known statically.
*/
styleAttribute?: string;
};

/**
* Information about the content queries made by the directive.
*/
queries?: R3DeclareQueryMetadata[];

/**
* Information about the view queries made by the directive.
*/
viewQueries?: R3DeclareQueryMetadata[];

/**
* The list of providers provided by the directive.
*/
providers?: o.Expression;

/**
* The names by which the directive is exported.
*/
exportAs?: string[];

/**
* Whether the directive has an inheritance clause. Defaults to false.
*/
usesInheritance?: boolean;

/**
* Whether the directive implements the `ngOnChanges` hook. Defaults to false.
*/
usesOnChanges?: boolean;

/**
* A reference to the `@angular/core` ES module, which allows access
* to all Angular exports, including Ivy instructions.
*/
ngImport: o.Expression;
}

export interface R3DeclareQueryMetadata {
/**
* Name of the property on the class to update with query results.
*/
propertyName: string;

/**
* Whether to read only the first matching result, or an array of results. Defaults to false.
*/
first?: boolean;

/**
* Either an expression representing a type or `InjectionToken` for the query
* predicate, or a set of string selectors.
*/
predicate: o.Expression|string[];

/**
* Whether to include only direct children or all descendants. Defaults to false.
*/
descendants?: boolean;

/**
* An expression representing a type to read from each matched node, or null if the default value
* for a given node is to be returned.
*/
read?: o.Expression;

/**
* Whether or not this query should collect only static results. Defaults to false.
*
* If static is true, the query's results will be set on the component after nodes are created,
* but before change detection runs. This means that any results that relied upon change detection
* to run (e.g. results inside *ngIf or *ngFor views) will not be collected. Query results are
* available in the ngOnInit hook.
*
* If static is false, the query's results will be set on the component after change detection
* runs. This means that the query results can contain nodes inside *ngIf or *ngFor views, but
* the results will not be available in the ngOnInit hook (only in the ngAfterContentInit for
* content hooks and ngAfterViewInit for view hooks).
*/
static?: boolean;
}
143 changes: 143 additions & 0 deletions packages/compiler/src/render3/partial/directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as o from '../../output/output_ast';
import {Identifiers as R3} from '../r3_identifiers';
import {R3DirectiveDef, R3DirectiveMetadata, R3HostMetadata, R3QueryMetadata} from '../view/api';
import {createDirectiveTypeParams} from '../view/compiler';
import {asLiteral, conditionallyCreateMapObjectLiteral, DefinitionMap} from '../view/util';


/**
* Compile a directive declaration defined by the `R3DirectiveMetadata`.
*/
export function compileDeclareDirectiveFromMetadata(meta: R3DirectiveMetadata): R3DirectiveDef {
const definitionMap = createDirectiveDefinitionMap(meta);

const expression = o.importExpr(R3.declareDirective).callFn([definitionMap.toLiteralMap()]);

const typeParams = createDirectiveTypeParams(meta);
const type = o.expressionType(o.importExpr(R3.DirectiveDefWithMeta, typeParams));

return {expression, type};
}

/**
* Gathers the declaration fields for a directive into a `DefinitionMap`. This allows for reusing
* this logic for components, as they extend the directive metadata.
*/
export function createDirectiveDefinitionMap(meta: R3DirectiveMetadata): DefinitionMap {
const definitionMap = new DefinitionMap();

definitionMap.set('version', o.literal(1));

// e.g. `type: MyDirective`
definitionMap.set('type', meta.internalType);

// e.g. `selector: 'some-dir'`
if (meta.selector !== null) {
definitionMap.set('selector', o.literal(meta.selector));
}

definitionMap.set('inputs', conditionallyCreateMapObjectLiteral(meta.inputs, true));
definitionMap.set('outputs', conditionallyCreateMapObjectLiteral(meta.outputs));

definitionMap.set('host', compileHostMetadata(meta.host));

definitionMap.set('providers', meta.providers);

if (meta.queries.length > 0) {
definitionMap.set('queries', o.literalArr(meta.queries.map(compileQuery)));
}
if (meta.viewQueries.length > 0) {
definitionMap.set('viewQueries', o.literalArr(meta.viewQueries.map(compileQuery)));
}

if (meta.exportAs !== null) {
definitionMap.set('exportAs', asLiteral(meta.exportAs));
}

if (meta.usesInheritance) {
definitionMap.set('usesInheritance', o.literal(true));
}
if (meta.lifecycle.usesOnChanges) {
definitionMap.set('usesOnChanges', o.literal(true));
}

definitionMap.set('ngImport', o.importExpr(R3.core));

return definitionMap;
}

/**
* Compiles the metadata of a single query into its partial declaration form as declared
* by `R3DeclareQueryMetadata`.
*/
function compileQuery(query: R3QueryMetadata): o.LiteralMapExpr {
const meta = new DefinitionMap();
meta.set('propertyName', o.literal(query.propertyName));
if (query.first) {
meta.set('first', o.literal(true));
}
meta.set(
'predicate', Array.isArray(query.predicate) ? asLiteral(query.predicate) : query.predicate);
if (query.descendants) {
meta.set('descendants', o.literal(true));
}
meta.set('read', query.read);
if (query.static) {
meta.set('static', o.literal(true));
}
return meta.toLiteralMap();
}

/**
* Compiles the host metadata into its partial declaration form as declared
* in `R3DeclareDirectiveMetadata['host']`
*/
function compileHostMetadata(meta: R3HostMetadata): o.LiteralMapExpr|null {
const hostMetadata = new DefinitionMap();
hostMetadata.set('attributes', toOptionalLiteralMap(meta.attributes, expression => expression));
hostMetadata.set('listeners', toOptionalLiteralMap(meta.listeners, o.literal));
hostMetadata.set('properties', toOptionalLiteralMap(meta.properties, o.literal));

if (meta.specialAttributes.styleAttr) {
hostMetadata.set('styleAttribute', o.literal(meta.specialAttributes.styleAttr));
}
if (meta.specialAttributes.classAttr) {
hostMetadata.set('classAttribute', o.literal(meta.specialAttributes.classAttr));
}

if (hostMetadata.values.length > 0) {
return hostMetadata.toLiteralMap();
} else {
return null;
}
}

/**
* Creates an object literal expression from the given object, mapping all values to an expression
* using the provided mapping function. If the object has no keys, then null is returned.
*
* @param object The object to transfer into an object literal expression.
* @param mapper The logic to use for creating an expression for the object's values.
* @returns An object literal expression representing `object`, or null if `object` does not have
* any keys.
*/
function toOptionalLiteralMap<T>(
object: {[key: string]: T}, mapper: (value: T) => o.Expression): o.LiteralMapExpr|null {
const entries = Object.keys(object).map(key => {
const value = object[key];
return {key, value: mapper(value), quoted: true};
});

if (entries.length > 0) {
return o.literalMap(entries);
} else {
return null;
}
}

0 comments on commit 8c0a92b

Please sign in to comment.