Skip to content

Commit a1f3f60

Browse files
authored
fix: fix entry order and update docs and examples (#158)
* fix: sort entries to determine final one * chore: update docs * chore: update docs * chore: update nest example * chore: uncomment ci cases * chore: update docs
1 parent dd3bbeb commit a1f3f60

4 files changed

Lines changed: 195 additions & 71 deletions

File tree

ci/ci.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ const baseCases: Array<{
2121
file: string;
2222
packageManager?: "pnpm" | "bun";
2323
}> = [
24-
// { framework: "simple-standalone", file: "src/entry.server.ts" },
25-
// { framework: "express", file: "src/routes/home.ts" },
26-
// { framework: "express-server", file: "src/routes/home.ts" },
27-
// { framework: "fastify", file: "src/routes/home.ts" },
28-
// { framework: "koa", file: "src/routes/home.ts" },
29-
// { framework: "hapi", file: "src/routes/home.ts" },
30-
// { framework: "ssr-react-express", file: "src/pages/Home.tsx" },
31-
// { framework: "nestjs", file: "src/app.controller.ts" },
24+
{ framework: "simple-standalone", file: "src/entry.server.ts" },
25+
{ framework: "express", file: "src/routes/home.ts" },
26+
{ framework: "express-server", file: "src/routes/home.ts" },
27+
{ framework: "fastify", file: "src/routes/home.ts" },
28+
{ framework: "koa", file: "src/routes/home.ts" },
29+
{ framework: "hapi", file: "src/routes/home.ts" },
30+
{ framework: "ssr-react-express", file: "src/pages/Home.tsx" },
31+
{ framework: "nestjs", file: "src/app.controller.ts" },
3232
{
3333
framework: "bun-server",
3434
file: "src/routes/home.ts",

examples/nestjs/src/entry.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ if (import.meta.env.COMMAND === "build") {
1818

1919
if (import.meta.hot) {
2020
import.meta.hot.accept();
21+
import.meta.hot.dispose(() => app.close());
2122
}

packages/vavite/readme.md

Lines changed: 163 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -6,102 +6,180 @@ Vite, despite being mainly a frontend tool, has support for transpiling server-s
66

77
Vite's official SSR guide describes a workflow where Vite's development server is used as a middleware function in a server application made with a [Connect](https://github.com/senchalabs/connect) compatible Node.js framework (like [Express](https://expressjs.com)). If your server-side code needs transpilation (e.g. for TypeScript or JSX), you're required to use another set of tools (say [`ts-node`](https://typestrong.org/ts-node/) and [`nodemon`](https://nodemon.io/)) for development and building. `vavite` enables you to use Vite itself to transpile all of your server-side code.
88

9-
## Operating modes
9+
## Getting started
1010

11-
Vavite has two operating modes, determined by the `type` property of the entries you provide via the `entries` configuration option:
11+
The easiest way to get started is to use one of the [examples](#examples) as a template. To start from scratch, follow these steps:
1212

13-
### Handler mode (`type: "runnable-handler"`)
13+
1. Install `vavite` as a development dependency in your project.
14+
```sh
15+
npm install --save-dev vavite
16+
```
17+
2. Add the `vavite` plugin to your Vite config.
1418

15-
In this mode, Vavite will import your entry and call the exported `node:http`-compatible request handler for incoming requests. This is the default mode and is suitable for most use cases. In this mode, your entry module will not be loaded until the first request comes in.
19+
```ts
20+
// vite.config.ts
21+
import { defineConfig } from "vite";
22+
import { vavite } from "vavite";
1623

17-
For production, you need to explicitly start the server by calling `app.listen` or similar in your entry module, guarded by `if (import.meta.env.COMMAND === "build") { ... }` to prevent it from running in development.
24+
export default defineConfig({
25+
appType: "custom", // Prevent Vite from serving index.html for the / route
26+
plugins: [
27+
vavite({
28+
// Optional configuration options go here
29+
}),
30+
],
31+
});
32+
```
1833

19-
### Server mode (`type: "runnable-server"`)
34+
3. Now you can create a handler or server entry (Vavite looks for `/src/entry.server.{js,ts,jsx,tsx}` by default).
2035

21-
In this mode, Vavite will run your entry which is expected to start a server on its own, on a separate port from the Vite's dev server. Vavite will proxy incoming requests to that server. This mode is less efficient and less well tested but it can be useful if you need control over server creation even in development or you want to use an HTTP serving library that is not compatible with `node:http`, like `Bun.serve`.
36+
## Entry types
2237

23-
## Examples
38+
Vavite recognizes two types of entries specified by the `type` property of the entries you provide via the `entries` configuration option: handler entries (`"runnable-handler"`, the default) and server entries (`"runnable-server"`).
2439

25-
The easiest way to start with Vavite is to follow the examples:
40+
Handler entries are expected to export a `node:http`-compatible request handler which will be added to Vite's dev server as a middleware. Server entries are expected to start a server on their own, on a separate port from the Vite's dev server. Vavite will proxy incoming requests to that server.
2641

27-
- Handler entry setups:
28-
- Server-only setups:
29-
- [simple-standalone](/examples/simple-standalone): Simple `node:http` server example ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/simple-standalone))
30-
- [express](/examples/express): Integrating with Express ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/express))
31-
- [koa](/examples/koa): Integrating with Koa ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/koa))
32-
- [fastify](/examples/fastify): Integrating with Fastify ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/fastify))
33-
- [hapi](/examples/hapi): Integrating with Hapi ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/hapi))
34-
- [Nest.js](/examples/nestjs): [Nest.js](https://nestjs.com/) with Express ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/nestjs))
35-
- SSR setups
36-
- [ssr-react-express](/examples/ssr-react-express): React SSR with Express ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/ssr-react-express))
37-
- Other examples
38-
- [resource-cleanup](/examples/resource-cleanup): Demonstrating patterns for cleaning up resources on hot reload
39-
- [ws](/examples/ws): WebSocket server example ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/ws))
40-
- Server entry setups:
41-
- [express-server](/examples/express-server): Integrating with Express in server mode ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/express-server))
42-
- [bun-server](/examples/bun-server): Integrating with `Bun.serve` in server mode
42+
### Handler entries (`type: "runnable-handler"`)
4343

