@decocms/apps-vtex 8.0.0 → 8.1.0-next.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.
Files changed (34) hide show
  1. package/package.json +5 -5
  2. package/src/__conformance__/upstream.test.ts +158 -0
  3. package/src/__tests__/client-set-cookie-forward.test.ts +4 -4
  4. package/src/client.ts +61 -4
  5. package/src/index.ts +17 -2
  6. package/src/loaders/__tests__/legacyProductList.test.ts +157 -0
  7. package/src/loaders/autocomplete.ts +2 -4
  8. package/src/loaders/intelligentSearch/productDetailsPage.ts +2 -4
  9. package/src/loaders/intelligentSearch/productList.ts +21 -5
  10. package/src/loaders/intelligentSearch/productListingPage.ts +2 -3
  11. package/src/loaders/intelligentSearch/suggestions.ts +2 -4
  12. package/src/loaders/legacy/relatedProductsLoader.ts +2 -4
  13. package/src/loaders/legacy.ts +77 -2
  14. package/src/loaders/productListFull.ts +2 -4
  15. package/src/loaders/workflow/products.ts +2 -4
  16. package/src/middleware.cacheHeaders.test.ts +44 -0
  17. package/src/mod.ts +18 -2
  18. package/src/utils/__tests__/cookieSanitizer.test.ts +1 -1
  19. package/src/utils/__tests__/storefrontBaseUrl.test.ts +61 -0
  20. package/src/utils/__tests__/transform.test.ts +349 -7
  21. package/src/utils/buildOfferShelf.test.ts +59 -0
  22. package/src/utils/enrichment.ts +3 -7
  23. package/src/utils/fetch.ts +5 -1
  24. package/src/utils/fetchCache.ts +24 -168
  25. package/src/utils/instrumentedFetch.ts +2 -1
  26. package/src/utils/minicart.ts +1 -1
  27. package/src/utils/operationRouter.ts +3 -9
  28. package/src/utils/proxy.ts +64 -4
  29. package/src/utils/similars.ts +2 -4
  30. package/src/utils/sitemap.ts +3 -2
  31. package/src/utils/transform.ts +222 -53
  32. package/src/utils/vtexId.ts +15 -2
  33. package/src/vtexClient.test.ts +204 -0
  34. package/src/vtexClient.ts +433 -0
@@ -23,6 +23,7 @@ import {
23
23
  toAdditionalPropertySpecification,
24
24
  toBrand,
25
25
  toPostalAddress,
26
+ toProduct,
26
27
  toProductShelf,
27
28
  toProductVariant,
28
29
  } from "../transform";
@@ -648,17 +649,19 @@ describe("toProductVariant", () => {
648
649
  expect(offer.inventoryLevel?.value).toBe(0);
649
650
  });
650
651
 
