> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/vitejs/vite/llms.txt
> Use this file to discover all available pages before exploring further.

# Environment Runtimes

> Creating custom environment factories and module runners for different runtimes

<Warning>
  **Release Candidate**: The Environment API is generally in the release candidate phase. We'll maintain stability in the APIs between major releases to allow the ecosystem to experiment and build upon them. However, note that [some specific APIs](/changes/#considering) are still considered experimental.

  Resources:

  * [Feedback discussion](https://github.com/vitejs/vite/discussions/16358)
  * [Environment API PR](https://github.com/vitejs/vite/pull/16471)

  Please share your feedback with us.
</Warning>

## Environment Factories

Environment factories are intended to be implemented by Environment providers like Cloudflare, and not by end users. Environment factories return a `EnvironmentOptions` for the most common case of using the target runtime for both dev and build environments.

The default environment options can also be set so the user doesn't need to do it.

```ts theme={null}
function createWorkerdEnvironment(
  userConfig: EnvironmentOptions,
): EnvironmentOptions {
  return mergeConfig(
    {
      resolve: {
        conditions: [/*...*/],
      },
      dev: {
        createEnvironment(name, config) {
          return createWorkerdDevEnvironment(name, config, {
            hot: true,
            transport: customHotChannel(),
          })
        },
      },
      build: {
        createEnvironment(name, config) {
          return createWorkerdBuildEnvironment(name, config)
        },
      },
    },
    userConfig,
  )
}
```

### Usage in Config

Then the config file can be written as:

```js theme={null}
import { createWorkerdEnvironment } from 'vite-environment-workerd'

export default {
  environments: {
    ssr: createWorkerdEnvironment({
      build: {
        outDir: '/dist/ssr',
      },
    }),
    rsc: createWorkerdEnvironment({
      build: {
        outDir: '/dist/rsc',
      },
    }),
  },
}
```

And frameworks can use an environment with the workerd runtime to do SSR using:

```js theme={null}
const ssrEnvironment = server.environments.ssr
```

## Creating a New Environment Factory

A Vite dev server exposes two environments by default:

* A `client` environment (browser environment by default)
* An `ssr` environment (runs in the same Node runtime as the Vite server by default)

The client environment is a browser environment by default, and the module runner is implemented by importing the virtual module `/@vite/client` to client apps. The SSR environment runs in the same Node runtime as the Vite server by default and allows application servers to be used to render requests during dev with full HMR support.

### Module Processing and Execution

The transformed source code is called a module, and the relationships between the modules processed in each environment are kept in a module graph. The transformed code for these modules is sent to the runtimes associated with each environment to be executed.

When a module is evaluated in the runtime, its imported modules will be requested triggering the processing of a section of the module graph.

### Module Runners

A Vite Module Runner allows running any code by processing it with Vite plugins first. It is different from `server.ssrLoadModule` because the runner implementation is decoupled from the server.

This allows library and framework authors to implement their layer of communication between the Vite server and the runner:

* The browser communicates with its corresponding environment using the server WebSocket and through HTTP requests
* The Node Module runner can directly do function calls to process modules as it is running in the same process
* Other environments could run modules connecting to a JS runtime like workerd, or a Worker Thread as Vitest does

### Creating Custom Environments

One of the goals of this feature is to provide a customizable API to process and run code. Users can create new environment factories using the exposed primitives.

```ts theme={null}
import { DevEnvironment, HotChannel } from 'vite'

function createWorkerdDevEnvironment(
  name: string,
  config: ResolvedConfig,
  context: DevEnvironmentContext
) {
  const connection = /* ... */
  const transport: HotChannel = {
    on: (listener) => { connection.on('message', listener) },
    send: (data) => connection.send(data),
  }

  const workerdDevEnvironment = new DevEnvironment(name, config, {
    options: {
      resolve: { conditions: ['custom'] },
      ...context.options,
    },
    hot: true,
    transport,
  })
  return workerdDevEnvironment
}
```

<Note>
  There are [multiple communication levels for the `DevEnvironment`](/guide/api-environment-frameworks#devenvironment-communication-levels). To make it easier for frameworks to write runtime agnostic code, we recommend to implement the most flexible communication level possible.
</Note>

## ModuleRunner

A module runner is instantiated in the target runtime. All APIs in this section are imported from `vite/module-runner` unless stated otherwise. This export entry point is kept as lightweight as possible, only exporting the minimal needed to create module runners.

### Type Signature

```ts theme={null}
export class ModuleRunner {
  constructor(
    public options: ModuleRunnerOptions,
    public evaluator: ModuleEvaluator = new ESModulesEvaluator(),
    private debug?: ModuleRunnerDebugger,
  ) {}
  
  public async import<T = any>(url: string): Promise<T>
  public clearCache(): void
  public async close(): Promise<void>
  public isClosed(): boolean
}
```

### Constructor

<ParamField path="options" type="ModuleRunnerOptions" required>
  Configuration options for the module runner
</ParamField>

<ParamField path="evaluator" type="ModuleEvaluator" optional default="new ESModulesEvaluator()">
  Module evaluator responsible for executing the code. Vite exports `ESModulesEvaluator` which uses `new AsyncFunction` to evaluate code.
</ParamField>

<ParamField path="debug" type="ModuleRunnerDebugger" optional>
  Debugger instance for debugging module execution
</ParamField>

### Methods

<ParamField path="import" type="<T = any>(url: string) => Promise<T>">
  Execute a module by URL. Accepts file path, server path, or id relative to the root.
</ParamField>

<ParamField path="clearCache" type="() => void">
  Clear all caches including HMR listeners
</ParamField>

<ParamField path="close" type="() => Promise<void>">
  Clear all caches, remove all HMR listeners, reset sourcemap support. This method doesn't stop the HMR connection.
</ParamField>

<ParamField path="isClosed" type="() => boolean">
  Returns `true` if the runner has been closed by calling `close()`
</ParamField>

### Example Usage

```js theme={null}
import {
  ModuleRunner,
  ESModulesEvaluator,
  createNodeImportMeta,
} from 'vite/module-runner'
import { transport } from './rpc-implementation.js'

const moduleRunner = new ModuleRunner(
  {
    transport,
    createImportMeta: createNodeImportMeta, // if the module runner runs in Node.js
  },
  new ESModulesEvaluator(),
)

await moduleRunner.import('/src/entry-point.js')
```

<Note>
  The module evaluator in `ModuleRunner` is responsible for executing the code. You can provide your own implementation if your JavaScript runtime doesn't support unsafe evaluation.

  When Vite server triggers `full-reload` HMR event, all affected modules will be re-executed. Be aware that Module Runner doesn't update `exports` object when this happens (it overrides it), you would need to run `import` or get the module from `evaluatedModules` again if you rely on having the latest `exports` object.
</Note>

## ModuleRunnerOptions

```ts theme={null}
interface ModuleRunnerOptions {
  transport: ModuleRunnerTransport
  sourcemapInterceptor?:
    | false
    | 'node'
    | 'prepareStackTrace'
    | InterceptorOptions
  hmr?: boolean | ModuleRunnerHmr
  evaluatedModules?: EvaluatedModules
}
```

### Properties

<ParamField path="transport" type="ModuleRunnerTransport" required>
  A set of methods to communicate with the server
</ParamField>

<ParamField path="sourcemapInterceptor" type="false | 'node' | 'prepareStackTrace' | InterceptorOptions" optional>
  Configure how source maps are resolved. Prefers `node` if `process.setSourceMapsEnabled` is available. Otherwise it will use `prepareStackTrace` by default which overrides `Error.prepareStackTrace` method. You can provide an object to configure how file contents and source maps are resolved for files that were not processed by Vite.
</ParamField>

<ParamField path="hmr" type="boolean | ModuleRunnerHmr" optional default="true">
  Disable HMR or configure HMR options
</ParamField>

<ParamField path="evaluatedModules" type="EvaluatedModules" optional>
  Custom module cache. If not provided, it creates a separate module cache for each module runner instance.
</ParamField>

## ModuleEvaluator

The module evaluator interface for executing transformed code.

```ts theme={null}
interface ModuleEvaluator {
  startOffset?: number
  
  runInlinedModule(
    context: ModuleRunnerContext,
    code: string,
    id: string,
  ): Promise<any>
  
  runExternalModule(file: string): Promise<any>
}
```

### Properties

<ParamField path="startOffset" type="number" optional>
  Number of prefixed lines in the transformed code
</ParamField>

### Methods

<ParamField path="runInlinedModule" type="(context: ModuleRunnerContext, code: string, id: string) => Promise<any>" required>
  Evaluate code that was transformed by Vite

  * `context`: Function context
  * `code`: Transformed code
  * `id`: ID that was used to fetch the module
</ParamField>

<ParamField path="runExternalModule" type="(file: string) => Promise<any>" required>
  Evaluate externalized module

  * `file`: File URL to the external module
</ParamField>

Vite exports `ESModulesEvaluator` that implements this interface by default. It uses `new AsyncFunction` to evaluate code, so if the code has inlined source map it should contain an [offset of 2 lines](https://tc39.es/ecma262/#sec-createdynamicfunction) to accommodate for new lines added.

This is done automatically by the `ESModulesEvaluator`. Custom evaluators will not add additional lines.

## ModuleRunnerTransport

Transport object that communicates with the environment via an RPC or by directly calling the function.

```ts theme={null}
interface ModuleRunnerTransport {
  connect?(handlers: ModuleRunnerTransportHandlers): Promise<void> | void
  disconnect?(): Promise<void> | void
  send?(data: HotPayload): Promise<void> | void
  invoke?(data: HotPayload): Promise<{ result: any } | { error: any }>
  timeout?: number
}
```

### Properties and Methods

<ParamField path="connect" type="(handlers: ModuleRunnerTransportHandlers) => Promise<void> | void" optional>
  Connect to the environment and register message handlers
</ParamField>

<ParamField path="disconnect" type="() => Promise<void> | void" optional>
  Disconnect from the environment
</ParamField>

<ParamField path="send" type="(data: HotPayload) => Promise<void> | void" optional>
  Send data to the environment (required if `invoke` is not implemented)
</ParamField>

<ParamField path="invoke" type="(data: HotPayload) => Promise<{ result: any } | { error: any }>" optional>
  Invoke a method on the environment and get the result. If not implemented, Vite will construct it internally using `send` and `connect`.
</ParamField>

<ParamField path="timeout" type="number" optional>
  Timeout in milliseconds for invoke calls
</ParamField>

### Worker Thread Example

You need to couple the transport with the `HotChannel` instance on the server. Here's an example where module runner is created in the worker thread:

<CodeGroup>
  ```js worker.js theme={null}
  import { parentPort } from 'node:worker_threads'
  import { fileURLToPath } from 'node:url'
  import {
    ESModulesEvaluator,
    ModuleRunner,
    createNodeImportMeta,
  } from 'vite/module-runner'

  /** @type {import('vite/module-runner').ModuleRunnerTransport} */
  const transport = {
    connect({ onMessage, onDisconnection }) {
      parentPort.on('message', onMessage)
      parentPort.on('close', onDisconnection)
    },
    send(data) {
      parentPort.postMessage(data)
    },
  }

  const runner = new ModuleRunner(
    {
      transport,
      createImportMeta: createNodeImportMeta,
    },
    new ESModulesEvaluator(),
  )
  ```

  ```js server.js theme={null}
  import { BroadcastChannel } from 'node:worker_threads'
  import { createServer, RemoteEnvironmentTransport, DevEnvironment } from 'vite'

  function createWorkerEnvironment(name, config, context) {
    const worker = new Worker('./worker.js')
    const handlerToWorkerListener = new WeakMap()
    const client = {
      send(payload: HotPayload) {
        worker.postMessage(payload)
      },
    }

    const workerHotChannel = {
      send: (data) => worker.postMessage(data),
      on: (event, handler) => {
        // client is already connected
        if (event === 'vite:client:connect') return
        if (event === 'vite:client:disconnect') {
          const listener = () => {
            handler(undefined, client)
          }
          handlerToWorkerListener.set(handler, listener)
          worker.on('exit', listener)
          return
        }

        const listener = (value) => {
          if (value.type === 'custom' && value.event === event) {
            handler(value.data, client)
          }
        }
        handlerToWorkerListener.set(handler, listener)
        worker.on('message', listener)
      },
      off: (event, handler) => {
        if (event === 'vite:client:connect') return
        if (event === 'vite:client:disconnect') {
          const listener = handlerToWorkerListener.get(handler)
          if (listener) {
            worker.off('exit', listener)
            handlerToWorkerListener.delete(handler)
          }
          return
        }

        const listener = handlerToWorkerListener.get(handler)
        if (listener) {
          worker.off('message', listener)
          handlerToWorkerListener.delete(handler)
        }
      },
    }

    return new DevEnvironment(name, config, {
      transport: workerHotChannel,
    })
  }

  await createServer({
    environments: {
      worker: {
        dev: {
          createEnvironment: createWorkerEnvironment,
        },
      },
    },
  })
  ```
</CodeGroup>

<Warning>
  Make sure to implement the `vite:client:connect` / `vite:client:disconnect` events in the `on` / `off` methods when those methods exist. `vite:client:connect` event should be emitted when the connection is established, and `vite:client:disconnect` event should be emitted when the connection is closed. The `HotChannelClient` object passed to the event handler must have the same reference for the same connection.
</Warning>

### HTTP Request Example

A different example using an HTTP request to communicate between the runner and the server:

```ts theme={null}
import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner'

export const runner = new ModuleRunner(
  {
    transport: {
      async invoke(data) {
        const response = await fetch(`http://my-vite-server/invoke`, {
          method: 'POST',
          body: JSON.stringify(data),
        })
        return response.json()
      },
    },
    hmr: false, // disable HMR as HMR requires transport.connect
  },
  new ESModulesEvaluator(),
)

await runner.import('/entry.js')
```

In this case, the `handleInvoke` method in the `NormalizedHotChannel` can be used:

```ts theme={null}
const customEnvironment = new DevEnvironment(name, config, context)

server.onRequest((request: Request) => {
  const url = new URL(request.url)
  if (url.pathname === '/invoke') {
    const payload = (await request.json()) as HotPayload
    const result = customEnvironment.hot.handleInvoke(payload)
    return new Response(JSON.stringify(result))
  }
  return Response.error()
})
```

<Note>
  For HMR support, `send` and `connect` methods are required. The `send` method is usually called when the custom event is triggered (like, `import.meta.hot.send("my-event")`).

  Vite exports `createServerHotChannel` from the main entry point to support HMR during Vite SSR.
</Note>
