@decocms/start 1.6.2 → 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
|
@@ -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,27 +8,78 @@ 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(` *
|
|
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
|
-
|
|
22
|
-
|
|
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 {
|
|
25
|
-
lines.push(`import { deletePaymentFromRequest } from "@decocms/apps/vtex/actions/
|
|
26
|
-
lines.push(`import {
|
|
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
|
|
|
@@ -34,88 +87,140 @@ export function generateCommerceLoaders(ctx: MigrationContext): string {
|
|
|
34
87
|
|
|
35
88
|
if (ctx.platform === "vtex") {
|
|
36
89
|
lines.push(` ...vtexLoaders,`);
|
|
90
|
+
lines.push(` ...siteLoaders,`);
|
|
37
91
|
lines.push(``);
|
|
38
92
|
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
//
|
|
63
|
-
lines.push(` // VTEX
|
|
64
|
-
lines.push(` "vtex/
|
|
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
|
|
68
|
-
if (
|
|
69
|
-
lines.push(` //
|
|
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
|
|
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
|
|
75
|
-
lines.push(` const
|
|
76
|
-
lines.push(` const
|
|
77
|
-
lines.push(` headers
|
|
78
|
-
lines.push(`
|
|
79
|
-
lines.push(`
|
|
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
|
-
//
|
|
87
|
-
if (
|
|
88
|
-
lines.push(` //
|
|
89
|
-
lines.push(` "site/loaders/
|
|
90
|
-
lines.push(` "
|
|
91
|
-
lines.push(`
|
|
92
|
-
lines.push(`
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
lines.push(`
|
|
115
|
-
lines.push(`
|
|
116
|
-
lines.push(`
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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:
|
|
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",
|