Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce RunnableDevEnvironment #18190

Merged
merged 10 commits into from
Sep 27, 2024
43 changes: 27 additions & 16 deletions docs/guide/api-environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,22 @@ interface TransformResult {
}
```

Vite also supports a `RunnableDevEnvironment`, that extends a `DevEnvironment` exposing a `ModuleRunner` instance. You can guard any runnable environment with an `isRunnableDevEnvironment` function.

:::warning
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.
:::

```ts
export class RunnableDevEnvironment extends DevEnvironment {
public readonly runner: ModuleRunnner
}

if (isRunnableDevEnvironment(server.environments.ssr)) {
await server.environments.ssr.runner.import('/entry-point.js')
}
patak-dev marked this conversation as resolved.
Show resolved Hide resolved
```

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.

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.
Expand All @@ -119,7 +135,7 @@ We are using `transformRequest(url)` and `warmupRequest(url)` in the current ver
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.
:::

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:
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:

```ts
class ModuleRunner {
Expand All @@ -137,15 +153,10 @@ class ModuleRunner {
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.
:::

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.
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.

```js
import {
createServer,
createServerHotChannel,
createServerModuleRunner,
createNodeDevEnvironment,
} from 'vite'
import { createServer, createRunnableDevEnvironment } from 'vite'

const server = await createServer({
server: { middlewareMode: true },
Expand All @@ -156,16 +167,16 @@ const server = await createServer({
// Default Vite SSR environment can be overridden in the config, so
// make sure you have a Node environment before the request is received.
createEnvironment(name, config) {
return createNodeDevEnvironment(name, config, {
hot: createServerHotChannel(),
})
return createRunnableDevEnvironment(name, config)
},
},
},
},
})

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

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

// 4. render the app HTML. This assumes entry-server.js's exported
// `render` function calls appropriate framework SSR APIs,
Expand Down Expand Up @@ -310,7 +321,7 @@ function createWorkerdDevEnvironment(name: string, config: ResolvedConfig, conte
...context.options,
},
hot,
runner: {
remoteRunner: {
transport,
},
})
Expand Down Expand Up @@ -395,7 +406,7 @@ export default {
dev: {
createEnvironment(name, config, { watcher }) {
// Called with 'rsc' and the resolved config during dev
return createNodeDevEnvironment(name, config, {
return createRunnableDevEnvironment(name, config, {
hot: customHotChannel(),
watcher
})
Expand Down Expand Up @@ -786,7 +797,7 @@ function createWorkerEnvironment(name, config, context) {
const worker = new Worker('./worker.js')
return new DevEnvironment(name, config, {
hot: /* custom hot channel */,
runner: {
remoteRunner: {
transport: new RemoteEnvironmentTransport({
send: (data) => worker.postMessage(data),
onMessage: (listener) => worker.on('message', listener),
Expand Down
7 changes: 2 additions & 5 deletions packages/vite/src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ import { resolveBuildEnvironmentOptions, resolveBuilderOptions } from './build'
import type { ResolvedServerOptions, ServerOptions } from './server'
import { resolveServerOptions } from './server'
import { DevEnvironment } from './server/environment'
import { createNodeDevEnvironment } from './server/environments/nodeEnvironment'
import { createServerHotChannel } from './server/hmr'
import { createRunnableDevEnvironment } from './server/environments/runnableEnvironment'
import type { WebSocketServer } from './server/ws'
import type { PreviewOptions, ResolvedPreviewOptions } from './preview'
import { resolvePreviewOptions } from './preview'
Expand Down Expand Up @@ -213,9 +212,7 @@ function defaultCreateSsrDevEnvironment(
name: string,
config: ResolvedConfig,
): DevEnvironment {
return createNodeDevEnvironment(name, config, {
hot: createServerHotChannel(),
})
return createRunnableDevEnvironment(name, config)
}

function defaultCreateDevEnvironment(name: string, config: ResolvedConfig) {
Expand Down
7 changes: 6 additions & 1 deletion packages/vite/src/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ export { transformWithEsbuild } from './plugins/esbuild'
export { buildErrorMessage } from './server/middlewares/error'

export { RemoteEnvironmentTransport } from './server/environmentTransport'
export { createNodeDevEnvironment } from './server/environments/nodeEnvironment'
export {
createRunnableDevEnvironment,
isRunnableDevEnvironment,
type RunnableDevEnvironment,
type RunnableDevEnvironmentContext,
} from './server/environments/runnableEnvironment'
export {
DevEnvironment,
type DevEnvironmentContext,
Expand Down
12 changes: 6 additions & 6 deletions packages/vite/src/node/server/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import type {
} from '../config'
import { getDefaultResolvedEnvironmentOptions } from '../config'
import { mergeConfig, promiseWithResolvers } from '../utils'
import type { FetchModuleOptions } from '../ssr/fetchModule'
import { fetchModule } from '../ssr/fetchModule'
import type { DepsOptimizer } from '../optimizer'
import { isDepOptimizationDisabled } from '../optimizer'
Expand All @@ -35,7 +34,8 @@ import type { RemoteEnvironmentTransport } from './environmentTransport'
export interface DevEnvironmentContext {
hot: false | HotChannel
options?: EnvironmentOptions
runner?: FetchModuleOptions & {
remoteRunner?: {
inlineSourceMap?: boolean
transport?: RemoteEnvironmentTransport
}
depsOptimizer?: DepsOptimizer
Expand All @@ -49,7 +49,7 @@ export class DevEnvironment extends BaseEnvironment {
/**
* @internal
*/
_ssrRunnerOptions: FetchModuleOptions | undefined
_remoteRunnerOptions: DevEnvironmentContext['remoteRunner']

get pluginContainer(): EnvironmentPluginContainer {
if (!this._pluginContainer)
Expand Down Expand Up @@ -123,8 +123,8 @@ export class DevEnvironment extends BaseEnvironment {
this._onCrawlEndCallbacks.forEach((cb) => cb())
})

this._ssrRunnerOptions = context.runner ?? {}
context.runner?.transport?.register(this)
this._remoteRunnerOptions = context.remoteRunner ?? {}
context.remoteRunner?.transport?.register(this)

this.hot.on('vite:invalidate', async ({ path, message }) => {
invalidateModule(this, {
Expand Down Expand Up @@ -172,7 +172,7 @@ export class DevEnvironment extends BaseEnvironment {
options?: FetchFunctionOptions,
): Promise<FetchResult> {
return fetchModule(this, id, importer, {
...this._ssrRunnerOptions,
...this._remoteRunnerOptions,
...options,
})
}
Expand Down
17 changes: 0 additions & 17 deletions packages/vite/src/node/server/environments/nodeEnvironment.ts

This file was deleted.

61 changes: 61 additions & 0 deletions packages/vite/src/node/server/environments/runnableEnvironment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { ModuleRunner } from 'vite/module-runner'
import type { ResolvedConfig } from '../../config'
import type { DevEnvironmentContext } from '../environment'
import { DevEnvironment } from '../environment'
import { createServerModuleRunner } from '../../ssr/runtime/serverModuleRunner'
import type { HotChannel } from '../hmr'
import { createServerHotChannel } from '../hmr'
import type { Environment } from '../../environment'

export function createRunnableDevEnvironment(
name: string,
config: ResolvedConfig,
context: RunnableDevEnvironmentContext = {},
): DevEnvironment {
if (context.hot == null) {
context.hot = createServerHotChannel()
}

return new RunnableDevEnvironment(name, config, context)
}

export interface RunnableDevEnvironmentContext
extends Omit<DevEnvironmentContext, 'hot'> {
runner?: (environment: RunnableDevEnvironment) => ModuleRunner
hot?: false | HotChannel
}

export function isRunnableDevEnvironment(
environment: Environment,
): environment is RunnableDevEnvironment {
return environment instanceof RunnableDevEnvironment
}

class RunnableDevEnvironment extends DevEnvironment {
private _runner: ModuleRunner | undefined
private _runnerFactory:
| ((environment: RunnableDevEnvironment) => ModuleRunner)
| undefined

constructor(
name: string,
config: ResolvedConfig,
context: RunnableDevEnvironmentContext,
) {
super(name, config, context as DevEnvironmentContext)
this._runnerFactory = context.runner
}

get runner(): ModuleRunner {
if (this._runner) {
return this._runner
}
if (this._runnerFactory) {
this._runner = this._runnerFactory(this)
return this._runner
}
return createServerModuleRunner(this)
}
}

export type { RunnableDevEnvironment }
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('running module runner inside a worker', () => {
dev: {
createEnvironment: (name, config) => {
return new DevEnvironment(name, config, {
runner: {
remoteRunner: {
transport: new RemoteEnvironmentTransport({
send: (data) => worker.postMessage(data),
onMessage: (handler) => worker.on('message', handler),
Expand Down
36 changes: 25 additions & 11 deletions playground/hmr-ssr/__tests__/hmr-ssr.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ import {
test,
vi,
} from 'vitest'
import type { InlineConfig, ViteDevServer } from 'vite'
import { createServer, createServerModuleRunner } from 'vite'
import type { InlineConfig, RunnableDevEnvironment, ViteDevServer } from 'vite'
import {
createRunnableDevEnvironment,
createServer,
createServerHotChannel,
createServerModuleRunner,
} from 'vite'
import type { ModuleRunner } from 'vite/module-runner'
import {
addFile,
Expand Down Expand Up @@ -1036,6 +1041,10 @@ async function setupModuleRunner(

globalThis.__HMR__ = initHmrState as any

const logger = new HMRMockLogger()
// @ts-expect-error not typed for HMR
globalThis.log = (...msg) => logger.log(...msg)

server = await createServer({
configFile: resolve(testDir, 'vite.config.ts'),
root: testDir,
Expand All @@ -1053,6 +1062,19 @@ async function setupModuleRunner(
},
preTransformRequests: false,
},
environments: {
ssr: {
dev: {
createEnvironment(name, config) {
return createRunnableDevEnvironment(name, config, {
runner: (env) =>
createServerModuleRunner(env, { hmr: { logger } }),
hot: createServerHotChannel(),
})
},
},
},
},
optimizeDeps: {
disabled: true,
noDiscovery: true,
Expand All @@ -1061,15 +1083,7 @@ async function setupModuleRunner(
...serverOptions,
})

const logger = new HMRMockLogger()
// @ts-expect-error not typed for HMR
globalThis.log = (...msg) => logger.log(...msg)

runner = createServerModuleRunner(server.environments.ssr, {
hmr: {
logger,
},
})
runner = (server.environments.ssr as RunnableDevEnvironment).runner

await waitForWatcher(server, waitForFile)

Expand Down