@nifrajs/web 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/README.md +3 -3
  2. package/dist/build-vite.d.ts +4 -4
  3. package/dist/build-vite.d.ts.map +1 -1
  4. package/dist/build-vite.js +20 -74
  5. package/dist/build-vite.js.map +1 -1
  6. package/dist/build.d.ts +64 -42
  7. package/dist/build.d.ts.map +1 -1
  8. package/dist/build.js +100 -78
  9. package/dist/build.js.map +1 -1
  10. package/dist/bun-dev-entry.d.ts +1 -1
  11. package/dist/bun-dev-entry.js +1 -1
  12. package/dist/chunk-names.js +1 -1
  13. package/dist/client.d.ts +3 -9
  14. package/dist/client.d.ts.map +1 -1
  15. package/dist/client.js +201 -29
  16. package/dist/client.js.map +1 -1
  17. package/dist/conformance.d.ts +2 -2
  18. package/dist/conformance.js +3 -3
  19. package/dist/deferred.d.ts +11 -11
  20. package/dist/deferred.d.ts.map +1 -1
  21. package/dist/deferred.js +16 -16
  22. package/dist/dev-diagnostics.d.ts +20 -0
  23. package/dist/dev-diagnostics.d.ts.map +1 -0
  24. package/dist/dev-diagnostics.js +34 -0
  25. package/dist/dev-diagnostics.js.map +1 -0
  26. package/dist/dev-error.d.ts +10 -13
  27. package/dist/dev-error.d.ts.map +1 -1
  28. package/dist/dev-error.js +51 -37
  29. package/dist/dev-error.js.map +1 -1
  30. package/dist/dev-port.d.ts +3 -3
  31. package/dist/dev-port.d.ts.map +1 -1
  32. package/dist/dev-port.js +10 -4
  33. package/dist/dev-port.js.map +1 -1
  34. package/dist/dev.d.ts +4 -4
  35. package/dist/dev.d.ts.map +1 -1
  36. package/dist/dev.js +28 -12
  37. package/dist/dev.js.map +1 -1
  38. package/dist/diagnostic.d.ts +93 -0
  39. package/dist/diagnostic.d.ts.map +1 -0
  40. package/dist/diagnostic.js +205 -0
  41. package/dist/diagnostic.js.map +1 -0
  42. package/dist/draft.d.ts +12 -12
  43. package/dist/draft.js +11 -11
  44. package/dist/fn-state.d.ts +56 -0
  45. package/dist/fn-state.d.ts.map +1 -0
  46. package/dist/fn-state.js +77 -0
  47. package/dist/fn-state.js.map +1 -0
  48. package/dist/fn.d.ts +111 -0
  49. package/dist/fn.d.ts.map +1 -0
  50. package/dist/fn.js +125 -0
  51. package/dist/fn.js.map +1 -0
  52. package/dist/fonts-google.d.ts +9 -9
  53. package/dist/fonts-google.js +10 -10
  54. package/dist/fonts.d.ts +7 -7
  55. package/dist/fonts.js +1 -1
  56. package/dist/forms.d.ts +8 -8
  57. package/dist/fs.d.ts +1 -1
  58. package/dist/fs.js +1 -1
  59. package/dist/index.d.ts +151 -70
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +317 -139
  62. package/dist/index.js.map +1 -1
  63. package/dist/internal/head-attributes.d.ts +15 -0
  64. package/dist/internal/head-attributes.d.ts.map +1 -0
  65. package/dist/internal/head-attributes.js +99 -0
  66. package/dist/internal/head-attributes.js.map +1 -0
  67. package/dist/internal/script-types.d.ts +14 -0
  68. package/dist/internal/script-types.d.ts.map +1 -0
  69. package/dist/internal/script-types.js +17 -0
  70. package/dist/internal/script-types.js.map +1 -0
  71. package/dist/internal/server-boundary.d.ts +60 -0
  72. package/dist/internal/server-boundary.d.ts.map +1 -0
  73. package/dist/internal/server-boundary.js +174 -0
  74. package/dist/internal/server-boundary.js.map +1 -0
  75. package/dist/internal/timing-safe-equal.d.ts +1 -1
  76. package/dist/internal/timing-safe-equal.js +1 -1
  77. package/dist/internal/vite-import.d.ts +63 -0
  78. package/dist/internal/vite-import.d.ts.map +1 -0
  79. package/dist/internal/vite-import.js +94 -0
  80. package/dist/internal/vite-import.js.map +1 -0
  81. package/dist/islands.d.ts +5 -5
  82. package/dist/islands.js +5 -5
  83. package/dist/isr.d.ts +15 -15
  84. package/dist/isr.js +7 -7
  85. package/dist/manifest.d.ts +73 -35
  86. package/dist/manifest.d.ts.map +1 -1
  87. package/dist/manifest.js +10 -16
  88. package/dist/manifest.js.map +1 -1
  89. package/dist/module-graph.d.ts +5 -5
  90. package/dist/module-graph.js +4 -4
  91. package/dist/navigation.d.ts +120 -5
  92. package/dist/navigation.d.ts.map +1 -1
  93. package/dist/navigation.js +37 -5
  94. package/dist/navigation.js.map +1 -1
  95. package/dist/plugins/css-modules.d.ts +3 -3
  96. package/dist/plugins/css-modules.js +0 -0
  97. package/dist/plugins/kit.d.ts +8 -8
  98. package/dist/plugins/kit.js +9 -9
  99. package/dist/plugins/scss.js +3 -3
  100. package/dist/plugins/vite-leak-guard.d.ts +1 -1
  101. package/dist/plugins/vite-leak-guard.js +1 -1
  102. package/dist/plugins/vite-server-fn.d.ts +23 -0
  103. package/dist/plugins/vite-server-fn.d.ts.map +1 -0
  104. package/dist/plugins/vite-server-fn.js +51 -0
  105. package/dist/plugins/vite-server-fn.js.map +1 -0
  106. package/dist/plugins/vite-server-only.d.ts +49 -0
  107. package/dist/plugins/vite-server-only.d.ts.map +1 -0
  108. package/dist/plugins/vite-server-only.js +57 -0
  109. package/dist/plugins/vite-server-only.js.map +1 -0
  110. package/dist/prerender.d.ts +5 -5
  111. package/dist/prerender.js +7 -7
  112. package/dist/query.d.ts +10 -10
  113. package/dist/query.js +15 -15
  114. package/dist/route-manifest.d.ts +1 -1
  115. package/dist/route-manifest.js +2 -2
  116. package/dist/router.d.ts +30 -21
  117. package/dist/router.d.ts.map +1 -1
  118. package/dist/router.js +62 -32
  119. package/dist/router.js.map +1 -1
  120. package/dist/search.d.ts +94 -0
  121. package/dist/search.d.ts.map +1 -0
  122. package/dist/search.js +232 -0
  123. package/dist/search.js.map +1 -0
  124. package/dist/server-only.d.ts +6 -6
  125. package/dist/server-only.js +7 -7
  126. package/dist/service-worker.d.ts +56 -0
  127. package/dist/service-worker.d.ts.map +1 -0
  128. package/dist/service-worker.js +127 -0
  129. package/dist/service-worker.js.map +1 -0
  130. package/dist/vite.d.ts +16 -14
  131. package/dist/vite.d.ts.map +1 -1
  132. package/dist/vite.js +51 -24
  133. package/dist/vite.js.map +1 -1
  134. package/package.json +38 -4
  135. package/server-manifest.d.ts +31 -0
  136. package/src/build-vite.ts +24 -77
  137. package/src/build.ts +137 -105
  138. package/src/bun-dev-entry.ts +1 -1
  139. package/src/chunk-names.ts +1 -1
  140. package/src/client.ts +215 -32
  141. package/src/conformance.ts +3 -3
  142. package/src/deferred.ts +17 -17
  143. package/src/dev-diagnostics.ts +47 -0
  144. package/src/dev-error.ts +64 -33
  145. package/src/dev-port.ts +17 -5
  146. package/src/dev.ts +36 -20
  147. package/src/diagnostic.ts +284 -0
  148. package/src/draft.ts +15 -15
  149. package/src/fn-state.ts +107 -0
  150. package/src/fn.ts +199 -0
  151. package/src/fonts-google.ts +13 -13
  152. package/src/fonts.ts +7 -7
  153. package/src/forms.ts +8 -8
  154. package/src/fs.ts +2 -2
  155. package/src/index.ts +460 -181
  156. package/src/internal/head-attributes.ts +109 -0
  157. package/src/internal/script-types.ts +18 -0
  158. package/src/internal/server-boundary.ts +187 -0
  159. package/src/internal/timing-safe-equal.ts +1 -1
  160. package/src/internal/vite-import.ts +92 -0
  161. package/src/islands.ts +7 -7
  162. package/src/isr.ts +17 -17
  163. package/src/manifest.ts +83 -42
  164. package/src/module-graph.ts +6 -6
  165. package/src/navigation.ts +151 -5
  166. package/src/plugins/css-modules.ts +0 -0
  167. package/src/plugins/kit.ts +11 -11
  168. package/src/plugins/scss.ts +3 -3
  169. package/src/plugins/vite-leak-guard.ts +2 -2
  170. package/src/plugins/vite-server-fn.ts +67 -0
  171. package/src/plugins/vite-server-only.ts +75 -0
  172. package/src/prerender.ts +10 -10
  173. package/src/query.ts +22 -22
  174. package/src/route-manifest.ts +3 -3
  175. package/src/router.ts +98 -45
  176. package/src/search.ts +297 -0
  177. package/src/server-only.ts +7 -7
  178. package/src/service-worker.ts +165 -0
  179. package/src/vite.ts +61 -32
