Description
ssrLoadModule() crashes with TypeError: Cannot read properties of undefined (reading 'outsideEmitter') when the SSR environment is a FetchableDevEnvironment (e.g. created by @cloudflare/vite-plugin with viteEnvironment: { name: "ssr" }).
Reproduction
import { createServer } from 'vite'
// User's vite.config includes @cloudflare/vite-plugin with
// viteEnvironment: { name: "ssr" }, which replaces the SSR
// environment with a FetchableDevEnvironment (CloudflareDevEnvironment)
const server = await createServer({
root: '/path/to/project',
server: { middlewareMode: true },
})
// This crashes:
await server.ssrLoadModule('/path/to/module.ts')
Root Cause
SSRCompatModuleRunner unconditionally creates a transport that accesses environment.hot.api.outsideEmitter, but FetchableDevEnvironment's hot channel doesn't have the api property.
Call chain
-
ssrLoadModule() (ssrModuleLoader.ts) lazily creates SSRCompatModuleRunner:
server._ssrCompatModuleRunner ||= new SSRCompatModuleRunner(environment)
-
SSRCompatModuleRunner constructor unconditionally passes environment.hot to the transport:
super({
transport: createServerModuleRunnerTransport({ channel: environment.hot }),
// ...
})
-
createServerModuleRunnerTransport connect() accesses api without a null check:
connect({ onMessage }) {
options.channel.api.outsideEmitter.on("send", onMessage) // 💥 api is undefined
}
Why api is missing
RunnableDevEnvironment works because createRunnableDevEnvironment() calls createServerHotChannel(), which creates the channel with api: { innerEmitter, outsideEmitter }.
FetchableDevEnvironment extends DevEnvironment directly. Its hot channel is initialized via normalizeHotChannel({}, context.hot) which spreads an empty object — no api property.
Note
createHMROptions() already has the right guard:
if (!("api" in environment.hot)) return false
This correctly disables HMR, but the transport is still created and its connect() method is called in the ModuleRunner constructor regardless.
Expected Behavior
ssrLoadModule() should work with any dev environment type, including FetchableDevEnvironment. Either:
SSRCompatModuleRunner should check for api before creating the transport
createServerModuleRunnerTransport connect() should handle missing api gracefully
- The
ModuleRunner constructor should not call connect() when HMR is disabled
Environment
- Vite:
8.0.0-beta.14
@cloudflare/vite-plugin: 1.25.6
Description
ssrLoadModule()crashes withTypeError: Cannot read properties of undefined (reading 'outsideEmitter')when the SSR environment is aFetchableDevEnvironment(e.g. created by@cloudflare/vite-pluginwithviteEnvironment: { name: "ssr" }).Reproduction
Root Cause
SSRCompatModuleRunnerunconditionally creates a transport that accessesenvironment.hot.api.outsideEmitter, butFetchableDevEnvironment's hot channel doesn't have theapiproperty.Call chain
ssrLoadModule()(ssrModuleLoader.ts) lazily createsSSRCompatModuleRunner:SSRCompatModuleRunnerconstructor unconditionally passesenvironment.hotto the transport:createServerModuleRunnerTransportconnect()accessesapiwithout a null check:Why
apiis missingRunnableDevEnvironmentworks becausecreateRunnableDevEnvironment()callscreateServerHotChannel(), which creates the channel withapi: { innerEmitter, outsideEmitter }.FetchableDevEnvironmentextendsDevEnvironmentdirectly. Its hot channel is initialized vianormalizeHotChannel({}, context.hot)which spreads an empty object — noapiproperty.Note
createHMROptions()already has the right guard:This correctly disables HMR, but the transport is still created and its
connect()method is called in theModuleRunnerconstructor regardless.Expected Behavior
ssrLoadModule()should work with any dev environment type, includingFetchableDevEnvironment. Either:SSRCompatModuleRunnershould check forapibefore creating the transportcreateServerModuleRunnerTransportconnect()should handle missingapigracefullyModuleRunnerconstructor should not callconnect()when HMR is disabledEnvironment
8.0.0-beta.14@cloudflare/vite-plugin:1.25.6