Skip to content

Commit fb292f2

Browse files
authored
feat: introduce RunnableDevEnvironment (#18190)
1 parent 34041b9 commit fb292f2

File tree

8 files changed

+128
-57
lines changed

8 files changed

+128
-57
lines changed

docs/guide/api-environment.md

+27-16
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,22 @@ interface TransformResult {
107107
}
108108
```
109109

110+
Vite also supports a `RunnableDevEnvironment`, that extends a `DevEnvironment` exposing a `ModuleRunner` instance. You can guard any runnable environment with an `isRunnableDevEnvironment` function.
111+
112+
:::warning
113+
The `runner` is evaluated eagerly when it's accessed for the first time. Beware that Vite enables source map support when the `runner` is created by calling `process.setSourceMapsEnabled` or by overriding `Error.prepareStackTrace` if it's not available.
114+
:::
115+
116+
```ts
117+
export class RunnableDevEnvironment extends DevEnvironment {
118+
public readonly runner: ModuleRunnner
119+
}
120+
121+
if (isRunnableDevEnvironment(server.environments.ssr)) {
122+
await server.environments.ssr.runner.import('/entry-point.js')
123+
}
124+
```
125+
110126
An environment instance in the Vite server lets you process a URL using the `environment.transformRequest(url)` method. This function will use the plugin pipeline to resolve the `url` to a module `id`, load it (reading the file from the file system or through a plugin that implements a virtual module), and then transform the code. While transforming the module, imports and other metadata will be recorded in the environment module graph by creating or updating the corresponding module node. When processing is done, the transform result is also stored in the module.
111127

112128
But the environment instance can't execute the code itself, as the runtime where the module will be run could be different from the one the Vite server is running in. This is the case for the browser environment. When a html is loaded in the browser, its scripts are executed triggering the evaluation of the entire static module graph. Each imported URL generates a request to the Vite server to get the module code, which ends up handled by the Transform Middleware by calling `server.environments.client.transformRequest(url)`. The connection between the environment instance in the server and the module runner in the browser is carried out through HTTP in this case.
@@ -119,7 +135,7 @@ We are using `transformRequest(url)` and `warmupRequest(url)` in the current ver
119135
The initial proposal had a `run` method that would allow consumers to invoke an import on the runner side by using the `transport` option. During our testing we found out that the API was not universal enough to start recommending it. We are open to implement a built-in layer for remote SSR implementation based on the frameworks feedback. In the meantime, Vite still exposes a [`RunnerTransport` API](#runnertransport) to hide the complexity of the runner RPC.
120136
:::
121137

122-
For the `ssr` environment running in Node by default, Vite creates a module runner that implements evaluation using `new AsyncFunction` running in the same JS runtime as the dev server. This runner is an instance of `ModuleRunner` that exposes:
138+
In dev mode the default `ssr` environment is a `RunnableDevEnvironment` with a module runner that implements evaluation using `new AsyncFunction` running in the same JS runtime as the dev server. This runner is an instance of `ModuleRunner` that exposes:
123139

124140
```ts
125141
class ModuleRunner {
@@ -137,15 +153,10 @@ class ModuleRunner {
137153
In the v5.1 Runtime API, there were `executeUrl` and `executeEntryPoint` methods - they are now merged into a single `import` method. If you want to opt-out of the HMR support, create a runner with `hmr: false` flag.
138154
:::
139155
140-
The default SSR Node module runner is not exposed. You can use `createNodeEnvironment` API with `createServerModuleRunner` together to create a runner that runs code in the same thread, supports HMR and doesn't conflict with the SSR implementation (in case it's been overridden in the config). Given a Vite server configured in middleware mode as described by the [SSR setup guide](/guide/ssr#setting-up-the-dev-server), let's implement the SSR middleware using the environment API. Error handling is omitted.
156+
Given a Vite server configured in middleware mode as described by the [SSR setup guide](/guide/ssr#setting-up-the-dev-server), let's implement the SSR middleware using the environment API. Error handling is omitted.
141157
142158
```js
143-
import {
144-
createServer,
145-
createServerHotChannel,
146-
createServerModuleRunner,
147-
createNodeDevEnvironment,
148-
} from 'vite'
159+
import { createServer, createRunnableDevEnvironment } from 'vite'
149160

150161
const server = await createServer({
151162
server: { middlewareMode: true },
@@ -156,16 +167,16 @@ const server = await createServer({
156167
// Default Vite SSR environment can be overridden in the config, so
157168
// make sure you have a Node environment before the request is received.
158169
createEnvironment(name, config) {
159-
return createNodeDevEnvironment(name, config, {
160-
hot: createServerHotChannel(),
161-
})
170+
return createRunnableDevEnvironment(name, config)
162171
},
163172
},
164173
},
165174
},
166175
})
167176

168-
const runner = createServerModuleRunner(server.environments.node)
177+
// You might need to cast this to RunnableDevEnvironment in TypeScript or use
178+
// the "isRunnableDevEnvironment" function to guard the access to the runner
179+
const environment = server.environments.node
169180

170181
app.use('*', async (req, res, next) => {
171182
const url = req.originalUrl
@@ -181,7 +192,7 @@ app.use('*', async (req, res, next) => {
181192
// 3. Load the server entry. import(url) automatically transforms
182193
// ESM source code to be usable in Node.js! There is no bundling
183194
// required, and provides full HMR support.
184-
const { render } = await runner.import('/src/entry-server.js')
195+
const { render } = await environment.runner.import('/src/entry-server.js')
185196

186197
// 4. render the app HTML. This assumes entry-server.js's exported
187198
// `render` function calls appropriate framework SSR APIs,
@@ -310,7 +321,7 @@ function createWorkerdDevEnvironment(name: string, config: ResolvedConfig, conte
310321
...context.options,
311322
},
312323
hot,
313-
runner: {
324+
remoteRunner: {
314325
transport,
315326
},
316327
})
@@ -395,7 +406,7 @@ export default {
395406
dev: {
396407
createEnvironment(name, config, { watcher }) {
397408
// Called with 'rsc' and the resolved config during dev
398-
return createNodeDevEnvironment(name, config, {
409+
return createRunnableDevEnvironment(name, config, {
399410
hot: customHotChannel(),
400411
watcher
401412
})
@@ -786,7 +797,7 @@ function createWorkerEnvironment(name, config, context) {
786797
const worker = new Worker('./worker.js')
787798
return new DevEnvironment(name, config, {
788799
hot: /* custom hot channel */,
789-
runner: {
800+
remoteRunner: {
790801
transport: new RemoteEnvironmentTransport({
791802
send: (data) => worker.postMessage(data),
792803
onMessage: (listener) => worker.on('message', listener),

packages/vite/src/node/config.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,7 @@ import { resolveBuildEnvironmentOptions, resolveBuilderOptions } from './build'
3939
import type { ResolvedServerOptions, ServerOptions } from './server'
4040
import { resolveServerOptions } from './server'
4141
import { DevEnvironment } from './server/environment'
42-
import { createNodeDevEnvironment } from './server/environments/nodeEnvironment'
43-
import { createServerHotChannel } from './server/hmr'
42+
import { createRunnableDevEnvironment } from './server/environments/runnableEnvironment'
4443
import type { WebSocketServer } from './server/ws'
4544
import type { PreviewOptions, ResolvedPreviewOptions } from './preview'
4645
import { resolvePreviewOptions } from './preview'
@@ -213,9 +212,7 @@ function defaultCreateSsrDevEnvironment(
213212
name: string,
214213
config: ResolvedConfig,
215214
): DevEnvironment {
216-
return createNodeDevEnvironment(name, config, {
217-
hot: createServerHotChannel(),
218-
})
215+
return createRunnableDevEnvironment(name, config)
219216
}
220217

221218
function defaultCreateDevEnvironment(name: string, config: ResolvedConfig) {

packages/vite/src/node/index.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ export { transformWithEsbuild } from './plugins/esbuild'
2020
export { buildErrorMessage } from './server/middlewares/error'
2121

2222
export { RemoteEnvironmentTransport } from './server/environmentTransport'
23-
export { createNodeDevEnvironment } from './server/environments/nodeEnvironment'
23+
export {
24+
createRunnableDevEnvironment,
25+
isRunnableDevEnvironment,
26+
type RunnableDevEnvironment,
27+
type RunnableDevEnvironmentContext,
28+
} from './server/environments/runnableEnvironment'
2429
export {
2530
DevEnvironment,
2631
type DevEnvironmentContext,

packages/vite/src/node/server/environment.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import type {
1010
} from '../config'
1111
import { getDefaultResolvedEnvironmentOptions } from '../config'
1212
import { mergeConfig, promiseWithResolvers } from '../utils'
13-
import type { FetchModuleOptions } from '../ssr/fetchModule'
1413
import { fetchModule } from '../ssr/fetchModule'
1514
import type { DepsOptimizer } from '../optimizer'
1615
import { isDepOptimizationDisabled } from '../optimizer'
@@ -36,7 +35,8 @@ import { isWebSocketServer } from './ws'
3635
export interface DevEnvironmentContext {
3736
hot: false | HotChannel
3837
options?: EnvironmentOptions
39-
runner?: FetchModuleOptions & {
38+
remoteRunner?: {
39+
inlineSourceMap?: boolean
4040
transport?: RemoteEnvironmentTransport
4141
}
4242
depsOptimizer?: DepsOptimizer
@@ -50,7 +50,7 @@ export class DevEnvironment extends BaseEnvironment {
5050
/**
5151
* @internal
5252
*/
53-
_ssrRunnerOptions: FetchModuleOptions | undefined
53+
_remoteRunnerOptions: DevEnvironmentContext['remoteRunner']
5454

5555
get pluginContainer(): EnvironmentPluginContainer {
5656
if (!this._pluginContainer)
@@ -117,8 +117,8 @@ export class DevEnvironment extends BaseEnvironment {
117117

118118
this._crawlEndFinder = setupOnCrawlEnd()
119119

120-
this._ssrRunnerOptions = context.runner ?? {}
121-
context.runner?.transport?.register(this)
120+
this._remoteRunnerOptions = context.remoteRunner ?? {}
121+
context.remoteRunner?.transport?.register(this)
122122

123123
this.hot.on('vite:invalidate', async ({ path, message }) => {
124124
invalidateModule(this, {
@@ -166,7 +166,7 @@ export class DevEnvironment extends BaseEnvironment {
166166
options?: FetchFunctionOptions,
167167
): Promise<FetchResult> {
168168
return fetchModule(this, id, importer, {
169-
...this._ssrRunnerOptions,
169+
...this._remoteRunnerOptions,
170170
...options,
171171
})
172172
}

packages/vite/src/node/server/environments/nodeEnvironment.ts

-17
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type { ModuleRunner } from 'vite/module-runner'
2+
import type { ResolvedConfig } from '../../config'
3+
import type { DevEnvironmentContext } from '../environment'
4+
import { DevEnvironment } from '../environment'
5+
import { createServerModuleRunner } from '../../ssr/runtime/serverModuleRunner'
6+
import type { HotChannel } from '../hmr'
7+
import { createServerHotChannel } from '../hmr'
8+
import type { Environment } from '../../environment'
9+
10+
export function createRunnableDevEnvironment(
11+
name: string,
12+
config: ResolvedConfig,
13+
context: RunnableDevEnvironmentContext = {},
14+
): DevEnvironment {
15+
if (context.hot == null) {
16+
context.hot = createServerHotChannel()
17+
}
18+
19+
return new RunnableDevEnvironment(name, config, context)
20+
}
21+
22+
export interface RunnableDevEnvironmentContext
23+
extends Omit<DevEnvironmentContext, 'hot'> {
24+
runner?: (environment: RunnableDevEnvironment) => ModuleRunner
25+
hot?: false | HotChannel
26+
}
27+
28+
export function isRunnableDevEnvironment(
29+
environment: Environment,
30+
): environment is RunnableDevEnvironment {
31+
return environment instanceof RunnableDevEnvironment
32+
}
33+
34+
class RunnableDevEnvironment extends DevEnvironment {
35+
private _runner: ModuleRunner | undefined
36+
private _runnerFactory:
37+
| ((environment: RunnableDevEnvironment) => ModuleRunner)
38+
| undefined
39+
40+
constructor(
41+
name: string,
42+
config: ResolvedConfig,
43+
context: RunnableDevEnvironmentContext,
44+
) {
45+
super(name, config, context as DevEnvironmentContext)
46+
this._runnerFactory = context.runner
47+
}
48+
49+
get runner(): ModuleRunner {
50+
if (this._runner) {
51+
return this._runner
52+
}
53+
if (this._runnerFactory) {
54+
this._runner = this._runnerFactory(this)
55+
return this._runner
56+
}
57+
return createServerModuleRunner(this)
58+
}
59+
}
60+
61+
export type { RunnableDevEnvironment }

packages/vite/src/node/ssr/runtime/__tests__/server-worker-runner.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ describe('running module runner inside a worker', () => {
3232
dev: {
3333
createEnvironment: (name, config) => {
3434
return new DevEnvironment(name, config, {
35-
runner: {
35+
remoteRunner: {
3636
transport: new RemoteEnvironmentTransport({
3737
send: (data) => worker.postMessage(data),
3838
onMessage: (handler) => worker.on('message', handler),

playground/hmr-ssr/__tests__/hmr-ssr.spec.ts

+25-11
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ import {
1010
test,
1111
vi,
1212
} from 'vitest'
13-
import type { InlineConfig, ViteDevServer } from 'vite'
14-
import { createServer, createServerModuleRunner } from 'vite'
13+
import type { InlineConfig, RunnableDevEnvironment, ViteDevServer } from 'vite'
14+
import {
15+
createRunnableDevEnvironment,
16+
createServer,
17+
createServerHotChannel,
18+
createServerModuleRunner,
19+
} from 'vite'
1520
import type { ModuleRunner } from 'vite/module-runner'
1621
import {
1722
addFile,
@@ -1036,6 +1041,10 @@ async function setupModuleRunner(
10361041

10371042
globalThis.__HMR__ = initHmrState as any
10381043

1044+
const logger = new HMRMockLogger()
1045+
// @ts-expect-error not typed for HMR
1046+
globalThis.log = (...msg) => logger.log(...msg)
1047+
10391048
server = await createServer({
10401049
configFile: resolve(testDir, 'vite.config.ts'),
10411050
root: testDir,
@@ -1053,6 +1062,19 @@ async function setupModuleRunner(
10531062
},
10541063
preTransformRequests: false,
10551064
},
1065+
environments: {
1066+
ssr: {
1067+
dev: {
1068+
createEnvironment(name, config) {
1069+
return createRunnableDevEnvironment(name, config, {
1070+
runner: (env) =>
1071+
createServerModuleRunner(env, { hmr: { logger } }),
1072+
hot: createServerHotChannel(),
1073+
})
1074+
},
1075+
},
1076+
},
1077+
},
10561078
optimizeDeps: {
10571079
disabled: true,
10581080
noDiscovery: true,
@@ -1061,15 +1083,7 @@ async function setupModuleRunner(
10611083
...serverOptions,
10621084
})
10631085

1064-
const logger = new HMRMockLogger()
1065-
// @ts-expect-error not typed for HMR
1066-
globalThis.log = (...msg) => logger.log(...msg)
1067-
1068-
runner = createServerModuleRunner(server.environments.ssr, {
1069-
hmr: {
1070-
logger,
1071-
},
1072-
})
1086+
runner = (server.environments.ssr as RunnableDevEnvironment).runner
10731087

10741088
await waitForWatcher(server, waitForFile)
10751089

0 commit comments

Comments
 (0)