651
- it("filters additionalProperty to variant-differentiating names only", () => {
652
+ it("filters spec additionalProperty to variant-differentiating names only", () => {
652
653
  const product = makeISProduct({ items: [makeISSku()] });
653
654
  const sku = makeISSku();
654
655
  const result = toProductVariant(product, sku, baseOptions);
655
656
 
656
- const propNames = result.additionalProperty?.map((p) => p.name) ?? [];
657
+ const specNames = (result.additionalProperty ?? [])
658
+ .filter((p) => p.valueReference !== "ReferenceID")
659
+ .map((p) => p.name);
657
660
  // Should only contain Cor, Voltagem, Tamanho (from VARIANT_PROPERTY_NAMES)
658
- for (const name of propNames) {
661
+ for (const name of specNames) {
659
662
  expect(["Cor", "Voltagem", "Tamanho"]).toContain(name);
660
663
  }
661
- expect(propNames.length).toBeGreaterThan(0);
664
+ expect(specNames.length).toBeGreaterThan(0);
662
665
  });
663
666
 
664
667
  it("respects custom variantPropertyNames", () => {
@@ -669,8 +672,32 @@ describe("toProductVariant", () => {
669
672
  variantPropertyNames: new Set(["Cor"]),
670
673
  });
671
674
 
672
- const propNames = result.additionalProperty?.map((p) => p.name) ?? [];
673
- expect(propNames).toEqual(["Cor"]);
675
+ const specNames = (result.additionalProperty ?? [])
676
+ .filter((p) => p.valueReference !== "ReferenceID")
677
+ .map((p) => p.name);
678
+ expect(specNames).toEqual(["Cor"]);
679
+ });
680
+
681
+ // Analytics reads the SKU RefId off additionalProperty for GA4 dimension2
682
+ // (view_item_list / select_item) and its productImpression / productClick
683
+ // equivalents. It lives in `referenceId`, not `variations`, so the
684
+ // variantProps filter used to drop it and blank the dimension.
685
+ it("keeps the SKU reference ids (RefId) alongside the variant specs", () => {
686
+ const product = makeISProduct({ items: [makeISSku()] });
687
+ const sku = { ...makeISSku(), referenceId: [{ Key: "RefId", Value: "010276949803" }] };
688
+ const result = toProductVariant(product, sku, baseOptions);
689
+
690
+ const refId = result.additionalProperty?.find((p) => p.name === "RefId");
691
+ expect(refId?.value).toBe("010276949803");
692
+ expect(refId?.valueReference).toBe("ReferenceID");
693
+ });
694
+
695
+ it("omits RefId when the SKU carries no referenceId", () => {
696
+ const product = makeISProduct({ items: [makeISSku()] });
697
+ const sku = { ...makeISSku(), referenceId: undefined };
698
+ const result = toProductVariant(product, sku, baseOptions);
699
+
700
+ expect(result.additionalProperty?.some((p) => p.name === "RefId")).toBe(false);
674
701
  });
675
702
 
676
703
  it("produces lean offers with availability but no priceSpecification details", () => {
@@ -722,10 +749,11 @@ describe("toProductShelf", () => {
722
749
  },
723
750
  ],
724
751
  variations: [{ name: "Cor", values: ["Preto"] }],
752
+ ...overrides,
725
753
  }) as any;
726
754
 
727
755
  // Campanha is a PRODUCT specification (in specificationGroups), NOT a sku.variation.
728
- // It is the marker MonteCarlo uses to tag launches ("Lançamentos").
756
+ // It is the marker one storefront uses to tag launches ("Lançamentos").
729
757
  const makeProduct = (overrides: Record<string, unknown> = {}) =>
730
758
  ({
731
759
  origin: "intelligent-search",
@@ -766,4 +794,318 @@ describe("toProductShelf", () => {
766
794
  );
767
795
  expect(result.isVariantOf?.additionalProperty).toEqual([]);
768
796
  });
797
+
798
+ // Analytics reads the product reference off isVariantOf.model for GA4
799
+ // dimension1 (view_item_list / select_item) and its productImpression /
800
+ // productClick equivalents. toProductShelf used to drop it, blanking the
801
+ // dimension on every shelf, PLP and search result.
802
+ it("carries productReference on isVariantOf.model (analytics dimension1)", () => {
803
+ const result = toProductShelf(
804
+ makeProduct({ productReference: "0102769498" }),
805
+ makeSku(),
806
+ 0,
807
+ options,
808
+ );
809
+ expect(result.isVariantOf?.model).toBe("0102769498");
810
+ });
811
+
812
+ it("keeps the SKU RefId reachable on the complete-variants shelf (dimension2)", () => {
813
+ const result = toProductShelf(makeProduct(), makeSku(), 0, {
814
+ ...options,
815
+ shelfCompleteVariants: true,
816
+ });
817
+ const variantProps = result.isVariantOf?.hasVariant?.[0]?.additionalProperty ?? [];
818
+ expect(variantProps).toEqual(
819
+ expect.arrayContaining([expect.objectContaining({ name: "RefId", value: "REF-SKU1" })]),
820
+ );
821
+ });
822
+
823
+ // A real catalog registers the packshot and the colour thumbnail LAST:
824
+ // measured on a live store, `secondary` sits at index 6 and `color-thumbnail`
825
+ // at index 7 of 8. A card that selects either BY NAME gets nothing from a
826
+ // positional cap and silently falls back to the first photo (the model
827
+ // wearing the piece).
828
+ const labelledImages = [
829
+ ...Array.from({ length: 6 }, (_, i) => ({
830
+ imageUrl: `https://img.com/photo-${i}.jpg`,
831
+ imageText: `Photo ${i}`,
832
+ imageLabel: `product-photo-${i}`,
833
+ })),
834
+ { imageUrl: "https://img.com/still.jpg", imageText: "Still", imageLabel: "secondary" },
835
+ { imageUrl: "https://img.com/thumb.jpg", imageText: "Thumb", imageLabel: "color-thumbnail" },
836
+ ];
837
+ const skuWithLabels = () => makeSku({ images: labelledImages });
838
+ const labelledProduct = () => makeProduct({ items: [skuWithLabels()] });
839
+
840
+ it("keeps the 2-image cap when keepImageNames is absent", () => {
841
+ const result = toProductShelf(labelledProduct(), skuWithLabels(), 0, options);
842
+ expect(result.image?.map((i) => i.name)).toEqual(["product-photo-0", "product-photo-1"]);
843
+ });
844
+
845
+ it("keepImageNames adds back labelled images that fall outside the cap", () => {
846
+ const result = toProductShelf(labelledProduct(), skuWithLabels(), 0, {
847
+ ...options,
848
+ keepImageNames: ["secondary", "color-thumbnail"],
849
+ });
850
+ expect(result.image?.map((i) => i.name)).toEqual([
851
+ "product-photo-0",
852
+ "product-photo-1",
853
+ "secondary",
854
+ "color-thumbnail",
855
+ ]);
856
+ });
857
+
858
+ it("keepImageNames does not duplicate a label already inside the cap", () => {
859
+ const result = toProductShelf(labelledProduct(), skuWithLabels(), 0, {
860
+ ...options,
861
+ keepImageNames: ["product-photo-0", "secondary"],
862
+ });
863
+ expect(result.image?.map((i) => i.name)).toEqual([
864
+ "product-photo-0",
865
+ "product-photo-1",
866
+ "secondary",
867
+ ]);
868
+ });
869
+
870
+ it("keepImageNames listing a label the SKU does not have changes nothing", () => {
871
+ const result = toProductShelf(labelledProduct(), skuWithLabels(), 0, {
872
+ ...options,
873
+ keepImageNames: ["vira"],
874
+ });
875
+ expect(result.image?.map((i) => i.name)).toEqual(["product-photo-0", "product-photo-1"]);
876
+ });
877
+
878
+ it("maxImages overrides the shelf default cap", () => {
879
+ const result = toProductShelf(labelledProduct(), skuWithLabels(), 0, {
880
+ ...options,
881
+ maxImages: 3,
882
+ });
883
+ expect(result.image).toHaveLength(3);
884
+ });
885
+ });
886
+
887
+ // ---------------------------------------------------------------------------
888
+ // toProduct — escape hatches for leanVariants listings
889
+ // ---------------------------------------------------------------------------
890
+ //
891
+ // `leanVariants` assumes the card renders the ROOT sku, so `buildOfferVariant`
892
+ // empties `priceSpecification` on every `hasVariant[]` entry. A card that picks
893
+ // a representative variant instead (e.g. "cheapest in stock") reads that
894
+ // entry's own offer for list price and installments — blank with the ladder
895
+ // emptied. `displayedVariantId` keeps exactly that one entry full.
896
+ //
897
+ // `maxImages` caps `image[]` by POSITION. Both default to undefined, which
898
+ // preserves the previous output byte for byte — that is what the last two cases
899
+ // pin down.
900
+
901
+ describe("toProduct — displayedVariantId / maxImages", () => {
902
+ const sellers = (price: number) => [
903
+ {
904
+ sellerId: "1",
905
+ sellerName: "Seller One",
906
+ commertialOffer: {
907
+ AvailableQuantity: 5,
908
+ Price: price,
909
+ ListPrice: price + 30,
910
+ spotPrice: price,
911
+ PriceValidUntil: "2025-12-31",
912
+ // A real ladder: every payment method the store accepts, every
913
+ // installment count. Measured on a live listing: 46 rungs per SKU,
914
+ // 12.0 KB — the thing `leanOffer` exists to drop.
915
+ Installments: ["Visa", "Master", "Amex", "PIX"].flatMap((method) =>
916
+ Array.from({ length: 3 }, (_, i) => ({
917
+ Value: price / (i + 1),
918
+ NumberOfInstallments: i + 1,
919
+ Name: `${method} ${i + 1}x`,
920
+ InterestRate: 0,
921
+ TotalValuePlusInterestRate: price,
922
+ PaymentSystemName: method,
923
+ })),
924
+ ),
925
+ GiftSkuIds: [],
926
+ teasers: [],
927
+ },
928
+ },
929
+ ];
930
+
931
+ const makeSku = (itemId: string, price: number, imageCount = 4) =>
932
+ ({
933
+ itemId,
934
+ name: `SKU ${itemId}`,
935
+ ean: "1234567890123",
936
+ referenceId: [{ Key: "RefId", Value: `REF-${itemId}` }],
937
+ images: Array.from({ length: imageCount }, (_, i) => ({
938
+ imageUrl: `https://img.com/${itemId}-${i}.jpg`,
939
+ imageText: `img${i}`,
940
+ imageLabel: `label${i}`,
941
+ })),
942
+ videos: [],
943
+ sellers: sellers(price),
944
+ variations: [{ name: "Cor", values: ["Preto"] }],
945
+ kitItems: [],
946
+ complementName: "",
947
+ estimatedDateArrival: null,
948
+ modalType: null,
949
+ }) as any;
950
+
951
+ const makeProduct = (items: any[]) =>
952
+ ({
953
+ origin: "intelligent-search",
954
+ productId: "PROD1",
955
+ productName: "Test Product",
956
+ brand: "TestBrand",
957
+ brandId: 1,
958
+ brandImageUrl: null,
959
+ productReference: "REF1",
960
+ description: "desc",
961
+ releaseDate: "2024-01-01",
962
+ linkText: "test-product",
963
+ categories: ["/Electronics/"],
964
+ categoriesIds: ["/1/"],
965
+ categoryId: "1",
966
+ productClusters: [],
967
+ clusterHighlights: [],
968
+ items,
969
+ }) as any;
970
+
971
+ const options = { baseUrl: "https://example.com", priceCurrency: "BRL" };
972
+ const items = [makeSku("SKU1", 90), makeSku("SKU2", 60), makeSku("SKU3", 120)];
973
+ const product = makeProduct(items);
974
+
975
+ const ladderOf = (variant: any) => variant?.offers?.offers?.[0]?.priceSpecification ?? [];
976
+
977
+ /** Regra do caller: só os tipos base, nenhuma parcela. */
978
+ const onlyListAndSale = (specs: any[]) =>
979
+ specs.filter((s) => !s.priceComponentType);
980
+
981
+ it("leanVariants alone empties the payment ladder on every variant", () => {
982
+ const result = toProduct(product, items[0], 0, { ...options, leanVariants: true });
983
+ const variants = result.isVariantOf?.hasVariant ?? [];
984
+ expect(variants).toHaveLength(3);
985
+ for (const v of variants) expect(ladderOf(v)).toEqual([]);
986
+ });
987
+
988
+ it("displayedVariantId keeps the ladder on the ONE variant the card renders", () => {
989
+ const result = toProduct(product, items[0], 0, {
990
+ ...options,
991
+ leanVariants: true,
992
+ displayedVariantId: (skus) => skus.find((s: any) => s.itemId === "SKU2")?.itemId,
993
+ });
994
+ const variants = result.isVariantOf?.hasVariant ?? [];
995
+ const kept = variants.find((v: any) => v.sku === "SKU2");
996
+ const lean = variants.filter((v: any) => v.sku !== "SKU2");
997
+
998
+ expect(ladderOf(kept).length).toBeGreaterThan(0);
999
+ for (const v of lean) expect(ladderOf(v)).toEqual([]);
1000
+ });
1001
+
1002
+ it("the kept variant stays LEAN — only its offer is upgraded", () => {
1003
+ const result = toProduct(product, items[0], 0, {
1004
+ ...options,
1005
+ leanVariants: true,
1006
+ displayedVariantId: () => "SKU2",
1007
+ });
1008
+ const kept: any = (result.isVariantOf?.hasVariant ?? []).find((v: any) => v.sku === "SKU2");
1009
+
1010
+ // A full toProduct here would re-emit these; the lean shape must not.
1011
+ expect(kept.description).toBeUndefined();
1012
+ expect(kept.brand).toBeUndefined();
1013
+ expect(kept.gtin).toBeUndefined();
1014
+ expect(kept.isVariantOf).toBeUndefined();
1015
+ // ...but the ladder is real. Without a `priceSpecifications` rule it is
1016
+ // the FULL one — dropping description/brand/isVariantOf is the win here,
1017
+ // and the ladder stays whatever the caller asked for.
1018
+ const full = ladderOf(toProduct(product, items[1], 0, options));
1019
+ expect(ladderOf(kept).length).toBe(full.length);
1020
+ });
1021
+
1022
+ it("the kept variant keeps its real inventoryLevel (selectors read it per SKU)", () => {
1023
+ const result = toProduct(product, items[0], 0, {
1024
+ ...options,
1025
+ leanVariants: true,
1026
+ displayedVariantId: () => "SKU2",
1027
+ });
1028
+ const kept: any = (result.isVariantOf?.hasVariant ?? []).find((v: any) => v.sku === "SKU2");
1029
+ // buildOfferShelf hard-zeroes inventoryLevel; the variant path must not
1030
+ // inherit that, or every variant reads as out of stock.
1031
+ expect(kept.offers?.offers?.[0]?.inventoryLevel?.value).toBe(5);
1032
+ });
1033
+
1034
+ it("the ladder flag does NOT leak into the lean variants", () => {
1035
+ // Regression: the internal `variantKeepLadder` reached `variantOptions`,
1036
+ // so every lean variant emitted a ladder instead of an empty one and the
1037
+ // option made the payload BIGGER — measured on a real listing,
1038
+ // isVariantOf 38.6 -> 49.6 KB.
1039
+ const result = toProduct(product, items[0], 0, {
1040
+ ...options,
1041
+ leanVariants: true,
1042
+ priceSpecifications: onlyListAndSale,
1043
+ displayedVariantId: () => "SKU2",
1044
+ });
1045
+ const variants = result.isVariantOf?.hasVariant ?? [];
1046
+ for (const v of variants.filter((v: any) => v.sku !== "SKU2")) {
1047
+ expect(ladderOf(v)).toEqual([]);
1048
+ }
1049
+ expect(ladderOf(variants.find((v: any) => v.sku === "SKU2")).length).toBeGreaterThan(0);
1050
+ });
1051
+
1052
+ it("priceSpecifications rewrites the ROOT ladder with the caller's own rule", () => {
1053
+ const full = toProduct(product, items[0], 0, options);
1054
+ const lean = toProduct(product, items[0], 0, {
1055
+ ...options,
1056
+ priceSpecifications: onlyListAndSale,
1057
+ });
1058
+
1059
+ expect(ladderOf(full).length).toBeGreaterThan(ladderOf(lean).length);
1060
+ const types = new Set(ladderOf(lean).map((s: any) => s.priceType));
1061
+ expect(types.has("https://schema.org/ListPrice")).toBe(true);
1062
+ expect(types.has("https://schema.org/SalePrice")).toBe(true);
1063
+ });
1064
+
1065
+ it("displayedVariantId returning undefined leaves every variant lean", () => {
1066
+ const result = toProduct(product, items[0], 0, {
1067
+ ...options,
1068
+ leanVariants: true,
1069
+ displayedVariantId: () => undefined,
1070
+ });
1071
+ for (const v of result.isVariantOf?.hasVariant ?? []) expect(ladderOf(v)).toEqual([]);
1072
+ });
1073
+
1074
+ it("maxImages caps image[] by position", () => {
1075
+ const full = toProduct(product, items[0], 0, options);
1076
+ expect(full.image).toHaveLength(4);
1077
+
1078
+ const capped = toProduct(product, items[0], 0, { ...options, maxImages: 2 });
1079
+ expect(capped.image).toHaveLength(2);
1080
+ expect(capped.image?.map((i) => i.url)).toEqual(
1081
+ full.image?.slice(0, 2).map((i) => i.url),
1082
+ );
1083
+ });
1084
+
1085
+ it("keepImageNames rescues a labelled image from the maxImages cap", () => {
1086
+ const capped = toProduct(product, items[0], 0, {
1087
+ ...options,
1088
+ maxImages: 2,
1089
+ keepImageNames: ["label3"],
1090
+ });
1091
+ expect(capped.image?.map((i) => i.name)).toEqual(["label0", "label1", "label3"]);
1092
+ });
1093
+
1094
+ it("keepImageNames without maxImages is a no-op — every image is already there", () => {
1095
+ const before = toProduct(product, items[0], 0, options);
1096
+ const after = toProduct(product, items[0], 0, { ...options, keepImageNames: ["label3"] });
1097
+ expect(JSON.stringify(after)).toBe(JSON.stringify(before));
1098
+ });
1099
+
1100
+ it("every option absent is byte-for-byte the previous output", () => {
1101
+ const before = toProduct(product, items[0], 0, options);
1102
+ const after = toProduct(product, items[0], 0, {
1103
+ ...options,
1104
+ displayedVariantId: undefined,
1105
+ maxImages: undefined,
1106
+ keepImageNames: undefined,
1107
+ priceSpecifications: undefined,
1108
+ });
1109
+ expect(JSON.stringify(after)).toBe(JSON.stringify(before));
1110
+ });
769
1111
  });
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { AggregateOffer, Offer, UnitPriceSpecification } from "@decocms/apps-commerce/types";
3
+ import { useOffer } from "@decocms/apps-commerce/sdk/useOffer";
4
+ import { buildOfferShelf } from "./transform";
5
+
6
+ const installment = (
7
+ over: Partial<UnitPriceSpecification>,
8
+ ): UnitPriceSpecification =>
9
+ ({
10
+ "@type": "UnitPriceSpecification",
11
+ priceType: "https://schema.org/SalePrice",
12
+ priceComponentType: "https://schema.org/Installment",
13
+ ...over,
14
+ }) as UnitPriceSpecification;
15
+
16
+ const offerWith = (specs: UnitPriceSpecification[]): Offer =>
17
+ ({
18
+ "@type": "Offer",
19
+ price: 100,
20
+ priceSpecification: [
21
+ { "@type": "UnitPriceSpecification", priceType: "https://schema.org/ListPrice", price: 120 },
22
+ { "@type": "UnitPriceSpecification", priceType: "https://schema.org/SalePrice", price: 100 },
23
+ ...specs,
24
+ ],
25
+ }) as Offer;
26
+
27
+ const pickInstallment = (offer: Offer) =>
28
+ useOffer({ "@type": "AggregateOffer", offers: [offer] } as AggregateOffer).installment;
29
+
30
+ describe("buildOfferShelf installment selection", () => {
31
+ it("keeps the same installment useOffer picks on PLP/PDP (shelf == detail)", () => {
32
+ // 12x whose stated total (100) is the lowest and duration the highest, but VTEX
33
+ // rounding makes 12 * 8.34 = 100.08 — the old 1-cent isNoInterest check rejected
34
+ // it and the shelf fell back to 10x, diverging from the detail page's 12x.
35
+ const full = offerWith([
36
+ installment({ name: "PIX", billingDuration: 1, billingIncrement: 100, price: 100 }),
37
+ installment({ name: "Visa", billingDuration: 10, billingIncrement: 10, price: 100 }),
38
+ installment({ name: "Visa", billingDuration: 12, billingIncrement: 8.34, price: 100 }),
39
+ installment({ name: "Visa", billingDuration: 6, billingIncrement: 17.67, price: 106 }),
40
+ ]);
41
+
42
+ const lean = buildOfferShelf(full);
43
+
44
+ expect(pickInstallment(lean)).toEqual(pickInstallment(full));
45
+ expect(pickInstallment(lean)?.billingDuration).toBe(12);
46
+ });
47
+
48
+ it("keeps the lowest-total plan even when every installment has interest", () => {
49
+ const full = offerWith([
50
+ installment({ name: "Visa", billingDuration: 3, billingIncrement: 34, price: 102 }),
51
+ installment({ name: "Visa", billingDuration: 6, billingIncrement: 18, price: 108 }),
52
+ ]);
53
+
54
+ const lean = buildOfferShelf(full);
55
+
56
+ expect(pickInstallment(lean)).toEqual(pickInstallment(full));
57
+ expect(pickInstallment(lean)?.price).toBe(102);
58
+ });
59
+ });
@@ -24,7 +24,7 @@
24
24
  */
25
25
 
26
26
  import type { Product, ProductLeaf } from "@decocms/apps-commerce/types";
27
- import { getVtexConfig, vtexFetch, vtexIOGraphQL } from "../client";
27
+ import { getVtexConfig, storefrontBaseUrl, vtexFetch, vtexIOGraphQL } from "../client";
28
28
  import { listBrands } from "../loaders/brands";
29
29
  import { batch } from "./batch";
30
30
  import { withIsSimilarTo } from "./similars";
@@ -372,9 +372,7 @@ export function withKitItems(): ProductEnricher {
372
372
  if (!productIDs.size) return products;
373
373
 
374
374
  const config = getVtexConfig();
375
- const baseUrl = config.publicUrl
376
- ? `https://${config.publicUrl}`
377
- : `https://${config.account}.vtexcommercestable.${config.domain ?? "com.br"}`;
375
+ const baseUrl = storefrontBaseUrl(config);
378
376
 
379
377
  const batches = batch([...productIDs], KIT_ITEMS_BATCH_SIZE);
380
378
  const productsById = new Map<string, ProductLeaf>();
@@ -428,9 +426,7 @@ export function withVariants(): ProductEnricher {
428
426
  if (!productIDs.size) return products;
429
427
 
430
428
  const config = getVtexConfig();
431
- const baseUrl = config.publicUrl
432
- ? `https://${config.publicUrl}`
433
- : `https://${config.account}.vtexcommercestable.${config.domain ?? "com.br"}`;
429
+ const baseUrl = storefrontBaseUrl(config);
434
430
 
435
431
  const batches = batch([...productIDs], VARIANTS_BATCH_SIZE);
436
432
  const productsById = new Map<string, Product>();
@@ -15,6 +15,10 @@
15
15
  * the platform.
16
16
  */
17
17
 
18
+ import { withFetchTimeout } from "@decocms/blocks/sdk/fetchTimeout";
19
+
20
+ const timeoutFetch = withFetchTimeout();
21
+
18
22
  type CachingMode = "stale-while-revalidate";
19
23
 
20
24
  type DecoInit = {
@@ -88,7 +92,7 @@ export async function fetchSafe(
88
92
  init?: DecoRequestInit,
89
93
  ): Promise<Response> {
90
94
  const sanitized = sanitizeUrl(input);
91
- const response = await fetch(sanitized as RequestInfo, init);
95
+ const response = await timeoutFetch(sanitized as RequestInfo, init);
92
96
  if (!response.ok) {
93
97
  throw new HttpError(response);
94
98
  }