@shopify/hydrogen 2025.7.0 → 2025.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,7 @@ var react = require('react');
7
7
  var reactRouter = require('react-router');
8
8
  var jsxRuntime = require('react/jsx-runtime');
9
9
  var hydrogenReact = require('@shopify/hydrogen-react');
10
+ var loadScript = require('@shopify/hydrogen-react/load-script');
10
11
  var graphqlClient = require('@shopify/graphql-client');
11
12
  var cookie = require('worktop/cookie');
12
13
  var cspBuilder = require('content-security-policy-builder');
@@ -281,7 +282,62 @@ var AnalyticsEvent = {
281
282
  // Custom
282
283
  CUSTOM_EVENT: `custom_`
283
284
  };
284
- var CONSENT_API = "https://cdn.shopify.com/shopifycloud/consent-tracking-api/v0.1/consent-tracking-api.js";
285
+
286
+ // src/constants.ts
287
+ var STOREFRONT_REQUEST_GROUP_ID_HEADER = "Custom-Storefront-Request-Group-ID";
288
+ var STOREFRONT_ACCESS_TOKEN_HEADER = "X-Shopify-Storefront-Access-Token";
289
+ var SDK_VARIANT_HEADER = "X-SDK-Variant";
290
+ var SDK_VARIANT_SOURCE_HEADER = "X-SDK-Variant-Source";
291
+ var SDK_VERSION_HEADER = "X-SDK-Version";
292
+ var SHOPIFY_CLIENT_IP_HEADER = "X-Shopify-Client-IP";
293
+ var SHOPIFY_CLIENT_IP_SIG_HEADER = "X-Shopify-Client-IP-Sig";
294
+ var HYDROGEN_SFAPI_PROXY_KEY = "_sfapi_proxy";
295
+ var HYDROGEN_SERVER_TRACKING_KEY = "_server_tracking";
296
+
297
+ // src/utils/server-timing.ts
298
+ function buildServerTimingHeader(values) {
299
+ return Object.entries(values).map(([key, value]) => value ? `${key};desc=${value}` : void 0).filter(Boolean).join(", ");
300
+ }
301
+ function appendServerTimingHeader(response, values) {
302
+ const header = typeof values === "string" ? values : buildServerTimingHeader(values);
303
+ if (header) {
304
+ response.headers.append("Server-Timing", header);
305
+ }
306
+ }
307
+ var trackedTimings = ["_y", "_s", "_cmp"];
308
+ function extractServerTimingHeader(serverTimingHeader) {
309
+ const values = {};
310
+ if (!serverTimingHeader) return values;
311
+ const re = new RegExp(
312
+ `\\b(${trackedTimings.join("|")});desc="?([^",]+)"?`,
313
+ "g"
314
+ );
315
+ let match;
316
+ while ((match = re.exec(serverTimingHeader)) !== null) {
317
+ values[match[1]] = match[2];
318
+ }
319
+ return values;
320
+ }
321
+ function hasServerTimingInNavigationEntry(key) {
322
+ if (typeof window === "undefined") return false;
323
+ try {
324
+ const navigationEntry = window.performance.getEntriesByType(
325
+ "navigation"
326
+ )[0];
327
+ return !!navigationEntry?.serverTiming?.some((entry) => entry.name === key);
328
+ } catch (e) {
329
+ return false;
330
+ }
331
+ }
332
+ function isSfapiProxyEnabled() {
333
+ return hasServerTimingInNavigationEntry(HYDROGEN_SFAPI_PROXY_KEY);
334
+ }
335
+ function hasServerReturnedTrackingValues() {
336
+ return hasServerTimingInNavigationEntry(HYDROGEN_SERVER_TRACKING_KEY);
337
+ }
338
+
339
+ // src/customer-privacy/ShopifyCustomerPrivacy.tsx
340
+ var CONSENT_API = "https://cdn.shopify.com/shopifycloud/consent-tracking-api/v0.2/consent-tracking-api.js";
285
341
  var CONSENT_API_WITH_BANNER = "https://cdn.shopify.com/shopifycloud/privacy-banner/storefront-banner.js";
