diff --git a/docs/guide/api-environment.md b/docs/guide/api-environment.md index e1b3c0c4bebf6b..19efb8a8d0a0ca 100644 --- a/docs/guide/api-environment.md +++ b/docs/guide/api-environment.md @@ -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') +} +``` + 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. @@ -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 { @@ -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 }, @@ -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 @@ -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, @@ -310,7 +321,7 @@ function createWorkerdDevEnvironment(name: string, config: ResolvedConfig, conte ...context.options, }, hot, - runner: { + remoteRunner: { transport, }, }) @@ -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 }) @@ -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), diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index af3a07c242899d..ae5d1bca82fb89 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -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' @@ -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) { diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index 485e9db43be60e..5aaff7552740ba 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -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, diff --git a/packages/vite/src/node/server/environment.ts b/packages/vite/src/node/server/environment.ts index 3bedc2bdb3ba2d..9bbeb1f8b3f5b5 100644 --- a/packages/vite/src/node/server/environment.ts +++ b/packages/vite/src/node/server/environment.ts @@ -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' @@ -36,7 +35,8 @@ import { isWebSocketServer } from './ws' export interface DevEnvironmentContext { hot: false | HotChannel options?: EnvironmentOptions - runner?: FetchModuleOptions & { + remoteRunner?: { + inlineSourceMap?: boolean transport?: RemoteEnvironmentTransport } depsOptimizer?: DepsOptimizer @@ -50,7 +50,7 @@ export class DevEnvironment extends BaseEnvironment { /** * @internal */ - _ssrRunnerOptions: FetchModuleOptions | undefined + _remoteRunnerOptions: DevEnvironmentContext['remoteRunner'] get pluginContainer(): EnvironmentPluginContainer { if (!this._pluginContainer) @@ -117,8 +117,8 @@ export class DevEnvironment extends BaseEnvironment { this._crawlEndFinder = setupOnCrawlEnd() - 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, { @@ -166,7 +166,7 @@ export class DevEnvironment extends BaseEnvironment { options?: FetchFunctionOptions, ): Promise { return fetchModule(this, id, importer, { - ...this._ssrRunnerOptions, + ...this._remoteRunnerOptions, ...options, }) } diff --git a/packages/vite/src/node/server/environments/nodeEnvironment.ts b/packages/vite/src/node/server/environments/nodeEnvironment.ts deleted file mode 100644 index 9e63c814e7bcf3..00000000000000 --- a/packages/vite/src/node/server/environments/nodeEnvironment.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { ResolvedConfig } from '../../config' -import type { DevEnvironmentContext } from '../environment' -import { DevEnvironment } from '../environment' - -export function createNodeDevEnvironment( - name: string, - config: ResolvedConfig, - context: DevEnvironmentContext, -): DevEnvironment { - if (context.hot == null) { - throw new Error( - '`hot` is a required option. Either explicitly opt out of HMR by setting `hot: false` or provide a hot channel.', - ) - } - - return new DevEnvironment(name, config, context) -} diff --git a/packages/vite/src/node/server/environments/runnableEnvironment.ts b/packages/vite/src/node/server/environments/runnableEnvironment.ts new file mode 100644 index 00000000000000..0675f48b1accaa --- /dev/null +++ b/packages/vite/src/node/server/environments/runnableEnvironment.ts @@ -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 { + 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 } diff --git a/packages/vite/src/node/ssr/runtime/__tests__/server-worker-runner.spec.ts b/packages/vite/src/node/ssr/runtime/__tests__/server-worker-runner.spec.ts index d756d57273bbf4..afdee8263cebcb 100644 --- a/packages/vite/src/node/ssr/runtime/__tests__/server-worker-runner.spec.ts +++ b/packages/vite/src/node/ssr/runtime/__tests__/server-worker-runner.spec.ts @@ -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), diff --git a/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts b/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts index b71d77289158ca..f6684242a2ea81 100644 --- a/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts +++ b/playground/hmr-ssr/__tests__/hmr-ssr.spec.ts @@ -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, @@ -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, @@ -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, @@ -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)