@nifrajs/web 2.2.0 → 2.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/README.md +3 -3
- package/dist/build-vite.d.ts +4 -4
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +20 -74
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +64 -42
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +100 -78
- package/dist/build.js.map +1 -1
- package/dist/bun-dev-entry.d.ts +1 -1
- package/dist/bun-dev-entry.js +1 -1
- package/dist/chunk-names.js +1 -1
- package/dist/client.d.ts +3 -9
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +201 -29
- package/dist/client.js.map +1 -1
- package/dist/conformance.d.ts +2 -2
- package/dist/conformance.js +3 -3
- package/dist/deferred.d.ts +11 -11
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +16 -16
- package/dist/dev-error.d.ts +2 -2
- package/dist/dev-error.js +3 -3
- package/dist/dev-port.d.ts +2 -2
- package/dist/dev-port.js +1 -1
- package/dist/dev.d.ts +3 -3
- package/dist/dev.js +9 -9
- package/dist/draft.d.ts +12 -12
- package/dist/draft.js +11 -11
- package/dist/fn-state.d.ts +56 -0
- package/dist/fn-state.d.ts.map +1 -0
- package/dist/fn-state.js +77 -0
- package/dist/fn-state.js.map +1 -0
- package/dist/fn.d.ts +111 -0
- package/dist/fn.d.ts.map +1 -0
- package/dist/fn.js +125 -0
- package/dist/fn.js.map +1 -0
- package/dist/fonts-google.d.ts +9 -9
- package/dist/fonts-google.js +10 -10
- package/dist/fonts.d.ts +7 -7
- package/dist/fonts.js +1 -1
- package/dist/forms.d.ts +8 -8
- package/dist/fs.d.ts +1 -1
- package/dist/fs.js +1 -1
- package/dist/index.d.ts +151 -70
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +317 -139
- package/dist/index.js.map +1 -1
- package/dist/internal/head-attributes.d.ts +15 -0
- package/dist/internal/head-attributes.d.ts.map +1 -0
- package/dist/internal/head-attributes.js +99 -0
- package/dist/internal/head-attributes.js.map +1 -0
- package/dist/internal/script-types.d.ts +14 -0
- package/dist/internal/script-types.d.ts.map +1 -0
- package/dist/internal/script-types.js +17 -0
- package/dist/internal/script-types.js.map +1 -0
- package/dist/internal/server-boundary.d.ts +60 -0
- package/dist/internal/server-boundary.d.ts.map +1 -0
- package/dist/internal/server-boundary.js +174 -0
- package/dist/internal/server-boundary.js.map +1 -0
- package/dist/internal/timing-safe-equal.d.ts +1 -1
- package/dist/internal/timing-safe-equal.js +1 -1
- package/dist/internal/vite-import.d.ts +63 -0
- package/dist/internal/vite-import.d.ts.map +1 -0
- package/dist/internal/vite-import.js +94 -0
- package/dist/internal/vite-import.js.map +1 -0
- package/dist/islands.d.ts +5 -5
- package/dist/islands.js +5 -5
- package/dist/isr.d.ts +15 -15
- package/dist/isr.js +7 -7
- package/dist/manifest.d.ts +73 -35
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +10 -16
- package/dist/manifest.js.map +1 -1
- package/dist/module-graph.d.ts +5 -5
- package/dist/module-graph.js +4 -4
- package/dist/navigation.d.ts +120 -5
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +37 -5
- package/dist/navigation.js.map +1 -1
- package/dist/plugins/css-modules.d.ts +3 -3
- package/dist/plugins/css-modules.js +0 -0
- package/dist/plugins/kit.d.ts +8 -8
- package/dist/plugins/kit.js +9 -9
- package/dist/plugins/scss.js +3 -3
- package/dist/plugins/vite-leak-guard.d.ts +1 -1
- package/dist/plugins/vite-leak-guard.js +1 -1
- package/dist/plugins/vite-server-fn.d.ts +23 -0
- package/dist/plugins/vite-server-fn.d.ts.map +1 -0
- package/dist/plugins/vite-server-fn.js +51 -0
- package/dist/plugins/vite-server-fn.js.map +1 -0
- package/dist/plugins/vite-server-only.d.ts +49 -0
- package/dist/plugins/vite-server-only.d.ts.map +1 -0
- package/dist/plugins/vite-server-only.js +57 -0
- package/dist/plugins/vite-server-only.js.map +1 -0
- package/dist/prerender.d.ts +5 -5
- package/dist/prerender.js +7 -7
- package/dist/query.d.ts +10 -10
- package/dist/query.js +15 -15
- package/dist/route-manifest.d.ts +1 -1
- package/dist/route-manifest.js +2 -2
- package/dist/router.d.ts +30 -21
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +57 -28
- package/dist/router.js.map +1 -1
- package/dist/search.d.ts +94 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +232 -0
- package/dist/search.js.map +1 -0
- package/dist/server-only.d.ts +6 -6
- package/dist/server-only.js +7 -7
- package/dist/service-worker.d.ts +56 -0
- package/dist/service-worker.d.ts.map +1 -0
- package/dist/service-worker.js +127 -0
- package/dist/service-worker.js.map +1 -0
- package/dist/vite.d.ts +15 -13
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +31 -21
- package/dist/vite.js.map +1 -1
- package/package.json +33 -4
- package/server-manifest.d.ts +31 -0
- package/src/build-vite.ts +24 -77
- package/src/build.ts +137 -105
- package/src/bun-dev-entry.ts +1 -1
- package/src/chunk-names.ts +1 -1
- package/src/client.ts +215 -32
- package/src/conformance.ts +3 -3
- package/src/deferred.ts +17 -17
- package/src/dev-error.ts +3 -3
- package/src/dev-port.ts +2 -2
- package/src/dev.ts +12 -12
- package/src/draft.ts +15 -15
- package/src/fn-state.ts +107 -0
- package/src/fn.ts +199 -0
- package/src/fonts-google.ts +13 -13
- package/src/fonts.ts +7 -7
- package/src/forms.ts +8 -8
- package/src/fs.ts +2 -2
- package/src/index.ts +460 -181
- package/src/internal/head-attributes.ts +109 -0
- package/src/internal/script-types.ts +18 -0
- package/src/internal/server-boundary.ts +187 -0
- package/src/internal/timing-safe-equal.ts +1 -1
- package/src/internal/vite-import.ts +92 -0
- package/src/islands.ts +7 -7
- package/src/isr.ts +17 -17
- package/src/manifest.ts +83 -42
- package/src/module-graph.ts +6 -6
- package/src/navigation.ts +151 -5
- package/src/plugins/css-modules.ts +0 -0
- package/src/plugins/kit.ts +11 -11
- package/src/plugins/scss.ts +3 -3
- package/src/plugins/vite-leak-guard.ts +2 -2
- package/src/plugins/vite-server-fn.ts +67 -0
- package/src/plugins/vite-server-only.ts +75 -0
- package/src/prerender.ts +10 -10
- package/src/query.ts +22 -22
- package/src/route-manifest.ts +3 -3
- package/src/router.ts +91 -41
- package/src/search.ts +297 -0
- package/src/server-only.ts +7 -7
- package/src/service-worker.ts +165 -0
- package/src/vite.ts +41 -29
package/src/plugins/scss.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@nifrajs/web/plugins/scss`
|
|
2
|
+
* `@nifrajs/web/plugins/scss` - a SASS/SCSS (`*.scss`, `*.sass`) Bun plugin, in its OWN module so the SSR
|
|
3
3
|
* preload registers it BEFORE any Sass file loads. Mirrors the `@nifrajs/web-vue/plugin` seam: pass
|
|
4
4
|
* `"dom"` for the client bundle (`buildClient({ plugins: [...] })`) and preload `"ssr"` for the server
|
|
5
5
|
* (`bun --preload`).
|
|
6
6
|
*
|
|
7
|
-
* The Dart Sass compiler is an **optional peer** (`sass`, or the faster `sass-embedded`
|
|
7
|
+
* The Dart Sass compiler is an **optional peer** (`sass`, or the faster `sass-embedded` - same API), not
|
|
8
8
|
* a hard dependency: it's loaded on first Sass file and fails loud with an install hint if absent. Pass
|
|
9
9
|
* your own `compiler` to override (e.g. `sass-embedded`, or a stub in tests).
|
|
10
10
|
*
|
|
11
11
|
* Composes with CSS Modules: a `*.module.scss` / `*.module.sass` file is compiled to CSS and then run
|
|
12
12
|
* through the same scoped-class transform as `@nifrajs/web/plugins/css-modules`, so its `import styles`
|
|
13
13
|
* yields the `{ original: scoped }` map (SSR/dom class-map parity included). A plain `*.scss` is a
|
|
14
|
-
* side-effect import
|
|
14
|
+
* side-effect import - its CSS is bundled (dom) and it resolves to an empty module (ssr).
|
|
15
15
|
*/
|
|
16
16
|
import { dirname } from "node:path"
|
|
17
17
|
import { pathToFileURL } from "node:url"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@nifrajs/web/plugins/vite-leak-guard`
|
|
2
|
+
* `@nifrajs/web/plugins/vite-leak-guard` - nifra's two client-leak guards, for a Vite/Rollup production
|
|
3
3
|
* build.
|
|
4
4
|
*
|
|
5
5
|
* ## Why this exists
|
|
@@ -60,7 +60,7 @@ interface RollupPluginContext {
|
|
|
60
60
|
error(error: Error): never
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
/** The minimal Rollup plugin shape this returns
|
|
63
|
+
/** The minimal Rollup plugin shape this returns - `generateBundle` bound to the plugin context. */
|
|
64
64
|
export interface LeakGuardPlugin {
|
|
65
65
|
readonly name: string
|
|
66
66
|
/**
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nifrajs/web/plugins/vite-server-fn` - the Vite half of the server-function client transform.
|
|
3
|
+
*
|
|
4
|
+
* A `*.fn` module must never reach a browser: it holds the function bodies and everything they import.
|
|
5
|
+
* The Bun build replaces each one with client stubs, and this does the same for Vite, in both the dev
|
|
6
|
+
* server and the production build.
|
|
7
|
+
*
|
|
8
|
+
* ## Why the generation is not in here
|
|
9
|
+
*
|
|
10
|
+
* The stub text comes from `internal/server-boundary.ts`, shared verbatim with the Bun plugin. Two
|
|
11
|
+
* implementations of "what a stub looks like" would drift, and the drift would be invisible until a
|
|
12
|
+
* client that worked in dev 404'd in production - the exact shape of failure this codebase has already
|
|
13
|
+
* paid for once. A parity test asserts both pipelines emit the same bytes.
|
|
14
|
+
*
|
|
15
|
+
* ## Why `transform` and not `load`
|
|
16
|
+
*
|
|
17
|
+
* Vite resolves and reads the file itself; taking `load` would mean re-implementing its resolution.
|
|
18
|
+
* `transform` receives the source Vite already read, and returning replacement code discards the
|
|
19
|
+
* original along with its imports - so the server subtree is never followed. `enforce: "pre"` puts this
|
|
20
|
+
* ahead of framework transforms, which have no business seeing server code either.
|
|
21
|
+
*/
|
|
22
|
+
import {
|
|
23
|
+
generateServerFnStub,
|
|
24
|
+
SERVER_FN_MODULE,
|
|
25
|
+
serverFnNamespace,
|
|
26
|
+
} from "../internal/server-boundary.ts"
|
|
27
|
+
|
|
28
|
+
/** The slice of a Vite/Rollup plugin this returns. Structural, so `vite` stays an optional peer. */
|
|
29
|
+
export interface ServerFnStubPlugin {
|
|
30
|
+
readonly name: string
|
|
31
|
+
readonly enforce: "pre"
|
|
32
|
+
/** Applied only to the CLIENT build; the server build keeps the real module. */
|
|
33
|
+
readonly applyToEnvironment?: (environment: { readonly name: string }) => boolean
|
|
34
|
+
transform(
|
|
35
|
+
code: string,
|
|
36
|
+
id: string,
|
|
37
|
+
options?: { readonly ssr?: boolean },
|
|
38
|
+
): { code: string; map: null } | null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Strips a Vite id's `?query` and `#hash` so the suffix test sees a plain path. */
|
|
42
|
+
const bare = (id: string): string => id.split("?")[0]?.split("#")[0] ?? id
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Replace every `*.fn` module with its client stubs.
|
|
46
|
+
*
|
|
47
|
+
* Client-only by construction: nifra passes this to the client build and the dev server, never to the
|
|
48
|
+
* SSR build, because the server is what actually runs these functions.
|
|
49
|
+
*/
|
|
50
|
+
export function viteServerFnStub(): ServerFnStubPlugin {
|
|
51
|
+
return {
|
|
52
|
+
name: "nifra:server-fn-stub",
|
|
53
|
+
enforce: "pre",
|
|
54
|
+
// Vite 6+ environments: the SSR environment must keep the real module. Older Vite ignores this and
|
|
55
|
+
// relies on nifra only registering the plugin on the client side.
|
|
56
|
+
applyToEnvironment: (environment) => environment.name === "client",
|
|
57
|
+
transform(code, id, options) {
|
|
58
|
+
// Vite 5 ignores `applyToEnvironment` but supplies this transform context for SSR.
|
|
59
|
+
if (options?.ssr === true) return null
|
|
60
|
+
const path = bare(id)
|
|
61
|
+
if (!SERVER_FN_MODULE.test(path)) return null
|
|
62
|
+
// `map: null` is honest rather than lazy: the stub shares no lines with the source, so any
|
|
63
|
+
// mapping would point a debugger at unrelated code.
|
|
64
|
+
return { code: generateServerFnStub(code, serverFnNamespace(path)), map: null }
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nifrajs/web/plugins/vite-server-only` - the Vite half of the `.server` convention.
|
|
3
|
+
*
|
|
4
|
+
* A module named `*.server.ts(x)` is server-only: the Bun client build empties it so its import
|
|
5
|
+
* subtree - `node:` builtins, native modules, secrets - never reaches a browser. This does the same
|
|
6
|
+
* for Vite, in both the dev server and the production build.
|
|
7
|
+
*
|
|
8
|
+
* ## Why this exists separately
|
|
9
|
+
*
|
|
10
|
+
* The convention was implemented once, as a `Bun.build` plugin, which meant it held in exactly one of
|
|
11
|
+
* the four pipelines. `nifra build` emptied the module; `nifra dev` (Vite), a Vite production build,
|
|
12
|
+
* and `nifra dev --bun` all shipped it whole. A guard that holds in one pipeline and not the others is
|
|
13
|
+
* worse than none, because the file name reads as protection everywhere.
|
|
14
|
+
*
|
|
15
|
+
* `*.fn` already had this shape - a Bun plugin, a Vite plugin, and a refusal in the one pipeline that
|
|
16
|
+
* cannot transform. `.server` now matches it.
|
|
17
|
+
*
|
|
18
|
+
* ## Why `transform` and not `load`
|
|
19
|
+
*
|
|
20
|
+
* Vite resolves and reads the file itself; taking `load` would mean re-implementing its resolution.
|
|
21
|
+
* `transform` receives the source Vite already read, and returning replacement code discards the
|
|
22
|
+
* original along with its imports - so the server subtree is never followed. `enforce: "pre"` puts
|
|
23
|
+
* this ahead of framework transforms, which have no business seeing server code either.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
SERVER_ONLY_MODULE,
|
|
28
|
+
SERVER_ONLY_REPLACEMENT,
|
|
29
|
+
viteServerOnlyReplacement,
|
|
30
|
+
} from "../internal/server-boundary.ts"
|
|
31
|
+
|
|
32
|
+
// Re-exported so a caller wiring its own bundler gets the matcher from the same owner the pipelines
|
|
33
|
+
// use, rather than writing a fifth copy of the regex.
|
|
34
|
+
export { SERVER_ONLY_MODULE, SERVER_ONLY_REPLACEMENT, viteServerOnlyReplacement }
|
|
35
|
+
|
|
36
|
+
/** The slice of a Vite/Rollup plugin this returns. Structural, so `vite` stays an optional peer. */
|
|
37
|
+
export interface ServerOnlyEmptyPlugin {
|
|
38
|
+
readonly name: string
|
|
39
|
+
readonly enforce: "pre"
|
|
40
|
+
/** Applied only to the CLIENT build; the server build keeps the real module. */
|
|
41
|
+
readonly applyToEnvironment?: (environment: { readonly name: string }) => boolean
|
|
42
|
+
transform(
|
|
43
|
+
code: string,
|
|
44
|
+
id: string,
|
|
45
|
+
options?: { readonly ssr?: boolean },
|
|
46
|
+
): { code: string; map: null } | null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Strips a Vite id's `?query` and `#hash` so the suffix test sees a plain path. */
|
|
50
|
+
const bare = (id: string): string => id.split("?")[0]?.split("#")[0] ?? id
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Empty every `*.server` module in the client build.
|
|
54
|
+
*
|
|
55
|
+
* Client-only by construction: nifra passes this to the client build and the dev server, never to the
|
|
56
|
+
* SSR build, where the real module is what runs.
|
|
57
|
+
*/
|
|
58
|
+
export function viteServerOnlyEmpty(): ServerOnlyEmptyPlugin {
|
|
59
|
+
return {
|
|
60
|
+
name: "nifra:server-only-empty",
|
|
61
|
+
enforce: "pre",
|
|
62
|
+
// Vite 6+ environments: the SSR environment must keep the real module. Older Vite ignores this and
|
|
63
|
+
// relies on nifra only registering the plugin on the client side.
|
|
64
|
+
applyToEnvironment: (environment) => environment.name === "client",
|
|
65
|
+
transform(code, id, options) {
|
|
66
|
+
// Vite 5 ignores `applyToEnvironment` but supplies this transform context for SSR.
|
|
67
|
+
if (options?.ssr === true) return null
|
|
68
|
+
const path = bare(id)
|
|
69
|
+
if (!SERVER_ONLY_MODULE.test(path)) return null
|
|
70
|
+
// `map: null` is honest rather than lazy: the replacement shares no lines with the source, so any
|
|
71
|
+
// mapping would point a debugger at unrelated code.
|
|
72
|
+
return { code: viteServerOnlyReplacement(code), map: null }
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/prerender.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Build-time SSG driver. Prerendering is framework-agnostic by construction: a nifra SSR page is
|
|
3
3
|
* "just `app.fetch(GET)` returning a streamed `Response`", so prerendering = drive that same handler
|
|
4
4
|
* at build with a synthetic request, drain the body to bytes, and write the HTML to disk. No adapter
|
|
5
|
-
* (React/Solid/Vue/Preact/Svelte) is touched
|
|
5
|
+
* (React/Solid/Vue/Preact/Svelte) is touched - this sits entirely above the render seam.
|
|
6
6
|
*
|
|
7
7
|
* Handles **static** routes (opt in via `export const prerender = true`) and **dynamic** `:param`
|
|
8
8
|
* routes (enumerate concrete params via `export const getStaticPaths`). Catch-all/wildcard (`*`)
|
|
@@ -13,7 +13,7 @@ import { dirname, resolve, sep } from "node:path"
|
|
|
13
13
|
import { fillRoutePattern, type RouteEntry } from "./manifest.ts"
|
|
14
14
|
import { DATA_HEADER } from "./router.ts"
|
|
15
15
|
|
|
16
|
-
/** Minimal app surface the driver needs
|
|
16
|
+
/** Minimal app surface the driver needs - just a fetch handler (a built `createWebApp`). */
|
|
17
17
|
export interface PrerenderApp {
|
|
18
18
|
fetch(req: Request): Response | Promise<Response>
|
|
19
19
|
}
|
|
@@ -47,7 +47,7 @@ export interface PrerenderEntry {
|
|
|
47
47
|
export interface PrerenderResult {
|
|
48
48
|
readonly prerendered: readonly PrerenderEntry[]
|
|
49
49
|
readonly skipped: readonly { readonly path: string; readonly reason: string }[]
|
|
50
|
-
/** Per dynamic route pattern, its `getStaticPaths` `fallback` (`"ssr"` default)
|
|
50
|
+
/** Per dynamic route pattern, its `getStaticPaths` `fallback` (`"ssr"` default) - so the deploy
|
|
51
51
|
* layer knows whether unlisted paths should hit the worker (`"ssr"`) or 404 (`"404"`). */
|
|
52
52
|
readonly fallbacks: Readonly<Record<string, "ssr" | "404">>
|
|
53
53
|
}
|
|
@@ -71,7 +71,7 @@ const outputPath = (root: string, file: string): string | undefined => {
|
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
73
|
* Render every opted-in static route to a static `index.html` under `outDir`. Run AFTER `buildClient`
|
|
74
|
-
* (so the app references the hashed client entry). Returns a report of what was emitted vs skipped
|
|
74
|
+
* (so the app references the hashed client entry). Returns a report of what was emitted vs skipped -
|
|
75
75
|
* the caller can use `prerendered` to wire a hybrid deploy (e.g. exclude those paths from the SSR
|
|
76
76
|
* worker so the CDN serves the static file). Never throws on a per-route miss: a non-OK response or a
|
|
77
77
|
* non-opted/dynamic route is recorded in `skipped` and the build continues.
|
|
@@ -88,7 +88,7 @@ export async function prerenderRoutes(options: PrerenderOptions): Promise<Preren
|
|
|
88
88
|
const renderPath = async (path: string): Promise<void> => {
|
|
89
89
|
const res = await options.app.fetch(new Request(`${origin}${path}`))
|
|
90
90
|
if (!res.ok) {
|
|
91
|
-
// A failed render is a per-route skip, not a build failure
|
|
91
|
+
// A failed render is a per-route skip, not a build failure - surfaced for the caller to log.
|
|
92
92
|
skipped.push({ path, reason: `render returned HTTP ${res.status}` })
|
|
93
93
|
return
|
|
94
94
|
}
|
|
@@ -104,7 +104,7 @@ export async function prerenderRoutes(options: PrerenderOptions): Promise<Preren
|
|
|
104
104
|
|
|
105
105
|
// Also emit the loader data as a static `_data.json` next to it, so a client soft-nav into this
|
|
106
106
|
// route fetches a file instead of hitting the worker. Only when the data-mode response is plain
|
|
107
|
-
// JSON
|
|
107
|
+
// JSON - a deferred loader answers with NDJSON, which the static fast-path doesn't cover (the
|
|
108
108
|
// client falls back to the worker for those).
|
|
109
109
|
let dataFile: string | undefined
|
|
110
110
|
const dataRes = await options.app.fetch(
|
|
@@ -123,7 +123,7 @@ export async function prerenderRoutes(options: PrerenderOptions): Promise<Preren
|
|
|
123
123
|
writeFileSync(dataAbs, await dataRes.text())
|
|
124
124
|
}
|
|
125
125
|
} else {
|
|
126
|
-
await dataRes.body?.cancel() // not emitting (non-OK or NDJSON)
|
|
126
|
+
await dataRes.body?.cancel() // not emitting (non-OK or NDJSON) - release the stream
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
prerendered.push({ path, file, bytes: html.length, ...(dataFile ? { dataFile } : {}) })
|
|
@@ -131,7 +131,7 @@ export async function prerenderRoutes(options: PrerenderOptions): Promise<Preren
|
|
|
131
131
|
|
|
132
132
|
for (const route of options.routes) {
|
|
133
133
|
if (route.pattern.includes("*")) {
|
|
134
|
-
skipped.push({ path: route.pattern, reason: "catch-all/wildcard route
|
|
134
|
+
skipped.push({ path: route.pattern, reason: "catch-all/wildcard route - not supported" })
|
|
135
135
|
continue
|
|
136
136
|
}
|
|
137
137
|
const mod = await route.load()
|
|
@@ -182,7 +182,7 @@ export interface CloudflarePagesRoutes {
|
|
|
182
182
|
|
|
183
183
|
export interface CloudflarePagesRoutesOptions {
|
|
184
184
|
/** Prerendered request paths (e.g. `prerenderRoutes(...).prerendered.map(p => p.path)`, or the
|
|
185
|
-
* build's `prerendered.json`). Each is excluded from the worker
|
|
185
|
+
* build's `prerendered.json`). Each is excluded from the worker - plus its static `_data.json`. */
|
|
186
186
|
readonly prerendered: readonly string[]
|
|
187
187
|
/** Extra globs to keep OFF the worker (CDN-served). Default `["/assets/*"]` (the hashed bundle). */
|
|
188
188
|
readonly staticGlobs?: readonly string[]
|
|
@@ -195,7 +195,7 @@ export interface CloudflarePagesRoutesOptions {
|
|
|
195
195
|
*
|
|
196
196
|
* ⚠️ Cloudflare caps `_routes.json` at **100** include+exclude rules total, and each prerendered path
|
|
197
197
|
* costs 2 (the doc + its `_data.json`). For a large SSG site, exclude by **prefix glob** (e.g.
|
|
198
|
-
* `/blog/*`) instead of listing every path
|
|
198
|
+
* `/blog/*`) instead of listing every path - pass those via `staticGlobs` and a smaller `prerendered`.
|
|
199
199
|
*/
|
|
200
200
|
export function cloudflarePagesRoutes(
|
|
201
201
|
options: CloudflarePagesRoutesOptions,
|
package/src/query.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The agnostic keyed query-cache
|
|
2
|
+
* The agnostic keyed query-cache - a `query(key, fn)` primitive with in-flight dedup, `staleTime`
|
|
3
3
|
* freshness, prefix `invalidateQueries`, imperative cache read/write, prefetch, SSR dehydrate/hydrate,
|
|
4
4
|
* infinite (paged) queries, and standalone mutations. Pure logic, no `window`, no framework, no
|
|
5
|
-
* `Date.now`/timers (the clock is injected)
|
|
5
|
+
* `Date.now`/timers (the clock is injected) - so it unit-tests deterministically and is safe to import
|
|
6
6
|
* from the SSR core's main entry. A per-adapter binding (`useQuery`/`useMutation`/`createQuery`)
|
|
7
7
|
* subscribes to a query/mutation's `subscribe`/`snapshot` store (the same shape the router + fetchers
|
|
8
8
|
* use).
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
/** A query's lifecycle status. `pending` = no data yet; `success`/`error` once it has settled once. */
|
|
16
16
|
export type QueryStatus = "pending" | "success" | "error"
|
|
17
17
|
|
|
18
|
-
/** A query's observable state
|
|
18
|
+
/** A query's observable state - what a binding renders. A new (frozen) object per transition, so a
|
|
19
19
|
* `useSyncExternalStore`/signal binding can compare by reference. */
|
|
20
20
|
export interface QueryState<T = unknown> {
|
|
21
21
|
readonly status: QueryStatus
|
|
22
22
|
readonly data: T | undefined
|
|
23
23
|
readonly error: unknown
|
|
24
|
-
/** True while a fetch is in flight
|
|
24
|
+
/** True while a fetch is in flight - the initial load OR a background refetch (data may still show). */
|
|
25
25
|
readonly isFetching: boolean
|
|
26
26
|
/** When `data` was last set (via the injected clock); drives `staleTime`. `-Infinity` until first set. */
|
|
27
27
|
readonly updatedAt: number
|
|
@@ -46,14 +46,14 @@ export interface QueryOptions {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export interface QueryClientOptions {
|
|
49
|
-
/** Monotonic clock in ms
|
|
49
|
+
/** Monotonic clock in ms - injected so the core stays deterministic + testable; the adapter binding
|
|
50
50
|
* passes `() => Date.now()`. */
|
|
51
51
|
readonly now: () => number
|
|
52
52
|
/** Data stays fresh this long after a fetch; older ⇒ refetch on the next `fetch()` (default `0`). */
|
|
53
53
|
readonly staleTime?: number
|
|
54
54
|
/** Evict an entry this long (ms) after its last subscriber leaves (default 5 min). */
|
|
55
55
|
readonly gcTime?: number
|
|
56
|
-
/** Hard cap on cached entries
|
|
56
|
+
/** Hard cap on cached entries - LRU-evict the oldest *unsubscribed* entry past it (default 1000). */
|
|
57
57
|
readonly max?: number
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -65,7 +65,7 @@ export interface InfiniteData<T, P> {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/** Options for an {@link InfiniteQueryHandle}. `getNextPageParam` (required) derives the param for the
|
|
68
|
-
* next page from the last page
|
|
68
|
+
* next page from the last page - return `undefined`/`null` to signal there is no next page. */
|
|
69
69
|
export interface InfiniteQueryOptions<T, P> extends QueryOptions {
|
|
70
70
|
/** The param the FIRST page is fetched with. */
|
|
71
71
|
readonly initialPageParam: P
|
|
@@ -104,7 +104,7 @@ export interface InfiniteQueryHandle<T, P> {
|
|
|
104
104
|
hasPreviousPage: () => boolean
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
/** A serializable snapshot of the cache's successful queries
|
|
107
|
+
/** A serializable snapshot of the cache's successful queries - the SSR→client bridge payload. */
|
|
108
108
|
export interface DehydratedState {
|
|
109
109
|
readonly queries: ReadonlyArray<{
|
|
110
110
|
readonly key: unknown
|
|
@@ -144,7 +144,7 @@ export interface QueryClient {
|
|
|
144
144
|
|
|
145
145
|
/**
|
|
146
146
|
* Hash a query key to a stable cache string. Object keys are sorted (so `{a,b}` ≡ `{b,a}`); arrays
|
|
147
|
-
* keep order. Keys must be serializable
|
|
147
|
+
* keep order. Keys must be serializable - a function/symbol in the key throws (it can't be a stable
|
|
148
148
|
* identity). Mirrors TanStack Query's structural hashing.
|
|
149
149
|
*/
|
|
150
150
|
export function hashQueryKey(key: unknown): string {
|
|
@@ -179,7 +179,7 @@ interface Entry {
|
|
|
179
179
|
staleTime: number
|
|
180
180
|
state: QueryState
|
|
181
181
|
promise: Promise<unknown> | undefined
|
|
182
|
-
/** The {@link generation} the in-flight {@link promise} started at
|
|
182
|
+
/** The {@link generation} the in-flight {@link promise} started at - so a fetch begun *before* an
|
|
183
183
|
* `invalidateQueries` (which bumps `generation`) is recognized as superseded, not joined. */
|
|
184
184
|
promiseGen: number
|
|
185
185
|
/** Bumped by `invalidateQueries`/`refetch` to supersede any in-flight fetch begun before it. */
|
|
@@ -217,7 +217,7 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
|
|
|
217
217
|
const cache = new Map<string, Entry>()
|
|
218
218
|
|
|
219
219
|
// Lazily evict entries whose GC timer has elapsed (no subscribers), then enforce the hard cap by
|
|
220
|
-
// dropping the oldest-inserted unsubscribed entries. Called on every `query()`
|
|
220
|
+
// dropping the oldest-inserted unsubscribed entries. Called on every `query()` - no background timer.
|
|
221
221
|
const sweep = (): void => {
|
|
222
222
|
const t = now()
|
|
223
223
|
for (const [h, e] of cache) {
|
|
@@ -251,8 +251,8 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
|
|
|
251
251
|
handle: undefined as unknown as QueryHandle,
|
|
252
252
|
}
|
|
253
253
|
const emit = (): void => {
|
|
254
|
-
// Iterate the live set
|
|
255
|
-
// listener must not synchronously subscribe/unsubscribe during notification
|
|
254
|
+
// Iterate the live set - no per-emit copy (the common case is 0-1 subscribers). Contract: a
|
|
255
|
+
// listener must not synchronously subscribe/unsubscribe during notification - `useSyncExternalStore`/
|
|
256
256
|
// signal/store bindings don't (un/subscribe happens in a later effect/cleanup tick).
|
|
257
257
|
if (entry.listeners.size === 0) return
|
|
258
258
|
for (const l of entry.listeners) l()
|
|
@@ -265,19 +265,19 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
|
|
|
265
265
|
entry.invalidated || now() - entry.state.updatedAt >= entry.staleTime
|
|
266
266
|
|
|
267
267
|
entry.run = (force: boolean): Promise<unknown> => {
|
|
268
|
-
// Dedup
|
|
268
|
+
// Dedup - but join the in-flight fetch ONLY when it belongs to the current generation. An
|
|
269
269
|
// `invalidateQueries` bumps `generation`, so a fetch that started *before* the invalidation is
|
|
270
270
|
// superseded and must NOT satisfy this call (its data is pre-mutation); fall through and kick a
|
|
271
271
|
// fresh fetch instead of returning the stale in-flight one.
|
|
272
272
|
if (entry.promise !== undefined && entry.promiseGen === entry.generation) return entry.promise
|
|
273
273
|
if (!force && entry.state.status === "success" && !isStale())
|
|
274
|
-
return Promise.resolve(entry.state.data) // fresh cache hit
|
|
274
|
+
return Promise.resolve(entry.state.data) // fresh cache hit - no fn call
|
|
275
275
|
const gen = entry.generation
|
|
276
276
|
entry.setState({ ...entry.state, isFetching: true }) // show the background/initial fetch
|
|
277
277
|
const promise = (async () => {
|
|
278
278
|
try {
|
|
279
279
|
const data = await entry.fn()
|
|
280
|
-
// A newer generation (a later invalidate/refetch) owns the entry now
|
|
280
|
+
// A newer generation (a later invalidate/refetch) owns the entry now - discard this result
|
|
281
281
|
// rather than publishing pre-mutation data as fresh or clearing `invalidated`. The fetch
|
|
282
282
|
// that superseded this one publishes the post-mutation data.
|
|
283
283
|
if (gen !== entry.generation) return data
|
|
@@ -304,7 +304,7 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
|
|
|
304
304
|
snapshot: () => entry.state,
|
|
305
305
|
subscribe: (listener: () => void) => {
|
|
306
306
|
entry.subscribers++
|
|
307
|
-
entry.gcAt = undefined // in use again
|
|
307
|
+
entry.gcAt = undefined // in use again - cancel any pending eviction
|
|
308
308
|
entry.listeners.add(listener)
|
|
309
309
|
return () => {
|
|
310
310
|
entry.listeners.delete(listener)
|
|
@@ -342,7 +342,7 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
|
|
|
342
342
|
for (const entry of cache.values()) {
|
|
343
343
|
if (!keyMatchesPrefix(entry.key, keyOrPrefix)) continue
|
|
344
344
|
entry.invalidated = true // next fetch refetches even within staleTime
|
|
345
|
-
// Supersede any in-flight fetch begun before this point
|
|
345
|
+
// Supersede any in-flight fetch begun before this point - its result is pre-mutation and must
|
|
346
346
|
// not resolve as "fresh" or clear `invalidated`. The refetch below (or the next `fetch()`)
|
|
347
347
|
// starts a new generation that publishes the post-mutation data.
|
|
348
348
|
entry.generation++
|
|
@@ -358,7 +358,7 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
|
|
|
358
358
|
}
|
|
359
359
|
|
|
360
360
|
const setQueryData = <T>(key: unknown, updater: T | ((prev: T | undefined) => T)): void => {
|
|
361
|
-
// Create a placeholder entry if none exists
|
|
361
|
+
// Create a placeholder entry if none exists - its `fn` throws until a real `query(key, fn)` re-binds
|
|
362
362
|
// it, so a `setQueryData` for a key nobody queries yet never triggers a fetch on its own.
|
|
363
363
|
const entry = upsert(
|
|
364
364
|
key,
|
|
@@ -370,7 +370,7 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
|
|
|
370
370
|
const prev = entry.state.status === "success" ? (entry.state.data as T) : undefined
|
|
371
371
|
const data =
|
|
372
372
|
typeof updater === "function" ? (updater as (p: T | undefined) => T)(prev) : updater
|
|
373
|
-
// A direct write is the new truth
|
|
373
|
+
// A direct write is the new truth - bump the generation so any in-flight fetch can't overwrite it,
|
|
374
374
|
// and clear the invalidated flag (the caller just supplied fresh data).
|
|
375
375
|
entry.generation++
|
|
376
376
|
entry.invalidated = false
|
|
@@ -629,7 +629,7 @@ const IDLE_MUTATION: MutationState<unknown, unknown> = Object.freeze({
|
|
|
629
629
|
})
|
|
630
630
|
|
|
631
631
|
/**
|
|
632
|
-
* Create a standalone mutation state machine
|
|
632
|
+
* Create a standalone mutation state machine - framework-agnostic, so a per-adapter `useMutation`
|
|
633
633
|
* binding just subscribes to it. Single-flight by a monotonic token: overlapping `mutate` calls each
|
|
634
634
|
* run their `fn`, but only the latest publishes state (an older, slower response can't clobber a newer
|
|
635
635
|
* one). The callbacks fire in TanStack order: `onMutate` (before), then `onSuccess`/`onError`, then
|
|
@@ -667,7 +667,7 @@ export function createMutation<TData, TVariables>(
|
|
|
667
667
|
mutate: async (variables) => {
|
|
668
668
|
const mine = ++token
|
|
669
669
|
setState({ status: "pending", data: undefined, error: undefined, variables })
|
|
670
|
-
// Only await a callback when one is actually supplied
|
|
670
|
+
// Only await a callback when one is actually supplied - an `await undefined` would insert a
|
|
671
671
|
// needless microtask before the mutationFn runs (and complicates single-flight timing).
|
|
672
672
|
if (cbs.onMutate !== undefined) await cbs.onMutate(variables)
|
|
673
673
|
try {
|
package/src/route-manifest.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@nifrajs/web/route-manifest`
|
|
2
|
+
* `@nifrajs/web/route-manifest` - what each route actually DOES, as one artifact.
|
|
3
3
|
*
|
|
4
4
|
* The facts are already in the route modules: `prerender`, `getStaticPaths`, `revalidate`, `hydrate`.
|
|
5
5
|
* What has never existed is anywhere to read them together. To answer "which pages are static?", "which
|
|
@@ -199,7 +199,7 @@ export function renderRouteManifest(manifest: RouteManifest): string {
|
|
|
199
199
|
lines.push(
|
|
200
200
|
manifest.target === undefined
|
|
201
201
|
? "nifra route manifest"
|
|
202
|
-
: `nifra route manifest
|
|
202
|
+
: `nifra route manifest - target: ${manifest.target}`,
|
|
203
203
|
)
|
|
204
204
|
lines.push("")
|
|
205
205
|
if (manifest.routes.length === 0) {
|
|
@@ -233,7 +233,7 @@ export function renderRouteManifest(manifest: RouteManifest): string {
|
|
|
233
233
|
lines.push("")
|
|
234
234
|
lines.push(`✗ ${manifest.conflicts.length} route(s) the target cannot honour:`)
|
|
235
235
|
for (const conflict of manifest.conflicts) {
|
|
236
|
-
lines.push(` ✗ ${conflict.pattern}
|
|
236
|
+
lines.push(` ✗ ${conflict.pattern} - ${conflict.consequence}`)
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
239
|
return lines.join("\n")
|