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 75937df6ccf68..6bc6a64f257ef 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts @@ -12,8 +12,7 @@ import ts from 'typescript'; import {Cycle, CycleAnalyzer, CycleHandlingStrategy} from '../../../cycles'; import {ErrorCode, FatalDiagnosticError, makeDiagnostic, makeRelatedInformation} from '../../../diagnostics'; import {absoluteFrom, relative} from '../../../file_system'; -import {assertSuccessfulReferenceEmit, ImportedFile, ModuleResolver, Reference, ReferenceEmitter} from '../../../imports'; -import {DeferredSymbolTracker} from '../../../imports/src/deferred_symbol_tracker'; +import {assertSuccessfulReferenceEmit, DeferredSymbolTracker, ImportedFile, ModuleResolver, Reference, ReferenceEmitter} from '../../../imports'; import {DependencyTracker} from '../../../incremental/api'; import {extractSemanticTypeParameters, SemanticDepGraphUpdater} from '../../../incremental/semantic_graph'; import {IndexingContext} from '../../../indexer'; @@ -987,13 +986,15 @@ export class ComponentDecoratorHandler implements // the `isDeferrable` flag and the `importPath` to reflect the current // state after visiting all components during the `resolve` phase. for (const [_, deferBlockDeps] of resolution.deferBlocks) { - for (const dep of deferBlockDeps) { - const classDecl = - (dep as unknown as {classDeclaration: ts.ClassDeclaration}).classDeclaration; - const importDecl = resolution.deferrableDeclToImportDecl.get(classDecl) ?? null; + for (const deferBlockDep of deferBlockDeps) { + const dep = deferBlockDep as unknown as {classDeclaration: ts.ClassDeclaration}; + const classDecl = dep.classDeclaration as unknown as Expression; + const importDecl = resolution.deferrableDeclToImportDecl.get(classDecl) as unknown as + ts.ImportDeclaration ?? + null; if (importDecl && this.deferredSymbolTracker.canDefer(importDecl)) { - dep.isDeferrable = true; - dep.importPath = importDecl.moduleSpecifier.text; + deferBlockDep.isDeferrable = true; + deferBlockDep.importPath = (importDecl.moduleSpecifier as ts.StringLiteral).text; } } } @@ -1128,7 +1129,7 @@ export class ComponentDecoratorHandler implements resolutionData.deferBlocks.set(deferBlock, deps); } - // For standalone components with the `imports` field - inspect teh list of + // For standalone components with the `imports` field - inspect the list of // referenced symbols and mark the ones used in defer blocks as potential candidates // for defer loading. if (analysisData.meta.isStandalone && analysisData.rawImports !== null && @@ -1186,7 +1187,8 @@ 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); // We're okay deferring this reference to the imported symbol. this.deferredSymbolTracker.markAsDeferrableCandidate(node, imp.node); diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 57463b0a37430..abe03b0d0dfc0 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -8872,23 +8872,52 @@ function allTests(os: string) { }); }); - // TODO: replace with tests for `defer`. - // TODO: maybe put deferred tests in a separate file? describe('deferred blocks', () => { it('should not error for deferred blocks', () => { env.tsconfig({_enabledBlockTypes: ['defer']}); + env.write('cmp-a.ts', ` + import {Component} from '@angular/core'; + + @Component({ + standalone: true, + selector: 'cmp-a', + template: 'CmpA!' + }) + export class CmpA {} + `); + env.write('/test.ts', ` import {Component} from '@angular/core'; + import {CmpA} from './cmp-a'; + + @Component({ + selector: 'local-dep', + standalone: true, + template: 'Local dependency', + }) + export class LocalDep {} @Component({ selector: 'test-cmp', - template: '{#defer}hello{/defer}', + standalone: true, + imports: [CmpA, LocalDep], + template: \` + {#defer} + + + {/defer} + \`, }) export class TestCmp {} - `); + `); - const diags = env.driveDiagnostics(); - expect(diags.length).toBe(0); + env.driveMain(); + + const jsContents = env.getContents('test.js'); + expect(jsContents).toContain('ɵɵdefer(0, TestCmp_Defer_0_DepsFn)'); + expect(jsContents) + .toContain( + 'function TestCmp_Defer_0_DepsFn() { return [import("./cmp-a").then(function (m) { return m.CmpA; }), LocalDep]; }'); }); }); }); diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 6ed21604cfb0a..6a3565787ba10 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -139,6 +139,8 @@ export class Identifiers { static templateCreate: o.ExternalReference = {name: 'ɵɵtemplate', moduleName: CORE}; + static defer: o.ExternalReference = {name: 'ɵɵdefer', moduleName: CORE}; + static text: o.ExternalReference = {name: 'ɵɵtext', moduleName: CORE}; static enableBindings: o.ExternalReference = {name: 'ɵɵenableBindings', moduleName: CORE}; diff --git a/packages/compiler/src/render3/view/api.ts b/packages/compiler/src/render3/view/api.ts index d65e28fbe72ad..d44a1c52ff091 100644 --- a/packages/compiler/src/render3/view/api.ts +++ b/packages/compiler/src/render3/view/api.ts @@ -218,11 +218,11 @@ export interface R3ComponentMetadata declarations: DeclarationT[]; /** - * Map of all types that can be defer loaded -> corresponding module specifier - * strings (that can later be used as a value in dynamic imports). + * Map of all types that can be defer loaded (ts.ClassDeclaration) -> + * corresponding import declaration (ts.ImportDeclaration) within + * the current source file. */ - // TODO: fix types! (ClassDeclaration -> ImportDeclaration) - deferrableDeclToImportDecl: Map; + deferrableDeclToImportDecl: Map; /** * Map of {#defer} blocks -> their corresponding dependencies. diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index 726dd2ca8f52f..4151b4d3752d5 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -194,7 +194,7 @@ 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); + R3.namespaceHTML, meta.relativeContextFilePath, meta.i18nUseExternalIds, meta.deferBlocks); const templateFunctionExpression = templateBuilder.buildTemplateFunction(template.nodes, []); diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index 7cebf020f56d7..4dc9ac88af896 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -33,6 +33,7 @@ import {Identifiers as R3} from '../r3_identifiers'; import {htmlAstToRender3Ast} from '../r3_template_transform'; import {prepareSyntheticListenerFunctionName, prepareSyntheticListenerName, prepareSyntheticPropertyName} from '../util'; +import {DeferBlockTemplateDependency} from './api'; import {I18nContext} from './i18n/context'; import {createGoogleGetMsgStatements} from './i18n/get_msg_utils'; import {createLocalizeStatements} from './i18n/localize_utils'; @@ -221,6 +222,7 @@ 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 _constants: ComponentDefConsts = createComponentDefConsts()) { this._bindingScope = parentBindingScope.nestedScope(level); @@ -923,7 +925,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver const templateVisitor = new TemplateDefinitionBuilder( this.constantPool, this._bindingScope, this.level + 1, contextName, this.i18n, templateIndex, templateName, this._namespace, this.fileBasedI18nSuffix, - this.i18nUseExternalIds, this._constants); + this.i18nUseExternalIds, this.deferBlocks, 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 @@ -1071,8 +1073,53 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver return null; } - // TODO: implement deferred block instructions. - visitDeferredBlock(deferred: t.DeferredBlock): void {} + visitDeferredBlock(deferred: t.DeferredBlock): void { + const templateIndex = this.allocateDataSlot(); + const deferredDeps = this.deferBlocks.get(deferred); + + const contextName = `${this.contextName}_Defer_${templateIndex}`; + const depsFnName = `${contextName}_DepsFn`; + + const parameters: o.Expression[] = [ + o.literal(templateIndex), + deferredDeps ? o.variable(depsFnName) : o.TYPED_NULL_EXPR, + ]; + + if (deferredDeps) { + // This defer block has deps for which we need to generate dynamic imports. + const dependencyExp: o.Expression[] = []; + for (const deferredDep of deferredDeps) { + if (deferredDep.isDeferrable) { + // Callback function, e.g. `function(m) { return m.MyCmp; }`. + const innerFn = o.fn( + [new o.FnParam('m', o.DYNAMIC_TYPE)], + [new o.ReturnStatement(o.variable('m').prop(deferredDep.symbolName))]); + + const fileName = deferredDep.importPath!; + // Dynamic import, e.g. `import('./a').then(...)`. + const importExpr = (new o.DynamicImportExpr(fileName)).prop('then').callFn([innerFn]); + dependencyExp.push(importExpr); + } else { + // Non-deferrable symbol, just use a reference to the type. + dependencyExp.push(deferredDep.type); + } + } + + const depsFnBody: o.Statement[] = []; + depsFnBody.push(new o.ReturnStatement(o.literalArr(dependencyExp))); + + const depsFnExpr = o.fn([] /* args */, depsFnBody, o.INFERRED_TYPE, null, depsFnName); + + this.constantPool.statements.push(depsFnExpr.toDeclStmt(depsFnName)); + } + + // e.g. `defer(1, MyComp_Defer_1_DepsFn, ...)` + this.creationInstruction(deferred.sourceSpan, R3.defer, () => { + return trimTrailingNulls(parameters); + }); + } + + // TODO: implement nested deferred block instructions. visitDeferredTrigger(trigger: t.DeferredTrigger): void {} visitDeferredBlockPlaceholder(block: t.DeferredBlockPlaceholder): void {} visitDeferredBlockError(block: t.DeferredBlockError): void {} diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 3f9a7655128ed..e05c964e0091d 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -197,6 +197,7 @@ export { ɵɵsyntheticHostProperty, ɵɵtemplate, ɵɵtemplateRefExtractor, + ɵɵdefer, ɵɵtext, ɵɵtextInterpolate, ɵɵtextInterpolate1, diff --git a/packages/core/src/render3/index.ts b/packages/core/src/render3/index.ts index 9ccf3f1b54117..9bdacff1764f0 100644 --- a/packages/core/src/render3/index.ts +++ b/packages/core/src/render3/index.ts @@ -118,6 +118,8 @@ export { ɵɵtemplate, + ɵɵdefer, + ɵɵtext, ɵɵtextInterpolate, ɵɵtextInterpolate1, diff --git a/packages/core/src/render3/instructions/all.ts b/packages/core/src/render3/instructions/all.ts index b4d0473008039..f71a8c74fc1f6 100644 --- a/packages/core/src/render3/instructions/all.ts +++ b/packages/core/src/render3/instructions/all.ts @@ -50,4 +50,5 @@ export * from './style_map_interpolation'; export * from './style_prop_interpolation'; export * from './host_property'; export * from './i18n'; +export * from './defer'; export {ɵgetUnknownElementStrictMode, ɵsetUnknownElementStrictMode, ɵgetUnknownPropertyStrictMode, ɵsetUnknownPropertyStrictMode} from './element_validation'; diff --git a/packages/core/src/render3/instructions/defer.ts b/packages/core/src/render3/instructions/defer.ts new file mode 100644 index 0000000000000..c21e5f983e1dc --- /dev/null +++ b/packages/core/src/render3/instructions/defer.ts @@ -0,0 +1,23 @@ +/** + * @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 {Type} from '../../interface/type'; + +export type DeferredDepsFn = () => Array>|Type>; + +/** + * Creates runtime data structures for `{#defer}` blocks. + * + * @param index The index of the defer block in the data array + * @param deferredDepsFn Function that contains dependencies for this defer block + * + * @codeGenApi + */ +export function ɵɵdefer(index: number, deferredDepsFn: DeferredDepsFn|null) { + // TODO: implement runtime logic. +} diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index 4458f4fa79b69..b114cf8ccd3ad 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -138,6 +138,7 @@ export const angularCoreEnv: {[name: string]: Function} = 'ɵɵclassProp': r3.ɵɵclassProp, 'ɵɵadvance': r3.ɵɵadvance, 'ɵɵtemplate': r3.ɵɵtemplate, + 'ɵɵdefer': r3.ɵɵdefer, 'ɵɵtext': r3.ɵɵtext, 'ɵɵtextInterpolate': r3.ɵɵtextInterpolate, 'ɵɵtextInterpolate1': r3.ɵɵtextInterpolate1,