package/src/vite.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  /**
2
- * `@nifrajs/web/vite` — a **dev-only** server with true HMR, backed by Vite in middleware mode. nifra's
2
+ * `@nifrajs/web/vite` - a **dev-only** server with true HMR, backed by Vite in middleware mode. nifra's
3
3
  * production pipeline stays Bun-native (`@nifrajs/web/build` → `Bun.build`); this is purely for the dev
4
4
  * loop. Bun's own HMR is dev-server-only and DCE's `import.meta.hot` under `Bun.build` (nifra's bundler),
5
5
  * so HMR comes from Vite + the framework's official Vite plugin (React Fast Refresh, Vue/Svelte/Solid
6
- * HMR) — which you inject via `plugins` (the same structural-injection idiom as the Bun/codec plugins).
6
+ * HMR) - which you inject via `plugins` (the same structural-injection idiom as the Bun/codec plugins).
7
7
  *
8
8
  * Flow: Vite serves + HMR-swaps the client modules (nifra's codegen'd entry + the route files); nifra
9
9
  * still **SSRs** each request, and the rendered HTML is run through `vite.transformIndexHtml` so Vite
10
10
  * injects its HMR client + the framework's refresh preamble. Node `http` (not `Bun.serve`) because
11
- * Vite's `middlewares` are Connect-style — it runs fine under Bun.
11
+ * Vite's `middlewares` are Connect-style - it runs fine under Bun.
12
12
  */
