@weaverse/hydrogen 5.24.0 → 6.0.0-beta.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 (80) hide show
  1. package/dist/index.js +23 -2444
  2. package/dist/index.mjs +4 -2407
  3. package/dist/internal.js +64 -0
  4. package/dist/internal.js.map +1 -0
  5. package/dist/internal.mjs +16 -0
  6. package/dist/internal.mjs.map +1 -0
  7. package/dist/next-server.js +14 -0
  8. package/dist/next-server.mjs +2 -0
  9. package/dist/next.js +119 -0
  10. package/dist/next.mjs +5 -0
  11. package/dist/react-router.js +68 -0
  12. package/dist/react-router.mjs +5 -0
  13. package/dist/react.js +26 -0
  14. package/dist/react.mjs +2 -0
  15. package/dist/translation-store-BpPWqZ0B.js +548 -0
  16. package/dist/translation-store-BpPWqZ0B.js.map +1 -0
  17. package/dist/translation-store-C42QdI0e.mjs +441 -0
  18. package/dist/translation-store-C42QdI0e.mjs.map +1 -0
  19. package/dist/types/WeaverseHydrogenRoot.d.ts +7 -7
  20. package/dist/types/WeaverseHydrogenRoot.d.ts.map +1 -1
  21. package/dist/types/components/index.d.ts +2 -2
  22. package/dist/types/components/main.d.ts +2 -2
  23. package/dist/types/hooks/translation-context.d.ts +2 -8
  24. package/dist/types/hooks/translation-context.d.ts.map +1 -1
  25. package/dist/types/index.d.ts +12 -33
  26. package/dist/types/index.d.ts.map +1 -1
  27. package/dist/types/internal.d.ts +24 -0
  28. package/dist/types/internal.d.ts.map +1 -0
  29. package/dist/types/next-server.d.ts +8 -0
  30. package/dist/types/next-server.d.ts.map +1 -0
  31. package/dist/types/next.d.ts +12 -0
  32. package/dist/types/next.d.ts.map +1 -0
  33. package/dist/types/react-router.d.ts +18 -0
  34. package/dist/types/react-router.d.ts.map +1 -0
  35. package/dist/types/react.d.ts +11 -0
  36. package/dist/types/react.d.ts.map +1 -0
  37. package/dist/types/seo.d.ts +4 -4
  38. package/dist/types/seo.d.ts.map +1 -1
  39. package/dist/types/types.d.ts +88 -68
  40. package/dist/types/types.d.ts.map +1 -1
  41. package/dist/types/utils/index.d.ts +4 -5
  42. package/dist/types/utils/index.d.ts.map +1 -1
  43. package/dist/types/utils/nested-key.d.ts +9 -0
  44. package/dist/types/utils/nested-key.d.ts.map +1 -0
  45. package/dist/types/utils/normalize-page-url.d.ts +1 -1
  46. package/dist/types/utils/product-options.d.ts +18 -0
  47. package/dist/types/utils/product-options.d.ts.map +1 -0
  48. package/dist/types/utils/use-studio.d.ts +1 -1
  49. package/dist/types/utils/use-studio.d.ts.map +1 -1
  50. package/dist/types/utils/use-theme-settings-store.d.ts +8 -8
  51. package/dist/types/utils/use-theme-settings-store.d.ts.map +1 -1
  52. package/dist/types/weaverse-client.d.ts +14 -16
  53. package/dist/types/weaverse-client.d.ts.map +1 -1
  54. package/dist/weaverse-client-Clu3u7md.mjs +1966 -0
  55. package/dist/weaverse-client-Clu3u7md.mjs.map +1 -0
  56. package/dist/weaverse-client-yC4TYuVV.js +2073 -0
  57. package/dist/weaverse-client-yC4TYuVV.js.map +1 -0
  58. package/package.json +43 -5
  59. package/readme.md +44 -24
  60. package/src/WeaverseHydrogenRoot.tsx +18 -15
  61. package/src/components/index.ts +2 -2
  62. package/src/components/main.tsx +2 -2
  63. package/src/hooks/translation-context.tsx +3 -28
  64. package/src/index.ts +42 -81
  65. package/src/internal.ts +64 -0
  66. package/src/next-server.ts +13 -0
  67. package/src/next.ts +35 -0
  68. package/src/react-router.ts +52 -0
  69. package/src/react.ts +16 -0
  70. package/src/seo.ts +5 -5
  71. package/src/types.ts +104 -74
  72. package/src/utils/index.ts +15 -14
  73. package/src/utils/nested-key.ts +28 -0
  74. package/src/utils/normalize-page-url.ts +1 -1
  75. package/src/utils/product-options.ts +28 -0
  76. package/src/utils/use-studio.ts +1 -1
  77. package/src/utils/use-theme-settings-store.ts +7 -7
  78. package/src/weaverse-client.ts +88 -96
  79. package/dist/index.js.map +0 -1
  80. package/dist/index.mjs.map +0 -1
