From 6290368edcb3a2f54652db79ccfc731276cd4a2f Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 5 Jan 2026 16:26:19 +0900 Subject: [PATCH 1/8] feat(rsc): standardize `loadModule` runner global --- packages/plugin-rsc/src/plugin.ts | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index dc46a90f6..69a4570de 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -61,6 +61,7 @@ import { parseReferenceValidationVirtual, } from './plugins/shared' import { stripLiteral } from 'strip-literal' +import type { ModuleRunner } from 'vite/module-runner' const isRolldownVite = 'rolldownVersion' in vite @@ -301,6 +302,12 @@ export function vitePluginRscMinimal( ] } +declare global { + function __VITE_EXPERIMENTAL_GET_MODULE_RUNNER__( + environmentName: string, + ): Promise +} + export default function vitePluginRsc( rscPluginOptions: RscPluginOptions = {}, ): Plugin[] { @@ -516,7 +523,22 @@ export default function vitePluginRsc( }, }, configureServer(server) { - ;(globalThis as any).__viteRscDevServer = server + globalThis.__VITE_EXPERIMENTAL_GET_MODULE_RUNNER__ = async function ( + environmentName, + ) { + const environment = server.environments[environmentName] + if (!environment) { + throw new Error( + `[vite-rsc] unknown environment '${environmentName}'`, + ) + } + if (!vite.isRunnableDevEnvironment(environment)) { + throw new Error( + `[vite-rsc] environment '${environmentName}' is not runnable`, + ) + } + return environment.runner + } // intercept client hmr to propagate client boundary invalidation to server environment const oldSend = server.environments.client.hot.send @@ -768,10 +790,9 @@ export default function vitePluginRsc( const source = getEntrySource(environment.config, entryName) const resolved = await environment.pluginContainer.resolveId(source) assert(resolved, `[vite-rsc] failed to resolve entry '${source}'`) - replacement = - `globalThis.__viteRscDevServer.environments[${JSON.stringify( - environmentName, - )}]` + `.runner.import(${JSON.stringify(resolved.id)})` + const environmentNameEscaped = JSON.stringify(environmentName) + const resolveIdEscaped = JSON.stringify(resolved.id) + replacement = `globalThis.__VITE_EXPERIMENTAL_GET_MODULE_RUNNER__(${environmentNameEscaped}).then(runner => runner.import(${resolveIdEscaped}))` } else { replacement = JSON.stringify( `__vite_rsc_load_module:${this.environment.name}:${environmentName}:${entryName}`, From 6a769d59f4a441299582a0d7fc30ea19a5774879 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 5 Jan 2026 16:31:26 +0900 Subject: [PATCH 2/8] chore: rename to `__VITE_GET_MODULE_RUNNER__` --- packages/plugin-rsc/src/plugin.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index 69a4570de..d87d42603 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -303,7 +303,7 @@ export function vitePluginRscMinimal( } declare global { - function __VITE_EXPERIMENTAL_GET_MODULE_RUNNER__( + function __VITE_GET_MODULE_RUNNER__( environmentName: string, ): Promise } @@ -523,7 +523,7 @@ export default function vitePluginRsc( }, }, configureServer(server) { - globalThis.__VITE_EXPERIMENTAL_GET_MODULE_RUNNER__ = async function ( + globalThis.__VITE_GET_MODULE_RUNNER__ = async function ( environmentName, ) { const environment = server.environments[environmentName] @@ -790,9 +790,9 @@ export default function vitePluginRsc( const source = getEntrySource(environment.config, entryName) const resolved = await environment.pluginContainer.resolveId(source) assert(resolved, `[vite-rsc] failed to resolve entry '${source}'`) - const environmentNameEscaped = JSON.stringify(environmentName) - const resolveIdEscaped = JSON.stringify(resolved.id) - replacement = `globalThis.__VITE_EXPERIMENTAL_GET_MODULE_RUNNER__(${environmentNameEscaped}).then(runner => runner.import(${resolveIdEscaped}))` + const environmentNameJson = JSON.stringify(environmentName) + const resolvedIdJson = JSON.stringify(resolved.id) + replacement = `globalThis.__VITE_GET_MODULE_RUNNER__(${environmentNameJson}).then(runner => runner.import(${resolvedIdJson}))` } else { replacement = JSON.stringify( `__vite_rsc_load_module:${this.environment.name}:${environmentName}:${entryName}`, From f9d00392c61f655a06055d17d1363341d0967b0a Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 5 Jan 2026 16:45:50 +0900 Subject: [PATCH 3/8] docs(rsc): document `__VITE_GET_MODULE_RUNNER__` global MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- packages/plugin-rsc/README.md | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/plugin-rsc/README.md b/packages/plugin-rsc/README.md index 0628f2ad5..28c32e655 100644 --- a/packages/plugin-rsc/README.md +++ b/packages/plugin-rsc/README.md @@ -228,7 +228,9 @@ The plugin provides an additional helper for multi environment interaction. This allows importing `ssr` environment module specified by `environments.ssr.build.rollupOptions.input[entryName]` inside `rsc` environment and vice versa. -During development, by default, this API assumes both `rsc` and `ssr` environments execute under the main Vite process. When enabling `rsc({ loadModuleDevProxy: true })` plugin option, the loaded module is implemented as a proxy with `fetch`-based RPC to call in node environment on the main Vite process, which for example, allows `rsc` environment inside cloudflare workers to access `ssr` environment on the main Vite process. This proxy mechanism uses [turbo-stream](https://github.com/jacob-ebey/turbo-stream) for serializing data types beyond JSON, with custom encoders/decoders to additionally support `Request` and `Response` instances. +During development, by default, this API assumes both `rsc` and `ssr` environments execute under the main Vite process as `RunnableDevEnvironment`. Internally, `loadModule` uses the global `__VITE_GET_MODULE_RUNNER__` function to obtain the module runner for the target environment (see [`__VITE_GET_MODULE_RUNNER__`](#__vite_get_module_runner__) below). + +When enabling `rsc({ loadModuleDevProxy: true })` plugin option, the loaded module is implemented as a proxy with `fetch`-based RPC to call in node environment on the main Vite process, which for example, allows `rsc` environment inside cloudflare workers to access `ssr` environment on the main Vite process. This proxy mechanism uses [turbo-stream](https://github.com/jacob-ebey/turbo-stream) for serializing data types beyond JSON, with custom encoders/decoders to additionally support `Request` and `Response` instances. During production build, this API will be rewritten into a static import of the specified entry of other environment build and the modules are executed inside the same runtime. @@ -327,6 +329,37 @@ import.meta.hot.on('rsc:update', async () => { }) ``` +### Global API + +#### `__VITE_GET_MODULE_RUNNER__` + +- Type: `(environmentName: string) => Promise` +- Availability: Development only (set by `configureServer`) + +This global function provides a standardized way to obtain a Vite [ModuleRunner](https://vite.dev/guide/api-environment#modulerunner) for a given environment during development. It is used internally by `import.meta.viteRsc.loadModule` to execute modules in the target environment. + +By default, the plugin sets this global to retrieve the runner from the Vite dev server as both run in the same main process: + +```js +globalThis.__VITE_GET_MODULE_RUNNER__ = async (environmentName) => { + return server.environments[environmentName].runner +} +``` + +**Custom Environment Integration:** + +Frameworks with custom environment setups (e.g., environments running in separate workers) can override this global to provide their own module runner resolution. + +```js +// worker-entry.js +globalThis.__VITE_GET_MODULE_RUNNER__ = async (environmentName) => { + // Custom logic to get the runner, e.g., from a worker + return myWorkerModuleRunners[environmentName] +} +``` + +This allows `import.meta.viteRsc.loadModule` to work seamlessly with different runtime configurations without requiring changes to user code. + ## Plugin API ### `@vitejs/plugin-rsc` From 3c0faa860a38f8090e296d9b2dc62afe0cb1f27f Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 5 Jan 2026 16:46:55 +0900 Subject: [PATCH 4/8] chore: readme --- packages/plugin-rsc/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/plugin-rsc/README.md b/packages/plugin-rsc/README.md index 28c32e655..526f7439d 100644 --- a/packages/plugin-rsc/README.md +++ b/packages/plugin-rsc/README.md @@ -351,9 +351,8 @@ globalThis.__VITE_GET_MODULE_RUNNER__ = async (environmentName) => { Frameworks with custom environment setups (e.g., environments running in separate workers) can override this global to provide their own module runner resolution. ```js -// worker-entry.js +// Custom logic to get the runner, e.g., from a worker runtime globalThis.__VITE_GET_MODULE_RUNNER__ = async (environmentName) => { - // Custom logic to get the runner, e.g., from a worker return myWorkerModuleRunners[environmentName] } ``` From e2d726ee7641a7068970a0e9dfb0d178f6d4be27 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 5 Jan 2026 17:03:25 +0900 Subject: [PATCH 5/8] refactor(rsc): rename to `__VITE_ENVIRONMENT_RUNNER_IMPORT__` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplified API from `(environmentName) => Promise` to `(environmentName, id) => Promise` for better abstraction. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- packages/plugin-rsc/README.md | 22 +++++++++++----------- packages/plugin-rsc/src/plugin.ts | 15 +++++++-------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/packages/plugin-rsc/README.md b/packages/plugin-rsc/README.md index 526f7439d..4a6d3c92a 100644 --- a/packages/plugin-rsc/README.md +++ b/packages/plugin-rsc/README.md @@ -228,7 +228,7 @@ The plugin provides an additional helper for multi environment interaction. This allows importing `ssr` environment module specified by `environments.ssr.build.rollupOptions.input[entryName]` inside `rsc` environment and vice versa. -During development, by default, this API assumes both `rsc` and `ssr` environments execute under the main Vite process as `RunnableDevEnvironment`. Internally, `loadModule` uses the global `__VITE_GET_MODULE_RUNNER__` function to obtain the module runner for the target environment (see [`__VITE_GET_MODULE_RUNNER__`](#__vite_get_module_runner__) below). +During development, by default, this API assumes both `rsc` and `ssr` environments execute under the main Vite process as `RunnableDevEnvironment`. Internally, `loadModule` uses the global `__VITE_ENVIRONMENT_RUNNER_IMPORT__` function to import modules in the target environment (see [`__VITE_ENVIRONMENT_RUNNER_IMPORT__`](#__vite_environment_runner_import__) below). When enabling `rsc({ loadModuleDevProxy: true })` plugin option, the loaded module is implemented as a proxy with `fetch`-based RPC to call in node environment on the main Vite process, which for example, allows `rsc` environment inside cloudflare workers to access `ssr` environment on the main Vite process. This proxy mechanism uses [turbo-stream](https://github.com/jacob-ebey/turbo-stream) for serializing data types beyond JSON, with custom encoders/decoders to additionally support `Request` and `Response` instances. @@ -331,29 +331,29 @@ import.meta.hot.on('rsc:update', async () => { ### Global API -#### `__VITE_GET_MODULE_RUNNER__` +#### `__VITE_ENVIRONMENT_RUNNER_IMPORT__` -- Type: `(environmentName: string) => Promise` +- Type: `(environmentName: string, id: string) => Promise` - Availability: Development only (set by `configureServer`) -This global function provides a standardized way to obtain a Vite [ModuleRunner](https://vite.dev/guide/api-environment#modulerunner) for a given environment during development. It is used internally by `import.meta.viteRsc.loadModule` to execute modules in the target environment. +This global function provides a standardized way to import a module in a given environment during development. It is used internally by `import.meta.viteRsc.loadModule` to execute modules in the target environment. -By default, the plugin sets this global to retrieve the runner from the Vite dev server as both run in the same main process: +By default, the plugin sets this global to import via the environment's module runner: ```js -globalThis.__VITE_GET_MODULE_RUNNER__ = async (environmentName) => { - return server.environments[environmentName].runner +globalThis.__VITE_ENVIRONMENT_RUNNER_IMPORT__ = async (environmentName, id) => { + return server.environments[environmentName].runner.import(id) } ``` **Custom Environment Integration:** -Frameworks with custom environment setups (e.g., environments running in separate workers) can override this global to provide their own module runner resolution. +Frameworks with custom environment setups (e.g., environments running in separate workers or with custom module loading) can override this global to provide their own module import logic. ```js -// Custom logic to get the runner, e.g., from a worker runtime -globalThis.__VITE_GET_MODULE_RUNNER__ = async (environmentName) => { - return myWorkerModuleRunners[environmentName] +// Custom logic to import module, e.g., via RPC to a worker +globalThis.__VITE_ENVIRONMENT_RUNNER_IMPORT__ = async (environmentName, id) => { + return myWorkerRpc.import(environmentName, id) } ``` diff --git a/packages/plugin-rsc/src/plugin.ts b/packages/plugin-rsc/src/plugin.ts index d87d42603..c3ec2baac 100644 --- a/packages/plugin-rsc/src/plugin.ts +++ b/packages/plugin-rsc/src/plugin.ts @@ -61,7 +61,6 @@ import { parseReferenceValidationVirtual, } from './plugins/shared' import { stripLiteral } from 'strip-literal' -import type { ModuleRunner } from 'vite/module-runner' const isRolldownVite = 'rolldownVersion' in vite @@ -303,9 +302,10 @@ export function vitePluginRscMinimal( } declare global { - function __VITE_GET_MODULE_RUNNER__( + function __VITE_ENVIRONMENT_RUNNER_IMPORT__( environmentName: string, - ): Promise + id: string, + ): Promise } export default function vitePluginRsc( @@ -523,8 +523,9 @@ export default function vitePluginRsc( }, }, configureServer(server) { - globalThis.__VITE_GET_MODULE_RUNNER__ = async function ( + globalThis.__VITE_ENVIRONMENT_RUNNER_IMPORT__ = async function ( environmentName, + id, ) { const environment = server.environments[environmentName] if (!environment) { @@ -537,7 +538,7 @@ export default function vitePluginRsc( `[vite-rsc] environment '${environmentName}' is not runnable`, ) } - return environment.runner + return environment.runner.import(id) } // intercept client hmr to propagate client boundary invalidation to server environment @@ -790,9 +791,7 @@ export default function vitePluginRsc( const source = getEntrySource(environment.config, entryName) const resolved = await environment.pluginContainer.resolveId(source) assert(resolved, `[vite-rsc] failed to resolve entry '${source}'`) - const environmentNameJson = JSON.stringify(environmentName) - const resolvedIdJson = JSON.stringify(resolved.id) - replacement = `globalThis.__VITE_GET_MODULE_RUNNER__(${environmentNameJson}).then(runner => runner.import(${resolvedIdJson}))` + replacement = `globalThis.__VITE_ENVIRONMENT_RUNNER_IMPORT__(${JSON.stringify(environmentName)}, ${JSON.stringify(resolved.id)})` } else { replacement = JSON.stringify( `__vite_rsc_load_module:${this.environment.name}:${environmentName}:${entryName}`, From 83fcbc6bc7569744f4639074991e1db079c9bc77 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 5 Jan 2026 17:04:59 +0900 Subject: [PATCH 6/8] chore: readme --- packages/plugin-rsc/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/plugin-rsc/README.md b/packages/plugin-rsc/README.md index 4a6d3c92a..f7e6af5d0 100644 --- a/packages/plugin-rsc/README.md +++ b/packages/plugin-rsc/README.md @@ -334,7 +334,6 @@ import.meta.hot.on('rsc:update', async () => { #### `__VITE_ENVIRONMENT_RUNNER_IMPORT__` - Type: `(environmentName: string, id: string) => Promise` -- Availability: Development only (set by `configureServer`) This global function provides a standardized way to import a module in a given environment during development. It is used internally by `import.meta.viteRsc.loadModule` to execute modules in the target environment. @@ -351,9 +350,9 @@ globalThis.__VITE_ENVIRONMENT_RUNNER_IMPORT__ = async (environmentName, id) => { Frameworks with custom environment setups (e.g., environments running in separate workers or with custom module loading) can override this global to provide their own module import logic. ```js -// Custom logic to import module, e.g., via RPC to a worker +// Custom logic to import module between multiple environments inside worker globalThis.__VITE_ENVIRONMENT_RUNNER_IMPORT__ = async (environmentName, id) => { - return myWorkerRpc.import(environmentName, id) + return myWorkerRunners[environmentname].import(id) } ``` From 22cbae8e13d99e714197ad36c6986f7a7366f8ca Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 5 Jan 2026 17:42:15 +0900 Subject: [PATCH 7/8] feat: support single rollupOptions.input --- packages/plugin-rsc/src/plugins/utils.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/plugin-rsc/src/plugins/utils.ts b/packages/plugin-rsc/src/plugins/utils.ts index 42dfe42d1..0a46f4063 100644 --- a/packages/plugin-rsc/src/plugins/utils.ts +++ b/packages/plugin-rsc/src/plugins/utils.ts @@ -65,6 +65,11 @@ export function getEntrySource( name: string = 'index', ): string { const input = config.build.rollupOptions.input + // TODO: not documented feature yet, but for now, + // this is for Nitro's single entry convention. + if (typeof input === 'string') { + return input + } assert( typeof input === 'object' && !Array.isArray(input) && From 15fa1bc8a2b36a7cfd0fff1f0658a18404899635 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 5 Jan 2026 17:56:10 +0900 Subject: [PATCH 8/8] Revert "feat: support single rollupOptions.input" This reverts commit 22cbae8e13d99e714197ad36c6986f7a7366f8ca. --- packages/plugin-rsc/src/plugins/utils.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/plugin-rsc/src/plugins/utils.ts b/packages/plugin-rsc/src/plugins/utils.ts index 0a46f4063..42dfe42d1 100644 --- a/packages/plugin-rsc/src/plugins/utils.ts +++ b/packages/plugin-rsc/src/plugins/utils.ts @@ -65,11 +65,6 @@ export function getEntrySource( name: string = 'index', ): string { const input = config.build.rollupOptions.input - // TODO: not documented feature yet, but for now, - // this is for Nitro's single entry convention. - if (typeof input === 'string') { - return input - } assert( typeof input === 'object' && !Array.isArray(input) &&