@decocms/start 2.8.0 → 2.9.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "2.8.0",
3
+ "version": "2.9.0",
4
4
  "type": "module",
5
5
  "description": "Deco framework for TanStack Start - CMS bridge, admin protocol, hooks, schema generation",
6
6
  "main": "./src/index.ts",
@@ -0,0 +1,85 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { generateHooks } from "./hooks";
3
+ import type { MigrationContext } from "../types";
4
+
5
+ function makeCtx(platform: MigrationContext["platform"]): MigrationContext {
6
+ return {
7
+ sourceDir: "/tmp",
8
+ siteName: "test",
9
+ platform,
10
+ vtexAccount: null,
11
+ gtmId: null,
12
+ importMap: {},
13
+ discoveredNpmDeps: {},
14
+ themeColors: {},
15
+ fontFamily: null,
16
+ files: [],
17
+ sectionMetas: [],
18
+ islandClassifications: [],
19
+ islandWrapperTargets: new Map(),
20
+ loaderInventory: [],
21
+ scaffoldedFiles: [],
22
+ transformedFiles: [],
23
+ deletedFiles: [],
24
+ movedFiles: [],
25
+ manualReviewItems: [],
26
+ frameworkFindings: [],
27
+ dryRun: false,
28
+ verbose: false,
29
+ };
30
+ }
31
+
32
+ describe("generateHooks (vtex)", () => {
33
+ const files = generateHooks(makeCtx("vtex"));
34
+
35
+ it("emits all three hook files", () => {
36
+ expect(Object.keys(files).sort()).toEqual([
37
+ "src/hooks/useCart.ts",
38
+ "src/hooks/useUser.ts",
39
+ "src/hooks/useWishlist.ts",
40
+ ]);
41
+ });
42
+
43
+ it("useCart is the createUseCart factory shim, not 250-line legacy boilerplate", () => {
44
+ const code = files["src/hooks/useCart.ts"];
45
+ // Imports from the framework factory.
46
+ expect(code).toContain(
47
+ 'import { createUseCart } from "@decocms/apps/vtex/hooks/createUseCart"',
48
+ );
49
+ expect(code).toContain('import { invoke } from "~/server/invoke"');
50
+ // Re-exports types from @decocms/apps directly.
51
+ expect(code).toContain(
52
+ 'export type { OrderForm, OrderFormItem } from "@decocms/apps/vtex/types"',
53
+ );
54
+ // Calls the factory with invoke and destructures the public API.
55
+ expect(code).toContain(
56
+ "export const { useCart, resetCart, itemToAnalyticsItem } = createUseCart",
57
+ );
58
+ // And does NOT contain the old singleton machinery.
59
+ expect(code).not.toContain("const _listeners = new Set");
60
+ expect(code).not.toContain("forceRender");
61
+ expect(code).not.toContain("function getOrderFormIdFromCookie");
62
+ });
63
+
64
+ it("useCart shim is dramatically smaller than the legacy template", () => {
65
+ const lineCount = files["src/hooks/useCart.ts"].split("\n").length;
66
+ // Should be well under 20 lines (factory call + re-export + imports).
67
+ expect(lineCount).toBeLessThan(20);
68
+ });
69
+ });
70
+
71
+ describe("generateHooks (non-vtex)", () => {
72
+ it("custom platform falls back to the generic stub", () => {
73
+ const files = generateHooks(makeCtx("custom"));
74
+ const code = files["src/hooks/useCart.ts"];
75
+ expect(code).toContain("Cart Hook stub");
76
+ expect(code).toContain("TODO: Implement");
77
+ });
78
+
79
+ it("shopify currently shares the generic stub", () => {
80
+ const files = generateHooks(makeCtx("shopify"));
81
+ const code = files["src/hooks/useCart.ts"];
82
+ // Until a shopify factory exists, non-vtex platforms get the generic stub.
83
+ expect(code).toContain("Cart Hook stub");
84
+ });
85
+ });
@@ -16,254 +16,17 @@ export function generateHooks(ctx: MigrationContext): Record<string, string> {
16
16
  }
17
17
 
18
18
  function generateVtexUseCart(): string {
19
- return `import { useState, useEffect } from "react";
19
+ // The legacy invoke-based useCart hook is now a 5-line factory call —
20
+ // the heavy lifting (singleton state, listener pattern, async actions,
21
+ // analytics helpers) lives in @decocms/apps/vtex/hooks/createUseCart.
22
+ return `import { createUseCart } from "@decocms/apps/vtex/hooks/createUseCart";
20
23
  import { invoke } from "~/server/invoke";
21
- import type { OrderForm, OrderFormItem } from "@decocms/apps/vtex/types";
22
24
 
23
- export type { OrderForm, OrderFormItem };
25
+ export type { OrderForm, OrderFormItem } from "@decocms/apps/vtex/types";
24
26
 
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
- }
27
+ export const { useCart, resetCart, itemToAnalyticsItem } = createUseCart({
28
+ invoke,
29
+ });
267
30
  `;
268
31
  }
269
32