@decocms/start 1.2.6 → 1.2.8
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/package.json +1 -1
- package/scripts/deco-migrate-cli.ts +444 -0
- package/scripts/migrate/analyzers/island-classifier.ts +73 -0
- package/scripts/migrate/analyzers/loader-inventory.ts +63 -0
- package/scripts/migrate/analyzers/section-metadata.ts +91 -0
- package/scripts/migrate/analyzers/theme-extractor.ts +122 -0
- package/scripts/migrate/phase-analyze.ts +147 -17
- package/scripts/migrate/phase-cleanup.ts +124 -2
- package/scripts/migrate/phase-report.ts +44 -16
- package/scripts/migrate/phase-scaffold.ts +38 -132
- package/scripts/migrate/phase-transform.ts +28 -3
- package/scripts/migrate/phase-verify.ts +127 -5
- package/scripts/migrate/templates/app-css.ts +204 -0
- package/scripts/migrate/templates/cache-config.ts +26 -0
- package/scripts/migrate/templates/commerce-loaders.ts +124 -0
- package/scripts/migrate/templates/hooks.ts +358 -0
- package/scripts/migrate/templates/package-json.ts +29 -6
- package/scripts/migrate/templates/routes.ts +41 -136
- package/scripts/migrate/templates/sdk-gen.ts +59 -0
- package/scripts/migrate/templates/section-loaders.ts +108 -0
- package/scripts/migrate/templates/server-entry.ts +174 -67
- package/scripts/migrate/templates/setup.ts +64 -55
- package/scripts/migrate/templates/types-gen.ts +119 -0
- package/scripts/migrate/templates/ui-components.ts +113 -0
- package/scripts/migrate/templates/vite-config.ts +18 -1
- package/scripts/migrate/templates/wrangler.ts +4 -1
- package/scripts/migrate/transforms/dead-code.ts +23 -2
- package/scripts/migrate/transforms/imports.ts +40 -10
- package/scripts/migrate/transforms/jsx.ts +9 -0
- package/scripts/migrate/transforms/section-conventions.ts +83 -0
- package/scripts/migrate/types.ts +74 -0
- package/src/routes/cmsRoute.ts +26 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { MigrationContext } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
export function generateCacheConfig(ctx: MigrationContext): string {
|
|
4
|
+
if (ctx.platform !== "vtex") {
|
|
5
|
+
return `/**
|
|
6
|
+
* Cache configuration — customize edge cache profiles per route type.
|
|
7
|
+
* See @decocms/start/sdk/cacheHeaders for available profiles.
|
|
8
|
+
*/
|
|
9
|
+
// import { setCacheProfile } from "@decocms/start/sdk/cacheHeaders";
|
|
10
|
+
// setCacheProfile("product", { sMaxAge: 300, staleWhileRevalidate: 600 });
|
|
11
|
+
`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return `/**
|
|
15
|
+
* Cache configuration for VTEX storefront.
|
|
16
|
+
* Overrides default cache profiles from @decocms/start/sdk/cacheHeaders.
|
|
17
|
+
*/
|
|
18
|
+
// import { setCacheProfile } from "@decocms/start/sdk/cacheHeaders";
|
|
19
|
+
|
|
20
|
+
// Uncomment and adjust as needed:
|
|
21
|
+
// setCacheProfile("product", { sMaxAge: 300, staleWhileRevalidate: 600 });
|
|
22
|
+
// setCacheProfile("listing", { sMaxAge: 120, staleWhileRevalidate: 300 });
|
|
23
|
+
// setCacheProfile("search", { sMaxAge: 60, staleWhileRevalidate: 120 });
|
|
24
|
+
// setCacheProfile("static", { sMaxAge: 86400, staleWhileRevalidate: 172800 });
|
|
25
|
+
`;
|
|
26
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { MigrationContext } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
function hasLoaderByName(ctx: MigrationContext, name: string): boolean {
|
|
4
|
+
return ctx.loaderInventory.some(
|
|
5
|
+
(l) => l.path.toLowerCase().includes(name.toLowerCase()),
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function generateCommerceLoaders(ctx: MigrationContext): string {
|
|
10
|
+
const lines: string[] = [];
|
|
11
|
+
|
|
12
|
+
lines.push(`/**`);
|
|
13
|
+
lines.push(` * Commerce Loaders — data fetchers registered for CMS block resolution.`);
|
|
14
|
+
lines.push(` *`);
|
|
15
|
+
lines.push(` * Standard VTEX loaders come from createVtexCommerceLoaders().`);
|
|
16
|
+
lines.push(` * Custom loaders use dynamic imports to the migrated loader files.`);
|
|
17
|
+
lines.push(` */`);
|
|
18
|
+
|
|
19
|
+
if (ctx.platform === "vtex") {
|
|
20
|
+
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";`);
|
|
23
|
+
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";`);
|
|
27
|
+
lines.push(``);
|
|
28
|
+
lines.push(`export const vtexLoaders = createVtexCommerceLoaders();`);
|
|
29
|
+
lines.push(`export const cachedPDP = createCachedPDPLoader();`);
|
|
30
|
+
lines.push(``);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
lines.push(`export const COMMERCE_LOADERS: Record<string, (props: any) => Promise<any>> = {`);
|
|
34
|
+
|
|
35
|
+
if (ctx.platform === "vtex") {
|
|
36
|
+
lines.push(` ...vtexLoaders,`);
|
|
37
|
+
lines.push(``);
|
|
38
|
+
|
|
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(``);
|
|
56
|
+
|
|
57
|
+
// Generic VTEX payment actions
|
|
58
|
+
lines.push(` // VTEX Payment actions`);
|
|
59
|
+
lines.push(` "vtex/actions/payments/delete": deletePaymentFromRequest,`);
|
|
60
|
+
lines.push(``);
|
|
61
|
+
|
|
62
|
+
// Generic VTEX profile loaders
|
|
63
|
+
lines.push(` // VTEX Profile loaders`);
|
|
64
|
+
lines.push(` "vtex/loaders/profile/passwordLastUpdate": getPasswordLastUpdate,`);
|
|
65
|
+
lines.push(``);
|
|
66
|
+
|
|
67
|
+
// Auth cookie stripping wrapper
|
|
68
|
+
if (hasLoaderByName(ctx, "vtex-auth-loader")) {
|
|
69
|
+
lines.push(` // Auth loader with cookie domain stripping for Workers`);
|
|
70
|
+
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);`);
|
|
73
|
+
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 });`);
|
|
80
|
+
lines.push(` }`);
|
|
81
|
+
lines.push(` return result;`);
|
|
82
|
+
lines.push(` },`);
|
|
83
|
+
lines.push(``);
|
|
84
|
+
}
|
|
85
|
+
|
|
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(``);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Add custom loaders from inventory
|
|
99
|
+
for (const loader of ctx.loaderInventory) {
|
|
100
|
+
if (!loader.isCustom) continue;
|
|
101
|
+
|
|
102
|
+
const siteKey = `site/${loader.path}`;
|
|
103
|
+
const importPath = `../${loader.path}`.replace(/\.tsx?$/, "");
|
|
104
|
+
|
|
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(` },`);
|
|
110
|
+
|
|
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);`);
|
|
117
|
+
lines.push(` },`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
lines.push(`};`);
|
|
122
|
+
|
|
123
|
+
return lines.join("\n") + "\n";
|
|
124
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import type { MigrationContext } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
export function generateHooks(ctx: MigrationContext): Record<string, string> {
|
|
4
|
+
const files: Record<string, string> = {};
|
|
5
|
+
|
|
6
|
+
if (ctx.platform === "vtex") {
|
|
7
|
+
files["src/hooks/useCart.ts"] = generateVtexUseCart();
|
|
8
|
+
} else {
|
|
9
|
+
files["src/hooks/useCart.ts"] = generateGenericUseCart();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
files["src/hooks/useUser.ts"] = generateUseUser();
|
|
13
|
+
files["src/hooks/useWishlist.ts"] = generateUseWishlist();
|
|
14
|
+
|
|
15
|
+
return files;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function generateVtexUseCart(): string {
|
|
19
|
+
return `import { useState, useEffect } from "react";
|
|
20
|
+
import { invoke } from "~/server/invoke";
|
|
21
|
+
import type { OrderForm, OrderFormItem } from "@decocms/apps/vtex/types";
|
|
22
|
+
|
|
23
|
+
export type { OrderForm, OrderFormItem };
|
|
24
|
+
|
|
25
|
+
let _orderForm: OrderForm | null = null;
|
|
26
|
+
let _loading = false;
|
|
27
|
+
let _initStarted = false;
|
|
28
|
+
let _initFailed = false;
|
|
29
|
+
const _listeners = new Set<() => void>();
|
|
30
|
+
|
|
31
|
+
function notify() {
|
|
32
|
+
_listeners.forEach((fn) => fn());
|
|
33
|
+
}
|
|
34
|
+
function setOrderForm(of: OrderForm | null) {
|
|
35
|
+
_orderForm = of;
|
|
36
|
+
notify();
|
|
37
|
+
}
|
|
38
|
+
function setLoading(v: boolean) {
|
|
39
|
+
_loading = v;
|
|
40
|
+
notify();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getOrderFormIdFromCookie(): string | null {
|
|
44
|
+
if (typeof document === "undefined") return null;
|
|
45
|
+
const match = document.cookie.match(/checkout\\.vtex\\.com__orderFormId=([^;]*)/);
|
|
46
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function setOrderFormIdCookie(id: string) {
|
|
50
|
+
if (typeof document === "undefined") return;
|
|
51
|
+
document.cookie = \`checkout.vtex.com__orderFormId=\${encodeURIComponent(id)}; path=/; max-age=\${7 * 24 * 3600}; SameSite=Lax\`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function ensureOrderForm(): Promise<string> {
|
|
55
|
+
if (_orderForm?.orderFormId) return _orderForm.orderFormId;
|
|
56
|
+
|
|
57
|
+
const existing = getOrderFormIdFromCookie();
|
|
58
|
+
const of = await invoke.vtex.actions.getOrCreateCart({
|
|
59
|
+
data: { orderFormId: existing || undefined },
|
|
60
|
+
});
|
|
61
|
+
setOrderForm(of);
|
|
62
|
+
if (of?.orderFormId) setOrderFormIdCookie(of.orderFormId);
|
|
63
|
+
return of.orderFormId;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function itemToAnalyticsItem(item: OrderFormItem & { coupon?: string }, index: number) {
|
|
67
|
+
return {
|
|
68
|
+
item_id: item.productId,
|
|
69
|
+
item_group_id: item.productId,
|
|
70
|
+
item_name: item.name ?? item.skuName ?? "",
|
|
71
|
+
item_variant: item.skuName,
|
|
72
|
+
item_brand: item.additionalInfo?.brandName ?? "",
|
|
73
|
+
price: (item.sellingPrice ?? item.price ?? 0) / 100,
|
|
74
|
+
discount: Number(((item.listPrice - item.sellingPrice) / 100).toFixed(2)),
|
|
75
|
+
quantity: item.quantity,
|
|
76
|
+
coupon: item.coupon,
|
|
77
|
+
affiliation: item.seller,
|
|
78
|
+
index,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function resetCart() {
|
|
83
|
+
_orderForm = null;
|
|
84
|
+
_loading = false;
|
|
85
|
+
_initStarted = false;
|
|
86
|
+
_initFailed = false;
|
|
87
|
+
notify();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function useCart() {
|
|
91
|
+
const [, forceRender] = useState(0);
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
const listener = () => forceRender((n) => n + 1);
|
|
95
|
+
_listeners.add(listener);
|
|
96
|
+
|
|
97
|
+
if (!_orderForm && !_initStarted) {
|
|
98
|
+
_initStarted = true;
|
|
99
|
+
const ofId = getOrderFormIdFromCookie();
|
|
100
|
+
setLoading(true);
|
|
101
|
+
invoke.vtex.actions
|
|
102
|
+
.getOrCreateCart({ data: { orderFormId: ofId || undefined } })
|
|
103
|
+
.then((of: OrderForm) => {
|
|
104
|
+
setOrderForm(of);
|
|
105
|
+
if (of?.orderFormId) setOrderFormIdCookie(of.orderFormId);
|
|
106
|
+
})
|
|
107
|
+
.catch((err: unknown) => {
|
|
108
|
+
console.error("[useCart] init failed:", err);
|
|
109
|
+
if (!_orderForm) {
|
|
110
|
+
_initFailed = true;
|
|
111
|
+
notify();
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
.finally(() => setLoading(false));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return () => {
|
|
118
|
+
_listeners.delete(listener);
|
|
119
|
+
};
|
|
120
|
+
}, []);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
cart: {
|
|
124
|
+
get value() { return _orderForm; },
|
|
125
|
+
set value(v: OrderForm | null) { setOrderForm(v); },
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
loading: {
|
|
129
|
+
get value() { return _loading; },
|
|
130
|
+
set value(v: boolean) { setLoading(v); },
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
initFailed: {
|
|
134
|
+
get value() { return _initFailed; },
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
addItem: async (params: { id: string; seller: string; quantity?: number }) => {
|
|
138
|
+
setLoading(true);
|
|
139
|
+
try {
|
|
140
|
+
const ofId = await ensureOrderForm();
|
|
141
|
+
const updated = await invoke.vtex.actions.addItemsToCart({
|
|
142
|
+
data: {
|
|
143
|
+
orderFormId: ofId,
|
|
144
|
+
orderItems: [{ id: params.id, seller: params.seller, quantity: params.quantity ?? 1 }],
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
setOrderForm(updated);
|
|
148
|
+
if (updated?.orderFormId) setOrderFormIdCookie(updated.orderFormId);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
console.error("[useCart] addItem failed:", err);
|
|
151
|
+
throw err;
|
|
152
|
+
} finally {
|
|
153
|
+
setLoading(false);
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
addItems: async (params: {
|
|
158
|
+
orderItems: Array<{ id: string; seller: string; quantity: number }>;
|
|
159
|
+
}) => {
|
|
160
|
+
setLoading(true);
|
|
161
|
+
try {
|
|
162
|
+
const ofId = await ensureOrderForm();
|
|
163
|
+
const updated = await invoke.vtex.actions.addItemsToCart({
|
|
164
|
+
data: { orderFormId: ofId, orderItems: params.orderItems },
|
|
165
|
+
});
|
|
166
|
+
setOrderForm(updated);
|
|
167
|
+
if (updated?.orderFormId) setOrderFormIdCookie(updated.orderFormId);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.error("[useCart] addItems failed:", err);
|
|
170
|
+
throw err;
|
|
171
|
+
} finally {
|
|
172
|
+
setLoading(false);
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
updateItems: async (params: { orderItems: Array<{ index: number; quantity: number }> }) => {
|
|
177
|
+
const ofId = _orderForm?.orderFormId || getOrderFormIdFromCookie();
|
|
178
|
+
if (!ofId) return;
|
|
179
|
+
setLoading(true);
|
|
180
|
+
try {
|
|
181
|
+
const updated = await invoke.vtex.actions.updateCartItems({
|
|
182
|
+
data: { orderFormId: ofId, orderItems: params.orderItems },
|
|
183
|
+
});
|
|
184
|
+
setOrderForm(updated);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.error("[useCart] updateItems failed:", err);
|
|
187
|
+
} finally {
|
|
188
|
+
setLoading(false);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
removeItem: async (index: number) => {
|
|
193
|
+
const ofId = _orderForm?.orderFormId || getOrderFormIdFromCookie();
|
|
194
|
+
if (!ofId) return;
|
|
195
|
+
setLoading(true);
|
|
196
|
+
try {
|
|
197
|
+
const updated = await invoke.vtex.actions.updateCartItems({
|
|
198
|
+
data: { orderFormId: ofId, orderItems: [{ index, quantity: 0 }] },
|
|
199
|
+
});
|
|
200
|
+
setOrderForm(updated);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
console.error("[useCart] removeItem failed:", err);
|
|
203
|
+
} finally {
|
|
204
|
+
setLoading(false);
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
addCouponsToCart: async ({ text }: { text: string }) => {
|
|
209
|
+
const ofId = _orderForm?.orderFormId || getOrderFormIdFromCookie();
|
|
210
|
+
if (!ofId) return;
|
|
211
|
+
setLoading(true);
|
|
212
|
+
try {
|
|
213
|
+
const updated = await invoke.vtex.actions.addCouponToCart({
|
|
214
|
+
data: { orderFormId: ofId, text },
|
|
215
|
+
});
|
|
216
|
+
setOrderForm(updated);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
console.error("[useCart] addCoupon failed:", err);
|
|
219
|
+
} finally {
|
|
220
|
+
setLoading(false);
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
sendAttachment: async (params: { attachment: string; body: Record<string, unknown> }) => {
|
|
225
|
+
const ofId = _orderForm?.orderFormId || getOrderFormIdFromCookie();
|
|
226
|
+
if (!ofId) return;
|
|
227
|
+
setLoading(true);
|
|
228
|
+
try {
|
|
229
|
+
const updated = await invoke.vtex.actions.updateOrderFormAttachment({
|
|
230
|
+
data: {
|
|
231
|
+
orderFormId: ofId,
|
|
232
|
+
attachment: params.attachment,
|
|
233
|
+
body: params.body,
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
setOrderForm(updated);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
console.error("[useCart] sendAttachment failed:", err);
|
|
239
|
+
} finally {
|
|
240
|
+
setLoading(false);
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
simulate: async (data: {
|
|
245
|
+
items: Array<{ id: string; quantity: number; seller: string }>;
|
|
246
|
+
postalCode: string;
|
|
247
|
+
country: string;
|
|
248
|
+
}) => {
|
|
249
|
+
return await invoke.vtex.actions.simulateCart({
|
|
250
|
+
data: {
|
|
251
|
+
items: data.items.map((i) => ({
|
|
252
|
+
id: i.id,
|
|
253
|
+
quantity: i.quantity,
|
|
254
|
+
seller: i.seller,
|
|
255
|
+
})),
|
|
256
|
+
postalCode: data.postalCode,
|
|
257
|
+
country: data.country,
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
mapItemsToAnalyticsItems: (orderForm: OrderForm | null) => {
|
|
263
|
+
return (orderForm?.items || []).map((item, index) => itemToAnalyticsItem(item, index));
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function generateGenericUseCart(): string {
|
|
271
|
+
return `/**
|
|
272
|
+
* Cart Hook stub — implement for your platform.
|
|
273
|
+
*
|
|
274
|
+
* Wire invoke calls from ~/server/invoke to your commerce platform's
|
|
275
|
+
* cart API (addItem, updateItem, getCart, etc.).
|
|
276
|
+
*/
|
|
277
|
+
import { signal } from "~/sdk/signal";
|
|
278
|
+
|
|
279
|
+
const cart = signal<any>(null);
|
|
280
|
+
const loading = signal(false);
|
|
281
|
+
|
|
282
|
+
export function useCart() {
|
|
283
|
+
return {
|
|
284
|
+
cart,
|
|
285
|
+
loading,
|
|
286
|
+
async getCart() {
|
|
287
|
+
// TODO: Implement for your platform
|
|
288
|
+
return null;
|
|
289
|
+
},
|
|
290
|
+
async addItems(_items: any[]) {
|
|
291
|
+
// TODO: Implement
|
|
292
|
+
},
|
|
293
|
+
async updateItems(_items: any[]) {
|
|
294
|
+
// TODO: Implement
|
|
295
|
+
},
|
|
296
|
+
setCart(newCart: any) {
|
|
297
|
+
cart.value = newCart;
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export default useCart;
|
|
303
|
+
`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function generateUseUser(): string {
|
|
307
|
+
return `/**
|
|
308
|
+
* User Hook — wire to invoke.site.loaders for your platform's user API.
|
|
309
|
+
*/
|
|
310
|
+
import { signal } from "~/sdk/signal";
|
|
311
|
+
|
|
312
|
+
export interface User {
|
|
313
|
+
"@id": string;
|
|
314
|
+
email: string;
|
|
315
|
+
givenName?: string;
|
|
316
|
+
familyName?: string;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const user = signal<User | null>(null);
|
|
320
|
+
const loading = signal(false);
|
|
321
|
+
|
|
322
|
+
export function useUser() {
|
|
323
|
+
return { user, loading };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export default useUser;
|
|
327
|
+
`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function generateUseWishlist(): string {
|
|
331
|
+
return `/**
|
|
332
|
+
* Wishlist Hook — wire to invoke.site.loaders/actions for your platform.
|
|
333
|
+
*
|
|
334
|
+
* For VTEX: use invoke.site.loaders.getWishlistItems and
|
|
335
|
+
* invoke.site.actions.addWishlistItem / removeWishlistItem.
|
|
336
|
+
*/
|
|
337
|
+
import { signal } from "~/sdk/signal";
|
|
338
|
+
|
|
339
|
+
const loading = signal(false);
|
|
340
|
+
|
|
341
|
+
export function useWishlist() {
|
|
342
|
+
return {
|
|
343
|
+
loading,
|
|
344
|
+
async addItem(_productId: string, _productGroupId: string) {
|
|
345
|
+
// TODO: Implement
|
|
346
|
+
},
|
|
347
|
+
async removeItem(_productId: string) {
|
|
348
|
+
// TODO: Implement
|
|
349
|
+
},
|
|
350
|
+
getItem(_productId: string): boolean {
|
|
351
|
+
return false;
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export default useWishlist;
|
|
357
|
+
`;
|
|
358
|
+
}
|
|
@@ -31,8 +31,14 @@ function extractNpmDeps(importMap: Record<string, string>): Record<string, strin
|
|
|
31
31
|
if (key.startsWith("@deco/")) continue;
|
|
32
32
|
if (key === "daisyui") continue;
|
|
33
33
|
if (key === "preact-render-to-string") continue;
|
|
34
|
-
if (key === "simple-git") continue;
|
|
35
|
-
if (key === "fast-json-patch") 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
|
+
if (key.startsWith("@biomejs/")) continue;
|
|
39
|
+
if (key === "partytown") continue;
|
|
40
|
+
// Consolidate firebase/* split imports into single "firebase" package
|
|
41
|
+
if (key.startsWith("firebase/")) continue;
|
|
36
42
|
|
|
37
43
|
const raw = value.slice(4); // remove "npm:"
|
|
38
44
|
const atIdx = raw.lastIndexOf("@");
|
|
@@ -53,11 +59,23 @@ function extractNpmDeps(importMap: Record<string, string>): Record<string, strin
|
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
export function generatePackageJson(ctx: MigrationContext): string {
|
|
56
|
-
const
|
|
62
|
+
const extractedDeps = {
|
|
57
63
|
...extractNpmDeps(ctx.importMap),
|
|
58
64
|
...ctx.discoveredNpmDeps,
|
|
59
65
|
};
|
|
60
66
|
|
|
67
|
+
// Consolidate firebase/* split imports into single package
|
|
68
|
+
const hasFirebase = Object.keys(ctx.importMap).some((k) => k.startsWith("firebase"));
|
|
69
|
+
if (hasFirebase && !extractedDeps["firebase"]) {
|
|
70
|
+
extractedDeps["firebase"] = "^12.10.0";
|
|
71
|
+
}
|
|
72
|
+
// Remove wildcard versions
|
|
73
|
+
for (const [k, v] of Object.entries(extractedDeps)) {
|
|
74
|
+
if (v === "^*" || v === "*") extractedDeps[k] = "latest";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const siteDeps = extractedDeps;
|
|
78
|
+
|
|
61
79
|
// Fetch latest versions from npm registry
|
|
62
80
|
const startVersion = getLatestVersion("@decocms/start", "0.34.0");
|
|
63
81
|
const appsVersion = getLatestVersion("@decocms/apps", "0.27.0");
|
|
@@ -75,8 +93,13 @@ export function generatePackageJson(ctx: MigrationContext): string {
|
|
|
75
93
|
"tsx node_modules/@decocms/start/scripts/generate-blocks.ts",
|
|
76
94
|
"generate:routes": "tsr generate",
|
|
77
95
|
"generate:schema": `tsx node_modules/@decocms/start/scripts/generate-schema.ts --site ${ctx.siteName}`,
|
|
96
|
+
"generate:invoke":
|
|
97
|
+
"tsx node_modules/@decocms/start/scripts/generate-invoke.ts",
|
|
98
|
+
"generate:sections":
|
|
99
|
+
"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`,
|
|
78
101
|
build:
|
|
79
|
-
"npm run generate:blocks && npm run generate:schema && tsr generate && vite build",
|
|
102
|
+
"npm run generate:blocks && npm run generate:schema && npm run generate:sections && npm run generate:loaders && tsr generate && vite build",
|
|
80
103
|
preview: "vite preview",
|
|
81
104
|
deploy: "npm run build && wrangler deploy",
|
|
82
105
|
types: "wrangler types",
|
|
@@ -87,9 +110,9 @@ export function generatePackageJson(ctx: MigrationContext): string {
|
|
|
87
110
|
clean:
|
|
88
111
|
"rm -rf node_modules .cache dist .wrangler/state node_modules/.vite && npm install",
|
|
89
112
|
"tailwind:lint":
|
|
90
|
-
"tsx
|
|
113
|
+
"tsx scripts/tailwind-lint.ts",
|
|
91
114
|
"tailwind:fix":
|
|
92
|
-
"tsx
|
|
115
|
+
"tsx scripts/tailwind-lint.ts --fix",
|
|
93
116
|
},
|
|
94
117
|
author: "deco.cx",
|
|
95
118
|
license: "MIT",
|