44-
## Usage
44+
Handler entries are expected to export a `node:http`-compatible request handler. For this entry type, during develompent (`vite dev`) Vavite will import your entry and call the exported handler for each incoming request. For production, you need to explicitly start the server.
45+
46+
The simplest handler entry looks like this:
47+
48+
```ts
49+
// entry.server.ts
50+
51+
// Alternatively, you can put add these to the `types` option of your tsconfig.json
52+
/// <reference types="vite/client" />
53+
/// <reference types="vavite/types" />
54+
55+
import {
56+
createServer,
57+
type IncomingMessage,
58+
type ServerResponse,
59+
} from "node:http";
60+
61+
// Default export a handler for dev
62+
export default function handler(req: IncomingMessage, res: ServerResponse) {
63+
if (req.url === "/") {
64+
res
65+
.setHeader("Content-Type", "text/html; charset=utf-8")
66+
.end("<h1>Hello from standalone!</h1>");
67+
} else {
68+
res.statusCode = 404;
69+
res.end("Not found");
70+
}
71+
}
4572

46-
Install as a development dependency:
73+
// Start the standalone server in production mode
74+
if (import.meta.env.COMMAND === "build") {
75+
createServer(handler).listen(3000, () => {
76+
console.log("Server is listening on http://localhost:3000");
77+
});
78+
}
4779

48-
```sh
49-
npm install --save-dev vavite
80+
// Enable hot module replacement
81+
if (import.meta.hot) {
82+
import.meta.hot.accept();
83+
}
5084
```
5185

52-
### Server-only setup
86+
The above example uses `node:http` but it's possible to get a hold of a compatible handler from most Node.js server frameworks. See the examples for Express, Fastify, Koa, Hapi, and Nest.js for more details.
87+
88+
### Server entries (`type: "runnable-server"`)
5389

54-
In this setup, Vavite will route requests to your handler or server before most of Vite's own middlewares, effectively bypassing Vite's most client-side features. You also need `appType: "custom"` and a `builder.buildApp` option to only build the server environment.
90+
Server entries are expected to start an HTTP server (on a separate port from the Vite's dev server) and process incoming requests. For development, Vavite will proxy incoming requests to that server. For production, your server will be the one directly receiving incoming requests.
91+
92+
Proxying is less efficient than direct function calls and is less well tested. But it is useful when you need control over server creation even in development or when you want to use an HTTP serving library that is not compatible with `node:http`, like `Bun.serve`.
93+
94+
Since this isn't the default entry type, you need to specify the entry type and the address it will be running on explicitly in the Vite config:
5595

5696
```ts
5797
// vite.config.ts
98+
99+
// Alternatively, you can put add these to the `types` option of your tsconfig.json
100+
/// <reference types="vite/client" />
101+
/// <reference types="vavite/types" />
102+
58103
import { defineConfig } from "vite";
59104
import { vavite } from "vavite";
60105

