|
| 1 | +# Environment API for Frameworks |
| 2 | + |
| 3 | +:::warning Experimental |
| 4 | +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. |
| 5 | + |
| 6 | +Resources: |
| 7 | + |
| 8 | +- [Feedback discussion](https://github.com/vitejs/vite/discussions/16358) where we are gathering feedback about the new APIs. |
| 9 | +- [Environment API PR](https://github.com/vitejs/vite/pull/16471) where the new API were implemented and reviewed. |
| 10 | + |
| 11 | +Please share with us your feedback as you test the proposal. |
| 12 | +::: |
| 13 | + |
| 14 | +## Environments and frameworks |
| 15 | + |
| 16 | +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. |
| 17 | + |
| 18 | +```ts |
| 19 | +export class RunnableDevEnvironment extends DevEnvironment { |
| 20 | + public readonly runner: ModuleRunner |
| 21 | +} |
| 22 | + |
| 23 | +class ModuleRunner { |
| 24 | + /** |
| 25 | + * URL to execute. Accepts file path, server path, or id relative to the root. |
| 26 | + * Returns an instantiated module (same as in ssrLoadModule) |
| 27 | + */ |
| 28 | + public async import(url: string): Promise<Record<string, any>> |
| 29 | + /** |
| 30 | + * Other ModuleRunner methods... |
| 31 | + */ |
| 32 | +} |
| 33 | + |
| 34 | +if (isRunnableDevEnvironment(server.environments.ssr)) { |
| 35 | + await server.environments.ssr.runner.import('/entry-point.js') |
| 36 | +} |
| 37 | +``` |
| 38 | + |
| 39 | +:::warning |
| 40 | +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. |
| 41 | +::: |
| 42 | + |
| 43 | +## Default `RunnableDevEnvironment` |
| 44 | + |
| 45 | +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. |
| 46 | + |
| 47 | +```js |
| 48 | +import { createServer } from 'vite' |
| 49 | + |
| 50 | +const server = await createServer({ |
| 51 | + server: { middlewareMode: true }, |
| 52 | + appType: 'custom', |
| 53 | + environments: { |
| 54 | + server: { |
| 55 | + // by default, the modules are run in the same process as the vite dev server during dev |
| 56 | + }, |
| 57 | + }, |
| 58 | +}) |
| 59 | + |
| 60 | +// You might need to cast this to RunnableDevEnvironment in TypeScript or use |
| 61 | +// the "isRunnableDevEnvironment" function to guard the access to the runner |
| 62 | +const environment = server.environments.node |
| 63 | + |
| 64 | +app.use('*', async (req, res, next) => { |
| 65 | + const url = req.originalUrl |
| 66 | + |
| 67 | + // 1. Read index.html |
| 68 | + let template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8') |
| 69 | + |
| 70 | + // 2. Apply Vite HTML transforms. This injects the Vite HMR client, |
| 71 | + // and also applies HTML transforms from Vite plugins, e.g. global |
| 72 | + // preambles from @vitejs/plugin-react |
| 73 | + template = await server.transformIndexHtml(url, template) |
| 74 | + |
| 75 | + // 3. Load the server entry. import(url) automatically transforms |
| 76 | + // ESM source code to be usable in Node.js! There is no bundling |
| 77 | + // required, and provides full HMR support. |
| 78 | + const { render } = await environment.runner.import('/src/entry-server.js') |
| 79 | + |
| 80 | + // 4. render the app HTML. This assumes entry-server.js's exported |
| 81 | + // `render` function calls appropriate framework SSR APIs, |
| 82 | + // e.g. ReactDOMServer.renderToString() |
| 83 | + const appHtml = await render(url) |
| 84 | + |
| 85 | + // 5. Inject the app-rendered HTML into the template. |
| 86 | + const html = template.replace(`<!--ssr-outlet-->`, appHtml) |
| 87 | + |
| 88 | + // 6. Send the rendered HTML back. |
| 89 | + res.status(200).set({ 'Content-Type': 'text/html' }).end(html) |
| 90 | +}) |
| 91 | +``` |
| 92 | + |
| 93 | +## Runtime agnostic SSR |
| 94 | + |
| 95 | +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. |
| 96 | + |
| 97 | +:::info `FetchableDevEnvironment` proposal |
| 98 | + |
| 99 | +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). |
| 100 | + |
| 101 | +::: |
| 102 | + |
| 103 | +`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. |
| 104 | + |
| 105 | +For example, the following example uses the value of the user module from the code using the Vite's APIs: |
| 106 | + |
| 107 | +```ts |
| 108 | +// code using the Vite's APIs |
| 109 | +import { createServer } from 'vite' |
| 110 | + |
| 111 | +const server = createServer() |
| 112 | +const ssrEnvironment = server.environment.ssr |
| 113 | +const input = {} |
| 114 | + |
| 115 | +const { createHandler } = await ssrEnvironment.runner.import('./entrypoint.js') |
| 116 | +const handler = createHandler(input) |
| 117 | +const response = handler(new Request('/')) |
| 118 | + |
| 119 | +// ------------------------------------- |
| 120 | +// ./entrypoint.js |
| 121 | +export function createHandler(input) { |
| 122 | + return function handler(req) { |
| 123 | + return new Response('hello') |
| 124 | + } |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | +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. |
| 129 | + |
| 130 | +```ts |
| 131 | +// code using the Vite's APIs |
| 132 | +import { createServer } from 'vite' |
| 133 | + |
| 134 | +const server = createServer({ |
| 135 | + plugins: [ |
| 136 | + // a plugin that handles `virtual:entrypoint` |
| 137 | + { |
| 138 | + name: 'virtual-module', |
| 139 | + /* plugin implementation */ |
| 140 | + }, |
| 141 | + ], |
| 142 | +}) |
| 143 | +const ssrEnvironment = server.environment.ssr |
| 144 | +const input = {} |
| 145 | + |
| 146 | +// use exposed functions by each environment factories that runs the code |
| 147 | +// check for each environment factories what they provide |
| 148 | +if (ssrEnvironment instanceof RunnableDevEnvironment) { |
| 149 | + ssrEnvironment.runner.import('virtual:entrypoint') |
| 150 | +} else if (ssrEnvironment instanceof CustomDevEnvironment) { |
| 151 | + ssrEnvironment.runEntrypoint('virtual:entrypoint') |
| 152 | +} else { |
| 153 | + throw new Error(`Unsupported runtime for ${ssrEnvironment.name}`) |
| 154 | +} |
| 155 | + |
| 156 | +// ------------------------------------- |
| 157 | +// virtual:entrypoint |
| 158 | +const { createHandler } = await import('./entrypoint.js') |
| 159 | +const handler = createHandler(input) |
| 160 | +const response = handler(new Request('/')) |
| 161 | + |
| 162 | +// ------------------------------------- |
| 163 | +// ./entrypoint.js |
| 164 | +export function createHandler(input) { |
| 165 | + return function handler(req) { |
| 166 | + return new Response('hello') |
| 167 | + } |
| 168 | +} |
| 169 | +``` |
| 170 | + |
| 171 | +For example, to call `transformIndexHtml` on the user module, the following plugin can be used: |
| 172 | + |
| 173 | +```ts {13-21} |
| 174 | +function vitePluginVirtualIndexHtml(): Plugin { |
| 175 | + let server: ViteDevServer | undefined |
| 176 | + return { |
| 177 | + name: vitePluginVirtualIndexHtml.name, |
| 178 | + configureServer(server_) { |
| 179 | + server = server_ |
| 180 | + }, |
| 181 | + resolveId(source) { |
| 182 | + return source === 'virtual:index-html' ? '\0' + source : undefined |
| 183 | + }, |
| 184 | + async load(id) { |
| 185 | + if (id === '\0' + 'virtual:index-html') { |
| 186 | + let html: string |
| 187 | + if (server) { |
| 188 | + this.addWatchFile('index.html') |
| 189 | + html = await fs.promises.readFile('index.html', 'utf-8') |
| 190 | + html = await server.transformIndexHtml('/', html) |
| 191 | + } else { |
| 192 | + html = await fs.promises.readFile('dist/client/index.html', 'utf-8') |
| 193 | + } |
| 194 | + return `export default ${JSON.stringify(html)}` |
| 195 | + } |
| 196 | + return |
| 197 | + }, |
| 198 | + } |
| 199 | +} |
| 200 | +``` |
| 201 | + |
| 202 | +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. |
| 203 | + |
| 204 | +```ts |
| 205 | +// code using the Vite's APIs |
| 206 | +import { createServer } from 'vite' |
| 207 | + |
| 208 | +const server = createServer({ |
| 209 | + plugins: [ |
| 210 | + // a plugin that handles `virtual:entrypoint` |
| 211 | + { |
| 212 | + name: 'virtual-module', |
| 213 | + /* plugin implementation */ |
| 214 | + }, |
| 215 | + ], |
| 216 | +}) |
| 217 | +const ssrEnvironment = server.environment.ssr |
| 218 | +const input = {} |
| 219 | + |
| 220 | +// use exposed functions by each environment factories that runs the code |
| 221 | +// check for each environment factories what they provide |
| 222 | +if (ssrEnvironment instanceof RunnableDevEnvironment) { |
| 223 | + ssrEnvironment.runner.import('virtual:entrypoint') |
| 224 | +} else if (ssrEnvironment instanceof CustomDevEnvironment) { |
| 225 | + ssrEnvironment.runEntrypoint('virtual:entrypoint') |
| 226 | +} else { |
| 227 | + throw new Error(`Unsupported runtime for ${ssrEnvironment.name}`) |
| 228 | +} |
| 229 | + |
| 230 | +const req = new Request('/') |
| 231 | + |
| 232 | +const uniqueId = 'a-unique-id' |
| 233 | +ssrEnvironment.send('request', serialize({ req, uniqueId })) |
| 234 | +const response = await new Promise((resolve) => { |
| 235 | + ssrEnvironment.on('response', (data) => { |
| 236 | + data = deserialize(data) |
| 237 | + if (data.uniqueId === uniqueId) { |
| 238 | + resolve(data.res) |
| 239 | + } |
| 240 | + }) |
| 241 | +}) |
| 242 | + |
| 243 | +// ------------------------------------- |
| 244 | +// virtual:entrypoint |
| 245 | +const { createHandler } = await import('./entrypoint.js') |
| 246 | +const handler = createHandler(input) |
| 247 | + |
| 248 | +import.meta.hot.on('request', (data) => { |
| 249 | + const { req, uniqueId } = deserialize(data) |
| 250 | + const res = handler(req) |
| 251 | + import.meta.hot.send('response', serialize({ res: res, uniqueId })) |
| 252 | +}) |
| 253 | + |
| 254 | +const response = handler(new Request('/')) |
| 255 | + |
| 256 | +// ------------------------------------- |
| 257 | +// ./entrypoint.js |
| 258 | +export function createHandler(input) { |
| 259 | + return function handler(req) { |
| 260 | + return new Response('hello') |
| 261 | + } |
| 262 | +} |
| 263 | +``` |
| 264 | + |
| 265 | +## Environments during build |
| 266 | + |
| 267 | +In the CLI, calling `vite build` and `vite build --ssr` will still build the client only and ssr only environments for backward compatibility. |
| 268 | + |
| 269 | +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: |
| 270 | + |
| 271 | +```js |
| 272 | +export default { |
| 273 | + builder: { |
| 274 | + buildApp: async (builder) => { |
| 275 | + const environments = Object.values(builder.environments) |
| 276 | + return Promise.all( |
| 277 | + environments.map((environment) => builder.build(environment)), |
| 278 | + ) |
| 279 | + }, |
| 280 | + }, |
| 281 | +} |
| 282 | +``` |
| 283 | + |
| 284 | +## Environment agnostic code |
| 285 | + |
| 286 | +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. |
0 commit comments