@decocms/apps-vtex 8.1.0-next.0 → 8.1.0-next.1

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 (139) hide show
  1. package/package.json +7 -52
  2. package/src/index.ts +10 -28
  3. package/src/utils/constants.ts +2 -56
  4. package/src/utils/cookieSanitizer.ts +59 -59
  5. package/src/utils/operationRouter.ts +75 -65
  6. package/src/utils/types.ts +709 -1557
  7. package/src/vtexClient.ts +354 -343
  8. package/src/README.md +0 -6
  9. package/src/__conformance__/upstream.test.ts +0 -158
  10. package/src/__tests__/client-segment-cookie.test.ts +0 -255
  11. package/src/__tests__/client-set-cookie-forward.test.ts +0 -257
  12. package/src/__tests__/schemas.test.ts +0 -71
  13. package/src/actions/address.ts +0 -250
  14. package/src/actions/analytics/sendEvent.ts +0 -86
  15. package/src/actions/auth.ts +0 -333
  16. package/src/actions/checkout.ts +0 -659
  17. package/src/actions/index.ts +0 -11
  18. package/src/actions/masterData.ts +0 -168
  19. package/src/actions/misc.ts +0 -188
  20. package/src/actions/newsletter.ts +0 -105
  21. package/src/actions/orders.ts +0 -34
  22. package/src/actions/profile.ts +0 -201
  23. package/src/actions/session.ts +0 -85
  24. package/src/actions/trigger.ts +0 -43
  25. package/src/actions/wishlist.ts +0 -114
  26. package/src/client.ts +0 -724
  27. package/src/commerceLoaders.ts +0 -267
  28. package/src/hooks/__tests__/createCart.render.test.ts +0 -114
  29. package/src/hooks/__tests__/createCart.test.ts +0 -92
  30. package/src/hooks/__tests__/createUseCart.test.ts +0 -184
  31. package/src/hooks/__tests__/createUseUser.test.ts +0 -48
  32. package/src/hooks/__tests__/createUseWishlist.test.ts +0 -87
  33. package/src/hooks/cartQuery.ts +0 -204
  34. package/src/hooks/createCart.ts +0 -427
  35. package/src/hooks/createUseCart.ts +0 -360
  36. package/src/hooks/createUseUser.ts +0 -153
  37. package/src/hooks/createUseWishlist.ts +0 -242
  38. package/src/hooks/index.ts +0 -25
  39. package/src/hooks/useAutocomplete.ts +0 -83
  40. package/src/hooks/useCart.ts +0 -243
  41. package/src/hooks/useUser.ts +0 -78
  42. package/src/hooks/useWishlist.ts +0 -119
  43. package/src/invoke.ts +0 -235
  44. package/src/loaders/ProductDetailsPage.ts +0 -1
  45. package/src/loaders/ProductList.ts +0 -1
  46. package/src/loaders/ProductListingPage.ts +0 -1
  47. package/src/loaders/__tests__/legacyProductList.test.ts +0 -157
  48. package/src/loaders/address.ts +0 -115
  49. package/src/loaders/autocomplete.ts +0 -56
  50. package/src/loaders/brands.ts +0 -44
  51. package/src/loaders/cart/attachments.ts +0 -53
  52. package/src/loaders/cart/full.ts +0 -67
  53. package/src/loaders/cart/gifts.ts +0 -43
  54. package/src/loaders/cart/orderFormId.ts +0 -31
  55. package/src/loaders/cart/shipping.test.ts +0 -125
  56. package/src/loaders/cart/shipping.ts +0 -136
  57. package/src/loaders/cart/summary.ts +0 -36
  58. package/src/loaders/cart.ts +0 -52
  59. package/src/loaders/catalog.ts +0 -163
  60. package/src/loaders/collections.ts +0 -55
  61. package/src/loaders/index.ts +0 -20
  62. package/src/loaders/intelligentSearch/__tests__/productListingPage.test.ts +0 -51
  63. package/src/loaders/intelligentSearch/productDetailsPage.ts +0 -111
  64. package/src/loaders/intelligentSearch/productList.ts +0 -170
  65. package/src/loaders/intelligentSearch/productListingPage.ts +0 -525
  66. package/src/loaders/intelligentSearch/suggestions.ts +0 -44
  67. package/src/loaders/legacy/productDetailsPage.ts +0 -1
  68. package/src/loaders/legacy/productList.ts +0 -1
  69. package/src/loaders/legacy/relatedProductsLoader.ts +0 -79
  70. package/src/loaders/legacy.ts +0 -710
  71. package/src/loaders/logistics.ts +0 -111
  72. package/src/loaders/minicart.ts +0 -78
  73. package/src/loaders/navbar.ts +0 -26
  74. package/src/loaders/orders.ts +0 -92
  75. package/src/loaders/pageType.ts +0 -68
  76. package/src/loaders/paths/PDPDefaultPath.ts +0 -51
  77. package/src/loaders/payment.ts +0 -97
  78. package/src/loaders/productListFull.ts +0 -157
  79. package/src/loaders/profile.ts +0 -138
  80. package/src/loaders/promotion.ts +0 -30
  81. package/src/loaders/search.ts +0 -124
  82. package/src/loaders/session.ts +0 -83
  83. package/src/loaders/user.ts +0 -87
  84. package/src/loaders/wishlist.ts +0 -89
  85. package/src/loaders/wishlistProducts.ts +0 -69
  86. package/src/loaders/workflow/products.ts +0 -62
  87. package/src/loaders/workflow.ts +0 -316
  88. package/src/logo.png +0 -0
  89. package/src/manifest.gen.ts +0 -89
  90. package/src/middleware.cacheHeaders.test.ts +0 -44
  91. package/src/middleware.ts +0 -240
  92. package/src/mod.ts +0 -173
  93. package/src/registry.ts +0 -9
  94. package/src/schemas.gen.ts +0 -3191
  95. package/src/schemas.ts +0 -30
  96. package/src/types.ts +0 -248
  97. package/src/utils/__tests__/cartProjection.test.ts +0 -118
  98. package/src/utils/__tests__/cookieSanitizer.test.ts +0 -162
  99. package/src/utils/__tests__/fetch.test.ts +0 -80
  100. package/src/utils/__tests__/fetchCache.test.ts +0 -155
  101. package/src/utils/__tests__/instrumentedFetch.test.ts +0 -158
  102. package/src/utils/__tests__/intelligentSearch.test.ts +0 -88
  103. package/src/utils/__tests__/minicart.test.ts +0 -184
  104. package/src/utils/__tests__/operationRouter.test.ts +0 -227
  105. package/src/utils/__tests__/resilience.test.ts +0 -205
  106. package/src/utils/__tests__/resourceRange.test.ts +0 -32
  107. package/src/utils/__tests__/simulationCache.test.ts +0 -40
  108. package/src/utils/__tests__/sitemap.test.ts +0 -185
  109. package/src/utils/__tests__/slugify.test.ts +0 -31
  110. package/src/utils/__tests__/storefrontBaseUrl.test.ts +0 -61
  111. package/src/utils/__tests__/transform.test.ts +0 -1111
  112. package/src/utils/accountLoaders.ts +0 -203
  113. package/src/utils/authHelpers.ts +0 -85
  114. package/src/utils/batch.ts +0 -18
  115. package/src/utils/buildOfferShelf.test.ts +0 -59
  116. package/src/utils/cartProjection.ts +0 -96
  117. package/src/utils/cookies.ts +0 -167
  118. package/src/utils/enrichment.ts +0 -556
  119. package/src/utils/fetch.ts +0 -111
  120. package/src/utils/fetchCache.ts +0 -55
  121. package/src/utils/index.ts +0 -19
  122. package/src/utils/instrumentedFetch.ts +0 -98
  123. package/src/utils/intelligentSearch.ts +0 -96
  124. package/src/utils/legacy.ts +0 -139
  125. package/src/utils/minicart.ts +0 -139
  126. package/src/utils/pickAndOmit.ts +0 -25
  127. package/src/utils/proxy.ts +0 -495
  128. package/src/utils/resilience.ts +0 -352
  129. package/src/utils/resourceRange.ts +0 -10
  130. package/src/utils/segment.ts +0 -152
  131. package/src/utils/similars.ts +0 -35
  132. package/src/utils/simulationCache.ts +0 -73
  133. package/src/utils/sitemap.ts +0 -283
  134. package/src/utils/slugCache.ts +0 -28
  135. package/src/utils/slugify.ts +0 -13
  136. package/src/utils/transform.ts +0 -1691
  137. package/src/utils/vtexId.ts +0 -151
  138. package/src/vtexClient.test.ts +0 -204
  139. package/tsconfig.json +0 -7
