@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.
- package/README.md +3 -3
- package/dist/build-vite.d.ts +4 -4
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +20 -74
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +64 -42
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +100 -78
- package/dist/build.js.map +1 -1
- package/dist/bun-dev-entry.d.ts +1 -1
- package/dist/bun-dev-entry.js +1 -1
- package/dist/chunk-names.js +1 -1
- package/dist/client.d.ts +3 -9
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +201 -29
- package/dist/client.js.map +1 -1
- package/dist/conformance.d.ts +2 -2
- package/dist/conformance.js +3 -3
- package/dist/deferred.d.ts +11 -11
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +16 -16
- package/dist/dev-diagnostics.d.ts +20 -0
- package/dist/dev-diagnostics.d.ts.map +1 -0
- package/dist/dev-diagnostics.js +34 -0
- package/dist/dev-diagnostics.js.map +1 -0
- package/dist/dev-error.d.ts +10 -13
- package/dist/dev-error.d.ts.map +1 -1
- package/dist/dev-error.js +51 -37
- package/dist/dev-error.js.map +1 -1
- package/dist/dev-port.d.ts +3 -3
- package/dist/dev-port.d.ts.map +1 -1
- package/dist/dev-port.js +10 -4
- package/dist/dev-port.js.map +1 -1
- package/dist/dev.d.ts +4 -4
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +28 -12
- package/dist/dev.js.map +1 -1
- package/dist/diagnostic.d.ts +93 -0
- package/dist/diagnostic.d.ts.map +1 -0
- package/dist/diagnostic.js +205 -0
- package/dist/diagnostic.js.map +1 -0
- package/dist/draft.d.ts +12 -12
- package/dist/draft.js +11 -11
- package/dist/fn-state.d.ts +56 -0
- package/dist/fn-state.d.ts.map +1 -0
- package/dist/fn-state.js +77 -0
- package/dist/fn-state.js.map +1 -0
- package/dist/fn.d.ts +111 -0
- package/dist/fn.d.ts.map +1 -0
- package/dist/fn.js +125 -0
- package/dist/fn.js.map +1 -0
- package/dist/fonts-google.d.ts +9 -9
- package/dist/fonts-google.js +10 -10
- package/dist/fonts.d.ts +7 -7
- package/dist/fonts.js +1 -1
- package/dist/forms.d.ts +8 -8
- package/dist/fs.d.ts +1 -1
- package/dist/fs.js +1 -1
- package/dist/index.d.ts +151 -70
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +317 -139
- package/dist/index.js.map +1 -1
- package/dist/internal/head-attributes.d.ts +15 -0
- package/dist/internal/head-attributes.d.ts.map +1 -0
- package/dist/internal/head-attributes.js +99 -0
- package/dist/internal/head-attributes.js.map +1 -0
- package/dist/internal/script-types.d.ts +14 -0
- package/dist/internal/script-types.d.ts.map +1 -0
- package/dist/internal/script-types.js +17 -0
- package/dist/internal/script-types.js.map +1 -0
- package/dist/internal/server-boundary.d.ts +60 -0
- package/dist/internal/server-boundary.d.ts.map +1 -0
- package/dist/internal/server-boundary.js +174 -0
- package/dist/internal/server-boundary.js.map +1 -0
- package/dist/internal/timing-safe-equal.d.ts +1 -1
- package/dist/internal/timing-safe-equal.js +1 -1
- package/dist/internal/vite-import.d.ts +63 -0
- package/dist/internal/vite-import.d.ts.map +1 -0
- package/dist/internal/vite-import.js +94 -0
- package/dist/internal/vite-import.js.map +1 -0
- package/dist/islands.d.ts +5 -5
- package/dist/islands.js +5 -5
- package/dist/isr.d.ts +15 -15
- package/dist/isr.js +7 -7
- package/dist/manifest.d.ts +73 -35
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +10 -16
- package/dist/manifest.js.map +1 -1
- package/dist/module-graph.d.ts +5 -5
- package/dist/module-graph.js +4 -4
- package/dist/navigation.d.ts +120 -5
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +37 -5
- package/dist/navigation.js.map +1 -1
- package/dist/plugins/css-modules.d.ts +3 -3
- package/dist/plugins/css-modules.js +0 -0
- package/dist/plugins/kit.d.ts +8 -8
- package/dist/plugins/kit.js +9 -9
- package/dist/plugins/scss.js +3 -3
- package/dist/plugins/vite-leak-guard.d.ts +1 -1
- package/dist/plugins/vite-leak-guard.js +1 -1
- package/dist/plugins/vite-server-fn.d.ts +23 -0
- package/dist/plugins/vite-server-fn.d.ts.map +1 -0
- package/dist/plugins/vite-server-fn.js +51 -0
- package/dist/plugins/vite-server-fn.js.map +1 -0
- package/dist/plugins/vite-server-only.d.ts +49 -0
- package/dist/plugins/vite-server-only.d.ts.map +1 -0
- package/dist/plugins/vite-server-only.js +57 -0
- package/dist/plugins/vite-server-only.js.map +1 -0
- package/dist/prerender.d.ts +5 -5
- package/dist/prerender.js +7 -7
- package/dist/query.d.ts +10 -10
- package/dist/query.js +15 -15
- package/dist/route-manifest.d.ts +1 -1
- package/dist/route-manifest.js +2 -2
- package/dist/router.d.ts +30 -21
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +62 -32
- package/dist/router.js.map +1 -1
- package/dist/search.d.ts +94 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +232 -0
- package/dist/search.js.map +1 -0
- package/dist/server-only.d.ts +6 -6
- package/dist/server-only.js +7 -7
- package/dist/service-worker.d.ts +56 -0
- package/dist/service-worker.d.ts.map +1 -0
- package/dist/service-worker.js +127 -0
- package/dist/service-worker.js.map +1 -0
- package/dist/vite.d.ts +16 -14
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +51 -24
- package/dist/vite.js.map +1 -1
- package/package.json +38 -4
- package/server-manifest.d.ts +31 -0
- package/src/build-vite.ts +24 -77
- package/src/build.ts +137 -105
- package/src/bun-dev-entry.ts +1 -1
- package/src/chunk-names.ts +1 -1
- package/src/client.ts +215 -32
- package/src/conformance.ts +3 -3
- package/src/deferred.ts +17 -17
- package/src/dev-diagnostics.ts +47 -0
- package/src/dev-error.ts +64 -33
- package/src/dev-port.ts +17 -5
- package/src/dev.ts +36 -20
- package/src/diagnostic.ts +284 -0
- package/src/draft.ts +15 -15
- package/src/fn-state.ts +107 -0
- package/src/fn.ts +199 -0
- package/src/fonts-google.ts +13 -13
- package/src/fonts.ts +7 -7
- package/src/forms.ts +8 -8
- package/src/fs.ts +2 -2
- package/src/index.ts +460 -181
- package/src/internal/head-attributes.ts +109 -0
- package/src/internal/script-types.ts +18 -0
- package/src/internal/server-boundary.ts +187 -0
- package/src/internal/timing-safe-equal.ts +1 -1
- package/src/internal/vite-import.ts +92 -0
- package/src/islands.ts +7 -7
- package/src/isr.ts +17 -17
- package/src/manifest.ts +83 -42
- package/src/module-graph.ts +6 -6
- package/src/navigation.ts +151 -5
- package/src/plugins/css-modules.ts +0 -0
- package/src/plugins/kit.ts +11 -11
- package/src/plugins/scss.ts +3 -3
- package/src/plugins/vite-leak-guard.ts +2 -2
- package/src/plugins/vite-server-fn.ts +67 -0
- package/src/plugins/vite-server-only.ts +75 -0
- package/src/prerender.ts +10 -10
- package/src/query.ts +22 -22
- package/src/route-manifest.ts +3 -3
- package/src/router.ts +98 -45
- package/src/search.ts +297 -0
- package/src/server-only.ts +7 -7
- package/src/service-worker.ts +165 -0
- package/src/vite.ts +61 -32
package/src/build.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@nifrajs/web/build`
|
|
2
|
+
* `@nifrajs/web/build` - the production build (Bun-only, build-time). `buildClient` codegens + bundles
|
|
3
3
|
* the client entry (content-hashed, code-split); `buildServer` codegens the static-import server
|
|
4
4
|
* manifest + bundles a self-contained **worker** for the disk-less edge (Cloudflare Workers). Both
|
|
5
5
|
* are Bun-specific and never on the request path (own subpath, like `@nifrajs/web/fs`); the *output*
|
|
@@ -12,6 +12,13 @@ import { type BunPlugin, Glob } from "bun"
|
|
|
12
12
|
import { sanitizeOutputNames } from "./chunk-names.ts"
|
|
13
13
|
import { discoverRoutes } from "./fs.ts"
|
|
14
14
|
import { generateClientEntry, generateServerManifest } from "./index.ts"
|
|
15
|
+
import {
|
|
16
|
+
generateServerFnStub,
|
|
17
|
+
SERVER_FN_MODULE,
|
|
18
|
+
SERVER_ONLY_MODULE,
|
|
19
|
+
SERVER_ONLY_REPLACEMENT,
|
|
20
|
+
serverFnNamespace,
|
|
21
|
+
} from "./internal/server-boundary.ts"
|
|
15
22
|
// `buildTarget(static)` drives the SSG prerender engine directly (it's also re-exported below).
|
|
16
23
|
import { type ClientModuleGraph, fromBunMetafile } from "./module-graph.ts"
|
|
17
24
|
import { prerenderRoutes } from "./prerender.ts"
|
|
@@ -63,7 +70,7 @@ export interface BuildClientOptions {
|
|
|
63
70
|
* Prefix that opts an environment variable into the **client** bundle (Vite/Next convention; default
|
|
64
71
|
* `"PUBLIC_"`). Every var in the build environment whose name starts with this prefix is baked into
|
|
65
72
|
* the client `define` as `"process.env.NAME": JSON.stringify(value)`, so `process.env.PUBLIC_API_URL`
|
|
66
|
-
* compiles to its literal value in the browser. Vars WITHOUT the prefix are never exposed
|
|
73
|
+
* compiles to its literal value in the browser. Vars WITHOUT the prefix are never exposed - the bare
|
|
67
74
|
* `process.env` define resolves them to `undefined`, so server secrets can't leak into the client
|
|
68
75
|
* bundle. Set to `""` to disable auto-exposure entirely (no var is baked in). `options.define` still
|
|
69
76
|
* wins over an auto-exposed var (it's layered last). Sourced from `Bun.env` (falls back to
|
|
@@ -242,7 +249,7 @@ export function cloudflareRouteRules(
|
|
|
242
249
|
return { include, exclude, omitted }
|
|
243
250
|
}
|
|
244
251
|
|
|
245
|
-
/** The built asset map
|
|
252
|
+
/** The built asset map - the server reads `entry` for the client script + serves `assets`. */
|
|
246
253
|
/**
|
|
247
254
|
* Copy `from` into `to`, returning the URL paths copied (sorted).
|
|
248
255
|
*
|
|
@@ -276,9 +283,9 @@ export async function copyPublicDir(from: string, to: string): Promise<string[]>
|
|
|
276
283
|
export interface BuildManifest {
|
|
277
284
|
/** URL of the client entry module (content-hashed). */
|
|
278
285
|
readonly entry: string
|
|
279
|
-
/** URLs of every emitted asset (entry + chunks)
|
|
286
|
+
/** URLs of every emitted asset (entry + chunks) - for serving + preloading. */
|
|
280
287
|
readonly assets: readonly string[]
|
|
281
|
-
/** `routeId → [layout chunk URLs…, own chunk URL]`
|
|
288
|
+
/** `routeId → [layout chunk URLs…, own chunk URL]` - the chunks a route needs, for `createWebApp`'s
|
|
282
289
|
* `routePreload` (`<link rel="modulepreload">` the matched route alongside the entry). Each route +
|
|
283
290
|
* layout is also a build entrypoint, so it gets a named chunk the bootstrap's lazy import dedupes to. */
|
|
284
291
|
readonly routes: Readonly<Record<string, readonly string[]>>
|
|
@@ -286,16 +293,16 @@ export interface BuildManifest {
|
|
|
286
293
|
* directory per request, and lets an adapter that needs a file list (CDN upload, platform static
|
|
287
294
|
* assets) consume one. Omitted when there is no `public/`. */
|
|
288
295
|
readonly publicFiles?: readonly string[]
|
|
289
|
-
/** The app's bundled, content-hashed stylesheet(s)
|
|
296
|
+
/** The app's bundled, content-hashed stylesheet(s) - the bootstrap's **aggregate** CSS (every
|
|
290
297
|
* `import './x.css'` reachable from the app). The complete stylesheet regardless of which file
|
|
291
298
|
* imported the CSS; the always-safe fallback `createWebApp` links when a route has no per-route entry
|
|
292
299
|
* in {@link routeStyles}. Omitted when the app imports no CSS. */
|
|
293
300
|
readonly css?: readonly string[]
|
|
294
|
-
/** `routeId → [chain CSS URLs]`
|
|
301
|
+
/** `routeId → [chain CSS URLs]` - only the stylesheets the matched route's layout chain + own file
|
|
295
302
|
* actually use (Bun emits a per-entrypoint CSS bundle per route/layout, with shared-component CSS
|
|
296
303
|
* inlined into each consumer). `createWebApp` links these instead of the aggregate, so a page ships
|
|
297
304
|
* only its own CSS. A route is omitted (→ aggregate fallback) when its `[name]` collides with another
|
|
298
|
-
* route's basename (ambiguous CSS↔route) or the build emitted orphan shared-chunk CSS
|
|
305
|
+
* route's basename (ambiguous CSS↔route) or the build emitted orphan shared-chunk CSS - correctness
|
|
299
306
|
* over minimality. Absent entirely when the app imports no CSS. */
|
|
300
307
|
readonly routeStyles?: Readonly<Record<string, readonly string[]>>
|
|
301
308
|
}
|
|
@@ -348,10 +355,10 @@ export interface NodeBuiltinFinding {
|
|
|
348
355
|
* BFS the metafile import graph for the SHORTEST user-module path that pulls `builtin` into the
|
|
349
356
|
* bundle, returning it as display labels `[entryFile, …as-written specifiers…, builtin]`. The frontier
|
|
350
357
|
* starts at every `entryInput` (the route/user entrypoints) so the reported chain begins where the dev
|
|
351
|
-
* actually wrote `import`
|
|
358
|
+
* actually wrote `import` - the actionable root, not an arbitrary internal module. Traversal crosses
|
|
352
359
|
* only NON-`node:` edges (so Bun's polyfill chain never extends the path) and stops at the first edge
|
|
353
360
|
* whose target is the builtin. Returns `[builtin]` if no entry reaches it (defensive; a flagged builtin
|
|
354
|
-
* is reachable by construction). Pure
|
|
361
|
+
* is reachable by construction). Pure - operates on the graph, never the emitted text.
|
|
355
362
|
*/
|
|
356
363
|
function shortestBuiltinChain(
|
|
357
364
|
inputs: Readonly<Record<string, { readonly imports?: readonly BunMetafileImport[] }>>,
|
|
@@ -375,7 +382,7 @@ function shortestBuiltinChain(
|
|
|
375
382
|
if (nodeBuiltinOf(im) === builtin) return [...chain, builtin]
|
|
376
383
|
const target = im.path
|
|
377
384
|
// Only follow edges into user (non-`node:`) modules that exist in the input graph and haven't
|
|
378
|
-
// been visited
|
|
385
|
+
// been visited - so the polyfill subtree can't lengthen the path and there are no cycles.
|
|
379
386
|
if (
|
|
380
387
|
target === undefined ||
|
|
381
388
|
target.startsWith("node:") ||
|
|
@@ -386,24 +393,24 @@ function shortestBuiltinChain(
|
|
|
386
393
|
}
|
|
387
394
|
seen.add(target)
|
|
388
395
|
// Display the hop by its as-written specifier (`../db/client.ts`, `postgres`), falling back to
|
|
389
|
-
// the resolved path
|
|
396
|
+
// the resolved path - that's what the dev recognizes in their source, not the resolved path.
|
|
390
397
|
next.push({ node: target, chain: [...chain, im.original ?? target] })
|
|
391
398
|
}
|
|
392
399
|
}
|
|
393
400
|
frontier = next
|
|
394
401
|
}
|
|
395
|
-
return [builtin] // unreachable from any entry
|
|
402
|
+
return [builtin] // unreachable from any entry - degrade to just the builtin (shouldn't happen)
|
|
396
403
|
}
|
|
397
404
|
|
|
398
405
|
/**
|
|
399
406
|
* Scan a build's metafile for any `node:` builtin that a USER module pulled into a CLIENT output
|
|
400
407
|
* chunk, returning a sorted, deduped list of {@link NodeBuiltinFinding}s. Three graph facts combine so
|
|
401
408
|
* the report is precise AND actionable:
|
|
402
|
-
* 1. **What the user wrote**
|
|
409
|
+
* 1. **What the user wrote** - only builtins imported by a NON-`node:` input count, so Bun's own
|
|
403
410
|
* polyfill chain (`node:crypto` → `node:buffer`/`node:stream`/…) doesn't bury the real cause.
|
|
404
|
-
* 2. **Where it landed**
|
|
411
|
+
* 2. **Where it landed** - the chunk is read from the per-output `inputs`, so the error names the
|
|
405
412
|
* emitted file to look at.
|
|
406
|
-
* 3. **How it got there**
|
|
413
|
+
* 3. **How it got there** - the shortest import chain from a user entry to the builtin
|
|
407
414
|
* (`shortestBuiltinChain`), so the error points straight at the offending `import` line instead of
|
|
408
415
|
* leaving the dev to grep the dependency tree (the DX gap this closes).
|
|
409
416
|
* Graph-based (never the emitted text), so it survives minification and can't be fooled by a string
|
|
@@ -413,16 +420,16 @@ export function detectNodeBuiltinsInClient(
|
|
|
413
420
|
graph: ClientModuleGraph,
|
|
414
421
|
): ReadonlyArray<NodeBuiltinFinding> {
|
|
415
422
|
const inputs = graph.modules
|
|
416
|
-
// (1) The builtins a user (non-polyfill) module imports directly
|
|
423
|
+
// (1) The builtins a user (non-polyfill) module imports directly - the ones the author controls.
|
|
417
424
|
const userImported = new Set<string>()
|
|
418
425
|
for (const [inputKey, input] of Object.entries(inputs)) {
|
|
419
|
-
if (inputKey.startsWith("node:")) continue // a polyfill importing another builtin
|
|
426
|
+
if (inputKey.startsWith("node:")) continue // a polyfill importing another builtin - not the cause
|
|
420
427
|
for (const im of input.imports ?? []) {
|
|
421
428
|
const builtin = nodeBuiltinOf(im)
|
|
422
429
|
if (builtin !== undefined) userImported.add(builtin)
|
|
423
430
|
}
|
|
424
431
|
}
|
|
425
|
-
// The entry inputs (route/user entrypoints)
|
|
432
|
+
// The entry inputs (route/user entrypoints) - the chain BFS starts here so the reported path begins
|
|
426
433
|
// at the file the dev wrote an `import` in. Each output's `entryPoint` is one such input.
|
|
427
434
|
const entryInputs = [
|
|
428
435
|
...new Set(
|
|
@@ -464,8 +471,8 @@ export function detectNodeBuiltinsInClient(
|
|
|
464
471
|
// node-builtin guard: a module of PURE server logic with NO `node:` import (a secret-bearing constant,
|
|
465
472
|
// a server-only API call) that an author wants to FAIL LOUD if it reaches the client opts in with a
|
|
466
473
|
// side-effect `import "@nifrajs/web/server-only"` (Next's `import "server-only"`). On the SERVER build
|
|
467
|
-
// the marker is an empty no-op; the CLIENT build detects
|
|
468
|
-
// node-builtin guard walks
|
|
474
|
+
// the marker is an empty no-op; the CLIENT build detects - via the SAME Bun metafile graph the
|
|
475
|
+
// node-builtin guard walks - any module that imports the marker AND lands in a client chunk, and fails
|
|
469
476
|
// the build with the import chain. Graph-based (never the emitted text), so it survives minification.
|
|
470
477
|
// ---------------------------------------------------------------------------------------------------
|
|
471
478
|
|
|
@@ -476,7 +483,7 @@ export const SERVER_ONLY_MARKER = "@nifrajs/web/server-only"
|
|
|
476
483
|
|
|
477
484
|
/** True when an import edge is the `server-only` marker import. Reads the as-written `original` (the
|
|
478
485
|
* specifier the author typed); falls back to the resolved `path`'s basename so a pre-resolved edge
|
|
479
|
-
* (no `original`)
|
|
486
|
+
* (no `original`) - or a workspace-relative resolution - is still recognised. */
|
|
480
487
|
const isServerOnlyMarkerImport = (im: BunMetafileImport): boolean => {
|
|
481
488
|
if (im.original === SERVER_ONLY_MARKER) return true
|
|
482
489
|
// Defensive fallback: an edge that lost its `original` but resolved to the marker module file. The
|
|
@@ -504,8 +511,8 @@ export interface ServerOnlyFinding {
|
|
|
504
511
|
* "<module> (marked server-only)"]`. Mirrors {@link shortestBuiltinChain}: the frontier starts at
|
|
505
512
|
* every `entryInput` so the chain begins at the file the dev wrote an `import` in; traversal crosses
|
|
506
513
|
* only NON-`node:` user edges (no cycles via `seen`); it stops at the first node whose import set
|
|
507
|
-
* contains the marker (the marked module
|
|
508
|
-
* specifier the previous hop used to reach it. Pure
|
|
514
|
+
* contains the marker (the marked module - the actionable tail), labelling that node by the as-written
|
|
515
|
+
* specifier the previous hop used to reach it. Pure - operates on the graph, never the emitted text.
|
|
509
516
|
*/
|
|
510
517
|
function shortestServerOnlyChain(
|
|
511
518
|
inputs: Readonly<Record<string, { readonly imports?: readonly BunMetafileImport[] }>>,
|
|
@@ -516,7 +523,7 @@ function shortestServerOnlyChain(
|
|
|
516
523
|
// The label for the marked module's tail: its as-written specifier (filled when we cross the edge
|
|
517
524
|
// that reaches it) suffixed with `(marked server-only)`; the entry case uses the entry key itself.
|
|
518
525
|
const tail = (label: string): string => `${label} (marked server-only)`
|
|
519
|
-
// An entry that is ITSELF the marked module
|
|
526
|
+
// An entry that is ITSELF the marked module - the chain is just that one node.
|
|
520
527
|
if (entryInputs.includes(markedModule)) return [tail(markedModule)]
|
|
521
528
|
const seen = new Set<string>(entryInputs)
|
|
522
529
|
let frontier: Array<{ node: string; chain: string[] }> = entryInputs.map((node) => ({
|
|
@@ -527,7 +534,7 @@ function shortestServerOnlyChain(
|
|
|
527
534
|
const next: Array<{ node: string; chain: string[] }> = []
|
|
528
535
|
for (const { node, chain } of frontier) {
|
|
529
536
|
for (const im of inputs[node]?.imports ?? []) {
|
|
530
|
-
// Resolve the edge's `path` to the matching INPUT-GRAPH KEY
|
|
537
|
+
// Resolve the edge's `path` to the matching INPUT-GRAPH KEY - in a real build the edge `path`
|
|
531
538
|
// is absolute while the input keys are cwd-relative, so a raw equality/lookup would miss every
|
|
532
539
|
// multi-hop user edge (and the chain would degrade to just the tail). The resolver maps both.
|
|
533
540
|
const target = resolveTarget(im)
|
|
@@ -574,16 +581,16 @@ function inputKeyResolver(
|
|
|
574
581
|
* Scan a build's metafile for any module that opts into the `server-only` marker (a side-effect
|
|
575
582
|
* `import "@nifrajs/web/server-only"`) yet landed in a CLIENT output chunk, returning a sorted, deduped
|
|
576
583
|
* list of {@link ServerOnlyFinding}s. Mirrors {@link detectNodeBuiltinsInClient}: it reads the SAME
|
|
577
|
-
* graph facts
|
|
584
|
+
* graph facts - which inputs import the marker (the "marked" modules), which chunk each landed in (the
|
|
578
585
|
* per-output `inputs`), and the shortest import chain from a user entry to it. The marker module ITSELF
|
|
579
|
-
* (which imports nothing) is excluded
|
|
586
|
+
* (which imports nothing) is excluded - only the modules that *opt in* are flagged. Pure + exported for
|
|
580
587
|
* unit testing. Empty ⇒ clean.
|
|
581
588
|
*/
|
|
582
589
|
export function detectServerOnlyInClient(
|
|
583
590
|
graph: ClientModuleGraph,
|
|
584
591
|
): ReadonlyArray<ServerOnlyFinding> {
|
|
585
592
|
const inputs = graph.modules
|
|
586
|
-
// (1) The modules that import the marker
|
|
593
|
+
// (1) The modules that import the marker - the ones the author opted into the guard. The marker
|
|
587
594
|
// module itself is skipped: it's the import TARGET, not an opt-in (it imports nothing of its own).
|
|
588
595
|
const marked = new Set<string>()
|
|
589
596
|
for (const [inputKey, input] of Object.entries(inputs)) {
|
|
@@ -591,7 +598,7 @@ export function detectServerOnlyInClient(
|
|
|
591
598
|
if ((input.imports ?? []).some(isServerOnlyMarkerImport)) marked.add(inputKey)
|
|
592
599
|
}
|
|
593
600
|
if (marked.size === 0) return []
|
|
594
|
-
// The entry inputs
|
|
601
|
+
// The entry inputs - the chain BFS starts here, so the reported path begins at the file the dev
|
|
595
602
|
// wrote an `import` in. Same derivation as the node-builtin guard.
|
|
596
603
|
const entryInputs = [
|
|
597
604
|
...new Set(
|
|
@@ -627,12 +634,12 @@ export function detectServerOnlyInClient(
|
|
|
627
634
|
}
|
|
628
635
|
|
|
629
636
|
/** True when an INPUT graph key is the marker module file itself (`…/server-only.{ts,js}` under web).
|
|
630
|
-
* Excluded from the "marked" set
|
|
637
|
+
* Excluded from the "marked" set - the marker is the import target, not an opt-in module. */
|
|
631
638
|
const isServerOnlyMarkerModule = (inputKey: string): boolean =>
|
|
632
639
|
/(^|\/)server-only\.[cm]?[jt]s$/.test(inputKey)
|
|
633
640
|
|
|
634
641
|
// ---------------------------------------------------------------------------------------------------
|
|
635
|
-
// Guard MESSAGES
|
|
642
|
+
// Guard MESSAGES - one owner for both production pipelines. The Bun build (below) and the Vite/Rollup
|
|
636
643
|
// leak-guard plugin (plugins/vite-leak-guard.ts) both call these, so a leak reads IDENTICALLY whichever
|
|
637
644
|
// bundler produced it. A second bundler must not grow a second, subtly-different wording of a security
|
|
638
645
|
// error; that is exactly the "mostly ported" outcome the neutral graph seam exists to prevent.
|
|
@@ -649,7 +656,7 @@ export function formatNodeBuiltinLeak(
|
|
|
649
656
|
: ` - ${f.builtin} reached the client bundle via ${f.chunk}`,
|
|
650
657
|
)
|
|
651
658
|
return (
|
|
652
|
-
`[nifra/web] Node built-in(s) in the client bundle
|
|
659
|
+
`[nifra/web] Node built-in(s) in the client bundle - move them behind a server-only path ` +
|
|
653
660
|
`(a loader/action runs on the server; import the \`node:\` module there, not at a route's ` +
|
|
654
661
|
`top level):\n${lines.join("\n")}`
|
|
655
662
|
)
|
|
@@ -666,7 +673,7 @@ export function formatServerOnlyLeak(
|
|
|
666
673
|
: ` - server-only module reached the client bundle via ${f.chunk}`,
|
|
667
674
|
)
|
|
668
675
|
return (
|
|
669
|
-
`[nifra/web] server-only module(s) in the client bundle
|
|
676
|
+
`[nifra/web] server-only module(s) in the client bundle - a module marked ` +
|
|
670
677
|
`\`import "${SERVER_ONLY_MARKER}"\` reached the browser. Move it behind a server-only path ` +
|
|
671
678
|
`(reach it via a loader/action, or rename it \`*.server.ts\`), so its server logic never ships ` +
|
|
672
679
|
`to the client:\n${lines.join("\n")}`
|
|
@@ -697,7 +704,7 @@ export interface ChunkSize {
|
|
|
697
704
|
readonly gzip: number
|
|
698
705
|
}
|
|
699
706
|
|
|
700
|
-
/** A whole build's size report
|
|
707
|
+
/** A whole build's size report - every chunk (largest first) + the totals. */
|
|
701
708
|
export interface SizeReport {
|
|
702
709
|
/** Per-chunk sizes, sorted biggest gzip first (the regression you want to see at the top). */
|
|
703
710
|
readonly chunks: readonly ChunkSize[]
|
|
@@ -709,7 +716,7 @@ export interface SizeReport {
|
|
|
709
716
|
|
|
710
717
|
/**
|
|
711
718
|
* Aggregate a list of measured chunks into a {@link SizeReport}: sort biggest-gzip-first (ties broken
|
|
712
|
-
* by raw bytes, then name for stable output) and sum the totals. Pure
|
|
719
|
+
* by raw bytes, then name for stable output) and sum the totals. Pure - the measurement (reading the
|
|
713
720
|
* file + `Bun.gzipSync`) happens in the orchestrator; this is the deterministic, unit-testable core.
|
|
714
721
|
*/
|
|
715
722
|
export function aggregateSizeReport(chunks: readonly ChunkSize[]): SizeReport {
|
|
@@ -733,7 +740,7 @@ export function formatBytes(bytes: number): string {
|
|
|
733
740
|
}
|
|
734
741
|
|
|
735
742
|
/**
|
|
736
|
-
* Render a {@link SizeReport} as a terse aligned table (biggest first) with a totals row
|
|
743
|
+
* Render a {@link SizeReport} as a terse aligned table (biggest first) with a totals row - the text
|
|
737
744
|
* `nifra build --report` prints. Pure (string in, string out) so the formatting is unit-testable.
|
|
738
745
|
*/
|
|
739
746
|
export function renderSizeReport(report: SizeReport): string {
|
|
@@ -762,18 +769,18 @@ export function renderSizeReport(report: SizeReport): string {
|
|
|
762
769
|
// ---------------------------------------------------------------------------------------------------
|
|
763
770
|
// Server-manifest drift detection (#7). `server-manifest.ts` is a committed generated file: it bakes
|
|
764
771
|
// the route list + the client-entry hash for a disk-less worker (`generateServerManifest`). If `routes/`
|
|
765
|
-
// changes but the manifest isn't regenerated, the worker serves a stale route table
|
|
772
|
+
// changes but the manifest isn't regenerated, the worker serves a stale route table - a silent edge
|
|
766
773
|
// break. These pure helpers diff the COMMITTED manifest source against the freshly-discovered routes so
|
|
767
774
|
// `nifra check` (and `buildServer`) can fail with a named, actionable error before the drift ships.
|
|
768
775
|
// ---------------------------------------------------------------------------------------------------
|
|
769
776
|
|
|
770
777
|
/** A drift finding between a committed server-manifest and the live `routes/` tree. */
|
|
771
778
|
export interface ManifestDrift {
|
|
772
|
-
/** Route files present in `routes/` but ABSENT from the committed manifest (the manifest is stale
|
|
779
|
+
/** Route files present in `routes/` but ABSENT from the committed manifest (the manifest is stale -
|
|
773
780
|
* the new route won't be served by the worker). */
|
|
774
781
|
readonly missing: readonly string[]
|
|
775
782
|
/** Route files the committed manifest imports that no longer exist in `routes/` (a deleted/renamed
|
|
776
|
-
* route still wired into the worker
|
|
783
|
+
* route still wired into the worker - a build/runtime break). */
|
|
777
784
|
readonly extra: readonly string[]
|
|
778
785
|
}
|
|
779
786
|
|
|
@@ -789,7 +796,7 @@ const MANIFEST_CLIENT_ENTRY = /export\s+const\s+clientEntry\s*=\s*["']([^"']+)["
|
|
|
789
796
|
* `routes/`-relative keys `discoverRoutes` produces (e.g. `docs/index.tsx`). `routesPrefix` is the
|
|
790
797
|
* specifier prefix the manifest used for the routes dir (default `./routes/`, what `buildServer`'s
|
|
791
798
|
* default `resolve` emits). Only import specifiers under that prefix are route files; the
|
|
792
|
-
* `@nifrajs/web` import (and any other bare specifier) is ignored. Pure
|
|
799
|
+
* `@nifrajs/web` import (and any other bare specifier) is ignored. Pure - operates on source text.
|
|
793
800
|
*/
|
|
794
801
|
export function parseManifestRouteFiles(source: string, routesPrefix = "./routes/"): string[] {
|
|
795
802
|
const files = new Set<string>()
|
|
@@ -871,8 +878,8 @@ export function resyncServerManifestSource(
|
|
|
871
878
|
|
|
872
879
|
/**
|
|
873
880
|
* Diff the route files a committed server-manifest imports against the files freshly discovered in
|
|
874
|
-
* `routes/`. Returns the `missing` (in routes/, not in manifest
|
|
875
|
-
* manifest, gone from routes/
|
|
881
|
+
* `routes/`. Returns the `missing` (in routes/, not in manifest - stale manifest) and `extra` (in
|
|
882
|
+
* manifest, gone from routes/ - dangling import) sets. Empty arrays ⇒ in sync. Pure - the caller
|
|
876
883
|
* supplies both file lists (the committed source is parsed via {@link parseManifestRouteFiles}; the
|
|
877
884
|
* fresh list comes from `discoverRoutes`). Lists need not be pre-sorted; the result is sorted.
|
|
878
885
|
*/
|
|
@@ -903,16 +910,16 @@ export function formatManifestDrift(
|
|
|
903
910
|
): string | undefined {
|
|
904
911
|
if (isManifestInSync(drift)) return undefined
|
|
905
912
|
const lines: string[] = [
|
|
906
|
-
`[nifra/web] server-manifest drift
|
|
913
|
+
`[nifra/web] server-manifest drift - \`${manifestPath}\` is out of sync with routes/.`,
|
|
907
914
|
]
|
|
908
915
|
if (drift.missing.length > 0) {
|
|
909
916
|
lines.push(
|
|
910
|
-
` Missing (in routes/, not in the manifest
|
|
917
|
+
` Missing (in routes/, not in the manifest - these routes won't be served): ${drift.missing.join(", ")}`,
|
|
911
918
|
)
|
|
912
919
|
}
|
|
913
920
|
if (drift.extra.length > 0) {
|
|
914
921
|
lines.push(
|
|
915
|
-
` Extra (imported by the manifest, gone from routes/
|
|
922
|
+
` Extra (imported by the manifest, gone from routes/ - a dangling import): ${drift.extra.join(", ")}`,
|
|
916
923
|
)
|
|
917
924
|
}
|
|
918
925
|
lines.push(" Fix: re-run the build to regenerate the server manifest, then commit it.")
|
|
@@ -921,7 +928,7 @@ export function formatManifestDrift(
|
|
|
921
928
|
|
|
922
929
|
/**
|
|
923
930
|
* Build the client bundle for a file-routed app. Writes the hashed assets + `manifest.json` to
|
|
924
|
-
* `outDir` and returns the manifest. Throws (with the bundler logs) on build failure
|
|
931
|
+
* `outDir` and returns the manifest. Throws (with the bundler logs) on build failure - never
|
|
925
932
|
* silently ships a broken bundle.
|
|
926
933
|
*/
|
|
927
934
|
export async function buildClient(options: BuildClientOptions): Promise<BuildManifest> {
|
|
@@ -937,7 +944,7 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
937
944
|
// PUBLIC_*-prefixed env → client define (Vite/Next convention). Sourced from the build env (`Bun.env`,
|
|
938
945
|
// falling back to `process.env`). Only prefixed vars are baked in; unprefixed secrets stay undefined
|
|
939
946
|
// in the client via the bare `process.env` → `({})` define below. Caller's `options.define` wins (it's
|
|
940
|
-
// layered after these in the `define` object). `Bun` may be absent under non-Bun typecheck
|
|
947
|
+
// layered after these in the `define` object). `Bun` may be absent under non-Bun typecheck - guard it.
|
|
941
948
|
const buildEnv = (typeof Bun !== "undefined" ? Bun.env : undefined) ?? process.env
|
|
942
949
|
const publicDefines = publicEnvDefines(options.publicEnvPrefix ?? "PUBLIC_", buildEnv)
|
|
943
950
|
// Keep the generated source beside the project, not inside `outDir`: module resolution starts at the
|
|
@@ -952,7 +959,7 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
952
959
|
`globalThis.process ??= { env: {} };\n${generateClientEntry(routeManifest, { clientModule, resolve })}`,
|
|
953
960
|
)
|
|
954
961
|
|
|
955
|
-
// Every unique route/layout/`_404` file (sorted, stable), as ADDITIONAL entrypoints
|
|
962
|
+
// Every unique route/layout/`_404` file (sorted, stable), as ADDITIONAL entrypoints - Bun emits a
|
|
956
963
|
// named chunk per file that the bootstrap's lazy `import()` dedupes to (verified), so the manifest
|
|
957
964
|
// can map each route to its chunk URLs for matched-route preload. `resolve(file)` is the same
|
|
958
965
|
// specifier the bootstrap imports, so the entrypoint + lazy import are the same module (dedup).
|
|
@@ -964,7 +971,7 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
964
971
|
]),
|
|
965
972
|
].sort()
|
|
966
973
|
|
|
967
|
-
// `metafile: true` asks Bun for the input/output graph
|
|
974
|
+
// `metafile: true` asks Bun for the input/output graph - specifically `outputs[js].entryPoint`
|
|
968
975
|
// (the source file) + `outputs[js].cssBundle` (that entry's emitted stylesheet). It's the robust
|
|
969
976
|
// entry→CSS link for per-route splitting: keyed by the unique source path, so it survives
|
|
970
977
|
// same-basename collisions (`index.tsx` + `blog/index.tsx`) that a filename match can't. Not yet in
|
|
@@ -981,7 +988,7 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
981
988
|
splitting: true, // one chunk per lazily-imported route; shared deps deduped into shared chunks
|
|
982
989
|
// `import "./x.css"` in a route/component → bundled, minified, content-hashed `.css` asset (Bun
|
|
983
990
|
// strips the import from the JS; CSS bundling is on by default since Bun 1.2). Mapped to routes
|
|
984
|
-
// below
|
|
991
|
+
// below - both the aggregate and per-route - via the metafile, for `<link>` injection.
|
|
985
992
|
...buildExtras,
|
|
986
993
|
minify: options.minify ?? true,
|
|
987
994
|
plugins: [
|
|
@@ -989,13 +996,14 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
989
996
|
preactDedupePlugin(routesDir),
|
|
990
997
|
svelteDedupePlugin(routesDir),
|
|
991
998
|
serverOnlyEmptyPlugin(),
|
|
999
|
+
serverFnStubPlugin(),
|
|
992
1000
|
...(options.plugins ?? []),
|
|
993
1001
|
],
|
|
994
1002
|
...(options.conditions ? { conditions: [...options.conditions] } : {}),
|
|
995
1003
|
// Replace `process.env.*` at compile time so an app module reading config off `process.env` doesn't
|
|
996
1004
|
// hit a `process is not defined` crash in the browser. Bun does longest-match: NODE_ENV resolves to
|
|
997
1005
|
// the build mode (React's prod/dev branch); each PUBLIC_* var resolves to its baked VALUE; every
|
|
998
|
-
// other `process.env.X` becomes undefined (the bare `process.env` → `({})` fallback
|
|
1006
|
+
// other `process.env.X` becomes undefined (the bare `process.env` → `({})` fallback - so secrets
|
|
999
1007
|
// never leak). Callers can override any of these via `options.define` (layered last).
|
|
1000
1008
|
define: {
|
|
1001
1009
|
"process.env": "({})",
|
|
@@ -1016,7 +1024,7 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
1016
1024
|
|
|
1017
1025
|
// #4: a `node:` builtin (e.g. `node:crypto`) pulled into a CLIENT chunk builds fine (Bun substitutes
|
|
1018
1026
|
// a browser polyfill) but breaks/leaks at runtime. Fail the build with a named, actionable error
|
|
1019
|
-
// instead
|
|
1027
|
+
// instead - caught at build time, not by a confused user in the browser. Graph-based (the metafile's
|
|
1020
1028
|
// per-output `inputs`), so it can't false-positive on a `"node:..."` string literal and survives
|
|
1021
1029
|
// minification. Only the client build runs this; the server build's `node:` imports are legitimate.
|
|
1022
1030
|
const clientMeta = (result as unknown as { metafile?: BunMetafile }).metafile
|
|
@@ -1027,14 +1035,14 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
1027
1035
|
const nodeBuiltinLeak = formatNodeBuiltinLeak(detectNodeBuiltinsInClient(clientGraph))
|
|
1028
1036
|
if (nodeBuiltinLeak !== undefined) throw new Error(nodeBuiltinLeak)
|
|
1029
1037
|
|
|
1030
|
-
// §3.3/§5.1: a module that opted into the `server-only` marker yet reached a CLIENT chunk
|
|
1038
|
+
// §3.3/§5.1: a module that opted into the `server-only` marker yet reached a CLIENT chunk - catches
|
|
1031
1039
|
// pure-server logic (a secret, a server-only API call) carrying no `node:` import and not named
|
|
1032
1040
|
// `*.server`, so neither other guard fires. Same shared formatter as above.
|
|
1033
1041
|
const serverOnlyLeak = formatServerOnlyLeak(detectServerOnlyInClient(clientGraph))
|
|
1034
1042
|
if (serverOnlyLeak !== undefined) throw new Error(serverOnlyLeak)
|
|
1035
1043
|
|
|
1036
1044
|
// Rename any chunk whose basename isn't URL-safe (dynamic-route files become `[slug]-hash.js`) and
|
|
1037
|
-
// rewrite the references
|
|
1045
|
+
// rewrite the references - otherwise the lazy import 404s and the route silently never hydrates.
|
|
1038
1046
|
const renamed = sanitizeOutputNames(result.outputs)
|
|
1039
1047
|
const toUrl = (path: string): string =>
|
|
1040
1048
|
`${publicPath}${renamed.get(basename(path)) ?? basename(path)}`
|
|
@@ -1051,7 +1059,7 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
1051
1059
|
}
|
|
1052
1060
|
const fileToChunk = new Map<string, string>()
|
|
1053
1061
|
routeFiles.forEach((file, i) => {
|
|
1054
|
-
const out = entryPoints[i + 1] // in range
|
|
1062
|
+
const out = entryPoints[i + 1] // in range - length checked above
|
|
1055
1063
|
if (out !== undefined) fileToChunk.set(file, toUrl(out.path))
|
|
1056
1064
|
})
|
|
1057
1065
|
const chunksFor = (chainFiles: readonly string[]): string[] =>
|
|
@@ -1065,9 +1073,9 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
1065
1073
|
}
|
|
1066
1074
|
if (routeManifest.notFound) routes._404 = chunksFor([routeManifest.notFound.file])
|
|
1067
1075
|
|
|
1068
|
-
// CSS
|
|
1076
|
+
// CSS - aggregate: an `import "./x.css"` anywhere → a content-hashed `.css` asset (Bun strips the
|
|
1069
1077
|
// import from the JS). The bootstrap lazily imports every route, so its **aggregate** stylesheet is
|
|
1070
|
-
// the whole app's CSS
|
|
1078
|
+
// the whole app's CSS - the always-safe fallback `createWebApp` links when a route has no per-route
|
|
1071
1079
|
// entry below. Fallback to all CSS assets if Bun emitted no distinct aggregate.
|
|
1072
1080
|
const bootstrapName = entryName(entryFile) // `_nifra-entry`
|
|
1073
1081
|
const cssNameOf = (path: string): string => {
|
|
@@ -1081,12 +1089,12 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
1081
1089
|
const css: readonly string[] =
|
|
1082
1090
|
aggregate.length > 0 ? aggregate : cssAssets.map((o) => toUrl(o.path))
|
|
1083
1091
|
|
|
1084
|
-
// CSS
|
|
1085
|
-
// `cssBundle`
|
|
1092
|
+
// CSS - per-route: each route/layout file is its own entrypoint, so the build metafile records its
|
|
1093
|
+
// `cssBundle` - exactly the CSS that file's subtree uses (shared-component CSS is inlined into each
|
|
1086
1094
|
// consumer; verified). Keyed by the metafile's unique source `entryPoint`, so it survives
|
|
1087
1095
|
// same-basename collisions (`index.tsx` + `blog/index.tsx`) that a filename match can't. A page then
|
|
1088
1096
|
// links only its layout chain + own CSS (deduped); an empty array means the page needs no CSS at all.
|
|
1089
|
-
// Absent (→ aggregate fallback) only if Bun emits no metafile/cssBundle
|
|
1097
|
+
// Absent (→ aggregate fallback) only if Bun emits no metafile/cssBundle - never silently incomplete.
|
|
1090
1098
|
const cwd = process.cwd()
|
|
1091
1099
|
const cssByEntry = new Map<string, string>()
|
|
1092
1100
|
for (const out of Object.values(clientMeta?.outputs ?? {})) {
|
|
@@ -1132,30 +1140,30 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
|
|
|
1132
1140
|
export interface BuildServerOptions {
|
|
1133
1141
|
/** The `routes/` directory to discover (absolute path). */
|
|
1134
1142
|
readonly routesDir: string
|
|
1135
|
-
/** The worker entry module (absolute path)
|
|
1143
|
+
/** The worker entry module (absolute path) - your `worker.ts`. It imports `{ manifest, clientEntry }`
|
|
1136
1144
|
* from the generated `./server-manifest`, builds `createWebApp`, and `export default toFetchHandler(app)`. */
|
|
1137
1145
|
readonly serverEntry: string
|
|
1138
1146
|
/** Output directory for the bundled worker (absolute path). */
|
|
1139
1147
|
readonly outDir: string
|
|
1140
|
-
/** The content-hashed client entry URL (from `buildClient`'s manifest)
|
|
1148
|
+
/** The content-hashed client entry URL (from `buildClient`'s manifest) - **baked** into the generated
|
|
1141
1149
|
* server manifest, since a disk-less worker can't read `manifest.json` at runtime. */
|
|
1142
1150
|
readonly clientEntry: string
|
|
1143
|
-
/** The app's aggregate stylesheet URLs (`buildClient`'s `BuildManifest.css`)
|
|
1151
|
+
/** The app's aggregate stylesheet URLs (`buildClient`'s `BuildManifest.css`) - baked into the generated
|
|
1144
1152
|
* manifest so the server entry hands them to `createWebApp` (→ `<link rel="stylesheet">`). Omit ⇒ no CSS
|
|
1145
1153
|
* link (the built SSR page would otherwise render unstyled). */
|
|
1146
1154
|
readonly styles?: readonly string[] | undefined
|
|
1147
|
-
/** Per-route stylesheet URLs (`buildClient`'s `BuildManifest.routeStyles`)
|
|
1155
|
+
/** Per-route stylesheet URLs (`buildClient`'s `BuildManifest.routeStyles`) - baked alongside `styles`. */
|
|
1148
1156
|
readonly routeStyles?: Readonly<Record<string, readonly string[]>> | undefined
|
|
1149
1157
|
/** Route/layout file → import specifier in the generated manifest (default: a relative path from the
|
|
1150
|
-
* manifest's location
|
|
1158
|
+
* manifest's location - written next to `serverEntry` - to `routesDir`). */
|
|
1151
1159
|
readonly resolve?: (file: string) => string
|
|
1152
1160
|
/** Filename for the generated server-manifest module, written next to `serverEntry` (default
|
|
1153
1161
|
* `"server-manifest.ts"`); your `serverEntry` imports it as `./server-manifest`. */
|
|
1154
1162
|
readonly manifestFile?: string
|
|
1155
|
-
/** Adapter build plugins (e.g. `solidBunPlugin("ssr")`
|
|
1163
|
+
/** Adapter build plugins (e.g. `solidBunPlugin("ssr")` - Solid routes need their SSR transform at
|
|
1156
1164
|
* build time; React's JSX is Bun-native and needs none). */
|
|
1157
1165
|
readonly plugins?: readonly BunPlugin[]
|
|
1158
|
-
/** `Bun.build` resolution conditions (default `["workerd", "edge-light", "browser"]`)
|
|
1166
|
+
/** `Bun.build` resolution conditions (default `["workerd", "edge-light", "browser"]`) - selects each
|
|
1159
1167
|
* dependency's edge build. */
|
|
1160
1168
|
readonly conditions?: readonly string[]
|
|
1161
1169
|
/** Compile-time replacements (default `{ "process.env.NODE_ENV": '"production"' }` → production
|
|
@@ -1163,7 +1171,7 @@ export interface BuildServerOptions {
|
|
|
1163
1171
|
readonly define?: Readonly<Record<string, string>>
|
|
1164
1172
|
/** Minify the output (default `true`). */
|
|
1165
1173
|
readonly minify?: boolean
|
|
1166
|
-
/** `Bun.build` target (default `"browser"`
|
|
1174
|
+
/** `Bun.build` target (default `"browser"` - the right shape for edge runtimes: Cloudflare Workers,
|
|
1167
1175
|
* Vercel Edge, Deno, Deno Deploy). Use `"node"` for a `@nifrajs/node` server (so `node:*` built-ins
|
|
1168
1176
|
* stay external), or `"bun"` for a Bun server. The default `conditions` + the edge resolve shims
|
|
1169
1177
|
* only apply to the `"browser"` target; `"node"`/`"bun"` resolve their own renderer builds via the
|
|
@@ -1172,16 +1180,16 @@ export interface BuildServerOptions {
|
|
|
1172
1180
|
/** **Lazy/code-split routes** (default `false`): emit `() => import(route)` loaders + bundle with
|
|
1173
1181
|
* `splitting`, so each route is its own chunk loaded on first request (smaller cold-start parse)
|
|
1174
1182
|
* instead of all parsed at boot. The output becomes the worker entry **+ chunk files** in `outDir`
|
|
1175
|
-
*
|
|
1183
|
+
* - on Cloudflare, ship them with wrangler's `no_bundle` + `find_additional_modules` + an ESModule
|
|
1176
1184
|
* `rule` (Node/Deno import the chunks natively). Eager (one self-contained file) stays the default. */
|
|
1177
1185
|
readonly lazy?: boolean
|
|
1178
1186
|
}
|
|
1179
1187
|
|
|
1180
|
-
/** The built worker bundle
|
|
1188
|
+
/** The built worker bundle - point your `wrangler.toml`'s `main` at `worker`. */
|
|
1181
1189
|
export interface ServerBuild {
|
|
1182
1190
|
/** Path to the bundled, self-contained worker entry. */
|
|
1183
1191
|
readonly worker: string
|
|
1184
|
-
/** Paths of every emitted output (entry + any code-split chunks)
|
|
1192
|
+
/** Paths of every emitted output (entry + any code-split chunks) - what to ship to the platform. */
|
|
1185
1193
|
readonly outputs: readonly string[]
|
|
1186
1194
|
}
|
|
1187
1195
|
|
|
@@ -1190,7 +1198,7 @@ export interface ServerBuild {
|
|
|
1190
1198
|
* on workerd, and `Bun.build` always applies the `bun` condition (it wins over `workerd`/`edge-light`),
|
|
1191
1199
|
* so conditions alone can't select the edge build. This shim pins `react-dom/server` to its edge build
|
|
1192
1200
|
* (`server.edge.js`, which exports `renderToReadableStream`). A no-op when nothing imports react-dom
|
|
1193
|
-
* (e.g. a Solid worker)
|
|
1201
|
+
* (e.g. a Solid worker) - the resolver only runs on a match.
|
|
1194
1202
|
*/
|
|
1195
1203
|
const reactDomEdgePlugin = (from: string): BunPlugin => ({
|
|
1196
1204
|
name: "nifra-react-dom-edge",
|
|
@@ -1203,11 +1211,11 @@ const reactDomEdgePlugin = (from: string): BunPlugin => ({
|
|
|
1203
1211
|
|
|
1204
1212
|
/**
|
|
1205
1213
|
* Dedupe React to a single copy. A `file:`-linked package can ship its OWN `react` under its own
|
|
1206
|
-
* node_modules, so the bundle ends up with two React cores
|
|
1214
|
+
* node_modules, so the bundle ends up with two React cores - each with its own hook dispatcher - and SSR
|
|
1207
1215
|
* throws the cryptic `null is not an object (evaluating '…H.useState')` (the second renderer's dispatcher
|
|
1208
1216
|
* is null). This was the #1 time-sink in app builds. Pinning `react` + its JSX runtimes to ONE resolved
|
|
1209
1217
|
* copy (the app's, from `from`) guarantees a single dispatcher; `react-dom`, which imports `react`, then
|
|
1210
|
-
* shares it
|
|
1218
|
+
* shares it - so the class can't occur rather than needing a named error. No-op when React isn't used
|
|
1211
1219
|
* (an unresolvable spec is skipped; the resolver only fires on an exact match). React core is
|
|
1212
1220
|
* condition-agnostic, so pinning it doesn't disturb the edge/browser/server conditions that select
|
|
1213
1221
|
* react-dom's build.
|
|
@@ -1221,7 +1229,7 @@ export const reactDedupePlugin = (from: string): BunPlugin => ({
|
|
|
1221
1229
|
try {
|
|
1222
1230
|
resolved = Bun.resolveSync(spec, from)
|
|
1223
1231
|
} catch {
|
|
1224
|
-
continue // React (or this subpath) isn't resolvable here
|
|
1232
|
+
continue // React (or this subpath) isn't resolvable here - nothing to dedupe
|
|
1225
1233
|
}
|
|
1226
1234
|
const escaped = spec.replace(/[/\\^$*+?.()|[\]{}]/g, "\\$&")
|
|
1227
1235
|
build.onResolve({ filter: new RegExp(`^${escaped}$`) }, () => ({ path: resolved }))
|
|
@@ -1230,14 +1238,14 @@ export const reactDedupePlugin = (from: string): BunPlugin => ({
|
|
|
1230
1238
|
})
|
|
1231
1239
|
|
|
1232
1240
|
/**
|
|
1233
|
-
* Dedupe Preact to a single copy
|
|
1241
|
+
* Dedupe Preact to a single copy - the Preact analogue of `reactDedupePlugin`, closing the same class of
|
|
1234
1242
|
* bug for the Preact framework (which had NO build-time dedup before). A `file:`-linked package can ship
|
|
1235
1243
|
* its OWN `preact`, so the bundle ends up with two Preact cores; since `preact-render-to-string` mutates
|
|
1236
1244
|
* `preact`'s shared `options` global and `preact/hooks` writes the SAME global, two copies → two `options`
|
|
1237
1245
|
* → SSR throws `undefined is not an object (… __H)` (the vnode's hook state was set up on the other copy).
|
|
1238
1246
|
* Pinning `preact` + its hooks/compat/jsx subpaths to ONE resolved copy (the app's, from `from`) makes the
|
|
1239
1247
|
* renderer and the components share one core. No-op when Preact isn't used (an unresolvable spec is
|
|
1240
|
-
* skipped). Preact core is condition-agnostic, so pinning it doesn't disturb any condition selection
|
|
1248
|
+
* skipped). Preact core is condition-agnostic, so pinning it doesn't disturb any condition selection - and
|
|
1241
1249
|
* unlike react-dom there is no edge-vs-server build to preserve, so pinning the subpaths is safe.
|
|
1242
1250
|
*/
|
|
1243
1251
|
const PREACT_DEDUPE_SPECS = [
|
|
@@ -1255,7 +1263,7 @@ export const preactDedupePlugin = (from: string): BunPlugin => ({
|
|
|
1255
1263
|
try {
|
|
1256
1264
|
resolved = Bun.resolveSync(spec, from)
|
|
1257
1265
|
} catch {
|
|
1258
|
-
continue // Preact (or this subpath) isn't resolvable here
|
|
1266
|
+
continue // Preact (or this subpath) isn't resolvable here - nothing to dedupe
|
|
1259
1267
|
}
|
|
1260
1268
|
const escaped = spec.replace(/[/\\^$*+?.()|[\]{}]/g, "\\$&")
|
|
1261
1269
|
build.onResolve({ filter: new RegExp(`^${escaped}$`) }, () => ({ path: resolved }))
|
|
@@ -1264,10 +1272,10 @@ export const preactDedupePlugin = (from: string): BunPlugin => ({
|
|
|
1264
1272
|
})
|
|
1265
1273
|
|
|
1266
1274
|
/**
|
|
1267
|
-
* Dedupe Svelte to a single copy
|
|
1275
|
+
* Dedupe Svelte to a single copy - the Svelte analogue of `reactDedupePlugin`/`preactDedupePlugin`, closing
|
|
1268
1276
|
* the same class of bug for Svelte (which had NO build-time dedup before). A workspace- or file-linked
|
|
1269
1277
|
* `@nifrajs/web-svelte` can resolve its OWN `svelte` (e.g. a sibling repo's install store) while the app's
|
|
1270
|
-
* components resolve another
|
|
1278
|
+
* components resolve another - SAME version, two physical copies. Svelte 5's client runtime
|
|
1271
1279
|
* (`svelte/internal/client`) holds module-level component-context state, so two copies means the compiled
|
|
1272
1280
|
* components register on one runtime while `hydrate` runs on the other → hydration throws
|
|
1273
1281
|
* `Cannot read properties of undefined (reading 'call')` and the server-rendered markup is wiped.
|
|
@@ -1285,7 +1293,7 @@ export const svelteDedupePlugin = (from: string): BunPlugin => ({
|
|
|
1285
1293
|
try {
|
|
1286
1294
|
return { path: Bun.resolveSync(args.path, from) }
|
|
1287
1295
|
} catch {
|
|
1288
|
-
return undefined // not resolvable from the app root
|
|
1296
|
+
return undefined // not resolvable from the app root - leave Bun's default resolution
|
|
1289
1297
|
}
|
|
1290
1298
|
})
|
|
1291
1299
|
},
|
|
@@ -1293,19 +1301,43 @@ export const svelteDedupePlugin = (from: string): BunPlugin => ({
|
|
|
1293
1301
|
|
|
1294
1302
|
/**
|
|
1295
1303
|
* Remix-style `.server` convention for the CLIENT build. A module named `*.server.ts(x)` (`db.server.ts`,
|
|
1296
|
-
* `auth.server.ts`, …) is server-only
|
|
1304
|
+
* `auth.server.ts`, …) is server-only - empty it in the browser bundle so its (possibly `node:` / native /
|
|
1297
1305
|
* Capacitor) import subtree never reaches the client. The body is CJS-with-a-Proxy so any named OR default
|
|
1298
1306
|
* import resolves to `undefined` rather than a "missing export" bundle error (verified), and the real
|
|
1299
1307
|
* import subtree is gone. The complement to the node-builtin guard: when a server-only import is co-located
|
|
1300
1308
|
* in a route file (so it can't be tree-shaken out and the guard fails loud), moving it into a `*.server`
|
|
1301
|
-
* module is the fix. CLIENT-only
|
|
1309
|
+
* module is the fix. CLIENT-only - buildServer keeps the real module, which runs server-side.
|
|
1302
1310
|
*/
|
|
1303
|
-
const SERVER_ONLY_MODULE = /\.server(\.[cm]?[jt]sx?)?$/
|
|
1304
1311
|
export const serverOnlyEmptyPlugin = (): BunPlugin => ({
|
|
1305
1312
|
name: "nifra-server-only-empty",
|
|
1306
1313
|
setup(build) {
|
|
1307
1314
|
build.onLoad({ filter: SERVER_ONLY_MODULE }, () => ({
|
|
1308
|
-
contents:
|
|
1315
|
+
contents: SERVER_ONLY_REPLACEMENT,
|
|
1316
|
+
loader: "js",
|
|
1317
|
+
}))
|
|
1318
|
+
},
|
|
1319
|
+
})
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* Server functions in the CLIENT build: replace each `*.fn.ts` module with stubs that call the routes
|
|
1323
|
+
* the server mounted, so the function bodies - and everything they import - never reach a browser.
|
|
1324
|
+
*
|
|
1325
|
+
* The sibling of {@link serverOnlyEmptyPlugin}, and a deliberate contrast: a `*.server` module is
|
|
1326
|
+
* EMPTIED because nothing may call it from the client, while a `*.fn` module is REPLACED because the
|
|
1327
|
+
* client is supposed to call it, just over HTTP. The generation itself is in
|
|
1328
|
+
* `internal/server-boundary.ts` so the Vite pipeline emits identical stubs from the same code; two
|
|
1329
|
+
* hand-written copies would be a client that works in dev and 404s in production.
|
|
1330
|
+
*
|
|
1331
|
+
* CLIENT-only. The server build keeps the real module, which is what `serverFunctions()` mounts.
|
|
1332
|
+
*/
|
|
1333
|
+
export const serverFnStubPlugin = (): BunPlugin => ({
|
|
1334
|
+
name: "nifra-server-fn-stub",
|
|
1335
|
+
setup(build) {
|
|
1336
|
+
build.onLoad({ filter: SERVER_FN_MODULE }, async (args) => ({
|
|
1337
|
+
contents: generateServerFnStub(
|
|
1338
|
+
await Bun.file(args.path).text(),
|
|
1339
|
+
serverFnNamespace(args.path),
|
|
1340
|
+
),
|
|
1309
1341
|
loader: "js",
|
|
1310
1342
|
}))
|
|
1311
1343
|
},
|
|
@@ -1336,7 +1368,7 @@ const solidWebServerPlugin = (from: string): BunPlugin => ({
|
|
|
1336
1368
|
* (`generateServerManifest`, written next to `serverEntry`), then bundles `serverEntry` with
|
|
1337
1369
|
* `Bun.build` using **edge conditions** + the adapter's SSR plugins. The output imports no `node:fs`
|
|
1338
1370
|
* and does no dynamic-path import, so it runs on workerd: point `wrangler.toml`'s `main` at it and
|
|
1339
|
-
* serve the client assets via Workers Assets. Throws (with the bundler logs) on failure
|
|
1371
|
+
* serve the client assets via Workers Assets. Throws (with the bundler logs) on failure - never
|
|
1340
1372
|
* silently ships a broken worker.
|
|
1341
1373
|
*/
|
|
1342
1374
|
export async function buildServer(options: BuildServerOptions): Promise<ServerBuild> {
|
|
@@ -1353,7 +1385,7 @@ export async function buildServer(options: BuildServerOptions): Promise<ServerBu
|
|
|
1353
1385
|
// Edge (browser) target: Bun's `bun` condition contaminates react-dom's server build + the `worker`
|
|
1354
1386
|
// condition segfaults Bun.build on solid, so force the edge/server builds via shims. The `node`/`bun`
|
|
1355
1387
|
// targets resolve those correctly via their own condition, so the shims (and edge conditions) don't
|
|
1356
|
-
// apply
|
|
1388
|
+
// apply - defaults become `[target]` (e.g. react-dom → server.node.js under `node`).
|
|
1357
1389
|
const edge = target === "browser"
|
|
1358
1390
|
const conditions = options.conditions ?? (edge ? ["workerd", "edge-light", "browser"] : [target])
|
|
1359
1391
|
const manifest = discoverRoutes(routesDir)
|
|
@@ -1372,10 +1404,10 @@ export async function buildServer(options: BuildServerOptions): Promise<ServerBu
|
|
|
1372
1404
|
// Tag every BUNDLED SSR output so @nifrajs/web-react's react-dom adapter takes the static
|
|
1373
1405
|
// (bundled, deduped) react-dom instead of re-rooting react-dom/server to a DISK copy at runtime.
|
|
1374
1406
|
// A `target:"bun"` bundle still has `Bun.resolveSync` under the Bun runtime, so without this tag the
|
|
1375
|
-
// adapter re-imports a SECOND react-dom from node_modules
|
|
1407
|
+
// adapter re-imports a SECOND react-dom from node_modules - a second React core whose hook dispatcher
|
|
1376
1408
|
// is null for the bundled components → SSR throws `…H.useRef of null`. Always set (a structural fact
|
|
1377
1409
|
// of bundling, layered after any caller `define` so it can't be overridden). Unbundled Bun runtimes
|
|
1378
|
-
// (nifra dev/start, nifra_render) never define it, so they still re-root
|
|
1410
|
+
// (nifra dev/start, nifra_render) never define it, so they still re-root - the dev dual-install fix.
|
|
1379
1411
|
"process.env.NIFRA_SSR_BUNDLED": '"1"',
|
|
1380
1412
|
"globalThis.__NIFRA_EDGE_RUNTIME__": edge ? "true" : "false",
|
|
1381
1413
|
},
|
|
@@ -1402,12 +1434,12 @@ export async function buildServer(options: BuildServerOptions): Promise<ServerBu
|
|
|
1402
1434
|
}
|
|
1403
1435
|
|
|
1404
1436
|
// ===================================================================================================
|
|
1405
|
-
// `nifra build --target`
|
|
1437
|
+
// `nifra build --target` - package the engine above into one command that emits a full deploy dir.
|
|
1406
1438
|
//
|
|
1407
1439
|
// An app already declares everything the build needs through nifra's conventions: `adapter` +
|
|
1408
1440
|
// `clientModule` (nifra.config.ts / framework.ts), an optional `backend` (backend.ts), and `routes/`.
|
|
1409
1441
|
// The ONLY thing apps used to hand-write per target was the server entry (`_worker.ts`, `server-bun.ts`,
|
|
1410
|
-
// …)
|
|
1442
|
+
// …) - so we GENERATE it here (per target) instead of asking each app to ship five near-identical files.
|
|
1411
1443
|
// ===================================================================================================
|
|
1412
1444
|
|
|
1413
1445
|
/** A deploy target `nifra build --target <t>` can emit. `static` is pure SSG (no server). */
|
|
@@ -1428,7 +1460,7 @@ export function isBuildTarget(value: string): value is BuildTarget {
|
|
|
1428
1460
|
* - `deno`: same fetch-handler default, plus `Deno.serve` self-host when run directly.
|
|
1429
1461
|
* - `bun` / `node`: a self-hosting server that ALSO serves the client bundle from disk (those
|
|
1430
1462
|
* runtimes have a filesystem; the static `/assets/*` sit next to the entry).
|
|
1431
|
-
* `adapterImport`/`backendImport` are the specifiers the entry uses (relative to where it's written)
|
|
1463
|
+
* `adapterImport`/`backendImport` are the specifiers the entry uses (relative to where it's written) -
|
|
1432
1464
|
* `buildServer` writes the entry next to `serverEntry`, so they're resolved from there. Pure (string in,
|
|
1433
1465
|
* string out) so the generation is unit-testable without a real build.
|
|
1434
1466
|
*/
|
|
@@ -1499,7 +1531,7 @@ export function generateServerEntry(options: {
|
|
|
1499
1531
|
}
|
|
1500
1532
|
if (target === "vercel") {
|
|
1501
1533
|
lines.push(
|
|
1502
|
-
"// Vercel Edge Function
|
|
1534
|
+
"// Vercel Edge Function - Vercel serves /assets/* from its CDN; this only SSRs page routes.",
|
|
1503
1535
|
'export const config = { runtime: "edge" }',
|
|
1504
1536
|
"export default (req: Request): Response | Promise<Response> => app.fetch(req)",
|
|
1505
1537
|
)
|
|
@@ -1541,7 +1573,7 @@ export function generateServerEntry(options: {
|
|
|
1541
1573
|
" },",
|
|
1542
1574
|
"})",
|
|
1543
1575
|
// The `${...}` here is literal OUTPUT (a template in the GENERATED file), not a template in this
|
|
1544
|
-
// source
|
|
1576
|
+
// source - split so biome's noTemplateCurlyInString doesn't flag it; the emitted line is unchanged.
|
|
1545
1577
|
`console.log(\`nifra (Bun) → http://localhost:$${"{server.port}"}\`)`,
|
|
1546
1578
|
)
|
|
1547
1579
|
return `${lines.join("\n")}\n`
|
|
@@ -1574,13 +1606,13 @@ export function generateServerEntry(options: {
|
|
|
1574
1606
|
// deno
|
|
1575
1607
|
lines.push(
|
|
1576
1608
|
"const handler = toFetchHandler(app)",
|
|
1577
|
-
"// @ts-ignore
|
|
1609
|
+
"// @ts-ignore - Deno global is present on the Deno runtime this output targets.",
|
|
1578
1610
|
'Deno.serve({ port: Number(Deno.env.get("PORT") ?? "3000") }, async (req) => {',
|
|
1579
1611
|
" const { pathname } = new URL(req.url)",
|
|
1580
1612
|
" const filePath = staticPath(pathname)",
|
|
1581
1613
|
" if (filePath !== undefined) {",
|
|
1582
1614
|
" try {",
|
|
1583
|
-
" // @ts-ignore
|
|
1615
|
+
" // @ts-ignore - Deno.readFile is present on the Deno runtime.",
|
|
1584
1616
|
" const body = await Deno.readFile(new URL(filePath, STATIC_ROOT))",
|
|
1585
1617
|
' const ext = pathname.slice(pathname.lastIndexOf(".") + 1)',
|
|
1586
1618
|
' return new Response(body, { headers: { "content-type": TYPES[ext] ?? "application/octet-stream" } })',
|
|
@@ -1633,7 +1665,7 @@ export interface BuildTargetOptions {
|
|
|
1633
1665
|
readonly adapterImport: string
|
|
1634
1666
|
/** Import specifier (resolvable from `workDir`) of the module exporting `backend`, or `undefined`. */
|
|
1635
1667
|
readonly backendImport?: string
|
|
1636
|
-
/** Factory that builds the app for `static` prerendering, GIVEN the client build's manifest
|
|
1668
|
+
/** Factory that builds the app for `static` prerendering, GIVEN the client build's manifest - so the
|
|
1637
1669
|
* emitted hydration `<script src>` uses the REAL content-hashed entry (`client.entry`) plus the same
|
|
1638
1670
|
* styles/route-preload the server targets use. A pre-built instance can't work here: the hash isn't known
|
|
1639
1671
|
* until `buildClient` runs inside `buildTarget`, so a hardcoded entry 404s → pages render but never
|
|
@@ -1655,12 +1687,12 @@ export interface BuildTargetOptions {
|
|
|
1655
1687
|
readonly title?: string
|
|
1656
1688
|
}
|
|
1657
1689
|
|
|
1658
|
-
/** Minimal app surface `buildTarget`'s static path needs
|
|
1690
|
+
/** Minimal app surface `buildTarget`'s static path needs - a fetch handler (a built `createWebApp`). */
|
|
1659
1691
|
export interface PrerenderAppLike {
|
|
1660
1692
|
fetch(req: Request): Response | Promise<Response>
|
|
1661
1693
|
}
|
|
1662
1694
|
|
|
1663
|
-
/** The result of a target build
|
|
1695
|
+
/** The result of a target build - the deploy dir + the client manifest + an optional size report. */
|
|
1664
1696
|
export interface BuildTargetResult {
|
|
1665
1697
|
/** The deploy target that was built. */
|
|
1666
1698
|
readonly target: BuildTarget
|
|
@@ -1681,7 +1713,7 @@ export interface BuildTargetResult {
|
|
|
1681
1713
|
* - `static`: prerenders opted-in routes (`prerenderRoutes`) to `<outDir>/<path>/index.html` (+
|
|
1682
1714
|
* `_data.json`); needs `prerenderApp`. No server.
|
|
1683
1715
|
* - `cf-pages`: a `_worker.js` (edge bundle) + a `_routes.json` excluding /assets/* from the worker.
|
|
1684
|
-
* - `vercel`: a `.vercel/output`-shaped function isn't emitted here
|
|
1716
|
+
* - `vercel`: a `.vercel/output`-shaped function isn't emitted here - `vercel` emits the bundled edge
|
|
1685
1717
|
* entry as `<outDir>/index.js` (the CLI's docs point at `vercel`'s Build Output wrapper). [see note]
|
|
1686
1718
|
* - `deno`/`node`/`bun`: the self-hosting server bundle (`server.js`) next to the assets.
|
|
1687
1719
|
* The server entry is GENERATED (`generateServerEntry`) and bundled (`buildServer`); the app supplies
|
|
@@ -1691,7 +1723,7 @@ export interface BuildTargetResult {
|
|
|
1691
1723
|
* remain app-owned scripts; this command targets the common single-bundle deploys. See the CLI docs.
|
|
1692
1724
|
*/
|
|
1693
1725
|
/**
|
|
1694
|
-
* A build-tool STRATEGY for {@link buildTargetWith}
|
|
1726
|
+
* A build-tool STRATEGY for {@link buildTargetWith} - the two bundling steps, and nothing else. Everything
|
|
1695
1727
|
* around them (server-entry codegen, deploy assembly, prerender, size report) is bundler-agnostic and
|
|
1696
1728
|
* lives in `buildTargetWith`, so a second bundler (Vite) is this interface, not a second orchestrator.
|
|
1697
1729
|
*
|
|
@@ -1725,7 +1757,7 @@ export interface Bundler {
|
|
|
1725
1757
|
}): Promise<ServerBuild>
|
|
1726
1758
|
}
|
|
1727
1759
|
|
|
1728
|
-
/** The default (Bun) strategy
|
|
1760
|
+
/** The default (Bun) strategy - `buildClient`/`buildServer` from this module. */
|
|
1729
1761
|
export const bunBundler: Bundler = {
|
|
1730
1762
|
buildClient: (input) =>
|
|
1731
1763
|
buildClient({
|
|
@@ -1818,9 +1850,9 @@ export async function buildTargetWith(
|
|
|
1818
1850
|
})
|
|
1819
1851
|
if (result.prerendered.length === 0) {
|
|
1820
1852
|
// A static build that renders nothing is almost always a misconfig (no `prerender = true` / no
|
|
1821
|
-
// getStaticPaths)
|
|
1853
|
+
// getStaticPaths) - fail loudly rather than ship an empty dir the dev thinks is their site.
|
|
1822
1854
|
throw new Error(
|
|
1823
|
-
"[nifra/web] buildTarget(static): no routes were prerendered
|
|
1855
|
+
"[nifra/web] buildTarget(static): no routes were prerendered - opt routes in with " +
|
|
1824
1856
|
"`export const prerender = true` (static) or `getStaticPaths` (dynamic).",
|
|
1825
1857
|
)
|
|
1826
1858
|
}
|