286
342
  function logMissingConfig(fieldName) {
287
343
  console.error(
@@ -293,19 +349,34 @@ function useCustomerPrivacy(props) {
293
349
  withPrivacyBanner = false,
294
350
  onVisitorConsentCollected,
295
351
  onReady,
296
- ...consentConfig
352
+ checkoutDomain,
353
+ storefrontAccessToken,
354
+ country,
355
+ locale,
356
+ sameDomainForStorefrontApi
297
357
  } = props;
298
- hydrogenReact.useLoadScript(withPrivacyBanner ? CONSENT_API_WITH_BANNER : CONSENT_API, {
358
+ const hasSfapiProxy = react.useMemo(
359
+ () => sameDomainForStorefrontApi ?? isSfapiProxyEnabled(),
360
+ [sameDomainForStorefrontApi]
361
+ );
362
+ const fetchTrackingValuesFromBrowser = react.useMemo(
363
+ () => hasSfapiProxy && !hasServerReturnedTrackingValues(),
364
+ [hasSfapiProxy]
365
+ );
366
+ const cookiesReady = hydrogenReact.useShopifyCookies({
367
+ fetchTrackingValues: fetchTrackingValuesFromBrowser,
368
+ storefrontAccessToken,
369
+ ignoreDeprecatedCookies: true
370
+ });
371
+ const initialTrackingValues = react.useMemo(hydrogenReact.getTrackingValues, [cookiesReady]);
372
+ const { revalidate } = reactRouter.useRevalidator();
373
+ loadScript.useLoadScript(withPrivacyBanner ? CONSENT_API_WITH_BANNER : CONSENT_API, {
299
374
  attributes: {
300
375
  id: "customer-privacy-api"
301
376
  }
302
377
  });
303
- const { observing, setLoaded } = useApisLoaded({
304
- withPrivacyBanner,
305
- onLoaded: onReady
306
- });
378
+ const { observing, setLoaded, apisLoaded } = useApisLoaded({ withPrivacyBanner });
307
379
  const config = react.useMemo(() => {
308
- const { checkoutDomain, storefrontAccessToken } = consentConfig;
309
380
  if (!checkoutDomain) logMissingConfig("checkoutDomain");
310
381
  if (!storefrontAccessToken) logMissingConfig("storefrontAccessToken");
311
382
  if (storefrontAccessToken.startsWith("shpat_") || storefrontAccessToken.length !== 32) {
@@ -313,18 +384,54 @@ function useCustomerPrivacy(props) {
313
384
  `[h2:error:useCustomerPrivacy] It looks like you passed a private access token, make sure to use the public token`
314
385
  );
315
386
  }
387
+ const commonAncestorDomain = parseStoreDomain(checkoutDomain);
388
+ const sfapiDomain = (
389
+ // Check if standard route proxy is enabled in Hydrogen server
390
+ // to use it instead of doing a cross-origin request to checkout.
391
+ hasSfapiProxy && typeof window !== "undefined" ? window.location.host : checkoutDomain
392
+ );
316
393
  const config2 = {
317
- checkoutRootDomain: checkoutDomain,
394
+ // This domain is used to send requests to SFAPI for setting and getting consent.
395
+ checkoutRootDomain: sfapiDomain,
396
+ // Prefix with a dot to ensure this domain is different from checkoutRootDomain.
397
+ // This will ensure old cookies are set for a cross-subdomain checkout setup
398
+ // so that we keep backward compatibility until new cookies are rolled out.
399
+ // Once consent-tracking-api is updated to not rely on cookies anymore, we can remove this.
400
+ storefrontRootDomain: commonAncestorDomain ? "." + commonAncestorDomain : void 0,
318
401
  storefrontAccessToken,
319
- storefrontRootDomain: parseStoreDomain(checkoutDomain),
320
- country: consentConfig.country,
321
- locale: consentConfig.locale
402
+ country,
403
+ locale
322
404
  };
323
405
  return config2;
324
- }, [consentConfig, parseStoreDomain, logMissingConfig]);
406
+ }, [
407
+ logMissingConfig,
408
+ checkoutDomain,
409
+ storefrontAccessToken,
410
+ country,
411
+ locale
412
+ ]);
325
413
  react.useEffect(() => {
326
414
  const consentCollectedHandler = (event) => {
415
+ const latestTrackingValues = hydrogenReact.getTrackingValues();
416
+ if (initialTrackingValues.visitToken !== latestTrackingValues.visitToken || initialTrackingValues.uniqueToken !== latestTrackingValues.uniqueToken) {
417
+ revalidate().catch(() => {
418
+ console.warn(
419
+ "[h2:warn:useCustomerPrivacy] Revalidation failed after consent change."
420
+ );
421
+ });
422
+ }
327
423
  if (onVisitorConsentCollected) {
424
+ const customerPrivacy = getCustomerPrivacy();
425
+ if (customerPrivacy?.shouldShowBanner()) {
426
+ const consentValues = customerPrivacy.currentVisitorConsent();
427
+ if (consentValues) {
428
+ const NO_VALUE = "";
429
+ const noInteraction = consentValues.marketing === NO_VALUE && consentValues.analytics === NO_VALUE && consentValues.preferences === NO_VALUE;
430
+ if (noInteraction) {
431
+ return;
432
+ }
433
+ }
434
+ }
328
435
  onVisitorConsentCollected(event.detail);
329
436
  }
330
437
  };
@@ -350,14 +457,11 @@ function useCustomerPrivacy(props) {
350
457
  },
351
458
  set(value) {
352
459
  if (typeof value === "object" && value !== null && "showPreferences" in value && "loadBanner" in value) {
353
- const privacyBanner = value;
354
- privacyBanner.loadBanner(config);
355
460
  customPrivacyBanner = overridePrivacyBannerMethods({
356
- privacyBanner,
461
+ privacyBanner: value,
357
462
  config
358
463
  });
359
464
  setLoaded.privacyBanner();
360
- emitCustomerPrivacyApiLoaded();
361
465
  }
362
466
  }
363
467
  };
@@ -391,6 +495,8 @@ function useCustomerPrivacy(props) {
391
495
  const customerPrivacy = value2;
392
496
  customCustomerPrivacy = {
393
497
  ...customerPrivacy,
498
+ // Note: this method is not used by the privacy-banner,
499
+ // it bundles its own setTrackingConsent.
394
500
  setTrackingConsent: overrideCustomerPrivacySetTrackingConsent(
395
501
  { customerPrivacy, config }
396
502
  )
@@ -400,7 +506,6 @@ function useCustomerPrivacy(props) {
400
506
  customerPrivacy: customCustomerPrivacy
401
507
  };
402
508
  setLoaded.customerPrivacy();
403
- emitCustomerPrivacyApiLoaded();
404
509
  }
405
510
  }
406
511
  });
@@ -412,6 +517,24 @@ function useCustomerPrivacy(props) {
412
517
  overrideCustomerPrivacySetTrackingConsent,
413
518
  setLoaded.customerPrivacy
414
519
  ]);
