@shopify/cli-hydrogen 10.0.0 → 10.0.2

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.
@@ -1,5 +1,52 @@
1
1
  # skeleton
2
2
 
3
+ ## 2025.4.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Moved the Cursor rules into more generic LLM prompt files. If you were using the Cursor rules, you will find the prompts in the `cookbook/llms` folder and they can be put into your `.cursor/rules` folder manually. LLM prompt files will be maintained moving forward, while previous Cursor rules will not be updated anymore. ([#2936](https://github.com/Shopify/hydrogen/pull/2936)) by [@ruggishop](https://github.com/ruggishop)
8
+
9
+ - Added bundles recipe ([#2915](https://github.com/Shopify/hydrogen/pull/2915)) by [@ruggishop](https://github.com/ruggishop)
10
+
11
+ - Update copy for subscriptions, combined listings, bundles recipes ([#2924](https://github.com/Shopify/hydrogen/pull/2924)) by [@ruggishop](https://github.com/ruggishop)
12
+
13
+ - Bump skeleton @shopify/cli and @shopify/mini-oxygen ([#2883](https://github.com/Shopify/hydrogen/pull/2883)) by [@juanpprieto](https://github.com/juanpprieto)
14
+
15
+ - Remove rules from the template. ([#2931](https://github.com/Shopify/hydrogen/pull/2931)) by [@ruggishop](https://github.com/ruggishop)
16
+
17
+ - Update SFAPI and CAAPI versions to 2025.04 ([#2886](https://github.com/Shopify/hydrogen/pull/2886)) by [@juanpprieto](https://github.com/juanpprieto)
18
+
19
+ - Updated recipes: subscriptions, bundles, combined-listings. New recipe: markets. ([#2930](https://github.com/Shopify/hydrogen/pull/2930)) by [@ruggishop](https://github.com/ruggishop)
20
+
21
+ - Updated the subscriptions recipe to better display the purchase options. ([#2912](https://github.com/Shopify/hydrogen/pull/2912)) by [@ruggishop](https://github.com/ruggishop)
22
+
23
+ - Bump recipes with copy adjustments ([#2935](https://github.com/Shopify/hydrogen/pull/2935)) by [@ruggishop](https://github.com/ruggishop)
24
+
25
+ - Added a Combined Listings recipe. ([#2876](https://github.com/Shopify/hydrogen/pull/2876)) by [@ruggishop](https://github.com/ruggishop)
26
+
27
+ - Updated dependencies [[`af23e710`](https://github.com/Shopify/hydrogen/commit/af23e710dac83bb57498d9c2ef1d8bcf9df55d34), [`9d8a6644`](https://github.com/Shopify/hydrogen/commit/9d8a6644a5b67dca890c6687df390aee78fc85c3)]:
28
+ - @shopify/hydrogen@2025.4.0
29
+
30
+ ## 2025.1.7
31
+
32
+ ### Patch Changes
33
+
34
+ - Fix an issue with our starter template where duplicate content can exist on URLs that use internationalized handles. For example, if you have a product handle in english of `the-havoc` and translate it to `das-chaos` in German, duplicate content exists at both: ([#2821](https://github.com/Shopify/hydrogen/pull/2821)) by [@blittle](https://github.com/blittle)
35
+
36
+ 1. https://hydrogen.shop/de-de/products/das-chaos
37
+ 2. https://hydrogen.shop/de-de/products/the-havoc
38
+
39
+ We've changed the starter template to make the second redirect to the first.
40
+
41
+ - Added the Cursor rule for the subscriptions recipe. ([#2874](https://github.com/Shopify/hydrogen/pull/2874)) by [@ruggishop](https://github.com/ruggishop)
42
+
43
+ - Fix faulty truthiness check for cart quantity ([#2855](https://github.com/Shopify/hydrogen/pull/2855)) by [@frontsideair](https://github.com/frontsideair)
44
+
45
+ - Refactor ProductItem into a separate component ([#2872](https://github.com/Shopify/hydrogen/pull/2872)) by [@juanpprieto](https://github.com/juanpprieto)
46
+
47
+ - Updated dependencies [[`f80f3bc7`](https://github.com/Shopify/hydrogen/commit/f80f3bc7239b3ee6641cb468a17e15c77bb7815b), [`61ddf924`](https://github.com/Shopify/hydrogen/commit/61ddf92487524b3c04632ae2cfdaa2869a3ae02c), [`642bde4f`](https://github.com/Shopify/hydrogen/commit/642bde4f3df11511e125b013abd977618da25692)]:
48
+ - @shopify/hydrogen@2025.1.4
49
+
3
50
  ## 2025.1.6
4
51
 
5
52
  ### Patch Changes
@@ -117,13 +164,13 @@
117
164
  1. Add a routes.ts file. This is your new Remix route configuration file.
118
165
 
119
166
  ```ts
120
- import { flatRoutes } from "@remix-run/fs-routes";
121
- import { layout, type RouteConfig } from "@remix-run/route-config";
122
- import { hydrogenRoutes } from "@shopify/hydrogen";
167
+ import {flatRoutes} from '@remix-run/fs-routes';
168
+ import {layout, type RouteConfig} from '@remix-run/route-config';
169
+ import {hydrogenRoutes} from '@shopify/hydrogen';
123
170
 
124
171
  export default hydrogenRoutes([
125
172
  // Your entire app reading from routes folder using Layout from layout.tsx
126
- layout("./layout.tsx", await flatRoutes()),
173
+ layout('./layout.tsx', await flatRoutes()),
127
174
  ]) satisfies RouteConfig;
128
175
  ```
129
176
 
@@ -714,25 +761,25 @@
714
761
  8. Update the `ProductForm` component.
715
762
 
716
763
  ```tsx
717
- import { Link, useNavigate } from "@remix-run/react";
718
- import { type MappedProductOptions } from "@shopify/hydrogen";
764
+ import {Link, useNavigate} from '@remix-run/react';
765
+ import {type MappedProductOptions} from '@shopify/hydrogen';
719
766
  import type {
720
767
  Maybe,
721
768
  ProductOptionValueSwatch,
722
- } from "@shopify/hydrogen/storefront-api-types";
723
- import { AddToCartButton } from "./AddToCartButton";
724
- import { useAside } from "./Aside";
725
- import type { ProductFragment } from "storefrontapi.generated";
769
+ } from '@shopify/hydrogen/storefront-api-types';
770
+ import {AddToCartButton} from './AddToCartButton';
771
+ import {useAside} from './Aside';
772
+ import type {ProductFragment} from 'storefrontapi.generated';
726
773
 
727
774
  export function ProductForm({
728
775
  productOptions,
729
776
  selectedVariant,
730
777
  }: {
731
778
  productOptions: MappedProductOptions[];
732
- selectedVariant: ProductFragment["selectedOrFirstAvailableVariant"];
779
+ selectedVariant: ProductFragment['selectedOrFirstAvailableVariant'];
733
780
  }) {
734
781
  const navigate = useNavigate();
735
- const { open } = useAside();
782
+ const {open} = useAside();
736
783
  return (
737
784
  <div className="product-form">
738
785
  {productOptions.map((option) => (
@@ -766,8 +813,8 @@
766
813
  to={`/products/${handle}?${variantUriQuery}`}
767
814
  style={{
768
815
  border: selected
769
- ? "1px solid black"
770
- : "1px solid transparent",
816
+ ? '1px solid black'
817
+ : '1px solid transparent',
771
818
  opacity: available ? 1 : 0.3,
772
819
  }}
773
820
  >
@@ -784,13 +831,13 @@
784
831
  <button
785
832
  type="button"
786
833
  className={`product-options-item${
787
- exists && !selected ? " link" : ""
834
+ exists && !selected ? ' link' : ''
788
835
  }`}
789
836
  key={option.name + name}
790
837
  style={{
791
838
  border: selected
792
- ? "1px solid black"
793
- : "1px solid transparent",
839
+ ? '1px solid black'
840
+ : '1px solid transparent',
794
841
  opacity: available ? 1 : 0.3,
795
842
  }}
796
843
  disabled={!exists}
@@ -814,7 +861,7 @@
814
861
  <AddToCartButton
815
862
  disabled={!selectedVariant || !selectedVariant.availableForSale}
816
863
  onClick={() => {
817
- open("cart");
864
+ open('cart');
818
865
  }}
819
866
  lines={
820
867
  selectedVariant
@@ -828,7 +875,7 @@
828
875
  : []
829
876
  }
830
877
  >
831
- {selectedVariant?.availableForSale ? "Add to cart" : "Sold out"}
878
+ {selectedVariant?.availableForSale ? 'Add to cart' : 'Sold out'}
832
879
  </AddToCartButton>
833
880
  </div>
834
881
  );
@@ -851,7 +898,7 @@
851
898
  aria-label={name}
852
899
  className="product-option-label-swatch"
853
900
  style={{
854
- backgroundColor: color || "transparent",
901
+ backgroundColor: color || 'transparent',
855
902
  }}
856
903
  >
857
904
  {!!image && <img src={image} alt={name} />}
@@ -1352,21 +1399,21 @@
1352
1399
  New `withCache.fetch` is for caching simple fetch requests. This method caches the responses if they are OK responses, and you can pass `shouldCacheResponse`, `cacheKey`, etc. to modify behavior. `data` is the consumed body of the response (we need to consume to cache it).
1353
1400
 
1354
1401
  ```ts
1355
- const withCache = createWithCache({ cache, waitUntil, request });
1402
+ const withCache = createWithCache({cache, waitUntil, request});
1356
1403
 
1357
- const { data, response } = await withCache.fetch<{ data: T; error: string }>(
1358
- "my-cms.com/api",
1404
+ const {data, response} = await withCache.fetch<{data: T; error: string}>(
1405
+ 'my-cms.com/api',
1359
1406
  {
1360
- method: "POST",
1361
- headers: { "Content-type": "application/json" },
1407
+ method: 'POST',
1408
+ headers: {'Content-type': 'application/json'},
1362
1409
  body,
1363
1410
  },
1364
1411
  {
1365
1412
  cacheStrategy: CacheLong(),
1366
1413
  // Cache if there are no data errors or a specific data that make this result not suited for caching
1367
1414
  shouldCacheResponse: (result) => !result?.error,
1368
- cacheKey: ["my-cms", body],
1369
- displayName: "My CMS query",
1415
+ cacheKey: ['my-cms', body],
1416
+ displayName: 'My CMS query',
1370
1417
  },
1371
1418
  );
1372
1419
  ```
@@ -1942,9 +1989,9 @@
1942
1989
 
1943
1990
  ```tsx
1944
1991
  // app/lib/root-data.ts
1945
- import { useMatches } from "@remix-run/react";
1946
- import type { SerializeFrom } from "@shopify/remix-oxygen";
1947
- import type { loader } from "~/root";
1992
+ import {useMatches} from '@remix-run/react';
1993
+ import type {SerializeFrom} from '@shopify/remix-oxygen';
1994
+ import type {loader} from '~/root';
1948
1995
 
1949
1996
  /**
1950
1997
  * Access the result of the root loader from a React component.
@@ -2106,10 +2153,10 @@
2106
2153
  - This is an important fix to a bug with 404 routes and path-based i18n projects where some unknown routes would not properly render a 404. This fixes all new projects, but to fix existing projects, add a `($locale).tsx` route with the following contents: ([#1732](https://github.com/Shopify/hydrogen/pull/1732)) by [@blittle](https://github.com/blittle)
2107
2154
 
2108
2155
  ```ts
2109
- import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
2156
+ import {type LoaderFunctionArgs} from '@remix-run/server-runtime';
2110
2157
 
2111
- export async function loader({ params, context }: LoaderFunctionArgs) {
2112
- const { language, country } = context.storefront.i18n;
2158
+ export async function loader({params, context}: LoaderFunctionArgs) {
2159
+ const {language, country} = context.storefront.i18n;
2113
2160
 
2114
2161
  if (
2115
2162
  params.locale &&
@@ -2117,7 +2164,7 @@
2117
2164
  ) {
2118
2165
  // If the locale URL param is defined, yet we still are still at the default locale
2119
2166
  // then the the locale param must be invalid, send to the 404 page
2120
- throw new Response(null, { status: 404 });
2167
+ throw new Response(null, {status: 404});
2121
2168
  }
2122
2169
 
2123
2170
  return null;
@@ -2173,11 +2220,11 @@
2173
2220
  ```yaml
2174
2221
  projects:
2175
2222
  default:
2176
- schema: "node_modules/@shopify/hydrogen/storefront.schema.json"
2223
+ schema: 'node_modules/@shopify/hydrogen/storefront.schema.json'
2177
2224
  documents:
2178
- - "!*.d.ts"
2179
- - "*.{ts,tsx,js,jsx}"
2180
- - "app/**/*.{ts,tsx,js,jsx}"
2225
+ - '!*.d.ts'
2226
+ - '*.{ts,tsx,js,jsx}'
2227
+ - 'app/**/*.{ts,tsx,js,jsx}'
2181
2228
  ```
2182
2229
 
2183
2230
  - Improve resiliency of `HydrogenSession` ([#1583](https://github.com/Shopify/hydrogen/pull/1583)) by [@blittle](https://github.com/blittle)
@@ -2392,8 +2439,8 @@
2392
2439
  ```ts
2393
2440
  // root.tsx
2394
2441
 
2395
- import { useMatches } from "@remix-run/react";
2396
- import { type SerializeFrom } from "@shopify/remix-oxygen";
2442
+ import {useMatches} from '@remix-run/react';
2443
+ import {type SerializeFrom} from '@shopify/remix-oxygen';
2397
2444
 
2398
2445
  export const useRootLoaderData = () => {
2399
2446
  const [root] = useMatches();
@@ -123,6 +123,7 @@ function CartLineRemoveButton({
123
123
  }) {
124
124
  return (
125
125
  <CartForm
126
+ fetcherKey={getUpdateKey(lineIds)}
126
127
  route="/cart"
127
128
  action={CartForm.ACTIONS.LinesRemove}
128
129
  inputs={{lineIds}}
@@ -141,8 +142,11 @@ function CartLineUpdateButton({
141
142
  children: React.ReactNode;
142
143
  lines: CartLineUpdateInput[];
143
144
  }) {
145
+ const lineIds = lines.map((line) => line.id);
146
+
144
147
  return (
145
148
  <CartForm
149
+ fetcherKey={getUpdateKey(lineIds)}
146
150
  route="/cart"
147
151
  action={CartForm.ACTIONS.LinesUpdate}
148
152
  inputs={{lines}}
@@ -151,3 +155,14 @@ function CartLineUpdateButton({
151
155
  </CartForm>
152
156
  );
153
157
  }
158
+
159
+ /**
160
+ * Returns a unique key for the update action. This is used to make sure actions modifying the same line
161
+ * items are not run concurrently, but cancel each other. For example, if the user clicks "Increase quantity"
162
+ * and "Decrease quantity" in rapid succession, the actions will cancel each other and only the last one will run.
163
+ * @param lineIds - line ids affected by the update
164
+ * @returns
165
+ */
166
+ function getUpdateKey(lineIds: string[]) {
167
+ return [CartForm.ACTIONS.LinesUpdate, ...lineIds].join('-');
168
+ }
@@ -26,7 +26,7 @@ export function CartMain({layout, cart: originalCart}: CartMainProps) {
26
26
  cart &&
27
27
  Boolean(cart?.discountCodes?.filter((code) => code.applicable)?.length);
28
28
  const className = `cart-main ${withDiscount ? 'with-discount' : ''}`;
29
- const cartHasItems = cart?.totalQuantity && cart?.totalQuantity > 0;
29
+ const cartHasItems = cart?.totalQuantity ? cart.totalQuantity > 0 : false;
30
30
 
31
31
  return (
32
32
  <div className={className}>
@@ -0,0 +1,44 @@
1
+ import {Link} from '@remix-run/react';
2
+ import {Image, Money} from '@shopify/hydrogen';
3
+ import type {
4
+ ProductItemFragment,
5
+ CollectionItemFragment,
6
+ RecommendedProductFragment,
7
+ } from 'storefrontapi.generated';
8
+ import {useVariantUrl} from '~/lib/variants';
9
+
10
+ export function ProductItem({
11
+ product,
12
+ loading,
13
+ }: {
14
+ product:
15
+ | CollectionItemFragment
16
+ | ProductItemFragment
17
+ | RecommendedProductFragment;
18
+ loading?: 'eager' | 'lazy';
19
+ }) {
20
+ const variantUrl = useVariantUrl(product.handle);
21
+ const image = product.featuredImage;
22
+ return (
23
+ <Link
24
+ className="product-item"
25
+ key={product.id}
26
+ prefetch="intent"
27
+ to={variantUrl}
28
+ >
29
+ {image && (
30
+ <Image
31
+ alt={image.altText || product.title}
32
+ aspectRatio="1/1"
33
+ data={image}
34
+ loading={loading}
35
+ sizes="(min-width: 45em) 400px, 100vw"
36
+ />
37
+ )}
38
+ <h4>{product.title}</h4>
39
+ <small>
40
+ <Money data={product.priceRange.minVariantPrice} />
41
+ </small>
42
+ </Link>
43
+ );
44
+ }
@@ -0,0 +1,23 @@
1
+ import {redirect} from '@shopify/remix-oxygen';
2
+
3
+ export function redirectIfHandleIsLocalized(
4
+ request: Request,
5
+ ...localizedResources: Array<{
6
+ handle: string;
7
+ data: {handle: string} & unknown;
8
+ }>
9
+ ) {
10
+ const url = new URL(request.url);
11
+ let shouldRedirect = false;
12
+
13
+ localizedResources.forEach(({handle, data}) => {
14
+ if (handle !== data.handle) {
15
+ url.pathname = url.pathname.replace(handle, data.handle);
16
+ shouldRedirect = true;
17
+ }
18
+ });
19
+
20
+ if (shouldRedirect) {
21
+ throw redirect(url.toString());
22
+ }
23
+ }
@@ -6,6 +6,7 @@ import type {
6
6
  FeaturedCollectionFragment,
7
7
  RecommendedProductsQuery,
8
8
  } from 'storefrontapi.generated';
9
+ import {ProductItem} from '~/components/ProductItem';
9
10
 
10
11
  export const meta: MetaFunction = () => {
11
12
  return [{title: 'Hydrogen | Home'}];
@@ -101,21 +102,7 @@ function RecommendedProducts({
101
102
  <div className="recommended-products-grid">
102
103
  {response
103
104
  ? response.products.nodes.map((product) => (
104
- <Link
105
- key={product.id}
106
- className="recommended-product"
107
- to={`/products/${product.handle}`}
108
- >
109
- <Image
110
- data={product.images.nodes[0]}
111
- aspectRatio="1/1"
112
- sizes="(min-width: 45em) 20vw, 50vw"
113
- />
114
- <h4>{product.title}</h4>
115
- <small>
116
- <Money data={product.priceRange.minVariantPrice} />
117
- </small>
118
- </Link>
105
+ <ProductItem key={product.id} product={product} />
119
106
  ))
120
107
  : null}
121
108
  </div>
@@ -161,14 +148,12 @@ const RECOMMENDED_PRODUCTS_QUERY = `#graphql
161
148
  currencyCode
162
149
  }
163
150
  }
164
- images(first: 1) {
165
- nodes {
166
- id
167
- url
168
- altText
169
- width
170
- height
171
- }
151
+ featuredImage {
152
+ id
153
+ url
154
+ altText
155
+ width
156
+ height
172
157
  }
173
158
  }
174
159
  query RecommendedProducts ($country: CountryCode, $language: LanguageCode)
@@ -1,6 +1,7 @@
1
1
  import {type LoaderFunctionArgs} from '@shopify/remix-oxygen';
2
2
  import {useLoaderData, type MetaFunction} from '@remix-run/react';
3
3
  import {Image} from '@shopify/hydrogen';
4
+ import {redirectIfHandleIsLocalized} from '~/lib/redirect';
4
5
 
5
6
  export const meta: MetaFunction<typeof loader> = ({data}) => {
6
7
  return [{title: `Hydrogen | ${data?.article.title ?? ''} article`}];
@@ -20,7 +21,11 @@ export async function loader(args: LoaderFunctionArgs) {
20
21
  * Load data necessary for rendering content above the fold. This is the critical data
21
22
  * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
22
23
  */
23
- async function loadCriticalData({context, params}: LoaderFunctionArgs) {
24
+ async function loadCriticalData({
25
+ context,
26
+ request,
27
+ params,
28
+ }: LoaderFunctionArgs) {
24
29
  const {blogHandle, articleHandle} = params;
25
30
 
26
31
  if (!articleHandle || !blogHandle) {
@@ -38,6 +43,18 @@ async function loadCriticalData({context, params}: LoaderFunctionArgs) {
38
43
  throw new Response(null, {status: 404});
39
44
  }
40
45
 
46
+ redirectIfHandleIsLocalized(
47
+ request,
48
+ {
49
+ handle: articleHandle,
50
+ data: blog.articleByHandle,
51
+ },
52
+ {
53
+ handle: blogHandle,
54
+ data: blog,
55
+ },
56
+ );
57
+
41
58
  const article = blog.articleByHandle;
42
59
 
43
60
  return {article};
@@ -67,7 +84,8 @@ export default function Article() {
67
84
  <h1>
68
85
  {title}
69
86
  <div>
70
- {publishedDate} &middot; {author?.name}
87
+ <time dateTime={article.publishedAt}>{publishedDate}</time> &middot;{' '}
88
+ <address>{author?.name}</address>
71
89
  </div>
72
90
  </h1>
73
91
 
@@ -89,7 +107,9 @@ const ARTICLE_QUERY = `#graphql
89
107
  $language: LanguageCode
90
108
  ) @inContext(language: $language, country: $country) {
91
109
  blog(handle: $blogHandle) {
110
+ handle
92
111
  articleByHandle(handle: $articleHandle) {
112
+ handle
93
113
  title
94
114
  contentHtml
95
115
  publishedAt
@@ -3,6 +3,7 @@ import {Link, useLoaderData, type MetaFunction} from '@remix-run/react';
3
3
  import {Image, getPaginationVariables} from '@shopify/hydrogen';
4
4
  import type {ArticleItemFragment} from 'storefrontapi.generated';
5
5
  import {PaginatedResourceSection} from '~/components/PaginatedResourceSection';
6
+ import {redirectIfHandleIsLocalized} from '~/lib/redirect';
6
7
 
7
8
  export const meta: MetaFunction<typeof loader> = ({data}) => {
8
9
  return [{title: `Hydrogen | ${data?.blog.title ?? ''} blog`}];
@@ -49,6 +50,8 @@ async function loadCriticalData({
49
50
  throw new Response('Not found', {status: 404});
50
51
  }
51
52
 
53
+ redirectIfHandleIsLocalized(request, {handle: params.blogHandle, data: blog});
54
+
52
55
  return {blog};
53
56
  }
54
57
 
@@ -128,6 +131,7 @@ const BLOGS_QUERY = `#graphql
128
131
  ) @inContext(language: $language) {
129
132
  blog(handle: $blogHandle) {
130
133
  title
134
+ handle
131
135
  seo {
132
136
  title
133
137
  description
@@ -1,14 +1,9 @@
1
1
  import {redirect, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
2
- import {useLoaderData, Link, type MetaFunction} from '@remix-run/react';
3
- import {
4
- getPaginationVariables,
5
- Image,
6
- Money,
7
- Analytics,
8
- } from '@shopify/hydrogen';
9
- import type {ProductItemFragment} from 'storefrontapi.generated';
10
- import {useVariantUrl} from '~/lib/variants';
2
+ import {useLoaderData, type MetaFunction} from '@remix-run/react';
3
+ import {getPaginationVariables, Analytics} from '@shopify/hydrogen';
11
4
  import {PaginatedResourceSection} from '~/components/PaginatedResourceSection';
5
+ import {redirectIfHandleIsLocalized} from '~/lib/redirect';
6
+ import {ProductItem} from '~/components/ProductItem';
12
7
 
13
8
  export const meta: MetaFunction<typeof loader> = ({data}) => {
14
9
  return [{title: `Hydrogen | ${data?.collection.title ?? ''} Collection`}];
@@ -56,6 +51,9 @@ async function loadCriticalData({
56
51
  });
57
52
  }
58
53
 
54
+ // The API handle might be localized, so redirect to the localized handle
55
+ redirectIfHandleIsLocalized(request, {handle, data: collection});
56
+
59
57
  return {
60
58
  collection,
61
59
  };
@@ -101,38 +99,6 @@ export default function Collection() {
101
99
  );
102
100
  }
103
101
 
104
- function ProductItem({
105
- product,
106
- loading,
107
- }: {
108
- product: ProductItemFragment;
109
- loading?: 'eager' | 'lazy';
110
- }) {
111
- const variantUrl = useVariantUrl(product.handle);
112
- return (
113
- <Link
114
- className="product-item"
115
- key={product.id}
116
- prefetch="intent"
117
- to={variantUrl}
118
- >
119
- {product.featuredImage && (
120
- <Image
121
- alt={product.featuredImage.altText || product.title}
122
- aspectRatio="1/1"
123
- data={product.featuredImage}
124
- loading={loading}
125
- sizes="(min-width: 45em) 400px, 100vw"
126
- />
127
- )}
128
- <h4>{product.title}</h4>
129
- <small>
130
- <Money data={product.priceRange.minVariantPrice} />
131
- </small>
132
- </Link>
133
- );
134
- }
135
-
136
102
  const PRODUCT_ITEM_FRAGMENT = `#graphql
137
103
  fragment MoneyProductItem on MoneyV2 {
138
104
  amount
@@ -1,9 +1,8 @@
1
1
  import {type LoaderFunctionArgs} from '@shopify/remix-oxygen';
2
- import {useLoaderData, Link, type MetaFunction} from '@remix-run/react';
3
- import {getPaginationVariables, Image, Money} from '@shopify/hydrogen';
4
- import type {ProductItemFragment} from 'storefrontapi.generated';
5
- import {useVariantUrl} from '~/lib/variants';
2
+ import {useLoaderData, type MetaFunction} from '@remix-run/react';
3
+ import {getPaginationVariables} from '@shopify/hydrogen';
6
4
  import {PaginatedResourceSection} from '~/components/PaginatedResourceSection';
5
+ import {ProductItem} from '~/components/ProductItem';
7
6
 
8
7
  export const meta: MetaFunction<typeof loader> = () => {
9
8
  return [{title: `Hydrogen | Products`}];
@@ -69,44 +68,12 @@ export default function Collection() {
69
68
  );
70
69
  }
71
70
 
72
- function ProductItem({
73
- product,
74
- loading,
75
- }: {
76
- product: ProductItemFragment;
77
- loading?: 'eager' | 'lazy';
78
- }) {
79
- const variantUrl = useVariantUrl(product.handle);
80
- return (
81
- <Link
82
- className="product-item"
83
- key={product.id}
84
- prefetch="intent"
85
- to={variantUrl}
86
- >
87
- {product.featuredImage && (
88
- <Image
89
- alt={product.featuredImage.altText || product.title}
90
- aspectRatio="1/1"
91
- data={product.featuredImage}
92
- loading={loading}
93
- sizes="(min-width: 45em) 400px, 100vw"
94
- />
95
- )}
96
- <h4>{product.title}</h4>
97
- <small>
98
- <Money data={product.priceRange.minVariantPrice} />
99
- </small>
100
- </Link>
101
- );
102
- }
103
-
104
- const PRODUCT_ITEM_FRAGMENT = `#graphql
105
- fragment MoneyProductItem on MoneyV2 {
71
+ const COLLECTION_ITEM_FRAGMENT = `#graphql
72
+ fragment MoneyCollectionItem on MoneyV2 {
106
73
  amount
107
74
  currencyCode
108
75
  }
109
- fragment ProductItem on Product {
76
+ fragment CollectionItem on Product {
110
77
  id
111
78
  handle
112
79
  title
@@ -119,16 +86,16 @@ const PRODUCT_ITEM_FRAGMENT = `#graphql
119
86
  }
120
87
  priceRange {
121
88
  minVariantPrice {
122
- ...MoneyProductItem
89
+ ...MoneyCollectionItem
123
90
  }
124
91
  maxVariantPrice {
125
- ...MoneyProductItem
92
+ ...MoneyCollectionItem
126
93
  }
127
94
  }
128
95
  }
129
96
  ` as const;
130
97
 
131
- // NOTE: https://shopify.dev/docs/api/storefront/2024-01/objects/product
98
+ // NOTE: https://shopify.dev/docs/api/storefront/latest/objects/product
132
99
  const CATALOG_QUERY = `#graphql
133
100
  query Catalog(
134
101
  $country: CountryCode
@@ -140,7 +107,7 @@ const CATALOG_QUERY = `#graphql
140
107
  ) @inContext(country: $country, language: $language) {
141
108
  products(first: $first, last: $last, before: $startCursor, after: $endCursor) {
142
109
  nodes {
143
- ...ProductItem
110
+ ...CollectionItem
144
111
  }
145
112
  pageInfo {
146
113
  hasPreviousPage
@@ -150,5 +117,5 @@ const CATALOG_QUERY = `#graphql
150
117
  }
151
118
  }
152
119
  }
153
- ${PRODUCT_ITEM_FRAGMENT}
120
+ ${COLLECTION_ITEM_FRAGMENT}
154
121
  ` as const;
@@ -1,5 +1,6 @@
1
1
  import {type LoaderFunctionArgs} from '@shopify/remix-oxygen';
2
2
  import {useLoaderData, type MetaFunction} from '@remix-run/react';
3
+ import {redirectIfHandleIsLocalized} from '~/lib/redirect';
3
4
 
4
5
  export const meta: MetaFunction<typeof loader> = ({data}) => {
5
6
  return [{title: `Hydrogen | ${data?.page.title ?? ''}`}];
@@ -19,7 +20,11 @@ export async function loader(args: LoaderFunctionArgs) {
19
20
  * Load data necessary for rendering content above the fold. This is the critical data
20
21
  * needed to render the page. If it's unavailable, the whole page should 400 or 500 error.
21
22
  */
22
- async function loadCriticalData({context, params}: LoaderFunctionArgs) {
23
+ async function loadCriticalData({
24
+ context,
25
+ request,
26
+ params,
27
+ }: LoaderFunctionArgs) {
23
28
  if (!params.handle) {
24
29
  throw new Error('Missing page handle');
25
30
  }
@@ -37,6 +42,8 @@ async function loadCriticalData({context, params}: LoaderFunctionArgs) {
37
42
  throw new Response('Not Found', {status: 404});
38
43
  }
39
44
 
45
+ redirectIfHandleIsLocalized(request, {handle: params.handle, data: page});
46
+
40
47
  return {
41
48
  page,
42
49
  };
@@ -72,6 +79,7 @@ const PAGE_QUERY = `#graphql
72
79
  )
73
80
  @inContext(language: $language, country: $country) {
74
81
  page(handle: $handle) {
82
+ handle
75
83
  id
76
84
  title
77
85
  body
@@ -1,4 +1,4 @@
1
- import {type LoaderFunctionArgs} from '@shopify/remix-oxygen';
1
+ import {redirect, type LoaderFunctionArgs} from '@shopify/remix-oxygen';
2
2
  import {useLoaderData, type MetaFunction} from '@remix-run/react';
3
3
  import {
4
4
  getSelectedProductOptions,
@@ -11,6 +11,7 @@ import {
11
11
  import {ProductPrice} from '~/components/ProductPrice';
12
12
  import {ProductImage} from '~/components/ProductImage';
13
13
  import {ProductForm} from '~/components/ProductForm';
14
+ import {redirectIfHandleIsLocalized} from '~/lib/redirect';
14
15
 
15
16
  export const meta: MetaFunction<typeof loader> = ({data}) => {
16
17
  return [
@@ -59,6 +60,9 @@ async function loadCriticalData({
59
60
  throw new Response(null, {status: 404});
60
61
  }
61
62
 
63
+ // The API handle might be localized, so redirect to the localized handle
64
+ redirectIfHandleIsLocalized(request, {handle, data: product});
65
+
62
66
  return {
63
67
  product,
64
68
  };
@@ -27,12 +27,17 @@ img {
27
27
  * components/Aside
28
28
  * --------------------------------------------------
29
29
  */
30
+ @media (max-width: 45em) {
31
+ html:has(.overlay.expanded) {
32
+ overflow: hidden;
33
+ }
34
+ }
35
+
30
36
  aside {
31
37
  background: var(--color-light);
32
38
  box-shadow: 0 0 50px rgba(0, 0, 0, 0.3);
33
39
  height: 100vh;
34
- max-width: var(--aside-width);
35
- min-width: var(--aside-width);
40
+ width: min(var(--aside-width), 100vw);
36
41
  position: fixed;
37
42
  right: calc(-1 * var(--aside-width));
38
43
  top: 0;
@@ -201,6 +206,11 @@ button.reset:hover:not(:has(> *)) {
201
206
  margin-left: auto;
202
207
  }
203
208
 
209
+ .header-ctas > * {
210
+ min-width: fit-content;
211
+ }
212
+
213
+
204
214
  /*
205
215
  * --------------------------------------------------
206
216
  * components/Footer
@@ -212,14 +222,16 @@ button.reset:hover:not(:has(> *)) {
212
222
  }
213
223
 
214
224
  .footer-menu {
215
- align-items: center;
225
+ justify-content: center;
216
226
  display: flex;
227
+ flex-wrap: wrap;
217
228
  grid-gap: 1rem;
218
229
  padding: 1rem;
219
230
  }
220
231
 
221
232
  .footer-menu a {
222
233
  color: var(--color-light);
234
+ min-width: fit-content;
223
235
  }
224
236
 
225
237
  /*
@@ -2,7 +2,7 @@
2
2
  "name": "skeleton",
3
3
  "private": true,
4
4
  "sideEffects": false,
5
- "version": "2025.1.6",
5
+ "version": "2025.4.0",
6
6
  "type": "module",
7
7
  "scripts": {
8
8
  "build": "shopify hydrogen build --codegen",
@@ -17,7 +17,7 @@
17
17
  "@remix-run/react": "^2.16.1",
18
18
  "@remix-run/server-runtime": "^2.16.1",
19
19
  "graphql": "^16.10.0",
20
- "@shopify/hydrogen": "2025.1.3",
20
+ "@shopify/hydrogen": "2025.4.0",
21
21
  "@shopify/remix-oxygen": "^2.0.12",
22
22
  "graphql-tag": "^2.12.6",
23
23
  "isbot": "^5.1.22",
@@ -32,9 +32,9 @@
32
32
  "@remix-run/dev": "^2.16.1",
33
33
  "@remix-run/fs-routes": "^2.16.1",
34
34
  "@remix-run/route-config": "^2.16.1",
35
- "@shopify/cli": "~3.77.1",
35
+ "@shopify/cli": "~3.79.2",
36
36
  "@shopify/hydrogen-codegen": "^0.3.3",
37
- "@shopify/mini-oxygen": "^3.2.0",
37
+ "@shopify/mini-oxygen": "^3.2.1",
38
38
  "@shopify/oxygen-workers-types": "^4.1.6",
39
39
  "@shopify/prettier-config": "^1.1.2",
40
40
  "@total-typescript/ts-reset": "^0.6.1",
@@ -55,7 +55,7 @@
55
55
  "eslint-plugin-react-hooks": "^5.1.0",
56
56
  "globals": "^15.14.0",
57
57
  "typescript": "^5.2.2",
58
- "vite": "^6.2.0",
58
+ "vite": "^6.2.4",
59
59
  "vite-tsconfig-paths": "^4.3.1"
60
60
  },
61
61
  "engines": {
@@ -331,11 +331,9 @@ export type RecommendedProductFragment = Pick<
331
331
  priceRange: {
332
332
  minVariantPrice: Pick<StorefrontAPI.MoneyV2, 'amount' | 'currencyCode'>;
333
333
  };
334
- images: {
335
- nodes: Array<
336
- Pick<StorefrontAPI.Image, 'id' | 'url' | 'altText' | 'width' | 'height'>
337
- >;
338
- };
334
+ featuredImage?: StorefrontAPI.Maybe<
335
+ Pick<StorefrontAPI.Image, 'id' | 'url' | 'altText' | 'width' | 'height'>
336
+ >;
339
337
  };
340
338
 
341
339
  export type RecommendedProductsQueryVariables = StorefrontAPI.Exact<{
@@ -353,14 +351,12 @@ export type RecommendedProductsQuery = {
353
351
  'amount' | 'currencyCode'
354
352
  >;
355
353
  };
356
- images: {
357
- nodes: Array<
358
- Pick<
359
- StorefrontAPI.Image,
360
- 'id' | 'url' | 'altText' | 'width' | 'height'
361
- >
362
- >;
363
- };
354
+ featuredImage?: StorefrontAPI.Maybe<
355
+ Pick<
356
+ StorefrontAPI.Image,
357
+ 'id' | 'url' | 'altText' | 'width' | 'height'
358
+ >
359
+ >;
364
360
  }
365
361
  >;
366
362
  };
@@ -374,22 +370,29 @@ export type ArticleQueryVariables = StorefrontAPI.Exact<{
374
370
  }>;
375
371
 
376
372
  export type ArticleQuery = {
377
- blog?: StorefrontAPI.Maybe<{
378
- articleByHandle?: StorefrontAPI.Maybe<
379
- Pick<StorefrontAPI.Article, 'title' | 'contentHtml' | 'publishedAt'> & {
380
- author?: StorefrontAPI.Maybe<Pick<StorefrontAPI.ArticleAuthor, 'name'>>;
381
- image?: StorefrontAPI.Maybe<
382
- Pick<
383
- StorefrontAPI.Image,
384
- 'id' | 'altText' | 'url' | 'width' | 'height'
385
- >
386
- >;
387
- seo?: StorefrontAPI.Maybe<
388
- Pick<StorefrontAPI.Seo, 'description' | 'title'>
389
- >;
390
- }
391
- >;
392
- }>;
373
+ blog?: StorefrontAPI.Maybe<
374
+ Pick<StorefrontAPI.Blog, 'handle'> & {
375
+ articleByHandle?: StorefrontAPI.Maybe<
376
+ Pick<
377
+ StorefrontAPI.Article,
378
+ 'handle' | 'title' | 'contentHtml' | 'publishedAt'
379
+ > & {
380
+ author?: StorefrontAPI.Maybe<
381
+ Pick<StorefrontAPI.ArticleAuthor, 'name'>
382
+ >;
383
+ image?: StorefrontAPI.Maybe<
384
+ Pick<
385
+ StorefrontAPI.Image,
386
+ 'id' | 'altText' | 'url' | 'width' | 'height'
387
+ >
388
+ >;
389
+ seo?: StorefrontAPI.Maybe<
390
+ Pick<StorefrontAPI.Seo, 'description' | 'title'>
391
+ >;
392
+ }
393
+ >;
394
+ }
395
+ >;
393
396
  };
394
397
 
395
398
  export type BlogQueryVariables = StorefrontAPI.Exact<{
@@ -407,7 +410,7 @@ export type BlogQueryVariables = StorefrontAPI.Exact<{
407
410
 
408
411
  export type BlogQuery = {
409
412
  blog?: StorefrontAPI.Maybe<
410
- Pick<StorefrontAPI.Blog, 'title'> & {
413
+ Pick<StorefrontAPI.Blog, 'title' | 'handle'> & {
411
414
  seo?: StorefrontAPI.Maybe<
412
415
  Pick<StorefrontAPI.Seo, 'title' | 'description'>
413
416
  >;
@@ -587,6 +590,24 @@ export type StoreCollectionsQuery = {
587
590
  };
588
591
  };
589
592
 
593
+ export type MoneyCollectionItemFragment = Pick<
594
+ StorefrontAPI.MoneyV2,
595
+ 'amount' | 'currencyCode'
596
+ >;
597
+
598
+ export type CollectionItemFragment = Pick<
599
+ StorefrontAPI.Product,
600
+ 'id' | 'handle' | 'title'
601
+ > & {
602
+ featuredImage?: StorefrontAPI.Maybe<
603
+ Pick<StorefrontAPI.Image, 'id' | 'altText' | 'url' | 'width' | 'height'>
604
+ >;
605
+ priceRange: {
606
+ minVariantPrice: Pick<StorefrontAPI.MoneyV2, 'amount' | 'currencyCode'>;
607
+ maxVariantPrice: Pick<StorefrontAPI.MoneyV2, 'amount' | 'currencyCode'>;
608
+ };
609
+ };
610
+
590
611
  export type CatalogQueryVariables = StorefrontAPI.Exact<{
591
612
  country?: StorefrontAPI.InputMaybe<StorefrontAPI.CountryCode>;
592
613
  language?: StorefrontAPI.InputMaybe<StorefrontAPI.LanguageCode>;
@@ -637,7 +658,7 @@ export type PageQueryVariables = StorefrontAPI.Exact<{
637
658
 
638
659
  export type PageQuery = {
639
660
  page?: StorefrontAPI.Maybe<
640
- Pick<StorefrontAPI.Page, 'id' | 'title' | 'body'> & {
661
+ Pick<StorefrontAPI.Page, 'handle' | 'id' | 'title' | 'body'> & {
641
662
  seo?: StorefrontAPI.Maybe<
642
663
  Pick<StorefrontAPI.Seo, 'description' | 'title'>
643
664
  >;
@@ -1181,15 +1202,15 @@ interface GeneratedQueryTypes {
1181
1202
  return: FeaturedCollectionQuery;
1182
1203
  variables: FeaturedCollectionQueryVariables;
1183
1204
  };
1184
- '#graphql\n fragment RecommendedProduct on Product {\n id\n title\n handle\n priceRange {\n minVariantPrice {\n amount\n currencyCode\n }\n }\n images(first: 1) {\n nodes {\n id\n url\n altText\n width\n height\n }\n }\n }\n query RecommendedProducts ($country: CountryCode, $language: LanguageCode)\n @inContext(country: $country, language: $language) {\n products(first: 4, sortKey: UPDATED_AT, reverse: true) {\n nodes {\n ...RecommendedProduct\n }\n }\n }\n': {
1205
+ '#graphql\n fragment RecommendedProduct on Product {\n id\n title\n handle\n priceRange {\n minVariantPrice {\n amount\n currencyCode\n }\n }\n featuredImage {\n id\n url\n altText\n width\n height\n }\n }\n query RecommendedProducts ($country: CountryCode, $language: LanguageCode)\n @inContext(country: $country, language: $language) {\n products(first: 4, sortKey: UPDATED_AT, reverse: true) {\n nodes {\n ...RecommendedProduct\n }\n }\n }\n': {
1185
1206
  return: RecommendedProductsQuery;
1186
1207
  variables: RecommendedProductsQueryVariables;
1187
1208
  };
1188
- '#graphql\n query Article(\n $articleHandle: String!\n $blogHandle: String!\n $country: CountryCode\n $language: LanguageCode\n ) @inContext(language: $language, country: $country) {\n blog(handle: $blogHandle) {\n articleByHandle(handle: $articleHandle) {\n title\n contentHtml\n publishedAt\n author: authorV2 {\n name\n }\n image {\n id\n altText\n url\n width\n height\n }\n seo {\n description\n title\n }\n }\n }\n }\n': {
1209
+ '#graphql\n query Article(\n $articleHandle: String!\n $blogHandle: String!\n $country: CountryCode\n $language: LanguageCode\n ) @inContext(language: $language, country: $country) {\n blog(handle: $blogHandle) {\n handle\n articleByHandle(handle: $articleHandle) {\n handle\n title\n contentHtml\n publishedAt\n author: authorV2 {\n name\n }\n image {\n id\n altText\n url\n width\n height\n }\n seo {\n description\n title\n }\n }\n }\n }\n': {
1189
1210
  return: ArticleQuery;
1190
1211
  variables: ArticleQueryVariables;
1191
1212
  };
1192
- '#graphql\n query Blog(\n $language: LanguageCode\n $blogHandle: String!\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(language: $language) {\n blog(handle: $blogHandle) {\n title\n seo {\n title\n description\n }\n articles(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor\n ) {\n nodes {\n ...ArticleItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n hasNextPage\n endCursor\n startCursor\n }\n\n }\n }\n }\n fragment ArticleItem on Article {\n author: authorV2 {\n name\n }\n contentHtml\n handle\n id\n image {\n id\n altText\n url\n width\n height\n }\n publishedAt\n title\n blog {\n handle\n }\n }\n': {
1213
+ '#graphql\n query Blog(\n $language: LanguageCode\n $blogHandle: String!\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(language: $language) {\n blog(handle: $blogHandle) {\n title\n handle\n seo {\n title\n description\n }\n articles(\n first: $first,\n last: $last,\n before: $startCursor,\n after: $endCursor\n ) {\n nodes {\n ...ArticleItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n hasNextPage\n endCursor\n startCursor\n }\n\n }\n }\n }\n fragment ArticleItem on Article {\n author: authorV2 {\n name\n }\n contentHtml\n handle\n id\n image {\n id\n altText\n url\n width\n height\n }\n publishedAt\n title\n blog {\n handle\n }\n }\n': {
1193
1214
  return: BlogQuery;
1194
1215
  variables: BlogQueryVariables;
1195
1216
  };
@@ -1205,11 +1226,11 @@ interface GeneratedQueryTypes {
1205
1226
  return: StoreCollectionsQuery;
1206
1227
  variables: StoreCollectionsQueryVariables;
1207
1228
  };
1208
- '#graphql\n query Catalog(\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n products(first: $first, last: $last, before: $startCursor, after: $endCursor) {\n nodes {\n ...ProductItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n startCursor\n endCursor\n }\n }\n }\n #graphql\n fragment MoneyProductItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment ProductItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyProductItem\n }\n maxVariantPrice {\n ...MoneyProductItem\n }\n }\n }\n\n': {
1229
+ '#graphql\n query Catalog(\n $country: CountryCode\n $language: LanguageCode\n $first: Int\n $last: Int\n $startCursor: String\n $endCursor: String\n ) @inContext(country: $country, language: $language) {\n products(first: $first, last: $last, before: $startCursor, after: $endCursor) {\n nodes {\n ...CollectionItem\n }\n pageInfo {\n hasPreviousPage\n hasNextPage\n startCursor\n endCursor\n }\n }\n }\n #graphql\n fragment MoneyCollectionItem on MoneyV2 {\n amount\n currencyCode\n }\n fragment CollectionItem on Product {\n id\n handle\n title\n featuredImage {\n id\n altText\n url\n width\n height\n }\n priceRange {\n minVariantPrice {\n ...MoneyCollectionItem\n }\n maxVariantPrice {\n ...MoneyCollectionItem\n }\n }\n }\n\n': {
1209
1230
  return: CatalogQuery;
1210
1231
  variables: CatalogQueryVariables;
1211
1232
  };
1212
- '#graphql\n query Page(\n $language: LanguageCode,\n $country: CountryCode,\n $handle: String!\n )\n @inContext(language: $language, country: $country) {\n page(handle: $handle) {\n id\n title\n body\n seo {\n description\n title\n }\n }\n }\n': {
1233
+ '#graphql\n query Page(\n $language: LanguageCode,\n $country: CountryCode,\n $handle: String!\n )\n @inContext(language: $language, country: $country) {\n page(handle: $handle) {\n handle\n id\n title\n body\n seo {\n description\n title\n }\n }\n }\n': {
1213
1234
  return: PageQuery;
1214
1235
  variables: PageQueryVariables;
1215
1236
  };
@@ -2,7 +2,10 @@
2
2
  "browserslist": [
3
3
  "defaults"
4
4
  ],
5
+ "dependencies": {
6
+ "tailwindcss": "^4.1.6"
7
+ },
5
8
  "devDependencies": {
6
- "@tailwindcss/vite": "4.0.14"
9
+ "@tailwindcss/vite": "^4.1.6"
7
10
  }
8
11
  }
@@ -1748,5 +1748,5 @@
1748
1748
  ]
1749
1749
  }
1750
1750
  },
1751
- "version": "10.0.0"
1751
+ "version": "10.0.2"
1752
1752
  }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "access": "public",
5
5
  "@shopify:registry": "https://registry.npmjs.org"
6
6
  },
7
- "version": "10.0.0",
7
+ "version": "10.0.2",
8
8
  "license": "MIT",
9
9
  "type": "module",
10
10
  "repository": {
@@ -33,15 +33,15 @@
33
33
  "flame-chart-js": "2.3.2",
34
34
  "get-port": "^7.0.0",
35
35
  "type-fest": "^4.33.0",
36
- "vite": "^6.2.0",
36
+ "vite": "^6.2.4",
37
37
  "vitest": "^1.0.4"
38
38
  },
39
39
  "dependencies": {
40
40
  "@ast-grep/napi": "0.11.0",
41
41
  "@oclif/core": "3.26.5",
42
- "@shopify/cli-kit": "^3.77.1",
43
- "@shopify/oxygen-cli": "4.6.10",
44
- "@shopify/plugin-cloudflare": "^3.77.1",
42
+ "@shopify/cli-kit": "^3.79.2",
43
+ "@shopify/oxygen-cli": "4.6.18",
44
+ "@shopify/plugin-cloudflare": "^3.78.1",
45
45
  "ansi-escapes": "^6.2.0",
46
46
  "chokidar": "3.5.3",
47
47
  "cli-truncate": "^4.0.0",
@@ -62,7 +62,7 @@
62
62
  "@graphql-codegen/cli": "^5.0.2",
63
63
  "@remix-run/dev": "^2.16.1",
64
64
  "@shopify/hydrogen-codegen": "^0.3.3",
65
- "@shopify/mini-oxygen": "^3.2.0",
65
+ "@shopify/mini-oxygen": "^3.2.1",
66
66
  "graphql-config": "^5.0.3",
67
67
  "vite": "^5.1.0 || ^6.2.0"
68
68
  },