feat(compiler-cli): JIT compilation of component declarations (#40127)

The `ɵɵngDeclareComponent` 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 `ɵɵngDeclareComponent` which invokes the JIT compiler
using the declaration object, such that a compiled component definition
is made available to the Ivy runtime.

PR Close #40127
This commit is contained in:
JoostK 2020-12-15 12:47:35 +01:00 committed by Joey Perrott
parent 826b77b632
commit d4327d51d1
13 changed files with 784 additions and 55 deletions

View File

@ -5,7 +5,7 @@
* 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 {compileComponentFromMetadata, ConstantPool, DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig, makeBindingParser, parseTemplate, R3ComponentMetadata, R3DeclareComponentMetadata, R3PartialDeclaration, R3UsedDirectiveMetadata} from '@angular/compiler';
import {compileComponentFromMetadata, ConstantPool, DeclarationListEmitMode, DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig, makeBindingParser, parseTemplate, R3ComponentMetadata, R3DeclareComponentMetadata, R3PartialDeclaration, R3UsedDirectiveMetadata} from '@angular/compiler';
import {ChangeDetectionStrategy, ViewEncapsulation} from '@angular/compiler/src/core';
import * as o from '@angular/compiler/src/output/output_ast';
@ -63,7 +63,7 @@ export function toR3ComponentMeta<TExpression>(
templateSource.expression, `Errors found in the template:\n${errors}`);
}
let wrapDirectivesAndPipesInClosure = false;
let declarationListEmitMode = DeclarationListEmitMode.Direct;
let directives: R3UsedDirectiveMetadata[] = [];
if (metaObj.has('directives')) {
@ -76,7 +76,7 @@ export function toR3ComponentMeta<TExpression>(
const forwardRefType = extractForwardRef(type);
if (forwardRefType !== null) {
typeExpr = forwardRefType;
wrapDirectivesAndPipesInClosure = true;
declarationListEmitMode = DeclarationListEmitMode.Closure;
}
return {
@ -100,7 +100,7 @@ export function toR3ComponentMeta<TExpression>(
pipes = metaObj.getObject('pipes').toMap(pipe => {
const forwardRefType = extractForwardRef(pipe);
if (forwardRefType !== null) {
wrapDirectivesAndPipesInClosure = true;
declarationListEmitMode = DeclarationListEmitMode.Closure;
return forwardRefType;
} else {
return pipe.getOpaque();
@ -115,7 +115,7 @@ export function toR3ComponentMeta<TExpression>(
nodes: template.nodes,
ngContentSelectors: template.ngContentSelectors,
},
wrapDirectivesAndPipesInClosure,
declarationListEmitMode,
styles: metaObj.has('styles') ? metaObj.getArray('styles').map(entry => entry.getString()) : [],
encapsulation: metaObj.has('encapsulation') ?
parseEncapsulation(metaObj.getValue('encapsulation')) :

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {compileComponentFromMetadata, compileDeclareComponentFromMetadata, ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, Identifiers, InterpolationConfig, LexerRange, makeBindingParser, ParsedTemplate, ParseSourceFile, parseTemplate, R3ComponentDef, R3ComponentMetadata, R3FactoryTarget, R3TargetBinder, R3UsedDirectiveMetadata, SelectorMatcher, Statement, syntaxError, TmplAstNode, WrappedNodeExpr} from '@angular/compiler';
import {compileComponentFromMetadata, compileDeclareComponentFromMetadata, ConstantPool, CssSelector, DeclarationListEmitMode, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, Identifiers, InterpolationConfig, LexerRange, makeBindingParser, ParsedTemplate, ParseSourceFile, parseTemplate, R3ComponentDef, R3ComponentMetadata, R3FactoryTarget, R3TargetBinder, R3UsedDirectiveMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {CycleAnalyzer} from '../../cycles';
@ -42,7 +42,7 @@ const EMPTY_ARRAY: any[] = [];
* be included here.
*/
export type ComponentMetadataResolvedFields =
SubsetOfKeys<R3ComponentMetadata, 'directives'|'pipes'|'wrapDirectivesAndPipesInClosure'>;
SubsetOfKeys<R3ComponentMetadata, 'directives'|'pipes'|'declarationListEmitMode'>;
export interface ComponentAnalysisData {
/**
@ -451,7 +451,7 @@ export class ComponentDecoratorHandler implements
const data: ComponentResolutionData = {
directives: EMPTY_ARRAY,
pipes: EMPTY_MAP,
wrapDirectivesAndPipesInClosure: false,
declarationListEmitMode: DeclarationListEmitMode.Direct,
};
if (scope !== null && (!scope.compilation.isPoisoned || this.usePoisonedData)) {
@ -549,7 +549,9 @@ export class ComponentDecoratorHandler implements
data.directives = usedDirectives;
data.pipes = new Map(usedPipes.map(pipe => [pipe.pipeName, pipe.expression]));
data.wrapDirectivesAndPipesInClosure = wrapDirectivesAndPipesInClosure;
data.declarationListEmitMode = wrapDirectivesAndPipesInClosure ?
DeclarationListEmitMode.Closure :
DeclarationListEmitMode.Direct;
} else {
// Declaring the directiveDefs/pipeDefs arrays directly would require imports that would
// create a cycle. Instead, mark this component as requiring remote scoping, so that the

View File

@ -42,6 +42,9 @@ export interface CompilerFacade {
declaration: R3DeclareDirectiveFacade): any;
compileComponent(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3ComponentMetadataFacade): any;
compileComponentDeclaration(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string,
declaration: R3DeclareComponentFacade): any;
compileFactory(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3FactoryDefMetadataFacade): any;
@ -187,6 +190,24 @@ export interface R3DeclareDirectiveFacade {
usesOnChanges?: boolean;
}
export interface R3DeclareComponentFacade extends R3DeclareDirectiveFacade {
template: {source: string; isInline: boolean;};
styles?: string[];
directives?: {
selector: string; type: OpaqueValue | (() => OpaqueValue);
inputs?: string[];
outputs?: string[];
exportAs?: string[];
}[];
pipes?: {[pipeName: string]: OpaqueValue|(() => OpaqueValue)};
viewProviders?: OpaqueValue;
animations?: OpaqueValue;
changeDetection?: ChangeDetectionStrategy;
encapsulation?: ViewEncapsulation;
interpolation?: [string, string];
preserveWhitespaces?: boolean;
}
export interface R3UsedDirectiveMetadata {
selector: string;
inputs: string[];

View File

@ -7,9 +7,9 @@
*/
import {CompilerFacade, CoreEnvironment, ExportedCompilerFacade, OpaqueValue, R3ComponentMetadataFacade, R3DeclareDirectiveFacade, R3DeclareQueryMetadataFacade, R3DependencyMetadataFacade, R3DirectiveMetadataFacade, R3FactoryDefMetadataFacade, R3InjectableMetadataFacade, R3InjectorMetadataFacade, R3NgModuleMetadataFacade, R3PipeMetadataFacade, R3QueryMetadataFacade, StringMap, StringMapWithRename} from './compiler_facade_interface';
import {CompilerFacade, CoreEnvironment, ExportedCompilerFacade, OpaqueValue, R3ComponentMetadataFacade, R3DeclareComponentFacade, 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 {ChangeDetectionStrategy, HostBinding, HostListener, Input, Output, Type, ViewEncapsulation} from './core';
import {Identifiers} from './identifiers';
import {compileInjectable} from './injectable_compiler_2';
import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './ml_parser/interpolation_config';
@ -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, R3HostMetadata, R3QueryMetadata} from './render3/view/api';
import {DeclarationListEmitMode, R3ComponentMetadata, R3DirectiveMetadata, R3HostMetadata, R3QueryMetadata, R3UsedDirectiveMetadata} 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';
@ -132,32 +132,21 @@ export class CompilerFacadeImpl implements CompilerFacade {
compileComponent(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string,
facade: R3ComponentMetadataFacade): any {
// The ConstantPool is a requirement of the JIT'er.
const constantPool = new ConstantPool();
const interpolationConfig = facade.interpolation ?
InterpolationConfig.fromArray(facade.interpolation) :
DEFAULT_INTERPOLATION_CONFIG;
// Parse the template and check for errors.
const template = parseTemplate(
facade.template, sourceMapUrl,
{preserveWhitespaces: facade.preserveWhitespaces, interpolationConfig});
if (template.errors !== null) {
const errors = template.errors.map(err => err.toString()).join(', ');
throw new Error(`Errors during JIT compilation of template for ${facade.name}: ${errors}`);
}
const {template, interpolation} = parseJitTemplate(
facade.template, facade.name, sourceMapUrl, facade.preserveWhitespaces,
facade.interpolation);
// Compile the component metadata, including template, into an expression.
// TODO(alxhub): implement inputs, outputs, queries, etc.
const metadata: R3ComponentMetadata = {
const meta: R3ComponentMetadata = {
...facade as R3ComponentMetadataFacadeNoPropAndWhitespace,
...convertDirectiveFacadeToMetadata(facade),
selector: facade.selector || this.elementSchemaRegistry.getDefaultComponentElementName(),
template,
wrapDirectivesAndPipesInClosure: false,
declarationListEmitMode: DeclarationListEmitMode.Direct,
styles: [...facade.styles, ...template.styles],
encapsulation: facade.encapsulation as any,
interpolation: interpolationConfig,
interpolation,
changeDetection: facade.changeDetection,
animations: facade.animations != null ? new WrappedNodeExpr(facade.animations) : null,
viewProviders: facade.viewProviders != null ? new WrappedNodeExpr(facade.viewProviders) :
@ -165,11 +154,26 @@ export class CompilerFacadeImpl implements CompilerFacade {
relativeContextFilePath: '',
i18nUseExternalIds: true,
};
const res = compileComponentFromMetadata(
metadata, constantPool, makeBindingParser(interpolationConfig));
const jitExpressionSourceMap = `ng:///${facade.name}.js`;
return this.compileComponentFromMeta(angularCoreEnv, jitExpressionSourceMap, meta);
}
compileComponentDeclaration(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string,
declaration: R3DeclareComponentFacade): any {
const typeSourceSpan =
this.createParseSourceSpan('Component', declaration.type.name, sourceMapUrl);
const meta = convertDeclareComponentFacadeToMetadata(declaration, typeSourceSpan, sourceMapUrl);
return this.compileComponentFromMeta(angularCoreEnv, sourceMapUrl, meta);
}
private compileComponentFromMeta(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3ComponentMetadata): any {
const constantPool = new ConstantPool();
const bindingParser = makeBindingParser(meta.interpolation);
const res = compileComponentFromMetadata(meta, constantPool, bindingParser);
return this.jitExpression(
res.expression, angularCoreEnv, jitExpressionSourceMap, constantPool.statements);
res.expression, angularCoreEnv, sourceMapUrl, constantPool.statements);
}
compileFactory(
@ -337,6 +341,74 @@ function convertOpaqueValuesToExpressions(obj: {[key: string]: OpaqueValue}):
return result;
}
function convertDeclareComponentFacadeToMetadata(
declaration: R3DeclareComponentFacade, typeSourceSpan: ParseSourceSpan,
sourceMapUrl: string): R3ComponentMetadata {
const {template, interpolation} = parseJitTemplate(
declaration.template.source, declaration.type.name, sourceMapUrl,
declaration.preserveWhitespaces ?? false, declaration.interpolation);
return {
...convertDeclareDirectiveFacadeToMetadata(declaration, typeSourceSpan),
template,
styles: declaration.styles ?? [],
directives: (declaration.directives ?? []).map(convertUsedDirectiveDeclarationToMetadata),
pipes: convertUsedPipesToMetadata(declaration.pipes),
viewProviders: declaration.viewProviders !== undefined ?
new WrappedNodeExpr(declaration.viewProviders) :
null,
animations: declaration.animations !== undefined ? new WrappedNodeExpr(declaration.animations) :
null,
changeDetection: declaration.changeDetection ?? ChangeDetectionStrategy.Default,
encapsulation: declaration.encapsulation ?? ViewEncapsulation.Emulated,
interpolation,
declarationListEmitMode: DeclarationListEmitMode.ClosureResolved,
relativeContextFilePath: '',
i18nUseExternalIds: true,
};
}
function convertUsedDirectiveDeclarationToMetadata(
declaration: NonNullable<R3DeclareComponentFacade['directives']>[number]):
R3UsedDirectiveMetadata {
return {
selector: declaration.selector,
type: new WrappedNodeExpr(declaration.type),
inputs: declaration.inputs ?? [],
outputs: declaration.outputs ?? [],
exportAs: declaration.exportAs ?? null,
};
}
function convertUsedPipesToMetadata(declaredPipes: R3DeclareComponentFacade['pipes']):
Map<string, Expression> {
const pipes = new Map<string, Expression>();
if (declaredPipes === undefined) {
return pipes;
}
for (const pipeName of Object.keys(declaredPipes)) {
const pipeType = declaredPipes[pipeName];
pipes.set(pipeName, new WrappedNodeExpr(pipeType));
}
return pipes;
}
function parseJitTemplate(
template: string, typeName: string, sourceMapUrl: string, preserveWhitespaces: boolean,
interpolation: [string, string]|undefined) {
const interpolationConfig =
interpolation ? InterpolationConfig.fromArray(interpolation) : DEFAULT_INTERPOLATION_CONFIG;
// Parse the template and check for errors.
const parsed = parseTemplate(
template, sourceMapUrl, {preserveWhitespaces: preserveWhitespaces, interpolationConfig});
if (parsed.errors !== null) {
const errors = parsed.errors.map(err => err.toString()).join(', ');
throw new Error(`Errors during JIT compilation of template for ${typeName}: ${errors}`);
}
return {template: parsed, interpolation: interpolationConfig};
}
// This seems to be needed to placate TS v3.0 only
type R3DirectiveMetadataFacadeNoPropAndWhitespace =
Pick<R3DirectiveMetadataFacade, Exclude<keyof R3DirectiveMetadataFacade, 'propMetadata'>>;

View File

@ -9,7 +9,7 @@ import * as core from '../../core';
import {DEFAULT_INTERPOLATION_CONFIG} from '../../ml_parser/interpolation_config';
import * as o from '../../output/output_ast';
import {Identifiers as R3} from '../r3_identifiers';
import {R3ComponentDef, R3ComponentMetadata, R3UsedDirectiveMetadata} from '../view/api';
import {DeclarationListEmitMode, R3ComponentDef, R3ComponentMetadata, R3UsedDirectiveMetadata} from '../view/api';
import {createComponentType} from '../view/compiler';
import {ParsedTemplate} from '../view/template';
import {DefinitionMap} from '../view/util';
@ -91,8 +91,9 @@ function compileTemplateDefinition(template: ParsedTemplate): o.LiteralMapExpr {
* individual directives. If the component does not use any directives, then null is returned.
*/
function compileUsedDirectiveMetadata(meta: R3ComponentMetadata): o.LiteralArrayExpr|null {
const wrapType =
meta.wrapDirectivesAndPipesInClosure ? generateForwardRef : (expr: o.Expression) => expr;
const wrapType = meta.declarationListEmitMode !== DeclarationListEmitMode.Direct ?
generateForwardRef :
(expr: o.Expression) => expr;
return toOptionalLiteralArray(meta.directives, directive => {
const dirMeta = new DefinitionMap<R3UsedDirectiveMetadata>();
@ -115,8 +116,9 @@ function compileUsedPipeMetadata(meta: R3ComponentMetadata): o.LiteralMapExpr|nu
return null;
}
const wrapType =
meta.wrapDirectivesAndPipesInClosure ? generateForwardRef : (expr: o.Expression) => expr;
const wrapType = meta.declarationListEmitMode !== DeclarationListEmitMode.Direct ?
generateForwardRef :
(expr: o.Expression) => expr;
const entries = [];
for (const [name, pipe] of meta.pipes) {

View File

@ -230,6 +230,7 @@ export class Identifiers {
o.ExternalReference = {name: 'ɵɵtemplateRefExtractor', moduleName: CORE};
static forwardRef: o.ExternalReference = {name: 'forwardRef', moduleName: CORE};
static resolveForwardRef: o.ExternalReference = {name: 'resolveForwardRef', moduleName: CORE};
static resolveWindow: o.ExternalReference = {name: 'ɵɵresolveWindow', moduleName: CORE};
static resolveDocument: o.ExternalReference = {name: 'ɵɵresolveDocument', moduleName: CORE};

View File

@ -119,6 +119,50 @@ export interface R3DirectiveMetadata {
providers: o.Expression|null;
}
/**
* Specifies how a list of declaration type references should be emitted into the generated code.
*/
export const enum DeclarationListEmitMode {
/**
* The list of declarations is emitted into the generated code as is.
*
* ```
* directives: [MyDir],
* ```
*/
Direct,
/**
* The list of declarations is emitted into the generated code wrapped inside a closure, which
* is needed when at least one declaration is a forward reference.
*
* ```
* directives: function () { return [MyDir, ForwardDir]; },
* ```
*/
Closure,
/**
* Similar to `Closure`, with the addition that the list of declarations can contain individual
* items that are themselves forward references. This is relevant for JIT compilations, as
* unwrapping the forwardRef cannot be done statically so must be deferred. This mode emits
* the declaration list using a mapping transform through `resolveForwardRef` to ensure that
* any forward references within the list are resolved when the outer closure is invoked.
*
* Consider the case where the runtime has captured two declarations in two distinct values:
* ```
* const dirA = MyDir;
* const dirB = forwardRef(function() { return ForwardRef; });
* ```
*
* This mode would emit the declarations captured in `dirA` and `dirB` as follows:
* ```
* directives: function () { return [dirA, dirB].map(ng.resolveForwardRef); },
* ```
*/
ClosureResolved,
}
/**
* Information needed to compile a component for the render3 runtime.
*/
@ -152,11 +196,9 @@ export interface R3ComponentMetadata extends R3DirectiveMetadata {
directives: R3UsedDirectiveMetadata[];
/**
* Whether to wrap the 'directives' and/or `pipes` array, if one is generated, in a closure.
*
* This is done when the directives or pipes contain forward references.
* Specifies how the 'directives' and/or `pipes` array, if generated, need to be emitted.
*/
wrapDirectivesAndPipesInClosure: boolean;
declarationListEmitMode: DeclarationListEmitMode;
/**
* A collection of styling data that will be applied and scoped to the component.

View File

@ -27,7 +27,7 @@ import {Identifiers as R3} from '../r3_identifiers';
import {Render3ParseResult} from '../r3_template_transform';
import {prepareSyntheticListenerFunctionName, prepareSyntheticPropertyName, typeWithParameters} from '../util';
import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3HostMetadata, R3QueryMetadata} from './api';
import {DeclarationListEmitMode, R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3HostMetadata, R3QueryMetadata} from './api';
import {MIN_STYLING_BINDING_SLOTS_REQUIRED, StylingBuilder, StylingInstructionCall} from './styling_builder';
import {BindingScope, makeBindingParser, prepareEventListenerParameters, renderFlagCheckIfStmt, resolveSanitizationFn, TemplateDefinitionBuilder, ValueConverter} from './template';
import {asLiteral, chainedInstruction, conditionallyCreateMapObjectLiteral, CONTEXT_NAME, DefinitionMap, getQueryPredicate, RENDER_FLAGS, TEMPORARY_NAME, temporaryAllocator} from './util';
@ -213,19 +213,15 @@ export function compileComponentFromMetadata(
// e.g. `directives: [MyDirective]`
if (directivesUsed.size) {
let directivesExpr: o.Expression = o.literalArr(Array.from(directivesUsed));
if (meta.wrapDirectivesAndPipesInClosure) {
directivesExpr = o.fn([], [new o.ReturnStatement(directivesExpr)]);
}
const directivesList = o.literalArr(Array.from(directivesUsed));
const directivesExpr = compileDeclarationList(directivesList, meta.declarationListEmitMode);
definitionMap.set('directives', directivesExpr);
}
// e.g. `pipes: [MyPipe]`
if (pipesUsed.size) {
let pipesExpr: o.Expression = o.literalArr(Array.from(pipesUsed));
if (meta.wrapDirectivesAndPipesInClosure) {
pipesExpr = o.fn([], [new o.ReturnStatement(pipesExpr)]);
}
const pipesList = o.literalArr(Array.from(pipesUsed));
const pipesExpr = compileDeclarationList(pipesList, meta.declarationListEmitMode);
definitionMap.set('pipes', pipesExpr);
}
@ -277,6 +273,26 @@ export function createComponentType(meta: R3ComponentMetadata): o.Type {
return o.expressionType(o.importExpr(R3.ComponentDefWithMeta, typeParams));
}
/**
* Compiles the array literal of declarations into an expression according to the provided emit
* mode.
*/
function compileDeclarationList(
list: o.LiteralArrayExpr, mode: DeclarationListEmitMode): o.Expression {
switch (mode) {
case DeclarationListEmitMode.Direct:
// directives: [MyDir],
return list;
case DeclarationListEmitMode.Closure:
// directives: function () { return [MyDir]; }
return o.fn([], [new o.ReturnStatement(list)]);
case DeclarationListEmitMode.ClosureResolved:
// directives: function () { return [MyDir].map(ng.resolveForwardRef); }
const resolvedList = list.callMethod('map', [o.importExpr(R3.resolveForwardRef)]);
return o.fn([], [new o.ReturnStatement(resolvedList)]);
}
}
/**
* A wrapper around `compileDirective` which depends on render2 global analysis data as its input
* instead of the `R3DirectiveMetadata`.
@ -335,7 +351,7 @@ export function compileComponentFromRender2(
directives: [],
pipes: typeMapToExpressionMap(pipeTypeByName, outputCtx),
viewQueries: queriesFromGlobalMetadata(component.viewQueries, outputCtx),
wrapDirectivesAndPipesInClosure: false,
declarationListEmitMode: DeclarationListEmitMode.Direct,
styles: (summary.template && summary.template.styles) || EMPTY_ARRAY,
encapsulation:
(summary.template && summary.template.encapsulation) || core.ViewEncapsulation.Emulated,

View File

@ -108,6 +108,16 @@ const coreR3DeclareDirectiveFacade: core.R3DeclareDirectiveFacade =
const compilerR3DeclareDirectiveFacade: compiler.R3DeclareDirectiveFacade =
null! as core.R3DeclareDirectiveFacade;
const coreR3DeclareComponentFacade: core.R3DeclareComponentFacade =
null! as compiler.R3DeclareComponentFacade;
const compilerR3DeclareComponentFacade: compiler.R3DeclareComponentFacade =
null! as core.R3DeclareComponentFacade;
const coreR3UsedDirectiveMetadata: core.R3UsedDirectiveMetadata =
null! as compiler.R3UsedDirectiveMetadata;
const compilerR3UsedDirectiveMetadata: compiler.R3UsedDirectiveMetadata =
null! as core.R3UsedDirectiveMetadata;
const coreViewEncapsulation: core.ViewEncapsulation = null! as compiler.ViewEncapsulation;
const compilerViewEncapsulation: compiler.ViewEncapsulation = null! as core.ViewEncapsulation;

View File

@ -42,6 +42,9 @@ export interface CompilerFacade {
declaration: R3DeclareDirectiveFacade): any;
compileComponent(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3ComponentMetadataFacade): any;
compileComponentDeclaration(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string,
declaration: R3DeclareComponentFacade): any;
compileFactory(
angularCoreEnv: CoreEnvironment, sourceMapUrl: string, meta: R3FactoryDefMetadataFacade): any;
@ -187,6 +190,24 @@ export interface R3DeclareDirectiveFacade {
usesOnChanges?: boolean;
}
export interface R3DeclareComponentFacade extends R3DeclareDirectiveFacade {
template: {source: string; isInline: boolean;};
styles?: string[];
directives?: {
selector: string; type: OpaqueValue | (() => OpaqueValue);
inputs?: string[];
outputs?: string[];
exportAs?: string[];
}[];
pipes?: {[pipeName: string]: OpaqueValue|(() => OpaqueValue)};
viewProviders?: OpaqueValue;
animations?: OpaqueValue;
changeDetection?: ChangeDetectionStrategy;
encapsulation?: ViewEncapsulation;
interpolation?: [string, string];
preserveWhitespaces?: boolean;
}
export interface R3UsedDirectiveMetadata {
selector: string;
inputs: string[];

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {forwardRef} from '../../di/forward_ref';
import {forwardRef, resolveForwardRef} from '../../di/forward_ref';
import {ɵɵinject, ɵɵinvalidFactoryDep} from '../../di/injector_compatibility';
import {ɵɵdefineInjectable, ɵɵdefineInjector} from '../../di/interface/defs';
import * as sanitization from '../../sanitization/sanitization';
@ -170,4 +170,5 @@ export const angularCoreEnv: {[name: string]: Function} =
'ɵɵtrustConstantResourceUrl': sanitization.ɵɵtrustConstantResourceUrl,
'forwardRef': forwardRef,
'resolveForwardRef': resolveForwardRef,
}))();

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {getCompilerFacade, R3DeclareDirectiveFacade} from '../../compiler/compiler_facade';
import {getCompilerFacade, R3DeclareComponentFacade, R3DeclareDirectiveFacade} from '../../compiler/compiler_facade';
import {angularCoreEnv} from './environment';
/**
@ -25,6 +25,8 @@ export function ɵɵngDeclareDirective(decl: R3DeclareDirectiveFacade): unknown
*
* @codeGenApi
*/
export function ɵɵngDeclareComponent(decl: unknown): unknown {
throw new Error('Not yet implemented');
export function ɵɵngDeclareComponent(decl: R3DeclareComponentFacade): unknown {
const compiler = getCompilerFacade();
return compiler.compileComponentDeclaration(
angularCoreEnv, `ng:///${decl.type.name}/ɵcmp.js`, decl);
}

View File

@ -0,0 +1,539 @@
/**
* @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 {ChangeDetectionStrategy, Directive, ElementRef, forwardRef, Pipe, Type, ViewEncapsulation, ɵɵngDeclareComponent} from '@angular/core';
import {AttributeMarker, ComponentDef} from '../../../src/render3';
import {functionContaining} from './matcher';
describe('component declaration jit compilation', () => {
it('should compile a minimal component declaration', () => {
const def = ɵɵngDeclareComponent({
type: TestClass,
template: createTemplate(`<div></div>`),
}) as ComponentDef<TestClass>;
expectComponentDef(def, {
template: functionContaining([
/element[^(]*\(0,'div'\)/,
]),
});
});
it('should compile a selector', () => {
const def =
ɵɵngDeclareComponent(
{type: TestClass, template: createTemplate('<div></div>'), selector: '[dir], test'}) as
ComponentDef<TestClass>;
expectComponentDef(def, {
selectors: [['', 'dir', ''], ['test']],
});
});
it('should compile inputs and outputs', () => {
const def = ɵɵngDeclareComponent({
type: TestClass,
template: createTemplate('<div></div>'),
inputs: {
minifiedProperty: 'property',
minifiedClassProperty: ['bindingName', 'classProperty'],
},
outputs: {
minifiedEventName: 'eventBindingName',
},
}) as ComponentDef<TestClass>;
expectComponentDef(def, {
inputs: {
'property': 'minifiedProperty',
'bindingName': 'minifiedClassProperty',
},
declaredInputs: {
'property': 'property',
'bindingName': 'classProperty',
},
outputs: {
'eventBindingName': 'minifiedEventName',
},
});
});
it('should compile exportAs', () => {
const def = ɵɵngDeclareComponent({
type: TestClass,
template: createTemplate('<div></div>'),
exportAs: ['a', 'b'],
}) as ComponentDef<TestClass>;
expectComponentDef(def, {
exportAs: ['a', 'b'],
});
});
it('should compile providers', () => {
const def = ɵɵngDeclareComponent({
type: TestClass,
template: createTemplate('<div></div>'),
providers: [
{provide: 'token', useValue: 123},
],
}) as ComponentDef<TestClass>;
expectComponentDef(def, {
features: [jasmine.any(Function)],
providersResolver: jasmine.any(Function),
});
});
it('should compile view providers', () => {
const def = ɵɵngDeclareComponent({
type: TestClass,
template: createTemplate('<div></div>'),
viewProviders: [
{provide: 'token', useValue: 123},
],
}) as ComponentDef<TestClass>;
expectComponentDef(def, {
features: [jasmine.any(Function)],
providersResolver: jasmine.any(Function),
});
});
it('should compile content queries', () => {
const def = ɵɵngDeclareComponent({
type: TestClass,
template: createTemplate('<div></div>'),
queries: [
{
propertyName: 'byRef',
predicate: ['ref'],
},
{
propertyName: 'byToken',
predicate: String,
descendants: true,
static: true,
first: true,
read: ElementRef,
}
],
}) as ComponentDef<TestClass>;
expectComponentDef(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 = ɵɵngDeclareComponent({
type: TestClass,
template: createTemplate('<div></div>'),
viewQueries: [
{
propertyName: 'byRef',
predicate: ['ref'],
},
{
propertyName: 'byToken',
predicate: String,
descendants: true,
static: true,
first: true,
read: ElementRef,
}
],
}) as ComponentDef<TestClass>;
expectComponentDef(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 = ɵɵngDeclareComponent({
type: TestClass,
template: createTemplate('<div></div>'),
host: {
attributes: {
'attr': 'value',
},
listeners: {
'event': 'handleEvent($event)',
},
properties: {
'foo': 'foo.prop',
'attr.bar': 'bar.prop',
},
classAttribute: 'foo bar',
styleAttribute: 'width: 100px;',
},
}) as ComponentDef<TestClass>;
expectComponentDef(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 components with inheritance', () => {
const def = ɵɵngDeclareComponent({
type: TestClass,
template: createTemplate('<div></div>'),
usesInheritance: true,
}) as ComponentDef<TestClass>;
expectComponentDef(def, {
features: [functionContaining(['ɵɵInheritDefinitionFeature'])],
});
});
it('should compile components with onChanges lifecycle hook', () => {
const def = ɵɵngDeclareComponent({
type: TestClass,
template: createTemplate('<div></div>'),
usesOnChanges: true,
}) as ComponentDef<TestClass>;
expectComponentDef(def, {
features: [functionContaining(['ɵɵNgOnChangesFeature'])],
});
});
it('should compile components with OnPush change detection strategy', () => {
const def = ɵɵngDeclareComponent({
type: TestClass,
template: createTemplate('<div></div>'),
changeDetection: ChangeDetectionStrategy.OnPush,
}) as ComponentDef<TestClass>;
expectComponentDef(def, {
onPush: true,
});
});
it('should compile components with styles', () => {
const def = ɵɵngDeclareComponent({
type: TestClass,
template: createTemplate('<div></div>'),
styles: ['div {}'],
}) as ComponentDef<TestClass>;
expectComponentDef(def, {
styles: ['div[_ngcontent-%COMP%] {}'],
encapsulation: ViewEncapsulation.Emulated,
});
});
it('should compile components with view encapsulation', () => {
const def = ɵɵngDeclareComponent({
type: TestClass,
template: createTemplate('<div></div>'),
styles: ['div {}'],
encapsulation: ViewEncapsulation.ShadowDom,
}) as ComponentDef<TestClass>;
expectComponentDef(def, {
styles: ['div {}'],
encapsulation: ViewEncapsulation.ShadowDom,
});
});
it('should compile components with animations', () => {
const def = ɵɵngDeclareComponent({
type: TestClass,
template: createTemplate('<div></div>'),
animations: [{type: 'trigger'}],
}) as ComponentDef<TestClass>;
expectComponentDef(def, {
data: {
animation: [{type: 'trigger'}],
},
});
});
it('should honor preserveWhitespaces', () => {
const template = createTemplate('<div> Foo </div>');
const whenTrue = ɵɵngDeclareComponent({
type: TestClass,
template,
preserveWhitespaces: true,
}) as ComponentDef<TestClass>;
const whenOmitted = ɵɵngDeclareComponent({
type: TestClass,
template,
}) as ComponentDef<TestClass>;
expectComponentDef(whenTrue, {
template: functionContaining([
// NOTE: the `anonymous` match is to support IE11, as functions don't have a name there.
/(?:elementStart|anonymous)[^(]*\(0,'div'\)/,
/(?:text|anonymous)[^(]*\(1,' Foo '\)/,
]),
});
expectComponentDef(whenOmitted, {
template: functionContaining([
// NOTE: the `anonymous` match is to support IE11, as functions don't have a name there.
/(?:elementStart|anonymous)[^(]*\(0,'div'\)/,
/(?:text|anonymous)[^(]*\(1,' Foo '\)/,
]),
});
});
it('should honor custom interpolation config', () => {
const def = ɵɵngDeclareComponent({
type: TestClass,
template: createTemplate('{% foo %}'),
interpolation: ['{%', '%}'],
}) as ComponentDef<TestClass>;
expectComponentDef(def, {
template: functionContaining([
// NOTE: the `anonymous` match is to support IE11, as functions don't have a name there.
/(?:textInterpolate|anonymous)[^(]*\(ctx.foo\)/,
]),
});
});
it('should compile used directives', () => {
const def = ɵɵngDeclareComponent({
type: TestClass,
template: createTemplate('<div dir></div>'),
directives: [{
type: TestDir,
selector: '[dir]',
}],
}) as ComponentDef<TestClass>;
expectComponentDef(def, {
directives: [TestDir],
});
});
it('should compile forward declared directives', () => {
const def = ɵɵngDeclareComponent({
type: TestClass,
template: createTemplate('<div forward></div>'),
directives: [{
type: forwardRef(function() {
return ForwardDir;
}),
selector: '[forward]',
}],
}) as ComponentDef<TestClass>;
@Directive({selector: '[forward]'})
class ForwardDir {
}
expectComponentDef(def, {
directives: [ForwardDir],
});
});
it('should compile mixed forward and direct declared directives', () => {
const def = ɵɵngDeclareComponent({
type: TestClass,
template: createTemplate('<div dir forward></div>'),
directives: [
{
type: TestDir,
selector: '[dir]',
},
{
type: forwardRef(function() {
return ForwardDir;
}),
selector: '[forward]',
}
],
}) as ComponentDef<TestClass>;
@Directive({selector: '[forward]'})
class ForwardDir {
}
expectComponentDef(def, {
directives: [TestDir, ForwardDir],
});
});
it('should compile used pipes', () => {
const def = ɵɵngDeclareComponent({
type: TestClass,
template: createTemplate('{{ expr | test }}'),
pipes: {
'test': TestPipe,
},
}) as ComponentDef<TestClass>;
expectComponentDef(def, {
pipes: [TestPipe],
});
});
it('should compile forward declared pipes', () => {
const def = ɵɵngDeclareComponent({
type: TestClass,
template: createTemplate('{{ expr | forward }}'),
pipes: {
'forward': forwardRef(function() {
return ForwardPipe;
}),
},
}) as ComponentDef<TestClass>;
@Pipe({name: 'forward'})
class ForwardPipe {
}
expectComponentDef(def, {
pipes: [ForwardPipe],
});
});
it('should compile mixed forward and direct declared pipes', () => {
const def = ɵɵngDeclareComponent({
type: TestClass,
template: createTemplate('{{ expr | forward | test }}'),
pipes: {
'test': TestPipe,
'forward': forwardRef(function() {
return ForwardPipe;
}),
},
}) as ComponentDef<TestClass>;
@Pipe({name: 'forward'})
class ForwardPipe {
}
expectComponentDef(def, {
pipes: [TestPipe, ForwardPipe],
});
});
});
function createTemplate(template: string) {
return {source: template, isInline: true};
}
type ComponentDefExpectations = jasmine.Expected<Pick<
ComponentDef<unknown>,
'selectors'|'template'|'inputs'|'declaredInputs'|'outputs'|'features'|'hostAttrs'|
'hostBindings'|'hostVars'|'contentQueries'|'viewQuery'|'exportAs'|'providersResolver'|
'encapsulation'|'onPush'|'styles'|'data'>>&{
directives: Type<unknown>[]|null;
pipes: Type<unknown>[]|null;
};
/**
* Asserts that the provided component 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 expectComponentDef(
actual: ComponentDef<unknown>, expected: Partial<ComponentDefExpectations>): void {
const expectation: ComponentDefExpectations = {
selectors: [],
template: jasmine.any(Function),
inputs: {},
declaredInputs: {},
outputs: {},
features: null,
hostAttrs: null,
hostBindings: null,
hostVars: 0,
contentQueries: null,
viewQuery: null,
exportAs: null,
providersResolver: null,
// Although the default view encapsulation is `Emulated`, the default expected view
// encapsulation is `None` as this is chosen when no styles are present.
encapsulation: ViewEncapsulation.None,
onPush: false,
styles: [],
directives: null,
pipes: null,
data: {},
...expected,
};
expect(actual.type).toBe(TestClass);
expect(actual.selectors).toEqual(expectation.selectors);
expect(actual.template).toEqual(expectation.template);
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);
expect(actual.encapsulation).toEqual(expectation.encapsulation);
expect(actual.onPush).toEqual(expectation.onPush);
expect(actual.styles).toEqual(expectation.styles);
expect(actual.data).toEqual(expectation.data);
const directiveDefs =
typeof actual.directiveDefs === 'function' ? actual.directiveDefs() : actual.directiveDefs;
const directiveTypes = directiveDefs !== null ? directiveDefs.map(def => def.type) : null;
expect(directiveTypes).toEqual(expectation.directives);
const pipeDefs = typeof actual.pipeDefs === 'function' ? actual.pipeDefs() : actual.pipeDefs;
const pipeTypes = pipeDefs !== null ? pipeDefs.map(def => def.type) : null;
expect(pipeTypes).toEqual(expectation.pipes);
}
class TestClass {}
@Directive({selector: '[dir]'})
class TestDir {
}
@Pipe({name: 'test'})
class TestPipe {
}