@@ -0,0 +1,1966 @@
1
+ import { a as getPreviewData, c as getStudioScriptSrc, d as WeaverseLoadError, f as hasError, h as isProjectIdFunction, i as generateDataFromSchema, l as resolveStudioScriptSrc, n as TranslationStore, o as getRequestQueries, p as hasWeaverseStudio, r as getNestedKey, s as getWeaverseConfigs, u as WeaverseError } from "./translation-store-C42QdI0e.mjs";
2
+ import { Weaverse, WeaverseItemStore, WeaverseRoot, isBrowser, loadScript, useSafeExternalStore } from "@weaverse/react";
3
+ import { createSchema } from "@weaverse/schema";
4
+ import { Suspense, createContext, memo, useContext, useEffect, useMemo, useRef } from "react";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ import { Cache, createFetchWithCache, getSelectedProductOptions } from "@shopify/hydrogen";
7
+ import { ErrorBoundary } from "react-error-boundary";
8
+ import { Await, useLoaderData, useLocation, useMatches, useNavigate, useNavigation, useRevalidator, useRouteLoaderData } from "react-router";
9
+ //#region src/hooks/translation-context.tsx
10
+ /**
11
+ * Replace `{{variable}}` patterns in a string with provided values.
12
+ *
13
+ * @example
14
+ * interpolate('Hello {{name}}!', { name: 'World' }) // => 'Hello World!'
15
+ * interpolate('-{{percentage}}% Off', { percentage: 15 }) // => '-15% Off'
16
+ */
17
+ function interpolate(template, variables) {
18
+ if (!variables) return template;
19
+ return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
20
+ const value = variables[key];
21
+ return value === void 0 ? match : String(value);
22
+ });
23
+ }
24
+ const TranslationContext = createContext(null);
25
+ TranslationContext.displayName = "WeaverseTranslationContext";
26
+ /**
27
+ * Build the `t()` translation function over a fixed set of sources.
28
+ *
29
+ * Resolution order, highest priority first:
30
+ * 1. `externalT` – host i18n (e.g. Shopify), unless it echoes the key
31
+ * 2. `designOverrides` – live design-mode edits, flat dot-path keys
32
+ * 3. `merchantOverrides` – locale overrides from the API, nested
33
+ * 4. `staticContent` – the theme's default locale JSON
34
+ * 5. the key itself
35
+ *
36
+ * Design overrides are looked up per-key (own properties only) rather than
37
+ * merged into `merchantOverrides`: a flat key like `cart.title` must override
38
+ * exactly that leaf without wiping sibling keys (`cart.subtitle`), and a
39
+ * per-key own-property lookup never builds nested objects from
40
+ * merchant-controlled keys, so it cannot pollute `Object.prototype`.
41
+ */
42
+ function createTranslate(sources) {
43
+ const { staticContent, merchantOverrides, designOverrides, externalT } = sources;
44
+ return (key, variables) => {
45
+ if (externalT) {
46
+ const external = externalT(key, variables);
47
+ if (external && external !== key) return external;
48
+ }
49
+ const design = designOverrides && Object.hasOwn(designOverrides, key) ? designOverrides[key] : void 0;
50
+ const override = merchantOverrides ? getNestedKey(merchantOverrides, key) : void 0;
51
+ const staticValue = getNestedKey(staticContent, key);
52
+ return interpolate(design ?? override ?? staticValue ?? key, variables);
53
+ };
54
+ }
55
+ const EMPTY = {};
56
+ const NOOP_SUBSCRIBE = () => () => {};
57
+ const NOOP_SNAPSHOT = () => EMPTY;
58
+ /**
59
+ * Provider that wraps the app to expose the `t()` function.
60
+ *
61
+ * - `staticContent` – the theme's default locale JSON (from themeSchema.i18n)
62
+ * - `merchantOverrides` – locale-specific overrides fetched from the API
63
+ * - `translationStore` – optional store for live design-mode text overrides
64
+ * (the deprecated `themeTextStore` prop is accepted as a fallback)
65
+ */
66
+ function TranslationProvider({ staticContent, merchantOverrides, t: externalT, translationStore, themeTextStore, children }) {
67
+ const store = translationStore ?? themeTextStore ?? null;
68
+ const designOverrides = useSafeExternalStore(store?.subscribe ?? NOOP_SUBSCRIBE, store?.getSnapshot ?? NOOP_SNAPSHOT, store?.getServerSnapshot ?? NOOP_SNAPSHOT);
69
+ const value = useMemo(() => ({
70
+ t: createTranslate({
71
+ staticContent,
72
+ merchantOverrides,
73
+ designOverrides,
74
+ externalT
75
+ }),
76
+ translationStore: store,
77
+ themeTextStore: store,
78
+ merchantOverrides: merchantOverrides ?? null
79
+ }), [
80
+ staticContent,
81
+ merchantOverrides,
82
+ designOverrides,
83
+ externalT,
84
+ store
85
+ ]);
86
+ return /* @__PURE__ */ jsx(TranslationContext.Provider, {
87
+ value,
88
+ children
89
+ });
90
+ }
91
+ /**
92
+ * Hook to access the translation `t()` function.
93
+ *
94
+ * Must be used inside a component wrapped by `withWeaverse` or `TranslationProvider`.
95
+ *
96
+ * @example
97
+ * ```tsx
98
+ * function CartTitle() {
99
+ * const { t } = useTranslation()
100
+ * return <h2>{t('cart.title')}</h2>
101
+ * }
102
+ * ```
103
+ */
104
+ function useTranslation() {
105
+ const ctx = useContext(TranslationContext);
106
+ if (!ctx) throw new Error("useTranslation must be used within <TranslationProvider>. Wrap your app with withWeaverse().");
107
+ return ctx;
108
+ }
109
+ /**
110
+ * Legacy name for {@link TranslationProvider}.
111
+ * @deprecated Use {@link TranslationProvider} instead.
112
+ */
113
+ const ThemeTextProvider = TranslationProvider;
114
+ /**
115
+ * Legacy name for {@link useTranslation}.
116
+ * @deprecated Use {@link useTranslation} instead.
117
+ */
118
+ const useThemeText = useTranslation;
119
+ //#endregion
120
+ //#region src/seo.ts
121
+ /**
122
+ * Convert a Weaverse page-level SEO payload into React Router
123
+ * `MetaDescriptor[]`. Pure, synchronous, tree-shakeable — does not fetch.
124
+ *
125
+ * Behavior:
126
+ * - `title` → top-level `{ title }` descriptor.
127
+ * - `description` / `keywords` → `<meta name="...">`.
128
+ * - `canonicalUrl` → `<link rel="canonical">`.
129
+ * - `openGraph.*` → `<meta property="og:*">`. `og:type` defaults to
130
+ * `'website'` when any other OG field is present.
131
+ * - `twitter.*` → `<meta name="twitter:*">`. `twitter:card` defaults to
132
+ * `'summary'` when any other Twitter field is present.
133
+ * - `robots` is always emitted (defaults to `'index,follow'`) so themes
134
+ * stacking multiple SEO sources have a deterministic last word on
135
+ * indexing for Weaverse-managed pages.
136
+ * - Empty strings / missing fields are filtered before return so empty
137
+ * descriptors don't override earlier sources (e.g. Hydrogen
138
+ * `getSeoMeta`).
139
+ */
140
+ function formatMetaDescriptors(seo) {
141
+ if (!seo) return [{
142
+ name: "robots",
143
+ content: "index,follow"
144
+ }];
145
+ let descriptors = [];
146
+ if (seo.title) descriptors.push({ title: seo.title });
147
+ if (seo.description) descriptors.push({
148
+ name: "description",
149
+ content: seo.description
150
+ });
151
+ if (seo.keywords) descriptors.push({
152
+ name: "keywords",
153
+ content: seo.keywords
154
+ });
155
+ if (seo.canonicalUrl) descriptors.push({
156
+ tagName: "link",
157
+ rel: "canonical",
158
+ href: seo.canonicalUrl
159
+ });
160
+ let og = seo.openGraph;
161
+ if (og && (og.title || og.description || og.image || og.type)) {
162
+ if (og.title) descriptors.push({
163
+ property: "og:title",
164
+ content: og.title
165
+ });
166
+ if (og.description) descriptors.push({
167
+ property: "og:description",
168
+ content: og.description
169
+ });
170
+ if (og.image) descriptors.push({
171
+ property: "og:image",
172
+ content: og.image
173
+ });
174
+ descriptors.push({
175
+ property: "og:type",
176
+ content: og.type || "website"
177
+ });
178
+ }
179
+ let tw = seo.twitter;
180
+ if (tw && (tw.title || tw.description || tw.image || tw.cardType)) {
181
+ descriptors.push({
182
+ name: "twitter:card",
183
+ content: tw.cardType || "summary"
184
+ });
185
+ if (tw.title) descriptors.push({
186
+ name: "twitter:title",
187
+ content: tw.title
188
+ });
189
+ if (tw.description) descriptors.push({
190
+ name: "twitter:description",
191
+ content: tw.description
192
+ });
193
+ if (tw.image) descriptors.push({
194
+ name: "twitter:image",
195
+ content: tw.image
196
+ });
197
+ }
198
+ let index = seo.robots?.index !== false;
199
+ let follow = seo.robots?.follow !== false;
200
+ descriptors.push({
201
+ name: "robots",
202
+ content: `${index ? "index" : "noindex"},${follow ? "follow" : "nofollow"}`
203
+ });
204
+ return descriptors;
205
+ }
206
+ /**
207
+ * Convenience accessor: pull `page.seo` out of a `WeaverseLoaderData`
208
+ * (or `WeaversePageData`) and emit `MetaDescriptor[]`. Null-safe.
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * export const meta: MetaFunction<typeof loader> = ({ data }) => {
213
+ * return getWeaverseSeoMeta(data?.weaverseData)
214
+ * }
215
+ * ```
216
+ */
217
+ function getWeaverseSeoMeta(data) {
218
+ if (!data) return formatMetaDescriptors(null);
219
+ return formatMetaDescriptors(("items" in data && "id" in data ? data : data.page ?? null)?.seo ?? null);
220
+ }
221
+ //#endregion
222
+ //#region src/utils/product-options.ts
223
+ /**
224
+ * Filter out Weaverse-specific query parameters from product options.
225
+ * Removes `isDesignMode` and any parameters starting with `weaverse` prefix.
226
+ *
227
+ * @param request - The incoming HTTP request
228
+ * @returns Filtered product options for Shopify Hydrogen
229
+ *
230
+ * @example
231
+ * ```typescript
232
+ * export async function loader({ request }: LoaderFunctionArgs) {
233
+ * const selectedOptions = getSelectedProductOptions(request)
234
+ * // Use selectedOptions for product queries without Weaverse params
235
+ * }
236
+ * ```
237
+ */
238
+ function getSelectedProductOptions$1(request) {
239
+ return getSelectedProductOptions({ searchParams: new URL(request.url).searchParams }).filter(({ name }) => name !== "isDesignMode" && !name.startsWith("weaverse"));
240
+ }
241
+ //#endregion
242
+ //#region src/components/main.tsx
243
+ function Main(props) {
244
+ let { children, dangerouslySetInnerHTML, ...rest } = props;
245
+ return /* @__PURE__ */ jsx("div", {
246
+ ...rest,
247
+ children,
248
+ dangerouslySetInnerHTML: children ? void 0 : dangerouslySetInnerHTML
249
+ });
250
+ }
251
+ //#endregion
252
+ //#region src/components/index.ts
253
+ /** Built-in components required to render the root of every Weaverse page. */
254
+ let defaultComponents = [{
255
+ default: Main,
256
+ schema: createSchema({
257
+ type: "main",
258
+ title: "Main"
259
+ })
260
+ }];
261
+ //#endregion
262
+ //#region src/hooks/use-weaverse-data-context.ts
263
+ /**
264
+ * Creates flat route-keyed structure from matches
265
+ * Target: {root: {...rootData}, "routes/product": {...productData}}
266
+ * Eliminates all duplication by using route IDs as keys
267
+ */
268
+ function createFlatDataContext(matches) {
269
+ const context = {};
270
+ for (const match of matches) if (match.data && typeof match.data === "object") context[match.id] = match.data;
271
+ return context;
272
+ }
273
+ /**
274
+ * Hook to get flat route-keyed data context from matches
275
+ * Returns: {root: {...}, "routes/xxx": {...}}
276
+ * No more useLoaderData dependency - everything comes from matches
277
+ */
278
+ function useWeaverseDataContext() {
279
+ return createFlatDataContext(useMatches());
280
+ }
281
+ /**
282
+ * Helper function to create flat route-keyed data context from useMatches() result
283
+ * Returns: {root: {...}, "routes/xxx": {...}}
284
+ */
285
+ function createWeaverseDataContext(matches) {
286
+ return createFlatDataContext(matches);
287
+ }
288
+ //#endregion
289
+ //#region src/utils/pick-weaverse-data.ts
290
+ /**
291
+ * Resolve the `weaverseData` payload for a `<WeaverseHydrogenRoot>` instance.
292
+ *
293
+ * Resolution order (first match wins):
294
+ *
295
+ * 1. **Explicit prop** — the caller passed `data` directly. Used as-is,
296
+ * including when it is a `Promise` (handled upstream via `<Await>`) or
297
+ * an explicit `null` (suppresses rendering).
298
+ * 2. **Own route's loader data** — the loader data for the route that
299
+ * rendered this component, obtained via `useLoaderData()`. This is
300
+ * React Router's route-scoped primitive and is what makes nested
301
+ * layout + child Weaverse pages work: each `<WeaverseContent />`
302
+ * resolves to *its own* route's `weaverseData` regardless of siblings.
303
+ * 3. **Ancestor match fallback** — walk `useMatches()` from leaf to root
304
+ * and return the first ancestor whose `loaderData` exposes a
305
+ * `weaverseData` key. This preserves the long-standing behaviour where
306
+ * a child route with no loader (e.g. an `index.tsx` that omits
307
+ * `weaverseData`) still renders its layout's Weaverse page.
308
+ *
309
+ * The helper is intentionally pure (no hook calls) so it can be unit-tested
310
+ * without a React Router context. Hook calls happen in the component.
311
+ *
312
+ * ## Back-compat contract
313
+ *
314
+ * Themes that pre-date the `data` prop addition pass `dataProp === undefined`,
315
+ * so resolution starts at Tier 2. For the canonical single-instance pattern
316
+ * (one `<WeaverseContent />` inside a route whose loader returns
317
+ * `{ weaverseData }`), Tier 2 returns the same value the old `useMatches()`
318
+ * walk did. For the index-without-loader pattern (leaf route exposes no
319
+ * `weaverseData`, layout does), Tier 3's leaf→root walk produces an
320
+ * identical result to the pre-fix code. The only intentional behaviour
321
+ * change is the issue #451 case: two `<WeaverseContent />` instances on the
322
+ * same URL (layout + child, both with their own `weaverseData`) now each
323
+ * render their own page instead of both rendering the deepest one.
324
+ */
325
+ function pickWeaverseData(dataProp, ownLoaderData, matches) {
326
+ if (dataProp !== void 0) return dataProp;
327
+ if (hasWeaverseData(ownLoaderData)) return ownLoaderData.weaverseData;
328
+ for (let i = matches.length - 1; i >= 0; i--) {
329
+ const loaderData = matches[i].loaderData;
330
+ if (hasWeaverseData(loaderData)) return loaderData.weaverseData;
331
+ }
332
+ return null;
333
+ }
334
+ function hasWeaverseData(value) {
335
+ if (typeof value !== "object" || value === null || !("weaverseData" in value)) return false;
336
+ return value.weaverseData !== void 0;
337
+ }
338
+ //#endregion
339
+ //#region src/utils/studio-request-info.ts
340
+ function getBrowserLocation() {
341
+ if (typeof window === "undefined") return;
342
+ return window.location;
343
+ }
344
+ function getQueriesFromSearch(search) {
345
+ let queries = {};
346
+ let params = new URLSearchParams(search);
347
+ for (let [key, value] of params.entries()) if (value === "true") queries[key] = true;
348
+ else if (value === "false") queries[key] = false;
349
+ else queries[key] = value;
350
+ return queries;
351
+ }
352
+ /**
353
+ * React Router v7 single-fetch revalidation can render client data returned
354
+ * from a `*.data` request while the browser is still on the canonical URL.
355
+ * Instance identity must follow the browser URL, not the transient data
356
+ * endpoint, otherwise the SDK creates a fresh Weaverse instance mid-session:
357
+ * in design mode that drops the existing Studio bridge/interactions, and in
358
+ * the live theme it resets every section's runtime state after any
359
+ * revalidation (cart mutations, locale switches, `revalidate()`). See #451.
360
+ */
361
+ function normalizeRequestInfo(params, browserLocation = getBrowserLocation()) {
362
+ if (!browserLocation) return params;
363
+ let { pathname, search } = browserLocation;
364
+ if (params.requestInfo.pathname === pathname && params.requestInfo.search === search) return params;
365
+ return {
366
+ ...params,
367
+ requestInfo: {
368
+ ...params.requestInfo,
369
+ pathname,
370
+ queries: getQueriesFromSearch(search),
371
+ search
372
+ }
373
+ };
374
+ }
375
+ //#endregion
376
+ //#region src/utils/is-same-loader-payload.ts
377
+ /**
378
+ * Structural comparison used to detect whether a reused Weaverse instance
379
+ * must adopt a fresh loader payload.
380
+ *
381
+ * `JSON.stringify` cannot do this job on the client:
382
+ *
383
+ * - Route loader data reaches `dataContext` unserialized, so deferred values
384
+ * are still `Promise`s. React 19 development builds hang an enumerable,
385
+ * self-referencing `_debugInfo` off those promises, and stringifying one
386
+ * throws `TypeError: Converting circular structure to JSON` during render
387
+ * (issue #511).
388
+ * - In production every promise stringifies to `{}`, so a *fresh* deferred
389
+ * value looks unchanged and the reused instance keeps serving the previous
390
+ * render's promise. Tokenizing promises to a constant (`'[[promise]]'`) has
391
+ * the same defect — it is stable by construction, therefore blind to
392
+ * identity.
393
+ *
394
+ * Semantics, chosen deliberately:
395
+ *
396
+ * - **Promises/thenables are atomic**: compared by identity, never traversed.
397
+ * A fresh promise is a change; the same promise is not. Debug metadata is
398
+ * invisible to the comparison.
399
+ * - **Opaque objects are atomic**: anything that is not a plain object, an
400
+ * array, or `toJSON`-serializable (`Map`, `Set`, `URL`, `RegExp`, streams,
401
+ * async iterators, cross-realm promises, class instances) is compared by
402
+ * identity. `JSON.stringify` flattens all of them to `{}`, which would
403
+ * silently collapse two distinct async values into "unchanged".
404
+ * - **Cyclic graphs are compared coinductively**: a pair already being
405
+ * compared is assumed equal, so equal self/mutual cycles are equal while a
406
+ * differing leaf anywhere still reports a change. Nothing throws.
407
+ * - **JSON-visible fields only**: `undefined`/function/symbol properties are
408
+ * omitted from objects and read as `null` in arrays, and NaN/±Infinity read
409
+ * as `null` — matching the previous `JSON.stringify` behavior for plain wire
410
+ * payloads. Key order is irrelevant, which removes the old "false mismatch
411
+ * costs a re-render" caveat.
412
+ */
413
+ /** `JSON.stringify` drops these from objects and nulls them inside arrays. */
414
+ function isJsonOmitted(value) {
415
+ return value === void 0 || typeof value === "function" || typeof value === "symbol";
416
+ }
417
+ function isObjectLike(value) {
418
+ return value !== null && typeof value === "object";
419
+ }
420
+ /** Duck-typed so cross-realm promises and hand-rolled thenables also match. */
421
+ function isThenable(value) {
422
+ return typeof value.then === "function";
423
+ }
424
+ function isPlainObject$1(value) {
425
+ const proto = Object.getPrototypeOf(value);
426
+ return proto === Object.prototype || proto === null;
427
+ }
428
+ /** Returns the `toJSON()` projection, or the value itself when there is none. */
429
+ function toJsonValue(value) {
430
+ return typeof value.toJSON === "function" ? value.toJSON() : value;
431
+ }
432
+ /** Maps NaN/±Infinity to `null`, matching how `JSON.stringify` renders them. */
433
+ function toFiniteOrNull(value) {
434
+ return typeof value === "number" && !Number.isFinite(value) ? null : value;
435
+ }
436
+ /** Compares two loader payloads for JSON-visible structural equality. */
437
+ function isSameLoaderPayload(left, right) {
438
+ const activePairs = [];
439
+ function isSame(rawA, rawB) {
440
+ if (Object.is(rawA, rawB)) return true;
441
+ const a = toFiniteOrNull(rawA);
442
+ const b = toFiniteOrNull(rawB);
443
+ if (a === b) return true;
444
+ if (!(isObjectLike(a) && isObjectLike(b))) return false;
445
+ if (isThenable(a) || isThenable(b)) return false;
446
+ for (let index = 0; index < activePairs.length; index += 2) if (activePairs[index] === a && activePairs[index + 1] === b) return true;
447
+ activePairs.push(a, b);
448
+ try {
449
+ return isSameOpenPair(a, b);
450
+ } finally {
451
+ activePairs.length -= 2;
452
+ }
453
+ }
454
+ function isSameOpenPair(a, b) {
455
+ const aJson = toJsonValue(a);
456
+ const bJson = toJsonValue(b);
457
+ if (aJson !== a || bJson !== b) return isSame(aJson, bJson);
458
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
459
+ if (Array.isArray(a) && Array.isArray(b)) return isSameArray(a, b);
460
+ if (!(isPlainObject$1(a) && isPlainObject$1(b))) return false;
461
+ return isSameObject(a, b);
462
+ }
463
+ function isSameArray(a, b) {
464
+ if (a.length !== b.length) return false;
465
+ for (let index = 0; index < a.length; index++) if (!isSame(isJsonOmitted(a[index]) ? null : a[index], isJsonOmitted(b[index]) ? null : b[index])) return false;
466
+ return true;
467
+ }
468
+ function isSameObject(a, b) {
469
+ let aKeyCount = 0;
470
+ for (const key of Object.keys(a)) {
471
+ if (isJsonOmitted(a[key])) continue;
472
+ aKeyCount++;
473
+ if (!isSame(a[key], b[key])) return false;
474
+ }
475
+ let bKeyCount = 0;
476
+ for (const key of Object.keys(b)) if (!isJsonOmitted(b[key])) bKeyCount++;
477
+ return aKeyCount === bKeyCount;
478
+ }
479
+ return isSame(left, right);
480
+ }
481
+ //#endregion
482
+ //#region src/utils/sync-reused-instance.ts
483
+ /**
484
+ * Apply a fresh loader payload to a reused Weaverse instance.
485
+ *
486
+ * With requestInfo normalized to the browser URL, same-URL revalidations
487
+ * (cart mutations, `useRevalidator()`, locale-neutral re-fetches) reuse the
488
+ * cached `window.__weaverses[pageId]` instance instead of constructing a new
489
+ * one — which preserves section state, but would silently drop the fresh
490
+ * `data`/`dataContext` the loaders just returned. Sync them here.
491
+ *
492
+ * Design mode is excluded by the caller: Studio owns the instance data there
493
+ * (unsaved drafts must not be clobbered) and applies updates via
494
+ * `refreshStudio` instead.
495
+ *
496
+ * Change detection uses {@link isSameLoaderPayload}: a structural comparison
497
+ * that treats promises/thenables and other opaque objects as atomic values
498
+ * compared by identity, and tolerates cyclic graphs. `dataContext` carries
499
+ * live route loader data — including unresolved deferred promises whose React
500
+ * development metadata is cyclic — so it cannot be JSON-serialized here.
501
+ */
502
+ function syncReusedInstance(weaverse, params) {
503
+ weaverse.requestInfo = params.requestInfo;
504
+ weaverse.internal = params.internal;
505
+ let dataChanged = !isSameLoaderPayload(weaverse.data, params.data);
506
+ let contextChanged = !isSameLoaderPayload(weaverse.dataContext ?? null, params.dataContext ?? null);
507
+ if (contextChanged) weaverse.dataContext = params.dataContext ?? null;
508
+ if (dataChanged) weaverse.setProjectData(params.data);
509
+ else if (contextChanged) for (const { id } of params.data?.items || []) weaverse.itemInstances.get(id)?.setData({});
510
+ if (dataChanged || contextChanged) weaverse.triggerUpdate();
511
+ }
512
+ //#endregion
513
+ //#region src/utils/use-theme-settings-store.ts
514
+ /** Mutable, subscribable store for theme settings received from the root loader. */
515
+ var ThemeSettingsStore = class {
516
+ /** Current merged theme setting values. */
517
+ settings = {};
518
+ /** Theme schema exposed to Studio, when available. */
519
+ schema;
520
+ /** Storefront-safe environment values, when available. */
521
+ publicEnv;
522
+ listeners = /* @__PURE__ */ new Set();
523
+ hasWarnedListenerCount = false;
524
+ isDestroyed = false;
525
+ /** Creates a theme settings store from root-loader theme data. */
526
+ constructor(data) {
527
+ const { theme, schema, publicEnv } = data || {};
528
+ this.settings = { ...theme };
529
+ this.schema = schema;
530
+ this.publicEnv = publicEnv;
531
+ if (isBrowser) window.__weaverseThemeSettingsStore = this;
532
+ }
533
+ /** Merges setting changes into the current snapshot and notifies subscribers. */
534
+ updateThemeSettings = (newSettings) => {
535
+ if (this.isDestroyed) {
536
+ console.warn("ThemeSettingsStore: Cannot update destroyed store");
537
+ return;
538
+ }
539
+ this.settings = {
540
+ ...this.settings,
541
+ ...newSettings
542
+ };
543
+ this.emit();
544
+ };
545
+ /** Returns the current client snapshot for `useSyncExternalStore`. */
546
+ getSnapshot = () => this.settings;
547
+ /** Returns the current server snapshot for `useSyncExternalStore`. */
548
+ getServerSnapshot = () => this.settings;
549
+ /** Subscribes to setting changes and returns an unsubscribe callback. */
550
+ subscribe = (callback) => {
551
+ if (this.isDestroyed) {
552
+ console.warn("ThemeSettingsStore: Cannot subscribe to destroyed store");
553
+ return () => {};
554
+ }
555
+ this.listeners.add(callback);
556
+ if (!this.hasWarnedListenerCount && this.listeners.size > 500) {
557
+ this.hasWarnedListenerCount = true;
558
+ console.warn(`ThemeSettingsStore: ${this.listeners.size} listeners detected. This may indicate a performance issue. Consider using fewer useThemeSettings() calls.`);
559
+ }
560
+ return () => {
561
+ this.listeners.delete(callback);
562
+ };
563
+ };
564
+ emit = () => {
565
+ if (this.isDestroyed) return;
566
+ const currentListeners = Array.from(this.listeners);
567
+ for (const listener of currentListeners) try {
568
+ listener();
569
+ } catch (error) {
570
+ console.error("ThemeSettingsStore: Listener error", error);
571
+ }
572
+ };
573
+ /** Clears subscribers and detaches the browser-global store reference. */
574
+ destroy = () => {
575
+ if (this.isDestroyed) return;
576
+ this.listeners.clear();
577
+ this.isDestroyed = true;
578
+ if (isBrowser && window.__weaverseThemeSettingsStore === this) window.__weaverseThemeSettingsStore = void 0;
579
+ };
580
+ };
581
+ const ThemeSettingsStoreContext = createContext(null);
582
+ ThemeSettingsStoreContext.displayName = "ThemeSettingsStoreContext";
583
+ function useCreateThemeSettingsStore() {
584
+ const data = useRouteLoaderData("root");
585
+ const storeRef = useRef(null);
586
+ if (!storeRef.current) storeRef.current = new ThemeSettingsStore(data?.weaverseTheme);
587
+ const theme = data?.weaverseTheme?.theme;
588
+ useEffect(() => {
589
+ if (storeRef.current && theme) storeRef.current.updateThemeSettings(theme);
590
+ }, [theme]);
591
+ return storeRef.current;
592
+ }
593
+ function useThemeSettingsStore() {
594
+ const fromContext = useContext(ThemeSettingsStoreContext);
595
+ const data = useRouteLoaderData("root");
596
+ const fallbackRef = useRef(null);
597
+ if (!(fallbackRef.current || fromContext)) fallbackRef.current = new ThemeSettingsStore(data?.weaverseTheme);
598
+ const store = fromContext ?? fallbackRef.current;
599
+ const theme = data?.weaverseTheme?.theme;
600
+ useEffect(() => {
601
+ if (!fromContext && theme) store.updateThemeSettings(theme);
602
+ }, [
603
+ fromContext,
604
+ store,
605
+ theme
606
+ ]);
607
+ return store;
608
+ }
609
+ //#endregion
610
+ //#region src/utils/use-studio.ts
611
+ const STUDIO_READY_POLL_MS = 50;
612
+ const STUDIO_READY_TIMEOUT_MS = 5e3;
613
+ /**
614
+ * Resolve once `window.weaverseStudio` is available, or `null` on timeout.
615
+ * The studio script sets the global when it *executes*; `loadScript` can
616
+ * resolve earlier for tags it did not create (e.g. SSR-injected markup),
617
+ * so never assume readiness right after the script promise settles.
618
+ */
619
+ function waitForStudio() {
620
+ return new Promise((resolve) => {
621
+ let startedAt = Date.now();
622
+ function check() {
623
+ if (hasWeaverseStudio(window)) return resolve(window.weaverseStudio);
624
+ if (Date.now() - startedAt >= STUDIO_READY_TIMEOUT_MS) {
625
+ console.warn("[Weaverse] Studio script did not initialize in time");
626
+ return resolve(null);
627
+ }
628
+ setTimeout(check, STUDIO_READY_POLL_MS);
629
+ }
630
+ check();
631
+ });
632
+ }
633
+ /**
634
+ * Load the Studio bridge script on every design/preview render so Studio's
635
+ * `checkWeaversePage()` handshake is answered instead of timing out — crucially
636
+ * on routes where the page-scoped {@link useStudio} never mounts: 404s, error
637
+ * boundaries, and non-Weaverse routes. The bridge registers its RPC endpoint
638
+ * and `window.weaverseStudio` the moment the script executes, independent of any
639
+ * page binding. Without this, a content-less route never loads the script and
640
+ * Studio reports a false "Connection lost" instead of "this page has no Weaverse
641
+ * content set up".
642
+ *
643
+ * `loadScript` is idempotent, so on content routes this coexists with the
644
+ * page-scoped {@link useStudio} (same src) — that hook still binds the page via
645
+ * `init`. Gating this on whether a route "has Weaverse data" is deliberately
646
+ * avoided: route matches can't tell whether the page-scoped bridge actually
647
+ * mounts (an `ErrorBoundary` replacing a data route, or data passed via the
648
+ * `data` prop), and suppressing the script there would reintroduce the timeout
649
+ * this fixes. Loading the script is harmless; it only registers the responder.
650
+ *
651
+ * Known limitation: on a route whose `weaverseData` is a deferred Promise, the
652
+ * script can load before the page streams in, so `checkWeaversePage()` may
653
+ * briefly observe `NOT_WEAVERSE_PAGE`. The complete fix is a "loading" state in
654
+ * `checkWeaversePage` so the handshake retries rather than latching — that lives
655
+ * in the Studio script, not here. Tracked in Weaverse/builder#2515.
656
+ *
657
+ * Mounted via {@link withWeaverse}, which must be the root route's `Layout`
658
+ * export so the bridge renders even when a route's `ErrorBoundary` replaces the
659
+ * page. Outside Studio (no design/preview params) it is a no-op.
660
+ */
661
+ function useStudioConnect() {
662
+ let { search } = useLocation();
663
+ useEffect(() => {
664
+ let src = resolveStudioScriptSrc(search, typeof window === "undefined" ? "" : window.location.hostname);
665
+ if (src) loadScript(src).catch(console.error);
666
+ }, [search]);
667
+ }
668
+ function useStudio(weaverse) {
669
+ let { revalidate } = useRevalidator();
670
+ let navigation = useNavigation();
671
+ let { pathname, search } = useLocation();
672
+ let navigate = useNavigate();
673
+ let themeSettingsStore = useThemeSettingsStore();
674
+ let { translationStore, themeTextStore, merchantOverrides } = useTranslation();
675
+ let { isDesignMode, weaverseHost, weaverseVersion, isPreviewMode, isRevisionPreview } = weaverse;
676
+ useEffect(() => {
677
+ if (navigation.state !== "idle") return;
678
+ let src = getStudioScriptSrc({
679
+ isDesignMode,
680
+ isPreviewMode,
681
+ isRevisionPreview,
682
+ weaverseHost,
683
+ weaverseVersion
684
+ });
685
+ if (!src) return;
686
+ if (isRevisionPreview || isPreviewMode) loadScript(src).catch(console.error);
687
+ else {
688
+ weaverse.internal = {
689
+ ...weaverse.internal,
690
+ navigate,
691
+ revalidate,
692
+ merchantOverrides,
693
+ themeSettingsStore,
694
+ translationStore,
695
+ themeTextStore
696
+ };
697
+ loadScript(src).then(waitForStudio).then((studio) => studio?.init(weaverse)).catch(console.error);
698
+ }
699
+ }, [
700
+ pathname,
701
+ search,
702
+ navigation.state
703
+ ]);
704
+ }
705
+ //#endregion
706
+ //#region src/WeaverseHydrogenRoot.tsx
707
+ /** Runtime store for one rendered Hydrogen component instance. */
708
+ var WeaverseHydrogenItem = class extends WeaverseItemStore {
709
+ /**
710
+ * Cached merged snapshot (base + translations).
711
+ * Reset to `null` whenever `_store` or the item's translation entry changes.
712
+ */
713
+ _cachedSnapshot = null;
714
+ _snapshotStoreRef = null;
715
+ _snapshotTranslationRef = null;
716
+ /** Creates an item store from serialized component data and its owner. */
717
+ constructor(initialData, weaverse) {
718
+ super(initialData, weaverse);
719
+ const { data, ...rest } = initialData;
720
+ if (!this.Element?.schema) {
721
+ console.error("Element is missing schema or not found!");
722
+ return;
723
+ }
724
+ const schemaData = generateDataFromSchema(this.Element.schema);
725
+ Object.assign(this._store, schemaData, data, rest);
726
+ }
727
+ /** Registered component definition for this item's type. */
728
+ get Element() {
729
+ return super.Element;
730
+ }
731
+ /**
732
+ * Override setData to flatten serialized `data` the same way the constructor
733
+ * does. Core's `initProject()` reuses item stores keyed by item id (identical
734
+ * ids across locales), and the inherited setData only shallow-merges the raw
735
+ * item — fresh settings would land under `_store.data` while the stale
736
+ * flattened values keep rendering.
737
+ *
738
+ * Schema defaults are re-applied ahead of incoming settings because a
739
+ * serialized payload can omit fields — or the optional `data` object itself —
740
+ * when values equal schema defaults. The stored nested `data` is also replaced
741
+ * so the public serialized state does not retain the previous locale payload.
742
+ *
743
+ * An empty update is the context-only refresh from `syncReusedInstance`: swap
744
+ * the store reference so memoized subscribers re-render, and leave the
745
+ * settings untouched.
746
+ */
747
+ setData = (update) => {
748
+ let source = update ?? {};
749
+ let { data, ...rest } = source;
750
+ if (Object.keys(source).length === 0) this._store = { ...this._store };
751
+ else {
752
+ let type = source.type ?? this._store.type;
753
+ let schema = this.weaverse.elementRegistry.get(type)?.schema;
754
+ let isSerializedItem = "id" in source && "type" in source;
755
+ if (schema && (data !== void 0 || isSerializedItem)) {
756
+ let { data: _previousData, ...store } = this._store;
757
+ let schemaData = generateDataFromSchema(schema);
758
+ this._store = {
759
+ ...store,
760
+ ...data === void 0 ? {} : { data },
761
+ ...schemaData,
762
+ ...data,
763
+ ...rest
764
+ };
765
+ } else this._store = {
766
+ ...this._store,
767
+ ...source
768
+ };
769
+ }
770
+ this.triggerUpdate();
771
+ return this.data;
772
+ };
773
+ /**
774
+ * Override getSnapShot to merge translation data when in translation mode.
775
+ * Memoizes the result: returns the same object reference as long as
776
+ * both `_store` and the translation entry haven't changed.
777
+ * This is critical for `useSyncExternalStore` stability.
778
+ */
779
+ getSnapShot = () => {
780
+ const base = this._store;
781
+ const translations = this.weaverse.translationMap?.[this._id];
782
+ if (!translations) return base;
783
+ if (this._cachedSnapshot && this._snapshotStoreRef === base && this._snapshotTranslationRef === translations) return this._cachedSnapshot;
784
+ const merged = { ...base };
785
+ for (const [key, entry] of Object.entries(translations)) if (entry.translatedValue !== void 0) merged[key] = entry.translatedValue;
786
+ this._cachedSnapshot = merged;
787
+ this._snapshotStoreRef = base;
788
+ this._snapshotTranslationRef = translations;
789
+ return merged;
790
+ };
791
+ };
792
+ Weaverse.ItemConstructor = WeaverseHydrogenItem;
793
+ /**
794
+ * Hydrogen-specific Weaverse runtime that owns page data, component stores,
795
+ * Studio state, and translation sidecars for one rendered page.
796
+ */
797
+ var WeaverseHydrogen = class extends Weaverse {
798
+ /** Identifier of the rendered page. */
799
+ pageId;
800
+ /** Runtime services and project metadata exposed to integrations. */
801
+ internal;
802
+ /** Normalized URL and locale information for the current request. */
803
+ requestInfo;
804
+ /** Identifier of the project supplying the page. */
805
+ projectId;
806
+ /** Origin used for Studio assets and editor communication. */
807
+ weaverseHost;
808
+ /** Base URL used for public content API requests. */
809
+ weaverseApiBase;
810
+ /**
811
+ * API key read from environment/configuration for storefront requests or
812
+ * supplied by Studio requests while editing and previewing.
813
+ */
814
+ weaverseApiKey;
815
+ /** Studio asset version supplied by the preview request. */
816
+ weaverseVersion;
817
+ /** Whether the page is running inside the visual editor. */
818
+ isDesignMode;
819
+ /** Whether a single component preview is being rendered. */
820
+ isPreviewMode;
821
+ /** Whether a saved page revision is being previewed. */
822
+ isRevisionPreview;
823
+ /** Component type requested by section preview mode. */
824
+ sectionType;
825
+ /**
826
+ * Translation map received from the builder in design mode.
827
+ * Keyed by item ID, each entry maps field names to translated values.
828
+ * `WeaverseHydrogenItem.getSnapShot()` merges these into the data
829
+ * returned to components, so translations render without monkey-patching.
830
+ */
831
+ translationMap = {};
832
+ /** Locale code for the active translation sidecar. */
833
+ translationLocale = "";
834
+ /** Builder language identifier for the active translation sidecar. */
835
+ translationLanguageId = "";
836
+ /** Creates a page runtime from resolved loader data and request settings. */
837
+ constructor(params) {
838
+ const { internal, pageId, requestInfo, ...coreParams } = params;
839
+ super({ ...coreParams });
840
+ this.internal = internal;
841
+ this.pageId = pageId;
842
+ this.requestInfo = requestInfo;
843
+ this.projectId = params.projectId;
844
+ this.weaverseHost = params.weaverseHost;
845
+ this.weaverseApiBase = params.weaverseApiBase;
846
+ this.weaverseApiKey = params.weaverseApiKey;
847
+ this.weaverseVersion = params.weaverseVersion || "";
848
+ this.isDesignMode = params.isDesignMode ?? false;
849
+ this.isPreviewMode = params.isPreviewMode ?? false;
850
+ this.isRevisionPreview = params.isRevisionPreview ?? false;
851
+ this.sectionType = params.sectionType || "";
852
+ this.extractTranslationSidecar();
853
+ }
854
+ /**
855
+ * Extract translation sidecar data from `this.data` (the page data).
856
+ * The builder attaches `translationMap`, `translationLocale`, and
857
+ * `translationLanguageId` to the page data in design mode.
858
+ */
859
+ extractTranslationSidecar = () => {
860
+ const pageData = this.data;
861
+ if (pageData?.translationMap) {
862
+ this.translationMap = pageData.translationMap;
863
+ this.translationLocale = pageData.translationLocale || "";
864
+ this.translationLanguageId = pageData.translationLanguageId || "";
865
+ }
866
+ };
867
+ /**
868
+ * Set the translation sidecar data.
869
+ * Called by the studio bridge when translation data arrives or changes.
870
+ * Triggers a re-render of all items so translated snapshots are recalculated.
871
+ */
872
+ setTranslationSidecar = (translationMap, locale, languageId) => {
873
+ this.translationMap = translationMap;
874
+ this.translationLocale = locale;
875
+ this.translationLanguageId = languageId;
876
+ this.refreshAllItems();
877
+ };
878
+ /**
879
+ * Update a single translation entry for a specific item field.
880
+ * Used by the studio bridge when the user edits a translatable field.
881
+ */
882
+ updateTranslation = (itemId, key, originalValue, translatedValue) => {
883
+ if (!this.translationMap[itemId]) this.translationMap[itemId] = {};
884
+ this.translationMap[itemId] = {
885
+ ...this.translationMap[itemId],
886
+ [key]: {
887
+ originalValue,
888
+ translatedValue
889
+ }
890
+ };
891
+ const instance = this.itemInstances.get(itemId);
892
+ if (instance) instance.triggerUpdate();
893
+ };
894
+ /**
895
+ * Collect all translation changes across all weaverse instances.
896
+ * Used by the save flow to persist translation edits.
897
+ */
898
+ getTranslationChanges = () => {
899
+ const entries = [];
900
+ const languageId = this.translationLanguageId;
901
+ if (!(this.translationLocale && languageId)) return;
902
+ for (const [itemId, fields] of Object.entries(this.translationMap)) for (const [key, entry] of Object.entries(fields)) entries.push({
903
+ itemId,
904
+ key: `data.${key}`,
905
+ originalValue: entry.originalValue,
906
+ translatedValue: entry.translatedValue
907
+ });
908
+ if (entries.length === 0) return;
909
+ return {
910
+ languageId,
911
+ entries
912
+ };
913
+ };
914
+ /**
915
+ * Override setProjectData to re-extract translation sidecar from new page data.
916
+ */
917
+ setProjectData = (data) => {
918
+ this.data = data;
919
+ this.extractTranslationSidecar();
920
+ this.initProject();
921
+ };
922
+ };
923
+ function createWeaverseInstance(params) {
924
+ const normalizedParams = normalizeRequestInfo(params);
925
+ if (isBrowser) {
926
+ const runtimeWindow = window;
927
+ runtimeWindow.__weaverses = runtimeWindow.__weaverses || {};
928
+ const existing = runtimeWindow.__weaverses[normalizedParams.pageId];
929
+ const isReused = existing?.requestInfo?.pathname === normalizedParams.requestInfo.pathname && existing?.requestInfo?.search === normalizedParams.requestInfo.search;
930
+ const weaverse = isReused ? existing : new WeaverseHydrogen(normalizedParams);
931
+ runtimeWindow.__weaverses[normalizedParams.pageId] = weaverse;
932
+ if (weaverse.isDesignMode) {
933
+ weaverse.requestInfo = normalizedParams.requestInfo;
934
+ if (hasWeaverseStudio(window)) window.weaverseStudio.refreshStudio(normalizedParams);
935
+ } else if (isReused) syncReusedInstance(weaverse, normalizedParams);
936
+ return weaverse;
937
+ }
938
+ return new WeaverseHydrogen(normalizedParams);
939
+ }
940
+ const StudioBridge = memo(({ context }) => {
941
+ useStudio(context);
942
+ return null;
943
+ });
944
+ const StudioConnect = memo(() => {
945
+ useStudioConnect();
946
+ return null;
947
+ });
948
+ /**
949
+ * Component lists already pushed into the element registry. Registration is
950
+ * idempotent (the registry checks `has(type)`), but walking the full list
951
+ * allocates and loops on every render — theme component arrays are
952
+ * module-level constants, so register each list identity once.
953
+ */
954
+ const registeredComponentLists = /* @__PURE__ */ new WeakSet();
955
+ function RenderRoot(props) {
956
+ const { data, components, dataContext } = props;
957
+ if (!registeredComponentLists.has(components)) {
958
+ for (const comp of [...components, ...defaultComponents]) if (comp?.schema?.type) registerComponent({
959
+ type: comp.schema.type,
960
+ Component: comp.default,
961
+ schema: comp.schema,
962
+ loader: comp.loader
963
+ });
964
+ registeredComponentLists.add(components);
965
+ }
966
+ const { page, configs, project, pageAssignment } = data || {};
967
+ const weaverse = createWeaverseInstance({
968
+ ...configs,
969
+ dataContext,
970
+ data: page,
971
+ pageId: page?.id,
972
+ internal: {
973
+ project,
974
+ pageAssignment
975
+ }
976
+ });
977
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(WeaverseRoot, { context: weaverse }), /* @__PURE__ */ jsx(StudioBridge, { context: weaverse })] });
978
+ }
979
+ /**
980
+ * Register a Weaverse component with the global element registry.
981
+ * Must be called before rendering components in WeaverseHydrogenRoot.
982
+ *
983
+ * @param element - Component configuration with schema, Component, and optional loader
984
+ *
985
+ * @example
986
+ * ```typescript
987
+ * import Hero from './sections/Hero'
988
+ * import { schema } from './sections/Hero'
989
+ *
990
+ * registerComponent({
991
+ * type: 'hero',
992
+ * Component: Hero,
993
+ * schema,
994
+ * loader: async ({ data, weaverse }) => {
995
+ * return { someData: 'value' }
996
+ * }
997
+ * })
998
+ * ```
999
+ */
1000
+ function registerComponent(element) {
1001
+ WeaverseHydrogen.registerElement(element);
1002
+ }
1003
+ /**
1004
+ * Renders the Weaverse page for the current React Router route.
1005
+ *
1006
+ * Pass `data` to override automatic route-loader resolution. A promise is
1007
+ * handled with React Router's `Await`, while explicit `null` suppresses output.
1008
+ */
1009
+ const WeaverseHydrogenRoot = memo(({ components, data: dataProp, errorComponent: ErrorComponent = ({ error }) => /* @__PURE__ */ jsx("div", { children: error instanceof Error ? error.message : "An unexpected error occurred" }) }) => {
1010
+ const matches = useMatches();
1011
+ const enhancedDataContext = useMemo(() => createWeaverseDataContext(matches), [matches]);
1012
+ const ownLoaderData = useLoaderData();
1013
+ const weaverseData = useMemo(() => pickWeaverseData(dataProp, ownLoaderData, matches), [
1014
+ dataProp,
1015
+ ownLoaderData,
1016
+ matches
1017
+ ]);
1018
+ if (dataProp === null) return null;
1019
+ if (weaverseData) {
1020
+ if (weaverseData instanceof Promise) return /* @__PURE__ */ jsx(ErrorBoundary, {
1021
+ fallbackRender: ErrorComponent,
1022
+ children: /* @__PURE__ */ jsx(Suspense, { children: /* @__PURE__ */ jsx(Await, {
1023
+ resolve: weaverseData,
1024
+ children: (resolvedData) => /* @__PURE__ */ jsx(RenderRoot, {
1025
+ components,
1026
+ data: resolvedData,
1027
+ dataContext: enhancedDataContext
1028
+ })
1029
+ }) })
1030
+ });
1031
+ return /* @__PURE__ */ jsx(RenderRoot, {
1032
+ components,
1033
+ data: weaverseData,
1034
+ dataContext: enhancedDataContext
1035
+ });
1036
+ }
1037
+ return /* @__PURE__ */ jsx(ErrorComponent, { error: { message: "No Weaverse data found in route matches!" } });
1038
+ });
1039
+ /**
1040
+ * Higher-order component that wraps your app with Weaverse theme settings and
1041
+ * translation context, and mounts the Studio connect bridge.
1042
+ *
1043
+ * Export the result as your root route's **`Layout`** (not the default route
1044
+ * `Component`). In React Router the root `Layout` wraps both the matched route
1045
+ * and its `ErrorBoundary`, so the Studio bridge keeps rendering on 404s and
1046
+ * loader/render errors — exactly the pages where Studio must still answer its
1047
+ * handshake instead of reporting a false "Connection lost". A default-component
1048
+ * wrapper (`export default withWeaverse(App)`) is replaced by the error
1049
+ * boundary on those routes and would drop the bridge.
1050
+ *
1051
+ * @param Component - The root layout component to wrap
1052
+ * @param options - Optional configuration (e.g. an external `t` function)
1053
+ * @returns Wrapped component with theme settings + translation providers
1054
+ *
1055
+ * @example
1056
+ * ```typescript
1057
+ * import { withWeaverse } from '@weaverse/hydrogen'
1058
+ *
1059
+ * // Basic usage — export as the root route Layout
1060
+ * export const Layout = withWeaverse(RootLayout)
1061
+ *
1062
+ * // With an external translation function
1063
+ * export const Layout = withWeaverse(RootLayout, { t: myI18nFunction })
1064
+ * ```
1065
+ */
1066
+ function withWeaverse(Component, options) {
1067
+ return (props) => {
1068
+ const themeSettingsStore = useCreateThemeSettingsStore();
1069
+ const translationData = useTranslationData();
1070
+ const translationStoreRef = useRef(null);
1071
+ if (!translationStoreRef.current) translationStoreRef.current = new TranslationStore();
1072
+ return /* @__PURE__ */ jsx(ThemeSettingsStoreContext.Provider, {
1073
+ value: themeSettingsStore,
1074
+ children: /* @__PURE__ */ jsxs(TranslationProvider, {
1075
+ merchantOverrides: translationData.merchantOverrides,
1076
+ staticContent: translationData.staticContent,
1077
+ t: options?.t,
1078
+ translationStore: translationStoreRef.current,
1079
+ children: [/* @__PURE__ */ jsx(StudioConnect, {}), /* @__PURE__ */ jsx(Component, { ...props })]
1080
+ })
1081
+ });
1082
+ };
1083
+ }
1084
+ /**
1085
+ * Internal hook to read staticContent and merchantOverrides from root loader data.
1086
+ * Similar pattern to useThemeSettingsStore – reads from the 'root' route loader.
1087
+ */
1088
+ function useTranslationData() {
1089
+ const data = useRouteLoaderData("root");
1090
+ return {
1091
+ staticContent: data?.weaverseTheme?.staticContent ?? {},
1092
+ merchantOverrides: data?.weaverseTheme?.merchantOverrides
1093
+ };
1094
+ }
1095
+ /**
1096
+ * React hook to access Weaverse theme settings from anywhere in your component tree.
1097
+ * Must be used within a component wrapped by `withWeaverse` or inside `WeaverseHydrogenRoot`.
1098
+ *
1099
+ * @returns Current theme settings object
1100
+ *
1101
+ * @example
1102
+ * ```typescript
1103
+ * function MyComponent() {
1104
+ * const theme = useThemeSettings()
1105
+ * const primaryColor = theme.colors?.primary
1106
+ *
1107
+ * return <div style={{ color: primaryColor }}>Styled content</div>
1108
+ * }
1109
+ * ```
1110
+ */
1111
+ function useThemeSettings() {
1112
+ const themeSettingsStore = useThemeSettingsStore();
1113
+ return useSafeExternalStore(themeSettingsStore.subscribe, themeSettingsStore.getSnapshot, themeSettingsStore.getServerSnapshot);
1114
+ }
1115
+ //#endregion
1116
+ //#region src/utils/normalize-page-url.ts
1117
+ const KEPT_PARAM_RE = /^(?:__revisionId|isDesignMode|weaverse)/;
1118
+ /**
1119
+ * Normalize the storefront URL sent in the Builder page API request body.
1120
+ *
1121
+ * The POST body participates in Hydrogen's cached fetch key, so a raw
1122
+ * `request.url` fragments the cache by every marketing/tracking param:
1123
+ * each distinct `?utm_…`/`fbclid` URL is a guaranteed Builder round-trip
1124
+ * even when the resolved page is identical — precisely the paid/social
1125
+ * traffic that needs the cache most (builder#2450).
1126
+ *
1127
+ * The Builder reads only the pathname (locale/type/handle fallback) and
1128
+ * the params matched above from this URL, so keep exactly those. Params
1129
+ * are sorted for a stable cache key regardless of their order in the
1130
+ * incoming URL.
1131
+ */
1132
+ function normalizePageUrl(url) {
1133
+ let parsed;
1134
+ try {
1135
+ parsed = new URL(url);
1136
+ } catch {
1137
+ return url;
1138
+ }
1139
+ const kept = [...parsed.searchParams].filter(([key]) => KEPT_PARAM_RE.test(key));
1140
+ kept.sort(([a], [b]) => a.localeCompare(b));
1141
+ const search = new URLSearchParams(kept).toString();
1142
+ let { pathname } = parsed;
1143
+ if (pathname.endsWith(".data")) pathname = pathname.slice(0, -5);
1144
+ return `${parsed.origin}${pathname}${search ? `?${search}` : ""}`;
1145
+ }
1146
+ //#endregion
1147
+ //#region src/weaverse-client.ts
1148
+ /**
1149
+ * Cache duration constants (in seconds).
1150
+ * These values optimize CDN hit rates while maintaining fresh content.
1151
+ */
1152
+ const CACHE_DURATIONS = {
1153
+ /** Freshness for content that can change in Builder */
1154
+ SHORT: 60,
1155
+ /** Long stale window so slow Builder responses do not block storefront rendering */
1156
+ LONG: 86400
1157
+ };
1158
+ /**
1159
+ * Network timeout for fetch requests (in milliseconds).
1160
+ * Default is 10 seconds to balance reliability and user experience.
1161
+ */
1162
+ const DEFAULT_FETCH_TIMEOUT_MS = 1e4;
1163
+ /**
1164
+ * Bounded retry for the public API fetch. A single slow/cold origin response,
1165
+ * a transient 5xx, or a network blip would otherwise make loadPage return null
1166
+ * — rendering the "Please add new section" placeholder or throwing a 404 in
1167
+ * the route. Two attempts (one retry) is enough: by the time the first attempt
1168
+ * times out, the edge proxy's single-flight leader has usually populated its
1169
+ * versioned cache, so the retry lands on a warm HIT.
1170
+ */
1171
+ const MAX_FETCH_ATTEMPTS = 2;
1172
+ const FETCH_RETRY_BACKOFF_MS = 300;
1173
+ /**
1174
+ * Retry only transient failures: request timeouts, network errors, and origin
1175
+ * 5xx. A 4xx is deterministic (e.g. a genuine 404) — retrying just wastes time.
1176
+ */
1177
+ function isRetryableFetchError(error) {
1178
+ if (!(error instanceof WeaverseError)) return false;
1179
+ if (error.code === "TIMEOUT_ERROR" || error.code === "NETWORK_ERROR" || error.code === "INVALID_RESPONSE") return true;
1180
+ return error.code === "FETCH_ERROR" && (error.statusCode ?? 0) >= 500;
1181
+ }
1182
+ /** Parses a JSON body, or returns the raw text when it is not JSON. */
1183
+ function parseResponseText(text) {
1184
+ if (!text) return text;
1185
+ try {
1186
+ return JSON.parse(text);
1187
+ } catch {
1188
+ return text;
1189
+ }
1190
+ }
1191
+ function isPlainObject(value) {
1192
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1193
+ }
1194
+ /**
1195
+ * Whether a successful `/project` body is a page envelope.
1196
+ *
1197
+ * `page: null` is Builder's authoritative "nothing assigned at this URL" and
1198
+ * stays valid; anything without a `page` field, with an `error` field, or with
1199
+ * a page that has no item list is not an answer about this URL at all.
1200
+ */
1201
+ function isProjectEnvelope(payload) {
1202
+ if (!isPlainObject(payload) || hasError(payload) || !("page" in payload)) return false;
1203
+ const { page, project } = payload;
1204
+ return (page === null || isPlainObject(page) && Array.isArray(page.items)) && (project == null || isPlainObject(project));
1205
+ }
1206
+ /**
1207
+ * Whether a successful `/project_configs` body carries theme settings.
1208
+ *
1209
+ * Builder always sends the `theme` field; `null` means the merchant never
1210
+ * saved settings, so schema defaults are the real configuration.
1211
+ */
1212
+ function isThemeConfigsEnvelope(payload) {
1213
+ return isPlainObject(payload) && !hasError(payload) && "theme" in payload && (payload.theme === null || isPlainObject(payload.theme));
1214
+ }
1215
+ /**
1216
+ * Classifies a page-load failure. `'absent'` is Builder's 404 for a project
1217
+ * that does not exist, which keeps resolving to `null` like before.
1218
+ */
1219
+ function classifyPageLoadFailure(error) {
1220
+ if (error instanceof WeaverseLoadError) return error.kind;
1221
+ if (!(error instanceof WeaverseError)) return "invalid-response";
1222
+ if (error.code === "TIMEOUT_ERROR" || error.code === "NETWORK_ERROR") return "unavailable";
1223
+ if (error.code === "INVALID_RESPONSE") return "invalid-response";
1224
+ if (error.code === "FETCH_ERROR") {
1225
+ const status = error.statusCode ?? 0;
1226
+ if (status === 404) return "absent";
1227
+ if (status === 408 || status === 425 || status === 429 || status >= 500) return "unavailable";
1228
+ }
1229
+ return "configuration";
1230
+ }
1231
+ /**
1232
+ * Validators for Builder requests made by this client itself, keyed on the
1233
+ * exact options object (same reasoning as `bodyFreeCacheRequests`). On the
1234
+ * cached path such a request rejects a non-2xx status instead of resolving
1235
+ * to `null`, and only stores a body its validator accepts. Every other
1236
+ * `fetchWithCache` caller keeps the existing behavior.
1237
+ */
1238
+ const builderPayloadValidators = /* @__PURE__ */ new WeakMap();
1239
+ /**
1240
+ * Gives a rejection from Hydrogen's cached fetch the code `fetchOnce` gives
1241
+ * the same failure on the direct path.
1242
+ *
1243
+ * Hydrogen calls the platform `fetch` itself, so a request that never got a
1244
+ * response rejects with the platform's error instead of a `WeaverseError`.
1245
+ * The Fetch standard rejects a network failure with a `TypeError` and an
1246
+ * aborted request with the signal's `AbortError`/`TimeoutError`; those are
1247
+ * outages. Anything else is returned unchanged, so it is not reported as one.
1248
+ */
1249
+ function cachedTransportError(error, url) {
1250
+ if (error instanceof WeaverseError || !(error instanceof Error)) return error;
1251
+ if (error.name === "AbortError" || error.name === "TimeoutError") return new WeaverseError("Request timed out", "TIMEOUT_ERROR", void 0, { url });
1252
+ if (error instanceof TypeError) return new WeaverseError("Network error during fetch", "NETWORK_ERROR", void 0, {
1253
+ url,
1254
+ cause: error.message
1255
+ });
1256
+ return error;
1257
+ }
1258
+ /**
1259
+ * Weaverse API version path
1260
+ */
1261
+ const API_PATH = "v1";
1262
+ /**
1263
+ * Cache strategy for Weaverse API requests: fresh for a minute, then served
1264
+ * stale for a day while it refreshes or while Builder errors.
1265
+ *
1266
+ * Every Builder resource uses it. Hydrogen 2026.4's subrequest cache only read
1267
+ * `max-age` for freshness, so the old per-resource `sMaxAge` values never
1268
+ * changed how long an entry stayed fresh.
1269
+ */
1270
+ const DEFAULT_CACHE_STRATEGY = Cache({
1271
+ maxAge: CACHE_DURATIONS.SHORT,
1272
+ staleWhileRevalidate: CACHE_DURATIONS.LONG,
1273
+ staleIfError: CACHE_DURATIONS.LONG
1274
+ });
1275
+ /**
1276
+ * Options objects created by the internal `project_configs` request whose
1277
+ * subrequest cache identity deliberately excludes the body. Theme settings
1278
+ * carry the storefront origin so Builder can attribute hostnames; hashing the
1279
+ * body would give every domain of one project its own entry.
1280
+ *
1281
+ * The SUCCESSFUL theme payload is the same for every storefront of a project,
1282
+ * which is what makes one shared entry correct. This is not a claim that
1283
+ * Builder can never answer differently per host: a hostname-blocked request
1284
+ * returns a 403 `{ error }` body, and `shouldCacheResponse` rejects anything
1285
+ * `hasError()` matches, so a refusal is never stored under (or served from)
1286
+ * the shared entry. If a future response shape ever varies by host WITHOUT an
1287
+ * `error` field, this exclusion has to be revisited.
1288
+ *
1289
+ * Keyed on the exact options object, NOT on `cacheTarget`: `fetchWithCache`
1290
+ * is public API, so gating on the target would silently collapse the cache of
1291
+ * any consumer who selects `theme-settings` with varying bodies. An outside
1292
+ * caller's options object is never in this set and keeps a body-derived key.
1293
+ *
1294
+ * The WeakSet-membership slot in the cache key (`has(options)`) is the actual
1295
+ * discriminator — no caller-supplied value can reach it. The sentinel is not
1296
+ * load-bearing for that; it only keeps the omitted body legible in cache
1297
+ * debugging instead of a bare `undefined`.
1298
+ */
1299
+ const bodyFreeCacheRequests = /* @__PURE__ */ new WeakSet();
1300
+ const BODY_FREE_CACHE_SENTINEL = "\0weaverse:body-free";
1301
+ /**
1302
+ * Request-scoped client for loading Weaverse pages and theme settings in Hydrogen.
1303
+ *
1304
+ * Create one client from the Hydrogen app context, then call
1305
+ * {@link WeaverseClient.loadPage} from route loaders and
1306
+ * {@link WeaverseClient.loadThemeSettings} from the root loader.
1307
+ */
1308
+ var WeaverseClient = class {
1309
+ /** Builder API version used by this client. */
1310
+ API = API_PATH;
1311
+ /** Stable project configuration shared by page requests. */
1312
+ basePageConfigs;
1313
+ /** Stable request body fields shared by page requests. */
1314
+ basePageRequestBody;
1315
+ /** Resolved project and rendering-mode configuration. */
1316
+ configs;
1317
+ /** Hydrogen cached fetch used for Builder API requests. */
1318
+ cachedFetch;
1319
+ /** Incoming request associated with this client instance. */
1320
+ request;
1321
+ /** Customer Account API client, when the storefront passes one. */
1322
+ customerAccount;
1323
+ /** Hydrogen Storefront API client and locale context. */
1324
+ storefront;
1325
+ /** Components registered for page rendering and component loaders. */
1326
+ components;
1327
+ /** Theme schema used for defaults and Studio editing. */
1328
+ themeSchema;
1329
+ /** Runtime environment variables. */
1330
+ env;
1331
+ /** Cache storage supplied by the Hydrogen runtime. */
1332
+ cache;
1333
+ /** Background execution hook supplied by the runtime. */
1334
+ waitUntil;
1335
+ parsedUrl;
1336
+ componentsByType;
1337
+ fetchTimeoutMs;
1338
+ throwPageLoadFailures;
1339
+ /** Creates a request-scoped client from the storefront's request context. */
1340
+ constructor(args) {
1341
+ this.request = args.request;
1342
+ this.storefront = args.storefront;
1343
+ this.customerAccount = args.customerAccount;
1344
+ this.components = args.components;
1345
+ this.themeSchema = args.themeSchema;
1346
+ this.env = args.env;
1347
+ this.cache = args.cache;
1348
+ this.waitUntil = args.waitUntil;
1349
+ this.parsedUrl = new URL(args.request.url);
1350
+ this.componentsByType = this.buildComponentLookupMap(args.components);
1351
+ this.fetchTimeoutMs = args.fetchTimeoutMs || DEFAULT_FETCH_TIMEOUT_MS;
1352
+ this.throwPageLoadFailures = args.throwPageLoadFailures === true;
1353
+ this.initializeCache(args);
1354
+ this.initializeConfigs(args);
1355
+ }
1356
+ /**
1357
+ * Initialize Hydrogen cache with proper configuration.
1358
+ * Logs warning if waitUntil is missing.
1359
+ */
1360
+ initializeCache(args) {
1361
+ if (!args.waitUntil) console.warn("⚠️ WeaverseClient: waitUntil not provided, background operations may not complete");
1362
+ this.cachedFetch = createFetchWithCache({
1363
+ cache: args.cache,
1364
+ waitUntil: args.waitUntil
1365
+ });
1366
+ }
1367
+ /**
1368
+ * Initialize configs with projectId resolution and fail-fast validation.
1369
+ * Throws WeaverseError if projectId cannot be resolved.
1370
+ */
1371
+ initializeConfigs(args) {
1372
+ const baseConfigs = getWeaverseConfigs(args.request, args.env);
1373
+ const resolvedProjectId = this.resolveProjectIdSync(args.projectId, args.request, args.env, baseConfigs);
1374
+ if (!resolvedProjectId || resolvedProjectId.trim() === "") throw new WeaverseError("Failed to initialize WeaverseClient: No valid projectId found. Set WEAVERSE_PROJECT_ID environment variable or provide projectId parameter.", "MISSING_PROJECT_ID");
1375
+ this.configs = {
1376
+ ...baseConfigs,
1377
+ projectId: resolvedProjectId
1378
+ };
1379
+ this.basePageConfigs = {
1380
+ projectId: this.configs.projectId,
1381
+ weaverseHost: this.configs.weaverseHost,
1382
+ weaverseApiBase: this.configs.weaverseApiBase,
1383
+ weaverseApiKey: this.configs.weaverseApiKey,
1384
+ weaverseVersion: this.configs.weaverseVersion,
1385
+ isDesignMode: this.configs.isDesignMode,
1386
+ isPreviewMode: this.configs.isPreviewMode,
1387
+ isRevisionPreview: this.configs.isRevisionPreview
1388
+ };
1389
+ this.basePageRequestBody = {
1390
+ isDesignMode: this.configs.isDesignMode,
1391
+ i18n: args.storefront.i18n,
1392
+ projectId: this.configs.projectId
1393
+ };
1394
+ Object.freeze(this.configs);
1395
+ Object.freeze(this.basePageConfigs);
1396
+ Object.freeze(this.basePageRequestBody);
1397
+ }
1398
+ /**
1399
+ * Build component lookup map for O(1) access by type.
1400
+ * Significantly improves performance for large component lists.
1401
+ */
1402
+ buildComponentLookupMap(components) {
1403
+ return new Map(components.map((comp) => [comp.schema?.type, comp]));
1404
+ }
1405
+ /**
1406
+ * Extract comprehensive error information from unknown error type.
1407
+ * Preserves error context, codes, and stack traces for debugging.
1408
+ *
1409
+ * @param error - Unknown error to extract information from
1410
+ * @returns Structured error information with message, code, stack, and context
1411
+ *
1412
+ * @example
1413
+ * ```typescript
1414
+ * try {
1415
+ * // ... operation
1416
+ * } catch (error) {
1417
+ * const errorInfo = this.extractErrorInfo(error)
1418
+ * console.error('Operation failed:', errorInfo.message, errorInfo.context)
1419
+ * }
1420
+ * ```
1421
+ */
1422
+ extractErrorInfo(error) {
1423
+ if (error instanceof WeaverseError) return {
1424
+ message: error.message,
1425
+ code: error.code,
1426
+ context: error.context,
1427
+ stack: error.stack
1428
+ };
1429
+ if (error instanceof Error) return {
1430
+ message: error.message,
1431
+ stack: error.stack
1432
+ };
1433
+ return { message: String(error) };
1434
+ }
1435
+ /**
1436
+ * Extract error message only (for backward compatibility).
1437
+ * Use extractErrorInfo() for more detailed error information.
1438
+ *
1439
+ * @deprecated Use extractErrorInfo() instead to preserve error context
1440
+ */
1441
+ extractErrorMessage(error) {
1442
+ return this.extractErrorInfo(error).message;
1443
+ }
1444
+ /**
1445
+ * Validates projectId format (alphanumeric, hyphens, underscores only).
1446
+ * Returns detailed validation result with error message.
1447
+ *
1448
+ * @param projectId - Project identifier to validate
1449
+ * @returns Validation result with error message if invalid
1450
+ */
1451
+ validateProjectId(projectId) {
1452
+ if (!projectId || projectId.trim() === "") return {
1453
+ valid: false,
1454
+ error: "Project ID cannot be empty"
1455
+ };
1456
+ if (!/^[a-zA-Z0-9_-]+$/.test(projectId)) return {
1457
+ valid: false,
1458
+ error: `Invalid format: "${projectId}". Only alphanumeric characters, hyphens, and underscores are allowed.`
1459
+ };
1460
+ return { valid: true };
1461
+ }
1462
+ /**
1463
+ * Resolves projectId with multi-project architecture support.
1464
+ * Uses priority chain to determine the correct project ID.
1465
+ *
1466
+ * @param projectId - Project identifier (string, function, or undefined)
1467
+ * @param request - HTTP request for context access
1468
+ * @param env - Hydrogen environment with variables
1469
+ * @param baseConfigs - Pre-fetched base configs to avoid duplicate calls
1470
+ * @returns Resolved project ID string (empty string if none found)
1471
+ */
1472
+ resolveProjectIdSync(projectId, request, env, baseConfigs) {
1473
+ return this.resolveFromQueryParams(request) || this.resolveFromFunction(projectId) || this.resolveFromString(projectId) || this.resolveFromEnvironment(baseConfigs, request, env) || "";
1474
+ }
1475
+ /**
1476
+ * Priority 1: Resolve projectId from URL query parameters.
1477
+ * Used for design mode and preview mode overrides.
1478
+ */
1479
+ resolveFromQueryParams(request) {
1480
+ return getRequestQueries(request).weaverseProjectId || null;
1481
+ }
1482
+ /**
1483
+ * Priority 2: Resolve projectId from function parameter.
1484
+ * Detects and rejects async functions with helpful error.
1485
+ */
1486
+ resolveFromFunction(projectId) {
1487
+ if (!isProjectIdFunction(projectId)) return null;
1488
+ if (projectId.constructor.name === "AsyncFunction") throw new WeaverseError("Async projectId functions must be awaited before passing to WeaverseClient. Use: projectId: await getProjectAsync() instead of: projectId: getProjectAsync", "ASYNC_PROJECT_ID");
1489
+ try {
1490
+ const result = projectId();
1491
+ if (result instanceof Promise) throw new WeaverseError("Async projectId functions must be awaited before passing to WeaverseClient. Use: projectId: await getProjectAsync() instead of: projectId: getProjectAsync", "ASYNC_PROJECT_ID");
1492
+ if (result && result.trim() !== "") {
1493
+ const trimmed = result.trim();
1494
+ const validation = this.validateProjectId(trimmed);
1495
+ if (validation.valid) return trimmed;
1496
+ throw new WeaverseError(validation.error || "Invalid projectId format", "INVALID_PROJECT_ID");
1497
+ }
1498
+ } catch (err) {
1499
+ if (err instanceof WeaverseError) throw err;
1500
+ throw new WeaverseError(`Failed to evaluate projectId function: ${err instanceof Error ? err.message : String(err)}`, "PROJECT_ID_FUNCTION_ERROR", void 0, { cause: err instanceof Error ? err.stack : String(err) });
1501
+ }
1502
+ return null;
1503
+ }
1504
+ /**
1505
+ * Priority 3: Resolve projectId from explicit string parameter.
1506
+ */
1507
+ resolveFromString(projectId) {
1508
+ if (typeof projectId !== "string") return null;
1509
+ const trimmed = projectId.trim();
1510
+ if (trimmed === "") return null;
1511
+ const validation = this.validateProjectId(trimmed);
1512
+ if (validation.valid) return trimmed;
1513
+ throw new WeaverseError(validation.error || "Invalid projectId format", "INVALID_PROJECT_ID");
1514
+ }
1515
+ /**
1516
+ * Priority 4: Resolve projectId from environment variables.
1517
+ */
1518
+ resolveFromEnvironment(baseConfigs, request, env) {
1519
+ const configs = baseConfigs || getWeaverseConfigs(request, env);
1520
+ if (configs.projectId && configs.projectId.trim() !== "") return configs.projectId.trim();
1521
+ return null;
1522
+ }
1523
+ /**
1524
+ * Helper method for direct fetching in design mode.
1525
+ * Includes 10-second timeout to prevent hanging requests.
1526
+ */
1527
+ fetchOnce = async (url, options, timeoutMs) => {
1528
+ const controller = new AbortController();
1529
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1530
+ try {
1531
+ const response = await fetch(url, {
1532
+ ...options,
1533
+ signal: controller.signal
1534
+ });
1535
+ if (!response.ok) {
1536
+ const errorText = await response.text();
1537
+ throw new WeaverseError(`HTTP ${response.status}: ${response.statusText}`, "FETCH_ERROR", response.status, {
1538
+ url,
1539
+ errorText
1540
+ });
1541
+ }
1542
+ let data;
1543
+ try {
1544
+ data = await response.json();
1545
+ } catch (parseError) {
1546
+ if (parseError?.name === "AbortError") throw parseError;
1547
+ throw new WeaverseError("Response body is not valid JSON", "INVALID_RESPONSE", response.status, { url });
1548
+ }
1549
+ return {
1550
+ data,
1551
+ response
1552
+ };
1553
+ } catch (err) {
1554
+ if (err instanceof WeaverseError) throw err;
1555
+ if (err.name === "AbortError") throw new WeaverseError(`Request timeout after ${timeoutMs}ms`, "TIMEOUT_ERROR", void 0, {
1556
+ url,
1557
+ timeoutMs
1558
+ });
1559
+ throw new WeaverseError("Network error during fetch", "NETWORK_ERROR", void 0, {
1560
+ url,
1561
+ cause: err instanceof Error ? err.message : String(err)
1562
+ });
1563
+ } finally {
1564
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
1565
+ }
1566
+ };
1567
+ directFetch = async (url, options = {}, timeoutMs = this.fetchTimeoutMs) => {
1568
+ let lastError;
1569
+ for (let attempt = 1; attempt <= MAX_FETCH_ATTEMPTS; attempt++) try {
1570
+ return await this.fetchOnce(url, options, timeoutMs);
1571
+ } catch (err) {
1572
+ lastError = err;
1573
+ if (attempt >= MAX_FETCH_ATTEMPTS || !isRetryableFetchError(err)) throw err;
1574
+ await new Promise((resolve) => setTimeout(resolve, FETCH_RETRY_BACKOFF_MS * attempt));
1575
+ }
1576
+ throw lastError;
1577
+ };
1578
+ getApiUrl(endpoint) {
1579
+ const { weaverseApiBase } = this.configs;
1580
+ return `${weaverseApiBase}/api/public/${endpoint}`;
1581
+ }
1582
+ isExternalPublicApiUrl(url) {
1583
+ const { weaverseApiBase, weaverseHost } = this.configs;
1584
+ return weaverseApiBase !== weaverseHost && url.startsWith(`${weaverseApiBase}/api/public/`);
1585
+ }
1586
+ /**
1587
+ * Fetch data with caching support for both design mode and production.
1588
+ * Uses direct fetch in design mode, Hydrogen's cached fetch in production.
1589
+ * Automatically includes projectId in cache key for multi-project isolation.
1590
+ *
1591
+ * @param url - The URL to fetch from
1592
+ * @param options - Fetch options including optional cache strategy
1593
+ * @returns Promise resolving to the fetched data
1594
+ *
1595
+ * @example
1596
+ * ```typescript
1597
+ * const data = await weaverse.fetchWithCache<MyData>(
1598
+ * 'https://api.example.com/data',
1599
+ * { method: 'POST', body: JSON.stringify({ key: 'value' }) }
1600
+ * )
1601
+ * ```
1602
+ */
1603
+ fetchWithCache = async (url, options = {}) => {
1604
+ const { cacheTarget, strategy: strategyOverride, ...fetchOptions } = options;
1605
+ const strategy = strategyOverride || DEFAULT_CACHE_STRATEGY;
1606
+ const { body } = fetchOptions;
1607
+ const cacheKey = [
1608
+ "weaverse-fetch",
1609
+ url,
1610
+ options.method || "GET",
1611
+ bodyFreeCacheRequests.has(options) ? BODY_FREE_CACHE_SENTINEL : body == null ? null : String(body),
1612
+ bodyFreeCacheRequests.has(options),
1613
+ this.configs.projectId,
1614
+ cacheTarget || "default"
1615
+ ];
1616
+ const validateBuilderPayload = builderPayloadValidators.get(options);
1617
+ if (this.configs.isDesignMode || this.configs.isRevisionPreview || this.isExternalPublicApiUrl(url)) return (await this.directFetch(url, fetchOptions)).data;
1618
+ let response;
1619
+ let text;
1620
+ try {
1621
+ response = await this.cachedFetch(url, fetchOptions, {
1622
+ key: cacheKey,
1623
+ strategy,
1624
+ shouldCacheResponse: async ({ text: readText }) => {
1625
+ if (fetchOptions.signal?.aborted) return false;
1626
+ const payload = parseResponseText(await readText());
1627
+ if (validateBuilderPayload && !validateBuilderPayload(payload)) return false;
1628
+ return !hasError(payload) && payload !== null && payload !== "";
1629
+ }
1630
+ });
1631
+ text = await response.text();
1632
+ } catch (error) {
1633
+ throw validateBuilderPayload ? cachedTransportError(error, url) : error;
1634
+ }
1635
+ if (!response.ok) {
1636
+ if (validateBuilderPayload) throw new WeaverseError(`HTTP ${response.status}: ${response.statusText}`, "FETCH_ERROR", response.status, { url });
1637
+ return null;
1638
+ }
1639
+ return parseResponseText(text);
1640
+ };
1641
+ /**
1642
+ * Safe origin of the incoming storefront request: scheme, host and explicit
1643
+ * port. Userinfo, path, query and fragment are dropped, so no customer
1644
+ * route or credential-bearing value leaves the storefront. Returns undefined
1645
+ * for a non-http(s) request so the field is simply omitted.
1646
+ */
1647
+ safeStorefrontOrigin() {
1648
+ const { protocol, host } = this.parsedUrl;
1649
+ if (protocol !== "http:" && protocol !== "https:") return;
1650
+ return host ? `${protocol}//${host}` : void 0;
1651
+ }
1652
+ /**
1653
+ * Load theme settings from Weaverse API with fallback to schema defaults.
1654
+ * Merges remote settings with local theme schema and handles design mode serialization.
1655
+ *
1656
+ * @param strategy - Optional cache strategy override
1657
+ * @returns Promise resolving to theme settings response
1658
+ *
1659
+ * @example
1660
+ * ```typescript
1661
+ * const settings = await weaverse.loadThemeSettings()
1662
+ * const primaryColor = settings.theme.colors?.primary
1663
+ * ```
1664
+ */
1665
+ loadThemeSettings = async (strategy) => {
1666
+ const defaultThemeSettings = generateDataFromSchema(this.themeSchema);
1667
+ try {
1668
+ const { configs } = this;
1669
+ const { projectId, isDesignMode } = configs;
1670
+ if (!projectId) throw new Error("Missing Weaverse projectId!");
1671
+ const url = this.getApiUrl("project_configs");
1672
+ const themeSettingsOptions = {
1673
+ method: "POST",
1674
+ cacheTarget: "theme-settings",
1675
+ strategy,
1676
+ body: JSON.stringify({
1677
+ isDesignMode,
1678
+ projectId,
1679
+ storefrontUrl: this.safeStorefrontOrigin()
1680
+ }),
1681
+ headers: {
1682
+ "Content-Type": "application/json",
1683
+ Accept: "application/json",
1684
+ "Accept-Encoding": "gzip, deflate, br"
1685
+ }
1686
+ };
1687
+ bodyFreeCacheRequests.add(themeSettingsOptions);
1688
+ builderPayloadValidators.set(themeSettingsOptions, isThemeConfigsEnvelope);
1689
+ const [data, merchantOverrides] = await Promise.all([this.fetchWithCache(url, themeSettingsOptions), this.fetchMerchantOverrides(strategy)]);
1690
+ if (!isThemeConfigsEnvelope(data)) throw new WeaverseError("Invalid theme settings response format", "INVALID_RESPONSE");
1691
+ let result = data;
1692
+ if (this.themeSchema?.settings || this.themeSchema?.inspector) if (!result.theme || typeof result.theme !== "object") result.theme = defaultThemeSettings;
1693
+ else result.theme = {
1694
+ ...defaultThemeSettings,
1695
+ ...result.theme
1696
+ };
1697
+ const staticContent = this.themeSchema?.i18n?.staticContent;
1698
+ if (staticContent) result.staticContent = staticContent;
1699
+ if (merchantOverrides) result.merchantOverrides = merchantOverrides;
1700
+ if (this.configs.isDesignMode) result = {
1701
+ ...result,
1702
+ schema: this.themeSchema && typeof this.themeSchema === "object" ? {
1703
+ ...this.themeSchema,
1704
+ settings: this.themeSchema?.settings?.map((group) => ({
1705
+ ...group,
1706
+ inputs: group?.inputs?.map((input) => {
1707
+ if (typeof input?.condition === "function") return {
1708
+ ...input,
1709
+ condition: input.condition.toString()
1710
+ };
1711
+ return input;
1712
+ })
1713
+ }))
1714
+ } : void 0,
1715
+ publicEnv: this.configs.publicEnv
1716
+ };
1717
+ return result;
1718
+ } catch (error) {
1719
+ const errorMessage = "Unable to load theme settings";
1720
+ const errorDetails = this.extractErrorMessage(error);
1721
+ console.error(errorMessage, errorDetails);
1722
+ return {
1723
+ theme: defaultThemeSettings,
1724
+ staticContent: this.themeSchema?.i18n?.staticContent,
1725
+ _error: errorDetails,
1726
+ _loadFailed: true
1727
+ };
1728
+ }
1729
+ };
1730
+ /**
1731
+ * Fetch locale-specific merchant overrides from the translation API.
1732
+ * Returns a nested JSON object with translated values.
1733
+ * Falls back to empty object on error to avoid blocking theme rendering.
1734
+ */
1735
+ fetchMerchantOverrides = async (strategy) => {
1736
+ try {
1737
+ const { projectId, weaverseHost } = this.configs;
1738
+ const i18n = this.storefront.i18n;
1739
+ if (!this.themeSchema?.i18n) return;
1740
+ if (!(projectId && weaverseHost)) return;
1741
+ const url = `${weaverseHost}/api/translation/static?projectId=${projectId}&locale=${`${(i18n?.language || "en").toLowerCase()}-${(i18n?.country || "us").toLowerCase()}`}`;
1742
+ const overrides = await this.fetchWithCache(url, {
1743
+ method: "GET",
1744
+ cacheTarget: "merchant-overrides",
1745
+ strategy,
1746
+ headers: { Accept: "application/json" },
1747
+ signal: AbortSignal.timeout(this.fetchTimeoutMs)
1748
+ });
1749
+ if (typeof overrides === "object" && overrides !== null) return overrides;
1750
+ return;
1751
+ } catch (error) {
1752
+ console.warn("Unable to load merchant overrides, using theme defaults:", this.extractErrorMessage(error));
1753
+ return;
1754
+ }
1755
+ };
1756
+ /**
1757
+ * Generate fallback page with default structure when page data is unavailable.
1758
+ * Used internally when API returns no page or when page needs default content.
1759
+ *
1760
+ * @param message - Custom message to display in the fallback page
1761
+ * @returns WeaversePageData with single main component containing the message
1762
+ */
1763
+ generateFallbackPage = (message) => {
1764
+ const rootId = crypto.randomUUID();
1765
+ return {
1766
+ id: `fallback_page_${Date.now()}`,
1767
+ rootId,
1768
+ name: "Default Page",
1769
+ items: [{
1770
+ type: "main",
1771
+ id: rootId,
1772
+ data: { dangerouslySetInnerHTML: { __html: message } }
1773
+ }]
1774
+ };
1775
+ };
1776
+ /**
1777
+ * Fetch all published custom pages for sitemap generation.
1778
+ * Paginates automatically, never throws, returns accumulated results on partial failure.
1779
+ */
1780
+ async fetchCustomPages(opts) {
1781
+ const { weaverseApiBase, projectId } = this.configs;
1782
+ const entries = [];
1783
+ const MAX_PAGES = 100;
1784
+ let cursor = null;
1785
+ for (let i = 0; i < MAX_PAGES; i++) {
1786
+ const params = new URLSearchParams();
1787
+ if (opts?.locale) params.set("locale", opts.locale);
1788
+ if (opts?.limit) params.set("limit", String(opts.limit));
1789
+ if (cursor) params.set("cursor", cursor);
1790
+ const qs = params.toString();
1791
+ const url = `${weaverseApiBase}/api/public/v1/projects/${projectId}/custom-pages${qs ? `?${qs}` : ""}`;
1792
+ try {
1793
+ const result = await this.fetchWithCache(url, { cacheTarget: "custom-pages" });
1794
+ entries.push(...result.data || []);
1795
+ if (!result.nextCursor) return entries;
1796
+ cursor = result.nextCursor;
1797
+ } catch (err) {
1798
+ const status = err?.status;
1799
+ const msg = err?.message || String(err);
1800
+ console.warn(entries.length === 0 ? `[Weaverse] Failed to fetch custom pages: ${msg}${status ? ` (HTTP ${status})` : ""}` : `[Weaverse] Custom pages pagination interrupted: ${msg}`);
1801
+ return entries;
1802
+ }
1803
+ }
1804
+ console.warn("[Weaverse] Custom pages pagination cap reached (100 iterations)");
1805
+ return entries;
1806
+ }
1807
+ /**
1808
+ * Load page data from Weaverse API with optional route-level project override.
1809
+ * Fetches page content, executes component loaders, and handles empty items fallback.
1810
+ *
1811
+ * @param params - Optional page loading parameters
1812
+ * @param params.type - Page type (INDEX, PAGE, PRODUCT, COLLECTION, etc.)
1813
+ * @param params.handle - Page handle for dynamic routes
1814
+ * @param params.locale - Locale override
1815
+ * @param params.projectId - Route-level project ID override (for multi-project)
1816
+ * @param params.strategy - Custom cache strategy override
1817
+ * @returns Promise resolving to page data or null on error
1818
+ *
1819
+ * @example
1820
+ * ```typescript
1821
+ * // Basic usage
1822
+ * const data = await weaverse.loadPage()
1823
+ *
1824
+ * // With page type
1825
+ * const data = await weaverse.loadPage({ type: 'INDEX' })
1826
+ *
1827
+ * // Route-level project override
1828
+ * const data = await weaverse.loadPage({
1829
+ * type: 'PAGE',
1830
+ * handle: 'about',
1831
+ * projectId: 'campaign-project-123'
1832
+ * })
1833
+ * ```
1834
+ */
1835
+ loadPage = async (params = {}) => {
1836
+ if (params.projectId !== void 0) {
1837
+ const message = typeof params.projectId === "string" ? this.validateProjectId(params.projectId).error : `must be a string. Received: ${typeof params.projectId}`;
1838
+ if (message) {
1839
+ console.error(`Route-level projectId validation failed: ${message}`);
1840
+ if (this.throwPageLoadFailures) throw new WeaverseLoadError("Route-level projectId validation failed", "configuration");
1841
+ return null;
1842
+ }
1843
+ }
1844
+ try {
1845
+ const { request, storefront, basePageRequestBody, basePageConfigs } = this;
1846
+ const { projectId: clientProjectId, weaverseHost, isPreviewMode, sectionType } = this.configs;
1847
+ const effectiveProjectId = params.projectId || clientProjectId;
1848
+ if (isPreviewMode) return getPreviewData(sectionType || "", this.components, weaverseHost);
1849
+ if (!effectiveProjectId) throw new WeaverseLoadError("Missing Weaverse projectId!", "configuration");
1850
+ const { strategy, ...pageLoadParams } = params;
1851
+ const body = {
1852
+ ...basePageRequestBody,
1853
+ projectId: effectiveProjectId,
1854
+ params: pageLoadParams,
1855
+ url: normalizePageUrl(request.url)
1856
+ };
1857
+ const reqInit = {
1858
+ method: "POST",
1859
+ body: JSON.stringify(body),
1860
+ headers: {
1861
+ "Content-Type": "application/json",
1862
+ Accept: "application/json",
1863
+ "Accept-Encoding": "gzip, deflate, br",
1864
+ "X-Visitor-UA": request.headers.get("user-agent") || ""
1865
+ }
1866
+ };
1867
+ const url = this.getApiUrl("project");
1868
+ const projectOptions = {
1869
+ ...reqInit,
1870
+ cacheTarget: "project",
1871
+ strategy
1872
+ };
1873
+ builderPayloadValidators.set(projectOptions, isProjectEnvelope);
1874
+ const projectData = await this.fetchWithCache(url, projectOptions);
1875
+ if (hasError(projectData)) throw new WeaverseLoadError("Weaverse API returned an error body", "invalid-response");
1876
+ if (!isProjectEnvelope(projectData)) {
1877
+ if (this.throwPageLoadFailures) throw new WeaverseLoadError("Invalid project data structure received from API", "invalid-response");
1878
+ console.warn("Invalid project data structure received from API, using fallback data", { projectId: effectiveProjectId });
1879
+ }
1880
+ const responseData = projectData;
1881
+ let page = responseData?.page ?? void 0;
1882
+ let project = responseData?.project;
1883
+ const pageAssignment = responseData?.pageAssignment;
1884
+ if (!project) {
1885
+ console.warn("Weaverse project not found. Id:", effectiveProjectId);
1886
+ project = {
1887
+ id: effectiveProjectId,
1888
+ name: "Weaverse project not found.",
1889
+ weaverseShopId: ""
1890
+ };
1891
+ }
1892
+ if (!page) page = this.generateFallbackPage("<div style=\"text-align: center;\">Please add new section to start.</div>");
1893
+ if (page?.items?.length === 0) page.items.push({
1894
+ type: "main",
1895
+ id: page.rootId || crypto.randomUUID(),
1896
+ data: {}
1897
+ });
1898
+ if (page?.items) {
1899
+ const items = page.items;
1900
+ page.items = await Promise.all(items.map(this.execComponentLoader));
1901
+ }
1902
+ return {
1903
+ page,
1904
+ project,
1905
+ pageAssignment,
1906
+ configs: {
1907
+ ...basePageConfigs,
1908
+ requestInfo: {
1909
+ i18n: storefront.i18n,
1910
+ queries: getRequestQueries(request),
1911
+ pathname: this.parsedUrl.pathname,
1912
+ search: this.parsedUrl.search
1913
+ }
1914
+ }
1915
+ };
1916
+ } catch (error) {
1917
+ const failure = classifyPageLoadFailure(error);
1918
+ const errorMessage = "❌ Page load failed";
1919
+ const errorContext = { projectId: params.projectId || this.configs.projectId };
1920
+ if (error instanceof WeaverseError) console.error(errorMessage, {
1921
+ code: error.code,
1922
+ message: error.message,
1923
+ ...errorContext,
1924
+ ...error.context
1925
+ });
1926
+ else if (error instanceof Error) console.error(errorMessage, {
1927
+ message: error.message,
1928
+ stack: error.stack,
1929
+ ...errorContext
1930
+ });
1931
+ else console.error(errorMessage, {
1932
+ error: String(error),
1933
+ ...errorContext
1934
+ });
1935
+ if (failure === "absent" || !this.throwPageLoadFailures) return null;
1936
+ if (error instanceof WeaverseLoadError) throw error;
1937
+ throw new WeaverseLoadError("Unable to load Weaverse page", failure, error instanceof WeaverseError ? error.statusCode : void 0);
1938
+ }
1939
+ };
1940
+ /** Executes the registered loader for one component and attaches its result. */
1941
+ execComponentLoader = async (item) => {
1942
+ const { data = {}, type, id } = item;
1943
+ const component = this.componentsByType.get(type);
1944
+ const loader = component?.loader;
1945
+ if (typeof loader === "function" && component) try {
1946
+ return {
1947
+ ...item,
1948
+ loaderData: await Promise.resolve(loader({
1949
+ data: {
1950
+ ...generateDataFromSchema(component.schema),
1951
+ ...data
1952
+ },
1953
+ weaverse: this
1954
+ }))
1955
+ };
1956
+ } catch (error) {
1957
+ console.warn("❌ Item loader run failed.", type, id, error);
1958
+ return item;
1959
+ }
1960
+ return item;
1961
+ };
1962
+ };
1963
+ //#endregion
1964
+ export { useThemeText as _, registerComponent as a, createWeaverseDataContext as c, getSelectedProductOptions$1 as d, formatMetaDescriptors as f, interpolate as g, TranslationProvider as h, WeaverseHydrogenRoot as i, useWeaverseDataContext as l, ThemeTextProvider as m, WeaverseHydrogen as n, useThemeSettings as o, getWeaverseSeoMeta as p, WeaverseHydrogenItem as r, withWeaverse as s, WeaverseClient as t, defaultComponents as u, useTranslation as v };
1965
+
1966
+ //# sourceMappingURL=weaverse-client-Clu3u7md.mjs.map