From fb189c69970dc6ed98eb4a44585353a86893f3eb Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Thu, 7 Mar 2024 16:20:09 -0800 Subject: [PATCH] Revert "refactor(compiler): rework defer block analysis (#54700)" (#54758) This reverts commit eee620aa0013cdb6e860f402d2064c50ada46d4e. --- .../partial_component_linker_1.ts | 34 ++- .../annotations/component/src/handler.ts | 171 ++++++--------- .../annotations/component/src/metadata.ts | 65 +----- packages/compiler/src/jit_compiler_facade.ts | 48 +++-- packages/compiler/src/render3/view/api.ts | 77 +++++-- .../compiler/src/render3/view/compiler.ts | 41 +++- .../compiler/src/render3/view/template.ts | 197 +++++++++++++++++- .../template/pipeline/ir/src/ops/create.ts | 11 +- .../src/template/pipeline/src/compilation.ts | 6 +- .../src/template/pipeline/src/emit.ts | 4 +- .../src/template/pipeline/src/ingest.ts | 20 +- .../src/phases/create_defer_deps_fns.ts | 56 +++++ .../src/phases/resolve_defer_deps_fns.ts | 36 ---- 13 files changed, 496 insertions(+), 270 deletions(-) create mode 100644 packages/compiler/src/template/pipeline/src/phases/create_defer_deps_fns.ts delete mode 100644 packages/compiler/src/template/pipeline/src/phases/resolve_defer_deps_fns.ts 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 874364289af..87f46eee98f 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 {BoundTarget, ChangeDetectionStrategy, compileComponentFromMetadata, ConstantPool, DeclarationListEmitMode, DEFAULT_INTERPOLATION_CONFIG, DeferBlockDepsEmitMode, ForwardRefHandling, InterpolationConfig, makeBindingParser, outputAst as o, parseTemplate, R3ComponentMetadata, R3DeclareComponentMetadata, R3DeclareDirectiveDependencyMetadata, R3DeclarePipeDependencyMetadata, R3DeferMetadata, R3DirectiveDependencyMetadata, R3PartialDeclaration, R3TargetBinder, R3TemplateDependencyKind, R3TemplateDependencyMetadata, SelectorMatcher, TmplAstDeferredBlock, ViewEncapsulation} from '@angular/compiler'; +import {BoundTarget, ChangeDetectionStrategy, compileComponentFromMetadata, ConstantPool, DeclarationListEmitMode, DEFAULT_INTERPOLATION_CONFIG, DeferBlockDepsEmitMode, ForwardRefHandling, InterpolationConfig, makeBindingParser, outputAst as o, parseTemplate, R3ComponentMetadata, R3DeclareComponentMetadata, R3DeclareDirectiveDependencyMetadata, R3DeclarePipeDependencyMetadata, R3DeferBlockMetadata, R3DirectiveDependencyMetadata, R3PartialDeclaration, R3TargetBinder, R3TemplateDependencyKind, R3TemplateDependencyMetadata, SelectorMatcher, TmplAstDeferredBlock, TmplAstDeferredBlockTriggers, TmplAstDeferredTrigger, TmplAstElement, ViewEncapsulation} from '@angular/compiler'; import semver from 'semver'; import {AbsoluteFsPath} from '../../../../src/ngtsc/file_system'; @@ -178,7 +178,13 @@ export class PartialComponentLinkerVersion1 implements declarationListEmitMode, styles: metaObj.has('styles') ? metaObj.getArray('styles').map(entry => entry.getString()) : [], - defer: this.createR3DeferMetadata(boundTarget), + deferBlocks: this.createR3DeferredMetadata(boundTarget), + + // Defer blocks are not yet fully supported in partial compilation. + deferrableDeclToImportDecl: new Map(), + deferrableTypes: new Map(), + deferBlockDepsEmitMode: DeferBlockDepsEmitMode.PerBlock, + encapsulation: metaObj.has('encapsulation') ? parseEncapsulation(metaObj.getValue('encapsulation')) : ViewEncapsulation.Emulated, @@ -258,16 +264,32 @@ export class PartialComponentLinkerVersion1 implements }; } - private createR3DeferMetadata(boundTarget: BoundTarget): R3DeferMetadata { + private createR3DeferredMetadata(boundTarget: BoundTarget): + Map { const deferredBlocks = boundTarget.getDeferBlocks(); - const blocks = new Map(); + const meta = new Map(); for (const block of deferredBlocks) { + const triggerElements = new Map(); + + this.resolveDeferTriggers(block, block.triggers, boundTarget, triggerElements); + this.resolveDeferTriggers(block, block.prefetchTriggers, boundTarget, triggerElements); + // TODO: leaving `deps` empty for now, to be implemented as one of the next steps. - blocks.set(block, null); + meta.set(block, {deps: [], triggerElements}); } - return {mode: DeferBlockDepsEmitMode.PerBlock, blocks}; + return meta; + } + + private resolveDeferTriggers( + block: TmplAstDeferredBlock, triggers: TmplAstDeferredBlockTriggers, + boundTarget: BoundTarget, + triggerElements: Map): void { + Object.keys(triggers).forEach(key => { + const trigger = triggers[key as keyof TmplAstDeferredBlockTriggers]!; + triggerElements.set(trigger, boundTarget.getDeferredTriggerTarget(block, trigger)); + }); } } diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts index ec577540c92..f4e9536c237 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AnimationTriggerNames, BoundTarget, compileClassDebugInfo, compileComponentClassMetadata, compileComponentFromMetadata, compileDeclareClassMetadata, compileDeclareComponentFromMetadata, ConstantPool, CssSelector, DeclarationListEmitMode, DeclareComponentTemplateInfo, DEFAULT_INTERPOLATION_CONFIG, DeferBlockDepsEmitMode, DomElementSchemaRegistry, ExternalExpr, FactoryTarget, makeBindingParser, outputAst as o, R3ComponentMetadata, R3DeferMetadata, R3DirectiveDependencyMetadata, R3NgModuleDependencyMetadata, R3PipeDependencyMetadata, R3TargetBinder, R3TemplateDependency, R3TemplateDependencyKind, R3TemplateDependencyMetadata, SchemaMetadata, SelectorMatcher, TmplAstDeferredBlock, ViewEncapsulation} from '@angular/compiler'; +import {AnimationTriggerNames, BoundTarget, compileClassDebugInfo, compileComponentClassMetadata, compileComponentFromMetadata, compileDeclareClassMetadata, compileDeclareComponentFromMetadata, ConstantPool, CssSelector, DeclarationListEmitMode, DeclareComponentTemplateInfo, DEFAULT_INTERPOLATION_CONFIG, DeferBlockDepsEmitMode, DomElementSchemaRegistry, Expression, ExternalExpr, FactoryTarget, makeBindingParser, R3ComponentMetadata, R3DeferBlockMetadata, R3DeferBlockTemplateDependency, R3DirectiveDependencyMetadata, R3NgModuleDependencyMetadata, R3PipeDependencyMetadata, R3TargetBinder, R3TemplateDependency, R3TemplateDependencyKind, R3TemplateDependencyMetadata, SchemaMetadata, SelectorMatcher, TmplAstDeferredBlock, TmplAstDeferredBlockTriggers, TmplAstDeferredTrigger, TmplAstElement, ViewEncapsulation, WrappedNodeExpr} from '@angular/compiler'; import ts from 'typescript'; import {Cycle, CycleAnalyzer, CycleHandlingStrategy} from '../../../cycles'; @@ -32,7 +32,7 @@ import {extractDirectiveMetadata, parseDirectiveStyles} from '../../directive'; import {createModuleWithProvidersResolver, NgModuleSymbol} from '../../ng_module'; import {checkCustomElementSelectorForErrors, makeCyclicImportInfo} from './diagnostics'; -import {ComponentAnalysisData, ComponentResolutionData, DeferredComponentDependency} from './metadata'; +import {ComponentAnalysisData, ComponentResolutionData} from './metadata'; import {_extractTemplateStyleUrls, extractComponentStyleUrls, extractStyleResources, extractTemplate, makeResourceNotFoundError, ParsedTemplateWithSource, parseTemplateDeclaration, preloadAndParseTemplate, ResourceTypeForDiagnostics, StyleUrlMeta, transformDecoratorResources} from './resources'; import {ComponentSymbol} from './symbol'; import {animationTriggerResolver, collectAnimationNames, validateAndFlattenComponentImports} from './util'; @@ -245,19 +245,19 @@ export class ComponentDecoratorHandler implements resolveEncapsulationEnumValueLocally(component.get('encapsulation'))) ?? ViewEncapsulation.Emulated; - let changeDetection: number|o.Expression|null = null; + let changeDetection: number|Expression|null = null; if (this.compilationMode !== CompilationMode.LOCAL) { changeDetection = resolveEnumValue(this.evaluator, component, 'changeDetection', 'ChangeDetectionStrategy'); } else if (component.has('changeDetection')) { - changeDetection = new o.WrappedNodeExpr(component.get('changeDetection')!); + changeDetection = new WrappedNodeExpr(component.get('changeDetection')!); } - let animations: o.Expression|null = null; + let animations: Expression|null = null; let animationTriggerNames: AnimationTriggerNames|null = null; if (component.has('animations')) { const animationExpression = component.get('animations')!; - animations = new o.WrappedNodeExpr(animationExpression); + animations = new WrappedNodeExpr(animationExpression); const animationsValue = this.evaluator.evaluate(animationExpression, animationTriggerResolver); animationTriggerNames = {includesDynamicAnimations: false, staticTriggerNames: []}; @@ -281,13 +281,13 @@ export class ComponentDecoratorHandler implements // we can distinguish where an error is coming from when logging the diagnostics in `resolve`. let viewProvidersRequiringFactory: Set>|null = null; let providersRequiringFactory: Set>|null = null; - let wrappedViewProviders: o.Expression|null = null; + let wrappedViewProviders: Expression|null = null; if (component.has('viewProviders')) { const viewProviders = component.get('viewProviders')!; viewProvidersRequiringFactory = resolveProvidersRequiringFactory(viewProviders, this.reflector, this.evaluator); - wrappedViewProviders = new o.WrappedNodeExpr( + wrappedViewProviders = new WrappedNodeExpr( this.annotateForClosureCompiler ? wrapFunctionExpressionsInParens(viewProviders) : viewProviders); } @@ -520,7 +520,7 @@ export class ComponentDecoratorHandler implements viewProviders: wrappedViewProviders, i18nUseExternalIds: this.i18nUseExternalIds, relativeContextFilePath, - rawImports: rawImports !== null ? new o.WrappedNodeExpr(rawImports) : undefined, + rawImports: rawImports !== null ? new WrappedNodeExpr(rawImports) : undefined, useTemplatePipeline: this.useTemplatePipeline, }, typeCheckMeta: extractDirectiveTypeCheckMeta(node, inputs, this.reflector), @@ -710,10 +710,10 @@ export class ComponentDecoratorHandler implements declarationListEmitMode: (!analysis.meta.isStandalone || analysis.rawImports !== null) ? DeclarationListEmitMode.RuntimeResolved : DeclarationListEmitMode.Direct, - deferBlockDependencies: this.locateDeferBlocksWithoutScope(analysis.template), + deferBlocks: this.locateDeferBlocksWithoutScope(analysis.template), deferBlockDepsEmitMode: DeferBlockDepsEmitMode.PerComponent, deferrableDeclToImportDecl: new Map(), - deferrableTypes: analysis.explicitlyDeferredTypes ?? new Map(), + deferrableTypes: new Map(), }; if (this.localCompilationExtraImportsTracker === null) { @@ -726,7 +726,7 @@ export class ComponentDecoratorHandler implements data = { declarations: EMPTY_ARRAY, declarationListEmitMode: DeclarationListEmitMode.Direct, - deferBlockDependencies: new Map(), + deferBlocks: new Map(), deferBlockDepsEmitMode: DeferBlockDepsEmitMode.PerBlock, deferrableDeclToImportDecl: new Map(), deferrableTypes: new Map(), @@ -910,7 +910,8 @@ export class ComponentDecoratorHandler implements // Process information related to defer blocks if (this.compilationMode !== CompilationMode.LOCAL) { - this.resolveDeferBlocks(node, deferBlocks, declarations, data, analysis, eagerlyUsed); + this.resolveDeferBlocks( + node, deferBlocks, declarations, data, analysis, eagerlyUsed, bound); } const cyclesFromDirectives = new Map(); @@ -1027,7 +1028,7 @@ export class ComponentDecoratorHandler implements } else { // If there is no scope, we can still use the binder to retrieve *some* information about the // deferred blocks. - data.deferBlockDependencies = this.locateDeferBlocksWithoutScope(metadata.template); + data.deferBlocks = this.locateDeferBlocksWithoutScope(metadata.template); } // Run diagnostics only in global mode. @@ -1047,7 +1048,7 @@ export class ComponentDecoratorHandler implements } if (analysis.providersRequiringFactory !== null && - analysis.meta.providers instanceof o.WrappedNodeExpr) { + analysis.meta.providers instanceof WrappedNodeExpr) { const providerDiagnostics = getProviderDiagnostics( analysis.providersRequiringFactory, analysis.meta.providers!.node, this.injectableRegistry); @@ -1055,7 +1056,7 @@ export class ComponentDecoratorHandler implements } if (analysis.viewProvidersRequiringFactory !== null && - analysis.meta.viewProviders instanceof o.WrappedNodeExpr) { + analysis.meta.viewProviders instanceof WrappedNodeExpr) { const viewProviderDiagnostics = getProviderDiagnostics( analysis.viewProvidersRequiringFactory, analysis.meta.viewProviders!.node, this.injectableRegistry); @@ -1144,7 +1145,7 @@ export class ComponentDecoratorHandler implements const meta: R3ComponentMetadata = { ...analysis.meta, ...resolution, - defer: this.compileDeferBlocks(resolution), + deferrableTypes, useTemplatePipeline, }; const fac = compileNgFactoryDefField(toFactoryMetadata(meta, FactoryTarget.Component)); @@ -1175,7 +1176,7 @@ export class ComponentDecoratorHandler implements sourceUrl: analysis.template.declaration.resolvedTemplateUrl, isInline: analysis.template.declaration.isInline, inlineTemplateLiteralExpression: analysis.template.sourceMapping.type === 'direct' ? - new o.WrappedNodeExpr(analysis.template.sourceMapping.node) : + new WrappedNodeExpr(analysis.template.sourceMapping.node) : null, }; @@ -1183,7 +1184,6 @@ export class ComponentDecoratorHandler implements const meta: R3ComponentMetadata = { ...analysis.meta, ...resolution, - defer: this.compileDeferBlocks(resolution), useTemplatePipeline }; const fac = compileDeclareFactory(toFactoryMetadata(meta, FactoryTarget.Component)); @@ -1213,7 +1213,7 @@ export class ComponentDecoratorHandler implements const meta = { ...analysis.meta, ...resolution, - defer: this.compileDeferBlocks(resolution), + deferrableTypes: deferrableTypes ?? new Map(), useTemplatePipeline, } as R3ComponentMetadata; @@ -1240,15 +1240,18 @@ export class ComponentDecoratorHandler implements * For example, this happens in the local compilation mode. */ private locateDeferBlocksWithoutScope(template: ComponentTemplate): - Map { - const deferBlocks = new Map(); + Map { + const deferBlocks = new Map(); const directivelessBinder = new R3TargetBinder(new SelectorMatcher()); const bound = directivelessBinder.bind({template: template.nodes}); const deferredBlocks = bound.getDeferBlocks(); + const triggerElements = new Map(); for (const block of deferredBlocks) { + this.resolveDeferTriggers(block, block.triggers, bound, triggerElements); + this.resolveDeferTriggers(block, block.prefetchTriggers, bound, triggerElements); // We can't determine the dependencies without a scope so we leave them empty. - deferBlocks.set(block, []); + deferBlocks.set(block, {deps: [], triggerElements}); } return deferBlocks; } @@ -1262,10 +1265,14 @@ export class ComponentDecoratorHandler implements // Go over all dependencies of all defer blocks and update the value of // the `isDeferrable` flag and the `importPath` to reflect the current // state after visiting all components during the `resolve` phase. - for (const [_, deps] of resolution.deferBlockDependencies) { - for (const deferBlockDep of deps) { - const importDecl = - resolution.deferrableDeclToImportDecl.get(deferBlockDep.type.node) ?? null; + for (const [_, metadata] of resolution.deferBlocks) { + for (const deferBlockDep of metadata.deps) { + const dep = deferBlockDep as unknown as { + classDeclaration: ts.ClassDeclaration; + }; + const classDecl = dep.classDeclaration as unknown as Expression; + const importDecl = (resolution.deferrableDeclToImportDecl.get(classDecl) ?? + null) as (ts.ImportDeclaration | null); if (importDecl !== null && this.deferredSymbolTracker.canDefer(importDecl)) { deferBlockDep.isDeferrable = true; deferBlockDep.importPath = (importDecl.moduleSpecifier as ts.StringLiteral).text; @@ -1313,7 +1320,7 @@ export class ComponentDecoratorHandler implements * @returns a `Cycle` object if a cycle would be created, otherwise `null`. */ private _checkForCyclicImport( - importedFile: ImportedFile, expr: o.Expression, origin: ts.SourceFile): Cycle|null { + importedFile: ImportedFile, expr: Expression, origin: ts.SourceFile): Cycle|null { const imported = resolveImportedFile(this.moduleResolver, importedFile, expr, origin); if (imported === null) { return null; @@ -1323,7 +1330,7 @@ export class ComponentDecoratorHandler implements } private maybeRecordSyntheticImport( - importedFile: ImportedFile, expr: o.Expression, origin: ts.SourceFile): void { + importedFile: ImportedFile, expr: Expression, origin: ts.SourceFile): void { const imported = resolveImportedFile(this.moduleResolver, importedFile, expr, origin); if (imported === null) { return; @@ -1343,6 +1350,7 @@ export class ComponentDecoratorHandler implements resolutionData: ComponentResolutionData, analysisData: Readonly, eagerlyUsedDecls: Set, + componentBoundTarget: BoundTarget, ) { // Collect all deferred decls from all defer blocks from the entire template // to intersect with the information from the `imports` field of a particular @@ -1352,14 +1360,9 @@ export class ComponentDecoratorHandler implements for (const [deferBlock, bound] of deferBlocks) { const usedDirectives = new Set(bound.getEagerlyUsedDirectives().map(d => d.ref.node)); const usedPipes = new Set(bound.getEagerlyUsedPipes()); - let deps: DeferredComponentDependency[]; - - if (resolutionData.deferBlockDependencies.has(deferBlock)) { - deps = resolutionData.deferBlockDependencies.get(deferBlock)!; - } else { - deps = []; - resolutionData.deferBlockDependencies.set(deferBlock, deps); - } + const deps: Array = + []; + const triggerElements = new Map(); for (const decl of Array.from(deferrableDecls.values())) { if (decl.kind === R3TemplateDependencyKind.NgModule) { @@ -1376,14 +1379,23 @@ export class ComponentDecoratorHandler implements // `isDeferrable`, `importPath` and `isDefaultImport` will be // added later during the `compile` step. deps.push({ - type: decl.ref, - symbolName: decl.ref.node.name.text, + type: decl.type as WrappedNodeExpr, + symbolName: decl.ref.node.name.escapedText as string, isDeferrable: false, importPath: null, isDefaultImport: false, + // Extra info to match corresponding import during the `compile` phase. + classDeclaration: decl.ref.node as ts.ClassDeclaration, }); allDeferredDecls.add(decl.ref.node); } + + this.resolveDeferTriggers( + deferBlock, deferBlock.triggers, componentBoundTarget, triggerElements); + this.resolveDeferTriggers( + deferBlock, deferBlock.prefetchTriggers, componentBoundTarget, triggerElements); + + resolutionData.deferBlocks.set(deferBlock, {deps, triggerElements}); } // For standalone components with the `imports` and `deferredImports` fields - @@ -1471,78 +1483,23 @@ export class ComponentDecoratorHandler implements // Keep track of how this class made it into the current source file // (which ts.ImportDeclaration was used for this symbol). - resolutionData.deferrableDeclToImportDecl.set(decl.node, imp.node); + resolutionData.deferrableDeclToImportDecl.set( + decl.node as unknown as Expression, imp.node as unknown as Expression); this.deferredSymbolTracker.markAsDeferrableCandidate( node, imp.node, componentClassDecl, isDeferredImport); } } - private compileDeferBlocks(resolution: Readonly>): - R3DeferMetadata { - if (resolution.deferBlockDepsEmitMode === DeferBlockDepsEmitMode.PerBlock) { - if (!resolution.deferBlockDependencies) { - throw new Error( - 'Internal error: deferBlockDependencies must be present when compiling in PerBlock mode'); - } - - const blocks = new Map(); - - for (const [block, dependencies] of resolution.deferBlockDependencies) { - const depExpressions: o.Expression[] = []; - for (const dep of dependencies) { - if (dep.isDeferrable) { - // Callback function, e.g. `m () => m.MyCmp;`. - const innerFn = o.arrowFn( - // Default imports are always accessed through the `default` property. - [new o.FnParam('m', o.DYNAMIC_TYPE)], - o.variable('m').prop(dep.isDefaultImport ? 'default' : dep.symbolName)); - - // Dynamic import, e.g. `import('./a').then(...)`. - const importExpr = - (new o.DynamicImportExpr(dep.importPath!)).prop('then').callFn([innerFn]); - depExpressions.push(importExpr); - } else { - // Non-deferrable symbol, just use a reference to the type. - depExpressions.push(o.variable(dep.symbolName)); - } - } - blocks.set( - block, - depExpressions.length === 0 ? null : o.arrowFn([], o.literalArr(depExpressions))); - } - - return {mode: DeferBlockDepsEmitMode.PerBlock, blocks}; - } - - if (resolution.deferBlockDepsEmitMode === DeferBlockDepsEmitMode.PerComponent) { - if (!resolution.deferBlockDependencies || !resolution.deferrableTypes) { - throw new Error( - 'Internal error: deferBlockDependencies and deferrableTypes must be present in PerComponent mode'); - } - - // This defer block has deps for which we need to generate dynamic imports. - const depExpressions: o.Expression[] = []; - - for (const [symbolName, {importPath, isDefaultImport}] of resolution.deferrableTypes) { - // Callback function, e.g. `m () => m.MyCmp;`. - const innerFn = o.arrowFn( - [new o.FnParam('m', o.DYNAMIC_TYPE)], - o.variable('m').prop(isDefaultImport ? 'default' : symbolName)); - - // Dynamic import, e.g. `import('./a').then(...)`. - const importExpr = (new o.DynamicImportExpr(importPath)).prop('then').callFn([innerFn]); - depExpressions.push(importExpr); - } - - return { - mode: DeferBlockDepsEmitMode.PerComponent, - dependenciesFn: depExpressions.length === 0 ? null : - o.arrowFn([], o.literalArr(depExpressions)) - }; - } - - throw new Error(`Invalid deferBlockDepsEmitMode. Cannot compile deferred block metadata.`); + /** Resolves the triggers of the defer block to the elements that they're pointing to. */ + private resolveDeferTriggers( + block: TmplAstDeferredBlock, triggers: TmplAstDeferredBlockTriggers, + componentBoundTarget: BoundTarget, + triggerElements: Map): void { + Object.keys(triggers).forEach(key => { + const trigger = triggers[key as keyof TmplAstDeferredBlockTriggers]!; + triggerElements.set(trigger, componentBoundTarget.getDeferredTriggerTarget(block, trigger)); + }); } } @@ -1589,8 +1546,8 @@ function removeDeferrableTypesFromComponentDecorator( if (analysis.classMetadata) { const deferrableSymbols = new Set(deferrableTypes.keys()); const rewrittenDecoratorsNode = removeIdentifierReferences( - (analysis.classMetadata.decorators as o.WrappedNodeExpr).node, deferrableSymbols); - analysis.classMetadata.decorators = new o.WrappedNodeExpr(rewrittenDecoratorsNode); + (analysis.classMetadata.decorators as WrappedNodeExpr).node, deferrableSymbols); + analysis.classMetadata.decorators = new WrappedNodeExpr(rewrittenDecoratorsNode); } } diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/metadata.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/metadata.ts index 02b201f8f7c..f1a406d39f6 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/metadata.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/metadata.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {AnimationTriggerNames, DeclarationListEmitMode, DeferBlockDepsEmitMode, R3ClassDebugInfo, R3ClassMetadata, R3ComponentMetadata, R3TemplateDependencyMetadata, SchemaMetadata, TmplAstDeferredBlock} from '@angular/compiler'; +import {AnimationTriggerNames, R3ClassDebugInfo, R3ClassMetadata, R3ComponentMetadata, R3TemplateDependencyMetadata, SchemaMetadata} from '@angular/compiler'; import ts from 'typescript'; import {Reference} from '../../../imports'; @@ -24,7 +24,8 @@ import {ParsedTemplateWithSource, StyleUrlMeta} from './resources'; */ export type ComponentMetadataResolvedFields = SubsetOfKeys< R3ComponentMetadata, - 'declarations'|'declarationListEmitMode'|'defer'>; + 'declarations'|'declarationListEmitMode'|'deferBlocks'|'deferrableDeclToImportDecl'| + 'deferrableTypes'|'deferBlockDepsEmitMode'>; export interface ComponentAnalysisData { /** @@ -89,61 +90,5 @@ export interface ComponentAnalysisData { rawHostDirectives: ts.Expression|null; } -export interface ComponentResolutionData { - declarations: R3TemplateDependencyMetadata[]; - declarationListEmitMode: DeclarationListEmitMode; - - /** - * Map of all types that can be defer loaded (ts.ClassDeclaration) -> - * corresponding import declaration (ts.ImportDeclaration) within - * the current source file. - */ - deferrableDeclToImportDecl: Map; - - /** - * Map of `@defer` blocks -> their corresponding metadata. - */ - deferBlockDependencies: Map; - - /** - * Defines how dynamic imports for deferred dependencies should be grouped: - * - either in a function on per-component basis (in case of local compilation) - * - or in a function on per-block basis (in full compilation mode) - */ - deferBlockDepsEmitMode: DeferBlockDepsEmitMode; - - /** - * Map of deferrable symbol names -> corresponding import paths. - */ - deferrableTypes: Map; -} - -/** - * Describes a dependency used within a `@defer` block. - */ -export interface DeferredComponentDependency { - /** - * Reference to a dependency. - */ - type: Reference; - - /** - * Dependency class name. - */ - symbolName: string; - - /** - * Whether this dependency can be defer-loaded. - */ - isDeferrable: boolean; - - /** - * Import path where this dependency is located. - */ - importPath: string|null; - - /** - * Whether the symbol is the default export. - */ - isDefaultImport: boolean; -} +export type ComponentResolutionData = + Pick, ComponentMetadataResolvedFields>; diff --git a/packages/compiler/src/jit_compiler_facade.ts b/packages/compiler/src/jit_compiler_facade.ts index 4a565a500dc..e67a36b72bd 100644 --- a/packages/compiler/src/jit_compiler_facade.ts +++ b/packages/compiler/src/jit_compiler_facade.ts @@ -11,17 +11,17 @@ import {ConstantPool} from './constant_pool'; import {ChangeDetectionStrategy, HostBinding, HostListener, Input, Output, ViewEncapsulation} from './core'; import {compileInjectable} from './injectable_compiler_2'; import {DEFAULT_INTERPOLATION_CONFIG, InterpolationConfig} from './ml_parser/defaults'; -import {ArrowFunctionExpr, DeclareVarStmt, Expression, literal, LiteralExpr, Statement, StmtModifier, WrappedNodeExpr} from './output/output_ast'; +import {DeclareVarStmt, Expression, literal, LiteralExpr, Statement, StmtModifier, WrappedNodeExpr} from './output/output_ast'; import {JitEvaluator} from './output/output_jit'; import {ParseError, ParseSourceSpan, r3JitTypeSourceSpan} from './parse_util'; -import {DeferredBlock} from './render3/r3_ast'; +import {DeferredBlock, DeferredBlockTriggers, DeferredTrigger, Element} from './render3/r3_ast'; import {compileFactoryFunction, FactoryTarget, R3DependencyMetadata} from './render3/r3_factory'; import {compileInjector, R3InjectorMetadata} from './render3/r3_injector_compiler'; import {R3JitReflector} from './render3/r3_jit'; import {compileNgModule, compileNgModuleDeclarationExpression, R3NgModuleMetadata, R3NgModuleMetadataKind, R3SelectorScopeMode} from './render3/r3_module_compiler'; import {compilePipeFromMetadata, R3PipeMetadata} from './render3/r3_pipe_compiler'; import {createMayBeForwardRefExpression, ForwardRefHandling, getSafePropertyAccessString, MaybeForwardRefExpression, wrapReference} from './render3/util'; -import {DeclarationListEmitMode, DeferBlockDepsEmitMode, R3ComponentMetadata, R3DeferMetadata, R3DirectiveDependencyMetadata, R3DirectiveMetadata, R3HostDirectiveMetadata, R3HostMetadata, R3InputMetadata, R3PipeDependencyMetadata, R3QueryMetadata, R3TemplateDependency, R3TemplateDependencyKind, R3TemplateDependencyMetadata} from './render3/view/api'; +import {DeclarationListEmitMode, DeferBlockDepsEmitMode, R3ComponentMetadata, R3DeferBlockMetadata, R3DirectiveDependencyMetadata, R3DirectiveMetadata, R3HostDirectiveMetadata, R3HostMetadata, R3InputMetadata, R3PipeDependencyMetadata, R3QueryMetadata, R3TemplateDependency, R3TemplateDependencyKind, R3TemplateDependencyMetadata} from './render3/view/api'; import {compileComponentFromMetadata, compileDirectiveFromMetadata, ParsedHostBindings, parseHostBindings, verifyHostBindings} from './render3/view/compiler'; import type {BoundTarget} from './render3/view/t2_api'; import {R3TargetBinder} from './render3/view/t2_binder'; @@ -182,7 +182,7 @@ export class CompilerFacadeImpl implements CompilerFacade { angularCoreEnv: CoreEnvironment, sourceMapUrl: string, facade: R3ComponentMetadataFacade): any { // Parse the template and check for errors. - const {template, interpolation, defer} = parseJitTemplate( + const {template, interpolation, deferBlocks} = parseJitTemplate( facade.template, facade.name, sourceMapUrl, facade.preserveWhitespaces, facade.interpolation); @@ -194,7 +194,10 @@ export class CompilerFacadeImpl implements CompilerFacade { template, declarations: facade.declarations.map(convertDeclarationFacadeToMetadata), declarationListEmitMode: DeclarationListEmitMode.Direct, - defer, + deferBlocks, + deferrableTypes: new Map(), + deferrableDeclToImportDecl: new Map(), + deferBlockDepsEmitMode: DeferBlockDepsEmitMode.PerBlock, styles: [...facade.styles, ...template.styles], encapsulation: facade.encapsulation, @@ -444,7 +447,7 @@ function convertOpaqueValuesToExpressions(obj: {[key: string]: OpaqueValue}): function convertDeclareComponentFacadeToMetadata( decl: R3DeclareComponentFacade, typeSourceSpan: ParseSourceSpan, sourceMapUrl: string): R3ComponentMetadata { - const {template, interpolation, defer} = parseJitTemplate( + const {template, interpolation, deferBlocks} = parseJitTemplate( decl.template, decl.type.name, sourceMapUrl, decl.preserveWhitespaces ?? false, decl.interpolation); @@ -481,7 +484,10 @@ function convertDeclareComponentFacadeToMetadata( viewProviders: decl.viewProviders !== undefined ? new WrappedNodeExpr(decl.viewProviders) : null, animations: decl.animations !== undefined ? new WrappedNodeExpr(decl.animations) : null, - defer, + deferBlocks, + deferrableTypes: new Map(), + deferrableDeclToImportDecl: new Map(), + deferBlockDepsEmitMode: DeferBlockDepsEmitMode.PerBlock, changeDetection: decl.changeDetection ?? ChangeDetectionStrategy.Default, encapsulation: decl.encapsulation ?? ViewEncapsulation.Emulated, @@ -556,7 +562,7 @@ function parseJitTemplate( return { template: parsed, interpolation: interpolationConfig, - defer: createR3DeferMetadata(boundTarget) + deferBlocks: createR3DeferredMetadata(boundTarget) }; } @@ -627,17 +633,31 @@ function createR3DependencyMetadata( return {token, attributeNameType, host, optional, self, skipSelf}; } -function createR3DeferMetadata(boundTarget: BoundTarget): R3DeferMetadata { +function createR3DeferredMetadata(boundTarget: BoundTarget): + Map { const deferredBlocks = boundTarget.getDeferBlocks(); - const blocks = new Map(); + const meta = new Map(); for (const block of deferredBlocks) { - // TODO: leaving dependency function empty in JIT mode for now, - // to be implemented as one of the next steps. - blocks.set(block, null); + const triggerElements = new Map(); + + resolveDeferTriggers(block, block.triggers, boundTarget, triggerElements); + resolveDeferTriggers(block, block.prefetchTriggers, boundTarget, triggerElements); + + // TODO: leaving `deps` empty in JIT mode for now, to be implemented as one of the next steps. + meta.set(block, {deps: [], triggerElements}); } - return {mode: DeferBlockDepsEmitMode.PerBlock, blocks}; + return meta; +} + +function resolveDeferTriggers( + block: DeferredBlock, triggers: DeferredBlockTriggers, boundTarget: BoundTarget, + triggerElements: Map): void { + Object.keys(triggers).forEach(key => { + const trigger = triggers[key as keyof DeferredBlockTriggers]!; + triggerElements.set(trigger, boundTarget.getDeferredTriggerTarget(block, trigger)); + }); } function extractHostBindings( diff --git a/packages/compiler/src/render3/view/api.ts b/packages/compiler/src/render3/view/api.ts index f59b74b0e3e..07fb2d0dcc7 100644 --- a/packages/compiler/src/render3/view/api.ts +++ b/packages/compiler/src/render3/view/api.ts @@ -196,6 +196,47 @@ export const enum DeclarationListEmitMode { RuntimeResolved, } +/** + * Describes a dependency used within a `@defer` block. + */ +export interface R3DeferBlockTemplateDependency { + /** + * Reference to a dependency. + */ + type: o.WrappedNodeExpr; + + /** + * Dependency class name. + */ + symbolName: string; + + /** + * Whether this dependency can be defer-loaded. + */ + isDeferrable: boolean; + + /** + * Import path where this dependency is located. + */ + importPath: string|null; + + /** + * Whether the symbol is the default export. + */ + isDefaultImport: boolean; +} + +/** + * Information necessary to compile a `defer` block. + */ +export interface R3DeferBlockMetadata { + /** Dependencies used within the block. */ + deps: R3DeferBlockTemplateDependency[]; + + /** Mapping between triggers and the DOM nodes they refer to. */ + triggerElements: Map; +} + /** * Information needed to compile a component for the render3 runtime. */ @@ -224,8 +265,29 @@ export interface R3ComponentMetadata declarations: DeclarationT[]; - /** Metadata related to the deferred blocks in the component's template. */ - defer: R3DeferMetadata; + /** + * Map of all types that can be defer loaded (ts.ClassDeclaration) -> + * corresponding import declaration (ts.ImportDeclaration) within + * the current source file. + */ + deferrableDeclToImportDecl: Map; + + /** + * Map of `@defer` blocks -> their corresponding metadata. + */ + deferBlocks: Map; + + /** + * Defines how dynamic imports for deferred dependencies should be grouped: + * - either in a function on per-component basis (in case of local compilation) + * - or in a function on per-block basis (in full compilation mode) + */ + deferBlockDepsEmitMode: DeferBlockDepsEmitMode; + + /** + * Map of deferrable symbol names -> corresponding import paths. + */ + deferrableTypes: Map; /** * Specifies how the 'directives' and/or `pipes` array, if generated, need to be emitted. @@ -294,17 +356,6 @@ export interface R3ComponentMetadata useTemplatePipeline: boolean; } -/** - * Information about the deferred blocks in a component's template. - */ -export type R3DeferMetadata = { - mode: DeferBlockDepsEmitMode.PerBlock, - blocks: Map, -}|{ - mode: DeferBlockDepsEmitMode.PerComponent, - dependenciesFn: o.ArrowFunctionExpr | null, -}; - /** * Metadata for an individual input on a directive. */ diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index bcccd0f0d14..c4862e4486a 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -159,6 +159,34 @@ export function compileDirectiveFromMetadata( return {expression, type, statements: []}; } +/** + * Creates an AST for a function that contains dynamic imports representing + * deferrable dependencies. + */ +function createDeferredDepsFunction( + constantPool: ConstantPool, name: string, + deps: Map) { + // This defer block has deps for which we need to generate dynamic imports. + const dependencyExp: o.Expression[] = []; + + for (const [symbolName, {importPath, isDefaultImport}] of deps) { + // Callback function, e.g. `m () => m.MyCmp;`. + const innerFn = o.arrowFn( + [new o.FnParam('m', o.DYNAMIC_TYPE)], + o.variable('m').prop(isDefaultImport ? 'default' : symbolName)); + + // Dynamic import, e.g. `import('./a').then(...)`. + const importExpr = (new o.DynamicImportExpr(importPath)).prop('then').callFn([innerFn]); + dependencyExp.push(importExpr); + } + + const depsFnExpr = o.arrowFn([], o.literalArr(dependencyExp)); + + constantPool.statements.push(depsFnExpr.toDeclStmt(name, o.StmtModifier.Final)); + + return o.variable(name); +} + /** * Compile a component for the render3 runtime as defined by the `R3ComponentMetadata`. */ @@ -191,12 +219,10 @@ export function compileComponentFromMetadata( let allDeferrableDepsFn: o.ReadVarExpr|null = null; - if (meta.defer.mode === DeferBlockDepsEmitMode.PerComponent && - meta.defer.dependenciesFn !== null) { + if (meta.deferBlocks.size > 0 && meta.deferrableTypes.size > 0 && + meta.deferBlockDepsEmitMode === DeferBlockDepsEmitMode.PerComponent) { const fnName = `${templateTypeName}_DeferFn`; - constantPool.statements.push( - meta.defer.dependenciesFn.toDeclStmt(fnName, o.StmtModifier.Final)); - allDeferrableDepsFn = o.variable(fnName); + allDeferrableDepsFn = createDeferredDepsFunction(constantPool, fnName, meta.deferrableTypes); } // Template compilation is currently conditional as we're in the process of rewriting it. @@ -207,7 +233,8 @@ export function compileComponentFromMetadata( const template = meta.template; const templateBuilder = new TemplateDefinitionBuilder( constantPool, BindingScope.createRootScope(), 0, templateTypeName, null, null, templateName, - R3.namespaceHTML, meta.relativeContextFilePath, meta.i18nUseExternalIds, new Map()); + R3.namespaceHTML, meta.relativeContextFilePath, meta.i18nUseExternalIds, meta.deferBlocks, + new Map(), allDeferrableDepsFn); const templateFunctionExpression = templateBuilder.buildTemplateFunction(template.nodes, []); @@ -249,7 +276,7 @@ export function compileComponentFromMetadata( // ingested into IR: const tpl = ingestComponent( meta.name, meta.template.nodes, constantPool, meta.relativeContextFilePath, - meta.i18nUseExternalIds, meta.defer, allDeferrableDepsFn); + meta.i18nUseExternalIds, meta.deferBlocks, allDeferrableDepsFn); // Then the IR is transformed to prepare it for cod egeneration. transform(tpl, CompilationJobKind.Tmpl); diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index be36711a4f7..1d78b8da761 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -25,6 +25,7 @@ import {ParseError, ParseSourceSpan, sanitizeIdentifier} from '../../parse_util' import {DomElementSchemaRegistry} from '../../schema/dom_element_schema_registry'; import {isIframeSecuritySensitiveAttr} from '../../schema/dom_security_schema'; import {isTrustedTypesSink} from '../../schema/trusted_types_sinks'; +import {CssSelector} from '../../selector'; import {BindingParser} from '../../template_parser/binding_parser'; import {error, partitionArray} from '../../util'; import * as t from '../r3_ast'; @@ -32,6 +33,7 @@ import {Identifiers as R3} from '../r3_identifiers'; import {htmlAstToRender3Ast} from '../r3_template_transform'; import {prepareSyntheticListenerFunctionName, prepareSyntheticListenerName, prepareSyntheticPropertyName} from '../util'; +import {R3DeferBlockMetadata} from './api'; import {I18nContext} from './i18n/context'; import {createGoogleGetMsgStatements} from './i18n/get_msg_utils'; import {createLocalizeStatements} from './i18n/localize_utils'; @@ -248,7 +250,9 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver private templateIndex: number|null, private templateName: string|null, private _namespace: o.ExternalReference, relativeContextFilePath: string, private i18nUseExternalIds: boolean, + private deferBlocks: Map, private elementLocations: Map, + private allDeferrableDepsFn: o.ReadVarExpr|null, private _constants: ComponentDefConsts = createComponentDefConsts()) { this._bindingScope = parentBindingScope.nestedScope(level); @@ -968,8 +972,8 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver // Create the template function const visitor = new TemplateDefinitionBuilder( this.constantPool, this._bindingScope, this.level + 1, contextName, this.i18n, index, name, - this._namespace, this.fileBasedI18nSuffix, this.i18nUseExternalIds, this.elementLocations, - this._constants); + this._namespace, this.fileBasedI18nSuffix, this.i18nUseExternalIds, this.deferBlocks, + this.elementLocations, this.allDeferrableDepsFn, this._constants); // Nested templates must not be visited until after their parent templates have completed // processing, so they are queued here until after the initial pass. Otherwise, we wouldn't @@ -1323,9 +1327,192 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver } visitDeferredBlock(deferred: t.DeferredBlock): void { - throw new Error( - 'Deferred blocks are no longer supported by the obsolete TemplateDefinitionBuilder, ' + - 'Please enable the new template compilation pipeline for your application.'); + const {loading, placeholder, error, triggers, prefetchTriggers} = deferred; + const metadata = this.deferBlocks.get(deferred); + + if (!metadata) { + throw new Error('Could not resolve `defer` block metadata. Block may need to be analyzed.'); + } + + const primaryTemplateIndex = this.createEmbeddedTemplateFn( + null, deferred.children, '_Defer', deferred.sourceSpan, undefined, undefined, undefined, + deferred.i18n); + const loadingIndex = loading ? this.createEmbeddedTemplateFn( + null, loading.children, '_DeferLoading', loading.sourceSpan, + undefined, undefined, undefined, loading.i18n) : + null; + const loadingConsts = loading ? + trimTrailingNulls([o.literal(loading.minimumTime), o.literal(loading.afterTime)]) : + null; + + const placeholderIndex = placeholder ? + this.createEmbeddedTemplateFn( + null, placeholder.children, '_DeferPlaceholder', placeholder.sourceSpan, undefined, + undefined, undefined, placeholder.i18n) : + null; + const placeholderConsts = placeholder && placeholder.minimumTime !== null ? + // TODO(crisbeto): potentially pass the time directly instead of storing it in the `consts` + // since the placeholder block can only have one parameter? + o.literalArr([o.literal(placeholder.minimumTime)]) : + null; + + const errorIndex = error ? this.createEmbeddedTemplateFn( + null, error.children, '_DeferError', error.sourceSpan, undefined, + undefined, undefined, error.i18n) : + null; + + // Note: we generate this last so the index matches the instruction order. + const deferredIndex = this.allocateDataSlot(); + const depsFnName = `${this.contextName}_Defer_${deferredIndex}_DepsFn`; + + // e.g. `defer(1, 0, MyComp_Defer_1_DepsFn, ...)` + this.creationInstruction( + deferred.sourceSpan, R3.defer, trimTrailingNulls([ + o.literal(deferredIndex), + o.literal(primaryTemplateIndex), + this.allDeferrableDepsFn ?? this.createDeferredDepsFunction(depsFnName, metadata), + o.literal(loadingIndex), + o.literal(placeholderIndex), + o.literal(errorIndex), + loadingConsts?.length ? this.addToConsts(o.literalArr(loadingConsts)) : o.TYPED_NULL_EXPR, + placeholderConsts ? this.addToConsts(placeholderConsts) : o.TYPED_NULL_EXPR, + (loadingConsts?.length || placeholderConsts) ? + o.importExpr(R3.deferEnableTimerScheduling) : + o.TYPED_NULL_EXPR, + ])); + + // Allocate an extra data slot right after a defer block slot to store + // instance-specific state of that defer block at runtime. + this.allocateDataSlot(); + + // Note: the triggers need to be processed *after* the various templates, + // otherwise pipes injecting some symbols won't work (see #52102). + this.createDeferTriggerInstructions(deferredIndex, triggers, metadata, false); + this.createDeferTriggerInstructions(deferredIndex, prefetchTriggers, metadata, true); + } + + private createDeferredDepsFunction(name: string, metadata: R3DeferBlockMetadata) { + if (metadata.deps.length === 0) { + return o.TYPED_NULL_EXPR; + } + + // This defer block has deps for which we need to generate dynamic imports. + const dependencyExp: o.Expression[] = []; + + for (const deferredDep of metadata.deps) { + if (deferredDep.isDeferrable) { + // Callback function, e.g. `m () => m.MyCmp;`. + const innerFn = o.arrowFn( + [new o.FnParam('m', o.DYNAMIC_TYPE)], + // Default imports are always accessed through the `default` property. + o.variable('m').prop(deferredDep.isDefaultImport ? 'default' : deferredDep.symbolName)); + + // Dynamic import, e.g. `import('./a').then(...)`. + const importExpr = + (new o.DynamicImportExpr(deferredDep.importPath!)).prop('then').callFn([innerFn]); + dependencyExp.push(importExpr); + } else { + // Non-deferrable symbol, just use a reference to the type. + dependencyExp.push(deferredDep.type); + } + } + + const depsFnExpr = o.arrowFn([], o.literalArr(dependencyExp)); + + this.constantPool.statements.push(depsFnExpr.toDeclStmt(name, o.StmtModifier.Final)); + + return o.variable(name); + } + + private createDeferTriggerInstructions( + deferredIndex: number, triggers: t.DeferredBlockTriggers, metadata: R3DeferBlockMetadata, + prefetch: boolean) { + const {when, idle, immediate, timer, hover, interaction, viewport} = triggers; + + // `deferWhen(ctx.someValue)` + if (when) { + const value = when.value.visit(this._valueConverter); + this.allocateBindingSlots(value); + this.updateInstructionWithAdvance( + deferredIndex, when.sourceSpan, prefetch ? R3.deferPrefetchWhen : R3.deferWhen, + () => this.convertPropertyBinding(value)); + } + + // Note that we generate an implicit `on idle` if the `deferred` block has no triggers. + // `deferOnIdle()` + if (idle || (!prefetch && Object.keys(triggers).length === 0)) { + this.creationInstruction( + idle?.sourceSpan || null, prefetch ? R3.deferPrefetchOnIdle : R3.deferOnIdle); + } + + // `deferOnImmediate()` + if (immediate) { + this.creationInstruction( + immediate.sourceSpan, prefetch ? R3.deferPrefetchOnImmediate : R3.deferOnImmediate); + } + + // `deferOnTimer(1337)` + if (timer) { + this.creationInstruction( + timer.sourceSpan, prefetch ? R3.deferPrefetchOnTimer : R3.deferOnTimer, + [o.literal(timer.delay)]); + } + + // `deferOnHover(index, walkUpTimes)` + if (hover) { + this.domNodeBasedTrigger( + 'hover', hover, metadata, prefetch ? R3.deferPrefetchOnHover : R3.deferOnHover); + } + + // `deferOnInteraction(index, walkUpTimes)` + if (interaction) { + this.domNodeBasedTrigger( + 'interaction', interaction, metadata, + prefetch ? R3.deferPrefetchOnInteraction : R3.deferOnInteraction); + } + + // `deferOnViewport(index, walkUpTimes)` + if (viewport) { + this.domNodeBasedTrigger( + 'viewport', viewport, metadata, + prefetch ? R3.deferPrefetchOnViewport : R3.deferOnViewport); + } + } + + private domNodeBasedTrigger( + name: string, + trigger: t.InteractionDeferredTrigger|t.HoverDeferredTrigger|t.ViewportDeferredTrigger, + metadata: R3DeferBlockMetadata, instructionRef: o.ExternalReference) { + const triggerEl = metadata.triggerElements.get(trigger); + + // Don't generate anything if a trigger cannot be resolved. + // We'll have template diagnostics to surface these to users. + if (!triggerEl) { + return; + } + + this.creationInstruction(trigger.sourceSpan, instructionRef, () => { + const location = this.elementLocations.get(triggerEl); + + if (!location) { + throw new Error( + `Could not determine location of reference passed into ` + + `'${name}' trigger. Template may not have been fully analyzed.`); + } + + // A negative depth means that the trigger is inside the placeholder. + // Cap it at -1 since we only care whether or not it's negative. + const depth = Math.max(this.level - location.level, -1); + const params = [o.literal(location.index)]; + + // The most common case should be a trigger within the view so we can omit a depth of + // zero. For triggers in parent views and in the placeholder we need to pass it in. + if (depth !== 0) { + params.push(o.literal(depth)); + } + + return params; + }); } /** diff --git a/packages/compiler/src/template/pipeline/ir/src/ops/create.ts b/packages/compiler/src/template/pipeline/ir/src/ops/create.ts index 34c24f6d9a0..8c74481ee5a 100644 --- a/packages/compiler/src/template/pipeline/ir/src/ops/create.ts +++ b/packages/compiler/src/template/pipeline/ir/src/ops/create.ts @@ -10,6 +10,7 @@ import {SecurityContext} from '../../../../../core'; import * as i18n from '../../../../../i18n/i18n_ast'; import * as o from '../../../../../output/output_ast'; import {ParseSourceSpan} from '../../../../../parse_util'; +import {R3DeferBlockMetadata} from '../../../../../render3/view/api'; import {BindingKind, DeferTriggerKind, I18nContextKind, I18nParamValueFlags, Namespace, OpKind, TemplateKind} from '../enums'; import {SlotHandle} from '../handle'; import {Op, OpList, XrefId} from '../operations'; @@ -837,11 +838,9 @@ export interface DeferOp extends Op, ConsumesSlotOpTrait { loadingConfig: o.Expression|null; /** - * Depending on the compilation mode, there can be either one dependency resolution function - * per deferred block or one for the entire template. This field contains the function that - * belongs specifically to the current deferred block. + * Metadata about this defer block, provided by the parser. */ - ownResolverFn: o.ArrowFunctionExpr|null; + metadata: R3DeferBlockMetadata; /** * After processing, the resolver function for the defer deps will be extracted to the constant @@ -853,7 +852,7 @@ export interface DeferOp extends Op, ConsumesSlotOpTrait { } export function createDeferOp( - xref: XrefId, main: XrefId, mainSlot: SlotHandle, ownResolverFn: o.ArrowFunctionExpr|null, + xref: XrefId, main: XrefId, mainSlot: SlotHandle, metadata: R3DeferBlockMetadata, resolverFn: o.Expression|null, sourceSpan: ParseSourceSpan): DeferOp { return { kind: OpKind.Defer, @@ -872,7 +871,7 @@ export function createDeferOp( placeholderMinimumTime: null, errorView: null, errorSlot: null, - ownResolverFn, + metadata, resolverFn, sourceSpan, ...NEW_OP, diff --git a/packages/compiler/src/template/pipeline/src/compilation.ts b/packages/compiler/src/template/pipeline/src/compilation.ts index 8e1b02357fe..ab76bfe9b5e 100644 --- a/packages/compiler/src/template/pipeline/src/compilation.ts +++ b/packages/compiler/src/template/pipeline/src/compilation.ts @@ -8,7 +8,8 @@ import {ConstantPool} from '../../../constant_pool'; import * as o from '../../../output/output_ast'; -import {R3DeferMetadata} from '../../../render3/view/api'; +import * as t from '../../../render3/r3_ast'; +import {R3DeferBlockMetadata} from '../../../render3/view/api'; import * as ir from '../ir'; export enum CompilationJobKind { @@ -66,7 +67,8 @@ export class ComponentCompilationJob extends CompilationJob { constructor( componentName: string, pool: ConstantPool, compatibility: ir.CompatibilityMode, readonly relativeContextFilePath: string, readonly i18nUseExternalIds: boolean, - readonly deferMeta: R3DeferMetadata, readonly allDeferrableDepsFn: o.ReadVarExpr|null) { + readonly deferBlocksMeta: Map, + readonly allDeferrableDepsFn: o.ReadVarExpr|null) { super(componentName, pool, compatibility); this.root = new ViewCompilationUnit(this, this.allocateXrefId(), null); this.views.set(this.root.xref, this.root); diff --git a/packages/compiler/src/template/pipeline/src/emit.ts b/packages/compiler/src/template/pipeline/src/emit.ts index 42a82e834e3..6dcb863864d 100644 --- a/packages/compiler/src/template/pipeline/src/emit.ts +++ b/packages/compiler/src/template/pipeline/src/emit.ts @@ -23,7 +23,7 @@ import {collapseSingletonInterpolations} from './phases/collapse_singleton_inter import {generateConditionalExpressions} from './phases/conditionals'; import {collectElementConsts} from './phases/const_collection'; import {convertI18nBindings} from './phases/convert_i18n_bindings'; -import {resolveDeferDepsFns} from './phases/resolve_defer_deps_fns'; +import {createDeferDepsFns} from './phases/create_defer_deps_fns'; import {createI18nContexts} from './phases/create_i18n_contexts'; import {deduplicateTextBindings} from './phases/deduplicate_text_bindings'; import {configureDeferInstructions} from './phases/defer_configs'; @@ -140,7 +140,7 @@ const phases: Phase[] = [ {kind: Kind.Tmpl, fn: generateAdvance}, {kind: Kind.Both, fn: optimizeVariables}, {kind: Kind.Both, fn: nameFunctionsAndVariables}, - {kind: Kind.Tmpl, fn: resolveDeferDepsFns}, + {kind: Kind.Tmpl, fn: createDeferDepsFns}, {kind: Kind.Tmpl, fn: mergeNextContextExpressions}, {kind: Kind.Tmpl, fn: generateNgContainerOps}, {kind: Kind.Tmpl, fn: collapseEmptyInstructions}, diff --git a/packages/compiler/src/template/pipeline/src/ingest.ts b/packages/compiler/src/template/pipeline/src/ingest.ts index 609e36fb011..f2a43f32ccb 100644 --- a/packages/compiler/src/template/pipeline/src/ingest.ts +++ b/packages/compiler/src/template/pipeline/src/ingest.ts @@ -14,7 +14,7 @@ import {splitNsName} from '../../../ml_parser/tags'; import * as o from '../../../output/output_ast'; import {ParseSourceSpan} from '../../../parse_util'; import * as t from '../../../render3/r3_ast'; -import {DeferBlockDepsEmitMode, R3DeferMetadata} from '../../../render3/view/api'; +import {R3DeferBlockMetadata} from '../../../render3/view/api'; import {icuFromI18nMessage, isSingleI18nIcu} from '../../../render3/view/i18n/util'; import {DomElementSchemaRegistry} from '../../../schema/dom_element_schema_registry'; import {BindingParser} from '../../../template_parser/binding_parser'; @@ -38,11 +38,12 @@ const NG_TEMPLATE_TAG_NAME = 'ng-template'; */ export function ingestComponent( componentName: string, template: t.Node[], constantPool: ConstantPool, - relativeContextFilePath: string, i18nUseExternalIds: boolean, deferMeta: R3DeferMetadata, + relativeContextFilePath: string, i18nUseExternalIds: boolean, + deferBlocksMeta: Map, allDeferrableDepsFn: o.ReadVarExpr|null): ComponentCompilationJob { const job = new ComponentCompilationJob( componentName, constantPool, compatibilityMode, relativeContextFilePath, i18nUseExternalIds, - deferMeta, allDeferrableDepsFn); + deferBlocksMeta, allDeferrableDepsFn); ingestNodes(job.root, template); return job; } @@ -442,14 +443,9 @@ function ingestDeferView( } function ingestDeferBlock(unit: ViewCompilationUnit, deferBlock: t.DeferredBlock): void { - let ownResolverFn: o.ArrowFunctionExpr|null = null; - - if (unit.job.deferMeta.mode === DeferBlockDepsEmitMode.PerBlock) { - if (!unit.job.deferMeta.blocks.has(deferBlock)) { - throw new Error( - `AssertionError: unable to find a dependency function for this deferred block`); - } - ownResolverFn = unit.job.deferMeta.blocks.get(deferBlock) ?? null; + const blockMeta = unit.job.deferBlocksMeta.get(deferBlock); + if (blockMeta === undefined) { + throw new Error(`AssertionError: unable to find metadata for deferred block`); } // Generate the defer main view and all secondary views. @@ -468,7 +464,7 @@ function ingestDeferBlock(unit: ViewCompilationUnit, deferBlock: t.DeferredBlock // Create the main defer op, and ops for all secondary views. const deferXref = unit.job.allocateXrefId(); const deferOp = ir.createDeferOp( - deferXref, main.xref, main.handle, ownResolverFn, unit.job.allDeferrableDepsFn, + deferXref, main.xref, main.handle, blockMeta, unit.job.allDeferrableDepsFn, deferBlock.sourceSpan); deferOp.placeholderView = placeholder?.xref ?? null; deferOp.placeholderSlot = placeholder?.handle ?? null; diff --git a/packages/compiler/src/template/pipeline/src/phases/create_defer_deps_fns.ts b/packages/compiler/src/template/pipeline/src/phases/create_defer_deps_fns.ts new file mode 100644 index 00000000000..dd2fbd9a425 --- /dev/null +++ b/packages/compiler/src/template/pipeline/src/phases/create_defer_deps_fns.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 + */ + +import * as o from '../../../../output/output_ast'; +import * as ir from '../../ir'; +import {ComponentCompilationJob} from '../compilation'; + +/** + * Create extracted deps functions for defer ops. + */ +export function createDeferDepsFns(job: ComponentCompilationJob): void { + for (const unit of job.units) { + for (const op of unit.create) { + if (op.kind === ir.OpKind.Defer) { + if (op.metadata.deps.length === 0) { + continue; + } + if (op.resolverFn !== null) { + continue; + } + const dependencies: o.Expression[] = []; + for (const dep of op.metadata.deps) { + if (dep.isDeferrable) { + // Callback function, e.g. `m () => m.MyCmp;`. + const innerFn = o.arrowFn( + // Default imports are always accessed through the `default` property. + [new o.FnParam('m', o.DYNAMIC_TYPE)], + o.variable('m').prop(dep.isDefaultImport ? 'default' : dep.symbolName)); + + // Dynamic import, e.g. `import('./a').then(...)`. + const importExpr = + (new o.DynamicImportExpr(dep.importPath!)).prop('then').callFn([innerFn]); + dependencies.push(importExpr); + } else { + // Non-deferrable symbol, just use a reference to the type. + dependencies.push(dep.type); + } + } + const depsFnExpr = o.arrowFn([], o.literalArr(dependencies)); + if (op.handle.slot === null) { + throw new Error( + 'AssertionError: slot must be assigned bfore extracting defer deps functions'); + } + const fullPathName = unit.fnName?.replace(`_Template`, ``); + op.resolverFn = job.pool.getSharedFunctionReference( + depsFnExpr, `${fullPathName}_Defer_${op.handle.slot}_DepsFn`, + /* Don't use unique names for TDB compatibility */ false); + } + } + } +} diff --git a/packages/compiler/src/template/pipeline/src/phases/resolve_defer_deps_fns.ts b/packages/compiler/src/template/pipeline/src/phases/resolve_defer_deps_fns.ts deleted file mode 100644 index b1bfd48919c..00000000000 --- a/packages/compiler/src/template/pipeline/src/phases/resolve_defer_deps_fns.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @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 * as ir from '../../ir'; -import {ComponentCompilationJob} from '../compilation'; - -/** - * Resolve the dependency function of a deferred block. - */ -export function resolveDeferDepsFns(job: ComponentCompilationJob): void { - for (const unit of job.units) { - for (const op of unit.create) { - if (op.kind === ir.OpKind.Defer) { - if (op.resolverFn !== null) { - continue; - } - - if (op.ownResolverFn !== null) { - if (op.handle.slot === null) { - throw new Error( - 'AssertionError: slot must be assigned before extracting defer deps functions'); - } - const fullPathName = unit.fnName?.replace('_Template', ''); - op.resolverFn = job.pool.getSharedFunctionReference( - op.ownResolverFn, `${fullPathName}_Defer_${op.handle.slot}_DepsFn`, - /* Don't use unique names for TDB compatibility */ false); - } - } - } - } -}