feat(compiler-cli): JIT compilation of directive declarations (#40101)

The `ɵɵngDeclareDirective` calls are designed to be translated to fully
AOT compiled code during a build transform, but in cases this is not
done it is still possible to compile the declaration object in the
browser using the JIT compiler. This commit adds a runtime
implementation of `ɵɵngDeclareDirective` which invokes the JIT compiler
using the declaration object, such that a compiled directive definition
is made available to the Ivy runtime.

PR Close #40101
This commit is contained in:
JoostK 2020-12-13 20:25:20 +01:00 committed by Joey Perrott
parent e54261b8d8
commit 9186f1feea
11 changed files with 486 additions and 16 deletions

View File

@ -37,6 +37,9 @@ export interface CompilerFacade {
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3NgModuleMetadataFacade): any;
compileDirective(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3DirectiveMetadataFacade): any;
compileDirectiveDeclaration(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string,
declaration: R3DeclareDirectiveFacade): any;
compileComponent(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3ComponentMetadataFacade): any;
compileFactory(
@ -162,6 +165,28 @@ export interface R3ComponentMetadataFacade extends R3DirectiveMetadataFacade {
changeDetection?: ChangeDetectionStrategy;
}
export type OpaqueValue = unknown;
export interface R3DeclareDirectiveFacade {
selector?: string;
type: Function;
inputs?: {[classPropertyName: string]: string|[string, string]};
outputs?: {[classPropertyName: string]: string};
host?: {
attributes?: {[key: string]: OpaqueValue};
listeners?: {[key: string]: string};
properties?: {[key: string]: string};
classAttribute?: string;
styleAttribute?: string;
};
queries?: R3DeclareQueryMetadataFacade[];
viewQueries?: R3DeclareQueryMetadataFacade[];
providers?: OpaqueValue;
exportAs?: string[];
usesInheritance?: boolean;
usesOnChanges?: boolean;
}
export interface R3UsedDirectiveMetadata {
selector: string;
inputs: string[];
@ -197,6 +222,15 @@ export interface R3QueryMetadataFacade {
static: boolean;
}
export interface R3DeclareQueryMetadataFacade {
propertyName: string;
first?: boolean;
predicate: OpaqueValue|string[];
descendants?: boolean;
read?: OpaqueValue;
static?: boolean;
}
export interface ParseSourceSpan {
start: any;
end: any;

View File

@ -7,7 +7,7 @@
*/
import {CompilerFacade, CoreEnvironment, ExportedCompilerFacade, R3ComponentMetadataFacade, R3DependencyMetadataFacade, R3DirectiveMetadataFacade, R3FactoryDefMetadataFacade, R3InjectableMetadataFacade, R3InjectorMetadataFacade, R3NgModuleMetadataFacade, R3PipeMetadataFacade, R3QueryMetadataFacade, StringMap, StringMapWithRename} from './compiler_facade_interface';
import {CompilerFacade, CoreEnvironment, ExportedCompilerFacade, OpaqueValue, R3ComponentMetadataFacade, R3DeclareDirectiveFacade, R3DeclareQueryMetadataFacade, R3DependencyMetadataFacade, R3DirectiveMetadataFacade, R3FactoryDefMetadataFacade, R3InjectableMetadataFacade, R3InjectorMetadataFacade, R3NgModuleMetadataFacade, R3PipeMetadataFacade, R3QueryMetadataFacade, StringMap, StringMapWithRename} from './compiler_facade_interface';
import {ConstantPool} from './constant_pool';
import {HostBinding, HostListener, Input, Output, Type} from './core';
import {Identifiers} from './identifiers';
@ -21,7 +21,7 @@ import {R3JitReflector} from './render3/r3_jit';
import {compileInjector, compileNgModule, R3InjectorMetadata, R3NgModuleMetadata} from './render3/r3_module_compiler';
import {compilePipeFromMetadata, R3PipeMetadata} from './render3/r3_pipe_compiler';
import {R3Reference} from './render3/util';
import {R3ComponentMetadata, R3DirectiveMetadata, R3QueryMetadata} from './render3/view/api';
import {R3ComponentMetadata, R3DirectiveMetadata, R3HostMetadata, R3QueryMetadata} from './render3/view/api';
import {compileComponentFromMetadata, compileDirectiveFromMetadata, ParsedHostBindings, parseHostBindings, verifyHostBindings} from './render3/view/compiler';
import {makeBindingParser, parseTemplate} from './render3/view/template';
import {ResourceLoader} from './resource_loader';
@ -107,10 +107,23 @@ export class CompilerFacadeImpl implements CompilerFacade {
compileDirective(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string,
facade: R3DirectiveMetadataFacade): any {
const meta: R3DirectiveMetadata = convertDirectiveFacadeToMetadata(facade);
return this.compileDirectiveFromMeta(angularCoreEnv, sourceMapUrl, meta);
}
compileDirectiveDeclaration(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string,
declaration: R3DeclareDirectiveFacade): any {
const typeSourceSpan =
this.createParseSourceSpan('Directive', declaration.type.name, sourceMapUrl);
const meta = convertDeclareDirectiveFacadeToMetadata(declaration, typeSourceSpan);
return this.compileDirectiveFromMeta(angularCoreEnv, sourceMapUrl, meta);
}
private compileDirectiveFromMeta(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3DirectiveMetadata): any {
const constantPool = new ConstantPool();
const bindingParser = makeBindingParser();
const meta: R3DirectiveMetadata = convertDirectiveFacadeToMetadata(facade);
const res = compileDirectiveFromMetadata(meta, constantPool, bindingParser);
return this.jitExpression(
res.expression, angularCoreEnv, sourceMapUrl, constantPool.statements);
@ -230,6 +243,19 @@ function convertToR3QueryMetadata(facade: R3QueryMetadataFacade): R3QueryMetadat
};
}
function convertQueryDeclarationToMetadata(declaration: R3DeclareQueryMetadataFacade):
R3QueryMetadata {
return {
propertyName: declaration.propertyName,
first: declaration.first ?? false,
predicate: Array.isArray(declaration.predicate) ? declaration.predicate :
new WrappedNodeExpr(declaration.predicate),
descendants: declaration.descendants ?? false,
read: declaration.read ? new WrappedNodeExpr(declaration.read) : null,
static: declaration.static ?? false,
};
}
function convertDirectiveFacadeToMetadata(facade: R3DirectiveMetadataFacade): R3DirectiveMetadata {
const inputsFromMetadata = parseInputOutputs(facade.inputs || []);
const outputsFromMetadata = parseInputOutputs(facade.outputs || []);
@ -265,6 +291,52 @@ function convertDirectiveFacadeToMetadata(facade: R3DirectiveMetadataFacade): R3
};
}
function convertDeclareDirectiveFacadeToMetadata(
declaration: R3DeclareDirectiveFacade, typeSourceSpan: ParseSourceSpan): R3DirectiveMetadata {
return {
name: declaration.type.name,
type: wrapReference(declaration.type),
typeSourceSpan,
internalType: new WrappedNodeExpr(declaration.type),
selector: declaration.selector ?? null,
inputs: declaration.inputs ?? {},
outputs: declaration.outputs ?? {},
host: convertHostDeclarationToMetadata(declaration.host),
queries: (declaration.queries ?? []).map(convertQueryDeclarationToMetadata),
viewQueries: (declaration.viewQueries ?? []).map(convertQueryDeclarationToMetadata),
providers: declaration.providers !== undefined ? new WrappedNodeExpr(declaration.providers) :
null,
exportAs: declaration.exportAs ?? null,
usesInheritance: declaration.usesInheritance ?? false,
lifecycle: {usesOnChanges: declaration.usesOnChanges ?? false},
deps: null,
typeArgumentCount: 0,
fullInheritance: false,
};
}
function convertHostDeclarationToMetadata(host: R3DeclareDirectiveFacade['host'] = {}):
R3HostMetadata {
return {
attributes: convertOpaqueValuesToExpressions(host.attributes ?? {}),
listeners: host.listeners ?? {},
properties: host.properties ?? {},
specialAttributes: {
classAttr: host.classAttribute,
styleAttr: host.styleAttribute,
},
};
}
function convertOpaqueValuesToExpressions(obj: {[key: string]: OpaqueValue}):
{[key: string]: WrappedNodeExpr<unknown>} {
const result: {[key: string]: WrappedNodeExpr<unknown>} = {};
for (const key of Object.keys(obj)) {
result[key] = new WrappedNodeExpr(obj[key]);
}
return result;
}
// This seems to be needed to placate TS v3.0 only
type R3DirectiveMetadataFacadeNoPropAndWhitespace =
Pick<R3DirectiveMetadataFacade, Exclude<keyof R3DirectiveMetadataFacade, 'propMetadata'>>;

View File

@ -46,8 +46,7 @@ export interface R3DeclareDirectiveMetadata extends R3PartialDeclaration {
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.
* A mapping of outputs from class property names to binding property names.
*/
outputs?: {[classPropertyName: string]: string};

View File

@ -98,17 +98,24 @@ function mapToExpression(
let declaredName: string;
let publicName: string;
let minifiedName: string;
let needsDeclaredName: boolean;
if (Array.isArray(value)) {
[publicName, declaredName] = value;
minifiedName = key;
needsDeclaredName = publicName !== declaredName;
} else {
[declaredName, publicName] = splitAtColon(key, [key, value]);
minifiedName = declaredName;
// Only include the declared name if extracted from the key, i.e. the key contains a colon.
// Otherwise the declared name should be omitted even if it is different from the public name,
// as it may have already been minified.
needsDeclaredName = publicName !== declaredName && key.includes(':');
}
minifiedName = declaredName;
return {
key: minifiedName,
// put quotes around keys that contain potentially unsafe characters
quoted: UNSAFE_OBJECT_KEY_NAME_REGEXP.test(minifiedName),
value: (keepDeclared && publicName !== declaredName) ?
value: (keepDeclared && needsDeclaredName) ?
o.literalArr([asLiteral(publicName), asLiteral(declaredName)]) :
asLiteral(publicName)
};

View File

@ -103,6 +103,11 @@ const coreR3ComponentMetadataFacade: core.R3ComponentMetadataFacade =
const compilerR3ComponentMetadataFacade: compiler.R3ComponentMetadataFacade =
null! as core.R3ComponentMetadataFacade;
const coreR3DeclareDirectiveFacade: core.R3DeclareDirectiveFacade =
null! as compiler.R3DeclareDirectiveFacade;
const compilerR3DeclareDirectiveFacade: compiler.R3DeclareDirectiveFacade =
null! as core.R3DeclareDirectiveFacade;
const coreViewEncapsulation: core.ViewEncapsulation = null! as compiler.ViewEncapsulation;
const compilerViewEncapsulation: compiler.ViewEncapsulation = null! as core.ViewEncapsulation;
@ -110,3 +115,8 @@ const coreR3QueryMetadataFacade: core.R3QueryMetadataFacade =
null! as compiler.R3QueryMetadataFacade;
const compilerR3QueryMetadataFacade: compiler.R3QueryMetadataFacade =
null! as core.R3QueryMetadataFacade;
const coreR3DeclareQueryMetadataFacade: core.R3DeclareQueryMetadataFacade =
null! as compiler.R3DeclareQueryMetadataFacade;
const compilerR3DeclareQueryMetadataFacade: compiler.R3DeclareQueryMetadataFacade =
null! as core.R3DeclareQueryMetadataFacade;

View File

@ -37,6 +37,9 @@ export interface CompilerFacade {
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3NgModuleMetadataFacade): any;
compileDirective(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3DirectiveMetadataFacade): any;
compileDirectiveDeclaration(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string,
declaration: R3DeclareDirectiveFacade): any;
compileComponent(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3ComponentMetadataFacade): any;
compileFactory(
@ -162,6 +165,28 @@ export interface R3ComponentMetadataFacade extends R3DirectiveMetadataFacade {
changeDetection?: ChangeDetectionStrategy;
}
export type OpaqueValue = unknown;
export interface R3DeclareDirectiveFacade {
selector?: string;
type: Function;
inputs?: {[classPropertyName: string]: string|[string, string]};
outputs?: {[classPropertyName: string]: string};
host?: {
attributes?: {[key: string]: OpaqueValue};
listeners?: {[key: string]: string};
properties?: {[key: string]: string};
classAttribute?: string;
styleAttribute?: string;
};
queries?: R3DeclareQueryMetadataFacade[];
viewQueries?: R3DeclareQueryMetadataFacade[];
providers?: OpaqueValue;
exportAs?: string[];
usesInheritance?: boolean;
usesOnChanges?: boolean;
}
export interface R3UsedDirectiveMetadata {
selector: string;
inputs: string[];
@ -197,6 +222,15 @@ export interface R3QueryMetadataFacade {
static: boolean;
}
export interface R3DeclareQueryMetadataFacade {
propertyName: string;
first?: boolean;
predicate: OpaqueValue|string[];
descendants?: boolean;
read?: OpaqueValue;
static?: boolean;
}
export interface ParseSourceSpan {
start: any;
end: any;

View File

@ -165,8 +165,6 @@ export {
ɵɵnamespaceMathML,
ɵɵnamespaceSVG,
ɵɵnextContext,
ɵɵngDeclareComponent,
ɵɵngDeclareDirective,
ɵɵNgOnChangesFeature,
ɵɵpipe,
ɵɵpipeBind1,
@ -274,6 +272,10 @@ export {
resetCompiledComponents as ɵresetCompiledComponents,
transitiveScopesFor as ɵtransitiveScopesFor,
} from './render3/jit/module';
export {
ɵɵngDeclareComponent,
ɵɵngDeclareDirective,
} from './render3/jit/partial';
export {
compilePipe as ɵcompilePipe,
} from './render3/jit/pipe';

View File

@ -134,10 +134,6 @@ export {
AttributeMarker
} from './interfaces/node';
export {CssSelectorList, ProjectionSlots} from './interfaces/projection';
export {
ɵɵngDeclareComponent,
ɵɵngDeclareDirective,
} from './jit/partial';
export {
setClassMetadata,
} from './metadata';

View File

@ -6,13 +6,18 @@
* found in the LICENSE file at https://angular.io/license
*/
import {getCompilerFacade, R3DeclareDirectiveFacade} from '../../compiler/compiler_facade';
import {angularCoreEnv} from './environment';
/**
* 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');
export function ɵɵngDeclareDirective(decl: R3DeclareDirectiveFacade): unknown {
const compiler = getCompilerFacade();
return compiler.compileDirectiveDeclaration(
angularCoreEnv, `ng:///${decl.type.name}/ɵfac.js`, decl);
}
/**

View File

@ -0,0 +1,255 @@
/**
* @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 {ElementRef, ɵɵngDeclareDirective} from '@angular/core';
import {AttributeMarker, DirectiveDef} from '../../../src/render3';
import {functionContaining} from './matcher';
describe('directive declaration jit compilation', () => {
it('should compile a minimal directive declaration', () => {
const def = ɵɵngDeclareDirective({
type: TestClass,
}) as DirectiveDef<TestClass>;
expectDirectiveDef(def, {});
});
it('should compile a selector', () => {
const def =
ɵɵngDeclareDirective({type: TestClass, selector: '[dir], test'}) as DirectiveDef<TestClass>;
expectDirectiveDef(def, {
selectors: [['', 'dir', ''], ['test']],
});
});
it('should compile inputs and outputs', () => {
const def = ɵɵngDeclareDirective({
type: TestClass,
inputs: {
minifiedProperty: 'property',
minifiedClassProperty: ['bindingName', 'classProperty'],
},
outputs: {
minifiedEventName: 'eventBindingName',
},
}) as DirectiveDef<TestClass>;
expectDirectiveDef(def, {
inputs: {
'property': 'minifiedProperty',
'bindingName': 'minifiedClassProperty',
},
declaredInputs: {
'property': 'property',
'bindingName': 'classProperty',
},
outputs: {
'eventBindingName': 'minifiedEventName',
},
});
});
it('should compile exportAs', () => {
const def = ɵɵngDeclareDirective({
type: TestClass,
exportAs: ['a', 'b'],
}) as DirectiveDef<TestClass>;
expectDirectiveDef(def, {
exportAs: ['a', 'b'],
});
});
it('should compile providers', () => {
const def = ɵɵngDeclareDirective({
type: TestClass,
providers: [
{provide: 'token', useValue: 123},
],
}) as DirectiveDef<TestClass>;
expectDirectiveDef(def, {
features: [jasmine.any(Function)],
providersResolver: jasmine.any(Function),
});
});
it('should compile content queries', () => {
const def = ɵɵngDeclareDirective({
type: TestClass,
queries: [
{
propertyName: 'byRef',
predicate: ['ref'],
},
{
propertyName: 'byToken',
predicate: String,
descendants: true,
static: true,
first: true,
read: ElementRef,
}
],
}) as DirectiveDef<TestClass>;
expectDirectiveDef(def, {
contentQueries: functionContaining([
// "byRef" should use `contentQuery` with `false` for descendants flag without a read token,
// and bind to the full query result.
// NOTE: the `anonymous` match is to support IE11, as functions don't have a name there.
/(?:contentQuery|anonymous)[^(]*\(dirIndex,_c0,false\)/,
'(ctx.byRef = _t)',
// "byToken" should use `staticContentQuery` with `true` for descendants flag and
// `ElementRef` as read token, and bind to the first result in the query result.
// NOTE: the `anonymous` match is to support IE11, as functions don't have a name there.
/(?:staticContentQuery|anonymous)[^(]*\(dirIndex,[^,]*String[^,]*,true,[^)]*ElementRef[^)]*\)/,
'(ctx.byToken = _t.first)',
]),
});
});
it('should compile view queries', () => {
const def = ɵɵngDeclareDirective({
type: TestClass,
viewQueries: [
{
propertyName: 'byRef',
predicate: ['ref'],
},
{
propertyName: 'byToken',
predicate: String,
descendants: true,
static: true,
first: true,
read: ElementRef,
}
],
}) as DirectiveDef<TestClass>;
expectDirectiveDef(def, {
viewQuery: functionContaining([
// "byRef" should use `viewQuery` with `false` for descendants flag without a read token,
// and bind to the full query result.
// NOTE: the `anonymous` match is to support IE11, as functions don't have a name there.
/(?:viewQuery|anonymous)[^(]*\(_c0,false\)/,
'(ctx.byRef = _t)',
// "byToken" should use `staticViewQuery` with `true` for descendants flag and
// `ElementRef` as read token, and bind to the first result in the query result.
// NOTE: the `anonymous` match is to support IE11, as functions don't have a name there.
/(?:staticViewQuery|anonymous)[^(]*\([^,]*String[^,]*,true,[^)]*ElementRef[^)]*\)/,
'(ctx.byToken = _t.first)',
]),
});
});
it('should compile host bindings', () => {
const def = ɵɵngDeclareDirective({
type: TestClass,
host: {
attributes: {
'attr': 'value',
},
listeners: {
'event': 'handleEvent($event)',
},
properties: {
'foo': 'foo.prop',
'attr.bar': 'bar.prop',
},
classAttribute: 'foo bar',
styleAttribute: 'width: 100px;',
},
}) as DirectiveDef<TestClass>;
expectDirectiveDef(def, {
hostAttrs: [
'attr', 'value', AttributeMarker.Classes, 'foo', 'bar', AttributeMarker.Styles, 'width',
'100px'
],
hostBindings: functionContaining([
'return ctx.handleEvent($event)',
// NOTE: the `anonymous` match is to support IE11, as functions don't have a name there.
/(?:hostProperty|anonymous)[^(]*\('foo',ctx\.foo\.prop\)/,
/(?:attribute|anonymous)[^(]*\('bar',ctx\.bar\.prop\)/,
]),
hostVars: 2,
});
});
it('should compile directives with inheritance', () => {
const def = ɵɵngDeclareDirective({
type: TestClass,
usesInheritance: true,
}) as DirectiveDef<TestClass>;
expectDirectiveDef(def, {
features: [functionContaining(['ɵɵInheritDefinitionFeature'])],
});
});
it('should compile directives with onChanges lifecycle hook', () => {
const def = ɵɵngDeclareDirective({
type: TestClass,
usesOnChanges: true,
}) as DirectiveDef<TestClass>;
expectDirectiveDef(def, {
features: [functionContaining(['ɵɵNgOnChangesFeature'])],
});
});
});
type DirectiveDefExpectations = jasmine.Expected<Pick<
DirectiveDef<unknown>,
'selectors'|'inputs'|'declaredInputs'|'outputs'|'features'|'hostAttrs'|'hostBindings'|
'hostVars'|'contentQueries'|'viewQuery'|'exportAs'|'providersResolver'>>;
/**
* Asserts that the provided directive definition is according to the provided expectation.
* Definition fields for which no expectation is present are verified to be initialized to their
* default value.
*/
function expectDirectiveDef(
actual: DirectiveDef<unknown>, expected: Partial<DirectiveDefExpectations>): void {
const expectation: DirectiveDefExpectations = {
selectors: [],
inputs: {},
declaredInputs: {},
outputs: {},
features: null,
hostAttrs: null,
hostBindings: null,
hostVars: 0,
contentQueries: null,
viewQuery: null,
exportAs: null,
providersResolver: null,
...expected,
};
expect(actual.type).toBe(TestClass);
expect(actual.selectors).toEqual(expectation.selectors);
expect(actual.inputs).toEqual(expectation.inputs);
expect(actual.declaredInputs).toEqual(expectation.declaredInputs);
expect(actual.outputs).toEqual(expectation.outputs);
expect(actual.features).toEqual(expectation.features);
expect(actual.hostAttrs).toEqual(expectation.hostAttrs);
expect(actual.hostBindings).toEqual(expectation.hostBindings);
expect(actual.hostVars).toEqual(expectation.hostVars);
expect(actual.contentQueries).toEqual(expectation.contentQueries);
expect(actual.viewQuery).toEqual(expectation.viewQuery);
expect(actual.exportAs).toEqual(expectation.exportAs);
expect(actual.providersResolver).toEqual(expectation.providersResolver);
}
class TestClass {}

View File

@ -0,0 +1,56 @@
/**
* @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
*/
/**
* Jasmine matcher to verify that a function contains the provided code fragments.
*/
export function functionContaining(expectedFragments: Array<string|RegExp>):
jasmine.AsymmetricMatcher<Function> {
let _actual: Function|null = null;
const matches = (code: string, fragment: string|RegExp): boolean => {
if (typeof fragment === 'string') {
return code.includes(fragment);
} else {
return fragment.test(code);
}
};
return {
asymmetricMatch(actual: Function): boolean {
_actual = actual;
if (typeof actual !== 'function') {
return false;
}
const code = actual.toString();
for (const fragment of expectedFragments) {
if (!matches(code, fragment)) {
return false;
}
}
return true;
},
jasmineToString(): string {
if (typeof _actual !== 'function') {
return `Expected function to contain code fragments ${
jasmine.pp(expectedFragments)} but got ${jasmine.pp(_actual)}`;
}
const errors: string[] = [];
const code = _actual.toString();
errors.push(
`The actual function with code:\n${code}\n\ndid not contain the following fragments:`);
for (const fragment of expectedFragments) {
if (!matches(code, fragment)) {
errors.push(`- ${fragment}`);
}
}
return errors.join('\n');
}
};
}