@decocms/start 1.6.2 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) 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/.releaserc.json +1 -0
  7. package/package.json +1 -1
  8. package/scripts/generate-blocks.ts +8 -5
  9. package/scripts/generate-loaders.ts +79 -12
  10. package/scripts/migrate/analyzers/island-classifier.ts +23 -0
  11. package/scripts/migrate/analyzers/section-metadata.ts +63 -7
  12. package/scripts/migrate/phase-analyze.ts +190 -11
  13. package/scripts/migrate/phase-cleanup.ts +1162 -7
  14. package/scripts/migrate/phase-scaffold.ts +294 -5
  15. package/scripts/migrate/phase-transform.ts +56 -3
  16. package/scripts/migrate/templates/app-css.ts +149 -2
  17. package/scripts/migrate/templates/commerce-loaders.ts +174 -69
  18. package/scripts/migrate/templates/lib-utils.ts +255 -0
  19. package/scripts/migrate/templates/package-json.ts +30 -22
  20. package/scripts/migrate/templates/routes.ts +81 -11
  21. package/scripts/migrate/templates/section-loaders.ts +369 -33
  22. package/scripts/migrate/templates/server-entry.ts +350 -80
  23. package/scripts/migrate/templates/setup.ts +78 -8
  24. package/scripts/migrate/templates/types-gen.ts +58 -0
  25. package/scripts/migrate/templates/ui-components.ts +47 -16
  26. package/scripts/migrate/templates/vite-config.ts +17 -6
  27. package/scripts/migrate/templates/wrangler.ts +3 -1
  28. package/scripts/migrate/transforms/dead-code.ts +330 -4
  29. package/scripts/migrate/transforms/deno-isms.ts +19 -0
  30. package/scripts/migrate/transforms/imports.ts +93 -30
  31. package/scripts/migrate/transforms/jsx.ts +79 -4
  32. package/scripts/migrate/transforms/section-conventions.ts +105 -3
  33. package/scripts/migrate/types.ts +9 -1
  34. package/src/cms/resolve.ts +12 -1
  35. package/src/sdk/useScript.ts +27 -6
  36. package/src/sdk/workerEntry.ts +11 -2
  37. package/src/setup.ts +1 -1
@@ -1,4 +1,6 @@
1
1
  import type { MigrationContext } from "../types.ts";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
2
4
 
