@decocms/blocks 7.58.0 → 7.59.1
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/index.ts +1 -0
- package/src/cms/resolve.test.ts +193 -18
- package/src/cms/resolve.ts +76 -17
package/package.json
CHANGED
package/src/cms/index.ts
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
|
@@ -310,9 +310,35 @@ export function getDeferredRawProps(
|
|
|
310
310
|
// Bot detection — bots always receive fully eager pages for SEO
|
|
311
311
|
// ---------------------------------------------------------------------------
|
|
312
312
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
313
|
+
/**
|
|
314
|
+
* User-Agent substrings that classify a request as a bot.
|
|
315
|
+
*
|
|
316
|
+
* Exported as data, not just baked into the regex, because the CDN cache rules
|
|
317
|
+
* have to bypass exactly the same requests this list makes eager — see
|
|
318
|
+
* `@decocms/blocks-cli/scripts/cdn-rules.ts`. Two hand-maintained copies would
|
|
319
|
+
* drift, and the failure mode is a crawler's eager HTML (~10x larger) being
|
|
320
|
+
* served to humans from the CDN, or the deferred one being served to Google.
|
|
321
|
+
*/
|
|
322
|
+
export const BOT_UA_SUBSTRINGS = [
|
|
323
|
+
"bot",
|
|
324
|
+
"crawl",
|
|
325
|
+
"spider",
|
|
326
|
+
"slurp",
|
|
327
|
+
"facebookexternalhit",
|
|
328
|
+
"mediapartners",
|
|
329
|
+
"google",
|
|
330
|
+
"bing",
|
|
331
|
+
"yandex",
|
|
332
|
+
"baidu",
|
|
333
|
+
"duckduck",
|
|
334
|
+
"teoma",
|
|
335
|
+
"ia_archiver",
|
|
336
|
+
"semrush",
|
|
337
|
+
"ahrefs",
|
|
338
|
+
"lighthouse",
|
|
339
|
+
] as const;
|
|
340
|
+
|
|
341
|
+
const botPatterns: RegExp[] = [new RegExp(BOT_UA_SUBSTRINGS.join("|"), "i")];
|
|
316
342
|
|
|
317
343
|
/**
|
|
318
344
|
* Add a custom bot detection regex.
|
|
@@ -362,9 +388,10 @@ function hasForceEagerParam(ctx?: MatcherContext): boolean {
|
|
|
362
388
|
* (`image`/`script`/`style`/`font`) do not — navigations stay deferred because
|
|
363
389
|
* they CAN hydrate and resolve deferred sections. SPA navigations (TanStack
|
|
364
390
|
* `<Link>` → `/_serverFn`) also send `empty` but set `isClientNavigation`, and
|
|
365
|
-
* are excluded here
|
|
366
|
-
* (decocms/blocks#286)
|
|
367
|
-
*
|
|
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.
|
|
368
395
|
*
|
|
369
396
|
* Like {@link hasForceEagerParam}, this must stay in lock-step with the edge
|
|
370
397
|
* cache key: `workerEntry` keys `Sec-Fetch-Dest: empty` page requests into a
|
|
@@ -421,11 +448,15 @@ export interface MatcherContext {
|
|
|
421
448
|
*/
|
|
422
449
|
flags?: StoredFlag[];
|
|
423
450
|
/**
|
|
424
|
-
* Client-side (SPA) navigation via TanStack `<Link>`.
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
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`.
|
|
429
460
|
*/
|
|
430
461
|
isClientNavigation?: boolean;
|
|
431
462
|
}
|
|
@@ -1497,6 +1528,15 @@ function isCmsDeferralWrapped(section: unknown, matcherCtx?: MatcherContext): bo
|
|
|
1497
1528
|
* then they can only force a NON-⚡ section eager — they never override the
|
|
1498
1529
|
* editor's ⚡ choice.
|
|
1499
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
|
+
*
|
|
1500
1540
|
* Exported for unit testing.
|
|
1501
1541
|
*/
|
|
1502
1542
|
export function shouldDeferSection(
|
|
@@ -1982,13 +2022,32 @@ async function resolveDecoPageImpl(
|
|
|
1982
2022
|
}
|
|
1983
2023
|
|
|
1984
2024
|
const isBotReq = isEagerRequest(matcherCtx);
|
|
1985
|
-
//
|
|
1986
|
-
//
|
|
1987
|
-
//
|
|
1988
|
-
//
|
|
1989
|
-
|
|
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.
|
|
1990
2049
|
const currentAsyncConfig = getAsyncConfig();
|
|
1991
|
-
const useAsync = currentAsyncConfig !== null && !isBotReq
|
|
2050
|
+
const useAsync = currentAsyncConfig !== null && !isBotReq;
|
|
1992
2051
|
|
|
1993
2052
|
const eagerResults: (ResolvedSection[] | Promise<ResolvedSection[]>)[] = [];
|
|
1994
2053
|
const deferredSections: DeferredSection[] = [];
|