@decocms/blocks-cli 7.0.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/package.json +44 -0
- package/scripts/analyze-traces.mjs +1117 -0
- package/scripts/audit-observability-config.test.ts +446 -0
- package/scripts/audit-observability-config.ts +511 -0
- package/scripts/deco-migrate-cli.ts +444 -0
- package/scripts/fast-deploy-kv.test.ts +131 -0
- package/scripts/generate-blocks.test.ts +94 -0
- package/scripts/generate-blocks.ts +274 -0
- package/scripts/generate-invoke.test.ts +195 -0
- package/scripts/generate-invoke.ts +469 -0
- package/scripts/generate-loaders.ts +217 -0
- package/scripts/generate-schema.ts +1287 -0
- package/scripts/generate-sections.ts +237 -0
- package/scripts/htmx-analyze.ts +226 -0
- package/scripts/lib/blocks-dedupe.test.ts +179 -0
- package/scripts/lib/blocks-dedupe.ts +142 -0
- package/scripts/lib/cf-kv-rest.ts +78 -0
- package/scripts/lib/jsonc.ts +122 -0
- package/scripts/lib/kv-snapshot.ts +51 -0
- package/scripts/lib/read-decofile.ts +70 -0
- package/scripts/lib/sync-helpers.ts +44 -0
- package/scripts/migrate/analyzers/htmx-analyze.test.ts +372 -0
- package/scripts/migrate/analyzers/htmx-analyze.ts +425 -0
- package/scripts/migrate/analyzers/island-classifier.ts +96 -0
- package/scripts/migrate/analyzers/loader-inventory.ts +63 -0
- package/scripts/migrate/analyzers/section-metadata.ts +147 -0
- package/scripts/migrate/analyzers/theme-extractor.ts +122 -0
- package/scripts/migrate/colors.ts +46 -0
- package/scripts/migrate/config.test.ts +202 -0
- package/scripts/migrate/config.ts +186 -0
- package/scripts/migrate/phase-analyze.test.ts +63 -0
- package/scripts/migrate/phase-analyze.ts +782 -0
- package/scripts/migrate/phase-cleanup-audit.test.ts +137 -0
- package/scripts/migrate/phase-cleanup-audit.ts +105 -0
- package/scripts/migrate/phase-cleanup.test.ts +141 -0
- package/scripts/migrate/phase-cleanup.ts +1588 -0
- package/scripts/migrate/phase-compile.test.ts +193 -0
- package/scripts/migrate/phase-compile.ts +177 -0
- package/scripts/migrate/phase-report.ts +243 -0
- package/scripts/migrate/phase-scaffold.ts +593 -0
- package/scripts/migrate/phase-transform.ts +310 -0
- package/scripts/migrate/phase-verify.test.ts +127 -0
- package/scripts/migrate/phase-verify.ts +572 -0
- package/scripts/migrate/post-cleanup/rules.ts +1708 -0
- package/scripts/migrate/post-cleanup/runner.test.ts +1771 -0
- package/scripts/migrate/post-cleanup/runner.ts +137 -0
- package/scripts/migrate/post-cleanup/shim-classify.test.ts +352 -0
- package/scripts/migrate/post-cleanup/shim-classify.ts +246 -0
- package/scripts/migrate/post-cleanup/types.ts +106 -0
- package/scripts/migrate/source-layout.test.ts +111 -0
- package/scripts/migrate/source-layout.ts +103 -0
- package/scripts/migrate/templates/app-css.ts +366 -0
- package/scripts/migrate/templates/cache-config.ts +26 -0
- package/scripts/migrate/templates/commerce-loaders.ts +230 -0
- package/scripts/migrate/templates/cursor-rules.test.ts +59 -0
- package/scripts/migrate/templates/cursor-rules.ts +70 -0
- package/scripts/migrate/templates/hooks.test.ts +141 -0
- package/scripts/migrate/templates/hooks.ts +152 -0
- package/scripts/migrate/templates/knip-config.ts +27 -0
- package/scripts/migrate/templates/lib-utils.test.ts +139 -0
- package/scripts/migrate/templates/lib-utils.ts +326 -0
- package/scripts/migrate/templates/lockfile-check-yml.test.ts +26 -0
- package/scripts/migrate/templates/lockfile-check-yml.ts +66 -0
- package/scripts/migrate/templates/package-json.ts +177 -0
- package/scripts/migrate/templates/routes.ts +237 -0
- package/scripts/migrate/templates/sdk-gen.ts +59 -0
- package/scripts/migrate/templates/section-loaders.ts +456 -0
- package/scripts/migrate/templates/server-entry.ts +561 -0
- package/scripts/migrate/templates/setup.ts +148 -0
- package/scripts/migrate/templates/tsconfig.ts +21 -0
- package/scripts/migrate/templates/types-gen.ts +174 -0
- package/scripts/migrate/templates/ui-components.ts +144 -0
- package/scripts/migrate/templates/vite-config.ts +101 -0
- package/scripts/migrate/transforms/dead-code.ts +455 -0
- package/scripts/migrate/transforms/deno-isms.ts +85 -0
- package/scripts/migrate/transforms/fresh-apis.ts +223 -0
- package/scripts/migrate/transforms/htmx-on-events.test.ts +305 -0
- package/scripts/migrate/transforms/htmx-on-events.ts +193 -0
- package/scripts/migrate/transforms/imports.ts +385 -0
- package/scripts/migrate/transforms/jsx.ts +317 -0
- package/scripts/migrate/transforms/section-conventions.ts +210 -0
- package/scripts/migrate/transforms/tailwind.ts +739 -0
- package/scripts/migrate/types.ts +244 -0
- package/scripts/migrate-blocks-to-kv.ts +104 -0
- package/scripts/migrate-post-cleanup.ts +191 -0
- package/scripts/migrate-to-cf-observability.test.ts +215 -0
- package/scripts/migrate-to-cf-observability.ts +699 -0
- package/scripts/migrate.ts +282 -0
- package/scripts/smoke-otlp-errorlog.ts +46 -0
- package/scripts/smoke-otlp-meter.ts +48 -0
- package/scripts/sync-blocks-to-kv.ts +153 -0
- package/scripts/tailwind-lint.ts +518 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import type { MigrationContext } from "../types";
|
|
2
|
+
|
|
3
|
+
function capitalize(s: string): string {
|
|
4
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function generateServerEntry(
|
|
8
|
+
ctx: MigrationContext,
|
|
9
|
+
): Record<string, string> {
|
|
10
|
+
const files: Record<string, string> = {
|
|
11
|
+
"src/server.ts": generateServer(),
|
|
12
|
+
"src/worker-entry.ts": generateWorkerEntry(ctx),
|
|
13
|
+
"src/router.tsx": generateRouter(),
|
|
14
|
+
"src/runtime.ts": generateRuntime(),
|
|
15
|
+
"src/context.ts": generateContext(ctx),
|
|
16
|
+
"src/server/invoke.ts": generateInvoke(ctx),
|
|
17
|
+
"src/server/invoke.gen.ts": generateInvokeGen(ctx),
|
|
18
|
+
};
|
|
19
|
+
return files;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function generateServer(): string {
|
|
23
|
+
return `import {
|
|
24
|
+
createStartHandler,
|
|
25
|
+
defaultStreamHandler,
|
|
26
|
+
} from "@tanstack/react-start/server";
|
|
27
|
+
|
|
28
|
+
export default createStartHandler(defaultStreamHandler);
|
|
29
|
+
`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function generateWorkerEntry(ctx: MigrationContext): string {
|
|
33
|
+
const isVtex = ctx.platform === "vtex";
|
|
34
|
+
|
|
35
|
+
if (isVtex) {
|
|
36
|
+
return generateVtexWorkerEntry(ctx);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const isCommerce = ctx.platform !== "custom";
|
|
40
|
+
const platformLabel = isCommerce ? ctx.platform : null;
|
|
41
|
+
|
|
42
|
+
return `/**
|
|
43
|
+
* Cloudflare Worker entry point.
|
|
44
|
+
*
|
|
45
|
+
* Wraps TanStack Start with admin protocol handlers, edge caching, and
|
|
46
|
+
* the @decocms/start observability stack (5.0+, Cloudflare-native):
|
|
47
|
+
* - logs: console.* -> CF Workers Logs (captured by the platform
|
|
48
|
+
* when wrangler.jsonc has \`observability.enabled: true\`).
|
|
49
|
+
* View in the CF dashboard -> Workers & Pages -> <site> ->
|
|
50
|
+
* Observability -> Logs.
|
|
51
|
+
* - traces: @opentelemetry/api global tracer bridge (so
|
|
52
|
+
* withTracing() spans are picked up by Cloudflare's
|
|
53
|
+
* auto-instrumentation) -> CF Workers Tracing. View in the
|
|
54
|
+
* same dashboard, Traces tab.
|
|
55
|
+
* - metrics: AE (DECO_METRICS binding). Query via the AE SQL API or
|
|
56
|
+
* Grafana ClickHouse datasource pointed at AE.
|
|
57
|
+
*
|
|
58
|
+
* No in-Worker OTLP exporter ships with 5.x — the CF dashboard is the
|
|
59
|
+
* destination. A ClickHouse-collector adapter is scaffolded at
|
|
60
|
+
* @decocms/start/sdk/otelAdapters/clickhouseCollector but throws if
|
|
61
|
+
* called; it'll get a real implementation once the OTel collector
|
|
62
|
+
* gateway lands.
|
|
63
|
+
*
|
|
64
|
+
* To wire wrangler.jsonc with the canonical observability block, run:
|
|
65
|
+
* npx -p @decocms/start deco-cf-observability --write
|
|
66
|
+
*/
|
|
67
|
+
import "./setup";
|
|
68
|
+
import handler, { createServerEntry } from "@tanstack/react-start/server-entry";
|
|
69
|
+
import { createDecoWorkerEntry } from "@decocms/start/sdk/workerEntry";
|
|
70
|
+
import { instrumentWorker } from "@decocms/start/sdk/observability";
|
|
71
|
+
import {
|
|
72
|
+
handleMeta,
|
|
73
|
+
handleDecofileRead,
|
|
74
|
+
handleDecofileReload,
|
|
75
|
+
handleRender,
|
|
76
|
+
corsHeaders,
|
|
77
|
+
} from "@decocms/start/admin";
|
|
78
|
+
${isCommerce ? `
|
|
79
|
+
// TODO: Uncomment and wire proxy for ${platformLabel}
|
|
80
|
+
// import { shouldProxyTo${capitalize(platformLabel!)}, proxyTo${capitalize(platformLabel!)} } from "@decocms/apps/${platformLabel}/utils/proxy";
|
|
81
|
+
` : ""}
|
|
82
|
+
const serverEntry = createServerEntry({ fetch: handler.fetch });
|
|
83
|
+
|
|
84
|
+
const decoWorker = createDecoWorkerEntry(serverEntry, {
|
|
85
|
+
admin: {
|
|
86
|
+
handleMeta,
|
|
87
|
+
handleDecofileRead,
|
|
88
|
+
handleDecofileReload,
|
|
89
|
+
handleRender,
|
|
90
|
+
corsHeaders,
|
|
91
|
+
},
|
|
92
|
+
// Region splits the cache so a RJ-cached response isn't served to SP visitors
|
|
93
|
+
// when pages use the website/matchers/location.ts matcher. Without this, the
|
|
94
|
+
// first geo-resolved response leaks across regions.
|
|
95
|
+
buildSegment: (request) => {
|
|
96
|
+
const cf = (request as unknown as { cf?: { regionCode?: string } }).cf;
|
|
97
|
+
const regionCode = request.headers.get("cf-region-code") ?? cf?.regionCode ?? "";
|
|
98
|
+
return regionCode ? { regionId: regionCode } : {};
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export default instrumentWorker(decoWorker, {
|
|
103
|
+
serviceName: "${ctx.siteName}",
|
|
104
|
+
});
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function generateVtexWorkerEntry(ctx: MigrationContext): string {
|
|
109
|
+
const vtexAccount = ctx.vtexAccount || ctx.siteName;
|
|
110
|
+
|
|
111
|
+
return `import "./setup";
|
|
112
|
+
import handler, { createServerEntry } from "@tanstack/react-start/server-entry";
|
|
113
|
+
import { createDecoWorkerEntry } from "@decocms/start/sdk/workerEntry";
|
|
114
|
+
import { instrumentWorker } from "@decocms/start/sdk/observability";
|
|
115
|
+
import {
|
|
116
|
+
handleMeta,
|
|
117
|
+
handleDecofileRead,
|
|
118
|
+
handleDecofileReload,
|
|
119
|
+
handleRender,
|
|
120
|
+
corsHeaders,
|
|
121
|
+
} from "@decocms/start/admin";
|
|
122
|
+
import { shouldProxyToVtex, createVtexCheckoutProxy } from "@decocms/apps/vtex/utils/proxy";
|
|
123
|
+
import { extractVtexContext } from "@decocms/apps/vtex/middleware";
|
|
124
|
+
import { loadRedirects, matchRedirect } from "@decocms/start/sdk/redirects";
|
|
125
|
+
import { withABTesting } from "@decocms/start/sdk/abTesting";
|
|
126
|
+
import { loadBlocks } from "@decocms/start/cms";
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// VTEX checkout proxy — configured via @decocms/apps factory
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
const proxyCheckout = createVtexCheckoutProxy({
|
|
133
|
+
account: "${vtexAccount}",
|
|
134
|
+
checkoutOrigin: "${vtexAccount}.vtexcommercestable.com.br",
|
|
135
|
+
// TODO: Set secure checkout origin if different (e.g. "secure.yourdomain.com.br")
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Site-specific CSP directives — third-party script domains vary per site.
|
|
139
|
+
// MANUAL REVIEW: Add analytics, CDN, and tag manager domains.
|
|
140
|
+
const CSP_DIRECTIVES = [
|
|
141
|
+
"default-src 'self'",
|
|
142
|
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.googletagmanager.com https://connect.facebook.net https://analytics.tiktok.com https://script.hotjar.com https://static.hotjar.com https://scripts.clarity.ms https://www.clarity.ms https://sp.vtex.com https://bat.bing.com https://s.lilstts.com https://storage.googleapis.com",
|
|
143
|
+
"img-src 'self' data: https: blob:",
|
|
144
|
+
"style-src 'self' 'unsafe-inline' https:",
|
|
145
|
+
"font-src 'self' data: https:",
|
|
146
|
+
"connect-src 'self' https: wss:",
|
|
147
|
+
"frame-src 'self' https://www.googletagmanager.com https://*.firebaseapp.com",
|
|
148
|
+
"media-src 'self' https:",
|
|
149
|
+
"object-src 'none'",
|
|
150
|
+
"base-uri 'self'",
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
const serverEntry = createServerEntry({ fetch: handler.fetch });
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// CMS Redirects — loaded once at module level from .deco/blocks/
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
const cmsRedirects = loadRedirects(loadBlocks());
|
|
159
|
+
|
|
160
|
+
const MOBILE_RE = /mobile|android|iphone/i;
|
|
161
|
+
|
|
162
|
+
const decoWorker = createDecoWorkerEntry(serverEntry, {
|
|
163
|
+
csp: CSP_DIRECTIVES,
|
|
164
|
+
buildSegment: (request) => {
|
|
165
|
+
const vtx = extractVtexContext(request);
|
|
166
|
+
const cf = (request as unknown as { cf?: { regionCode?: string } }).cf;
|
|
167
|
+
const geoRegion = request.headers.get("cf-region-code") ?? cf?.regionCode ?? "";
|
|
168
|
+
return {
|
|
169
|
+
device: MOBILE_RE.test(request.headers.get("user-agent") ?? "") ? "mobile" : "desktop",
|
|
170
|
+
loggedIn: vtx.isLoggedIn,
|
|
171
|
+
salesChannel: vtx.salesChannel,
|
|
172
|
+
// Prefer VTEX regionalization regionId when present; otherwise fall back
|
|
173
|
+
// to Cloudflare geo so the website/matchers/location.ts matcher gets a
|
|
174
|
+
// properly segmented cache.
|
|
175
|
+
regionId: (vtx as any).regionId ?? (geoRegion || undefined),
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
admin: {
|
|
179
|
+
handleMeta,
|
|
180
|
+
handleDecofileRead,
|
|
181
|
+
handleDecofileReload,
|
|
182
|
+
handleRender,
|
|
183
|
+
corsHeaders,
|
|
184
|
+
},
|
|
185
|
+
proxyHandler: async (request, url) => {
|
|
186
|
+
if (url.pathname === "/login" || url.pathname === "/login/" ||
|
|
187
|
+
url.pathname === "/logout" || url.pathname === "/logout/") return null;
|
|
188
|
+
if (!shouldProxyToVtex(url.pathname)) return null;
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
return await proxyCheckout(request, url);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
console.error("[PROXY] Failed to proxy", url.pathname, err);
|
|
194
|
+
return new Response(\`Proxy error for \${url.pathname}: \${err}\`, {
|
|
195
|
+
status: 502,
|
|
196
|
+
headers: { "content-type": "text/plain" },
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// A/B wrapper — KV-driven traffic split between TanStack and legacy origin
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
const abTestedWorker = withABTesting(decoWorker, {
|
|
207
|
+
kvBinding: "SITES_KV",
|
|
208
|
+
preHandler: (request, url) => {
|
|
209
|
+
const redirect = matchRedirect(url.pathname, cmsRedirects);
|
|
210
|
+
if (redirect) {
|
|
211
|
+
const target = url.search ? \`\${redirect.to}\${url.search}\` : redirect.to;
|
|
212
|
+
return new Response(null, {
|
|
213
|
+
status: redirect.status,
|
|
214
|
+
headers: { Location: target },
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return null;
|
|
218
|
+
},
|
|
219
|
+
shouldBypassAB: (_request, url) => {
|
|
220
|
+
if (url.pathname === "/login" || url.pathname === "/login/" ||
|
|
221
|
+
url.pathname === "/logout" || url.pathname === "/logout/") return false;
|
|
222
|
+
return shouldProxyToVtex(url.pathname);
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// Observability wrap (outermost layer)
|
|
228
|
+
//
|
|
229
|
+
// @decocms/start 5.0+ converged on Cloudflare-native observability:
|
|
230
|
+
// - logs: console.* -> CF Workers Logs (dashboard captures, no
|
|
231
|
+
// app-side OTLP exporter)
|
|
232
|
+
// - traces: @opentelemetry/api global tracer (bridged from
|
|
233
|
+
// withTracing) -> CF Workers Tracing (auto-instrumented)
|
|
234
|
+
// - metrics: AE (DECO_METRICS binding only; OTLP metrics path
|
|
235
|
+
// removed in 5.0 — track ClickHouse roadmap at
|
|
236
|
+
// src/sdk/otelAdapters/clickhouseCollector.ts)
|
|
237
|
+
//
|
|
238
|
+
// Wire wrangler.jsonc with the canonical observability block via:
|
|
239
|
+
// npx -p @decocms/start deco-cf-observability --write
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
export default instrumentWorker(abTestedWorker, {
|
|
242
|
+
serviceName: "${ctx.siteName}",
|
|
243
|
+
});
|
|
244
|
+
`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function generateRouter(): string {
|
|
248
|
+
return `import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
249
|
+
import { createDecoRouter } from "@decocms/start/sdk/router";
|
|
250
|
+
import { routeTree } from "./routeTree.gen";
|
|
251
|
+
import "./setup";
|
|
252
|
+
|
|
253
|
+
const queryClient = new QueryClient({
|
|
254
|
+
defaultOptions: { queries: { staleTime: 30_000 } },
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
export function getRouter() {
|
|
258
|
+
return createDecoRouter({
|
|
259
|
+
routeTree,
|
|
260
|
+
context: { queryClient },
|
|
261
|
+
Wrap: ({ children }) => (
|
|
262
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
263
|
+
),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
declare module "@tanstack/react-router" {
|
|
268
|
+
interface Register {
|
|
269
|
+
router: ReturnType<typeof getRouter>;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function generateRuntime(): string {
|
|
276
|
+
return `/**
|
|
277
|
+
* Runtime invoke proxy — re-exports the framework canonical from @decocms/start/sdk.
|
|
278
|
+
*
|
|
279
|
+
* The implementation (typed RPC over /deco/invoke, dotted-path proxy, .ts
|
|
280
|
+
* suffix fallback) lives in @decocms/start/sdk/invoke. This file exists so
|
|
281
|
+
* existing site code can keep \`import { invoke } from "~/runtime"\` and
|
|
282
|
+
* \`Runtime.invoke\` shapes without churn.
|
|
283
|
+
*
|
|
284
|
+
* Don't reimplement here — extend @decocms/start/sdk/invoke instead.
|
|
285
|
+
*/
|
|
286
|
+
import { invoke } from "@decocms/start/sdk";
|
|
287
|
+
|
|
288
|
+
export { invoke };
|
|
289
|
+
|
|
290
|
+
export const Runtime = { invoke };
|
|
291
|
+
`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function generateContext(ctx: MigrationContext): string {
|
|
295
|
+
return `import { createContext } from "react";
|
|
296
|
+
|
|
297
|
+
export interface AccountContextValue {
|
|
298
|
+
name: string;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const Account = createContext<AccountContextValue>({
|
|
302
|
+
name: "${ctx.siteName}",
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
export default Account;
|
|
306
|
+
`;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function generateInvoke(ctx: MigrationContext): string {
|
|
310
|
+
if (ctx.platform !== "vtex") {
|
|
311
|
+
return `/**
|
|
312
|
+
* Site invoke — server functions placeholder.
|
|
313
|
+
* TODO: Add platform-specific invoke actions here.
|
|
314
|
+
*/
|
|
315
|
+
export const invoke = {} as const;
|
|
316
|
+
`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const hasVtexAuthLoader = ctx.loaderInventory.some((l) =>
|
|
320
|
+
l.path.includes("vtex-auth-loader")
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
return `/**
|
|
324
|
+
* Site invoke — extends generated VTEX actions with site-specific server functions.
|
|
325
|
+
*
|
|
326
|
+
* Standard VTEX actions (cart, session, masterdata, newsletter, misc) are
|
|
327
|
+
* auto-generated in invoke.gen.ts. Run \`npm run generate:invoke\` to update.
|
|
328
|
+
*/
|
|
329
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
330
|
+
import { getRequestHeader } from "@tanstack/react-start/server";
|
|
331
|
+
import { forwardResponseCookies } from "@decocms/start/sdk/cookiePassthrough";
|
|
332
|
+
import { vtexActions } from "./invoke.gen";
|
|
333
|
+
${hasVtexAuthLoader ? `import vtexAuthLoader from "../loaders/vtex-auth-loader";\n` : ""}import {
|
|
334
|
+
extractVtexCookiesFromHeader,
|
|
335
|
+
stripCookieDomain,
|
|
336
|
+
performVtexLogout,
|
|
337
|
+
parseVtexAuthJwt,
|
|
338
|
+
} from "@decocms/apps/vtex/utils/authHelpers";
|
|
339
|
+
|
|
340
|
+
export type { OrderForm } from "./invoke.gen";
|
|
341
|
+
|
|
342
|
+
function getVtexCookies(): string {
|
|
343
|
+
return extractVtexCookiesFromHeader(getRequestHeader("cookie") ?? "");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
${hasVtexAuthLoader ? `const _vtexAuth = createServerFn({ method: "POST" })
|
|
347
|
+
.inputValidator((data: { action: string; params: Record<string, any> }) => data)
|
|
348
|
+
.handler(async ({ data }): Promise<any> => {
|
|
349
|
+
const result = await vtexAuthLoader({
|
|
350
|
+
...data,
|
|
351
|
+
_cookies: getVtexCookies(),
|
|
352
|
+
} as any);
|
|
353
|
+
if (result instanceof Response) {
|
|
354
|
+
const setCookies = result.headers.getSetCookie?.() ?? [];
|
|
355
|
+
forwardResponseCookies(stripCookieDomain(setCookies));
|
|
356
|
+
return result.json();
|
|
357
|
+
}
|
|
358
|
+
return result;
|
|
359
|
+
});
|
|
360
|
+
` : ""}const _logout = createServerFn({ method: "POST" }).handler(
|
|
361
|
+
async (): Promise<{ success: boolean }> => {
|
|
362
|
+
const { setCookies } = await performVtexLogout(getVtexCookies());
|
|
363
|
+
forwardResponseCookies(setCookies);
|
|
364
|
+
return { success: true };
|
|
365
|
+
},
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const _getUserFromJwt = createServerFn({ method: "POST" }).handler(
|
|
369
|
+
async (): Promise<{ email: string; userId: string } | null> => {
|
|
370
|
+
return parseVtexAuthJwt(getRequestHeader("cookie") ?? "");
|
|
371
|
+
},
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
export const invoke = {
|
|
375
|
+
vtex: {
|
|
376
|
+
actions: vtexActions,
|
|
377
|
+
},
|
|
378
|
+
site: {
|
|
379
|
+
loaders: {
|
|
380
|
+
${hasVtexAuthLoader ? " vtexAuth: _vtexAuth,\n" : ""} getUserFromJwt: _getUserFromJwt,
|
|
381
|
+
},
|
|
382
|
+
actions: {
|
|
383
|
+
logout: _logout,
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
} as const;
|
|
387
|
+
`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function generateInvokeGen(ctx: MigrationContext): string {
|
|
391
|
+
if (ctx.platform !== "vtex") {
|
|
392
|
+
return `// invoke.gen.ts — no platform-specific actions to generate
|
|
393
|
+
export const vtexActions = {} as const;
|
|
394
|
+
`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return `// Auto-generated VTEX invoke actions
|
|
398
|
+
// Each server function is a top-level const so TanStack Start's compiler
|
|
399
|
+
// can transform createServerFn().handler() into RPC stubs on the client.
|
|
400
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
401
|
+
import { getOrCreateCart, addItemsToCart, updateCartItems, addCouponToCart, simulateCart, getSellersByRegion, setShippingPostalCode, updateOrderFormAttachment } from "@decocms/apps/vtex/actions/checkout";
|
|
402
|
+
import { createSession, editSession } from "@decocms/apps/vtex/actions/session";
|
|
403
|
+
import { createDocument, getDocument, patchDocument, searchDocuments, uploadAttachment } from "@decocms/apps/vtex/actions/masterData";
|
|
404
|
+
import { subscribe } from "@decocms/apps/vtex/actions/newsletter";
|
|
405
|
+
import { notifyMe } from "@decocms/apps/vtex/actions/misc";
|
|
406
|
+
import type { OrderForm } from "@decocms/apps/vtex/types";
|
|
407
|
+
import type { SimulationItem, RegionResult } from "@decocms/apps/vtex/actions/checkout";
|
|
408
|
+
import type { SessionData } from "@decocms/apps/vtex/actions/session";
|
|
409
|
+
import type { CreateDocumentResult, UploadAttachmentOpts } from "@decocms/apps/vtex/actions/masterData";
|
|
410
|
+
import type { SubscribeProps } from "@decocms/apps/vtex/actions/newsletter";
|
|
411
|
+
import type { NotifyMeProps } from "@decocms/apps/vtex/actions/misc";
|
|
412
|
+
|
|
413
|
+
function unwrapResult<T>(result: unknown): T {
|
|
414
|
+
if (result && typeof result === "object" && "data" in result) {
|
|
415
|
+
return (result as { data: T }).data;
|
|
416
|
+
}
|
|
417
|
+
return result as T;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const $getOrCreateCart = createServerFn({ method: "POST" })
|
|
421
|
+
.inputValidator((data: { orderFormId?: string }) => data)
|
|
422
|
+
.handler(async ({ data }): Promise<any> => {
|
|
423
|
+
const result = await getOrCreateCart(data);
|
|
424
|
+
return unwrapResult(result);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const $addItemsToCart = createServerFn({ method: "POST" })
|
|
428
|
+
.inputValidator((data: {
|
|
429
|
+
orderFormId: string;
|
|
430
|
+
orderItems: Array<{ id: string; seller: string; quantity: number }>;
|
|
431
|
+
}) => data)
|
|
432
|
+
.handler(async ({ data }): Promise<any> => {
|
|
433
|
+
const result = await addItemsToCart(data);
|
|
434
|
+
return unwrapResult(result);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const $updateCartItems = createServerFn({ method: "POST" })
|
|
438
|
+
.inputValidator((data: { orderFormId: string; orderItems: Array<{ index: number; quantity: number }> }) => data)
|
|
439
|
+
.handler(async ({ data }): Promise<any> => {
|
|
440
|
+
const result = await updateCartItems(data);
|
|
441
|
+
return unwrapResult(result);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
const $addCouponToCart = createServerFn({ method: "POST" })
|
|
445
|
+
.inputValidator((data: { orderFormId: string; text: string }) => data)
|
|
446
|
+
.handler(async ({ data }): Promise<any> => {
|
|
447
|
+
const result = await addCouponToCart(data);
|
|
448
|
+
return unwrapResult(result);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
const $simulateCart = createServerFn({ method: "POST" })
|
|
452
|
+
.inputValidator((data: { items: SimulationItem[]; postalCode: string; country?: string }) => data)
|
|
453
|
+
.handler(async ({ data }): Promise<any> => {
|
|
454
|
+
return simulateCart(data);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const $getSellersByRegion = createServerFn({ method: "POST" })
|
|
458
|
+
.inputValidator((data: { postalCode: string; salesChannel?: string }) => data)
|
|
459
|
+
.handler(async ({ data }): Promise<any> => {
|
|
460
|
+
return getSellersByRegion(data);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
const $setShippingPostalCode = createServerFn({ method: "POST" })
|
|
464
|
+
.inputValidator((data: { orderFormId: string; postalCode: string; country?: string }) => data)
|
|
465
|
+
.handler(async ({ data }): Promise<any> => {
|
|
466
|
+
return setShippingPostalCode(data);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const $updateOrderFormAttachment = createServerFn({ method: "POST" })
|
|
470
|
+
.inputValidator((data: { orderFormId: string; attachment: string; body: Record<string, unknown> }) => data)
|
|
471
|
+
.handler(async ({ data }): Promise<any> => {
|
|
472
|
+
const result = await updateOrderFormAttachment(data);
|
|
473
|
+
return unwrapResult(result);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
const $createSession = createServerFn({ method: "POST" })
|
|
477
|
+
.inputValidator((data: Record<string, any>) => data)
|
|
478
|
+
.handler(async ({ data }): Promise<any> => {
|
|
479
|
+
const result = await createSession(data);
|
|
480
|
+
return unwrapResult(result);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
const $editSession = createServerFn({ method: "POST" })
|
|
484
|
+
.inputValidator((data: { public: Record<string, { value: string }> }) => data)
|
|
485
|
+
.handler(async ({ data }): Promise<any> => {
|
|
486
|
+
const result = await editSession(data);
|
|
487
|
+
return unwrapResult(result);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
const $createDocument = createServerFn({ method: "POST" })
|
|
491
|
+
.inputValidator((data: { entity: string; data: Record<string, any> }) => data)
|
|
492
|
+
.handler(async ({ data }): Promise<any> => {
|
|
493
|
+
return createDocument(data);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const $getDocument = createServerFn({ method: "POST" })
|
|
497
|
+
.inputValidator((data: { entity: string; documentId: string }) => data)
|
|
498
|
+
.handler(async ({ data }): Promise<any> => {
|
|
499
|
+
return getDocument(data);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
const $patchDocument = createServerFn({ method: "POST" })
|
|
503
|
+
.inputValidator((data: { entity: string; documentId: string; data: Record<string, any> }) => data)
|
|
504
|
+
.handler(async ({ data }): Promise<any> => {
|
|
505
|
+
return patchDocument(data);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
const $searchDocuments = createServerFn({ method: "POST" })
|
|
509
|
+
.inputValidator((data: { entity: string; filter: string }) => data)
|
|
510
|
+
.handler(async ({ data }): Promise<any> => {
|
|
511
|
+
return searchDocuments(data);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
const $uploadAttachment = createServerFn({ method: "POST" })
|
|
515
|
+
.inputValidator((data: UploadAttachmentOpts) => data)
|
|
516
|
+
.handler(async ({ data }): Promise<any> => {
|
|
517
|
+
return uploadAttachment(data);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
const $subscribe = createServerFn({ method: "POST" })
|
|
521
|
+
.inputValidator((data: SubscribeProps) => data)
|
|
522
|
+
.handler(async ({ data }): Promise<any> => {
|
|
523
|
+
return subscribe(data);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
const $notifyMe = createServerFn({ method: "POST" })
|
|
527
|
+
.inputValidator((data: NotifyMeProps) => data)
|
|
528
|
+
.handler(async ({ data }): Promise<any> => {
|
|
529
|
+
return notifyMe(data);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
export const vtexActions = {
|
|
533
|
+
getOrCreateCart: $getOrCreateCart as unknown as (ctx: { data: { orderFormId?: string } }) => Promise<OrderForm>,
|
|
534
|
+
addItemsToCart: $addItemsToCart as unknown as (ctx: { data: { orderFormId: string; orderItems: Array<{ id: string; seller: string; quantity: number }> } }) => Promise<OrderForm>,
|
|
535
|
+
updateCartItems: $updateCartItems as unknown as (ctx: { data: { orderFormId: string; orderItems: Array<{ index: number; quantity: number }> } }) => Promise<OrderForm>,
|
|
536
|
+
addCouponToCart: $addCouponToCart as unknown as (ctx: { data: { orderFormId: string; text: string } }) => Promise<OrderForm>,
|
|
537
|
+
simulateCart: $simulateCart,
|
|
538
|
+
getSellersByRegion: $getSellersByRegion as unknown as (ctx: { data: { postalCode: string; salesChannel?: string } }) => Promise<RegionResult | null>,
|
|
539
|
+
setShippingPostalCode: $setShippingPostalCode as unknown as (ctx: { data: { orderFormId: string; postalCode: string; country?: string } }) => Promise<boolean>,
|
|
540
|
+
updateOrderFormAttachment: $updateOrderFormAttachment as unknown as (ctx: { data: { orderFormId: string; attachment: string; body: Record<string, unknown> } }) => Promise<OrderForm>,
|
|
541
|
+
createSession: $createSession,
|
|
542
|
+
editSession: $editSession as unknown as (ctx: { data: { public: Record<string, { value: string }> } }) => Promise<SessionData>,
|
|
543
|
+
createDocument: $createDocument as unknown as (ctx: { data: { entity: string; data: Record<string, any> } }) => Promise<CreateDocumentResult>,
|
|
544
|
+
getDocument: $getDocument,
|
|
545
|
+
patchDocument: $patchDocument as unknown as (ctx: { data: { entity: string; documentId: string; data: Record<string, any> } }) => Promise<void>,
|
|
546
|
+
searchDocuments: $searchDocuments,
|
|
547
|
+
uploadAttachment: $uploadAttachment as unknown as (ctx: { data: UploadAttachmentOpts }) => Promise<{ ok: true }>,
|
|
548
|
+
subscribe: $subscribe as unknown as (ctx: { data: SubscribeProps }) => Promise<void>,
|
|
549
|
+
notifyMe: $notifyMe as unknown as (ctx: { data: NotifyMeProps }) => Promise<void>,
|
|
550
|
+
} as const;
|
|
551
|
+
|
|
552
|
+
export type { OrderForm } from "@decocms/apps/vtex/types";
|
|
553
|
+
|
|
554
|
+
export const invoke = {
|
|
555
|
+
vtex: {
|
|
556
|
+
actions: vtexActions,
|
|
557
|
+
},
|
|
558
|
+
} as const;
|
|
559
|
+
`;
|
|
560
|
+
}
|
|
561
|
+
|