@decocms/start 0.19.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.
Files changed (185) hide show
  1. package/.cursor/skills/deco-api-call-dedup/SKILL.md +443 -0
  2. package/.cursor/skills/deco-apps-architecture/SKILL.md +255 -0
  3. package/.cursor/skills/deco-apps-architecture/app-pattern.md +288 -0
  4. package/.cursor/skills/deco-apps-architecture/commerce-types.md +239 -0
  5. package/.cursor/skills/deco-apps-architecture/new-app-guide.md +268 -0
  6. package/.cursor/skills/deco-apps-architecture/scripts-codegen.md +148 -0
  7. package/.cursor/skills/deco-apps-architecture/shared-utils.md +181 -0
  8. package/.cursor/skills/deco-apps-architecture/vtex-deep-structure.md +253 -0
  9. package/.cursor/skills/deco-apps-architecture/website-app.md +169 -0
  10. package/.cursor/skills/deco-apps-vtex-porting/SKILL.md +189 -0
  11. package/.cursor/skills/deco-apps-vtex-porting/adaptation-patterns.md +335 -0
  12. package/.cursor/skills/deco-apps-vtex-porting/commerce-porting.md +155 -0
  13. package/.cursor/skills/deco-apps-vtex-porting/cookie-auth-patterns.md +148 -0
  14. package/.cursor/skills/deco-apps-vtex-porting/structure-map.md +234 -0
  15. package/.cursor/skills/deco-apps-vtex-porting/transform-mapping.md +99 -0
  16. package/.cursor/skills/deco-apps-vtex-porting/website-porting.md +194 -0
  17. package/.cursor/skills/deco-apps-vtex-review/SKILL.md +234 -0
  18. package/.cursor/skills/deco-async-rendering-architecture/SKILL.md +270 -0
  19. package/.cursor/skills/deco-async-rendering-site-guide/SKILL.md +417 -0
  20. package/.cursor/skills/deco-cms-layout-caching/SKILL.md +293 -0
  21. package/.cursor/skills/deco-cms-route-config/SKILL.md +388 -0
  22. package/.cursor/skills/deco-core-architecture/SKILL.md +185 -0
  23. package/.cursor/skills/deco-core-architecture/blocks.md +196 -0
  24. package/.cursor/skills/deco-core-architecture/deco-vs-deco-start.md +191 -0
  25. package/.cursor/skills/deco-core-architecture/engine.md +220 -0
  26. package/.cursor/skills/deco-core-architecture/hooks-components.md +157 -0
  27. package/.cursor/skills/deco-core-architecture/plugins-clients.md +136 -0
  28. package/.cursor/skills/deco-core-architecture/runtime.md +116 -0
  29. package/.cursor/skills/deco-core-architecture/site-usage.md +165 -0
  30. package/.cursor/skills/deco-e2e-testing/SKILL.md +372 -0
  31. package/.cursor/skills/deco-e2e-testing/discovery.md +337 -0
  32. package/.cursor/skills/deco-e2e-testing/scripts/scaffold.sh +81 -0
  33. package/.cursor/skills/deco-e2e-testing/selectors.md +175 -0
  34. package/.cursor/skills/deco-e2e-testing/templates/package.json +18 -0
  35. package/.cursor/skills/deco-e2e-testing/templates/playwright.config.ts +65 -0
  36. package/.cursor/skills/deco-e2e-testing/templates/scripts/baseline.ts +279 -0
  37. package/.cursor/skills/deco-e2e-testing/templates/scripts/run-e2e.ts +194 -0
  38. package/.cursor/skills/deco-e2e-testing/templates/specs/ecommerce-flow.spec.ts +612 -0
  39. package/.cursor/skills/deco-e2e-testing/templates/tsconfig.json +12 -0
  40. package/.cursor/skills/deco-e2e-testing/templates/utils/metrics-collector.ts +918 -0
  41. package/.cursor/skills/deco-e2e-testing/troubleshooting.md +602 -0
  42. package/.cursor/skills/deco-edge-caching/SKILL.md +316 -0
  43. package/.cursor/skills/deco-full-analysis/SKILL.md +898 -0
  44. package/.cursor/skills/deco-full-analysis/checklists/asset-optimization.md +251 -0
  45. package/.cursor/skills/deco-full-analysis/checklists/bug-fix.md +189 -0
  46. package/.cursor/skills/deco-full-analysis/checklists/cache-strategy.md +144 -0
  47. package/.cursor/skills/deco-full-analysis/checklists/dependency-update.md +150 -0
  48. package/.cursor/skills/deco-full-analysis/checklists/hydration-fix.md +191 -0
  49. package/.cursor/skills/deco-full-analysis/checklists/image-optimization.md +180 -0
  50. package/.cursor/skills/deco-full-analysis/checklists/loader-optimization.md +165 -0
  51. package/.cursor/skills/deco-full-analysis/checklists/seo-fix.md +183 -0
  52. package/.cursor/skills/deco-full-analysis/checklists/site-cleanup.md +281 -0
  53. package/.cursor/skills/deco-full-analysis/discovery.md +548 -0
  54. package/.cursor/skills/deco-incident-debugging/SKILL.md +378 -0
  55. package/.cursor/skills/deco-incident-debugging/headless-mode.md +510 -0
  56. package/.cursor/skills/deco-incident-debugging/learnings-index.md +227 -0
  57. package/.cursor/skills/deco-incident-debugging/triage-workflow.md +312 -0
  58. package/.cursor/skills/deco-islands-migration/SKILL.md +251 -0
  59. package/.cursor/skills/deco-loader-n-plus-1-detector/SKILL.md +275 -0
  60. package/.cursor/skills/deco-performance-audit/SKILL.md +530 -0
  61. package/.cursor/skills/deco-performance-audit/tools-reference.md +428 -0
  62. package/.cursor/skills/deco-performance-audit/workflow.md +457 -0
  63. package/.cursor/skills/deco-server-functions-invoke/SKILL.md +92 -0
  64. package/.cursor/skills/deco-server-functions-invoke/architecture.md +166 -0
  65. package/.cursor/skills/deco-server-functions-invoke/generator.md +122 -0
  66. package/.cursor/skills/deco-server-functions-invoke/problem.md +98 -0
  67. package/.cursor/skills/deco-server-functions-invoke/troubleshooting.md +110 -0
  68. package/.cursor/skills/deco-site-deployment/SKILL.md +396 -0
  69. package/.cursor/skills/deco-site-memory-debugging/SKILL.md +121 -0
  70. package/.cursor/skills/deco-site-memory-debugging/cdp-connection.md +222 -0
  71. package/.cursor/skills/deco-site-memory-debugging/memory-analysis.md +362 -0
  72. package/.cursor/skills/deco-site-patterns/SKILL.md +124 -0
  73. package/.cursor/skills/deco-site-patterns/app-composition.md +337 -0
  74. package/.cursor/skills/deco-site-patterns/client-patterns.md +341 -0
  75. package/.cursor/skills/deco-site-patterns/cms-wiring.md +230 -0
  76. package/.cursor/skills/deco-site-patterns/section-patterns.md +340 -0
  77. package/.cursor/skills/deco-site-scaling-tuning/SKILL.md +240 -0
  78. package/.cursor/skills/deco-site-scaling-tuning/analysis-scripts.md +267 -0
  79. package/.cursor/skills/deco-start-architecture/SKILL.md +218 -0
  80. package/.cursor/skills/deco-start-architecture/admin-protocol.md +156 -0
  81. package/.cursor/skills/deco-start-architecture/cms-resolution.md +201 -0
  82. package/.cursor/skills/deco-start-architecture/code-quality.md +158 -0
  83. package/.cursor/skills/deco-start-architecture/gap-analysis.md +129 -0
  84. package/.cursor/skills/deco-start-architecture/sdk-utilities.md +197 -0
  85. package/.cursor/skills/deco-start-architecture/worker-entry-caching.md +154 -0
  86. package/.cursor/skills/deco-startup-analysis/SKILL.md +248 -0
  87. package/.cursor/skills/deco-storefront-test-checklist/SKILL.md +369 -0
  88. package/.cursor/skills/deco-tanstack-hydration-fixes/SKILL.md +468 -0
  89. package/.cursor/skills/deco-tanstack-navigation/SKILL.md +681 -0
  90. package/.cursor/skills/deco-tanstack-search/SKILL.md +411 -0
  91. package/.cursor/skills/deco-tanstack-storefront-patterns/SKILL.md +1013 -0
  92. package/.cursor/skills/deco-to-tanstack-migration/SKILL.md +518 -0
  93. package/.cursor/skills/deco-to-tanstack-migration/references/codemod-commands.md +174 -0
  94. package/.cursor/skills/deco-to-tanstack-migration/references/commerce/README.md +78 -0
  95. package/.cursor/skills/deco-to-tanstack-migration/references/deco-framework/README.md +128 -0
  96. package/.cursor/skills/deco-to-tanstack-migration/references/gotchas.md +719 -0
  97. package/.cursor/skills/deco-to-tanstack-migration/references/imports/README.md +70 -0
  98. package/.cursor/skills/deco-to-tanstack-migration/references/platform-hooks/README.md +154 -0
  99. package/.cursor/skills/deco-to-tanstack-migration/references/signals/README.md +220 -0
  100. package/.cursor/skills/deco-to-tanstack-migration/references/vite-config/README.md +78 -0
  101. package/.cursor/skills/deco-to-tanstack-migration/templates/package-json.md +55 -0
  102. package/.cursor/skills/deco-to-tanstack-migration/templates/root-route.md +110 -0
  103. package/.cursor/skills/deco-to-tanstack-migration/templates/router.md +96 -0
  104. package/.cursor/skills/deco-to-tanstack-migration/templates/setup-ts.md +167 -0
  105. package/.cursor/skills/deco-to-tanstack-migration/templates/vite-config.md +122 -0
  106. package/.cursor/skills/deco-to-tanstack-migration/templates/worker-entry.md +67 -0
  107. package/.cursor/skills/deco-typescript-fixes/SKILL.md +178 -0
  108. package/.cursor/skills/deco-typescript-fixes/common-fixes.md +330 -0
  109. package/.cursor/skills/deco-typescript-fixes/strategy.md +148 -0
  110. package/.cursor/skills/deco-variant-selection-perf/SKILL.md +272 -0
  111. package/.cursor/skills/deco-vtex-fetch-cache/SKILL.md +225 -0
  112. package/.cursor/skills/find-skills/SKILL.md +133 -0
  113. package/.cursor/skills/incident-report/SKILL.md +179 -0
  114. package/.cursor/skills/incident-report/references/5-whys.md +75 -0
  115. package/.cursor/skills/incident-report/templates/client-report.md +187 -0
  116. package/.cursor/skills/incident-report/templates/internal-report.md +206 -0
  117. package/.cursor/skills/template-skill/SKILL.md +38 -0
  118. package/.github/workflows/release.yml +32 -0
  119. package/.releaserc.json +25 -0
  120. package/CLAUDE.md +135 -0
  121. package/GAP_ANALYSIS.md +224 -0
  122. package/GAP_ANALYSIS_V2.md +1013 -0
  123. package/biome.json +39 -0
  124. package/knip.json +5 -0
  125. package/package.json +87 -0
  126. package/scripts/generate-blocks.ts +69 -0
  127. package/scripts/generate-invoke.ts +378 -0
  128. package/scripts/generate-schema.ts +657 -0
  129. package/src/admin/cors.ts +29 -0
  130. package/src/admin/decofile.ts +72 -0
  131. package/src/admin/index.ts +24 -0
  132. package/src/admin/invoke.ts +163 -0
  133. package/src/admin/liveControls.ts +29 -0
  134. package/src/admin/meta.ts +70 -0
  135. package/src/admin/render.ts +205 -0
  136. package/src/admin/schema.ts +686 -0
  137. package/src/admin/setup.ts +44 -0
  138. package/src/cms/index.ts +59 -0
  139. package/src/cms/loader.ts +180 -0
  140. package/src/cms/registry.ts +162 -0
  141. package/src/cms/resolve.ts +1005 -0
  142. package/src/cms/sectionLoaders.ts +294 -0
  143. package/src/hooks/DecoPageRenderer.tsx +444 -0
  144. package/src/hooks/LazySection.tsx +109 -0
  145. package/src/hooks/LiveControls.tsx +108 -0
  146. package/src/hooks/SectionErrorFallback.tsx +85 -0
  147. package/src/hooks/index.ts +8 -0
  148. package/src/index.ts +5 -0
  149. package/src/matchers/builtins.ts +184 -0
  150. package/src/matchers/posthog.ts +154 -0
  151. package/src/middleware/decoState.ts +55 -0
  152. package/src/middleware/healthMetrics.ts +131 -0
  153. package/src/middleware/index.ts +80 -0
  154. package/src/middleware/liveness.ts +21 -0
  155. package/src/middleware/observability.ts +205 -0
  156. package/src/routes/adminRoutes.ts +83 -0
  157. package/src/routes/cmsRoute.ts +302 -0
  158. package/src/routes/components.tsx +34 -0
  159. package/src/routes/index.ts +15 -0
  160. package/src/sdk/analytics.ts +72 -0
  161. package/src/sdk/cacheHeaders.ts +268 -0
  162. package/src/sdk/cachedLoader.ts +206 -0
  163. package/src/sdk/clx.ts +3 -0
  164. package/src/sdk/cookie.ts +39 -0
  165. package/src/sdk/createInvoke.ts +57 -0
  166. package/src/sdk/csp.ts +59 -0
  167. package/src/sdk/env.ts +27 -0
  168. package/src/sdk/index.ts +63 -0
  169. package/src/sdk/instrumentedFetch.ts +137 -0
  170. package/src/sdk/invoke.ts +133 -0
  171. package/src/sdk/mergeCacheControl.ts +150 -0
  172. package/src/sdk/redirects.ts +217 -0
  173. package/src/sdk/requestContext.ts +184 -0
  174. package/src/sdk/serverTimings.ts +68 -0
  175. package/src/sdk/signal.ts +41 -0
  176. package/src/sdk/sitemap.ts +143 -0
  177. package/src/sdk/urlUtils.ts +117 -0
  178. package/src/sdk/useDevice.ts +82 -0
  179. package/src/sdk/useId.ts +7 -0
  180. package/src/sdk/useScript.ts +101 -0
  181. package/src/sdk/workerEntry.ts +703 -0
  182. package/src/sdk/wrapCaughtErrors.ts +107 -0
  183. package/src/types/index.ts +39 -0
  184. package/src/types/widgets.ts +13 -0
  185. package/tsconfig.json +13 -0
