@decocms/blocks 7.59.0 → 7.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cms/resolve.test.ts +193 -18
- package/src/cms/resolve.ts +47 -14
package/package.json
CHANGED
package/src/cms/resolve.test.ts
CHANGED
|
@@ -23,14 +23,16 @@ vi.mock("./registry", () => ({
|
|
|
23
23
|
import { normalizeUrlsInObject } from "../sdk/normalizeUrls";
|
|
24
24
|
import { findPageByPath } from "./loader";
|
|
25
25
|
import { getSection } from "./registry";
|
|
26
|
-
import type { AsyncRenderingConfig, DeferredSection } from "./resolve";
|
|
26
|
+
import type { AsyncRenderingConfig, DeferredSection, MatcherContext } from "./resolve";
|
|
27
27
|
import {
|
|
28
28
|
clearCommerceLoaders,
|
|
29
29
|
DEFAULT_FOLD_THRESHOLD,
|
|
30
30
|
extractSeoFromProps,
|
|
31
31
|
getAsyncRenderingConfig,
|
|
32
32
|
isEagerRequest,
|
|
33
|
+
reExtractRawProps,
|
|
33
34
|
registerCommerceLoader,
|
|
35
|
+
registerMatcher,
|
|
34
36
|
registerEagerSections,
|
|
35
37
|
registerAlwaysDeferSections,
|
|
36
38
|
registerNeverDeferSections,
|
|
@@ -736,16 +738,6 @@ describe("resolvePageSeoBlock — per-section ignoreStructuredData drives the fe
|
|
|
736
738
|
});
|
|
737
739
|
});
|
|
738
740
|
|
|
739
|
-
// ---------------------------------------------------------------------------
|
|
740
|
-
// resolveDecoPage — #277 client-side navigation disables deferral
|
|
741
|
-
// ---------------------------------------------------------------------------
|
|
742
|
-
//
|
|
743
|
-
// When isClientNavigation is true, resolveDecoPageImpl sets useAsync = false so
|
|
744
|
-
// shouldDeferSection is never called. All sections — including CMS ⚡-wrapped
|
|
745
|
-
// ones — are resolved eagerly. This prevents client-nav from returning a
|
|
746
|
-
// deferredSections array that loadDeferredSection would then try to resolve
|
|
747
|
-
// without the per-request commerce app context.
|
|
748
|
-
|
|
749
741
|
describe("extractSeoFromProps — commerce jsonLD structured data", () => {
|
|
750
742
|
const plp = (overrides: Record<string, unknown> = {}) => ({
|
|
751
743
|
"@type": "ProductListingPage",
|
|
@@ -888,22 +880,42 @@ describe("extractSeoFromProps — commerce jsonLD structured data", () => {
|
|
|
888
880
|
});
|
|
889
881
|
});
|
|
890
882
|
|
|
891
|
-
|
|
883
|
+
// ---------------------------------------------------------------------------
|
|
884
|
+
// resolveDecoPage — client nav gets the SAME eager/deferred split as SSR
|
|
885
|
+
// ---------------------------------------------------------------------------
|
|
886
|
+
//
|
|
887
|
+
// A TanStack route loader is BLOCKING: the router will not commit the
|
|
888
|
+
// transition until the loader promise settles. Eager-resolving every ⚡
|
|
889
|
+
// below-fold section on client nav therefore freezes the previous page for as
|
|
890
|
+
// long as the slowest upstream takes (measured on a real PDP: 20 awaited
|
|
891
|
+
// sections, 2717ms, 3.41MB — worse than a full reload). So deferral must apply
|
|
892
|
+
// to client nav exactly as it does to SSR.
|
|
893
|
+
//
|
|
894
|
+
// The historical `!isClientNavigation` gate (decocms/blocks#277) was a
|
|
895
|
+
// workaround for deferred loaders that appeared to lose per-request app
|
|
896
|
+
// context. It traded a page-wide latency regression for that symptom. The
|
|
897
|
+
// second hop (`loadDeferredSection`) is the SAME server fn the SSR path has
|
|
898
|
+
// always used and rebuilds MatcherContext from the real request — see the
|
|
899
|
+
// "#277 — per-request context survives the deferred second hop" cases below.
|
|
900
|
+
// ---------------------------------------------------------------------------
|
|
901
|
+
|
|
902
|
+
describe("resolveDecoPage — deferral parity between SSR and client nav", () => {
|
|
892
903
|
const lazySec = {
|
|
893
904
|
__resolveType: WELL_KNOWN_TYPES.LAZY,
|
|
894
905
|
section: { __resolveType: "site/sections/Hero.tsx" },
|
|
895
906
|
};
|
|
907
|
+
const eagerSec = { __resolveType: "site/sections/Banner.tsx" };
|
|
896
908
|
|
|
897
909
|
beforeEach(() => {
|
|
898
|
-
// Enable async rendering so useAsync can be true
|
|
910
|
+
// Enable async rendering so useAsync can be true.
|
|
899
911
|
setAsyncRenderingConfig({ foldThreshold: Infinity, respectCmsLazy: true });
|
|
900
912
|
// resolveSectionShallow unwraps the ⚡ and looks up the inner key via
|
|
901
913
|
// getSection — return truthy so it produces a DeferredSection rather than
|
|
902
914
|
// falling back to eager resolution.
|
|
903
915
|
(getSection as ReturnType<typeof vi.fn>).mockReturnValue({ default: () => null });
|
|
904
|
-
//
|
|
916
|
+
// A page with one plain section followed by one CMS ⚡-wrapped section.
|
|
905
917
|
(findPageByPath as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
906
|
-
page: { name: "test", sections: [lazySec] },
|
|
918
|
+
page: { name: "test", sections: [eagerSec, lazySec] },
|
|
907
919
|
params: {},
|
|
908
920
|
blockKey: "test-page",
|
|
909
921
|
});
|
|
@@ -914,15 +926,178 @@ describe("resolveDecoPage — #277 client-side navigation disables deferral", ()
|
|
|
914
926
|
(findPageByPath as ReturnType<typeof vi.fn>).mockReset();
|
|
915
927
|
});
|
|
916
928
|
|
|
917
|
-
it("SSR request defers
|
|
929
|
+
it("SSR request defers the CMS ⚡ section and keeps the plain one eager", async () => {
|
|
918
930
|
const result = await resolveDecoPage("/product/foo", {});
|
|
919
931
|
expect(result?.deferredSections).toHaveLength(1);
|
|
920
932
|
expect(result?.deferredSections[0].component).toBe("site/sections/Hero.tsx");
|
|
933
|
+
expect(result?.resolvedSections.map((s) => s.component)).toEqual(["site/sections/Banner.tsx"]);
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
it("client nav produces the IDENTICAL split — deferral is not disabled", async () => {
|
|
937
|
+
const ssr = await resolveDecoPage("/product/foo", {});
|
|
938
|
+
const nav = await resolveDecoPage("/product/foo", { isClientNavigation: true });
|
|
939
|
+
|
|
940
|
+
// The acceptance criterion: client nav returns deferredSections and awaits
|
|
941
|
+
// only the eager set, exactly like SSR.
|
|
942
|
+
expect(nav?.deferredSections).toHaveLength(1);
|
|
943
|
+
expect(nav?.deferredSections.map((d) => d.component)).toEqual(
|
|
944
|
+
ssr?.deferredSections.map((d) => d.component),
|
|
945
|
+
);
|
|
946
|
+
expect(nav?.resolvedSections.map((s) => s.component)).toEqual(
|
|
947
|
+
ssr?.resolvedSections.map((s) => s.component),
|
|
948
|
+
);
|
|
921
949
|
});
|
|
922
950
|
|
|
923
|
-
it("
|
|
924
|
-
const
|
|
951
|
+
it("the ⚡ section's index is preserved on client nav (ordering on the merge)", async () => {
|
|
952
|
+
const nav = await resolveDecoPage("/product/foo", { isClientNavigation: true });
|
|
953
|
+
expect(nav?.deferredSections[0].index).toBe(1);
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
it("bots stay fully eager on a client-nav-flagged request (SEO guarantee)", async () => {
|
|
957
|
+
const result = await resolveDecoPage("/product/foo", {
|
|
958
|
+
isClientNavigation: true,
|
|
959
|
+
userAgent: "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
|
|
960
|
+
});
|
|
961
|
+
expect(result?.deferredSections).toHaveLength(0);
|
|
962
|
+
expect(result?.resolvedSections).toHaveLength(2);
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
it("?__deco_ssr=1 stays fully eager on a client-nav-flagged request", async () => {
|
|
966
|
+
const result = await resolveDecoPage("/product/foo", {
|
|
967
|
+
isClientNavigation: true,
|
|
968
|
+
url: "https://store.com/product/foo?__deco_ssr=1",
|
|
969
|
+
});
|
|
925
970
|
expect(result?.deferredSections).toHaveLength(0);
|
|
971
|
+
expect(result?.resolvedSections).toHaveLength(2);
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
it("a genuine programmatic fetch (Sec-Fetch-Dest: empty, no client-nav flag) stays eager", async () => {
|
|
975
|
+
const result = await resolveDecoPage("/product/foo", {
|
|
976
|
+
request: new Request("https://store.com/product/foo", {
|
|
977
|
+
headers: { "sec-fetch-dest": "empty" },
|
|
978
|
+
}),
|
|
979
|
+
});
|
|
980
|
+
expect(result?.deferredSections).toHaveLength(0);
|
|
981
|
+
});
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
// ---------------------------------------------------------------------------
|
|
985
|
+
// #277 — per-request context survives the deferred second hop
|
|
986
|
+
// ---------------------------------------------------------------------------
|
|
987
|
+
//
|
|
988
|
+
// This is the invariant that made it safe to re-enable deferral on client nav.
|
|
989
|
+
// #277 reported deferred sections rendering blank because their loaders "lost"
|
|
990
|
+
// per-request app context. The guarantee is that `resolveDeferredSectionFull`
|
|
991
|
+
// (and `loadDeferredSection`, which wraps it in @decocms/tanstack) threads the
|
|
992
|
+
// caller's MatcherContext — cookies, url, path, userAgent, request — into BOTH
|
|
993
|
+
// the cache-hit and the cache-miss (`reExtractRawProps`) branch. A different
|
|
994
|
+
// isolate misses the in-process rawProps Map, so the miss path is the one that
|
|
995
|
+
// actually runs in production on Cloudflare Workers; if it ever stops receiving
|
|
996
|
+
// matcherCtx, cookie-dependent loaders silently resolve against an anonymous
|
|
997
|
+
// request. Do not relax these assertions.
|
|
998
|
+
|
|
999
|
+
describe("#277 — deferred second hop keeps per-request context", () => {
|
|
1000
|
+
const PROBE = "test/matchers/probe.ts";
|
|
1001
|
+
const CTX: MatcherContext & { request: Request } = {
|
|
1002
|
+
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0) Mobile Safari",
|
|
1003
|
+
url: "https://store.com/product/foo?utm=x",
|
|
1004
|
+
path: "/product/foo",
|
|
1005
|
+
cookies: { deco_segment: "abc", VtexIdclientAutCookie: "tok" },
|
|
1006
|
+
request: new Request("https://store.com/product/foo?utm=x"),
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
/** Every MatcherContext the probe matcher was evaluated with. */
|
|
1010
|
+
let seen: MatcherContext[] = [];
|
|
1011
|
+
|
|
1012
|
+
// The ⚡ section sits behind a multivariate flag whose rule is the probe
|
|
1013
|
+
// matcher. Reaching the inner Shelf at all therefore PROVES the resolver
|
|
1014
|
+
// evaluated the rule, and the probe records exactly which context it saw —
|
|
1015
|
+
// an end-to-end assertion rather than a mock of the call site.
|
|
1016
|
+
const gatedLazy = {
|
|
1017
|
+
__resolveType: WELL_KNOWN_TYPES.MULTIVARIATE,
|
|
1018
|
+
variants: [
|
|
1019
|
+
{
|
|
1020
|
+
rule: { __resolveType: PROBE },
|
|
1021
|
+
value: {
|
|
1022
|
+
__resolveType: WELL_KNOWN_TYPES.LAZY,
|
|
1023
|
+
section: { __resolveType: "site/sections/Shelf.tsx", title: "Mais vendidos" },
|
|
1024
|
+
},
|
|
1025
|
+
},
|
|
1026
|
+
],
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
beforeEach(() => {
|
|
1030
|
+
seen = [];
|
|
1031
|
+
registerMatcher(PROBE, (_rule, ctx) => {
|
|
1032
|
+
seen.push(ctx);
|
|
1033
|
+
return true;
|
|
1034
|
+
});
|
|
1035
|
+
setAsyncRenderingConfig({ foldThreshold: Infinity, respectCmsLazy: true });
|
|
1036
|
+
(getSection as ReturnType<typeof vi.fn>).mockReturnValue({ default: () => null });
|
|
1037
|
+
(findPageByPath as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
1038
|
+
page: { name: "test", sections: [gatedLazy] },
|
|
1039
|
+
params: {},
|
|
1040
|
+
blockKey: "test-page",
|
|
1041
|
+
});
|
|
1042
|
+
(runSingleSectionLoader as ReturnType<typeof vi.fn>).mockImplementation(
|
|
1043
|
+
async (s: unknown) => s,
|
|
1044
|
+
);
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
afterEach(() => {
|
|
1048
|
+
(getSection as ReturnType<typeof vi.fn>).mockReset();
|
|
1049
|
+
(findPageByPath as ReturnType<typeof vi.fn>).mockReset();
|
|
1050
|
+
(runSingleSectionLoader as ReturnType<typeof vi.fn>).mockReset();
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
it("reExtractRawProps (cross-isolate cache miss) resolves with the caller's cookies/UA/url", async () => {
|
|
1054
|
+
// A cold isolate never populated the in-process rawProps Map, so this is
|
|
1055
|
+
// the branch that actually runs in production on Cloudflare Workers.
|
|
1056
|
+
const rawProps = await reExtractRawProps("/product/foo", "site/sections/Shelf.tsx", 0, CTX);
|
|
1057
|
+
|
|
1058
|
+
expect(rawProps).toMatchObject({ title: "Mais vendidos" });
|
|
1059
|
+
expect(seen).not.toHaveLength(0);
|
|
1060
|
+
for (const ctx of seen) {
|
|
1061
|
+
expect(ctx.cookies).toEqual(CTX.cookies);
|
|
1062
|
+
expect(ctx.userAgent).toBe(CTX.userAgent);
|
|
1063
|
+
expect(ctx.url).toBe(CTX.url);
|
|
1064
|
+
expect(ctx.request).toBe(CTX.request);
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
it("resolveDeferredSectionFull on a cold cache still resolves + enriches the section", async () => {
|
|
1069
|
+
const ds = {
|
|
1070
|
+
component: "site/sections/Shelf.tsx",
|
|
1071
|
+
index: 0,
|
|
1072
|
+
props: {},
|
|
1073
|
+
} as unknown as DeferredSection;
|
|
1074
|
+
|
|
1075
|
+
const section = await resolveDeferredSectionFull(ds, "/product/foo", CTX.request, CTX);
|
|
1076
|
+
|
|
1077
|
+
expect(section).not.toBeNull();
|
|
1078
|
+
expect(section?.component).toBe("site/sections/Shelf.tsx");
|
|
1079
|
+
expect(section?.index).toBe(0);
|
|
1080
|
+
// The deferred hop must run the section's own loader — this is what #277
|
|
1081
|
+
// reported as missing, and it is what makes the second hop equivalent to
|
|
1082
|
+
// eager resolution. Scope note: this asserts the request THIS function was
|
|
1083
|
+
// handed reaches the loader. `loadDeferredSection` in @decocms/tanstack
|
|
1084
|
+
// constructs its own `new Request(pageUrl || serverUrl, { headers })` before
|
|
1085
|
+
// calling in, so the fidelity of that reconstruction is a separate concern
|
|
1086
|
+
// and is not covered here.
|
|
1087
|
+
expect(runSingleSectionLoader).toHaveBeenCalled();
|
|
1088
|
+
const [, passedRequest] = (runSingleSectionLoader as ReturnType<typeof vi.fn>).mock
|
|
1089
|
+
.calls[0] as [unknown, Request];
|
|
1090
|
+
expect(passedRequest).toBe(CTX.request);
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
it("a context-free second hop is observably different — guards against dropping matcherCtx", async () => {
|
|
1094
|
+
// If reExtractRawProps ever stops threading matcherCtx, cookie/UA-gated
|
|
1095
|
+
// variants silently resolve against an anonymous request. Assert the probe
|
|
1096
|
+
// can actually tell the two apart, so the test above is not vacuous.
|
|
1097
|
+
await reExtractRawProps("/product/foo", "site/sections/Shelf.tsx", 0, undefined);
|
|
1098
|
+
expect(seen).not.toHaveLength(0);
|
|
1099
|
+
expect(seen[0].cookies).toBeUndefined();
|
|
1100
|
+
expect(seen[0].userAgent).toBeUndefined();
|
|
926
1101
|
});
|
|
927
1102
|
});
|
|
928
1103
|
|
package/src/cms/resolve.ts
CHANGED
|
@@ -388,9 +388,10 @@ function hasForceEagerParam(ctx?: MatcherContext): boolean {
|
|
|
388
388
|
* (`image`/`script`/`style`/`font`) do not — navigations stay deferred because
|
|
389
389
|
* they CAN hydrate and resolve deferred sections. SPA navigations (TanStack
|
|
390
390
|
* `<Link>` → `/_serverFn`) also send `empty` but set `isClientNavigation`, and
|
|
391
|
-
* are excluded here
|
|
392
|
-
* (decocms/blocks#286)
|
|
393
|
-
*
|
|
391
|
+
* are excluded here on both counts: page-SEO commerce loaders stay off for
|
|
392
|
+
* humans (decocms/blocks#286), and their sections stay deferred, because a
|
|
393
|
+
* client nav renders through `DecoPageRenderer` and therefore CAN resolve a
|
|
394
|
+
* deferred section on scroll — unlike a real AJAX consumer.
|
|
394
395
|
*
|
|
395
396
|
* Like {@link hasForceEagerParam}, this must stay in lock-step with the edge
|
|
396
397
|
* cache key: `workerEntry` keys `Sec-Fetch-Dest: empty` page requests into a
|
|
@@ -447,11 +448,15 @@ export interface MatcherContext {
|
|
|
447
448
|
*/
|
|
448
449
|
flags?: StoredFlag[];
|
|
449
450
|
/**
|
|
450
|
-
* Client-side (SPA) navigation via TanStack `<Link>`.
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
451
|
+
* Client-side (SPA) navigation via TanStack `<Link>`. Set by the route
|
|
452
|
+
* loaders, where the incoming request URL is the `/_serverFn/...` endpoint
|
|
453
|
+
* rather than the page being navigated to.
|
|
454
|
+
*
|
|
455
|
+
* Consumed by `derivePageUrl` (rebuilding the real page URL, #280) and by
|
|
456
|
+
* {@link isProgrammaticFetch} (a SPA nav sends `Sec-Fetch-Dest: empty` but is
|
|
457
|
+
* NOT an AJAX consumer). It deliberately does NOT affect section deferral —
|
|
458
|
+
* a TanStack route loader is blocking, so eager-resolving below-fold sections
|
|
459
|
+
* on client nav stalls the whole transition. See `resolveDecoPageImpl`.
|
|
455
460
|
*/
|
|
456
461
|
isClientNavigation?: boolean;
|
|
457
462
|
}
|
|
@@ -1523,6 +1528,15 @@ function isCmsDeferralWrapped(section: unknown, matcherCtx?: MatcherContext): bo
|
|
|
1523
1528
|
* then they can only force a NON-⚡ section eager — they never override the
|
|
1524
1529
|
* editor's ⚡ choice.
|
|
1525
1530
|
*
|
|
1531
|
+
* Because deferral now applies to client (SPA) navigations too, a ⚡ section is
|
|
1532
|
+
* resolved on a SECOND server hop (`loadDeferredSection`) in both cases. That
|
|
1533
|
+
* hop rebuilds MatcherContext from the real request, but it cannot reconstruct
|
|
1534
|
+
* "which branch of the page renders at all" — so a *gate* section (one whose
|
|
1535
|
+
* loader picks between `children`/`fallback`, e.g. a combined PDP/PLP route)
|
|
1536
|
+
* must be left un-⚡ in the admin. There is deliberately no code-level override
|
|
1537
|
+
* for this: `neverDefer`/`alwaysEager` sit below the admin check and cannot win
|
|
1538
|
+
* against an explicit editorial ⚡ (see decocms/blocks#277).
|
|
1539
|
+
*
|
|
1526
1540
|
* Exported for unit testing.
|
|
1527
1541
|
*/
|
|
1528
1542
|
export function shouldDeferSection(
|
|
@@ -2008,13 +2022,32 @@ async function resolveDecoPageImpl(
|
|
|
2008
2022
|
}
|
|
2009
2023
|
|
|
2010
2024
|
const isBotReq = isEagerRequest(matcherCtx);
|
|
2011
|
-
//
|
|
2012
|
-
//
|
|
2013
|
-
//
|
|
2014
|
-
//
|
|
2015
|
-
|
|
2025
|
+
// Deferral applies identically to SSR documents and SPA navigations. A
|
|
2026
|
+
// TanStack route loader is BLOCKING: the router does not commit the
|
|
2027
|
+
// transition until the loader promise settles, so resolving every
|
|
2028
|
+
// below-the-fold section eagerly on client nav freezes the previous page for
|
|
2029
|
+
// as long as the slowest shelf/upstream takes (measured: 2.7s and a 3.4MB
|
|
2030
|
+
// payload on a real PDP — worse than a full reload). Deferral's job is
|
|
2031
|
+
// decoupling first paint from below-fold data, which matters MORE here, not
|
|
2032
|
+
// less.
|
|
2033
|
+
//
|
|
2034
|
+
// #277 (deferred loaders missing per-request app context) is not a reason to
|
|
2035
|
+
// disable deferral on client nav: the deferred second hop is the same
|
|
2036
|
+
// `loadDeferredSection` server fn the SSR path has always used, and it runs
|
|
2037
|
+
// server-side with matcherCtx rebuilt from the real request
|
|
2038
|
+
// (url/path/cookies/request). It may well land in a DIFFERENT isolate — that
|
|
2039
|
+
// is exactly why `reExtractRawProps` exists as the rawProps cache-miss path —
|
|
2040
|
+
// but per-request state is reconstructed from the request either way, so a
|
|
2041
|
+
// client nav is no more exposed than an SSR document already was. A section
|
|
2042
|
+
// whose loader genuinely cannot be resolved on a second hop (e.g. a PDP/PLP
|
|
2043
|
+
// gate that decides what renders at all) must not be marked ⚡ in the admin;
|
|
2044
|
+
// see `shouldDeferSection`.
|
|
2045
|
+
//
|
|
2046
|
+
// `isClientNavigation` is still load-bearing for `derivePageUrl` (duplicate
|
|
2047
|
+
// query params) and for `isProgrammaticFetch` (SPA `Sec-Fetch-Dest: empty`
|
|
2048
|
+
// must not be mistaken for an AJAX call) — it just no longer gates deferral.
|
|
2016
2049
|
const currentAsyncConfig = getAsyncConfig();
|
|
2017
|
-
const useAsync = currentAsyncConfig !== null && !isBotReq
|
|
2050
|
+
const useAsync = currentAsyncConfig !== null && !isBotReq;
|
|
2018
2051
|
|
|
2019
2052
|
const eagerResults: (ResolvedSection[] | Promise<ResolvedSection[]>)[] = [];
|
|
2020
2053
|
const deferredSections: DeferredSection[] = [];
|