@decocms/apps-vtex 8.0.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.
- package/package.json +7 -52
- package/src/index.ts +12 -15
- package/src/utils/constants.ts +2 -56
- package/src/utils/cookieSanitizer.ts +59 -59
- package/src/utils/operationRouter.ts +76 -72
- package/src/utils/types.ts +709 -1557
- package/src/vtexClient.ts +444 -0
- package/src/README.md +0 -6
- package/src/__tests__/client-segment-cookie.test.ts +0 -255
- package/src/__tests__/client-set-cookie-forward.test.ts +0 -257
- package/src/__tests__/schemas.test.ts +0 -71
- package/src/actions/address.ts +0 -250
- package/src/actions/analytics/sendEvent.ts +0 -86
- package/src/actions/auth.ts +0 -333
- package/src/actions/checkout.ts +0 -659
- package/src/actions/index.ts +0 -11
- package/src/actions/masterData.ts +0 -168
- package/src/actions/misc.ts +0 -188
- package/src/actions/newsletter.ts +0 -105
- package/src/actions/orders.ts +0 -34
- package/src/actions/profile.ts +0 -201
- package/src/actions/session.ts +0 -85
- package/src/actions/trigger.ts +0 -43
- package/src/actions/wishlist.ts +0 -114
- package/src/client.ts +0 -667
- package/src/commerceLoaders.ts +0 -267
- package/src/hooks/__tests__/createCart.render.test.ts +0 -114
- package/src/hooks/__tests__/createCart.test.ts +0 -92
- package/src/hooks/__tests__/createUseCart.test.ts +0 -184
- package/src/hooks/__tests__/createUseUser.test.ts +0 -48
- package/src/hooks/__tests__/createUseWishlist.test.ts +0 -87
- package/src/hooks/cartQuery.ts +0 -204
- package/src/hooks/createCart.ts +0 -427
- package/src/hooks/createUseCart.ts +0 -360
- package/src/hooks/createUseUser.ts +0 -153
- package/src/hooks/createUseWishlist.ts +0 -242
- package/src/hooks/index.ts +0 -25
- package/src/hooks/useAutocomplete.ts +0 -83
- package/src/hooks/useCart.ts +0 -243
- package/src/hooks/useUser.ts +0 -78
- package/src/hooks/useWishlist.ts +0 -119
- package/src/invoke.ts +0 -235
- package/src/loaders/ProductDetailsPage.ts +0 -1
- package/src/loaders/ProductList.ts +0 -1
- package/src/loaders/ProductListingPage.ts +0 -1
- package/src/loaders/address.ts +0 -115
- package/src/loaders/autocomplete.ts +0 -58
- package/src/loaders/brands.ts +0 -44
- package/src/loaders/cart/attachments.ts +0 -53
- package/src/loaders/cart/full.ts +0 -67
- package/src/loaders/cart/gifts.ts +0 -43
- package/src/loaders/cart/orderFormId.ts +0 -31
- package/src/loaders/cart/shipping.test.ts +0 -125
- package/src/loaders/cart/shipping.ts +0 -136
- package/src/loaders/cart/summary.ts +0 -36
- package/src/loaders/cart.ts +0 -52
- package/src/loaders/catalog.ts +0 -163
- package/src/loaders/collections.ts +0 -55
- package/src/loaders/index.ts +0 -20
- package/src/loaders/intelligentSearch/__tests__/productListingPage.test.ts +0 -51
- package/src/loaders/intelligentSearch/productDetailsPage.ts +0 -113
- package/src/loaders/intelligentSearch/productList.ts +0 -154
- package/src/loaders/intelligentSearch/productListingPage.ts +0 -526
- package/src/loaders/intelligentSearch/suggestions.ts +0 -46
- package/src/loaders/legacy/productDetailsPage.ts +0 -1
- package/src/loaders/legacy/productList.ts +0 -1
- package/src/loaders/legacy/relatedProductsLoader.ts +0 -81
- package/src/loaders/legacy.ts +0 -635
- package/src/loaders/logistics.ts +0 -111
- package/src/loaders/minicart.ts +0 -78
- package/src/loaders/navbar.ts +0 -26
- package/src/loaders/orders.ts +0 -92
- package/src/loaders/pageType.ts +0 -68
- package/src/loaders/paths/PDPDefaultPath.ts +0 -51
- package/src/loaders/payment.ts +0 -97
- package/src/loaders/productListFull.ts +0 -159
- package/src/loaders/profile.ts +0 -138
- package/src/loaders/promotion.ts +0 -30
- package/src/loaders/search.ts +0 -124
- package/src/loaders/session.ts +0 -83
- package/src/loaders/user.ts +0 -87
- package/src/loaders/wishlist.ts +0 -89
- package/src/loaders/wishlistProducts.ts +0 -69
- package/src/loaders/workflow/products.ts +0 -64
- package/src/loaders/workflow.ts +0 -316
- package/src/logo.png +0 -0
- package/src/manifest.gen.ts +0 -89
- package/src/middleware.ts +0 -240
- package/src/mod.ts +0 -157
- package/src/registry.ts +0 -9
- package/src/schemas.gen.ts +0 -3191
- package/src/schemas.ts +0 -30
- package/src/types.ts +0 -248
- package/src/utils/__tests__/cartProjection.test.ts +0 -118
- package/src/utils/__tests__/cookieSanitizer.test.ts +0 -162
- package/src/utils/__tests__/fetch.test.ts +0 -80
- package/src/utils/__tests__/fetchCache.test.ts +0 -155
- package/src/utils/__tests__/instrumentedFetch.test.ts +0 -158
- package/src/utils/__tests__/intelligentSearch.test.ts +0 -88
- package/src/utils/__tests__/minicart.test.ts +0 -184
- package/src/utils/__tests__/operationRouter.test.ts +0 -227
- package/src/utils/__tests__/resilience.test.ts +0 -205
- package/src/utils/__tests__/resourceRange.test.ts +0 -32
- package/src/utils/__tests__/simulationCache.test.ts +0 -40
- package/src/utils/__tests__/sitemap.test.ts +0 -185
- package/src/utils/__tests__/slugify.test.ts +0 -31
- package/src/utils/__tests__/transform.test.ts +0 -769
- package/src/utils/accountLoaders.ts +0 -203
- package/src/utils/authHelpers.ts +0 -85
- package/src/utils/batch.ts +0 -18
- package/src/utils/cartProjection.ts +0 -96
- package/src/utils/cookies.ts +0 -167
- package/src/utils/enrichment.ts +0 -560
- package/src/utils/fetch.ts +0 -107
- package/src/utils/fetchCache.ts +0 -199
- package/src/utils/index.ts +0 -19
- package/src/utils/instrumentedFetch.ts +0 -97
- package/src/utils/intelligentSearch.ts +0 -96
- package/src/utils/legacy.ts +0 -139
- package/src/utils/minicart.ts +0 -139
- package/src/utils/pickAndOmit.ts +0 -25
- package/src/utils/proxy.ts +0 -435
- package/src/utils/resilience.ts +0 -352
- package/src/utils/resourceRange.ts +0 -10
- package/src/utils/segment.ts +0 -152
- package/src/utils/similars.ts +0 -37
- package/src/utils/simulationCache.ts +0 -73
- package/src/utils/sitemap.ts +0 -282
- package/src/utils/slugCache.ts +0 -28
- package/src/utils/slugify.ts +0 -13
- package/src/utils/transform.ts +0 -1522
- package/src/utils/vtexId.ts +0 -138
- package/tsconfig.json +0 -7
package/src/utils/fetchCache.ts
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SWR in-memory fetch cache for VTEX API responses.
|
|
3
|
-
*
|
|
4
|
-
* Inspired by deco-cx/deco runtime/fetch/fetchCache.ts.
|
|
5
|
-
* Provides in-flight deduplication + stale-while-revalidate for GET requests.
|
|
6
|
-
*
|
|
7
|
-
* Only caches on the server side. Keyed by full URL string.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
FETCH_CACHE_FRESH_TTL_MS,
|
|
12
|
-
FETCH_CACHE_INFLIGHT_BACKSTOP_MS,
|
|
13
|
-
FETCH_CACHE_MAX_ENTRIES,
|
|
14
|
-
FETCH_CACHE_STALE_IF_ERROR_MS,
|
|
15
|
-
} from "./constants";
|
|
16
|
-
|
|
17
|
-
interface CacheEntry {
|
|
18
|
-
body: unknown;
|
|
19
|
-
status: number;
|
|
20
|
-
createdAt: number;
|
|
21
|
-
refreshing: boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function freshTtlForStatus(status: number): number {
|
|
25
|
-
if (status >= 200 && status < 300) return FETCH_CACHE_FRESH_TTL_MS.success;
|
|
26
|
-
if (status === 404) return FETCH_CACHE_FRESH_TTL_MS.notFound;
|
|
27
|
-
if (status >= 500) return FETCH_CACHE_FRESH_TTL_MS.serverError;
|
|
28
|
-
return 0;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const store = new Map<string, CacheEntry>();
|
|
32
|
-
const inflight = new Map<string, Promise<CacheEntry>>();
|
|
33
|
-
|
|
34
|
-
function evictIfNeeded() {
|
|
35
|
-
if (store.size <= FETCH_CACHE_MAX_ENTRIES) return;
|
|
36
|
-
const sorted = [...store.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
|
|
37
|
-
const toRemove = sorted.slice(0, store.size - FETCH_CACHE_MAX_ENTRIES);
|
|
38
|
-
for (const [key] of toRemove) store.delete(key);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Race a Promise against a timeout so callers' `.finally()` always runs.
|
|
43
|
-
* Critical for evicting the inflight Map entry when a `fetch()` hangs —
|
|
44
|
-
* without this, a never-settling Promise leaks the Map slot forever and
|
|
45
|
-
* every subsequent request for the same key joins the zombie Promise.
|
|
46
|
-
*/
|
|
47
|
-
function withTimeout<T>(work: Promise<T>, ms: number, label: string): Promise<T> {
|
|
48
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
49
|
-
const timeout = new Promise<never>((_, reject) => {
|
|
50
|
-
timer = setTimeout(() => {
|
|
51
|
-
reject(new Error(`${label} timed out after ${ms}ms`));
|
|
52
|
-
}, ms);
|
|
53
|
-
});
|
|
54
|
-
return Promise.race([work, timeout]).finally(() => {
|
|
55
|
-
clearTimeout(timer);
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function executeFetch(url: string, doFetch: () => Promise<Response>): Promise<CacheEntry> {
|
|
60
|
-
// Single attempt on purpose. The resilience layer (`createResilientFetch`,
|
|
61
|
-
// wired as the VTEX fetch's baseFetch) owns retries, backoff+jitter, the
|
|
62
|
-
// per-host retry budget, and the circuit breaker. Retrying here would:
|
|
63
|
-
// - double-retry network errors (resilience 3× × fetchCache 3× = up to 9
|
|
64
|
-
// upstream calls per logical request — the retry storm the budget
|
|
65
|
-
// prevents), and
|
|
66
|
-
// - re-enter the breaker on every 5xx retry, opening it ~3× too fast.
|
|
67
|
-
const response = await doFetch();
|
|
68
|
-
|
|
69
|
-
if (response.status >= 500) {
|
|
70
|
-
throw new Error(`fetchWithCache: ${response.status} ${response.statusText} — ${url}`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const body = response.ok ? await response.json() : null;
|
|
74
|
-
return {
|
|
75
|
-
body,
|
|
76
|
-
status: response.status,
|
|
77
|
-
createdAt: Date.now(),
|
|
78
|
-
refreshing: false,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export interface FetchCacheOptions {
|
|
83
|
-
/**
|
|
84
|
-
* Custom TTL in ms. If provided, overrides status-based TTL.
|
|
85
|
-
*/
|
|
86
|
-
ttl?: number;
|
|
87
|
-
/**
|
|
88
|
-
* Stale-if-error window in ms. How long past the freshness TTL a last-good
|
|
89
|
-
* entry may still be served while the origin is failing. Defaults to
|
|
90
|
-
* {@link FETCH_CACHE_STALE_IF_ERROR_MS} (24h). Set to 0 to disable stale
|
|
91
|
-
* serving.
|
|
92
|
-
*/
|
|
93
|
-
sieMs?: number;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Wrap a GET fetch call with SWR caching and in-flight dedup.
|
|
98
|
-
*
|
|
99
|
-
* Returns `null` for non-2xx responses that are cached (e.g. 404).
|
|
100
|
-
* 5xx responses throw so the caller can handle them explicitly.
|
|
101
|
-
*
|
|
102
|
-
* @param cacheKey - Unique key (typically the full URL)
|
|
103
|
-
* @param doFetch - The actual fetch call to execute
|
|
104
|
-
* @param opts - Optional overrides
|
|
105
|
-
* @returns Parsed JSON body, or null for cacheable error responses (e.g. 404)
|
|
106
|
-
*/
|
|
107
|
-
export function fetchWithCache<T>(
|
|
108
|
-
cacheKey: string,
|
|
109
|
-
doFetch: () => Promise<Response>,
|
|
110
|
-
opts?: FetchCacheOptions,
|
|
111
|
-
): Promise<T | null> {
|
|
112
|
-
const now = Date.now();
|
|
113
|
-
const entry = store.get(cacheKey);
|
|
114
|
-
|
|
115
|
-
if (entry) {
|
|
116
|
-
const maxAge = opts?.ttl ?? freshTtlForStatus(entry.status);
|
|
117
|
-
const age = now - entry.createdAt;
|
|
118
|
-
const isStale = age > maxAge;
|
|
119
|
-
|
|
120
|
-
if (!isStale) return Promise.resolve(entry.body as T | null);
|
|
121
|
-
|
|
122
|
-
// Beyond the stale-if-error window the last-good entry is too old to keep
|
|
123
|
-
// serving during an outage: drop it and fall through to a foreground
|
|
124
|
-
// refetch (cold path). On a healthy origin this is never reached because
|
|
125
|
-
// the background refresh below keeps resetting `createdAt`.
|
|
126
|
-
const sieMs = opts?.sieMs ?? FETCH_CACHE_STALE_IF_ERROR_MS;
|
|
127
|
-
const tooStale = age > maxAge + sieMs;
|
|
128
|
-
|
|
129
|
-
if (!tooStale) {
|
|
130
|
-
if (!entry.refreshing) {
|
|
131
|
-
entry.refreshing = true;
|
|
132
|
-
// Background refresh: no retry — stale data is already being served.
|
|
133
|
-
// Timeout guards against a hung VTEX response leaving `refreshing`
|
|
134
|
-
// stuck true forever (which would silently disable revalidation).
|
|
135
|
-
withTimeout(
|
|
136
|
-
executeFetch(cacheKey, doFetch),
|
|
137
|
-
FETCH_CACHE_INFLIGHT_BACKSTOP_MS,
|
|
138
|
-
`fetchCache stale-refresh ${cacheKey}`,
|
|
139
|
-
)
|
|
140
|
-
.then((fresh) => {
|
|
141
|
-
const ttl = opts?.ttl ?? freshTtlForStatus(fresh.status);
|
|
142
|
-
const existingWasSuccess = entry.status >= 200 && entry.status < 300;
|
|
143
|
-
const freshIsError = fresh.status >= 400;
|
|
144
|
-
const wouldDowngrade = existingWasSuccess && freshIsError;
|
|
145
|
-
if (ttl > 0 && !wouldDowngrade) {
|
|
146
|
-
store.set(cacheKey, fresh);
|
|
147
|
-
} else {
|
|
148
|
-
entry.refreshing = false;
|
|
149
|
-
}
|
|
150
|
-
})
|
|
151
|
-
.catch(() => {
|
|
152
|
-
entry.refreshing = false;
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
// Serve last-good while it is within the SIE window (stale-if-error).
|
|
156
|
-
return Promise.resolve(entry.body as T | null);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
store.delete(cacheKey);
|
|
160
|
-
// fall through to the cold path below with the dead entry removed
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const existing = inflight.get(cacheKey);
|
|
164
|
-
if (existing) return existing.then((e) => e.body as T | null);
|
|
165
|
-
|
|
166
|
-
// Wrap with a timeout so the `.finally()` below always runs and evicts
|
|
167
|
-
// the inflight slot — even if `executeFetch` never settles. See the
|
|
168
|
-
// FETCH_CACHE_INFLIGHT_BACKSTOP_MS doc comment (constants.ts) for the leak
|
|
169
|
-
// this guards against.
|
|
170
|
-
const promise = withTimeout(
|
|
171
|
-
executeFetch(cacheKey, doFetch),
|
|
172
|
-
FETCH_CACHE_INFLIGHT_BACKSTOP_MS,
|
|
173
|
-
`fetchCache ${cacheKey}`,
|
|
174
|
-
)
|
|
175
|
-
.then((fresh) => {
|
|
176
|
-
const ttl = opts?.ttl ?? freshTtlForStatus(fresh.status);
|
|
177
|
-
if (ttl > 0) {
|
|
178
|
-
store.set(cacheKey, fresh);
|
|
179
|
-
evictIfNeeded();
|
|
180
|
-
}
|
|
181
|
-
return fresh;
|
|
182
|
-
})
|
|
183
|
-
.finally(() => inflight.delete(cacheKey));
|
|
184
|
-
|
|
185
|
-
inflight.set(cacheKey, promise);
|
|
186
|
-
return promise.then((e) => e.body as T | null);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export function clearFetchCache() {
|
|
190
|
-
store.clear();
|
|
191
|
-
inflight.clear();
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export function getFetchCacheStats() {
|
|
195
|
-
return {
|
|
196
|
-
entries: store.size,
|
|
197
|
-
inflight: inflight.size,
|
|
198
|
-
};
|
|
199
|
-
}
|
package/src/utils/index.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export type { PersonalDataOptions } from "./accountLoaders";
|
|
2
|
-
export { vtexAccountLoaders } from "./accountLoaders";
|
|
3
|
-
export * from "./batch";
|
|
4
|
-
export * from "./cookies";
|
|
5
|
-
export * from "./enrichment";
|
|
6
|
-
export * from "./fetchCache";
|
|
7
|
-
export * from "./intelligentSearch";
|
|
8
|
-
export * from "./legacy";
|
|
9
|
-
export * from "./pickAndOmit";
|
|
10
|
-
export * from "./proxy";
|
|
11
|
-
export * from "./resourceRange";
|
|
12
|
-
export * from "./segment";
|
|
13
|
-
export * from "./similars";
|
|
14
|
-
export * from "./sitemap";
|
|
15
|
-
export * from "./slugCache";
|
|
16
|
-
export * from "./slugify";
|
|
17
|
-
export * from "./transform";
|
|
18
|
-
export * from "./types";
|
|
19
|
-
export * from "./vtexId";
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pre-wired instrumented fetch factory for VTEX.
|
|
3
|
-
*
|
|
4
|
-
* Bundles the three pieces a storefront would otherwise have to wire
|
|
5
|
-
* by hand:
|
|
6
|
-
*
|
|
7
|
-
* 1. The `createInstrumentedFetch` boundary from `@decocms/start`
|
|
8
|
-
* (spans, traceparent injection, URL redaction, cache-header
|
|
9
|
-
* span attributes).
|
|
10
|
-
* 2. The `vtexOperationRouter` URL→operation mapping so unannotated
|
|
11
|
-
* callsites still get semantic span names + histogram labels.
|
|
12
|
-
* 3. An `onComplete` callback that records every call into the
|
|
13
|
-
* canonical `http.client.request.duration` histogram via the
|
|
14
|
-
* framework's `recordCommerceMetric(...)` helper in
|
|
15
|
-
* `@decocms/start/sdk/observability` — `provider`, `operation`,
|
|
16
|
-
* `status_class`, and `cached` labels.
|
|
17
|
-
*
|
|
18
|
-
* Sites opt in once at startup:
|
|
19
|
-
*
|
|
20
|
-
* ```ts
|
|
21
|
-
* import { setVtexFetch } from "@decocms/apps/vtex";
|
|
22
|
-
* import { createVtexFetch } from "@decocms/apps/vtex";
|
|
23
|
-
*
|
|
24
|
-
* setVtexFetch(createVtexFetch());
|
|
25
|
-
* ```
|
|
26
|
-
*
|
|
27
|
-
* Sites that need to wrap a custom underlying fetch (cookie passthrough,
|
|
28
|
-
* proxy, retry, etc.) pass it as `baseFetch`. The instrumentation
|
|
29
|
-
* still applies — `createInstrumentedFetch` preserves the wrapped
|
|
30
|
-
* behavior.
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
import {
|
|
34
|
-
createInstrumentedFetch,
|
|
35
|
-
type InstrumentedFetch,
|
|
36
|
-
} from "@decocms/blocks/sdk/instrumentedFetch";
|
|
37
|
-
import { recordCommerceMetric, statusClassFor } from "@decocms/blocks/sdk/observability";
|
|
38
|
-
import { vtexOperationRouter } from "./operationRouter";
|
|
39
|
-
import { createResilientFetch, type ResilienceConfig } from "./resilience";
|
|
40
|
-
|
|
41
|
-
export interface CreateVtexFetchOptions {
|
|
42
|
-
/**
|
|
43
|
-
* Underlying fetch to wrap. Defaults to `globalThis.fetch`.
|
|
44
|
-
* Pass an existing custom fetch (e.g. one that injects auth cookies
|
|
45
|
-
* or routes through a proxy) to preserve its behavior while adding
|
|
46
|
-
* the VTEX instrumentation layer on top.
|
|
47
|
-
*/
|
|
48
|
-
baseFetch?: typeof fetch;
|
|
49
|
-
/**
|
|
50
|
-
* Disable the `http.client.request.duration` histogram emission for
|
|
51
|
-
* VTEX calls. The framework's span and structured logs still emit.
|
|
52
|
-
* Useful when the consumer wants to record its own histogram with a
|
|
53
|
-
* custom shape. Default: false.
|
|
54
|
-
*/
|
|
55
|
-
disableHistogram?: boolean;
|
|
56
|
-
/**
|
|
57
|
-
* Resilience layer (real abort/timeout + per-host circuit breaker +
|
|
58
|
-
* idempotent-only retry with a budget). ON by default so every VTEX
|
|
59
|
-
* consumer is protected against upstream slowness/outages without
|
|
60
|
-
* per-site wiring. Pass a partial config to tune, `false` to opt out,
|
|
61
|
-
* or flip the `VTEX_RESILIENCE_DISABLED=true` env var as a runtime
|
|
62
|
-
* kill-switch. See {@link createResilientFetch}.
|
|
63
|
-
*/
|
|
64
|
-
resilience?: Partial<ResilienceConfig> | false;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Construct a pre-wired VTEX `InstrumentedFetch`. Pass the result to
|
|
69
|
-
* `setVtexFetch(...)`. See module docstring for details.
|
|
70
|
-
*/
|
|
71
|
-
export function createVtexFetch(options: CreateVtexFetchOptions = {}): InstrumentedFetch {
|
|
72
|
-
const { baseFetch, disableHistogram = false, resilience } = options;
|
|
73
|
-
// Compose the resilience layer UNDER the instrumentation so spans/histograms
|
|
74
|
-
// see the real outcome (including breaker fast-fails and timeouts). A single
|
|
75
|
-
// instrumented call = one logical request, which may span several underlying
|
|
76
|
-
// attempts. Covers every VTEX egress path — cached GETs, IS, and checkout
|
|
77
|
-
// POSTs — because they all funnel through this one fetch.
|
|
78
|
-
const resilientBase =
|
|
79
|
-
resilience === false
|
|
80
|
-
? baseFetch
|
|
81
|
-
: createResilientFetch(baseFetch ?? globalThis.fetch, resilience ?? {});
|
|
82
|
-
return createInstrumentedFetch({
|
|
83
|
-
name: "vtex",
|
|
84
|
-
baseFetch: resilientBase,
|
|
85
|
-
resolveOperation: vtexOperationRouter,
|
|
86
|
-
onComplete: disableHistogram
|
|
87
|
-
? undefined
|
|
88
|
-
: ({ operation, status, durationMs, cached }) => {
|
|
89
|
-
recordCommerceMetric(durationMs, {
|
|
90
|
-
provider: "vtex",
|
|
91
|
-
operation,
|
|
92
|
-
status_class: statusClassFor(status),
|
|
93
|
-
cached,
|
|
94
|
-
});
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { vtexFetch } from "../client";
|
|
2
|
-
import type { PageType, SelectedFacet, SimulationBehavior, Sort } from "./types";
|
|
3
|
-
|
|
4
|
-
export const SESSION_COOKIE = "vtex_is_session";
|
|
5
|
-
export const ANONYMOUS_COOKIE = "vtex_is_anonymous";
|
|
6
|
-
|
|
7
|
-
export const withDefaultFacets = (allFacets: readonly SelectedFacet[]) => {
|
|
8
|
-
return [...allFacets];
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export const toPath = (facets: SelectedFacet[]) =>
|
|
12
|
-
facets.map(({ key, value }) => (key ? `${key}/${value}` : value)).join("/");
|
|
13
|
-
|
|
14
|
-
interface Params {
|
|
15
|
-
query: string;
|
|
16
|
-
page: number;
|
|
17
|
-
count: number;
|
|
18
|
-
sort: Sort;
|
|
19
|
-
fuzzy: string;
|
|
20
|
-
locale: string;
|
|
21
|
-
hideUnavailableItems: boolean;
|
|
22
|
-
simulationBehavior: SimulationBehavior;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export const withDefaultParams = ({
|
|
26
|
-
query = "",
|
|
27
|
-
page = 0,
|
|
28
|
-
count = 12,
|
|
29
|
-
sort = "",
|
|
30
|
-
fuzzy = "auto",
|
|
31
|
-
locale = "pt-BR",
|
|
32
|
-
hideUnavailableItems,
|
|
33
|
-
simulationBehavior = "default",
|
|
34
|
-
}: Partial<Params>) => ({
|
|
35
|
-
page: page + 1,
|
|
36
|
-
count,
|
|
37
|
-
query,
|
|
38
|
-
sort,
|
|
39
|
-
...(fuzzy ? { fuzzy } : {}),
|
|
40
|
-
locale,
|
|
41
|
-
hideUnavailableItems: hideUnavailableItems ?? false,
|
|
42
|
-
simulationBehavior,
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
export const isFilterParam = (keyFilter: string): boolean => keyFilter.startsWith("filter.");
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Valid VTEX Intelligent Search sort values.
|
|
49
|
-
* Anything else (e.g. "orders:desc)" with a trailing paren from legacy URLs)
|
|
50
|
-
* causes IS API to return 400.
|
|
51
|
-
*/
|
|
52
|
-
export const VALID_IS_SORTS = new Set([
|
|
53
|
-
"",
|
|
54
|
-
"orders:desc",
|
|
55
|
-
"price:asc",
|
|
56
|
-
"price:desc",
|
|
57
|
-
"name:asc",
|
|
58
|
-
"name:desc",
|
|
59
|
-
"release:desc",
|
|
60
|
-
"discount:desc",
|
|
61
|
-
]);
|
|
62
|
-
|
|
63
|
-
/** Sanitize an IS sort parameter — returns empty string for invalid values. */
|
|
64
|
-
export function sanitizeISSort(sort: string): string {
|
|
65
|
-
return VALID_IS_SORTS.has(sort) ? sort : "";
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const segmentsFromTerm = (term: string) => term.split("/").filter(Boolean);
|
|
69
|
-
|
|
70
|
-
const segmentsFromSearchParams = (url: string) => {
|
|
71
|
-
const searchParams = new URLSearchParams(url).entries();
|
|
72
|
-
|
|
73
|
-
const categories = Array.from(searchParams)
|
|
74
|
-
.sort()
|
|
75
|
-
.reduce((acc, [key, value]) => {
|
|
76
|
-
if (key.includes("filter.category")) {
|
|
77
|
-
acc.push(value);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return acc;
|
|
81
|
-
}, [] as string[]);
|
|
82
|
-
|
|
83
|
-
return categories.length ? categories : segmentsFromTerm(url);
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
export const pageTypesFromUrl = async (url: string): Promise<PageType[]> => {
|
|
87
|
-
const segments = segmentsFromSearchParams(url);
|
|
88
|
-
|
|
89
|
-
return await Promise.all(
|
|
90
|
-
segments.map((_, index) =>
|
|
91
|
-
vtexFetch<PageType>(
|
|
92
|
-
`/api/catalog_system/pub/portal/pagetype/${segments.slice(0, index + 1).join("/")}`,
|
|
93
|
-
),
|
|
94
|
-
),
|
|
95
|
-
);
|
|
96
|
-
};
|
package/src/utils/legacy.ts
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import type { Seo } from "@decocms/apps-commerce/types";
|
|
2
|
-
import { vtexFetch } from "../client";
|
|
3
|
-
import type { WrappedSegment } from "./segment";
|
|
4
|
-
import { slugify } from "./slugify";
|
|
5
|
-
import type { PageType } from "./types";
|
|
6
|
-
|
|
7
|
-
const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
8
|
-
|
|
9
|
-
export const toSegmentParams = ({ payload: segment }: WrappedSegment) =>
|
|
10
|
-
Object.fromEntries(
|
|
11
|
-
Object.entries({
|
|
12
|
-
utmi_campaign: segment.utmi_campaign ?? undefined,
|
|
13
|
-
utm_campaign: segment.utm_campaign ?? undefined,
|
|
14
|
-
utm_source: segment.utm_source ?? undefined,
|
|
15
|
-
sc: segment.channel ?? undefined,
|
|
16
|
-
}).filter(([_, v]) => v !== undefined),
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
const PAGE_TYPE_TO_MAP_PARAM = {
|
|
20
|
-
Brand: "b",
|
|
21
|
-
Category: "c",
|
|
22
|
-
Department: "c",
|
|
23
|
-
SubCategory: "c",
|
|
24
|
-
Collection: "productClusterIds",
|
|
25
|
-
Cluster: "productClusterIds",
|
|
26
|
-
Search: "ft",
|
|
27
|
-
FullText: "ft",
|
|
28
|
-
Product: "p",
|
|
29
|
-
NotFound: null,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const segmentsFromTerm = (term: string) => term.split("/").filter(Boolean);
|
|
33
|
-
|
|
34
|
-
export const getValidTypesFromPageTypes = (pagetypes: PageType[]) => {
|
|
35
|
-
return pagetypes.filter((type) => PAGE_TYPE_TO_MAP_PARAM[type.pageType]);
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export const pageTypesFromPathname = async (term: string): Promise<PageType[]> => {
|
|
39
|
-
const segments = segmentsFromTerm(term);
|
|
40
|
-
|
|
41
|
-
return await Promise.all(
|
|
42
|
-
segments.map((_, index) =>
|
|
43
|
-
vtexFetch<PageType>(
|
|
44
|
-
`/api/catalog_system/pub/portal/pagetype/${segments.slice(0, index + 1).join("/")}`,
|
|
45
|
-
),
|
|
46
|
-
),
|
|
47
|
-
);
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export const getMapAndTerm = (pageTypes: PageType[]) => {
|
|
51
|
-
const term = pageTypes
|
|
52
|
-
.map((type, index) =>
|
|
53
|
-
type.url ? segmentsFromTerm(new URL(`http://${type.url}`).pathname)[index] : null,
|
|
54
|
-
)
|
|
55
|
-
.filter(Boolean)
|
|
56
|
-
.join("/");
|
|
57
|
-
|
|
58
|
-
const map = pageTypes
|
|
59
|
-
.map((type) => PAGE_TYPE_TO_MAP_PARAM[type.pageType])
|
|
60
|
-
.filter(Boolean)
|
|
61
|
-
.join(",");
|
|
62
|
-
|
|
63
|
-
if (map === "ft" && term === "s") {
|
|
64
|
-
return ["", ""];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return [map, term];
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
export const pageTypesToBreadcrumbList = (pages: PageType[], baseUrl: string) => {
|
|
71
|
-
const filteredPages = pages.filter(
|
|
72
|
-
({ pageType }) =>
|
|
73
|
-
pageType === "Category" || pageType === "Department" || pageType === "SubCategory",
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
return filteredPages.map((page, index) => {
|
|
77
|
-
const position = index + 1;
|
|
78
|
-
const slug = filteredPages.slice(0, position).map((x) => slugify(x.name!));
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
"@type": "ListItem" as const,
|
|
82
|
-
name: page.name!,
|
|
83
|
-
item: new URL(`/${slug.join("/")}`, baseUrl).href,
|
|
84
|
-
position,
|
|
85
|
-
};
|
|
86
|
-
});
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
export const pageTypesToSeo = (
|
|
90
|
-
pages: PageType[],
|
|
91
|
-
baseUrl: string,
|
|
92
|
-
currentPage?: number,
|
|
93
|
-
): Seo | null => {
|
|
94
|
-
const current = pages.at(-1);
|
|
95
|
-
const url = new URL(baseUrl);
|
|
96
|
-
const fullTextSearch = url.searchParams.get("q");
|
|
97
|
-
const hasMapTermOrSkuId = !!(url.searchParams.get("map") || url.searchParams.get("skuId"));
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
(!current || current.pageType === "Search" || current.pageType === "FullText") &&
|
|
101
|
-
fullTextSearch
|
|
102
|
-
) {
|
|
103
|
-
return {
|
|
104
|
-
title: capitalize(fullTextSearch),
|
|
105
|
-
description: capitalize(fullTextSearch),
|
|
106
|
-
canonical: url.href,
|
|
107
|
-
noIndexing: hasMapTermOrSkuId,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (!current) {
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
title: current.title || current.name || "",
|
|
117
|
-
description: current.metaTagDescription!,
|
|
118
|
-
noIndexing: hasMapTermOrSkuId,
|
|
119
|
-
canonical: toCanonical(
|
|
120
|
-
new URL(
|
|
121
|
-
current.url && current.pageType !== "Collection"
|
|
122
|
-
? current.url.replace(/^[^/]*\//, "/").toLowerCase()
|
|
123
|
-
: url,
|
|
124
|
-
url,
|
|
125
|
-
),
|
|
126
|
-
currentPage,
|
|
127
|
-
),
|
|
128
|
-
};
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
function toCanonical(url: URL, page?: number) {
|
|
132
|
-
if (typeof page === "number") {
|
|
133
|
-
url.searchParams.set("page", `${page}`);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return url.href;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export { isFilterParam } from "./intelligentSearch";
|
package/src/utils/minicart.ts
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Map a VTEX OrderForm to the canonical `Minicart` contract.
|
|
3
|
-
*
|
|
4
|
-
* Pure function — no I/O, fully unit-testable. Pricing is converted from
|
|
5
|
-
* VTEX's native cents to major units (the canonical unit for `Minicart`).
|
|
6
|
-
*
|
|
7
|
-
* Locale and currency come from `orderForm.storePreferencesData` and follow
|
|
8
|
-
* VTEX's `storePreferencesData.countryCode` / `currencyCode` semantics.
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```ts
|
|
12
|
-
* import { vtexOrderFormToMinicart } from "@decocms/apps/vtex/utils/minicart";
|
|
13
|
-
* import { getCart } from "@decocms/apps/vtex/loaders/cart";
|
|
14
|
-
*
|
|
15
|
-
* const orderForm = await getCart(orderFormId);
|
|
16
|
-
* const minicart = vtexOrderFormToMinicart(orderForm, {
|
|
17
|
-
* freeShippingTarget: 0,
|
|
18
|
-
* checkoutHref: "/checkout",
|
|
19
|
-
* });
|
|
20
|
-
* ```
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import type { Minicart, MinicartItem } from "@decocms/apps-commerce/types";
|
|
24
|
-
import type { OrderForm, OrderFormItem, Totalizer } from "../types";
|
|
25
|
-
|
|
26
|
-
export interface VtexOrderFormToMinicartOptions {
|
|
27
|
-
/** Free-shipping threshold in major units. `0` disables the progress bar. */
|
|
28
|
-
freeShippingTarget?: number;
|
|
29
|
-
/** Override the OrderForm's `clientPreferencesData.locale` (BCP-47, e.g. `"pt-BR"`). */
|
|
30
|
-
locale?: string;
|
|
31
|
-
/** Where the checkout button sends the user. Default: `/checkout`. */
|
|
32
|
-
checkoutHref?: string;
|
|
33
|
-
/** Whether the UI should expose the coupon input. Default: `true`. */
|
|
34
|
-
enableCoupon?: boolean;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const CENTS_PER_MAJOR = 100;
|
|
38
|
-
|
|
39
|
-
/** Convert VTEX cents to major units. Always returns a finite number. */
|
|
40
|
-
function fromCents(cents: number | undefined | null): number {
|
|
41
|
-
if (cents == null || !Number.isFinite(cents)) return 0;
|
|
42
|
-
return cents / CENTS_PER_MAJOR;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function findTotalizer(totalizers: Totalizer[] | undefined, id: string): number {
|
|
46
|
-
if (!totalizers) return 0;
|
|
47
|
-
const t = totalizers.find((x) => x.id === id);
|
|
48
|
-
return t?.value ?? 0;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Locale heuristic. VTEX exposes `clientPreferencesData.locale` when set, but
|
|
53
|
-
* otherwise we synthesize one from `storePreferencesData.countryCode` so the UI
|
|
54
|
-
* always has a usable value for `Intl.NumberFormat`.
|
|
55
|
-
*/
|
|
56
|
-
function inferLocale(orderForm: OrderForm, override?: string): string {
|
|
57
|
-
if (override) return override;
|
|
58
|
-
const explicit = orderForm.clientPreferencesData?.locale;
|
|
59
|
-
if (explicit) return explicit;
|
|
60
|
-
|
|
61
|
-
const country = orderForm.storePreferencesData?.countryCode;
|
|
62
|
-
if (country === "BRA" || country === "BR") return "pt-BR";
|
|
63
|
-
if (country === "USA" || country === "US") return "en-US";
|
|
64
|
-
return "en-US";
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function vtexItemToMinicartItem(item: OrderFormItem, index: number, coupon?: string): MinicartItem {
|
|
68
|
-
const sellingPrice = fromCents(item.sellingPrice ?? item.price);
|
|
69
|
-
const listPrice = fromCents(item.listPrice ?? item.price);
|
|
70
|
-
const discount = Math.max(0, listPrice - sellingPrice);
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
// AnalyticsItem identifier — VTEX uses productId; sites map to numeric SKU
|
|
74
|
-
// when needed via `Number(item.item_id)` (see bagaggio Minicart).
|
|
75
|
-
item_id: item.id,
|
|
76
|
-
item_group_id: item.productId,
|
|
77
|
-
item_name: item.name ?? item.skuName ?? "",
|
|
78
|
-
item_variant: item.skuName,
|
|
79
|
-
item_brand: item.additionalInfo?.brandName ?? undefined,
|
|
80
|
-
item_url: item.detailUrl,
|
|
81
|
-
coupon,
|
|
82
|
-
affiliation: item.seller,
|
|
83
|
-
index,
|
|
84
|
-
// Cart-required fields
|
|
85
|
-
image: item.imageUrl?.replace(/^http:/, "https:") ?? "",
|
|
86
|
-
listPrice,
|
|
87
|
-
price: sellingPrice,
|
|
88
|
-
quantity: item.quantity,
|
|
89
|
-
discount: Number(discount.toFixed(2)),
|
|
90
|
-
// Platform-specific
|
|
91
|
-
seller: item.seller,
|
|
92
|
-
attachments: item.attachments as MinicartItem["attachments"],
|
|
93
|
-
attachmentOfferings: item.attachmentOfferings as MinicartItem["attachmentOfferings"],
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Map a VTEX `OrderForm` to the canonical platform-agnostic `Minicart`.
|
|
99
|
-
*
|
|
100
|
-
* @param orderForm - Result from `getCart()` or `getOrCreateCart()`.
|
|
101
|
-
* @param opts - Storefront-level overrides (free-shipping target, checkout href, ...).
|
|
102
|
-
*/
|
|
103
|
-
export function vtexOrderFormToMinicart(
|
|
104
|
-
orderForm: OrderForm,
|
|
105
|
-
opts: VtexOrderFormToMinicartOptions = {},
|
|
106
|
-
): Minicart<OrderForm> {
|
|
107
|
-
const totalizers = orderForm.totalizers;
|
|
108
|
-
const subtotal = fromCents(findTotalizer(totalizers, "Items"));
|
|
109
|
-
const discountsRaw = findTotalizer(totalizers, "Discounts");
|
|
110
|
-
const discounts = Math.abs(fromCents(discountsRaw));
|
|
111
|
-
const shippingRaw = findTotalizer(totalizers, "Shipping");
|
|
112
|
-
const shipping = totalizers?.some((t) => t.id === "Shipping")
|
|
113
|
-
? fromCents(shippingRaw)
|
|
114
|
-
: undefined;
|
|
115
|
-
const total = fromCents(orderForm.value);
|
|
116
|
-
|
|
117
|
-
const coupon = orderForm.marketingData?.coupon;
|
|
118
|
-
const items = (orderForm.items ?? []).map((item, index) =>
|
|
119
|
-
vtexItemToMinicartItem(item, index, coupon),
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
original: orderForm,
|
|
124
|
-
storefront: {
|
|
125
|
-
items,
|
|
126
|
-
subtotal,
|
|
127
|
-
discounts,
|
|
128
|
-
shipping,
|
|
129
|
-
total,
|
|
130
|
-
coupon,
|
|
131
|
-
locale: inferLocale(orderForm, opts.locale),
|
|
132
|
-
currency: orderForm.storePreferencesData?.currencyCode ?? "BRL",
|
|
133
|
-
enableCoupon: opts.enableCoupon ?? true,
|
|
134
|
-
freeShippingTarget: opts.freeShippingTarget ?? 0,
|
|
135
|
-
checkoutHref: opts.checkoutHref ?? "/checkout",
|
|
136
|
-
postalCode: orderForm.shippingData?.address?.postalCode ?? undefined,
|
|
137
|
-
},
|
|
138
|
-
};
|
|
139
|
-
}
|