@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,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 "{component}".
|
|
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
|
+
}
|