Skip to content

Commit

Permalink
refactor(compiler): handle defer blocks in TemplateDefinitionBuilder
Browse files Browse the repository at this point in the history
Updates the TemplateDefinitionBuilder class to generate the `defer` instruction for `{#defer}` blocks. Also generates dependency function that would be invoked at runtime (with dynamic imports inside).
  • Loading branch information
AndrewKushnir committed Jul 28, 2023
1 parent 0b0b02f commit cc6d2b6
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}
}
}
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -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);
Expand Down
41 changes: 35 additions & 6 deletions packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}
<cmp-a />
<local-dep />
{/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]; }');
});
});
});
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler/src/render3/r3_identifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
8 changes: 4 additions & 4 deletions packages/compiler/src/render3/view/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,11 @@ export interface R3ComponentMetadata<DeclarationT extends R3TemplateDependency>
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<any, any>;
deferrableDeclToImportDecl: Map<o.Expression, o.Expression>;

/**
* Map of {#defer} blocks -> their corresponding dependencies.
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler/src/render3/view/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, []);

Expand Down
53 changes: 50 additions & 3 deletions packages/compiler/src/render3/view/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -221,6 +222,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
private templateIndex: number|null, private templateName: string|null,
private _namespace: o.ExternalReference, relativeContextFilePath: string,
private i18nUseExternalIds: boolean,
private deferBlocks: Map<t.DeferredBlock, DeferBlockTemplateDependency[]>,
private _constants: ComponentDefConsts = createComponentDefConsts()) {
this._bindingScope = parentBindingScope.nestedScope(level);

Expand Down Expand Up @@ -923,7 +925,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, 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
Expand Down Expand Up @@ -1071,8 +1073,53 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, 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 {}
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/core_render3_private_export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ export {
ɵɵsyntheticHostProperty,
ɵɵtemplate,
ɵɵtemplateRefExtractor,
ɵɵdefer,
ɵɵtext,
ɵɵtextInterpolate,
ɵɵtextInterpolate1,
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/render3/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ export {

ɵɵtemplate,

ɵɵdefer,

ɵɵtext,
ɵɵtextInterpolate,
ɵɵtextInterpolate1,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/render3/instructions/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
23 changes: 23 additions & 0 deletions packages/core/src/render3/instructions/defer.ts
Original file line number Diff line number Diff line change
@@ -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<Promise<Type<unknown>>|Type<unknown>>;

/**
* 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.
}
1 change: 1 addition & 0 deletions packages/core/src/render3/jit/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit cc6d2b6

Please sign in to comment.