@@ -1,710 +0,0 @@
1
- /**
2
- * Legacy VTEX Catalog Search loaders.
3
- * Pure async functions — require configureVtex() to have been called.
4
- *
5
- * Ported from deco-cx/apps:
6
- * vtex/loaders/legacy/productDetailsPage.ts
7
- * vtex/loaders/legacy/productList.ts
8
- * vtex/loaders/legacy/productListingPage.ts
9
- * vtex/loaders/legacy/suggestions.ts
10
- *
11
- * @see https://developers.vtex.com/docs/api-reference/search-api
12
- */
13
- import type {
14
- Filter,
15
- Product,
16
- ProductDetailsPage,
17
- ProductListingPage,
18
- Suggestion,
19
- } from "@decocms/apps-commerce/types";
20
- import { getVtexConfig, vtexFetch, vtexFetchResponse } from "../client";
21
- import {
22
- getMapAndTerm,
23
- getValidTypesFromPageTypes,
24
- pageTypesFromPathname,
25
- pageTypesToBreadcrumbList,
26
- pageTypesToSeo,
27
- } from "../utils/legacy";
28
- import {
29
- legacyFacetToFilter,
30
- parsePageType,
31
- pickSku,
32
- sortProducts,
33
- toProduct,
34
- toProductPage,
35
- } from "../utils/transform";
36
- import type { ProductOptions } from "../utils/transform";
37
- import type { LegacyFacet, LegacyItem, LegacyProduct, LegacySort, PageType } from "../utils/types";
38
-
39
- // ---------------------------------------------------------------------------
40
- // Shared helpers
41
- // ---------------------------------------------------------------------------
42
-
43
- const MAX_ALLOWED_PAGES = 500;
44
-
45
- function salesChannelParam(): string {
46
- return getVtexConfig().salesChannel ?? "1";
47
- }
48
-
49
- function buildSearchParams(
50
- extra: Record<string, string | string[] | number | undefined>,
51
- ): URLSearchParams {
52
- const params = new URLSearchParams();
53
- const sc = salesChannelParam();
54
- if (sc) params.set("sc", sc);
55
-
56
- for (const [key, val] of Object.entries(extra)) {
57
- if (val == null) continue;
58
- if (Array.isArray(val)) {
59
- for (const v of val) params.append(key, v);
60
- } else {
61
- params.set(key, String(val));
62
- }
63
- }
64
- return params;
65
- }
66
-
67
- // ---------------------------------------------------------------------------
68
- // legacyProductDetailsPage
69
- // ---------------------------------------------------------------------------
70
-
71
- export interface LegacyPDPOptions {
72
- slug: string;
73
- skuId?: string;
74
- baseUrl: string;
75
- priceCurrency?: string;
76
- includeOriginalAttributes?: string[];
77
- preferDescription?: boolean;
78
- /** When true, pages with ?skuId are still indexable */
79
- indexingSkus?: boolean;
80
- }
81
-
82
- /**
83
- * Fetch a product details page using the legacy Catalog search API.
84
- *
85
- * @see https://developers.vtex.com/docs/api-reference/search-api#get-/api/catalog_system/pub/products/search/-slug-/p
86
- * Ported from: vtex/loaders/legacy/productDetailsPage.ts
87
- */
88
- export async function legacyProductDetailsPage(
89
- opts: LegacyPDPOptions,
90
- ): Promise<ProductDetailsPage | null> {
91
- const {
92
- slug,
93
- skuId,
94
- baseUrl,
95
- priceCurrency = "BRL",
96
- includeOriginalAttributes,
97
- preferDescription,
98
- indexingSkus,
99
- } = opts;
100
-
101
- const lowercaseSlug = slug.toLowerCase();
102
- const qs = buildSearchParams({});
103
-
104
- const response = await vtexFetch<LegacyProduct[]>(
105
- `/api/catalog_system/pub/products/search/${lowercaseSlug}/p?${qs}`,
106
- );
107
-
108
- if (response && !Array.isArray(response)) {
109
- throw new Error(`Error while fetching VTEX data ${JSON.stringify(response)}`);
110
- }
111
-
112
- const [product] = response;
113
- if (!product) return null;
114
-
115
- const sku = pickSku(product, skuId);
116
-
117
- const kitItems: LegacyProduct[] =
118
- Array.isArray(sku.kitItems) && sku.kitItems.length > 0
119
- ? await vtexFetch<LegacyProduct[]>(
120
- `/api/catalog_system/pub/products/search/?${buildSearchParams({
121
- _from: 0,
122
- _to: 49,
123
- fq: sku.kitItems.map((item) => `skuId:${item.itemId}`),
124
- })}`,
125
- )
126
- : [];
127
-
128
- const page = toProductPage(product, sku, kitItems, {
129
- baseUrl,
130
- priceCurrency,
131
- includeOriginalAttributes,
132
- });
133
-
134
- const url = new URL(baseUrl);
135
-
136
- return {
137
- ...page,
138
- seo: {
139
- title: product.productTitle || product.productName,
140
- description: preferDescription ? product.description : product.metaTagDescription,
141
- canonical: new URL(`/${product.linkText}/p`, url.origin).href,
142
- noIndexing: indexingSkus ? false : !!skuId,
143
- },
144
- };
145
- }
146
-
147
- // ---------------------------------------------------------------------------
148
- // legacyProductList
149
- // ---------------------------------------------------------------------------
150
-
151
- export type LegacyProductListQuery =
152
- | { collection: string; count: number; sort?: LegacySort }
153
- | { term?: string; count: number; sort?: LegacySort }
154
- | { fq: string[]; count: number; sort?: LegacySort }
155
- | { skuIds: string[] }
156
- | { productIds: string[] };
157
-
158
- export interface LegacyProductListOptions {
159
- query: LegacyProductListQuery;
160
- baseUrl: string;
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"];
179
- }
180
-
181
- function isCollectionQuery(
182
- q: LegacyProductListQuery,
183
- ): q is { collection: string; count: number; sort?: LegacySort } {
184
- return "collection" in q && typeof (q as any).collection === "string";
185
- }
186
- function isSkuIdsQuery(q: LegacyProductListQuery): q is { skuIds: string[] } {
187
- return "skuIds" in q && Array.isArray((q as any).skuIds);
188
- }
189
- function isProductIdsQuery(q: LegacyProductListQuery): q is { productIds: string[] } {
190
- return "productIds" in q && Array.isArray((q as any).productIds);
191
- }
192
- function isFqQuery(
193
- q: LegacyProductListQuery,
194
- ): q is { fq: string[]; count: number; sort?: LegacySort } {
195
- return "fq" in q && Array.isArray((q as any).fq);
196
- }
197
- function isTermQuery(
198
- q: LegacyProductListQuery,
199
- ): q is { term?: string; count: number; sort?: LegacySort } {
200
- return "term" in q || ("count" in q && !isCollectionQuery(q as any) && !isFqQuery(q as any));
201
- }
202
-
203
- function queryToSearchParams(
204
- query: LegacyProductListQuery,
205
- ): Record<string, string | string[] | number | undefined> {
206
- if (isSkuIdsQuery(query)) {
207
- return {
208
- fq: query.skuIds.map((id) => `skuId:${id}`),
209
- _from: 0,
210
- _to: Math.max(query.skuIds.length - 1, 0),
211
- };
212
- }
213
-
214
- if (isProductIdsQuery(query)) {
215
- return {
216
- fq: query.productIds.map((id) => `productId:${id}`),
217
- _from: 0,
218
- _to: Math.max(query.productIds.length - 1, 0),
219
- };
220
- }
221
-
222
- const count = "count" in query ? (query.count ?? 12) : 12;
223
- const sort = "sort" in query ? query.sort : undefined;
224
- const base: Record<string, string | string[] | number | undefined> = {
225
- _from: 0,
226
- _to: Math.max(count - 1, 0),
227
- O: sort,
228
- };
229
-
230
- if (isCollectionQuery(query)) {
231
- base.fq = [`productClusterIds:${query.collection}`];
232
- return base;
233
- }
234
-
235
- if (isFqQuery(query)) {
236
- base.fq = query.fq;
237
- return base;
238
- }
239
-
240
- if (isTermQuery(query) && query.term) {
241
- base.ft = encodeURIComponent(query.term);
242
- }
243
-
244
- return base;
245
- }
246
-
247
- /**
248
- * Fetch a product list using the legacy Catalog search API.
249
- *
250
- * @see https://developers.vtex.com/docs/api-reference/search-api#get-/api/catalog_system/pub/products/search
251
- * Ported from: vtex/loaders/legacy/productList.ts
252
- */
253
- export async function legacyProductList(opts: LegacyProductListOptions): Promise<Product[] | null> {
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;
266
- const searchArgs = queryToSearchParams(query);
267
- const qs = buildSearchParams(searchArgs);
268
-
269
- const vtexProducts = await vtexFetch<LegacyProduct[]>(
270
- `/api/catalog_system/pub/products/search/?${qs}`,
271
- );
272
-
273
- if (vtexProducts && !Array.isArray(vtexProducts)) {
274
- throw new Error(`Error while fetching VTEX data ${JSON.stringify(vtexProducts)}`);
275
- }
276
-
277
- const preferredSKU = (items: LegacyItem[]): LegacyItem => {
278
- if (isSkuIdsQuery(query)) {
279
- const fetchedSkus = new Set(query.skuIds);
280
- return items.find((item) => fetchedSkus.has(item.itemId)) || items[0];
281
- }
282
- return items[0];
283
- };
284
-
285
- let products = vtexProducts.map((p) =>
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
- }),
297
- );
298
-
299
- if (isSkuIdsQuery(query)) {
300
- products = sortProducts(products, query.skuIds, "sku");
301
- }
302
- if (isProductIdsQuery(query)) {
303
- products = sortProducts(products, query.productIds, "inProductGroupWithID");
304
- }
305
-
306
- return products;
307
- }
308
-
309
- // ---------------------------------------------------------------------------
310
- // legacyProductListingPage
311
- // ---------------------------------------------------------------------------
312
-
313
- export const LEGACY_SORT_OPTIONS = [
314
- { label: "price:desc", value: "OrderByPriceDESC" },
315
- { label: "price:asc", value: "OrderByPriceASC" },
316
- { label: "orders:desc", value: "OrderByTopSaleDESC" },
317
- { label: "name:desc", value: "OrderByNameDESC" },
318
- { label: "name:asc", value: "OrderByNameASC" },
319
- { label: "release:desc", value: "OrderByReleaseDateDESC" },
320
- { label: "discount:desc", value: "OrderByBestDiscountDESC" },
321
- { label: "relevance:desc", value: "OrderByScoreDESC" },
322
- ] as const;
323
-
324
- const IS_TO_LEGACY: Record<string, LegacySort> = {
325
- "price:desc": "OrderByPriceDESC",
326
- "price:asc": "OrderByPriceASC",
327
- "orders:desc": "OrderByTopSaleDESC",
328
- "name:desc": "OrderByNameDESC",
329
- "name:asc": "OrderByNameASC",
330
- "release:desc": "OrderByReleaseDateDESC",
331
- "discount:desc": "OrderByBestDiscountDESC",
332
- "relevance:desc": "OrderByScoreDESC",
333
- };
334
-
335
- const formatPriceFromPathToFacet = (term: string) =>
336
- term.replace(/de-\d+[,]?[\d]+-a-\d+[,]?[\d]+/, (match) => match.replaceAll(",", "."));
337
-
338
- const removeForwardSlash = (str: string) => str.slice(str.startsWith("/") ? 1 : 0);
339
-
340
- const getTerm = (path: string, map: string) => {
341
- const mapSegments = map.split(",");
342
- const pathSegments = removeForwardSlash(path).split("/");
343
- const term = pathSegments.slice(0, mapSegments.length).join("/");
344
- return mapSegments.includes("priceFrom") ? formatPriceFromPathToFacet(term) : term;
345
- };
346
-
347
- export const getFirstItemAvailable = (item: LegacyItem) =>
348
- !!item?.sellers?.find((s) => s.commertialOffer?.AvailableQuantity > 0);
349
-
350
- const getTermFallback = (url: URL, isPage: boolean, hasFilters: boolean) => {
351
- const pathList = url.pathname.split("/").slice(1);
352
- if (!isPage && !hasFilters && pathList.length === 1) return pathList[0];
353
- return "";
354
- };
355
-
356
- export interface LegacyPLPOptions {
357
- /** URL of the page being rendered (used for filter links, pagination, etc.) */
358
- url: URL;
359
- /** Override the search term (path). Defaults to url.pathname */
360
- term?: string;
361
- /** Items per page */
362
- count?: number;
363
- /** Current page number (0-indexed internally; see pageOffset) */
364
- page?: number;
365
- /** Starting page offset. Defaults to 1. */
366
- pageOffset?: number;
367
- sort?: LegacySort;
368
- /** FullText search term */
369
- ft?: string;
370
- /** Filter query */
371
- fq?: string;
372
- /** Map parameter */
373
- map?: string;
374
- /** Filter behavior: dynamic (default) or static */
375
- filters?: "dynamic" | "static";
376
- /** Base URL for building canonical/absolute links */
377
- baseUrl: string;
378
- priceCurrency?: string;
379
- /** Use collection name as page title */
380
- useCollectionName?: boolean;
381
- /** Ignore case when checking if a facet is selected */
382
- ignoreCaseSelected?: boolean;
383
- includeOriginalAttributes?: string[];
384
- /**
385
- * Build `isVariantOf.hasVariant[]` with the lean variant transform instead of
386
- * a full nested `toProduct` per SKU. Off by default (unchanged behaviour).
387
- *
388
- * `productDetailsPage` (intelligent search) already exposes this, but the
389
- * legacy PLP never plumbed it — so a listing always paid the full variant
390
- * tree. Measured on a real store (36 products, 26 variants each): ~9.6 MB
391
- * per page, 96% of each product in `isVariantOf.hasVariant`, because every
392
- * variant carries the whole 48-entry payment ladder plus a copy of the
393
- * parent `description` — none of which a listing card reads.
394
- */
395
- leanVariants?: boolean;
396
- /** Keep the ladder on the ONE variant the card renders — see ProductOptions.displayedVariantId. */
397
- displayedVariantId?: ProductOptions["displayedVariantId"];
398
- /** Forwarded to the lean variant transform. */
399
- variantPropertyNames?: Set<string>;
400
- variantIncludeImage?: boolean;
401
- variantIncludeInventory?: boolean;
402
- /** Cap image[] to the first N entries — see ProductOptions.maxImages. */
403
- maxImages?: number;
404
- /** Rewrite priceSpecification on emitted offers — see ProductOptions.priceSpecifications. */
405
- priceSpecifications?: ProductOptions["priceSpecifications"];
406
- }
407
-
408
- /**
409
- * Fetch a product listing page (PLP) using the legacy Catalog search API.
410
- * Handles categories, departments, brands, collections, and full-text search.
411
- *
412
- * @see https://developers.vtex.com/docs/api-reference/search-api#get-/api/catalog_system/pub/products/search
413
- * Ported from: vtex/loaders/legacy/productListingPage.ts
414
- */
415
- export async function legacyProductListingPage(
416
- opts: LegacyPLPOptions,
417
- ): Promise<ProductListingPage | null> {
418
- const {
419
- url,
420
- baseUrl,
421
- priceCurrency = "BRL",
422
- filters: filtersBehavior = "dynamic",
423
- ignoreCaseSelected,
424
- useCollectionName,
425
- includeOriginalAttributes,
426
- leanVariants,
427
- displayedVariantId,
428
- variantPropertyNames,
429
- variantIncludeImage,
430
- variantIncludeInventory,
431
- maxImages,
432
- priceSpecifications,
433
- } = opts;
434
-
435
- const currentPageOffset = opts.pageOffset ?? 1;
436
- const countFromSearchParams = url.searchParams.get("PS");
437
- const count = Number(countFromSearchParams ?? opts.count ?? 12);
438
-
439
- const maybeMap = opts.map || url.searchParams.get("map") || undefined;
440
- const maybeTerm = opts.term || url.pathname || "";
441
-
442
- const pageParam = url.searchParams.get("page")
443
- ? Number(url.searchParams.get("page")) - currentPageOffset
444
- : 0;
445
- const page = opts.page ?? pageParam;
446
- const O: LegacySort =
447
- (url.searchParams.get("O") as LegacySort) ??
448
- IS_TO_LEGACY[url.searchParams.get("sort") ?? ""] ??
449
- opts.sort ??
450
- (LEGACY_SORT_OPTIONS[0].value as LegacySort);
451
- const fq = [...new Set([...(opts.fq ? [opts.fq] : []), ...url.searchParams.getAll("fq")])];
452
- const _from = page * count;
453
- const _to = (page + 1) * count - 1;
454
-
455
- const allPageTypes = await pageTypesFromPathname(maybeTerm);
456
- const pageTypes = getValidTypesFromPageTypes(allPageTypes);
457
- const pageType: PageType = pageTypes.at(-1) || pageTypes[0];
458
-
459
- const missingParams = typeof maybeMap !== "string" || !maybeTerm;
460
- const [map, term] =
461
- missingParams && fq.length > 0
462
- ? ["", ""]
463
- : missingParams
464
- ? getMapAndTerm(pageTypes)
465
- : [maybeMap, maybeTerm];
466
-
467
- const isPage = pageTypes.length > 0;
468
- const hasFilters = fq.length > 0 || !map;
469
- const ftFallback = getTermFallback(url, isPage, hasFilters);
470
- const ft = opts.ft || url.searchParams.get("ft") || url.searchParams.get("q") || ftFallback;
471
- const isInSearchFormat = ft;
472
-
473
- if (!isPage && !hasFilters && !isInSearchFormat) return null;
474
-
475
- const fmap = url.searchParams.get("fmap") ?? map;
476
- const sc = salesChannelParam();
477
- const searchBase: Record<string, string | string[] | number | undefined> = {
478
- _from,
479
- _to,
480
- O,
481
- ft: ft || undefined,
482
- fq: fq.length > 0 ? fq : undefined,
483
- map,
484
- sc,
485
- };
486
-
487
- const [vtexProductsResponse, vtexFacets] = await Promise.all([
488
- vtexFetchResponse(
489
- `/api/catalog_system/pub/products/search/${getTerm(term, map)}?${buildSearchParams(searchBase)}`,
490
- ),
491
- vtexFetch<{
492
- CategoriesTrees: LegacyFacet[];
493
- Departments: LegacyFacet[];
494
- Brands: LegacyFacet[];
495
- SpecificationFilters: Record<string, LegacyFacet[]>;
496
- PriceRanges: LegacyFacet[];
497
- }>(
498
- `/api/catalog_system/pub/facets/search/${getTerm(term, fmap)}?${buildSearchParams({
499
- ...searchBase,
500
- map: fmap,
501
- })}`,
502
- ),
503
- ]);
504
-
505
- const vtexProducts = (await vtexProductsResponse.json()) as LegacyProduct[];
506
- const resources = vtexProductsResponse.headers.get("resources") ?? "";
507
- const [, _total] = resources.split("/");
508
-
509
- if (vtexProducts && !Array.isArray(vtexProducts)) {
510
- throw new Error(`Error while fetching VTEX data ${JSON.stringify(vtexProducts)}`);
511
- }
512
-
513
- const products = vtexProducts.map((p) =>
514
- toProduct(p, p.items.find(getFirstItemAvailable) ?? p.items[0], 0, {
515
- baseUrl,
516
- priceCurrency,
517
- includeOriginalAttributes,
518
- leanVariants,
519
- displayedVariantId,
520
- variantPropertyNames,
521
- variantIncludeImage,
522
- variantIncludeInventory,
523
- maxImages,
524
- priceSpecifications,
525
- }),
526
- );
527
-
528
- const currentPageTypes = !useCollectionName
529
- ? pageTypes
530
- : pageTypes.map((pt) => {
531
- if (pt.id !== pageTypes.at(-1)?.id) return pt;
532
- const name =
533
- products?.[0]?.additionalProperty?.find(
534
- (property) => property.name === "cluster" && property.propertyID === pt.name,
535
- )?.value ?? pt.name;
536
- return { ...pt, name };
537
- });
538
-
539
- const getFlatCategories = (trees: LegacyFacet[]): Record<string, LegacyFacet[]> => {
540
- const flat: Record<string, LegacyFacet[]> = {};
541
- trees.forEach((cat) => (flat[cat.Name] = cat.Children || []));
542
- return flat;
543
- };
544
-
545
- const getCategoryFacets = (trees: LegacyFacet[], isDeptOrCat: boolean): LegacyFacet[] => {
546
- if (!isDeptOrCat) return [];
547
- for (const category of trees) {
548
- if (category.Id === Number(pageType?.id)) return category.Children || [];
549
- if (category.Children?.length) {
550
- const child = getCategoryFacets(category.Children, isDeptOrCat);
551
- if (child.length) return child;
552
- }
553
- }
554
- return [];
555
- };
556
-
557
- const isDeptOrCat =
558
- pageType?.pageType === "Department" ||
559
- pageType?.pageType === "Category" ||
560
- pageType?.pageType === "SubCategory";
561
-
562
- const flatCategories = !isDeptOrCat ? getFlatCategories(vtexFacets.CategoriesTrees) : {};
563
-
564
- const filters = Object.entries({
565
- Departments: vtexFacets.Departments,
566
- Categories: getCategoryFacets(vtexFacets.CategoriesTrees, isDeptOrCat),
567
- Brands: vtexFacets.Brands,
568
- ...vtexFacets.SpecificationFilters,
569
- PriceRanges: vtexFacets.PriceRanges,
570
- ...flatCategories,
571
- })
572
- .flatMap(([name, facets]) =>
573
- legacyFacetToFilter(
574
- name,
575
- facets,
576
- url,
577
- map,
578
- term,
579
- filtersBehavior,
580
- ignoreCaseSelected,
581
- name === "Categories",
582
- ),
583
- )
584
- .filter((x): x is Filter => Boolean(x));
585
-
586
- const itemListElement = pageTypesToBreadcrumbList(pageTypes, baseUrl);
587
- const totalRecords = parseInt(_total, 10);
588
- const hasMoreResources = _to < totalRecords - 1;
589
- const hasNextPage = page < MAX_ALLOWED_PAGES && hasMoreResources;
590
- const hasPreviousPage = page > 0;
591
-
592
- const nextPage = new URLSearchParams(url.searchParams);
593
- const previousPage = new URLSearchParams(url.searchParams);
594
- if (hasNextPage) nextPage.set("page", String(page + currentPageOffset + 1));
595
- if (hasPreviousPage) previousPage.set("page", String(page + currentPageOffset - 1));
596
-
597
- const currentPage = page + currentPageOffset;
598
-
599
- return {
600
- "@type": "ProductListingPage",
601
- breadcrumb: {
602
- "@type": "BreadcrumbList",
603
- itemListElement,
604
- numberOfItems: itemListElement.length,
605
- },
606
- filters,
607
- products,
608
- pageInfo: {
609
- nextPage: hasNextPage ? `${url.pathname}?${nextPage.toString()}` : undefined,
610
- previousPage: hasPreviousPage ? `${url.pathname}?${previousPage.toString()}` : undefined,
611
- currentPage,
612
- records: totalRecords,
613
- recordPerPage: count,
614
- pageTypes: allPageTypes.map(parsePageType),
615
- },
616
- sortOptions: LEGACY_SORT_OPTIONS.map((o) => ({ ...o })),
617
- seo: pageTypesToSeo(currentPageTypes, baseUrl, hasPreviousPage ? currentPage : undefined),
618
- };
619
- }
620
-
621
- // ---------------------------------------------------------------------------
622
- // legacySuggestions
623
- // ---------------------------------------------------------------------------
624
-
625
- export interface LegacySuggestionsOptions {
626
- query?: string;
627
- /** Max results. Defaults to 4. */
628
- count?: number;
629
- }
630
-
631
- interface AutocompleteItem {
632
- productId: string;
633
- itemId: string;
634
- name: string;
635
- nameComplete: string;
636
- imageUrl: string;
637
- }
638
-
639
- interface AutocompleteResult {
640
- name: string;
641
- href: string;
642
- items: AutocompleteItem[];
643
- }
644
-
645
- interface AutocompleteResponse {
646
- itemsReturned: AutocompleteResult[];
647
- }
648
-
649
- /**
650
- * Fetch legacy autocomplete/search suggestions.
651
- *
652
- * @see https://developers.vtex.com/docs/api-reference/search-api#get-/buscaautocomplete
653
- * Ported from: vtex/loaders/legacy/suggestions.ts
654
- */
655
- export async function legacySuggestions(
656
- opts: LegacySuggestionsOptions = {},
657
- ): Promise<Suggestion | null> {
658
- const { count = 4, query } = opts;
659
-
660
- const params = new URLSearchParams({
661
- maxRows: String(count),
662
- productNameContains: encodeURIComponent(query ?? ""),
663
- suggestionsStack: "",
664
- });
665
-
666
- const { salesChannel } = getVtexConfig();
667
- if (salesChannel) params.set("sc", salesChannel);
668
-
669
- const suggestions = await vtexFetch<AutocompleteResponse>(`/buscaautocomplete?${params}`);
670
-
671
- const searches: Suggestion["searches"] = suggestions.itemsReturned
672
- .filter(({ items }) => !items?.length)
673
- .map(({ name, href }) => ({ term: name, href }));
674
-
675
- const products: Suggestion["products"] = suggestions.itemsReturned
676
- .filter(({ items }) => !!items.length)
677
- .map(({ items: [{ productId, itemId, imageUrl, name, nameComplete }], href }): Product => {
678
- const parsedUrl = new URL(href, "https://placeholder.com");
679
- return {
680
- "@type": "Product",
681
- productID: itemId,
682
- sku: itemId,
683
- inProductGroupWithID: productId,
684
- isVariantOf: {
685
- "@type": "ProductGroup",
686
- name: nameComplete,
687
- url: parsedUrl.pathname,
688
- hasVariant: [],
689
- additionalProperty: [],
690
- productGroupID: productId,
691
- },
692
- image: [{ "@type": "ImageObject", url: imageUrl }],
693
- name,
694
- url: parsedUrl.pathname + parsedUrl.search + parsedUrl.hash,
695
- };
696
- });
697
-
698
- return { searches, products };
699
- }
700
-
701
- // ---------------------------------------------------------------------------
702
- // Short aliases — sites use invoke.vtex.loaders.legacy.productDetailsPage()
703
- // ---------------------------------------------------------------------------
704
-
705
- export {
706
- legacyProductDetailsPage as productDetailsPage,
707
- legacyProductList as productList,
708
- legacyProductListingPage as productListingPage,
709
- legacySuggestions as suggestions,
710
- };