@weaverse/hydrogen 5.16.1 → 5.16.3

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.d.mts CHANGED
@@ -256,10 +256,18 @@ type WithWeaverseOptions = {
256
256
  t?: TranslateFunction;
257
257
  };
258
258
  /**
259
- * Higher-order component that wraps your app with Weaverse theme settings
260
- * and translation context.
259
+ * Higher-order component that wraps your app with Weaverse theme settings and
260
+ * translation context, and mounts the Studio connect bridge.
261
261
  *
262
- * @param Component - The root component to wrap
262
+ * Export the result as your root route's **`Layout`** (not the default route
263
+ * `Component`). In React Router the root `Layout` wraps both the matched route
264
+ * and its `ErrorBoundary`, so the Studio bridge keeps rendering on 404s and
265
+ * loader/render errors — exactly the pages where Studio must still answer its
266
+ * handshake instead of reporting a false "Connection lost". A default-component
267
+ * wrapper (`export default withWeaverse(App)`) is replaced by the error
268
+ * boundary on those routes and would drop the bridge.
269
+ *
270
+ * @param Component - The root layout component to wrap
263
271
  * @param options - Optional configuration (e.g. an external `t` function)
264
272
  * @returns Wrapped component with theme settings + translation providers
265
273
  *
@@ -267,11 +275,11 @@ type WithWeaverseOptions = {
267
275
  * ```typescript
268
276
  * import { withWeaverse } from '@weaverse/hydrogen'
269
277
  *
270
- * // Basic usage
271
- * export default withWeaverse(App)
278
+ * // Basic usage — export as the root route Layout
279
+ * export const Layout = withWeaverse(RootLayout)
272
280
  *
273
- * // With external translation function
274
- * export default withWeaverse(App, { t: myI18nFunction })
281
+ * // With an external translation function
282
+ * export const Layout = withWeaverse(RootLayout, { t: myI18nFunction })
275
283
  * ```
276
284
  */
277
285
  declare function withWeaverse(Component: ComponentType<any>, options?: WithWeaverseOptions): (props: JSX.IntrinsicAttributes) => react_jsx_runtime.JSX.Element;
