@weaverse/hydrogen 5.23.0 → 5.24.0

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