@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.
Files changed (32) hide show
  1. package/.cursor/skills/deco-to-tanstack-migration/SKILL.md +85 -12
  2. package/.cursor/skills/deco-to-tanstack-migration/references/gotchas.md +98 -0
  3. package/.cursor/skills/deco-to-tanstack-migration/templates/package-json.md +45 -25
  4. package/.cursor/skills/deco-to-tanstack-migration/templates/root-route.md +56 -39
  5. package/.cursor/skills/deco-to-tanstack-migration/templates/setup-ts.md +122 -141
  6. package/package.json +1 -1
  7. package/scripts/generate-blocks.ts +8 -5
  8. package/scripts/migrate/analyzers/island-classifier.ts +23 -0
  9. package/scripts/migrate/analyzers/section-metadata.ts +63 -7
  10. package/scripts/migrate/phase-analyze.ts +136 -11
  11. package/scripts/migrate/phase-cleanup.ts +1057 -6
  12. package/scripts/migrate/phase-scaffold.ts +294 -5
  13. package/scripts/migrate/phase-transform.ts +14 -3
  14. package/scripts/migrate/templates/app-css.ts +149 -2
  15. package/scripts/migrate/templates/commerce-loaders.ts +173 -68
  16. package/scripts/migrate/templates/lib-utils.ts +255 -0
  17. package/scripts/migrate/templates/package-json.ts +30 -22
  18. package/scripts/migrate/templates/routes.ts +81 -11
  19. package/scripts/migrate/templates/section-loaders.ts +365 -32
  20. package/scripts/migrate/templates/server-entry.ts +350 -80
  21. package/scripts/migrate/templates/setup.ts +78 -8
  22. package/scripts/migrate/templates/types-gen.ts +58 -0
  23. package/scripts/migrate/templates/ui-components.ts +47 -16
  24. package/scripts/migrate/templates/vite-config.ts +17 -6
  25. package/scripts/migrate/templates/wrangler.ts +3 -1
  26. package/scripts/migrate/transforms/dead-code.ts +330 -4
  27. package/scripts/migrate/transforms/deno-isms.ts +19 -0
  28. package/scripts/migrate/transforms/imports.ts +93 -30
  29. package/scripts/migrate/transforms/jsx.ts +79 -4
  30. package/scripts/migrate/transforms/section-conventions.ts +105 -3
  31. package/scripts/migrate/types.ts +6 -0
  32. 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
- return {
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
- return `/**
74
- * Cloudflare Worker entry point — VTEX storefront.
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
- shouldProxyToVtex,
95
- createVtexCheckoutProxy,
96
- } from "@decocms/apps/vtex/utils/proxy";
97
- import { getVtexConfig } from "@decocms/apps/vtex";
98
-
99
- const serverEntry = createServerEntry({ fetch: handler.fetch });
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
- "script-src 'self' 'unsafe-inline' 'unsafe-eval' *.vtex.com.br *.vteximg.com.br *.vtexassets.com",
103
- "img-src 'self' data: blob: *.vteximg.com.br *.vtexassets.com *.vtexcommercestable.com.br",
104
- "connect-src 'self' *.vtex.com.br *.vtexcommercestable.com.br *.vtexassets.com",
105
- "frame-src 'self' *.vtex.com.br",
106
- "style-src 'self' 'unsafe-inline' fonts.googleapis.com",
107
- "font-src 'self' fonts.gstatic.com data:",
108
- // TODO: Add site-specific domains (analytics, CDN, tag managers)
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 { account } = getVtexConfig();
119
+ const serverEntry = createServerEntry({ fetch: handler.fetch });
112
120
 
113
- const vtexProxy = createVtexCheckoutProxy({
114
- account,
115
- checkoutOrigin: \`\${account}.vtexcommercestable.com.br\`,
116
- // TODO: Set your secure checkout origin if different from default
117
- // checkoutOrigin: "secure.yourdomain.com.br",
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
- const { pathname } = url;
145
-
146
- // CMS-managed routes — don't proxy
147
- if (pathname === "/login" || pathname === "/logout") return null;
148
-
149
- // VTEX checkout and API proxy
150
- if (shouldProxyToVtex(pathname)) {
151
- return vtexProxy(request, url);
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
- const productionOrigins = [
8
- `"https://www.${siteName}.com.br"`,
9
- `"https://${siteName}.com.br"`,
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
- ${productionOrigins.join(",\n ")},
119
+ ${origins.join(",\n ")},
51
120
  ],
52
- previewWrapper: PreviewProviders,${isVtex ? `
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 {