@nifrajs/web 5.1.0 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3042 -0
- package/dist/build-plan.d.ts +7 -0
- package/dist/build-plan.d.ts.map +1 -1
- package/dist/build-plan.js +19 -0
- package/dist/build-plan.js.map +1 -1
- package/dist/build-vite.d.ts +1 -0
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +51 -3
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +23 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +224 -16
- package/dist/build.js.map +1 -1
- package/dist/client.d.ts +2 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +14 -7
- package/dist/client.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 +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/codegen.d.ts +6 -0
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +23 -5
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/copy-split.d.ts +27 -0
- package/dist/internal/copy-split.d.ts.map +1 -0
- package/dist/internal/copy-split.js +173 -0
- package/dist/internal/copy-split.js.map +1 -0
- package/dist/internal/deferred-free.d.ts +3 -0
- package/dist/internal/deferred-free.d.ts.map +1 -0
- package/dist/internal/deferred-free.js +15 -0
- package/dist/internal/deferred-free.js.map +1 -0
- package/dist/internal/module-preparation.d.ts +7 -0
- package/dist/internal/module-preparation.d.ts.map +1 -0
- package/dist/internal/module-preparation.js +24 -0
- package/dist/internal/module-preparation.js.map +1 -0
- package/dist/internal/output-guard.d.ts +2 -0
- package/dist/internal/output-guard.d.ts.map +1 -1
- package/dist/internal/output-guard.js +51 -9
- package/dist/internal/output-guard.js.map +1 -1
- package/dist/internal/page-execution.d.ts +20 -1
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +115 -50
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +3 -1
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/prerendered-pages.d.ts +42 -0
- package/dist/internal/prerendered-pages.d.ts.map +1 -0
- package/dist/internal/prerendered-pages.js +92 -0
- package/dist/internal/prerendered-pages.js.map +1 -0
- package/dist/internal/public-url.d.ts +7 -0
- package/dist/internal/public-url.d.ts.map +1 -1
- package/dist/internal/public-url.js +13 -0
- package/dist/internal/public-url.js.map +1 -1
- package/dist/internal/render-document.d.ts +11 -2
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +131 -41
- 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/typescript-api.d.ts +4 -0
- package/dist/internal/typescript-api.d.ts.map +1 -0
- package/dist/internal/typescript-api.js +8 -0
- package/dist/internal/typescript-api.js.map +1 -0
- package/dist/internal/vite-plugin-compat.d.ts +28 -0
- package/dist/internal/vite-plugin-compat.d.ts.map +1 -0
- package/dist/internal/vite-plugin-compat.js +82 -0
- package/dist/internal/vite-plugin-compat.js.map +1 -0
- package/dist/internal/web-app.d.ts +45 -0
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +32 -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/plugins/import-glob.d.ts +20 -0
- package/dist/plugins/import-glob.d.ts.map +1 -0
- package/dist/plugins/import-glob.js +401 -0
- package/dist/plugins/import-glob.js.map +1 -0
- package/dist/plugins/kit.d.ts +11 -0
- package/dist/plugins/kit.d.ts.map +1 -1
- package/dist/plugins/kit.js +32 -0
- package/dist/plugins/kit.js.map +1 -1
- package/dist/plugins/stylex.d.ts.map +1 -1
- package/dist/plugins/stylex.js +2 -2
- package/dist/plugins/stylex.js.map +1 -1
- package/dist/prerender.d.ts +4 -0
- package/dist/prerender.d.ts.map +1 -1
- package/dist/prerender.js +9 -1
- package/dist/prerender.js.map +1 -1
- package/dist/public-dir.d.ts +15 -0
- package/dist/public-dir.d.ts.map +1 -1
- package/dist/public-dir.js +91 -12
- package/dist/public-dir.js.map +1 -1
- package/dist/render-seam.d.ts +42 -2
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +33 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/router.d.ts +3 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +4 -4
- package/dist/router.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/server-components/typescript.d.ts.map +1 -1
- package/dist/server-components/typescript.js +2 -7
- package/dist/server-components/typescript.js.map +1 -1
- package/dist/vite.d.ts +7 -28
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +130 -123
- package/dist/vite.js.map +1 -1
- package/package.json +5 -4
- package/src/build-plan.ts +21 -0
- package/src/build-vite.ts +60 -3
- package/src/build.ts +283 -39
- package/src/client.ts +15 -7
- package/src/csp.ts +4 -0
- package/src/deferred.ts +20 -3
- package/src/index.ts +9 -0
- package/src/internal/codegen.ts +32 -3
- package/src/internal/copy-split.ts +220 -0
- package/src/internal/deferred-free.ts +15 -0
- package/src/internal/module-preparation.ts +30 -0
- package/src/internal/output-guard.ts +51 -9
- package/src/internal/page-execution.ts +137 -53
- package/src/internal/parity.ts +2 -1
- package/src/internal/prerendered-pages.ts +124 -0
- package/src/internal/public-url.ts +13 -0
- package/src/internal/render-document.ts +179 -58
- package/src/internal/request-url.ts +24 -0
- package/src/internal/secret-scan.ts +9 -1
- package/src/internal/typescript-api.ts +11 -0
- package/src/internal/vite-plugin-compat.ts +81 -0
- package/src/internal/web-app.ts +82 -2
- package/src/isr.ts +7 -4
- package/src/plugins/import-glob.ts +392 -0
- package/src/plugins/kit.ts +36 -0
- package/src/plugins/stylex.ts +2 -1
- package/src/prerender.ts +13 -1
- package/src/public-dir.ts +107 -12
- package/src/render-seam.ts +65 -2
- package/src/router.ts +7 -6
- package/src/search.ts +1 -0
- package/src/server-components/typescript.ts +2 -10
- package/src/vite.ts +133 -135
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,3042 @@
|
|
|
1
|
+
# @nifrajs/web
|
|
2
|
+
|
|
3
|
+
## 5.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
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`.
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
Keep custom start-only `ObservationLifecycle` implementations compatible. The factory returns the extended `RecordingObservationLifecycle` interface with the new `record` method.
|
|
16
|
+
|
|
17
|
+
Mark the migration parser's TypeScript keyword-kind comparison as syntax, so the security check does not mistake it for a secret comparison.
|
|
18
|
+
- 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.
|
|
19
|
+
|
|
20
|
+
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.
|
|
21
|
+
|
|
22
|
+
These keep rendering on demand:
|
|
23
|
+
- a request with a query string
|
|
24
|
+
- a navigation data or server-component request
|
|
25
|
+
- a draft-mode visitor
|
|
26
|
+
- a page whose build-time render set a cookie, was `private` or `no-store`, or varied on a request header
|
|
27
|
+
- every page of an app with a `nonce`, whose documents need a nonce minted per request.
|
|
28
|
+
|
|
29
|
+
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.
|
|
30
|
+
|
|
31
|
+
New in `@nifrajs/web`:
|
|
32
|
+
- `createWebApp({ prerenderedPages })`
|
|
33
|
+
- `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
|
|
34
|
+
- `PrerenderEntry.serveHeaders` on `prerenderRoutes` results.
|
|
35
|
+
|
|
36
|
+
Prerendering now runs during these builds, so it loads every route module and runs the opted-in loaders at build time. Two effects:
|
|
37
|
+
- If a route module cannot load there, the build warns and every page renders on demand, as before.
|
|
38
|
+
- A secret in a prerendered page fails the build, as it does for `--target static`.
|
|
39
|
+
|
|
40
|
+
The Vite pipeline, `deno`, and the edge targets are unchanged.
|
|
41
|
+
|
|
42
|
+
### Patch Changes
|
|
43
|
+
|
|
44
|
+
- 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.
|
|
45
|
+
- 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.
|
|
46
|
+
- 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.
|
|
47
|
+
- 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.
|
|
48
|
+
- 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.
|
|
49
|
+
- 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.
|
|
50
|
+
- 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.
|
|
51
|
+
- 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.
|
|
52
|
+
- 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.
|
|
53
|
+
- 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.
|
|
54
|
+
- 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.
|
|
55
|
+
- 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.
|
|
56
|
+
- 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.
|
|
57
|
+
- 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.
|
|
58
|
+
- 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.
|
|
59
|
+
- Updated dependencies [7ab9692]
|
|
60
|
+
- Updated dependencies [808bac8]
|
|
61
|
+
- Updated dependencies [d4bae56]
|
|
62
|
+
- Updated dependencies [007060d]
|
|
63
|
+
- Updated dependencies [190ab39]
|
|
64
|
+
- Updated dependencies [7cf8df3]
|
|
65
|
+
- @nifrajs/core@5.3.0
|
|
66
|
+
- @nifrajs/island-trigger@5.3.0
|
|
67
|
+
|
|
68
|
+
## 5.2.1
|
|
69
|
+
|
|
70
|
+
### Patch Changes
|
|
71
|
+
|
|
72
|
+
- @nifrajs/core@5.2.1
|
|
73
|
+
- @nifrajs/island-trigger@5.2.1
|
|
74
|
+
|
|
75
|
+
## 5.2.0
|
|
76
|
+
|
|
77
|
+
### Minor Changes
|
|
78
|
+
|
|
79
|
+
- 281a48e: A production client build (Bun or Vite) moves copy tables (`@nifrajs/i18n/copy`) into one chunk per locale, so a page's JavaScript carries the copy of the locale it renders in and none of the others; the server bundle keeps every table whole. The client manifest lists the chunks as `copyChunks`, the generated server entry passes them to the new `createWebApp({ copyChunks })` option (a hand-written entry passes the manifest's), and each page hands the map to the browser and preloads the chunks it reads: its URL's locale and the one its `<html lang>` names. A client navigation loads the next URL's chunk with the route's code; the router passes the target path to `loadModule` for that. The build reads tables with the app's TypeScript (5.9 or 6) and warns and leaves them whole without one; `buildClient({ copySplit: false })` turns it off. `readSource()` and `addSourceTransform()` in `@nifrajs/web/plugins/kit` let a plugin that claims a file still compile the rewritten source; the Solid and StyleX plugins read through it.
|
|
80
|
+
- 1d20d1f: An app registers its locale routing once: `export const i18n = urls` (the router `defineI18nRouting()` returns) from `backend/framework.ts`, which `nifra dev`, `nifra build` and the generated server entry pass to `createWebApp({ i18n })`. Every document then takes `<html lang dir>` from the registry's `documentMeta(localeOf(path))` unless the route's meta sets `lang`, on a hard load and after each client navigation, and carries `Content-Language`. Each adapter's new `useLocale()` returns the page's locale key, and `<I18nProvider>` formats in the page's locale when no `locale` is passed, with no loader handing either over. A route under a `[lang]` or `[[lang]]` directory runs `matchSegment()` before any middleware: an unserved locale, or a page missing from it, is a 404 through the app's `_404`, and a non-canonical prefix such as `/en/x` with an unprefixed default is a 308 to `/x`.
|
|
81
|
+
- b9e7c6b: The Bun production build (`buildClient`, `buildServer`, `nifra build`) and the `nifra dev --bun` client expand `import.meta.glob` the way Vite does, through the new `importGlobPlugin` in `@nifrajs/web/build`. A literal pattern, or an array of them, starting with `./`, `../` or `/` (the app root) with `!` exclusions becomes an object keyed as Vite keys it, of lazy `() => import(...)` loaders or, with `eager: true`, the modules themselves; `import: "default"` (or a name) picks one export, and `typeof import.meta.glob` reads `"function"`. A mention in a comment, a string or JSX text is left as written. A computed pattern, an alias or another option fails the build naming the file and line, and a client bundle that still calls `import.meta.glob` (a module another plugin loaded first) is refused instead of shipping a glob that loads nothing.
|
|
82
|
+
|
|
83
|
+
### Patch Changes
|
|
84
|
+
|
|
85
|
+
- 9498cf8: The package ships its `CHANGELOG.md`, so the installed package has the notes for every version, as the GitHub release does. Before, the tarball held only the code, `README.md`, `LICENSE` and `package.json`.
|
|
86
|
+
- 746ada9: Under Vite 8, a framework plugin's `optimizeDeps.rollupOptions` no longer prints Vite's "`optimizeDeps.rollupOptions` ... is deprecated" warning on every `nifra dev` start and Vite build. `@vitejs/plugin-react@4.x` sets `optimizeDeps.rollupOptions.jsx`; nifra already dropped the `jsx` key, but left the empty `rollupOptions` behind, which Vite still warns about. The key is now removed when nothing is left in it, and any remaining options move to `optimizeDeps.rolldownOptions`, where Vite reads them anyway. `ssr.optimizeDeps` is handled the same way, and the Vite production build now applies this to `vitePlugins` too.
|
|
87
|
+
- f9a19f3: The Bun build's `import.meta.glob` expansion writes each key, import path and `import` export name as a string literal with U+2028 and U+2029 escaped, so the generated code stays on one line for every JavaScript parser.
|
|
88
|
+
- c8d33eb: On Windows, the Bun build's `import.meta.glob` applies its `!` exclusions and its `node_modules` skip, and leaves the importing module out of its own matches: it matches every path with forward slashes, since a glob reads `\` as an escape.
|
|
89
|
+
- Updated dependencies [9498cf8]
|
|
90
|
+
- Updated dependencies [1f3ff32]
|
|
91
|
+
- Updated dependencies [4bb8e52]
|
|
92
|
+
- Updated dependencies [cdff19d]
|
|
93
|
+
- Updated dependencies [5edd0ba]
|
|
94
|
+
- Updated dependencies [000b4cb]
|
|
95
|
+
- Updated dependencies [c5455fb]
|
|
96
|
+
- Updated dependencies [6082ba5]
|
|
97
|
+
- Updated dependencies [fd58beb]
|
|
98
|
+
- Updated dependencies [ea25a61]
|
|
99
|
+
- Updated dependencies [ed13e36]
|
|
100
|
+
- @nifrajs/core@5.2.0
|
|
101
|
+
- @nifrajs/island-trigger@5.2.0
|
|
102
|
+
|
|
103
|
+
## 5.1.0
|
|
104
|
+
|
|
105
|
+
### Minor Changes
|
|
106
|
+
|
|
107
|
+
- 7bb8a67: `@nifrajs/web/seo` exports `canonical()`, `openGraph()` and `jsonLd()` (and `OpenGraphInput`) from a module that imports nothing at runtime. A route's `meta()` runs in the browser too, and the package root also exports server modules that import `node:fs`, `node:path` and `node:stream`. A bundle that cannot prune the root's unused re-exports would carry those into a browser chunk, and `nifra dev` refuses them. The root keeps exporting the same three functions. The SEO guide imports them from the new entry.
|
|
108
|
+
- 513ec28: `"nifra": { "zones": ... }` accepts single files and nested paths: an entry inside another entry's folder overrides it, so the most specific path decides a file's side (`"frontend": ["src/ui"], "backend": ["src/ui/mailer.ts"]`). Only one path listed under two sides is refused. Before, any two entries that overlapped were refused, and a file in a mixed folder could only move. A suffix that disagrees with a configured path names that path in the error.
|
|
109
|
+
- 8cc53d4: `/llms.txt` and `/llms-full.txt` are now off unless `createWebApp({ llmsTxt: true })`. Both are public and unauthenticated. They listed every page route and every backend route mounted over HTTP, admin and internal ones included, and the full text added each route's request and response schemas. An app that serves them on purpose sets `llmsTxt: true`. `publishLocalGuidelines` still controls whether `AGENTS.md` is included.
|
|
110
|
+
- 6594e19: `<svelte:head>` and Vue's `<Teleport>` are server-rendered. Both were dropped from the HTML, so a `noindex` robots tag, a canonical link or a teleported dialog appeared only after hydration, and never with JavaScript off.
|
|
111
|
+
|
|
112
|
+
- `@nifrajs/web-svelte` puts what `<svelte:head>` renders at the end of the document's `<head>`.
|
|
113
|
+
- `@nifrajs/web-vue` puts each teleport's content as the first children of its target, where Vue's hydration reads it. `body` content goes first in `<body>`. An `#id` target the app renders empty receives the content, and one the app does not render gets a container before the app root. Any other selector, a target with children of its own, and a page that uses `defer()` keep rendering teleports on the client, now with a development warning.
|
|
114
|
+
- `@nifrajs/web`: a render adapter that sets `documentParts` is passed `options.parts` and adds `head` and `body` markup there. An adapter without it is unchanged.
|
|
115
|
+
|
|
116
|
+
### Patch Changes
|
|
117
|
+
|
|
118
|
+
- 78b2a04: `revalidateEndpoint` refuses a JSON purge body over 1 MiB with `413 body_too_large`, counting bytes as the body streams in, instead of buffering whatever the caller sends.
|
|
119
|
+
- 638efe5: The private-environment check that `nifra check`, `nifra dev` and both builds share reads optional bracket access the way it reads dot access: `process.env?.["PUBLIC_X"]` is the public read it is, and `process.env?.["SECRET"]` is refused by name instead of as a whole-object read. Bracket access on the owner (`process["env"]`, `process?.["env"]`) and an optional call (`Deno.env.get?.("X")`) read the same way. A computed name (`process.env?.[name]`) is still refused.
|
|
120
|
+
- 2a0a5d3: Client form handling reads a form's `method`, `action` and `target` through the `HTMLFormElement` accessors and its `data-nifra-revalidate` attribute through `getAttribute`. A control named `target`, `action` or `method` shadows the form property of that name, so a form with `<input name="target">` was treated as targeting another frame and submitted with a full page load instead of the client router.
|
|
121
|
+
- 5372e3a: A package's own `"nifra": { "environment": ... }` may give each export entry its own side: `{ ".": "backend", "./react": "frontend", "./schema": "library" }`. An import is judged by the entry it names, the exact subpath first and then the most specific `*` pattern, so two entries backed by one file keep their own sides; importing an entry the map leaves out is an error, as is a key or side the map cannot use. A file reached by path alone, through a relative or aliased deep import, takes the side of every mapped entry that reaches it through the package's own imports under any export condition, and a file no mapped entry reaches is an error. An import that binds only types (`import type`, `export type`, or a `{ }` list of `type` specifiers) grants no ownership, so a backend file a frontend entry only types against stays backend. The Bun and Vite client and server builds, both dev servers, `nifra check` (NF-C028 and NF-C004) and the editor plugin apply the same rules. The Vite builds record the specifier behind each such import, since Rollup keeps only the resolved file. `classify(file, specifier)` on a zone classifier takes the import as written, and `declaredSide(environment, specifier)` reads one declaration. A package-wide string and the app's `"zones": { "packages": ... }` defaults behave as before, and a package's own declaration outranks them. A third-party package's side is read from its root manifest, past a nested `package.json` such as `dist/esm/package.json`.
|
|
122
|
+
- 29d6649: The build's credential scan no longer reports a translated field label or a documentation placeholder as a secret assigned to a secret-like name. A word-cased label (letters, marks and hyphen-joined words, such as `"Undirskriftar-Lykill"`) inside a top-level object literal that reads as a message catalog (at least ten string values, most of them prose with whitespace) is a label, and a value with a whole `_`/`-` segment such as `your`, `example`, `placeholder`, `sample`, `dummy`, `changeme`, `replace`, `generated` or `xxx` is a stand-in. Both apply only to the assigned-secret rule: a key format, a private key, a credential URL or the value of a private environment variable still fails the build wherever it is, and random data under a catalog key is still a finding.
|
|
123
|
+
- 2a0a5d3: The single-copy resolver and the build's duplicate-copy scan treat a `file:` dependency Bun installs as a folder of linked files the way they treat a linked package. Before, only a symlinked package directory counted, so the second React that such a package's own `node_modules` held went unredirected and unreported by `nifra check`.
|
|
124
|
+
- 744cc89: A layout loader that returns `redirect()` or a `Response` now refuses the request, the same way a thrown one does. A returned one used to be sent as layout data, next to the page loader's data, with a 200.
|
|
125
|
+
|
|
126
|
+
In development, nifra warns the first time a layout loader without `gate = true` refuses a request with a redirect, a `Response`, or a 401 or 403. Such a loader is not an authorization boundary: a client navigation can skip it, and an action below it runs without it. Refuse access in middleware, or export `gate = true`.
|
|
127
|
+
- ed6bb22: `redirect()` and the auth guards' `redirectTo` refuse a path whose dot segments resolve it to `//host`, such as `/.//evil.example/x` or `/a/..//evil.example/x` (`isSameOriginPath` is false for them). A redirect answering a client navigation or a JS-enhanced form submit no longer hands the router such a path either: a same-origin target whose path starts with `//` rides `x-nifra-redirect` as an absolute URL, so the browser stays on this origin instead of reading the path as a protocol-relative URL to another host.
|
|
128
|
+
- 5036c37: A page action now refuses a POST whose `Origin` header names another site, with `403`: plain text for a form submit and `{ ok: false, error: "forbidden_origin" }` for a client-router submit. This runs before middleware, gates or the action. Until now, any site could auto-submit a form to an action and the browser sent the visitor's cookies with it. Server functions and WebSocket routes already refused that. A request with no `Origin`, such as one from a server, still passes, and so does `Origin: null` with `Sec-Fetch-Site: same-origin`: a browser sends that for a form on the app's own page under `Referrer-Policy: no-referrer`, the `securityHeaders()` default. Same-origin is judged from the request URL's host, so a proxy must forward `Host`. A route that accepts posts from another site, such as an identity provider's `form_post` callback, exports `allowedOrigins` from its backend file: an array of exact origins, or a function `(origin: string | null) => boolean`.
|
|
129
|
+
- 2200f2d: The client router keeps a page whose path starts `//` or `/\` on its own origin. A catch-all route serves `https://shop.example//evil.example/x`. On that page, revalidating, navigating, submitting, and the full-page fallback used to pass the path to `fetch`, `history` or `location` as written. A browser reads such a path as a protocol-relative URL to `evil.example`, so it fetched that host's JSON and committed it as route data. These calls now resolve against the page's own origin.
|
|
130
|
+
- d3445a3: Prerendering writes each page under the decoded path a static host looks up: `/caf%C3%A9` is written to `café/index.html` instead of `caf%C3%A9/index.html`. Hosts, and nifra's own static file serving, decode the request path before they look for a file, so a page whose path had a non-ASCII or space character was never found. A path with a segment that no file name maps back to, such as a param that decodes to `/`, `\` or `..`, is skipped instead and left to the server to render.
|
|
131
|
+
- 2bb8d65: A root catch-all page (`routes/[...slug].tsx`, or `routes/[[lang]]/[...rest].tsx`) no longer makes `createWebApp` throw `conflicting parameter names for "/*"`. The framework's own fallback for unmatched paths is left out when a page already answers every path, the root included.
|
|
132
|
+
- df50368: When a Solid `<ErrorBoundary>` catches during server rendering, or a resource fails, the page no longer ships the error's message and stack in its inline hydration script. On Bun, that also included the server file path. The browser now receives `Error("Internal Server Error")`. In development and test the error keeps its text. That inline script now also carries the document's CSP nonce on pages rendered in one pass: `RenderAdapter.renderToString` receives `{ nonce }` as `renderToStream` already did. Before, a strict CSP blocked the script and hydration did not match the server's markup.
|
|
133
|
+
- Updated dependencies [abb4354]
|
|
134
|
+
- Updated dependencies [b276c67]
|
|
135
|
+
- Updated dependencies [2a0a5d3]
|
|
136
|
+
- Updated dependencies [72e1b4b]
|
|
137
|
+
- @nifrajs/island-trigger@5.1.0
|
|
138
|
+
- @nifrajs/core@5.1.0
|
|
139
|
+
|
|
140
|
+
## 5.0.0
|
|
141
|
+
|
|
142
|
+
### Major Changes
|
|
143
|
+
|
|
144
|
+
- a5963cb: An order-scoped hook (`derive`, `decorate`, `beforeHandle`, `afterHandle`, `around`, `aroundCapability`, `onError`) added to a `createWebApp` app after it returns now throws a `FrameworkError` with code `HOOK_AFTER_PAGES` outside production, naming the call site. Every page copies its hook chain when it is declared, so such a hook never ran on a page. 4.x accepted it without a word. Apply it in `createWebApp({ use })`, or inside `app.group(prefix, ...)` to scope it to the routes declared there. Global hooks (`onRequest`, `onResponse`), routes, groups and re-applying a named plugin that `use` already applied stay allowed.
|
|
145
|
+
- 3db36b3: `createWebApp` serves `api` over HTTP only at the `apiPrefix` it is given; without one the backend is loader-only (`ctx.api`) and a request under `/api` reaches page routing. 4.x mounted it at `/api` by default. `nifra dev`, `nifra build`, `nifra routes` and the page-under-mount check (NF-C027) follow the same rule for `backend/framework.ts`.
|
|
146
|
+
|
|
147
|
+
`nifra check` reports a `backend/framework.ts` that sets no `apiPrefix` while `backend/app.ts` exports a backend (NF-C035, info), and `nifra fix --code NF-C035` adds `export const apiPrefix = "/api"` to keep the 4.x mount. Set `apiPrefix = ""` to state that the backend is loader-only.
|
|
148
|
+
|
|
149
|
+
- d3e79c4: A layout's `meta` receives its own loader data as `data`, the value its component renders, or `null` when the layout has no loader, and the page's data as `pageData`. A layout that loads the locale sets `<html lang>` from its data, and a client navigation keeps it, since the soft-navigation head resolves each layout's `meta` with the same data the server used. A page's `meta` is unchanged and also receives `pageData`. `resolveChainMeta` from `@nifrajs/web/client` resolves a route's `meta` chain this way. Generated route types add `Route.MetaArgs`, typed with the module's own loader data.
|
|
150
|
+
|
|
151
|
+
Upgrading: a layout `meta` that read the page's data as `data` reads `pageData` now. `nifra check` reports a layout with no loader whose `meta` reads `data` (NF-C040), and the 5.0.0 upgrade notes name the change.
|
|
152
|
+
|
|
153
|
+
- 15a3a66: Root status pages (`_404.tsx`, `_<status>.tsx`) and `_error.tsx` render inside the layouts above them with their loader data, and the server puts a status page's `meta` in the head. For an unmatched URL the root middleware and layout loaders run first; otherwise the page reuses the layout data the request already loaded. When those layouts cannot render it (a loader throws, a gate or the middleware refuses) a root status page renders alone and is not hydrated, and an `_error` renders inside its layouts without their data, with the same status. A page rendered with layout data is `private, no-store`, and a client navigation that lands on one loads the URL as a document. `export const inLayouts = false` renders a page the 4.x way and runs no layout loader for it.
|
|
154
|
+
|
|
155
|
+
`createClientRouter` takes `layoutStatusRoutes`: status route ids whose page renders inside layouts, which a soft navigation loads as a document.
|
|
156
|
+
|
|
157
|
+
`nifra check` reports a root status page that imports the root layout to wrap itself, since it now renders that layout twice (NF-C034, warning). The site templates' `_404` pages no longer wrap the layout.
|
|
158
|
+
|
|
159
|
+
### Minor Changes
|
|
160
|
+
|
|
161
|
+
- 22e3df7: feat(web): the app's package.json can set zones with `"nifra": { "zones": { ... } }`. `packages` gives a side to packages that do not declare one, by exact name or a trailing-`*` prefix (`"@shop/*": "library"`); a package's own declaration still wins. `backend`, `frontend` and `shared` list extra app folders that count as that zone folder (`"backend": ["migrations"]`). Every build, dev server, `nifra check` and the TypeScript plugin read the setting; an invalid one fails with the exact key. An undeclared package is now reported once for the whole package, in the build and in `nifra check`, instead of once per file.
|
|
162
|
+
- 2173644: `cache.wrap` loads through a store outage: a store `get` that throws or rejects is a miss, so the loader answers, a failed `set` still returns the loaded value, and a `fence` that fails loads without caching. Failures go to the new `onStoreError(error, { op })`, which by default logs one line per operation per minute without the key. The explicit `get`, `set`, `delete`, `invalidateTag` and `clear` still reject. `revalidateEndpoint` takes `dataCaches`, caches such as a `@nifrajs/cache` holding loader data: a tag purge invalidates each of them before the page store and the CDN, and a failure answers `500` before the page store is touched. `createInvalidator` from `@nifrajs/web/cdn` takes the same `dataCaches`.
|
|
163
|
+
- 4d894b0: `nifra check`'s server-only import rule (NF-C004) names server code the way the browser build does. It knew eight drivers; it now takes the build's list of server packages and driver entries (`stripe`, `@prisma/client`, `jsonwebtoken`, `bcrypt`, `nodemailer` and the rest), bare Node builtins with no browser counterpart (`fs`, `child_process`, `crypto`, ...), `require("x")` and `import x = require("x")`, and an installed workspace package that declares `"nifra": { "environment": "backend" }`. Bare `events`, `buffer`, `path` and `util` still pass, since each also names a browser polyfill. `@nifrajs/web/zones` exports the check as `isServerPackageSpecifier`.
|
|
164
|
+
- 412a9ee: The Bun dev server answers only loopback host names (`localhost`, `*.localhost`, `127.0.0.1`, `[::1]`) and refuses any other `Host` with 403. It already listened on 127.0.0.1 only, but answered any `Host`, so a page on another site whose name a DNS rebind pointed at 127.0.0.1 could read SSR pages, loader data and the error overlay. A name the server should answer under (a hosts-file entry, a tunnel) goes in the new `allowedHosts` option of `createDevServer`, or in `export const dev = { allowedHosts: [...] }` in `nifra.config.ts`.
|
|
165
|
+
- de2cc1d: The Bun and Node servers that `buildTarget` generates cache only the client build's content-hashed outputs as immutable. A file copied from `public/` into `/assets/` now gets the one-day asset policy, so the browser revalidates it after a deploy. `servePublicDir` takes a `hashedFiles` set, and `serve`'s `static` mount takes `hashedFiles`, so a build manifest's `assets` list can decide which files are immutable. Without the list, both keep their prefix behavior.
|
|
166
|
+
- f8e0ed5: Under the default key, `withISR` no longer serves a stored page to a request carrying `Authorization` or a cookie: such a request renders fresh, because a signed-in visitor's page can differ from the anonymous one, and its private render no longer marks the URL uncacheable for everyone. The new `anonymousCookies` option lists cookie names that never change the page, such as consent or analytics cookies; a request whose cookies are all listed is served and stored like one with none. A custom `key` keeps deciding for itself.
|
|
167
|
+
- 1cdcfe5: An ISR purge that lands while a page renders now holds. The render stored the page it had built from data read before the purge, and that stale page was served as fresh until it expired. `withISR` now takes a fence from the store before each render, and the store refuses an entry whose key or tags were purged after its fence. `CacheStore` gains an optional `fence()`, and `ISRStoreOp` gains `"fence"`, so an exhaustive `switch` over it needs that case. `MemoryCacheStore` orders purges exactly. `KVCacheStore` orders them by time, as far as KV's eventual consistency has carried the purge, and its `delete` now leaves a marker that reads as a miss instead of removing the key.
|
|
168
|
+
- e195fe8: `withISR` serves through a cache-store outage. A `get` that throws, rejects, or outlives the new `storeTimeoutMs` is a miss and the page renders; a failed `set` still returns the rendered page and shares it with concurrent misses; a failed background regeneration keeps the stale entry. Failures go to the new `onStoreError(error, { op })`, which by default logs one line per operation per minute without the cache key. A store failure never changes the response status.
|
|
169
|
+
- fc0284a: Add opt-in server components across React, Vue, Svelte, Solid and Preact. Closed schemas project public props, owner GET admission and read capabilities govern every refresh, and private responses bypass shared caches. React uses native Flight; Vue, Svelte, Solid and Preact preserve native island state and owned ranges.
|
|
170
|
+
|
|
171
|
+
Add coordinated Vite builds and development generations, guarded component references, generated Bun/Node/Deno/Workers/Pages/Vercel outputs, typed outlets, router and server-function refresh integration, and release certification for browsers, packaged consumers, resource limits and performance. Existing applications retain their default rendering pipeline.
|
|
172
|
+
|
|
173
|
+
Reuse registered Web middleware continuations across requests while retaining request-local state.
|
|
174
|
+
|
|
175
|
+
- 9f58295: An opaque mount can declare the routes it serves: `opaque: { reason, routes: [{ method, path, capabilities }] }` on `mount()`, `mountFetch()` and a `createWebApp` `mounts` entry. Capability assurance reports those routes with `analysis: "declared"` beside the known gap, checks their capabilities against the policy (an unknown id, or a domain write on a safe method, fails), and records them in the lockfile. `reflectMounts` returns them, project evidence carries them, the `nifra context` index lists them, and an adversarial contract run notes them as having no contract to test. A malformed declaration or a path outside the mount stops the analysis with an error that names it. A mount without a reason fails as before.
|
|
176
|
+
- c613e7d: Infinite queries now take part in the query cache like plain ones. `invalidateQueries` marks a matching infinite query stale, refetches its loaded pages when it is mounted, and keeps a fetch begun before the invalidation from publishing. `dehydrate` includes infinite queries in a new `infiniteQueries` field, and `hydrate` seeds them, including for a key not yet queried. Unmounted infinite queries are evicted after `gcTime` and past `max`, where before they were kept forever.
|
|
177
|
+
|
|
178
|
+
`createMutation`: an `onMutate` that throws now fails the mutation (state `error`, `onError` and `onSettled` run) instead of leaving it `pending`. An `onSuccess` that throws after the write still rejects the call, but the state stays `success` and `onError` is not called, so an optimistic rollback no longer undoes a write the server kept.
|
|
179
|
+
|
|
180
|
+
- b70b18a: The generated `./+types/<name>` module declares `Route.Loader` and `Route.Action`. Annotate a
|
|
181
|
+
route's loader, `export const loader: Route.Loader = async ({ params, api }) => ...`, and what it
|
|
182
|
+
returns is checked against `loaderOutput` at compile time. A field of the wrong type, a `defer(...)`
|
|
183
|
+
whose value does not match its `t.deferred(...)` field, or any data from a loader with no
|
|
184
|
+
`loaderOutput` is a type error. A redirect, a `Response` and returning nothing stay allowed.
|
|
185
|
+
`Route.Action` does the same against `actionOutput`, `revalidate(paths, data)` included. A field the
|
|
186
|
+
schema does not declare is not a type error, because the output guard drops it.
|
|
187
|
+
|
|
188
|
+
`@nifrajs/client` exports the types behind them: `InputOf<Module, Name>` (what a route module's
|
|
189
|
+
output schema accepts), `RouteLoaderReturn<Module>` and `RouteActionReturn<Module>`.
|
|
190
|
+
|
|
191
|
+
`nifra scaffold` and the `create-nifra` starters write the annotation. `nifra schemas` reports a
|
|
192
|
+
generated schema over an annotated loader or action as unchecked (NF-C033 warns), because the
|
|
193
|
+
annotation, not the body, is then the function's return type.
|
|
194
|
+
|
|
195
|
+
- 2fbb4f0: `t.date()` and `t.json()` for page data. A `loaderOutput` or `actionOutput` field declared `t.date()` reaches the page as a `Date` in the server render, on hydration, on client navigations, in streamed `t.deferred` values and in server function results; a `Date` or an ISO date string is accepted, anything else fails the route by path. `t.json()` declares a field that is any JSON value and passes it through whole. `JsonValue` is exported from `@nifrajs/schema`.
|
|
196
|
+
- 9b1acc9: The Vite dev server, `buildClientVite` and `buildServerVite` apply the `package.json` single-copy declaration (`"nifra": { "singleCopy": [...] }`): each declared package, and each package installed under a declared `@scope/*`, joins `resolve.dedupe`, so a linked package with its own copy loads the app's copy, as the Bun build does. `singleCopyPackageNames(cwd)` from `@nifrajs/core/single-copy` returns that list.
|
|
197
|
+
|
|
198
|
+
### Patch Changes
|
|
199
|
+
|
|
200
|
+
- Authorize native React client references consistently across Windows and Vite path spellings. Escape route IDs and other values embedded in generated client and server source, including script terminators and Unicode line separators.
|
|
201
|
+
|
|
202
|
+
- 7169820: A form submitted without JavaScript re-renders the page with its layouts' loader data. After the action, the layout loaders run as they do for a GET of the page, so each layout renders its own data, its `meta` resolves from that data (a layout that loads the locale sets `<html lang>`), and the document's hydration data carries it. A gate still runs once, before the action. A layout or page loader that throws or calls `notFound()` after the action renders the same error boundary or status page a GET would, and an answer that shows layout data is `private, no-store`.
|
|
203
|
+
- 0aed425: Required `basicAuth`, `bearer`, `apiKey` and `jwt` refuse in the `authenticate()` stage: before the body is read or validated and before any `derive` or `beforeHandle`. An unauthenticated request gets the 401 and its challenge, never a 413 or a 422, on every route shape, lane and WebSocket handshake. With valid credentials an invalid body is still a 422. `optional: true` never refuses and keeps validation first. A `verify` that throws in required mode is a 503. A rate limiter written as a `beforeHandle` behind these guards no longer sees refused requests; `rateLimit()` runs in `onRequest` and still counts them.
|
|
204
|
+
|
|
205
|
+
The `authenticate()` input adds `params`, the raw `search`, the parsed and frozen `query` (the parser `c.query` uses; a repeated key is an array) and the `request` (its body must not be read). The query is for short-lived, single-use tickets, such as one for a WebSocket handshake.
|
|
206
|
+
|
|
207
|
+
Project evidence and reflection keep a route's chosen `validationOrder`.
|
|
208
|
+
|
|
209
|
+
`nifra check` reports a route that claims runtime authentication, declares an input schema and authenticates in a hook that runs after validation (NF-S008, warning). It reads the backend the contract-lock check loads, so it runs when `contracts.lock.json` exists. Fix it with `authenticate()`, or with `validationOrder: "auth-before-validation"` on the route.
|
|
210
|
+
|
|
211
|
+
The dev feed (`nifra_logs`, `nifra_errors`, request traces and the persisted log) records a query's keys and never its values.
|
|
212
|
+
|
|
213
|
+
- 7de0397: A loader, action or render failure that an `_error` page answers is written to the server's `logger` as `unhandled request error`, with the request line and the detail `errorLogDetail` allows, the same entry an error with no boundary gets. A soft-navigation data request that a boundary answers with a 500 is logged the same way. Control flow such as a thrown `redirect()` is not logged. `@nifrajs/core/edge-kit` exports `emitRequestErrorLog` and `ErrorLogDetail`, the function the server itself logs through.
|
|
214
|
+
- 674c658: `buildTarget`, `buildTargetVite` and `buildServerComponents` clear their output directory first, so they now refuse one that is a filesystem root or is or contains the routes directory, the public directory, an absolute entry or backend module, or the working directory, comparing paths after resolving symlinks. `nifra build --out .` fails with an error naming the path instead of clearing the project. `workDir` gets the same check.
|
|
215
|
+
- de27557: A Bun client build now links a stylesheet that the `clientModule` imports on every server-rendered page. When routes owned their own CSS, each page linked only its route's stylesheet, so global styles imported by the client module were missing until the client bundle loaded. Each page now links the client module's stylesheet first, then the route's.
|
|
216
|
+
- 61901cd: `buildServerComponents` and `devServerComponents` called without a `target` now compile for Bun, the target the build assembles and `nifra build` defaults to. They compiled for Node and then assembled a Bun server, so a server module that imports a `bun:` built-in was refused as "unsupported on node".
|
|
217
|
+
- 5e366e2: The generated Node and Vercel server for a server-components build takes the request's origin from the Host header only. A request target that starts with `//` (`GET //evil.example/admin`) used to resolve as host `evil.example`, path `/admin`; it is now path `//evil.example/admin` on the real host. An absolute-form target is routed by its path, and a Host header with characters outside a host name answers 400.
|
|
218
|
+
- 2938a0d: The server-component HTML check accepts `<!>`, the comment marker Solid's templates (and `<!$>`, `<!/>`) use, which every browser reads as a comment. Under Trusted Types the component policy checks every HTML string a page assigns, and it refused Solid's own templates, so a Solid page with components never hydrated. Other malformed markup, `<?...>` and `<![CDATA[` among it, is still refused.
|
|
219
|
+
- dc8439c: The server-components HTML check (run before SSR, on decoded component state, and by the opt-in Trusted Types default policy) reads `title` and `textarea` content the way a browser does. A browser treats that content as text, so `<title><!--</title><img onerror=...>-->` showed the validator a comment while the browser built an `<img>`. Content of those two elements must now hold no raw `<` before its own end tag, which escaped framework output never does, and `noembed` and `noframes` are refused like the other text-mode elements.
|
|
220
|
+
- b19cb37: A server-components build exposes only `PUBLIC_*` values (or the configured `publicEnvPrefix`) from the app's `.env` files, like every other nifra build. It set no `envPrefix`, so Vite's default inlined every `VITE_*` value from `.env.production` into client code that read `import.meta.env`, a dependency included. The secret scan of every Vite build now also knows the values Vite loads from `.env.<mode>` files, not only the process environment, so a credential that exists only in one of those files fails the build if it reaches the bundle.
|
|
221
|
+
- 4330e40: Server components and `nifra check` no longer crash on a project that installs TypeScript 7. TypeScript 7's package exports only `version`, with no in-process compiler API, so loading the component pipeline used to fail with a TypeError on first use. The pipeline now raises `NIFRA_TS_UNSUPPORTED` with the fix: pin the project to TypeScript 6. `nifra check` reports the same code. It gives no component module a receipt, so each one is checked as an ordinary backend import. The pipeline reads `componentPipeline` from a config file through `staticComponentPipeline` in `@nifrajs/web/server-components/source`.
|
|
222
|
+
- 65883e5: When a client disconnects mid-response, the streamed HTML document now cancels the framework renderer's stream instead of leaving it running, and neither the document nor the navigation data stream writes a deferred value that settles afterwards. Before, each such value logged a spurious "deferred value rejected" error.
|
|
223
|
+
- 0e20529: The draft-mode cookie now carries its expiry inside the signed value, and `isDraftEnabled` refuses it once that time passes. Before, every editor got the same signed marker, and the server never checked its age, so a copied cookie worked until the draft secret was rotated. `maxAgeSeconds` sets both the cookie's `Max-Age` and the signed expiry, and it must be a positive whole number of seconds. Cookies issued by earlier versions carry no expiry and are refused, so an editor enables draft mode once more after upgrading.
|
|
224
|
+
- f7e36ec: Enhanced forms no longer run an action twice, and they submit through the button that was pressed. A data submit answered with a non-2xx status fell back to posting the form natively, which ran the action a second time; any answer from the server now loads the page instead, and the rejection from `router.submit` names that page in `redirectTo` with the failure as its `cause`. The pressed button's `name`/`value` is now part of the body, its `formaction`, `formmethod` and `formtarget` apply, and a form or button that targets another browsing context posts natively. The native fallback, used only when nothing reached the server, goes through the same button.
|
|
225
|
+
- 262e46d: A server-rendered `_error` page shows the thrown error's own message only when `NODE_ENV` is `development` or `test`. With `NODE_ENV` unset, as in an unbuilt `bun app.ts`, it now gets the generic `Internal Server Error` that production already got, instead of the raw message.
|
|
226
|
+
- 944623d: A `gate` layout loader that returns a `redirect()`, a status such as `notFound()`, or a `Response` refuses the request exactly as one that throws it: the page loader and the action do not run, on document, data and form requests alike.
|
|
227
|
+
- 6d9869d: `loadGoogleFont` refuses a stylesheet whose `font-weight` is not a number, a variable range or a keyword. The weight is part of the self-hosted file's name, so a tampered stylesheet could otherwise name a file outside `outDir`.
|
|
228
|
+
- b9cc68c: `nifra doctor` flags a `buildManifest(...)` call whose file list, an array literal or
|
|
229
|
+
`import.meta.glob` patterns, reaches a page under `routes/` but not the `.backend.*` half beside it.
|
|
230
|
+
`buildManifest` pairs the halves only from the list it is given, so such a manifest serves that page
|
|
231
|
+
without its loader, action and guards, which a test then passes against. The advisory names the
|
|
232
|
+
missing halves and points to `discoverRoutes(dir)` from `@nifrajs/web/fs` or the generated
|
|
233
|
+
`server-manifest`. It never fails the run. The testing guide documents both as the way to give a test
|
|
234
|
+
the routes the app serves.
|
|
235
|
+
- 7775c2b: `withISR` and `withCdn` treat a request an authentication stage accepted like one carrying a cookie or `Authorization`, whichever header held the credential: its page is stored, or marked storable for the CDN, only when the response is explicitly `public`.
|
|
236
|
+
- 09c47c5: Concurrent cache misses for one `withISR` page share a single render: the first renders and stores it, and the others are answered from that stored page. A render that is not stored, such as a `private` page or a failure, sends each waiting request to render for itself, so no request receives a page that was not safe to share. Requests with a cookie or `Authorization` header never join.
|
|
237
|
+
- 326caee: A stored ISR record that a `Response` could not replay, such as one with status 999, a non-finite timestamp or an illegal header, now reads as a cache miss and the page renders. Before, it passed the shape check and every request for that page threw.
|
|
238
|
+
- a588c18: `KVCacheStore` orders purges against in-flight renders by wall-clock time, held strictly increasing per process. A purge from a process whose host had slept, or whose system clock was stepped after it started, could carry a time behind the wall clock, so a render that began before the purge but finished after it was stored as fresh and the purged page kept serving until its next purge or expiry.
|
|
239
|
+
- 7775c2b: Expose the supported layout gate flag on RouteModule so typed gate layouts compile while retaining their pre-action loader behavior.
|
|
240
|
+
- d922f65: With `apiStrip: true`, `/llms.txt` and `/llms-full.txt` list each backend route at the URL a visitor calls (`/api/v1/forms`), not at the backend's own unprefixed path (`/v1/forms`), which the page router answers instead.
|
|
241
|
+
- a1eda0a: After a native form POST runs a page's action, a loader that returns `redirect()` or a status such as `notFound()` now answers the request with that redirect or status page, as the page's GET does. Before, the returned value was rendered as the page's data with status 200.
|
|
242
|
+
- 2f7299a: A native form POST to a server-component owner route whose action sets a Secure cookie (the default) while the app sees the request as `http:`, as it does behind a TLS terminator, now answers 303 to the same path so the browser makes the owner GET with the cookie it kept. Before, the post-action render ran without the new cookie. The 303 `Location` of a progressive redirect is now relative, so the browser stays on its own scheme.
|
|
243
|
+
- 29e62a0: `buildTarget` and `buildTargetVite` refuse a public file whose path matches a file the target writes next to it: `server.js` for the bun, node and deno targets, and `_worker.js` or `_routes.json` for cloudflare. Before, the build overwrote the public file and the built server answered that public path with its own server bundle.
|
|
244
|
+
- 5975163: The React server-components pipeline is certified on React, React DOM and `react-server-dom-webpack` 19.2.8, which fixes GHSA-wx67-qw84-cm4g (a denial of service in server-function decoding) present in 19.2.7. The pipeline requires the three at exactly 19.2.8; upgrade them together.
|
|
245
|
+
- 7a165de: A Response returned to more than one request no longer carries one request's headers to another. A Response without a body (a module-level `new Response(null, { status: 204 })`, a kept redirect) can be sent any number of times, so the cookies a handler queues with `c.set.cookie`, the app's `responseHeaders`, and the headers written by `onResponse` hooks, response observers and response middleware go on a copy made for the request; the Response the handler returned stays as it was built. A Response with a body can be sent once: returning it again answers `500` with `{"ok":false,"error":"internal_error"}` and none of the headers written for the earlier request. On `@nifrajs/node`, a response whose body cannot be written is answered with a `500` that carries none of the headers set for it.
|
|
246
|
+
|
|
247
|
+
`writableResponse(response, request?)`, exported from `@nifrajs/core/server`, returns the Response a request may write headers on: the response itself when it has a body and writable headers, otherwise a copy. Pass the request and a copy already made for it is reused by its later writers. `toResponse`, exported from `@nifrajs/core/edge-kit`, takes the same optional last argument: the copies and body-less responses it builds are then reused by that request's later writers instead of being copied again. A Response the handler returned is never treated as the request's own.
|
|
248
|
+
|
|
249
|
+
`renderResult(result)`, exported from `@nifrajs/core/edge-kit`, renders a framework `ResponseResult` exactly as `toResponse` does without taking a handler's `Response`. `@nifrajs/edge` renders its rejections with it; responses are unchanged and the bundle is smaller.
|
|
250
|
+
|
|
251
|
+
- d8f11a5: The Bun client build maps each route, layout and the bootstrap to the chunk the build metafile names for that source file, so a page's `modulepreload` links and route chunk list always point at its own chunk. Position among the build's entry outputs is only the fallback for an entry the metafile does not name by path.
|
|
252
|
+
- e9e02cf: A route file whose name has a space or non-ASCII characters, such as `routes/café.tsx` or `routes/a b.tsx`, now serves its page. Browsers request those paths percent-encoded (`/caf%C3%A9`), and the server and client routers compared them against the raw name, so the page always answered 404. Route patterns are now matched in their requested form, composed (NFC) first, while `route.pattern` keeps the name as written.
|
|
253
|
+
- 6f42b38: Generated route types stay valid TypeScript for any route file name: import specifiers are written as escaped string literals, and a line break in a name no longer ends the header comment.
|
|
254
|
+
- 870227d: `nifra types` gives a route file with a part-literal segment, such as `routes/post-[id].tsx` or `routes/[year]-[month].xml.tsx`, the params inside that segment. Its `Params` was `Record<string, never>` although the route matches `/post-:id` and its loader receives `params.id`.
|
|
255
|
+
- 1d66252: Scoped style ids are now unique across a monorepo. Vue `data-v-*` scope ids, CSS-module class names and StyleX stylesheet ids hashed the file's path relative to its package, so `src/Card.vue` in two workspace packages got the same id, and each package's styles applied to the other's markup. The package's `name` now leads the hashed path. Every generated class name and scope id changes once.
|
|
256
|
+
- a6a6834: fix(web): the browser-build credential scan also flags hyphenated header keys. A random-looking literal under a quoted name such as `"x-api-key"` or `'x-auth-token'` now fails the build like `apiKey: "..."` does; the name must be the whole quoted string, so a ternary branch or a label that only mentions a header is not a target. The same holds for a header given by a call or written as a pair: `headers.set("x-api-key", "...")`, `.append(...)`, `new Headers([["x-api-key", "..."]])` or any two-element `["name", "value"]` array; a value spelled as words or as a constant's name (`["password", "passwordConfirmation"]`) is not a credential. An `Authorization` (or `Proxy-Authorization`) literal fails when it carries a random-looking Bearer token, or a Basic `user:password` that is not a placeholder (`user:password`, a user name repeated as the password). Names ending in `access-key`, `signing-key`, `encryption-key`, `master-key`, `service-account-key` or `connection-string` count when spelled with hyphens or underscores too.
|
|
257
|
+
- 166a6ff: The client build of a `*.fn.ts` module no longer drops a server function when a string literal before it contains `/*` or `//`. A `serverFn({ accept: "image/*" }, ...)` opened a comment that ran to the next `*/`, so the functions declared in between got no client stub and failed as "not a function".
|
|
258
|
+
- 79be5af: The worker from `generateServiceWorker` answers only the build's own precached URLs, and reads them, and the offline page, only from the build's own cache. Before, every same-origin GET, an API call included, was looked up with `caches.match` across all of the origin's caches, so a response a page or another app had stored with the Cache API could be served in place of the network.
|
|
259
|
+
- bf6eeb0: Static files, `public/` files and local image sources refuse a resolved path when any directory along it became a symlink between the containment check and the open. On macOS the open itself refuses such a path; on Linux the opened descriptor's path is checked; elsewhere the opened file must still be the one at the checked path. Before, only a symlink in the last component was refused. A refused `public/` file falls through like a missing one.
|
|
260
|
+
- 5a3c27a: On platforms other than macOS and Linux, Windows included, the check that a static, `public/` or local image file is still the file at its resolved path now runs its steps one after another and compares file identities exactly, so a single directory swap during the open cannot pass it. A race-free check does not exist there, because no file API can open a path without following a link in a middle directory; the `@nifrajs/node` README states that a tree a concurrent local writer can change is outside the guarantee on those platforms.
|
|
261
|
+
- 9b971fc: The Vite dev server crawls the generated client entry for dependencies to pre-bundle, so every route's dependencies are found up front. Opening a route for the first time no longer discovers a new dependency mid-session and reloads the page.
|
|
262
|
+
- 2577fc7: With the Vite builder, a page now links every stylesheet it needs. Each route's list held only the CSS of its own chunk, so a component two routes share (Vite gives it a chunk of its own) was unstyled on both, as was a stylesheet the client module imports. Because a route's list replaces the app-wide one, those pages rendered without that CSS. The list now follows the chunk's static imports, starts with the bootstrap's stylesheets, and includes the `_loading` pages', as the Bun builder already does.
|
|
263
|
+
- 391ab3c: feat(web): in development (`NODE_ENV=development`, which `nifra dev` sets), the data guard logs the paths of fields a loader, action or server function returned but its output schema does not declare, once per distinct set. Paths only, never values; array indices collapse to `*`. Production behavior and cost are unchanged.
|
|
264
|
+
- 3bf7d6b: `withISR` removes a stored page when a background regeneration answers 401 or 403, or answers 2xx with `Cache-Control: private` or `no-store`. The stale public copy no longer keeps serving after the page is withdrawn. A regeneration triggered by a request carrying a cookie or `Authorization` header keeps the entry: that visitor's own refusal or private copy says nothing about the shared page. A 5xx or 429 still keeps serving the stale copy.
|
|
265
|
+
- 7775c2b: Dispose island event listeners and release mounted hosts so the same DOM can safely remount.
|
|
266
|
+
|
|
267
|
+
Ignore stale session reads after sign-out, unmount, or replacement of the auth client or loader seed.
|
|
268
|
+
A new auth client object keeps the session already shown, and a completed sign-out stays signed out.
|
|
269
|
+
|
|
270
|
+
Allow concurrent web assurance evidence reads while rejecting recursive mount cycles. Read mounted
|
|
271
|
+
in-process backend evidence for generated application guidance, preserving mount prefixes and
|
|
272
|
+
keeping loader-only backends out of the published route list. Generate callable paths for reserved
|
|
273
|
+
typed-client segments and keep declared DELETE bodies in call options.
|
|
274
|
+
|
|
275
|
+
- ab1935b: The generated service worker names its cache `<cacheName>:<buildId>` and, on activation, deletes only older caches in that namespace. Caches another app or the page itself created on the same origin are left in place. A cache an earlier worker named `<cacheName>-<buildId>` is outside the namespace and is not deleted.
|
|
276
|
+
- Updated dependencies [a0a02f4]
|
|
277
|
+
- Updated dependencies [0aed425]
|
|
278
|
+
- Updated dependencies [eaf030b]
|
|
279
|
+
- Updated dependencies [7de0397]
|
|
280
|
+
- Updated dependencies [4199ff3]
|
|
281
|
+
- Updated dependencies [90d9ee6]
|
|
282
|
+
- Updated dependencies [364a234]
|
|
283
|
+
- Updated dependencies [c06cba5]
|
|
284
|
+
- Updated dependencies [f4d3852]
|
|
285
|
+
- Updated dependencies [4a82ae0]
|
|
286
|
+
- Updated dependencies [0ff867a]
|
|
287
|
+
- Updated dependencies [e7391bf]
|
|
288
|
+
- Updated dependencies [0b80a59]
|
|
289
|
+
- Updated dependencies [c64db12]
|
|
290
|
+
- Updated dependencies [3753104]
|
|
291
|
+
- Updated dependencies [4b3b1fa]
|
|
292
|
+
- Updated dependencies [3562557]
|
|
293
|
+
- Updated dependencies [53087f3]
|
|
294
|
+
- Updated dependencies [2def6c7]
|
|
295
|
+
- Updated dependencies [b5b5bc3]
|
|
296
|
+
- Updated dependencies [22e1777]
|
|
297
|
+
- Updated dependencies [3a483cd]
|
|
298
|
+
- Updated dependencies [35f52c7]
|
|
299
|
+
- Updated dependencies [343cb81]
|
|
300
|
+
- Updated dependencies [d91fd44]
|
|
301
|
+
- Updated dependencies [b13e071]
|
|
302
|
+
- Updated dependencies [077657f]
|
|
303
|
+
- Updated dependencies [a5963cb]
|
|
304
|
+
- Updated dependencies [05aac0e]
|
|
305
|
+
- Updated dependencies [1a6616f]
|
|
306
|
+
- Updated dependencies [7775c2b]
|
|
307
|
+
- Updated dependencies [46b269a]
|
|
308
|
+
- Updated dependencies [846e2f6]
|
|
309
|
+
- Updated dependencies [aadd9fc]
|
|
310
|
+
- Updated dependencies [ea16748]
|
|
311
|
+
- Updated dependencies [fc0284a]
|
|
312
|
+
- Updated dependencies [5b22f4d]
|
|
313
|
+
- Updated dependencies [1b94593]
|
|
314
|
+
- Updated dependencies [9f58295]
|
|
315
|
+
- Updated dependencies [b9608ac]
|
|
316
|
+
- Updated dependencies [7a165de]
|
|
317
|
+
- Updated dependencies [4064f36]
|
|
318
|
+
- Updated dependencies [48aad8c]
|
|
319
|
+
- Updated dependencies [41050ed]
|
|
320
|
+
- Updated dependencies [fc837cb]
|
|
321
|
+
- Updated dependencies [b80ce77]
|
|
322
|
+
- Updated dependencies [9b1acc9]
|
|
323
|
+
- Updated dependencies [740109f]
|
|
324
|
+
- Updated dependencies [e0a25fe]
|
|
325
|
+
- Updated dependencies [3630b6e]
|
|
326
|
+
- Updated dependencies [1290c6d]
|
|
327
|
+
- Updated dependencies [d0a7d58]
|
|
328
|
+
- @nifrajs/core@5.0.0
|
|
329
|
+
- @nifrajs/island-trigger@5.0.0
|
|
330
|
+
|
|
331
|
+
## 4.0.2
|
|
332
|
+
|
|
333
|
+
### Patch Changes
|
|
334
|
+
|
|
335
|
+
- fcd6a5b: The Vite dev server with `poll: true` (or `CHOKIDAR_USEPOLLING=1`) no longer loses a route file added or removed just after startup. The polling watcher reports ready before it has taken its first reading of a directory, so a change in that gap was never reported: the route stayed out of the route table and the client entry, or a deleted one stayed in, until something else in the directory changed. The dev server now rescans the routes directory on each poll and refreshes both for any route file the watcher did not report.
|
|
336
|
+
- @nifrajs/core@4.0.2
|
|
337
|
+
- @nifrajs/island-trigger@4.0.2
|
|
338
|
+
|
|
339
|
+
## 4.0.1
|
|
340
|
+
|
|
341
|
+
### Patch Changes
|
|
342
|
+
|
|
343
|
+
- bbb2b27: Client navigation works on a page served from a native webview's own scheme, such as Capacitor's
|
|
344
|
+
`capacitor://localhost` on iOS: link clicks, prefetch, `navigate()` and `<form method="post">` stay
|
|
345
|
+
in-app there instead of the click being swallowed. Whether a URL belongs to the app is decided by the
|
|
346
|
+
document's scheme and host rather than `URL.origin`, which reads `"null"` for every non-special scheme,
|
|
347
|
+
so a `javascript:` target is still never taken in. An opaque or `file:` page, where history cannot move
|
|
348
|
+
to another path, leaves its links to the browser.
|
|
349
|
+
- 2957acd: The credential scan no longer reads a ternary as an assignment. In
|
|
350
|
+
`mode === "signup" ? "new-password" : "current-password"` the tail of `"new-password"` was taken for a
|
|
351
|
+
key named `password`, and in `ready ? config.apiKey : "..."` the branch was taken for an `apiKey:` key,
|
|
352
|
+
so a client build and `nifra check` (NF-C032) failed on a plain string. A quoted name must now be the
|
|
353
|
+
whole string, and a name after a ternary's `?` or a member access's `.` is not a key. An object key
|
|
354
|
+
inside a ternary branch, a quoted JSON key and a property assignment are still checked.
|
|
355
|
+
- 4bd292a: A static route file beside a dynamic one builds again: `users/me.tsx` next to `users/[id].tsx`, or a
|
|
356
|
+
`[lang]/` tree next to `about.tsx`, no longer fails `buildManifest` with "overlapping routes". The path
|
|
357
|
+
they share goes to the more specific route, in the router's own order (static, then mixed, then param,
|
|
358
|
+
then wildcard, segment by segment), and each keeps the paths only it serves. Two routes of one shape
|
|
359
|
+
that share a path, such as `users/[id].tsx` and `users/[slug].tsx`, still fail at boot, because one of
|
|
360
|
+
them could never be served.
|
|
361
|
+
- 55e813b: The browser zone guard no longer refuses an extensionless import of a dotted source file. `import { x } from "../lib/calendar.shared"` (or `./date.utils`) was read as an asset with the extension `.shared`, judged by its bare path, refused, and then reported as "missing from the graph" once Bun loaded the real `.ts` file, so a `*.shared.ts` module outside the zone folders failed the client build and the dev server. Zone suffixes are no longer treated as asset extensions, and a path that names no file on disk is left to the source-file check, which judges the file Bun actually resolves.
|
|
362
|
+
- @nifrajs/core@4.0.1
|
|
363
|
+
- @nifrajs/island-trigger@4.0.1
|
|
364
|
+
|
|
365
|
+
## 4.0.0
|
|
366
|
+
|
|
367
|
+
### Major Changes
|
|
368
|
+
|
|
369
|
+
- 22e2af8: feat(web)!: client builds fail on what looks like a credential
|
|
370
|
+
|
|
371
|
+
Every client build (Bun and Vite), every `public/` copy and every prerendered page and `_data.json`
|
|
372
|
+
is scanned before it is written. The build fails, naming the file and line, on:
|
|
373
|
+
|
|
374
|
+
- a PEM private key, a URL with a password, or a known secret token format (AWS access keys, Stripe
|
|
375
|
+
secret and webhook keys, GitHub, Slack, OpenAI, Anthropic, SendGrid, Twilio, Mailgun and npm tokens,
|
|
376
|
+
service-role JWTs); publishable keys are not flagged;
|
|
377
|
+
- a random-looking string assigned to a secret-like name (`apiKey`, `clientSecret`, ...) in the app's
|
|
378
|
+
own browser code;
|
|
379
|
+
- the value of a build environment variable that is not public, raw or URL, JSON, HTML or base64
|
|
380
|
+
encoded, when its name says it is a secret or the value looks random.
|
|
381
|
+
|
|
382
|
+
A finding in a bundle names the module it came from. The report never prints the value. A reviewed
|
|
383
|
+
false positive is exempted with `secretExemptions` on the build options (`nifra.config.ts` for
|
|
384
|
+
`nifra build`): `{ rule, file, reason }`, or `{ rule: "private-env-value", env, reason }`. There is no
|
|
385
|
+
exemption by value. `prerenderRoutes` takes the same exemptions as `secrets`. `@nifrajs/web/zones`
|
|
386
|
+
exports the scanner as `scanForSecrets`.
|
|
387
|
+
|
|
388
|
+
- 214d674: feat(web)!: an app is split into zones, and the browser build admits only browser code
|
|
389
|
+
|
|
390
|
+
An app's files live in `routes/`, `frontend/`, `backend/`, `shared/` and `public/`. A route is two
|
|
391
|
+
files: `x.tsx` (or `.svelte`/`.vue`) is the page the browser receives, and `x.backend.ts` holds what
|
|
392
|
+
only the server runs - `loader`, `action`, `middleware`, `getStaticPaths`, `revalidate` and the other
|
|
393
|
+
server exports, a layout's `gate` and `shouldRevalidate` included. `_layout.backend.ts` exports a
|
|
394
|
+
directory's `middleware`; `_middleware.ts` is retired. `@nifrajs/web/route-manifest` exports the
|
|
395
|
+
placement table as `FRONTEND_ROUTE_EXPORTS` and `BACKEND_ROUTE_EXPORTS`.
|
|
396
|
+
|
|
397
|
+
Every browser build and dev server - Bun and Vite - refuses `backend/`, a route's backend half, server
|
|
398
|
+
packages, and first-party code that belongs to no zone, naming the import chain that reached it.
|
|
399
|
+
After bundling, the build checks the finished module graph and every emitted file (code, CSS, assets
|
|
400
|
+
and source maps) and writes nothing it cannot trace to browser code. A workspace package declares its
|
|
401
|
+
side with `"nifra": { "environment": "frontend" | "backend" | "shared" | "library" }`.
|
|
402
|
+
|
|
403
|
+
Server builds hold the same rules from the other side: every first-party file belongs to a zone,
|
|
404
|
+
backend code never imports frontend code, and shared code imports only shared code and third-party
|
|
405
|
+
packages, never a server built-in. A server bundle also refuses a built-in its target cannot load: a
|
|
406
|
+
`node:` or `bun:` import kept in an edge worker, or a `bun:` import kept in a Node server. The Vite
|
|
407
|
+
server build runs the zone rules through `viteServerZoneGuard` from
|
|
408
|
+
`@nifrajs/web/plugins/vite-leak-guard`.
|
|
409
|
+
|
|
410
|
+
Browser code - a route's frontend half, `frontend/` and `shared/` - may read only `NODE_ENV`, the
|
|
411
|
+
bundler's `import.meta.env` flags (`MODE`, `DEV`, `PROD`, `SSR`, `BASE_URL`) and variables named with
|
|
412
|
+
the public prefix (`PUBLIC_` unless `publicEnvPrefix` says otherwise). Any other `process.env`,
|
|
413
|
+
`import.meta.env`, `Bun.env` or `Deno.env` read fails the build and the dev request, naming the
|
|
414
|
+
variable. Strings, comments, JSX text and Markdown code samples that mention a variable are not reads.
|
|
415
|
+
|
|
416
|
+
`@nifrajs/web/zones` exports the classifier and the rules every build, dev server and `nifra check`
|
|
417
|
+
share: `createZoneClassifier`, `browserDenial`, `importAllowed`, `importRuleMessage`,
|
|
418
|
+
`privateEnvReads` and `privateEnvReason`.
|
|
419
|
+
|
|
420
|
+
Removed: the `*.server` file convention, the `@nifrajs/web/plugins/vite-server-only` export and
|
|
421
|
+
`SERVER_ONLY_MODULE`. The opt-in marker import is `@nifrajs/web/backend-only`, its brand type is
|
|
422
|
+
`BackendOnly<T>`, and the dev diagnostics are `NIFRA_BACKEND_ONLY_IN_CLIENT` and
|
|
423
|
+
`NIFRA_BACKEND_IN_CLIENT`.
|
|
424
|
+
|
|
425
|
+
- 4b8d8de: feat(web)!: everything a route sends to the browser passes a declared output schema
|
|
426
|
+
|
|
427
|
+
A route's backend half declares what its loader and action send to the browser:
|
|
428
|
+
`export const loaderOutput = t.object({ ... })` and `export const actionOutput = ...`. A boundary
|
|
429
|
+
loader declares `boundaryLoaders[name].output`, and a server function `serverFn({ output }, fn)`.
|
|
430
|
+
Before the page renders and before anything is serialized, the data is projected through its schema:
|
|
431
|
+
keys the schema does not declare are dropped, at every depth, even when the schema itself would accept
|
|
432
|
+
them, so the component and the browser see the same value. A declared field of the wrong shape fails
|
|
433
|
+
the request with a 500 whose message names the field's path, never its value. A loader that returns
|
|
434
|
+
data without an output schema fails the request; one that returns nothing, redirects or answers with
|
|
435
|
+
an error status needs none. A loader, action, boundary loader or server function that returns a 2xx
|
|
436
|
+
`Response` is refused, since its body would bypass the schema.
|
|
437
|
+
|
|
438
|
+
A deferred value is declared with `t.deferred(schema)`, and what it resolves to is projected and
|
|
439
|
+
validated by `schema` before it streams. A deferred value the schema does not declare as one is
|
|
440
|
+
refused.
|
|
441
|
+
|
|
442
|
+
An output schema that names a field such as `password`, `passwordHash`, `secret`, `token`, `apiKey`,
|
|
443
|
+
`privateKey` or `ssn` fails the route when it loads, unless the field is wrapped in
|
|
444
|
+
`t.declassified("why it may reach the browser", schema)`. `@nifrajs/web/zones` exports the name test
|
|
445
|
+
as `isSensitiveFieldName`.
|
|
446
|
+
|
|
447
|
+
Projection reads a nifra schema's JSON Schema, or a Standard JSON Schema such as zod's. A schema that
|
|
448
|
+
exposes neither keeps whatever its own `validate` returns.
|
|
449
|
+
|
|
450
|
+
The scaffolded site and ISR routes declare their loader and action output schemas.
|
|
451
|
+
|
|
452
|
+
- 6c978a1: feat(web)!: the Cloudflare Pages build target is `cloudflare`
|
|
453
|
+
|
|
454
|
+
`BUILD_TARGETS` and `buildTarget` / `buildTargetVite` name it `"cloudflare"` (it emits the same
|
|
455
|
+
`_worker.js` + `_routes.json` deploy directory). `"cf-pages"` is refused with the new name.
|
|
456
|
+
`parseBuildTarget(value)` returns a string as a `BuildTarget`, or throws with the same message.
|
|
457
|
+
|
|
458
|
+
- 38cf032: feat(web)!: Vercel builds use the Build Output API, and generated entries pass the client address
|
|
459
|
+
|
|
460
|
+
- The `vercel` target emits the Build Output API v3 layout: `config.json`, `static/` (assets and
|
|
461
|
+
public files) and an edge function at `functions/index.func/`, ready for `vercel deploy --prebuilt`.
|
|
462
|
+
`planBuildTarget` reports the new `outputFile` and a `staticDir` for every target.
|
|
463
|
+
- The generated Bun, Node and Deno server entries pass the socket peer to `app.fetch`, so
|
|
464
|
+
`c.clientIp` and `rateLimit`'s default key work in a built app.
|
|
465
|
+
- Edge bundles (`cloudflare`, `vercel`) accept `node:async_hooks`, `node:buffer`, `node:events`,
|
|
466
|
+
`node:util` and `node:assert`, which those runtimes provide.
|
|
467
|
+
|
|
468
|
+
### Minor Changes
|
|
469
|
+
|
|
470
|
+
- 963694f: `@nifrajs/web/cdn` puts a CDN in front of nifra pages and purges it by tag.
|
|
471
|
+
|
|
472
|
+
- `withCdn(app, { provider })` wraps an app or a `withISR` handler. Every page a shared cache may hold gets the CDN's tag header (the route's `revalidateTags` plus a tag for the page's path) and the CDN's TTL from the route's `revalidate`; browsers get `public, max-age=0, must-revalidate`. Every other HTML response, drafts included, is marked no-store for the CDN. Over `withISR`, the CDN is given only the freshness the page has left.
|
|
473
|
+
- Providers: `cloudflareZone`, `cloudflareWorkersCache` (refuses a host-routed app, since Workers Cache keys by path), `vercel` (`invalidateByTag` from `@vercel/functions`, or the REST API; invalidate by default) and `fastly` (soft purge by default). `defineCdnProvider` builds any other.
|
|
474
|
+
- Purges are coalesced for 250 ms, chunked to each provider's per-call limit, sent one call at a time, and retried on 429 or 5xx with capped backoff that honors `Retry-After`.
|
|
475
|
+
- `revalidateEndpoint({ cdn })` purges the origin store, then the CDN, and answers `200` when the CDN accepted, `202` when the purge is queued, and `502` with `retryable` when the CDN refused. It also takes a batch `{ "paths": [...], "tags": [...] }` of at most 100 paths and 32 tags. `createInvalidator` does the same from app code.
|
|
476
|
+
- `revalidateTags` may be a function of the route's params and URL, for tags per page. Invalid tags it returns are dropped with one `NIFRA_CDN_TAG_INVALID` warning per route.
|
|
477
|
+
- `isCacheablePage` is exported: the one rule ISR and the CDN cache by.
|
|
478
|
+
- New error codes: `NIFRA_CDN_HOST_ROUTED`, `NIFRA_CDN_TAG_INVALID`, `NIFRA_CDN_RATE_LIMITED`, `NIFRA_CDN_PURGE_FAILED`.
|
|
479
|
+
|
|
480
|
+
- a0cfffa: `export const clientIp = "platform"` in `nifra.config.ts` makes a `cloudflare` or `vercel` build read `c.clientIp` from the header that platform's edge overwrites (`cf-connecting-ip`, `x-real-ip`), so per-caller middleware such as `rateLimit` works there. Without it an edge build still has no caller address; Bun, Node and Deno builds use the socket peer either way. `buildTarget` and `generateServerEntry` take the same `clientIp` option. Site scaffolds declare it.
|
|
481
|
+
- 93e5e7f: Errors come with a prompt to paste into a coding agent: the error, where it is, the recognised cause, one fix, and steps that end in a check the agent runs itself (`nifra_errors` with a `since` cursor, then `nifra check`). App-supplied text is fenced and labeled as data, paths are project-relative, the home directory never appears, and the prompt is capped at 8000 characters.
|
|
482
|
+
|
|
483
|
+
- Codes with more than one right fix (`NIFRA_BACKEND_IN_CLIENT`, `NIFRA_BACKEND_ONLY_IN_CLIENT`, `NIFRA_OUTPUT_SENSITIVE_FIELD`, `NIFRA_OUTPUT_UNDECLARED_DEFERRED`, `NIFRA_OUTPUT_RAW_RESPONSE`, `NIFRA_OUTPUT_SCHEMA_MISMATCH`) list each as a labeled `fixOptions` entry on the `Diagnostic`, with one prompt per option.
|
|
484
|
+
- The dev overlay has a Copy prompt button per fix. A page load whose render throws gets the overlay on both dev pipelines; data requests and API calls keep the app's JSON 500.
|
|
485
|
+
- A dev page that reports a browser error shows a badge listing that page's errors with their code, message, codeframe, fix and Copy prompt buttons. It renders in a closed shadow root, loads under the page's CSP (nonce, exact URL, or `'strict-dynamic'`), is only fetched once a page errors, and turns off with `nifra dev --no-indicator`, `export const dev = { indicator: false }` in `nifra.config.ts`, or `indicator: false` on `createDevServer`/`createViteDevServer`.
|
|
486
|
+
- `nifra errors --prompt` (and `nifra_errors` with `prompt: true`) prints the prompt for the newest entry; `--id` picks an entry and `--option` picks a fix by its label.
|
|
487
|
+
- `@nifrajs/web/diagnostic-prompt` exports `buildFixPrompt`, `fixPrompts` and `catalogFixPrompts` without Node APIs, for use in a browser bundle.
|
|
488
|
+
|
|
489
|
+
- 4936309: feat(i18n): one locale registry and locale-prefixed routing; feat(web): PWA manifest builder
|
|
490
|
+
|
|
491
|
+
`defineLocales()` declares each locale once - URL segment, BCP-47 tag for `Intl`, `hreflang` value,
|
|
492
|
+
writing direction and native name, each defaulted from the segment - and marks unfinished
|
|
493
|
+
translations `draft`. It validates tags, segments and `hreflang` uniqueness at definition and gives
|
|
494
|
+
`served`, `get()`, a catalog fallback `chain()` (`fr-CA` → `fr` → default) and `documentMeta()` for a
|
|
495
|
+
route's `<html lang>`/`<html dir>`. `localeDirection()` derives the direction from an explicit
|
|
496
|
+
script, a right-to-left language, or the runtime's likely script.
|
|
497
|
+
|
|
498
|
+
`@nifrajs/i18n/routing` adds `defineI18nRouting(locales)` - the URL half of i18n next to
|
|
499
|
+
`negotiateLocale`'s detection half. Pure and dependency-free: prefix, strip and read locale prefixes
|
|
500
|
+
(served locales only, case-insensitive, so `/frank` never reads as French and a draft's prefix is an
|
|
501
|
+
ordinary segment); `alternates(path, { origin?, locales? })` returns the page's canonical URL and its
|
|
502
|
+
`hreflang` links - absolute or root-relative, limited to the locales the page exists in, listed in
|
|
503
|
+
registry order so every page of a cluster agrees, with `x-default` when the default is listed; and
|
|
504
|
+
`matchSegment()` checks a `[lang]` route segment for a route `middleware` guard, answering not-found for
|
|
505
|
+
an unknown or draft value and a redirect for the default's prefix or a wrong case. No path it builds
|
|
506
|
+
can start with `//` or `/\`. No regex runs on request input.
|
|
507
|
+
|
|
508
|
+
`@nifrajs/web/pwa-manifest` adds `pwaManifest()` - the declarative PWA half next to the
|
|
509
|
+
service-worker generator. Pure builder for `manifest.json` bytes (names, scope defaulted
|
|
510
|
+
from `start_url`, icons/screenshots/shortcuts, colors) with fail-loud validation of spec
|
|
511
|
+
shapes, plus `serializeManifest()` and the `<link rel="manifest">` tag helper.
|
|
512
|
+
|
|
513
|
+
- e8270d9: feat(web): SSR loader, action and boundary calls through `ctx.api` now carry the page request's
|
|
514
|
+
platform identity. A backend reached from a loader sees the visitor's `c.clientIp` (derived under the
|
|
515
|
+
page app's `server.clientIp` trust declaration), `c.env` and `c.waitUntil`, so per-visitor rate limits,
|
|
516
|
+
audit logs and bindings work during SSR. Headers are never copied: a loader call stays anonymous unless
|
|
517
|
+
the loader passes `cookie` or `authorization` itself. `inProcessClient()` / `testClient()` used outside a
|
|
518
|
+
render keep dispatching with no platform.
|
|
519
|
+
|
|
520
|
+
New seams in `@nifrajs/core/mount`: `NIFRA_BACKEND_BIND_PLATFORM` (returns a platform-bound view of an
|
|
521
|
+
in-process client) and `NIFRA_PLATFORM_CLIENT_IP_DERIVED` (marks a platform whose `clientIp` an enclosing
|
|
522
|
+
server already derived, so a backend with its own `clientIp` trust keeps it instead of re-reading
|
|
523
|
+
forwarding headers a synthesized request does not carry).
|
|
524
|
+
|
|
525
|
+
- d942c33: feat(web): a loader or action sets response headers and cookies through `ctx.set`. It has `headers`,
|
|
526
|
+
`cookie()`, and `deleteCookie()`, the page counterpart of a route handler's `c.set`.
|
|
527
|
+
`LoaderResponseControls` is exported from `@nifrajs/web` and `@nifrajs/client`.
|
|
528
|
+
|
|
529
|
+
- `ctx.set.headers` applies to the rendered document. It is not applied to a redirect, a status page,
|
|
530
|
+
an error page, or a navigation data response.
|
|
531
|
+
- Each loader has its own header record. They merge layouts root to leaf, then the page loader, then
|
|
532
|
+
the action, so the most specific writer wins a name whichever loader settles first.
|
|
533
|
+
- `content-type`, `set-cookie`, `location`, the transport headers, and the `x-nifra-` prefix are
|
|
534
|
+
refused. So is a name that is not an HTTP token, and a value with a line break, a control
|
|
535
|
+
character, or a character outside Latin-1. The error names the header, never its value.
|
|
536
|
+
- `ctx.set.cookie()` uses the same secure defaults as `c.set.cookie` and rides every outcome,
|
|
537
|
+
including a thrown `Response`. A queued cookie makes the response `cache-control: private, no-store`
|
|
538
|
+
- the document, a redirect, a status or error page, a hand-built `Response` - and keeps the page out
|
|
539
|
+
of `withISR`.
|
|
540
|
+
- The controls close when the loader or action settles. A later write throws.
|
|
541
|
+
- `enableDraft(ctx, secret)` works from a loader or action.
|
|
542
|
+
|
|
543
|
+
Behavior changes:
|
|
544
|
+
|
|
545
|
+
- A navigation data response, from a loader or an action, is now always
|
|
546
|
+
`cache-control: private, no-store` with `vary: x-nifra-data`. It shares its URL with the document,
|
|
547
|
+
so a cache keyed on the URL must not store it.
|
|
548
|
+
- A document that carries loader headers has `x-nifra-data` added to its `vary`.
|
|
549
|
+
- `withISR` stores a page whose only `vary` token is `x-nifra-data`, and replays `x-robots-tag`.
|
|
550
|
+
- `LoaderContext` and `LoaderArgs` have a required `set`. A test that builds one by hand needs to
|
|
551
|
+
supply it.
|
|
552
|
+
|
|
553
|
+
- 8e30090: feat: a mount nifra cannot analyze is a declared known gap, not a silent hole. Routes behind
|
|
554
|
+
`mount()` and `mountFetch()` are invisible to route reflection, so capability assurance could not
|
|
555
|
+
see them and said nothing. Both now take `opaque: "<reason>"`, and so does a `createWebApp`
|
|
556
|
+
`mounts` entry. Capability assurance lists a mount with a reason as a known gap
|
|
557
|
+
(`report.gaps`, `{ kind: "opaque-mount", path, reason }`): `nifra check` and
|
|
558
|
+
`nifra capabilities check` print it on every run, and it fails nothing and lowers no level. A mount
|
|
559
|
+
on the analyzed app without a reason fails with the new `opaque-mount-undeclared` finding, which
|
|
560
|
+
suggests `merge()` for a nifra `server()` and `opaque` for anything else. A mount whose app publishes
|
|
561
|
+
composed evidence (the API `createWebApp` mounts) is not reported. `webProjectEvidence` accepts a
|
|
562
|
+
`mounts` entry without an evidence provider when it declares `opaque`. New exports:
|
|
563
|
+
`reflectMounts` and `ReflectedMount` from `@nifrajs/core/reflection`, `CapabilityGap` from
|
|
564
|
+
`@nifrajs/core/capabilities`; project evidence snapshots gain an optional `mounts` list, absent for
|
|
565
|
+
an app with no mounts.
|
|
566
|
+
- 6d20355: feat(core): a path param can say which values it accepts, written in braces after the name. A
|
|
567
|
+
request whose value does not fit is not served by that route:
|
|
568
|
+
|
|
569
|
+
```ts
|
|
570
|
+
app
|
|
571
|
+
// /users/me is its own route; /users/42 is this one; /users/ada is a 404
|
|
572
|
+
.get("/users/me", () => ({ me: true }))
|
|
573
|
+
.get("/users/:id{[0-9]+}", (c) => ({ id: Number(c.params.id) }))
|
|
574
|
+
// a list of values, and a count
|
|
575
|
+
.get("/img/:size{thumb|full}/:file", (c) => ({
|
|
576
|
+
size: c.params.size,
|
|
577
|
+
file: c.params.file,
|
|
578
|
+
}))
|
|
579
|
+
.get("/countries/:code{[A-Z]{2}}", (c) => ({ code: c.params.code }))
|
|
580
|
+
// inside a segment, and optional at the end of a path
|
|
581
|
+
.get("/files/:name.:ext{png|jpg}", (c) => ({
|
|
582
|
+
name: c.params.name,
|
|
583
|
+
ext: c.params.ext,
|
|
584
|
+
}))
|
|
585
|
+
.get("/posts/:page{[0-9]+}?", (c) => ({ page: c.params.page ?? "1" }));
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
- A constraint is one character class with an optional count (`[0-9]`, `[a-z0-9_-]+`, `\d{4}`,
|
|
589
|
+
`\w{2,8}`), or a list of two or more values (`png|jpg|webp`). A class holds letters, digits, ranges
|
|
590
|
+
of them, `\d`, `\w` and `. _ ~ ! $ & ' ( ) + , ; = @ -`; there is no negated class and a count
|
|
591
|
+
starts at one. Anything else in braces (`:id{int}`, `:id{.+}`, `:id{[0-9]+|[a-z]+}`) is literal
|
|
592
|
+
text, as before.
|
|
593
|
+
- The param stays a `string`, keyed by its bare name: `Params<"/users/:id{[0-9]+}">` is
|
|
594
|
+
`{ id: string }`.
|
|
595
|
+
- The value is checked as it was sent, before percent-decoding. `/users/4%32` does not fit
|
|
596
|
+
`:id{[0-9]+}`; a broader route beside it serves that request.
|
|
597
|
+
- The narrowest route answers, whatever the order of registration: literal text, then a list, then a
|
|
598
|
+
class, then a bare `:param`, then a wildcard. Between two constraints of a kind, the one that
|
|
599
|
+
accepts fewer values is tried first. A method the narrowest matching route does not have answers
|
|
600
|
+
`405`, as it does for a literal route beside a param route.
|
|
601
|
+
- Two spellings of one constraint (`[0-9]+`, `\d+`, `[0-9]{1,}`) are one route: the same method
|
|
602
|
+
registered on both throws `DUPLICATE_ROUTE`.
|
|
603
|
+
- Inside a segment the text around the params is placed first and each value is then checked; the
|
|
604
|
+
router does not look for another split.
|
|
605
|
+
- `routePatternOverlap` takes constraints into account: `/users/me` and `/users/:id{[0-9]+}` do not
|
|
606
|
+
overlap.
|
|
607
|
+
- `@nifrajs/core/pattern` exports `paramConstraint(text)`, which reads a constraint at the start of
|
|
608
|
+
`text`, and the `ParamConstraint` type. A param part of a compiled mixed segment carries its
|
|
609
|
+
constraint as `c`.
|
|
610
|
+
|
|
611
|
+
feat(edge): the compact server accepts the same constraints.
|
|
612
|
+
|
|
613
|
+
feat(schema): `toOpenAPI` writes a constrained param into the path template by its bare name
|
|
614
|
+
(`/users/{id}`). Its schema is `{ type: "string", pattern }` for a character class and
|
|
615
|
+
`{ type: "string", enum }` for a list of values; a declared `params` schema still takes precedence.
|
|
616
|
+
|
|
617
|
+
feat(client): a constrained param is passed by its bare name, `api.users({ id: "42" }).get()`. Two
|
|
618
|
+
param routes at one position (`/users/:id{[0-9]+}` beside `/users/:slug`, or a param beside a
|
|
619
|
+
wildcard) are each callable, picked by the name of the key. The client does not check a value
|
|
620
|
+
against its constraint.
|
|
621
|
+
|
|
622
|
+
feat(cli): `nifra check` accepts a supported constraint and keeps reporting other text in braces
|
|
623
|
+
(`NF-C026`); `NF-C024` and `NF-C025` follow the router's reading of a constraint. `nifra routes` and
|
|
624
|
+
the generated client calls print the bare name. `nifra scaffold` refuses a page path that carries a
|
|
625
|
+
constraint.
|
|
626
|
+
|
|
627
|
+
feat(testing): `runAdversarialContract` builds a request path whose values satisfy each param's
|
|
628
|
+
constraint, and fills a part-literal segment (`/files/:name.json`) param by param.
|
|
629
|
+
|
|
630
|
+
feat(web): a route file name that would read as a param constraint (`[id]{a|b}.tsx`) is refused at
|
|
631
|
+
build time with a message that names the file. `llms.txt` prints client calls with the bare name.
|
|
632
|
+
|
|
633
|
+
- ba5dd1c: feat(web): a directory's route middleware runs before every page in it and below.
|
|
634
|
+
|
|
635
|
+
```ts
|
|
636
|
+
// routes/account/_layout.backend.ts
|
|
637
|
+
import { type RouteMiddleware, redirect } from "@nifrajs/web";
|
|
638
|
+
|
|
639
|
+
export const middleware: RouteMiddleware = ({ request }) => {
|
|
640
|
+
const signedIn =
|
|
641
|
+
request.headers.get("cookie")?.includes("session=") ?? false;
|
|
642
|
+
return signedIn ? undefined : redirect("/login");
|
|
643
|
+
};
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
Route middleware runs on the server, outermost first, before the layouts' loaders (gates included)
|
|
647
|
+
and the page's loader or action, for document requests, client navigations and form posts alike, and
|
|
648
|
+
before a nested `_404` in its directory. It returns nothing to let the request through, or returns or
|
|
649
|
+
throws a `redirect()`, a status such as `notFound()`, or a `Response` to answer with it. `ctx.set` adds
|
|
650
|
+
headers and cookies, and `ctx.params` holds the params of its directory's URL prefix. It never reaches
|
|
651
|
+
the client bundle, and a directory may export it from `_layout.backend.ts` without a frontend layout.
|
|
652
|
+
|
|
653
|
+
- d50f73e: feat: generated route types
|
|
654
|
+
|
|
655
|
+
Every route file gets a generated `Route` namespace, imported from `./+types/<name>` by both halves
|
|
656
|
+
of the route:
|
|
657
|
+
|
|
658
|
+
```ts
|
|
659
|
+
// routes/blog/[slug].backend.ts
|
|
660
|
+
import type { Route } from "./+types/[slug]"
|
|
661
|
+
export const loaderOutput = t.object({ title: t.string() })
|
|
662
|
+
export async function loader({ api, params }: Route.LoaderArgs) { ... }
|
|
663
|
+
|
|
664
|
+
// routes/blog/[slug].tsx
|
|
665
|
+
import type { Route } from "./+types/[slug]"
|
|
666
|
+
export default function Post({ data, params }: Route.ComponentProps) { ... }
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
`Route.Params` comes from the path (`[id]`, optional `[[lang]]`, catch-all `[...path]`).
|
|
670
|
+
`Route.LoaderData` and `Route.ActionData` are the output types of `loaderOutput` and `actionOutput`,
|
|
671
|
+
so a component is typed with exactly what reaches it. `Route.LoaderArgs` types `api` from the app's
|
|
672
|
+
`backend/app.ts`, which `.nifra/types/register.d.ts` registers once with `@nifrajs/client`'s new
|
|
673
|
+
`Register` interface; no route imports the backend for its types.
|
|
674
|
+
|
|
675
|
+
`nifra types` writes the files to `.nifra/types` (and `--check` fails when one is stale); `nifra dev`,
|
|
676
|
+
`nifra build` and `nifra check` refresh them, and `nifra dev` keeps them current as routes are added
|
|
677
|
+
or removed. A route resolves `./+types/<name>` through `"rootDirs": [".", "./.nifra/types"]` in
|
|
678
|
+
tsconfig, which the scaffolded site and ISR apps now carry; `nifra types` says so when it is missing.
|
|
679
|
+
`@nifrajs/web/route-types` exports the generator.
|
|
680
|
+
|
|
681
|
+
- 085e852: On the Bun dev pipeline, an error thrown from a `.vue` or `.svelte` file names the line written in that file, in the browser and on the server: the overlay, the in-page badge, `nifra errors` and the fix prompts all point there.
|
|
682
|
+
|
|
683
|
+
- `vueBunPlugin` and `svelteBunPlugin` attach their compile map while a dev server runs; `nifra build` output is unchanged.
|
|
684
|
+
- `@nifrajs/web/plugins/kit` exports `withDevSourceMap` and `concatSourceMaps` for other compiler plugins to do the same.
|
|
685
|
+
- Svelte's `hydration_html_changed` and `hydration_attribute_changed` warnings count as hydration mismatches. Vue's feature-flag warning, which names `__VUE_PROD_HYDRATION_MISMATCH_DETAILS__`, does not.
|
|
686
|
+
|
|
687
|
+
- ae815ab: The zone rules now cover files a client build copies or inlines without importing them:
|
|
688
|
+
|
|
689
|
+
- New `viteAssetUrlGuard()` in `@nifrajs/web/plugins/vite-leak-guard`, which the Vite client build installs. It fails the build when a `new URL("...", import.meta.url)` in browser code names backend code, a route's backend half, a server function's source, or a file in no zone. It applies whatever the file's size, a `?inline` query, or a comment inside the call. Such a file is also never inlined from a stylesheet `url()`, so the output accounting names it.
|
|
690
|
+
- An asset emitted from a `*.fn.*` file is refused: a server function reaches the browser only as its call stub.
|
|
691
|
+
- A `.fn` file that is not a script module (`report.fn.vue`, `post.fn.mdx`) is a zone error that asks for a rename. Previously it was treated as a server function whose source the browser build would ship unchanged.
|
|
692
|
+
|
|
693
|
+
- 3442e1c: feat(web): `export const ssr = false` keeps a page's component off the server. The route is still
|
|
694
|
+
matched, its layouts still render, its gates and loader still run and the data is still embedded;
|
|
695
|
+
the server puts the page's `HydrateFallback` export in the page slot - or nothing - and the browser
|
|
696
|
+
hydrates that before it renders the component with the data it already has. A client navigation to
|
|
697
|
+
the page renders the component directly. `HydrateFallback` receives the component's props. The
|
|
698
|
+
module is still imported on the server for its loader and options, so an import that needs a
|
|
699
|
+
browser at load time belongs inside the component. `ssr = false` together with `hydrate = false`
|
|
700
|
+
is refused when the page is rendered: nothing would render it. On Solid the layouts mount again
|
|
701
|
+
when the component takes over from the fallback, as they do on any Solid route change.
|
|
702
|
+
|
|
703
|
+
`assertRenderAdapterConformance` now also renders a chain whose leaf is a function returning
|
|
704
|
+
`null` and requires the layouts around an empty page slot (check `"empty leaf"`). An adapter that
|
|
705
|
+
cannot render such a leaf fails conformance.
|
|
706
|
+
|
|
707
|
+
fix(web-react): the mounted router hydrates against the state it was mounted with. A router that
|
|
708
|
+
changed before React reached the component - a client loader that answered first - no longer
|
|
709
|
+
hydrates a tree the server markup never had; React renders the newer state right after.
|
|
710
|
+
|
|
711
|
+
- 8b99424: feat: the dev server keeps a feed of errors, logs and requests for coding agents
|
|
712
|
+
|
|
713
|
+
`nifra dev` (Bun and Vite pipelines alike) records what happened while it ran and serves it to local
|
|
714
|
+
tools:
|
|
715
|
+
|
|
716
|
+
- Errors from every layer, each a structured diagnostic: SSR renders, loaders and actions, backend
|
|
717
|
+
handlers, builds, the browser, hydration mismatches, and a crash of the server itself. Repeats of
|
|
718
|
+
one failure collapse into one entry with a count; an entry recorded before the last file change is
|
|
719
|
+
flagged `stale`, and a build error clears when the next build passes.
|
|
720
|
+
- Console output from the server and from the browser.
|
|
721
|
+
- One trace per request (method, path, status, duration, bytes, ISR status, its errors and log
|
|
722
|
+
count). Every dev response carries `x-nifra-request-id`, and every entry from that request, server
|
|
723
|
+
or browser, carries the same id.
|
|
724
|
+
|
|
725
|
+
Each dev page carries a small inline script, first in `<head>`, that reports uncaught errors,
|
|
726
|
+
unhandled rejections, failed script and stylesheet loads, console output and errors passed to
|
|
727
|
+
`console.error`. Browser stacks are mapped to source through the dev server's own source maps, in
|
|
728
|
+
Chrome, Firefox and Safari formats. The script is admitted by hash in a page's CSP (by the page nonce
|
|
729
|
+
under Vite) along with its endpoint in `connect-src`; a page whose CSP allows no script gets none.
|
|
730
|
+
|
|
731
|
+
The server writes `.nifra/dev-server.json` (owner-only) with its port and a per-run token, and a log
|
|
732
|
+
under `.nifra/dev-server.log` that outlives a crash. The feed endpoints under `/__nifra/` answer only
|
|
733
|
+
a loopback `Host` presenting that token; browser reports need a separate page token and a same-origin
|
|
734
|
+
`Origin`, are size-capped and rate-limited. Values of non-public environment variables, keys, tokens,
|
|
735
|
+
JWTs and credential headers are redacted before anything is stored. `record: false` on
|
|
736
|
+
`createDevServer` / `createViteDevServer` turns the record and the log off. The feed's types, the
|
|
737
|
+
discovery record reader and the redacting store are exported from `@nifrajs/web/dev-feed`.
|
|
738
|
+
|
|
739
|
+
The dev overlay links each recognised error code to its section of the new error codes page. Dev only;
|
|
740
|
+
production builds and responses are unchanged.
|
|
741
|
+
|
|
742
|
+
- 0e14068: fix(web): the client build fails on a Node built-in the bundler left as an external import
|
|
743
|
+
|
|
744
|
+
Bun keeps a dynamic `import("node:fs")`, and a bare built-in such as `import("fs/promises")`, as an
|
|
745
|
+
import in a browser chunk instead of bundling a polyfill. The import shipped and failed in the
|
|
746
|
+
browser, and the Node built-in guard passed it because the module was in no output. The guard now
|
|
747
|
+
reports such an import in the chunk its importer landed in, with the import chain, and names a bare
|
|
748
|
+
built-in with its `node:` prefix.
|
|
749
|
+
|
|
750
|
+
The Vite pipeline gives the same result: `viteBareBuiltinExternal()` from
|
|
751
|
+
`@nifrajs/web/plugins/vite-leak-guard` keeps a bare built-in named instead of letting Vite replace it
|
|
752
|
+
with an empty stub, so the guard fails the build there too. A package of the same name the app
|
|
753
|
+
installed (`events`, `buffer`) still resolves to that package. `buildClientVite` adds the plugin.
|
|
754
|
+
|
|
755
|
+
- 5a63dd4: feat(web): pages can carry a strict Content-Security-Policy and still be cached.
|
|
756
|
+
A per-request nonce makes every document unique, so nifra marks a nonce-bearing page
|
|
757
|
+
`private, no-store` and no shared cache (`withISR`, a CDN) may store it. `createCspPolicy({ header })`
|
|
758
|
+
is the cacheable alternative: pass it to `createWebApp({ csp })` (or `renderPage({ csp })`) instead of
|
|
759
|
+
`nonce`. A document then carries a nonce only when it has a script specific to this request (a
|
|
760
|
+
deferred value, or `meta` naming the nonce in `unsafeInlineScript`). Every other document is
|
|
761
|
+
nonce-free, its constant inline scripts are allowed by sha256 hash, and its CSP header is the same on
|
|
762
|
+
every request. `header` receives `sources`, the `script-src` list the document needs.
|
|
763
|
+
`nifraScriptHashes(adapter)` returns the hashes for a policy set at a proxy or CDN. Passing both `csp`
|
|
764
|
+
and `nonce` throws.
|
|
765
|
+
|
|
766
|
+
The page-state handover is now one inert `<script type="application/json" id="__nifra-handover">`
|
|
767
|
+
instead of an executable script that assigned `window.__NIFRA_DATA__` and its siblings, so page data
|
|
768
|
+
needs no nonce or hash under any policy. The client entry assigns the same globals before anything
|
|
769
|
+
reads them. Code that read those globals from an inline script running before the client entry must
|
|
770
|
+
run after it, or read the handover element (`HANDOVER_ID`). `nifra assure --hydration` reads the new
|
|
771
|
+
format. `RenderAssemblyCache` loses its `tailMid` and `tailData` slots.
|
|
772
|
+
|
|
773
|
+
`withISR` warns once when it wraps an app created with `nonce`, since it can never store one of its
|
|
774
|
+
pages. It also remembers, for its revalidate window, keys whose page answered `private` or
|
|
775
|
+
`no-store` and skips the store lookup for them. A remembered key that turns cacheable is stored again.
|
|
776
|
+
|
|
777
|
+
- dfd19d8: feat(web)!: `withISR` no longer caches a page per query string by default.
|
|
778
|
+
The default key was `origin + pathname + search`, so `?a=1`, `?a=2`, ... each stored a full page and
|
|
779
|
+
anyone could grow the cache without bound. The new `query` option decides how a query string reaches
|
|
780
|
+
the default key:
|
|
781
|
+
|
|
782
|
+
- `"bypass"` (default): a request carrying any query parameter skips the cache. It is rendered fresh
|
|
783
|
+
and never stored. A request without one is keyed on `origin + pathname`.
|
|
784
|
+
- `["page", "sort"]`: those parameters join the key in any order (`?sort=new&page=2` and
|
|
785
|
+
`?page=2&sort=new` share an entry). A request carrying any other parameter skips the cache, so a
|
|
786
|
+
loader that reads it is never handed another request's page.
|
|
787
|
+
- `"all"`: the previous behavior, one entry per distinct query string.
|
|
788
|
+
|
|
789
|
+
`revalidateEndpoint` takes the same `query` option so a purged path's query is keyed the way it was
|
|
790
|
+
stored. A purge of a query string the policy never caches returns `400` with `uncached_query`. A
|
|
791
|
+
custom `key` on either still overrides the policy.
|
|
792
|
+
|
|
793
|
+
Migration: pass `query: "all"` to both `withISR` and `revalidateEndpoint` to keep the old keys.
|
|
794
|
+
|
|
795
|
+
- 6393b1e: feat(web): `_loading.tsx` is what the page slot shows while a client navigation loads. A navigation
|
|
796
|
+
still waiting on its data after about 120 ms swaps the page for the target route's nearest
|
|
797
|
+
`_loading` - the innermost one above it whose layouts are already on screen, which are the layouts
|
|
798
|
+
the two pages share; they stay mounted with their data. A `_loading` never renders outside a layout
|
|
799
|
+
above it. A navigation that settles sooner goes straight to the new page, as does one with no
|
|
800
|
+
eligible `_loading`; a change of search on the same path and a form submit keep the page. A newer
|
|
801
|
+
navigation, a form submit, or a refresh of the page on screen takes over from a pending loading
|
|
802
|
+
page, and a superseded navigation that fails later changes nothing. The
|
|
803
|
+
component receives `pending` and no `data`. It is browser-only: the server never renders one, and an
|
|
804
|
+
app without a `_loading` file ships no code for it. Where the browser runs view transitions, a
|
|
805
|
+
navigation's transition ends on the loading page instead of holding the old page until the data
|
|
806
|
+
arrives. `Manifest.loadings` lists the pages (`LoadingEntry`, each with the layouts it sits under)
|
|
807
|
+
and `RouteEntry.loadingIds` the ones above each route. In a `buildClient` build every page links the
|
|
808
|
+
loading pages' stylesheets.
|
|
809
|
+
|
|
810
|
+
A file named `_loading` was an ignored underscore file before; one kept in a routes directory for
|
|
811
|
+
another purpose is now a loading page.
|
|
812
|
+
|
|
813
|
+
fix(web-solid): the mounted router renders the chain the router names while a navigation is pending,
|
|
814
|
+
which is how a `_loading` page reaches the screen. A pending navigation on the same chain is still
|
|
815
|
+
skipped until it settles.
|
|
816
|
+
|
|
817
|
+
- a158b74: feat(web): a page file under a mount fails at startup, at build and in `nifra check`, instead of
|
|
818
|
+
answering with the mount's 404. A mount in front of the page router - the backend at `apiPrefix`, a
|
|
819
|
+
`mounts` entry, or an `app.mount()` inside `use` - answers every request under its path, and its 404
|
|
820
|
+
is final (`fallbackOn: 404` only tries the next mount), so a page there could never render.
|
|
821
|
+
`createWebApp` now throws at startup, naming each file, the URL it serves and the mount; `nifra build`
|
|
822
|
+
refuses to build; and `nifra check` reports `NF-C027` for a page under the backend's prefix, reading
|
|
823
|
+
`apiPrefix` when `backend/framework.ts` exports it as a string literal (an `info` finding says so when it does
|
|
824
|
+
not).
|
|
825
|
+
|
|
826
|
+
`backend/framework.ts` can export `apiPrefix`, `apiStrip`, `mounts`, `csp` and `nonce`. `nifra dev`,
|
|
827
|
+
`nifra build` (the generated server entry and the static prerender), `nifra mcp`'s render tool and the
|
|
828
|
+
hydration gate pass the same set to `createWebApp`, and the render tool and the hydration gate now
|
|
829
|
+
apply `use` as well. A field of the wrong type fails at load, naming it, and a value
|
|
830
|
+
`nifra.config.ts` exports must be the one `backend/framework.ts` exports, or the build stops. `nifra routes`
|
|
831
|
+
lists the backend under the configured prefix, at the served path when `apiStrip` is set.
|
|
832
|
+
|
|
833
|
+
New exports: `preRouteMountPaths` from `@nifrajs/core/mount`; `shadowedPages`,
|
|
834
|
+
`formatShadowedPages`, `normalizeMountPath` and `ShadowedPage` from `@nifrajs/web/route-manifest`;
|
|
835
|
+
`SERVER_ENTRY_OPTIONS` and the `optionImports` option of `generateServerEntry` and `buildTarget` from
|
|
836
|
+
`@nifrajs/web/build`.
|
|
837
|
+
|
|
838
|
+
Upgrading: an app with a page file under its backend prefix (`routes/api/*` with a backend)
|
|
839
|
+
started before and answered that page with a 404; it now fails to start until the file moves out of
|
|
840
|
+
the prefix or the prefix changes.
|
|
841
|
+
|
|
842
|
+
- ee19d29: feat(web): a `_404.tsx` in a directory answers for that part of the app. `routes/admin/_404.tsx`
|
|
843
|
+
renders for the unmatched URLs under `/admin` and for `notFound()` from the routes beneath it - the
|
|
844
|
+
nearest one wins - inside the layouts at or above it, with their loader data. The layouts run as
|
|
845
|
+
they do for a page, so a `gate` decides before anything renders. The page is served non-hydrated,
|
|
846
|
+
and with `cache-control: private, no-store` once a layout loaded data for it. `Manifest.notFounds`
|
|
847
|
+
lists these pages (`NotFoundEntry`, each with the `NotFoundScope` patterns it answers) and
|
|
848
|
+
`RouteEntry.notFoundIds` the ones above each route.
|
|
849
|
+
|
|
850
|
+
fix(web): a `_404.tsx` below the routes root no longer takes the place of the root one. Two route
|
|
851
|
+
groups that each hold a `_404` for the same URL prefix are refused at boot, unless a directory that
|
|
852
|
+
contains both holds one as well.
|
|
853
|
+
|
|
854
|
+
- 7e1e1c3: feat(web): `data-nifra-prefetch` picks when a link warms its route: `intent`, `viewport`, `render` or `none`.
|
|
855
|
+
|
|
856
|
+
```html
|
|
857
|
+
<nav data-nifra-prefetch="viewport">
|
|
858
|
+
<a href="/docs/routing">Routing</a>
|
|
859
|
+
<a href="/reports/annual" data-nifra-prefetch="none">Annual report</a>
|
|
860
|
+
</nav>
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
The attribute goes on a link or on any element around it; the nearest one wins. `intent` (the
|
|
864
|
+
default, as before) warms the route's code and loader data on hover or keyboard focus, `viewport`
|
|
865
|
+
once the link scrolls into view, `render` as soon as a page shows it, and `none` never. An unknown
|
|
866
|
+
value is `intent`. `viewport` and `render` links are found when the page loads and whenever the
|
|
867
|
+
router settles (a navigation, a submit); a link the page adds in between warms on intent until
|
|
868
|
+
then. It works under every adapter, and React's `<Link>` and `<NavLink>` take a `prefetch` prop
|
|
869
|
+
that renders it.
|
|
870
|
+
|
|
871
|
+
Prefetched data is now used by a click within 30 seconds of arriving; after that the click loads
|
|
872
|
+
the route again. The page already on screen is no longer prefetched. `PrefetchMode` is exported
|
|
873
|
+
from `@nifrajs/web`.
|
|
874
|
+
|
|
875
|
+
- bd11269: feat(web): a layout's `shouldRevalidate` decides whether its loader runs again on a client navigation.
|
|
876
|
+
|
|
877
|
+
```ts
|
|
878
|
+
// routes/orgs/[org]/_layout.backend.ts
|
|
879
|
+
export const shouldRevalidate: ShouldRevalidate = ({
|
|
880
|
+
currentParams,
|
|
881
|
+
nextParams,
|
|
882
|
+
}) => currentParams.org !== nextParams.org;
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
It receives the URL and params being left (`currentUrl`, `currentParams`), the ones being loaded
|
|
886
|
+
(`nextUrl`, `nextParams`, every route param rather than only the layout's own), and
|
|
887
|
+
`defaultShouldRevalidate`: `true` when a param the layout owns or the query changed. Returning
|
|
888
|
+
`false` keeps the data the browser already holds; any other value, a returned promise included, runs
|
|
889
|
+
the loader. A throw fails the navigation the way the loader's own error would.
|
|
890
|
+
|
|
891
|
+
It runs on the server, on a client navigation that keeps the layout on screen while the browser
|
|
892
|
+
holds its data. A document request, the revalidation after an action, and `router.invalidate()` run
|
|
893
|
+
every loader without asking. A `gate` layout is never asked: it runs on every request. A page's
|
|
894
|
+
`shouldRevalidate` is not read, because a page's loader runs on every navigation; query keys a page
|
|
895
|
+
never reads belong in its `searchClientKeys`.
|
|
896
|
+
|
|
897
|
+
`ShouldRevalidate` and `ShouldRevalidateArgs` are exported from `@nifrajs/web`.
|
|
898
|
+
|
|
899
|
+
- 64e7a42: fix(web): pages that `defer()` keep working under a nonce Content-Security-Policy.
|
|
900
|
+
React, Solid and Preact stream inline scripts to reveal a Suspense boundary that resolves after the
|
|
901
|
+
shell, and none of them carried the document's nonce, so a nonce CSP blocked them and the boundary
|
|
902
|
+
stayed on its fallback. `RenderAdapter.renderToStream` now receives `{ nonce }` as an optional third
|
|
903
|
+
argument (`RenderStreamOptions`), and every adapter that streams scripts applies it: React and Solid
|
|
904
|
+
through their own `nonce` option, Preact on the one island-runtime script it streams. A script the
|
|
905
|
+
app renders itself never inherits the nonce. Vue, Svelte and the vanilla adapter stream no scripts.
|
|
906
|
+
- d6f806f: An `*.svg?component` import compiles its file as markup only, for every framework:
|
|
907
|
+
|
|
908
|
+
- JSX (React, Preact, Solid) spells braces, `>` and `=` in text and CDATA as character references. An exported stylesheet (`.st0{fill:#FFF}`, `a > b`) now compiles, and text such as `{...}` in a `<title>` renders as written.
|
|
909
|
+
- Svelte spells braces in text and attribute values as character references, keeps a nested `<style>` or `<script>` as raw text, and refuses a directive attribute (`use:`, `on:`, `bind:`...) or a `svelte:` element.
|
|
910
|
+
- Vue marks the root `v-pre`, so `{{ }}`, `:bound` and `v-` attributes are not compiled. A `<template>` tag is refused.
|
|
911
|
+
- The file must be one well-formed `<svg>` element, with every attribute value quoted and nothing after the root. Anything else fails the build with a message naming the problem. The check is exported as `svgTemplateMarkup()`.
|
|
912
|
+
|
|
913
|
+
- 0f6babe: feat(web): a route can export a `handle`, and `useMatches()` reports the rendered chain on every adapter.
|
|
914
|
+
|
|
915
|
+
```tsx
|
|
916
|
+
// routes/orgs/[org]/_layout.tsx
|
|
917
|
+
import { useMatches } from "@nifrajs/web-react/router";
|
|
918
|
+
|
|
919
|
+
export const handle = { crumb: "Organization" };
|
|
920
|
+
|
|
921
|
+
export default function OrgLayout({
|
|
922
|
+
children,
|
|
923
|
+
}: {
|
|
924
|
+
children: React.ReactNode;
|
|
925
|
+
}) {
|
|
926
|
+
const crumbs = useMatches().flatMap(
|
|
927
|
+
(m) => (m.handle as { crumb?: string } | undefined)?.crumb ?? []
|
|
928
|
+
);
|
|
929
|
+
// ...
|
|
930
|
+
}
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
`useMatches()` lists the layouts being rendered, outermost first, then the page, each as
|
|
934
|
+
`{ id, pathname, params, data, handle }`: the route file without its extension, the part of the URL
|
|
935
|
+
it covers (never the query), the params it declares, its loader data (`null` without a loader), and
|
|
936
|
+
its `handle` export. The server render and the browser report the same list, so a breadcrumb trail
|
|
937
|
+
hydrates with no mismatch. `handle` is read from the module on each side and never serialized.
|
|
938
|
+
|
|
939
|
+
While a `_loading` page is up it takes the page's place; a nested `_404` reports the layouts it
|
|
940
|
+
renders inside; an `_error` page the server renders reports an empty list. The hook ships from each
|
|
941
|
+
adapter's `/router` entry: an array on React and Preact, a `Ref` on Vue, an accessor on Solid and
|
|
942
|
+
Svelte. An app that never calls it does not bundle it.
|
|
943
|
+
|
|
944
|
+
The Preact, Solid, Vue and Svelte client mounts now pass `params` and `path` to the rendered chain,
|
|
945
|
+
as the server render already did. `UIMatch` and `MatchChain` are exported from `@nifrajs/web`.
|
|
946
|
+
|
|
947
|
+
- 00f18bf: feat(web): `@nifrajs/web/vitals` reports Core Web Vitals from real users, each tagged with its route.
|
|
948
|
+
|
|
949
|
+
```ts
|
|
950
|
+
import { reportWebVitals } from "@nifrajs/web/vitals";
|
|
951
|
+
|
|
952
|
+
reportWebVitals((metric) => {
|
|
953
|
+
const { name, value, rating, id, route } = metric;
|
|
954
|
+
const body = JSON.stringify({ name, value, rating, id, route });
|
|
955
|
+
void fetch("/api/vitals", { method: "POST", body, keepalive: true });
|
|
956
|
+
});
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
`reportWebVitals` measures LCP, INP, CLS, FCP and TTFB with Google's `web-vitals`, an optional
|
|
960
|
+
peer dependency (`bun add web-vitals`), and reports each metric once its value is final, with the
|
|
961
|
+
id of the route it belongs to (the id `useMatches` reports). `softNavigations: true` measures each
|
|
962
|
+
client-side navigation as a page view of its own where the browser can (Chromium 151 and later),
|
|
963
|
+
and `reportAllChanges: true` reports every change instead of the final value. It returns a
|
|
964
|
+
function that stops reporting, and does nothing on the server.
|
|
965
|
+
|
|
966
|
+
### Patch Changes
|
|
967
|
+
|
|
968
|
+
- 86e2d0f: In Bun dev, deleting a route regenerates the client entry and runs the leak guard once instead of twice.
|
|
969
|
+
- 0e9b167: fix(web): a route's ISR freshness and tags stay between the app and its cache wrapper
|
|
970
|
+
|
|
971
|
+
`x-nifra-isr-revalidate` and `x-nifra-isr-tags` carry a route's `revalidate` and `revalidateTags`
|
|
972
|
+
to `withISR`. An app with no wrapper sent both to every visitor, and `withISR` passed them through
|
|
973
|
+
on responses it did not store (a query string under the default policy, a keyless request, a page
|
|
974
|
+
it refused to cache). `createWebApp` now emits them only once a wrapper attaches, and `withISR`
|
|
975
|
+
removes them from every response it returns.
|
|
976
|
+
|
|
977
|
+
- bbdc5a1: fix(core, web): JSON request bodies are matched by media type
|
|
978
|
+
|
|
979
|
+
A body is parsed as JSON when its `Content-Type` media type is `application/json` or an
|
|
980
|
+
`application/*+json` type. A header that only mentions `application/json` in a parameter, such as
|
|
981
|
+
`text/plain; x=application/json`, is no longer parsed as JSON; browsers send that type cross-origin
|
|
982
|
+
without a preflight. Server functions apply the same comparison and answer such requests with 415.
|
|
983
|
+
|
|
984
|
+
- ef28ef9: The route manifest, generated route types, build plan, parity report, secret scan and zone graph list their entries in code-unit order, the same on every machine and in every locale.
|
|
985
|
+
- 216fe27: fix(web): a redirect during a client navigation or form post lands on its target without a reload.
|
|
986
|
+
|
|
987
|
+
A loader, gate or middleware that answered a client navigation with `redirect()` had the redirect
|
|
988
|
+
followed by `fetch`, so the router rendered the target's data under the route it was navigating to, at
|
|
989
|
+
that route's URL. A data request now answers a redirect with a `204` carrying `x-nifra-redirect` and the
|
|
990
|
+
redirect's own headers, `Set-Cookie` included, and a same-origin target travels as a path. The client
|
|
991
|
+
router loads that target in place: the address bar shows it, replacing the entry a navigation added or
|
|
992
|
+
adding one after a form post, and `pendingPath` moves to it while it loads. A redirect to another origin
|
|
993
|
+
or to a `#fragment` loads as a document and replaces the entry it answered; a target whose scheme is not
|
|
994
|
+
`http:` or `https:` is never loaded, and the page navigated to loads as a document. A form whose action has run
|
|
995
|
+
is never posted a second time: when loading the page that shows the result fails, that page loads as a
|
|
996
|
+
document instead.
|
|
997
|
+
|
|
998
|
+
- 08250bf: `nifra build` no longer fails its development/production css parity check when a tool such as `wrangler pages dev`, Vercel or SvelteKit has left bundles in a dot-directory (`.wrangler`, `.vercel`, `.svelte-kit`) inside the app.
|
|
999
|
+
- 8fa902c: The private-env check reads every Svelte and Vue script block however its closing tag is spaced (`</script >`), CSP hashing finds the hydration head's inline scripts the same way, and development parity ends an SFC comment at `--!>` as a browser does.
|
|
1000
|
+
- 0dac7ec: Checking a Svelte or Vue file for private environment reads, and the development parity check, take time in proportion to the file's length when an HTML comment or a `{{` interpolation is left open.
|
|
1001
|
+
- b64c3ee: fix(web): the identity preflight behind `nifra check`, `nifra doctor` and the build now scans every
|
|
1002
|
+
package declared in `"nifra": { "singleCopy": [...] }`, not only the built-in identity-sensitive set.
|
|
1003
|
+
A declared package installed at two versions is a fatal `version-skew` finding with the same
|
|
1004
|
+
remediation as a framework skew (align the ranges; nifra never redirects across versions), and one
|
|
1005
|
+
version at two paths is reported as deduplicated.
|
|
1006
|
+
|
|
1007
|
+
feat(core): `@nifrajs/core/single-copy/register` no longer skips silently. Each declared package it
|
|
1008
|
+
cannot collapse - a version skew, or a linked file with no counterpart in the app's copy - prints one
|
|
1009
|
+
warning per process naming both copies, both versions and the reason. Strict mode turns that into a
|
|
1010
|
+
startup failure: declare `"singleCopy": { "packages": [...], "strict": true }` or set
|
|
1011
|
+
`NIFRA_SINGLE_COPY_STRICT=1`. `SingleCopySkip` gains optional `to`, `fromVersion` and `toVersion`;
|
|
1012
|
+
`SingleCopyOptions` gains `strict` and `onSkip`; new exports `readSingleCopyStrict` and
|
|
1013
|
+
`SINGLE_COPY_STRICT_ENV`.
|
|
1014
|
+
|
|
1015
|
+
- dc2d4d3: fix(cli): duplicate-install findings name a copy reached through a symlink out of its install
|
|
1016
|
+
|
|
1017
|
+
When an importer reaches a copy of an identity-sensitive package through a symlink that points
|
|
1018
|
+
outside its own install (another project's `node_modules` linked into a shared package, or a
|
|
1019
|
+
`bun link`), `nifra doctor` prints a `links:` line with each link and its target, and the
|
|
1020
|
+
`nifra check` duplicate-install diagnostic names the link on that copy and lists it ahead of both
|
|
1021
|
+
fixes. The identity preflight carries it as `copies[].links` and `provenance`. Package-manager store
|
|
1022
|
+
links inside an install are not reported.
|
|
1023
|
+
|
|
1024
|
+
- 81c720e: fix(client): a segment that is part literal, part param is callable through the typed client
|
|
1025
|
+
|
|
1026
|
+
A route such as `/files/:name.json`, `/post-:id` or `/v:major.:minor` could not be reached through
|
|
1027
|
+
the typed client: `:name.json` was typed as a param named `name.json` whose call sent the value
|
|
1028
|
+
without `.json`, and `post-:id` was typed as a property that sent the pattern text itself. Such a
|
|
1029
|
+
segment is now a call with the segment as the request carries it:
|
|
1030
|
+
|
|
1031
|
+
```ts
|
|
1032
|
+
await api.files("report.json").get(); // GET /files/:name.json, c.params.name === "report"
|
|
1033
|
+
await api("post-42").get(); // GET /post-:id
|
|
1034
|
+
await api("v1.2").get(); // GET /v:major.:minor
|
|
1035
|
+
```
|
|
1036
|
+
|
|
1037
|
+
- The argument is typed as the segment's literal text around any string (`` `${string}.json` ``), so
|
|
1038
|
+
`api.files("report.txt")` does not compile. A constraint is not part of the text:
|
|
1039
|
+
`/img/:id{[0-9]+}.png` takes `` `${string}.png` ``, and the server decides whether the value fits.
|
|
1040
|
+
- The value is sent as one encoded segment, as a param value is: a `/` in it never adds a path level.
|
|
1041
|
+
- A static segment at the same position keeps its exact text (`api.files("index.json")` is
|
|
1042
|
+
`/files/index.json` when that route exists, which is the route the server picks), and a
|
|
1043
|
+
whole-segment param keeps its call by name (`api.files({ id })`).
|
|
1044
|
+
- Two such segments at one position that accept the same text resolve to the types of the one
|
|
1045
|
+
registered first.
|
|
1046
|
+
- `@nifrajs/core` exports `RequestPath<Path>`, the same reading for a whole path:
|
|
1047
|
+
`RequestPath<"/files/:name.json">` is `` `/files/${string}.json` ``, a constraint reads as
|
|
1048
|
+
`${string}`, and a path ending in optional params is one template per form.
|
|
1049
|
+
- The cli's route listings and the generated `llms.txt` print the call the same way
|
|
1050
|
+
(`` api.files(`${name}.json`) ``), and print an unnamed wildcard as `({ "*": rest })`.
|
|
1051
|
+
|
|
1052
|
+
- 0150ed4: fix: Vite dev keeps a page's response headers, and a CSP page still boots
|
|
1053
|
+
|
|
1054
|
+
Under `nifra dev --vite` an HTML page reached the browser with only a `content-type`: the cookies a
|
|
1055
|
+
loader set, the Content-Security-Policy, `cache-control`, `vary` and the `x-nifra-*` headers were
|
|
1056
|
+
dropped, where the Bun dev pipeline kept them. They are kept now. On a page with a CSP, the tags
|
|
1057
|
+
Vite adds (its HMR client, a framework's refresh preamble, the `<style>` it injects for each imported
|
|
1058
|
+
stylesheet) carry the page's nonce - or a fresh one on a hash-based page - and each directive that
|
|
1059
|
+
governs them names it, unless that directive already allows inline. Dev only; production responses
|
|
1060
|
+
are unchanged.
|
|
1061
|
+
|
|
1062
|
+
- ea2ee87: The Bun dev server refuses a backend file of any type that browser code imports, such as a `.sql` or `.pem` file imported `with { type: "text" }`, as the production build does.
|
|
1063
|
+
- 85d636b: In a client build, a bare `process.env` (one not followed by a baked `PUBLIC_*` name or `NODE_ENV`) is now an empty object. It was the string `"({})"`, so `"X" in process.env` threw a `TypeError` in the browser and `Object.keys(process.env)` listed characters. Reading any other variable still yields `undefined`.
|
|
1064
|
+
- bfe29b6: The CSS Modules transform scopes the keyframe name in a vendor-prefixed `-webkit-animation` / `-webkit-animation-name` declaration and in a declaration that follows a comment (`/* slow */ animation: spin 3s`). Before, both kept the unscoped name and the animation silently did not run.
|
|
1065
|
+
- 87783f0: On the Bun pipeline's dev server, an edit to a module imported through a path alias (`@/components/Button`, `~/lib/x`, `#internal/x`) is picked up by server rendering. Before, only relative imports were re-evaluated, so SSR kept rendering the aliased module as it was when the server started. An alias shaped like a package name (`@app/x`, `src/x`) is still not recognized.
|
|
1066
|
+
- 135aba4: A page, action or data request rendered in draft mode answers `cache-control: private, no-store` and advertises no ISR freshness, whatever `cache-control` its loader set, so neither `withISR` (with or without its own `draftSecret`) nor a URL-keyed CDN stores unpublished content for the next visitor.
|
|
1067
|
+
- 47b0d65: fix(web): a hydrating page with an empty `clientEntry` throws instead of rendering `<script type="module" src="">`.
|
|
1068
|
+
An empty `src` resolves to the page's own URL, so the browser would load the document as a module.
|
|
1069
|
+
Rendering a hydrating page with an empty or missing entry now throws a `TypeError` naming the fix,
|
|
1070
|
+
from `renderPage` and from `createWebApp` pages alike. A `hydrate: false` page never references the
|
|
1071
|
+
entry and still renders without one.
|
|
1072
|
+
- e32268d: fix(web): portable public-directory serving and required endpoint secrets
|
|
1073
|
+
|
|
1074
|
+
`publicDir()` now serves files on Node and Deno as well as Bun, sets `content-type` from the file
|
|
1075
|
+
extension, sends `x-content-type-options: nosniff`, and never serves dot-prefixed paths other than
|
|
1076
|
+
`/.well-known/`.
|
|
1077
|
+
|
|
1078
|
+
`revalidateEndpoint()` and `previewEndpoint()` throw at construction when `secret` is empty or
|
|
1079
|
+
missing. The ISR starter answers 404 on its revalidate route until `REVALIDATE_SECRET` is set.
|
|
1080
|
+
|
|
1081
|
+
- 432fec3: In production a server-rendered `_error` page receives `{ name: "Error", message: "Internal Server Error" }` instead of the thrown error's own name and text, which can carry a connection string or a query. Development still shows the real error.
|
|
1082
|
+
- 8f1b780: A generated `server-manifest.ts` counts as framework output wherever the build writes it, so `nifra check` and editor diagnostics no longer report its route imports when it sits under `backend/`.
|
|
1083
|
+
- dcc9ff6: The generated client entry reads page state only from the server's own `<script type="application/json">` handover (the last one in the document), and sets only the page-state globals it names. Page content that carries an element with the same `id` - sanitized user HTML, say - no longer reaches `window`.
|
|
1084
|
+
- c49882f: `unsafeInlineScript()` code is emitted exactly as written, so a script with a comparison (`if (innerWidth < 600)`) runs instead of failing to parse, and server rendering matches what a soft navigation applies. Code containing `</script` or `<!--` (any case) is refused with a `TypeError`, both when the descriptor is built and when the head renders. Write `<\/script` inside a string literal instead. Inert `head.script` content (JSON-LD) is escaped as before.
|
|
1085
|
+
- 28e091f: perf(web): a page with no layouts, and a layout loader that returns a plain value, render without
|
|
1086
|
+
extra promise turns. A layout loader may still return a promise, a thenable, or a promise from
|
|
1087
|
+
another realm; each is awaited exactly as before, and a loader that throws synchronously fails the
|
|
1088
|
+
page the same way a rejected one does.
|
|
1089
|
+
- 046e79d: `/llms.txt` and `/llms-full.txt` list backend routes only when the backend is served over HTTP at `apiPrefix`; a backend the loaders call in-process (`apiPrefix: ""`) is not described. An app route at either path takes precedence instead of failing to boot, each text is built once per app, and the new `llmsTxt: false` option registers neither path.
|
|
1090
|
+
- 293d3c8: A `Date` a loader, action or server function returns passes its output schema as the ISO string the browser receives, so a timestamp column is declared `t.string()`. The page also renders on the server with that string, the same value it hydrates with.
|
|
1091
|
+
- 0cd5f6e: A large `prerenderedPaths` set no longer rides in every page. Over 4 KB of JSON, `createWebApp` serves the set once from `/__nifra/prerendered.json?v=<version>` (cached for good at that versioned URL) and pages hand over only the URL; the client router fetches it once, on load, and uses it as before. `prerenderRoutes` writes the file into a static output whenever its pages reference it. Smaller sets are still inlined, unchanged.
|
|
1092
|
+
- 2245bee: The development/production parity check lists a public file by the same percent-encoded URL the build records for it. A build with a public file whose name holds a space or a non-ASCII character (`My Logo.png`, `café.txt`) no longer fails parity.
|
|
1093
|
+
- feeec4a: fix(web): a client navigation that changes the query re-runs the layout loaders. A layout loader was
|
|
1094
|
+
kept whenever the params its layout owns were unchanged, so one that reads `ctx.search` or the request
|
|
1095
|
+
URL went on showing the previous query's data. Keys that no loader should see belong in the route's
|
|
1096
|
+
`searchClientKeys`, which still skips the request entirely.
|
|
1097
|
+
|
|
1098
|
+
After an action that redirects, every layout loader of the target runs, the same as the revalidation
|
|
1099
|
+
after an action that does not redirect. `submit(action, body, { revalidate: false })` keeps the layout
|
|
1100
|
+
data an ordinary navigation would.
|
|
1101
|
+
|
|
1102
|
+
- 3eb6339: The build's credential scan stays fast on a long unbroken run of letters and digits, such as an inlined base64 asset, instead of slowing the build to minutes.
|
|
1103
|
+
- f784c32: fix(web): a field named `apiToken` counts as sensitive
|
|
1104
|
+
|
|
1105
|
+
`isSensitiveFieldName` matches `apiToken` in any casing or separator (`api_token`, `API-TOKEN`) and
|
|
1106
|
+
any name ending in it (`githubApiToken`), like `apiKey` and `accessToken`. An output schema that
|
|
1107
|
+
declares one fails the route when it loads unless the field is wrapped in `t.declassified`,
|
|
1108
|
+
`nifra check` reports it, and the build's secret scan treats a literal assigned to one as a
|
|
1109
|
+
credential.
|
|
1110
|
+
|
|
1111
|
+
- e3b2b97: fix(web): the server build fails when `clientEntry` is missing.
|
|
1112
|
+
`buildServer`, `buildServerVite`, and `generateServerManifest` throw a `TypeError` when `clientEntry`
|
|
1113
|
+
is not a string - for example an option spelled `client` - instead of baking `clientEntry = undefined`
|
|
1114
|
+
into the server manifest and leaving every hydrating page to fail at request time. An empty string
|
|
1115
|
+
is still accepted for an app with no client script.
|
|
1116
|
+
- 5934b5d: A generated Bun, Node or Deno server serves `public/` files and the client bundle with their content type, `x-content-type-options: nosniff` and a cache policy: immutable for the hashed `/assets/`, one day for the rest. A `robots.txt` or an `.svg` no longer arrives as a download, and the bundle no longer downloads again on every visit.
|
|
1117
|
+
- 03a3729: fix: a generated `server-manifest.ts` type-checks under a strict tsconfig
|
|
1118
|
+
|
|
1119
|
+
The manifest `generateServerManifest` and `nifra sync-manifest` write for a hand-written server entry
|
|
1120
|
+
now compiles under `strict` with `noUncheckedIndexedAccess`, including routes with a backend half or
|
|
1121
|
+
a typed `meta`. Its route table is typed as plain modules and handed to `buildManifest` as route
|
|
1122
|
+
modules, the same way `discoverRoutes` loads them.
|
|
1123
|
+
|
|
1124
|
+
- 579d9a9: A static boundary's `load` receives no `origin` when it runs during a request: its value is cached for every visitor, so it no longer takes the first request's `Host` header. A static load that fails is loaded again on the next request instead of serving the error until restart. `StaticBoundaryCache` gains an optional `delete`, which `MemoryStaticBoundaryCache` implements.
|
|
1125
|
+
- 3554ad8: fix(web): a page the server rendered as a status page (`_404`, `_410`, ...) hydrates as that page,
|
|
1126
|
+
even when the URL also matches a route pattern, so a loader's `notFound()` stays a 404 in the browser.
|
|
1127
|
+
Routable `_`-prefixed directories such as `routes/_admin/` still hydrate as themselves.
|
|
1128
|
+
|
|
1129
|
+
fix(web): build-vs-dev manifest parity passes for apps with `_`-prefixed routes. Both sides order
|
|
1130
|
+
route ids with the exported `compareRouteIds`, and a module-graph mismatch names what differs: the
|
|
1131
|
+
routes on one side only, each route's chunk counts, or the route order.
|
|
1132
|
+
|
|
1133
|
+
- ee19d29: fix(web): a Svelte app built with `buildClient` hydrates. `svelteDedupePlugin` pinned the bare
|
|
1134
|
+
`svelte` import to the entry the build process itself runs - Svelte's server runtime - so the
|
|
1135
|
+
browser bundle's `hydrate` threw and the server-rendered page never became interactive. For a
|
|
1136
|
+
browser bundle it now pins the app's one copy and reads that copy's export map with the bundle's own
|
|
1137
|
+
conditions, which selects the client runtime. A server bundle resolves as before.
|
|
1138
|
+
- b94e5cb: A loader, action or boundary loader that throws a 2xx `Response` is refused exactly as returning one is: its body would reach the browser without passing the output schema. A thrown redirect or error status still answers the request.
|
|
1139
|
+
- a84f546: fix(web): every pipeline names backend code that reaches the browser, however it is imported
|
|
1140
|
+
|
|
1141
|
+
- A side-effect import of backend code (`import "../backend/x.ts"`) fails the Bun client build with
|
|
1142
|
+
the module and the import chain that reached it, even when the bundler drops the import.
|
|
1143
|
+
- A module marked backend-only fails the Vite client build even when it was inlined into no chunk.
|
|
1144
|
+
- The server build sees imports of modules the bundler later dropped.
|
|
1145
|
+
- The Vite dev server classifies what an alias, a tsconfig path or a package export resolves to, and
|
|
1146
|
+
names the importing file in the error.
|
|
1147
|
+
- `createViteDevServer(...).stop()` no longer hangs when it is called while Vite is still
|
|
1148
|
+
pre-bundling dependencies for the first time.
|
|
1149
|
+
|
|
1150
|
+
- 669b6a2: On Windows, the development guards and the build name files with `/` relative to the app, a Vite dev refusal shows its reason instead of a generic transform error, a pre-bundled dependency is judged by the source Vite built it from, and browser source maps resolve modules Vite serves from outside the root (`/@fs/`). A message about an emitted file names it relative to the app, and a `routes/` directory created after the dev server started holds routes whatever spelling the app root was given in.
|
|
1151
|
+
- Updated dependencies [dde125b]
|
|
1152
|
+
- Updated dependencies [72b62fa]
|
|
1153
|
+
- Updated dependencies [aa44e93]
|
|
1154
|
+
- Updated dependencies [4a3ee60]
|
|
1155
|
+
- Updated dependencies [aad6297]
|
|
1156
|
+
- Updated dependencies [dad0d41]
|
|
1157
|
+
- Updated dependencies [538adc2]
|
|
1158
|
+
- Updated dependencies [f47edd1]
|
|
1159
|
+
- Updated dependencies [df9530a]
|
|
1160
|
+
- Updated dependencies [3e6973f]
|
|
1161
|
+
- Updated dependencies [25e8edf]
|
|
1162
|
+
- Updated dependencies [2b5e5fc]
|
|
1163
|
+
- Updated dependencies [3b090de]
|
|
1164
|
+
- Updated dependencies [da7d792]
|
|
1165
|
+
- Updated dependencies [612a296]
|
|
1166
|
+
- Updated dependencies [fb14dfa]
|
|
1167
|
+
- Updated dependencies [8ae97f6]
|
|
1168
|
+
- Updated dependencies [4af6f39]
|
|
1169
|
+
- Updated dependencies [ca8b50d]
|
|
1170
|
+
- Updated dependencies [b00a889]
|
|
1171
|
+
- Updated dependencies [b53d64f]
|
|
1172
|
+
- Updated dependencies [66fd712]
|
|
1173
|
+
- Updated dependencies [9c3d524]
|
|
1174
|
+
- Updated dependencies [738e7a1]
|
|
1175
|
+
- Updated dependencies [4801cac]
|
|
1176
|
+
- Updated dependencies [1b2d53a]
|
|
1177
|
+
- Updated dependencies [25fe13d]
|
|
1178
|
+
- Updated dependencies [0852290]
|
|
1179
|
+
- Updated dependencies [0589dbe]
|
|
1180
|
+
- Updated dependencies [2e2d8c0]
|
|
1181
|
+
- Updated dependencies [856f5ce]
|
|
1182
|
+
- Updated dependencies [18aa5aa]
|
|
1183
|
+
- Updated dependencies [cfd86b3]
|
|
1184
|
+
- Updated dependencies [8ff96c9]
|
|
1185
|
+
- Updated dependencies [4c46199]
|
|
1186
|
+
- Updated dependencies [eef4932]
|
|
1187
|
+
- Updated dependencies [6de8686]
|
|
1188
|
+
- Updated dependencies [d7892ea]
|
|
1189
|
+
- Updated dependencies [4936309]
|
|
1190
|
+
- Updated dependencies [ff5a779]
|
|
1191
|
+
- Updated dependencies [bbdc5a1]
|
|
1192
|
+
- Updated dependencies [10bc446]
|
|
1193
|
+
- Updated dependencies [e8270d9]
|
|
1194
|
+
- Updated dependencies [ff4a062]
|
|
1195
|
+
- Updated dependencies [43ba944]
|
|
1196
|
+
- Updated dependencies [46c741a]
|
|
1197
|
+
- Updated dependencies [7bfa25e]
|
|
1198
|
+
- Updated dependencies [4936309]
|
|
1199
|
+
- Updated dependencies [6e257a6]
|
|
1200
|
+
- Updated dependencies [4a03d30]
|
|
1201
|
+
- Updated dependencies [8e30090]
|
|
1202
|
+
- Updated dependencies [28f3aaf]
|
|
1203
|
+
- Updated dependencies [6d20355]
|
|
1204
|
+
- Updated dependencies [6907cbe]
|
|
1205
|
+
- Updated dependencies [b64c3ee]
|
|
1206
|
+
- Updated dependencies [bda9637]
|
|
1207
|
+
- Updated dependencies [81c720e]
|
|
1208
|
+
- Updated dependencies [ed60b23]
|
|
1209
|
+
- Updated dependencies [a158b74]
|
|
1210
|
+
- Updated dependencies [ff25d68]
|
|
1211
|
+
- @nifrajs/core@4.0.0
|
|
1212
|
+
- @nifrajs/island-trigger@4.0.0
|
|
1213
|
+
|
|
1214
|
+
## 3.5.0
|
|
1215
|
+
|
|
1216
|
+
### Minor Changes
|
|
1217
|
+
|
|
1218
|
+
- ac27343: Add a per-request `nonce` resolver to `createWebApp`. Nonces flow through framework-owned document scripts, status pages, error pages, and 404s; nonce-bearing responses are marked `private, no-store` so request-specific CSP values are not replayed from caches.
|
|
1219
|
+
|
|
1220
|
+
### Patch Changes
|
|
1221
|
+
|
|
1222
|
+
- 6046984: Close fresh security and correctness gaps in table allowlists, ISR/cache behavior, streamed response capture, idempotency ownership, and canonical redirects.
|
|
1223
|
+
- Updated dependencies [6046984]
|
|
1224
|
+
- Updated dependencies [d5b7c22]
|
|
1225
|
+
- @nifrajs/core@3.5.0
|
|
1226
|
+
- @nifrajs/island-trigger@3.5.0
|
|
1227
|
+
|
|
1228
|
+
## 3.4.0
|
|
1229
|
+
|
|
1230
|
+
### Minor Changes
|
|
1231
|
+
|
|
1232
|
+
- Harden MCP error responses and browser-origin defaults, confine scaffold writes to `routes/`, and
|
|
1233
|
+
make sanitized HTML require an explicit sanitizer function.
|
|
1234
|
+
|
|
1235
|
+
### Patch Changes
|
|
1236
|
+
|
|
1237
|
+
- 8d23613: Add the opt-in `@nifrajs/webmcp` package: typed WebMCP registration, core-backed receipts, deterministic predictive-UI reconciliation, and host-independent conformance checks. Also tighten agent execution cancellation cleanup so aborted local work cannot leak into later turns.
|
|
1238
|
+
- 8d23613: Add an opt-in Vite aggregate-CSS and deferred stylesheet-loading path that prevents lazy-route prefetch from attaching additional stylesheets before hydration.
|
|
1239
|
+
- Updated dependencies [719d82e]
|
|
1240
|
+
- @nifrajs/core@3.4.0
|
|
1241
|
+
- @nifrajs/island-trigger@3.4.0
|
|
1242
|
+
|
|
1243
|
+
## 3.3.0
|
|
1244
|
+
|
|
1245
|
+
### Patch Changes
|
|
1246
|
+
|
|
1247
|
+
- @nifrajs/core@3.3.0
|
|
1248
|
+
- @nifrajs/island-trigger@3.3.0
|
|
1249
|
+
|
|
1250
|
+
## 3.2.0
|
|
1251
|
+
|
|
1252
|
+
### Minor Changes
|
|
1253
|
+
|
|
1254
|
+
- 652201a: Make islands the first-class interactivity lane for zero-runtime vanilla pages. `@nifrajs/web/islands`
|
|
1255
|
+
now exports `defineIsland` to type an enhancer's props and `createIslandBus` for typed pub/sub between
|
|
1256
|
+
islands that share no state. `nifra check` gains `NF-C020`, warning when an island enhancer wires an
|
|
1257
|
+
event listener but returns no cleanup, and `nifra scaffold` emits a golden vanilla route stub. New
|
|
1258
|
+
"Islands" cookbook documents the counter, cart-badge, and filter patterns.
|
|
1259
|
+
- 25305bb: Add `@nifrajs/web/nano`, the explicit-reactivity lane for small apps that want local state without a
|
|
1260
|
+
framework runtime. It exports `signal`, `computed(fn, [deps])` with an explicit dependency array,
|
|
1261
|
+
`resource(fetcher, [deps])` - an async cell with a `pending`/`error`/`ready` value union that aborts a
|
|
1262
|
+
superseded fetch and drops its stale result - and `bind` / `bindList` / `bindResource` DOM edges, all
|
|
1263
|
+
with no virtual DOM and no auto-tracking. Because every reactive edge is a visible call, `nifra check`
|
|
1264
|
+
gains three static lints: `NF-C021` (a `bind`/`bindList`/`bindResource` whose disposer is discarded),
|
|
1265
|
+
`NF-C022` (a `bindList` keyed by the array index), and `NF-C023` (a `computed`/`resource` that reads a
|
|
1266
|
+
signal its deps omit). `nifra scaffold` accepts `variant: "stateful"` to emit the golden nano island
|
|
1267
|
+
pattern on a vanilla project, and a new "nano" cookbook documents signals, keyed lists, async state,
|
|
1268
|
+
and where the lane stops.
|
|
1269
|
+
|
|
1270
|
+
### Patch Changes
|
|
1271
|
+
|
|
1272
|
+
- 3aefb12: Make the development/production stylesheet parity guard ignore generated output, comments, and documentation code examples while continuing to detect real stylesheet imports.
|
|
1273
|
+
- c4ed8f7: Avoid quadratic regular-expression backtracking when rendering build instructions for separator-heavy output paths.
|
|
1274
|
+
- c39712e: Move route policy compilation and deploy-target planning behind shared internal seams, pin the Solid adapter's Babel type identity, and add a cleanup-safe cross-runtime contract-lab runner.
|
|
1275
|
+
- 7551709: Harden runtime boundaries and defaults: clean up subprocess abort listeners, support short Cloudflare
|
|
1276
|
+
KV sessions, bound and incrementally sweep the default memory cache, make image reads and cancellation
|
|
1277
|
+
safe, emit content-derived image validators, require trusted forwarded hosts, avoid caching dynamic SSR
|
|
1278
|
+
metadata, and reject invalid upload or image limits.
|
|
1279
|
+
- Updated dependencies [8b58d1f]
|
|
1280
|
+
- Updated dependencies [095c320]
|
|
1281
|
+
- Updated dependencies [7504864]
|
|
1282
|
+
- Updated dependencies [e88c23a]
|
|
1283
|
+
- Updated dependencies [c39712e]
|
|
1284
|
+
- Updated dependencies [9010fd3]
|
|
1285
|
+
- Updated dependencies [ea2356e]
|
|
1286
|
+
- Updated dependencies [a816b87]
|
|
1287
|
+
- @nifrajs/core@3.2.0
|
|
1288
|
+
- @nifrajs/island-trigger@3.2.0
|
|
1289
|
+
|
|
1290
|
+
## 3.1.0
|
|
1291
|
+
|
|
1292
|
+
### Patch Changes
|
|
1293
|
+
|
|
1294
|
+
- 8b136ee: Framework bindings now share consistent query and fetcher idle behavior while keeping mounted-router state isolated per adapter.
|
|
1295
|
+
- a7db515: Frontend framework deduplication is now applied consistently across the Bun build, the Vite build, and the Vite dev server, so React, Preact, and Svelte each load a single copy in every client bundling path.
|
|
1296
|
+
- Updated dependencies [5b78473]
|
|
1297
|
+
- Updated dependencies [1400f6c]
|
|
1298
|
+
- Updated dependencies [a7db515]
|
|
1299
|
+
- @nifrajs/core@3.1.0
|
|
1300
|
+
- @nifrajs/island-trigger@3.1.0
|
|
1301
|
+
|
|
1302
|
+
## 3.0.0
|
|
1303
|
+
|
|
1304
|
+
### Major Changes
|
|
1305
|
+
|
|
1306
|
+
- 004deee: `redirect()` returns a plain render instead of a `Response`.
|
|
1307
|
+
|
|
1308
|
+
A redirect is a status line and one header - the most body-less response there is - and building a Web `Response` for it costs the whole object, plus a stream drained back out on Node. It is now the same plain-data value `status(...)` produces, rendered on the lane an ordinary return takes: same bytes on the wire, now with a `content-length` on Node rather than a chunked empty body.
|
|
1309
|
+
|
|
1310
|
+
`redirect(...)` is still returned or thrown from exactly the same places - loader, action, layout gate - and `return redirect()` / `throw redirect()` stay interchangeable, including the client-submit conversion to a 204 + `X-Nifra-Redirect`.
|
|
1311
|
+
|
|
1312
|
+
**Breaking:** the returned value is no longer a `Response`, so `.status`, `.headers`, and `instanceof Response` are gone from it.
|
|
1313
|
+
|
|
1314
|
+
- Reading it: `redirect("/x").plain` is `{ status, headers, body }`. `toResponse()` builds the `Response` if something genuinely needs one.
|
|
1315
|
+
- Adding headers: pass them - `redirect("/x", { headers: { "cache-control": "no-store" } })`. Cookies are unaffected; they still ride `c.set` and apply to a redirect exactly as before.
|
|
1316
|
+
- Testing it: assert on `.plain` (or `toResponse()`), not on `.status`.
|
|
1317
|
+
|
|
1318
|
+
The Node writer was framing a body-less response as chunked: `writeHead` followed by a bare `end()` leaves Node to pick the framing, so the shortest response the framework emits went out with a chunk terminator and no length, where every Web-native runtime sends `content-length: 0`. It now declares the zero length on both lanes - a plain render, and a hand-rolled `Response` whose body is `null`.
|
|
1319
|
+
|
|
1320
|
+
A `Response` built from bytes - `new Response("hi")`, a `Uint8Array`, a `Blob` - now declares its length too. It hands those bytes over as a stream, exactly as a live producer does, so the writer could not tell the two apart and framed both as chunked. It now reads one chunk and gives the stream a microtask to say it is done: a source-backed body has already enqueued everything and closes inside it, a producer still generating does not. At most one chunk is held, never the whole body, so a large or endless stream is unaffected - and the microtask costs a streaming response no bytes on the wire, since Node does not flush the header until the first write either way.
|
|
1321
|
+
|
|
1322
|
+
Excluded throughout: HEAD, whose length describes the GET's body that neither lane knows; a status that cannot carry a body; and a length the caller set for itself. A relayed upstream body is also left alone - its length is the upstream's business.
|
|
1323
|
+
|
|
1324
|
+
A hand-rolled `Response` from a loader or action is untouched - still passed through verbatim, still converted the same way on a data request. Only what `redirect()` itself returns changed.
|
|
1325
|
+
|
|
1326
|
+
### Patch Changes
|
|
1327
|
+
|
|
1328
|
+
- 6e43c15: A failed boundary load no longer publishes the thrown error's own message. Boundary states are serialized into the document, so a driver or fetch failure was putting hosts, credentials, and query text on the page for every visitor who loaded it while the dependency was down. The client slot is now always `Boundary failed`, an `Error` subclass name is withheld for the same reason it names the failing internal library, and the real error is reported to the server console - the split already used for a rejected deferred value. A boundary that wants to show the user something specific catches its own failure inside `load` and returns that as data.
|
|
1329
|
+
|
|
1330
|
+
Opt-in outbound WebSocket validation (`validateSend`) contains a rejected diagnostic instead of recursing. An `error()` handler that answers a dropped frame by sending one of its own re-entered the reporter through the two paths that bypassed its guard - schema rejection and an async validator - and unwound only when the stack was exhausted, silently. Every failure path now reports through the guard.
|
|
1331
|
+
|
|
1332
|
+
A content index cursor is checked for the query that issued it separately from running off the end: a cursor from a different sort or filter is rejected at any position, and a matching cursor left past the last row by an index that shrank between pages returns an exhausted page rather than throwing.
|
|
1333
|
+
|
|
1334
|
+
- 293a7fe: Identity-parity findings now state the install topology, and `nifra doctor` and the build guard now answer on the same basis.
|
|
1335
|
+
|
|
1336
|
+
Both tools already shared one walker, but they anchored it differently: doctor scanned the workspace that governs the project while the build guard scanned the app directory it was invoked in, so a duplicate that lives in a sibling workspace package could show up in one output and not the other. Since neither printed which directory it had scanned, that read as two tools contradicting each other about the same invariant. The scan is now always anchored on the governing workspace, both tools name the root they answered on, and a scan that stopped at the workspace-enumeration cap reports itself as partial instead of returning "no duplicates".
|
|
1337
|
+
|
|
1338
|
+
A finding whose copies lie outside the directory the command was run in now says why it is still fatal there: the gate is workspace-wide deliberately, because a copy reached through a workspace-linked dependency is not visible from the app directory - the exact case that once had the check report "none" against an already-broken dev server. Running a build inside one app can therefore fail on a copy held by a sibling app, and the message states that trade rather than leaving it to look like the tool checking the wrong project.
|
|
1339
|
+
|
|
1340
|
+
Each finding also carries a topology line: how many physical paths, how many install roots they fall under, and whether any of those roots sits outside the scanned root. That distinction is the whole fix decision - copies under one workspace collapse with a single reinstall from the root, while a copy under a linked checkout or a standalone sibling install belongs to another project and no reinstall here can remove it. Previously the error listed paths only, leaving that to be reverse-engineered.
|
|
1341
|
+
|
|
1342
|
+
The hard gate now fails closed on a truncated scan. A scan that stopped at any of its caps - workspace packages, linked packages, or link probes - and then found no duplicates has not shown there is none; the duplicate can be sitting in the part it never reached. `assertIdentityParity` treats that state as inconclusive and throws, rather than reading an incomplete scan as a pass. The reporting surfaces (`nifra doctor`, the dev warning) still print the partial result and name the limit that was hit.
|
|
1343
|
+
|
|
1344
|
+
- 485ae60: `nifra build` no longer fails when a route imports a non-JS asset. The development/production manifest parity check compared a source-derived expectation against the build output; its module-graph section held every emitted asset, so any emitted non-JS file (an `import logo from "./logo.svg"`, a font) was a set difference no application could close. The module-graph contract is now the JavaScript module graph only - emitted assets are an output detail, served from source in development and hashed in production, with no dev/prod claim to compare.
|
|
1345
|
+
|
|
1346
|
+
The stylesheet check is now directional instead of an equality. The development-side scanner is sound but incomplete by construction - it cannot see a dynamic `import()`, a `require()`, or a bare package `exports` subpath - so a production stylesheet the scanner missed passes, while the load-bearing direction (development found styles the build does not ship, so the page renders unstyled) still fails. The scanner also now recognizes `import(...)` and `require(...)` of a literal stylesheet path.
|
|
1347
|
+
|
|
1348
|
+
`manifest.css` now means the same thing on both bundlers: the union of every emitted stylesheet, bootstrap aggregate first. The Bun pipeline previously kept only the aggregate, so a route-scoped stylesheet landed in `assets` but not `css` and normalized to a spurious asset difference.
|
|
1349
|
+
|
|
1350
|
+
Parity failures now name the offending files - the production stylesheet URLs and the scanned source root for a css mismatch, the symmetric difference for a module-graph mismatch - and the identity-parity remediation is cause-specific: a version skew says reinstall, a duplicate path across a linked sibling repo says a reinstall will not collapse it and one tree must resolve into the other.
|
|
1351
|
+
|
|
1352
|
+
- 627b0ba: Duplicate-install detection (`NF-C009` / `NF-D001`) no longer sweeps an unrelated sibling repository.
|
|
1353
|
+
|
|
1354
|
+
A dependency symlinked into another project's package store (bun's `node_modules/.bun/<pkg>@<version>`, an `npm link` target, a shared global store) was treated as a linked source checkout, so the scan walked up to that project's `.git` and reported its whole dependency tree as duplicates of the project being checked - findings in a repo the developer is not working in, that no change in their own project could ever clear. A linked root that resolves inside a `node_modules` directory is now clamped to itself: only its own nested `node_modules` is scanned, which is exactly the set of modules it can load. The store copy itself is still reported, because it is genuinely what the project resolves; a real linked source checkout (`link:../../pkg`) still has its whole repo scanned, because its imports really do resolve there.
|
|
1355
|
+
|
|
1356
|
+
- f0fd370: The same-origin check behind `redirect()` and the guards' `redirectTo` now rejects the paths a URL parser resolves onto another origin, and lives in one place.
|
|
1357
|
+
|
|
1358
|
+
A leading `/` that is not `//` is not sufficient to keep a destination on this origin. Under a special scheme a backslash parses as a path separator, and tab, CR and LF are stripped from the input before parsing, so `/\evil.example` and `/<TAB>/evil.example` both pass a `//` test and then resolve to the host `evil.example` - an open redirect reachable from any unvalidated `?next=` parameter. Both forms are now refused: `redirect()` throws as it already did for `//host`, and an auth guard falls back to its configured destination instead of honouring the value.
|
|
1359
|
+
|
|
1360
|
+
New export `isSameOriginPath` from `@nifrajs/core/server`, which is the single implementation the three gates now share - a security predicate kept in three copies is three chances for one of them to be hardened alone. It answers about a path, so an absolute URL is false even when it names the current origin: the point of the gate is that the value never got to name a host at all.
|
|
1361
|
+
|
|
1362
|
+
A percent-encoded backslash (`/%5Cevil.example`) is still a same-origin path, because that is what it resolves to.
|
|
1363
|
+
|
|
1364
|
+
- 36801ae: New: a static `singleCopy` declaration that collapses an identity-sensitive package to one physical copy, for the duplicate no install can remove.
|
|
1365
|
+
|
|
1366
|
+
An app that consumes a package by `link:` from a **separate checkout** cannot deduplicate React (or `@nifrajs/*`) by installing differently. The linked files live in the other repo, so their imports resolve from that repo's real path, and that repo's install owns its `node_modules`: peer dependencies are already satisfied there, `overrides` govern the consuming install only, and a deleted nested copy returns on the sibling's next install. The build's existing per-framework dedupe covers bundled output, but nothing covered `bun test`, `bun run`, or a preloaded script - and `nifra check` failed the app with remediation ("deduplicate the install") that the topology makes impossible.
|
|
1367
|
+
|
|
1368
|
+
An app now declares the packages in its own `package.json`:
|
|
1369
|
+
|
|
1370
|
+
```json
|
|
1371
|
+
{ "nifra": { "singleCopy": ["react", "react-dom", "@nifrajs/*"] } }
|
|
1372
|
+
```
|
|
1373
|
+
|
|
1374
|
+
Entries are exact names or `@scope/*` patterns; `true` expands to the built-in identity-sensitive set (`@nifrajs/*`, `react`, `react-dom`, `preact`, `solid-js`, `svelte`, `vue`). `@nifrajs/*` is in that set because two copies of `@nifrajs/core` are two distinct `Server` classes, so `.merge()` stops accepting an app built against the other one. The declaration is static so `nifra check` can read it without importing the app's config, which would mean executing app code inside a preflight.
|
|
1375
|
+
|
|
1376
|
+
`buildClient` and `buildServer` inject the resolver from the declaration, so bundled output needs no wiring. Unbundled phases are not covered automatically - Bun's runtime resolver never offers a bare specifier to a plugin - so an app preloads `@nifrajs/core/single-copy/register` from `bunfig.toml` (`preload` for `bun run`, `[test].preload` for `bun test`). `nifra check` now names the phase that is left uncovered when the declaration exists without the preload.
|
|
1377
|
+
|
|
1378
|
+
The redirect refuses to cross versions: two copies at different versions are skipped as `version-skew` and stay fatal, because collapsing them would turn a loud install problem into a quiet behavioural one. A declared duplicate is reported, not suppressed - `nifra check` keeps printing the copies as a warning and `nifra doctor` lists them under `deduplicatedInstalls` - so the topology stays visible without failing the gate.
|
|
1379
|
+
|
|
1380
|
+
New exports: `@nifrajs/core/single-copy` (`singleCopyPlugin`, `registerSingleCopy`, `planSingleCopy`, `readSingleCopyDeclaration`, `readSingleCopyRegistration`, `matchesSingleCopyDeclaration`, `IDENTITY_SENSITIVE_PACKAGES`) and the side-effect entry `@nifrajs/core/single-copy/register`.
|
|
1381
|
+
|
|
1382
|
+
The registration proof reads `preload` as entries rather than as text: an entry counts only when it **is** the register specifier, not when it merely contains it. A neighbouring path such as `"./vendor/@nifrajs/core/single-copy/register-shim.ts"` used to satisfy the check while Bun loaded that other module and the registrar never ran, so enforcement was reported as armed on a process still loading both copies. A `preload` this cannot read as quoted entries - a multi-line array, an interpolated value - now reports "not registered" instead of standing in for proof.
|
|
1383
|
+
|
|
1384
|
+
- Updated dependencies [f3d2a35]
|
|
1385
|
+
- Updated dependencies [6e43c15]
|
|
1386
|
+
- Updated dependencies [f0fd370]
|
|
1387
|
+
- Updated dependencies [86a555b]
|
|
1388
|
+
- Updated dependencies [8c5f4cf]
|
|
1389
|
+
- Updated dependencies [f0fd370]
|
|
1390
|
+
- Updated dependencies [381bbf3]
|
|
1391
|
+
- Updated dependencies [36801ae]
|
|
1392
|
+
- Updated dependencies [9acadba]
|
|
1393
|
+
- Updated dependencies [99fc683]
|
|
1394
|
+
- Updated dependencies [73d894d]
|
|
1395
|
+
- @nifrajs/core@3.0.0
|
|
1396
|
+
- @nifrajs/island-trigger@3.0.0
|
|
1397
|
+
|
|
1398
|
+
## 2.14.1
|
|
1399
|
+
|
|
1400
|
+
### Patch Changes
|
|
1401
|
+
|
|
1402
|
+
- Updated dependencies [bf93902]
|
|
1403
|
+
- @nifrajs/core@2.14.1
|
|
1404
|
+
|
|
1405
|
+
## 2.14.0
|
|
1406
|
+
|
|
1407
|
+
### Minor Changes
|
|
1408
|
+
|
|
1409
|
+
- 489c6b6: `nifra dev` gains `--allow-duplicate-identity`, which downgrades the Vite dev server's startup
|
|
1410
|
+
identity-parity check from a hard failure to a loud warning. The check catches two physical copies of
|
|
1411
|
+
an identity-sensitive package (React, the framework adapter, `@nifrajs/core`) resolving in one process,
|
|
1412
|
+
which reliably breaks hydration and framework context - so it stays a hard stop by default, and
|
|
1413
|
+
`nifra build` never honors the flag. But when the duplicate originates in a linked sibling repo you
|
|
1414
|
+
cannot fix in the moment, the previous behavior took the dev server down with no way to keep working.
|
|
1415
|
+
With the flag, the server prints the same finding detail (packages, versions, resolved paths) and a
|
|
1416
|
+
reminder that duplicate identity can still corrupt hydration, then continues at exit 0. The web option
|
|
1417
|
+
`createViteDevServer({ allowDuplicateIdentity: true })` exposes the same escape programmatically.
|
|
1418
|
+
|
|
1419
|
+
### Patch Changes
|
|
1420
|
+
|
|
1421
|
+
- 62e22e2: The generated `server-manifest.ts` now imports each route module by an extensionless specifier, so a
|
|
1422
|
+
plain `tsc` over the project compiles it without `allowImportingTsExtensions`. Previously the manifest
|
|
1423
|
+
emitted `.tsx`/`.ts` import paths that only `nifra check` (which sets that flag) accepted, and a bare
|
|
1424
|
+
`tsc` reported TS5097 for every route. Route identity is unchanged: the manifest map keys still carry
|
|
1425
|
+
the original file path with its extension.
|
|
1426
|
+
|
|
1427
|
+
Manifest drift detection (`nifra check`, `nifra sync-manifest`) now reads those extension-bearing map
|
|
1428
|
+
keys rather than the import specifiers, so it keeps matching the discovered `routes/` tree exactly and
|
|
1429
|
+
does not false-report every route as drifted against the extensionless specifiers.
|
|
1430
|
+
|
|
1431
|
+
- 62e22e2: `nifra sync-manifest` now preserves a lazy manifest's shape instead of silently rewriting it as eager.
|
|
1432
|
+
The re-sync detected the lazy (`() => import(...)` per route) vs eager (`import * as`) form by looking
|
|
1433
|
+
for `const loaders =` in the source, but the generated declaration is `const loaders: Record<...> = {` -
|
|
1434
|
+
the type annotation sits between the name and the `=`, so the check never matched and every lazy
|
|
1435
|
+
manifest came back eager, collapsing per-route code splitting into a single boot-time bundle. Detection
|
|
1436
|
+
now anchors on the `const loaders` declaration itself, so a route-table refresh keeps the app's chunking
|
|
1437
|
+
exactly as committed.
|
|
1438
|
+
- 8dffdf4: `.use(plugin)` no longer silently collapses the app's typed route registry to `any` when a plugin's own
|
|
1439
|
+
types are unpinned. A plugin whose parameter and return infer as `Server<any, any>` - an auth or router
|
|
1440
|
+
plugin that widened - now makes `.use()` return the non-callable `PluginTypeCollapsed` marker at the
|
|
1441
|
+
call site, rather than an `any` that only surfaces hundreds of lines away as `never`/`any` in the typed
|
|
1442
|
+
client. Build the plugin with `defineIdentityPlugin`/`defineContextPlugin`, or pin its input server
|
|
1443
|
+
type, and it threads the caller's registry and context unchanged.
|
|
1444
|
+
|
|
1445
|
+
`serverFunctions()` now ships as such an identity plugin, so `app.use(serverFunctions(...))` keeps every
|
|
1446
|
+
route declared before and after it fully typed.
|
|
1447
|
+
|
|
1448
|
+
- 489c6b6: The Vite dev pipeline no longer serves a stale SSR module after an edit to a file a route imports. Vite
|
|
1449
|
+
re-evaluates a directly-changed module on its own, but a parent that merely imports the changed leaf
|
|
1450
|
+
kept its cached SSR bindings, so the re-created app walked a graph that was fresh at the leaf and stale
|
|
1451
|
+
above it. That surfaced as phantom hydration mismatches (SSR rendered through an old module, the client
|
|
1452
|
+
through the new one) and stale i18n catalogs. On every change the dev server now evicts the changed file
|
|
1453
|
+
together with its transitive importer closure from the SSR graph before rebuilding the app, so the next
|
|
1454
|
+
render re-walks the whole affected subtree. Apps whose only transforms are `vitePlugins` (and are
|
|
1455
|
+
therefore forced onto the Vite pipeline) get correct hot reloads without a manual restart.
|
|
1456
|
+
- Updated dependencies [701961a]
|
|
1457
|
+
- Updated dependencies [62133bf]
|
|
1458
|
+
- Updated dependencies [8dffdf4]
|
|
1459
|
+
- @nifrajs/core@2.14.0
|
|
1460
|
+
|
|
1461
|
+
## 2.13.0
|
|
1462
|
+
|
|
1463
|
+
### Minor Changes
|
|
1464
|
+
|
|
1465
|
+
- 6510fdc: Fail a build or dev-server start when identity-sensitive packages (the framework runtime, React,
|
|
1466
|
+
Preact, Svelte, Solid, Vue) resolve to more than one physical copy, or when the development and
|
|
1467
|
+
production manifests diverge in routes, public files, or styles. Single-file-component `<style>`
|
|
1468
|
+
blocks count as styles, so a scoped-style Svelte or Vue component is not reported as diverging from a
|
|
1469
|
+
production manifest that carries its extracted stylesheet.
|
|
1470
|
+
|
|
1471
|
+
### Patch Changes
|
|
1472
|
+
|
|
1473
|
+
- e0b2dd6: Harden three regex-adjacent input paths against pathological input. The byte-range parser bounds an
|
|
1474
|
+
oversized `Range` header before the matcher runs rather than after; the problem-details type builder
|
|
1475
|
+
strips trailing slashes with a linear scan instead of a backtracking pattern; and the dev SSR
|
|
1476
|
+
import-graph specifier pattern no longer has an ambiguous whitespace group. Behavior is unchanged for
|
|
1477
|
+
valid input.
|
|
1478
|
+
- Updated dependencies [e0b2dd6]
|
|
1479
|
+
- Updated dependencies [7535ce1]
|
|
1480
|
+
- Updated dependencies [1704308]
|
|
1481
|
+
- @nifrajs/core@2.13.0
|
|
1482
|
+
|
|
1483
|
+
## 2.12.1
|
|
1484
|
+
|
|
1485
|
+
### Patch Changes
|
|
1486
|
+
|
|
1487
|
+
- Updated dependencies [fba30c7]
|
|
1488
|
+
- @nifrajs/core@2.12.1
|
|
1489
|
+
|
|
1490
|
+
## 2.12.0
|
|
1491
|
+
|
|
1492
|
+
### Minor Changes
|
|
1493
|
+
|
|
1494
|
+
- 4c2123d: `navigate()` accepts a `state` option: opaque, structured-cloneable per-entry state stored on the
|
|
1495
|
+
history entry it creates, in both the string and object call forms. It is written under
|
|
1496
|
+
`history.state.nifraState` so it can never collide with the router's own bookkeeping keys
|
|
1497
|
+
(`nifraIndex`/`nifraScroll`), read back as `history.state.nifraState`, and restored by the browser on
|
|
1498
|
+
back/forward at no cost. History-delta navigations (`navigate(-1)`) ignore it.
|
|
1499
|
+
- c55f7a3: `/llms.txt` and `/llms-full.txt` no longer publish the project's `AGENTS.md`. Those endpoints are
|
|
1500
|
+
public and unauthenticated, while `AGENTS.md` is a repo file written for the team - unreleased feature
|
|
1501
|
+
names, internal hostnames, and "don't touch X yet" notes live in it routinely, and every app that
|
|
1502
|
+
happened to have one was serving it to anyone who asked. Set `publishLocalGuidelines: true` on
|
|
1503
|
+
`createWebApp` to restore the old behaviour for a repo whose guidelines you would publish as a page.
|
|
1504
|
+
Everything else in both endpoints (routes, pages, client-call examples) is unchanged.
|
|
1505
|
+
|
|
1506
|
+
### Patch Changes
|
|
1507
|
+
|
|
1508
|
+
- fa51aba: Restore the dev-phase environment flags a programmatically started dev server sets when it stops, so a later in-process consumer sharing the process does not read them as if a dev server were still running.
|
|
1509
|
+
- 33ee9ff: `loadGoogleFont` emits a variable weight range in the dotted form the fonts endpoint expects. A
|
|
1510
|
+
range spelled CSS-style (`"100 900"`) was passed through with its literal space, which the endpoint
|
|
1511
|
+
answers with a `400`; both spellings are accepted now and normalized to `"100..900"`. A reversed or
|
|
1512
|
+
degenerate range (min not less than max) throws at the call site instead of producing a request that
|
|
1513
|
+
fails at load time.
|
|
1514
|
+
- 0863ef0: `withISR` no longer serves a cached page to a soft-navigation data request. Cache entries are full
|
|
1515
|
+
HTML documents keyed by URL, so a client-side navigation's loader fetch (`x-nifra-data`) could
|
|
1516
|
+
receive a document where it expects a loader payload. Those GETs now bypass the cache entirely,
|
|
1517
|
+
matching the write path, which already refused to store data-mode responses.
|
|
1518
|
+
- 24f1787: Align Vite production CSS Module scoped names with the Bun pipeline and the Vite development server so switching pipelines preserves the class-name map.
|
|
1519
|
+
- df07059: `redirect()` (and any `Response` control-flow signal) now behaves identically whether a loader or action returns it or throws it. A loader that RETURNS `redirect(...)` passes the response through to the client verbatim instead of serializing the `Response` object as loader data; a returned status signal renders its boundary exactly like a thrown one. An action (or a layout gate on the mutation path) that THROWS `redirect(...)` gets the same treatment as a returned one: a client submit receives the `X-Nifra-Redirect` header on a 204 and navigates, a native form POST receives the 3xx.
|
|
1520
|
+
- a5d3f5b: Add stable diagnostic codes, application-supplied rule packs, fix recipes, assurance bundles, contract lock snapshots, hydration assurance hooks, replay metadata, security verification rules, and idempotency proofs.
|
|
1521
|
+
- 64d25db: Deferred-data reconstruction stores every key with `Object.defineProperty`, on the server walk and in
|
|
1522
|
+
the injected client mapper alike. A `__proto__` key in serialized data previously went through plain
|
|
1523
|
+
assignment, which walks the inherited setter instead of storing data - so the key silently vanished
|
|
1524
|
+
from the reconstructed object, and on the client it reached a prototype setter with attacker-shaped
|
|
1525
|
+
data. The result is still a plain `{}` with `Object.prototype` intact, so `toString`, `hasOwnProperty`
|
|
1526
|
+
and `constructor` keep working on the value the app receives.
|
|
1527
|
+
- Updated dependencies [df100d3]
|
|
1528
|
+
- Updated dependencies [0efacea]
|
|
1529
|
+
- Updated dependencies [cd1732c]
|
|
1530
|
+
- Updated dependencies [df100d3]
|
|
1531
|
+
- Updated dependencies [9a9346e]
|
|
1532
|
+
- Updated dependencies [b5f47c0]
|
|
1533
|
+
- Updated dependencies [fc33c0f]
|
|
1534
|
+
- Updated dependencies [c4e8bb0]
|
|
1535
|
+
- Updated dependencies [11d1658]
|
|
1536
|
+
- Updated dependencies [5f71c23]
|
|
1537
|
+
- Updated dependencies [3788b36]
|
|
1538
|
+
- Updated dependencies [ae5338f]
|
|
1539
|
+
- Updated dependencies [8847825]
|
|
1540
|
+
- Updated dependencies [9a9346e]
|
|
1541
|
+
- Updated dependencies [5e4e31a]
|
|
1542
|
+
- Updated dependencies [9a9346e]
|
|
1543
|
+
- Updated dependencies [b045f9e]
|
|
1544
|
+
- Updated dependencies [9a9346e]
|
|
1545
|
+
- Updated dependencies [9a9346e]
|
|
1546
|
+
- Updated dependencies [dbc0b79]
|
|
1547
|
+
- Updated dependencies [bd5c624]
|
|
1548
|
+
- Updated dependencies [a5d3f5b]
|
|
1549
|
+
- Updated dependencies [00819c5]
|
|
1550
|
+
- Updated dependencies [e2bdd4a]
|
|
1551
|
+
- Updated dependencies [e2d1939]
|
|
1552
|
+
- Updated dependencies [e83e6eb]
|
|
1553
|
+
- Updated dependencies [f8b0097]
|
|
1554
|
+
- @nifrajs/core@2.12.0
|
|
1555
|
+
|
|
1556
|
+
## 2.11.0
|
|
1557
|
+
|
|
1558
|
+
### Minor Changes
|
|
1559
|
+
|
|
1560
|
+
- ed5e91c: Vue, Solid and Svelte hot-patch a component edit in place on the Bun dev pipeline: the page keeps its scroll position, its open dialogs and the rest of its client state, and only the edited component re-renders. Route modules still full-reload on save, since a route carries the loader and meta the server ran. The wiring is emitted for a dev server's client compile only, so nothing reaches a production bundle.
|
|
1561
|
+
|
|
1562
|
+
`@nifrajs/web-svelte/plugin` adds `svelteHmrBoundary`, the same boundary for the Vite pipeline - pass it to `@sveltejs/vite-plugin-svelte` as `dynamicCompileOptions`. Svelte's hot-patch wrapper resolves a component through a signal, which reconciles against server markup only where the component is a plain child in a template; a layout or a page is neither, and wrapping one desyncs hydration on first load. The boundary keeps the wrapper on the app's own views. (Svelte recreates the patched component, so its own `$state` restarts - Vue and Solid preserve theirs.)
|
|
1563
|
+
|
|
1564
|
+
`Router.svelte` now takes `searchOfChain` from `@nifrajs/web/client` rather than the package root, so a client bundle no longer reaches server-only modules through it.
|
|
1565
|
+
|
|
1566
|
+
- 30f5ea3: SSR renders the code that is on disk on the Bun dev pipeline, not just for route files. An edit to anything a route imports - a component, a helper, a `*.server` module, at any depth - now reaches the server-rendered HTML on the next request, so a saved change no longer shows up on the client while the SSR pass still renders the version the server started with. All four frameworks, and both `import "./Counter"` and `import "./Counter.tsx"`.
|
|
1567
|
+
|
|
1568
|
+
A module nobody edited keeps its identity, so a database client or any other module-scope singleton shared between the backend and a route is still a single instance; editing that module deliberately gives the routes the new code. The Vite pipeline is unchanged - it already owned an SSR module graph.
|
|
1569
|
+
|
|
1570
|
+
`@nifrajs/web/plugins/kit` adds `rewriteSsrImports`, which a plugin that compiles its own file type passes its `generate: "ssr"` output through - the compiling plugin is the only code that sees that file's imports, so it is the only place they can be re-keyed.
|
|
1571
|
+
|
|
1572
|
+
- c29e0d0: Svelte runs on the Vite pipeline. `nifra dev` and `nifra build` now serve and build a Svelte app on either bundler, so a Svelte app is no longer the one framework pinned to a single pipeline, and pages render with routing context, typed search and layout data intact on both.
|
|
1573
|
+
|
|
1574
|
+
`@nifrajs/web` adds `setSsrModuleLoader` / `ssrModuleLoader`, the seam that makes it work. A dev server that owns SSR resolution publishes its module loader; a render adapter that has to load a compiled asset on the server reads it and loads through it, so that asset is compiled by the same toolchain as the app's routes and renders through the same copy of the framework runtime. Adapters that ship no compiled assets are unaffected.
|
|
1575
|
+
|
|
1576
|
+
`conditions` on the Bun dev pipeline reaches SSR, and says so when it cannot reach the client bundle Bun's dev server serves - a one-line startup notice instead of a package that quietly resolves to one file in dev and another in `nifra build`.
|
|
1577
|
+
|
|
1578
|
+
CSS Modules class names are now identical on both pipelines. The same class hashes to the same scoped name under `nifra dev`, `nifra dev --bun` and `nifra build`, so a selector written against a generated name behaves the same everywhere.
|
|
1579
|
+
|
|
1580
|
+
The dev-and-HMR guide gains a Gotchas section covering the config/adapter file split, plugin slots, resolve conditions, non-route SSR freshness, and the adapter loader.
|
|
1581
|
+
|
|
1582
|
+
### Patch Changes
|
|
1583
|
+
|
|
1584
|
+
- @nifrajs/core@2.11.0
|
|
1585
|
+
|
|
1586
|
+
## 2.10.0
|
|
1587
|
+
|
|
1588
|
+
### Minor Changes
|
|
1589
|
+
|
|
1590
|
+
- 5263c4e: `KVCacheStore` accepts a `minExpirationTtl`. The 60-second floor is Cloudflare KV's, and it stays the default, but `KVNamespaceLike` is three structural methods that Redis, Deno KV and Upstash satisfy too - and those accept far shorter TTLs. Declare the binding's real minimum, or `0` for a backend without one. A non-integer or negative TTL is now rejected at construction rather than reaching the binding.
|
|
1591
|
+
- 15bffdd: Serve `public/` with byte ranges. Static files now advertise `accept-ranges`, answer a single-range request with `206` and `content-range`, return `416` for an unsatisfiable range, and publish `last-modified` with `if-modified-since` and `if-range` handling. HEAD reports the same `content-type` and length metadata GET does. `parseByteRange` moves to `@nifrajs/core/range` so the static handler and `@nifrajs/middleware`'s `rangeResponse` share one parser; the middleware export is unchanged.
|
|
1592
|
+
- 15bffdd: Add request-bound data capability evidence, resumable bounded channel subscriptions, ISR tag
|
|
1593
|
+
invalidation for memory and KV stores, and dependency-free Open Graph image responses with an
|
|
1594
|
+
optional rasterizer seam.
|
|
1595
|
+
|
|
1596
|
+
### Patch Changes
|
|
1597
|
+
|
|
1598
|
+
- Updated dependencies [15bffdd]
|
|
1599
|
+
- Updated dependencies [15bffdd]
|
|
1600
|
+
- Updated dependencies [15bffdd]
|
|
1601
|
+
- @nifrajs/core@2.10.0
|
|
1602
|
+
|
|
1603
|
+
## 2.9.1
|
|
1604
|
+
|
|
1605
|
+
### Patch Changes
|
|
1606
|
+
|
|
1607
|
+
- Updated dependencies [01e36fb]
|
|
1608
|
+
- @nifrajs/core@2.9.1
|
|
1609
|
+
|
|
1610
|
+
## 2.9.0
|
|
1611
|
+
|
|
1612
|
+
### Patch Changes
|
|
1613
|
+
|
|
1614
|
+
- e05e56d: Improve hot paths across runtimes and the browser: a validated-POST fused lane for Bun/Deno Web
|
|
1615
|
+
requests (measured +12.7% Deno, +3.5% Bun on `POST /users`) plus a registration-compiled body
|
|
1616
|
+
validation/handler continuation shared by Web and Node-direct (about 9.6% faster than the generic
|
|
1617
|
+
body lane in-process), client route matching indexed on the core router instead of a linear scan
|
|
1618
|
+
(measured ~18x faster on a 100-route app), search-param parsing in one pass instead of O(keys²), and
|
|
1619
|
+
allocation-free fast paths for static asset URLs and safe SSR script serialization.
|
|
1620
|
+
Bare fused-lane Web requests with no active timeout or deadline now run on the lazy request
|
|
1621
|
+
context (the one the Node direct path already uses), with the platform - `c.env`, `c.clientIp`,
|
|
1622
|
+
`c.waitUntil` - carried through and `c.signal`/`c.budget`/`c.query` resolving lazily to identical
|
|
1623
|
+
values, pinned by a regression test. Measured +4.5% on a bare `GET /users/:id` on Deno. Routing
|
|
1624
|
+
also stops allocating a `{ pathname, search }` pair per request on the portable path.
|
|
1625
|
+
Node serving now keeps synchronous Web request middleware on the direct renderer, adapts in-place Web
|
|
1626
|
+
response middleware back to direct buffered writes, and avoids redundant params/body lifecycle stages
|
|
1627
|
+
for common validated reads. Header-only built-ins (`cache-control`, `powered-by`, and related response
|
|
1628
|
+
mutators) no longer clone buffered responses on Node.
|
|
1629
|
+
New portable middleware hook: `onResponseBody(body, headers, req, status)` - the post-serialization
|
|
1630
|
+
payload tier. The hook receives the FINAL framework-serialized bytes plus the mutable header view,
|
|
1631
|
+
and may return replacement bytes. On the Node direct writer the bytes come straight off the outcome
|
|
1632
|
+
record; on the Web serving paths they ride the framework-built Response as an inert tag (attached
|
|
1633
|
+
only once a body hook is registered), so no body stream is ever drained on any runtime. A
|
|
1634
|
+
handler-returned raw `Response` (a proxied fetch, SSE, a streamed page) is skipped by contract, a
|
|
1635
|
+
structured return (`{ body, status }`) can drop the body or change the status (an ETag `304`), and
|
|
1636
|
+
transforming those remains `onResponse`'s job. A body-observing middleware written this way
|
|
1637
|
+
measures at ~92% of a raw `node:http` server on the realistic route, vs ~50% through the full
|
|
1638
|
+
`onResponse` contract.
|
|
1639
|
+
New middleware hook: `onResponseRaw(response, req)` - the raw-response tier. It runs ONLY for
|
|
1640
|
+
responses the payload tier skips (streams, proxied fetches, and framework-generated error
|
|
1641
|
+
responses); a framework-serialized JSON body stays on `onResponseBody` and, on Node, on the direct
|
|
1642
|
+
socket writer (the raw hook self-pairs with a no-op native twin, so registering one does not force
|
|
1643
|
+
the fallback path). Together the two tiers cover every response without double-processing any.
|
|
1644
|
+
Response-body tagging is now scoped per app instance instead of process-wide: one app registering a
|
|
1645
|
+
body hook no longer makes unrelated apps in the same process pay for tagging, `merge()` keeps the
|
|
1646
|
+
tag readable across merged apps, and a foreign Response carrying a look-alike marker is not treated
|
|
1647
|
+
as a framework-serialized body.
|
|
1648
|
+
Bodiless statuses are normalized on every render path: a handler returning a `204`/`205`/`304` (or
|
|
1649
|
+
a body hook converting to one, e.g. an ETag `304`) always ships with no body and no
|
|
1650
|
+
`content-length`, on the Web paths and the Node direct writer alike.
|
|
1651
|
+
Response-header records are built null-prototyped everywhere user-influenced names can land in
|
|
1652
|
+
them, so header names like `__proto__` stay data instead of touching the record's prototype; the
|
|
1653
|
+
header view over Node outcomes also resolves names via a one-time per-request index instead of
|
|
1654
|
+
scanning the record on every get/set (measured at roughly a fifth of the framework's own CPU on a
|
|
1655
|
+
realistic middleware-carrying route).
|
|
1656
|
+
Guarded response headers (a raw `fetch()`ed Response) are now detected with a reversible probe
|
|
1657
|
+
before any header hook runs, instead of catching the mutation `TypeError` and re-running the hook
|
|
1658
|
+
against a clone - a hook that itself throws `TypeError` no longer runs twice. Framework-constructed
|
|
1659
|
+
responses stamp their headers as known-mutable at construction, so the hot path answers that
|
|
1660
|
+
question with a single weak-set lookup and only a handler-returned foreign `Response` ever pays the
|
|
1661
|
+
probe, once per headers object.
|
|
1662
|
+
New portable middleware hook: `onResponseHeaders(headers, req, status)` - the recommended shape for
|
|
1663
|
+
response middleware that only reads or writes headers. One implementation runs on every runtime: on
|
|
1664
|
+
the Web serving paths it mutates the response's own `Headers` inside the normal response walk (no
|
|
1665
|
+
clone), and on Node it self-pairs as a native hook against the outcome record, so registering one
|
|
1666
|
+
never forces the Node adapter off its direct socket writer the way a full `onResponse(res:
|
|
1667
|
+
Response)` hook does.
|
|
1668
|
+
Native Node hook lanes now engage as a unit - the response-side native hooks run only when the
|
|
1669
|
+
request side is native too - which makes the native request context's identity stable across a
|
|
1670
|
+
request; the context also carries `url`, and both are documented so middleware twins can key
|
|
1671
|
+
per-request state on it. Building a Web `Request` from a Node request fills its header list once
|
|
1672
|
+
from a plain record instead of copying a prebuilt `Headers` a second time.
|
|
1673
|
+
Query and cookie parsing intern repeated key names on V8-based runtimes (Node, Deno) through a small
|
|
1674
|
+
bounded cache - V8 pays ~13x to store a freshly-sliced string key on the null-prototype records the
|
|
1675
|
+
parsers build, so handing back the first-seen key makes the store take the fast path. High-cardinality
|
|
1676
|
+
or oversized keys bypass the cache and behave exactly as before, and JSC (Bun) skips the scheme
|
|
1677
|
+
entirely (it has no such cost).
|
|
1678
|
+
When any route registers `onResponseBody`, every JSON response with caller-set headers (the common
|
|
1679
|
+
shape once a route has middleware) stopped pre-building a throwaway `Headers` instance just to
|
|
1680
|
+
check for an existing `content-type` - that instance was immediately handed to `new Response()`,
|
|
1681
|
+
which does its own header ingestion regardless, so the pre-build was pure waste. The check now runs
|
|
1682
|
+
against whatever shape the headers already are (a plain record gets a shallow copy only when
|
|
1683
|
+
`content-type` is absent; an already-built `Headers` is mutated in place, as before) and that result
|
|
1684
|
+
goes straight into the `Response` constructor. Deno/V8 charged far more for the discarded `Headers`
|
|
1685
|
+
instance than Bun/JSC did - measured previously as the entire gap between the payload tier's Deno
|
|
1686
|
+
row and its own raw ceiling on the realistic-shape benchmark; that row now leads every peer
|
|
1687
|
+
framework and sits within a few percent of raw `Deno.serve` again.
|
|
1688
|
+
On Deno, JSON responses that carry caller-set headers are now built as a bare `Response` whose
|
|
1689
|
+
headers are set individually afterwards, instead of handing the header record to the constructor -
|
|
1690
|
+
Deno charges far more to ingest a header-record init than to mutate a built response's `Headers`.
|
|
1691
|
+
The runtime's own `Response.json` content-type is probed once and reused, so the wire contract is
|
|
1692
|
+
exactly what `Response.json` ships on that runtime, and Bun keeps the constructor path it measures
|
|
1693
|
+
faster on. Measured +7% on the realistic middleware GET row and +12.7% on its body-hash variant;
|
|
1694
|
+
with this, the realistic Deno rows lead the closest peer framework on both GET and POST.
|
|
1695
|
+
The native header view's one-time name index is now authoritative for every operation, including
|
|
1696
|
+
writes of names not yet present: setting a new header no longer walks and lowercases the whole
|
|
1697
|
+
record on the way in, which had made each fresh `set()` cost grow with the headers already written
|
|
1698
|
+
(measured +2.2% end to end on the realistic middleware-carrying Node GET row). Case-insensitive
|
|
1699
|
+
reads still cover the record as first seen plus everything written through the view; a native twin
|
|
1700
|
+
writing the record directly uses lowercase names - the wire form the record documents.
|
|
1701
|
+
New response tier: `app.responseHeaders(record)` (and `responseHeaders` on a middleware bundle) for
|
|
1702
|
+
response headers with no per-request decision behind them. Declaring them registers no response hook,
|
|
1703
|
+
so the values fold into response construction - one prebuilt init for JSON renders, one record merge
|
|
1704
|
+
where the request set its own headers - and an app whose response middleware is only static keeps the
|
|
1705
|
+
lanes a hook closes: Bun's fused native routes, and the Node direct socket writer that a full
|
|
1706
|
+
`onResponse` gives up. They still apply to every response a hook would cover (success, error,
|
|
1707
|
+
404/405, timeout, short-circuit), byte-identically to registering the same names as an
|
|
1708
|
+
`onResponseHeaders` hook, on every runtime - pinned by parity suites over `app.fetch`, the Node
|
|
1709
|
+
adapter's own socket writes, and the Deno serve path. Declared headers are DEFAULTS: a value the
|
|
1710
|
+
request produced (`c.set.headers`, or a response hook) wins, whatever casing it used, and one name
|
|
1711
|
+
spelled two ways still ships as one header line. Names are lowercased once at wire-up; a non-string
|
|
1712
|
+
value, an invalid name, `__proto__`, or a name the render owns (`content-type`, `content-length`,
|
|
1713
|
+
`transfer-encoding`, `set-cookie`) throws a `TypeError` there instead of surfacing on the wire.
|
|
1714
|
+
Declarations made before any response hook merge into one record; one made after a hook registers as
|
|
1715
|
+
an ordinary header hook so registration order is preserved.
|
|
1716
|
+
`securityHeaders()` and `poweredBy()` (in its default respect-existing configuration) now declare
|
|
1717
|
+
their headers instead of writing them from a hook: measured +11% on a bare Bun `GET` behind
|
|
1718
|
+
`securityHeaders()`, within noise on Node and Deno (where the per-response header writes, not the
|
|
1719
|
+
response walk, dominate). Middleware whose headers depend on the request keeps the hook - `cors`
|
|
1720
|
+
reflects an origin, and `cacheControl` gates on method and status.
|
|
1721
|
+
- Updated dependencies [e05e56d]
|
|
1722
|
+
- @nifrajs/core@2.9.0
|
|
1723
|
+
|
|
1724
|
+
## 2.8.2
|
|
1725
|
+
|
|
1726
|
+
### Patch Changes
|
|
1727
|
+
|
|
1728
|
+
- f7d68e8: Numeric limit options (body/payload byte caps, TTLs, cache sizes, concurrency, ISR revalidate windows) are now validated at construction and throw a `RangeError` on non-finite or out-of-range values instead of silently disabling the bound - a `NaN` cap previously made `size > max` comparisons fail open. JWT `requiredClaims` now checks own properties only, so inherited names like `toString` no longer satisfy a required claim. `@nifrajs/mcp-db` gates multi-statement input with a real tokenizer, bounds `run_query` materialization to `maxRows + 1` via a wrapping subquery, and skips SQLite planner pseudo-nodes when verifying the table allowlist. `nifra scaffold` refuses to write through symlinked route directories.
|
|
1729
|
+
- Updated dependencies [f7d68e8]
|
|
1730
|
+
- @nifrajs/core@2.8.2
|
|
1731
|
+
|
|
1732
|
+
## 2.8.1
|
|
1733
|
+
|
|
1734
|
+
### Patch Changes
|
|
1735
|
+
|
|
1736
|
+
- Updated dependencies [78d66a4]
|
|
1737
|
+
- Updated dependencies [93fdc89]
|
|
1738
|
+
- @nifrajs/core@2.8.1
|
|
1739
|
+
|
|
1740
|
+
## 2.8.0
|
|
1741
|
+
|
|
1742
|
+
### Patch Changes
|
|
1743
|
+
|
|
1744
|
+
- 118e4a5: `nifra dev --bun` now supports server functions and `*.server` modules. Bun's dev-server bundler accepts plugins only through bunfig's `[serve.static]` channel, so the CLI generates a config under `.nifra/dev-bun/` carrying the same production boundary plugins (server-fn RPC stubs, server-only emptying), merges the app's own bunfig `[serve.static] plugins` and `preload` entries, and re-launches itself once with `--config=` pointing at it - identical stubs across dev and build, and the old fail-closed refusal for those modules is gone. The relaunch is verified with a per-launch random token (matched against a file the parent just wrote, consumed on first read, constant-time compare), so no fixed environment value can make the server skip the boundary configuration; an unverifiable launch regenerates and re-launches instead of serving. The app's entire bunfig is carried into the generated config verbatim - jsx, defines, loaders, install settings - with path-bearing entries re-rooted; a bunfig field the generator cannot round-trip, or malformed TOML, fails loudly instead of being dropped. CSS Modules remain gated under `--bun`.
|
|
1745
|
+
|
|
1746
|
+
Also fixed in `@nifrajs/web`: `serverFn<Input, Output>(...)` with explicit type arguments is now recognized by the client-boundary scanner - it previously produced an exportless stub that failed the client build with a missing-export link error (a type argument containing parentheses still fails loudly with guidance, never silently). The dev loop's background leak guard now reports the underlying bundler errors instead of a bare "Bundle failed".
|
|
1747
|
+
|
|
1748
|
+
- @nifrajs/core@2.8.0
|
|
1749
|
+
|
|
1750
|
+
## 2.7.1
|
|
1751
|
+
|
|
1752
|
+
### Patch Changes
|
|
1753
|
+
|
|
1754
|
+
- Updated dependencies [52c89e0]
|
|
1755
|
+
- @nifrajs/core@2.7.1
|
|
1756
|
+
|
|
1757
|
+
## 2.7.0
|
|
1758
|
+
|
|
1759
|
+
### Patch Changes
|
|
1760
|
+
|
|
1761
|
+
- @nifrajs/core@2.7.0
|
|
1762
|
+
|
|
1763
|
+
## 2.6.1
|
|
1764
|
+
|
|
1765
|
+
### Patch Changes
|
|
1766
|
+
|
|
1767
|
+
- 80419f5: Non-hydrated pages (`hydrate: false`) omit the adapter's hydration bootstrap from `<head>` - with no client takeover, scripts like Solid's hydration registry were dead bytes on a static document. The Preact adapter's `renderToString` also returns synchronously once its renderer module is loaded (first call still resolves it lazily), so buffered renders take the synchronous fast path on every subsequent request.
|
|
1768
|
+
- Updated dependencies [5840c98]
|
|
1769
|
+
- @nifrajs/core@2.6.1
|
|
1770
|
+
|
|
1771
|
+
## 2.6.0
|
|
1772
|
+
|
|
1773
|
+
### Minor Changes
|
|
1774
|
+
|
|
1775
|
+
- 08fe221: Per-route document-assembly caching for SSR. `renderPageResult` accepts an `assemblyCache` slot (new `RenderAssemblyCache` type): the request-invariant document pieces - the head (title, meta tags, style links, module preloads, hydration head) and the tail statics - are built once per route and reused, while per-request values (loader data, params, deferred state, action and layout globals) are always assembled fresh. `createWebApp` wires a slot automatically for every route whose meta chain is static (a `meta(data)` function keeps that route on fresh assembly), invalidating on module identity change so dev HMR stays correct, and a per-request CSP nonce bypasses the cache entirely. Output is byte-identical; on a realistic page (meta set + stylesheets + route chunks + an island) the render+assembly step is ~21% faster, which measures as roughly 10-15% more requests per second on the Node SSR path.
|
|
1776
|
+
|
|
1777
|
+
### Patch Changes
|
|
1778
|
+
|
|
1779
|
+
- e6349e5: Security hardening across input parsing and code generation. Every regex that runs on caller-influenced input (URL paths, route patterns, stylesheet and SVG sources, manifest text) is now linear - no polynomial backtracking on adversarial input. SVG preamble stripping and tag removal can no longer splice removed delimiters into new markers. Static file serving rejects `..` traversal in the request form outright and confines the resolved path with a `relative()` containment check. Generated code embeds strings through an escaper that neutralizes `</script>` breakout and the U+2028/U+2029 line separators, and HTML entity decoding resolves `&` last so double-encoded entities cannot double-unescape.
|
|
1780
|
+
- 8383063: The SSR render fast path does less work per request, with byte-identical output. The hydration-head nonce rewrite is skipped when no CSP nonce is set - it was a `.replace` over the whole (constant) hydration script that produced identical output on the common no-nonce path - and the combined deferred-list is reused instead of re-spread into a fresh array on the common page-only render (no layout loader, no action).
|
|
1781
|
+
- Updated dependencies [e6349e5]
|
|
1782
|
+
- @nifrajs/core@2.6.0
|
|
1783
|
+
|
|
1784
|
+
## 2.5.0
|
|
1785
|
+
|
|
1786
|
+
### Patch Changes
|
|
1787
|
+
|
|
1788
|
+
- 02d9aa8: Routing hooks now SSR-render correctly on the dev server. In dev, the adapter is imported by Bun while route modules load through Vite's SSR runner, so the router module could be evaluated twice in one process - two context objects, and `useSearch`/`useParams`/`useLocation` read a context the render never provided. The result was hooks SSR-rendering their empty defaults (`useSearch()` gave `{}`) while the same request's loader saw the validated values; hydration then papered over it on the client, so it surfaced as "the search schema doesn't work in dev". The router context in every adapter is now a `globalThis` singleton (keyed by `Symbol.for`), so both evaluations share the one context React/Vue/Solid/Preact matches providers to readers by. The Vite dev server also mirrors its client `resolve.conditions` into `ssr.resolve.{conditions,externalConditions}`, so dev SSR resolves the same `bun`-conditioned source files Bun does instead of a package's `dist` artifact - which nothing in the dev loop rebuilds, and whose staleness previously 500'd inside framework code.
|
|
1789
|
+
- @nifrajs/core@2.5.0
|
|
1790
|
+
|
|
1791
|
+
## 2.4.0
|
|
1792
|
+
|
|
1793
|
+
### Minor Changes
|
|
1794
|
+
|
|
1795
|
+
- 23e6eb1: Errors resolve to a structured diagnostic - one object a person reads in the overlay and an agent reads as JSON.
|
|
1796
|
+
|
|
1797
|
+
The dev error overlay now shows a source codeframe around the offending line and, for failures nifra recognises (a server-only module or a `node:` built-in reaching the client, a schema mismatch), a plain-language cause and fix with a docs anchor. A new `@nifrajs/web/diagnostic` export builds that `Diagnostic` - stable `code`, the top frame in your own source, the codeframe, and the cause/fix - from any thrown value, and the dev server serves the most recent one as JSON at `/__nifra/last-error`.
|
|
1798
|
+
|
|
1799
|
+
`nifra_explain` (MCP) turns an error - pasted from `nifra_run`/`nifra_test` output, or the dev server's last - into that same diagnostic, so an agent gets the code, the codeframe in your source, and the fix instead of eyeballing a stack trace.
|
|
1800
|
+
|
|
1801
|
+
### Patch Changes
|
|
1802
|
+
|
|
1803
|
+
- 1c2bf5a: Harden the dev-only diagnostics endpoint and the agent-facing reads of it. The dev server now binds to `127.0.0.1`, answers `/__nifra/last-error` with an identity header, and resolves source paths so the codeframe stays inside the project. The Vite dev server serves that endpoint at parity with the Bun one. `nifra_explain` and `nifra_inspect` validate the target port, time out, cap the response size, and only return a body from a verified nifra endpoint - so pointing them at an unrelated local service returns a clear error instead of that service's response.
|
|
1804
|
+
- Updated dependencies [138bfba]
|
|
1805
|
+
- @nifrajs/core@2.4.0
|
|
1806
|
+
|
|
1807
|
+
## 2.3.0
|
|
1808
|
+
|
|
1809
|
+
### Minor Changes
|
|
1810
|
+
|
|
1811
|
+
- ea0a27f: `head.script` carries data; executable code goes through a named escape hatch with a CSP nonce.
|
|
1812
|
+
|
|
1813
|
+
```ts
|
|
1814
|
+
head: {
|
|
1815
|
+
script: [{ content: JSON.stringify(article) }], // application/ld+json or application/json
|
|
1816
|
+
unsafeScript: [unsafeInlineScript("window.dataLayer = []", { nonce })],
|
|
1817
|
+
}
|
|
1818
|
+
```
|
|
1819
|
+
|
|
1820
|
+
**Breaking**, deliberately. The slot used to take any `type`, including `module`. Its escaping guards
|
|
1821
|
+
against closing the element early - `</script>`, `<!--`, `]]>` - which is exactly what inert JSON needs
|
|
1822
|
+
and is no protection whatsoever for code. A route interpolating loader data into an executable body
|
|
1823
|
+
therefore had escaping that looked like a boundary and was not one. The slot now accepts only what it
|
|
1824
|
+
can actually make safe: a wrong `type` is a compile error, and it throws at render for callers the
|
|
1825
|
+
types do not reach.
|
|
1826
|
+
|
|
1827
|
+
`unsafeInlineScript()` is the replacement, named after what it is and requiring a nonce. Pass the same
|
|
1828
|
+
nonce to `renderPage` and it reaches every framework-owned script in the document - the hydration
|
|
1829
|
+
bootstrap, the pre-hydration guard, the data script, streamed deferred resolutions, island tags, and an
|
|
1830
|
+
adapter's own hydration head - so a strict `script-src 'nonce-…'` policy becomes achievable rather than
|
|
1831
|
+
aspirational. A render without a nonce emits exactly the bytes it did before.
|
|
1832
|
+
|
|
1833
|
+
Both `type` fields are allowlists rather than escaped values, checked in one place for the server
|
|
1834
|
+
render and the client soft-navigation together: a head that renders and then throws on the next
|
|
1835
|
+
navigation is worse than one that never rendered.
|
|
1836
|
+
|
|
1837
|
+
- 45b0733: `nifra dev --bun` no longer waves `.fn.mts` / `.fn.cts` / `.fn.mjs` / `.fn.cjs` into the browser.
|
|
1838
|
+
|
|
1839
|
+
The refusal that keeps a server function off the Bun dev pipeline matched a hand-written glob,
|
|
1840
|
+
`**/*.fn.{ts,tsx,js,jsx}`, while both build pipelines stub anything matching
|
|
1841
|
+
`/\.fn(\.[cm]?[jt]sx?)?$/`. So a `todos.fn.mts` was a server function everywhere except the one check
|
|
1842
|
+
that exists to stop it leaking - it started the dev server and shipped the function bodies, and
|
|
1843
|
+
whatever they close over, to the browser. Measured across all eight extensions, four leaked.
|
|
1844
|
+
|
|
1845
|
+
Both refusals now test against the same matchers the transforms use, exported as `SERVER_FN_MODULE`
|
|
1846
|
+
and `SERVER_ONLY_MODULE` from `@nifrajs/web`. `SERVER_ONLY_MODULE` had two definitions and now has one
|
|
1847
|
+
owner. A test drives every accepted extension through the guard, so widening a convention without
|
|
1848
|
+
widening the guard fails there rather than in someone's browser.
|
|
1849
|
+
|
|
1850
|
+
- c42d777: `clientEntry` is optional on a `hydrate: false` page.
|
|
1851
|
+
|
|
1852
|
+
A non-hydrated document references it nowhere - all three uses sit behind the `hydrate` guard - yet
|
|
1853
|
+
every static page had to pass one anyway. `RenderPageInput` is now a union, so omitting it is allowed
|
|
1854
|
+
exactly when `hydrate: false` is set and stays a compile error otherwise, rather than rendering
|
|
1855
|
+
`<script src="">` and silently failing to hydrate.
|
|
1856
|
+
|
|
1857
|
+
Found by writing `examples/web-vanilla`, which is the first caller that genuinely has no client entry
|
|
1858
|
+
to give.
|
|
1859
|
+
|
|
1860
|
+
- ea0a27f: A server function has one type per half, so both the server call and the hook argument are honest.
|
|
1861
|
+
|
|
1862
|
+
```ts
|
|
1863
|
+
export type ClientServerFn<Input, Output> = (
|
|
1864
|
+
input: Input
|
|
1865
|
+
) => MaybePromise<Output>;
|
|
1866
|
+
export type ServerFnReference<Input, Output> =
|
|
1867
|
+
| ServerFn<Input, Output>
|
|
1868
|
+
| ClientServerFn<Input, Output>;
|
|
1869
|
+
```
|
|
1870
|
+
|
|
1871
|
+
One type could not describe both halves. `ServerFn` is the SERVER declaration and takes `(input,
|
|
1872
|
+
context)`; the client imports a generated stub that takes one argument. Widening the single type so a
|
|
1873
|
+
one-argument call compiled made a direct server call type-check while handing the declaration
|
|
1874
|
+
`undefined` for a context its implementation requires - a runtime failure the compiler had just been
|
|
1875
|
+
told to allow.
|
|
1876
|
+
|
|
1877
|
+
Now the two are separate and `useServerFn` accepts either through `ServerFnReference`, which is the
|
|
1878
|
+
one place the two halves legitimately meet. Calling a declaration from your own server code needs the
|
|
1879
|
+
context, and omitting it is a compile error again.
|
|
1880
|
+
|
|
1881
|
+
- d190b1c: Add `@nifrajs/web/fn` - server functions, mounted as ordinary routes.
|
|
1882
|
+
|
|
1883
|
+
```ts
|
|
1884
|
+
// app/actions/todos.fn.ts
|
|
1885
|
+
export const addTodo = serverFn(
|
|
1886
|
+
{
|
|
1887
|
+
input: t.object({ text: t.string({ minLength: 1 }) }),
|
|
1888
|
+
capabilities: ["db.write"],
|
|
1889
|
+
},
|
|
1890
|
+
async ({ text }, c) => db.todos.insert({ text })
|
|
1891
|
+
);
|
|
1892
|
+
|
|
1893
|
+
// server
|
|
1894
|
+
import * as todos from "./actions/todos.fn";
|
|
1895
|
+
app.use(serverFunctions("todos", todos)); // -> POST /_nifra/fn/todos/addTodo
|
|
1896
|
+
```
|
|
1897
|
+
|
|
1898
|
+
A mounted function registers through the ordinary public `register()`, so it is a route like any other
|
|
1899
|
+
and inherits the body cap, schema validation, capability declarations, the effect ledger, and
|
|
1900
|
+
`nifra assure`. Nothing in `@nifrajs/core` changed and no request-path branch was added, so an app that
|
|
1901
|
+
mounts none of them pays nothing.
|
|
1902
|
+
|
|
1903
|
+
Every server function is a public POST endpoint whose arguments the caller controls entirely, so the
|
|
1904
|
+
guards are structural rather than documented:
|
|
1905
|
+
|
|
1906
|
+
- **`application/json` only.** A cross-origin form can send only urlencoded, multipart or text/plain,
|
|
1907
|
+
so requiring JSON forces a preflight the browser blocks. Both alternatives were measured first: a
|
|
1908
|
+
body schema alone still accepts a cross-origin urlencoded form, and `c.boundedJson` alone accepts a
|
|
1909
|
+
`text/plain` body crafted to parse as JSON. A function with no input schema has no body reader at
|
|
1910
|
+
all, which is where this guard is the only defence.
|
|
1911
|
+
- **Same-origin only** when an `Origin` is present - defence in depth behind the JSON requirement.
|
|
1912
|
+
- **Input is always validated**; no schema means no argument, never an unchecked one.
|
|
1913
|
+
- **No closures.** A function is a module-level export taking explicit arguments, which removes the
|
|
1914
|
+
serialised-closure class rather than defending it.
|
|
1915
|
+
|
|
1916
|
+
The client build replaces a `*.fn.ts` module with one stub per export, each POSTing to its mounted
|
|
1917
|
+
route, so the function bodies and everything they import never reach a browser.
|
|
1918
|
+
|
|
1919
|
+
`nifra dev --bun` refuses to start on an app that has server functions. Bun's dev-server bundler takes
|
|
1920
|
+
no plugins - a runtime `Bun.plugin` onLoad does not reach it, measured rather than assumed - so the
|
|
1921
|
+
module would ship whole, secrets included. Refusing matches how that pipeline already handles CSS
|
|
1922
|
+
Modules. `nifra build` and `nifra dev` (Vite) transform it correctly.
|
|
1923
|
+
|
|
1924
|
+
- a4ecca9: The `.server` convention now holds in every client pipeline, not just one.
|
|
1925
|
+
|
|
1926
|
+
A `*.server.ts` module is server-only: the client build empties it so its import subtree - `node:`
|
|
1927
|
+
builtins, native modules, secrets - never reaches a browser. That was implemented once, as a
|
|
1928
|
+
`Bun.build` plugin, so it held in exactly one of the four client paths. `nifra build` emptied the
|
|
1929
|
+
module; `nifra dev` (Vite), a Vite production build, and `nifra dev --bun` all bundled it whole.
|
|
1930
|
+
|
|
1931
|
+
A guard that holds in one pipeline is worse than no guard, because the file NAME reads as protection
|
|
1932
|
+
everywhere it appears.
|
|
1933
|
+
|
|
1934
|
+
- `@nifrajs/web/plugins/vite-server-only` empties the module for Vite, registered in both the dev
|
|
1935
|
+
server and the production build. A parity test asserts it emits bytes identical to the Bun plugin.
|
|
1936
|
+
- `nifra dev --bun` refuses to start on an app containing one, naming the files - the same treatment
|
|
1937
|
+
`*.fn` already had, because Bun's dev bundler takes no plugins and cannot be fixed with one.
|
|
1938
|
+
|
|
1939
|
+
The three `nifra dev --bun` refusals (CSS Modules, `*.fn`, `*.server`) now have tests. Two of them
|
|
1940
|
+
guard secrets, and none of them had one.
|
|
1941
|
+
|
|
1942
|
+
- de8d992: `@nifrajs/web/service-worker` generates a service worker from a build manifest.
|
|
1943
|
+
|
|
1944
|
+
```ts
|
|
1945
|
+
const sw = generateServiceWorker(manifest, {
|
|
1946
|
+
buildId: gitSha,
|
|
1947
|
+
offlineUrl: "/offline",
|
|
1948
|
+
});
|
|
1949
|
+
```
|
|
1950
|
+
|
|
1951
|
+
Opt-in and generated at build time, so an app that never calls it ships nothing.
|
|
1952
|
+
|
|
1953
|
+
The generated worker is deliberately narrow, because a service worker outlives a deploy and can hand
|
|
1954
|
+
one visitor a response produced for another:
|
|
1955
|
+
|
|
1956
|
+
- **Only content-hashed assets are precached.** A hashed URL names its bytes, so cache-first is correct
|
|
1957
|
+
by construction; unhashed URLs are left to the network.
|
|
1958
|
+
- **Documents are never cached.** Only navigations that FAIL are answered, and only with the static
|
|
1959
|
+
offline page you nominate. Caching HTML is how a worker serves one signed-in user the page rendered
|
|
1960
|
+
for another.
|
|
1961
|
+
- **GET, same-origin, `ok`, not `no-store`.** Everything else goes straight to the network.
|
|
1962
|
+
- **The cache name carries the build id**, and activation deletes every older cache, so a stale worker
|
|
1963
|
+
cannot pin an old build.
|
|
1964
|
+
|
|
1965
|
+
Omit `offlineUrl` and a failed navigation simply fails - an offline page you have not written is not
|
|
1966
|
+
better than a browser error.
|
|
1967
|
+
|
|
1968
|
+
- c823915: Typed, validated search params: a route declares a `searchSchema` and both its loader and its component read the parsed, validated query.
|
|
1969
|
+
|
|
1970
|
+
Export a Standard Schema as `searchSchema` from a route. The loader's `ctx.search` becomes the parsed URL query validated against it (typed via `LoaderArgs<typeof app, Env, typeof searchSchema>`), and the component reads the same value with `useSearch<typeof searchSchema>()`. Invalid or hostile input fails closed to the schema's defaults (never a 500); without a `searchSchema`, both are the raw parsed query. Validation runs at match time and the value is derived identically on the server and on client navigation, so a component never parses `window.location.search` by hand and the query it renders hydrates with no mismatch.
|
|
1971
|
+
|
|
1972
|
+
```tsx
|
|
1973
|
+
export const searchSchema = v.object({
|
|
1974
|
+
page: v.optional(v.fallback(v.number(), 1), 1),
|
|
1975
|
+
});
|
|
1976
|
+
|
|
1977
|
+
export async function loader({
|
|
1978
|
+
search,
|
|
1979
|
+
api,
|
|
1980
|
+
}: LoaderArgs<typeof backend, unknown, typeof searchSchema>) {
|
|
1981
|
+
return { rows: await api.reports.list(search).get() }; // search.page is a number
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
export default function Reports({ data }) {
|
|
1985
|
+
const { page } = useSearch<typeof searchSchema>(); // page: number, SSR-correct
|
|
1986
|
+
return <Pager page={page} />;
|
|
1987
|
+
}
|
|
1988
|
+
```
|
|
1989
|
+
|
|
1990
|
+
A `_layout` can declare its own `searchSchema` for keys shared across a section (`?org`, `?theme`); the route's effective search merges the layout chain's schemas with the page's, page-wins on a conflict, so both the layout and the page read their validated slice from one object.
|
|
1991
|
+
|
|
1992
|
+
A route can also list `searchClientKeys` - search keys that are purely client-side UI (`?tab`, a client-side `?sort`, `?modal`). When a client navigation changes only those keys, the URL updates (so `useSearch` re-renders) without re-running the loader; any other key change revalidates as before, so data is never stale.
|
|
1993
|
+
|
|
1994
|
+
`useSearch` ships on every adapter - React (a value), Preact (a value), Vue (a `Ref`), Solid (an `Accessor`), and Svelte (an accessor), each in that framework's own shape.
|
|
1995
|
+
|
|
1996
|
+
`navigate` gains an object form on every adapter: `navigate({ to, search, replace })` serializes `search` onto `to` (no hand-built query strings). Run `nifra sync-routes` to generate `nifra-routes.d.ts` (each static route mapped to its schema output) and include it in your tsconfig, and `search` is typed against the target route's `searchSchema` - a wrong shape for a known route is a compile error, while an unmapped path takes a loose `search`. Regenerated from the route files, so a stale shape becomes a `tsc` error. The string-path and history-delta forms are unchanged.
|
|
1997
|
+
|
|
1998
|
+
```ts
|
|
1999
|
+
navigate({ to: "/reports", search: { page: 2 } }); // search typed against /reports's schema
|
|
2000
|
+
```
|
|
2001
|
+
|
|
2002
|
+
- 62a8d03: Add `useServerFn` - a server function's pending, data and error state - to all five adapters.
|
|
2003
|
+
|
|
2004
|
+
```tsx
|
|
2005
|
+
const addTodo = useServerFn(fns.addTodo)
|
|
2006
|
+
<button disabled={addTodo.pending} onClick={() => addTodo.call({ text }).catch(() => {})}>add</button>
|
|
2007
|
+
```
|
|
2008
|
+
|
|
2009
|
+
Calling a server function never needed a binding: the client stub is `(input) => Promise<Output>`.
|
|
2010
|
+
This adds only the state a component wants around it.
|
|
2011
|
+
|
|
2012
|
+
The state machine is `@nifrajs/web`'s `createServerFnStore`, shared by every adapter, so "is it
|
|
2013
|
+
pending" has one answer rather than five that drift. Each binding contributes just its subscription
|
|
2014
|
+
primitive: `useSyncExternalStore` (React, Preact), a signal (Solid), a `shallowRef` (Vue), a `readable`
|
|
2015
|
+
(Svelte).
|
|
2016
|
+
|
|
2017
|
+
Two behaviours worth knowing:
|
|
2018
|
+
|
|
2019
|
+
- **The last call wins.** A response that is no longer the newest is discarded rather than written, so
|
|
2020
|
+
a slow first call landing after a fast second cannot overwrite fresh data with stale.
|
|
2021
|
+
- **`call` still rejects.** The error is recorded for rendering AND the promise rejects, so `await`
|
|
2022
|
+
behaves normally. A caller that only renders from state should attach `.catch(() => {})`, as with
|
|
2023
|
+
`useFetcher`'s `submit`.
|
|
2024
|
+
|
|
2025
|
+
`data` is kept while the next call is in flight, so a rendered list does not blank on every refetch.
|
|
2026
|
+
|
|
2027
|
+
- dcacfe7: Guard navigation away from unsaved work with `useBlocker`.
|
|
2028
|
+
|
|
2029
|
+
Mirrors react-router's shape: pass a boolean or a `({ currentLocation, nextLocation }) => boolean`
|
|
2030
|
+
predicate and get back `{ state, proceed, reset }`. When a navigation is intercepted - a `<Link>` or
|
|
2031
|
+
anchor click, `useNavigate`, or a browser back/forward - `state` becomes `"blocked"`, so you render
|
|
2032
|
+
your OWN confirmation and call `proceed()` to continue or `reset()` to stay. A plain boolean can't
|
|
2033
|
+
express an async "are you sure?"; these two callbacks can.
|
|
2034
|
+
|
|
2035
|
+
```tsx
|
|
2036
|
+
import { useBlocker } from "@nifrajs/web-react/router";
|
|
2037
|
+
|
|
2038
|
+
const blocker = useBlocker(form.isDirty);
|
|
2039
|
+
|
|
2040
|
+
return blocker.state === "blocked" ? (
|
|
2041
|
+
<ConfirmDialog onConfirm={blocker.proceed} onCancel={blocker.reset} />
|
|
2042
|
+
) : null;
|
|
2043
|
+
```
|
|
2044
|
+
|
|
2045
|
+
Back and forward are guarded too: the destination URL is restored before you are asked, so the page
|
|
2046
|
+
never changes underneath the prompt. It also arms the browser's native "Leave site?" prompt on tab
|
|
2047
|
+
close and reload. Idle on the server and before hydration, so it degrades to native navigation and
|
|
2048
|
+
stays hydration-safe.
|
|
2049
|
+
|
|
2050
|
+
- ea0a27f: The Vite pipelines honour Nifra's public-env boundary, keep `.server` out of the browser on Vite 5,
|
|
2051
|
+
and serve a valid ESM replacement in dev.
|
|
2052
|
+
|
|
2053
|
+
**Breaking if you relied on `VITE_*` reaching the browser.** Vite exposes any variable matching its own
|
|
2054
|
+
`envPrefix` to client code, and Nifra never overrode it - so an app with one documented boundary
|
|
2055
|
+
(`publicEnvPrefix`, default `PUBLIC_`) silently had a second one it had not configured. The dev server
|
|
2056
|
+
and the production build now bind Vite's prefix to Nifra's, including the "expose nothing" setting.
|
|
2057
|
+
Move anything the browser genuinely needs to your public prefix.
|
|
2058
|
+
|
|
2059
|
+
Two more holes in the same convention:
|
|
2060
|
+
|
|
2061
|
+
- Vite 5 has no `applyToEnvironment`, so the `.server` and `.fn` transforms ran for the SSR graph too
|
|
2062
|
+
and stubbed the modules the server itself needs. Both now decline when the transform is told it is
|
|
2063
|
+
running for SSR.
|
|
2064
|
+
- The `.server` replacement was CommonJS, which is right for the Bun bundler and invalid in Vite dev's
|
|
2065
|
+
native ESM graph. Dev now gets inert ESM bindings derived from the module's exported names, with the
|
|
2066
|
+
implementation and its imports discarded. A shape the generator does not model declares no binding,
|
|
2067
|
+
so the import fails to link - server code is never served as the fallback.
|
|
2068
|
+
|
|
2069
|
+
- 0c2de22: Ambient types for a generated `server-manifest` module, so a hand-written server entry that imports `./server-manifest` typechecks before the first build has generated that file - no `@ts-nocheck` on the file that deploys. Reference it with `/// <reference types="@nifrajs/web/server-manifest" />`, or list `"@nifrajs/web/server-manifest"` in your tsconfig `compilerOptions.types`. Once a build (or `nifra sync-manifest`) writes the real file next to your entry, TypeScript resolves the import to it and its types win. Not needed with `nifra build --target`, which generates and bundles its own entry.
|
|
2070
|
+
|
|
2071
|
+
### Patch Changes
|
|
2072
|
+
|
|
2073
|
+
- 7293a1c: A custom `rootId` hydrates.
|
|
2074
|
+
|
|
2075
|
+
```ts
|
|
2076
|
+
renderPage({ adapter, chain, data, clientEntry, rootId: "app" });
|
|
2077
|
+
```
|
|
2078
|
+
|
|
2079
|
+
That render produced a flawless server-rendered document that then hydrated nothing, forever, with an
|
|
2080
|
+
empty console: the generated client entry mounted into `document.getElementById("root")` and skipped
|
|
2081
|
+
mounting when it found nothing. Every framework binding, every loader and the whole SSR pass worked,
|
|
2082
|
+
and the page was static.
|
|
2083
|
+
|
|
2084
|
+
`rootId` is chosen per RENDER while the client entry is emitted once per BUILD, so an id baked into the
|
|
2085
|
+
entry can only ever be a guess. The container announces itself instead - a non-default `rootId` also
|
|
2086
|
+
carries `ROOT_ATTRIBUTE` (`data-nifra-root`), which the entry looks for before falling back to `#root`.
|
|
2087
|
+
It has to be the DOM and not another `window.__NIFRA_*` global, because a second copy of the id can
|
|
2088
|
+
drift from the markup while an attribute written by the same expression as the id cannot.
|
|
2089
|
+
|
|
2090
|
+
A default render emits exactly the bytes it did before - the marker is absent, and `#root` is still
|
|
2091
|
+
what the entry finds. When a document has neither, the entry now throws and names what it looked for
|
|
2092
|
+
rather than leaving a live page that answers no clicks.
|
|
2093
|
+
|
|
2094
|
+
The container is looked up as `body > div[data-nifra-root]` rather than by the attribute alone. A
|
|
2095
|
+
`<meta>` in the head may legally carry any `data-*` attribute, and one carrying this marker appears
|
|
2096
|
+
earlier in document order - so an unscoped lookup would hand hydration a tag in the head and mount the
|
|
2097
|
+
application into it.
|
|
2098
|
+
|
|
2099
|
+
- ea0a27f: The same-origin check works behind a TLS-terminating proxy, and both seams that use it now agree.
|
|
2100
|
+
|
|
2101
|
+
Nifra had two of these - the WebSocket handshake's cross-origin default and the server-function mount -
|
|
2102
|
+
and they answered differently for one request, so a browser that could open a socket was told its POST
|
|
2103
|
+
was cross-origin. `isSameOriginRequest` from `@nifrajs/core/server` is now the single owner.
|
|
2104
|
+
|
|
2105
|
+
The rule it encodes: the host must match, and the Origin's scheme may be equal or STRONGER, never
|
|
2106
|
+
weaker. A server behind Cloudflare, a tunnel or an ingress sees a plain HTTP socket, so `request.url`
|
|
2107
|
+
says `http:` while the browser correctly reports an `https:` page - the shape almost every deployment
|
|
2108
|
+
produces. Comparing full origins rejects it, which is not the stricter option but an outage: measured,
|
|
2109
|
+
every server-function POST returned 403 behind a terminating proxy, while the cross-origin caller the
|
|
2110
|
+
comparison aimed at had already been rejected on the host alone. The reverse - an `http:` page against
|
|
2111
|
+
an `https:` request URL - is a downgrade with no legitimate cause, and is refused, as is any Origin
|
|
2112
|
+
that is not an http(s) page origin at all.
|
|
2113
|
+
|
|
2114
|
+
`X-Forwarded-Proto` is still not read, and that is the point: a forwarded header is attacker-controlled
|
|
2115
|
+
unless something upstream is proven to overwrite it, so trusting one by default would hand every
|
|
2116
|
+
unproxied deployment a spoofable origin check. Ordering the two schemes reconciles them without it.
|
|
2117
|
+
|
|
2118
|
+
- a92104e: The interpolated-SQL rule catches string concatenation, the oldest injection shape.
|
|
2119
|
+
|
|
2120
|
+
```ts
|
|
2121
|
+
db.query("SELECT * FROM users WHERE id = " + req.params.id); // now fails the check
|
|
2122
|
+
```
|
|
2123
|
+
|
|
2124
|
+
The rule inspected template literals only: a quoted string was skipped before the SQL-keyword test ever
|
|
2125
|
+
ran. So a codebase predating template literals - or an LLM emitting older-style JS - got a clean
|
|
2126
|
+
`nifra check` on textbook-injectable SQL, and `nifra check` reporting clean is a security claim that
|
|
2127
|
+
feeds the assurance ladder.
|
|
2128
|
+
|
|
2129
|
+
The same keyword requirement applies, so `cache.query("user:" + id)` stays quiet, and a statement built
|
|
2130
|
+
into a variable elsewhere still says nothing at the call site.
|
|
2131
|
+
|
|
2132
|
+
Also fixes the service-worker test suite, whose offline case never reached the code it claimed to test:
|
|
2133
|
+
the stub was assigned to `globalThis.fetch` after the worker had already captured `fetch` as a
|
|
2134
|
+
parameter, so the assertion was satisfied by a real network failure. A worker mutated to serve the
|
|
2135
|
+
offline page to every visitor passed all 19 tests; it now fails, and an online navigation is covered.
|
|
2136
|
+
|
|
2137
|
+
- 28704d7: The dev server's public-env boundary is covered by a test that drives it.
|
|
2138
|
+
|
|
2139
|
+
Vite inlines any `VITE_*` variable into client source by default, which is a second boundary running
|
|
2140
|
+
beside Nifra's `PUBLIC_` one - so a `VITE_DATABASE_URL` reached the browser without passing the policy
|
|
2141
|
+
meant to decide that. Both pipelines were fixed, but only the production build was tested. A guard
|
|
2142
|
+
holding in one pipeline while the option reads like protection in both is the exact shape of the bug
|
|
2143
|
+
that motivated the fix.
|
|
2144
|
+
|
|
2145
|
+
The dev server now has the equivalent: a real dev server, a module asking for both variables, and an
|
|
2146
|
+
assertion about what it actually serves. It fails without the fix, and it cannot pass on an error page.
|
|
2147
|
+
|
|
2148
|
+
- Updated dependencies [6f5b3ad]
|
|
2149
|
+
- Updated dependencies [85b354d]
|
|
2150
|
+
- Updated dependencies [8514caa]
|
|
2151
|
+
- Updated dependencies [ea0a27f]
|
|
2152
|
+
- Updated dependencies [ea0a27f]
|
|
2153
|
+
- Updated dependencies [b271164]
|
|
2154
|
+
- Updated dependencies [8c77d47]
|
|
2155
|
+
- Updated dependencies [ea0a27f]
|
|
2156
|
+
- Updated dependencies [5fe332a]
|
|
2157
|
+
- Updated dependencies [d2840ac]
|
|
2158
|
+
- @nifrajs/core@2.3.0
|
|
2159
|
+
|
|
2160
|
+
## 2.2.0
|
|
2161
|
+
|
|
2162
|
+
### Minor Changes
|
|
2163
|
+
|
|
2164
|
+
- 39b1670: `@nifrajs/web/dev` runs on Bun's native HMR, so the Bun pipeline no longer needs Vite anywhere.
|
|
2165
|
+
|
|
2166
|
+
nifra has two dev pipelines and one rule between them: a pipeline owns a whole phase. The Vite server
|
|
2167
|
+
stays the default and now resolves SSR as well as the client, so both halves agree on every specifier.
|
|
2168
|
+
This is the other half of that split. `Bun.serve` bundles and hot-reloads the client, Bun's runtime
|
|
2169
|
+
resolves SSR, and only one toolchain is present - so the two cannot disagree. Previously this server
|
|
2170
|
+
rebuilt the whole client and forced a full page reload on every save; now Bun rebuilds incrementally and
|
|
2171
|
+
pushes over its own HMR socket.
|
|
2172
|
+
|
|
2173
|
+
Joining the two halves takes some care, because Bun's dev server bundles HTML routes and nifra renders the
|
|
2174
|
+
document itself. A throwaway HTML route exists purely so Bun bundles the generated client entry and
|
|
2175
|
+
assigns it a URL, which nifra reads back and points its pages at. Three things follow from that, each of
|
|
2176
|
+
which is a silent failure if you skip it:
|
|
2177
|
+
|
|
2178
|
+
- **The entry URL expires.** It is a content hash over the whole client graph, so any file the entry
|
|
2179
|
+
reaches re-hashes it. Pages therefore reference a stable nifra URL that redirects to whichever chunk Bun
|
|
2180
|
+
is currently serving. Injecting a remembered URL is not a stale-cache annoyance: Bun answers a
|
|
2181
|
+
superseded chunk with a `location.reload()` stub, so the page reloads, gets the same dead URL from SSR,
|
|
2182
|
+
and loops forever - with every reload wiping the console that would have explained it.
|
|
2183
|
+
- **Stylesheets have to be carried across.** Bun lifts `import "./app.css"` out of the JS graph and links
|
|
2184
|
+
it from the page it bundled, which is the throwaway. Without forwarding those links, the entire dev
|
|
2185
|
+
session renders unstyled while production, which reads CSS from the build manifest, is perfectly fine.
|
|
2186
|
+
- **SSR freshness is ordered by request, not by clock.** Bun rebuilds and reloads the browser the instant
|
|
2187
|
+
a file is saved, which is faster than any file watcher can answer; SSR rendered from a watcher tick is
|
|
2188
|
+
still on the previous code when that reload lands, and React discards the server tree with a hydration
|
|
2189
|
+
mismatch. Rebuilding when Bun's entry hash moves - the same value already fetched to render the page -
|
|
2190
|
+
removes the race instead of shrinking it.
|
|
2191
|
+
|
|
2192
|
+
The client-leak guards still run. Bun's dev server does its own bundling, so the `buildClient` pass that
|
|
2193
|
+
enforces them is no longer on the path that serves the app; it now runs beside the dev loop, off the hot
|
|
2194
|
+
path, and reports. These stop server-only code and `node:` builtins reaching a browser, and a dev loop
|
|
2195
|
+
that quietly stops enforcing them is how a leak reaches a deploy. Opt out with `guardLeaks: false`.
|
|
2196
|
+
|
|
2197
|
+
Reading Bun's output for the entry URL is isolated in one adapter with its own tests, including one that
|
|
2198
|
+
runs a real Bun dev server and fails if the markup Bun emits ever changes shape - so a Bun upgrade breaks
|
|
2199
|
+
a test rather than producing a dev server that boots cleanly and serves pages whose scripts 404.
|
|
2200
|
+
|
|
2201
|
+
Both pipelines now report a port collision the same way, since `Bun.serve` throws synchronously where
|
|
2202
|
+
Node's http server emits an async `error` event.
|
|
2203
|
+
|
|
2204
|
+
- 1394641: Layout loaders: request data in the component that wraps every page.
|
|
2205
|
+
|
|
2206
|
+
`routes/_layout.tsx` rendered, but a `loader` it exported never ran, so nothing request-derived could
|
|
2207
|
+
reach a layout - host, session, locale, feature flags, tenant. An app hit this and moved its host guard
|
|
2208
|
+
out of the component tree into the server entry, where it could not be typechecked with the rest of the
|
|
2209
|
+
app. That is the real cost: the gap pushed security-relevant code to the one place nifra's typed-boundary
|
|
2210
|
+
promise does not reach. Remix, React Router and SvelteKit all support this; nifra was alone in not.
|
|
2211
|
+
|
|
2212
|
+
```tsx
|
|
2213
|
+
// routes/orgs/[org]/_layout.tsx
|
|
2214
|
+
export const gate = true // optional; see below
|
|
2215
|
+
export async function loader({ params, req }) {
|
|
2216
|
+
return { org: await findOrg(params.org) } // params is { org } - nothing deeper
|
|
2217
|
+
}
|
|
2218
|
+
export default function Layout({ data, children }) { … }
|
|
2219
|
+
```
|
|
2220
|
+
|
|
2221
|
+
**Scoped, not global.** A layout owns the URL prefix it wraps, so it receives only the params inside
|
|
2222
|
+
that prefix and its loader is skipped on a navigation that did not change them. Navigating
|
|
2223
|
+
`/orgs/acme/a` → `/orgs/acme/b` does not re-run the org layout's loader. Scope is derived at build time
|
|
2224
|
+
per `(route, layout)` pair, because one layout can own different params on different expanded patterns:
|
|
2225
|
+
`[[lang]]/docs/_layout` owns nothing on `/docs/:slug` and `{lang}` on `/:lang/docs/:slug`. Layouts are
|
|
2226
|
+
not router nodes and did not become any - the router is untouched.
|
|
2227
|
+
|
|
2228
|
+
**Execution order is declared, and this matters for security.** By default a layout loader runs in
|
|
2229
|
+
parallel with the page's, which is right for data and wrong for a guard: a page loader running
|
|
2230
|
+
concurrently with a guard has already queried by the time the guard says no. `export const gate = true`
|
|
2231
|
+
makes a layout blocking - nothing beneath it runs until it resolves, and nothing beneath a rejected gate
|
|
2232
|
+
runs at all. **A layout loader without `gate: true` is not an authorization boundary.** Gates also run on
|
|
2233
|
+
the data-only request, so a client navigation cannot bypass one by sending the data header, and a gate is
|
|
2234
|
+
never skipped by the retention hint.
|
|
2235
|
+
|
|
2236
|
+
A layout may throw `notFound()` / `gone()` / `redirect()`. Its errors resolve to the `_error` boundary at
|
|
2237
|
+
or above its OWN segment, never one below it - rendering there would wrap the boundary in the very layout
|
|
2238
|
+
whose loader just failed.
|
|
2239
|
+
|
|
2240
|
+
Every adapter passes each layout its own data. A layout with no loader receives `null`, and an app where
|
|
2241
|
+
no layout has a loader emits byte-identical HTML and unchanged props.
|
|
2242
|
+
|
|
2243
|
+
The data-mode response becomes a versioned envelope when a chain carries layout data. It is recognised
|
|
2244
|
+
by structure, and the bare pre-envelope shape is still accepted - a prerendered `_data.json` is a static
|
|
2245
|
+
file that outlives the deploy that wrote it.
|
|
2246
|
+
|
|
2247
|
+
- e713cab: Let a route loader answer 404 and 410.
|
|
2248
|
+
|
|
2249
|
+
A matched route whose loader finds nothing had no supported way to set its page's status, so the path
|
|
2250
|
+
of least resistance was to return empty data and render "not found" inside a **200**. That is a soft
|
|
2251
|
+
404: search engines penalise it and keep the dead URL indexed, and because the page looks correct in a
|
|
2252
|
+
browser it ships and stays shipped. It is the most common page shape there is - a detail route whose
|
|
2253
|
+
record may not exist.
|
|
2254
|
+
|
|
2255
|
+
`notFound()`, `gone()`, and `statusPage(status)` are thrown from a loader, the way `redirect()` already
|
|
2256
|
+
is. They render the `_404` page - or `_410.tsx` / `_<status>.tsx` if the app authored one - inside the
|
|
2257
|
+
normal layout chain, hydrated, at the right status. A `headers` option carries the cache policy each
|
|
2258
|
+
status wants: a 404 may be racing publication and wants a short TTL, while a 410 is a promise that the
|
|
2259
|
+
URL is permanently gone. Typed `never`, so a loader narrows without a redundant `return`.
|
|
2260
|
+
|
|
2261
|
+
410 is not a pedantic 404: it tells a crawler to drop the URL instead of re-fetching it for weeks.
|
|
2262
|
+
|
|
2263
|
+
Existing behaviour is unchanged by construction. The signal is a branded `Response` and the brand is
|
|
2264
|
+
checked before the verbatim pass-through, so `throw redirect(...)`, `throw new Response(...)`, and a
|
|
2265
|
+
real `Error` reaching `_error` all behave exactly as before. Client-side navigation and prerendering
|
|
2266
|
+
already handle a non-ok render correctly and now have tests pinning that: a soft-nav falls back to a
|
|
2267
|
+
full navigation and lands on the same page, and a prerendered path whose loader signals is omitted
|
|
2268
|
+
from the build rather than baked as a static 200 shell.
|
|
2269
|
+
|
|
2270
|
+
`renderPageResult` gains a `headers` option. `content-type` and the ISR freshness header stay
|
|
2271
|
+
framework-owned and cannot be overridden through it.
|
|
2272
|
+
|
|
2273
|
+
Also trims the router's rejected-parameter message added in the previous release. The explanation cost
|
|
2274
|
+
~0.3 KB gzip in every bundle; it now states the grammar rule and the two ways out without building an
|
|
2275
|
+
example path, which is a third smaller and keeps the base bundle inside its budget.
|
|
2276
|
+
|
|
2277
|
+
- a4645e2: Support path segments that are part literal, part parameter.
|
|
2278
|
+
|
|
2279
|
+
A route segment had to be wholly static, wholly a parameter, or wholly a wildcard. `/:key.txt`,
|
|
2280
|
+
`/post-[id].html` and `/[locale]-sitemap.xml` did not merely fail to match - they failed to
|
|
2281
|
+
**compile**. The trigger was an IndexNow key-verification file, which the protocol requires at
|
|
2282
|
+
`<origin>/<key>.txt` with the key coming from deploy-time config, and at the root, because a key
|
|
2283
|
+
served from a subdirectory only authorises URLs beneath it. The workaround was an exact-match check in
|
|
2284
|
+
the app's server entry, which moved a routing concern out of the router and never ran in dev.
|
|
2285
|
+
|
|
2286
|
+
Both spellings now work: `:key.txt` in a route pattern, and `[inKey].txt.tsx` as a file route. The
|
|
2287
|
+
parameter name is the longest identifier run after `:`; everything else in the segment is literal.
|
|
2288
|
+
Precedence is static > mixed > param > wildcard, decided by shape rather than registration order, so
|
|
2289
|
+
`/robots.txt` still beats `/:key.txt` and `/jobs/:id.txt` beats `/jobs/:id`.
|
|
2290
|
+
|
|
2291
|
+
Inside a mixed segment, `[[optional]]` and `[...catchAll]` are **rejected** at build time rather than
|
|
2292
|
+
given a meaning: there is no sensible absent form for `/[[locale]]-feed.xml`, and a catch-all captures
|
|
2293
|
+
the rest of the path, which a trailing literal can never follow.
|
|
2294
|
+
|
|
2295
|
+
**Literal colons keep their meaning.** A `:` that follows an identifier character and runs to the end
|
|
2296
|
+
of its segment is literal, so the established RPC-style action shape - `/v1/things:batchGet` - still
|
|
2297
|
+
routes as written rather than capturing `batchGet` into a parameter named after the verb. Mixed
|
|
2298
|
+
parameters remain available everywhere they are unambiguous: at the start of a segment (`/:key.txt`),
|
|
2299
|
+
after punctuation (`/post-:id`), or with a literal suffix (`/v:major.json`). A `:` not followed by a
|
|
2300
|
+
valid identifier start (`/ratio:2`) is literal as before.
|
|
2301
|
+
|
|
2302
|
+
Mixed siblings are ordered by ONE total comparator shared between the server's trie router and the
|
|
2303
|
+
browser's matcher. Ordering by literal weight alone left ties broken differently on each side, so
|
|
2304
|
+
`/bar.:value` and `/:value.foo` could resolve to different routes for the same URL - visible only as a
|
|
2305
|
+
soft navigation rendering the wrong page.
|
|
2306
|
+
|
|
2307
|
+
Adding a mixed pattern can also make a previously unambiguous path ambiguous: with both `/jobs/:id` and
|
|
2308
|
+
`/jobs/:id.txt` registered, `/jobs/a.txt` now matches the mixed route with `id="a"` where before it
|
|
2309
|
+
could only match the bare param with `id="a.txt"`. Deterministic, and only for apps that opt in by
|
|
2310
|
+
registering a mixed pattern.
|
|
2311
|
+
|
|
2312
|
+
An app that registers no mixed segment allocates nothing for this and pays one `undefined` check on
|
|
2313
|
+
the match path. The rejected-parameter hint added in the previous release is removed - `:id.json` was
|
|
2314
|
+
the shape it explained, and `:id.json` now compiles.
|
|
2315
|
+
|
|
2316
|
+
- a7d740a: Mount sub-apps and standalone-shaped backends without a `Proxy`; bound the render worker; lint removed imports.
|
|
2317
|
+
|
|
2318
|
+
**`mounts` and `apiStrip` on `createWebApp`.** Two shapes previously needed a hand-written ~40-line
|
|
2319
|
+
`Proxy` around the backend. The auto-mount dispatches the full `/api/v1/forms`, but a backend that also
|
|
2320
|
+
runs standalone declares its routes without the prefix and lets its own shell supply it - so every
|
|
2321
|
+
request 404'd inside it. And better-auth is not a `backend` route, so `/api/auth/*` hit the backend and
|
|
2322
|
+
404'd silently. `apiStrip: true` removes the prefix before dispatch, and `mounts` takes any
|
|
2323
|
+
`{ path, app: { fetch } }` - so any library exposing its routes in that shape mounts directly, with no
|
|
2324
|
+
dependency from `@nifrajs/web` on it. Mounts are matched longest-path-first and before the `api`
|
|
2325
|
+
prefix, so `/api/auth` wins over `/api` regardless of declaration order.
|
|
2326
|
+
|
|
2327
|
+
**A layout that exports a `loader` now fails loudly.** Layouts do not run one - only route files do -
|
|
2328
|
+
and rendering while ignoring the export was the worst possible handling: it looks wired, the page
|
|
2329
|
+
renders, and the data is simply never there. The error names the file and says where the fetch should
|
|
2330
|
+
go instead. Running loaders in layouts is a real feature and is not this change.
|
|
2331
|
+
|
|
2332
|
+
**`nifra_render` and `nifra_run` can no longer hang.** The cold-path child wrote its result and then
|
|
2333
|
+
fell off the end of the module without exiting, unlike the warm-worker branch beside it. Loading an
|
|
2334
|
+
app runs its module side effects, so a database pool, a Redis client, or an interval kept the child's
|
|
2335
|
+
event loop alive forever while the parent waited on `proc.exited`. The child now exits explicitly, and
|
|
2336
|
+
both the cold and warm paths carry a 30s timeout as a backstop, reporting the likely cause rather than
|
|
2337
|
+
hanging.
|
|
2338
|
+
|
|
2339
|
+
**A `removed-import` lint in `nifra check`.** `@nifrajs/budget` folded into core in 2.x with no npm
|
|
2340
|
+
deprecation - `latest` is still 1.13.0, so a `^2` range resolves to nothing and `bun install` fails
|
|
2341
|
+
workspace-wide with an error naming neither cause nor replacement. The 2.0 WebSocket change has the
|
|
2342
|
+
same shape: `import "@nifrajs/core/ws"` no longer installs the runtime, and a consuming package kept
|
|
2343
|
+
it while its whole test suite stayed green and the app could not boot. Both are now caught before
|
|
2344
|
+
boot, with the replacement named. The WS rule flags only the bare side-effect form, since the module
|
|
2345
|
+
still exports `websocket` and a rule that fires on correct code gets ignored.
|
|
2346
|
+
|
|
2347
|
+
- 6e996a1: A dev/prod parity gate, and the CSS Modules divergence it immediately found.
|
|
2348
|
+
|
|
2349
|
+
nifra runs two pipelines and each is internally coherent: dev is Vite end to end, production is Bun end
|
|
2350
|
+
to end. That split is deliberate. What is not acceptable is the two regimes disagreeing about a fact an
|
|
2351
|
+
app depends on, because that failure always presents the same way - as "it worked locally", discovered
|
|
2352
|
+
after a deploy. The gate builds one fixture app through both pipelines and asserts they agree on four
|
|
2353
|
+
facts, each of which is a bug that already shipped or the mechanism behind one: the served `public/` set
|
|
2354
|
+
(byte-for-byte, not just the path list), that a module imported by two routes stays one module, CSS
|
|
2355
|
+
Modules behaviour, and the route manifest.
|
|
2356
|
+
|
|
2357
|
+
Scoped CSS class names are deliberately not compared. A scoped name never crosses the regime boundary,
|
|
2358
|
+
since each regime compiles both of its own halves, so requiring equal hashes would freeze both naming
|
|
2359
|
+
schemes forever while proving nothing. What is compared is the contract: the same exported keys, every
|
|
2360
|
+
one actually scoped, and `:global` left alone on both sides.
|
|
2361
|
+
|
|
2362
|
+
That comparison found a real divergence on its first run. `@keyframes` names are part of the CSS Modules
|
|
2363
|
+
export namespace - postcss-modules exports them, so Vite does, so nifra's dev pipeline did - but the Bun
|
|
2364
|
+
plugin omitted them. `styles.spin` was therefore a usable scoped name in dev and `undefined` in
|
|
2365
|
+
production, with no error at either end. Keyframe names are now exported, so anything reaching for one
|
|
2366
|
+
(`style={{ animationName: styles.spin }}`) behaves the same in both.
|
|
2367
|
+
|
|
2368
|
+
When a file has both a class and a keyframe under one name, the class wins the export. That resolution is
|
|
2369
|
+
fixed by construction rather than by declaration order, because a name that has to agree across two
|
|
2370
|
+
pipelines cannot depend on which rule was seen first; the keyframe stays scoped in the stylesheet under
|
|
2371
|
+
its own distinctly salted name either way.
|
|
2372
|
+
|
|
2373
|
+
`createViteDevServer` also now reports the port it actually bound rather than the one it was asked for.
|
|
2374
|
+
They differ for `port: 0` - the way to ask the OS for a free port, and what a test or a second app wants -
|
|
2375
|
+
where it previously echoed back a literal `0`, which connects to nothing.
|
|
2376
|
+
|
|
2377
|
+
- 6aa0aac: Add `previewEndpoint` for draft/preview mode, and make transport codec decode errors uniform.
|
|
2378
|
+
|
|
2379
|
+
`previewEndpoint({ secret, draftSecret })` is a `fetch` handler for the link your CMS's "Preview"
|
|
2380
|
+
button points at: it checks the preview token in constant time, turns draft mode on with the signed
|
|
2381
|
+
`__nifra_draft` cookie, and redirects the editor to the requested page. It is the link-borne sibling
|
|
2382
|
+
of `revalidateEndpoint`, and it exists because gating the route by hand means writing two checks that
|
|
2383
|
+
are easy to get subtly wrong and that fail silently when you do - the token compare must not exit
|
|
2384
|
+
early on the first wrong character, and the `?to=` destination must not be allowed off-site
|
|
2385
|
+
(`//evil.com` and `/\evil.com` both start with a slash yet navigate away). Wrong or missing token
|
|
2386
|
+
gives `401`, an off-site destination `400`, and success a `302` carrying `Cache-Control: no-store`
|
|
2387
|
+
so no shared cache can replay one editor's draft session to a visitor. Param names, the fallback
|
|
2388
|
+
destination, and cookie lifetime/path/`Secure` are all configurable.
|
|
2389
|
+
|
|
2390
|
+
`decodeTransportFrame` and `decodeTransportResponse` now raise `TransportCodecError` for a malformed
|
|
2391
|
+
payload instead of letting the underlying `SyntaxError` through, with the original kept as `cause`.
|
|
2392
|
+
Every other failure in that module was already a `TransportCodecError`, so a malformed payload - the
|
|
2393
|
+
likeliest hostile input - was the one case that slipped past callers catching the documented error
|
|
2394
|
+
type. `TransportCodecError` accepts an `ErrorOptions` second argument to carry that cause. Bytes that
|
|
2395
|
+
are not valid UTF-8 take the same path: the `TypeError` from the strict decoder used to escape ahead
|
|
2396
|
+
of any codec, so the one input that never reached a codec at all was also the one that reported
|
|
2397
|
+
differently from every other decode failure.
|
|
2398
|
+
|
|
2399
|
+
- 1857d39: Serve `public/` in production, not just in dev.
|
|
2400
|
+
|
|
2401
|
+
`nifra dev` served a project's `public/` directory; production did not. There was no `publicDir`
|
|
2402
|
+
concept anywhere - dev got the behaviour for free because the HMR path runs on Vite, and Vite serves
|
|
2403
|
+
`public/` by default. So a file worked all the way through development and 404'd the moment it was
|
|
2404
|
+
deployed, and every app had to notice this and hand-roll static serving in its own server entry.
|
|
2405
|
+
|
|
2406
|
+
The failure is inverted, which is what makes it expensive: it appears only in production, and only for
|
|
2407
|
+
the assets nobody smoke-tests. It has already shipped once as a self-hosted webfont that 404'd in prod
|
|
2408
|
+
and silently fell back to a system font. Nothing errored and nothing alerted.
|
|
2409
|
+
|
|
2410
|
+
`publicDir` (default `"public"`) is now a first-class option. The build copies the directory into the
|
|
2411
|
+
output and records the file list on the build manifest, and `servePublicDir` is exported for a server
|
|
2412
|
+
entry to mount. Dev routes through the **same** handler rather than inheriting Vite's - two code paths
|
|
2413
|
+
with different defaults was the whole bug, so there is now one owner.
|
|
2414
|
+
|
|
2415
|
+
Behaviour, matching what apps arrived at independently: only paths with a file extension are probed,
|
|
2416
|
+
so a page route never pays a filesystem stat; a miss falls through to routing, so no route is shadowed;
|
|
2417
|
+
and cache headers differ by subtree - content-hashed `/assets/*` immutable, `public/` a day, both
|
|
2418
|
+
overridable. Path traversal is confined by resolving and then verifying containment, rather than
|
|
2419
|
+
scanning the input for `..` - a blocklist over encodings is the version that gets bypassed - and
|
|
2420
|
+
percent-encoded and NUL-bearing paths have tests.
|
|
2421
|
+
|
|
2422
|
+
Note that `publicPath` is a different thing: the URL prefix for content-hashed bundle chunks. It never
|
|
2423
|
+
covered user-authored files, and the name similarity actively misleads.
|
|
2424
|
+
|
|
2425
|
+
`nifra check` now points out that an app with a `public/` directory can delete its hand-rolled static
|
|
2426
|
+
serving. A tip rather than a finding - an existing handler still works, since it runs first.
|
|
2427
|
+
|
|
2428
|
+
On Cloudflare Pages the copied files are named individually in `_routes.json` so the CDN serves them
|
|
2429
|
+
without invoking the worker, within Cloudflare's cap of 100 include+exclude rules of at most 100
|
|
2430
|
+
characters each. An ordinary `public/` of icons, fonts and share images clears that cap, and the
|
|
2431
|
+
rejection lands at `wrangler pages deploy` - after a build that reported success.
|
|
2432
|
+
|
|
2433
|
+
Past the cap a directory is compacted to one `/dir/*` rule, but only after checking it against the
|
|
2434
|
+
app's real route patterns. The glob does not merely describe today's files; it hands Pages every future
|
|
2435
|
+
path under that prefix, so a `public/blog/hero.png` beside a `/blog/:slug` route would send
|
|
2436
|
+
`/blog/my-post` to a CDN that has no such file and 404 the page in production only. A directory
|
|
2437
|
+
therefore collapses only when no route can be served beneath it, and a single route with a dynamic
|
|
2438
|
+
first segment (`/:locale/…`) disables collapsing everywhere, since it can match under any name. A
|
|
2439
|
+
collapsed directory does give up the app's 404 page for a missing file beneath it - the right trade for
|
|
2440
|
+
a directory of static files, where a missing image should fail as a fast CDN 404 rather than an HTML
|
|
2441
|
+
error page.
|
|
2442
|
+
|
|
2443
|
+
Whatever still does not fit is dropped rather than widened, which is safe because the list is only an
|
|
2444
|
+
optimization: the emitted worker serves any path it recognises through the `ASSETS` binding, so an
|
|
2445
|
+
omitted file costs one worker invocation rather than a 404, and nothing else reaches that binding. The
|
|
2446
|
+
build prints how many it left out, since a cap you cannot see reads as coverage you do not have.
|
|
2447
|
+
|
|
2448
|
+
- 6ba3173: `nifra routes --modes` - every route's render mode, hydration, and cache policy, gated against the target.
|
|
2449
|
+
|
|
2450
|
+
The facts were always in the route modules: `prerender`, `getStaticPaths`, `revalidate`, `hydrate`.
|
|
2451
|
+
Nowhere read them together. Answering "which pages are static?", "which revalidate, how often?", "which
|
|
2452
|
+
ship no JS?" meant opening every route file and holding the answer in your head - and the answer changes
|
|
2453
|
+
with the deploy target, which is nowhere near the route file.
|
|
2454
|
+
|
|
2455
|
+
The new `@nifrajs/web/route-manifest` derives one record per route - static | isr | ssr, hydration, cache
|
|
2456
|
+
policy - and resolves it against a target. `nifra routes --modes` prints the table; `--modes --target <t>`
|
|
2457
|
+
gates: a route the target cannot honour exits non-zero, so CI fails the build instead of production
|
|
2458
|
+
failing the request. The two cases that were previously silent are exactly the ones it catches: an ssr or
|
|
2459
|
+
isr route in a `static` build (no server, so the URL 404s in production while working in dev), and ISR
|
|
2460
|
+
where the target has no revalidation. Each conflict names the consequence, not the rule.
|
|
2461
|
+
|
|
2462
|
+
Two derivations are deliberate. `prerender` wins over `revalidate`, because a page rendered at build time
|
|
2463
|
+
is not revalidated at runtime - the build-time answer is the one that ships. And a dynamic route counts as
|
|
2464
|
+
static only once the build has actually emitted paths for it: `getStaticPaths` is the intent, the emitted
|
|
2465
|
+
paths are the evidence, and treating intent as sufficient is how a "static" build ships a page that 404s.
|
|
2466
|
+
Pass the paths `prerenderRoutes` produced via `buildRouteManifest`'s `prerendered` option to resolve those
|
|
2467
|
+
routes against what was really built.
|
|
2468
|
+
|
|
2469
|
+
- 0fc215b: The Vite dev pipeline now resolves SSR too, which removes the dual-React class of bug.
|
|
2470
|
+
|
|
2471
|
+
`nifra dev` served the client through Vite while resolving route modules for SSR through Bun - two
|
|
2472
|
+
resolvers disagreeing about one specifier, inside one process. That is what made `resolve.dedupe`
|
|
2473
|
+
govern only half the app: an app added a hand-written React alias in `nifra.config.ts`, and it still
|
|
2474
|
+
crashed, because the alias reached Vite and SSR never asked Vite. The symptom named a React internal
|
|
2475
|
+
(`resolveDispatcher().useState` is null), so the actual cause - two copies at different paths - was
|
|
2476
|
+
hours of inference away.
|
|
2477
|
+
|
|
2478
|
+
`discoverRoutes` accepts a `load` option, and the Vite dev server passes `ssrLoadModule`. Both halves
|
|
2479
|
+
now resolve through the same toolchain, so an alias, a `dedupe`, or a condition configured for the
|
|
2480
|
+
client governs the server as well.
|
|
2481
|
+
|
|
2482
|
+
Two things follow from that and are removed rather than kept "just in case":
|
|
2483
|
+
|
|
2484
|
+
- **Bun's SFC plugins are no longer registered in `nifra dev`.** Vite compiles `.vue` / `.svelte` /
|
|
2485
|
+
Solid for SSR through the app's `vitePlugins`. Registering Bun's alongside was the intermix itself -
|
|
2486
|
+
two toolchains compiling one file, only one of them governed by Vite's resolution. `nifra start` and
|
|
2487
|
+
the build path keep theirs, which is correct: those are the Bun pipeline.
|
|
2488
|
+
- **The `importQuery` cache-buster is gone from the Vite path.** It existed to defeat Bun's import
|
|
2489
|
+
cache; Vite re-evaluates changed modules itself. `discoverRoutes` ignores `importQuery` when `load`
|
|
2490
|
+
is supplied, because appending one would mint a new module id per request and defeat that.
|
|
2491
|
+
|
|
2492
|
+
The dev server still re-creates the app on change, now only so a hard reload picks up a route add or
|
|
2493
|
+
remove - the manifest comes from a directory scan, which `ssrLoadModule` cannot invalidate.
|
|
2494
|
+
|
|
2495
|
+
- 2ff661f: The full Vite/Rollup production build: `buildClientVite`, `buildServerVite`, `buildTargetVite`, and `nifra build --vite`.
|
|
2496
|
+
|
|
2497
|
+
Production stays Bun by default - faster and Bun-native, the profile nifra is tuned for. This completes
|
|
2498
|
+
the escape hatch for the one case that default cannot serve: an app whose client needs a Vite-only
|
|
2499
|
+
transform with no Bun equivalent. It now has a real, full production path, not just the leak-guard plugin.
|
|
2500
|
+
|
|
2501
|
+
The design point is that this is NOT a second orchestrator. `buildTarget`'s deploy assembly - per-target
|
|
2502
|
+
server-entry codegen, `_worker.js` / `server.js` placement, `_routes.json`, prerender, size report - is
|
|
2503
|
+
bundler-agnostic and now lives behind `buildTargetWith(target, options, bundler)`, which takes a `Bundler`
|
|
2504
|
+
strategy (its two bundling steps). `buildTarget` passes the Bun strategy; `buildTargetVite` passes the
|
|
2505
|
+
Vite one. So the deploy-dir shape is produced in exactly one place and cannot drift between pipelines -
|
|
2506
|
+
`buildTargetVite` emits the identical directory for every target.
|
|
2507
|
+
|
|
2508
|
+
`buildClientVite` reconstructs the same `BuildManifest` the Bun `buildClient` does - content-hashed entry,
|
|
2509
|
+
per-route chunk lists, aggregate + per-route CSS, copied `public/` - from Vite's own `.vite/manifest.json`.
|
|
2510
|
+
It wires `viteLeakGuard()`, and it keeps `node:` builtins external so the guard names the offending builtin:
|
|
2511
|
+
left alone, rolldown-vite silently rewrites a `node:` import to a browser stub, which builds and ships and
|
|
2512
|
+
is a no-op at runtime - a worse footgun than Bun's polyfill, now a build failure. `buildServerVite`
|
|
2513
|
+
produces the same self-contained `ServerBuild`, tagging the bundle `NIFRA_SSR_BUNDLED` so the web-react
|
|
2514
|
+
adapter uses the bundled, deduped react-dom rather than re-rooting to a disk copy.
|
|
2515
|
+
|
|
2516
|
+
`nifra build --vite` selects the pipeline; the app's Vite plugins drive both halves. Verified end to end by
|
|
2517
|
+
building the React example for the `node` target, running the server, and confirming SSR plus live
|
|
2518
|
+
hydration in a real browser, alongside `cf-pages` (`_worker.js` + `_routes.json`) and `static` (prerender,
|
|
2519
|
+
no server) deploy shapes.
|
|
2520
|
+
|
|
2521
|
+
No change to the default Bun build: `buildTarget` now delegates to `buildTargetWith` with the Bun strategy,
|
|
2522
|
+
and its behaviour and output are identical.
|
|
2523
|
+
|
|
2524
|
+
- a1327a4: A Vite/Rollup production build carries the same client-leak guards as the Bun build.
|
|
2525
|
+
|
|
2526
|
+
Production stays Bun by default - it is faster and Bun-native, and that is what nifra competes on. But
|
|
2527
|
+
some apps need a Vite-only transform with no Bun equivalent, and for those a Vite/Rollup production client
|
|
2528
|
+
build is the escape hatch. The moment that hatch exists, the two client-leak guards have to come with it:
|
|
2529
|
+
they are security guards, not lints - one stops secrets and database access shipping to a browser - and a
|
|
2530
|
+
second production pipeline arriving without them, or with a "mostly ported" copy, is exactly the failure
|
|
2531
|
+
the bundler-neutral module graph was introduced to prevent.
|
|
2532
|
+
|
|
2533
|
+
`@nifrajs/web/plugins/vite-leak-guard` is not a second implementation. It adapts Rollup's bundle into the
|
|
2534
|
+
neutral `ClientModuleGraph` (`fromRollupBundle`) and runs the SAME `detectNodeBuiltinsInClient` /
|
|
2535
|
+
`detectServerOnlyInClient` the Bun build runs, through the SAME failure messages - now extracted so both
|
|
2536
|
+
pipelines share one owner. A `node:` builtin or a `server-only`-marked module reaching the client fails
|
|
2537
|
+
the build identically whichever bundler produced it, including via dynamic `import()`.
|
|
2538
|
+
|
|
2539
|
+
One adapter subtlety is load-bearing: the guards locate a leak by finding the builtin inside a chunk's
|
|
2540
|
+
module list. Bun bundles the `node:` polyfill, so it appears there; Rollup externalizes `node:`, so it
|
|
2541
|
+
never would - the adapter synthesizes the builtin into the importing chunk so the neutral graph is
|
|
2542
|
+
identical to Bun's. Proven with a real `vite build` that fails on a real leak (static and dynamic) and
|
|
2543
|
+
passes clean code, alongside unit tests asserting the two adapters yield the same finding.
|
|
2544
|
+
|
|
2545
|
+
No behaviour change for existing Bun builds: the guard logic and messages are unchanged, only relocated
|
|
2546
|
+
behind shared formatters.
|
|
2547
|
+
|
|
2548
|
+
### Patch Changes
|
|
2549
|
+
|
|
2550
|
+
- d428f52: `nifra dev` names a port collision instead of dying inside `node:events`.
|
|
2551
|
+
|
|
2552
|
+
Starting the dev server while an earlier one still holds the port produced a raw internal stack from
|
|
2553
|
+
Node's event emitter and killed the new process in the background. The old server kept answering on that
|
|
2554
|
+
port, so the browser carried on rendering the previous build. The symptom that reaches the developer is
|
|
2555
|
+
not "the port is taken" but "my edits stopped reaching SSR" - which reads as broken HMR or a stale module
|
|
2556
|
+
graph and sends you looking anywhere but at the one process that never started.
|
|
2557
|
+
|
|
2558
|
+
Binding now fails with the port named, the stale-output consequence spelled out, and both fixes given
|
|
2559
|
+
with the port already substituted: free it, or take the next one. Vite is torn down on that path, because
|
|
2560
|
+
by the time the bind is attempted its watchers and dep optimizer are holding the event loop open - a
|
|
2561
|
+
handled error alone would leave the process printing a diagnosis and then hanging on it, which looks
|
|
2562
|
+
exactly like a dev server still starting up. Other bind failures pass through unchanged rather than
|
|
2563
|
+
inheriting port-collision advice that would not apply.
|
|
2564
|
+
|
|
2565
|
+
- 135d0c6: Find duplicate installs when `nifra check` runs from an app subdirectory.
|
|
2566
|
+
|
|
2567
|
+
The duplicate-install check anchored its discovery at the directory it was run from. In a monorepo you
|
|
2568
|
+
run it from the app - `apps/web` - and that manifest declares no `workspaces`, so the scan collapsed to
|
|
2569
|
+
the app itself and the sibling package holding the second copy was never probed. It printed
|
|
2570
|
+
`✓ duplicate identity-sensitive dependency install: none` while the dev server returned 500 on every
|
|
2571
|
+
page using a shared-kit hook, from exactly the condition the check exists to detect. Running from an
|
|
2572
|
+
app subdirectory is the normal case, so that was the configuration it was blind in.
|
|
2573
|
+
|
|
2574
|
+
Discovery now walks up to the workspace root that actually governs the directory, and probes from
|
|
2575
|
+
there. An ancestor is adopted only when its `workspaces` patterns genuinely match - a parent that
|
|
2576
|
+
merely contains a manifest is not this project's root - and the walk stops at a `.git` boundary.
|
|
2577
|
+
Findings are still reported relative to where you ran the command.
|
|
2578
|
+
|
|
2579
|
+
**Expect this to start reporting real findings in monorepos that were previously green.** That is the
|
|
2580
|
+
point rather than a regression: the duplicate was always there, and the check simply never looked in
|
|
2581
|
+
the right place.
|
|
2582
|
+
|
|
2583
|
+
An SSR error carrying a duplicate-instance signature (`resolveDispatcher()`, `Invalid hook call`, a
|
|
2584
|
+
null hook read) now names the likely cause. Two copies at the same version still fail, because module
|
|
2585
|
+
identity is path-based, and the raw error points at a React internal - so the message now says two
|
|
2586
|
+
copies are installed at different paths, that matching versions do not fix it, and to run
|
|
2587
|
+
`nifra check` for the paths.
|
|
2588
|
+
|
|
2589
|
+
- 15ad6ca: Enforce pipeline separation, and make the client-leak guards bundler-neutral.
|
|
2590
|
+
|
|
2591
|
+
nifra supports both Vite and Bun, and the config already keeps them in separate slots - `vitePlugins`
|
|
2592
|
+
for the Vite pipeline, `clientPlugins` / `serverPlugins` for the Bun one. Nothing enforced the split,
|
|
2593
|
+
and the failure mode is silent: `Bun.build` has no `transform` hook and Vite never calls `setup`, so a
|
|
2594
|
+
plugin in the wrong slot is accepted, never invoked, and the build succeeds with the transform simply
|
|
2595
|
+
missing.
|
|
2596
|
+
|
|
2597
|
+
Loading an app now refuses that config, naming the plugin, the slot it is in, the pipeline that slot
|
|
2598
|
+
feeds, and where to move it. Checked at load rather than in `nifra check`, which holds a deliberate
|
|
2599
|
+
pre-`loadApp` invariant - reading plugins means importing the app's config. So `dev`, `build` and
|
|
2600
|
+
`start` are covered from one place, immediately before the plugins reach a bundler. Detection is by
|
|
2601
|
+
hook shape and deliberately conservative: a plugin matching neither shape is left alone, because a
|
|
2602
|
+
guard that fires on correct config is a guard people turn off.
|
|
2603
|
+
|
|
2604
|
+
The two client-leak guards - server-only code reaching the browser, `node:` builtins in client code -
|
|
2605
|
+
now take a bundler-neutral module graph instead of Bun's metafile, with a `fromBunMetafile` adapter
|
|
2606
|
+
behind it. Nothing changes today: Bun remains the only producer and the existing 19 tests pass
|
|
2607
|
+
unchanged, now routed through the adapter so it is covered too.
|
|
2608
|
+
|
|
2609
|
+
The point is what it makes possible. These are security guards - one stops secrets and database access
|
|
2610
|
+
shipping to a browser - so a second production pipeline must not arrive without them, and porting them
|
|
2611
|
+
under pressure beside a new bundler is how a guard ends up "mostly" ported. Introducing the seam while
|
|
2612
|
+
Bun is the only producer means the adapter can be verified against known-good behaviour, and adding
|
|
2613
|
+
Rollup later is one more adapter rather than a second copy of the detection logic.
|
|
2614
|
+
|
|
2615
|
+
- ca71a2e: `nifra_types` collapses oversized declarations in search results, and a bad `clientModule` says so.
|
|
2616
|
+
|
|
2617
|
+
**`nifra_types` query mode.** A search returned the complete declaration of every match, and the corpus
|
|
2618
|
+
is wildly uneven: the median symbol is small while `Server` alone is ~32,000 characters, five times the
|
|
2619
|
+
next largest. One broad query that happened to match it returned the entire class for near-zero value.
|
|
2620
|
+
|
|
2621
|
+
A `query` now returns a one-line summary plus the signature, collapsing an oversized body to its head
|
|
2622
|
+
and a member count, and saying which `name` call returns the rest. Measured on the real corpus:
|
|
2623
|
+
`"server"` drops from 36,355 to 1,739 characters (95%), `"route schema"` by 87%, `"rate limit"` by 69%.
|
|
2624
|
+
|
|
2625
|
+
An exact `name` lookup is **never** collapsed - there the caller asked for that symbol. Pass
|
|
2626
|
+
`full: true` to opt a query back into whole declarations.
|
|
2627
|
+
|
|
2628
|
+
**`clientModule`.** The option is a module specifier resolved by the bundler, so nothing type-checks
|
|
2629
|
+
that the module actually exports `mountRouter`. A self-executing client entry therefore built cleanly
|
|
2630
|
+
and failed at first paint with `mountRouter is not a function`, from inside a bundled chunk, naming
|
|
2631
|
+
neither the module nor the requirement - diagnosable only by reading `build.ts`.
|
|
2632
|
+
|
|
2633
|
+
The generated bootstrap now throws immediately, naming the offending specifier, the missing export, its
|
|
2634
|
+
call signature, and the specific trap that a self-executing entry will not work. The contract is also
|
|
2635
|
+
spelled out on the option's own type rather than in a parenthetical.
|
|
2636
|
+
|
|
2637
|
+
- 2500705: The Vite production build now works on runtimes whose `Error.captureStackTrace` is stricter than V8's.
|
|
2638
|
+
|
|
2639
|
+
`captureStackTrace` is a V8 API and V8 decorates any object handed to it. Some runtimes require a real
|
|
2640
|
+
Error - one with the internal slot, which an object merely inheriting `Error.prototype` does not have -
|
|
2641
|
+
and throw `First argument must be an Error object`.
|
|
2642
|
+
|
|
2643
|
+
Vite bundles `follow-redirects`, which defines its error types the pre-class way:
|
|
2644
|
+
|
|
2645
|
+
CustomError.prototype = new (baseClass || Error)()
|
|
2646
|
+
|
|
2647
|
+
That constructs the base class while defining the subclass, so `captureStackTrace` receives an object
|
|
2648
|
+
that inherits from Error but was never built by it. On a strict runtime the throw happens while vite's
|
|
2649
|
+
own module is still evaluating, so `import("vite")` fails outright and every Vite build dies with a
|
|
2650
|
+
message about stack traces that names nothing about vite.
|
|
2651
|
+
|
|
2652
|
+
`loadVite` now probes for that strictness and, only when present, restores the V8 contract: it delegates
|
|
2653
|
+
to the runtime and swallows the refusal, since decorating a stack is best-effort. A runtime that already
|
|
2654
|
+
follows V8 is left untouched.
|
|
2655
|
+
|
|
2656
|
+
Also: a Vite build that fails for any reason no longer reports "vite is not installed" when vite is
|
|
2657
|
+
installed and merely failed to load - a resolution failure and an evaluation failure are now described
|
|
2658
|
+
as what they are.
|
|
2659
|
+
|
|
2660
|
+
- Updated dependencies [5f460db]
|
|
2661
|
+
- Updated dependencies [e713cab]
|
|
2662
|
+
- Updated dependencies [a4645e2]
|
|
2663
|
+
- Updated dependencies [6aa0aac]
|
|
2664
|
+
- @nifrajs/core@2.2.0
|
|
2665
|
+
|
|
2666
|
+
## 2.1.0
|
|
2667
|
+
|
|
2668
|
+
### Minor Changes
|
|
2669
|
+
|
|
2670
|
+
- bd294bb: Add `executeCapability()` as a correlated, policy-aware effect boundary.
|
|
2671
|
+
|
|
2672
|
+
- Correlate intent and terminal evidence with a random `effectId`, record committed/failed outcomes
|
|
2673
|
+
automatically, and combine request cancellation with bounded async `aroundCapability()` admission
|
|
2674
|
+
policies while preserving the synchronous `useCapability()` path.
|
|
2675
|
+
- Retain idempotency results for every completed response, including non-2xx outcomes, so a retry
|
|
2676
|
+
cannot repeat an effect that succeeded before a later handler failure.
|
|
2677
|
+
- Add durable approval, effect journal, saga/compensation, and reconciliation primitives behind the
|
|
2678
|
+
`durable-execution` subpath, plus token-only OpenTelemetry effect spans from `@nifrajs/otel/effects`.
|
|
2679
|
+
Reconciliation supports bounded cursor pages, approval resume tokens stay out of ordinary error
|
|
2680
|
+
serialization, durable terminal states are monotonic, crash ambiguity has an effect-ID-bound operator
|
|
2681
|
+
resolution API, and unmatched effect spans have bounded retention.
|
|
2682
|
+
- Add one shared owned-effect scope across capabilities, saga execution, compensation, idempotency
|
|
2683
|
+
evidence, durable transitions, and telemetry. An explicit `markIdempotencySafeToRetry()` outcome
|
|
2684
|
+
releases a resolved 5xx only while the scope proves no effect began.
|
|
2685
|
+
- Add negotiated, versioned transport codecs with bounded plain-JSON and rich-wire adapters for HTTP,
|
|
2686
|
+
the typed client, loader NDJSON, and WebSocket frames.
|
|
2687
|
+
- Add Postgres, SQLite, and Durable Object durable-execution adapters with one reusable conformance
|
|
2688
|
+
suite, plus leased reconciliation workers with bounded pages/concurrency, durable cursors, filters,
|
|
2689
|
+
cancellation, backpressure, and token-only metrics.
|
|
2690
|
+
|
|
2691
|
+
### Patch Changes
|
|
2692
|
+
|
|
2693
|
+
- Updated dependencies [bd294bb]
|
|
2694
|
+
- Updated dependencies [d3aac63]
|
|
2695
|
+
- @nifrajs/core@2.1.0
|
|
2696
|
+
|
|
2697
|
+
## 2.0.0
|
|
2698
|
+
|
|
2699
|
+
### Major Changes
|
|
2700
|
+
|
|
2701
|
+
- d91a45b: Remove Nifra's remaining deprecated and compatibility-only public surfaces for the 2.0 cutover.
|
|
2702
|
+
|
|
2703
|
+
- `@nifrajs/core` and `nifra` now expose only the lean HTTP server API at their package roots. Import
|
|
2704
|
+
optional systems from their documented subpaths. The deprecated invariant runner and the
|
|
2705
|
+
`@nifrajs/budget` compatibility package are removed; use `@nifrajs/testing` and
|
|
2706
|
+
`@nifrajs/core/budget` respectively.
|
|
2707
|
+
- Web redirects accept only an options object as their second argument, the prerender enumeration
|
|
2708
|
+
wrapper is removed in favor of `enumerateStaticRoutes()`, and fragment navigation resolves IDs only.
|
|
2709
|
+
- MCP Apps metadata uses only `_meta.ui.resourceUri`; the deprecated flat `ui/resourceUri` key is gone.
|
|
2710
|
+
- Telemetry uses `ObservationAdapter` directly; the `AgentSpan`, `AgentSpanExporter`, and `SpanExporter`
|
|
2711
|
+
aliases are removed.
|
|
2712
|
+
- Invalid HTTP method overrides always fail closed with 400; the legacy ignore mode is removed.
|
|
2713
|
+
- `nifra build` always emits a complete target deploy directory and defaults to Bun. The old
|
|
2714
|
+
client-only build branch is removed; `nifra start` runs the generated Bun `server.js`.
|
|
2715
|
+
|
|
2716
|
+
- d91a45b: The in-process backend mount is now exclusively the symbol-keyed `BackendMount` interface that `inProcessClient()` / `testClient()` implement.
|
|
2717
|
+
|
|
2718
|
+
`createWebApp({ api })` auto-mounts a backend only through that symbol seam - the platform-aware path that forwards `env` / `waitUntil`. The `.fetch(url, init)` mount convention is gone: an `api` that only exposes a callable `.fetch` is no longer auto-mounted. Backends passed as `inProcessClient(app)` / `testClient(app)` are unaffected, since they carry the symbol mount already.
|
|
2719
|
+
|
|
2720
|
+
### Minor Changes
|
|
2721
|
+
|
|
2722
|
+
- e97a92f: `nifra sync-manifest`, plus two toolchain guards that turn opaque failures into actionable ones.
|
|
2723
|
+
|
|
2724
|
+
- **`nifra sync-manifest`.** After adding/renaming/removing a page route, the committed `server-manifest.ts` drifts and `nifra check` flags it - and clearing that used to mean a full build (server + worker + migrate bundles). `nifra sync-manifest` re-scans `routes/` and rewrites just the manifest's route table in milliseconds, preserving the baked client-asset references. It does not rebuild the client bundle, so it prints a caveat: a brand-new hydrating route component still needs a full build for its client chunk. `@nifrajs/web/build` gains the pure `resyncServerManifestSource` (+ `parseManifestStyles` / `parseManifestRouteStyles`) it is built on.
|
|
2725
|
+
- **`nifra dev` peer preflight.** Run under `bunx @nifrajs/cli dev` (an isolated install where the project's peers do not resolve), the Vite import failed with an opaque `ERR_MODULE_NOT_FOUND`. It now checks `vite` resolves from the project first and, if not, says to run the workspace-local `bun run dev`.
|
|
2726
|
+
- **`nifra start` build-target guard.** Pointed at a Cloudflare Pages output (a `_worker.js` bundle, no `server.js`), `nifra start` now names the mismatch and the fix (`nifra build --target bun`, or serve with `wrangler pages`) instead of a bare "no server.js".
|
|
2727
|
+
|
|
2728
|
+
- e8e49d1: Two new build plugins for the `Bun.build` production step, both opt-in and dependency-free until used.
|
|
2729
|
+
|
|
2730
|
+
- **`postcssBunPlugin` (`@nifrajs/web/plugins/postcss`)** - runs `*.css` / `*.pcss` / `*.postcss` through PostCSS, feeding the result into the existing stylesheet pipeline (and the CSS-modules scoped-class transform for `*.module.*`). This is the Tailwind v4 path: a `postcss.config.js` with `@tailwindcss/postcss` compiles `app.css` importing `tailwindcss` at build time with no framework-specific code. `postcss` (and `postcss-load-config`, when you don't pass `plugins` explicitly) are optional peers, loaded lazily and failing loud with an install hint. Mirrors the SCSS plugin: pass `"dom"` for the client bundle, preload `"ssr"` for the server.
|
|
2731
|
+
|
|
2732
|
+
- **`svgComponentBunPlugin` (`@nifrajs/web/plugins/svg`)** - import an SVG as a component, `import Icon from "./icon.svg?component"`, then `<Icon className="w-6 h-6" />` with props spread onto the root `<svg>` (the Vite `svgr` workflow). Emits an automatic-JSX-runtime component, so it works for React and Preact today; Solid/Svelte/Vue are out of this version. Optional `svgo` optimization. A plain `import "./icon.svg"` asset URL is untouched - only the `?component` marker is intercepted.
|
|
2733
|
+
|
|
2734
|
+
- a7d34e5: Navigation loading UI for `@nifrajs/web-react/router`, plus a per-link pending signal.
|
|
2735
|
+
|
|
2736
|
+
nifra navigates imperatively - it fetches the next route's chunk and loader data while the current route stays on screen, then swaps - so a route transition is signalled by the router's `pending` flag, not a Suspense boundary.
|
|
2737
|
+
|
|
2738
|
+
- `useNavigation()` returns `{ pending, state: "idle" | "loading", location }` (Remix-shaped); `location` is the `pathname + search` being navigated to while pending. `usePending()` is the boolean form.
|
|
2739
|
+
- `NavLink`'s render-prop `isPending` is now real: it is `true` while a navigation to that link's own target is in flight (matched like `isActive`), so a link can show its own spinner. Previously always `false`.
|
|
2740
|
+
- The agnostic router now publishes `pendingPath` (the navigation target) on its state while `pending`, and `compose` threads `pending`/`pendingPath` into the router context. Both are `false`/absent on the server and the initial client render, so they are hydration-safe.
|
|
2741
|
+
|
|
2742
|
+
### Patch Changes
|
|
2743
|
+
|
|
2744
|
+
- ade0c7a: Add a curated `@nifrajs/core/server` entry for the common HTTP runtime and dedicated subpaths for
|
|
2745
|
+
contracts, classification, cookies, logging, routing, Standard Schema, SEO, SSE, and webhooks. The
|
|
2746
|
+
package root remains backwards compatible, while new scaffolds and first-party runtime packages avoid
|
|
2747
|
+
eagerly parsing opt-in causality, invariant, manifest, reflection, capability, and assurance tooling.
|
|
2748
|
+
- Updated dependencies [a7b1d60]
|
|
2749
|
+
- Updated dependencies [eaac3d7]
|
|
2750
|
+
- Updated dependencies [ade0c7a]
|
|
2751
|
+
- Updated dependencies [82676e0]
|
|
2752
|
+
- Updated dependencies [1522d06]
|
|
2753
|
+
- Updated dependencies [a7b1d60]
|
|
2754
|
+
- Updated dependencies [a7b1d60]
|
|
2755
|
+
- @nifrajs/core@2.0.0
|
|
2756
|
+
|
|
2757
|
+
## 1.13.0
|
|
2758
|
+
|
|
2759
|
+
### Minor Changes
|
|
2760
|
+
|
|
2761
|
+
- 5b6127a: Make route batches atomic, seal server configuration after `listen()`, encode array query values as
|
|
2762
|
+
repeated keys, and align web route matching with the server.
|
|
2763
|
+
|
|
2764
|
+
Three behavior changes to know about:
|
|
2765
|
+
|
|
2766
|
+
- **Configuring a server after `listen()` now throws** instead of reaching some traffic and not the
|
|
2767
|
+
rest. Bun's native route table is compiled when you listen, so a hook added afterwards applied to
|
|
2768
|
+
`app.fetch()` but not to real HTTP requests: an `onRequest` guard installed late was silently
|
|
2769
|
+
skipped on the wire. Register routes, hooks, plugins, and context before listening.
|
|
2770
|
+
- **Array query values serialize as repeated keys** (`?tag=a&tag=b`), not `?tag=a%2Cb`, so a route
|
|
2771
|
+
whose `query` schema declares an array now receives one.
|
|
2772
|
+
- **The web matcher applies the server's trailing-slash rule.** `/users/7/` no longer matches
|
|
2773
|
+
`/users/:id` in the browser, matching the 404 the server already returns, and a malformed percent
|
|
2774
|
+
encoding reports no route instead of throwing.
|
|
2775
|
+
|
|
2776
|
+
A route batch from `implement()` or `merge()` commits only once every route in it validates, so a
|
|
2777
|
+
collision partway through leaves matching and reflection untouched instead of stranding the routes
|
|
2778
|
+
registered before it.
|
|
2779
|
+
|
|
2780
|
+
Each route now owns one immutable compiled execution plan shared by portable, Node-direct, and
|
|
2781
|
+
Bun-native dispatch. This also fixes validation recovery being skipped when a derive moved a route
|
|
2782
|
+
from a specialized lane to the generic lifecycle.
|
|
2783
|
+
|
|
2784
|
+
Core, browser navigation, Bun-native parameter metadata, and mock routing now consume the same
|
|
2785
|
+
compiled pattern kernel. Static routes beat parameters and parameters beat wildcards regardless of
|
|
2786
|
+
manifest order, with one grammar, trailing-slash policy, and malformed-encoding rule.
|
|
2787
|
+
|
|
2788
|
+
### Patch Changes
|
|
2789
|
+
|
|
2790
|
+
- Updated dependencies [aae8614]
|
|
2791
|
+
- Updated dependencies [5b6127a]
|
|
2792
|
+
- @nifrajs/core@1.13.0
|
|
2793
|
+
|
|
2794
|
+
## 1.12.0
|
|
2795
|
+
|
|
2796
|
+
### Patch Changes
|
|
2797
|
+
|
|
2798
|
+
- Updated dependencies [63d3845]
|
|
2799
|
+
- Updated dependencies [246f498]
|
|
2800
|
+
- @nifrajs/core@1.12.0
|
|
2801
|
+
|
|
2802
|
+
## 1.11.0
|
|
2803
|
+
|
|
2804
|
+
### Minor Changes
|
|
2805
|
+
|
|
2806
|
+
- 5638ada: Add an explicit symbol-keyed in-process backend mount interface. `inProcessClient` implements the
|
|
2807
|
+
interface and `createWebApp` forwards the outer request's platform context through it, so an
|
|
2808
|
+
auto-mounted backend receives the same Workers `env` bindings and `waitUntil` lifetime as the web app.
|
|
2809
|
+
|
|
2810
|
+
The released `.fetch(url, init)` duck-typed mount remains as a compatibility fallback for custom
|
|
2811
|
+
bridges. `Server.onRequest` now receives the optional platform object as its second argument.
|
|
2812
|
+
|
|
2813
|
+
### Patch Changes
|
|
2814
|
+
|
|
2815
|
+
- Updated dependencies [2dde7e5]
|
|
2816
|
+
- Updated dependencies [279f80c]
|
|
2817
|
+
- Updated dependencies [5638ada]
|
|
2818
|
+
- Updated dependencies [279f80c]
|
|
2819
|
+
- @nifrajs/core@1.11.0
|
|
2820
|
+
|
|
2821
|
+
## 1.10.0
|
|
2822
|
+
|
|
2823
|
+
### Patch Changes
|
|
2824
|
+
|
|
2825
|
+
- Updated dependencies [92181be]
|
|
2826
|
+
- Updated dependencies [3773f0a]
|
|
2827
|
+
- Updated dependencies [92181be]
|
|
2828
|
+
- @nifrajs/core@1.10.0
|
|
2829
|
+
|
|
2830
|
+
## 1.9.1
|
|
2831
|
+
|
|
2832
|
+
### Patch Changes
|
|
2833
|
+
|
|
2834
|
+
- 3eb27ae: Internal tidy - remove a dead local variable in the query engine and clean up example wording in doc comments. No API or behavior change.
|
|
2835
|
+
- @nifrajs/core@1.9.1
|
|
2836
|
+
|
|
2837
|
+
## 1.9.0
|
|
2838
|
+
|
|
2839
|
+
### Minor Changes
|
|
2840
|
+
|
|
2841
|
+
- 0e1b4cc: Add a full React Query core on `@nifrajs/web-react/query` - `useQuery` (now with `enabled`/`staleTime`),
|
|
2842
|
+
`useMutation`, `useInfiniteQuery`, `useQueryClient`, `QueryClientProvider`, and the SSR
|
|
2843
|
+
`HydrationBoundary` - a drop-in for the TanStack Query surface, backed by an expanded agnostic engine in
|
|
2844
|
+
`@nifrajs/web`.
|
|
2845
|
+
|
|
2846
|
+
The engine (`createQueryClient`) gains imperative cache ops (`getQueryData`/`setQueryData` for optimistic
|
|
2847
|
+
updates, `prefetchQuery`), per-query `staleTime`, SSR `dehydrate`/`hydrate`, and paged (`infiniteQuery`)
|
|
2848
|
+
support; plus a standalone `createMutation` state machine (single-flight, TanStack callback order). All
|
|
2849
|
+
logic lives in the injected-clock, framework-free engine so it's deterministically tested; the React
|
|
2850
|
+
bindings are thin `useSyncExternalStore` wrappers. A hook without a `QueryClientProvider` uses a
|
|
2851
|
+
client-side singleton (SSR-idle); with a `HydrationBoundary`-fed provider client, queries render their
|
|
2852
|
+
server-seeded data during SSR with no hydration flash.
|
|
2853
|
+
|
|
2854
|
+
- 6b67833: Add first-class React routing bindings on the new `@nifrajs/web-react/router` subpath - `<Link>`,
|
|
2855
|
+
`<NavLink>`, `useNavigate`, `useParams`, `useLocation`, `useSearchParams`, and `<Navigate>` - a
|
|
2856
|
+
drop-in replacement for `react-router-dom`'s routing surface over nifra's own file-based router.
|
|
2857
|
+
|
|
2858
|
+
The read hooks are SSR-correct: `@nifrajs/web` now threads the matched route's `params` and the
|
|
2859
|
+
request `path` (`pathname + search`) through the render seam (`RenderProps`), and the React adapter's
|
|
2860
|
+
`compose` provides them via a `RouterContext` on both the server render and the client mount - so
|
|
2861
|
+
`useParams`/`useLocation`/`useSearchParams` return the same value on each side with no hydration
|
|
2862
|
+
mismatch. Programmatic navigation flows through a new DOM-free bridge (`getBrowserNavigate` /
|
|
2863
|
+
`setBrowserNavigate`, populated by `installHistory`), which also gains history `replace` support, so a
|
|
2864
|
+
route component reaches history-aware navigation without importing the browser-only client layer.
|
|
2865
|
+
|
|
2866
|
+
### Patch Changes
|
|
2867
|
+
|
|
2868
|
+
- Updated dependencies [03cd76f]
|
|
2869
|
+
- Updated dependencies [03cd76f]
|
|
2870
|
+
- @nifrajs/core@1.9.0
|
|
2871
|
+
|
|
2872
|
+
## 1.8.0
|
|
2873
|
+
|
|
2874
|
+
### Patch Changes
|
|
2875
|
+
|
|
2876
|
+
- 1ffd48b: fix(web): the static/client build no longer ships the generated `_nifra-entry.ts` source. `buildClient`
|
|
2877
|
+
wrote the client-entry source into the output dir purely as a `Bun.build` entrypoint but never removed it
|
|
2878
|
+
after bundling - so `nifra build --target static` leaked the TypeScript source next to the content-hashed
|
|
2879
|
+
`_nifra-entry-<hash>.js` the HTML actually references. It's now deleted once the client bundle succeeds; a
|
|
2880
|
+
static-build test asserts the `.ts` is absent from the output.
|
|
2881
|
+
- Updated dependencies [e47c4c5]
|
|
2882
|
+
- @nifrajs/core@1.8.0
|
|
2883
|
+
|
|
2884
|
+
## 1.7.0
|
|
2885
|
+
|
|
2886
|
+
### Minor Changes
|
|
2887
|
+
|
|
2888
|
+
- 9f23e90: Fix `nifra build --target static` producing pages that render but never hydrate. The prerender pass hardcoded a placeholder client entry, but the real bundle is content-hashed - so the prerendered HTML's hydration `<script src>` 404'd and every control was inert. `BuildTargetOptions.prerenderApp` is now a factory `(client: BuildManifest) => app` invoked with the completed client build, so the emitted `<script src>` uses the real hashed entry (plus the same styles / route-preload the SSR targets use). A regression test asserts the static HTML references the emitted hashed entry and that the file exists under `/assets`. Breaking only for code calling `buildTarget("static", …)` directly (pass a factory instead of a prebuilt app); `nifra build --target static` users just get working hydration.
|
|
2889
|
+
|
|
2890
|
+
### Patch Changes
|
|
2891
|
+
|
|
2892
|
+
- Updated dependencies [bd95181]
|
|
2893
|
+
- @nifrajs/core@1.7.0
|
|
2894
|
+
|
|
2895
|
+
## 1.6.0
|
|
2896
|
+
|
|
2897
|
+
### Patch Changes
|
|
2898
|
+
|
|
2899
|
+
- @nifrajs/core@1.6.0
|
|
2900
|
+
|
|
2901
|
+
## 1.5.0
|
|
2902
|
+
|
|
2903
|
+
### Patch Changes
|
|
2904
|
+
|
|
2905
|
+
- Updated dependencies [1ac2fde]
|
|
2906
|
+
- Updated dependencies [bd3433f]
|
|
2907
|
+
- Updated dependencies [70aa836]
|
|
2908
|
+
- @nifrajs/core@1.5.0
|
|
2909
|
+
|
|
2910
|
+
## 1.4.0
|
|
2911
|
+
|
|
2912
|
+
### Minor Changes
|
|
2913
|
+
|
|
2914
|
+
- 4d25970: Add one fail-open request-observation lifecycle shared by tracing, agent telemetry, and DevTools; secured development tooling; contract-based mock responses; validator-neutral schema/route reflection; executable render and storage adapter conformance modules; optional storage pagination/signing/copy capabilities; and metadata-preserving local file storage.
|
|
2915
|
+
|
|
2916
|
+
### Patch Changes
|
|
2917
|
+
|
|
2918
|
+
- Updated dependencies [4d25970]
|
|
2919
|
+
- @nifrajs/core@1.4.0
|
|
2920
|
+
|
|
2921
|
+
## 1.3.1
|
|
2922
|
+
|
|
2923
|
+
### Patch Changes
|
|
2924
|
+
|
|
2925
|
+
- @nifrajs/core@1.3.1
|
|
2926
|
+
|
|
2927
|
+
## 1.3.0
|
|
2928
|
+
|
|
2929
|
+
### Minor Changes
|
|
2930
|
+
|
|
2931
|
+
- 4a4b1c4: feat: `errors` response contract on routes + typed client error bodies
|
|
2932
|
+
|
|
2933
|
+
A route's `RouteSchema` may now declare `errors` - a `{ status → Standard Schema }` map of its failure modes.
|
|
2934
|
+
Like `response`, it's a compile-time + introspection contract (not validated at runtime, zero hot-path cost):
|
|
2935
|
+
the declared error bodies flow into OpenAPI as non-2xx `responses` and into the `/llms.txt` context, so
|
|
2936
|
+
tooling and coding agents can read the _whole_ contract, not just the happy path.
|
|
2937
|
+
|
|
2938
|
+
The **typed client** now surfaces them: on a failure `Result`, `data` is the parsed error body typed from the
|
|
2939
|
+
route's `errors` (a union across declared statuses; `unknown` when none declared), discriminated by `ok`.
|
|
2940
|
+
`error` remains the normalized `{ error, issues }` summary. The **decoupled contract client**
|
|
2941
|
+
(`client(contract, url)`) gets the same treatment - its failure `data` is typed from the op's non-2xx
|
|
2942
|
+
`responses` schemas.
|
|
2943
|
+
|
|
2944
|
+
**Behavior change:** on failure, `data` is now the parsed error response body (previously always `null`) - so
|
|
2945
|
+
`const { ok, data } = await api.orders.post(...)` gives you the typed error body in the `!ok` branch. `data`
|
|
2946
|
+
is still `null` only on a transport error (status `0`, no response).
|
|
2947
|
+
|
|
2948
|
+
### Patch Changes
|
|
2949
|
+
|
|
2950
|
+
- Updated dependencies [4a4b1c4]
|
|
2951
|
+
- Updated dependencies [4a4b1c4]
|
|
2952
|
+
- Updated dependencies [4a4b1c4]
|
|
2953
|
+
- Updated dependencies [4a4b1c4]
|
|
2954
|
+
- Updated dependencies [4a4b1c4]
|
|
2955
|
+
- @nifrajs/core@1.3.0
|
|
2956
|
+
|
|
2957
|
+
## 1.2.2
|
|
2958
|
+
|
|
2959
|
+
### Patch Changes
|
|
2960
|
+
|
|
2961
|
+
- @nifrajs/core@1.2.2
|
|
2962
|
+
|
|
2963
|
+
## 1.2.1
|
|
2964
|
+
|
|
2965
|
+
### Patch Changes
|
|
2966
|
+
|
|
2967
|
+
- c3ebd73: fix(web): silence the spurious `jsx` "Invalid key" warning at `nifra dev` boot under rolldown-vite
|
|
2968
|
+
|
|
2969
|
+
`@vitejs/plugin-react`'s `react()` returns an ARRAY of plugins, and `nifra.config.ts` lists it as
|
|
2970
|
+
`vitePlugins = [react()]`, so the plugin list reaches nifra NESTED (`[[babel, refresh]]`).
|
|
2971
|
+
`normalizeRolldownPlugins` - which strips the stale `optimizeDeps.rollupOptions.jsx` key that Vite 8's
|
|
2972
|
+
rolldown dep-optimizer rejects - mapped over the outer array without flattening, so it never reached the
|
|
2973
|
+
inner `vite:react-babel` plugin that emits the key, and Vite (which flattens plugin arrays itself) then ran
|
|
2974
|
+
the un-stripped hook. It now flattens first, so the strip reaches every plugin and the harmless-but-noisy
|
|
2975
|
+
`Warning: Invalid input options … "jsx" Invalid key: Expected never but received "jsx"` is gone. No
|
|
2976
|
+
behavior change - JSX transform, HMR, and Fast Refresh are unaffected.
|
|
2977
|
+
|
|
2978
|
+
- @nifrajs/core@1.2.1
|
|
2979
|
+
|
|
2980
|
+
## 1.2.0
|
|
2981
|
+
|
|
2982
|
+
### Patch Changes
|
|
2983
|
+
|
|
2984
|
+
- Updated dependencies [0ac2182]
|
|
2985
|
+
- @nifrajs/core@1.2.0
|
|
2986
|
+
|
|
2987
|
+
## 1.1.0
|
|
2988
|
+
|
|
2989
|
+
### Minor Changes
|
|
2990
|
+
|
|
2991
|
+
- 37d2383: feat(web): `@nifrajs/web/forms` - typed form ↔ backend-schema binding
|
|
2992
|
+
|
|
2993
|
+
`formFor<typeof backend, "/route">()` binds a form's field names and reads to the route's body schema at
|
|
2994
|
+
the type level, derived purely from `typeof backend`. `f.field("text")` (spread onto any framework's
|
|
2995
|
+
`<input>`) and `f.read(formData, "text")` are constrained to the body's keys - a typo, an orphan field,
|
|
2996
|
+
or a wrong route path becomes a COMPILE error (caught by `nifra check`) instead of a silent runtime
|
|
2997
|
+
empty. Framework-agnostic, dependency-free, no schema bundled into the client (the runtime is a trivial
|
|
2998
|
+
pass-through; all the work is in the types). It checks the field KEY, not its MEANING.
|
|
2999
|
+
|
|
3000
|
+
### Patch Changes
|
|
3001
|
+
|
|
3002
|
+
- @nifrajs/core@1.1.0
|
|
3003
|
+
|
|
3004
|
+
## 1.0.0
|
|
3005
|
+
|
|
3006
|
+
### Patch Changes
|
|
3007
|
+
|
|
3008
|
+
- f1f0e18: Context ergonomics, from beta feedback building on Nifra.
|
|
3009
|
+
|
|
3010
|
+
- **`c.json(body, status?)` / `c.text(body, status?)`** - build a `Response` in one line; the second arg is a status number or a full `ResponseInit`, and it works whether you `return` or `throw` it. Ideal for an auth / rate-limit short-circuit from a `derive`/`beforeHandle`: `throw c.json({ error: "unauthorized" }, 401)` instead of `new Response(JSON.stringify(…), { status: 401, headers: … })`. (In a route's happy path keep returning a plain object so the typed client stays in sync.) Added as prototype methods - no per-request allocation.
|
|
3011
|
+
- **One name for the request across routes and loaders.** A route handler's `c.req` is now also `c.request`, and a page loader/action's `ctx.request` is now also `ctx.req` - fixing the `c.req`-vs-`ctx.request` mismatch that was easy to trip over.
|
|
3012
|
+
|
|
3013
|
+
Docs: the API page documents `c.json`/`c.text` + the request alias; a new troubleshooting entry covers a `never` typed client (raw-`Response` return, or a non-identity plugin → `defineIdentityPlugin`).
|
|
3014
|
+
|
|
3015
|
+
- Updated dependencies [f1f0e18]
|
|
3016
|
+
- Updated dependencies [3efb7cd]
|
|
3017
|
+
- Updated dependencies [de9675b]
|
|
3018
|
+
- @nifrajs/core@1.0.0
|
|
3019
|
+
|
|
3020
|
+
## 1.0.0-beta.4
|
|
3021
|
+
|
|
3022
|
+
### Patch Changes
|
|
3023
|
+
|
|
3024
|
+
- @nifrajs/core@1.0.0-beta.4
|
|
3025
|
+
|
|
3026
|
+
## 1.0.0-beta.3
|
|
3027
|
+
|
|
3028
|
+
### Patch Changes
|
|
3029
|
+
|
|
3030
|
+
- @nifrajs/core@1.0.0-beta.3
|
|
3031
|
+
|
|
3032
|
+
## 0.1.0-beta.2
|
|
3033
|
+
|
|
3034
|
+
### Patch Changes
|
|
3035
|
+
|
|
3036
|
+
- 5018546: fix(web): built apps now ship their CSS link. `buildServer`/`generateServerManifest` bake the client build's
|
|
3037
|
+
stylesheet URLs (`BuildManifest.css` + `routeStyles`) into the server manifest, and the generated server entry
|
|
3038
|
+
passes them to `createWebApp` - which already emits `<link rel="stylesheet">` in the SSR `<head>`. Previously the
|
|
3039
|
+
head carried the JS modulepreload but no stylesheet, so every built (non-dev) app rendered unstyled. `styles`
|
|
3040
|
+
and `routeStyles` are now always exported from the generated manifest (default empty), so hand-written server
|
|
3041
|
+
entries can `import { styles, routeStyles } from "./server-manifest"` and forward them too.
|
|
3042
|
+
- @nifrajs/core@0.1.0-beta.2
|