@nifrajs/web 5.2.0 → 5.3.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.
- package/CHANGELOG.md +72 -0
- package/dist/build.d.ts +11 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +182 -11
- package/dist/build.js.map +1 -1
- package/dist/csp.d.ts +2 -0
- package/dist/csp.d.ts.map +1 -1
- package/dist/csp.js +2 -0
- package/dist/csp.js.map +1 -1
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +23 -3
- package/dist/deferred.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/deferred-free.d.ts +3 -0
- package/dist/internal/deferred-free.d.ts.map +1 -0
- package/dist/internal/deferred-free.js +15 -0
- package/dist/internal/deferred-free.js.map +1 -0
- package/dist/internal/module-preparation.d.ts +7 -0
- package/dist/internal/module-preparation.d.ts.map +1 -0
- package/dist/internal/module-preparation.js +24 -0
- package/dist/internal/module-preparation.js.map +1 -0
- package/dist/internal/output-guard.d.ts +2 -0
- package/dist/internal/output-guard.d.ts.map +1 -1
- package/dist/internal/output-guard.js +51 -9
- package/dist/internal/output-guard.js.map +1 -1
- package/dist/internal/page-execution.d.ts +1 -0
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +53 -43
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +3 -1
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/prerendered-pages.d.ts +42 -0
- package/dist/internal/prerendered-pages.d.ts.map +1 -0
- package/dist/internal/prerendered-pages.js +92 -0
- package/dist/internal/prerendered-pages.js.map +1 -0
- package/dist/internal/public-url.d.ts +7 -0
- package/dist/internal/public-url.d.ts.map +1 -1
- package/dist/internal/public-url.js +13 -0
- package/dist/internal/public-url.js.map +1 -1
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +93 -35
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/request-url.d.ts +4 -0
- package/dist/internal/request-url.d.ts.map +1 -1
- package/dist/internal/request-url.js +24 -0
- package/dist/internal/request-url.js.map +1 -1
- package/dist/internal/secret-scan.d.ts +4 -0
- package/dist/internal/secret-scan.d.ts.map +1 -1
- package/dist/internal/secret-scan.js +8 -1
- package/dist/internal/secret-scan.js.map +1 -1
- package/dist/internal/web-app.d.ts +11 -0
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +10 -1
- package/dist/internal/web-app.js.map +1 -1
- package/dist/isr.d.ts +1 -0
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +7 -4
- package/dist/isr.js.map +1 -1
- package/dist/prerender.d.ts +4 -0
- package/dist/prerender.d.ts.map +1 -1
- package/dist/prerender.js +9 -1
- package/dist/prerender.js.map +1 -1
- package/dist/public-dir.d.ts +15 -0
- package/dist/public-dir.d.ts.map +1 -1
- package/dist/public-dir.js +91 -12
- package/dist/public-dir.js.map +1 -1
- package/dist/render-seam.d.ts +5 -2
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js.map +1 -1
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +2 -0
- package/dist/search.js.map +1 -1
- package/dist/vite.d.ts +6 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +127 -54
- package/dist/vite.js.map +1 -1
- package/package.json +3 -3
- package/src/build.ts +226 -35
- package/src/csp.ts +4 -0
- package/src/deferred.ts +20 -3
- package/src/index.ts +3 -0
- package/src/internal/deferred-free.ts +15 -0
- package/src/internal/module-preparation.ts +30 -0
- package/src/internal/output-guard.ts +51 -9
- package/src/internal/page-execution.ts +55 -47
- package/src/internal/parity.ts +2 -1
- package/src/internal/prerendered-pages.ts +124 -0
- package/src/internal/public-url.ts +13 -0
- package/src/internal/render-document.ts +116 -52
- package/src/internal/request-url.ts +24 -0
- package/src/internal/secret-scan.ts +9 -1
- package/src/internal/web-app.ts +21 -1
- package/src/isr.ts +7 -4
- package/src/prerender.ts +13 -1
- package/src/public-dir.ts +107 -12
- package/src/render-seam.ts +5 -2
- package/src/search.ts +1 -0
- package/src/vite.ts +130 -54
package/src/vite.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Vite's `middlewares` are Connect-style - it runs fine under Bun.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { type Dirent, readdirSync, writeFileSync } from "node:fs"
|
|
14
|
+
import { type Dirent, existsSync, readdirSync, realpathSync, writeFileSync } from "node:fs"
|
|
15
15
|
import {
|
|
16
16
|
createServer as createHttpServer,
|
|
17
17
|
type IncomingMessage,
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
formatIdentityParityFindings,
|
|
34
34
|
identityParityHeadline,
|
|
35
35
|
} from "./internal/parity.ts"
|
|
36
|
+
import { isHiddenPublicPath } from "./internal/public-url.ts"
|
|
36
37
|
import { vitePublicEnvPrefix } from "./internal/server-boundary.ts"
|
|
37
38
|
import { importVite } from "./internal/vite-import.ts"
|
|
38
39
|
import { normalizeRolldownPlugins } from "./internal/vite-plugin-compat.ts"
|
|
@@ -77,6 +78,8 @@ interface ViteLike {
|
|
|
77
78
|
ssrFixStacktrace(err: Error): void
|
|
78
79
|
/** The module graph, used to invalidate a changed file AND its transitive importers before reload. */
|
|
79
80
|
readonly moduleGraph?: ViteModuleGraph
|
|
81
|
+
/** The resolved config: `publicDir` is absolute, or `""` when the public directory is off. */
|
|
82
|
+
readonly config?: { readonly publicDir?: string }
|
|
80
83
|
readonly watcher: {
|
|
81
84
|
on(event: "change" | "add" | "unlink", cb: (path: string) => void): void
|
|
82
85
|
on(event: "ready", cb: () => void): void
|
|
@@ -251,8 +254,13 @@ function toWebRequest(req: IncomingMessage, body: Buffer | undefined): Request {
|
|
|
251
254
|
|
|
252
255
|
/** The bits of a Node ServerResponse `pipeWebBodyToNode` touches - structural, to avoid a node:http dep here. */
|
|
253
256
|
interface NodeResLike {
|
|
257
|
+
/** Set once the socket is gone: `close` has fired, and a write returns `false` for good. */
|
|
258
|
+
readonly destroyed?: boolean
|
|
254
259
|
flushHeaders?(): void
|
|
255
260
|
on(event: "close", cb: () => void): void
|
|
261
|
+
// Optional so a bare sink still fits; without them a full buffer is not waited out.
|
|
262
|
+
once?(event: "drain", cb: () => void): void
|
|
263
|
+
off?(event: "close" | "drain", cb: () => void): void
|
|
256
264
|
write(chunk: Uint8Array): boolean
|
|
257
265
|
end(): void
|
|
258
266
|
}
|
|
@@ -282,6 +290,8 @@ export function applyResponseHeaders(headers: Headers, res: NodeHeaderSink): voi
|
|
|
282
290
|
* `arrayBuffer()`) waits for the stream to END - which an open-ended SSE (`text/event-stream`) body never
|
|
283
291
|
* does, so it hung `nifra dev` (the Bun production server streamed it fine). This flushes each chunk as it
|
|
284
292
|
* arrives and cancels the reader if the client disconnects; a finite body just streams its chunk(s) + ends.
|
|
293
|
+
* A write the socket can't take yet is waited out (`drain`) before the next read, so a slow client
|
|
294
|
+
* holds one chunk rather than the whole body.
|
|
285
295
|
*/
|
|
286
296
|
export async function pipeWebBodyToNode(
|
|
287
297
|
body: ReadableStream<Uint8Array> | null,
|
|
@@ -291,17 +301,48 @@ export async function pipeWebBodyToNode(
|
|
|
291
301
|
res.end()
|
|
292
302
|
return
|
|
293
303
|
}
|
|
304
|
+
// A call, so the check after an await is not narrowed by the one before it.
|
|
305
|
+
const destroyed = (): boolean => res.destroyed === true
|
|
306
|
+
// The client left while the app ran: `close` fired before anyone listened, and no `drain` comes.
|
|
307
|
+
if (destroyed()) {
|
|
308
|
+
await body.cancel().catch(() => {})
|
|
309
|
+
return
|
|
310
|
+
}
|
|
294
311
|
const reader = body.getReader()
|
|
295
312
|
res.flushHeaders?.()
|
|
296
|
-
|
|
313
|
+
let closed = false
|
|
314
|
+
let wake: (() => void) | undefined
|
|
315
|
+
const onClose = (): void => {
|
|
316
|
+
closed = true
|
|
317
|
+
void reader.cancel().catch(() => {})
|
|
318
|
+
wake?.()
|
|
319
|
+
}
|
|
320
|
+
res.on("close", onClose)
|
|
297
321
|
try {
|
|
298
322
|
for (;;) {
|
|
299
323
|
const { done, value } = await reader.read()
|
|
300
324
|
if (done) break
|
|
301
|
-
res.write(value)
|
|
325
|
+
if (!res.write(value) && !closed && res.once !== undefined) {
|
|
326
|
+
if (destroyed()) {
|
|
327
|
+
onClose()
|
|
328
|
+
break
|
|
329
|
+
}
|
|
330
|
+
let drain!: () => void
|
|
331
|
+
const drained = new Promise<void>((resolve) => {
|
|
332
|
+
drain = resolve
|
|
333
|
+
})
|
|
334
|
+
wake = drain
|
|
335
|
+
res.once("drain", drain)
|
|
336
|
+
await drained
|
|
337
|
+
res.off?.("drain", drain)
|
|
338
|
+
wake = undefined
|
|
339
|
+
}
|
|
302
340
|
}
|
|
303
341
|
} catch {
|
|
304
342
|
// client disconnected mid-stream - the `close` handler already cancelled the reader
|
|
343
|
+
} finally {
|
|
344
|
+
res.off?.("close", onClose)
|
|
345
|
+
reader.releaseLock()
|
|
305
346
|
}
|
|
306
347
|
res.end()
|
|
307
348
|
}
|
|
@@ -416,7 +457,38 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
|
|
|
416
457
|
handleWithVite(req, res)
|
|
417
458
|
})
|
|
418
459
|
|
|
460
|
+
// Vite's public middleware serves every file under the public directory, dotfiles included. A
|
|
461
|
+
// build ships no hidden file and the Bun pipeline's `servePublicDir` refuses one, so a request for
|
|
462
|
+
// `/.env` or `/.git/config` goes to the app here too, as it does in production.
|
|
463
|
+
function isHiddenPublicFile(url: string): boolean {
|
|
464
|
+
const configured = vite.config?.publicDir
|
|
465
|
+
if (configured === undefined || configured === "") return false
|
|
466
|
+
// Vite writes the path with `/`; resolved, it uses the platform's separator like `file`.
|
|
467
|
+
const publicDir = resolvePath(configured)
|
|
468
|
+
let rel: string
|
|
469
|
+
try {
|
|
470
|
+
rel = decodeURIComponent(new URL(url, "http://localhost").pathname).slice(1)
|
|
471
|
+
} catch {
|
|
472
|
+
return false
|
|
473
|
+
}
|
|
474
|
+
const file = resolvePath(publicDir, rel)
|
|
475
|
+
if (!file.startsWith(publicDir + sep) || !existsSync(file)) return false
|
|
476
|
+
if (isHiddenPublicPath(rel)) return true
|
|
477
|
+
// A visible name linking to a hidden file is refused too, as `servePublicDir` refuses it.
|
|
478
|
+
try {
|
|
479
|
+
const real = realpathSync(file)
|
|
480
|
+
const realRoot = realpathSync(publicDir)
|
|
481
|
+
return real.startsWith(realRoot + sep) && isHiddenPublicPath(real.slice(realRoot.length + 1))
|
|
482
|
+
} catch {
|
|
483
|
+
return false
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
419
487
|
function handleWithVite(req: IncomingMessage, res: ServerResponse): void {
|
|
488
|
+
if (isHiddenPublicFile(req.url ?? "/")) {
|
|
489
|
+
handleWithApp(req, res)
|
|
490
|
+
return
|
|
491
|
+
}
|
|
420
492
|
vite.middlewares(req, res, (error) => {
|
|
421
493
|
// In middleware mode Vite reports a module's transform error to the overlay and passes the
|
|
422
494
|
// request on. A request naming a source file was for a module, never a page: it gets the error,
|
|
@@ -437,60 +509,64 @@ export async function createViteDevServer(options: ViteDevServerOptions): Promis
|
|
|
437
509
|
return
|
|
438
510
|
}
|
|
439
511
|
// Not a Vite asset → nifra SSR. (`next` runs after Vite declines, so the body is still readable.)
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if (requestId !== null) res.setHeader(DEV_REQUEST_ID_HEADER, requestId)
|
|
463
|
-
if (overlay || session.isOverlay(nifraRes)) {
|
|
464
|
-
res.setHeader("content-type", "text/html; charset=utf-8")
|
|
465
|
-
res.end(await nifraRes.text())
|
|
466
|
-
return
|
|
467
|
-
}
|
|
468
|
-
if (!contentType.includes("text/html")) {
|
|
469
|
-
// Data / redirect / asset response - pass through untouched, streamed (SSE-safe). Set-Cookie is
|
|
470
|
-
// emitted per-header so multi-cookie responses (e.g. better-auth sessions) aren't collapsed.
|
|
471
|
-
applyResponseHeaders(nifraRes.headers, res)
|
|
472
|
-
await pipeWebBodyToNode(nifraRes.body, res)
|
|
473
|
-
return
|
|
512
|
+
handleWithApp(req, res)
|
|
513
|
+
})
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function handleWithApp(req: IncomingMessage, res: ServerResponse): void {
|
|
517
|
+
void (async () => {
|
|
518
|
+
try {
|
|
519
|
+
const body = await readNodeBody(req)
|
|
520
|
+
const webRequest = toWebRequest(req, body)
|
|
521
|
+
let overlay = false
|
|
522
|
+
const nifraRes = await session.track(webRequest, async () => {
|
|
523
|
+
try {
|
|
524
|
+
return await app.fetch(webRequest)
|
|
525
|
+
} catch (err) {
|
|
526
|
+
// Source-map the stack first (Vite maps the bundled frames back to your `.ts`), then render
|
|
527
|
+
// the readable dev overlay instead of a bare text dump. Dev-only - production maps to `_error`.
|
|
528
|
+
if (err instanceof Error) vite.ssrFixStacktrace(err)
|
|
529
|
+
overlay = true
|
|
530
|
+
return new Response(
|
|
531
|
+
session.failure(err, { method: req.method ?? "GET", url: req.url ?? "/" }),
|
|
532
|
+
{ status: 500, headers: { "content-type": "text/html; charset=utf-8" } },
|
|
533
|
+
)
|
|
474
534
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
applyResponseHeaders(headers, res)
|
|
535
|
+
})
|
|
536
|
+
const contentType = nifraRes.headers.get("content-type") ?? ""
|
|
537
|
+
res.statusCode = nifraRes.status
|
|
538
|
+
const requestId = nifraRes.headers.get(DEV_REQUEST_ID_HEADER)
|
|
539
|
+
if (requestId !== null) res.setHeader(DEV_REQUEST_ID_HEADER, requestId)
|
|
540
|
+
if (overlay || session.isOverlay(nifraRes)) {
|
|
482
541
|
res.setHeader("content-type", "text/html; charset=utf-8")
|
|
483
|
-
res.end(
|
|
484
|
-
|
|
485
|
-
// A failure past the app (reading the body, Vite's HTML transform): same overlay, same feed.
|
|
486
|
-
if (err instanceof Error) vite.ssrFixStacktrace(err)
|
|
487
|
-
const html = session.failure(err, { method: req.method ?? "GET", url: req.url ?? "/" })
|
|
488
|
-
res.statusCode = 500
|
|
489
|
-
res.setHeader("content-type", "text/html; charset=utf-8")
|
|
490
|
-
res.end(html)
|
|
542
|
+
res.end(await nifraRes.text())
|
|
543
|
+
return
|
|
491
544
|
}
|
|
492
|
-
|
|
493
|
-
|
|
545
|
+
if (!contentType.includes("text/html")) {
|
|
546
|
+
// Data / redirect / asset response - pass through untouched, streamed (SSE-safe). Set-Cookie is
|
|
547
|
+
// emitted per-header so multi-cookie responses (e.g. better-auth sessions) aren't collapsed.
|
|
548
|
+
applyResponseHeaders(nifraRes.headers, res)
|
|
549
|
+
await pipeWebBodyToNode(nifraRes.body, res)
|
|
550
|
+
return
|
|
551
|
+
}
|
|
552
|
+
// Inject Vite's HMR client + the framework's refresh preamble into the SSR'd HTML, keeping the
|
|
553
|
+
// app's headers (cookies, CSP, cache-control) as the Bun pipeline does.
|
|
554
|
+
const headers = new Headers(nifraRes.headers)
|
|
555
|
+
headers.delete("content-length") // the body grows with Vite's tags
|
|
556
|
+
const page = session.decorateHtml(webRequest, await nifraRes.text(), headers, requestId)
|
|
557
|
+
const html = admitViteTags(await vite.transformIndexHtml(req.url ?? "/", page), headers)
|
|
558
|
+
applyResponseHeaders(headers, res)
|
|
559
|
+
res.setHeader("content-type", "text/html; charset=utf-8")
|
|
560
|
+
res.end(html)
|
|
561
|
+
} catch (err) {
|
|
562
|
+
// A failure past the app (reading the body, Vite's HTML transform): same overlay, same feed.
|
|
563
|
+
if (err instanceof Error) vite.ssrFixStacktrace(err)
|
|
564
|
+
const html = session.failure(err, { method: req.method ?? "GET", url: req.url ?? "/" })
|
|
565
|
+
res.statusCode = 500
|
|
566
|
+
res.setHeader("content-type", "text/html; charset=utf-8")
|
|
567
|
+
res.end(html)
|
|
568
|
+
}
|
|
569
|
+
})()
|
|
494
570
|
}
|
|
495
571
|
|
|
496
572
|
// `conditions: ["bun"]` makes Vite resolve nifra's workspace packages (`@nifrajs/web-react/client`, …) to
|