@@ -0,0 +1,1005 @@
1
+ import { findPageByPath, loadBlocks } from "./loader";
2
+ import { getSection } from "./registry";
3
+ import { isLayoutSection } from "./sectionLoaders";
4
+
5
+ // globalThis-backed: share state across Vite server function split modules
6
+ const G = globalThis as any;
7
+ if (!G.__deco) G.__deco = {};
8
+ if (!G.__deco.commerceLoaders) G.__deco.commerceLoaders = {};
9
+ if (!G.__deco.customMatchers) G.__deco.customMatchers = {};
10
+
11
+ export type ResolvedSection = {
12
+ component: string;
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ props: Record<string, any>;
15
+ key: string;
16
+ /** Original position in the raw section list (used by mergeSections). */
17
+ index?: number;
18
+ };
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Deferred section — a placeholder for sections loaded on scroll
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export interface DeferredSection {
25
+ component: string;
26
+ key: string;
27
+ /** Position in the original page section list. */
28
+ index: number;
29
+ /** CMS-resolved props without section-loader enrichment. */
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
+ rawProps: Record<string, any>;
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Async rendering configuration
36
+ // ---------------------------------------------------------------------------
37
+
38
+ export interface AsyncRenderingConfig {
39
+ /**
40
+ * When true, sections wrapped in `website/sections/Rendering/Lazy.tsx`
41
+ * in the CMS are deferred and loaded on scroll. This respects the
42
+ * editor's per-section choices made in the admin.
43
+ * @default true
44
+ */
45
+ respectCmsLazy: boolean;
46
+ /**
47
+ * Fallback threshold: sections at or above this index are rendered
48
+ * eagerly regardless. Only applies to sections NOT wrapped in Lazy.
49
+ * Set to `Infinity` to disable threshold-based deferral entirely
50
+ * (rely only on CMS Lazy wrappers).
51
+ * @default Infinity
52
+ */
53
+ foldThreshold: number;
54
+ /** Section component keys that must always be rendered eagerly. */
55
+ alwaysEager: Set<string>;
56
+ }
57
+
58
+ // Always read from globalThis so split-module copies see updates
59
+ function getAsyncConfig(): AsyncRenderingConfig | null {
60
+ return G.__deco.asyncConfig ?? null;
61
+ }
62
+
63
+ /**
64
+ * Enable async section rendering.
65
+ *
66
+ * By default, respects the CMS Lazy wrapper (`website/sections/Rendering/Lazy.tsx`)
67
+ * as the source of truth — sections the editor marked as Lazy are deferred,
68
+ * everything else is eager.
69
+ *
70
+ * Optionally, `foldThreshold` can be used as a fallback for sections NOT
71
+ * wrapped in Lazy (e.g. defer everything below index 3).
72
+ *
73
+ * When not called, all sections are resolved eagerly (backward compatible).
74
+ */
75
+ export function setAsyncRenderingConfig(config?: {
76
+ foldThreshold?: number;
77
+ alwaysEager?: string[];
78
+ respectCmsLazy?: boolean;
79
+ }): void {
80
+ G.__deco.asyncConfig = {
81
+ respectCmsLazy: config?.respectCmsLazy ?? true,
82
+ foldThreshold: config?.foldThreshold ?? Infinity,
83
+ alwaysEager: new Set(config?.alwaysEager ?? []),
84
+ };
85
+ }
86
+
87
+ /** Read-only access to the current config (null when disabled). */
88
+ export function getAsyncRenderingConfig(): AsyncRenderingConfig | null {
89
+ return getAsyncConfig();
90
+ }
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // Bot detection — bots always receive fully eager pages for SEO
94
+ // ---------------------------------------------------------------------------
95
+
96
+ const BOT_UA_RE =
97
+ /bot|crawl|spider|slurp|facebookexternalhit|mediapartners|google|bing|yandex|baidu|duckduck|teoma|ia_archiver|semrush|ahrefs|lighthouse/i;
98
+
99
+ function isBot(userAgent?: string): boolean {
100
+ if (!userAgent) return false;
101
+ return BOT_UA_RE.test(userAgent);
102
+ }
103
+
104
+ export type CommerceLoader = (props: any) => Promise<any>;
105
+
106
+ /**
107
+ * Context passed through the resolution pipeline.
108
+ * Includes HTTP request info for matcher evaluation and per-request memoization.
109
+ */
110
+ export interface MatcherContext {
111
+ userAgent?: string;
112
+ url?: string;
113
+ path?: string;
114
+ cookies?: Record<string, string>;
115
+ headers?: Record<string, string>;
116
+ request?: Request;
117
+ }
118
+
119
+ /**
120
+ * Internal resolution context with memoization and error tracking.
121
+ * Created once per resolveDecoPage / resolveValue call tree.
122
+ */
123
+ interface ResolveContext {
124
+ routeParams?: Record<string, string>;
125
+ matcherCtx: MatcherContext;
126
+ memo: Map<string, unknown>;
127
+ depth: number;
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Configuration
132
+ // ---------------------------------------------------------------------------
133
+
134
+ const SKIP_RESOLVE_TYPES = new Set([
135
+ "Deco",
136
+ "htmx/sections/htmx.tsx",
137
+ "website/sections/Analytics/Analytics.tsx",
138
+ "algolia/sections/Analytics/Algolia.tsx",
139
+ "shopify/loaders/proxy.ts",
140
+ "vtex/loaders/proxy.ts",
141
+ "website/loaders/pages.ts",
142
+ "website/loaders/redirects.ts",
143
+ "website/loaders/fonts/googleFonts.ts",
144
+ "commerce/sections/Seo/SeoPDP.tsx",
145
+ "commerce/sections/Seo/SeoPDPV2.tsx",
146
+ "commerce/sections/Seo/SeoPLP.tsx",
147
+ "commerce/sections/Seo/SeoPLPV2.tsx",
148
+ "website/sections/Seo/Seo.tsx",
149
+ "website/sections/Seo/SeoV2.tsx",
150
+ "deco-sites/std/sections/SEO.tsx",
151
+ ]);
152
+
153
+ /** Add a __resolveType that should be skipped during resolution. */
154
+ export function addSkipResolveType(resolveType: string) {
155
+ SKIP_RESOLVE_TYPES.add(resolveType);
156
+ }
157
+
158
+ const MAX_RESOLVE_DEPTH = 20;
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // Commerce loaders
162
+ // ---------------------------------------------------------------------------
163
+
164
+ const commerceLoaders: Record<string, CommerceLoader> = G.__deco.commerceLoaders;
165
+
166
+ export function registerCommerceLoader(key: string, loader: CommerceLoader) {
167
+ commerceLoaders[key] = loader;
168
+ }
169
+
170
+ export function registerCommerceLoaders(loaders: Record<string, CommerceLoader>) {
171
+ Object.assign(commerceLoaders, loaders);
172
+ }
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // Custom matchers
176
+ // ---------------------------------------------------------------------------
177
+
178
+ const customMatchers: Record<
179
+ string,
180
+ (rule: Record<string, unknown>, ctx: MatcherContext) => boolean
181
+ > = G.__deco.customMatchers;
182
+
183
+ export function registerMatcher(
184
+ key: string,
185
+ fn: (rule: Record<string, unknown>, ctx: MatcherContext) => boolean,
186
+ ) {
187
+ customMatchers[key] = fn;
188
+ }
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // Error handling
192
+ // ---------------------------------------------------------------------------
193
+
194
+ export type ResolveErrorHandler = (error: unknown, resolveType: string, context: string) => void;
195
+
196
+ let onResolveError: ResolveErrorHandler = (error, resolveType, context) => {
197
+ console.error(`[CMS] ${context} "${resolveType}" failed:`, error);
198
+ };
199
+
200
+ /** Configure a custom error handler for resolution failures. */
201
+ export function setResolveErrorHandler(handler: ResolveErrorHandler) {
202
+ onResolveError = handler;
203
+ }
204
+
205
+ // ---------------------------------------------------------------------------
206
+ // Dangling reference handling
207
+ // ---------------------------------------------------------------------------
208
+
209
+ export type DanglingReferenceHandler = (resolveType: string) => unknown;
210
+
211
+ let onDanglingReference: DanglingReferenceHandler = (resolveType) => {
212
+ console.warn(`[CMS] Unhandled resolver: ${resolveType}`);
213
+ return null;
214
+ };
215
+
216
+ /** Configure how unresolvable __resolveType references are handled. */
217
+ export function setDanglingReferenceHandler(handler: DanglingReferenceHandler) {
218
+ onDanglingReference = handler;
219
+ }
220
+
221
+ // ---------------------------------------------------------------------------
222
+ // Init hook
223
+ // ---------------------------------------------------------------------------
224
+
225
+ export function onBeforeResolve(callback: () => void) {
226
+ G.__deco.initCallback = callback;
227
+ }
228
+
229
+ function ensureInitialized() {
230
+ if (!G.__deco.initialized && G.__deco.initCallback) {
231
+ G.__deco.initCallback();
232
+ G.__deco.initialized = true;
233
+ }
234
+ }
235
+
236
+ // ---------------------------------------------------------------------------
237
+ // Matcher evaluation
238
+ // ---------------------------------------------------------------------------
239
+
240
+ function evaluateMatcher(rule: Record<string, unknown> | undefined, ctx: MatcherContext): boolean {
241
+ if (!rule) return true;
242
+
243
+ const resolveType = rule.__resolveType as string | undefined;
244
+ if (!resolveType) return true;
245
+
246
+ const blocks = loadBlocks();
247
+
248
+ if (blocks[resolveType]) {
249
+ const resolvedRule = blocks[resolveType] as Record<string, unknown>;
250
+ return evaluateMatcher(
251
+ { ...resolvedRule, ...rule, __resolveType: resolvedRule.__resolveType as string },
252
+ ctx,
253
+ );
254
+ }
255
+
256
+ switch (resolveType) {
257
+ case "website/matchers/always.ts":
258
+ case "$live/matchers/MatchAlways.ts":
259
+ return true;
260
+
261
+ case "website/matchers/never.ts":
262
+ return false;
263
+
264
+ case "website/matchers/device.ts": {
265
+ const ua = (ctx.userAgent || "").toLowerCase();
266
+ const isMobile = /mobile|android|iphone|ipad|ipod|webos|blackberry|opera mini|iemobile/i.test(
267
+ ua,
268
+ );
269
+ if (rule.mobile) return isMobile;
270
+ if (rule.desktop) return !isMobile;
271
+ return true;
272
+ }
273
+
274
+ case "website/matchers/random.ts": {
275
+ const traffic = typeof rule.traffic === "number" ? rule.traffic : 0.5;
276
+ return Math.random() < traffic;
277
+ }
278
+
279
+ case "website/matchers/date.ts": {
280
+ const now = Date.now();
281
+ const start = typeof rule.start === "string" ? new Date(rule.start).getTime() : 0;
282
+ const end = typeof rule.end === "string" ? new Date(rule.end).getTime() : Infinity;
283
+ return now >= start && now <= end;
284
+ }
285
+
286
+ default: {
287
+ const customMatcher = customMatchers[resolveType];
288
+ if (customMatcher) {
289
+ try {
290
+ return customMatcher(rule, ctx);
291
+ } catch {
292
+ return false;
293
+ }
294
+ }
295
+ console.warn(`[CMS] Unknown matcher: ${resolveType}, defaulting to false`);
296
+ return false;
297
+ }
298
+ }
299
+ }
300
+
301
+ // ---------------------------------------------------------------------------
302
+ // Select (partial field picking)
303
+ // ---------------------------------------------------------------------------
304
+
305
+ function applySelect(value: unknown, select?: string[]): unknown {
306
+ if (!select || !select.length || !value || typeof value !== "object") return value;
307
+ if (Array.isArray(value)) return value.map((item) => applySelect(item, select));
308
+
309
+ const result: Record<string, unknown> = {};
310
+ for (const key of select) {
311
+ if (key in (value as Record<string, unknown>)) {
312
+ result[key] = (value as Record<string, unknown>)[key];
313
+ }
314
+ }
315
+ return result;
316
+ }
317
+
318
+ // ---------------------------------------------------------------------------
319
+ // Core resolution
320
+ // ---------------------------------------------------------------------------
321
+
322
+ async function resolveProps(
323
+ obj: Record<string, unknown>,
324
+ rctx: ResolveContext,
325
+ ): Promise<Record<string, unknown>> {
326
+ const entries = Object.entries(obj);
327
+ const resolvedEntries = await Promise.all(
328
+ entries.map(async ([k, v]) => [k, await internalResolve(v, rctx)] as const),
329
+ );
330
+ return Object.fromEntries(resolvedEntries);
331
+ }
332
+
333
+ async function internalResolve(value: unknown, rctx: ResolveContext): Promise<unknown> {
334
+ if (!value || typeof value !== "object") return value;
335
+ if (Array.isArray(value)) {
336
+ return Promise.all(value.map((item) => internalResolve(item, rctx)));
337
+ }
338
+
339
+ const obj = value as Record<string, unknown>;
340
+
341
+ if (!obj.__resolveType) {
342
+ return resolveProps(obj, rctx);
343
+ }
344
+
345
+ const resolveType = obj.__resolveType as string;
346
+
347
+ if (SKIP_RESOLVE_TYPES.has(resolveType)) return null;
348
+
349
+ if (rctx.depth > MAX_RESOLVE_DEPTH) {
350
+ console.error(`[CMS] Max resolution depth (${MAX_RESOLVE_DEPTH}) exceeded at: ${resolveType}`);
351
+ return null;
352
+ }
353
+
354
+ const childCtx: ResolveContext = { ...rctx, depth: rctx.depth + 1 };
355
+
356
+ // "resolved" short-circuit
357
+ if (resolveType === "resolved") return obj.data ?? null;
358
+
359
+ // Lazy section wrapper
360
+ if (resolveType === "website/sections/Rendering/Lazy.tsx") {
361
+ return obj.section ? internalResolve(obj.section, childCtx) : null;
362
+ }
363
+
364
+ // Request param extraction
365
+ if (resolveType === "website/functions/requestToParam.ts") {
366
+ const paramName = (obj as any).param as string;
367
+ return rctx.routeParams?.[paramName] ?? null;
368
+ }
369
+
370
+ // Commerce extension wrappers — unwrap to inner data
371
+ if (
372
+ resolveType === "commerce/loaders/product/extensions/detailsPage.ts" ||
373
+ resolveType === "commerce/loaders/product/extensions/listingPage.ts"
374
+ ) {
375
+ return obj.data ? internalResolve(obj.data, childCtx) : null;
376
+ }
377
+
378
+ // Multivariate flags
379
+ if (
380
+ resolveType === "website/flags/multivariate.ts" ||
381
+ resolveType === "website/flags/multivariate/section.ts"
382
+ ) {
383
+ const variants = obj.variants as Array<{ value: unknown; rule?: unknown }> | undefined;
384
+ if (!variants || variants.length === 0) return null;
385
+
386
+ for (const variant of variants) {
387
+ const rule = variant.rule as Record<string, unknown> | undefined;
388
+ if (evaluateMatcher(rule, rctx.matcherCtx)) {
389
+ return internalResolve(variant.value, childCtx);
390
+ }
391
+ }
392
+ return null;
393
+ }
394
+
395
+ // Commerce loaders
396
+ const commerceLoader = commerceLoaders[resolveType];
397
+ if (commerceLoader) {
398
+ const { __resolveType: _, ...loaderProps } = obj;
399
+ const resolvedProps = await resolveProps(loaderProps, childCtx);
400
+
401
+ if (rctx.matcherCtx.path) {
402
+ resolvedProps.__pagePath = rctx.matcherCtx.path;
403
+ }
404
+ if (rctx.matcherCtx.url) {
405
+ resolvedProps.__pageUrl = rctx.matcherCtx.url;
406
+ }
407
+
408
+ try {
409
+ return await commerceLoader(resolvedProps);
410
+ } catch (error) {
411
+ onResolveError(error, resolveType, "Commerce loader");
412
+ return null;
413
+ }
414
+ }
415
+
416
+ // Named block reference (memoized)
417
+ const blocks = loadBlocks();
418
+ if (blocks[resolveType]) {
419
+ const memoKey = JSON.stringify(obj);
420
+ if (rctx.memo.has(memoKey)) {
421
+ return rctx.memo.get(memoKey);
422
+ }
423
+
424
+ const referencedBlock = blocks[resolveType] as Record<string, unknown>;
425
+ const { __resolveType: _rt, ...restOverrides } = obj;
426
+ const resultPromise = internalResolve({ ...referencedBlock, ...restOverrides }, childCtx);
427
+ rctx.memo.set(memoKey, resultPromise);
428
+
429
+ const result = await resultPromise;
430
+ rctx.memo.set(memoKey, result);
431
+ return result;
432
+ }
433
+
434
+ // Dangling reference — unresolvable __resolveType
435
+ if (resolveType.includes("/loaders/") || resolveType.includes("/actions/")) {
436
+ return onDanglingReference(resolveType);
437
+ }
438
+
439
+ // Unknown type — resolve props but preserve __resolveType (it's a section)
440
+ const { __resolveType: _, ...rest } = obj;
441
+ const resolvedRest = await resolveProps(rest, childCtx);
442
+ return { __resolveType: resolveType, ...resolvedRest };
443
+ }
444
+
445
+ // ---------------------------------------------------------------------------
446
+ // Nested section normalization
447
+ // ---------------------------------------------------------------------------
448
+
449
+ /**
450
+ * Recursively walks resolved props and converts nested sections
451
+ * from `{ __resolveType: "site/sections/...", ...props }` to
452
+ * `{ component: "site/sections/...", props: {...} }`.
453
+ *
454
+ * This preserves the same `{ Component, props }` shape used by deco-cx/deco (Fresh)
455
+ * so that section code can be ported without API changes. In TanStack, `Component`
456
+ * is the registry key string (not a function ref), and the renderer does the lookup.
457
+ */
458
+ function normalizeNestedSections(value: unknown): unknown {
459
+ if (!value || typeof value !== "object") return value;
460
+
461
+ if (Array.isArray(value)) {
462
+ return value.map(normalizeNestedSections);
463
+ }
464
+
465
+ const obj = value as Record<string, unknown>;
466
+ const rt = obj.__resolveType as string | undefined;
467
+
468
+ if (rt && getSection(rt)) {
469
+ const { __resolveType: _, ...rest } = obj;
470
+ const normalizedProps: Record<string, unknown> = {};
471
+ for (const [k, v] of Object.entries(rest)) {
472
+ normalizedProps[k] = normalizeNestedSections(v);
473
+ }
474
+ return { Component: rt, props: normalizedProps };
475
+ }
476
+
477
+ const result: Record<string, unknown> = {};
478
+ for (const [k, v] of Object.entries(obj)) {
479
+ result[k] = normalizeNestedSections(v);
480
+ }
481
+ return result;
482
+ }
483
+
484
+ // ---------------------------------------------------------------------------
485
+ // Public API
486
+ // ---------------------------------------------------------------------------
487
+
488
+ /**
489
+ * Resolve a value by recursively processing __resolveType references.
490
+ * Supports memoization, commerce loaders, matchers, and flags.
491
+ */
492
+ export async function resolveValue(
493
+ value: unknown,
494
+ routeParams?: Record<string, string>,
495
+ matcherCtx?: MatcherContext,
496
+ options?: { select?: string[] },
497
+ ): Promise<unknown> {
498
+ const rctx: ResolveContext = {
499
+ routeParams,
500
+ matcherCtx: matcherCtx ?? {},
501
+ memo: new Map(),
502
+ depth: 0,
503
+ };
504
+ const result = await internalResolve(value, rctx);
505
+ return options?.select ? applySelect(result, options.select) : result;
506
+ }
507
+
508
+ // ---------------------------------------------------------------------------
509
+ // Layout section CMS-resolve cache
510
+ // Caches the fully-resolved CMS output for layout sections so that
511
+ // commerce loaders (intelligent-search, cross-selling, etc.) inside
512
+ // Header/Footer blocks aren't re-executed on every page navigation.
513
+ // ---------------------------------------------------------------------------
514
+
515
+ const RESOLVE_CACHE_TTL = 5 * 60_000;
516
+
517
+ interface ResolvedSectionsCache {
518
+ sections: ResolvedSection[];
519
+ expiresAt: number;
520
+ }
521
+
522
+ const resolvedLayoutCache = new Map<string, ResolvedSectionsCache>();
523
+ const resolvedLayoutInflight = new Map<string, Promise<ResolvedSection[]>>();
524
+
525
+ function getCachedResolvedLayout(blockKey: string): ResolvedSection[] | null {
526
+ const entry = resolvedLayoutCache.get(blockKey);
527
+ if (!entry) return null;
528
+ if (Date.now() > entry.expiresAt) {
529
+ resolvedLayoutCache.delete(blockKey);
530
+ return null;
531
+ }
532
+ return entry.sections;
533
+ }
534
+
535
+ function setCachedResolvedLayout(blockKey: string, sections: ResolvedSection[]): void {
536
+ resolvedLayoutCache.set(blockKey, {
537
+ sections,
538
+ expiresAt: Date.now() + RESOLVE_CACHE_TTL,
539
+ });
540
+ }
541
+
542
+ /**
543
+ * Resolves a raw section block to ResolvedSection[].
544
+ * Used for both layout and non-layout sections.
545
+ */
546
+ async function resolveRawSection(
547
+ section: unknown,
548
+ rctx: ResolveContext,
549
+ ): Promise<ResolvedSection[]> {
550
+ const resolved = await internalResolve(section, rctx);
551
+ if (!resolved || typeof resolved !== "object") return [];
552
+
553
+ const items = Array.isArray(resolved) ? resolved : [resolved];
554
+ const results: ResolvedSection[] = [];
555
+
556
+ for (const item of items) {
557
+ if (!item || typeof item !== "object") continue;
558
+ const obj = item as Record<string, unknown>;
559
+ if (!obj.__resolveType) continue;
560
+
561
+ const resolveType = obj.__resolveType as string;
562
+ const sectionLoader = getSection(resolveType);
563
+ if (!sectionLoader) {
564
+ console.warn(`[CMS] No component registered for: ${resolveType}`);
565
+ continue;
566
+ }
567
+
568
+ const { __resolveType: _, ...rawProps } = obj;
569
+ const props = normalizeNestedSections(rawProps) as Record<string, unknown>;
570
+ results.push({
571
+ component: resolveType,
572
+ props,
573
+ key: resolveType,
574
+ });
575
+ }
576
+
577
+ return results;
578
+ }
579
+
580
+ /**
581
+ * Check if a raw CMS section block will produce a layout section.
582
+ * Walks the block reference chain (up to 5 levels) to find the final
583
+ * section component key. Returns the top-level block name for caching.
584
+ *
585
+ * Example: {"__resolveType": "Header - 01"} -> block has
586
+ * __resolveType "site/sections/Header/Header.tsx" -> that's a layout section
587
+ * -> returns "Header - 01" as the cache key.
588
+ */
589
+ function isRawSectionLayout(section: unknown): string | null {
590
+ if (!section || typeof section !== "object") return null;
591
+ const obj = section as Record<string, unknown>;
592
+ const topLevelRt = obj.__resolveType as string | undefined;
593
+ if (!topLevelRt) return null;
594
+
595
+ if (isLayoutSection(topLevelRt)) return topLevelRt;
596
+
597
+ const blocks = loadBlocks();
598
+ let currentRt: string | undefined = topLevelRt;
599
+ for (let i = 0; i < 5; i++) {
600
+ const block = blocks[currentRt] as Record<string, unknown> | undefined;
601
+ if (!block) break;
602
+ currentRt = block.__resolveType as string | undefined;
603
+ if (!currentRt) break;
604
+ if (isLayoutSection(currentRt)) return topLevelRt;
605
+ }
606
+
607
+ return null;
608
+ }
609
+
610
+ /**
611
+ * Resolve the final section component key by walking block references,
612
+ * unwrapping Lazy wrappers, and evaluating multivariate flags.
613
+ * Returns null if not determinable.
614
+ */
615
+ function resolveFinalSectionKey(
616
+ section: unknown,
617
+ matcherCtx?: MatcherContext,
618
+ ): string | null {
619
+ if (!section || typeof section !== "object") return null;
620
+
621
+ const blocks = loadBlocks();
622
+ let current = section as Record<string, unknown>;
623
+
624
+ for (let depth = 0; depth < 10; depth++) {
625
+ const rt = current.__resolveType as string | undefined;
626
+ if (!rt) return null;
627
+
628
+ if (rt === "website/sections/Rendering/Lazy.tsx") {
629
+ const inner = current.section;
630
+ if (!inner || typeof inner !== "object") return null;
631
+ current = inner as Record<string, unknown>;
632
+ continue;
633
+ }
634
+
635
+ if (
636
+ rt === "website/flags/multivariate.ts" ||
637
+ rt === "website/flags/multivariate/section.ts"
638
+ ) {
639
+ const variants = current.variants as
640
+ | Array<{ value: unknown; rule?: unknown }>
641
+ | undefined;
642
+ if (!variants?.length) return null;
643
+
644
+ let matched: unknown = null;
645
+ for (const variant of variants) {
646
+ const rule = variant.rule as Record<string, unknown> | undefined;
647
+ if (evaluateMatcher(rule, matcherCtx ?? {})) {
648
+ matched = variant.value;
649
+ break;
650
+ }
651
+ }
652
+ if (!matched || typeof matched !== "object") return null;
653
+ current = matched as Record<string, unknown>;
654
+ continue;
655
+ }
656
+
657
+ if (getSection(rt)) return rt;
658
+
659
+ const block = blocks[rt] as Record<string, unknown> | undefined;
660
+ if (block) {
661
+ const { __resolveType: _, ...overrides } = current;
662
+ current = { ...block, ...overrides };
663
+ continue;
664
+ }
665
+
666
+ return rt;
667
+ }
668
+
669
+ return null;
670
+ }
671
+
672
+ /**
673
+ * Check if a raw CMS section is directly wrapped in `Lazy.tsx`.
674
+ * Checks both the section itself and one level of block reference.
675
+ */
676
+ function isCmsLazyWrapped(section: unknown): boolean {
677
+ if (!section || typeof section !== "object") return false;
678
+ const obj = section as Record<string, unknown>;
679
+ const rt = obj.__resolveType as string | undefined;
680
+ if (!rt) return false;
681
+
682
+ if (rt === "website/sections/Rendering/Lazy.tsx") return true;
683
+
684
+ const blocks = loadBlocks();
685
+ const block = blocks[rt] as Record<string, unknown> | undefined;
686
+ if (block && block.__resolveType === "website/sections/Rendering/Lazy.tsx") return true;
687
+
688
+ return false;
689
+ }
690
+
691
+ function shouldDeferSection(
692
+ section: unknown,
693
+ flatIndex: number,
694
+ cfg: AsyncRenderingConfig,
695
+ isBotReq: boolean,
696
+ matcherCtx?: MatcherContext,
697
+ ): boolean {
698
+ if (isBotReq) return false;
699
+
700
+ if (!section || typeof section !== "object") return false;
701
+ const obj = section as Record<string, unknown>;
702
+ const rt = obj.__resolveType as string | undefined;
703
+ if (!rt) return false;
704
+
705
+ // Top-level flags (not wrapped in Lazy) — keep eager for safety
706
+ if (rt === "website/flags/multivariate.ts" || rt === "website/flags/multivariate/section.ts") {
707
+ return false;
708
+ }
709
+
710
+ const finalKey = resolveFinalSectionKey(section, matcherCtx);
711
+ if (!finalKey) return false;
712
+
713
+ if (cfg.alwaysEager.has(finalKey)) return false;
714
+ if (isLayoutSection(finalKey)) return false;
715
+
716
+ // Primary: respect CMS Lazy wrapper as source of truth
717
+ if (cfg.respectCmsLazy && isCmsLazyWrapped(section)) return true;
718
+
719
+ // Fallback: threshold-based deferral for non-Lazy sections
720
+ if (flatIndex >= cfg.foldThreshold) return true;
721
+
722
+ return false;
723
+ }
724
+
725
+ /**
726
+ * Follow the block reference chain to find the final section component
727
+ * and collect the CMS props WITHOUT running commerce loaders.
728
+ * Resolves named block references, lazy wrappers, and multivariate flags.
729
+ */
730
+ function resolveSectionShallow(
731
+ section: unknown,
732
+ matcherCtx?: MatcherContext,
733
+ ): DeferredSection | null {
734
+ if (!section || typeof section !== "object") return null;
735
+
736
+ const blocks = loadBlocks();
737
+ let current = section as Record<string, unknown>;
738
+ const MAX_DEPTH = 10;
739
+
740
+ for (let depth = 0; depth < MAX_DEPTH; depth++) {
741
+ const rt = current.__resolveType as string | undefined;
742
+ if (!rt) return null;
743
+
744
+ if (SKIP_RESOLVE_TYPES.has(rt)) return null;
745
+
746
+ // Lazy wrapper — unwrap to the inner section
747
+ if (rt === "website/sections/Rendering/Lazy.tsx") {
748
+ const inner = current.section;
749
+ if (!inner || typeof inner !== "object") return null;
750
+ current = inner as Record<string, unknown>;
751
+ continue;
752
+ }
753
+
754
+ // Multivariate flags — evaluate matchers and continue with matched variant
755
+ if (
756
+ rt === "website/flags/multivariate.ts" ||
757
+ rt === "website/flags/multivariate/section.ts"
758
+ ) {
759
+ const variants = current.variants as
760
+ | Array<{ value: unknown; rule?: unknown }>
761
+ | undefined;
762
+ if (!variants?.length) return null;
763
+
764
+ let matched: unknown = null;
765
+ for (const variant of variants) {
766
+ const rule = variant.rule as Record<string, unknown> | undefined;
767
+ if (evaluateMatcher(rule, matcherCtx ?? {})) {
768
+ matched = variant.value;
769
+ break;
770
+ }
771
+ }
772
+ if (!matched || typeof matched !== "object") return null;
773
+ current = matched as Record<string, unknown>;
774
+ continue;
775
+ }
776
+
777
+ // Check if this is a registered section — we found it
778
+ if (getSection(rt)) {
779
+ const { __resolveType: _, ...rawProps } = current;
780
+ return {
781
+ component: rt,
782
+ key: rt,
783
+ index: -1,
784
+ rawProps: rawProps as Record<string, unknown>,
785
+ };
786
+ }
787
+
788
+ // Named block reference — follow the chain
789
+ const block = blocks[rt] as Record<string, unknown> | undefined;
790
+ if (block) {
791
+ const { __resolveType: _rtOuter, ...overrides } = current;
792
+ current = { ...block, ...overrides };
793
+ continue;
794
+ }
795
+
796
+ return null;
797
+ }
798
+
799
+ return null;
800
+ }
801
+
802
+ /**
803
+ * Resolve outer wrappers (flags, block references) around the sections list
804
+ * to get the raw section array without resolving each individual section.
805
+ * This allows the eager/deferred split to happen before section resolution.
806
+ */
807
+ async function resolveSectionsList(
808
+ value: unknown,
809
+ rctx: ResolveContext,
810
+ depth = 0,
811
+ ): Promise<unknown[]> {
812
+ if (depth > MAX_RESOLVE_DEPTH) return [];
813
+ if (!value || typeof value !== "object") return [];
814
+ if (Array.isArray(value)) return value;
815
+
816
+ const obj = value as Record<string, unknown>;
817
+ const rt = obj.__resolveType as string | undefined;
818
+ if (!rt) return [];
819
+
820
+ // Multivariate flags — evaluate matchers and recurse into matched variant
821
+ if (rt === "website/flags/multivariate.ts" || rt === "website/flags/multivariate/section.ts") {
822
+ const variants = obj.variants as Array<{ value: unknown; rule?: unknown }> | undefined;
823
+ if (!variants?.length) return [];
824
+ for (const variant of variants) {
825
+ const rule = variant.rule as Record<string, unknown> | undefined;
826
+ if (evaluateMatcher(rule, rctx.matcherCtx)) {
827
+ return resolveSectionsList(variant.value, rctx, depth + 1);
828
+ }
829
+ }
830
+ return [];
831
+ }
832
+
833
+ // Named block reference — follow the chain
834
+ const blocks = loadBlocks();
835
+ if (blocks[rt]) {
836
+ const referencedBlock = blocks[rt] as Record<string, unknown>;
837
+ const { __resolveType: _rtOuter, ...restOverrides } = obj;
838
+ return resolveSectionsList({ ...referencedBlock, ...restOverrides }, rctx, depth + 1);
839
+ }
840
+
841
+ // Resolved — unwrap
842
+ if (rt === "resolved") {
843
+ const data = obj.data;
844
+ if (Array.isArray(data)) return data;
845
+ return [];
846
+ }
847
+
848
+ return [];
849
+ }
850
+
851
+ export interface DecoPageResult {
852
+ name: string;
853
+ path: string;
854
+ params: Record<string, string>;
855
+ resolvedSections: ResolvedSection[];
856
+ deferredSections: DeferredSection[];
857
+ }
858
+
859
+ export async function resolveDecoPage(
860
+ targetPath: string,
861
+ matcherCtx?: MatcherContext,
862
+ ): Promise<DecoPageResult | null> {
863
+ ensureInitialized();
864
+
865
+ const match = findPageByPath(targetPath);
866
+ if (!match) {
867
+ console.warn(`[CMS] No page found for path: ${targetPath}`);
868
+ return null;
869
+ }
870
+
871
+ const { page, params } = match;
872
+ const ctx: MatcherContext = { ...matcherCtx, path: targetPath };
873
+ const rctx: ResolveContext = { routeParams: params, matcherCtx: ctx, memo: new Map(), depth: 0 };
874
+
875
+ let rawSections: unknown[];
876
+ if (Array.isArray(page.sections)) {
877
+ rawSections = page.sections;
878
+ } else {
879
+ // Resolve outer flag/wrapper to get the section array
880
+ // without recursively resolving each individual section.
881
+ const resolved = await resolveSectionsList(page.sections, rctx);
882
+ rawSections = resolved;
883
+ }
884
+
885
+ const isBotReq = isBot(matcherCtx?.userAgent);
886
+ const currentAsyncConfig = getAsyncConfig();
887
+ const useAsync = currentAsyncConfig !== null && !isBotReq;
888
+
889
+ const eagerResults: (ResolvedSection[] | Promise<ResolvedSection[]>)[] = [];
890
+ const deferredSections: DeferredSection[] = [];
891
+ let flatIndex = 0;
892
+
893
+ for (const section of rawSections) {
894
+ const currentFlatIndex = flatIndex;
895
+
896
+ const shouldDefer =
897
+ useAsync && shouldDeferSection(section, currentFlatIndex, currentAsyncConfig!, isBotReq, ctx);
898
+
899
+ if (shouldDefer) {
900
+ let deferredOk = false;
901
+ try {
902
+ const deferred = resolveSectionShallow(section, ctx);
903
+ if (deferred) {
904
+ deferred.index = currentFlatIndex;
905
+ deferredSections.push(deferred);
906
+ deferredOk = true;
907
+ }
908
+ } catch (e) {
909
+ onResolveError(e, "section", "Deferred section resolution");
910
+ }
911
+
912
+ if (!deferredOk) {
913
+ // Shallow resolution failed — fall back to eager resolution
914
+ const idx = currentFlatIndex;
915
+ eagerResults.push(
916
+ resolveRawSection(section, rctx).then((sections) => {
917
+ for (const s of sections) s.index = idx;
918
+ return sections;
919
+ }),
920
+ );
921
+ }
922
+ flatIndex++;
923
+ } else {
924
+ // Eager: full resolution (existing logic)
925
+ const promise = (async (): Promise<ResolvedSection[]> => {
926
+ try {
927
+ const layoutKey = isRawSectionLayout(section);
928
+
929
+ if (layoutKey) {
930
+ const cached = getCachedResolvedLayout(layoutKey);
931
+ if (cached) return cached;
932
+
933
+ const inflight = resolvedLayoutInflight.get(layoutKey);
934
+ if (inflight) return inflight;
935
+
936
+ const p = resolveRawSection(section, rctx).then((results) => {
937
+ setCachedResolvedLayout(layoutKey, results);
938
+ return results;
939
+ });
940
+ resolvedLayoutInflight.set(layoutKey, p);
941
+ p.finally(() => resolvedLayoutInflight.delete(layoutKey));
942
+ return p;
943
+ }
944
+
945
+ return resolveRawSection(section, rctx);
946
+ } catch (e) {
947
+ onResolveError(e, "section", "Section resolution");
948
+ return [];
949
+ }
950
+ })();
951
+
952
+ const idx = currentFlatIndex;
953
+ eagerResults.push(promise.then((sections) => {
954
+ for (const s of sections) s.index = idx;
955
+ return sections;
956
+ }));
957
+ flatIndex++;
958
+ }
959
+ }
960
+
961
+ const allResults = await Promise.all(eagerResults);
962
+
963
+ return {
964
+ name: page.name,
965
+ path: page.path || targetPath,
966
+ params,
967
+ resolvedSections: allResults.flat(),
968
+ deferredSections,
969
+ };
970
+ }
971
+
972
+ /**
973
+ * Resolve a single deferred section's raw props into a fully resolved section.
974
+ * Called by the loadDeferredSection server function when a section scrolls into view.
975
+ *
976
+ * This runs the full resolution pipeline on the raw CMS props:
977
+ * - Resolves nested __resolveType references (commerce loaders, block refs, flags)
978
+ * - Normalizes nested sections to { Component, props } shape
979
+ *
980
+ * After this, the section goes through runSingleSectionLoader for final enrichment.
981
+ */
982
+ export async function resolveDeferredSection(
983
+ component: string,
984
+ rawProps: Record<string, unknown>,
985
+ pagePath: string,
986
+ matcherCtx?: MatcherContext,
987
+ ): Promise<ResolvedSection | null> {
988
+ ensureInitialized();
989
+
990
+ const ctx: MatcherContext = { ...matcherCtx, path: pagePath };
991
+ const rctx: ResolveContext = {
992
+ matcherCtx: ctx,
993
+ memo: new Map(),
994
+ depth: 0,
995
+ };
996
+
997
+ const resolvedProps = await resolveProps(rawProps, rctx);
998
+ const normalizedProps = normalizeNestedSections(resolvedProps) as Record<string, unknown>;
999
+
1000
+ return {
1001
+ component,
1002
+ props: normalizedProps,
1003
+ key: component,
1004
+ };
1005
+ }