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:
parent
e54261b8d8
commit
9186f1feea
|
@ -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;
|
||||
|
|
|
@ -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'>>;
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -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)
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 {}
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue