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: experimental Vite Runtime API #12165

Merged
merged 34 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3ba7436
feat: add vite-node into the core
sheremet-va Jan 19, 2024
c0de2ff
fix: normalize entry url before calling fetchModule
sheremet-va Jan 20, 2024
2423810
refactor: expose more types, fix caching with the query issue, requir…
sheremet-va Jan 21, 2024
a91fc5a
chore: export HMRRuntimeConnection
sheremet-va Jan 21, 2024
8f4b6d1
refactor: remove cleanup function from onUpdate
sheremet-va Jan 22, 2024
090246e
refactor: remove processImport from the public API
sheremet-va Jan 22, 2024
95434b4
test: add test for HMR on files imported with queries
sheremet-va Jan 22, 2024
a6fd97b
feat: support source map
sheremet-va Jan 22, 2024
1475aeb
chore: add check for types chunk
sheremet-va Jan 22, 2024
f83f0e3
test: remove only
sheremet-va Jan 22, 2024
3205c54
chore: change default to use node interceptor if available
sheremet-va Jan 22, 2024
d8fce47
refactor: expose "fetchModule" via a "vite" entypoint, improve types
sheremet-va Jan 25, 2024
2949d96
fix: allow several runtimes to influece source maps
sheremet-va Jan 29, 2024
bcfe993
fix: externalize network urls by default
sheremet-va Jan 29, 2024
aa5354e
chore: cleanup
sheremet-va Jan 29, 2024
db80f51
Merge branch 'main' into feat/vite-node-core
sheremet-va Jan 29, 2024
3f044b0
chore: cleanup
sheremet-va Jan 29, 2024
0b31ff7
refactor: add more jsdocs to vite runtime, add isDestroyed
sheremet-va Jan 30, 2024
84283ff
docs: add documentation about vite runtime
sheremet-va Jan 30, 2024
0e0263e
chore: typo
patak-dev Jan 30, 2024
ff81b02
chore: typo
patak-dev Jan 30, 2024
fb46227
chore: typo
patak-dev Jan 30, 2024
bab1559
chore: wording
patak-dev Jan 30, 2024
34f2363
chore: wording
patak-dev Jan 30, 2024
c58fbe0
chore: typo
patak-dev Jan 30, 2024
cf38151
chore: typo
patak-dev Jan 30, 2024
9764b51
chore: warning about potential changes in Vite 5.2
patak-dev Jan 31, 2024
3e18bb7
chore: wording
patak-dev Jan 31, 2024
294a50a
test: correct hmr ports
sheremet-va Jan 31, 2024
fea94b5
test: fix ports
sheremet-va Jan 31, 2024
0573c0d
chore: review feedback
sheremet-va Jan 31, 2024
4c0d3fa
docs: add experimental jsdoc to runtime APIs available from "vite" en…
sheremet-va Jan 31, 2024
1db9c01
refactor: update source map folder name
sheremet-va Feb 1, 2024
f7c3c25
chore: add a link to the discussion in runtime docs
sheremet-va Feb 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,10 @@ export default defineConfig({
text: 'JavaScript API',
link: '/guide/api-javascript',
},
{
text: 'Vite Runtime API',
link: '/guide/api-vite-runtime',
},
{
text: 'Config Reference',
link: '/config/',
Expand Down
241 changes: 241 additions & 0 deletions docs/guide/api-vite-runtime.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
# Vite Runtime API

:::warning Low-level API
This API was introduced in Vite 5.1 as an experimental feature. It was a added to gather feedback. 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.
:::

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.

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.
Copy link
Member

@patak-dev patak-dev Jan 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will be helpful to have some links to custom implementations like @sapphi-red's one. At least during the experimental phase as it is a great resource to see the API in action.

We can add these links in another PR though, once https://github.com/sapphi-red/vite-envs/tree/use-vite-runtime is merged into main in vite-envs

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, sounds like a good idea.


All APIs can be imported from `vite/runtime` unless stated otherwise.

## `ViteRuntime`

**Type Signature:**

```ts
export class ViteRuntime {
constructor(
public options: ViteRuntimeOptions,
public runner: ViteModuleRunner,
private debug?: ViteRuntimeDebugger,
) {}
/**
* URL to execute. Accepts file path, server path, or id relative to the root.
*/
public async executeUrl<T = any>(url: string): Promise<T>
/**
* Entry point URL to execute. Accepts file path, server path or id relative to the root.
* In the case of a full reload triggered by HMR, this is the module that will be reloaded.
* If this method is called multiple times, all entry points will be reloaded one at a time.
*/
public async executeEntrypoint<T = any>(url: string): Promise<T>
/**
* Clear all caches including HMR listeners.
*/
public clearCache(): void
/**
* Clears all caches, removes all HMR listeners, and resets source map support.
* This method doesn't stop the HMR connection.
*/
public async destroy(): Promise<void>
/**
* Returns `true` if the runtime has been destroyed by calling `destroy()` method.
*/
public isDestroyed(): boolean
}
```

::: tip Advanced Usage
If you are just migrating from `server.ssrLoadModule` and want to support HMR, consider using [`createViteRuntime`](#createviteruntime) instead.
:::

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.

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.

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.

**Example Usage:**

```js
import { ViteRuntime, ESModulesRunner } from 'vite/runtime'
import { root, fetchModule } from './rpc-implementation.js'

const runtime = new ViteRuntime(
{
root,
fetchModule,
// you can also provide hmr.connection to support HMR
},
new ESModulesRunner(),
)

await runtime.executeEntrypoint('/src/entry-point.js')
```

## `ViteRuntimeOptions`

```ts
export interface ViteRuntimeOptions {
/**
* Root of the project
*/
root: string
/**
* A method to get the information about the module.
* For SSR, Vite exposes `server.ssrFetchModule` function that you can use here.
* For other runtime use cases, Vite also exposes `fetchModule` from its main entry point.
*/
fetchModule: FetchFunction
/**
* Custom environment variables available on `import.meta.env`. This doesn't modify the actual `process.env`.
*/
environmentVariables?: Record<string, any>
/**
* Configure how source maps are resolved. Prefers `node` if `process.setSourceMapsEnabled` is available.
* Otherwise it will use `prepareStackTrace` by default.
*/
sourcemapInterceptor?:
| false
| 'node'
| 'prepareStackTrace'
| InterceptorOptions
/**
* Disable HMR or configure HMR options.
*/
hmr?:
| false
| {
/**
* Configure how HMR communicates between the client and the server.
*/
connection: HMRRuntimeConnection
/**
* Configure HMR logger.
*/
logger?: false | HMRLogger
}
/**
* Custom module cache. If not provided, it creates a separate module cache for each ViteRuntime instance.
*/
moduleCache?: ModuleCacheMap
/**
* Resolved modules that will be returned instead of executing the code.
*/
requestStubs?: Record<string, any>
}
```

## `ViteModuleRunner`

**Type Signature:**

```ts
export interface ViteModuleRunner {
/**
* Run code that was transformed by Vite.
* @param context Function context
* @param code Transformed code
* @param id ID that was used to fetch the module
*/
runViteModule(
context: ViteRuntimeModuleContext,
code: string,
id: string,
): Promise<any>
/**
* Run externalized module.
* @param file File URL to the external module
*/
runExternalModule(file: string): Promise<any>
}
```

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.

## HMRRuntimeConnection

**Type Signature:**

```ts
export interface HMRRuntimeConnection {
/**
* Checked before sending messages to the client.
*/
isReady(): boolean
/**
* Send message to the client.
*/
send(message: string): void
/**
* Configure how HMR is handled when this connection triggers an update.
* This method expects that connection will start listening for HMR updates and call this callback when it's received.
*/
onUpdate(callback: (payload: HMRPayload) => void): void
}
```

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")`).

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

```js
function onUpdate(callback) {
this.connection.on('hmr', (event) => callback(event.data))
}
```

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.

## `createViteRuntime`

**Type Signature:**

```ts
async function createViteRuntime(
server: ViteDevServer,
options?: MainThreadRuntimeOptions,
): Promise<ViteRuntime>
```

**Example Usage:**

```js
import { createServer } from 'vite'

const __dirname = fileURLToPath(new URL('.', import.meta.url))

;(async () => {
const server = await createServer({
root: __dirname,
})
await server.listen()

const runtime = await createViteRuntime(server)
await runtime.executeEntrypoint('/src/entry-point.js')
})()
```

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.

## `MainThreadRuntimeOptions`

```ts
export interface MainThreadRuntimeOptions
extends Omit<ViteRuntimeOptions, 'root' | 'fetchModule' | 'hmr'> {
/**
* Disable HMR or configure HMR logger.
*/
hmr?:
| false
| {
logger?: false | HMRLogger
}
/**
* Provide a custom module runner. This controls how the code is executed.
*/
runner?: ViteModuleRunner
}
```
14 changes: 10 additions & 4 deletions docs/guide/ssr.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,16 @@ app.use('*', async (req, res, next) => {
// preambles from @vitejs/plugin-react
template = await vite.transformIndexHtml(url, template)

// 3. Load the server entry. ssrLoadModule automatically transforms
// 3a. Load the server entry. ssrLoadModule automatically transforms
// ESM source code to be usable in Node.js! There is no bundling
// required, and provides efficient invalidation similar to HMR.
const { render } = await vite.ssrLoadModule('/src/entry-server.js')
// 3b. Since Vite 5.1, you can use createViteRuntime API instead.
// It fully supports HMR and works in a simillar way to ssrLoadModule
// More advanced use case would be creating a runtime in a separate
// thread or even a different machine using ViteRuntime class
const runtime = await vite.createViteRuntime(server)
const { render } = await runtime.executeEntrypoint('/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 @@ -163,7 +169,7 @@ The `dev` script in `package.json` should also be changed to use the server scri
To ship an SSR project for production, we need to:

1. Produce a client build as normal;
2. Produce an SSR build, which can be directly loaded via `import()` so that we don't have to go through Vite's `ssrLoadModule`;
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`;

Our scripts in `package.json` will look like this:

Expand All @@ -181,9 +187,9 @@ Note the `--ssr` flag which indicates this is an SSR build. It should also speci

Then, in `server.js` we need to add some production specific logic by checking `process.env.NODE_ENV`:

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

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

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

Expand Down
13 changes: 12 additions & 1 deletion packages/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,23 @@
"./client": {
"types": "./client.d.ts"
},
"./runtime": {
"types": "./dist/node/runtime.d.ts",
"import": "./dist/node/runtime.js"
},
"./dist/client/*": "./dist/client/*",
"./types/*": {
"types": "./types/*"
},
"./package.json": "./package.json"
},
"typesVersions": {
"*": {
"runtime": [
"dist/node/runtime.d.ts"
]
}
},
"files": [
"bin",
"dist",
Expand All @@ -64,7 +75,7 @@
"build": "rimraf dist && run-s build-bundle build-types",
"build-bundle": "rollup --config rollup.config.ts --configPlugin typescript",
"build-types": "run-s build-types-temp build-types-roll build-types-check",
"build-types-temp": "tsc --emitDeclarationOnly --outDir temp/node -p src/node",
"build-types-temp": "tsc --emitDeclarationOnly --outDir temp -p src/node",
"build-types-roll": "rollup --config rollup.dts.config.ts --configPlugin typescript && rimraf temp",
"build-types-check": "tsc --project tsconfig.check.json",
"typecheck": "tsc --noEmit",
Expand Down
8 changes: 7 additions & 1 deletion packages/vite/rollup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ function createNodeConfig(isProduction: boolean) {
index: path.resolve(__dirname, 'src/node/index.ts'),
cli: path.resolve(__dirname, 'src/node/cli.ts'),
constants: path.resolve(__dirname, 'src/node/constants.ts'),
runtime: path.resolve(__dirname, 'src/node/ssr/runtime/index.ts'),
},
output: {
...sharedNodeOptions.output,
Expand Down Expand Up @@ -299,7 +300,12 @@ const __require = require;
name: 'cjs-chunk-patch',
renderChunk(code, chunk) {
if (!chunk.fileName.includes('chunks/dep-')) return

// don't patch runtime utils chunk because it should stay lightweight and we know it doesn't use require
if (
chunk.name === 'utils' &&
chunk.moduleIds.some((id) => id.endsWith('/ssr/runtime/utils.ts'))
)
return
const match = code.match(/^(?:import[\s\S]*?;\s*)+/)
const index = match ? match.index! + match[0].length : 0
const s = new MagicString(code)
Expand Down
Loading
Loading