Skip to content

Commit 5256892

Browse files
committed
Merge remote-tracking branch 'origin/main' into pr-4453/fix/vite-dev-import-query
# Conflicts: # src/build/vite/dev.ts # test/vite/app.test.ts
2 parents 9d17ea7 + 1dfe4e8 commit 5256892

20 files changed

Lines changed: 5184 additions & 2867 deletions

File tree

.agents/vite-dev.md

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
# Vite Dev Middleware — Request Routing
2+
3+
> Scope: the request-routing logic in `src/build/vite/dev.ts` — the two
4+
> `server.middlewares.use(...)` registrations (`nitroDevMiddlewarePre` and the
5+
> catch-all `nitroDevMiddleware`): why the current design looks the way it does.
6+
7+
## 1. Where it lives & how it is wired
8+
9+
- Logic: `src/build/vite/dev.ts``configureViteDevServer(ctx, server)`.
10+
- Entry: `src/build/vite/plugin.ts:266` — the plugin's `configureServer` hook
11+
`return`s the result of `configureViteDevServer`.
12+
13+
Vite's `configureServer` contract: middleware registered **synchronously**
14+
inside the hook runs **before** Vite's internal middlewares; a function
15+
**returned** from the hook runs **after** them (the "post" hook). Nitro uses
16+
both ends:
17+
18+
| Registration | Site | Runs | Role |
19+
|---|---|---|---|
20+
| `nitroDevMiddlewarePre` (`const` form) | `dev.ts:284` | **before** Vite static/transform | Classifier. Route explicit-Nitro + definite navigations to Nitro immediately; let definite assets fall through to Vite, marking them `_nitroHandled` (transparent catch-all) or `_nitroAssetCheck` (opaque catch-all / no match). |
21+
| `nitroDevMiddleware` | defined at `dev.ts:213`, registered inside the returned `() => { ... }` | **after** Vite static/transform | Catch-all fallback. Wraps req as web `Request`, tries `ctx.devApp.fetch` then `nitroEnv.dispatchFetch`, honoring `baseURL`; inspects the response for `_nitroAssetCheck` requests. Skipped for `_nitroHandled`. |
22+
23+
**Why two, and why pre?** Without the pre-pass, Vite's static/transform
24+
middleware serves files from the project root and would answer server routes
25+
before Nitro sees them (see upstream **vitejs/vite#20866**, which made Vite
26+
consult `sec-fetch-dest` to let document requests fall through — the same
27+
header Nitro's classifier leans on).
28+
29+
Facts about Vite's side (verified against vite@8.1.4): Nitro defaults
30+
`appType` to `"custom"` (`plugin.ts:199` — user-overridable; the reasoning
31+
below assumes the default), so Vite registers none of
32+
`htmlFallback`/`indexHtml`/`notFound` middlewares, and **every plain miss
33+
`next()`s** (sirv and transformMiddleware never self-emit a 404 for a missing
34+
file/module). The only terminal 404 is connect's `finalhandler`, which sits
35+
**after** the Nitro post-hook. So Vite genuinely hands off every request it
36+
cannot serve — the post catch-all is the last handler before `finalhandler`.
37+
38+
## 2. Classification
39+
40+
`nitro.routing.routes` in an SSR app **always** contains a catch-all `/**`, so
41+
"does a Nitro route match?" is nearly always *yes*. The design distinguishes:
42+
43+
- **Explicit route**`route` set, `!== "/**"`, not `startsWith("/**:")` (a
44+
prefixed splat like `/api/photos/**` is explicit). Deterministic → **always
45+
Nitro**, no heuristic may override (#4108, #4241, #4252, #4270).
46+
- **Explicit public asset** — a public-asset dir under a non-root `baseURL`
47+
with `fallthrough: false` owns its subtree → Nitro, like an explicit route.
48+
- **Transparent catch-all** — a *user route file* at root level
49+
(`routes/[...].ts``/**`, `routes/[...slug].ts``/**:slug`). Nitro sees
50+
everything it can handle, so Vite stays the definitive asset handler: an
51+
asset-tagged request is marked `_nitroHandled` and a Vite miss must **not**
52+
fall back into it (#4234/#4266).
53+
- **Opaque catch-all** — the SSR renderer `/**` or a custom `serverEntry` `/**`
54+
(both registered in `routing.ts` with identifiable `handler` paths). Their
55+
*real* routes are invisible to the host (`server.ts` H3 app routes, framework
56+
routers like TanStack Start), so an asset-tagged miss from Vite must still be
57+
**dispatched** — marked `_nitroAssetCheck`, decided by response inspection.
58+
- **None** — no match. Extensionless → page navigation → Nitro; asset-tagged →
59+
same as opaque (`_nitroAssetCheck`, dispatch after Vite).
60+
61+
## 3. The asset heuristic (catch-all / none case only)
62+
63+
Set `Vary: sec-fetch-dest, accept`, then:
64+
65+
- **`Sec-Fetch-Dest` present & concrete** (not `empty`): `document`/`iframe`/
66+
`frame` = navigation → Nitro; anything else (`image`, `video`, `style`, …) =
67+
asset.
68+
- **Absent or `empty`**: fall back to extension — `ASSET_EXT_RE.test(ext)`
69+
**and** no `text/html` in `Accept` ⇒ asset. (`empty` = fetch/XHR is ambiguous:
70+
it tags both API calls and `fetch()`ed assets.)
71+
- **Only `GET`/`HEAD` can be assets at all** — a `POST /upload.png` is never a
72+
browser asset load, so other methods bypass the heuristic entirely.
73+
- Non-asset + (matched or extensionless) → Nitro immediately (pre-Vite).
74+
75+
Two regexes to keep intact:
76+
- `ASSET_EXT_RE` (`dev.ts:24`) — narrow on purpose, so dotted Nitro params like
77+
`/foo.bar.1` still reach Nitro (#4108).
78+
- Extension is extracted from the path only (query/hash stripped) so
79+
`?file=bar.png` does not misclassify.
80+
81+
## 4. Response inspection (`_nitroAssetCheck`)
82+
83+
An asset-tagged request that only an opaque catch-all could handle cannot be
84+
classified **a priori**`/image.png` may be a real custom-entry route (#4252)
85+
or a genuinely missing asset that a naive SSR `/**` would render as a 200 page
86+
(#4234). It *can* be classified from the **response**: after Vite declines and
87+
the post catch-all dispatches to Nitro, a 2xx with a `text/html` content-type
88+
(inline check in the post middleware) means the catch-all rendered a page for a
89+
missing asset → the response is discarded via `next()` (connect `finalhandler`
90+
404, same as before). Anything else — real asset types, JSON, `text/plain`, no
91+
content-type, non-2xx (framework 404 pages, redirects) — passes through
92+
verbatim.
93+
94+
Deny-list rationale (all verified empirically):
95+
96+
- Only `text/html` counts as a swallow. `application/json` was originally
97+
denied too, but that broke real opaque frameworks: TanStack Start answers
98+
API routes tagged as asset loads with JSON on purpose
99+
(`<img src="/api/.../thumbnail">`, TanStack/router#7403, nitro PR #4274),
100+
and sourcemaps (`.map``ASSET_EXT_RE`) are legitimately JSON. The
101+
accepted trade-off: an SSR entry that swallows missing assets with *JSON*
102+
(pathological — real naive SSR renders HTML) now returns 200 JSON instead
103+
of 404 in dev.
104+
- `text/plain` is excluded because a bare string returned from an h3 handler
105+
has **no** content-type at the h3 layer but arrives as `text/plain;
106+
charset=UTF-8` — srvx's node-adapter default applied on the worker's HTTP hop
107+
(`srvx/dist/adapters/node.mjs`; runner-dependent but converges) — and must
108+
pass through (custom-entry handlers returning strings).
109+
- Transparent (user-file) catch-alls can **not** use inspection instead of the
110+
divert: their string 200s arrive with no distinguishing content-type.
111+
112+
This keeps everything zero-config, host-side only (no runtime/bundle change,
113+
no production behavior change). Known leftovers:
114+
115+
- **Production** SSR still renders 200 HTML for missing-asset URLs —
116+
pre-existing behavior; changing it is a separate, deliberate breaking-change
117+
discussion.
118+
- Opaque semantics require registration via `renderer` / `serverEntry`. A `/**`
119+
catch-all added through `routes:` / `handlers:` config or a module is
120+
classified **transparent** and stays pre-empted for asset-tagged requests
121+
(#4252-class limitation) — a framework integrating its renderer that way
122+
should use `renderer` instead.
123+
- Dev-only cost: a missing asset matching an opaque catch-all triggers a full
124+
(discarded) SSR render before the 404.
125+
126+
## 5. Issue / PR lineage (chronological)
127+
128+
- **#3649** first crude "has extension ⇒ Vite" rule; **#3804/#3805/#3817**
129+
middleware improvements, mounted-path skip, internal-prefix skip (survives as
130+
the `^\/(?:__|@)` guard); **#4098** `sec-websocket-protocol` to tell Vite HMR
131+
sockets from Nitro websockets (the `upgrade` handler).
132+
- **#4108** baseURL-aware matching for dotted Nitro routes
133+
(TanStack/router#6903).
134+
- **#4234** non-loopback plain-HTTP origins omit `Sec-Fetch-*` → splat swallowed
135+
`<script src>` loads. Fixed by **#4238**: `Accept` + `ASSET_EXT_RE` fallback +
136+
the `_nitroHandled` marker.
137+
- **#4241** #4238 over-eager: `sec-fetch-dest: image` on a real route
138+
(`/api/image`) sent to Vite → 404. Fixed by `7d49dcae` + `08f2ec69`
139+
(query-string stripped before extension matching).
140+
- **#4252 / #4270** even explicit routes lost when the URL had an asset
141+
extension. Fixed by **#4272** (`5b7e152b`): explicit vs catch-all vs none
142+
classification — **an explicit route is a deterministic win no heuristic can
143+
touch**.
144+
- `7765bcb7` added `isExplicitPublicAsset`.
145+
- **#4252 follow-ups** (katywings, jantimon/TanStack/router#7403): routes the
146+
classifier cannot see — a custom `server.ts` H3 app serving `/image.png`, or
147+
an SSR framework serving assets — were pre-empted by `_nitroHandled` and
148+
could never run. Fixed by the opaque-catch-all + response-inspection design
149+
above (§2, §4): the pre-emption now applies only to transparent user-file
150+
catch-alls, and opaque dispatches are judged by their response content-type.
151+
152+
## 6. Runtime dispatch flow
153+
154+
The catch-all `nitroDevMiddleware` dispatches in two stages:
155+
156+
1. **`ctx.devApp.fetch(req)`**`NitroDevApp` (`src/dev/app.ts`), host-side:
157+
`devHandlers`, `/_vfs/**`, `/_nitro/tasks`, public asset dirs, `devProxy`.
158+
No catch-all → a 404 means "not mine, hand off"; any non-404 is returned
159+
directly (never inspected — deterministic host serves are authoritative).
160+
2. **`nitroEnv.dispatchFetch(req)`** → env-runner → worker → `nitroApp.fetch`
161+
full route table, middleware, catch-alls.
162+
163+
Catch-all registration (`src/routing.ts`): a custom `serverEntry` is pushed as
164+
`/**` with `handler = nitro.options.serverEntry.handler`; the SSR renderer as
165+
`/**` with `handler = nitro.options.renderer.handler` (set by `plugin.ts`
166+
`configResolved` to `internal/vite/ssr-renderer` when an `ssr` service exists).
167+
These handler paths are how the pre-pass identifies opaque catch-alls.
168+
169+
## 7. Test coverage map
170+
171+
All under `test/vite/`; run via `test:rollup` and `test:rolldown`. Each starts
172+
a real Vite dev server and `fetch()`es with hand-set headers.
173+
174+
- `app.test.ts` (`app-fixture/`) — SSR `/**` path. #4234 swallow contracts
175+
(missing `.css`/`.js` under `style`/absent/`empty` dests must not 200 — the
176+
fixture entry renders an HTML page for extensioned misses), JSON API routes
177+
under `sec-fetch-dest: image` pass through (TanStack/router#7403), #4252
178+
deliberate asset serve (`/dynamic-asset.png``image/png` passes
179+
inspection), `HTTPError` propagation, navigations, storage/config sharing.
180+
- `server-entry.test.ts` (`server-entry-fixture/`) — #4252 custom `server.ts`
181+
H3 app: asset-extensioned routes reachable under `image`/absent/`script`
182+
dests (including a no-content-type string return), JSON sourcemap
183+
(`/generated.js.map`) passes through, `POST` to an asset-extensioned route
184+
reaches its handler (non-GET/HEAD are never assets), missing-asset 404
185+
contract, navigation.
186+
- `root-wildcard.test.ts` — transparent root catch-all `/**:path`: must never
187+
200 for `/entry-client.ts` under `script`/`style`/`image`/absent dests
188+
(#4234/#4266), navigations still reach it.
189+
- `baseurl-dotted-param.test.ts` — explicit splat routes under `baseURL:
190+
/subdir/`: #4241, #4252, #4270, query-string extension, unmatched asset →
191+
Vite.
192+
- Tangential: `hmr.test.ts` (existing module served by Vite under `script`
193+
dest), `openapi.test.ts` (explicit routes), others (build/env, not routing).
194+
195+
## 8. Gotchas
196+
197+
- The `server.middlewares.stack` scan in `nitroDevMiddleware` skips requests
198+
whose URL starts with any other middleware's mounted `base` (#3805).
199+
- `_nitroHandled` is a one-way latch set by the pre-pass (transparent
200+
catch-all asset) and by the post catch-all itself (re-entry guard). Never set
201+
it for a request a real or opaque handler should still see (#4252 root
202+
cause).
203+
- Extension detection must stay path-only (strip `?#`) and `ASSET_EXT_RE` must
204+
stay narrow, or dotted Nitro params regress (#4108).
205+
- The inspection's html-only check must not grow `text/plain` (bridge default
206+
for string returns) or `application/json` (deliberate API serves, #7403), and it must only apply to `envRes.ok` — framework 404 pages
207+
and redirects pass through verbatim.
208+
- The websocket `upgrade` handler is a separate concern sharing the "Vite's or
209+
Nitro's?" theme.

docs/.docs/components/AppHeroLinks.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
:href="`${baseURL}llms.txt`"
2222
target="_blank"
2323
class="basis-full text-sm text-muted hover:text-default transition-colors inline-flex items-center justify-center gap-1"
24-
@click.prevent="copyPrompt"
24+
@click="copyPrompt"
2525
>
2626
<UIcon :name="copied ? 'i-lucide-clipboard-check' : 'i-lucide-bot'" />
2727
Docs for AI

docs/1.docs/50.database.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,9 @@ const result = await stmt.bind("1001").all();
123123

124124
## Configuration
125125

126-
You can configure database connections using `database` config:
126+
You can configure database connections using `database` config.
127+
128+
Each connection is a `DatabaseConnectionConfig` with a `connector` name and an optional `options` object. Connector-specific settings (such as `url`, `host`, or `name`) belong under `options` — not at the top level of the connection config.
127129

128130
```ts [nitro.config.ts]
129131
import { defineConfig } from "nitro";
@@ -132,18 +134,30 @@ export default defineConfig({
132134
database: {
133135
default: {
134136
connector: "sqlite",
135-
options: { name: "db" }
137+
options: { name: "db" },
136138
},
137139
users: {
138140
connector: "postgresql",
139141
options: {
140-
url: "postgresql://username:password@hostname:port/database_name"
142+
url: "postgresql://username:password@hostname:port/database_name",
143+
},
144+
},
145+
analytics: {
146+
connector: "mysql2",
147+
options: {
148+
host: "localhost",
149+
port: 3306,
150+
user: "root",
151+
password: "password",
152+
database: "analytics",
141153
},
142154
},
143155
},
144156
});
145157
```
146158

159+
See the [db0 connector docs](https://db0.unjs.io/connectors) for the `options` each connector accepts.
160+
147161
### Development Database
148162

149163
Use the `devDatabase` config to override the database configuration **only for development mode**. This is useful for using a local SQLite database during development while targeting a different database in production.

docs/2.deploy/20.providers/cloudflare.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,33 @@ export default defineConfig({
103103

104104
No manual Wrangler configuration is needed - Nitro handles it for you.
105105

106+
### Tracing
107+
108+
**🧪 Experimental!**
109+
110+
When the experimental [`tracingChannel`](/config#tracingchannel) option is enabled, the Cloudflare presets report Nitro's tracing-channel events (h3 routes and middleware, srvx, unstorage operations, …) as [custom spans](https://developers.cloudflare.com/workers/observability/traces/custom-spans/), alongside Cloudflare's automatic instrumentation (fetch calls, KV reads, D1 queries, …) — no OpenTelemetry SDK required.
111+
112+
```ts [nitro.config.ts]
113+
import { defineConfig } from "nitro";
114+
115+
export default defineConfig({
116+
preset: "cloudflare_module",
117+
tracingChannel: true,
118+
});
119+
```
120+
121+
Tracing must be enabled on the Worker for spans to be recorded:
122+
123+
```jsonc [wrangler.jsonc]
124+
{
125+
"observability": {
126+
"traces": {
127+
"enabled": true
128+
}
129+
}
130+
}
131+
```
132+
106133
## Cloudflare Pages
107134

108135
**Preset:** `cloudflare_pages`

docs/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
"automd": "^0.4.3",
99
"geist": "^1.7.2",
1010
"motion-v": "^2.3.0",
11-
"shaders": "^2.5.130",
12-
"undocs": "npm:undocs-nightly@0.4.17-20260413-104322-a4a49c4",
11+
"shaders": "^2.5.135",
12+
"undocs": "npm:undocs-nightly@0.4.17-20260708-180838-02fc272",
1313
"zod": "^4.4.3"
1414
}
1515
}

0 commit comments

Comments
 (0)