Skip to content

Commit c322304

Browse files
authored
Merge branch 'main' into niklasmh/patch-19494
2 parents 41fdd82 + 5552583 commit c322304

File tree

63 files changed

+2311
-2167
lines changed

Some content is hidden

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

63 files changed

+2311
-2167
lines changed

docs/.vitepress/config.ts

+24-3
Original file line numberDiff line numberDiff line change
@@ -315,12 +315,33 @@ export default defineConfig({
315315
link: '/guide/api-javascript',
316316
},
317317
{
318-
text: 'Environment API',
318+
text: 'Config Reference',
319+
link: '/config/',
320+
},
321+
],
322+
},
323+
{
324+
text: 'Environment API',
325+
items: [
326+
{
327+
text: 'Introduction',
319328
link: '/guide/api-environment',
320329
},
321330
{
322-
text: 'Config Reference',
323-
link: '/config/',
331+
text: 'Environment instances',
332+
link: '/guide/api-environment-instances',
333+
},
334+
{
335+
text: 'Plugins',
336+
link: '/guide/api-environment-plugins',
337+
},
338+
{
339+
text: 'Frameworks',
340+
link: '/guide/api-environment-frameworks',
341+
},
342+
{
343+
text: 'Runtimes',
344+
link: '/guide/api-environment-runtimes',
324345
},
325346
],
326347
},
+286
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
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

Comments
 (0)