@@ -394,6 +402,7 @@ declare class WeaverseClient {
394
402
  * Returns same format as withCache.fetch for consistency.
395
403
  * Includes 10-second timeout to prevent hanging requests.
396
404
  */
405
+ private readonly fetchOnce;
397
406
  private readonly directFetch;
398
407
  private getApiUrl;
399
408
  private isExternalPublicApiUrl;
package/dist/index.d.ts CHANGED
@@ -256,10 +256,18 @@ type WithWeaverseOptions = {
256
256
  t?: TranslateFunction;
257
257
  };
258
258
  /**
259
- * Higher-order component that wraps your app with Weaverse theme settings
260
- * and translation context.
259
+ * Higher-order component that wraps your app with Weaverse theme settings and
260
+ * translation context, and mounts the Studio connect bridge.
261
261
  *
262
- * @param Component - The root component to wrap
262
+ * Export the result as your root route's **`Layout`** (not the default route
263
+ * `Component`). In React Router the root `Layout` wraps both the matched route
264
+ * and its `ErrorBoundary`, so the Studio bridge keeps rendering on 404s and
265
+ * loader/render errors — exactly the pages where Studio must still answer its
266
+ * handshake instead of reporting a false "Connection lost". A default-component
267
+ * wrapper (`export default withWeaverse(App)`) is replaced by the error
268
+ * boundary on those routes and would drop the bridge.
269
+ *
270
+ * @param Component - The root layout component to wrap
263
271
  * @param options - Optional configuration (e.g. an external `t` function)
264
272
  * @returns Wrapped component with theme settings + translation providers
265
273
  *
@@ -267,11 +275,11 @@ type WithWeaverseOptions = {
267
275
  * ```typescript
268
276
  * import { withWeaverse } from '@weaverse/hydrogen'
269
277
  *
270
- * // Basic usage
271
- * export default withWeaverse(App)
278
+ * // Basic usage — export as the root route Layout
279
+ * export const Layout = withWeaverse(RootLayout)
272
280
  *
273
- * // With external translation function
274
- * export default withWeaverse(App, { t: myI18nFunction })
281
+ * // With an external translation function
282
+ * export const Layout = withWeaverse(RootLayout, { t: myI18nFunction })
275
283
  * ```
276
284
  */
277
285
  declare function withWeaverse(Component: ComponentType<any>, options?: WithWeaverseOptions): (props: JSX.IntrinsicAttributes) => react_jsx_runtime.JSX.Element;
@@ -394,6 +402,7 @@ declare class WeaverseClient {
394
402
  * Returns same format as withCache.fetch for consistency.
395
403
  * Includes 10-second timeout to prevent hanging requests.
396
404
  */
405
+ private readonly fetchOnce;
397
406
  private readonly directFetch;
398
407
  private getApiUrl;
399
408
  private isExternalPublicApiUrl;
package/dist/index.js CHANGED
@@ -251,6 +251,67 @@ function hasWeaverseStudio(window2) {
251
251
  return "weaverseStudio" in window2 && typeof window2.weaverseStudio === "object";
252
252
  }
253
253
 
254
+ // src/utils/studio-script-src.ts
255
+ var DEFAULT_WEAVERSE_HOST = "https://studio.weaverse.io";
256
+ var TRUSTED_STUDIO_DOMAINS = ["weaverse.io", "weaverse.dev"];
257
+ function isLoopbackHostname(hostname) {
258
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname.endsWith(".localhost");
259
+ }
260
+ function isTrustedStudioHost(host, options) {
261
+ let parsed;
262
+ try {
263
+ parsed = new URL(host);
264
+ } catch {
265
+ return false;
266
+ }
267
+ let { hostname, protocol } = parsed;
268
+ if (options?.allowLoopback && isLoopbackHostname(hostname)) {
269
+ return protocol === "http:" || protocol === "https:";
270
+ }
271
+ if (protocol !== "https:") {
272
+ return false;
273
+ }
274
+ return TRUSTED_STUDIO_DOMAINS.some(
275
+ (domain) => hostname === domain || hostname.endsWith(`.${domain}`)
276
+ );
277
+ }
278
+ function getStudioScriptSrc(mode) {
279
+ let {
280
+ isDesignMode,
281
+ isPreviewMode,
282
+ isRevisionPreview,
283
+ weaverseHost,
284
+ weaverseVersion
285
+ } = mode;
286
+ let base = `${weaverseHost}/static/studio/hydrogen`;
287
+ let version = `?v=${weaverseVersion}`;
288
+ if (isRevisionPreview || isPreviewMode) {
289
+ return `${base}/preview.js${version}`;
290
+ }
291
+ if (isDesignMode) {
292
+ return `${base}/index.js${version}`;
293
+ }
294
+ return null;
295
+ }
296
+ function resolveStudioScriptSrc(search, storefrontHostname = "") {
297
+ let params = new URLSearchParams(search);
298
+ let last = (key) => params.getAll(key).at(-1);
299
+ let host = last("weaverseHost");
300
+ if (host) {
301
+ let allowLoopback = isLoopbackHostname(storefrontHostname);
302
+ if (!isTrustedStudioHost(host, { allowLoopback })) {
303
+ return null;
304
+ }
305
+ }
306
+ return getStudioScriptSrc({
307
+ isDesignMode: last("isDesignMode") === "true",
308
+ isPreviewMode: last("isPreviewMode") === "true",
309
+ isRevisionPreview: params.has("__revisionId"),
310
+ weaverseHost: host || DEFAULT_WEAVERSE_HOST,
311
+ weaverseVersion: last("weaverseVersion") || ""
312
+ });
313
+ }
314
+
254
315
  // src/utils/index.ts
255
316
  var warnedTypes = /* @__PURE__ */ new Set();
256
317
  function logOnce(type, message) {
@@ -314,8 +375,9 @@ function getWeaverseConfigs(request, env) {
314
375
  let url = new URL(request.url);
315
376
  let isRevisionPreview = url.searchParams.has("__revisionId");
316
377
  let configuredWeaverseHost = WEAVERSE_HOST || envFromProcess.WEAVERSE_HOST;
317
- let resolvedWeaverseHost = weaverseHost || configuredWeaverseHost || "https://studio.weaverse.io";
318
- let resolvedWeaverseApiBase = weaverseHost ? weaverseHost : WEAVERSE_PUBLIC_API_BASE || envFromProcess.WEAVERSE_PUBLIC_API_BASE || (configuredWeaverseHost && configuredWeaverseHost !== "https://studio.weaverse.io" ? configuredWeaverseHost : "https://api.weaverse.io");
378
+ let trustedUrlHost = typeof weaverseHost === "string" && isTrustedStudioHost(weaverseHost) ? weaverseHost : "";
379
+ let resolvedWeaverseHost = trustedUrlHost || configuredWeaverseHost || "https://studio.weaverse.io";
380
+ let resolvedWeaverseApiBase = trustedUrlHost ? trustedUrlHost : WEAVERSE_PUBLIC_API_BASE || envFromProcess.WEAVERSE_PUBLIC_API_BASE || (configuredWeaverseHost && configuredWeaverseHost !== "https://studio.weaverse.io" ? configuredWeaverseHost : "https://api.weaverse.io");
319
381
  return {
320
382
  projectId: weaverseProjectId || WEAVERSE_PROJECT_ID || envFromProcess.WEAVERSE_PROJECT_ID || "",
321
383
  weaverseHost: resolvedWeaverseHost,
@@ -720,6 +782,16 @@ function waitForStudio() {
720
782
  check();
721
783
  });
722
784
  }
785
+ function useStudioConnect() {
786
+ let { search } = reactRouter.useLocation();
787
+ react$1.useEffect(() => {
788
+ let hostname = typeof window === "undefined" ? "" : window.location.hostname;
789
+ let src = resolveStudioScriptSrc(search, hostname);
790
+ if (src) {
791
+ react.loadScript(src).catch(console.error);
792
+ }
793
+ }, [search]);
794
+ }
723
795
  function useStudio(weaverse) {
724
796
  let { revalidate } = reactRouter.useRevalidator();
725
797
  let navigation = reactRouter.useNavigation();
@@ -735,22 +807,31 @@ function useStudio(weaverse) {
735
807
  isRevisionPreview
736
808
  } = weaverse;
737
809
  react$1.useEffect(() => {
738
- if (navigation.state === "idle") {
739
- if (isRevisionPreview || isPreviewMode) {
740
- let previewSrc = `${weaverseHost}/static/studio/hydrogen/preview.js?v=${weaverseVersion}`;
741
- react.loadScript(previewSrc).catch(console.error);
742
- } else if (isDesignMode) {
743
- weaverse.internal = {
744
- ...weaverse.internal,
745
- navigate,
746
- revalidate,
747
- merchantOverrides,
748
- themeSettingsStore,
749
- themeTextStore
750
- };
751
- let studioSrc = `${weaverseHost}/static/studio/hydrogen/index.js?v=${weaverseVersion}`;
752
- react.loadScript(studioSrc).then(waitForStudio).then((studio) => studio?.init(weaverse)).catch(console.error);
753
- }
810
+ if (navigation.state !== "idle") {
811
+ return;
812
+ }
813
+ let src = getStudioScriptSrc({
814
+ isDesignMode,
815
+ isPreviewMode,
816
+ isRevisionPreview,
817
+ weaverseHost,
818
+ weaverseVersion
819
+ });
820
+ if (!src) {
821
+ return;
822
+ }
823
+ if (isRevisionPreview || isPreviewMode) {
824
+ react.loadScript(src).catch(console.error);
825
+ } else {
826
+ weaverse.internal = {
827
+ ...weaverse.internal,
828
+ navigate,
829
+ revalidate,
830
+ merchantOverrides,
831
+ themeSettingsStore,
832
+ themeTextStore
833
+ };
834
+ react.loadScript(src).then(waitForStudio).then((studio) => studio?.init(weaverse)).catch(console.error);
754
835
  }
755
836
  }, [pathname, search, navigation.state]);
756
837
  usePixel(weaverse);
@@ -959,6 +1040,10 @@ var StudioBridge = react$1.memo(({ context }) => {
959
1040
  useStudio(context);
960
1041
  return null;
961
1042
  });
1043
+ var StudioConnect = react$1.memo(() => {
1044
+ useStudioConnect();
1045
+ return null;
1046
+ });
962
1047
  var registeredComponentLists = /* @__PURE__ */ new WeakSet();
963
1048
  function RenderRoot(props) {
964
1049
  const { data, components, dataContext } = props;
@@ -1046,14 +1131,17 @@ function withWeaverse(Component, options) {
1046
1131
  if (!themeTextStoreRef.current) {
1047
1132
  themeTextStoreRef.current = new ThemeTextStore();
1048
1133
  }
1049
- return /* @__PURE__ */ jsxRuntime.jsx(ThemeSettingsStoreContext.Provider, { value: themeSettingsStore, children: /* @__PURE__ */ jsxRuntime.jsx(
1134
+ return /* @__PURE__ */ jsxRuntime.jsx(ThemeSettingsStoreContext.Provider, { value: themeSettingsStore, children: /* @__PURE__ */ jsxRuntime.jsxs(
1050
1135
  ThemeTextProvider,
1051
1136
  {
1052
1137
  merchantOverrides: themeTextData.merchantOverrides,
1053
1138
  staticContent: themeTextData.staticContent,
1054
1139
  t: options?.t,
1055
1140
  themeTextStore: themeTextStoreRef.current,
1056
- children: /* @__PURE__ */ jsxRuntime.jsx(Component, { ...props })
1141
+ children: [
1142
+ /* @__PURE__ */ jsxRuntime.jsx(StudioConnect, {}),
1143
+ /* @__PURE__ */ jsxRuntime.jsx(Component, { ...props })
1144
+ ]
1057
1145
  }
1058
1146
  ) });
1059
1147
  };
@@ -1089,7 +1177,11 @@ function normalizePageUrl(url) {
1089
1177
  );
1090
1178
  kept.sort(([a], [b]) => a.localeCompare(b));
1091
1179
  const search = new URLSearchParams(kept).toString();
1092
- return `${parsed.origin}${parsed.pathname}${search ? `?${search}` : ""}`;
1180
+ let { pathname } = parsed;
1181
+ if (pathname.endsWith(".data")) {
1182
+ pathname = pathname.slice(0, -".data".length);
1183
+ }
1184
+ return `${parsed.origin}${pathname}${search ? `?${search}` : ""}`;
1093
1185
  }
1094
1186
 
1095
1187
  // src/weaverse-client.ts
@@ -1104,6 +1196,17 @@ var CACHE_DURATIONS = {
1104
1196
  STATIC: 3600
1105
1197
  };
1106
1198
  var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
1199
+ var MAX_FETCH_ATTEMPTS = 2;
1200
+ var FETCH_RETRY_BACKOFF_MS = 300;
1201
+ function isRetryableFetchError(error) {
1202
+ if (!(error instanceof WeaverseError)) {
1203
+ return false;
1204
+ }
1205
+ if (error.code === "TIMEOUT_ERROR" || error.code === "NETWORK_ERROR") {
1206
+ return true;
1207
+ }
1208
+ return error.code === "FETCH_ERROR" && (error.statusCode ?? 0) >= 500;
1209
+ }
1107
1210
  var API_PATH = "v1";
1108
1211
  var DEFAULT_CACHE_STRATEGY = {
1109
1212
  maxAge: CACHE_DURATIONS.SHORT,
@@ -1394,7 +1497,7 @@ var WeaverseClient = class {
1394
1497
  * Returns same format as withCache.fetch for consistency.
1395
1498
  * Includes 10-second timeout to prevent hanging requests.
1396
1499
  */
1397
- directFetch = async (url, options = {}, timeoutMs = this.fetchTimeoutMs) => {
1500
+ fetchOnce = async (url, options, timeoutMs) => {
1398
1501
  const controller = new AbortController();
1399
1502
  const timeoutId = setTimeout(
1400
1503
  () => controller.abort(),
@@ -1440,6 +1543,23 @@ var WeaverseClient = class {
1440
1543
  }
1441
1544
  }
1442
1545
  };
1546
+ directFetch = async (url, options = {}, timeoutMs = this.fetchTimeoutMs) => {
1547
+ let lastError;
1548
+ for (let attempt = 1; attempt <= MAX_FETCH_ATTEMPTS; attempt++) {
1549
+ try {
1550
+ return await this.fetchOnce(url, options, timeoutMs);
1551
+ } catch (err) {
1552
+ lastError = err;
1553
+ if (attempt >= MAX_FETCH_ATTEMPTS || !isRetryableFetchError(err)) {
1554
+ throw err;
1555
+ }
1556
+ await new Promise(
1557
+ (resolve) => setTimeout(resolve, FETCH_RETRY_BACKOFF_MS * attempt)
1558
+ );
1559
+ }
1560
+ }
1561
+ throw lastError;
1562
+ };
1443
1563
  // Helper method for building API URLs consistently
1444
1564
  getApiUrl(endpoint) {
1445
1565
  const { weaverseApiBase } = this.configs;