@decocms/start 1.2.6 → 1.2.7

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/package.json +1 -1
  2. package/scripts/deco-migrate-cli.ts +444 -0
  3. package/scripts/migrate/analyzers/island-classifier.ts +73 -0
  4. package/scripts/migrate/analyzers/loader-inventory.ts +63 -0
  5. package/scripts/migrate/analyzers/section-metadata.ts +91 -0
  6. package/scripts/migrate/analyzers/theme-extractor.ts +122 -0
  7. package/scripts/migrate/phase-analyze.ts +147 -17
  8. package/scripts/migrate/phase-cleanup.ts +124 -2
  9. package/scripts/migrate/phase-report.ts +44 -16
  10. package/scripts/migrate/phase-scaffold.ts +38 -132
  11. package/scripts/migrate/phase-transform.ts +28 -3
  12. package/scripts/migrate/phase-verify.ts +127 -5
  13. package/scripts/migrate/templates/app-css.ts +204 -0
  14. package/scripts/migrate/templates/cache-config.ts +26 -0
  15. package/scripts/migrate/templates/commerce-loaders.ts +124 -0
  16. package/scripts/migrate/templates/hooks.ts +358 -0
  17. package/scripts/migrate/templates/package-json.ts +29 -6
  18. package/scripts/migrate/templates/routes.ts +41 -136
  19. package/scripts/migrate/templates/sdk-gen.ts +59 -0
  20. package/scripts/migrate/templates/section-loaders.ts +108 -0
  21. package/scripts/migrate/templates/server-entry.ts +174 -67
  22. package/scripts/migrate/templates/setup.ts +64 -55
  23. package/scripts/migrate/templates/types-gen.ts +119 -0
  24. package/scripts/migrate/templates/ui-components.ts +113 -0
  25. package/scripts/migrate/templates/vite-config.ts +18 -1
  26. package/scripts/migrate/templates/wrangler.ts +4 -1
  27. package/scripts/migrate/transforms/dead-code.ts +23 -2
  28. package/scripts/migrate/transforms/imports.ts +40 -10
  29. package/scripts/migrate/transforms/jsx.ts +9 -0
  30. package/scripts/migrate/transforms/section-conventions.ts +83 -0
  31. package/scripts/migrate/types.ts +74 -0
  32. package/src/routes/cmsRoute.ts +13 -0
@@ -27,40 +27,19 @@ export default createStartHandler(defaultStreamHandler);
27
27
  }
28
28
 
29
29
  function generateWorkerEntry(ctx: MigrationContext): string {
30
+ const isVtex = ctx.platform === "vtex";
31
+
32
+ if (isVtex) {
33
+ return generateVtexWorkerEntry(ctx);
34
+ }
35
+
30
36
  const isCommerce = ctx.platform !== "custom";
31
- const proxyImport = isCommerce
32
- ? `\n// Uncomment to enable checkout/API proxy for ${ctx.platform}:
33
- // import { shouldProxyTo${ctx.platform === "vtex" ? "Vtex" : capitalize(ctx.platform)}, proxyTo${ctx.platform === "vtex" ? "Vtex" : capitalize(ctx.platform)} } from "@decocms/apps/${ctx.platform}/utils/proxy";\n`
34
- : "";
35
-
36
- const proxyOption = isCommerce
37
- ? `
38
- // Uncomment to enable checkout/API proxy for ${ctx.platform}:
39
- // proxyHandler: (request, url) => {
40
- // if (shouldProxyTo${ctx.platform === "vtex" ? "Vtex" : capitalize(ctx.platform)}(url.pathname)) {
41
- // return proxyTo${ctx.platform === "vtex" ? "Vtex" : capitalize(ctx.platform)}(request);
42
- // }
43
- // return null;
44
- // },`
45
- : "";
46
-
47
- const segmentOption = ctx.platform === "vtex"
48
- ? `
49
- // Uncomment for per-sales-channel/region cache segmentation:
50
- // buildSegment: (request) => {
51
- // const ua = request.headers.get("user-agent") ?? "";
52
- // return {
53
- // device: /mobile|android|iphone/i.test(ua) ? "mobile" : "desktop",
54
- // // loggedIn: true bypasses cache automatically
55
- // };
56
- // },`
57
- : "";
37
+ const platformLabel = isCommerce ? ctx.platform : null;
58
38
 
59
39
  return `/**
60
40
  * Cloudflare Worker entry point.
61
41
  *
62
42
  * Wraps TanStack Start with admin protocol handlers and edge caching.
63
- * For commerce sites, uncomment proxyHandler and buildSegment options.
64
43
  */
65
44
  import "./setup";
66
45
  import handler, { createServerEntry } from "@tanstack/react-start/server-entry";
@@ -72,7 +51,10 @@ import {
72
51
  handleRender,
73
52
  corsHeaders,
74
53
  } from "@decocms/start/admin";
75
- ${proxyImport}
54
+ ${isCommerce ? `
55
+ // TODO: Uncomment and wire proxy for ${platformLabel}
56
+ // import { shouldProxyTo${capitalize(platformLabel!)}, proxyTo${capitalize(platformLabel!)} } from "@decocms/apps/${platformLabel}/utils/proxy";
57
+ ` : ""}
76
58
  const serverEntry = createServerEntry({ fetch: handler.fetch });
77
59
 
78
60
  export default createDecoWorkerEntry(serverEntry, {
@@ -82,52 +64,142 @@ export default createDecoWorkerEntry(serverEntry, {
82
64
  handleDecofileReload,
83
65
  handleRender,
84
66
  corsHeaders,
85
- },${proxyOption}${segmentOption}
67
+ },
86
68
  });
87
69
  `;
88
70
  }
