@nifrajs/web 5.2.0 → 5.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/CHANGELOG.md +112 -0
- package/dist/build.d.ts +11 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +265 -15
- 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/node-adapter.d.ts +19 -0
- package/dist/internal/node-adapter.d.ts.map +1 -0
- package/dist/internal/node-adapter.js +48 -0
- package/dist/internal/node-adapter.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 -0
- 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/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 +19 -7
- package/dist/public-dir.d.ts.map +1 -1
- package/dist/public-dir.js +130 -99
- 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 +330 -49
- 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/node-adapter.ts +57 -0
- package/src/internal/output-guard.ts +51 -9
- package/src/internal/page-execution.ts +55 -47
- package/src/internal/parity.ts +2 -0
- package/src/internal/prerendered-pages.ts +124 -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 +158 -97
- package/src/render-seam.ts +5 -2
- package/src/search.ts +1 -0
- package/src/vite.ts +130 -54
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,117 @@
|
|
|
1
1
|
# @nifrajs/web
|
|
2
2
|
|
|
3
|
+
## 5.4.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- e7d43ca: On Bun, `servePublicDir` now sends `content-length` for files up to 1 MiB, whole or ranged. This covers the Bun server entry and `nifra dev`. `Bun.serve` drops the declared length of a stream body and sends it chunked, so these files used to arrive with no length: no download progress, and caches and proxies could not size them. They are now read whole through the already-open file, as files up to 256 KiB already were, which serves a 1 MiB file about a third faster. Files above 1 MiB still stream, and Bun still sends them chunked. Node and Deno keep a stream's length and are unchanged.
|
|
8
|
+
- 5183c00: The server entry `buildTarget` generates for `deno` now answers static files with the validators the Bun and Node entries send:
|
|
9
|
+
|
|
10
|
+
- an `etag` and `last-modified`, and a `304` with no body for a current copy, HEAD included.
|
|
11
|
+
- `Range` honored only when `If-Range` is absent or names the file's current validator; anything else gets the whole file.
|
|
12
|
+
- a link is typed by the file it resolves to, as on Bun and Node.
|
|
13
|
+
|
|
14
|
+
`@nifrajs/core/static-file` holds the comparisons `servePublicDir`, the Deno entry and `@nifrajs/node`'s `static` reader now share.
|
|
15
|
+
- e589893: The server entry `buildTarget` generates for `node` now passes the app itself to `@nifrajs/node`'s `serve` and serves files with the adapter's own reader. The entry needs the `@nifrajs/node` of the same release, so a `node` build with an older one installed stops before bundling, names both versions and leaves the last deploy dir in place. A 5.3 adapter used to build cleanly and then throw when the server started. It used to wrap `app.fetch` in a plain `{ fetch }` handler. That wrapper hid the node-direct lane, so every page built an undici `Request` and drained a `Response`, and every file went through one too. The entry mounts `/assets` and, at the root, only the `public/` files the build copied, with the same type, cache, range, HEAD and hidden-file answers the Bun entry gives.
|
|
16
|
+
|
|
17
|
+
`serve`'s `static` option on `@nifrajs/node`:
|
|
18
|
+
|
|
19
|
+
- takes an array of mounts; a path goes to the first mount that takes it, by its prefix and, when set, its `files`.
|
|
20
|
+
- `files` lists the only URL paths a mount serves; any other path passes to the next mount, then the app, without that mount touching the disk.
|
|
21
|
+
- `cacheControl` sets `cache-control` for files not served immutable.
|
|
22
|
+
- types files from the same table as `servePublicDir`, so `.pdf`, `.mp4`, `.xml`, `.csv` and other common types are no longer sent as `application/octet-stream`; a link is typed by the file it resolves to.
|
|
23
|
+
- serves `/.well-known` by default, as `servePublicDir` does, and refuses a visible name that links to a dotfile.
|
|
24
|
+
- answers a link that leaves the directory with `404`, the same as a missing file, instead of `403`.
|
|
25
|
+
- sends files up to 256 KiB, and HEAD, `304` and `416` answers, in one write with no `Response` or stream.
|
|
26
|
+
|
|
27
|
+
`@nifrajs/core/static-file` exports `CONTENT_TYPES`, `contentTypeOf` and `isHiddenPath`, which both readers now use.
|
|
28
|
+
- 02ac072: Static files on Bun and Deno evaluate `If-None-Match` before `If-Modified-Since`, including the wildcard condition for an existing file. A stale entity tag can no longer produce a `304` from its accompanying date.
|
|
29
|
+
- 529f8d2: Static files are tagged by their content on every self-hosting entry: `servePublicDir` (the Bun entry and `nifra dev`), `@nifrajs/node`'s `static` reader and the generated Deno entry. Each sends a strong `ETag` of 128 bits of the file's SHA-256, the same for the same bytes on every instance and runtime. A replacement that keeps a file's size and modification time (`cp -p`, `rsync -a`, tar, a container image layer, SOURCE_DATE_EPOCH, the Nix store's 1 s) is no longer answered with a `304` for the old copy, and a resumed download is no longer spliced from two versions. The Node reader's `"<mtime>-<size>"` tag is gone; the Bun and Deno entries send a tag for the first time.
|
|
30
|
+
|
|
31
|
+
- A tag is remembered per process for the file version it was read from (device, inode, size, mtime and ctime), so a known file costs no extra read. The first request for a file reads it once, and a file within the buffered size is then served from those bytes. Concurrent first requests share one read. A file changed in the last 2 s is read again until its ctime settles. A file over 256 MiB (`CONTENT_TAG_MAX`) is tagged by that identity instead: still strong, but different on each instance.
|
|
32
|
+
- `Last-Modified` is sent only while the file's mtime is the time of its own last change (within 1 s of its ctime). A file whose mtime a copy or build set, such as every file in a container image, is sent without it, so `If-Modified-Since` cannot validate it.
|
|
33
|
+
- An `If-Range` date no longer keeps a range for a file with a strong tag; a client resumes by the tag. On Node a configured strong `ETag` likewise sends the whole file for a date, while a configured weak one still lets a configured `Last-Modified` resume by its exact date.
|
|
34
|
+
- `@nifrajs/core/static-file` exports `contentTags`, `CONTENT_TAG_MAX`, `lastModifiedOf` and `isNotModified`, the `If-None-Match` and `If-Modified-Since` check all three readers share. `ifRangeAllowsPartial` now takes the file's tag.
|
|
35
|
+
- Updated dependencies [5183c00]
|
|
36
|
+
- Updated dependencies [01b97ec]
|
|
37
|
+
- Updated dependencies [e589893]
|
|
38
|
+
- Updated dependencies [ff66db6]
|
|
39
|
+
- Updated dependencies [529f8d2]
|
|
40
|
+
- @nifrajs/core@5.4.0
|
|
41
|
+
- @nifrajs/island-trigger@5.4.0
|
|
42
|
+
|
|
43
|
+
## 5.3.0
|
|
44
|
+
|
|
45
|
+
### Minor Changes
|
|
46
|
+
|
|
47
|
+
- 7ab9692: Add build-time schema compilation that emits typed static validators without runtime code generation. Generated modules preserve strict validation and structured issues while avoiding the schema builder and dynamic compiler in deployed bundles.
|
|
48
|
+
|
|
49
|
+
A schema whose property key, `format` name or custom kind holds a quote, backslash or line break, or whose literal holds a backslash or line break, now validates on the eval-free checker and is refused by build-time compilation. TypeBox splices that text into generated source unescaped, so a quote could end the string and run the rest as code.
|
|
50
|
+
|
|
51
|
+
Prepare immutable route modules and layout chains in production while executing authorization gates and loaders for every request. Mutable manifests can disable module preparation with `cacheRouteModules: false`.
|
|
52
|
+
|
|
53
|
+
Reduce output projection bookkeeping for uniform subtrees, reuse request-bound cache views, and record completed cache spans without allocating an active observation. Span parentage, sampling, redaction, exporter isolation, and capability enforcement remain intact.
|
|
54
|
+
|
|
55
|
+
Keep custom start-only `ObservationLifecycle` implementations compatible. The factory returns the extended `RecordingObservationLifecycle` interface with the new `record` method.
|
|
56
|
+
|
|
57
|
+
Mark the migration parser's TypeScript keyword-kind comparison as syntax, so the security check does not mistake it for a secret comparison.
|
|
58
|
+
- 2a429e6: `nifra build --target bun` and `--target node` prerender the routes that opt in (`export const prerender = true`, or `getStaticPaths`) into `_prerendered/` beside `server.js`. The server then answers those pages from their file instead of rendering them on every request.
|
|
59
|
+
|
|
60
|
+
The file is served from the page route, so `use` middleware and every request hook still run first. A gate in front of a page still applies, and response hooks still add their headers. The page carries the `content-language` and Content-Security-Policy its build-time render produced. Loader headers and cookies are not sent, as for any prerendered page.
|
|
61
|
+
|
|
62
|
+
These keep rendering on demand:
|
|
63
|
+
- a request with a query string
|
|
64
|
+
- a navigation data or server-component request
|
|
65
|
+
- a draft-mode visitor
|
|
66
|
+
- a page whose build-time render set a cookie, was `private` or `no-store`, or varied on a request header
|
|
67
|
+
- every page of an app with a `nonce`, whose documents need a nonce minted per request.
|
|
68
|
+
|
|
69
|
+
On Node, the SSR benchmark's prerendered catalog page is served about 2.1x (Svelte) to 2.6x (Vue) faster than rendering it on each request.
|
|
70
|
+
|
|
71
|
+
New in `@nifrajs/web`:
|
|
72
|
+
- `createWebApp({ prerenderedPages })`
|
|
73
|
+
- `prerenderedPageReader`, which opens only the build's own files inside the directory and keeps each page in memory once read, up to 32 MiB by default
|
|
74
|
+
- `PrerenderEntry.serveHeaders` on `prerenderRoutes` results.
|
|
75
|
+
|
|
76
|
+
Prerendering now runs during these builds, so it loads every route module and runs the opted-in loaders at build time. Two effects:
|
|
77
|
+
- If a route module cannot load there, the build warns and every page renders on demand, as before.
|
|
78
|
+
- A secret in a prerendered page fails the build, as it does for `--target static`.
|
|
79
|
+
|
|
80
|
+
The Vite pipeline, `deno`, and the edge targets are unchanged.
|
|
81
|
+
|
|
82
|
+
### Patch Changes
|
|
83
|
+
|
|
84
|
+
- 0e1d03f: A build no longer copies hidden files from `public/` - any path segment starting with `.` other than `.well-known`, such as `.env`, `.git/` or `.DS_Store`. The Bun development pipeline never served them (the Vite one did; see below), but the build copied them and every target then served them: the Bun, Node and Deno entries from their allowlist, the static, Pages and Vercel hosts from the directory. The development parity check lists public files by the same rule, and `servePublicDir` also refuses a visible name whose real target is hidden.
|
|
85
|
+
- 605c246: Under a hash-based `createCspPolicy`, a buffered render that writes a script for that request alone (Solid serializing a server error an `ErrorBoundary` caught, for hydration) no longer ships it unauthorized. The render is handed a nonce; when the HTML it returns carries it, the document keeps the nonce in its `Content-Security-Policy` and becomes `private, no-store`, and every other document stays nonce-free and cacheable as before.
|
|
86
|
+
- 271917a: A soft navigation's deferred data stream is cancelled and released when a line fails to parse. A malformed first line threw out of `parseNdjsonData` with the body still locked and streaming and the abort listener attached, and a malformed later line stopped reading without cancelling the body; every exit now detaches from the signal, cancels an unfinished body and releases the reader.
|
|
87
|
+
- 69dfb60: `rangeResponse()` and `servePublicDir` ignore a `Range` header on a HEAD request, as RFC 9110 section 14.2 defines range handling for GET only: HEAD answers 200 with the full `content-length` and no `content-range`, the same as the Node static bridge. Before, HEAD with `Range` answered 206 or 416 with the metadata of the requested slice.
|
|
88
|
+
- 605c246: A React page that suspends without `defer()` (`use(promise)` or `lazy` inside `<Suspense>`) now renders its resolved content on the server. The buffered fast path rendered it with React's string renderer, which cannot wait, so the document kept the fallback for good (with `hydrate: false` or JavaScript off, the content never appeared), and a component suspending with no boundary above it failed the render. `RenderAdapter.renderToString` may now return `undefined` for a render that suspended; the React adapter does so, and the page is streamed instead, with a CSP nonce when the policy needs one.
|
|
89
|
+
- 13224fe: The server entry `buildTarget` generates for `bun`, `node` and `deno` serves static files the way `nifra dev` does. Bun and Node now hand a manifest-approved path to `servePublicDir`, and Deno runs the same checks on its own file API: the real path must sit inside the deploy directory, so a symbolic link planted in the deployed tree no longer reaches a file outside it; a HEAD request is answered from the file's size instead of reading it whole (the Node entry read the entire file); and a single byte range gets a `206` (an unsatisfiable one a `416`), with `accept-ranges` advertised.
|
|
90
|
+
- 00c266e: A streamed page now renders only as fast as the client reads it. The document stream read the whole app stream up front, so a client that stopped reading left the full rendered body queued in memory; it now pulls one renderer chunk per read. The Vite dev bridge (`pipeWebBodyToNode`) likewise waits for `drain` when the socket buffer is full instead of writing every chunk at once, and removes its listeners when the body ends.
|
|
91
|
+
- 042db5d: A Preact page that suspends without `defer()` (a `preact/compat` `<Suspense>` around a thrown promise, or `lazy`) is streamed and resolves on the server; it was a 500. A page whose string render suspended skips the string pass for its next 32 requests and goes straight to the stream, so a React page that suspends on `use` renders once per request rather than twice.
|
|
92
|
+
- 49e37bd: `nifra dev` on the Vite pipeline no longer serves hidden files from `public/`. Vite's public middleware served every file there, dotfiles included, so `/.env`, `/.git/config` and `/%2eenv` answered with their contents. A request for a hidden file, or a visible name whose real target is hidden, now goes to the app, as it does in production and on the Bun pipeline. The dev server binds 127.0.0.1 and Vite refuses a foreign `Host`, so the exposure was limited to local processes and the developer's own browser.
|
|
93
|
+
- 6826956: The Vite dev bridge cancels a non-HTML response body when the client left before piping began, or while a write was refused. It waited for a `drain` that never came, so an SSE source or a streamed body from the app stayed open for every aborted request.
|
|
94
|
+
- 3318232: `servePublicDir` reads a body of up to 256 KiB whole through its pinned descriptor and closes it before the response leaves. A larger body still streams. On Bun, a 200 KB public file is served in 0.17 ms where a `node:fs` stream took 0.28 ms; a 10 KB file takes 0.12-0.14 ms instead of 0.16 ms. The self-hosted Bun, Node and Deno entries serve static files through this handler.
|
|
95
|
+
- 7e5cd65: The self-hosted Deno entry's static handler applies `servePublicDir`'s hidden-file rule to the real path, so `/assets/<name>` linking to a `.env` inside the deploy directory is 404 rather than served. It also checks that the file it opened is still the one at the checked real path. `Deno.open` follows links, so a directory swapped for a link after the containment check used to redirect the open outside the deploy directory.
|
|
96
|
+
- d874102: No behavior change: the error a secret in a build artifact throws sets its name in a constructor, and the prerendered page reader's failure paths are tested.
|
|
97
|
+
- 80bf986: Page documents render with less work per request. Loader data that a closed output schema has settled skips the scan for deferred values. The request origin is parsed once per host. An empty query string skips search parsing. The render options are assembled without a copy per optional field. On the SSR bench apps this measured 6 to 10% more requests per second on Bun and Node.
|
|
98
|
+
- d890695: `MemoryCacheStore` no longer stores a render that began before a purge whose record it has since evicted. Purge records are bounded by `max`; once a page's or tag's record was evicted, a slow render that began before that purge was accepted and served the purged page again (one purge followed by 500 others was enough at the default `max`). Such a render is now served but not cached; a render that began after the purge is cached as before.
|
|
99
|
+
- Updated dependencies [7ab9692]
|
|
100
|
+
- Updated dependencies [808bac8]
|
|
101
|
+
- Updated dependencies [d4bae56]
|
|
102
|
+
- Updated dependencies [007060d]
|
|
103
|
+
- Updated dependencies [190ab39]
|
|
104
|
+
- Updated dependencies [7cf8df3]
|
|
105
|
+
- @nifrajs/core@5.3.0
|
|
106
|
+
- @nifrajs/island-trigger@5.3.0
|
|
107
|
+
|
|
108
|
+
## 5.2.1
|
|
109
|
+
|
|
110
|
+
### Patch Changes
|
|
111
|
+
|
|
112
|
+
- @nifrajs/core@5.2.1
|
|
113
|
+
- @nifrajs/island-trigger@5.2.1
|
|
114
|
+
|
|
3
115
|
## 5.2.0
|
|
4
116
|
|
|
5
117
|
### Minor Changes
|
package/dist/build.d.ts
CHANGED
|
@@ -3,8 +3,10 @@ import { type BunPlugin } from "bun";
|
|
|
3
3
|
import { type BuildManifest, type BuildTarget, type Bundler, type ServerBuild, type SizeReport } from "./build-plan.js";
|
|
4
4
|
import { type CssLoadingMode } from "./css-contract.js";
|
|
5
5
|
import { generateServerManifest } from "./index.js";
|
|
6
|
+
import { type ServerPrerenderedPage } from "./internal/prerendered-pages.js";
|
|
6
7
|
import { type SecretExemption } from "./internal/secret-scan.js";
|
|
7
8
|
export * from "./build-plan.js";
|
|
9
|
+
export type { ServerPrerenderedPage } from "./internal/prerendered-pages.js";
|
|
8
10
|
export type { SecretExemption, SecretRule } from "./internal/secret-scan.js";
|
|
9
11
|
export { type CloudflarePagesRoutes, type CloudflarePagesRoutesOptions, cloudflarePagesRoutes, dataFileFor, htmlFileFor, type PrerenderApp, type PrerenderEntry, type PrerenderOptions, type PrerenderResult, prerenderRoutes, } from "./prerender.js";
|
|
10
12
|
export interface BuildClientOptions {
|
|
@@ -93,7 +95,9 @@ export declare function cloudflareRouteRules(publicFiles: readonly string[],
|
|
|
93
95
|
routePatterns: readonly string[]): CloudflareRouteRules;
|
|
94
96
|
/** The built asset map - the server reads `entry` for the client script + serves `assets`. */
|
|
95
97
|
/**
|
|
96
|
-
* Copy `from` into `to`, returning the URL paths copied (sorted).
|
|
98
|
+
* Copy `from` into `to`, returning the URL paths copied (sorted). Hidden entries other than
|
|
99
|
+
* `.well-known` are skipped, as development never serves them: every target ships this list, and a
|
|
100
|
+
* CDN-hosted one serves whatever was copied.
|
|
97
101
|
*
|
|
98
102
|
* Lives here, not beside `servePublicDir`, because it is BUILD-time: it reaches for `Bun.Glob` and
|
|
99
103
|
* `node:fs`, and `public-dir.ts` is reachable from the client bundle graph through the package
|
|
@@ -276,6 +280,9 @@ export declare function generateServerEntry(options: {
|
|
|
276
280
|
readonly clientIp?: ServerEntryClientIp;
|
|
277
281
|
/** The server manifest exports `copyChunks` (the client build split copy tables by locale). */
|
|
278
282
|
readonly copyChunks?: boolean;
|
|
283
|
+
/** `bun` / `node` only: the pages the build prerendered into `_prerendered/`, by request path, which
|
|
284
|
+
* the app answers from their file (see `createWebApp`'s `prerenderedPages`). */
|
|
285
|
+
readonly prerenderedPages?: Readonly<Record<string, ServerPrerenderedPage>>;
|
|
279
286
|
}): string;
|
|
280
287
|
export interface BuildTargetOptions {
|
|
281
288
|
/** The `routes/` directory to discover (absolute path). */
|
|
@@ -304,7 +311,9 @@ export interface BuildTargetOptions {
|
|
|
304
311
|
* emitted hydration `<script src>` uses the REAL content-hashed entry (`client.entry`) plus the same
|
|
305
312
|
* styles/route-preload the server targets use. A pre-built instance can't work here: the hash isn't known
|
|
306
313
|
* until `buildClient` runs inside `buildTarget`, so a hardcoded entry 404s → pages render but never
|
|
307
|
-
* hydrate. Required for `target: "static"` (SSG drives `app.fetch`)
|
|
314
|
+
* hydrate. Required for `target: "static"` (SSG drives `app.fetch`). For `bun` and `node` it is
|
|
315
|
+
* optional: given, the opted-in routes are prerendered into `_prerendered/` and the server answers
|
|
316
|
+
* them from their file; the other targets ignore it. */
|
|
308
317
|
readonly prerenderApp?: (client: BuildManifest) => PrerenderAppLike | Promise<PrerenderAppLike>;
|
|
309
318
|
/** Client-build plugins (e.g. the MDX/Vue/Solid Bun plugins). */
|
|
310
319
|
readonly clientPlugins?: readonly BunPlugin[];
|
package/dist/build.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"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"}
|
package/dist/build.js
CHANGED
|
@@ -11,6 +11,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync
|
|
|
11
11
|
import { cp, lstat, mkdir, realpath } from "node:fs/promises";
|
|
12
12
|
import { dirname, isAbsolute, join, basename as pathBasename, posix, relative, resolve as resolvePath, sep, } from "node:path";
|
|
13
13
|
import { singleCopyPlugin as declaredSingleCopyPlugin, readSingleCopyDeclaration, } from "@nifrajs/core/single-copy";
|
|
14
|
+
import { isHiddenPath } from "@nifrajs/core/static-file";
|
|
14
15
|
import { Glob } from "bun";
|
|
15
16
|
import { aggregateSizeReport, detectNodeBuiltinsInClient, detectServerOnlyInClient, formatNodeBuiltinLeak, formatServerOnlyLeak, parseBuildTarget, parseManifestClientEntry, parseManifestCopyChunks, parseManifestCssLoading, parseManifestRouteStyles, parseManifestStyles, planBuildTarget, } from "./build-plan.js";
|
|
16
17
|
import { sanitizeOutputNames } from "./chunk-names.js";
|
|
@@ -21,10 +22,12 @@ import { loadCopySplit } from "./internal/copy-split.js";
|
|
|
21
22
|
import { assertDisposableDir } from "./internal/disposable-dir.js";
|
|
22
23
|
import { entryChunks } from "./internal/entry-chunks.js";
|
|
23
24
|
import { dedupePolicyFor } from "./internal/identity-policy.js";
|
|
25
|
+
import { nodeAdapterRefusal } from "./internal/node-adapter.js";
|
|
24
26
|
import { assertDevelopmentProductionParity, assertIdentityParity, collectDevelopmentParityInput, } from "./internal/parity.js";
|
|
27
|
+
import { servablePrerenderedPages, } from "./internal/prerendered-pages.js";
|
|
25
28
|
import { privateEnvCheck } from "./internal/private-env.js";
|
|
26
29
|
import { publicUrlPath } from "./internal/public-url.js";
|
|
27
|
-
import { assertNoSecrets, buildEnvironment, emittedScanFiles, formatSecretFindings, graphScanInput, originName, publicScanFiles, scanForSecrets, textOf, } from "./internal/secret-scan.js";
|
|
30
|
+
import { assertNoSecrets, buildEnvironment, emittedScanFiles, formatSecretFindings, graphScanInput, originName, publicScanFiles, SecretScanError, scanForSecrets, textOf, } from "./internal/secret-scan.js";
|
|
28
31
|
import { generateServerFnStub, SERVER_FN_MODULE, serverFnNamespace, } from "./internal/server-boundary.js";
|
|
29
32
|
import { formatUnsupportedBuiltins, unsupportedBuiltins } from "./internal/target-compat.js";
|
|
30
33
|
import { accountEmittedFiles, bunModuleSource, formatClientGraphVerdict, formatServerGraphVerdict, formatUnaccountedOutput, verifyClientGraph, verifyServerGraph, } from "./internal/zone-graph.js";
|
|
@@ -33,7 +36,7 @@ import { assertNoImportGlob, importGlobPlugin } from "./plugins/import-glob.js";
|
|
|
33
36
|
import { zoneGuardPlugin } from "./plugins/zone-guard.js";
|
|
34
37
|
// `buildTarget(static)` drives the SSG prerender engine directly (it's also re-exported below).
|
|
35
38
|
import { prerenderRoutes } from "./prerender.js";
|
|
36
|
-
import {
|
|
39
|
+
import { IMMUTABLE, ONE_DAY } from "./public-dir.js";
|
|
37
40
|
import { createZoneClassifier } from "./zones.js";
|
|
38
41
|
export * from "./build-plan.js";
|
|
39
42
|
const basename = (path) => pathBasename(path);
|
|
@@ -166,7 +169,9 @@ routePatterns) {
|
|
|
166
169
|
}
|
|
167
170
|
/** The built asset map - the server reads `entry` for the client script + serves `assets`. */
|
|
168
171
|
/**
|
|
169
|
-
* Copy `from` into `to`, returning the URL paths copied (sorted).
|
|
172
|
+
* Copy `from` into `to`, returning the URL paths copied (sorted). Hidden entries other than
|
|
173
|
+
* `.well-known` are skipped, as development never serves them: every target ships this list, and a
|
|
174
|
+
* CDN-hosted one serves whatever was copied.
|
|
170
175
|
*
|
|
171
176
|
* Lives here, not beside `servePublicDir`, because it is BUILD-time: it reaches for `Bun.Glob` and
|
|
172
177
|
* `node:fs`, and `public-dir.ts` is reachable from the client bundle graph through the package
|
|
@@ -177,6 +182,8 @@ export async function copyPublicDir(from, to) {
|
|
|
177
182
|
const sourceReal = await realpath(source);
|
|
178
183
|
const copied = [];
|
|
179
184
|
for await (const rel of new Glob("**/*").scan({ cwd: source, dot: true, onlyFiles: false })) {
|
|
185
|
+
if (isHiddenPath(rel))
|
|
186
|
+
continue;
|
|
180
187
|
const candidate = join(source, rel);
|
|
181
188
|
const candidateStat = await lstat(candidate);
|
|
182
189
|
if (candidateStat.isSymbolicLink()) {
|
|
@@ -982,6 +989,8 @@ const PLATFORM_CLIENT_IP_HEADERS = {
|
|
|
982
989
|
cloudflare: "cf-connecting-ip",
|
|
983
990
|
vercel: "x-real-ip",
|
|
984
991
|
};
|
|
992
|
+
/** Where a self-hosting build writes its prerendered pages, beside `server.js`. */
|
|
993
|
+
const PRERENDERED_DIR = "_prerendered";
|
|
985
994
|
/**
|
|
986
995
|
* Codegen the per-target **server entry** module (source text) for `buildServer` to bundle. It imports
|
|
987
996
|
* the app's `adapter` (from `framework.ts`), the optional `backend` (from `backend.ts`), and the
|
|
@@ -1016,7 +1025,20 @@ export function generateServerEntry(options) {
|
|
|
1016
1025
|
if (target === "static") {
|
|
1017
1026
|
throw new Error("[nifra/web] generateServerEntry: `static` has no server entry (SSG only)");
|
|
1018
1027
|
}
|
|
1019
|
-
|
|
1028
|
+
// Bun serves the client bundle through `servePublicDir` and Node through @nifrajs/node's own reader,
|
|
1029
|
+
// both on core's static-file policy. Deno bundles for the browser condition, where `node:fs` is a
|
|
1030
|
+
// stub, so its entry runs the same checks on Deno's own file API.
|
|
1031
|
+
const nodeFs = target === "bun" || target === "node";
|
|
1032
|
+
const prerendered = Object.entries(options.prerenderedPages ?? {});
|
|
1033
|
+
if (prerendered.length > 0 && !nodeFs) {
|
|
1034
|
+
throw new Error(`[nifra/web] generateServerEntry: prerendered pages are served by the bun and node targets, not ${target}`);
|
|
1035
|
+
}
|
|
1036
|
+
const webImports = ["createWebApp"];
|
|
1037
|
+
if (prerendered.length > 0)
|
|
1038
|
+
webImports.push("prerenderedPageReader");
|
|
1039
|
+
if (target === "bun")
|
|
1040
|
+
webImports.push("servePublicDir");
|
|
1041
|
+
const lines = [`import { ${webImports.join(", ")} } from "@nifrajs/web"`];
|
|
1020
1042
|
if (backendImport !== undefined)
|
|
1021
1043
|
lines.push('import { inProcessClient } from "@nifrajs/client"');
|
|
1022
1044
|
lines.push(`import { adapter } from ${JSON.stringify(adapterImport)}`);
|
|
@@ -1036,7 +1058,17 @@ export function generateServerEntry(options) {
|
|
|
1036
1058
|
lines.push('import { toFetchHandler } from "@nifrajs/core/server"');
|
|
1037
1059
|
if (target === "node")
|
|
1038
1060
|
lines.push('import { serve } from "@nifrajs/node"');
|
|
1039
|
-
|
|
1061
|
+
if (nodeFs)
|
|
1062
|
+
lines.push('import { fileURLToPath } from "node:url"');
|
|
1063
|
+
if (target === "deno") {
|
|
1064
|
+
lines.push('import { parseByteRange } from "@nifrajs/core/range"', 'import { contentTags, contentTypeOf, ifRangeAllowsPartial, isHiddenPath, isNotModified, lastModifiedOf } from "@nifrajs/core/static-file"', 'import { createHash } from "node:crypto"');
|
|
1065
|
+
}
|
|
1066
|
+
if (prerendered.length > 0) {
|
|
1067
|
+
const headers = Object.fromEntries(prerendered.map(([path, page]) => [path, page.headers]));
|
|
1068
|
+
const files = Object.fromEntries(prerendered.map(([path, page]) => [path, page.file]));
|
|
1069
|
+
lines.push("", `// The pages the build prerendered into ${PRERENDERED_DIR}/, answered from their file after \`use\`.`, "const prerenderedPages = {", ` pages: ${JSON.stringify(headers)},`, " read: prerenderedPageReader({", ` dir: fileURLToPath(new URL(${JSON.stringify(`./${PRERENDERED_DIR}/`)}, import.meta.url)),`, ` files: ${JSON.stringify(files)},`, " }),", "}");
|
|
1070
|
+
}
|
|
1071
|
+
lines.push("", "const app = createWebApp({", " adapter,", ...(useImport !== undefined ? [" use,"] : []), ...(prerendered.length > 0 ? [" prerenderedPages,"] : []), ...importedOptions.map((name) => ` ${name},`), " manifest,", " clientEntry,", " styles,", " routeStyles,", " cssLoading,", ...(options.copyChunks === true ? [" copyChunks,"] : []), ...(backendImport !== undefined ? [" api: inProcessClient(backend),"] : []), ` title: ${JSON.stringify(title)},`,
|
|
1040
1072
|
// Core's own `{ header }` trust, so the mounted backend and every loader see the derived caller.
|
|
1041
1073
|
...(clientIpHeader !== undefined
|
|
1042
1074
|
? [` server: { clientIp: { header: ${JSON.stringify(clientIpHeader)} } },`]
|
|
@@ -1059,28 +1091,203 @@ export function generateServerEntry(options) {
|
|
|
1059
1091
|
// bun / node / deno self-host AND serve the client bundle from disk (it sits next to this entry).
|
|
1060
1092
|
lines.push("// The client bundle and public files live next to this entry; serve only manifest-approved paths.", 'const STATIC_ROOT = new URL("./", import.meta.url)', `const PUBLIC_FILES = new Set(${JSON.stringify(publicFiles)})`, ...(hashedFiles !== undefined
|
|
1061
1093
|
? [`const HASHED_FILES = new Set(${JSON.stringify(hashedFiles)})`]
|
|
1062
|
-
: [])
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1094
|
+
: []));
|
|
1095
|
+
if (target === "node") {
|
|
1096
|
+
// The app itself with the adapter's own reader, not a `{ fetch }` wrapper around app.fetch: the
|
|
1097
|
+
// wrapper hid the node-direct lane, so every page built an undici Request and drained a Response,
|
|
1098
|
+
// and every file went through one too. The root mount serves only the files the build listed.
|
|
1099
|
+
lines.push("await serve(app, {", " port: Number(process.env.PORT ?? 3000),", " static: [", " {", ' dir: fileURLToPath(new URL("./assets/", import.meta.url)),', ' prefix: "/assets",', ...(hashedFiles !== undefined ? [" hashedFiles: HASHED_FILES,"] : []), ` cacheControl: ${JSON.stringify(ONE_DAY)},`, " },", " {", " dir: fileURLToPath(STATIC_ROOT),", ' prefix: "/",', " files: PUBLIC_FILES,", " immutable: false,", ` cacheControl: ${JSON.stringify(ONE_DAY)},`, " },", " ],", "})");
|
|
1100
|
+
return `${lines.join("\n")}\n`;
|
|
1101
|
+
}
|
|
1102
|
+
lines.push("const staticPath = (pathname: string): string | undefined => {", " if (PUBLIC_FILES.has(pathname)) return '.' + pathname", ' if (!pathname.startsWith("/assets/")) return undefined', ' const segments = pathname.slice(1).split("/")', " // `.` and `..` match the character class, and the path is joined onto a base URL that resolves", " // dot segments - so they are rejected explicitly rather than relying on the request URL having", " // been normalized upstream.", " const safe = (segment: string): boolean =>", ' segment !== "." && segment !== ".." && /^[A-Za-z0-9._-]+$/.test(segment)', " return segments.every(safe) ? '.' + pathname : undefined", "}",
|
|
1103
|
+
// The handler `nifra dev` serves public/ with: its type table, nosniff and cache policy, content
|
|
1104
|
+
// tags, byte ranges and conditional requests, and a descriptor opened only once the real path is
|
|
1105
|
+
// inside the root, so a link planted in the deployed tree cannot reach a file outside it.
|
|
1106
|
+
...(target === "bun"
|
|
1107
|
+
? [
|
|
1108
|
+
"const serveStatic = servePublicDir({",
|
|
1109
|
+
" dir: fileURLToPath(STATIC_ROOT),",
|
|
1110
|
+
...(hashedFiles !== undefined ? [" hashedFiles: HASHED_FILES,"] : []),
|
|
1111
|
+
"})",
|
|
1112
|
+
"const staticResponse = async (req: Request): Promise<Response> =>",
|
|
1113
|
+
' (await serveStatic(req)) ?? new Response("not found", { status: 404 })',
|
|
1114
|
+
]
|
|
1115
|
+
: denoStaticResponse(hashedFiles !== undefined)));
|
|
1066
1116
|
// Bun.serve and Deno.serve delimit the body themselves, so its declared length is the transport
|
|
1067
1117
|
// frame - the guarantee core's own listen() and the Deno adapter mark (see markTrustedBodyFraming).
|
|
1068
1118
|
const markFramed = '(req as unknown as Record<symbol, unknown>)[Symbol.for("nifra.body.trustedFraming")] = true';
|
|
1069
1119
|
if (target === "bun") {
|
|
1070
|
-
lines.push("const server = Bun.serve({", " port: Number(Bun.env.PORT ?? 3000),", " async fetch(req, server) {", " const { pathname } = new URL(req.url)",
|
|
1120
|
+
lines.push("const server = Bun.serve({", " port: Number(Bun.env.PORT ?? 3000),", " async fetch(req, server) {", " const { pathname } = new URL(req.url)",
|
|
1121
|
+
// Braced: the next emitted line opens with `(`, which would otherwise call the returned value.
|
|
1122
|
+
" if (staticPath(pathname) !== undefined) {", " return staticResponse(req)", " }", ` ${markFramed}`, " // The socket peer, looked up only when something reads it (as core's own listen() does).", " return app.fetch(req, { get clientIp() { return server.requestIP(req)?.address } })", " },", "})",
|
|
1071
1123
|
// The `${...}` here is literal OUTPUT (a template in the GENERATED file), not a template in this
|
|
1072
1124
|
// source - split so biome's noTemplateCurlyInString doesn't flag it; the emitted line is unchanged.
|
|
1073
1125
|
`console.log(\`nifra (Bun) → http://localhost:$${"{server.port}"}\`)`);
|
|
1074
1126
|
return `${lines.join("\n")}\n`;
|
|
1075
1127
|
}
|
|
1076
|
-
if (target === "node") {
|
|
1077
|
-
lines.push('import { readFile } from "node:fs/promises"', "await serve(", " {", " async fetch(req, platform) {", " const { pathname } = new URL(req.url)", " const filePath = staticPath(pathname)", " if (filePath !== undefined) {", " try {", " const body = await readFile(new URL(filePath, STATIC_ROOT))", " return new Response(body, { headers: staticHeaders(pathname) })", " } catch {", ' return new Response("not found", { status: 404 })', " }", " }", " return app.fetch(req, platform)", " },", " },", " { port: Number(process.env.PORT ?? 3000) },", ")");
|
|
1078
|
-
return `${lines.join("\n")}\n`;
|
|
1079
|
-
}
|
|
1080
1128
|
// deno
|
|
1081
|
-
lines.push("// @ts-ignore - Deno global is present on the Deno runtime this output targets.", 'Deno.serve({ port: Number(Deno.env.get("PORT") ?? "3000") }, async (req, info) => {', " const { pathname } = new URL(req.url)", " const filePath = staticPath(pathname)", " if (filePath !== undefined) {", "
|
|
1129
|
+
lines.push("// @ts-ignore - Deno global is present on the Deno runtime this output targets.", 'Deno.serve({ port: Number(Deno.env.get("PORT") ?? "3000") }, async (req, info) => {', " const { pathname } = new URL(req.url)", " const filePath = staticPath(pathname)", " if (filePath !== undefined) {", " return staticResponse(req, filePath, pathname)", " }", ` ${markFramed}`, " return app.fetch(req, { clientIp: info.remoteAddr.hostname })", "})");
|
|
1082
1130
|
return `${lines.join("\n")}\n`;
|
|
1083
1131
|
}
|
|
1132
|
+
/**
|
|
1133
|
+
* The Deno entry's static handler: `servePublicDir`'s checks and answers on Deno's file API. The real
|
|
1134
|
+
* path must sit inside the root and cross no hidden segment, and the opened file must still be the one
|
|
1135
|
+
* that path names, so a link planted in the deployed tree cannot reach a file outside it or a `.env`
|
|
1136
|
+
* inside it. The file is typed by its real path, tagged by its content and dated only by an mtime its
|
|
1137
|
+
* own last write set, through the same policy as servePublicDir: a current cached copy is a 304, and a
|
|
1138
|
+
* range survives only an `If-Range` naming that tag. A file is read once to tag it, and one small
|
|
1139
|
+
* enough is then served from those bytes; otherwise one byte range is served from a seek, several as
|
|
1140
|
+
* the whole file.
|
|
1141
|
+
*/
|
|
1142
|
+
function denoStaticResponse(hashed) {
|
|
1143
|
+
return [
|
|
1144
|
+
"const staticHeaders = (pathname: string, file: string): Record<string, string> => ({",
|
|
1145
|
+
' "content-type": contentTypeOf(file),',
|
|
1146
|
+
` "cache-control": ${hashed ? "HASHED_FILES.has(pathname)" : 'pathname.startsWith("/assets/")'} ? ${JSON.stringify(IMMUTABLE)} : ${JSON.stringify(ONE_DAY)},`,
|
|
1147
|
+
' "x-content-type-options": "nosniff",',
|
|
1148
|
+
' "accept-ranges": "bytes",',
|
|
1149
|
+
"})",
|
|
1150
|
+
"// @ts-ignore - Deno global is present on the Deno runtime this output targets.",
|
|
1151
|
+
"const STATIC_REAL: string = await Deno.realPath(STATIC_ROOT)",
|
|
1152
|
+
"// One per process: the tag of a file is remembered by its real path.",
|
|
1153
|
+
"const STATIC_TAGS = contentTags(createHash)",
|
|
1154
|
+
"// Up to this many bytes are read whole to tag a file, and the response is then served from them.",
|
|
1155
|
+
"const STATIC_INLINE_MAX = 256 * 1024",
|
|
1156
|
+
"const staticResponse = async (req: Request, filePath: string, pathname: string): Promise<Response> => {",
|
|
1157
|
+
' const missing = () => new Response("not found", { status: 404 })',
|
|
1158
|
+
' if (req.method !== "GET" && req.method !== "HEAD") return missing()',
|
|
1159
|
+
" let file, info",
|
|
1160
|
+
' let real = ""',
|
|
1161
|
+
" try {",
|
|
1162
|
+
" // @ts-ignore - Deno global is present on the Deno runtime this output targets.",
|
|
1163
|
+
" real = await Deno.realPath(new URL(filePath, STATIC_ROOT))",
|
|
1164
|
+
' if (!real.startsWith(STATIC_REAL + (STATIC_REAL.includes("\\\\") ? "\\\\" : "/"))) return missing()',
|
|
1165
|
+
" if (isHiddenPath(real.slice(STATIC_REAL.length + 1))) return missing()",
|
|
1166
|
+
" // @ts-ignore - Deno global is present on the Deno runtime this output targets.",
|
|
1167
|
+
" file = await Deno.open(real)",
|
|
1168
|
+
" // Deno.open follows links, so a directory swapped for one after the check above redirects it.",
|
|
1169
|
+
" // The opened file must be the one at `real`, which must still resolve to itself.",
|
|
1170
|
+
" const opened = await file.stat()",
|
|
1171
|
+
" // @ts-ignore - Deno global is present on the Deno runtime this output targets.",
|
|
1172
|
+
" const named = await Deno.stat(real)",
|
|
1173
|
+
" // @ts-ignore - Deno global is present on the Deno runtime this output targets.",
|
|
1174
|
+
" if (opened.dev !== named.dev || opened.ino !== named.ino || (await Deno.realPath(real)) !== real) {",
|
|
1175
|
+
" file.close()",
|
|
1176
|
+
" return missing()",
|
|
1177
|
+
" }",
|
|
1178
|
+
" info = opened",
|
|
1179
|
+
" } catch {",
|
|
1180
|
+
" file?.close()",
|
|
1181
|
+
" return missing()",
|
|
1182
|
+
" }",
|
|
1183
|
+
" if (!info.isFile) {",
|
|
1184
|
+
" file.close()",
|
|
1185
|
+
" return missing()",
|
|
1186
|
+
" }",
|
|
1187
|
+
" const size: number = info.size",
|
|
1188
|
+
" const headers = new Headers(staticHeaders(pathname, real))",
|
|
1189
|
+
" // Deno reports a field the platform does not keep as null; NaN never matches a remembered version.",
|
|
1190
|
+
" const version = {",
|
|
1191
|
+
" dev: info.dev,",
|
|
1192
|
+
" ino: info.ino ?? Number.NaN,",
|
|
1193
|
+
" size,",
|
|
1194
|
+
" mtimeMs: info.mtime?.getTime() ?? Number.NaN,",
|
|
1195
|
+
" ctimeMs: info.ctime?.getTime() ?? Number.NaN,",
|
|
1196
|
+
" }",
|
|
1197
|
+
" let whole: Uint8Array<ArrayBuffer> | undefined",
|
|
1198
|
+
" let moved = false",
|
|
1199
|
+
" let etag: string | undefined",
|
|
1200
|
+
" try {",
|
|
1201
|
+
" etag = STATIC_TAGS.cached(real, version)",
|
|
1202
|
+
" if (etag === undefined && size <= STATIC_INLINE_MAX) {",
|
|
1203
|
+
" whole = new Uint8Array(size)",
|
|
1204
|
+
" let filled = 0",
|
|
1205
|
+
" while (filled < size) {",
|
|
1206
|
+
" const read: number | null = await file.read(whole.subarray(filled))",
|
|
1207
|
+
" if (read === null || read === 0) break",
|
|
1208
|
+
" filled += read",
|
|
1209
|
+
" }",
|
|
1210
|
+
" if (filled < size) whole = whole.subarray(0, filled)",
|
|
1211
|
+
" etag = STATIC_TAGS.ofBytes(real, version, whole)",
|
|
1212
|
+
" }",
|
|
1213
|
+
" etag ??= await STATIC_TAGS.ofFile(real, version, async (bytes: Uint8Array, position: number) => {",
|
|
1214
|
+
" moved = true",
|
|
1215
|
+
" // @ts-ignore - Deno global is present on the Deno runtime this output targets.",
|
|
1216
|
+
" await file.seek(position, Deno.SeekMode.Start)",
|
|
1217
|
+
" return (await file.read(bytes)) ?? 0",
|
|
1218
|
+
" })",
|
|
1219
|
+
" } catch {",
|
|
1220
|
+
" file.close()",
|
|
1221
|
+
" return missing()",
|
|
1222
|
+
" }",
|
|
1223
|
+
' if (etag !== undefined) headers.set("etag", etag)',
|
|
1224
|
+
" const lastModified = lastModifiedOf(version)",
|
|
1225
|
+
' if (lastModified !== undefined) headers.set("last-modified", new Date(lastModified).toUTCString())',
|
|
1226
|
+
' if (isNotModified(req.headers.get("if-none-match"), req.headers.get("if-modified-since"), etag, lastModified)) {',
|
|
1227
|
+
" file.close()",
|
|
1228
|
+
' headers.delete("content-type")',
|
|
1229
|
+
" return new Response(null, { status: 304, headers })",
|
|
1230
|
+
" }",
|
|
1231
|
+
' const rangeHeader = req.method === "GET" ? req.headers.get("range") : null',
|
|
1232
|
+
" const range =",
|
|
1233
|
+
' rangeHeader !== null && ifRangeAllowsPartial(req.headers.get("if-range"), etag, lastModified)',
|
|
1234
|
+
" ? parseByteRange(rangeHeader, size)",
|
|
1235
|
+
' : { kind: "none" as const }',
|
|
1236
|
+
' if (req.method === "HEAD" || range.kind === "unsatisfiable") {',
|
|
1237
|
+
" file.close()",
|
|
1238
|
+
' if (range.kind !== "unsatisfiable") {',
|
|
1239
|
+
' headers.set("content-length", String(size))',
|
|
1240
|
+
" return new Response(null, { headers })",
|
|
1241
|
+
" }",
|
|
1242
|
+
' headers.delete("content-type")',
|
|
1243
|
+
' headers.set("content-range", "bytes */" + size)',
|
|
1244
|
+
" return new Response(null, { status: 416, headers })",
|
|
1245
|
+
" }",
|
|
1246
|
+
' if (range.kind === "satisfiable" && range.ranges.length === 1) {',
|
|
1247
|
+
" const { start, end } = range.ranges[0]!",
|
|
1248
|
+
' headers.set("content-range", "bytes " + start + "-" + end + "/" + size)',
|
|
1249
|
+
" if (whole !== undefined) {",
|
|
1250
|
+
" file.close()",
|
|
1251
|
+
" const part = whole.subarray(start, end + 1)",
|
|
1252
|
+
' headers.set("content-length", String(part.byteLength))',
|
|
1253
|
+
" return new Response(part, { status: 206, headers })",
|
|
1254
|
+
" }",
|
|
1255
|
+
" // @ts-ignore - Deno global is present on the Deno runtime this output targets.",
|
|
1256
|
+
" await file.seek(start, Deno.SeekMode.Start)",
|
|
1257
|
+
" let left = end - start + 1",
|
|
1258
|
+
' headers.set("content-length", String(left))',
|
|
1259
|
+
" const body = new ReadableStream<Uint8Array>({",
|
|
1260
|
+
" async pull(controller) {",
|
|
1261
|
+
" const chunk = new Uint8Array(Math.min(left, 65_536))",
|
|
1262
|
+
" const read: number | null = await file.read(chunk)",
|
|
1263
|
+
" if (read === null || read === 0) {",
|
|
1264
|
+
" file.close()",
|
|
1265
|
+
" controller.close()",
|
|
1266
|
+
" return",
|
|
1267
|
+
" }",
|
|
1268
|
+
" controller.enqueue(chunk.subarray(0, read))",
|
|
1269
|
+
" left -= read",
|
|
1270
|
+
" if (left <= 0) {",
|
|
1271
|
+
" file.close()",
|
|
1272
|
+
" controller.close()",
|
|
1273
|
+
" }",
|
|
1274
|
+
" },",
|
|
1275
|
+
" cancel: () => file.close(),",
|
|
1276
|
+
" })",
|
|
1277
|
+
" return new Response(body, { status: 206, headers })",
|
|
1278
|
+
" }",
|
|
1279
|
+
" if (whole !== undefined) {",
|
|
1280
|
+
" file.close()",
|
|
1281
|
+
' headers.set("content-length", String(whole.byteLength))',
|
|
1282
|
+
" return new Response(whole, { headers })",
|
|
1283
|
+
" }",
|
|
1284
|
+
" // @ts-ignore - Deno global is present on the Deno runtime this output targets.",
|
|
1285
|
+
" if (moved) await file.seek(0, Deno.SeekMode.Start)",
|
|
1286
|
+
' headers.set("content-length", String(size))',
|
|
1287
|
+
" return new Response(file.readable, { headers })",
|
|
1288
|
+
"}",
|
|
1289
|
+
];
|
|
1290
|
+
}
|
|
1084
1291
|
/** Measure each emitted output file's raw + gzip size. Reads the file off disk and gzips it with
|
|
1085
1292
|
* `Bun.gzipSync` (the over-the-wire weight). Async only because it reads files; the aggregation is the
|
|
1086
1293
|
* pure {@link aggregateSizeReport}. */
|
|
@@ -1161,6 +1368,36 @@ function assertNoPublicCollision(plan, publicFiles, outDir) {
|
|
|
1161
1368
|
`build's own output in ${outDir}. Rename or move them out of the public directory.`);
|
|
1162
1369
|
}
|
|
1163
1370
|
}
|
|
1371
|
+
/** Prerender a self-hosting build's opted-in routes into `dir` and return the pages its server may
|
|
1372
|
+
* answer from their file: those whose render was not specific to its request. */
|
|
1373
|
+
async function prerenderForServer(prerenderApp, client, routesDir, dir, publicFiles, secrets) {
|
|
1374
|
+
const owned = `/${PRERENDERED_DIR}/`;
|
|
1375
|
+
const clashes = publicFiles.filter((file) => file.toLowerCase().startsWith(owned));
|
|
1376
|
+
if (clashes.length > 0) {
|
|
1377
|
+
throw new Error(`[nifra/web] public file(s) ${clashes.join(", ")} sit under ${owned}, where the build writes its ` +
|
|
1378
|
+
"prerendered pages. Rename or move them out of the public directory.");
|
|
1379
|
+
}
|
|
1380
|
+
let result;
|
|
1381
|
+
try {
|
|
1382
|
+
result = await prerenderRoutes({
|
|
1383
|
+
app: await prerenderApp(client),
|
|
1384
|
+
routes: discoverRoutes(routesDir).routes,
|
|
1385
|
+
outDir: dir,
|
|
1386
|
+
secrets,
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
catch (error) {
|
|
1390
|
+
// Prerendering loads every route module at build time, which a server build never needed before:
|
|
1391
|
+
// one that cannot load there (a connection opened on import, an env var only production sets) costs
|
|
1392
|
+
// the speedup, not the build. A secret in a page still fails it.
|
|
1393
|
+
if (error instanceof SecretScanError)
|
|
1394
|
+
throw error;
|
|
1395
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1396
|
+
console.warn(`[nifra/web] prerendering failed, so the server renders every page on demand: ${error instanceof Error ? error.message : String(error)}`);
|
|
1397
|
+
return {};
|
|
1398
|
+
}
|
|
1399
|
+
return servablePrerenderedPages(result.prerendered);
|
|
1400
|
+
}
|
|
1164
1401
|
/** Build a full deploy dir for `target` using the default Bun bundler. See {@link buildTargetWith}. */
|
|
1165
1402
|
export async function buildTarget(target, options) {
|
|
1166
1403
|
return buildTargetWith(target, options, bunBundler);
|
|
@@ -1186,6 +1423,12 @@ export async function buildTargetWith(target, options, bundler) {
|
|
|
1186
1423
|
];
|
|
1187
1424
|
assertDisposableDir("outDir", outDir, sources);
|
|
1188
1425
|
assertDisposableDir("workDir", workDir, sources);
|
|
1426
|
+
// Before anything is cleared, so a refused build leaves the last deploy dir in place.
|
|
1427
|
+
if (targetPlan.target === "node") {
|
|
1428
|
+
const refusal = nodeAdapterRefusal(workDir);
|
|
1429
|
+
if (refusal !== undefined)
|
|
1430
|
+
throw new Error(refusal);
|
|
1431
|
+
}
|
|
1189
1432
|
const { rmSync } = await import("node:fs");
|
|
1190
1433
|
rmSync(outDir, { recursive: true, force: true });
|
|
1191
1434
|
rmSync(workDir, { recursive: true, force: true });
|
|
@@ -1243,6 +1486,12 @@ export async function buildTargetWith(target, options, bundler) {
|
|
|
1243
1486
|
if (requestedCssLoading !== undefined || publicFiles.length > 0) {
|
|
1244
1487
|
writeFileSync(`${assetsDir}/manifest.json`, JSON.stringify(client, null, 2));
|
|
1245
1488
|
}
|
|
1489
|
+
// A self-hosting server answers its prerendered pages from their file. The static target is all
|
|
1490
|
+
// prerendered pages; the edge targets keep rendering them on demand.
|
|
1491
|
+
const prerenderedPages = options.prerenderApp !== undefined &&
|
|
1492
|
+
(targetPlan.target === "bun" || targetPlan.target === "node")
|
|
1493
|
+
? await prerenderForServer(options.prerenderApp, client, routesDir, `${staticRoot}/${PRERENDERED_DIR}`, publicFiles, secretScan)
|
|
1494
|
+
: undefined;
|
|
1246
1495
|
if (targetPlan.kind === "static") {
|
|
1247
1496
|
if (options.prerenderApp === undefined) {
|
|
1248
1497
|
throw new Error("[nifra/web] buildTarget(static) requires `prerenderApp` (a factory `(client) => createWebApp`)");
|
|
@@ -1289,6 +1538,7 @@ export async function buildTargetWith(target, options, bundler) {
|
|
|
1289
1538
|
hashedFiles: client.assets,
|
|
1290
1539
|
...(options.clientIp !== undefined ? { clientIp: options.clientIp } : {}),
|
|
1291
1540
|
...(client.copyChunks !== undefined ? { copyChunks: true } : {}),
|
|
1541
|
+
...(prerenderedPages !== undefined ? { prerenderedPages } : {}),
|
|
1292
1542
|
}));
|
|
1293
1543
|
const { worker } = await bundler.buildServer({
|
|
1294
1544
|
routesDir,
|