feat(compiler-cli): partial compilation of directives (#39518)

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
This commit is contained in:
JoostK 2020-10-30 23:45:15 +01:00 committed by Joey Perrott
parent ded7beed32
commit 8c0a92bb45
13 changed files with 363 additions and 15 deletions

View File

@ -1,3 +1,6 @@
/** @codeGenApi */
export declare function $ngDeclareDirective(decl: unknown): unknown;
export declare interface AbstractType<T> extends Function {
prototype: T;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,8 @@ export class Identifiers {
static TRANSFORM_METHOD = 'transform';
static PATCH_DEPS = 'patchedDeps';
static core: o.ExternalReference = {name: null, moduleName: CORE};
/* Instructions */
static namespaceHTML: o.ExternalReference = {name: 'ɵɵnamespaceHTML', moduleName: CORE};
@ -245,10 +247,8 @@ export class Identifiers {
moduleName: CORE,
};
static defineDirective: o.ExternalReference = {
name: 'ɵɵdefineDirective',
moduleName: CORE,
};
static defineDirective: o.ExternalReference = {name: 'ɵɵdefineDirective', moduleName: CORE};
static declareDirective: o.ExternalReference = {name: '$ngDeclareDirective', moduleName: CORE};
static DirectiveDefWithMeta: o.ExternalReference = {
name: 'ɵɵDirectiveDefWithMeta',

View File

@ -86,12 +86,14 @@ export interface R3DirectiveMetadata {
};
/**
* A mapping of input field names to the property names.
* 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: {[field: string]: string|[string, string]};
/**
* A mapping of output field names to the property names.
* 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: {[field: string]: string};

View File

@ -492,7 +492,7 @@ function stringArrayAsType(arr: ReadonlyArray<string|null>): o.Type {
o.NONE_TYPE;
}
function createDirectiveTypeParams(meta: R3DirectiveMetadata): o.Type[] {
export function createDirectiveTypeParams(meta: R3DirectiveMetadata): o.Type[] {
// On the type side, remove newlines from the selector as it will need to fit into a TypeScript
// string literal, which must be on one line.
const selectorForType = meta.selector !== null ? meta.selector.replace(/\n/g, '') : null;

View File

@ -77,6 +77,8 @@ export {
NG_PIPE_DEF as ɵNG_PIPE_DEF,
} from './render3/fields';
export {
$ngDeclareDirective,
AttributeMarker as ɵAttributeMarker,
ComponentDef as ɵComponentDef,
ComponentFactory as ɵRender3ComponentFactory,

View File

@ -133,6 +133,9 @@ export {
AttributeMarker
} from './interfaces/node';
export {CssSelectorList, ProjectionSlots} from './interfaces/projection';
export {
$ngDeclareDirective,
} from './jit/partial';
export {
setClassMetadata,
} from './metadata';

View File

@ -10,6 +10,7 @@ import {ɵɵinject, ɵɵinvalidFactoryDep} from '../../di/injector_compatibility
import {ɵɵdefineInjectable, ɵɵdefineInjector} from '../../di/interface/defs';
import * as sanitization from '../../sanitization/sanitization';
import * as r3 from '../index';
import * as partial from './partial';
@ -169,4 +170,6 @@ export const angularCoreEnv: {[name: string]: Function} =
'ɵɵtrustConstantHtml': sanitization.ɵɵtrustConstantHtml,
'ɵɵtrustConstantScript': sanitization.ɵɵtrustConstantScript,
'ɵɵtrustConstantResourceUrl': sanitization.ɵɵtrustConstantResourceUrl,
'$ngDeclareDirective': partial.$ngDeclareDirective,
}))();

View File

@ -0,0 +1,16 @@
/**
* @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
*/
/**
* Compiles a partial directive declaration object into a full directive definition object.
*
* @codeGenApi
*/
export function $ngDeclareDirective(decl: unknown): unknown {
throw new Error('Not yet implemented');
}