Skip to content

Commit 8b3ab07

Browse files
authored
feat: experimental Vite Runtime API (#12165)
1 parent 734a9e3 commit 8b3ab07

File tree

143 files changed

+5122
-36
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

143 files changed

+5122
-36
lines changed

docs/.vitepress/config.ts

+4
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,10 @@ export default defineConfig({
279279
text: 'JavaScript API',
280280
link: '/guide/api-javascript',
281281
},
282+
{
283+
text: 'Vite Runtime API',
284+
link: '/guide/api-vite-runtime',
285+
},
282286
{
283287
text: 'Config Reference',
284288
link: '/config/',

docs/guide/api-vite-runtime.md

+234
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
# Vite Runtime API
2+
3+
:::warning Low-level API
4+
This API was introduced in Vite 5.1 as an experimental feature. It was added to [gather feedback](https://github.com/vitejs/vite/discussions/15774). There will probably be breaking changes to it in Vite 5.2, so make sure to pin the Vite version to `~5.1.0` when using it. This is a low-level API meant for library and framework authors. If your goal is to create an application, make sure to check out the higher-level SSR plugins and tools at [Awesome Vite SSR section](https://github.com/vitejs/awesome-vite#ssr) first.
5+
:::
6+
7+
The "Vite Runtime" is a tool that allows running any code by processing it with Vite plugins first. It is different from `server.ssrLoadModule` because the runtime implementation is decoupled from the server. This allows library and framework authors to implement their own layer of communication between the server and the runtime.
8+
9+
One of the goals of this feature is to provide a customizable API to process and run the code. Vite provides enough tools to use Vite Runtime out of the box, but users can build upon it if their needs do not align with Vite's built-in implementation.
10+
11+
All APIs can be imported from `vite/runtime` unless stated otherwise.
12+
13+
## `ViteRuntime`
14+
15+
**Type Signature:**
16+
17+
```ts
18+
export class ViteRuntime {
19+
constructor(
20+
public options: ViteRuntimeOptions,
21+
public runner: ViteModuleRunner,
22+
private debug?: ViteRuntimeDebugger,
23+
) {}
24+
/**
25+
* URL to execute. Accepts file path, server path, or id relative to the root.
26+
*/
27+
public async executeUrl<T = any>(url: string): Promise<T>
28+
/**
29+
* Entry point URL to execute. Accepts file path, server path or id relative to the root.
30+
* In the case of a full reload triggered by HMR, this is the module that will be reloaded.
31+
* If this method is called multiple times, all entry points will be reloaded one at a time.
32+
*/
33+
public async executeEntrypoint<T = any>(url: string): Promise<T>
34+
/**
35+
* Clear all caches including HMR listeners.
36+
*/
37+
public clearCache(): void
38+
/**
39+
* Clears all caches, removes all HMR listeners, and resets source map support.
40+
* This method doesn't stop the HMR connection.
41+
*/
42+
public async destroy(): Promise<void>
43+
/**
44+
* Returns `true` if the runtime has been destroyed by calling `destroy()` method.
45+
*/
46+
public isDestroyed(): boolean
47+
}
48+
```
49+
50+
::: tip Advanced Usage
51+
If you are just migrating from `server.ssrLoadModule` and want to support HMR, consider using [`createViteRuntime`](#createviteruntime) instead.
52+
:::
53+
54+
The `ViteRuntime` class requires `root` and `fetchModule` options when initiated. Vite exposes `ssrFetchModule` on the [`server`](/guide/api-javascript) instance for easier integration with Vite SSR. Vite also exports `fetchModule` from its main entry point - it doesn't make any assumptions about how the code is running unlike `ssrFetchModule` that expects the code to run using `new Function`. This can be seen in source maps that these functions return.
55+
56+
Runner in `ViteRuntime` is responsible for executing the code. Vite exports `ESModulesRunner` out of the box, it uses `new AsyncFunction` to run the code. You can provide your own implementation if your JavaScript runtime doesn't support unsafe evaluation.
57+
58+
The two main methods that runtime exposes are `executeUrl` and `executeEntrypoint`. The only difference between them is that all modules executed by `executeEntrypoint` will be reexecuted if HMR triggers `full-reload` event. Be aware that Vite Runtime doesn't update `exports` object when this happens (it overrides it), you would need to run `executeUrl` or get the module from `moduleCache` again if you rely on having the latest `exports` object.
59+
60+
**Example Usage:**
61+
62+
```js
63+
import { ViteRuntime, ESModulesRunner } from 'vite/runtime'
64+
import { root, fetchModule } from './rpc-implementation.js'
65+
66+
const runtime = new ViteRuntime(
67+
{
68+
root,
69+
fetchModule,
70+
// you can also provide hmr.connection to support HMR
71+
},
72+
new ESModulesRunner(),
73+
)
74+
75+
await runtime.executeEntrypoint('/src/entry-point.js')
76+
```
77+
78+
## `ViteRuntimeOptions`
79+
80+
```ts
81+
export interface ViteRuntimeOptions {
82+
/**
83+
* Root of the project
84+
*/
85+
root: string
86+
/**
87+
* A method to get the information about the module.
88+
* For SSR, Vite exposes `server.ssrFetchModule` function that you can use here.
89+
* For other runtime use cases, Vite also exposes `fetchModule` from its main entry point.
90+
*/
91+
fetchModule: FetchFunction
92+
/**
93+
* Configure how source maps are resolved. Prefers `node` if `process.setSourceMapsEnabled` is available.
94+
* Otherwise it will use `prepareStackTrace` by default which overrides `Error.prepareStackTrace` method.
95+
* You can provide an object to configure how file contents and source maps are resolved for files that were not processed by Vite.
96+
*/
97+
sourcemapInterceptor?:
98+
| false
99+
| 'node'
100+
| 'prepareStackTrace'
101+
| InterceptorOptions
102+
/**
103+
* Disable HMR or configure HMR options.
104+
*/
105+
hmr?:
106+
| false
107+
| {
108+
/**
109+
* Configure how HMR communicates between the client and the server.
110+
*/
111+
connection: HMRRuntimeConnection
112+
/**
113+
* Configure HMR logger.
114+
*/
115+
logger?: false | HMRLogger
116+
}
117+
/**
118+
* Custom module cache. If not provided, it creates a separate module cache for each ViteRuntime instance.
119+
*/
120+
moduleCache?: ModuleCacheMap
121+
}
122+
```
123+
124+
## `ViteModuleRunner`
125+
126+
**Type Signature:**
127+
128+
```ts
129+
export interface ViteModuleRunner {
130+
/**
131+
* Run code that was transformed by Vite.
132+
* @param context Function context
133+
* @param code Transformed code
134+
* @param id ID that was used to fetch the module
135+
*/
136+
runViteModule(
137+
context: ViteRuntimeModuleContext,
138+
code: string,
139+
id: string,
140+
): Promise<any>
141+
/**
142+
* Run externalized module.
143+
* @param file File URL to the external module
144+
*/
145+
runExternalModule(file: string): Promise<any>
146+
}
147+
```
148+
149+
Vite exports `ESModulesRunner` that implements this interface by default. It uses `new AsyncFunction` to run 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 `server.ssrFetchModule`. If your runner implementation doesn't have this constraint, you should use `fetchModule` (exported from `vite`) directly.
150+
151+
## HMRRuntimeConnection
152+
153+
**Type Signature:**
154+
155+
```ts
156+
export interface HMRRuntimeConnection {
157+
/**
158+
* Checked before sending messages to the client.
159+
*/
160+
isReady(): boolean
161+
/**
162+
* Send message to the client.
163+
*/
164+
send(message: string): void
165+
/**
166+
* Configure how HMR is handled when this connection triggers an update.
167+
* This method expects that connection will start listening for HMR updates and call this callback when it's received.
168+
*/
169+
onUpdate(callback: (payload: HMRPayload) => void): void
170+
}
171+
```
172+
173+
This interface defines how HMR communication is established. Vite exports `ServerHMRConnector` from the main entry point to support HMR during Vite SSR. The `isReady` and `send` methods are usually called when the custom event is triggered (like, `import.meta.hot.send("my-event")`).
174+
175+
`onUpdate` is called only once when the new runtime is initiated. It passed down a method that should be called when connection triggers the HMR event. The implementation depends on the type of connection (as an example, it can be `WebSocket`/`EventEmitter`/`MessageChannel`), but it usually looks something like this:
176+
177+
```js
178+
function onUpdate(callback) {
179+
this.connection.on('hmr', (event) => callback(event.data))
180+
}
181+
```
182+
183+
The callback is queued and it will wait for the current update to be resolved before processing the next update. Unlike the browser implementation, HMR updates in Vite Runtime wait until all listeners (like, `vite:beforeUpdate`/`vite:beforeFullReload`) are finished before updating the modules.
184+
185+
## `createViteRuntime`
186+
187+
**Type Signature:**
188+
189+
```ts
190+
async function createViteRuntime(
191+
server: ViteDevServer,
192+
options?: MainThreadRuntimeOptions,
193+
): Promise<ViteRuntime>
194+
```
195+
196+
**Example Usage:**
197+
198+
```js
199+
import { createServer } from 'vite'
200+
201+
const __dirname = fileURLToPath(new URL('.', import.meta.url))
202+
203+
;(async () => {
204+
const server = await createServer({
205+
root: __dirname,
206+
})
207+
await server.listen()
208+
209+
const runtime = await createViteRuntime(server)
210+
await runtime.executeEntrypoint('/src/entry-point.js')
211+
})()
212+
```
213+
214+
This method serves as an easy replacement for `server.ssrLoadModule`. Unlike `ssrLoadModule`, `createViteRuntime` provides HMR support out of the box. You can pass down [`options`](#mainthreadruntimeoptions) to customize how SSR runtime behaves to suit your needs.
215+
216+
## `MainThreadRuntimeOptions`
217+
218+
```ts
219+
export interface MainThreadRuntimeOptions
220+
extends Omit<ViteRuntimeOptions, 'root' | 'fetchModule' | 'hmr'> {
221+
/**
222+
* Disable HMR or configure HMR logger.
223+
*/
224+
hmr?:
225+
| false
226+
| {
227+
logger?: false | HMRLogger
228+
}
229+
/**
230+
* Provide a custom module runner. This controls how the code is executed.
231+
*/
232+
runner?: ViteModuleRunner
233+
}
234+
```

docs/guide/ssr.md

+10-4
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,16 @@ app.use('*', async (req, res, next) => {
125125
// preambles from @vitejs/plugin-react
126126
template = await vite.transformIndexHtml(url, template)
127127

128-
// 3. Load the server entry. ssrLoadModule automatically transforms
128+
// 3a. Load the server entry. ssrLoadModule automatically transforms
129129
// ESM source code to be usable in Node.js! There is no bundling
130130
// required, and provides efficient invalidation similar to HMR.
131131
const { render } = await vite.ssrLoadModule('/src/entry-server.js')
132+
// 3b. Since Vite 5.1, you can use createViteRuntime API instead.
133+
// It fully supports HMR and works in a simillar way to ssrLoadModule
134+
// More advanced use case would be creating a runtime in a separate
135+
// thread or even a different machine using ViteRuntime class
136+
const runtime = await vite.createViteRuntime(server)
137+
const { render } = await runtime.executeEntrypoint('/src/entry-server.js')
132138

133139
// 4. render the app HTML. This assumes entry-server.js's exported
134140
// `render` function calls appropriate framework SSR APIs,
@@ -163,7 +169,7 @@ The `dev` script in `package.json` should also be changed to use the server scri
163169
To ship an SSR project for production, we need to:
164170
165171
1. Produce a client build as normal;
166-
2. Produce an SSR build, which can be directly loaded via `import()` so that we don't have to go through Vite's `ssrLoadModule`;
172+
2. Produce an SSR build, which can be directly loaded via `import()` so that we don't have to go through Vite's `ssrLoadModule` or `runtime.executeEntrypoint`;
167173
168174
Our scripts in `package.json` will look like this:
169175
@@ -181,9 +187,9 @@ Note the `--ssr` flag which indicates this is an SSR build. It should also speci
181187
182188
Then, in `server.js` we need to add some production specific logic by checking `process.env.NODE_ENV`:
183189
184-
- Instead of reading the root `index.html`, use the `dist/client/index.html` as the template instead, since it contains the correct asset links to the client build.
190+
- Instead of reading the root `index.html`, use the `dist/client/index.html` as the template, since it contains the correct asset links to the client build.
185191
186-
- Instead of `await vite.ssrLoadModule('/src/entry-server.js')`, use `import('./dist/server/entry-server.js')` instead (this file is the result of the SSR build).
192+
- Instead of `await vite.ssrLoadModule('/src/entry-server.js')` or `await runtime.executeEntrypoint('/src/entry-server.js')`, use `import('./dist/server/entry-server.js')` (this file is the result of the SSR build).
187193
188194
- Move the creation and all usage of the `vite` dev server behind dev-only conditional branches, then add static file serving middlewares to serve files from `dist/client`.
189195

packages/vite/package.json

+12-1
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,23 @@
3232
"./client": {
3333
"types": "./client.d.ts"
3434
},
35+
"./runtime": {
36+
"types": "./dist/node/runtime.d.ts",
37+
"import": "./dist/node/runtime.js"
38+
},
3539
"./dist/client/*": "./dist/client/*",
3640
"./types/*": {
3741
"types": "./types/*"
3842
},
3943
"./package.json": "./package.json"
4044
},
45+
"typesVersions": {
46+
"*": {
47+
"runtime": [
48+
"dist/node/runtime.d.ts"
49+
]
50+
}
51+
},
4152
"files": [
4253
"bin",
4354
"dist",
@@ -64,7 +75,7 @@
6475
"build": "rimraf dist && run-s build-bundle build-types",
6576
"build-bundle": "rollup --config rollup.config.ts --configPlugin typescript",
6677
"build-types": "run-s build-types-temp build-types-roll build-types-check",
67-
"build-types-temp": "tsc --emitDeclarationOnly --outDir temp/node -p src/node",
78+
"build-types-temp": "tsc --emitDeclarationOnly --outDir temp -p src/node",
6879
"build-types-roll": "rollup --config rollup.dts.config.ts --configPlugin typescript && rimraf temp",
6980
"build-types-check": "tsc --project tsconfig.check.json",
7081
"typecheck": "tsc --noEmit",

packages/vite/rollup.config.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ function createNodeConfig(isProduction: boolean) {
153153
index: path.resolve(__dirname, 'src/node/index.ts'),
154154
cli: path.resolve(__dirname, 'src/node/cli.ts'),
155155
constants: path.resolve(__dirname, 'src/node/constants.ts'),
156+
runtime: path.resolve(__dirname, 'src/node/ssr/runtime/index.ts'),
156157
},
157158
output: {
158159
...sharedNodeOptions.output,
@@ -299,7 +300,12 @@ const __require = require;
299300
name: 'cjs-chunk-patch',
300301
renderChunk(code, chunk) {
301302
if (!chunk.fileName.includes('chunks/dep-')) return
302-
303+
// don't patch runtime utils chunk because it should stay lightweight and we know it doesn't use require
304+
if (
305+
chunk.name === 'utils' &&
306+
chunk.moduleIds.some((id) => id.endsWith('/ssr/runtime/utils.ts'))
307+
)
308+
return
303309
const match = code.match(/^(?:import[\s\S]*?;\s*)+/)
304310
const index = match ? match.index! + match[0].length : 0
305311
const s = new MagicString(code)

0 commit comments

Comments
 (0)