@decocms/apps-vtex 7.60.0 → 7.62.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/apps-vtex",
3
- "version": "7.60.0",
3
+ "version": "7.62.0",
4
4
  "type": "module",
5
5
  "description": "Deco commerce app: VTEX integration",
6
6
  "repository": {
@@ -51,10 +51,10 @@
51
51
  "lint:unused": "knip"
52
52
  },
53
53
  "dependencies": {
54
- "@decocms/blocks": "7.60.0",
55
- "@decocms/apps-commerce": "7.60.0",
56
- "@decocms/apps-website": "7.60.0",
57
- "@decocms/tanstack": "7.60.0"
54
+ "@decocms/blocks": "7.62.0",
55
+ "@decocms/apps-commerce": "7.62.0",
56
+ "@decocms/apps-website": "7.62.0",
57
+ "@decocms/tanstack": "7.62.0"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "@tanstack/react-query": ">=5.0.0",
@@ -0,0 +1,157 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ // `legacyProductList` is the SHELF loader. It accepted no payload-shaping
4
+ // options, so every shelf paid the full product for every entry — measured on a
5
+ // real home shelf (`count: 28`): 1095 KB, 39.1 KB per product, of which
6
+ // `isVariantOf` 19.1 KB and the 48-rung `offers.priceSpecification` 12.8 KB.
7
+ // `legacyProductListingPage` already took them; this file pins the pass-through,
8
+ // because declaring the options WITHOUT forwarding them is exactly the bug the
9
+ // PLP loader had.
10
+
11
+ vi.mock("../../client", () => ({
12
+ getVtexConfig: () => ({ account: "test", salesChannel: "1" }),
13
+ vtexFetch: vi.fn(),
14
+ vtexFetchResponse: vi.fn(),
15
+ }));
16
+
17
+ import { vtexFetch } from "../../client";
18
+ import { legacyProductList } from "../legacy";
19
+
20
+ const sellers = (price: number) => [
21
+ {
22
+ sellerId: "1",
23
+ sellerName: "Seller One",
24
+ sellerDefault: true,
25
+ commertialOffer: {
26
+ AvailableQuantity: 5,
27
+ Price: price,
28
+ ListPrice: price + 30,
29
+ PriceWithoutDiscount: price + 30,
30
+ spotPrice: price,
31
+ PriceValidUntil: "2025-12-31",
32
+ // A real ladder: every method the store accepts, every installment count.
33
+ Installments: ["Visa", "Master", "Amex", "Boleto"].flatMap((method) =>
34
+ Array.from({ length: 4 }, (_, i) => ({
35
+ Value: price / (i + 1),
36
+ NumberOfInstallments: i + 1,
37
+ Name: `${method} ${i + 1}x`,
38
+ InterestRate: 0,
39
+ TotalValuePlusInterestRate: price,
40
+ PaymentSystemName: method,
41
+ })),
42
+ ),
43
+ GiftSkuIds: [],
44
+ teasers: [],
45
+ },
46
+ },
47
+ ];
48
+
49
+ const sku = (itemId: string, price: number) => ({
50
+ itemId,
51
+ name: `SKU ${itemId}`,
52
+ nameComplete: `SKU ${itemId}`,
53
+ complementName: "",
54
+ ean: "1234567890123",
55
+ referenceId: [{ Key: "RefId", Value: `REF-${itemId}` }],
56
+ images: Array.from({ length: 4 }, (_, i) => ({
57
+ imageId: `${itemId}-${i}`,
58
+ imageUrl: `https://img.com/${itemId}-${i}.jpg`,
59
+ imageText: `img${i}`,
60
+ imageLabel: `label${i}`,
61
+ })),
62
+ sellers: sellers(price),
63
+ Videos: [],
64
+ estimatedDateArrival: null,
65
+ measurementUnit: "un",
66
+ unitMultiplier: 1,
67
+ variations: [],
68
+ attachments: [],
69
+ isKit: false,
70
+ });
71
+
72
+ const legacyProduct = () => ({
73
+ productId: "PROD1",
74
+ productName: "Test Product",
75
+ brand: "TestBrand",
76
+ brandId: 1,
77
+ brandImageUrl: null,
78
+ linkText: "test-product",
79
+ productReference: "REF1",
80
+ categoryId: "1",
81
+ productTitle: "Test Product",
82
+ metaTagDescription: "meta",
83
+ clusterHighlights: {},
84
+ productClusters: {},
85
+ searchableClusters: {},
86
+ categories: ["/Electronics/"],
87
+ categoriesIds: ["/1/"],
88
+ link: "https://test/test-product/p",
89
+ description: "x".repeat(2000),
90
+ items: [sku("SKU1", 90), sku("SKU2", 60), sku("SKU3", 120)],
91
+ allSpecifications: [],
92
+ allSpecificationsGroups: [],
93
+ skuSpecifications: [],
94
+ releaseDate: "2024-01-01",
95
+ });
96
+
97
+ const opts = { query: { count: 1 } as any, baseUrl: "https://example.com" };
98
+ const ladderOf = (p: any) => p?.offers?.offers?.[0]?.priceSpecification ?? [];
99
+
100
+ describe("legacyProductList — payload-shaping options reach toProduct", () => {
101
+ beforeEach(() => {
102
+ (vtexFetch as any).mockReset();
103
+ (vtexFetch as any).mockResolvedValue([legacyProduct()]);
104
+ });
105
+
106
+ it("no options: full product, full ladder, every variant, every image", async () => {
107
+ const [p] = (await legacyProductList(opts)) as any[];
108
+ expect(ladderOf(p).length).toBeGreaterThan(10);
109
+ expect(p.isVariantOf.hasVariant).toHaveLength(3);
110
+ expect(ladderOf(p.isVariantOf.hasVariant[0]).length).toBeGreaterThan(10);
111
+ expect(p.image).toHaveLength(4);
112
+ expect(p.description).toBeTruthy();
113
+ });
114
+
115
+ it("leanVariants empties the ladder on every variant", async () => {
116
+ const [p] = (await legacyProductList({ ...opts, leanVariants: true })) as any[];
117
+ for (const v of p.isVariantOf.hasVariant) expect(ladderOf(v)).toEqual([]);
118
+ });
119
+
120
+ it("displayedVariantId keeps the ladder on the one variant the card renders", async () => {
121
+ const [p] = (await legacyProductList({
122
+ ...opts,
123
+ leanVariants: true,
124
+ displayedVariantId: (items: any[]) => items[1].itemId,
125
+ })) as any[];
126
+ const kept = p.isVariantOf.hasVariant.find((v: any) => v.sku === "SKU2");
127
+ expect(ladderOf(kept).length).toBeGreaterThan(0);
128
+ // ...and it is the LEAN shape, not a second full product.
129
+ expect(kept.description).toBeUndefined();
130
+ });
131
+
132
+ it("priceSpecifications rewrites the root ladder with the caller's rule", async () => {
133
+ const [p] = (await legacyProductList({
134
+ ...opts,
135
+ priceSpecifications: (specs) => specs.filter((s) => !s.priceComponentType),
136
+ })) as any[];
137
+ expect(ladderOf(p).every((s: any) => !s.priceComponentType)).toBe(true);
138
+ expect(ladderOf(p).length).toBeLessThan(10);
139
+ });
140
+
141
+ it("maxImages caps image[] by position", async () => {
142
+ const [p] = (await legacyProductList({ ...opts, maxImages: 2 })) as any[];
143
+ expect(p.image).toHaveLength(2);
144
+ });
145
+
146
+ it("every option absent is byte-for-byte the previous output", async () => {
147
+ const [before] = (await legacyProductList(opts)) as any[];
148
+ const [after] = (await legacyProductList({
149
+ ...opts,
150
+ leanVariants: undefined,
151
+ displayedVariantId: undefined,
152
+ maxImages: undefined,
153
+ priceSpecifications: undefined,
154
+ })) as any[];
155
+ expect(JSON.stringify(after)).toBe(JSON.stringify(before));
156
+ });
157
+ });
@@ -159,6 +159,23 @@ export interface LegacyProductListOptions {
159
159
  query: LegacyProductListQuery;
160
160
  baseUrl: string;
161
161
  priceCurrency?: string;
162
+ /**
163
+ * Payload-shaping options, forwarded verbatim to `toProduct`. Same set the
164
+ * PLP loader already accepts, and for the same reason: a shelf renders a
165
+ * CARD, but this loader built the full product for every entry. Measured on
166
+ * a real home shelf (`count: 28`), 1095 KB — 39.1 KB per product, of which
167
+ * `isVariantOf` 19.1 KB and the 48-rung `offers.priceSpecification` 12.8 KB.
168
+ *
169
+ * All default to undefined, which keeps the previous output byte for byte.
170
+ * See the matching fields on {@link ProductOptions}.
171
+ */
172
+ leanVariants?: ProductOptions["leanVariants"];
173
+ displayedVariantId?: ProductOptions["displayedVariantId"];
174
+ variantPropertyNames?: ProductOptions["variantPropertyNames"];
175
+ variantIncludeImage?: ProductOptions["variantIncludeImage"];
176
+ variantIncludeInventory?: ProductOptions["variantIncludeInventory"];
177
+ maxImages?: ProductOptions["maxImages"];
178
+ priceSpecifications?: ProductOptions["priceSpecifications"];
162
179
  }
163
180
 
164
181
  function isCollectionQuery(
@@ -234,7 +251,18 @@ function queryToSearchParams(
234
251
  * Ported from: vtex/loaders/legacy/productList.ts
235
252
  */
236
253
  export async function legacyProductList(opts: LegacyProductListOptions): Promise<Product[] | null> {
237
- const { query, baseUrl, priceCurrency = "BRL" } = opts;
254
+ const {
255
+ query,
256
+ baseUrl,
257
+ priceCurrency = "BRL",
258
+ leanVariants,
259
+ displayedVariantId,
260
+ variantPropertyNames,
261
+ variantIncludeImage,
262
+ variantIncludeInventory,
263
+ maxImages,
264
+ priceSpecifications,
265
+ } = opts;
238
266
  const searchArgs = queryToSearchParams(query);
239
267
  const qs = buildSearchParams(searchArgs);
240
268
 
@@ -255,7 +283,17 @@ export async function legacyProductList(opts: LegacyProductListOptions): Promise
255
283
  };
256
284
 
257
285
  let products = vtexProducts.map((p) =>
258
- toProduct(p, preferredSKU(p.items), 0, { baseUrl, priceCurrency }),
286
+ toProduct(p, preferredSKU(p.items), 0, {
287
+ baseUrl,
288
+ priceCurrency,
289
+ leanVariants,
290
+ displayedVariantId,
291
+ variantPropertyNames,
292
+ variantIncludeImage,
293
+ variantIncludeInventory,
294
+ maxImages,
295
+ priceSpecifications,
296
+ }),
259
297
  );
260
298
 
261
299
  if (isSkuIdsQuery(query)) {