UNPKG

@shopify/cli

Version:

A CLI tool to build for the Shopify platform

1,299 lines (1,027 loc) • 112 kB
# skeleton ## 2026.1.1 ### Patch Changes - Added a notice with relevant information on how to link a store in case there is no store linked yet ([#3448](https://github.com/Shopify/hydrogen/pull/3448)) by [@fredericoo](https://github.com/fredericoo) - Fixed an issue where users without addresses could not add the first one ([#3456](https://github.com/Shopify/hydrogen/pull/3456)) by [@fredericoo](https://github.com/fredericoo) - Updated dependencies [[`ff93a1daf2207e52e1f8331f9ff2ccd1f9b7fed6`](https://github.com/Shopify/hydrogen/commit/ff93a1daf2207e52e1f8331f9ff2ccd1f9b7fed6)]: - @shopify/hydrogen@2026.1.1 ## 2026.1.0 ### Major Changes - Updated to Storefront API 2026-01 and Customer Account API 2026-01. ([#3434](https://github.com/Shopify/hydrogen/pull/3434)) by [@kdaviduik](https://github.com/kdaviduik) This is a quarterly API version update aligned with Shopify's API release schedule. **Action Required**: The `cartDiscountCodesUpdate` mutation now requires the `discountCodes` argument. If you have custom cart discount code logic, verify your mutations include this field. Review the changelogs for other changes that may affect your storefront: - [Storefront API 2026-01 changelog](https://shopify.dev/changelog?filter=api&api_version=2026-01&api_type=storefront-graphql) - [Customer Account API 2026-01 changelog](https://shopify.dev/changelog?filter=api&api_version=2026-01&api_type=customer-account-graphql) ### Patch Changes - Updated dependencies [[`d46c8864aea059cac7dda4871a565f76a04b1495`](https://github.com/Shopify/hydrogen/commit/d46c8864aea059cac7dda4871a565f76a04b1495)]: - @shopify/hydrogen@2026.0.0 ## 2025.10.1 ### Major Changes - Update Storefront API and Customer Account API to version 2025-10 ([#3430](https://github.com/Shopify/hydrogen/pull/3430)) by [@kdaviduik](https://github.com/kdaviduik) ### Patch Changes - Add support for Bun's text-based lockfile (`bun.lock`) introduced in Bun 1.2, and npm's shrinkwrap lockfile (`npm-shrinkwrap.json`), as alternatives to their respective primary lockfiles (`bun.lockb` and `package-lock.json`). ([#3430](https://github.com/Shopify/hydrogen/pull/3430)) by [@kdaviduik](https://github.com/kdaviduik) - Add `cartGiftCardCodesAdd` mutation ([#3430](https://github.com/Shopify/hydrogen/pull/3430)) by [@kdaviduik](https://github.com/kdaviduik) ## New Feature: cartGiftCardCodesAdd The skeleton template has been updated to use the new `cartGiftCardCodesAdd` mutation: - Removed `UpdateGiftCardForm` component from `CartSummary.tsx` - Added `AddGiftCardForm` component using `CartForm.ACTIONS.GiftCardCodesAdd` If you customized the gift card form in your project, you may want to migrate to the new `Add` action for simpler code. ## Usage ```typescript import {CartForm} from '@shopify/hydrogen'; <CartForm action={CartForm.ACTIONS.GiftCardCodesAdd} inputs={{giftCardCodes: ['CODE1', 'CODE2']}}> <button>Add Gift Cards</button> </CartForm> ``` Or with createCartHandler: ```typescript const cart = createCartHandler({storefront, getCartId, setCartId}); await cart.addGiftCardCodes(['SUMMER2025', 'WELCOME10']); ``` - Add support for nested cart line items (warranties, gift wrapping, etc.) ([#3430](https://github.com/Shopify/hydrogen/pull/3430)) by [@kdaviduik](https://github.com/kdaviduik) Storefront API 2025-10 introduces `parentRelationship` on cart line items, enabling parent-child relationships for add-ons. This update displays nested line items in the cart. ### Changes - Updates GraphQL fragments to include `parentRelationship` and `lineComponents` fields - Updates `CartMain` and `CartLineItem` to render child line items with visual hierarchy ### Note This update focuses on **displaying** nested line items. To add both a product and its child (e.g., warranty) in a single action: ```tsx <AddToCartButton lines={[ {merchandiseId: 'gid://shopify/ProductVariant/laptop-456', quantity: 1}, { merchandiseId: 'gid://shopify/ProductVariant/warranty-123', quantity: 1, parent: {merchandiseId: 'gid://shopify/ProductVariant/laptop-456'}, }, ]} > Add to Cart with Warranty </AddToCartButton> ``` - Updated dependencies [[`722915130410086bc7af22215ba57ee77aa14156`](https://github.com/Shopify/hydrogen/commit/722915130410086bc7af22215ba57ee77aa14156)]: - @shopify/hydrogen@2025.10.1 ## 2025.10.0 ### Patch Changes - Updated dependencies [[`cd653456fbd1e7e1ab1f6fecff04c89a74b6cad9`](https://github.com/Shopify/hydrogen/commit/cd653456fbd1e7e1ab1f6fecff04c89a74b6cad9), [`24d26ad94e90ab0a859c274838f7f31e75a7808c`](https://github.com/Shopify/hydrogen/commit/24d26ad94e90ab0a859c274838f7f31e75a7808c), [`13a6f8987ea20d33a30a9c0329d7c11528b892ea`](https://github.com/Shopify/hydrogen/commit/13a6f8987ea20d33a30a9c0329d7c11528b892ea), [`403c1f5b6e266c3dfad30f7cfed229e3304570b0`](https://github.com/Shopify/hydrogen/commit/403c1f5b6e266c3dfad30f7cfed229e3304570b0), [`38f8a79625838a9cd4520b20c0db2e5d331f7d26`](https://github.com/Shopify/hydrogen/commit/38f8a79625838a9cd4520b20c0db2e5d331f7d26)]: - @shopify/hydrogen@2026.0.0 ## 2025.7.3 ### Patch Changes - Support OAuth parameters via URL query strings in skeleton login route ([#3391](https://github.com/Shopify/hydrogen/pull/3391)) by [@kdaviduik](https://github.com/kdaviduik) The skeleton template's login route (`account_.login.tsx`) now reads OAuth parameters from the URL and forwards them to `customerAccount.login()`. This enables deep linking to the login page with pre-configured authentication options. ### Supported Query Parameters | Query Parameter | Description | | ----------------- | ---------------------------------------------------------------------------------- | | `acr_values` | Direct users to a specific login method (e.g., `provider:google` for social login) | | `login_hint` | Pre-fill the email address field | | `login_hint_mode` | When set to `submit` with `login_hint`, auto-submits the login form | | `locale` | Display the login page in a specific language (e.g., `fr`, `zh-CN`) | ### Usage Examples ``` /account/login?login_hint=user@example.com /account/login?login_hint=user@example.com&login_hint_mode=submit /account/login?acr_values=provider:google /account/login?locale=fr ``` - Updated dependencies [[`7c077f5f21a595c0355873ac8073b716dfeaf4d0`](https://github.com/Shopify/hydrogen/commit/7c077f5f21a595c0355873ac8073b716dfeaf4d0), [`7c077f5f21a595c0355873ac8073b716dfeaf4d0`](https://github.com/Shopify/hydrogen/commit/7c077f5f21a595c0355873ac8073b716dfeaf4d0), [`7c077f5f21a595c0355873ac8073b716dfeaf4d0`](https://github.com/Shopify/hydrogen/commit/7c077f5f21a595c0355873ac8073b716dfeaf4d0)]: - @shopify/hydrogen@2025.8.0 ## 2025.7.2 ### Patch Changes - Updated dependencies [[`6d22e45d29e89a7a8dddfe3c06459d89e4590483`](https://github.com/Shopify/hydrogen/commit/6d22e45d29e89a7a8dddfe3c06459d89e4590483), [`702f966c8e60eba7434363c9012f12bed92a8e4d`](https://github.com/Shopify/hydrogen/commit/702f966c8e60eba7434363c9012f12bed92a8e4d)]: - @shopify/hydrogen@2025.7.2 ## 2025.7.1 ### Patch Changes - Update React Router to 7.12.0 with stabilized future flags ([#3346](https://github.com/Shopify/hydrogen/pull/3346)) by [@kdaviduik](https://github.com/kdaviduik) This release uses React Router's newly stabilized future flags (`v8_splitRouteModules`, `v8_middleware`) instead of their unstable counterparts - Moved server build in `server.ts` from a dynamic import to a static import to speed up first rendering during local development (2s => 200ms). ([#3331](https://github.com/Shopify/hydrogen/pull/3331)) by [@frandiox](https://github.com/frandiox) ```diff // server.ts +import * as serverBuild from 'virtual:react-router/server-build'; const handleRequest = createRequestHandler({ - build: await import('virtual:react-router/server-build'), + build: serverBuild, ``` Updated ESLint config to allow `virtual:` imports: ```diff // eslint.config.js rules: { + 'import/no-unresolved': ['error', {ignore: ['^virtual:']}], ``` - Updated dependencies [[`264e13349168f17cc1f096c84135d13d38cfc8df`](https://github.com/Shopify/hydrogen/commit/264e13349168f17cc1f096c84135d13d38cfc8df), [`6d5e3d371e7e02e15be17029a0f34daae53a978e`](https://github.com/Shopify/hydrogen/commit/6d5e3d371e7e02e15be17029a0f34daae53a978e), [`ee00f1025867c40d5f67fa89d4ffb215bf280e8f`](https://github.com/Shopify/hydrogen/commit/ee00f1025867c40d5f67fa89d4ffb215bf280e8f), [`5a38948133766e358c5f357f52562f6fdcfe7969`](https://github.com/Shopify/hydrogen/commit/5a38948133766e358c5f357f52562f6fdcfe7969), [`264e13349168f17cc1f096c84135d13d38cfc8df`](https://github.com/Shopify/hydrogen/commit/264e13349168f17cc1f096c84135d13d38cfc8df)]: - @shopify/hydrogen@2025.7.1 ## 2025.7.0 ### Major Changes - Update Storefront API and Customer Account API to version 2025-07 ([#3082](https://github.com/Shopify/hydrogen/pull/3082)) by [@juanpprieto](https://github.com/juanpprieto) This update includes: - Updated API version constants to 2025-07 - Regenerated GraphQL types for both Storefront and Customer Account APIs - Updated all hardcoded API version references in documentation and tests - Regenerated skeleton template types - Updated skeleton's @shopify/cli dependency to ~3.83.3 Breaking changes may occur due to API schema changes between versions. ### Patch Changes - Fix defer/streaming in development & preview ([#3039](https://github.com/Shopify/hydrogen/pull/3039)) by [@kdaviduik](https://github.com/kdaviduik) - Upgrade Miniflare from v2 to v4 in mini-oxygen package. ([#3039](https://github.com/Shopify/hydrogen/pull/3039)) by [@kdaviduik](https://github.com/kdaviduik) - Internal MiniOxygen API has been refactored to work with Miniflare v4's new architecture. - Simplified MiniOxygen class - no longer extends MiniflareCore. - Updated global fetch handling to use Miniflare v4's `outboundService` API. - Fixed test infrastructure to use project-relative temporary directories. - Added support for Oxygen compatibility parameters (`compatibilityDate`, `compatibilityFlags`). - Removed dependency on multiple `@miniflare/*` packages in favor of the consolidated `miniflare` package. - Update and pin react-router to 7.9.2 for 2025.7.0 ([#3138](https://github.com/Shopify/hydrogen/pull/3138)) by [@juanpprieto](https://github.com/juanpprieto) - Add TypeScript ESLint rules for promise handling to prevent Cloudflare Workers errors ([#3146](https://github.com/Shopify/hydrogen/pull/3146)) by [@kdaviduik](https://github.com/kdaviduik) Added `@typescript-eslint/no-floating-promises` and `@typescript-eslint/no-misused-promises` rules to help prevent "The script will never generate a response" errors when deploying to Oxygen/Cloudflare Workers. These rules ensure promises are properly handled with await, return, or void operators, as recommended by [Cloudflare's error documentation](https://developers.cloudflare.com/workers/observability/errors/#the-script-will-never-generate-a-response-errors). - Fixed React Context error that occurred during client-side hydration when using Content Security Policy (CSP) with nonces. The error "Cannot read properties of null (reading 'useContext')" was caused by the `NonceProvider` being present during server-side rendering but missing during client hydration. ([#3082](https://github.com/Shopify/hydrogen/pull/3082)) by [@juanpprieto](https://github.com/juanpprieto) #### Changes for Existing Projects If you have customized your `app/entry.client.tsx` file, you may need to wrap your app with the `NonceProvider` during hydration to avoid this error: ```diff // app/entry.client.tsx import {HydratedRouter} from 'react-router/dom'; import {startTransition, StrictMode} from 'react'; import {hydrateRoot} from 'react-dom/client'; + import {NonceProvider} from '@shopify/hydrogen'; if (!window.location.origin.includes('webcache.googleusercontent.com')) { startTransition(() => { + // Extract nonce from existing script tags + const existingNonce = document + .querySelector<HTMLScriptElement>('script[nonce]') + ?.nonce; + hydrateRoot( document, <StrictMode> - <HydratedRouter /> + <NonceProvider value={existingNonce}> + <HydratedRouter /> + </NonceProvider> </StrictMode>, ); }); } ``` This ensures the React Context tree matches between server and client rendering, preventing hydration mismatches. #### Package Changes - **@shopify/hydrogen**: Exported `NonceProvider` from the main package to allow client-side usage and simplified Vite configuration to improve React Context stability during development - **skeleton**: Updated the template's `entry.client.tsx` to include the `NonceProvider` wrapper during hydration - Add `fulfillmentStatus` to CAAPI order query and route ([#3039](https://github.com/Shopify/hydrogen/pull/3039)) by [@kdaviduik](https://github.com/kdaviduik) - Add GraphQL @defer directive support to storefront client ([#3039](https://github.com/Shopify/hydrogen/pull/3039)) by [@kdaviduik](https://github.com/kdaviduik) - Unpin react-router and react-router-dom versions in the skeleton template ([#3039](https://github.com/Shopify/hydrogen/pull/3039)) by [@kdaviduik](https://github.com/kdaviduik) - Add `@inContext` language support to Customer Account API mutations ([#3039](https://github.com/Shopify/hydrogen/pull/3039)) by [@kdaviduik](https://github.com/kdaviduik) - Add order filtering support to the skeleton /account/orders route for Customer Account API flow ([#3125](https://github.com/Shopify/hydrogen/pull/3125)) by [@juanpprieto](https://github.com/juanpprieto) - Updated dependencies [[`6d067665562223ce2865f1c14be54b0b50258bd4`](https://github.com/Shopify/hydrogen/commit/6d067665562223ce2865f1c14be54b0b50258bd4), [`d57782a1ae3fa0017836d6010fb6ac5ab5d25965`](https://github.com/Shopify/hydrogen/commit/d57782a1ae3fa0017836d6010fb6ac5ab5d25965), [`48cbd450699a29a5667bee7174f3856430508ecc`](https://github.com/Shopify/hydrogen/commit/48cbd450699a29a5667bee7174f3856430508ecc), [`6d067665562223ce2865f1c14be54b0b50258bd4`](https://github.com/Shopify/hydrogen/commit/6d067665562223ce2865f1c14be54b0b50258bd4), [`0b4f01c9aa0e09332140a6a4e3114949873fb0f9`](https://github.com/Shopify/hydrogen/commit/0b4f01c9aa0e09332140a6a4e3114949873fb0f9), [`0d165ff280692411712176427bcd7e0df43b56fe`](https://github.com/Shopify/hydrogen/commit/0d165ff280692411712176427bcd7e0df43b56fe), [`ae7bedc89c1968b4a035f421b5ee6908f6376b1b`](https://github.com/Shopify/hydrogen/commit/ae7bedc89c1968b4a035f421b5ee6908f6376b1b), [`ae7bedc89c1968b4a035f421b5ee6908f6376b1b`](https://github.com/Shopify/hydrogen/commit/ae7bedc89c1968b4a035f421b5ee6908f6376b1b), [`75623a5bfdd8d6f0eab0d3547860341c20d9076c`](https://github.com/Shopify/hydrogen/commit/75623a5bfdd8d6f0eab0d3547860341c20d9076c), [`6681f92e84d42b5a6aca153fb49e31dcd8af84f6`](https://github.com/Shopify/hydrogen/commit/6681f92e84d42b5a6aca153fb49e31dcd8af84f6), [`4daf37ea291334b23bd543fdad5673ab7c9a6133`](https://github.com/Shopify/hydrogen/commit/4daf37ea291334b23bd543fdad5673ab7c9a6133)]: - @shopify/hydrogen@2026.0.0 ## 2025.5.2 ### Patch Changes - Fixing the skeleton's Vite Config ([#2958](https://github.com/Shopify/hydrogen/pull/2958)) by [@balazsbajorics](https://github.com/balazsbajorics) ## 2025.5.1 ### Patch Changes - Bumping the cli to 3.80.4 ([#2956](https://github.com/Shopify/hydrogen/pull/2956)) by [@balazsbajorics](https://github.com/balazsbajorics) ## 2025.5.0 ### Patch Changes - Migrating to React Router 7 ([#2866](https://github.com/Shopify/hydrogen/pull/2866)) by [@balazsbajorics](https://github.com/balazsbajorics) - Updated dependencies [[`e9132d88`](https://github.com/Shopify/hydrogen/commit/e9132d8888ad090d3db41fe4d5d63569a30e9d8e), [`e9132d88`](https://github.com/Shopify/hydrogen/commit/e9132d8888ad090d3db41fe4d5d63569a30e9d8e)]: - @shopify/remix-oxygen@3.0.0 - @shopify/hydrogen@2025.5.0 ## 2025.4.0 ### Patch Changes - 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) - Bump skeleton @shopify/cli and @shopify/mini-oxygen ([#2883](https://github.com/Shopify/hydrogen/pull/2883)) by [@juanpprieto](https://github.com/juanpprieto) - Update SFAPI and CAAPI versions to 2025.04 ([#2886](https://github.com/Shopify/hydrogen/pull/2886)) by [@juanpprieto](https://github.com/juanpprieto) - Updated dependencies [[`af23e710`](https://github.com/Shopify/hydrogen/commit/af23e710dac83bb57498d9c2ef1d8bcf9df55d34), [`9d8a6644`](https://github.com/Shopify/hydrogen/commit/9d8a6644a5b67dca890c6687df390aee78fc85c3)]: - @shopify/hydrogen@2025.4.0 ## 2025.1.7 ### Patch Changes - 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) 1. https://hydrogen.shop/de-de/products/das-chaos 2. https://hydrogen.shop/de-de/products/the-havoc We've changed the starter template to make the second redirect to the first. - Added the Cursor rule for the subscriptions recipe. ([#2874](https://github.com/Shopify/hydrogen/pull/2874)) by [@ruggishop](https://github.com/ruggishop) - Fix faulty truthiness check for cart quantity ([#2855](https://github.com/Shopify/hydrogen/pull/2855)) by [@frontsideair](https://github.com/frontsideair) - Refactor ProductItem into a separate component ([#2872](https://github.com/Shopify/hydrogen/pull/2872)) by [@juanpprieto](https://github.com/juanpprieto) - 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)]: - @shopify/hydrogen@2025.1.4 ## 2025.1.6 ### Patch Changes - Moved the `Layout` component back into `root.tsx` to avoid issues with styled errors. ([#2829](https://github.com/Shopify/hydrogen/pull/2829)) by [@ruggishop](https://github.com/ruggishop) 1. If you have a separate `app/layout.tsx` file, delete it and move its default exported component into your `root.tsx`. For example: ```ts // /app/root.tsx export function Layout({children}: {children?: React.ReactNode}) { const nonce = useNonce(); const data = useRouteLoaderData<RootLoader>('root'); return ( <html lang="en"> ... ); } ``` ## 2025.1.5 ### Patch Changes - Fixed an issue with the creation of JavaScript projects. ([#2818](https://github.com/Shopify/hydrogen/pull/2818)) by [@seanparsons](https://github.com/seanparsons) ## 2025.1.4 ### Patch Changes - Updates the `@shopify/cli`, `@shopify/cli-kit` and `@shopify/plugin-cloudflare` dependencies to 3.77.1. ([#2816](https://github.com/Shopify/hydrogen/pull/2816)) by [@seanparsons](https://github.com/seanparsons) ## 2025.1.3 ### Patch Changes - Bump Remix to 2.16.1 and vite to 6.2.0 ([#2784](https://github.com/Shopify/hydrogen/pull/2784)) by [@wizardlyhel](https://github.com/wizardlyhel) - Update skeleton and create-hydrogen cli to 3.75.4 ([#2769](https://github.com/Shopify/hydrogen/pull/2769)) by [@juanpprieto](https://github.com/juanpprieto) - Fixing typescript compile ([#2787](https://github.com/Shopify/hydrogen/pull/2787)) by [@balazsbajorics](https://github.com/balazsbajorics) In tsconfig.json: ```diff "types": [ "@shopify/oxygen-workers-types", - "@remix-run/node", + "@remix-run/server-runtime", "vite/client" ], ``` - Updates `@shopify/cli-kit`, `@shopify/cli` and `@shopify/plugin-cloudflare` to `3.77.0`. ([#2810](https://github.com/Shopify/hydrogen/pull/2810)) by [@seanparsons](https://github.com/seanparsons) - Support for the Remix future flag `v3_routeConfig`. ([#2722](https://github.com/Shopify/hydrogen/pull/2722)) by [@seanparsons](https://github.com/seanparsons) Please refer to the Remix documentation for more details on `v3_routeConfig` future flag: [https://remix.run/docs/en/main/start/future-flags#v3_routeconfig](https://remix.run/docs/en/main/start/future-flags#v3_routeconfig) 1. Update your `vite.config.ts`. ```diff export default defineConfig({ plugins: [ hydrogen(), oxygen(), remix({ - presets: [hydrogen.preset()], + presets: [hydrogen.v3preset()], future: { v3_fetcherPersist: true, v3_relativeSplatPath: true, v3_throwAbortReason: true, v3_lazyRouteDiscovery: true, v3_singleFetch: true, + v3_routeConfig: true, }, }), tsconfigPaths(), ], ``` 1. Update your `package.json` and install the new packages. Make sure to match the Remix version along with other Remix npm packages and ensure the versions are 2.16.1 or above: ```diff "devDependencies": { "@remix-run/dev": "^2.16.1", + "@remix-run/fs-routes": "^2.16.1", + "@remix-run/route-config": "^2.16.1", ``` 1. Move the `Layout` component export from `root.tsx` into its own file. Make sure to supply an `<Outlet>` so Remix knows where to inject your route content. ```ts // /app/layout.tsx import {Outlet} from '@remix-run/react'; export default function Layout() { const nonce = useNonce(); const data = useRouteLoaderData<RootLoader>('root'); return ( <html lang="en"> ... <Outlet /> ... </html> ); } // Remember to remove the Layout export from your root.tsx ``` 1. Add a routes.ts file. This is your new Remix route configuration file. ```ts import {flatRoutes} from '@remix-run/fs-routes'; import {layout, type RouteConfig} from '@remix-run/route-config'; import {hydrogenRoutes} from '@shopify/hydrogen'; export default hydrogenRoutes([ // Your entire app reading from routes folder using Layout from layout.tsx layout('./layout.tsx', await flatRoutes()), ]) satisfies RouteConfig; ``` - Updated dependencies [[`0425e50d`](https://github.com/Shopify/hydrogen/commit/0425e50dafe2f42326cba67076e5fcea2905e885), [`74ef1ba7`](https://github.com/Shopify/hydrogen/commit/74ef1ba7d41988350e9d2c81731c90381943d1f0)]: - @shopify/remix-oxygen@2.0.12 - @shopify/hydrogen@2025.1.3 ## 2025.1.2 ### Patch Changes - Bump cli version ([#2760](https://github.com/Shopify/hydrogen/pull/2760)) by [@rbshop](https://github.com/rbshop) - Updated dependencies [[`128dfcd6`](https://github.com/Shopify/hydrogen/commit/128dfcd6b254a7465d93be49d3bcbff5251e5ffc)]: - @shopify/hydrogen@2025.1.2 ## 2025.1.1 ### Patch Changes - Upgrade eslint to version 9 and unify eslint config across all packages (with the exception of the skeleton, which still keeps its own config) ([#2716](https://github.com/Shopify/hydrogen/pull/2716)) by [@liady](https://github.com/liady) - Bump remix version ([#2740](https://github.com/Shopify/hydrogen/pull/2740)) by [@wizardlyhel](https://github.com/wizardlyhel) - Turn on Remix `v3_singleFetch` future flag ([#2708](https://github.com/Shopify/hydrogen/pull/2708)) by [@wizardlyhel](https://github.com/wizardlyhel) Remix single fetch migration quick guide: https://remix.run/docs/en/main/start/future-flags#v3_singlefetch Remix single fetch migration guide: https://remix.run/docs/en/main/guides/single-fetch **Note:** If you have any routes that appends (or looks for) a search param named `_data`, make sure to rename it to something else. 1. In your `vite.config.ts`, add the single fetch future flag. ```diff + declare module "@remix-run/server-runtime" { + interface Future { + v3_singleFetch: true; + } + } export default defineConfig({ plugins: [ hydrogen(), oxygen(), remix({ presets: [hydrogen.preset()], future: { v3_fetcherPersist: true, v3_relativeSplatPath: true, v3_throwAbortReason: true, v3_lazyRouteDiscovery: true, + v3_singleFetch: true, }, }), tsconfigPaths(), ], ``` 2. In your `entry.server.tsx`, add `nonce` to the `<RemixServer>`. ```diff const body = await renderToReadableStream( <NonceProvider> <RemixServer context={remixContext} url={request.url} + nonce={nonce} /> </NonceProvider>, ``` 3. Update the `shouldRevalidate` function in `root.tsx`. Defaulting to no revalidation for root loader data to improve performance. When using this feature, you risk your UI getting out of sync with your server. Use with caution. If you are uncomfortable with this optimization, update the `return false;` to `return defaultShouldRevalidate;` instead. For more details see: https://remix.run/docs/en/main/route/should-revalidate ```diff export const shouldRevalidate: ShouldRevalidateFunction = ({ formMethod, currentUrl, nextUrl, - defaultShouldRevalidate, }) => { // revalidate when a mutation is performed e.g add to cart, login... if (formMethod && formMethod !== 'GET') return true; // revalidate when manually revalidating via useRevalidator if (currentUrl.toString() === nextUrl.toString()) return true; - return defaultShouldRevalidate; + return false; }; ``` 4. Update `cart.tsx` to add a headers export and update to `data` import usage. ```diff import { - json, + data, type LoaderFunctionArgs, type ActionFunctionArgs, type HeadersFunction } from '@shopify/remix-oxygen'; + export const headers: HeadersFunction = ({actionHeaders}) => actionHeaders; export async function action({request, context}: ActionFunctionArgs) { ... - return json( + return data( { cart: cartResult, errors, warnings, analytics: { cartId, }, }, {status, headers}, ); } export async function loader({context}: LoaderFunctionArgs) { const {cart} = context; - return json(await cart.get()); + return await cart.get(); } ``` 5. Deprecate `json` and `defer` import usage from `@shopify/remix-oxygen`. Remove `json()`/`defer()` in favor of raw objects. Single Fetch supports JSON objects and Promises out of the box, so you can return the raw data from your loader/action functions: ```diff - import {json} from "@shopify/remix-oxygen"; export async function loader({}: LoaderFunctionArgs) { let tasks = await fetchTasks(); - return json(tasks); + return tasks; } ``` ```diff - import {defer} from "@shopify/remix-oxygen"; export async function loader({}: LoaderFunctionArgs) { let lazyStuff = fetchLazyStuff(); let tasks = await fetchTasks(); - return defer({ tasks, lazyStuff }); + return { tasks, lazyStuff }; } ``` If you were using the second parameter of json/defer to set a custom status or headers on your response, you can continue doing so via the new data API: ```diff - import {json} from "@shopify/remix-oxygen"; + import {data, type HeadersFunction} from "@shopify/remix-oxygen"; + /** + * If your loader or action is returning a response with headers, + * make sure to export a headers function that merges your headers + * on your route. Otherwise, your headers may be lost. + * Remix doc: https://remix.run/docs/en/main/route/headers + **/ + export const headers: HeadersFunction = ({loaderHeaders}) => loaderHeaders; export async function loader({}: LoaderFunctionArgs) { let tasks = await fetchTasks(); - return json(tasks, { + return data(tasks, { headers: { "Cache-Control": "public, max-age=604800" } }); } ``` 6. If you are using legacy customer account flow or multipass, there are a couple more files that requires updating: In `root.tsx` and `routes/account.tsx`, add a `headers` export for `loaderHeaders`. ```diff + export const headers: HeadersFunction = ({loaderHeaders}) => loaderHeaders; ``` In `routes/account_.register.tsx`, add a `headers` export for `actionHeaders`. ```diff + export const headers: HeadersFunction = ({actionHeaders}) => actionHeaders; ``` 7. If you are using multipass, in `routes/account_.login.multipass.tsx` a. export a `headers` export ```diff + export const headers: HeadersFunction = ({actionHeaders}) => actionHeaders; ``` b. Update all `json` response wrapper to `remixData` ```diff import { - json, + data as remixData, } from '@shopify/remix-oxygen'; - return json( + return remixData( ... ); ``` - Updated dependencies [[`3af2e453`](https://github.com/Shopify/hydrogen/commit/3af2e4534eafe1467f70a35885a2fa2ef7724fa8), [`6bff6b62`](https://github.com/Shopify/hydrogen/commit/6bff6b6260af21b8025426c7031ab862dbecbc34), [`cd65685c`](https://github.com/Shopify/hydrogen/commit/cd65685c1036233faaead0330f25183900b102a7), [`8c717570`](https://github.com/Shopify/hydrogen/commit/8c7175701d9f4dd05d271ea46b6ab40d6e3210cb), [`4e81bd1b`](https://github.com/Shopify/hydrogen/commit/4e81bd1b0e99b5c760679b565d2f95c4fc15b934), [`3ea25820`](https://github.com/Shopify/hydrogen/commit/3ea25820b0b0094d982e481782e413165435cf00)]: - @shopify/hydrogen@2025.1.1 - @shopify/remix-oxygen@2.0.11 ## 2025.1.0 ### Patch Changes - Bump vite, Remix versions and tailwind v4 alpha to beta ([#2696](https://github.com/Shopify/hydrogen/pull/2696)) by [@wizardlyhel](https://github.com/wizardlyhel) - Workaround for "Error: failed to execute 'insertBefore' on 'Node'" that sometimes happen during development. ([#2701](https://github.com/Shopify/hydrogen/pull/2701)) by [@wizardlyhel](https://github.com/wizardlyhel) ```diff // root.tsx /** * The main and reset stylesheets are added in the Layout component * to prevent a bug in development HMR updates. * * This avoids the "failed to execute 'insertBefore' on 'Node'" error * that occurs after editing and navigating to another page. * * It's a temporary fix until the issue is resolved. * https://github.com/remix-run/remix/issues/9242 */ export function links() { return [ - {rel: 'stylesheet', href: resetStyles}, - {rel: 'stylesheet', href: appStyles}, { rel: 'preconnect', href: 'https://cdn.shopify.com', }, { rel: 'preconnect', href: 'https://shop.app', }, {rel: 'icon', type: 'image/svg+xml', href: favicon}, ]; } ... export function Layout({children}: {children?: React.ReactNode}) { const nonce = useNonce(); const data = useRouteLoaderData<RootLoader>('root'); return ( <html lang="en"> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> + <link rel="stylesheet" href={resetStyles}></link> + <link rel="stylesheet" href={appStyles}></link> ``` - Turn on future flag `v3_lazyRouteDiscovery` ([#2702](https://github.com/Shopify/hydrogen/pull/2702)) by [@wizardlyhel](https://github.com/wizardlyhel) In your vite.config.ts, add the following line: ```diff export default defineConfig({ plugins: [ hydrogen(), oxygen(), remix({ presets: [hydrogen.preset()], future: { v3_fetcherPersist: true, v3_relativeSplatPath: true, v3_throwAbortReason: true, + v3_lazyRouteDiscovery: true, }, }), tsconfigPaths(), ], ``` Test your app by running `npm run dev` and nothing should break - Fix image size warnings on collections page ([#2703](https://github.com/Shopify/hydrogen/pull/2703)) by [@wizardlyhel](https://github.com/wizardlyhel) - Bump cli version ([#2732](https://github.com/Shopify/hydrogen/pull/2732)) by [@wizardlyhel](https://github.com/wizardlyhel) - Bump SFAPI to 2025-01 ([#2715](https://github.com/Shopify/hydrogen/pull/2715)) by [@rbshop](https://github.com/rbshop) - Updated dependencies [[`fdab06f5`](https://github.com/Shopify/hydrogen/commit/fdab06f5d34076b526d406698bdf6fca6787660b), [`ae6d71f0`](https://github.com/Shopify/hydrogen/commit/ae6d71f0976f520ca177c69ff677f852af63859e), [`650d57b3`](https://github.com/Shopify/hydrogen/commit/650d57b3e07125661e23900e73c0bb3027ddbcde), [`064de138`](https://github.com/Shopify/hydrogen/commit/064de13890c68cabb1c3fdbe7f77409a0cf1c384)]: - @shopify/remix-oxygen@2.0.10 - @shopify/hydrogen@2025.1.0 ## 2024.10.4 ### Patch Changes - Bump cli version ([#2694](https://github.com/Shopify/hydrogen/pull/2694)) by [@wizardlyhel](https://github.com/wizardlyhel) ## 2024.10.3 ### Patch Changes - Prevent scroll reset on variant change ([#2672](https://github.com/Shopify/hydrogen/pull/2672)) by [@scottdixon](https://github.com/scottdixon) ## 2024.10.2 ### Patch Changes - Remove initial redirect from product display page ([#2643](https://github.com/Shopify/hydrogen/pull/2643)) by [@scottdixon](https://github.com/scottdixon) - Optional updates for the product route and product form to handle combined listing and 2000 variant limit. ([#2659](https://github.com/Shopify/hydrogen/pull/2659)) by [@wizardlyhel](https://github.com/wizardlyhel) 1. Update your SFAPI product query to bring in the new query fields: ```diff const PRODUCT_FRAGMENT = `#graphql fragment Product on Product { id title vendor handle descriptionHtml description + encodedVariantExistence + encodedVariantAvailability options { name optionValues { name + firstSelectableVariant { + ...ProductVariant + } + swatch { + color + image { + previewImage { + url + } + } + } } } - selectedVariant: selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) { + selectedOrFirstAvailableVariant(selectedOptions: $selectedOptions, ignoreUnknownOptions: true, caseInsensitiveMatch: true) { + ...ProductVariant + } + adjacentVariants (selectedOptions: $selectedOptions) { + ...ProductVariant + } - variants(first: 1) { - nodes { - ...ProductVariant - } - } seo { description title } } ${PRODUCT_VARIANT_FRAGMENT} ` as const; ``` 2. Update `loadDeferredData` function. We no longer need to load in all the variants. You can also remove `VARIANTS_QUERY` variable. ```diff function loadDeferredData({context, params}: LoaderFunctionArgs) { + // Put any API calls that is not critical to be available on first page render + // For example: product reviews, product recommendations, social feeds. - // In order to show which variants are available in the UI, we need to query - // all of them. But there might be a *lot*, so instead separate the variants - // into it's own separate query that is deferred. So there's a brief moment - // where variant options might show as available when they're not, but after - // this deferred query resolves, the UI will update. - const variants = context.storefront - .query(VARIANTS_QUERY, { - variables: {handle: params.handle!}, - }) - .catch((error) => { - // Log query errors, but don't throw them so the page can still render - console.error(error); - return null; - }); + return {} - return { - variants, - }; } ``` 3. Remove the redirect logic in the `loadCriticalData` function and completely remove `redirectToFirstVariant` function ```diff async function loadCriticalData({ context, params, request, }: LoaderFunctionArgs) { const {handle} = params; const {storefront} = context; if (!handle) { throw new Error('Expected product handle to be defined'); } const [{product}] = await Promise.all([ storefront.query(PRODUCT_QUERY, { variables: {handle, selectedOptions: getSelectedProductOptions(request)}, }), // Add other queries here, so that they are loaded in parallel ]); if (!product?.id) { throw new Response(null, {status: 404}); } - const firstVariant = product.variants.nodes[0]; - const firstVariantIsDefault = Boolean( - firstVariant.selectedOptions.find( - (option: SelectedOption) => - option.name === 'Title' && option.value === 'Default Title', - ), - ); - if (firstVariantIsDefault) { - product.selectedVariant = firstVariant; - } else { - // if no selected variant was returned from the selected options, - // we redirect to the first variant's url with it's selected options applied - if (!product.selectedVariant) { - throw redirectToFirstVariant({product, request}); - } - } return { product, }; } ... - function redirectToFirstVariant({ - product, - request, - }: { - product: ProductFragment; - request: Request; - }) { - ... - } ``` 4. Update the `Product` component to use the new data fields. ```diff import { getSelectedProductOptions, Analytics, useOptimisticVariant, + getAdjacentAndFirstAvailableVariants, } from '@shopify/hydrogen'; export default function Product() { + const {product} = useLoaderData<typeof loader>(); - const {product, variants} = useLoaderData<typeof loader>(); + // Optimistically selects a variant with given available variant information + const selectedVariant = useOptimisticVariant( + product.selectedOrFirstAvailableVariant, + getAdjacentAndFirstAvailableVariants(product), + ); - const selectedVariant = useOptimisticVariant( - product.selectedVariant, - variants, - ); ``` 5. Handle missing search query param in url from selecting a first variant ```diff import { getSelectedProductOptions, Analytics, useOptimisticVariant, getAdjacentAndFirstAvailableVariants, + useSelectedOptionInUrlParam, } from '@shopify/hydrogen'; export default function Product() { const {product} = useLoaderData<typeof loader>(); // Optimistically selects a variant with given available variant information const selectedVariant = useOptimisticVariant( product.selectedOrFirstAvailableVariant, getAdjacentAndFirstAvailableVariants(product), ); + // Sets the search param to the selected variant without navigation + // only when no search params are set in the url + useSelectedOptionInUrlParam(selectedVariant.selectedOptions); ``` 6. Get the product options array using `getProductOptions` ```diff import { getSelectedProductOptions, Analytics, useOptimisticVariant, + getProductOptions, getAdjacentAndFirstAvailableVariants, useSelectedOptionInUrlParam, } from '@shopify/hydrogen'; export default function Product() { const {product} = useLoaderData<typeof loader>(); // Optimistically selects a variant with given available variant information const selectedVariant = useOptimisticVariant( product.selectedOrFirstAvailableVariant, getAdjacentAndFirstAvailableVariants(product), ); // Sets the search param to the selected variant without navigation // only when no search params are set in the url useSelectedOptionInUrlParam(selectedVariant.selectedOptions); + // Get the product options array + const productOptions = getProductOptions({ + ...product, + selectedOrFirstAvailableVariant: selectedVariant, + }); ``` 7. Remove the `Await` and `Suspense` from the `ProductForm`. We no longer have any queries that we need to wait for. ```diff export default function Product() { ... return ( ... + <ProductForm + productOptions={productOptions} + selectedVariant={selectedVariant} + /> - <Suspense - fallback={ - <ProductForm - product={product} - selectedVariant={selectedVariant} - variants={[]} - /> - } - > - <Await - errorElement="There was a problem loading product variants" - resolve={variants} - > - {(data) => ( - <ProductForm - product={product} - selectedVariant={selectedVariant} - variants={data?.product?.variants.nodes || []} - /> - )} - </Await> - </Suspense> ``` 8. Update the `ProductForm` component. ```tsx import {Link, useNavigate} from '@remix-run/react'; import {type MappedProductOptions} from '@shopify/hydrogen'; import type { Maybe, ProductOptionValueSwatch, } from '@shopify/hydrogen/storefront-api-types'; import {AddToCartButton} from './AddToCartButton'; import {useAside} from './Aside'; import type {ProductFragment} from 'storefrontapi.generated'; export function ProductForm({ productOptions, selectedVariant, }: { productOptions: MappedProductOptions[]; selectedVariant: ProductFragment['selectedOrFirstAvailableVariant']; }) { const navigate = useNavigate(); const {open} = useAside(); return ( <div className="product-form"> {productOptions.map((option) => ( <div className="product-options" key={option.name}> <h5>{option.name}</h5> <div className="product-options-grid"> {option.optionValues.map((value) => { const { name, handle, variantUriQuery, selected, available, exists, isDifferentProduct, swatch, } = value; if (isDifferentProduct) { // SEO // When the variant is a combined listing child product // that leads to a different url, we need to render it // as an anchor tag return ( <Link className="product-options-item" key={option.name + name} prefetch="intent" preventScrollReset replace to={`/products/${handle}?${variantUriQuery}`} style={{ border: selected ? '1px solid black' : '1px solid transparent', opacity: available ? 1 : 0.3, }} > <ProductOptionSwatch swatch={swatch} name={name} /> </Link> ); } else { // SEO // When the variant is an update to the search param, // render it as a button with javascript navigating to // the variant so that SEO bots do not index these as // duplicated links return ( <button type="button" className={`product-options-item${ exists && !selected ? ' link' : '' }`} key={option.name + name} style={{ border: selected ? '1px solid black' : '1px solid transparent', opacity: available ? 1 : 0.3, }} disabled={!exists} onClick={() => { if (!selected) { navigate(`?${variantUriQuery}`, { replace: true, }); } }} > <ProductOptionSwatch swatch={swatch} name={name} /> </button> ); } })} </div> <br /> </div> ))} <AddToCartButton disabled={!selectedVariant || !selectedVariant.availableForSale} onClick={() => { open('cart'); }} lines={ selectedVariant ? [ { merchandiseId: selectedVariant.id, quantity: 1, selectedVariant, }, ] : [] } > {selectedVariant?.availableForSale ? 'Add to cart' : 'Sold out'} </AddToCartButton> </div> ); } function ProductOptionSwatch({ swatch, name, }: { swatch?: Maybe<ProductOptionValueSwatch> | undefined; name: string; }) { const image = swatch?.image?.previewImage?.url; const color = swatch?.color; if (!image && !color) return name; return ( <div aria-label={name} className="product-option-label-swatch" style={{ backgroundColor: color || 'transparent', }} > {!!image && <img src={image} alt={name} />} </div> ); } ``` 9. Update `app.css` ```diff + /* + * -------------------------------------------------- + * Non anchor links + * -------------------------------------------------- + */ + .link:hover { + text-decoration: underline; + cursor: pointer; + } ... - .product-options-item { + .product-options-item, + .product-options-item:disabled { + padding: 0.25rem 0.5rem; + background-color: transparent; + font-size: 1rem; + font-family: inherit; + } + .product-option-label-swatch { + width: 1.25rem; + height: 1.25rem; + margin: 0.25rem 0; + } + .product-option-label-swatch img { + width: 100%; + } ``` 10. Update `lib/variants.ts` Make `useVariantUrl` and `getVariantUrl` flexible to supplying a selected option param ```diff export function useVariantUrl( handle: string, - selectedOptions: SelectedOption[], + selectedOptions?: SelectedOption[], ) { const {pathname} = useLocation(); return useMemo(() => { return getVariantUrl({ handle, pathname, searchParams: new URLSearchParams(), selectedOptions, }); }, [handle, selectedOptions, pathname]); } export function getVariantUrl({ handle, pathname, searchParams, selectedOptions, }: { handle: string; pathname: string; searchParams: URLSearchParams; - selectedOptions: SelectedOption[]; + selectedOptions?: SelectedOption[], }) { const match = /(\/[a-zA-Z]{2}-[a-zA-Z]{2}\/)/g.exec(pathname); const isLocalePathname = match && match.length > 0; const path = isLocalePathname ? `${match![0]}products/${handle}` : `/products/${handle}`; - selectedOptions.forEach((option) => { + selectedOptions?.forEach((option) => { searchParams.set(option.name, option.value); }); ``` 11. Update `routes/collections.$handle.tsx` We no longer need to query for the variants since product route can efficiently obtain the first available variants. Update the code to reflect that: ```diff const PRODUCT_ITEM_FRAGMENT = `#graphql fragment MoneyProductItem on MoneyV2 { amount currencyCode } fragment ProductItem on Product { id handle title featuredImage { id altText url width height } priceRange { minVariantPrice { ...MoneyProductItem } maxVariantPrice { ...MoneyProductItem } } - variants(first: 1) { - nodes { - selectedOptions { - name - value - } - } - } } ` as const; ``` and remove the variant reference ```diff function ProductItem({ product, loading, }: { product: ProductItemFragmen