-
-
Notifications
You must be signed in to change notification settings - Fork 6.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
docs: refactor environment api docs (#18316)
Co-authored-by: sapphi-red <[email protected]> Co-authored-by: Vladimir <[email protected]>
- Loading branch information
1 parent
0fe95d4
commit 896d6e3
Showing
6 changed files
with
1,075 additions
and
918 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,286 @@ | ||
# Environment API for Frameworks | ||
|
||
:::warning Experimental | ||
Initial work for this API was introduced in Vite 5.1 with the name "Vite Runtime API". This guide describes a revised API, renamed to Environment API. This API will be released in Vite 6 as experimental. You can already test it in the latest `[email protected]` version. | ||
|
||
Resources: | ||
|
||
- [Feedback discussion](https://github.com/vitejs/vite/discussions/16358) where we are gathering feedback about the new APIs. | ||
- [Environment API PR](https://github.com/vitejs/vite/pull/16471) where the new API were implemented and reviewed. | ||
|
||
Please share with us your feedback as you test the proposal. | ||
::: | ||
|
||
## Environments and frameworks | ||
|
||
The implicit `ssr` environment and other non-client environments use a `RunnableDevEnvironment` by default during dev. While this requires the runtime to be the same with the one the Vite server is running in, this works similarly with `ssrLoadModule` and allows frameworks to migrate and enable HMR for their SSR dev story. You can guard any runnable environment with an `isRunnableDevEnvironment` function. | ||
|
||
```ts | ||
export class RunnableDevEnvironment extends DevEnvironment { | ||
public readonly runner: ModuleRunner | ||
} | ||
|
||
class ModuleRunner { | ||
/** | ||
* URL to execute. Accepts file path, server path, or id relative to the root. | ||
* Returns an instantiated module (same as in ssrLoadModule) | ||
*/ | ||
public async import(url: string): Promise<Record<string, any>> | ||
/** | ||
* Other ModuleRunner methods... | ||
*/ | ||
} | ||
|
||
if (isRunnableDevEnvironment(server.environments.ssr)) { | ||
await server.environments.ssr.runner.import('/entry-point.js') | ||
} | ||
``` | ||
|
||
:::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. | ||
::: | ||
|
||
## Default `RunnableDevEnvironment` | ||
|
||
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 } from 'vite' | ||
|
||
const server = await createServer({ | ||
server: { middlewareMode: true }, | ||
appType: 'custom', | ||
environments: { | ||
server: { | ||
// by default, the modules are run in the same process as the vite dev server during dev | ||
}, | ||
}, | ||
}) | ||
|
||
// 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 | ||
|
||
// 1. Read index.html | ||
let template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8') | ||
|
||
// 2. Apply Vite HTML transforms. This injects the Vite HMR client, | ||
// and also applies HTML transforms from Vite plugins, e.g. global | ||
// preambles from @vitejs/plugin-react | ||
template = await server.transformIndexHtml(url, template) | ||
|
||
// 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 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, | ||
// e.g. ReactDOMServer.renderToString() | ||
const appHtml = await render(url) | ||
|
||
// 5. Inject the app-rendered HTML into the template. | ||
const html = template.replace(`<!--ssr-outlet-->`, appHtml) | ||
|
||
// 6. Send the rendered HTML back. | ||
res.status(200).set({ 'Content-Type': 'text/html' }).end(html) | ||
}) | ||
``` | ||
|
||
## Runtime agnostic SSR | ||
|
||
Since the `RunnableDevEnvironment` can only be used to run the code in the same runtime as the Vite server, it requires a runtime that can run the Vite Server (a runtime that is compatible with Node.js). This means that you will need to use the raw `DevEnvironment` to make it runtime agnostic. | ||
|
||
:::info `FetchableDevEnvironment` proposal | ||
|
||
The initial proposal had a `run` method on the `DevEnvironment` class 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. At the moment, we are looking for feedback on [the `FetchableDevEnvironment` proposal](https://github.com/vitejs/vite/discussions/18191). | ||
|
||
::: | ||
|
||
`RunnableDevEnvironment` has a `runner.import` function that returns the value of the module. But this function is not available in the raw `DevEnvironment` and requires the code using the Vite's APIs and the user modules to be decoupled. | ||
|
||
For example, the following example uses the value of the user module from the code using the Vite's APIs: | ||
|
||
```ts | ||
// code using the Vite's APIs | ||
import { createServer } from 'vite' | ||
|
||
const server = createServer() | ||
const ssrEnvironment = server.environment.ssr | ||
const input = {} | ||
|
||
const { createHandler } = await ssrEnvironment.runner.import('./entrypoint.js') | ||
const handler = createHandler(input) | ||
const response = handler(new Request('/')) | ||
|
||
// ------------------------------------- | ||
// ./entrypoint.js | ||
export function createHandler(input) { | ||
return function handler(req) { | ||
return new Response('hello') | ||
} | ||
} | ||
``` | ||
|
||
If your code can run in the same runtime as the user modules (i.e., it does not rely on Node.js-specific APIs), you can use a virtual module. This approach eliminates the need to access the value from the code using Vite's APIs. | ||
|
||
```ts | ||
// code using the Vite's APIs | ||
import { createServer } from 'vite' | ||
|
||
const server = createServer({ | ||
plugins: [ | ||
// a plugin that handles `virtual:entrypoint` | ||
{ | ||
name: 'virtual-module', | ||
/* plugin implementation */ | ||
}, | ||
], | ||
}) | ||
const ssrEnvironment = server.environment.ssr | ||
const input = {} | ||
|
||
// use exposed functions by each environment factories that runs the code | ||
// check for each environment factories what they provide | ||
if (ssrEnvironment instanceof RunnableDevEnvironment) { | ||
ssrEnvironment.runner.import('virtual:entrypoint') | ||
} else if (ssrEnvironment instanceof CustomDevEnvironment) { | ||
ssrEnvironment.runEntrypoint('virtual:entrypoint') | ||
} else { | ||
throw new Error(`Unsupported runtime for ${ssrEnvironment.name}`) | ||
} | ||
|
||
// ------------------------------------- | ||
// virtual:entrypoint | ||
const { createHandler } = await import('./entrypoint.js') | ||
const handler = createHandler(input) | ||
const response = handler(new Request('/')) | ||
|
||
// ------------------------------------- | ||
// ./entrypoint.js | ||
export function createHandler(input) { | ||
return function handler(req) { | ||
return new Response('hello') | ||
} | ||
} | ||
``` | ||
|
||
For example, to call `transformIndexHtml` on the user module, the following plugin can be used: | ||
|
||
```ts {13-21} | ||
function vitePluginVirtualIndexHtml(): Plugin { | ||
let server: ViteDevServer | undefined | ||
return { | ||
name: vitePluginVirtualIndexHtml.name, | ||
configureServer(server_) { | ||
server = server_ | ||
}, | ||
resolveId(source) { | ||
return source === 'virtual:index-html' ? '\0' + source : undefined | ||
}, | ||
async load(id) { | ||
if (id === '\0' + 'virtual:index-html') { | ||
let html: string | ||
if (server) { | ||
this.addWatchFile('index.html') | ||
html = await fs.promises.readFile('index.html', 'utf-8') | ||
html = await server.transformIndexHtml('/', html) | ||
} else { | ||
html = await fs.promises.readFile('dist/client/index.html', 'utf-8') | ||
} | ||
return `export default ${JSON.stringify(html)}` | ||
} | ||
return | ||
}, | ||
} | ||
} | ||
``` | ||
|
||
If your code requires Node.js APIs, you can use `hot.send` to communicate with the code that uses Vite's APIs from the user modules. However, be aware that this approach may not work the same way after the build process. | ||
|
||
```ts | ||
// code using the Vite's APIs | ||
import { createServer } from 'vite' | ||
|
||
const server = createServer({ | ||
plugins: [ | ||
// a plugin that handles `virtual:entrypoint` | ||
{ | ||
name: 'virtual-module', | ||
/* plugin implementation */ | ||
}, | ||
], | ||
}) | ||
const ssrEnvironment = server.environment.ssr | ||
const input = {} | ||
|
||
// use exposed functions by each environment factories that runs the code | ||
// check for each environment factories what they provide | ||
if (ssrEnvironment instanceof RunnableDevEnvironment) { | ||
ssrEnvironment.runner.import('virtual:entrypoint') | ||
} else if (ssrEnvironment instanceof CustomDevEnvironment) { | ||
ssrEnvironment.runEntrypoint('virtual:entrypoint') | ||
} else { | ||
throw new Error(`Unsupported runtime for ${ssrEnvironment.name}`) | ||
} | ||
|
||
const req = new Request('/') | ||
|
||
const uniqueId = 'a-unique-id' | ||
ssrEnvironment.send('request', serialize({ req, uniqueId })) | ||
const response = await new Promise((resolve) => { | ||
ssrEnvironment.on('response', (data) => { | ||
data = deserialize(data) | ||
if (data.uniqueId === uniqueId) { | ||
resolve(data.res) | ||
} | ||
}) | ||
}) | ||
|
||
// ------------------------------------- | ||
// virtual:entrypoint | ||
const { createHandler } = await import('./entrypoint.js') | ||
const handler = createHandler(input) | ||
|
||
import.meta.hot.on('request', (data) => { | ||
const { req, uniqueId } = deserialize(data) | ||
const res = handler(req) | ||
import.meta.hot.send('response', serialize({ res: res, uniqueId })) | ||
}) | ||
|
||
const response = handler(new Request('/')) | ||
|
||
// ------------------------------------- | ||
// ./entrypoint.js | ||
export function createHandler(input) { | ||
return function handler(req) { | ||
return new Response('hello') | ||
} | ||
} | ||
``` | ||
|
||
## Environments during build | ||
|
||
In the CLI, calling `vite build` and `vite build --ssr` will still build the client only and ssr only environments for backward compatibility. | ||
|
||
When `builder.entireApp` is `true` (or when calling `vite build --app`), `vite build` will opt-in into building the entire app instead. This would later on become the default in a future major. A `ViteBuilder` instance will be created (build-time equivalent to a `ViteDevServer`) to build all configured environments for production. By default the build of environments is run in series respecting the order of the `environments` record. A framework or user can further configure how the environments are built using: | ||
|
||
```js | ||
export default { | ||
builder: { | ||
buildApp: async (builder) => { | ||
const environments = Object.values(builder.environments) | ||
return Promise.all( | ||
environments.map((environment) => builder.build(environment)), | ||
) | ||
}, | ||
}, | ||
} | ||
``` | ||
|
||
## Environment agnostic code | ||
|
||
Most of the time, the current `environment` instance will be available as part of the context of the code being run so the need to access them through `server.environments` should be rare. For example, inside plugin hooks the environment is exposed as part of the `PluginContext`, so it can be accessed using `this.environment`. See [Environment API for Plugins](./api-environment-plugins.md) to learn about how to build environment aware plugins. |
Oops, something went wrong.