diff --git a/packages/compiler/src/compiler_facade_interface.ts b/packages/compiler/src/compiler_facade_interface.ts index d60b8be014..92d8d18f26 100644 --- a/packages/compiler/src/compiler_facade_interface.ts +++ b/packages/compiler/src/compiler_facade_interface.ts @@ -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; diff --git a/packages/compiler/src/jit_compiler_facade.ts b/packages/compiler/src/jit_compiler_facade.ts index e7a7ab4342..750e925bf5 100644 --- a/packages/compiler/src/jit_compiler_facade.ts +++ b/packages/compiler/src/jit_compiler_facade.ts @@ -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} { + const result: {[key: string]: WrappedNodeExpr} = {}; + 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>; diff --git a/packages/compiler/src/render3/partial/api.ts b/packages/compiler/src/render3/partial/api.ts index 836685b591..ac07694f2b 100644 --- a/packages/compiler/src/render3/partial/api.ts +++ b/packages/compiler/src/render3/partial/api.ts @@ -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}; diff --git a/packages/compiler/src/render3/view/util.ts b/packages/compiler/src/render3/view/util.ts index 34e4fedd70..8f37e2d7f5 100644 --- a/packages/compiler/src/render3/view/util.ts +++ b/packages/compiler/src/render3/view/util.ts @@ -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) }; diff --git a/packages/compiler/test/compiler_facade_interface_spec.ts b/packages/compiler/test/compiler_facade_interface_spec.ts index 963bfa5d17..05b83010f0 100644 --- a/packages/compiler/test/compiler_facade_interface_spec.ts +++ b/packages/compiler/test/compiler_facade_interface_spec.ts @@ -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; diff --git a/packages/core/src/compiler/compiler_facade_interface.ts b/packages/core/src/compiler/compiler_facade_interface.ts index d60b8be014..92d8d18f26 100644 --- a/packages/core/src/compiler/compiler_facade_interface.ts +++ b/packages/core/src/compiler/compiler_facade_interface.ts @@ -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; diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index e87609da44..856b98fc49 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -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'; diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 483f45e7b4..24d1d92309 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -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'; diff --git a/packages/core/src/render3/jit/partial.ts b/packages/core/src/render3/jit/partial.ts index 402fcd499b..7e2d14a9ac 100644 --- a/packages/core/src/render3/jit/partial.ts +++ b/packages/core/src/render3/jit/partial.ts @@ -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); } /** diff --git a/packages/core/test/render3/jit/declare_directive_spec.ts b/packages/core/test/render3/jit/declare_directive_spec.ts new file mode 100644 index 0000000000..e83e956098 --- /dev/null +++ b/packages/core/test/render3/jit/declare_directive_spec.ts @@ -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; + + expectDirectiveDef(def, {}); + }); + + it('should compile a selector', () => { + const def = + ɵɵngDeclareDirective({type: TestClass, selector: '[dir], test'}) as DirectiveDef; + + 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; + + 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; + + expectDirectiveDef(def, { + exportAs: ['a', 'b'], + }); + }); + + it('should compile providers', () => { + const def = ɵɵngDeclareDirective({ + type: TestClass, + providers: [ + {provide: 'token', useValue: 123}, + ], + }) as DirectiveDef; + + 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; + + 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; + + 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; + + 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; + + expectDirectiveDef(def, { + features: [functionContaining(['ɵɵInheritDefinitionFeature'])], + }); + }); + + it('should compile directives with onChanges lifecycle hook', () => { + const def = ɵɵngDeclareDirective({ + type: TestClass, + usesOnChanges: true, + }) as DirectiveDef; + + expectDirectiveDef(def, { + features: [functionContaining(['ɵɵNgOnChangesFeature'])], + }); + }); +}); + +type DirectiveDefExpectations = jasmine.Expected, + '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, expected: Partial): 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 {} diff --git a/packages/core/test/render3/jit/matcher.ts b/packages/core/test/render3/jit/matcher.ts new file mode 100644 index 0000000000..e5e6f169ca --- /dev/null +++ b/packages/core/test/render3/jit/matcher.ts @@ -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): + jasmine.AsymmetricMatcher { + 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'); + } + }; +}