diff --git a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts index 83f9a5b183..d309596fb4 100644 --- a/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts +++ b/packages/compiler-cli/linker/src/file_linker/partial_linkers/partial_component_linker_1.ts @@ -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( 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( const forwardRefType = extractForwardRef(type); if (forwardRefType !== null) { typeExpr = forwardRefType; - wrapDirectivesAndPipesInClosure = true; + declarationListEmitMode = DeclarationListEmitMode.Closure; } return { @@ -100,7 +100,7 @@ export function toR3ComponentMeta( 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( 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')) : diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index e4d39317b6..7c2979297b 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -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; + SubsetOfKeys; 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 diff --git a/packages/compiler/src/compiler_facade_interface.ts b/packages/compiler/src/compiler_facade_interface.ts index 92d8d18f26..55051b92a4 100644 --- a/packages/compiler/src/compiler_facade_interface.ts +++ b/packages/compiler/src/compiler_facade_interface.ts @@ -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[]; diff --git a/packages/compiler/src/jit_compiler_facade.ts b/packages/compiler/src/jit_compiler_facade.ts index 750e925bf5..5ee1c6328c 100644 --- a/packages/compiler/src/jit_compiler_facade.ts +++ b/packages/compiler/src/jit_compiler_facade.ts @@ -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[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 { + const pipes = new Map(); + 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>; diff --git a/packages/compiler/src/render3/partial/component.ts b/packages/compiler/src/render3/partial/component.ts index 271a836fa8..87de9be5c1 100644 --- a/packages/compiler/src/render3/partial/component.ts +++ b/packages/compiler/src/render3/partial/component.ts @@ -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(); @@ -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) { diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 6e3d2f7ae2..c4cb1d1771 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -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}; diff --git a/packages/compiler/src/render3/view/api.ts b/packages/compiler/src/render3/view/api.ts index 51f1b310ef..4a207cc1a6 100644 --- a/packages/compiler/src/render3/view/api.ts +++ b/packages/compiler/src/render3/view/api.ts @@ -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. diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index 7bb1788593..a6eb649a32 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -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, diff --git a/packages/compiler/test/compiler_facade_interface_spec.ts b/packages/compiler/test/compiler_facade_interface_spec.ts index 05b83010f0..ebb49c9c29 100644 --- a/packages/compiler/test/compiler_facade_interface_spec.ts +++ b/packages/compiler/test/compiler_facade_interface_spec.ts @@ -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; diff --git a/packages/core/src/compiler/compiler_facade_interface.ts b/packages/core/src/compiler/compiler_facade_interface.ts index 92d8d18f26..55051b92a4 100644 --- a/packages/core/src/compiler/compiler_facade_interface.ts +++ b/packages/core/src/compiler/compiler_facade_interface.ts @@ -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[]; diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index 4556e25dd8..97ec1d822a 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -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, }))(); diff --git a/packages/core/src/render3/jit/partial.ts b/packages/core/src/render3/jit/partial.ts index 7e2d14a9ac..7a2b359d04 100644 --- a/packages/core/src/render3/jit/partial.ts +++ b/packages/core/src/render3/jit/partial.ts @@ -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); } diff --git a/packages/core/test/render3/jit/declare_component_spec.ts b/packages/core/test/render3/jit/declare_component_spec.ts new file mode 100644 index 0000000000..baa07488c5 --- /dev/null +++ b/packages/core/test/render3/jit/declare_component_spec.ts @@ -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(`
`), + }) as ComponentDef; + + expectComponentDef(def, { + template: functionContaining([ + /element[^(]*\(0,'div'\)/, + ]), + }); + }); + + it('should compile a selector', () => { + const def = + ɵɵngDeclareComponent( + {type: TestClass, template: createTemplate('
'), selector: '[dir], test'}) as + ComponentDef; + + expectComponentDef(def, { + selectors: [['', 'dir', ''], ['test']], + }); + }); + + it('should compile inputs and outputs', () => { + const def = ɵɵngDeclareComponent({ + type: TestClass, + template: createTemplate('
'), + inputs: { + minifiedProperty: 'property', + minifiedClassProperty: ['bindingName', 'classProperty'], + }, + outputs: { + minifiedEventName: 'eventBindingName', + }, + }) as ComponentDef; + + 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('
'), + exportAs: ['a', 'b'], + }) as ComponentDef; + + expectComponentDef(def, { + exportAs: ['a', 'b'], + }); + }); + + it('should compile providers', () => { + const def = ɵɵngDeclareComponent({ + type: TestClass, + template: createTemplate('
'), + providers: [ + {provide: 'token', useValue: 123}, + ], + }) as ComponentDef; + + expectComponentDef(def, { + features: [jasmine.any(Function)], + providersResolver: jasmine.any(Function), + }); + }); + + it('should compile view providers', () => { + const def = ɵɵngDeclareComponent({ + type: TestClass, + template: createTemplate('
'), + viewProviders: [ + {provide: 'token', useValue: 123}, + ], + }) as ComponentDef; + + expectComponentDef(def, { + features: [jasmine.any(Function)], + providersResolver: jasmine.any(Function), + }); + }); + + it('should compile content queries', () => { + const def = ɵɵngDeclareComponent({ + type: TestClass, + template: createTemplate('
'), + queries: [ + { + propertyName: 'byRef', + predicate: ['ref'], + }, + { + propertyName: 'byToken', + predicate: String, + descendants: true, + static: true, + first: true, + read: ElementRef, + } + ], + }) as ComponentDef; + + 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('
'), + viewQueries: [ + { + propertyName: 'byRef', + predicate: ['ref'], + }, + { + propertyName: 'byToken', + predicate: String, + descendants: true, + static: true, + first: true, + read: ElementRef, + } + ], + }) as ComponentDef; + + 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('
'), + host: { + attributes: { + 'attr': 'value', + }, + listeners: { + 'event': 'handleEvent($event)', + }, + properties: { + 'foo': 'foo.prop', + 'attr.bar': 'bar.prop', + }, + classAttribute: 'foo bar', + styleAttribute: 'width: 100px;', + }, + }) as ComponentDef; + + 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('
'), + usesInheritance: true, + }) as ComponentDef; + + expectComponentDef(def, { + features: [functionContaining(['ɵɵInheritDefinitionFeature'])], + }); + }); + + it('should compile components with onChanges lifecycle hook', () => { + const def = ɵɵngDeclareComponent({ + type: TestClass, + template: createTemplate('
'), + usesOnChanges: true, + }) as ComponentDef; + + expectComponentDef(def, { + features: [functionContaining(['ɵɵNgOnChangesFeature'])], + }); + }); + + it('should compile components with OnPush change detection strategy', () => { + const def = ɵɵngDeclareComponent({ + type: TestClass, + template: createTemplate('
'), + changeDetection: ChangeDetectionStrategy.OnPush, + }) as ComponentDef; + + expectComponentDef(def, { + onPush: true, + }); + }); + + it('should compile components with styles', () => { + const def = ɵɵngDeclareComponent({ + type: TestClass, + template: createTemplate('
'), + styles: ['div {}'], + }) as ComponentDef; + + expectComponentDef(def, { + styles: ['div[_ngcontent-%COMP%] {}'], + encapsulation: ViewEncapsulation.Emulated, + }); + }); + + it('should compile components with view encapsulation', () => { + const def = ɵɵngDeclareComponent({ + type: TestClass, + template: createTemplate('
'), + styles: ['div {}'], + encapsulation: ViewEncapsulation.ShadowDom, + }) as ComponentDef; + + expectComponentDef(def, { + styles: ['div {}'], + encapsulation: ViewEncapsulation.ShadowDom, + }); + }); + + it('should compile components with animations', () => { + const def = ɵɵngDeclareComponent({ + type: TestClass, + template: createTemplate('
'), + animations: [{type: 'trigger'}], + }) as ComponentDef; + + expectComponentDef(def, { + data: { + animation: [{type: 'trigger'}], + }, + }); + }); + + it('should honor preserveWhitespaces', () => { + const template = createTemplate('
Foo
'); + const whenTrue = ɵɵngDeclareComponent({ + type: TestClass, + template, + preserveWhitespaces: true, + }) as ComponentDef; + const whenOmitted = ɵɵngDeclareComponent({ + type: TestClass, + template, + }) as ComponentDef; + + 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; + + 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('
'), + directives: [{ + type: TestDir, + selector: '[dir]', + }], + }) as ComponentDef; + + expectComponentDef(def, { + directives: [TestDir], + }); + }); + + it('should compile forward declared directives', () => { + const def = ɵɵngDeclareComponent({ + type: TestClass, + template: createTemplate('
'), + directives: [{ + type: forwardRef(function() { + return ForwardDir; + }), + selector: '[forward]', + }], + }) as ComponentDef; + + @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('
'), + directives: [ + { + type: TestDir, + selector: '[dir]', + }, + { + type: forwardRef(function() { + return ForwardDir; + }), + selector: '[forward]', + } + ], + }) as ComponentDef; + + @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; + + 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; + + @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; + + @Pipe({name: 'forward'}) + class ForwardPipe { + } + + expectComponentDef(def, { + pipes: [TestPipe, ForwardPipe], + }); + }); +}); + +function createTemplate(template: string) { + return {source: template, isInline: true}; +} + +type ComponentDefExpectations = jasmine.Expected, + 'selectors'|'template'|'inputs'|'declaredInputs'|'outputs'|'features'|'hostAttrs'| + 'hostBindings'|'hostVars'|'contentQueries'|'viewQuery'|'exportAs'|'providersResolver'| + 'encapsulation'|'onPush'|'styles'|'data'>>&{ + directives: Type[]|null; + pipes: Type[]|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, expected: Partial): 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 { +}