@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,444 @@
1
+ import {
2
+ type ComponentType,
3
+ createElement,
4
+ lazy,
5
+ type ReactNode,
6
+ Suspense,
7
+ useEffect,
8
+ useRef,
9
+ useState,
10
+ } from "react";
11
+ import type { SectionOptions } from "../cms/registry";
12
+ import {
13
+ getResolvedComponent,
14
+ getSectionOptions,
15
+ getSectionRegistry,
16
+ getSyncComponent,
17
+ preloadSectionModule,
18
+ setResolvedComponent,
19
+ } from "../cms/registry";
20
+ import type { DeferredSection, ResolvedSection } from "../cms/resolve";
21
+ import { SectionErrorBoundary } from "./SectionErrorFallback";
22
+
23
+ type LazyComponent = ReturnType<typeof lazy>;
24
+
25
+ const lazyCache = new Map<string, LazyComponent>();
26
+
27
+ /**
28
+ * Create a React.lazy-compatible thenable that is already fulfilled.
29
+ * React internally checks `thenable.status === "fulfilled"` and reads
30
+ * `thenable.value` synchronously — no Suspense activation, no microtask.
31
+ */
32
+ function syncThenable(mod: {
33
+ default: ComponentType<any>;
34
+ }): Promise<{ default: ComponentType<any> }> {
35
+ const t = Promise.resolve(mod);
36
+ // React uses these internal properties to detect sync-resolved thenables
37
+ (t as any).status = "fulfilled";
38
+ (t as any).value = mod;
39
+ return t;
40
+ }
41
+
42
+ function getLazyComponent(key: string) {
43
+ if (!lazyCache.has(key)) {
44
+ const registry = getSectionRegistry();
45
+ const loader = registry[key];
46
+ if (!loader) return null;
47
+ lazyCache.set(
48
+ key,
49
+ lazy(() => {
50
+ // If already resolved (from preloadSectionComponents on server,
51
+ // or from route loader on client SPA), return a sync thenable.
52
+ // React reads thenable.status/value synchronously — no Suspense.
53
+ const resolved = getResolvedComponent(key);
54
+ if (resolved) {
55
+ return syncThenable({ default: resolved });
56
+ }
57
+
58
+ return (loader as () => Promise<{ default: ComponentType<any> }>)().then((mod) => {
59
+ if (!mod?.default) {
60
+ console.error(`[DecoSection] "${key}" has no default export`, Object.keys(mod ?? {}));
61
+ return { default: () => null } as { default: ComponentType<any> };
62
+ }
63
+ setResolvedComponent(key, mod.default);
64
+ return mod as { default: ComponentType<any> };
65
+ });
66
+ }),
67
+ );
68
+ }
69
+ return lazyCache.get(key)!;
70
+ }
71
+
72
+ function DefaultSectionFallback() {
73
+ return <div className="w-full h-48 bg-base-200 animate-pulse rounded" />;
74
+ }
75
+
76
+ function NestedSectionFallback() {
77
+ return <div className="w-full h-24 bg-base-200 animate-pulse rounded" />;
78
+ }
79
+
80
+ import { isDevMode } from "../sdk/env";
81
+
82
+ const isDev = isDevMode();
83
+
84
+ const DEFERRED_FADE_CSS = `@keyframes decoFadeIn{from{opacity:0}to{opacity:1}}`;
85
+
86
+ function FadeInStyle() {
87
+ return <style dangerouslySetInnerHTML={{ __html: DEFERRED_FADE_CSS }} />;
88
+ }
89
+
90
+ function DevMissingFallbackWarning({ component }: { component: string }) {
91
+ if (!isDev) return null;
92
+ return (
93
+ <div
94
+ style={{
95
+ position: "relative",
96
+ border: "2px dashed #e53e3e",
97
+ borderRadius: 8,
98
+ padding: 8,
99
+ margin: "4px 0",
100
+ }}
101
+ >
102
+ <div
103
+ style={{
104
+ fontSize: 11,
105
+ color: "#e53e3e",
106
+ fontFamily: "monospace",
107
+ marginBottom: 4,
108
+ }}
109
+ >
110
+ [AsyncRender] Missing LoadingFallback for &quot;{component}&quot;.
111
+ <br />
112
+ Export a LoadingFallback from your section for better UX.
113
+ <br />
114
+ See: https://deco.cx/docs/async-rendering
115
+ </div>
116
+ <DefaultSectionFallback />
117
+ </div>
118
+ );
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Section type — same shape as deco-cx/deco (Fresh): { Component, props }
123
+ // ---------------------------------------------------------------------------
124
+
125
+ interface Section {
126
+ Component: string | ComponentType<any>;
127
+ props: Record<string, unknown>;
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // SectionRenderer — renders a single nested section
132
+ // ---------------------------------------------------------------------------
133
+
134
+ export function SectionRenderer({ section }: { section: Section | null | undefined }) {
135
+ if (!section?.Component) return null;
136
+
137
+ if (typeof section.Component === "function") {
138
+ const Comp = section.Component;
139
+ return <Comp {...(section.props ?? {})} />;
140
+ }
141
+
142
+ const Lazy = getLazyComponent(section.Component);
143
+ if (!Lazy) {
144
+ console.warn(`[SectionRenderer] No component registered for: ${section.Component}`);
145
+ return null;
146
+ }
147
+
148
+ return (
149
+ <Suspense fallback={<NestedSectionFallback />}>
150
+ <Lazy {...(section.props ?? {})} />
151
+ </Suspense>
152
+ );
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // SectionList — renders an array of nested sections
157
+ // ---------------------------------------------------------------------------
158
+
159
+ export function SectionList({ sections }: { sections: Section[] | null | undefined }) {
160
+ if (!sections?.length) return null;
161
+ return (
162
+ <>
163
+ {sections.map((section, i) => {
164
+ const key = typeof section.Component === "string" ? section.Component : `nested-${i}`;
165
+ return <SectionRenderer key={key} section={section} />;
166
+ })}
167
+ </>
168
+ );
169
+ }
170
+
171
+ // ---------------------------------------------------------------------------
172
+ // DeferredSectionWrapper — loads a section when it scrolls into view
173
+ // ---------------------------------------------------------------------------
174
+
175
+ interface DeferredSectionWrapperProps {
176
+ deferred: DeferredSection;
177
+ pagePath: string;
178
+ loadingFallback?: ReactNode;
179
+ errorFallback?: ReactNode;
180
+ loadFn: (data: {
181
+ component: string;
182
+ rawProps: Record<string, unknown>;
183
+ pagePath: string;
184
+ }) => Promise<ResolvedSection | null>;
185
+ }
186
+
187
+ function DeferredSectionWrapper({
188
+ deferred,
189
+ pagePath,
190
+ loadingFallback,
191
+ errorFallback,
192
+ loadFn,
193
+ }: DeferredSectionWrapperProps) {
194
+ const stableKey = `${pagePath}::${deferred.component}::${deferred.index}`;
195
+ const [section, setSection] = useState<ResolvedSection | null>(null);
196
+ const [error, setError] = useState<Error | null>(null);
197
+ const [loadedOptions, setLoadedOptions] = useState<SectionOptions | undefined>(() =>
198
+ getSectionOptions(deferred.component),
199
+ );
200
+ const isSSR = typeof document === "undefined";
201
+ const [optionsReady, setOptionsReady] = useState(() =>
202
+ isSSR ? false : !!getSectionOptions(deferred.component),
203
+ );
204
+ const ref = useRef<HTMLDivElement>(null);
205
+ const triggered = useRef(false);
206
+ const prevKeyRef = useRef(stableKey);
207
+
208
+ if (prevKeyRef.current !== stableKey) {
209
+ prevKeyRef.current = stableKey;
210
+ triggered.current = false;
211
+ if (section) setSection(null);
212
+ if (error) setError(null);
213
+ }
214
+
215
+ useEffect(() => {
216
+ if (optionsReady) return;
217
+ preloadSectionModule(deferred.component).then((opts) => {
218
+ if (opts) setLoadedOptions(opts);
219
+ setOptionsReady(true);
220
+ });
221
+ }, [deferred.component, optionsReady]);
222
+
223
+ const hasCustomFallback = !!loadedOptions?.loadingFallback;
224
+ const skeleton = !optionsReady
225
+ ? null
226
+ : hasCustomFallback
227
+ ? createElement(loadedOptions!.loadingFallback!)
228
+ : (loadingFallback ??
229
+ (isDev ? (
230
+ <DevMissingFallbackWarning component={deferred.component} />
231
+ ) : (
232
+ <DefaultSectionFallback />
233
+ )));
234
+
235
+ useEffect(() => {
236
+ if (triggered.current || section) return;
237
+
238
+ const el = ref.current;
239
+ if (!el) return;
240
+
241
+ if (typeof IntersectionObserver === "undefined") {
242
+ triggered.current = true;
243
+ loadFn({
244
+ component: deferred.component,
245
+ rawProps: deferred.rawProps,
246
+ pagePath,
247
+ })
248
+ .then(setSection)
249
+ .catch((e) => setError(e));
250
+ return;
251
+ }
252
+
253
+ const observer = new IntersectionObserver(
254
+ ([entry]) => {
255
+ if (entry?.isIntersecting && !triggered.current) {
256
+ triggered.current = true;
257
+ observer.disconnect();
258
+ loadFn({
259
+ component: deferred.component,
260
+ rawProps: deferred.rawProps,
261
+ pagePath,
262
+ })
263
+ .then(setSection)
264
+ .catch((e) => setError(e));
265
+ }
266
+ },
267
+ { rootMargin: "300px" },
268
+ );
269
+
270
+ observer.observe(el);
271
+ return () => observer.disconnect();
272
+ }, [deferred.component, deferred.rawProps, pagePath, section, loadFn]);
273
+
274
+ if (error) {
275
+ const errFallback = loadedOptions?.errorFallback
276
+ ? createElement(loadedOptions.errorFallback, { error })
277
+ : errorFallback;
278
+ return <>{errFallback ?? null}</>;
279
+ }
280
+
281
+ if (section) {
282
+ const sectionId = section.key
283
+ .replace(/\//g, "-")
284
+ .replace(/\.tsx$/, "")
285
+ .replace(/^site-sections-/, "");
286
+
287
+ const LazyComponent = getLazyComponent(section.component);
288
+ if (!LazyComponent) return null;
289
+
290
+ return (
291
+ <section
292
+ id={sectionId}
293
+ data-manifest-key={section.key}
294
+ style={{ animation: "decoFadeIn 0.3s ease-out" }}
295
+ >
296
+ <SectionErrorBoundary sectionKey={section.key} fallback={errorFallback}>
297
+ <Suspense fallback={skeleton}>
298
+ <LazyComponent {...section.props} />
299
+ </Suspense>
300
+ </SectionErrorBoundary>
301
+ </section>
302
+ );
303
+ }
304
+
305
+ const sectionId = deferred.key
306
+ .replace(/\//g, "-")
307
+ .replace(/\.tsx$/, "")
308
+ .replace(/^site-sections-/, "");
309
+
310
+ return (
311
+ <section ref={ref} id={sectionId} data-manifest-key={deferred.key} data-deferred="true">
312
+ {skeleton}
313
+ </section>
314
+ );
315
+ }
316
+
317
+ // ---------------------------------------------------------------------------
318
+ // Merge helper — combines eager and deferred sections in original order
319
+ // ---------------------------------------------------------------------------
320
+
321
+ type PageItem =
322
+ | { type: "eager"; section: ResolvedSection; originalIndex: number }
323
+ | { type: "deferred"; deferred: DeferredSection };
324
+
325
+ function mergeSections(resolved: ResolvedSection[], deferred: DeferredSection[]): PageItem[] {
326
+ if (!resolved?.length && !deferred?.length) return [];
327
+ const safeResolved = resolved ?? [];
328
+ const safeDeferred = deferred ?? [];
329
+
330
+ if (!safeDeferred.length) {
331
+ return safeResolved.map((s, i) => ({ type: "eager", section: s, originalIndex: i }));
332
+ }
333
+
334
+ // Use the `index` property stamped by resolveDecoPage to sort all
335
+ // sections (eager + deferred) back into their original CMS order.
336
+ const items: (PageItem & { _sort: number })[] = [];
337
+
338
+ for (let i = 0; i < safeResolved.length; i++) {
339
+ const s = safeResolved[i];
340
+ items.push({ type: "eager", section: s, originalIndex: i, _sort: s.index ?? i });
341
+ }
342
+
343
+ for (const d of safeDeferred) {
344
+ items.push({ type: "deferred", deferred: d, _sort: d.index } as PageItem & { _sort: number });
345
+ }
346
+
347
+ items.sort((a, b) => a._sort - b._sort);
348
+
349
+ return items;
350
+ }
351
+
352
+ // ---------------------------------------------------------------------------
353
+ // DecoPageRenderer — renders top-level resolved sections from a CMS page
354
+ // ---------------------------------------------------------------------------
355
+
356
+ interface Props {
357
+ sections: ResolvedSection[];
358
+ deferredSections?: DeferredSection[];
359
+ pagePath?: string;
360
+ loadingFallback?: ReactNode;
361
+ errorFallback?: ReactNode;
362
+ loadDeferredSectionFn?: (data: {
363
+ component: string;
364
+ rawProps: Record<string, unknown>;
365
+ pagePath: string;
366
+ }) => Promise<ResolvedSection | null>;
367
+ }
368
+
369
+ export function DecoPageRenderer({
370
+ sections,
371
+ deferredSections,
372
+ pagePath = "/",
373
+ loadingFallback,
374
+ errorFallback,
375
+ loadDeferredSectionFn,
376
+ }: Props) {
377
+ const items = mergeSections(sections ?? [], deferredSections ?? []);
378
+ const hasDeferred = deferredSections && deferredSections.length > 0;
379
+
380
+ return (
381
+ <>
382
+ {hasDeferred && <FadeInStyle />}
383
+ {items.map((item, index) => {
384
+ if (item.type === "deferred") {
385
+ if (!loadDeferredSectionFn) {
386
+ return null;
387
+ }
388
+ return (
389
+ <DeferredSectionWrapper
390
+ key={`deferred-${pagePath}-${item.deferred.key}-${item.deferred.index}`}
391
+ deferred={item.deferred}
392
+ pagePath={pagePath}
393
+ loadingFallback={loadingFallback}
394
+ errorFallback={errorFallback}
395
+ loadFn={loadDeferredSectionFn}
396
+ />
397
+ );
398
+ }
399
+
400
+ const { section } = item;
401
+
402
+ const options = getSectionOptions(section.component);
403
+ const errFallback = options?.errorFallback
404
+ ? createElement(options.errorFallback, { error: new Error("") })
405
+ : errorFallback;
406
+
407
+ const sectionId = section.key
408
+ .replace(/\//g, "-")
409
+ .replace(/\.tsx$/, "")
410
+ .replace(/^site-sections-/, "");
411
+
412
+ // Only use sync path for sections explicitly registered via registerSectionsSync.
413
+ // DO NOT fallback to getResolvedComponent: that is populated server-side by
414
+ // preloadSectionComponents but NOT on the client, causing hydration mismatches
415
+ // (server renders <ul>, client renders <Suspense> for the same component).
416
+ const SyncComp = getSyncComponent(section.component);
417
+ if (SyncComp) {
418
+ return (
419
+ <section key={`${section.key}-${index}`} id={sectionId} data-manifest-key={section.key}>
420
+ <SectionErrorBoundary sectionKey={section.key} fallback={errFallback}>
421
+ <SyncComp {...section.props} />
422
+ </SectionErrorBoundary>
423
+ </section>
424
+ );
425
+ }
426
+
427
+ // Fallback: React.lazy with syncThenable for pre-loaded modules.
428
+ // fallback={null} preserves server HTML during hydration.
429
+ const LazyComponent = getLazyComponent(section.component);
430
+ if (!LazyComponent) return null;
431
+
432
+ return (
433
+ <section key={`${section.key}-${index}`} id={sectionId} data-manifest-key={section.key}>
434
+ <SectionErrorBoundary sectionKey={section.key} fallback={errFallback}>
435
+ <Suspense fallback={null}>
436
+ <LazyComponent {...section.props} />
437
+ </Suspense>
438
+ </SectionErrorBoundary>
439
+ </section>
440
+ );
441
+ })}
442
+ </>
443
+ );
444
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * LazySection -- Intersection Observer-based deferred rendering.
3
+ *
4
+ * Wraps section content and defers rendering until the element scrolls
5
+ * into view. Uses IntersectionObserver for zero-JS-cost detection and
6
+ * renders a lightweight placeholder until the content is needed.
7
+ *
8
+ * For SSR: the placeholder is rendered server-side. On hydration,
9
+ * the observer is set up and content loads when visible.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * <LazySection fallback={<div style={{ height: 400 }} />}>
14
+ * <HeavyProductCarousel products={products} />
15
+ * </LazySection>
16
+ * ```
17
+ */
18
+
19
+ import { type ReactNode, useEffect, useRef, useState } from "react";
20
+
21
+ export interface LazySectionProps {
22
+ children: ReactNode;
23
+ /**
24
+ * Content shown before the section scrolls into view.
25
+ * Should have a fixed height to prevent layout shifts.
26
+ */
27
+ fallback?: ReactNode;
28
+ /**
29
+ * IntersectionObserver rootMargin.
30
+ * Positive values trigger loading before the element is visible.
31
+ * @default "200px"
32
+ */
33
+ rootMargin?: string;
34
+ /**
35
+ * Minimum height for the wrapper div (prevents CLS).
36
+ * Applied as CSS min-height.
37
+ */
38
+ minHeight?: string | number;
39
+ /**
40
+ * CSS class for the wrapper div.
41
+ */
42
+ className?: string;
43
+ /**
44
+ * If true, render the content immediately (bypass lazy loading).
45
+ * Useful for sections above the fold.
46
+ * @default false
47
+ */
48
+ eager?: boolean;
49
+ }
50
+
51
+ export function LazySection({
52
+ children,
53
+ fallback,
54
+ rootMargin = "200px",
55
+ minHeight,
56
+ className,
57
+ eager = false,
58
+ }: LazySectionProps) {
59
+ const [isVisible, setVisible] = useState(eager);
60
+ const ref = useRef<HTMLDivElement>(null);
61
+
62
+ useEffect(() => {
63
+ if (eager || isVisible) return;
64
+
65
+ const element = ref.current;
66
+ if (!element) return;
67
+
68
+ if (typeof IntersectionObserver === "undefined") {
69
+ setVisible(true);
70
+ return;
71
+ }
72
+
73
+ const observer = new IntersectionObserver(
74
+ ([entry]) => {
75
+ if (entry?.isIntersecting) {
76
+ setVisible(true);
77
+ observer.disconnect();
78
+ }
79
+ },
80
+ { rootMargin },
81
+ );
82
+
83
+ observer.observe(element);
84
+ return () => observer.disconnect();
85
+ }, [eager, isVisible, rootMargin]);
86
+
87
+ const style: React.CSSProperties | undefined = minHeight
88
+ ? { minHeight: typeof minHeight === "number" ? `${minHeight}px` : minHeight }
89
+ : undefined;
90
+
91
+ return (
92
+ <div ref={ref} className={className} style={style}>
93
+ {isVisible ? children : (fallback ?? null)}
94
+ </div>
95
+ );
96
+ }
97
+
98
+ /**
99
+ * Determine if a section index is "below the fold" and should be lazy-loaded.
100
+ *
101
+ * Used by DecoPageRenderer to auto-wrap distant sections.
102
+ *
103
+ * @param index - Zero-based section index on the page
104
+ * @param foldThreshold - Sections at or above this index render eagerly
105
+ * @default foldThreshold = 3
106
+ */
107
+ export function isBelowFold(index: number, foldThreshold = 3): boolean {
108
+ return index >= foldThreshold;
109
+ }
@@ -0,0 +1,108 @@
1
+ import React from "react";
2
+
3
+ interface LiveControlsProps {
4
+ site?: string;
5
+ page?: { id?: string; pathTemplate?: string };
6
+ flags?: any[];
7
+ }
8
+
9
+ /**
10
+ * LiveControls bridges the deco admin (parent window) with the storefront (iframe).
11
+ *
12
+ * Mirrors production behavior (apps/website/components/_Controls.tsx):
13
+ * 1. Injects __DECO_STATE for the admin to read
14
+ * 2. Listens for postMessage events from the admin (inject scripts, scroll, rerender)
15
+ * 3. "." opens admin in same tab, Ctrl/Cmd+"." opens in new tab, Ctrl+Shift+E also works
16
+ */
17
+ export function LiveControls({ site, page, flags }: LiveControlsProps) {
18
+ return (
19
+ <>
20
+ <script
21
+ id="__DECO_STATE"
22
+ type="application/json"
23
+ dangerouslySetInnerHTML={{
24
+ __html: JSON.stringify({
25
+ page: page || {},
26
+ site: { name: site || "storefront" },
27
+ flags: flags || [],
28
+ }),
29
+ }}
30
+ />
31
+ <LiveControlsScript />
32
+ </>
33
+ );
34
+ }
35
+
36
+ function LiveControlsScript() {
37
+ const script = `
38
+ (function() {
39
+ if (window.__DECO_LIVE_CONTROLS__) return;
40
+ window.__DECO_LIVE_CONTROLS__ = true;
41
+
42
+ var LIVE = JSON.parse(document.getElementById("__DECO_STATE")?.textContent || "{}");
43
+ window.LIVE = { ...window.LIVE, ...LIVE };
44
+
45
+ window.addEventListener("message", function(event) {
46
+ var data = event.data;
47
+ if (!data || typeof data !== "object") return;
48
+
49
+ switch (data.type) {
50
+ case "editor::inject":
51
+ if (data.args && data.args.script) {
52
+ try { eval(data.args.script); } catch(e) { console.error("[deco] inject error:", e); }
53
+ }
54
+ break;
55
+
56
+ case "scrollToComponent":
57
+ var el = document.querySelector('[data-manifest-key="' + data.args?.id + '"]');
58
+ if (!el) el = document.getElementById(data.args?.id);
59
+ if (el) el.scrollIntoView({ behavior: "smooth", block: "center" });
60
+ break;
61
+
62
+ case "DOMInspector":
63
+ break;
64
+
65
+ case "editor::rerender":
66
+ if (data.args?.url) {
67
+ window.location.href = data.args.url;
68
+ }
69
+ break;
70
+ }
71
+ });
72
+
73
+ if (window.self === window.top) {
74
+ document.body.addEventListener("keydown", function(e) {
75
+ if (e.target !== document.body) return;
76
+ if (e.defaultPrevented) return;
77
+
78
+ if (
79
+ (e.ctrlKey && e.shiftKey && e.key === "E") ||
80
+ e.key === "."
81
+ ) {
82
+ e.preventDefault();
83
+ e.stopPropagation();
84
+
85
+ var siteName = (window.LIVE.site && window.LIVE.site.name) || window.LIVE.site || "storefront";
86
+ var pageId = (window.LIVE.page && window.LIVE.page.id) || "";
87
+ var pathTemplate = (window.LIVE.page && window.LIVE.page.pathTemplate) || "/*";
88
+
89
+ var href = new URL("/choose-editor", "https://admin.deco.cx");
90
+ href.searchParams.set("site", siteName);
91
+ href.searchParams.set("domain", window.location.origin);
92
+ if (pageId) href.searchParams.set("pageId", pageId);
93
+ href.searchParams.set("path", encodeURIComponent(window.location.pathname + window.location.search));
94
+ href.searchParams.set("pathTemplate", encodeURIComponent(pathTemplate));
95
+
96
+ if ((e.ctrlKey || e.metaKey) && e.key === ".") {
97
+ window.open(href.toString(), "_blank");
98
+ return;
99
+ }
100
+ window.location.href = href.toString();
101
+ }
102
+ });
103
+ }
104
+ })();
105
+ `;
106
+
107
+ return <script type="module" dangerouslySetInnerHTML={{ __html: script }} />;
108
+ }