Incorporate the v3_routeConfig future flag for Remix.#2722
Conversation
|
Oxygen deployed a preview of your
Learn more about Hydrogen's GitHub integration. |
|
|
||
| 2. In the Vite config (`vite.config.ts` usually) the `remix` plugin needs to have it's configuration slightly altered. | ||
|
|
||
| From this: |
There was a problem hiding this comment.
I would wrap this instruction on a diff -/+ block instead
juanpprieto
left a comment
There was a problem hiding this comment.
Looking great Sean!
Tophat found no issues
| "dependencies": { | ||
| "@remix-run/react": "^2.15.3", | ||
| "@remix-run/server-runtime": "^2.15.3", | ||
| "@remix-run/fs-routes": "^2.15.3", |
There was a problem hiding this comment.
A reminder that we will need to add these new dependencies to our changelog.json entry when the time comes to release this
|
Updated this PR so it's just a single patch on |
|
/snapit |
|
🫰✨ Thanks @wizardlyhel! Your snapshots have been published to npm. Test the snapshots by updating your "@shopify/cli-hydrogen": "0.0.0-snapshot-20250321172553",
"@shopify/hydrogen": "0.0.0-snapshot-20250321172553",
"@shopify/hydrogen-codegen": "0.0.0-snapshot-20250321172553",
"@shopify/mini-oxygen": "0.0.0-snapshot-20250321172553",
"@shopify/remix-oxygen": "0.0.0-snapshot-20250321172553"
|
|
/snapit |
|
🫰✨ Thanks @wizardlyhel! Your snapshots have been published to npm. Test the snapshots by updating your "@shopify/cli-hydrogen": "0.0.0-snapshot-20250321203919",
"@shopify/hydrogen": "0.0.0-snapshot-20250321203919",
"@shopify/hydrogen-codegen": "0.0.0-snapshot-20250321203919",
"@shopify/mini-oxygen": "0.0.0-snapshot-20250321203919",
"@shopify/remix-oxygen": "0.0.0-snapshot-20250321203919"
|
|
/snapit |
|
🫰✨ Thanks @wizardlyhel! Your snapshots have been published to npm. Test the snapshots by updating your "@shopify/cli-hydrogen": "0.0.0-snapshot-20250321210338",
"@shopify/hydrogen": "0.0.0-snapshot-20250321210338",
"@shopify/hydrogen-codegen": "0.0.0-snapshot-20250321210338",
"@shopify/mini-oxygen": "0.0.0-snapshot-20250321210338",
"@shopify/remix-oxygen": "0.0.0-snapshot-20250321210338"
|
|
@wizardlyhel , I may have missed it but moving the layout out of root along with imports we loose the ability to have styled error pages / error messages wrapped in layout ? |
|
@jasonloeve |
* WIP adding the v3_routeConfig flag. * WIP testing the example projects. * WIP Trying to diagnose the skeleton project startup failure. * Reimplemented virtual routes to build paths without node libraries. * Removed change to multipass project. * Multipass example now uses v3preset. * Fix virtual route layout * fix the ordering of virtual routes * CLI test fixes. * Working unit tests. * Additional patch to get the tests working correctly. * Fixing eslint errors that arose after the merge. * Applied prettier fixes to virtual-root.tsx. * Added changeset for v3_routeConfig. * Removed change to docs page because it's unnecessary. * Update so that there is only 1 patch on remix-run/dev * fix package lock * remove patch * fix vite import * format * clean up changeset * Fix test * Fix Prettier failure. * fix virtual route without routeConfig flag * format * Fix test * fix test again * fix dev mode * fix test again --------- Co-authored-by: Helen Lin <helen.lin@shopify.com>
Three bugs were preventing HMR from working after the migration to Vite's Environment API: 1. `server.watch: null` (accidentally introduced in #2722) caused Vite to create a NoopWatcher, so no file-change events were ever emitted. Removed it to restore chokidar. 2. The SSR environment was passed `transport: context.ws` (the same WebSocket server the browser uses). Because `isWebSocketServer` is true on that object, Vite used it directly as the SSR environment's hot channel. When the SSR module graph found no React Fast Refresh boundaries (SSR code has none), it sent `full-reload` over that shared WebSocket — hard-refreshing the browser on every file change. Dropping the transport gives the SSR environment a noop hot channel so those events are silently discarded. 3. `@vite/client` was never injected into HTML responses from workerd, so the browser never established a WebSocket connection to Vite's HMR server. React Router's <Scripts /> already injects the inject-hmr-runtime preamble; the only missing piece was @vite/client. Now injected via a simple string replace on HTML responses. Server-side invalidation (route handlers, server.ts) is handled by a new watcher listener in environment.ts: when a file in the SSR module graph changes, the mini-oxygen instance is disposed so the next request starts a fresh workerd with a clean module-runner cache. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Three bugs were preventing HMR from working after the migration to Vite's Environment API: 1. `server.watch: null` (accidentally introduced in #2722) caused Vite to create a NoopWatcher, so no file-change events were ever emitted. Removed it to restore chokidar. 2. The SSR environment was passed `transport: context.ws` (the same WebSocket server the browser uses). Because `isWebSocketServer` is true on that object, Vite used it directly as the SSR environment's hot channel. When the SSR module graph found no React Fast Refresh boundaries (SSR code has none), it sent `full-reload` over that shared WebSocket — hard-refreshing the browser on every file change. Dropping the transport gives the SSR environment a noop hot channel so those events are silently discarded. 3. `@vite/client` was never injected into HTML responses from workerd, so the browser never established a WebSocket connection to Vite's HMR server. React Router's <Scripts /> already injects the inject-hmr-runtime preamble; the only missing piece was @vite/client. Now injected via a simple string replace on HTML responses. Server-side invalidation (route handlers, server.ts) is handled by a new watcher listener in environment.ts: when a file in the SSR module graph changes, the mini-oxygen instance is disposed so the next request starts a fresh workerd with a clean module-runner cache. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Three bugs were preventing HMR from working after the migration to Vite's Environment API: 1. `server.watch: null` (accidentally introduced in #2722) caused Vite to create a NoopWatcher, so no file-change events were ever emitted. Removed it to restore chokidar. 2. The SSR environment was passed `transport: context.ws` (the same WebSocket server the browser uses). Because `isWebSocketServer` is true on that object, Vite used it directly as the SSR environment's hot channel. When the SSR module graph found no React Fast Refresh boundaries (SSR code has none), it sent `full-reload` over that shared WebSocket — hard-refreshing the browser on every file change. Dropping the transport gives the SSR environment a noop hot channel so those events are silently discarded. 3. `@vite/client` was never injected into HTML responses from workerd, so the browser never established a WebSocket connection to Vite's HMR server. React Router's <Scripts /> already injects the inject-hmr-runtime preamble; the only missing piece was @vite/client. Now injected via a simple string replace on HTML responses. Server-side invalidation (route handlers, server.ts) is handled by a new watcher listener in environment.ts: when a file in the SSR module graph changes, the mini-oxygen instance is disposed so the next request starts a fresh workerd with a clean module-runner cache. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* refactor: move mini-oxygen dev runtime to Vite 8 environments Create a fetchable mini-oxygen SSR environment using Vite 8's environment APIs and move runtime lifecycle management into a dedicated environment module. Keep Hydrogen and CLI option passing compatible via registerPluginOptions while routing dev requests through server.environments.ssr and preserving build-time compatibility_date handling. Update the custom workerd module runner bridge for Vite 8's invoke contract, including getBuiltins support and fetchModule options forwarding, so local SSR still runs inside MiniOxygen. Upgrade workspace Vite dependencies to v8, switch example/template configs to native resolve.tsconfigPaths, and add the environment API refactor plan document. Co-authored-by: Codex <codex@openai.com> * refactor: delegate mini-oxygen runner invokes to Vite Replace mini-oxygen's custom /__vite_fetch_module bridge with a service binding that forwards Vite runner invoke payloads to the SSR environment's built-in handleInvoke() path. This removes bespoke request parsing and builtins serialization while keeping the current fetchable SSR environment and Hydrogen/CLI runtime option flow intact. Co-authored-by: Codex <codex@openai.com> * Simplify Vite 8 cleanup around MiniOxygen Drop the leftover unused Hydrogen plugin lookup in dev, and switch MiniOxygen and bundle analyzer asset emission to emitFile() so Vite 8/Rolldown no longer warns about mutating the bundle object. Co-authored-by: OpenAI Codex <codex@openai.com> * Fix Vite 8 CLI typecheck fallout Align CLI internal MiniOxygen typing with monorepo source types, make the bundle analyzer tolerate Vite 8 and Rolldown RenderedModule typing, and fix MiniOxygen proxy port parsing strictness. Co-authored-by: OpenAI Codex <codex@openai.com> * Remove unused MiniOxygen Vite root binding Drop the leftover __VITE_ROOT runtime binding from the MiniOxygen Vite worker bridge now that the Vite 8 Environment API path no longer uses it. Co-authored-by: OpenAI Codex <codex@openai.com> * fix: harden mini-oxygen environment lifecycle Throw on late runtime reconfiguration, remove the refactor plan doc, and add coverage for MiniOxygen's Vite environment lifecycle. Co-authored-by: OpenAI Codex <codex@openai.com> * Fix CLI tests for Vite 8 compatibility - Upgrade vitest from ^1.0.4 to ^3.2.4 in packages/cli (only package still on v1, which only supports Vite ^5) - Update build test assertions to match Vite 8 renamed log messages Co-Authored-By: Claude <noreply@anthropic.com> * Support Vite 6 * fix: restore HMR in Vite Environment API setup Three bugs were preventing HMR from working after the migration to Vite's Environment API: 1. `server.watch: null` (accidentally introduced in #2722) caused Vite to create a NoopWatcher, so no file-change events were ever emitted. Removed it to restore chokidar. 2. The SSR environment was passed `transport: context.ws` (the same WebSocket server the browser uses). Because `isWebSocketServer` is true on that object, Vite used it directly as the SSR environment's hot channel. When the SSR module graph found no React Fast Refresh boundaries (SSR code has none), it sent `full-reload` over that shared WebSocket — hard-refreshing the browser on every file change. Dropping the transport gives the SSR environment a noop hot channel so those events are silently discarded. 3. `@vite/client` was never injected into HTML responses from workerd, so the browser never established a WebSocket connection to Vite's HMR server. React Router's <Scripts /> already injects the inject-hmr-runtime preamble; the only missing piece was @vite/client. Now injected via a simple string replace on HTML responses. Server-side invalidation (route handlers, server.ts) is handled by a new watcher listener in environment.ts: when a file in the SSR module graph changes, the mini-oxygen instance is disposed so the next request starts a fresh workerd with a clean module-runner cache. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * refactor: simplify mini-oxygen Vite HMR wiring Remove redundant MiniOxygen watcher and HTML injection cleanup while keeping the SSR environment request-driven. Co-authored-by: Codex <codex@openai.com> * refactor: trim mini-oxygen environment plumbing Remove the custom MiniOxygen environment marker/getter and read the entrypoint error handler from plugin state instead. Co-authored-by: Codex <codex@openai.com> * docs: clarify mini-oxygen Vite runtime behavior Update MiniOxygen Vite runtime comments to reflect the current request-driven server refresh model and inspector behavior. Co-authored-by: Codex <codex@openai.com> * docs: clarify mini-oxygen environment lifecycle Document the warmup timing and SSR environment replacement behavior in MiniOxygen's Vite environment integration. Co-authored-by: Codex <codex@openai.com> * fix: use accurate type cast for response body Replace the double-cast `as unknown as BodyInit` with `as ReadableStream | null`, which accurately describes the Miniflare Response.body type and avoids the lossy intermediate cast through `unknown`. * fix: extract magic number to WARMUP_SETTLE_DELAY_MS Replace the bare `200` timeout with a named constant that communicates intent at a glance. The delay lets Vite settle after listen/config reload before the synthetic warmup request fires. * chore: add TODO for Vite 6 getBuiltins shim removal Mark the getBuiltins workaround so it's easy to find and remove once Vite 6 support is dropped. Vite 7+ handles this internally. * chore: changeset for mini-oxygen review feedback Patch changeset covering the type cast improvement, magic number extraction, and Vite 6 shim TODO marker. * fix: correct vite compat range in changeset Mini Oxygen requires Vite 6+ (Environment API didn't exist in Vite 5). Only Hydrogen itself supports Vite 5+. * fix: bump react-router from 7.12.0 to 7.14.0 React Router 7.14.0 adds Vite 8 support by switching from the deprecated `esbuild` config API to `oxc`. This eliminates the deprecation warning: `esbuild` option was specified by "react-router" plugin. This option is deprecated, please use `oxc` instead. Also adds `unstable_mask` to mock Location objects in test files to satisfy the new property added to react-router's Location type in 7.14.0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cookbook): regenerate recipes for Vite 8 skeleton changes The Vite 8 migration changed the skeleton template (react-router 7.14.0, vite ^8.0.1, removed vite-tsconfig-paths, updated vite.config.ts structure). All recipe patches referencing old skeleton state needed regeneration. Regenerated 13 recipes: b2b, bundles, combined-listings, custom-cart-method, gtm, infinite-scroll, legacy-customer-account-flow, markets, metaobjects, multipass, partytown, subscriptions, third-party-api. The express recipe was NOT regenerated — its vite.config.ts patch references removed imports (oxygen(), tsconfigPaths) and requires a full rewrite. No E2E test covers it, so it's not blocking CI. For multipass and partytown, the standard `regenerate` command fails because withResolvedCatalog() resolves workspace:* before patch application, breaking context matching. Worked around by calling applyRecipe() directly (bypassing catalog resolution), then running generate and render separately. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cookbook): update multipass and partytown vite.config.ts patches for optimizeDeps format The merge from main introduced the nested dependency syntax in ssr.optimizeDeps.include (e.g., 'react-router > set-cookie-parser'). The multipass and partytown recipe patches still referenced the old single-line format, causing E2E test failures when applying patches to the updated skeleton. Manually rewrote both patches to match the current skeleton's multi-line include array format. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cookbook): correct hunk headers in multipass and partytown patches The previous patch fix had an off-by-one in the @@ hunk header — the first context line started at line 29 but the header claimed line 28. This caused `patch` to apply with an offset, creating .orig backup files, which the apply code treats as a conflict. Fixed by including the `include: [` line (line 28) as leading context so the hunk header accurately matches the file content. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cookbook): regenerate multipass/partytown vite.config.ts patches via git diff The previous patches were hand-edited across three commits (e396456, 862f005, 6b79693) to keep up with skeleton refactors. Hand-edits dropped the git `index` header line, producing patches that BSD patch accepted but GNU patch on Ubuntu CI applied with fuzz — creating `.orig` files that cookbook's apply logic (`apply.ts:148`) treats as fatal. The three failing e2e multipass specs all cascaded from this single fixture generation failure. Regenerated both patches via `pnpm cookbook generate --filePath templates/skeleton/vite.config.ts --onlyFiles`, which uses `git diff` under the hood and always produces a well-formed unified diff with the `index` header restored. Verified locally under GNU patch 2.7.6 on Ubuntu 24.04 (podman): both patches apply with zero offset, no fuzz, no `.orig`/`.rej`. README and LLM-prompt files re-rendered via `pnpm cookbook render` so the diff shown in docs matches the committed patch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(cli): regenerate oclif manifest after main merge Picks up the `uncommited → uncommitted` typo fix from main's packages/cli/src/commands/hydrogen/deploy.ts. The manifest is a derived artifact that must stay in sync with the source; CI's manifest-drift guard flagged the mismatch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(changelog): add missing devDependencies field to 2026.4.1 entry Main's packages/cli/src/commands/hydrogen/changelog-schema.test.ts (added in #3630) asserts every release has a .devDependencies field. The 2026.4.1 entry (added in #3731) was missing it, causing the unit test to fail post-merge. PR #3721 didn't actually change any skeleton devDependencies between 2026.4.0 and 2026.4.1 — only version strings were bumped. An empty object accurately reflects that and satisfies the schema with zero impact on merchant upgrade behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: remove not needed statement from changeset we previously mentioned vite 6 as a requirement for CLI and hydrogen-react but they dont actually bundle vite at all --------- Co-authored-by: Codex <codex@openai.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: freddie <freddie.batista@shopify.com> Co-authored-by: ✦ freddie <45042736+fredericoo@users.noreply.github.com>
* WIP adding the v3_routeConfig flag. * WIP testing the example projects. * WIP Trying to diagnose the skeleton project startup failure. * Reimplemented virtual routes to build paths without node libraries. * Removed change to multipass project. * Multipass example now uses v3preset. * Fix virtual route layout * fix the ordering of virtual routes * CLI test fixes. * Working unit tests. * Additional patch to get the tests working correctly. * Fixing eslint errors that arose after the merge. * Applied prettier fixes to virtual-root.tsx. * Added changeset for v3_routeConfig. * Removed change to docs page because it's unnecessary. * Update so that there is only 1 patch on remix-run/dev * fix package lock * remove patch * fix vite import * format * clean up changeset * Fix test * Fix Prettier failure. * fix virtual route without routeConfig flag * format * Fix test * fix test again * fix dev mode * fix test again --------- Co-authored-by: Helen Lin <helen.lin@shopify.com>
* refactor: move mini-oxygen dev runtime to Vite 8 environments Create a fetchable mini-oxygen SSR environment using Vite 8's environment APIs and move runtime lifecycle management into a dedicated environment module. Keep Hydrogen and CLI option passing compatible via registerPluginOptions while routing dev requests through server.environments.ssr and preserving build-time compatibility_date handling. Update the custom workerd module runner bridge for Vite 8's invoke contract, including getBuiltins support and fetchModule options forwarding, so local SSR still runs inside MiniOxygen. Upgrade workspace Vite dependencies to v8, switch example/template configs to native resolve.tsconfigPaths, and add the environment API refactor plan document. Co-authored-by: Codex <codex@openai.com> * refactor: delegate mini-oxygen runner invokes to Vite Replace mini-oxygen's custom /__vite_fetch_module bridge with a service binding that forwards Vite runner invoke payloads to the SSR environment's built-in handleInvoke() path. This removes bespoke request parsing and builtins serialization while keeping the current fetchable SSR environment and Hydrogen/CLI runtime option flow intact. Co-authored-by: Codex <codex@openai.com> * Simplify Vite 8 cleanup around MiniOxygen Drop the leftover unused Hydrogen plugin lookup in dev, and switch MiniOxygen and bundle analyzer asset emission to emitFile() so Vite 8/Rolldown no longer warns about mutating the bundle object. Co-authored-by: OpenAI Codex <codex@openai.com> * Fix Vite 8 CLI typecheck fallout Align CLI internal MiniOxygen typing with monorepo source types, make the bundle analyzer tolerate Vite 8 and Rolldown RenderedModule typing, and fix MiniOxygen proxy port parsing strictness. Co-authored-by: OpenAI Codex <codex@openai.com> * Remove unused MiniOxygen Vite root binding Drop the leftover __VITE_ROOT runtime binding from the MiniOxygen Vite worker bridge now that the Vite 8 Environment API path no longer uses it. Co-authored-by: OpenAI Codex <codex@openai.com> * fix: harden mini-oxygen environment lifecycle Throw on late runtime reconfiguration, remove the refactor plan doc, and add coverage for MiniOxygen's Vite environment lifecycle. Co-authored-by: OpenAI Codex <codex@openai.com> * Fix CLI tests for Vite 8 compatibility - Upgrade vitest from ^1.0.4 to ^3.2.4 in packages/cli (only package still on v1, which only supports Vite ^5) - Update build test assertions to match Vite 8 renamed log messages Co-Authored-By: Claude <noreply@anthropic.com> * Support Vite 6 * fix: restore HMR in Vite Environment API setup Three bugs were preventing HMR from working after the migration to Vite's Environment API: 1. `server.watch: null` (accidentally introduced in #2722) caused Vite to create a NoopWatcher, so no file-change events were ever emitted. Removed it to restore chokidar. 2. The SSR environment was passed `transport: context.ws` (the same WebSocket server the browser uses). Because `isWebSocketServer` is true on that object, Vite used it directly as the SSR environment's hot channel. When the SSR module graph found no React Fast Refresh boundaries (SSR code has none), it sent `full-reload` over that shared WebSocket — hard-refreshing the browser on every file change. Dropping the transport gives the SSR environment a noop hot channel so those events are silently discarded. 3. `@vite/client` was never injected into HTML responses from workerd, so the browser never established a WebSocket connection to Vite's HMR server. React Router's <Scripts /> already injects the inject-hmr-runtime preamble; the only missing piece was @vite/client. Now injected via a simple string replace on HTML responses. Server-side invalidation (route handlers, server.ts) is handled by a new watcher listener in environment.ts: when a file in the SSR module graph changes, the mini-oxygen instance is disposed so the next request starts a fresh workerd with a clean module-runner cache. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * refactor: simplify mini-oxygen Vite HMR wiring Remove redundant MiniOxygen watcher and HTML injection cleanup while keeping the SSR environment request-driven. Co-authored-by: Codex <codex@openai.com> * refactor: trim mini-oxygen environment plumbing Remove the custom MiniOxygen environment marker/getter and read the entrypoint error handler from plugin state instead. Co-authored-by: Codex <codex@openai.com> * docs: clarify mini-oxygen Vite runtime behavior Update MiniOxygen Vite runtime comments to reflect the current request-driven server refresh model and inspector behavior. Co-authored-by: Codex <codex@openai.com> * docs: clarify mini-oxygen environment lifecycle Document the warmup timing and SSR environment replacement behavior in MiniOxygen's Vite environment integration. Co-authored-by: Codex <codex@openai.com> * fix: use accurate type cast for response body Replace the double-cast `as unknown as BodyInit` with `as ReadableStream | null`, which accurately describes the Miniflare Response.body type and avoids the lossy intermediate cast through `unknown`. * fix: extract magic number to WARMUP_SETTLE_DELAY_MS Replace the bare `200` timeout with a named constant that communicates intent at a glance. The delay lets Vite settle after listen/config reload before the synthetic warmup request fires. * chore: add TODO for Vite 6 getBuiltins shim removal Mark the getBuiltins workaround so it's easy to find and remove once Vite 6 support is dropped. Vite 7+ handles this internally. * chore: changeset for mini-oxygen review feedback Patch changeset covering the type cast improvement, magic number extraction, and Vite 6 shim TODO marker. * fix: correct vite compat range in changeset Mini Oxygen requires Vite 6+ (Environment API didn't exist in Vite 5). Only Hydrogen itself supports Vite 5+. * fix: bump react-router from 7.12.0 to 7.14.0 React Router 7.14.0 adds Vite 8 support by switching from the deprecated `esbuild` config API to `oxc`. This eliminates the deprecation warning: `esbuild` option was specified by "react-router" plugin. This option is deprecated, please use `oxc` instead. Also adds `unstable_mask` to mock Location objects in test files to satisfy the new property added to react-router's Location type in 7.14.0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cookbook): regenerate recipes for Vite 8 skeleton changes The Vite 8 migration changed the skeleton template (react-router 7.14.0, vite ^8.0.1, removed vite-tsconfig-paths, updated vite.config.ts structure). All recipe patches referencing old skeleton state needed regeneration. Regenerated 13 recipes: b2b, bundles, combined-listings, custom-cart-method, gtm, infinite-scroll, legacy-customer-account-flow, markets, metaobjects, multipass, partytown, subscriptions, third-party-api. The express recipe was NOT regenerated — its vite.config.ts patch references removed imports (oxygen(), tsconfigPaths) and requires a full rewrite. No E2E test covers it, so it's not blocking CI. For multipass and partytown, the standard `regenerate` command fails because withResolvedCatalog() resolves workspace:* before patch application, breaking context matching. Worked around by calling applyRecipe() directly (bypassing catalog resolution), then running generate and render separately. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cookbook): update multipass and partytown vite.config.ts patches for optimizeDeps format The merge from main introduced the nested dependency syntax in ssr.optimizeDeps.include (e.g., 'react-router > set-cookie-parser'). The multipass and partytown recipe patches still referenced the old single-line format, causing E2E test failures when applying patches to the updated skeleton. Manually rewrote both patches to match the current skeleton's multi-line include array format. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cookbook): correct hunk headers in multipass and partytown patches The previous patch fix had an off-by-one in the @@ hunk header — the first context line started at line 29 but the header claimed line 28. This caused `patch` to apply with an offset, creating .orig backup files, which the apply code treats as a conflict. Fixed by including the `include: [` line (line 28) as leading context so the hunk header accurately matches the file content. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cookbook): regenerate multipass/partytown vite.config.ts patches via git diff The previous patches were hand-edited across three commits (e396456, 862f005, 6b79693) to keep up with skeleton refactors. Hand-edits dropped the git `index` header line, producing patches that BSD patch accepted but GNU patch on Ubuntu CI applied with fuzz — creating `.orig` files that cookbook's apply logic (`apply.ts:148`) treats as fatal. The three failing e2e multipass specs all cascaded from this single fixture generation failure. Regenerated both patches via `pnpm cookbook generate --filePath templates/skeleton/vite.config.ts --onlyFiles`, which uses `git diff` under the hood and always produces a well-formed unified diff with the `index` header restored. Verified locally under GNU patch 2.7.6 on Ubuntu 24.04 (podman): both patches apply with zero offset, no fuzz, no `.orig`/`.rej`. README and LLM-prompt files re-rendered via `pnpm cookbook render` so the diff shown in docs matches the committed patch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(cli): regenerate oclif manifest after main merge Picks up the `uncommited → uncommitted` typo fix from main's packages/cli/src/commands/hydrogen/deploy.ts. The manifest is a derived artifact that must stay in sync with the source; CI's manifest-drift guard flagged the mismatch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(changelog): add missing devDependencies field to 2026.4.1 entry Main's packages/cli/src/commands/hydrogen/changelog-schema.test.ts (added in #3630) asserts every release has a .devDependencies field. The 2026.4.1 entry (added in #3731) was missing it, causing the unit test to fail post-merge. PR #3721 didn't actually change any skeleton devDependencies between 2026.4.0 and 2026.4.1 — only version strings were bumped. An empty object accurately reflects that and satisfies the schema with zero impact on merchant upgrade behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: remove not needed statement from changeset we previously mentioned vite 6 as a requirement for CLI and hydrogen-react but they dont actually bundle vite at all --------- Co-authored-by: Codex <codex@openai.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: freddie <freddie.batista@shopify.com> Co-authored-by: ✦ freddie <45042736+fredericoo@users.noreply.github.com>
* WIP adding the v3_routeConfig flag. * WIP testing the example projects. * WIP Trying to diagnose the skeleton project startup failure. * Reimplemented virtual routes to build paths without node libraries. * Removed change to multipass project. * Multipass example now uses v3preset. * Fix virtual route layout * fix the ordering of virtual routes * CLI test fixes. * Working unit tests. * Additional patch to get the tests working correctly. * Fixing eslint errors that arose after the merge. * Applied prettier fixes to virtual-root.tsx. * Added changeset for v3_routeConfig. * Removed change to docs page because it's unnecessary. * Update so that there is only 1 patch on remix-run/dev * fix package lock * remove patch * fix vite import * format * clean up changeset * Fix test * Fix Prettier failure. * fix virtual route without routeConfig flag * format * Fix test * fix test again * fix dev mode * fix test again --------- Co-authored-by: Helen Lin <helen.lin@shopify.com>
* refactor: move mini-oxygen dev runtime to Vite 8 environments Create a fetchable mini-oxygen SSR environment using Vite 8's environment APIs and move runtime lifecycle management into a dedicated environment module. Keep Hydrogen and CLI option passing compatible via registerPluginOptions while routing dev requests through server.environments.ssr and preserving build-time compatibility_date handling. Update the custom workerd module runner bridge for Vite 8's invoke contract, including getBuiltins support and fetchModule options forwarding, so local SSR still runs inside MiniOxygen. Upgrade workspace Vite dependencies to v8, switch example/template configs to native resolve.tsconfigPaths, and add the environment API refactor plan document. Co-authored-by: Codex <codex@openai.com> * refactor: delegate mini-oxygen runner invokes to Vite Replace mini-oxygen's custom /__vite_fetch_module bridge with a service binding that forwards Vite runner invoke payloads to the SSR environment's built-in handleInvoke() path. This removes bespoke request parsing and builtins serialization while keeping the current fetchable SSR environment and Hydrogen/CLI runtime option flow intact. Co-authored-by: Codex <codex@openai.com> * Simplify Vite 8 cleanup around MiniOxygen Drop the leftover unused Hydrogen plugin lookup in dev, and switch MiniOxygen and bundle analyzer asset emission to emitFile() so Vite 8/Rolldown no longer warns about mutating the bundle object. Co-authored-by: OpenAI Codex <codex@openai.com> * Fix Vite 8 CLI typecheck fallout Align CLI internal MiniOxygen typing with monorepo source types, make the bundle analyzer tolerate Vite 8 and Rolldown RenderedModule typing, and fix MiniOxygen proxy port parsing strictness. Co-authored-by: OpenAI Codex <codex@openai.com> * Remove unused MiniOxygen Vite root binding Drop the leftover __VITE_ROOT runtime binding from the MiniOxygen Vite worker bridge now that the Vite 8 Environment API path no longer uses it. Co-authored-by: OpenAI Codex <codex@openai.com> * fix: harden mini-oxygen environment lifecycle Throw on late runtime reconfiguration, remove the refactor plan doc, and add coverage for MiniOxygen's Vite environment lifecycle. Co-authored-by: OpenAI Codex <codex@openai.com> * Fix CLI tests for Vite 8 compatibility - Upgrade vitest from ^1.0.4 to ^3.2.4 in packages/cli (only package still on v1, which only supports Vite ^5) - Update build test assertions to match Vite 8 renamed log messages Co-Authored-By: Claude <noreply@anthropic.com> * Support Vite 6 * fix: restore HMR in Vite Environment API setup Three bugs were preventing HMR from working after the migration to Vite's Environment API: 1. `server.watch: null` (accidentally introduced in Shopify#2722) caused Vite to create a NoopWatcher, so no file-change events were ever emitted. Removed it to restore chokidar. 2. The SSR environment was passed `transport: context.ws` (the same WebSocket server the browser uses). Because `isWebSocketServer` is true on that object, Vite used it directly as the SSR environment's hot channel. When the SSR module graph found no React Fast Refresh boundaries (SSR code has none), it sent `full-reload` over that shared WebSocket — hard-refreshing the browser on every file change. Dropping the transport gives the SSR environment a noop hot channel so those events are silently discarded. 3. `@vite/client` was never injected into HTML responses from workerd, so the browser never established a WebSocket connection to Vite's HMR server. React Router's <Scripts /> already injects the inject-hmr-runtime preamble; the only missing piece was @vite/client. Now injected via a simple string replace on HTML responses. Server-side invalidation (route handlers, server.ts) is handled by a new watcher listener in environment.ts: when a file in the SSR module graph changes, the mini-oxygen instance is disposed so the next request starts a fresh workerd with a clean module-runner cache. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> * refactor: simplify mini-oxygen Vite HMR wiring Remove redundant MiniOxygen watcher and HTML injection cleanup while keeping the SSR environment request-driven. Co-authored-by: Codex <codex@openai.com> * refactor: trim mini-oxygen environment plumbing Remove the custom MiniOxygen environment marker/getter and read the entrypoint error handler from plugin state instead. Co-authored-by: Codex <codex@openai.com> * docs: clarify mini-oxygen Vite runtime behavior Update MiniOxygen Vite runtime comments to reflect the current request-driven server refresh model and inspector behavior. Co-authored-by: Codex <codex@openai.com> * docs: clarify mini-oxygen environment lifecycle Document the warmup timing and SSR environment replacement behavior in MiniOxygen's Vite environment integration. Co-authored-by: Codex <codex@openai.com> * fix: use accurate type cast for response body Replace the double-cast `as unknown as BodyInit` with `as ReadableStream | null`, which accurately describes the Miniflare Response.body type and avoids the lossy intermediate cast through `unknown`. * fix: extract magic number to WARMUP_SETTLE_DELAY_MS Replace the bare `200` timeout with a named constant that communicates intent at a glance. The delay lets Vite settle after listen/config reload before the synthetic warmup request fires. * chore: add TODO for Vite 6 getBuiltins shim removal Mark the getBuiltins workaround so it's easy to find and remove once Vite 6 support is dropped. Vite 7+ handles this internally. * chore: changeset for mini-oxygen review feedback Patch changeset covering the type cast improvement, magic number extraction, and Vite 6 shim TODO marker. * fix: correct vite compat range in changeset Mini Oxygen requires Vite 6+ (Environment API didn't exist in Vite 5). Only Hydrogen itself supports Vite 5+. * fix: bump react-router from 7.12.0 to 7.14.0 React Router 7.14.0 adds Vite 8 support by switching from the deprecated `esbuild` config API to `oxc`. This eliminates the deprecation warning: `esbuild` option was specified by "react-router" plugin. This option is deprecated, please use `oxc` instead. Also adds `unstable_mask` to mock Location objects in test files to satisfy the new property added to react-router's Location type in 7.14.0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cookbook): regenerate recipes for Vite 8 skeleton changes The Vite 8 migration changed the skeleton template (react-router 7.14.0, vite ^8.0.1, removed vite-tsconfig-paths, updated vite.config.ts structure). All recipe patches referencing old skeleton state needed regeneration. Regenerated 13 recipes: b2b, bundles, combined-listings, custom-cart-method, gtm, infinite-scroll, legacy-customer-account-flow, markets, metaobjects, multipass, partytown, subscriptions, third-party-api. The express recipe was NOT regenerated — its vite.config.ts patch references removed imports (oxygen(), tsconfigPaths) and requires a full rewrite. No E2E test covers it, so it's not blocking CI. For multipass and partytown, the standard `regenerate` command fails because withResolvedCatalog() resolves workspace:* before patch application, breaking context matching. Worked around by calling applyRecipe() directly (bypassing catalog resolution), then running generate and render separately. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cookbook): update multipass and partytown vite.config.ts patches for optimizeDeps format The merge from main introduced the nested dependency syntax in ssr.optimizeDeps.include (e.g., 'react-router > set-cookie-parser'). The multipass and partytown recipe patches still referenced the old single-line format, causing E2E test failures when applying patches to the updated skeleton. Manually rewrote both patches to match the current skeleton's multi-line include array format. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cookbook): correct hunk headers in multipass and partytown patches The previous patch fix had an off-by-one in the @@ hunk header — the first context line started at line 29 but the header claimed line 28. This caused `patch` to apply with an offset, creating .orig backup files, which the apply code treats as a conflict. Fixed by including the `include: [` line (line 28) as leading context so the hunk header accurately matches the file content. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(cookbook): regenerate multipass/partytown vite.config.ts patches via git diff The previous patches were hand-edited across three commits (e396456, 862f005, 6b79693) to keep up with skeleton refactors. Hand-edits dropped the git `index` header line, producing patches that BSD patch accepted but GNU patch on Ubuntu CI applied with fuzz — creating `.orig` files that cookbook's apply logic (`apply.ts:148`) treats as fatal. The three failing e2e multipass specs all cascaded from this single fixture generation failure. Regenerated both patches via `pnpm cookbook generate --filePath templates/skeleton/vite.config.ts --onlyFiles`, which uses `git diff` under the hood and always produces a well-formed unified diff with the `index` header restored. Verified locally under GNU patch 2.7.6 on Ubuntu 24.04 (podman): both patches apply with zero offset, no fuzz, no `.orig`/`.rej`. README and LLM-prompt files re-rendered via `pnpm cookbook render` so the diff shown in docs matches the committed patch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(cli): regenerate oclif manifest after main merge Picks up the `uncommited → uncommitted` typo fix from main's packages/cli/src/commands/hydrogen/deploy.ts. The manifest is a derived artifact that must stay in sync with the source; CI's manifest-drift guard flagged the mismatch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(changelog): add missing devDependencies field to 2026.4.1 entry Main's packages/cli/src/commands/hydrogen/changelog-schema.test.ts (added in Shopify#3630) asserts every release has a .devDependencies field. The 2026.4.1 entry (added in Shopify#3731) was missing it, causing the unit test to fail post-merge. PR Shopify#3721 didn't actually change any skeleton devDependencies between 2026.4.0 and 2026.4.1 — only version strings were bumped. An empty object accurately reflects that and satisfies the schema with zero impact on merchant upgrade behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: remove not needed statement from changeset we previously mentioned vite 6 as a requirement for CLI and hydrogen-react but they dont actually bundle vite at all --------- Co-authored-by: Codex <codex@openai.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: freddie <freddie.batista@shopify.com> Co-authored-by: ✦ freddie <45042736+fredericoo@users.noreply.github.com>
Upgrade instructions
Please refer to the Remix documentation for more details on
v3_routeConfigfuture flag: https://remix.run/docs/en/main/start/future-flags#v3_routeconfigUpdate your
vite.config.ts.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(), ],Update your
package.jsonand 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:"devDependencies": { "@remix-run/dev": "^2.16.1", + "@remix-run/fs-routes": "^2.16.1", + "@remix-run/route-config": "^2.16.1",Add a routes.ts file. This is your new Remix route configuration file.
WHY are these changes introduced?
These changes are to support an upgrade to the next version of Remix, by using the future flag and supporting libraries to get the code ready for that upgrade in place before the upgrade itself happens.
WHAT is this pull request doing?
v3_routeConfigflag set totrue.hydrogen.v3presetfunction which excludes theroutesattribute that thehydrogen.presetfunction includes because that is incompatible with the flag being turned on.hydrogenRoutesfunction which transforms the route specified inroutes.tsto include the virtual routes.getVirtualRoutesto work without using any node libraries so that it can run in MiniOxygen.LANGenvironment variable in theclipackage so that the date formatting uses the US formatting.build.test.tsnow closes resources so that the test doesn't fail due to the hanging file watchers.build.tssets theserver.watchconfiguration option as that may be necessary to disable the file watching.hydrogen/dev.tsshuts down the watcher on theviteServerwhen finishing up.setup.test.tsmakes sure to include theroutes.tsfile.classic-compiler/dev.tsmakes sure to filter out the virtual routes whendisableVirtualRoutesis true.remix-config.tsconstructs a valueroutesViteNodeContextwhich is necessary when resolving the config ifv3_routeConfigis enabled.route-validator.tsis now null safe when assigningcurrentRoute.path.get-virtual-routes.test.tsnow checks for thejsxfiles instead of thetsxfiles of the virtual routes.@remix-run/devto not watch inremixVitePluginwhen in a test because the file watchers cannot be reached to switch them off which causes tests relying on that to fail.viteto handle thewatchproperties of the config to set it if the value isnullas that has meaning in that particular case.HOW to test your changes?
routes.tswith thehydrogenRoutesfunction, to add the supporting routes.hydrogen.preset()tohydrogen.v3preset().Post-merge steps
Checklist
Amended changes
The following changes were reverted/amended in #2829 .
Move the
Layoutcomponent export fromroot.tsxinto its own file. Make sure to supply an<Outlet>so Remix knows where to inject your route content.Add a routes.ts file. This is your new Remix route configuration file.
common.tsnow makes sure to includeroutes.tsandlayout.tsx.local.test.tsslightly tweaked to make it easier to find out what files are missing and also now correctly takes account of the content that was shifted fromroot.tsxtolayout.tsx.replacers.tslooks forlayoutinstead ofroot.