61106
export default defineConfig({
62-
appType: "custom",
63-
builder: {
64-
async buildApp(builder) {
65-
await builder.build(builder.environments.ssr!);
66-
},
67-
},
68-
plugins: [vavite()],
107+
appType: "custom", // Prevent Vite from serving index.html for the / route
108+
plugins: [
109+
vavite({
110+
entries: [
111+
{
112+
entry: "/src/entry.server",
113+
type: "runnable-server",
114+
proxyOptions: {
115+
target: "http://localhost:3000",
116+
},
117+
},
118+
],
119+
}),
120+
],
69121
});
70122
```
71123

72-
Then add a `src/entry.server.ts` (or name it explicitly via Vavite's `entries` option):
124+
And the server entry itself looks like this:
73125

74126
```ts
75127
// entry.server.ts
128+
import { createServer } from "node:http";
76129

77-
// Alternatively, you can put the following in the `types` option of your tsconfig.json
78-
/// <reference types="vite/client" />
79-
/// <reference types="vavite/types" />
130+
createServer((req, res) => {
131+
if (req.url === "/") {
132+
res
133+
.setHeader("Content-Type", "text/html; charset=utf-8")
134+
.end("<h1>Hello from standalone!</h1>");
135+
} else {
136+
res.statusCode = 404;
137+
res.end("Not found");
138+
}
139+
}).listen(3000, () => {
140+
console.log("Server is listening on http://localhost:3000");
141+
});
142+
```
80143

81-
import { createServerApp } from "your-favorite-server-framework";
144+
## Entry order
82145

83-
const app = FavoriteServerFramework.createServer();
146+
Each entry has an optional `order` property which can be either `"pre"` or `"post"`. It determines whether the entry will be placed before or after Vite's own middlewares. `"pre"` entries will run before Vite's middlewares while `"post"` entries will run after. If not specified, Vavite will try to automatically determine the order based on whether you have configured a client entry or not. If you have a client entry, the default order will be `"post"`, otherwise it will be `"pre"`.
84147

85-
// Add your middleware and routes here
148+
`"pre"` entries are useful for server-only setups where you don't need Vite's client-side features or you need some processing before Vite's middlewares.
86149

87-
// Default export a Connect-compatible handler for dev
88-
export app.getNodeHttpHandler();
150+
`"post"` entries are useful for SSR setups where you want to leverage Vite's asset transformation pipeline. In this case, you can import client assets in your server code and Vite will transform them correctly in development and production.
89151

90-
if (import.meta.env.COMMAND === "build") {
91-
// Start the server in production mode
92-
app.listen(3000, () => {
93-
console.log("Server is listening on http://localhost:3000");
94-
});
95-
}
152+
You can mark an entry with `final: true` to stop the request processing chain. Vavite will mark an entry as final by default if it is the last entry in the chain (respecting the pre/post order).
96153

97-
if (import.meta.hot) {
98-
import.meta.hot.accept();
99-
}
154+
In development, non-final entries can pass unhandled requests to the next entry or Vite's own middlewares. This can be useful, for example, when you want to selectively pass some requests to Vite's own middlewares from `"pre"` entries. `/@vite/client` is a common example of this, since it needs to be handled by Vite's own middlewares for client-side HMR to work.
155+
156+
To forward unhandled requests in handler entries, just call the `next` function provided as the third argument. For server entries, return a response with status code 404 and the header `Vavite-Try-Next-Upstream` set to `true`. Note that the latter is less well tested and some proxy options might not work as expected when you use this feature.
157+
158+
In production, Vite's middleware stack will not exist so you will need to handle the processing order yourself. Typically, you will start a server for the frontmost entry and pass unhandled requests to the next handler by placing it in the middleware stack or to the next server by proxying.
159+
160+
### Server-only setup
161+
162+
In this setup, Vavite will route requests to your handler or server before most of Vite's own middlewares, effectively bypassing Vite's most client-side features. Typically, you also need `appType: "custom"` and a `builder.buildApp` option to only build the server environment.
163+
164+
```ts
165+
// vite.config.ts
166+
import { defineConfig } from "vite";
167+
import { vavite } from "vavite";
168+
169+
export default defineConfig({
170+
appType: "custom",
171+
builder: {
172+
async buildApp(builder) {
173+
await builder.build(builder.environments.ssr!);
174+
},
175+
},
176+
plugins: [vavite()],
177+
});
100178
```
101179

102180
### SSR setup
103181

104-
In this setup, Vavite will place your server handler after Vite's asset transformation pipeline. It will do it automatically if it detects a client entry in your Vite config. If it fails for any reason, use `vavite({ entries: [{ entry: "/src/entry.server", order: "post" }] })` to force it to insert the handler in the correct position.
182+
In this setup, Vavite will place your server handler after Vite's asset transformation pipeline.
105183

106184
```ts
107185
// vite.config.ts
@@ -137,7 +215,32 @@ export default defineConfig({
137215

138216
You can use `vite build --app` to build both environments or you can provide a `builder.buildApp` option to control the order of the builds.
139217

140-
### Other features
218+
In production, Vite's asset transformation pipeline will not exist. Instead, you will typically serve the built client assets with a static file serving middleware.
219+
220+
## Passing unhandled requests to the next entry or Vite
221+
222+
## Examples
223+
224+
- Handler entry setups:
225+
- Server-only setups:
226+
- [simple-standalone](/examples/simple-standalone): Simple `node:http` server example ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/simple-standalone))
227+
- [express](/examples/express): Integrating with Express ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/express))
228+
- [koa](/examples/koa): Integrating with Koa ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/koa))
229+
- [fastify](/examples/fastify): Integrating with Fastify ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/fastify))
230+
- [hapi](/examples/hapi): Integrating with Hapi ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/hapi))
231+
- [Nest.js](/examples/nestjs): [Nest.js](https://nestjs.com/) with Express ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/nestjs))
232+
- SSR setups
233+
- [ssr-react-express](/examples/ssr-react-express): React SSR with Express ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/ssr-react-express))
234+
- Other examples
235+
- [resource-cleanup](/examples/resource-cleanup): Demonstrating patterns for cleaning up resources on hot reload
236+
- [ws](/examples/ws): WebSocket server example ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/ws))
237+
- Server entry setups:
238+
- [express-server](/examples/express-server): Integrating with Express in server mode ([Stackblitz](https://stackblitz.com/github/cyco130/vavite/tree/main/examples/express-server))
239+
- [bun-server](/examples/bun-server): Integrating with `Bun.serve` in server mode
240+
241+
## Other features and considerations
242+
243+
### Environment information and Vite dev server access
141244

142245
By default, Vavite will expose some information about the environment your code is running on:
143246

@@ -232,13 +335,12 @@ All packages under the `@vavite` namespace and the `vavite` CLI command are now
232335

233336
### Other changes needed in the Vite config
234337

235-
- Set `appType: "custom"` unless you really want Vite to server `index.html` for the `/` route.
338+
- Set `appType: "custom"` unless you really want Vite to serve `index.html` for the `/` route.
236339
- Remove `buildSteps` and use `vite build --app` or the `builder.buildApp` Vite config option to orchestrate the build process programmatically.
237340

238341
### Changes needed in your server code
239342

240-
- Do not call the `next` function from your exported handler entry, just end the response with a 404.
241-
- Explicitly add code to start the server in production: `if (import.meta.env.COMMAND === "serve") { startMyServer(myHandler); }`
343+
- For handler entry setups, explicitly add code to start the server in production: `if (import.meta.env.COMMAND === "serve") { startMyServer(myHandler); }`
242344
- Import `viteDevServer` from `vavite:vite-dev-server` instead of `vavite/vite-dev-server`.
243345
- Add `if (import.meta.hot) { import.meta.hot.accept(); }` to your server entry file for better performance.
244346

packages/vavite/src/index.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,16 +213,37 @@ export function vavite(options: VaviteOptions = {}): Plugin[] {
213213
},
214214

215215
async configureServer(server) {
216+
const sortedEntries = entries
217+
.map((entry) => ({
218+
...entry,
219+
order: entry.order ?? defaultMiddlewareOrder,
220+
}))
221+
.sort((a, b) => {
222+
if (a.order === b.order) {
223+
return 0;
224+
}
225+
226+
return a.order === "pre" ? -1 : 1;
227+
});
228+
216229
const postMiddlewares: Connect.NextHandleFunction[] = [];
217230

218-
for (const [index, entry] of entries.entries()) {
231+
for (const [index, entry] of sortedEntries.entries()) {
219232
const {
220233
environment: environmentName = "ssr",
221234
entry: entryPath,
222-
final = index === entries.length - 1,
235+
final = index === sortedEntries.length - 1,
223236
order = defaultMiddlewareOrder,
224237
} = entry;
225238

239+
if (final && index !== sortedEntries.length - 1) {
240+
this.warn(
241+
`Entry ${JSON.stringify(
242+
entryPath,
243+
)} is marked as final but it's not the last entry in the chain. This might lead to unexpected behavior.`,
244+
);
245+
}
246+
226247
const environment = server.environments[environmentName];
227248

228249
if (!environment) {

0 commit comments

Comments
 (0)