13
13
  import { writeFileSync } from "node:fs"
14
14
  import {
@@ -17,12 +17,16 @@ import {
17
17
  type ServerResponse,
18
18
  } from "node:http"
19
19
  import { relative, resolve as resolvePath } from "node:path"
20
- import { renderDevErrorOverlay } from "./dev-error.ts"
20
+ import { createDevDiagnostics } from "./dev-diagnostics.ts"
21
21
  import { listenOrExplain } from "./dev-port.ts"
22
22
  import { discoverRoutes } from "./fs.ts"
23
23
  import { DEFAULT_DEV_PORT, generateClientEntry } from "./index.ts"
24
+ import { vitePublicEnvPrefix } from "./internal/server-boundary.ts"
25
+ import { importVite } from "./internal/vite-import.ts"
26
+ import { viteServerFnStub } from "./plugins/vite-server-fn.ts"
27
+ import { viteServerOnlyEmpty } from "./plugins/vite-server-only.ts"
24
28
 
25
- /** Minimal app surface — `createWebApp(...)` satisfies it. */
29
+ /** Minimal app surface - `createWebApp(...)` satisfies it. */
26
30
  interface FetchApp {
27
31
  fetch(request: Request): Response | Promise<Response>
28
32
  }
@@ -45,7 +49,7 @@ interface ViteModule {
45
49
  readonly rolldownVersion?: string
46
50
  }
47
51
 
48
- // `node:http` server type the request handler runs on (also the HMR WebSocket host — see below).
52
+ // `node:http` server type the request handler runs on (also the HMR WebSocket host - see below).
49
53
  type NodeHttpServer = ReturnType<typeof createHttpServer>
50
54
 
51
55
  export interface ViteDevServerOptions {
@@ -66,10 +70,10 @@ export interface ViteDevServerOptions {
66
70
  clientEntry: string,
67
71
  load: (absolutePath: string) => Promise<unknown>,
68
72
  ) => FetchApp | Promise<FetchApp>
69
- /** Vite plugins — inject your framework's official plugin, e.g. `[react()]`. */
73
+ /** Vite plugins - inject your framework's official plugin, e.g. `[react()]`. */
70
74
  readonly plugins?: readonly unknown[]
71
75
  /**
72
- * Extra `resolve.conditions` prepended ahead of nifra's defaults — some frameworks need their own
76
+ * Extra `resolve.conditions` prepended ahead of nifra's defaults - some frameworks need their own
73
77
  * (e.g. Solid's `"solid"` condition routes `solid-js` to its source/JSX-dev build).
74
78
  */
75
79
  readonly conditions?: readonly string[]
@@ -84,12 +88,14 @@ export interface ViteDevServerOptions {
84
88
  readonly port?: number
85
89
  /**
86
90
  * Use polling for the file watcher. Native fs events (fsevents/inotify) are unreliable inside
87
- * containers, networked filesystems, and some sandboxes — there, HMR silently never fires. Set
91
+ * containers, networked filesystems, and some sandboxes - there, HMR silently never fires. Set
88
92
  * `true` (or the env var `CHOKIDAR_USEPOLLING=1`) to poll instead. Default: off (native events).
89
93
  */
90
94
  readonly poll?: boolean
91
95
  /** Vite public directory. Defaults to `<root>/public`; `false` disables it. */
92
96
  readonly publicDir?: string | false
97
+ /** Client-visible environment prefix (default `"PUBLIC_"`; empty disables exposure). */
98
+ readonly publicEnvPrefix?: string
93
99
  }
94
100
 
95
101
  export interface ViteDevServer {
@@ -97,6 +103,8 @@ export interface ViteDevServer {
97
103
  stop(): Promise<void>
98
104
  }
99
105
 
106
+ export { LAST_ERROR_PATH } from "./diagnostic.ts"
107
+
100
108
  // The codegen'd client entry is written here (at the Vite root) so Vite serves + HMRs it.
101
109
  const DEV_ENTRY = ".nifra-vite-entry.tsx"
102
110
 
@@ -128,11 +136,11 @@ function toWebRequest(req: IncomingMessage, body: Buffer | undefined): Request {
128
136
  }
129
137
 
130
138
  // A Vite `config` hook: a plain function, OR the object form `{ handler, order }` Vite accepts for hook
131
- // ordering (`order: "pre" | "post"`). We wrap the handler in either shape — see normalizeRolldownPlugins.
139
+ // ordering (`order: "pre" | "post"`). We wrap the handler in either shape - see normalizeRolldownPlugins.
132
140
  type ConfigFn = (config: unknown, env: unknown) => unknown
133
141
  type ConfigHook = ConfigFn | { readonly handler: ConfigFn; readonly order?: unknown }
134
142
 
135
- // A Vite plugin — the only hook we wrap is `config`. Typed structurally (no `vite` type dep). Everything
143
+ // A Vite plugin - the only hook we wrap is `config`. Typed structurally (no `vite` type dep). Everything
136
144
  // else on the plugin object is preserved by spread, so wrapping is transparent to Vite.
137
145
  interface VitePluginLike {
138
146
  readonly name?: string
@@ -140,7 +148,7 @@ interface VitePluginLike {
140
148
  readonly [key: string]: unknown
141
149
  }
142
150
 
143
- /** The bits of a Node ServerResponse `pipeWebBodyToNode` touches — structural, to avoid a node:http dep here. */
151
+ /** The bits of a Node ServerResponse `pipeWebBodyToNode` touches - structural, to avoid a node:http dep here. */
144
152
  interface NodeResLike {
145
153
  flushHeaders?(): void
146
154
  on(event: "close", cb: () => void): void
@@ -155,7 +163,7 @@ interface NodeHeaderSink {
155
163
 
156
164
  /**
157
165
  * Copy a Web `Response`'s headers onto a Node response, emitting EACH `Set-Cookie` as its own header. The
158
- * `Headers` iterator (and `.get`) join multiple set-cookie values with ", ", which corrupts cookies — e.g.
166
+ * `Headers` iterator (and `.get`) join multiple set-cookie values with ", ", which corrupts cookies - e.g.
159
167
  * better-auth's `session_token` + `session_data` collapse into one unparseable cookie and the session is
160
168
  * silently lost. `getSetCookie()` returns them split; Node's `setHeader` emits one header per array element.
161
169
  */
@@ -170,7 +178,7 @@ export function applyResponseHeaders(headers: Headers, res: NodeHeaderSink): voi
170
178
 
171
179
  /**
172
180
  * Stream a Web `Response` body to a Node response chunk-by-chunk. Buffering the whole body (e.g.
173
- * `arrayBuffer()`) waits for the stream to END — which an open-ended SSE (`text/event-stream`) body never
181
+ * `arrayBuffer()`) waits for the stream to END - which an open-ended SSE (`text/event-stream`) body never
174
182
  * does, so it hung `nifra dev` (the Bun production server streamed it fine). This flushes each chunk as it
175
183
  * arrives and cancels the reader if the client disconnects; a finite body just streams its chunk(s) + ends.
176
184
  */
@@ -192,23 +200,23 @@ export async function pipeWebBodyToNode(
192
200
  res.write(value)
193
201
  }
194
202
  } catch {
195
- // client disconnected mid-stream — the `close` handler already cancelled the reader
203
+ // client disconnected mid-stream - the `close` handler already cancelled the reader
196
204
  }
197
205
  res.end()
198
206
  }
199
207
 
200
208
  /**
201
209
  * Strip `optimizeDeps.rollupOptions.jsx` from a plugin's `config` hook output when running under
202
- * rolldown-vite — the source of the scary, harmless `Warning: Invalid input options … "jsx" Invalid
210
+ * rolldown-vite - the source of the scary, harmless `Warning: Invalid input options … "jsx" Invalid
203
211
  * key: Expected never but received "jsx"` on `nifra dev`.
204
212
  *
205
213
  * Why it happens: `@vitejs/plugin-react@4.x` (and peers) target an *older* rolldown-vite optimizeDeps
206
- * API — they inject `optimizeDeps.rollupOptions.jsx` to tell the dep pre-bundler to transform JSX. Vite
214
+ * API - they inject `optimizeDeps.rollupOptions.jsx` to tell the dep pre-bundler to transform JSX. Vite
207
215
  * 8's rolldown dep-optimizer renamed that surface to `optimizeDeps.rolldownOptions` (and moved jsx under
208
216
  * `transform.jsx`), so the stale `rollupOptions.jsx` is an unrecognized input option → the warning. It's
209
217
  * a version-skew artifact, not a real misconfig: the route source JSX transform runs through the
210
218
  * plugin's own `transform` hook (untouched here), and node_modules deps that get pre-bundled almost
211
- * never contain raw JSX — so dropping the dead key changes no behavior and keeps HMR/Fast Refresh
219
+ * never contain raw JSX - so dropping the dead key changes no behavior and keeps HMR/Fast Refresh
212
220
  * intact. We *strip* (rather than translate to `rolldownOptions`) so the fix is version-agnostic: a
213
221
  * plugin already emitting the correct `rolldownOptions` is left untouched, and a future plugin bump that
214
222
  * stops emitting `rollupOptions.jsx` makes this a no-op.
@@ -216,12 +224,12 @@ export async function pipeWebBodyToNode(
216
224
  * Scoped narrowly: only the `optimizeDeps.rollupOptions.jsx` key is removed, only under rolldown-vite,
217
225
  * and only from the value a plugin's `config` hook returns. Non-rolldown Vite is passed through verbatim.
218
226
  *
219
- * FLATTEN FIRST: a Vite plugin factory may return an ARRAY of plugins — `@vitejs/plugin-react`'s `react()`
227
+ * FLATTEN FIRST: a Vite plugin factory may return an ARRAY of plugins - `@vitejs/plugin-react`'s `react()`
220
228
  * returns `[vite:react-babel, vite:react-refresh]`, and it's `react:react-babel`'s `config` hook that emits
221
229
  * the offending `optimizeDeps.rollupOptions.jsx`. `nifra.config.ts` writes `vitePlugins = [react()]`, so the
222
230
  * plugin list arrives NESTED (`[[babel, refresh]]`). Without flattening, `.map` sees the inner array (which
223
- * has no `config`), leaves it untouched, and Vite — which flattens plugin arrays itself before running them
224
- * — then executes the un-stripped babel hook, so the warning survives. Flattening here (Vite accepts a flat
231
+ * has no `config`), leaves it untouched, and Vite - which flattens plugin arrays itself before running them
232
+ * - then executes the un-stripped babel hook, so the warning survives. Flattening here (Vite accepts a flat
225
233
  * list identically) is what lets the strip reach every real plugin.
226
234
  */
227
235
  export function normalizeRolldownPlugins(
@@ -251,7 +259,7 @@ export function normalizeRolldownPlugins(
251
259
  if (typeof handler !== "function") return plugin
252
260
  const wrappedHandler: ConfigFn = (config, env) => {
253
261
  const returned = handler(config, env)
254
- // The hook may return a promise — normalize both shapes.
262
+ // The hook may return a promise - normalize both shapes.
255
263
  return returned instanceof Promise ? returned.then(stripJsxKey) : stripJsxKey(returned)
256
264
  }
257
265
  // Preserve the ORIGINAL shape: a function stays a function; the object form keeps its `order`
@@ -285,14 +293,25 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
285
293
  const entryUrl = `/${DEV_ENTRY}`
286
294
 
287
295
  // Create our HTTP server FIRST, then hand it to Vite as the HMR WebSocket host (`hmr.server`). In
288
- // middleware mode Vite would otherwise open a *separate* ws port (24678) — fragile: it conflicts
296
+ // middleware mode Vite would otherwise open a *separate* ws port (24678) - fragile: it conflicts
289
297
  // across restarts ("Port undefined is already in use" → the client never connects). Sharing one
290
298
  // port means HMR rides the same origin as the app, robust across restarts. The handler closes over
291
299
  // `vite`, which is assigned just below before the server starts listening.
292
300
  let vite!: ViteLike
293
301
  let app: FetchApp
302
+ // The most recent SSR failure, served as JSON at LAST_ERROR_PATH. Shared with the Bun adapter so the
303
+ // endpoint and its headers can't drift between the two dev servers.
304
+ const devDiagnostics = createDevDiagnostics(root)
294
305
 
295
306
  const server: NodeHttpServer = createHttpServer((req, res) => {
307
+ // Keep this before Vite's middleware: it is an agent endpoint owned by nifra, not a file or app route.
308
+ if (devDiagnostics.isLastErrorPath((req.url ?? "/").split("?", 1)[0] ?? "/")) {
309
+ const { body, headers } = devDiagnostics.lastError()
310
+ res.statusCode = 200
311
+ for (const [key, value] of Object.entries(headers)) res.setHeader(key, value)
312
+ res.end(body)
313
+ return
314
+ }
296
315
  vite.middlewares(req, res, () => {
297
316
  // Not a Vite asset → nifra SSR. (`next` runs after Vite declines, so the body is still readable.)
298
317
  void (async () => {
@@ -302,7 +321,7 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
302
321
  const contentType = nifraRes.headers.get("content-type") ?? ""
303
322
  res.statusCode = nifraRes.status
304
323
  if (!contentType.includes("text/html")) {
305
- // Data / redirect / asset response — pass through untouched, streamed (SSE-safe). Set-Cookie is
324
+ // Data / redirect / asset response - pass through untouched, streamed (SSE-safe). Set-Cookie is
306
325
  // emitted per-header so multi-cookie responses (e.g. better-auth sessions) aren't collapsed.
307
326
  applyResponseHeaders(nifraRes.headers, res)
308
327
  await pipeWebBodyToNode(nifraRes.body, res)
@@ -314,29 +333,37 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
314
333
  res.end(html)
315
334
  } catch (err) {
316
335
  // Source-map the stack first (Vite maps the bundled frames back to your `.ts`), then render
317
- // the readable dev overlay instead of a bare text dump. Dev-only — production maps to `_error`.
336
+ // the readable dev overlay instead of a bare text dump. Dev-only - production maps to `_error`.
318
337
  if (err instanceof Error) vite.ssrFixStacktrace(err)
338
+ const html = devDiagnostics.capture(err, {
339
+ method: req.method ?? "GET",
340
+ url: req.url ?? "/",
341
+ })
319
342
  res.statusCode = 500
320
343
  res.setHeader("content-type", "text/html; charset=utf-8")
321
- res.end(renderDevErrorOverlay(err, { method: req.method ?? "GET", url: req.url ?? "/" }))
344
+ res.end(html)
322
345
  }
323
346
  })()
324
347
  })
325
348
  })
326
349
 
327
350
  // `conditions: ["bun"]` makes Vite resolve nifra's workspace packages (`@nifrajs/web-react/client`, …) to
328
- // their TS **source** — so the dev server needs no prior `dist` build of the adapter packages.
351
+ // their TS **source** - so the dev server needs no prior `dist` build of the adapter packages.
329
352
  const usePolling = options.poll ?? process.env.CHOKIDAR_USEPOLLING === "1"
330
- const viteModule = (await import("vite")) as unknown as ViteModule
353
+ // Never `import("vite")` directly here: the guard in `importVite` has to run first, and the dev
354
+ // server importing vite unguarded is precisely what poisoned the module for the whole process.
355
+ const viteModule = await importVite<ViteModule>()
331
356
  const { createServer } = viteModule
332
357
  // Under rolldown-vite (Vite 8+), strip the stale `optimizeDeps.rollupOptions.jsx` some framework
333
- // plugins still emit — it triggers a noisy "Invalid key … jsx" warning but does nothing useful here.
358
+ // plugins still emit - it triggers a noisy "Invalid key … jsx" warning but does nothing useful here.
334
359
  const plugins = normalizeRolldownPlugins(
335
360
  options.plugins ?? [],
336
361
  viteModule.rolldownVersion !== undefined,
337
362
  )
338
363
  vite = await createServer({
339
364
  root,
365
+ // Dev must not inherit Vite's default `VITE_*` exposure beside Nifra's documented boundary.
366
+ envPrefix: vitePublicEnvPrefix(options.publicEnvPrefix),
340
367
  ...(options.publicDir !== undefined ? { publicDir: options.publicDir } : {}),
341
368
  appType: "custom",
342
369
  server: {
@@ -345,13 +372,15 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
345
372
  // Explicit watch config; poll when native fs events aren't delivered (containers/sandboxes).
346
373
  watch: usePolling ? { usePolling: true, interval: 80 } : {},
347
374
  },
348
- plugins: [...plugins],
375
+ // Ahead of the user's plugins: a `*.fn` module must be replaced before anything else
376
+ // reads it, and the dev server is a client bundler like any other.
377
+ plugins: [viteServerFnStub(), viteServerOnlyEmpty(), ...plugins],
349
378
  resolve: {
350
379
  conditions: [...(options.conditions ?? []), "bun", "module", "browser", "development"],
351
380
  // Dedupe React to ONE copy. In a multi-root workspace a shared package can pull react/react-dom
352
381
  // from a SIBLING app's node_modules, so the dev server loads two React cores → a second hook
353
382
  // dispatcher → `resolveDispatcher().useState` null on any hook-using route (the error points at the
354
- // component, not the resolution — brutal to diagnose). Mirrors the build-time reactDedupePlugin so
383
+ // component, not the resolution - brutal to diagnose). Mirrors the build-time reactDedupePlugin so
355
384
  // dev matches prod. No-op for non-React apps (the package simply isn't present to dedupe).
356
385
  dedupe: ["react", "react-dom"],
357
386
  },
@@ -383,7 +412,7 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
383
412
  }
384
413
 
385
414
  try {
386
- await listenOrExplain(server, port)
415
+ await listenOrExplain(server, port, "127.0.0.1")
387
416
  } catch (err) {
388
417
  // Nothing is listening, but Vite is fully up by now - watchers, the dep optimizer, its own sockets -
389
418
  // and every one of those keeps the event loop alive. Left open, the process prints the diagnosis and