89
71
 
90
- function generateRouter(): string {
91
- return `import { createRouter as createTanStackRouter } from "@tanstack/react-router";
92
- import type { SearchSerializer, SearchParser } from "@tanstack/react-router";
93
- import { routeTree } from "./routeTree.gen";
72
+ 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
+ */
94
81
  import "./setup";
82
+ import handler, { createServerEntry } from "@tanstack/react-start/server-entry";
83
+ import { createDecoWorkerEntry } from "@decocms/start/sdk/workerEntry";
84
+ import { detectDevice } from "@decocms/start/sdk/useDevice";
85
+ import {
86
+ handleMeta,
87
+ handleDecofileRead,
88
+ handleDecofileReload,
89
+ handleRender,
90
+ corsHeaders,
91
+ } from "@decocms/start/admin";
92
+ 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";
95
98
 
96
- const parseSearch: SearchParser = (searchStr) => {
97
- const str = searchStr.startsWith("?") ? searchStr.slice(1) : searchStr;
98
- if (!str) return {};
99
- const params = new URLSearchParams(str);
100
- const result: Record<string, string | string[]> = {};
101
- for (const key of new Set(params.keys())) {
102
- const values = params.getAll(key);
103
- result[key] = values.length === 1 ? values[0] : values;
104
- }
105
- return result;
106
- };
99
+ const serverEntry = createServerEntry({ fetch: handler.fetch });
100
+
101
+ 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)
109
+ ];
110
+
111
+ const { account } = getVtexConfig();
112
+
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
+ });
119
+
120
+ const decoWorker = createDecoWorkerEntry(serverEntry, {
121
+ admin: {
122
+ handleMeta,
123
+ handleDecofileRead,
124
+ handleDecofileReload,
125
+ handleRender,
126
+ corsHeaders,
127
+ },
128
+
129
+ csp: CSP_DIRECTIVES,
130
+
131
+ buildSegment: (request) => {
132
+ const vtx = extractVtexContext(request);
133
+ const device = detectDevice(request.headers.get("user-agent") ?? "");
107
134
 
108
- const stringifySearch: SearchSerializer = (search) => {
109
- const params = new URLSearchParams();
110
- for (const [key, value] of Object.entries(search)) {
111
- if (value === undefined || value === null || value === "") continue;
112
- if (Array.isArray(value)) {
113
- for (const v of value) params.append(key, String(v));
114
- } else {
115
- params.append(key, String(value));
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
+ 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);
116
152
  }
117
- }
118
- const str = params.toString();
119
- return str ? \`?\${str}\` : "";
120
- };
153
+
154
+ return null;
155
+ },
156
+ });
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
+ `;
183
+ }
184
+
185
+ function generateRouter(): string {
186
+ return `import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
187
+ import { createDecoRouter } from "@decocms/start/sdk/router";
188
+ import { routeTree } from "./routeTree.gen";
189
+ import "./setup";
190
+
191
+ const queryClient = new QueryClient({
192
+ defaultOptions: { queries: { staleTime: 30_000 } },
193
+ });
121
194
 
122
195
  export function getRouter() {
123
- const router = createTanStackRouter({
196
+ return createDecoRouter({
124
197
  routeTree,
125
- scrollRestoration: true,
126
- defaultPreload: "intent",
127
- parseSearch,
128
- stringifySearch,
198
+ context: { queryClient },
199
+ Wrap: ({ children }) => (
200
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
201
+ ),
129
202
  });
130
- return router;
131
203
  }
132
204
 
133
205
  declare module "@tanstack/react-router" {
@@ -140,15 +212,50 @@ declare module "@tanstack/react-router" {
140
212
 
141
213
  function generateRuntime(): string {
142
214
  return `/**
143
- * Runtime invoke proxy — turns nested property access into typed RPC calls.
215
+ * Runtime invoke proxy.
144
216
  *
217
+ * Turns nested property access into a typed RPC call to /deco/invoke.
218
+ * Converts dot-notation paths to slash-separated keys:
145
219
  * invoke.vtex.loaders.productList(props)
146
220
  * → POST /deco/invoke/vtex/loaders/productList
221
+ *
222
+ * The .ts suffix variant is also tried if the primary key isn't found
223
+ * (registered loaders may have ".ts" extensions in their keys).
147
224
  */
148
- import { createAppInvoke } from "@decocms/start/sdk/invoke";
225
+ function createNestedInvokeProxy(path: string[] = []): any {
226
+ return new Proxy(
227
+ Object.assign(async (props: any) => {
228
+ const key = path.join("/");
229
+ for (const k of [key, \`\${key}.ts\`]) {
230
+ const response = await fetch(\`/deco/invoke/\${k}\`, {
231
+ method: "POST",
232
+ headers: { "Content-Type": "application/json" },
233
+ body: JSON.stringify(props ?? {}),
234
+ });
235
+ if (response.status === 404) continue;
236
+ if (!response.ok) {
237
+ throw new Error(\`invoke(\${k}) failed: \${response.status}\`);
238
+ }
239
+ return response.json();
240
+ }
241
+ throw new Error(\`invoke(\${key}) failed: handler not found\`);
242
+ }, {}),
243
+ {
244
+ get(_target: any, prop: string) {
245
+ if (prop === "then" || prop === "catch" || prop === "finally") {
246
+ return undefined;
247
+ }
248
+ return createNestedInvokeProxy([...path, prop]);
249
+ },
250
+ },
251
+ );
252
+ }
253
+
254
+ export const invoke = createNestedInvokeProxy() as any;
149
255
 
150
- export const invoke = createAppInvoke();
151
- export const Runtime = { invoke };
256
+ export const Runtime = {
257
+ invoke,
258
+ };
152
259
  `;
153
260
  }
154
261
 
@@ -1,69 +1,78 @@
1
1
  import type { MigrationContext } from "../types.ts";
2
2
 
3
3
  export function generateSetup(ctx: MigrationContext): string {
4
- // Detect layout sections (Header, Footer, Theme) from source files
5
- const layoutSections: string[] = [];
6
- for (const f of ctx.files) {
7
- if (f.category !== "section" || f.action === "delete") continue;
8
- const name = f.path.replace(/^sections\//, "").replace(/\.tsx$/, "");
9
- const lower = name.toLowerCase();
10
- if (lower.includes("header") || lower.includes("footer") || lower.includes("theme")) {
11
- layoutSections.push(`site/sections/${name}.tsx`);
12
- }
13
- }
14
- // Also check islands that became sections
15
- for (const f of ctx.files) {
16
- if (f.category !== "island") continue;
17
- const name = f.path.replace(/^islands\//, "").replace(/\.tsx$/, "");
18
- const lower = name.toLowerCase();
19
- if (lower.includes("header") || lower.includes("footer") || lower.includes("theme")) {
20
- layoutSections.push(`site/sections/${name}.tsx`);
21
- }
22
- }
4
+ const isVtex = ctx.platform === "vtex";
5
+ const siteName = ctx.siteName;
23
6
 
24
- const layoutRegistration = layoutSections.length > 0
25
- ? `\n// -- Layout Sections (cached across navigations) --
26
- registerLayoutSections([
27
- ${layoutSections.map((s) => ` "${s}",`).join("\n")}
28
- ]);\n`
29
- : "";
30
-
31
- const layoutImport = layoutSections.length > 0
32
- ? "\n registerLayoutSections," : "";
7
+ const productionOrigins = [
8
+ `"https://www.${siteName}.com.br"`,
9
+ `"https://${siteName}.com.br"`,
10
+ ];
33
11
 
34
12
  return `/**
35
- * Site setup — registers all sections, loaders and matchers with the CMS.
13
+ * Site setup — orchestrator that wires framework, commerce, and sections.
14
+ *
15
+ * Actual logic lives in focused modules:
16
+ * setup/commerce-loaders.ts — COMMERCE_LOADERS map (data fetchers)
17
+ * setup/section-loaders.ts — registerSectionLoaders (per-section prop enrichment)
36
18
  *
37
- * This file is imported by router.tsx and worker-entry.ts at startup.
38
- * It uses import.meta.glob to lazily discover all section components.
19
+ * Section metadata (eager, sync, layout, cache, LoadingFallback) is declared
20
+ * in each section file and auto-extracted by generate-sections.ts.
39
21
  */
40
- import { blocks as generatedBlocks } from "./server/cms/blocks.gen";
22
+
23
+ import "./cache-config";
24
+
41
25
  import {
42
- registerSections,${layoutImport}
43
- setBlocks,
26
+ registerCommerceLoaders,
27
+ applySectionConventions,
44
28
  } from "@decocms/start/cms";
45
- import { registerBuiltinMatchers } from "@decocms/start/matchers/builtins";
46
- import { autoconfigApps } from "@decocms/start/apps/autoconfig";
29
+ import { createSiteSetup } from "@decocms/start/setup";
30
+ import { setInvokeLoaders } from "@decocms/start/admin";${isVtex ? `
31
+ import { createInstrumentedFetch } from "@decocms/start/sdk/instrumentedFetch";
32
+ import { initVtexFromBlocks, setVtexFetch } from "@decocms/apps/vtex";` : ""}
33
+ import { blocks as generatedBlocks } from "./server/cms/blocks.gen";
34
+ import { sectionMeta, syncComponents, loadingFallbacks } from "./server/cms/sections.gen";
35
+ import { PreviewProviders } from "@decocms/start/hooks";
36
+ // @ts-ignore Vite ?url import
37
+ import appCss from "./styles/app.css?url";
47
38
 
48
- // -- CMS Blocks --
49
- // The Vite plugin intercepts the blocks.gen import and injects .deco/blocks/ data.
50
- if (typeof document === "undefined") {
51
- setBlocks(generatedBlocks);
52
- // Auto-configure apps from CMS blocks — registers invoke handlers,
53
- // app state, and middleware (cookie forwarding, etc.)
54
- autoconfigApps(generatedBlocks);
55
- }
39
+ import { COMMERCE_LOADERS } from "./setup/commerce-loaders";
40
+ import "./setup/section-loaders";
56
41
 
57
- // -- Section Registry --
58
- // CMS blocks reference sections as "site/sections/X.tsx", so we remap the glob keys.
59
- const sectionGlob = import.meta.glob("./sections/**/*.tsx") as Record<string, () => Promise<any>>;
60
- const sections: Record<string, () => Promise<any>> = {};
61
- for (const [path, loader] of Object.entries(sectionGlob)) {
62
- sections["site/" + path.slice(2)] = loader;
63
- }
64
- registerSections(sections);
65
- ${layoutRegistration}
66
- // -- Matchers --
67
- registerBuiltinMatchers();
42
+ // -- Framework setup --
43
+ createSiteSetup({
44
+ sections: import.meta.glob("./sections/**/*.tsx") as Record<string, () => Promise<any>>,
45
+ blocks: generatedBlocks,
46
+ meta: () => import("./server/admin/meta.gen.json").then((m) => m.default),
47
+ css: appCss,
48
+ fonts: [],
49
+ productionOrigins: [
50
+ ${productionOrigins.join(",\n ")},
51
+ ],
52
+ previewWrapper: PreviewProviders,${isVtex ? `
53
+ initPlatform: (blocks) => initVtexFromBlocks(blocks),` : ""}
54
+ onResolveError: (error, resolveType, context) => {
55
+ console.error(\`[CMS-DEBUG] \${context} "\${resolveType}" failed:\`, error);
56
+ },
57
+ onDanglingReference: (resolveType) => {
58
+ console.warn(\`[CMS-DEBUG] Dangling reference: \${resolveType}\`);
59
+ return null;
60
+ },
61
+ });
62
+ ${isVtex ? `
63
+ // -- VTEX wiring --
64
+ setVtexFetch(createInstrumentedFetch("vtex"));
65
+ ` : ""}
66
+ // -- Convention-driven section registration --
67
+ applySectionConventions({
68
+ meta: sectionMeta,
69
+ syncComponents,
70
+ loadingFallbacks,
71
+ sectionGlob: import.meta.glob("./sections/**/*.tsx") as Record<string, () => Promise<any>>,
72
+ });
73
+
74
+ // -- Commerce + invoke --
75
+ registerCommerceLoaders(COMMERCE_LOADERS);
76
+ setInvokeLoaders(() => COMMERCE_LOADERS);
68
77
  `;
69
78
  }
@@ -0,0 +1,119 @@
1
+ import type { MigrationContext } from "../types.ts";
2
+
3
+ export function generateTypeFiles(ctx: MigrationContext): Record<string, string> {
4
+ const files: Record<string, string> = {};
5
+
6
+ files["src/types/widgets.ts"] = `export type ImageWidget = string;
7
+ export type HTMLWidget = string;
8
+ export type VideoWidget = string;
9
+ export type TextWidget = string;
10
+ export type RichText = string;
11
+ export type Secret = string;
12
+ export type Color = string;
13
+ export type ButtonWidget = string;
14
+ `;
15
+
16
+ files["src/types/deco.ts"] = `export type SectionProps<T extends (...args: any[]) => any> = Awaited<
17
+ ReturnType<T>
18
+ >;
19
+
20
+ export type Resolved<T = any> = T;
21
+
22
+ export type Section = any;
23
+
24
+ export type Block = any;
25
+
26
+ export type LoadingFallbackProps = {
27
+ height?: number;
28
+ };
29
+
30
+ export function asResolved<T>(value: T): T {
31
+ return value;
32
+ }
33
+
34
+ export function isDeferred(value: unknown): boolean {
35
+ return false;
36
+ }
37
+
38
+ export const context = {
39
+ isDeploy: false,
40
+ platform: "tanstack-start" as const,
41
+ site: "${ctx.siteName}",
42
+ siteId: 0,
43
+ };
44
+
45
+ export function redirect(_url: string, _status?: number): never {
46
+ throw new Error("redirect is not supported in TanStack Start -- use router navigation instead");
47
+ }
48
+ `;
49
+
50
+ files["src/types/commerce-app.ts"] = `export type AppContext = {
51
+ device: "mobile" | "desktop" | "tablet";
52
+ };
53
+ `;
54
+
55
+ // Compat shim for apps/website/loaders/extension.ts
56
+ files["src/types/website.ts"] = `export type ExtensionOf<T = any> = T;
57
+ `;
58
+
59
+ if (ctx.platform === "vtex") {
60
+ files["src/types/vtex-app.ts"] = `export interface VtexConfig {
61
+ account: string;
62
+ publicUrl?: string;
63
+ }
64
+ `;
65
+
66
+ files["src/types/vtex-loaders.ts"] = `import type { Product, ProductListingPage } from "@decocms/apps/commerce/types";
67
+
68
+ export interface ProductListProps {
69
+ page: ProductListingPage | null;
70
+ }
71
+
72
+ export interface ProductDetailsProps {
73
+ page: {
74
+ product: Product;
75
+ seo?: { title?: string; description?: string; canonical?: string };
76
+ } | null;
77
+ }
78
+
79
+ export interface SearchProps {
80
+ query?: string;
81
+ page?: number;
82
+ sort?: string;
83
+ filters?: Record<string, string>;
84
+ }
85
+ `;
86
+
87
+ files["src/types/vtex-actions.ts"] = `export interface UserMutation {
88
+ firstName?: string;
89
+ lastName?: string;
90
+ email?: string;
91
+ document?: string;
92
+ phone?: string;
93
+ gender?: string;
94
+ birthDate?: string;
95
+ corporateName?: string;
96
+ corporateDocument?: string;
97
+ stateRegistration?: string;
98
+ isCorporate?: boolean;
99
+ }
100
+
101
+ export interface AddressMutation {
102
+ addressName?: string;
103
+ addressType?: string;
104
+ postalCode?: string;
105
+ street?: string;
106
+ number?: string;
107
+ complement?: string;
108
+ neighborhood?: string;
109
+ city?: string;
110
+ state?: string;
111
+ country?: string;
112
+ receiverName?: string;
113
+ reference?: string;
114
+ }
115
+ `;
116
+ }
117
+
118
+ return files;
119
+ }
@@ -0,0 +1,113 @@
1
+ import type { MigrationContext } from "../types.ts";
2
+
3
+ export function generateUiComponents(_ctx: MigrationContext): Record<string, string> {
4
+ const files: Record<string, string> = {};
5
+
6
+ files["src/components/ui/Image.tsx"] = `export {
7
+ Image as default,
8
+ Image,
9
+ getOptimizedMediaUrl,
10
+ getSrcSet,
11
+ registerImageCdnDomain,
12
+ getImageCdnDomain,
13
+ FACTORS,
14
+ type ImageProps,
15
+ type FitOptions,
16
+ } from "@decocms/apps/commerce/components/Image";
17
+ `;
18
+
19
+ files["src/components/ui/Picture.tsx"] = `import {
20
+ Image,
21
+ getSrcSet,
22
+ type FitOptions,
23
+ type ImageProps,
24
+ } from "@decocms/apps/commerce/components/Image";
25
+
26
+ export interface PictureSourceProps {
27
+ src: string;
28
+ width: number;
29
+ height?: number;
30
+ media: string;
31
+ fit?: FitOptions;
32
+ sizes?: string;
33
+ }
34
+
35
+ export interface PictureProps extends Omit<ImageProps, "sizes"> {
36
+ sources: PictureSourceProps[];
37
+ }
38
+
39
+ export function Picture({
40
+ sources,
41
+ src,
42
+ width,
43
+ height,
44
+ fit = "cover",
45
+ preload,
46
+ ...rest
47
+ }: PictureProps) {
48
+ return (
49
+ <picture>
50
+ {sources.map((source, i) => {
51
+ const srcSet = getSrcSet(source.src, source.width, source.height, source.fit ?? fit);
52
+ return (
53
+ <source
54
+ key={i}
55
+ srcSet={srcSet}
56
+ media={source.media}
57
+ width={source.width}
58
+ height={source.height}
59
+ sizes={source.sizes ?? \`\${source.width}px\`}
60
+ />
61
+ );
62
+ })}
63
+ <Image src={src} width={width} height={height} fit={fit} preload={preload} {...rest} />
64
+ </picture>
65
+ );
66
+ }
67
+ `;
68
+
69
+ files["src/components/ui/Video.tsx"] = `interface Props {
70
+ src: string;
71
+ width?: number;
72
+ height?: number;
73
+ autoPlay?: boolean;
74
+ muted?: boolean;
75
+ loop?: boolean;
76
+ playsInline?: boolean;
77
+ controls?: boolean;
78
+ className?: string;
79
+ loading?: "lazy" | "eager";
80
+ poster?: string;
81
+ }
82
+
83
+ export default function Video({
84
+ src,
85
+ width,
86
+ height,
87
+ autoPlay = true,
88
+ muted = true,
89
+ loop = true,
90
+ playsInline = true,
91
+ controls = false,
92
+ className,
93
+ poster,
94
+ }: Props) {
95
+ return (
96
+ <video
97
+ src={src}
98
+ width={width}
99
+ height={height}
100
+ autoPlay={autoPlay}
101
+ muted={muted}
102
+ loop={loop}
103
+ playsInline={playsInline}
104
+ controls={controls}
105
+ className={className}
106
+ poster={poster}
107
+ />
108
+ );
109
+ }
110
+ `;
111
+
112
+ return files;
113
+ }
@@ -1,6 +1,23 @@
1
1
  import type { MigrationContext } from "../types.ts";
2
2
 
3
3
  export function generateViteConfig(ctx: MigrationContext): string {
4
+ const isVtex = ctx.platform === "vtex";
5
+
6
+ const vtexProxy = isVtex ? `
7
+ // VTEX API proxy for local development
8
+ proxy: {
9
+ "/api/": {
10
+ target: "https://\${process.env.VTEX_ACCOUNT || "${ctx.siteName}"}.vtexcommercestable.com.br",
11
+ changeOrigin: true,
12
+ secure: true,
13
+ },
14
+ "/checkout/": {
15
+ target: "https://\${process.env.VTEX_ACCOUNT || "${ctx.siteName}"}.vtexcommercestable.com.br",
16
+ changeOrigin: true,
17
+ secure: true,
18
+ },
19
+ },` : "";
20
+
4
21
  return `import { cloudflare } from "@cloudflare/vite-plugin";
5
22
  import { tanstackStart } from "@tanstack/react-start/plugin/vite";
6
23
  import { decoVitePlugin } from "@decocms/start/vite";
@@ -13,7 +30,7 @@ const srcDir = path.resolve(__dirname, "src");
13
30
 
14
31
  export default defineConfig({
15
32
  server: {
16
- allowedHosts: [".decocdn.com"],
33
+ allowedHosts: [".decocdn.com"],${vtexProxy}
17
34
  },
18
35
  plugins: [
19
36
  cloudflare({ viteEnvironment: { name: "ssr" } }),