Skip to content

ssrLoadModule() crashes with FetchableDevEnvironment (outsideEmitter undefined) #21726

@hyf0

Description

@hyf0

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

  1. ssrLoadModule() (ssrModuleLoader.ts) lazily creates SSRCompatModuleRunner:

    server._ssrCompatModuleRunner ||= new SSRCompatModuleRunner(environment)
  2. SSRCompatModuleRunner constructor unconditionally passes environment.hot to the transport:

    super({
      transport: createServerModuleRunnerTransport({ channel: environment.hot }),
      // ...
    })
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions