@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,72 @@
1
+ import { getRevision, loadBlocks, setBlocks } from "../cms/loader";
2
+ import { clearLoaderCache } from "../sdk/cachedLoader";
3
+ import { invalidateMetaCache } from "./meta";
4
+
5
+ export function handleDecofileRead(): Response {
6
+ const blocks = loadBlocks();
7
+ const revision = getRevision();
8
+
9
+ return new Response(JSON.stringify({ blocks, revision }), {
10
+ status: 200,
11
+ headers: {
12
+ "Content-Type": "application/json",
13
+ "Cache-Control": "no-cache",
14
+ ...(revision ? { ETag: `"${revision}"` } : {}),
15
+ },
16
+ });
17
+ }
18
+
19
+ export async function handleDecofileReload(
20
+ request: Request,
21
+ env?: Record<string, unknown>,
22
+ ): Promise<Response> {
23
+ const authHeader = request.headers.get("authorization") || "";
24
+ const expectedToken =
25
+ (env?.DECO_RELOAD_TOKEN as string | undefined) ??
26
+ (typeof globalThis.process !== "undefined"
27
+ ? globalThis.process.env?.DECO_RELOAD_TOKEN
28
+ : undefined);
29
+
30
+ if (expectedToken && !authHeader.includes(expectedToken)) {
31
+ return new Response("Unauthorized", { status: 401 });
32
+ }
33
+
34
+ let newBlocks: Record<string, unknown>;
35
+ try {
36
+ newBlocks = await request.json();
37
+ } catch {
38
+ return new Response(JSON.stringify({ error: "Invalid JSON body" }), {
39
+ status: 400,
40
+ headers: { "Content-Type": "application/json" },
41
+ });
42
+ }
43
+
44
+ if (!newBlocks || typeof newBlocks !== "object") {
45
+ return new Response(JSON.stringify({ error: "Body must be a JSON object" }), {
46
+ status: 400,
47
+ headers: { "Content-Type": "application/json" },
48
+ });
49
+ }
50
+
51
+ const previousBlockCount = Object.keys(loadBlocks()).length;
52
+
53
+ setBlocks(newBlocks);
54
+ // Invalidate the meta ETag so the admin re-fetches the schema on next poll.
55
+ invalidateMetaCache();
56
+ // Clear stale loader cache entries after decofile update
57
+ clearLoaderCache();
58
+
59
+ const newBlockCount = Object.keys(newBlocks).length;
60
+ const revision = getRevision();
61
+
62
+ return new Response(
63
+ JSON.stringify({
64
+ ok: true,
65
+ previousBlockCount,
66
+ newBlockCount,
67
+ revision,
68
+ timestamp: Date.now(),
69
+ }),
70
+ { status: 200, headers: { "Content-Type": "application/json" } },
71
+ );
72
+ }
@@ -0,0 +1,24 @@
1
+ export { corsHeaders, isAdminOrLocalhost } from "./cors";
2
+ export { handleDecofileRead, handleDecofileReload } from "./decofile";
3
+ export {
4
+ handleInvoke,
5
+ type InvokeAction,
6
+ type InvokeLoader,
7
+ setInvokeActions,
8
+ setInvokeLoaders,
9
+ } from "./invoke";
10
+ export { LIVE_CONTROLS_SCRIPT } from "./liveControls";
11
+ export { handleMeta, setMetaData } from "./meta";
12
+ export { handleRender, setRenderShell } from "./render";
13
+ export {
14
+ composeMeta,
15
+ getRegisteredLoaders,
16
+ getRegisteredMatchers,
17
+ type LoaderConfig,
18
+ type MatcherConfig,
19
+ type MetaResponse,
20
+ registerLoaderSchema,
21
+ registerLoaderSchemas,
22
+ registerMatcherSchema,
23
+ registerMatcherSchemas,
24
+ } from "./schema";
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Handles /deco/invoke -- executes loaders and actions by key.
3
+ *
4
+ * Supports:
5
+ * - Single invoke by key: POST /deco/invoke/some/loader.ts
6
+ * - Batch invoke: POST /deco/invoke with { key: payload } body
7
+ * - FormData parsing for file uploads and form submissions
8
+ * - `?select=field1,field2` to pick fields from the result
9
+ * - Resolves __resolveType in batch payloads
10
+ */
11
+
12
+ export type InvokeLoader = (props: any, request: Request) => Promise<any>;
13
+ export type InvokeAction = (props: any, request: Request) => Promise<any>;
14
+
15
+ let getRegisteredLoaders: () => Record<string, InvokeLoader> = () => ({});
16
+ let getRegisteredActions: () => Record<string, InvokeAction> = () => ({});
17
+
18
+ export function setInvokeLoaders(getter: () => Record<string, InvokeLoader>) {
19
+ getRegisteredLoaders = getter;
20
+ }
21
+
22
+ export function setInvokeActions(getter: () => Record<string, InvokeAction>) {
23
+ getRegisteredActions = getter;
24
+ }
25
+
26
+ const JSON_HEADERS = { "Content-Type": "application/json" } as const;
27
+
28
+ const isDev =
29
+ typeof globalThis.process !== "undefined" && globalThis.process.env?.NODE_ENV === "development";
30
+
31
+ function selectFields(data: unknown, select?: string[]): unknown {
32
+ if (!select?.length || !data || typeof data !== "object") return data;
33
+ if (Array.isArray(data)) return data.map((item) => selectFields(item, select));
34
+ const result: Record<string, unknown> = {};
35
+ for (const key of select) {
36
+ if (key in (data as Record<string, unknown>)) {
37
+ result[key] = (data as Record<string, unknown>)[key];
38
+ }
39
+ }
40
+ return result;
41
+ }
42
+
43
+ function errorResponse(message: string, status: number, error?: unknown) {
44
+ const body: Record<string, unknown> = { error: message };
45
+ if (isDev && error instanceof Error && error.stack) {
46
+ body.stack = error.stack;
47
+ }
48
+ return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
49
+ }
50
+
51
+ async function parseBody(request: Request): Promise<any> {
52
+ const contentType = request.headers.get("content-type") ?? "";
53
+
54
+ // FormData
55
+ if (
56
+ contentType.includes("multipart/form-data") ||
57
+ contentType.includes("application/x-www-form-urlencoded")
58
+ ) {
59
+ try {
60
+ const formData = await request.formData();
61
+ const obj: Record<string, unknown> = {};
62
+ for (const [key, value] of formData.entries()) {
63
+ if (obj[key] !== undefined) {
64
+ // Multiple values → array
65
+ const existing = Array.isArray(obj[key]) ? (obj[key] as unknown[]) : [obj[key]];
66
+ existing.push(value);
67
+ obj[key] = existing;
68
+ } else {
69
+ obj[key] = value;
70
+ }
71
+ }
72
+ return obj;
73
+ } catch {
74
+ return {};
75
+ }
76
+ }
77
+
78
+ // URL-encoded search params (for GET fallback)
79
+ if (request.method === "GET") {
80
+ const url = new URL(request.url);
81
+ const propsParam = url.searchParams.get("props");
82
+ if (propsParam) {
83
+ try {
84
+ return JSON.parse(decodeURIComponent(propsParam));
85
+ } catch {
86
+ return {};
87
+ }
88
+ }
89
+ return {};
90
+ }
91
+
92
+ // JSON (default for POST)
93
+ try {
94
+ return await request.json();
95
+ } catch {
96
+ return {};
97
+ }
98
+ }
99
+
100
+ function findHandler(
101
+ key: string,
102
+ ): { handler: InvokeLoader | InvokeAction; type: "loader" | "action" } | null {
103
+ const loaders = getRegisteredLoaders();
104
+ if (loaders[key]) return { handler: loaders[key], type: "loader" };
105
+
106
+ const actions = getRegisteredActions();
107
+ if (actions[key]) return { handler: actions[key], type: "action" };
108
+
109
+ return null;
110
+ }
111
+
112
+ export async function handleInvoke(request: Request): Promise<Response> {
113
+ const url = new URL(request.url);
114
+ const pathParts = url.pathname.split("/deco/invoke/");
115
+ const invokeKey = pathParts[1] || "";
116
+ const select = url.searchParams.get("select")?.split(",").filter(Boolean);
117
+
118
+ const body = await parseBody(request);
119
+
120
+ // Single invoke by key
121
+ if (invokeKey) {
122
+ const found = findHandler(invokeKey);
123
+ if (!found) {
124
+ return errorResponse(`Unknown handler: ${invokeKey}`, 404);
125
+ }
126
+
127
+ try {
128
+ const result = await found.handler(body, request);
129
+ const filtered = selectFields(result, select);
130
+ return new Response(JSON.stringify(filtered), { status: 200, headers: JSON_HEADERS });
131
+ } catch (error) {
132
+ return errorResponse((error as Error).message, 500, error);
133
+ }
134
+ }
135
+
136
+ // Batch invoke
137
+ if (request.method === "POST" && body && typeof body === "object" && !Array.isArray(body)) {
138
+ const results: Record<string, unknown> = {};
139
+
140
+ const entries = Object.entries(body as Record<string, unknown>);
141
+ await Promise.all(
142
+ entries.map(async ([key, payload]) => {
143
+ const resolveType = (payload as any)?.__resolveType || key;
144
+ const found = findHandler(resolveType);
145
+
146
+ if (found) {
147
+ try {
148
+ const result = await found.handler(payload, request);
149
+ results[key] = selectFields(result, select);
150
+ } catch (error) {
151
+ results[key] = { error: (error as Error).message };
152
+ }
153
+ } else {
154
+ results[key] = { error: `Unknown handler: ${resolveType}` };
155
+ }
156
+ }),
157
+ );
158
+
159
+ return new Response(JSON.stringify(results), { status: 200, headers: JSON_HEADERS });
160
+ }
161
+
162
+ return errorResponse("No invoke key specified", 400);
163
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Admin Live Controls - Inline script for storefront pages.
3
+ *
4
+ * When the storefront is embedded in the admin's iframe, the admin sends
5
+ * `editor::inject` postMessage events containing a script that handles
6
+ * the preview navigation loop. This listener receives that script and
7
+ * evaluates it in the page context.
8
+ *
9
+ * Include this script in the root layout (e.g., via a <script> tag) so
10
+ * the admin can communicate with the storefront.
11
+ */
12
+ export const LIVE_CONTROLS_SCRIPT = `
13
+ (function() {
14
+ if (window.__DECO_LIVE_CONTROLS__) return;
15
+ window.__DECO_LIVE_CONTROLS__ = true;
16
+
17
+ addEventListener("message", function(event) {
18
+ var data = event.data;
19
+ if (!data || typeof data !== "object") return;
20
+ switch (data.type) {
21
+ case "editor::inject":
22
+ if (data.args && data.args.script) {
23
+ try { eval(data.args.script); } catch(e) { console.error("[deco] inject error:", e); }
24
+ }
25
+ break;
26
+ }
27
+ });
28
+ })();
29
+ `;
@@ -0,0 +1,70 @@
1
+ import { composeMeta, type MetaResponse } from "./schema";
2
+
3
+ let metaData: MetaResponse | null = null;
4
+ let cachedEtag: string | null = null;
5
+
6
+ /**
7
+ * Invalidate the cached ETag so the admin re-fetches meta after a
8
+ * hot-reload or decofile change.
9
+ *
10
+ * Called by decofile.ts after setBlocks() — no server-side loader import
11
+ * needed here, keeping this module safe for client-side bundles.
12
+ */
13
+ export function invalidateMetaCache() {
14
+ cachedEtag = null;
15
+ }
16
+
17
+ /**
18
+ * Set the schema metadata that /deco/meta will return.
19
+ * Runs composeMeta() to inject framework-level schemas (pages, etc.)
20
+ * on top of the site-generated section schemas.
21
+ */
22
+ export function setMetaData(data: MetaResponse) {
23
+ metaData = composeMeta(data);
24
+ cachedEtag = null;
25
+ }
26
+
27
+ /**
28
+ * Content-based hash for ETag.
29
+ * Uses a simple DJB2-style hash over the serialised JSON so any
30
+ * definition change results in a different ETag, forcing admin to
31
+ * re-fetch rather than use stale cached meta.
32
+ */
33
+ function getEtag(): string {
34
+ if (!cachedEtag) {
35
+ const str = JSON.stringify(metaData || {});
36
+ let hash = 5381;
37
+ for (let i = 0; i < str.length; i++) {
38
+ hash = ((hash << 5) + hash + str.charCodeAt(i)) >>> 0;
39
+ }
40
+ cachedEtag = `"meta-${hash.toString(36)}"`;
41
+ }
42
+ return cachedEtag;
43
+ }
44
+
45
+ export function handleMeta(request: Request): Response {
46
+ if (!metaData) {
47
+ return new Response(JSON.stringify({ error: "Schema not initialized" }), {
48
+ status: 503,
49
+ headers: { "Content-Type": "application/json" },
50
+ });
51
+ }
52
+
53
+ const ifNoneMatch = request.headers.get("if-none-match");
54
+ const etag = getEtag();
55
+
56
+ if (ifNoneMatch === etag) {
57
+ return new Response(null, { status: 304, headers: { ETag: etag } });
58
+ }
59
+
60
+ const body = JSON.stringify({ ...metaData, etag });
61
+
62
+ return new Response(body, {
63
+ status: 200,
64
+ headers: {
65
+ "Content-Type": "application/json",
66
+ ETag: etag,
67
+ "Cache-Control": "must-revalidate",
68
+ },
69
+ });
70
+ }
@@ -0,0 +1,205 @@
1
+ import { createElement } from "react";
2
+ import { loadBlocks, withBlocksOverride } from "../cms/loader";
3
+ import { getSection } from "../cms/registry";
4
+ import { resolveValue } from "../cms/resolve";
5
+ import { LIVE_CONTROLS_SCRIPT } from "./liveControls";
6
+ import { getRenderShellConfig } from "./setup";
7
+
8
+ export { setRenderShell } from "./setup";
9
+
10
+ function wrapInHtmlShell(sectionHtml: string): string {
11
+ const { cssHref, fontHrefs, themeName, bodyClass, htmlLang } = getRenderShellConfig();
12
+ const stylesheets = [
13
+ ...fontHrefs.map((href) => `<link rel="stylesheet" href="${href}" />`),
14
+ cssHref ? `<link rel="stylesheet" href="${cssHref}" />` : "",
15
+ ].join("\n ");
16
+
17
+ const themeAttr = themeName ? ` data-theme="${themeName}"` : "";
18
+ const langAttr = htmlLang ? ` lang="${htmlLang}"` : "";
19
+ const bodyAttr = bodyClass ? ` class="${bodyClass}"` : "";
20
+
21
+ return `<!DOCTYPE html>
22
+ <html${langAttr}${themeAttr}>
23
+ <head>
24
+ <meta charset="utf-8" />
25
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
26
+ ${stylesheets}
27
+ <script>${LIVE_CONTROLS_SCRIPT}</script>
28
+ </head>
29
+ <body${bodyAttr}>
30
+ ${sectionHtml}
31
+ </body>
32
+ </html>`;
33
+ }
34
+
35
+ /**
36
+ * Render a single resolved section object to an HTML string.
37
+ * Returns empty string for unknown or SEO-only sections.
38
+ */
39
+ async function renderOneSection(section: Record<string, unknown>): Promise<string> {
40
+ const resolveType = section.__resolveType as string | undefined;
41
+ if (!resolveType) return "";
42
+
43
+ const sectionLoader = getSection(resolveType);
44
+ if (!sectionLoader) {
45
+ return `<div style="padding:8px;color:orange;font-size:12px;border:1px dashed orange;margin:4px 0;">Unsupported: ${resolveType}</div>`;
46
+ }
47
+
48
+ try {
49
+ const { __resolveType: _, ...sectionProps } = section;
50
+ const { renderToString } = await import("react-dom/server");
51
+ const mod = await sectionLoader();
52
+ return renderToString(createElement(mod.default, sectionProps));
53
+ } catch (error) {
54
+ return `<div style="padding:8px;color:red;font-size:12px;">Error rendering ${resolveType}: ${(error as Error).message}</div>`;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Handles /live/previews/* -- renders sections to HTML for the admin preview.
60
+ *
61
+ * Supports:
62
+ * - Page compositor (website/pages/Page.tsx): resolves + renders all child sections
63
+ * - Single section render with full __resolveType resolution
64
+ * - Per-request decofile override via AsyncLocalStorage
65
+ */
66
+ export async function handleRender(request: Request): Promise<Response> {
67
+ const url = new URL(request.url);
68
+ const resolveChain = url.searchParams.get("resolveChain");
69
+ const propsParam = url.searchParams.get("props");
70
+
71
+ const pathPrefix = "/live/previews/";
72
+ const pathComponent = url.pathname.startsWith(pathPrefix)
73
+ ? url.pathname.slice(pathPrefix.length)
74
+ : "";
75
+ let component = resolveChain || pathComponent || "";
76
+ let props: Record<string, unknown> = {};
77
+ let decofileOverride: Record<string, unknown> | null = null;
78
+
79
+ if (request.method === "POST") {
80
+ try {
81
+ const body = await request.json();
82
+ if (body && typeof body === "object") {
83
+ if (body.__decofile && typeof body.__decofile === "object") {
84
+ decofileOverride = body.__decofile;
85
+ }
86
+ if (body.__props && typeof body.__props === "object") {
87
+ props = body.__props;
88
+ if (body.__props.__resolveType) {
89
+ component = body.__props.__resolveType as string;
90
+ }
91
+ } else if (body.props && typeof body.props === "object") {
92
+ props = body.props;
93
+ } else if (body.__resolveType) {
94
+ component = body.__resolveType as string;
95
+ const { __decofile: _, __resolveType: __, ...rest } = body;
96
+ props = rest;
97
+ } else if (!body.__decofile) {
98
+ props = body;
99
+ }
100
+ }
101
+ } catch {
102
+ // fall through to query-param handling
103
+ }
104
+ }
105
+
106
+ if (!decofileOverride) {
107
+ const decofileParam = url.searchParams.get("__decofile");
108
+ if (decofileParam) {
109
+ try {
110
+ decofileOverride = JSON.parse(decodeURIComponent(decofileParam));
111
+ } catch {
112
+ // invalid __decofile param, ignore
113
+ }
114
+ }
115
+ }
116
+
117
+ if (propsParam && Object.keys(props).length === 0) {
118
+ try {
119
+ props = JSON.parse(decodeURIComponent(propsParam));
120
+ } catch {
121
+ // props parsing failed
122
+ }
123
+ }
124
+
125
+ if (props.__resolveType && !component) {
126
+ component = props.__resolveType as string;
127
+ }
128
+
129
+ const renderFn = async () => {
130
+ const blocks = loadBlocks();
131
+
132
+ // Resolve named block at the component level
133
+ if (blocks[component]) {
134
+ const block = blocks[component] as Record<string, unknown>;
135
+ if (block.__resolveType) {
136
+ component = block.__resolveType as string;
137
+ props = { ...block, ...props };
138
+ }
139
+ }
140
+
141
+ // Page compositor: resolve + render all child sections
142
+ if (component === "website/pages/Page.tsx") {
143
+ const rawSections = props.sections;
144
+ const resolvedSections = await resolveValue(rawSections);
145
+ const sectionsList = Array.isArray(resolvedSections)
146
+ ? resolvedSections
147
+ : resolvedSections
148
+ ? [resolvedSections]
149
+ : [];
150
+
151
+ const htmlParts: string[] = [];
152
+ for (const section of sectionsList) {
153
+ if (!section || typeof section !== "object" || Array.isArray(section)) {
154
+ continue;
155
+ }
156
+ const sectionObj = section as Record<string, unknown>;
157
+ if (!sectionObj.__resolveType) continue;
158
+ const html = await renderOneSection(sectionObj);
159
+ if (html) htmlParts.push(html);
160
+ }
161
+
162
+ return new Response(wrapInHtmlShell(htmlParts.join("\n")), {
163
+ status: 200,
164
+ headers: { "Content-Type": "text/html; charset=utf-8" },
165
+ });
166
+ }
167
+
168
+ // Single section render
169
+ const sectionLoader = getSection(component);
170
+ if (!sectionLoader) {
171
+ const unknownHtml = wrapInHtmlShell(
172
+ `<div style="padding:20px;color:red;">Unknown section: ${component}</div>`,
173
+ );
174
+ return new Response(unknownHtml, {
175
+ status: 200,
176
+ headers: { "Content-Type": "text/html" },
177
+ });
178
+ }
179
+
180
+ try {
181
+ const resolvedProps = (await resolveValue(props)) as Record<string, unknown>;
182
+ const { __resolveType: _, ...cleanProps } = resolvedProps;
183
+ const { renderToString } = await import("react-dom/server");
184
+ const mod = await sectionLoader();
185
+ const sectionHtml = renderToString(createElement(mod.default, cleanProps));
186
+ return new Response(wrapInHtmlShell(sectionHtml), {
187
+ status: 200,
188
+ headers: { "Content-Type": "text/html; charset=utf-8" },
189
+ });
190
+ } catch (error) {
191
+ const errorHtml = wrapInHtmlShell(
192
+ `<div style="padding:20px;color:red;">Render error: ${(error as Error).message}</div>`,
193
+ );
194
+ return new Response(errorHtml, {
195
+ status: 200,
196
+ headers: { "Content-Type": "text/html" },
197
+ });
198
+ }
199
+ };
200
+
201
+ if (decofileOverride) {
202
+ return withBlocksOverride(decofileOverride, renderFn);
203
+ }
204
+ return renderFn();
205
+ }