@shopify/hydrogen 2026.4.2 → 2026.4.4

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.
@@ -437,7 +437,7 @@ function getPrivacyBanner() {
437
437
  }
438
438
 
439
439
  // package.json
440
- var version = "2026.4.2";
440
+ var version = "2026.4.4";
441
441
 
442
442
  // src/analytics-manager/ShopifyAnalytics.tsx
443
443
  function getCustomerPrivacyRequired() {
@@ -846,7 +846,7 @@ function PerfKit({ shop }) {
846
846
  "data-storefront-id": shop.hydrogenSubchannelId,
847
847
  "data-monorail-region": "global",
848
848
  "data-spa-mode": "true",
849
- "data-resource-timing-sampling-rate": "100"
849
+ "data-resource-timing-sampling-rate": "10"
850
850
  }
851
851
  });
852
852
  useEffect(() => {
@@ -1833,7 +1833,7 @@ function generateUUID() {
1833
1833
  }
1834
1834
 
1835
1835
  // src/version.ts
1836
- var LIB_VERSION = "2026.4.2";
1836
+ var LIB_VERSION = "2026.4.4";
1837
1837
 
1838
1838
  // src/utils/graphql.ts
1839
1839
  function minifyQuery(string) {
@@ -2383,7 +2383,7 @@ function cartGetDefault({
2383
2383
  cartFragment
2384
2384
  }) {
2385
2385
  return async (cartInput) => {
2386
- const cartId = getCartId();
2386
+ const cartId = cartInput?.cartId ?? getCartId();
2387
2387
  if (!cartId) return null;
2388
2388
  const includeVisitorConsent = shouldIncludeVisitorConsent(cartInput);
2389
2389
  const [isCustomerLoggedIn, { cart, errors: errors2 }] = await Promise.all([
@@ -2396,10 +2396,10 @@ function cartGetDefault({
2396
2396
  }
2397
2397
  )
2398
2398
  ]);
2399
- if (isCustomerLoggedIn && cart?.checkoutUrl) {
2399
+ if (isCustomerLoggedIn && cart && typeof cart === "object" && "checkoutUrl" in cart && typeof cart.checkoutUrl === "string") {
2400
2400
  const finalCheckoutUrl = new URL(cart.checkoutUrl);
2401
2401
  finalCheckoutUrl.searchParams.set("logged_in", "true");
2402
- cart.checkoutUrl = finalCheckoutUrl.toString();
2402
+ Object.assign(cart, { checkoutUrl: finalCheckoutUrl.toString() });
2403
2403
  }
2404
2404
  return cart || errors2 ? formatAPIResult(cart, errors2) : null;
2405
2405
  };
@@ -3352,7 +3352,12 @@ function createCartHandler(options) {
3352
3352
  ...args[0].buyerIdentity
3353
3353
  };
3354
3354
  const result = await _cartCreate(...args);
3355
- cartId = result?.cart?.id;
3355
+ if (result?.cart && (typeof result.cart !== "object" || !("id" in result.cart) || typeof result.cart.id !== "string")) {
3356
+ throw new Error(
3357
+ "[h2:error:createCartHandler] Cart created but response is missing a valid `id` field. Ensure your cart query fragment includes the `id` field."
3358
+ );
3359
+ }
3360
+ cartId = result?.cart && typeof result.cart === "object" && "id" in result.cart && typeof result.cart.id === "string" ? result.cart.id : void 0;
3356
3361
  return result;
3357
3362
  };
3358
3363
  const methods = {
@@ -3400,7 +3405,10 @@ function createCartHandler(options) {
3400
3405
  ) : await cartCreate({ buyerIdentity: buyerIdentity2 }, optionalParams);
3401
3406
  },
3402
3407
  updateNote: async (note, optionalParams) => {
3403
- return cartId || optionalParams?.cartId ? await cartNoteUpdateDefault(mutateOptions)(note, optionalParams) : await cartCreate({ note }, optionalParams);
3408
+ return cartId || optionalParams?.cartId ? await cartNoteUpdateDefault(mutateOptions)(
3409
+ note,
3410
+ optionalParams
3411
+ ) : await cartCreate({ note }, optionalParams);
3404
3412
  },
3405
3413
  updateSelectedDeliveryOption: cartSelectedDeliveryOptionsUpdateDefault(mutateOptions),
3406
3414
  updateAttributes: async (attributes, optionalParams) => {
@@ -4405,7 +4413,9 @@ function createHydrogenContext(options, additionalContext) {
4405
4413
  setCartId: cartOptions.setId || cartSetIdDefault(),
4406
4414
  cartQueryFragment: cartOptions.queryFragment,
4407
4415
  cartMutateFragment: cartOptions.mutateFragment,
4408
- customMethods: cartOptions.customMethods,
4416
+ ...cartOptions.customMethods && {
4417
+ customMethods: cartOptions.customMethods
4418
+ },
4409
4419
  buyerIdentity,
4410
4420
  // defaults
4411
4421
  storefront,
@@ -5131,9 +5141,9 @@ function hydrogenPreset() {
5131
5141
  v8_middleware: true,
5132
5142
  v8_splitRouteModules: true,
5133
5143
  v8_viteEnvironmentApi: false,
5134
- unstable_optimizeDeps: true,
5135
- unstable_subResourceIntegrity: false
5136
- }
5144
+ unstable_optimizeDeps: true
5145
+ },
5146
+ subResourceIntegrity: false
5137
5147
  }),
5138
5148
  reactRouterConfigResolved: ({ reactRouterConfig }) => {
5139
5149
  if (reactRouterConfig.basename && reactRouterConfig.basename !== "/") {
@@ -5156,9 +5166,9 @@ function hydrogenPreset() {
5156
5166
  "[Hydrogen Preset] buildEnd is not supported in Hydrogen 2025.7.0.\nReason: Hydrogen CLI bypasses React Router buildEnd hook execution.\nWorkaround: Use external build scripts or package.json post-build hooks."
5157
5167
  );
5158
5168
  }
5159
- if (reactRouterConfig.future?.unstable_subResourceIntegrity === true) {
5169
+ if (reactRouterConfig.subResourceIntegrity === true) {
5160
5170
  throw new Error(
5161
- "[Hydrogen Preset] unstable_subResourceIntegrity cannot be enabled.\nReason: Conflicts with Hydrogen CSP nonce-based authentication.\nImpact: Would break Content Security Policy and cause script execution failures."
5171
+ "[Hydrogen Preset] subResourceIntegrity cannot be enabled.\nReason: Conflicts with Hydrogen CSP nonce-based authentication.\nImpact: Would break Content Security Policy and cause script execution failures."
5162
5172
  );
5163
5173
  }
5164
5174
  }
@@ -5552,6 +5562,9 @@ var graphiqlLoader = async function graphiqlLoader2({
5552
5562
  };
5553
5563
 
5554
5564
  // src/routing/redirect.ts
5565
+ var SINGLE_FETCH_DATA_SUFFIX = ".data";
5566
+ var SINGLE_FETCH_ROOT_SEGMENT = "_root.data";
5567
+ var SINGLE_FETCH_TRAILING_SLASH_SEGMENT = "_.data";
5555
5568
  async function storefrontRedirect(options) {
5556
5569
  const {
5557
5570
  storefront,
@@ -5561,13 +5574,13 @@ async function storefrontRedirect(options) {
5561
5574
  response = new Response("Not Found", { status: 404 })
5562
5575
  } = options;
5563
5576
  const url = new URL(request.url);
5564
- const { pathname, searchParams } = url;
5565
- const isSoftNavigation = searchParams.has("_data");
5577
+ const { searchParams } = url;
5578
+ const { pathname, isSoftNavigation } = parseSingleFetchPathname(url.pathname);
5566
5579
  searchParams.delete("redirect");
5567
5580
  searchParams.delete("return_to");
5568
- searchParams.delete("_data");
5569
- const redirectFrom = (matchQueryParams ? url.toString().replace(url.origin, "") : pathname).toLowerCase();
5570
- if (url.pathname === "/admin" && !noAdminRedirect) {
5581
+ searchParams.delete("_routes");
5582
+ const redirectFrom = (matchQueryParams ? `${pathname}${url.search}` : pathname).toLowerCase();
5583
+ if (pathname === "/admin" && !noAdminRedirect) {
5571
5584
  return createRedirectResponse(
5572
5585
  `${storefront.getShopifyDomain()}/admin`,
5573
5586
  isSoftNavigation,
@@ -5617,7 +5630,7 @@ function createRedirectResponse(location, isSoftNavigation, searchParams, matchQ
5617
5630
  }
5618
5631
  if (isSoftNavigation) {
5619
5632
  return new Response(null, {
5620
- status: 200,
5633
+ status: 204,
5621
5634
  headers: {
5622
5635
  "X-Remix-Redirect": url.toString().replace(TEMP_DOMAIN, ""),
5623
5636
  "X-Remix-Status": "301"
@@ -5630,6 +5643,28 @@ function createRedirectResponse(location, isSoftNavigation, searchParams, matchQ
5630
5643
  });
5631
5644
  }
5632
5645
  }
5646
+ function parseSingleFetchPathname(rawPathname) {
5647
+ if (!rawPathname.endsWith(SINGLE_FETCH_DATA_SUFFIX)) {
5648
+ return { pathname: rawPathname, isSoftNavigation: false };
5649
+ }
5650
+ if (rawPathname.endsWith("/" + SINGLE_FETCH_TRAILING_SLASH_SEGMENT)) {
5651
+ return {
5652
+ pathname: rawPathname.slice(
5653
+ 0,
5654
+ -SINGLE_FETCH_TRAILING_SLASH_SEGMENT.length
5655
+ ),
5656
+ isSoftNavigation: true
5657
+ };
5658
+ }
5659
+ if (rawPathname === "/" + SINGLE_FETCH_ROOT_SEGMENT || rawPathname.endsWith("/" + SINGLE_FETCH_ROOT_SEGMENT)) {
5660
+ const prefix = rawPathname.slice(0, -SINGLE_FETCH_ROOT_SEGMENT.length);
5661
+ return { pathname: prefix || "/", isSoftNavigation: true };
5662
+ }
5663
+ return {
5664
+ pathname: rawPathname.slice(0, -SINGLE_FETCH_DATA_SUFFIX.length),
5665
+ isSoftNavigation: true
5666
+ };
5667
+ }
5633
5668
  var REDIRECT_QUERY = `#graphql
5634
5669
  query redirects($query: String) {
5635
5670
  urlRedirects(first: 1, query: $query) {