@decocms/start 1.6.1 → 1.6.3
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-to-tanstack-migration/SKILL.md +85 -12
- package/.cursor/skills/deco-to-tanstack-migration/references/gotchas.md +98 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/package-json.md +45 -25
- package/.cursor/skills/deco-to-tanstack-migration/templates/root-route.md +56 -39
- package/.cursor/skills/deco-to-tanstack-migration/templates/setup-ts.md +122 -141
- package/package.json +1 -1
- package/scripts/generate-blocks.ts +8 -5
- package/scripts/migrate/analyzers/island-classifier.ts +23 -0
- package/scripts/migrate/analyzers/section-metadata.ts +63 -7
- package/scripts/migrate/phase-analyze.ts +136 -11
- package/scripts/migrate/phase-cleanup.ts +1057 -6
- package/scripts/migrate/phase-scaffold.ts +294 -5
- package/scripts/migrate/phase-transform.ts +14 -3
- package/scripts/migrate/templates/app-css.ts +149 -2
- package/scripts/migrate/templates/commerce-loaders.ts +173 -68
- package/scripts/migrate/templates/lib-utils.ts +255 -0
- package/scripts/migrate/templates/package-json.ts +30 -22
- package/scripts/migrate/templates/routes.ts +81 -11
- package/scripts/migrate/templates/section-loaders.ts +365 -32
- package/scripts/migrate/templates/server-entry.ts +350 -80
- package/scripts/migrate/templates/setup.ts +78 -8
- package/scripts/migrate/templates/types-gen.ts +58 -0
- package/scripts/migrate/templates/ui-components.ts +47 -16
- package/scripts/migrate/templates/vite-config.ts +17 -6
- package/scripts/migrate/templates/wrangler.ts +3 -1
- package/scripts/migrate/transforms/dead-code.ts +330 -4
- package/scripts/migrate/transforms/deno-isms.ts +19 -0
- package/scripts/migrate/transforms/imports.ts +93 -30
- package/scripts/migrate/transforms/jsx.ts +79 -4
- package/scripts/migrate/transforms/section-conventions.ts +105 -3
- package/scripts/migrate/types.ts +6 -0
- package/src/cms/resolve.ts +4 -0
|
@@ -7,13 +7,16 @@ function capitalize(s: string): string {
|
|
|
7
7
|
export function generateServerEntry(
|
|
8
8
|
ctx: MigrationContext,
|
|
9
9
|
): Record<string, string> {
|
|
10
|
-
|
|
10
|
+
const files: Record<string, string> = {
|
|
11
11
|
"src/server.ts": generateServer(),
|
|
12
12
|
"src/worker-entry.ts": generateWorkerEntry(ctx),
|
|
13
13
|
"src/router.tsx": generateRouter(),
|
|
14
14
|
"src/runtime.ts": generateRuntime(),
|
|
15
15
|
"src/context.ts": generateContext(ctx),
|
|
16
|
+
"src/server/invoke.ts": generateInvoke(ctx),
|
|
17
|
+
"src/server/invoke.gen.ts": generateInvokeGen(ctx),
|
|
16
18
|
};
|
|
19
|
+
return files;
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
function generateServer(): string {
|
|
@@ -70,18 +73,11 @@ export default createDecoWorkerEntry(serverEntry, {
|
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
function generateVtexWorkerEntry(ctx: MigrationContext): string {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
* Handles admin protocol, VTEX checkout proxy, CSP,
|
|
77
|
-
* segment building, and edge caching.
|
|
78
|
-
*
|
|
79
|
-
* MANUAL REVIEW: Add site-specific CSP domains (analytics, CDN, tag managers).
|
|
80
|
-
*/
|
|
81
|
-
import "./setup";
|
|
76
|
+
const vtexAccount = ctx.vtexAccount || ctx.siteName;
|
|
77
|
+
|
|
78
|
+
return `import "./setup";
|
|
82
79
|
import handler, { createServerEntry } from "@tanstack/react-start/server-entry";
|
|
83
80
|
import { createDecoWorkerEntry } from "@decocms/start/sdk/workerEntry";
|
|
84
|
-
import { detectDevice } from "@decocms/start/sdk/useDevice";
|
|
85
81
|
import {
|
|
86
82
|
handleMeta,
|
|
87
83
|
handleDecofileRead,
|
|
@@ -89,35 +85,57 @@ import {
|
|
|
89
85
|
handleRender,
|
|
90
86
|
corsHeaders,
|
|
91
87
|
} from "@decocms/start/admin";
|
|
88
|
+
import { shouldProxyToVtex, createVtexCheckoutProxy } from "@decocms/apps/vtex/utils/proxy";
|
|
92
89
|
import { extractVtexContext } from "@decocms/apps/vtex/middleware";
|
|
93
|
-
import {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
90
|
+
import { loadRedirects, matchRedirect } from "@decocms/start/sdk/redirects";
|
|
91
|
+
import { withABTesting } from "@decocms/start/sdk/abTesting";
|
|
92
|
+
import { loadBlocks } from "@decocms/start/cms";
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// VTEX checkout proxy — configured via @decocms/apps factory
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
const proxyCheckout = createVtexCheckoutProxy({
|
|
99
|
+
account: "${vtexAccount}",
|
|
100
|
+
checkoutOrigin: "${vtexAccount}.vtexcommercestable.com.br",
|
|
101
|
+
// TODO: Set secure checkout origin if different (e.g. "secure.yourdomain.com.br")
|
|
102
|
+
});
|
|
100
103
|
|
|
104
|
+
// Site-specific CSP directives — third-party script domains vary per site.
|
|
105
|
+
// MANUAL REVIEW: Add analytics, CDN, and tag manager domains.
|
|
101
106
|
const CSP_DIRECTIVES = [
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
"
|
|
107
|
-
"
|
|
108
|
-
|
|
107
|
+
"default-src 'self'",
|
|
108
|
+
"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",
|
|
109
|
+
"img-src 'self' data: https: blob:",
|
|
110
|
+
"style-src 'self' 'unsafe-inline' https:",
|
|
111
|
+
"font-src 'self' data: https:",
|
|
112
|
+
"connect-src 'self' https: wss:",
|
|
113
|
+
"frame-src 'self' https://www.googletagmanager.com https://*.firebaseapp.com",
|
|
114
|
+
"media-src 'self' https:",
|
|
115
|
+
"object-src 'none'",
|
|
116
|
+
"base-uri 'self'",
|
|
109
117
|
];
|
|
110
118
|
|
|
111
|
-
const {
|
|
119
|
+
const serverEntry = createServerEntry({ fetch: handler.fetch });
|
|
112
120
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// CMS Redirects — loaded once at module level from .deco/blocks/
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
const cmsRedirects = loadRedirects(loadBlocks());
|
|
125
|
+
|
|
126
|
+
const MOBILE_RE = /mobile|android|iphone/i;
|
|
119
127
|
|
|
120
128
|
const decoWorker = createDecoWorkerEntry(serverEntry, {
|
|
129
|
+
csp: CSP_DIRECTIVES,
|
|
130
|
+
buildSegment: (request) => {
|
|
131
|
+
const vtx = extractVtexContext(request);
|
|
132
|
+
return {
|
|
133
|
+
device: MOBILE_RE.test(request.headers.get("user-agent") ?? "") ? "mobile" : "desktop",
|
|
134
|
+
loggedIn: vtx.isLoggedIn,
|
|
135
|
+
salesChannel: vtx.salesChannel,
|
|
136
|
+
regionId: (vtx as any).regionId ?? undefined,
|
|
137
|
+
};
|
|
138
|
+
},
|
|
121
139
|
admin: {
|
|
122
140
|
handleMeta,
|
|
123
141
|
handleDecofileRead,
|
|
@@ -125,60 +143,46 @@ const decoWorker = createDecoWorkerEntry(serverEntry, {
|
|
|
125
143
|
handleRender,
|
|
126
144
|
corsHeaders,
|
|
127
145
|
},
|
|
128
|
-
|
|
129
|
-
csp: CSP_DIRECTIVES,
|
|
130
|
-
|
|
131
|
-
buildSegment: (request) => {
|
|
132
|
-
const vtx = extractVtexContext(request);
|
|
133
|
-
const device = detectDevice(request.headers.get("user-agent") ?? "");
|
|
134
|
-
|
|
135
|
-
return {
|
|
136
|
-
device,
|
|
137
|
-
...(vtx.isLoggedIn ? { loggedIn: true } : {}),
|
|
138
|
-
...(vtx.salesChannel !== "1" ? { salesChannel: vtx.salesChannel } : {}),
|
|
139
|
-
...(vtx.regionId ? { regionId: vtx.regionId } : {}),
|
|
140
|
-
};
|
|
141
|
-
},
|
|
142
|
-
|
|
143
146
|
proxyHandler: async (request, url) => {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
147
|
+
if (url.pathname === "/login" || url.pathname === "/login/" ||
|
|
148
|
+
url.pathname === "/logout" || url.pathname === "/logout/") return null;
|
|
149
|
+
if (!shouldProxyToVtex(url.pathname)) return null;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
return await proxyCheckout(request, url);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.error("[PROXY] Failed to proxy", url.pathname, err);
|
|
155
|
+
return new Response(\`Proxy error for \${url.pathname}: \${err}\`, {
|
|
156
|
+
status: 502,
|
|
157
|
+
headers: { "content-type": "text/plain" },
|
|
158
|
+
});
|
|
152
159
|
}
|
|
160
|
+
},
|
|
161
|
+
});
|
|
153
162
|
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// A/B wrapper — KV-driven traffic split between TanStack and legacy origin
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
export default withABTesting(decoWorker, {
|
|
168
|
+
kvBinding: "SITES_KV",
|
|
169
|
+
preHandler: (request, url) => {
|
|
170
|
+
const redirect = matchRedirect(url.pathname, cmsRedirects);
|
|
171
|
+
if (redirect) {
|
|
172
|
+
const target = url.search ? \`\${redirect.to}\${url.search}\` : redirect.to;
|
|
173
|
+
return new Response(null, {
|
|
174
|
+
status: redirect.status,
|
|
175
|
+
headers: { Location: target },
|
|
176
|
+
});
|
|
177
|
+
}
|
|
154
178
|
return null;
|
|
155
179
|
},
|
|
180
|
+
shouldBypassAB: (_request, url) => {
|
|
181
|
+
if (url.pathname === "/login" || url.pathname === "/login/" ||
|
|
182
|
+
url.pathname === "/logout" || url.pathname === "/logout/") return false;
|
|
183
|
+
return shouldProxyToVtex(url.pathname);
|
|
184
|
+
},
|
|
156
185
|
});
|
|
157
|
-
|
|
158
|
-
export default decoWorker;
|
|
159
|
-
|
|
160
|
-
// ─── A/B Testing + Redirects (uncomment when ready) ─────────────────
|
|
161
|
-
// import { withABTesting } from "@decocms/start/sdk/abTesting";
|
|
162
|
-
// import { loadBlocks } from "@decocms/start/cms";
|
|
163
|
-
// import { loadRedirects, matchRedirect } from "@decocms/start/sdk/redirects";
|
|
164
|
-
//
|
|
165
|
-
// const cmsRedirects = loadRedirects(loadBlocks());
|
|
166
|
-
//
|
|
167
|
-
// export default withABTesting(decoWorker, {
|
|
168
|
-
// kvBinding: "AB_TESTING",
|
|
169
|
-
// preHandler: (request) => {
|
|
170
|
-
// const url = new URL(request.url);
|
|
171
|
-
// const redirect = matchRedirect(url.pathname, cmsRedirects);
|
|
172
|
-
// if (redirect) {
|
|
173
|
-
// return new Response(null, {
|
|
174
|
-
// status: redirect.type === "temporary" ? 307 : 301,
|
|
175
|
-
// headers: { Location: redirect.to },
|
|
176
|
-
// });
|
|
177
|
-
// }
|
|
178
|
-
// return null;
|
|
179
|
-
// },
|
|
180
|
-
// shouldBypassAB: (_request, url) => shouldProxyToVtex(url.pathname),
|
|
181
|
-
// });
|
|
182
186
|
`;
|
|
183
187
|
}
|
|
184
188
|
|
|
@@ -273,3 +277,269 @@ const Account = createContext<AccountContextValue>({
|
|
|
273
277
|
export default Account;
|
|
274
278
|
`;
|
|
275
279
|
}
|
|
280
|
+
|
|
281
|
+
function generateInvoke(ctx: MigrationContext): string {
|
|
282
|
+
if (ctx.platform !== "vtex") {
|
|
283
|
+
return `/**
|
|
284
|
+
* Site invoke — server functions placeholder.
|
|
285
|
+
* TODO: Add platform-specific invoke actions here.
|
|
286
|
+
*/
|
|
287
|
+
export const invoke = {} as const;
|
|
288
|
+
`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const hasVtexAuthLoader = ctx.loaderInventory.some((l) =>
|
|
292
|
+
l.path.includes("vtex-auth-loader")
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
return `/**
|
|
296
|
+
* Site invoke — extends generated VTEX actions with site-specific server functions.
|
|
297
|
+
*
|
|
298
|
+
* Standard VTEX actions (cart, session, masterdata, newsletter, misc) are
|
|
299
|
+
* auto-generated in invoke.gen.ts. Run \`npm run generate:invoke\` to update.
|
|
300
|
+
*/
|
|
301
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
302
|
+
import {
|
|
303
|
+
getRequestHeader,
|
|
304
|
+
getResponseHeaders,
|
|
305
|
+
setResponseHeader,
|
|
306
|
+
} from "@tanstack/react-start/server";
|
|
307
|
+
import { vtexActions } from "./invoke.gen";
|
|
308
|
+
${hasVtexAuthLoader ? `import vtexAuthLoader from "../loaders/vtex-auth-loader";\n` : ""}import {
|
|
309
|
+
extractVtexCookiesFromHeader,
|
|
310
|
+
stripCookieDomain,
|
|
311
|
+
performVtexLogout,
|
|
312
|
+
parseVtexAuthJwt,
|
|
313
|
+
} from "@decocms/apps/vtex/utils/authHelpers";
|
|
314
|
+
|
|
315
|
+
export type { OrderForm } from "./invoke.gen";
|
|
316
|
+
|
|
317
|
+
function mergeSetCookies(newCookies: string[]): void {
|
|
318
|
+
if (newCookies.length === 0) return;
|
|
319
|
+
const existing: string[] =
|
|
320
|
+
typeof getResponseHeaders().getSetCookie === "function"
|
|
321
|
+
? getResponseHeaders().getSetCookie()
|
|
322
|
+
: [];
|
|
323
|
+
setResponseHeader("set-cookie", [...existing, ...newCookies]);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function getVtexCookies(): string {
|
|
327
|
+
return extractVtexCookiesFromHeader(getRequestHeader("cookie") ?? "");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
${hasVtexAuthLoader ? `const _vtexAuth = createServerFn({ method: "POST" })
|
|
331
|
+
.inputValidator((data: { action: string; params: Record<string, any> }) => data)
|
|
332
|
+
.handler(async ({ data }): Promise<any> => {
|
|
333
|
+
const result = await vtexAuthLoader({
|
|
334
|
+
...data,
|
|
335
|
+
_cookies: getVtexCookies(),
|
|
336
|
+
} as any);
|
|
337
|
+
if (result instanceof Response) {
|
|
338
|
+
const setCookies = result.headers.getSetCookie?.() ?? [];
|
|
339
|
+
mergeSetCookies(stripCookieDomain(setCookies));
|
|
340
|
+
return result.json();
|
|
341
|
+
}
|
|
342
|
+
return result;
|
|
343
|
+
});
|
|
344
|
+
` : ""}const _logout = createServerFn({ method: "POST" }).handler(
|
|
345
|
+
async (): Promise<{ success: boolean }> => {
|
|
346
|
+
const { setCookies } = await performVtexLogout(getVtexCookies());
|
|
347
|
+
mergeSetCookies(setCookies);
|
|
348
|
+
return { success: true };
|
|
349
|
+
},
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
const _getUserFromJwt = createServerFn({ method: "POST" }).handler(
|
|
353
|
+
async (): Promise<{ email: string; userId: string } | null> => {
|
|
354
|
+
return parseVtexAuthJwt(getRequestHeader("cookie") ?? "");
|
|
355
|
+
},
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
export const invoke = {
|
|
359
|
+
vtex: {
|
|
360
|
+
actions: vtexActions,
|
|
361
|
+
},
|
|
362
|
+
site: {
|
|
363
|
+
loaders: {
|
|
364
|
+
${hasVtexAuthLoader ? " vtexAuth: _vtexAuth,\n" : ""} getUserFromJwt: _getUserFromJwt,
|
|
365
|
+
},
|
|
366
|
+
actions: {
|
|
367
|
+
logout: _logout,
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
} as const;
|
|
371
|
+
`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function generateInvokeGen(ctx: MigrationContext): string {
|
|
375
|
+
if (ctx.platform !== "vtex") {
|
|
376
|
+
return `// invoke.gen.ts — no platform-specific actions to generate
|
|
377
|
+
export const vtexActions = {} as const;
|
|
378
|
+
`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return `// Auto-generated VTEX invoke actions
|
|
382
|
+
// Each server function is a top-level const so TanStack Start's compiler
|
|
383
|
+
// can transform createServerFn().handler() into RPC stubs on the client.
|
|
384
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
385
|
+
import { getOrCreateCart, addItemsToCart, updateCartItems, addCouponToCart, simulateCart, getSellersByRegion, setShippingPostalCode, updateOrderFormAttachment } from "@decocms/apps/vtex/actions/checkout";
|
|
386
|
+
import { createSession, editSession } from "@decocms/apps/vtex/actions/session";
|
|
387
|
+
import { createDocument, getDocument, patchDocument, searchDocuments, uploadAttachment } from "@decocms/apps/vtex/actions/masterData";
|
|
388
|
+
import { subscribe } from "@decocms/apps/vtex/actions/newsletter";
|
|
389
|
+
import { notifyMe } from "@decocms/apps/vtex/actions/misc";
|
|
390
|
+
import type { OrderForm } from "@decocms/apps/vtex/types";
|
|
391
|
+
import type { SimulationItem, RegionResult } from "@decocms/apps/vtex/actions/checkout";
|
|
392
|
+
import type { SessionData } from "@decocms/apps/vtex/actions/session";
|
|
393
|
+
import type { CreateDocumentResult, UploadAttachmentOpts } from "@decocms/apps/vtex/actions/masterData";
|
|
394
|
+
import type { SubscribeProps } from "@decocms/apps/vtex/actions/newsletter";
|
|
395
|
+
import type { NotifyMeProps } from "@decocms/apps/vtex/actions/misc";
|
|
396
|
+
|
|
397
|
+
function unwrapResult<T>(result: unknown): T {
|
|
398
|
+
if (result && typeof result === "object" && "data" in result) {
|
|
399
|
+
return (result as { data: T }).data;
|
|
400
|
+
}
|
|
401
|
+
return result as T;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const $getOrCreateCart = createServerFn({ method: "POST" })
|
|
405
|
+
.inputValidator((data: { orderFormId?: string }) => data)
|
|
406
|
+
.handler(async ({ data }): Promise<any> => {
|
|
407
|
+
const result = await getOrCreateCart(data);
|
|
408
|
+
return unwrapResult(result);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const $addItemsToCart = createServerFn({ method: "POST" })
|
|
412
|
+
.inputValidator((data: {
|
|
413
|
+
orderFormId: string;
|
|
414
|
+
orderItems: Array<{ id: string; seller: string; quantity: number }>;
|
|
415
|
+
}) => data)
|
|
416
|
+
.handler(async ({ data }): Promise<any> => {
|
|
417
|
+
const result = await addItemsToCart(data);
|
|
418
|
+
return unwrapResult(result);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
const $updateCartItems = createServerFn({ method: "POST" })
|
|
422
|
+
.inputValidator((data: { orderFormId: string; orderItems: Array<{ index: number; quantity: number }> }) => data)
|
|
423
|
+
.handler(async ({ data }): Promise<any> => {
|
|
424
|
+
const result = await updateCartItems(data);
|
|
425
|
+
return unwrapResult(result);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
const $addCouponToCart = createServerFn({ method: "POST" })
|
|
429
|
+
.inputValidator((data: { orderFormId: string; text: string }) => data)
|
|
430
|
+
.handler(async ({ data }): Promise<any> => {
|
|
431
|
+
const result = await addCouponToCart(data);
|
|
432
|
+
return unwrapResult(result);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const $simulateCart = createServerFn({ method: "POST" })
|
|
436
|
+
.inputValidator((data: { items: SimulationItem[]; postalCode: string; country?: string }) => data)
|
|
437
|
+
.handler(async ({ data }): Promise<any> => {
|
|
438
|
+
return simulateCart(data);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const $getSellersByRegion = createServerFn({ method: "POST" })
|
|
442
|
+
.inputValidator((data: { postalCode: string; salesChannel?: string }) => data)
|
|
443
|
+
.handler(async ({ data }): Promise<any> => {
|
|
444
|
+
return getSellersByRegion(data);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const $setShippingPostalCode = createServerFn({ method: "POST" })
|
|
448
|
+
.inputValidator((data: { orderFormId: string; postalCode: string; country?: string }) => data)
|
|
449
|
+
.handler(async ({ data }): Promise<any> => {
|
|
450
|
+
return setShippingPostalCode(data);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
const $updateOrderFormAttachment = createServerFn({ method: "POST" })
|
|
454
|
+
.inputValidator((data: { orderFormId: string; attachment: string; body: Record<string, unknown> }) => data)
|
|
455
|
+
.handler(async ({ data }): Promise<any> => {
|
|
456
|
+
const result = await updateOrderFormAttachment(data);
|
|
457
|
+
return unwrapResult(result);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
const $createSession = createServerFn({ method: "POST" })
|
|
461
|
+
.inputValidator((data: Record<string, any>) => data)
|
|
462
|
+
.handler(async ({ data }): Promise<any> => {
|
|
463
|
+
const result = await createSession(data);
|
|
464
|
+
return unwrapResult(result);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
const $editSession = createServerFn({ method: "POST" })
|
|
468
|
+
.inputValidator((data: { public: Record<string, { value: string }> }) => data)
|
|
469
|
+
.handler(async ({ data }): Promise<any> => {
|
|
470
|
+
const result = await editSession(data);
|
|
471
|
+
return unwrapResult(result);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const $createDocument = createServerFn({ method: "POST" })
|
|
475
|
+
.inputValidator((data: { entity: string; data: Record<string, any> }) => data)
|
|
476
|
+
.handler(async ({ data }): Promise<any> => {
|
|
477
|
+
return createDocument(data);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const $getDocument = createServerFn({ method: "POST" })
|
|
481
|
+
.inputValidator((data: { entity: string; documentId: string }) => data)
|
|
482
|
+
.handler(async ({ data }): Promise<any> => {
|
|
483
|
+
return getDocument(data);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const $patchDocument = createServerFn({ method: "POST" })
|
|
487
|
+
.inputValidator((data: { entity: string; documentId: string; data: Record<string, any> }) => data)
|
|
488
|
+
.handler(async ({ data }): Promise<any> => {
|
|
489
|
+
return patchDocument(data);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
const $searchDocuments = createServerFn({ method: "POST" })
|
|
493
|
+
.inputValidator((data: { entity: string; filter: string }) => data)
|
|
494
|
+
.handler(async ({ data }): Promise<any> => {
|
|
495
|
+
return searchDocuments(data);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const $uploadAttachment = createServerFn({ method: "POST" })
|
|
499
|
+
.inputValidator((data: UploadAttachmentOpts) => data)
|
|
500
|
+
.handler(async ({ data }): Promise<any> => {
|
|
501
|
+
return uploadAttachment(data);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const $subscribe = createServerFn({ method: "POST" })
|
|
505
|
+
.inputValidator((data: SubscribeProps) => data)
|
|
506
|
+
.handler(async ({ data }): Promise<any> => {
|
|
507
|
+
return subscribe(data);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
const $notifyMe = createServerFn({ method: "POST" })
|
|
511
|
+
.inputValidator((data: NotifyMeProps) => data)
|
|
512
|
+
.handler(async ({ data }): Promise<any> => {
|
|
513
|
+
return notifyMe(data);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
export const vtexActions = {
|
|
517
|
+
getOrCreateCart: $getOrCreateCart as unknown as (ctx: { data: { orderFormId?: string } }) => Promise<OrderForm>,
|
|
518
|
+
addItemsToCart: $addItemsToCart as unknown as (ctx: { data: { orderFormId: string; orderItems: Array<{ id: string; seller: string; quantity: number }> } }) => Promise<OrderForm>,
|
|
519
|
+
updateCartItems: $updateCartItems as unknown as (ctx: { data: { orderFormId: string; orderItems: Array<{ index: number; quantity: number }> } }) => Promise<OrderForm>,
|
|
520
|
+
addCouponToCart: $addCouponToCart as unknown as (ctx: { data: { orderFormId: string; text: string } }) => Promise<OrderForm>,
|
|
521
|
+
simulateCart: $simulateCart,
|
|
522
|
+
getSellersByRegion: $getSellersByRegion as unknown as (ctx: { data: { postalCode: string; salesChannel?: string } }) => Promise<RegionResult | null>,
|
|
523
|
+
setShippingPostalCode: $setShippingPostalCode as unknown as (ctx: { data: { orderFormId: string; postalCode: string; country?: string } }) => Promise<boolean>,
|
|
524
|
+
updateOrderFormAttachment: $updateOrderFormAttachment as unknown as (ctx: { data: { orderFormId: string; attachment: string; body: Record<string, unknown> } }) => Promise<OrderForm>,
|
|
525
|
+
createSession: $createSession,
|
|
526
|
+
editSession: $editSession as unknown as (ctx: { data: { public: Record<string, { value: string }> } }) => Promise<SessionData>,
|
|
527
|
+
createDocument: $createDocument as unknown as (ctx: { data: { entity: string; data: Record<string, any> } }) => Promise<CreateDocumentResult>,
|
|
528
|
+
getDocument: $getDocument,
|
|
529
|
+
patchDocument: $patchDocument as unknown as (ctx: { data: { entity: string; documentId: string; data: Record<string, any> } }) => Promise<void>,
|
|
530
|
+
searchDocuments: $searchDocuments,
|
|
531
|
+
uploadAttachment: $uploadAttachment as unknown as (ctx: { data: UploadAttachmentOpts }) => Promise<{ ok: true }>,
|
|
532
|
+
subscribe: $subscribe as unknown as (ctx: { data: SubscribeProps }) => Promise<void>,
|
|
533
|
+
notifyMe: $notifyMe as unknown as (ctx: { data: NotifyMeProps }) => Promise<void>,
|
|
534
|
+
} as const;
|
|
535
|
+
|
|
536
|
+
export type { OrderForm } from "@decocms/apps/vtex/types";
|
|
537
|
+
|
|
538
|
+
export const invoke = {
|
|
539
|
+
vtex: {
|
|
540
|
+
actions: vtexActions,
|
|
541
|
+
},
|
|
542
|
+
} as const;
|
|
543
|
+
`;
|
|
544
|
+
}
|
|
545
|
+
|
|
@@ -1,19 +1,87 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
1
3
|
import type { MigrationContext } from "../types.ts";
|
|
2
4
|
|
|
5
|
+
function discoverFonts(ctx: MigrationContext): string[] {
|
|
6
|
+
// Check public/fonts (post-move)
|
|
7
|
+
const fontsDir = path.join(ctx.sourceDir, "public", "fonts");
|
|
8
|
+
if (fs.existsSync(fontsDir)) return scanFontDir(fontsDir);
|
|
9
|
+
|
|
10
|
+
// Check static/fonts (pre-move)
|
|
11
|
+
const staticFonts = path.join(ctx.sourceDir, "static", "fonts");
|
|
12
|
+
if (fs.existsSync(staticFonts)) return scanFontDir(staticFonts);
|
|
13
|
+
|
|
14
|
+
// Check static-*/fonts/ (multi-brand sites like casaevideo)
|
|
15
|
+
try {
|
|
16
|
+
const entries = fs.readdirSync(ctx.sourceDir, { withFileTypes: true });
|
|
17
|
+
for (const e of entries) {
|
|
18
|
+
if (e.isDirectory() && e.name.startsWith("static-")) {
|
|
19
|
+
const brandFonts = path.join(ctx.sourceDir, e.name, "fonts");
|
|
20
|
+
if (fs.existsSync(brandFonts)) return scanFontDir(brandFonts);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
} catch { /* ignore */ }
|
|
24
|
+
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function scanFontDir(dir: string): string[] {
|
|
29
|
+
try {
|
|
30
|
+
const allFonts = fs.readdirSync(dir)
|
|
31
|
+
.filter((f) => /\.(woff2?|ttf|otf|eot)$/i.test(f));
|
|
32
|
+
|
|
33
|
+
// Only preload the most critical fonts (Regular + Bold of the primary family).
|
|
34
|
+
// All fonts are still available via @font-face in CSS — this just controls
|
|
35
|
+
// which ones get <link rel="preload"> for faster rendering.
|
|
36
|
+
const critical = allFonts.filter((f) =>
|
|
37
|
+
/[-_](Regular|Bold)\.(woff2?|ttf|otf)$/i.test(f) &&
|
|
38
|
+
!/Italic/i.test(f)
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// If we found critical weights, use those; otherwise take first 2
|
|
42
|
+
const toPreload = critical.length > 0
|
|
43
|
+
? critical.slice(0, 4) // max 4 preloads
|
|
44
|
+
: allFonts.slice(0, 2);
|
|
45
|
+
|
|
46
|
+
return toPreload.map((f) => `/fonts/${f}`);
|
|
47
|
+
} catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hasMatchers(ctx: MigrationContext): boolean {
|
|
53
|
+
const matchersDir = path.join(ctx.sourceDir, "matchers");
|
|
54
|
+
if (fs.existsSync(matchersDir)) return true;
|
|
55
|
+
const srcMatchers = path.join(ctx.sourceDir, "src", "matchers");
|
|
56
|
+
return fs.existsSync(srcMatchers);
|
|
57
|
+
}
|
|
58
|
+
|
|
3
59
|
export function generateSetup(ctx: MigrationContext): string {
|
|
4
60
|
const isVtex = ctx.platform === "vtex";
|
|
5
61
|
const siteName = ctx.siteName;
|
|
62
|
+
const fonts = discoverFonts(ctx);
|
|
63
|
+
const hasLocationMatcher = hasMatchers(ctx);
|
|
6
64
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
65
|
+
// Build productionOrigins from known domain patterns
|
|
66
|
+
const origins: string[] = [];
|
|
67
|
+
// Check if source has productionOrigins in existing setup files
|
|
68
|
+
const possibleDomains = [
|
|
69
|
+
`www.${siteName}.com.br`,
|
|
70
|
+
`${siteName}.com.br`,
|
|
10
71
|
];
|
|
72
|
+
for (const domain of possibleDomains) {
|
|
73
|
+
origins.push(`"https://${domain}"`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const fontEntries = fonts.length > 0
|
|
77
|
+
? fonts.map((f) => `"${f}"`).join(", ")
|
|
78
|
+
: "";
|
|
11
79
|
|
|
12
80
|
return `/**
|
|
13
81
|
* Site setup — orchestrator that wires framework, commerce, and sections.
|
|
14
82
|
*
|
|
15
83
|
* Actual logic lives in focused modules:
|
|
16
|
-
* setup/commerce-loaders.ts — COMMERCE_LOADERS map (data fetchers)
|
|
84
|
+
* setup/commerce-loaders.ts — COMMERCE_LOADERS map (VTEX + site data fetchers)
|
|
17
85
|
* setup/section-loaders.ts — registerSectionLoaders (per-section prop enrichment)
|
|
18
86
|
*
|
|
19
87
|
* Section metadata (eager, sync, layout, cache, LoadingFallback) is declared
|
|
@@ -29,7 +97,8 @@ import {
|
|
|
29
97
|
import { createSiteSetup } from "@decocms/start/setup";
|
|
30
98
|
import { setInvokeLoaders } from "@decocms/start/admin";${isVtex ? `
|
|
31
99
|
import { createInstrumentedFetch } from "@decocms/start/sdk/instrumentedFetch";
|
|
32
|
-
import { initVtexFromBlocks, setVtexFetch } from "@decocms/apps/vtex";` : ""}
|
|
100
|
+
import { initVtexFromBlocks, setVtexFetch } from "@decocms/apps/vtex";` : ""}${hasLocationMatcher ? `
|
|
101
|
+
import { registerLocationMatcher } from "./matchers/location";` : ""}
|
|
33
102
|
import { blocks as generatedBlocks } from "./server/cms/blocks.gen";
|
|
34
103
|
import { sectionMeta, syncComponents, loadingFallbacks } from "./server/cms/sections.gen";
|
|
35
104
|
import { PreviewProviders } from "@decocms/start/hooks";
|
|
@@ -45,11 +114,12 @@ createSiteSetup({
|
|
|
45
114
|
blocks: generatedBlocks,
|
|
46
115
|
meta: () => import("./server/admin/meta.gen.json").then((m) => m.default),
|
|
47
116
|
css: appCss,
|
|
48
|
-
fonts: [],
|
|
117
|
+
fonts: [${fontEntries}],
|
|
49
118
|
productionOrigins: [
|
|
50
|
-
${
|
|
119
|
+
${origins.join(",\n ")},
|
|
51
120
|
],
|
|
52
|
-
previewWrapper: PreviewProviders,${
|
|
121
|
+
previewWrapper: PreviewProviders,${hasLocationMatcher ? `
|
|
122
|
+
customMatchers: [registerLocationMatcher],` : ""}${isVtex ? `
|
|
53
123
|
initPlatform: (blocks) => initVtexFromBlocks(blocks),` : ""}
|
|
54
124
|
onResolveError: (error, resolveType, context) => {
|
|
55
125
|
console.error(\`[CMS-DEBUG] \${context} "\${resolveType}" failed:\`, error);
|
|
@@ -57,10 +57,49 @@ export function redirect(_url: string, _status?: number): never {
|
|
|
57
57
|
`;
|
|
58
58
|
|
|
59
59
|
if (ctx.platform === "vtex") {
|
|
60
|
+
const vtexAccount = ctx.vtexAccount || ctx.siteName;
|
|
60
61
|
files["src/types/vtex-app.ts"] = `export interface VtexConfig {
|
|
61
62
|
account: string;
|
|
62
63
|
publicUrl?: string;
|
|
63
64
|
}
|
|
65
|
+
|
|
66
|
+
export interface AppInvoke {
|
|
67
|
+
vtex: {
|
|
68
|
+
loaders: {
|
|
69
|
+
user: (props: Record<string, unknown>) => Promise<any>;
|
|
70
|
+
address: { list: (props: Record<string, unknown>) => Promise<any> };
|
|
71
|
+
payments: { userPayments: (props: Record<string, unknown>) => Promise<any> };
|
|
72
|
+
intelligentSearch: {
|
|
73
|
+
productList: (props: any) => Promise<any>;
|
|
74
|
+
productListingPage: (props: any) => Promise<any>;
|
|
75
|
+
};
|
|
76
|
+
[key: string]: any;
|
|
77
|
+
};
|
|
78
|
+
actions: {
|
|
79
|
+
payments: { delete: (props: { id: string }) => Promise<any> };
|
|
80
|
+
[key: string]: any;
|
|
81
|
+
};
|
|
82
|
+
[key: string]: any;
|
|
83
|
+
};
|
|
84
|
+
site: {
|
|
85
|
+
loaders: {
|
|
86
|
+
Wishlist: Record<string, (props: any) => Promise<any>>;
|
|
87
|
+
[key: string]: any;
|
|
88
|
+
};
|
|
89
|
+
[key: string]: any;
|
|
90
|
+
};
|
|
91
|
+
[key: string]: any;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type AppContext = {
|
|
95
|
+
device: "mobile" | "desktop" | "tablet";
|
|
96
|
+
platform: "vtex";
|
|
97
|
+
account: string;
|
|
98
|
+
invoke: AppInvoke;
|
|
99
|
+
[key: string]: unknown;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type LegacyAppContext = AppContext;
|
|
64
103
|
`;
|
|
65
104
|
|
|
66
105
|
files["src/types/vtex-loaders.ts"] = `import type { Product, ProductListingPage } from "@decocms/apps/commerce/types";
|
|
@@ -82,6 +121,25 @@ export interface SearchProps {
|
|
|
82
121
|
sort?: string;
|
|
83
122
|
filters?: Record<string, string>;
|
|
84
123
|
}
|
|
124
|
+
|
|
125
|
+
export type LabelledFuzzy = "disabled" | "automatic" | "always";
|
|
126
|
+
|
|
127
|
+
export function mapLabelledFuzzyToFuzzy(fuzzy: LabelledFuzzy): string {
|
|
128
|
+
const mapping: Record<LabelledFuzzy, string> = { disabled: "0", automatic: "auto", always: "1" };
|
|
129
|
+
return mapping[fuzzy] ?? "0";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Props type compatible with intelligentSearch/productListingPage loader */
|
|
133
|
+
export interface PLPProps {
|
|
134
|
+
query?: string;
|
|
135
|
+
page?: number;
|
|
136
|
+
count?: number;
|
|
137
|
+
sort?: string;
|
|
138
|
+
selectedFacets?: Array<{ key: string; value: string }>;
|
|
139
|
+
fuzzy?: LabelledFuzzy;
|
|
140
|
+
hideUnavailableItems?: boolean;
|
|
141
|
+
[key: string]: unknown;
|
|
142
|
+
}
|
|
85
143
|
`;
|
|
86
144
|
|
|
87
145
|
files["src/types/vtex-actions.ts"] = `export interface UserMutation {
|