@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.
- package/.cursor/skills/deco-api-call-dedup/SKILL.md +443 -0
- package/.cursor/skills/deco-apps-architecture/SKILL.md +255 -0
- package/.cursor/skills/deco-apps-architecture/app-pattern.md +288 -0
- package/.cursor/skills/deco-apps-architecture/commerce-types.md +239 -0
- package/.cursor/skills/deco-apps-architecture/new-app-guide.md +268 -0
- package/.cursor/skills/deco-apps-architecture/scripts-codegen.md +148 -0
- package/.cursor/skills/deco-apps-architecture/shared-utils.md +181 -0
- package/.cursor/skills/deco-apps-architecture/vtex-deep-structure.md +253 -0
- package/.cursor/skills/deco-apps-architecture/website-app.md +169 -0
- package/.cursor/skills/deco-apps-vtex-porting/SKILL.md +189 -0
- package/.cursor/skills/deco-apps-vtex-porting/adaptation-patterns.md +335 -0
- package/.cursor/skills/deco-apps-vtex-porting/commerce-porting.md +155 -0
- package/.cursor/skills/deco-apps-vtex-porting/cookie-auth-patterns.md +148 -0
- package/.cursor/skills/deco-apps-vtex-porting/structure-map.md +234 -0
- package/.cursor/skills/deco-apps-vtex-porting/transform-mapping.md +99 -0
- package/.cursor/skills/deco-apps-vtex-porting/website-porting.md +194 -0
- package/.cursor/skills/deco-apps-vtex-review/SKILL.md +234 -0
- package/.cursor/skills/deco-async-rendering-architecture/SKILL.md +270 -0
- package/.cursor/skills/deco-async-rendering-site-guide/SKILL.md +417 -0
- package/.cursor/skills/deco-cms-layout-caching/SKILL.md +293 -0
- package/.cursor/skills/deco-cms-route-config/SKILL.md +388 -0
- package/.cursor/skills/deco-core-architecture/SKILL.md +185 -0
- package/.cursor/skills/deco-core-architecture/blocks.md +196 -0
- package/.cursor/skills/deco-core-architecture/deco-vs-deco-start.md +191 -0
- package/.cursor/skills/deco-core-architecture/engine.md +220 -0
- package/.cursor/skills/deco-core-architecture/hooks-components.md +157 -0
- package/.cursor/skills/deco-core-architecture/plugins-clients.md +136 -0
- package/.cursor/skills/deco-core-architecture/runtime.md +116 -0
- package/.cursor/skills/deco-core-architecture/site-usage.md +165 -0
- package/.cursor/skills/deco-e2e-testing/SKILL.md +372 -0
- package/.cursor/skills/deco-e2e-testing/discovery.md +337 -0
- package/.cursor/skills/deco-e2e-testing/scripts/scaffold.sh +81 -0
- package/.cursor/skills/deco-e2e-testing/selectors.md +175 -0
- package/.cursor/skills/deco-e2e-testing/templates/package.json +18 -0
- package/.cursor/skills/deco-e2e-testing/templates/playwright.config.ts +65 -0
- package/.cursor/skills/deco-e2e-testing/templates/scripts/baseline.ts +279 -0
- package/.cursor/skills/deco-e2e-testing/templates/scripts/run-e2e.ts +194 -0
- package/.cursor/skills/deco-e2e-testing/templates/specs/ecommerce-flow.spec.ts +612 -0
- package/.cursor/skills/deco-e2e-testing/templates/tsconfig.json +12 -0
- package/.cursor/skills/deco-e2e-testing/templates/utils/metrics-collector.ts +918 -0
- package/.cursor/skills/deco-e2e-testing/troubleshooting.md +602 -0
- package/.cursor/skills/deco-edge-caching/SKILL.md +316 -0
- package/.cursor/skills/deco-full-analysis/SKILL.md +898 -0
- package/.cursor/skills/deco-full-analysis/checklists/asset-optimization.md +251 -0
- package/.cursor/skills/deco-full-analysis/checklists/bug-fix.md +189 -0
- package/.cursor/skills/deco-full-analysis/checklists/cache-strategy.md +144 -0
- package/.cursor/skills/deco-full-analysis/checklists/dependency-update.md +150 -0
- package/.cursor/skills/deco-full-analysis/checklists/hydration-fix.md +191 -0
- package/.cursor/skills/deco-full-analysis/checklists/image-optimization.md +180 -0
- package/.cursor/skills/deco-full-analysis/checklists/loader-optimization.md +165 -0
- package/.cursor/skills/deco-full-analysis/checklists/seo-fix.md +183 -0
- package/.cursor/skills/deco-full-analysis/checklists/site-cleanup.md +281 -0
- package/.cursor/skills/deco-full-analysis/discovery.md +548 -0
- package/.cursor/skills/deco-incident-debugging/SKILL.md +378 -0
- package/.cursor/skills/deco-incident-debugging/headless-mode.md +510 -0
- package/.cursor/skills/deco-incident-debugging/learnings-index.md +227 -0
- package/.cursor/skills/deco-incident-debugging/triage-workflow.md +312 -0
- package/.cursor/skills/deco-islands-migration/SKILL.md +251 -0
- package/.cursor/skills/deco-loader-n-plus-1-detector/SKILL.md +275 -0
- package/.cursor/skills/deco-performance-audit/SKILL.md +530 -0
- package/.cursor/skills/deco-performance-audit/tools-reference.md +428 -0
- package/.cursor/skills/deco-performance-audit/workflow.md +457 -0
- package/.cursor/skills/deco-server-functions-invoke/SKILL.md +92 -0
- package/.cursor/skills/deco-server-functions-invoke/architecture.md +166 -0
- package/.cursor/skills/deco-server-functions-invoke/generator.md +122 -0
- package/.cursor/skills/deco-server-functions-invoke/problem.md +98 -0
- package/.cursor/skills/deco-server-functions-invoke/troubleshooting.md +110 -0
- package/.cursor/skills/deco-site-deployment/SKILL.md +396 -0
- package/.cursor/skills/deco-site-memory-debugging/SKILL.md +121 -0
- package/.cursor/skills/deco-site-memory-debugging/cdp-connection.md +222 -0
- package/.cursor/skills/deco-site-memory-debugging/memory-analysis.md +362 -0
- package/.cursor/skills/deco-site-patterns/SKILL.md +124 -0
- package/.cursor/skills/deco-site-patterns/app-composition.md +337 -0
- package/.cursor/skills/deco-site-patterns/client-patterns.md +341 -0
- package/.cursor/skills/deco-site-patterns/cms-wiring.md +230 -0
- package/.cursor/skills/deco-site-patterns/section-patterns.md +340 -0
- package/.cursor/skills/deco-site-scaling-tuning/SKILL.md +240 -0
- package/.cursor/skills/deco-site-scaling-tuning/analysis-scripts.md +267 -0
- package/.cursor/skills/deco-start-architecture/SKILL.md +218 -0
- package/.cursor/skills/deco-start-architecture/admin-protocol.md +156 -0
- package/.cursor/skills/deco-start-architecture/cms-resolution.md +201 -0
- package/.cursor/skills/deco-start-architecture/code-quality.md +158 -0
- package/.cursor/skills/deco-start-architecture/gap-analysis.md +129 -0
- package/.cursor/skills/deco-start-architecture/sdk-utilities.md +197 -0
- package/.cursor/skills/deco-start-architecture/worker-entry-caching.md +154 -0
- package/.cursor/skills/deco-startup-analysis/SKILL.md +248 -0
- package/.cursor/skills/deco-storefront-test-checklist/SKILL.md +369 -0
- package/.cursor/skills/deco-tanstack-hydration-fixes/SKILL.md +468 -0
- package/.cursor/skills/deco-tanstack-navigation/SKILL.md +681 -0
- package/.cursor/skills/deco-tanstack-search/SKILL.md +411 -0
- package/.cursor/skills/deco-tanstack-storefront-patterns/SKILL.md +1013 -0
- package/.cursor/skills/deco-to-tanstack-migration/SKILL.md +518 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/codemod-commands.md +174 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/commerce/README.md +78 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/deco-framework/README.md +128 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/gotchas.md +719 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/imports/README.md +70 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/platform-hooks/README.md +154 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/signals/README.md +220 -0
- package/.cursor/skills/deco-to-tanstack-migration/references/vite-config/README.md +78 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/package-json.md +55 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/root-route.md +110 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/router.md +96 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/setup-ts.md +167 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/vite-config.md +122 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/worker-entry.md +67 -0
- package/.cursor/skills/deco-typescript-fixes/SKILL.md +178 -0
- package/.cursor/skills/deco-typescript-fixes/common-fixes.md +330 -0
- package/.cursor/skills/deco-typescript-fixes/strategy.md +148 -0
- package/.cursor/skills/deco-variant-selection-perf/SKILL.md +272 -0
- package/.cursor/skills/deco-vtex-fetch-cache/SKILL.md +225 -0
- package/.cursor/skills/find-skills/SKILL.md +133 -0
- package/.cursor/skills/incident-report/SKILL.md +179 -0
- package/.cursor/skills/incident-report/references/5-whys.md +75 -0
- package/.cursor/skills/incident-report/templates/client-report.md +187 -0
- package/.cursor/skills/incident-report/templates/internal-report.md +206 -0
- package/.cursor/skills/template-skill/SKILL.md +38 -0
- package/.github/workflows/release.yml +32 -0
- package/.releaserc.json +25 -0
- package/CLAUDE.md +135 -0
- package/GAP_ANALYSIS.md +224 -0
- package/GAP_ANALYSIS_V2.md +1013 -0
- package/biome.json +39 -0
- package/knip.json +5 -0
- package/package.json +87 -0
- package/scripts/generate-blocks.ts +69 -0
- package/scripts/generate-invoke.ts +378 -0
- package/scripts/generate-schema.ts +657 -0
- package/src/admin/cors.ts +29 -0
- package/src/admin/decofile.ts +72 -0
- package/src/admin/index.ts +24 -0
- package/src/admin/invoke.ts +163 -0
- package/src/admin/liveControls.ts +29 -0
- package/src/admin/meta.ts +70 -0
- package/src/admin/render.ts +205 -0
- package/src/admin/schema.ts +686 -0
- package/src/admin/setup.ts +44 -0
- package/src/cms/index.ts +59 -0
- package/src/cms/loader.ts +180 -0
- package/src/cms/registry.ts +162 -0
- package/src/cms/resolve.ts +1005 -0
- package/src/cms/sectionLoaders.ts +294 -0
- package/src/hooks/DecoPageRenderer.tsx +444 -0
- package/src/hooks/LazySection.tsx +109 -0
- package/src/hooks/LiveControls.tsx +108 -0
- package/src/hooks/SectionErrorFallback.tsx +85 -0
- package/src/hooks/index.ts +8 -0
- package/src/index.ts +5 -0
- package/src/matchers/builtins.ts +184 -0
- package/src/matchers/posthog.ts +154 -0
- package/src/middleware/decoState.ts +55 -0
- package/src/middleware/healthMetrics.ts +131 -0
- package/src/middleware/index.ts +80 -0
- package/src/middleware/liveness.ts +21 -0
- package/src/middleware/observability.ts +205 -0
- package/src/routes/adminRoutes.ts +83 -0
- package/src/routes/cmsRoute.ts +302 -0
- package/src/routes/components.tsx +34 -0
- package/src/routes/index.ts +15 -0
- package/src/sdk/analytics.ts +72 -0
- package/src/sdk/cacheHeaders.ts +268 -0
- package/src/sdk/cachedLoader.ts +206 -0
- package/src/sdk/clx.ts +3 -0
- package/src/sdk/cookie.ts +39 -0
- package/src/sdk/createInvoke.ts +57 -0
- package/src/sdk/csp.ts +59 -0
- package/src/sdk/env.ts +27 -0
- package/src/sdk/index.ts +63 -0
- package/src/sdk/instrumentedFetch.ts +137 -0
- package/src/sdk/invoke.ts +133 -0
- package/src/sdk/mergeCacheControl.ts +150 -0
- package/src/sdk/redirects.ts +217 -0
- package/src/sdk/requestContext.ts +184 -0
- package/src/sdk/serverTimings.ts +68 -0
- package/src/sdk/signal.ts +41 -0
- package/src/sdk/sitemap.ts +143 -0
- package/src/sdk/urlUtils.ts +117 -0
- package/src/sdk/useDevice.ts +82 -0
- package/src/sdk/useId.ts +7 -0
- package/src/sdk/useScript.ts +101 -0
- package/src/sdk/workerEntry.ts +703 -0
- package/src/sdk/wrapCaughtErrors.ts +107 -0
- package/src/types/index.ts +39 -0
- package/src/types/widgets.ts +13 -0
- 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
|
+
}
|