Conversation
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>
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>
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>
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>
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>
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>
- 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>
|
Oxygen deployed a preview of your
Learn more about Hydrogen's GitHub integration. |
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>
|
/snapit |
Remove redundant MiniOxygen watcher and HTML injection cleanup while keeping the SSR environment request-driven. Co-authored-by: Codex <codex@openai.com>
Remove the custom MiniOxygen environment marker/getter and read the entrypoint error handler from plugin state instead. Co-authored-by: Codex <codex@openai.com>
Update MiniOxygen Vite runtime comments to reflect the current request-driven server refresh model and inspector behavior. Co-authored-by: Codex <codex@openai.com>
| plugins: [hydrogen(), oxygen(), reactRouter(), tsconfigPaths()], | ||
| plugins: [hydrogen(), oxygen(), reactRouter()], | ||
| resolve: { | ||
| tsconfigPaths: true, | ||
| }, |
There was a problem hiding this comment.
New in Vite 8, it does the tsconfigPaths behavior by default. Vite 6 and 7 still need the plugin though.
There was a problem hiding this comment.
if we are going to default to vite 8, sure!
| "vite": "^8.0.1", | ||
| "vitest": "^3.2.4" | ||
| }, | ||
| "peerDependencies": { | ||
| "vite": "^6.2.1" | ||
| "vite": "^6.2.1 || ^7.0.0 || ^8.0.0" |
There was a problem hiding this comment.
As long as MiniOxygen is built with Vite 8 including the fixes of this PR, it seems to be compatible when consumed in apps using Vite 6 and 7 (and 8 obv), so there's no breaking change to my understanding 🤔
| "ts-expect": "^1.3.0", | ||
| "typescript": "5.9.2", | ||
| "vite": "^6.2.4", | ||
| "vite": "^8.0.1", |
There was a problem hiding this comment.
This change in hydrogen-react means we start building it with Vite 8
|
|
||
| export async function createCpuStartupProfiler(root: string) { | ||
| type MiniOxygenType = typeof import('@shopify/mini-oxygen/node'); | ||
| type MiniOxygenType = typeof import('~/mini-oxygen/node/index.js'); |
There was a problem hiding this comment.
This type of change (here and in other files) is just a fix to remove build order requirement for TS types (before we needed mini-oxygen built before CLI, now it doesn't matter).
It can be ignored for review.
| bundle[BUNDLE_ANALYZER_HTML_FILE] = { | ||
| this.emitFile({ | ||
| type: 'asset', | ||
| fileName: BUNDLE_ANALYZER_HTML_FILE, | ||
| needsCodeReference: false, | ||
| source: injectAnalyzerTemplateData( | ||
| analysisTemplate, | ||
| JSON.stringify(metafile), | ||
| ), | ||
| names: [BUNDLE_ANALYZER_HTML_FILE], | ||
| originalFileNames: [BUNDLE_ANALYZER_HTML_FILE], | ||
| // name and originalFileName should be deprecated .. but | ||
| // for some reason, removing them breaks typescript check | ||
| name: BUNDLE_ANALYZER_HTML_FILE, | ||
| originalFileName: BUNDLE_ANALYZER_HTML_FILE, | ||
| }; | ||
| }); |
There was a problem hiding this comment.
Same functionality, just fixing some types
| Request as MiniOxygenRequest, | ||
| Response as MiniOxygenResponse, |
There was a problem hiding this comment.
Renamed for clarity
This comment has been minimized.
This comment has been minimized.
Document the warmup timing and SSR environment replacement behavior in MiniOxygen's Vite environment integration. Co-authored-by: Codex <codex@openai.com>
| InternalMiniOxygenOptions, | ||
| MiniOxygenViteOptions, | ||
| } from './server-middleware.js'; | ||
| import { |
There was a problem hiding this comment.
non-blocking: these imports (startMiniOxygenRuntime, toMiniflareRequest, WARMUP_PATHNAME, getViteUrl) are all implementation details of this module's handleRequest and warmup functions. Meanwhile server-middleware.ts also provides setupOxygenMiddleware consumed by plugin.ts - so it's serving two unrelated concerns.
Worth considering in a follow-up whether these helpers should live here (or in a shared runtime.ts) to align the module boundary with the information boundary. Not blocking for a migration PR though.
| let apiOptions: OxygenApiOptions = {}; | ||
| let miniOxygenEnvironment: MiniOxygenDevEnvironment | undefined; | ||
|
|
||
| const resolveMiniOxygenOptions = async ( |
There was a problem hiding this comment.
non-blocking: this ~25-line function understands the full internal structure of MiniOxygenViteOptions - entry resolution, env merging, debug/inspector defaults, logRequestLine priority. That's option-resolution knowledge leaking into the plugin layer.
Worth considering whether environment.ts could own this logic (accepting OxygenPluginOptions directly) so plugin.ts doesn't need to understand MiniOxygenViteOptions internals. Not blocking for this migration, but a design observation for follow-up.
There was a problem hiding this comment.
It needs to understand both, plugin/CLI provided options and MiniOxygen receiving options, because it's transforming from the former to the latter. So it can live in any of those 2 areas, or a separate one.
| toMiniflareRequest(request), | ||
| ); | ||
|
|
||
| return new Response(response.body as unknown as BodyInit, { |
There was a problem hiding this comment.
| return new Response(response.body as unknown as BodyInit, { | |
| return new Response(response.body as ReadableStream | null, { |
still a type cast because of the freaking <any> type parameter, but this is a less fake one
- @shopify/hydrogen 2025.7.0 -> 2026.1.2 - react/react-dom 18.3.1 -> 19.2.4 - react-router 7.9.2 -> 7.13.2 - @react-router/dev 7.9.2 -> 7.13.2 - vite 6.2.4 -> 8.0.3 - tailwindcss 4.1.6 -> 4.2.2 - typescript 5.9.2 -> 6.0.2 - lucide-react 0.546.0 -> 1.7.0 - all other deps bumped to latest - remove baseUrl from tsconfig (deprecated in TS 6) - use relative paths in tsconfig paths Build: PASSES (client + SSR) Dev server: FAILS (__vite_ssr_exportName__ in mini-oxygen) Blocked on Shopify/hydrogen#3617 landing
Build @shopify/hydrogen, @shopify/mini-oxygen, and @shopify/cli-hydrogen from the fd-vite-8 branch (Shopify/hydrogen#3617) and install as local tarballs to test the full Vite 8 Environment API migration. Results: - Build: PASSES (both client and SSR) - Dev server: PASSES (no __vite_ssr_exportName__ error) - oxygen:main bundle-assign warning: FIXED by PR - hydrogen:bundle-analyzer warning: still present (fix is in CLI) - react-router esbuild deprecation: cosmetic, not blocking - Invalid comparator warning: artifact of file: dep references PR #3617 is validated. The Environment API rewrite in mini-oxygen resolves the Vite 8 SSR module format incompatibility.
Complete patch set for Shopify/hydrogen#3617: - @shopify/mini-oxygen@4.0.1 (environment API rewrite) - @shopify/hydrogen@2026.1.2 (vite plugin, peerDep) - @shopify/hydrogen-react@2026.1.1 (vite peerDep) Verified: build, dev server, HMR, all routes 200, zero console errors, zero hydration errors, clean checkout applies patches.
Pin @shopify/mini-oxygen to 4.0.1 for patch compatibility. Three pnpm patches from Shopify/hydrogen#3617 (fd-vite-8 branch): - @shopify/mini-oxygen@4.0.1 - @shopify/hydrogen@2026.1.2 - @shopify/hydrogen-react@2026.1.1 Tested on Vite 6.4.1, 7.3.1, 8.0.3. All pass.
…ronment API) Patches built from fd-vite-8 branch, tested on Vite 6.4.1, 7.3.1, 8.0.3: - @shopify/mini-oxygen@4.0.1: environment API rewrite, worker-entry transport - @shopify/hydrogen@2026.1.2: vite plugin, peerDep ^8.0.0 - @shopify/hydrogen-react@2026.1.1: vite peerDep ^8.0.0
…ronment API) Patches built from fd-vite-8 branch, tested on Vite 6.4.1, 7.3.1, 8.0.3: - @shopify/mini-oxygen@4.0.1: environment API rewrite, worker-entry transport - @shopify/hydrogen@2026.1.2: vite plugin, peerDep ^8.0.0 - @shopify/hydrogen-react@2026.1.1: vite peerDep ^8.0.0
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`.
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.
Mark the getBuiltins workaround so it's easy to find and remove once Vite 6 support is dropped. Vite 7+ handles this internally.
Patch changeset covering the type cast improvement, magic number extraction, and Vite 6 shim TODO marker.
.changeset/warm-planets-glow.md
Outdated
| '@shopify/create-hydrogen': patch | ||
| --- | ||
|
|
||
| Add support for Vite 7 and Vite 8. Hydrogen, Mini Oxygen, the Hydrogen CLI, and Hydrogen React now accept Vite 7 and 8 as peer dependencies while remaining backwards-compatible with Vite 5 and 6. |
There was a problem hiding this comment.
@fredericoo I think the only one compatible with Vite 5 is Hydrogen. MiniOxygen likely requires 6+ since older versions didn't have Environment API support (I think).
|
|
||
| Mini Oxygen's dev server has been refactored to use the [Vite Environment API](https://vite.dev/guide/api-environment), which is the standard way to run non-browser runtimes in Vite. This replaces the previous custom middleware approach with a first-class `FetchableDevEnvironment`, improving compatibility with Vite's built-in HMR and module invalidation. | ||
|
|
||
| New Hydrogen projects created with `npm create @shopify/hydrogen` will default to Vite 8. The `vite-tsconfig-paths` plugin is no longer needed in the skeleton template since Vite 8 supports `resolve.tsconfigPaths` natively. |
There was a problem hiding this comment.
As mentioned in OP, perhaps it would be good to sync with the RR7 folks and see if they add official support for Vite 8 as well. Mostly to remove warnings from the terminal, otherwise it already works.
Mini Oxygen requires Vite 6+ (Environment API didn't exist in Vite 5). Only Hydrogen itself supports Vite 5+.
Migrate to Vite Environment API to support Vite 8 (backwards compat with 6 and 7).
This PR also changes the local Vite versions to 8, and the default one in skeleton template too.
There's no migration required from Vite 6/7 to 8 in the skeleton, the only change is removing tsconfigPaths plugin because it's now native in Vite 8 and it would show a warning otherwise.
The only problem I saw is that React Router is not officially supporting Vite 8 yet. It works, but Vite shows a warning about this because RR is using
esbuildoptions (Vite still support this under the hood but warns about it).So the question is: do we migrate to Vite 8 directly, or change the skeleton version to Vite 7 until RR updates?
Let's maybe sync with the RR folks and see what's their plan.