520
+ react.useEffect(() => {
521
+ if (!apisLoaded || !cookiesReady) return;
522
+ const customerPrivacy = getCustomerPrivacy();
523
+ if (customerPrivacy && !customerPrivacy.cachedConsent) {
524
+ const trackingValues = hydrogenReact.getTrackingValues();
525
+ if (trackingValues.consent) {
526
+ customerPrivacy.cachedConsent = trackingValues.consent;
527
+ }
528
+ }
529
+ if (withPrivacyBanner) {
530
+ const privacyBanner = getPrivacyBanner();
531
+ if (privacyBanner) {
532
+ privacyBanner.loadBanner(config);
533
+ }
534
+ }
535
+ emitCustomerPrivacyApiLoaded();
536
+ onReady?.();
537
+ }, [apisLoaded, cookiesReady]);
415
538
  const result = {
416
539
  customerPrivacy: getCustomerPrivacy()
417
540
  };
@@ -427,15 +550,12 @@ function emitCustomerPrivacyApiLoaded() {
427
550
  const event = new CustomEvent("shopifyCustomerPrivacyApiLoaded");
428
551
  document.dispatchEvent(event);
429
552
  }
430
- function useApisLoaded({
431
- withPrivacyBanner,
432
- onLoaded
433
- }) {
553
+ function useApisLoaded({ withPrivacyBanner }) {
434
554
  const observing = react.useRef({ customerPrivacy: false, privacyBanner: false });
435
- const [apisLoaded, setApisLoaded] = react.useState(
555
+ const [apisLoadedArray, setApisLoaded] = react.useState(
436
556
  withPrivacyBanner ? [false, false] : [false]
437
557
  );
438
- const loaded = apisLoaded.every(Boolean);
558
+ const apisLoaded = apisLoadedArray.every(Boolean);
439
559
  const setLoaded = {
440
560
  customerPrivacy: () => {
441
561
  if (withPrivacyBanner) {
@@ -451,16 +571,11 @@ function useApisLoaded({
451
571
  setApisLoaded((prev) => [prev[0], true]);
452
572
  }
453
573
  };
454
- react.useEffect(() => {
455
- if (loaded && onLoaded) {
456
- onLoaded();
457
- }
458
- }, [loaded, onLoaded]);
459
- return { observing, setLoaded };
574
+ return { observing, setLoaded, apisLoaded };
460
575
  }
461
576
  function parseStoreDomain(checkoutDomain) {
462
577
  if (typeof window === "undefined") return;
463
- const host = window.document.location.host;
578
+ const host = window.location.host;
464
579
  const checkoutDomainParts = checkoutDomain.split(".").reverse();
465
580
  const currentDomainParts = host.split(".").reverse();
466
581
  const sameDomainParts = [];
@@ -469,7 +584,7 @@ function parseStoreDomain(checkoutDomain) {
469
584
  sameDomainParts.push(part);
470
585
  }
471
586
  });
472
- return sameDomainParts.reverse().join(".");
587
+ return sameDomainParts.reverse().join(".") || void 0;
473
588
  }
474
589
  function overrideCustomerPrivacySetTrackingConsent({
475
590
  customerPrivacy,
@@ -527,7 +642,7 @@ function getPrivacyBanner() {
527
642
  }
528
643
 
529
644
  // package.json
530
- var version = "2025.7.0";
645
+ var version = "2025.7.1";
531
646
 
532
647
  // src/analytics-manager/ShopifyAnalytics.tsx
533
648
  function getCustomerPrivacyRequired() {
@@ -547,6 +662,7 @@ function ShopifyAnalytics({
547
662
  const { subscribe: subscribe2, register: register2, canTrack } = useAnalytics();
548
663
  const [shopifyReady, setShopifyReady] = react.useState(false);
549
664
  const [privacyReady, setPrivacyReady] = react.useState(false);
665
+ const [collectedConsent, setCollectedConsent] = react.useState("");
550
666
  const init = react.useRef(false);
551
667
  const { checkoutDomain, storefrontAccessToken, language } = consent;
552
668
  const { ready: shopifyAnalyticsReady } = register2("Internal_Shopify_Analytics");
@@ -555,14 +671,31 @@ function ShopifyAnalytics({
555
671
  locale: language,
556
672
  checkoutDomain: !checkoutDomain ? "mock.shop" : checkoutDomain,
557
673
  storefrontAccessToken: !storefrontAccessToken ? "abcdefghijklmnopqrstuvwxyz123456" : storefrontAccessToken,
558
- onVisitorConsentCollected: () => setPrivacyReady(true),
559
- onReady: () => setPrivacyReady(true)
674
+ // If we use privacy banner, we should wait until consent is collected.
675
+ // Otherwise, we can consider privacy ready immediately:
676
+ onReady: () => !consent.withPrivacyBanner && setPrivacyReady(true),
677
+ onVisitorConsentCollected: (consent2) => {
678
+ try {
679
+ setCollectedConsent(JSON.stringify(consent2));
680
+ } catch (e) {
681
+ }
682
+ setPrivacyReady(true);
683
+ }
560
684
  });
685
+ const hasUserConsent = react.useMemo(
686
+ // must be initialized with true to avoid removing cookies too early
687
+ () => privacyReady ? canTrack() : true,
688
+ // Make this value depend on collectedConsent to re-run `canTrack()` when consent changes
689
+ [privacyReady, canTrack, collectedConsent]
690
+ );
561
691
  hydrogenReact.useShopifyCookies({
562
- hasUserConsent: privacyReady ? canTrack() : true,
563
- // must be initialized with true
692
+ hasUserConsent,
564
693
  domain,
565
- checkoutDomain
694
+ checkoutDomain,
695
+ // Already done inside useCustomerPrivacy
696
+ fetchTrackingValues: false,
697
+ // Avoid creating local cookies too early
698
+ ignoreDeprecatedCookies: !privacyReady
566
699
  });
567
700
  react.useEffect(() => {
568
701
  if (init.current) return;
@@ -612,11 +745,11 @@ function prepareBasePageViewPayload(payload) {
612
745
  ...payload.shop,
613
746
  hasUserConsent,
614
747
  ...hydrogenReact.getClientBrowserParameters(),
615
- ccpaEnforced: !customerPrivacy.saleOfDataAllowed(),
616
- gdprEnforced: !(customerPrivacy.marketingAllowed() && customerPrivacy.analyticsProcessingAllowed()),
617
748
  analyticsAllowed: customerPrivacy.analyticsProcessingAllowed(),
618
749
  marketingAllowed: customerPrivacy.marketingAllowed(),
619
- saleOfDataAllowed: customerPrivacy.saleOfDataAllowed()
750
+ saleOfDataAllowed: customerPrivacy.saleOfDataAllowed(),
751
+ ccpaEnforced: !customerPrivacy.saleOfDataAllowed(),
752
+ gdprEnforced: !(customerPrivacy.marketingAllowed() && customerPrivacy.analyticsProcessingAllowed())
620
753
  };
621
754
  return eventPayload;
622
755
  }
@@ -1049,11 +1182,11 @@ function AnalyticsProvider({
1049
1182
  shop: shopProp = null,
1050
1183
  cookieDomain
1051
1184
  }) {
1052
- const listenerSet = react.useRef(false);
1053
1185
  const { shop } = useShopAnalytics(shopProp);
1054
1186
  const [analyticsLoaded, setAnalyticsLoaded] = react.useState(
1055
1187
  customCanTrack ? true : false
1056
1188
  );
1189
+ const [consentCollected, setConsentCollected] = react.useState(false);
1057
1190
  const [carts, setCarts] = react.useState({ cart: null, prevCart: null });
1058
1191
  const [canTrack, setCanTrack] = react.useState(
1059
1192
  customCanTrack ? () => customCanTrack : () => shopifyCanTrack
@@ -1121,21 +1254,21 @@ function AnalyticsProvider({
1121
1254
  children,
1122
1255
  !!shop && /* @__PURE__ */ jsxRuntime.jsx(AnalyticsPageView, {}),
1123
1256
  !!shop && !!currentCart && /* @__PURE__ */ jsxRuntime.jsx(CartAnalytics, { cart: currentCart, setCarts }),
1124
- !!shop && consent.checkoutDomain && /* @__PURE__ */ jsxRuntime.jsx(
1257
+ !!shop && /* @__PURE__ */ jsxRuntime.jsx(
1125
1258
  ShopifyAnalytics,
1126
1259
  {
1127
1260
  consent,
1128
1261
  onReady: () => {
1129
- listenerSet.current = true;
1130
1262
  setAnalyticsLoaded(true);
1131
1263
  setCanTrack(
1132
1264
  customCanTrack ? () => customCanTrack : () => shopifyCanTrack
1133
1265
  );
1266
+ setConsentCollected(true);
1134
1267
  },
1135
1268
  domain: cookieDomain
1136
1269
  }
1137
1270
  ),
1138
- !!shop && /* @__PURE__ */ jsxRuntime.jsx(PerfKit, { shop })
1271
+ !!shop && consentCollected && /* @__PURE__ */ jsxRuntime.jsx(PerfKit, { shop })
1139
1272
  ] });
1140
1273
  }
1141
1274
  function useAnalytics() {
@@ -1214,6 +1347,31 @@ function getDebugHeaders(request) {
1214
1347
  purpose: request ? getHeader(request, "purpose") : void 0
1215
1348
  };
1216
1349
  }
1350
+ function getStorefrontHeaders(request) {
1351
+ return {
1352
+ requestGroupId: getHeader(request, "request-id"),
1353
+ buyerIp: getHeader(request, "oxygen-buyer-ip"),
1354
+ buyerIpSig: getHeader(request, SHOPIFY_CLIENT_IP_SIG_HEADER),
1355
+ cookie: getHeader(request, "cookie"),
1356
+ // sec-purpose is added by browsers automatically when using link/prefetch or Speculation Rules
1357
+ purpose: getHeader(request, "sec-purpose") || getHeader(request, "purpose")
1358
+ };
1359
+ }
1360
+ var SFAPI_RE = /^\/api\/(unstable|2\d{3}-\d{2})\/graphql\.json$/;
1361
+ var getSafePathname = (url) => {
1362
+ try {
1363
+ return new URL(url, "http://e.c").pathname;
1364
+ } catch {
1365
+ return "/";
1366
+ }
1367
+ };
1368
+ function extractHeaders(extract, keys) {
1369
+ return keys.reduce((acc, key) => {
1370
+ const forwardedValue = extract(key);
1371
+ if (forwardedValue) acc.push([key, forwardedValue]);
1372
+ return acc;
1373
+ }, []);
1374
+ }
1217
1375
 
1218
1376
  // src/utils/callsites.ts
1219
1377
  function withSyncStack(promise, options = {}) {
@@ -1581,13 +1739,16 @@ async function runWithCache(cacheKey, actionFn, {
1581
1739
  }
1582
1740
  return result;
1583
1741
  }
1742
+ var excludedHeaders = ["set-cookie", "server-timing"];
1584
1743
  function toSerializableResponse(body, response) {
1585
1744
  return [
1586
1745
  body,
1587
1746
  {
1588
1747
  status: response.status,
1589
1748
  statusText: response.statusText,
1590
- headers: Array.from(response.headers.entries())
1749
+ headers: [...response.headers].filter(
1750
+ ([key]) => !excludedHeaders.includes(key.toLowerCase())
1751
+ )
1591
1752
  }
1592
1753
  ];
1593
1754
  }
@@ -1601,7 +1762,8 @@ async function fetchWithServerCache(url, requestInit, {
1601
1762
  shouldCacheResponse,
1602
1763
  waitUntil,
1603
1764
  debugInfo,
1604
- streamConfig
1765
+ streamConfig,
1766
+ onRawHeaders
1605
1767
  }) {
1606
1768
  if (!cacheOptions && (!requestInit.method || requestInit.method === "GET")) {
1607
1769
  cacheOptions = CacheShort();
@@ -1615,6 +1777,7 @@ async function fetchWithServerCache(url, requestInit, {
1615
1777
  url,
1616
1778
  customFetchApi: async (url2, options) => {
1617
1779
  rawResponse = await fetch(url2, options);
1780
+ onRawHeaders?.(rawResponse.headers);
1618
1781
  return rawResponse;
1619
1782
  },
1620
1783
  headers: requestInit.headers
@@ -1638,6 +1801,7 @@ async function fetchWithServerCache(url, requestInit, {
1638
1801
  );
1639
1802
  }
1640
1803
  const response = await fetch(url, requestInit);
1804
+ onRawHeaders?.(response.headers);
1641
1805
  if (!response.ok) {
1642
1806
  return response;
1643
1807
  }
@@ -1861,13 +2025,6 @@ var cartSetIdDefault = (cookieOptions) => {
1861
2025
  };
1862
2026
  };
1863
2027
 
1864
- // src/constants.ts
1865
- var STOREFRONT_REQUEST_GROUP_ID_HEADER = "Custom-Storefront-Request-Group-ID";
1866
- var STOREFRONT_ACCESS_TOKEN_HEADER = "X-Shopify-Storefront-Access-Token";
1867
- var SDK_VARIANT_HEADER = "X-SDK-Variant";
1868
- var SDK_VARIANT_SOURCE_HEADER = "X-SDK-Variant-Source";
1869
- var SDK_VERSION_HEADER = "X-SDK-Version";
1870
-
1871
2028
  // src/utils/uuid.ts
1872
2029
  function generateUUID() {
1873
2030
  if (typeof crypto !== "undefined" && !!crypto.randomUUID) {
@@ -1878,7 +2035,7 @@ function generateUUID() {
1878
2035
  }
1879
2036
 
1880
2037
  // src/version.ts
1881
- var LIB_VERSION = "2025.7.0";
2038
+ var LIB_VERSION = "2025.7.1";
1882
2039
 
1883
2040
  // src/utils/graphql.ts
1884
2041
  function minifyQuery(string) {
@@ -2042,16 +2199,34 @@ function createStorefrontClient(options) {
2042
2199
  contentType: "json",
2043
2200
  buyerIp: storefrontHeaders?.buyerIp || ""
2044
2201
  });
2202
+ if (storefrontHeaders?.buyerIp) {
2203
+ defaultHeaders[SHOPIFY_CLIENT_IP_HEADER] = storefrontHeaders.buyerIp;
2204
+ }
2205
+ if (storefrontHeaders?.buyerIpSig) {
2206
+ defaultHeaders[SHOPIFY_CLIENT_IP_SIG_HEADER] = storefrontHeaders.buyerIpSig;
2207
+ }
2045
2208
  defaultHeaders[STOREFRONT_REQUEST_GROUP_ID_HEADER] = storefrontHeaders?.requestGroupId || generateUUID();
2046
2209
  if (storefrontId) defaultHeaders[hydrogenReact.SHOPIFY_STOREFRONT_ID_HEADER] = storefrontId;
2047
2210
  defaultHeaders["user-agent"] = `Hydrogen ${LIB_VERSION}`;
2048
- if (storefrontHeaders && storefrontHeaders.cookie) {
2049
- const cookies = hydrogenReact.getShopifyCookies(storefrontHeaders.cookie ?? "");
2050
- if (cookies[hydrogenReact.SHOPIFY_Y])
2051
- defaultHeaders[hydrogenReact.SHOPIFY_STOREFRONT_Y_HEADER] = cookies[hydrogenReact.SHOPIFY_Y];
2052
- if (cookies[hydrogenReact.SHOPIFY_S])
2053
- defaultHeaders[hydrogenReact.SHOPIFY_STOREFRONT_S_HEADER] = cookies[hydrogenReact.SHOPIFY_S];
2054
- }
2211
+ const requestCookie = storefrontHeaders?.cookie ?? "";
2212
+ if (requestCookie) defaultHeaders["cookie"] = requestCookie;
2213
+ let uniqueToken;
2214
+ let visitToken;
2215
+ if (!/\b_shopify_(analytics|marketing)=/.test(requestCookie)) {
2216
+ const legacyUniqueToken = requestCookie.match(/\b_shopify_y=([^;]+)/)?.[1];
2217
+ const legacyVisitToken = requestCookie.match(/\b_shopify_s=([^;]+)/)?.[1];
2218
+ if (legacyUniqueToken) {
2219
+ defaultHeaders[hydrogenReact.SHOPIFY_STOREFRONT_Y_HEADER] = legacyUniqueToken;
2220
+ }
2221
+ if (legacyVisitToken) {
2222
+ defaultHeaders[hydrogenReact.SHOPIFY_STOREFRONT_S_HEADER] = legacyVisitToken;
2223
+ }
2224
+ uniqueToken = legacyUniqueToken ?? generateUUID();
2225
+ visitToken = legacyVisitToken ?? generateUUID();
2226
+ defaultHeaders[hydrogenReact.SHOPIFY_UNIQUE_TOKEN_HEADER] = uniqueToken;
2227
+ defaultHeaders[hydrogenReact.SHOPIFY_VISIT_TOKEN_HEADER] = visitToken;
2228
+ }
2229
+ let collectedSubrequestHeaders;
2055
2230
  const cacheKeyHeader = JSON.stringify({
2056
2231
  "content-type": defaultHeaders["content-type"],
2057
2232
  "user-agent": defaultHeaders["user-agent"],
@@ -2118,7 +2293,13 @@ function createStorefrontClient(options) {
2118
2293
  graphql: graphqlData,
2119
2294
  purpose: storefrontHeaders?.purpose
2120
2295
  },
2121
- streamConfig
2296
+ streamConfig,
2297
+ onRawHeaders: (headers2) => {
2298
+ collectedSubrequestHeaders ??= {
2299
+ setCookie: headers2.getSetCookie(),
2300
+ serverTiming: headers2.get("server-timing") ?? ""
2301
+ };
2302
+ }
2122
2303
  });
2123
2304
  const errorOptions = {
2124
2305
  url,
@@ -2217,9 +2398,90 @@ function createStorefrontClient(options) {
2217
2398
  generateCacheControlHeader,
2218
2399
  getPublicTokenHeaders,
2219
2400
  getPrivateTokenHeaders,
2401
+ getHeaders: () => ({ ...defaultHeaders }),
2220
2402
  getShopifyDomain,
2221
2403
  getApiUrl: getStorefrontApiUrl,
2222
- i18n: i18n ?? defaultI18n
2404
+ i18n: i18n ?? defaultI18n,
2405
+ /**
2406
+ * Checks if the request is targeting the Storefront API endpoint.
2407
+ */
2408
+ isStorefrontApiUrl(request) {
2409
+ return SFAPI_RE.test(getSafePathname(request.url ?? ""));
2410
+ },
2411
+ /**
2412
+ * Forwards the request to the Storefront API.
2413
+ */
2414
+ async forward(request, options2) {
2415
+ const forwardedHeaders = new Headers([
2416
+ // Forward only a selected set of headers to the Storefront API
2417
+ // to avoid getting 403 errors due to unexpected headers.
2418
+ ...extractHeaders(
2419
+ (key) => request.headers.get(key),
2420
+ [
2421
+ "accept",
2422
+ "accept-encoding",
2423
+ "accept-language",
2424
+ // Access-Control headers are used for CORS preflight requests.
2425
+ "access-control-request-headers",
2426
+ "access-control-request-method",
2427
+ "content-type",
2428
+ "content-length",
2429
+ "cookie",
2430
+ "origin",
2431
+ "referer",
2432
+ "user-agent",
2433
+ STOREFRONT_ACCESS_TOKEN_HEADER,
2434
+ hydrogenReact.SHOPIFY_UNIQUE_TOKEN_HEADER,
2435
+ hydrogenReact.SHOPIFY_VISIT_TOKEN_HEADER
2436
+ ]
2437
+ ),
2438
+ // Add some headers to help with geolocalization and debugging
2439
+ ...extractHeaders(
2440
+ (key) => defaultHeaders[key],
2441
+ [
2442
+ SHOPIFY_CLIENT_IP_HEADER,
2443
+ SHOPIFY_CLIENT_IP_SIG_HEADER,
2444
+ hydrogenReact.SHOPIFY_STOREFRONT_ID_HEADER,
2445
+ STOREFRONT_REQUEST_GROUP_ID_HEADER
2446
+ ]
2447
+ )
2448
+ ]);
2449
+ if (storefrontHeaders?.buyerIp) {
2450
+ forwardedHeaders.set("x-forwarded-for", storefrontHeaders.buyerIp);
2451
+ }
2452
+ const storefrontApiVersion = options2?.storefrontApiVersion ?? getSafePathname(request.url).match(SFAPI_RE)?.[1];
2453
+ const sfapiResponse = await fetch(
2454
+ getStorefrontApiUrl({ storefrontApiVersion }),
2455
+ {
2456
+ method: request.method,
2457
+ body: request.body,
2458
+ headers: forwardedHeaders
2459
+ }
2460
+ );
2461
+ return new Response(sfapiResponse.body, sfapiResponse);
2462
+ },
2463
+ setCollectedSubrequestHeaders: (response) => {
2464
+ if (collectedSubrequestHeaders) {
2465
+ for (const value of collectedSubrequestHeaders.setCookie) {
2466
+ response.headers.append("Set-Cookie", value);
2467
+ }
2468
+ }
2469
+ const serverTiming = extractServerTimingHeader(
2470
+ collectedSubrequestHeaders?.serverTiming
2471
+ );
2472
+ const isDocumentResponse = response.headers.get("content-type")?.startsWith("text/html");
2473
+ const fallbackValues = isDocumentResponse ? { _y: uniqueToken, _s: visitToken } : void 0;
2474
+ appendServerTimingHeader(response, {
2475
+ ...fallbackValues,
2476
+ ...serverTiming
2477
+ });
2478
+ if (isDocumentResponse && collectedSubrequestHeaders && // _shopify_essential cookie is always set, but we need more than that
2479
+ collectedSubrequestHeaders.setCookie.length > 1 && serverTiming?._y && serverTiming?._s && serverTiming?._cmp) {
2480
+ appendServerTimingHeader(response, {
2481
+ [HYDROGEN_SERVER_TRACKING_KEY]: "1"
2482
+ });
2483
+ }
2484
+ }
2223
2485
  }
2224
2486
  };
2225
2487
  }
@@ -4145,12 +4407,58 @@ function createHydrogenContext(options, additionalContext) {
4145
4407
  });
4146
4408
  return hybridProvider;
4147
4409
  }
4148
- function getStorefrontHeaders(request) {
4149
- return {
4150
- requestGroupId: getHeader(request, "request-id"),
4151
- buyerIp: getHeader(request, "oxygen-buyer-ip"),
4152
- cookie: getHeader(request, "cookie"),
4153
- purpose: getHeader(request, "purpose")
4410
+ function createRequestHandler({
4411
+ build,
4412
+ mode,
4413
+ poweredByHeader = true,
4414
+ getLoadContext,
4415
+ collectTrackingInformation = true,
4416
+ proxyStandardRoutes = true
4417
+ }) {
4418
+ const handleRequest = reactRouter.createRequestHandler(build, mode);
4419
+ const appendPoweredByHeader = poweredByHeader ? (response) => response.headers.append("powered-by", "Shopify, Hydrogen") : void 0;
4420
+ return async (request) => {
4421
+ const method = request.method;
4422
+ if ((method === "GET" || method === "HEAD") && request.body) {
4423
+ return new Response(`${method} requests cannot have a body`, {
4424
+ status: 400
4425
+ });
4426
+ }
4427
+ const url = new URL(request.url);
4428
+ if (url.pathname.includes("//")) {
4429
+ return new Response(null, {
4430
+ status: 301,
4431
+ headers: {
4432
+ location: url.pathname.replace(/\/+/g, "/")
4433
+ }
4434
+ });
4435
+ }
4436
+ const context = await getLoadContext?.(request);
4437
+ const storefront = context?.storefront || context?.get?.(storefrontContext);
4438
+ if (proxyStandardRoutes) {
4439
+ if (!storefront) {
4440
+ warnOnce(
4441
+ "[h2:createRequestHandler] Storefront instance is required to proxy standard routes."
4442
+ );
4443
+ }
4444
+ if (storefront?.isStorefrontApiUrl(request)) {
4445
+ const response2 = await storefront.forward(request);
4446
+ appendPoweredByHeader?.(response2);
4447
+ return response2;
4448
+ }
4449
+ }
4450
+ const response = await handleRequest(request, context);
4451
+ if (storefront && proxyStandardRoutes) {
4452
+ if (collectTrackingInformation) {
4453
+ storefront.setCollectedSubrequestHeaders(response);
4454
+ }
4455
+ const fetchDest = request.headers.get("sec-fetch-dest");
4456
+ if (fetchDest && fetchDest === "document" || request.headers.get("accept")?.includes("text/html")) {
4457
+ appendServerTimingHeader(response, { [HYDROGEN_SFAPI_PROXY_KEY]: "1" });
4458
+ }
4459
+ }
4460
+ appendPoweredByHeader?.(response);
4461
+ return response;
4154
4462
  };
4155
4463
  }
4156
4464
  var NonceContext = react.createContext(void 0);
@@ -4764,10 +5072,10 @@ function hydrogenPreset() {
4764
5072
  ssr: true,
4765
5073
  future: {
4766
5074
  v8_middleware: true,
5075
+ v8_splitRouteModules: true,
5076
+ v8_viteEnvironmentApi: false,
4767
5077
  unstable_optimizeDeps: true,
4768
- unstable_splitRouteModules: true,
4769
- unstable_subResourceIntegrity: false,
4770
- unstable_viteEnvironmentApi: false
5078
+ unstable_subResourceIntegrity: false
4771
5079
  }
4772
5080
  }),
4773
5081
  reactRouterConfigResolved: ({ reactRouterConfig }) => {
@@ -4783,7 +5091,7 @@ function hydrogenPreset() {
4783
5091
  }
4784
5092
  if (reactRouterConfig.serverBundles) {
4785
5093
  throw new Error(
4786
- "[Hydrogen Preset] serverBundles is not supported in Hydrogen 2025.7.0.\nReason: React Router plugin manifest incompatibility with Hydrogen CLI.\nAlternative: Route-level code splitting via unstable_splitRouteModules is enabled."
5094
+ "[Hydrogen Preset] serverBundles is not supported in Hydrogen 2025.7.0.\nReason: React Router plugin manifest incompatibility with Hydrogen CLI.\nAlternative: Route-level code splitting via v8_splitRouteModules is enabled."
4787
5095
  );
4788
5096
  }
4789
5097
  if (reactRouterConfig.buildEnd) {
@@ -5298,10 +5606,10 @@ var schema = {
5298
5606
  if (typeof value !== "string") {
5299
5607
  throw new Error(ERROR_PREFIX.concat("`title` should be a string"));
5300
5608
  }
5301
- if (typeof value === "string" && value.length > 120) {
5609
+ if (typeof value === "string" && value.length > 70) {
5302
5610
  throw new Error(
5303
5611
  ERROR_PREFIX.concat(
5304
- "`title` should not be longer than 120 characters"
5612
+ "`title` should not be longer than 70 characters"
5305
5613
  )
5306
5614
  );
5307
5615
  }
@@ -5318,7 +5626,7 @@ var schema = {
5318
5626
  if (typeof value === "string" && value.length > 155) {
5319
5627
  throw new Error(
5320
5628
  ERROR_PREFIX.concat(
5321
- "`description` should not be longer than 155 characters"
5629
+ "`description` should not be longer than 160 characters"
5322
5630
  )
5323
5631
  );
5324
5632
  }
@@ -6262,6 +6570,10 @@ Object.defineProperty(exports, "getShopifyCookies", {
6262
6570
  enumerable: true,
6263
6571
  get: function () { return hydrogenReact.getShopifyCookies; }
6264
6572
  });
6573
+ Object.defineProperty(exports, "getTrackingValues", {
6574
+ enumerable: true,
6575
+ get: function () { return hydrogenReact.getTrackingValues; }
6576
+ });
6265
6577
  Object.defineProperty(exports, "isOptionValueCombinationInEncodedVariant", {
6266
6578
  enumerable: true,
6267
6579
  get: function () { return hydrogenReact.isOptionValueCombinationInEncodedVariant; }
@@ -6339,6 +6651,7 @@ exports.createCartHandler = createCartHandler;
6339
6651
  exports.createContentSecurityPolicy = createContentSecurityPolicy;
6340
6652
  exports.createCustomerAccountClient = createCustomerAccountClient;
6341
6653
  exports.createHydrogenContext = createHydrogenContext;
6654
+ exports.createRequestHandler = createRequestHandler;
6342
6655
  exports.createStorefrontClient = createStorefrontClient;
6343
6656
  exports.createWithCache = createWithCache;
6344
6657
  exports.formatAPIResult = formatAPIResult;