3
5
  function hasLoaderByName(ctx: MigrationContext, name: string): boolean {
4
6
  return ctx.loaderInventory.some(
@@ -6,116 +8,219 @@ function hasLoaderByName(ctx: MigrationContext, name: string): boolean {
6
8
  );
7
9
  }
8
10
 
11
+ function fileExists(ctx: MigrationContext, relPath: string): boolean {
12
+ const full = path.join(ctx.sourceDir, relPath);
13
+ if (fs.existsSync(full)) return true;
14
+ const src = path.join(ctx.sourceDir, "src", relPath);
15
+ return fs.existsSync(src);
16
+ }
17
+
9
18
  export function generateCommerceLoaders(ctx: MigrationContext): string {
10
19
  const lines: string[] = [];
20
+ const hasSecrets = fileExists(ctx, "utils/secrets.ts") || fileExists(ctx, "src/utils/secrets.ts");
21
+ const hasProductReviews = hasLoaderByName(ctx, "reviews/productReviews");
22
+ const hasBuyTogether = hasLoaderByName(ctx, "product/buyTogether");
23
+ const hasAutocomplete = hasLoaderByName(ctx, "intelligenseSearch") || hasLoaderByName(ctx, "intelligentSearch");
24
+ const hasVtexAuth = hasLoaderByName(ctx, "vtex-auth-loader");
25
+ const hasCollectionPLP = hasLoaderByName(ctx, "productListPageCollection");
26
+ const hasStores = hasLoaderByName(ctx, "stores");
27
+ const hasProductCard = hasLoaderByName(ctx, "Layouts/ProductCard");
28
+ const hasSitename = fileExists(ctx, "utils/sitename.ts") || fileExists(ctx, "src/utils/sitename.ts");
11
29
 
12
30
  lines.push(`/**`);
13
31
  lines.push(` * Commerce Loaders — data fetchers registered for CMS block resolution.`);
14
32
  lines.push(` *`);
15
33
  lines.push(` * Standard VTEX loaders come from createVtexCommerceLoaders().`);
16
- lines.push(` * Custom loaders use dynamic imports to the migrated loader files.`);
34
+ lines.push(` * Auto-generated pass-throughs come from loaders.gen.ts.`);
35
+ lines.push(` * This file only contains entries that need custom wiring`);
36
+ lines.push(` * (cookie injection, secrets, serialization, etc.).`);
17
37
  lines.push(` */`);
18
38
 
19
39
  if (ctx.platform === "vtex") {
40
+ lines.push(`import { getVtexConfig } from "@decocms/apps/vtex";`);
20
41
  lines.push(`import { createVtexCommerceLoaders, createCachedPDPLoader } from "@decocms/apps/vtex/commerceLoaders";`);
21
- lines.push(`import { createCachedLoader } from "@decocms/start/sdk/cachedLoader";`);
22
- lines.push(`import { createAddressFromRequest, updateAddressFromRequest, deleteAddressFromRequest } from "@decocms/apps/vtex/actions/address";`);
42
+ if (hasAutocomplete) {
43
+ lines.push(`import { autocompleteSearch } from "@decocms/apps/vtex/loaders/autocomplete";`);
44
+ }
23
45
  lines.push(`import { getAddressByPostalCode } from "@decocms/apps/vtex/loaders/address";`);
24
- lines.push(`import { updateProfileFromRequest, newsletterProfileFromRequest } from "@decocms/apps/vtex/actions/profile";`);
25
- lines.push(`import { deletePaymentFromRequest } from "@decocms/apps/vtex/actions/payments";`);
26
- lines.push(`import { getPasswordLastUpdate } from "@decocms/apps/vtex/loaders/profile";`);
46
+ lines.push(`import { createAddressFromRequest, updateAddressFromRequest, deleteAddressFromRequest } from "@decocms/apps/vtex/actions/address";`);
47
+ lines.push(`import { updateProfileFromRequest, newsletterProfileFromRequest, deletePaymentFromRequest, getPasswordLastUpdate } from "@decocms/apps/vtex/actions/profile";`);
48
+ lines.push(`import { createCachedLoader } from "@decocms/start/sdk/cachedLoader";`);
49
+
50
+ if (hasVtexAuth) {
51
+ lines.push(`import vtexAuthLoader from "../loaders/vtex-auth-loader";`);
52
+ }
53
+ if (hasProductReviews) {
54
+ lines.push(`import productReviewsLoader from "../loaders/reviews/productReviews";`);
55
+ }
56
+ if (hasBuyTogether) {
57
+ lines.push(`import buyTogetherLoader from "../loaders/product/buyTogether";`);
58
+ }
59
+ if (hasSecrets) {
60
+ lines.push(`import { secrets } from "../utils/secrets";`);
61
+ }
62
+ if (hasSitename && hasCollectionPLP) {
63
+ lines.push(`import { useAccount } from "../utils/sitename";`);
64
+ }
65
+
66
+ // Always import siteLoaders — the generate:loaders script creates this file
67
+ lines.push(`import { siteLoaders } from "../server/cms/loaders.gen";`);
68
+ lines.push(``);
69
+
70
+ lines.push(`const DOMAIN_RE = /;\\s*domain=[^;]*/gi;`);
27
71
  lines.push(``);
28
72
  lines.push(`export const vtexLoaders = createVtexCommerceLoaders();`);
29
73
  lines.push(`export const cachedPDP = createCachedPDPLoader();`);
74
+
75
+ if (hasCollectionPLP) {
76
+ lines.push(`const cachedPLP = vtexLoaders["vtex/loaders/intelligentSearch/productListingPage.ts"];`);
77
+ }
78
+ if (hasAutocomplete) {
79
+ lines.push(`const cachedAutocomplete = createCachedLoader("vtex/autocomplete", autocompleteSearch, "search");`);
80
+ }
81
+
82
+ lines.push(``);
30
83
  lines.push(``);
31
84
  }
32
85
 
33
- lines.push(`export const COMMERCE_LOADERS: Record<string, (props: any) => Promise<any>> = {`);
86
+ lines.push(`export const COMMERCE_LOADERS: Record<string, (props: any, request?: Request) => Promise<any>> = {`);
34
87
 
35
88
  if (ctx.platform === "vtex") {
36
89
  lines.push(` ...vtexLoaders,`);
90
+ lines.push(` ...siteLoaders,`);
37
91
  lines.push(``);
38
92
 
39
- // Generic VTEX address actions
40
- lines.push(` // VTEX Address CRUD`);
41
- lines.push(` "vtex/actions/address/createAddress": createAddressFromRequest,`);
42
- lines.push(` "vtex/actions/address/updateAddress": updateAddressFromRequest,`);
43
- lines.push(` "vtex/actions/address/deleteAddress": deleteAddressFromRequest,`);
44
- lines.push(` "vtex/loaders/address/getAddressByZIP": async (props: any) => {`);
45
- lines.push(` return getAddressByPostalCode(props.countryCode ?? "BRA", props.postalCode);`);
46
- lines.push(` },`);
47
- lines.push(``);
48
-
49
- // Generic VTEX profile actions
50
- lines.push(` // VTEX Profile actions`);
51
- lines.push(` "vtex/actions/profile/updateProfile": updateProfileFromRequest,`);
52
- lines.push(` "vtex/actions/profile/updateProfile.ts": updateProfileFromRequest,`);
53
- lines.push(` "vtex/actions/profile/newsletterProfile": newsletterProfileFromRequest,`);
54
- lines.push(` "vtex/actions/profile/newsletterProfile.ts": newsletterProfileFromRequest,`);
55
- lines.push(``);
93
+ // Autocomplete aliases
94
+ if (hasAutocomplete) {
95
+ lines.push(` // Autocomplete search — from @decocms/apps`);
96
+ lines.push(` "site/loaders/search/intelligenseSearch.ts": cachedAutocomplete,`);
97
+ lines.push(` "site/loaders/search/intelligenseSearch": cachedAutocomplete,`);
98
+ lines.push(``);
99
+ }
56
100
 
57
- // Generic VTEX payment actions
58
- lines.push(` // VTEX Payment actions`);
59
- lines.push(` "vtex/actions/payments/delete": deletePaymentFromRequest,`);
60
- lines.push(``);
101
+ // Stores pass-through
102
+ if (hasStores) {
103
+ lines.push(` // Stores pass-through`);
104
+ lines.push(` "site/loaders/stores.ts": async (props: any) => {`);
105
+ lines.push(` const result = props.stores ?? props ?? [];`);
106
+ lines.push(` return Array.isArray(result) ? result : [];`);
107
+ lines.push(` },`);
108
+ lines.push(``);
109
+ }
61
110
 
62
- // Generic VTEX profile loaders
63
- lines.push(` // VTEX Profile loaders`);
64
- lines.push(` "vtex/loaders/profile/passwordLastUpdate": getPasswordLastUpdate,`);
111
+ // VTEX address CRUD
112
+ lines.push(` // VTEX address CRUD — request-aware wrappers from @decocms/apps`);
113
+ lines.push(` "vtex/actions/address/createAddress": createAddressFromRequest as any,`);
114
+ lines.push(` "vtex/actions/address/updateAddress": updateAddressFromRequest as any,`);
115
+ lines.push(` "vtex/actions/address/deleteAddress": deleteAddressFromRequest as any,`);
116
+ lines.push(` "vtex/loaders/address/getAddressByZIP": async (props: any) => {`);
117
+ lines.push(` return getAddressByPostalCode(props.countryCode, props.postalCode);`);
118
+ lines.push(` },`);
65
119
  lines.push(``);
66
120
 
67
- // Auth cookie stripping wrapper
68
- if (hasLoaderByName(ctx, "vtex-auth-loader")) {
69
- lines.push(` // Auth loader with cookie domain stripping for Workers`);
121
+ // Auth cookie stripping
122
+ if (hasVtexAuth) {
123
+ lines.push(` // VTEX auth (Set-Cookie forwarding)`);
70
124
  lines.push(` "site/loaders/vtex-auth-loader": async (props: any) => {`);
71
- lines.push(` const mod = await import("../loaders/vtex-auth-loader");`);
72
- lines.push(` const result = await mod.default(props);`);
125
+ lines.push(` const result = await vtexAuthLoader(props);`);
73
126
  lines.push(` if (result instanceof Response) {`);
74
- lines.push(` const headers = new Headers(result.headers);`);
75
- lines.push(` const cookies = headers.getSetCookie?.() ?? [];`);
76
- lines.push(` const stripped = cookies.map((c: string) => c.replace(/Domain=[^;]*/i, ""));`);
77
- lines.push(` headers.delete("Set-Cookie");`);
78
- lines.push(` stripped.forEach((c: string) => headers.append("Set-Cookie", c));`);
79
- lines.push(` return new Response(result.body, { status: result.status, headers });`);
127
+ lines.push(` const setCookies = result.headers.getSetCookie?.() ?? [];`);
128
+ lines.push(` const strippedCookies = setCookies.map((c) => c.replace(DOMAIN_RE, ""));`);
129
+ lines.push(` const body = await result.text();`);
130
+ lines.push(` const headers = new Headers({ "Content-Type": "application/json" });`);
131
+ lines.push(` for (const cookie of strippedCookies) {`);
132
+ lines.push(` headers.append("Set-Cookie", cookie);`);
133
+ lines.push(` }`);
134
+ lines.push(` return new Response(body, { status: result.status, headers });`);
80
135
  lines.push(` }`);
81
136
  lines.push(` return result;`);
82
137
  lines.push(` },`);
83
138
  lines.push(``);
84
139
  }
85
140
 
86
- // Cached autocomplete alias
87
- if (hasLoaderByName(ctx, "intelligenseSearch") || hasLoaderByName(ctx, "intelligentSearch")) {
88
- lines.push(` // Cached autocomplete search alias`);
89
- lines.push(` "site/loaders/search/intelligenseSearch.ts": createCachedLoader(`);
90
- lines.push(` "vtex/autocomplete",`);
91
- lines.push(` vtexLoaders["vtex/loaders/intelligentSearch/autocomplete"] as any,`);
92
- lines.push(` "search",`);
93
- lines.push(` ),`);
94
- lines.push(``);
141
+ // ProductCard dynamic import
142
+ if (hasProductCard) {
143
+ lines.push(` // CMS-referenced site loaders`);
144
+ lines.push(` "site/loaders/Layouts/ProductCard.tsx": async (props: any) => {`);
145
+ lines.push(` const mod = await import("../loaders/Layouts/ProductCard");`);
146
+ lines.push(` return mod.default(props);`);
147
+ lines.push(` },`);
95
148
  }
96
- }
97
-
98
- // Add custom loaders from inventory
99
- for (const loader of ctx.loaderInventory) {
100
- if (!loader.isCustom) continue;
101
149
 
102
- const siteKey = `site/${loader.path}`;
103
- const importPath = `../${loader.path}`.replace(/\.tsx?$/, "");
150
+ // Reviews with secrets
151
+ if (hasProductReviews) {
152
+ lines.push(` "site/loaders/reviews/productReviews.ts": async (props: any) => {`);
153
+ lines.push(` const { account } = getVtexConfig();`);
154
+ lines.push(` const result = await productReviewsLoader(props, null as any, { account${hasSecrets ? ", ...secrets" : ""} } as any);`);
155
+ lines.push(` if (!result) return result;`);
156
+ lines.push(` const { getProductReview: _r, reviewLikeAction: _l, reviewVote: _v, getProductsListReviews: _p, ...serializable } = result as any;`);
157
+ lines.push(` return serializable;`);
158
+ lines.push(` },`);
159
+ }
104
160
 
105
- lines.push(` // Custom: ${loader.path}`);
106
- lines.push(` "${siteKey}": async (props: any) => {`);
107
- lines.push(` const mod = await import("${importPath}");`);
108
- lines.push(` return mod.default(props);`);
109
- lines.push(` },`);
161
+ // BuyTogether with secrets
162
+ if (hasBuyTogether) {
163
+ lines.push(` "site/loaders/product/buyTogether.ts": async (props: any) => {`);
164
+ lines.push(` const { account } = getVtexConfig();`);
165
+ lines.push(` return buyTogetherLoader(props, null as any, { account${hasSecrets ? ", ...secrets" : ""} } as any);`);
166
+ lines.push(` },`);
167
+ }
110
168
 
111
- // Also add without extension
112
- const siteKeyNoExt = siteKey.replace(/\.tsx?$/, "");
113
- if (siteKeyNoExt !== siteKey) {
114
- lines.push(` "${siteKeyNoExt}": async (props: any) => {`);
115
- lines.push(` const mod = await import("${importPath}");`);
116
- lines.push(` return mod.default(props);`);
169
+ // Collection PLP
170
+ if (hasCollectionPLP && hasSitename) {
171
+ lines.push(``);
172
+ lines.push(` // Collection PLP`);
173
+ lines.push(` "site/loaders/search/productListPageCollection.ts": async (props: any) => {`);
174
+ lines.push(` const url = new URL(props.__pageUrl || props.__pagePath || "/", "https://localhost");`);
175
+ lines.push(` const { search_collection_urls_cvlb } = await import("../utils/search-collection-url-cvlb");`);
176
+ lines.push(` const { createBreadcrumbFromPath } = await import("../utils/plpHelpers/plpCollection");`);
177
+ lines.push(` const { an: accountName } = useAccount();`);
178
+ lines.push(` const collections = search_collection_urls_cvlb[accountName] ?? [];`);
179
+ lines.push(``);
180
+ lines.push(` const normalize = (t: string) =>`);
181
+ lines.push(` t.normalize("NFD").replace(/[\\u0300-\\u036f]/g, "").toLowerCase()`);
182
+ lines.push(` .replace(/[^a-z0-9\\s-]/g, "").replace(/\\s+/g, "-").replace(/-+/g, "-");`);
183
+ lines.push(``);
184
+ lines.push(` const slug = decodeURIComponent(url.pathname.split("/").pop() ?? "").replace(/-/g, " ");`);
185
+ lines.push(` const now = Date.now();`);
186
+ lines.push(` const collection = collections.find((c: any) =>`);
187
+ lines.push(` normalize(c.name) === normalize(slug) &&`);
188
+ lines.push(` new Date(c.dateFrom).getTime() <= now &&`);
189
+ lines.push(` now <= new Date(c.dateTo).getTime()`);
190
+ lines.push(` );`);
191
+ lines.push(` if (!collection) return null;`);
192
+ lines.push(``);
193
+ lines.push(` const collectionId = String(collection.id);`);
194
+ lines.push(` const response = await cachedPLP({`);
195
+ lines.push(` ...props,`);
196
+ lines.push(` selectedFacets: [{ key: "productClusterIds", value: collectionId }],`);
197
+ lines.push(` __pageUrl: url.toString(),`);
198
+ lines.push(` __pagePath: url.pathname,`);
199
+ lines.push(` });`);
200
+ lines.push(` if (!response) return null;`);
201
+ lines.push(``);
202
+ lines.push(` return {`);
203
+ lines.push(` ...response,`);
204
+ lines.push(` breadcrumb: createBreadcrumbFromPath(url.pathname, url, collection.name) ?? {},`);
205
+ lines.push(` seo: {`);
206
+ lines.push(` title: collection.name,`);
207
+ lines.push(` description: "O melhor site de compras online para sua casa: compre itens de cozinha, móveis para sala e escritório, acessórios de tecnologia e mais. Clique já!",`);
208
+ lines.push(` noIndexing: false,`);
209
+ lines.push(` canonical: url.toString(),`);
210
+ lines.push(` },`);
211
+ lines.push(` };`);
117
212
  lines.push(` },`);
118
213
  }
214
+
215
+ lines.push(``);
216
+ // Profile actions
217
+ lines.push(` // Profile actions — request-aware wrappers from @decocms/apps`);
218
+ lines.push(` "vtex/actions/profile/updateProfile": updateProfileFromRequest as any,`);
219
+ lines.push(` "vtex/actions/profile/updateProfile.ts": updateProfileFromRequest as any,`);
220
+ lines.push(` "vtex/actions/profile/newsletterProfile": newsletterProfileFromRequest as any,`);
221
+ lines.push(` "vtex/actions/profile/newsletterProfile.ts": newsletterProfileFromRequest as any,`);
222
+ lines.push(` "vtex/actions/payments/delete": deletePaymentFromRequest as any,`);
223
+ lines.push(` "vtex/loaders/profile/passwordLastUpdate": getPasswordLastUpdate as any,`);
119
224
  }
120
225
 
121
226
  lines.push(`};`);
@@ -0,0 +1,255 @@
1
+ import type { MigrationContext } from "../types.ts";
2
+
3
+ /**
4
+ * Generates src/lib/ utility wrappers that provide signature-compatible
5
+ * stubs for VTEX utilities. The old stack (deco-cx/apps) exports functions
6
+ * with different signatures than @decocms/apps-start, and some types
7
+ * (VTEXCommerceStable, LabelledFuzzy) don't exist at all. These wrappers
8
+ * bridge the gap so custom loaders continue to compile and run.
9
+ */
10
+ export function generateLibUtils(_ctx: MigrationContext): Record<string, string> {
11
+ return {
12
+ "src/lib/vtex-transform.ts": LIB_VTEX_TRANSFORM,
13
+ "src/lib/vtex-intelligent-search.ts": LIB_VTEX_INTELLIGENT_SEARCH,
14
+ "src/lib/vtex-segment.ts": LIB_VTEX_SEGMENT,
15
+ "src/lib/http-utils.ts": LIB_HTTP_UTILS,
16
+ "src/lib/vtex-client.ts": LIB_VTEX_CLIENT,
17
+ "src/lib/fetch-utils.ts": LIB_FETCH_UTILS,
18
+ "src/lib/vtex-fetch.ts": LIB_VTEX_FETCH,
19
+ "src/lib/vtex-id.ts": LIB_VTEX_ID,
20
+ "src/lib/graphql-utils.ts": LIB_GRAPHQL_UTILS,
21
+ "src/lib/filter-navigate.ts": LIB_FILTER_NAVIGATE,
22
+ };
23
+ }
24
+
25
+ const LIB_VTEX_TRANSFORM = `import type { Product } from "@decocms/apps/commerce/types";
26
+
27
+ export function toProduct(vtexProduct: any): Product {
28
+ return vtexProduct as Product;
29
+ }
30
+ `;
31
+
32
+ const LIB_VTEX_INTELLIGENT_SEARCH = `export function getISCookiesFromBag(_req?: any): Record<string, string> {
33
+ return {};
34
+ }
35
+
36
+ export function isFilterParam(key: string): boolean {
37
+ return key.startsWith("filter.");
38
+ }
39
+
40
+ export function toPath(facets: { key: string; value: string }[]): string {
41
+ return facets.map((f) => \`\${f.key}/\${f.value}\`).join("/");
42
+ }
43
+
44
+ export function withDefaultFacets(
45
+ facets: { key: string; value: string }[],
46
+ defaults?: any,
47
+ ): { key: string; value: string }[] {
48
+ if (Array.isArray(defaults)) {
49
+ return [...defaults, ...facets];
50
+ }
51
+ return [...facets];
52
+ }
53
+
54
+ export function withDefaultParams(
55
+ params: any,
56
+ defaults?: Record<string, string>,
57
+ ): any {
58
+ if (params instanceof URLSearchParams) {
59
+ if (defaults) {
60
+ for (const [key, value] of Object.entries(defaults)) {
61
+ if (!params.has(key)) {
62
+ params.set(key, value);
63
+ }
64
+ }
65
+ }
66
+ return params;
67
+ }
68
+ return { ...params, ...defaults };
69
+ }
70
+ `;
71
+
72
+ const LIB_VTEX_SEGMENT = `export function getSegmentFromBag(_req?: any): Record<string, unknown> | null {
73
+ return null;
74
+ }
75
+
76
+ export function withSegmentCookie(..._args: any[]): any {
77
+ for (const arg of _args) {
78
+ if (arg instanceof Headers) {
79
+ return arg;
80
+ }
81
+ }
82
+ return new Headers();
83
+ }
84
+ `;
85
+
86
+ const LIB_HTTP_UTILS = `/**
87
+ * Drop-in replacement for the typed HTTP client from deco-cx/apps.
88
+ * Supports both simple \`.get(path)\` / \`.post(path, body)\` calls AND
89
+ * the indexed pattern \`client["GET /api/path"]({params}, {init})\`
90
+ * used by legacy loaders.
91
+ */
92
+ export function createHttpClient<_T = any>(options: {
93
+ base: string;
94
+ headers?: Record<string, string> | Headers;
95
+ fetcher?: typeof fetch;
96
+ }) {
97
+ const base = options.base.replace(/\\/$/, "");
98
+ const defaultHeaders: Record<string, string> =
99
+ options.headers instanceof Headers
100
+ ? Object.fromEntries(options.headers.entries())
101
+ : (options.headers || {});
102
+
103
+ const handler: ProxyHandler<Record<string, unknown>> = {
104
+ get(_target, prop) {
105
+ if (prop === "get") {
106
+ return async <R = any>(path: string, init?: RequestInit): Promise<R> => {
107
+ const res = await fetch(\`\${base}\${path}\`, {
108
+ ...init,
109
+ headers: { ...defaultHeaders, ...(init?.headers as Record<string, string>) },
110
+ });
111
+ return res.json();
112
+ };
113
+ }
114
+ if (prop === "post") {
115
+ return async <R = any>(path: string, body: unknown, init?: RequestInit): Promise<R> => {
116
+ const res = await fetch(\`\${base}\${path}\`, {
117
+ method: "POST",
118
+ ...init,
119
+ headers: {
120
+ "Content-Type": "application/json",
121
+ ...defaultHeaders,
122
+ ...(init?.headers as Record<string, string>),
123
+ },
124
+ body: JSON.stringify(body),
125
+ });
126
+ return res.json();
127
+ };
128
+ }
129
+ if (typeof prop === "string" && /^(GET|POST|PUT|PATCH|DELETE)\\s+/.test(prop)) {
130
+ const spaceIdx = prop.indexOf(" ");
131
+ const method = prop.slice(0, spaceIdx);
132
+ let apiPath = prop.slice(spaceIdx + 1);
133
+
134
+ return async (params: Record<string, any> = {}, init?: RequestInit) => {
135
+ const cleanParams = { ...params };
136
+
137
+ const starMatch = apiPath.match(/\\*(\\w+)/);
138
+ if (starMatch) {
139
+ const paramName = starMatch[1];
140
+ if (cleanParams[paramName] != null) {
141
+ apiPath = apiPath.replace(\`*\${paramName}\`, String(cleanParams[paramName]));
142
+ delete cleanParams[paramName];
143
+ } else {
144
+ apiPath = apiPath.replace(/\\/\\*\\w+/, "");
145
+ }
146
+ }
147
+
148
+ let url = \`\${base}\${apiPath}\`;
149
+
150
+ if (method === "GET") {
151
+ const sp = new URLSearchParams();
152
+ for (const [k, v] of Object.entries(cleanParams)) {
153
+ if (v !== undefined && v !== null) sp.set(k, String(v));
154
+ }
155
+ const qs = sp.toString();
156
+ if (qs) url += (url.includes("?") ? "&" : "?") + qs;
157
+ }
158
+
159
+ const fetchInit: RequestInit = {
160
+ method,
161
+ ...init,
162
+ headers: {
163
+ ...defaultHeaders,
164
+ ...(init?.headers instanceof Headers
165
+ ? Object.fromEntries(init.headers.entries())
166
+ : (init?.headers as Record<string, string>)),
167
+ },
168
+ ...(method !== "GET" && Object.keys(cleanParams).length > 0
169
+ ? { body: JSON.stringify(cleanParams) }
170
+ : {}),
171
+ };
172
+
173
+ const res = await fetch(url, fetchInit);
174
+ return { json: () => res.json(), ok: res.ok, status: res.status, headers: res.headers };
175
+ };
176
+ }
177
+ return undefined;
178
+ },
179
+ };
180
+
181
+ return new Proxy({} as Record<string, unknown>, handler) as any;
182
+ }
183
+ `;
184
+
185
+ const LIB_VTEX_CLIENT = `export interface VTEXCommerceStable {
186
+ account: string;
187
+ environment?: string;
188
+ }
189
+ `;
190
+
191
+ const LIB_FETCH_UTILS = `export const STALE = {
192
+ "Cache-Control": "public, max-age=120, stale-while-revalidate=600",
193
+ };
194
+ `;
195
+
196
+ const LIB_VTEX_FETCH = `export async function fetchSafe(
197
+ input: string | URL | Request,
198
+ init?: RequestInit,
199
+ ): Promise<Response> {
200
+ const response = await fetch(input, init);
201
+ if (!response.ok) {
202
+ console.error(\`VTEX fetch failed: \${response.status} \${response.statusText}\`);
203
+ }
204
+ return response;
205
+ }
206
+ `;
207
+
208
+ const LIB_VTEX_ID = `export function parseCookie(cookieStr?: string | null): Record<string, string> {
209
+ if (!cookieStr) return {};
210
+ return Object.fromEntries(
211
+ cookieStr.split(";").map((c) => {
212
+ const [key, ...rest] = c.trim().split("=");
213
+ return [key, rest.join("=")];
214
+ }),
215
+ );
216
+ }
217
+ `;
218
+
219
+ const LIB_GRAPHQL_UTILS = `export function createGraphqlClient(options: {
220
+ endpoint: string;
221
+ headers?: Record<string, string>;
222
+ }) {
223
+ return {
224
+ async query<T = any>(query: string, variables?: Record<string, unknown>): Promise<T> {
225
+ const res = await fetch(options.endpoint, {
226
+ method: "POST",
227
+ headers: {
228
+ "Content-Type": "application/json",
229
+ ...options.headers,
230
+ },
231
+ body: JSON.stringify({ query, variables }),
232
+ });
233
+ const json = await res.json();
234
+ return json.data;
235
+ },
236
+ };
237
+ }
238
+ `;
239
+
240
+ const LIB_FILTER_NAVIGATE = `/**
241
+ * Converts a VTEX filter URL string (e.g. "?filter.brand=x&filter.price=10:50")
242
+ * into a clean search string without internal params like \`payload\`.
243
+ * Returns "" or "?filter.brand=x&..." ready to append to pathname.
244
+ */
245
+ export function toFilterSearchString(filterUrl: string): string {
246
+ const str = filterUrl.startsWith("?") ? filterUrl.slice(1) : filterUrl;
247
+ if (!str) return "";
248
+
249
+ const params = new URLSearchParams(str);
250
+ params.delete("payload");
251
+
252
+ const clean = params.toString();
253
+ return clean ? \`?\${clean}\` : "";
254
+ }
255
+ `;
@@ -24,35 +24,43 @@ function getLatestVersion(pkg: string, fallback: string): string {
24
24
  */
25
25
  function extractNpmDeps(importMap: Record<string, string>): Record<string, string> {
26
26
  const deps: Record<string, string> = {};
27
+ const SKIP_KEYS = new Set([
28
+ "daisyui", "preact-render-to-string", "simple-git", "fast-json-patch",
29
+ "postcss", "cssnano", "partytown",
30
+ ]);
27
31
  for (const [key, value] of Object.entries(importMap)) {
28
- if (!value.startsWith("npm:")) continue;
29
32
  // Skip framework deps we handle ourselves
30
33
  if (key.startsWith("preact") || key.startsWith("@preact/")) continue;
31
34
  if (key.startsWith("@deco/")) continue;
32
- if (key === "daisyui") continue;
33
- if (key === "preact-render-to-string") continue;
34
- if (key === "simple-git") continue;
35
- if (key === "fast-json-patch") continue;
36
- if (key === "postcss") continue;
37
- if (key === "cssnano") continue;
38
35
  if (key.startsWith("@biomejs/")) continue;
39
- if (key === "partytown") continue;
40
- // Consolidate firebase/* split imports into single "firebase" package
41
36
  if (key.startsWith("firebase/")) continue;
37
+ if (SKIP_KEYS.has(key)) continue;
42
38
 
43
- const raw = value.slice(4); // remove "npm:"
44
- const atIdx = raw.lastIndexOf("@");
45
- if (atIdx <= 0) {
46
- deps[raw] = "*";
47
- } else {
48
- const name = raw.slice(0, atIdx);
49
- let version = raw.slice(atIdx + 1);
50
- // Don't double-prefix with ^ if version already has a range prefix
51
- if (/^[~^>=<]/.test(version)) {
52
- deps[name] = version;
39
+ // npm: protocol direct npm import
40
+ if (value.startsWith("npm:")) {
41
+ const raw = value.slice(4);
42
+ const atIdx = raw.lastIndexOf("@");
43
+ if (atIdx <= 0) {
44
+ deps[raw] = "*";
53
45
  } else {
54
- deps[name] = `^${version}`;
46
+ const name = raw.slice(0, atIdx);
47
+ let version = raw.slice(atIdx + 1);
48
+ if (/^[~^>=<]/.test(version)) {
49
+ deps[name] = version;
50
+ } else {
51
+ deps[name] = `^${version}`;
52
+ }
55
53
  }
54
+ continue;
55
+ }
56
+
57
+ // esm.sh URLs — extract package name and version
58
+ const esmMatch = value.match(/esm\.sh\/(@?[^@?]+)@([^?/]+)/);
59
+ if (esmMatch) {
60
+ const [, name, version] = esmMatch;
61
+ if (name.startsWith("preact") || name.startsWith("@preact/")) continue;
62
+ deps[name] = `^${version}`;
63
+ continue;
56
64
  }
57
65
  }
58
66
  return deps;
@@ -97,9 +105,9 @@ export function generatePackageJson(ctx: MigrationContext): string {
97
105
  "tsx node_modules/@decocms/start/scripts/generate-invoke.ts",
98
106
  "generate:sections":
99
107
  "tsx node_modules/@decocms/start/scripts/generate-sections.ts",
100
- "generate:loaders": `tsx node_modules/@decocms/start/scripts/generate-loaders.ts --exclude vtex/loaders,vtex/actions`,
108
+ "generate:loaders": `tsx node_modules/@decocms/start/scripts/generate-loaders.ts --exclude vtex/loaders,vtex/actions,loaders/vtex-auth-loader,loaders/reviews/productReviews,loaders/product/buyTogether,loaders/search/productListPageCollection,loaders/search/intelligenseSearch,loaders/Layouts/ProductCard`,
101
109
  build:
102
- "npm run generate:blocks && npm run generate:schema && npm run generate:sections && npm run generate:loaders && tsr generate && vite build",
110
+ "npm run generate:blocks && npm run generate:sections && npm run generate:loaders && npm run generate:schema && npm run generate:invoke && tsr generate && vite build",
103
111
  preview: "vite preview",
104
112
  deploy: "npm run build && wrangler deploy",
105
113
  types: "wrangler types",