@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.
Files changed (163) hide show
  1. package/README.md +3 -3
  2. package/dist/build-vite.d.ts +4 -4
  3. package/dist/build-vite.d.ts.map +1 -1
  4. package/dist/build-vite.js +20 -74
  5. package/dist/build-vite.js.map +1 -1
  6. package/dist/build.d.ts +64 -42
  7. package/dist/build.d.ts.map +1 -1
  8. package/dist/build.js +100 -78
  9. package/dist/build.js.map +1 -1
  10. package/dist/bun-dev-entry.d.ts +1 -1
  11. package/dist/bun-dev-entry.js +1 -1
  12. package/dist/chunk-names.js +1 -1
  13. package/dist/client.d.ts +3 -9
  14. package/dist/client.d.ts.map +1 -1
  15. package/dist/client.js +201 -29
  16. package/dist/client.js.map +1 -1
  17. package/dist/conformance.d.ts +2 -2
  18. package/dist/conformance.js +3 -3
  19. package/dist/deferred.d.ts +11 -11
  20. package/dist/deferred.d.ts.map +1 -1
  21. package/dist/deferred.js +16 -16
  22. package/dist/dev-error.d.ts +2 -2
  23. package/dist/dev-error.js +3 -3
  24. package/dist/dev-port.d.ts +2 -2
  25. package/dist/dev-port.js +1 -1
  26. package/dist/dev.d.ts +3 -3
  27. package/dist/dev.js +9 -9
  28. package/dist/draft.d.ts +12 -12
  29. package/dist/draft.js +11 -11
  30. package/dist/fn-state.d.ts +56 -0
  31. package/dist/fn-state.d.ts.map +1 -0
  32. package/dist/fn-state.js +77 -0
  33. package/dist/fn-state.js.map +1 -0
  34. package/dist/fn.d.ts +111 -0
  35. package/dist/fn.d.ts.map +1 -0
  36. package/dist/fn.js +125 -0
  37. package/dist/fn.js.map +1 -0
  38. package/dist/fonts-google.d.ts +9 -9
  39. package/dist/fonts-google.js +10 -10
  40. package/dist/fonts.d.ts +7 -7
  41. package/dist/fonts.js +1 -1
  42. package/dist/forms.d.ts +8 -8
  43. package/dist/fs.d.ts +1 -1
  44. package/dist/fs.js +1 -1
  45. package/dist/index.d.ts +151 -70
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +317 -139
  48. package/dist/index.js.map +1 -1
  49. package/dist/internal/head-attributes.d.ts +15 -0
  50. package/dist/internal/head-attributes.d.ts.map +1 -0
  51. package/dist/internal/head-attributes.js +99 -0
  52. package/dist/internal/head-attributes.js.map +1 -0
  53. package/dist/internal/script-types.d.ts +14 -0
  54. package/dist/internal/script-types.d.ts.map +1 -0
  55. package/dist/internal/script-types.js +17 -0
  56. package/dist/internal/script-types.js.map +1 -0
  57. package/dist/internal/server-boundary.d.ts +60 -0
  58. package/dist/internal/server-boundary.d.ts.map +1 -0
  59. package/dist/internal/server-boundary.js +174 -0
  60. package/dist/internal/server-boundary.js.map +1 -0
  61. package/dist/internal/timing-safe-equal.d.ts +1 -1
  62. package/dist/internal/timing-safe-equal.js +1 -1
  63. package/dist/internal/vite-import.d.ts +63 -0
  64. package/dist/internal/vite-import.d.ts.map +1 -0
  65. package/dist/internal/vite-import.js +94 -0
  66. package/dist/internal/vite-import.js.map +1 -0
  67. package/dist/islands.d.ts +5 -5
  68. package/dist/islands.js +5 -5
  69. package/dist/isr.d.ts +15 -15
  70. package/dist/isr.js +7 -7
  71. package/dist/manifest.d.ts +73 -35
  72. package/dist/manifest.d.ts.map +1 -1
  73. package/dist/manifest.js +10 -16
  74. package/dist/manifest.js.map +1 -1
  75. package/dist/module-graph.d.ts +5 -5
  76. package/dist/module-graph.js +4 -4
  77. package/dist/navigation.d.ts +120 -5
  78. package/dist/navigation.d.ts.map +1 -1
  79. package/dist/navigation.js +37 -5
  80. package/dist/navigation.js.map +1 -1
  81. package/dist/plugins/css-modules.d.ts +3 -3
  82. package/dist/plugins/css-modules.js +0 -0
  83. package/dist/plugins/kit.d.ts +8 -8
  84. package/dist/plugins/kit.js +9 -9
  85. package/dist/plugins/scss.js +3 -3
  86. package/dist/plugins/vite-leak-guard.d.ts +1 -1
  87. package/dist/plugins/vite-leak-guard.js +1 -1
  88. package/dist/plugins/vite-server-fn.d.ts +23 -0
  89. package/dist/plugins/vite-server-fn.d.ts.map +1 -0
  90. package/dist/plugins/vite-server-fn.js +51 -0
  91. package/dist/plugins/vite-server-fn.js.map +1 -0
  92. package/dist/plugins/vite-server-only.d.ts +49 -0
  93. package/dist/plugins/vite-server-only.d.ts.map +1 -0
  94. package/dist/plugins/vite-server-only.js +57 -0
  95. package/dist/plugins/vite-server-only.js.map +1 -0
  96. package/dist/prerender.d.ts +5 -5
  97. package/dist/prerender.js +7 -7
  98. package/dist/query.d.ts +10 -10
  99. package/dist/query.js +15 -15
  100. package/dist/route-manifest.d.ts +1 -1
  101. package/dist/route-manifest.js +2 -2
  102. package/dist/router.d.ts +30 -21
  103. package/dist/router.d.ts.map +1 -1
  104. package/dist/router.js +57 -28
  105. package/dist/router.js.map +1 -1
  106. package/dist/search.d.ts +94 -0
  107. package/dist/search.d.ts.map +1 -0
  108. package/dist/search.js +232 -0
  109. package/dist/search.js.map +1 -0
  110. package/dist/server-only.d.ts +6 -6
  111. package/dist/server-only.js +7 -7
  112. package/dist/service-worker.d.ts +56 -0
  113. package/dist/service-worker.d.ts.map +1 -0
  114. package/dist/service-worker.js +127 -0
  115. package/dist/service-worker.js.map +1 -0
  116. package/dist/vite.d.ts +15 -13
  117. package/dist/vite.d.ts.map +1 -1
  118. package/dist/vite.js +31 -21
  119. package/dist/vite.js.map +1 -1
  120. package/package.json +33 -4
  121. package/server-manifest.d.ts +31 -0
  122. package/src/build-vite.ts +24 -77
  123. package/src/build.ts +137 -105
  124. package/src/bun-dev-entry.ts +1 -1
  125. package/src/chunk-names.ts +1 -1
  126. package/src/client.ts +215 -32
  127. package/src/conformance.ts +3 -3
  128. package/src/deferred.ts +17 -17
  129. package/src/dev-error.ts +3 -3
  130. package/src/dev-port.ts +2 -2
  131. package/src/dev.ts +12 -12
  132. package/src/draft.ts +15 -15
  133. package/src/fn-state.ts +107 -0
  134. package/src/fn.ts +199 -0
  135. package/src/fonts-google.ts +13 -13
  136. package/src/fonts.ts +7 -7
  137. package/src/forms.ts +8 -8
  138. package/src/fs.ts +2 -2
  139. package/src/index.ts +460 -181
  140. package/src/internal/head-attributes.ts +109 -0
  141. package/src/internal/script-types.ts +18 -0
  142. package/src/internal/server-boundary.ts +187 -0
  143. package/src/internal/timing-safe-equal.ts +1 -1
  144. package/src/internal/vite-import.ts +92 -0
  145. package/src/islands.ts +7 -7
  146. package/src/isr.ts +17 -17
  147. package/src/manifest.ts +83 -42
  148. package/src/module-graph.ts +6 -6
  149. package/src/navigation.ts +151 -5
  150. package/src/plugins/css-modules.ts +0 -0
  151. package/src/plugins/kit.ts +11 -11
  152. package/src/plugins/scss.ts +3 -3
  153. package/src/plugins/vite-leak-guard.ts +2 -2
  154. package/src/plugins/vite-server-fn.ts +67 -0
  155. package/src/plugins/vite-server-only.ts +75 -0
  156. package/src/prerender.ts +10 -10
  157. package/src/query.ts +22 -22
  158. package/src/route-manifest.ts +3 -3
  159. package/src/router.ts +91 -41
  160. package/src/search.ts +297 -0
  161. package/src/server-only.ts +7 -7
  162. package/src/service-worker.ts +165 -0
  163. package/src/vite.ts +41 -29
@@ -1,17 +1,17 @@
1
1
  /**
2
- * `@nifrajs/web/plugins/scss` — a SASS/SCSS (`*.scss`, `*.sass`) Bun plugin, in its OWN module so the SSR
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` — same API), not
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 — its CSS is bundled (dom) and it resolves to an empty module (ssr).
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` — nifra's two client-leak guards, for a Vite/Rollup production
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 — `generateBundle` bound to the plugin context. */
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 — this sits entirely above the render seam.
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 — just a fetch handler (a built `createWebApp`). */
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) — so the deploy
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 — surfaced for the caller to log.
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 — a deferred loader answers with NDJSON, which the static fast-path doesn't cover (the
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) — release the stream
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 — not supported" })
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 — plus its static `_data.json`. */
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 — pass those via `staticGlobs` and a smaller `prerendered`.
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 — a `query(key, fn)` primitive with in-flight dedup, `staleTime`
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) — so it unit-tests deterministically and is safe to import
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 — what a binding renders. A new (frozen) object per transition, so a
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 — the initial load OR a background refetch (data may still show). */
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 — injected so the core stays deterministic + testable; the adapter binding
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 — LRU-evict the oldest *unsubscribed* entry past it (default 1000). */
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 — return `undefined`/`null` to signal there is no next 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 — the SSR→client bridge payload. */
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 — a function/symbol in the key throws (it can't be a stable
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 — so a fetch begun *before* an
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()` — no background timer.
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 — no per-emit copy (the common case is 0–1 subscribers). Contract: a
255
- // listener must not synchronously subscribe/unsubscribe during notification — `useSyncExternalStore`/
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 — but join the in-flight fetch ONLY when it belongs to the current generation. An
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 — no fn call
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 — discard this result
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 — cancel any pending eviction
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 — its result is pre-mutation and must
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 — its `fn` throws until a real `query(key, fn)` re-binds
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 — bump the generation so any in-flight fetch can't overwrite it,
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 — framework-agnostic, so a per-adapter `useMutation`
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 — an `await undefined` would insert a
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 {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `@nifrajs/web/route-manifest` — what each route actually DOES, as one artifact.
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 — target: ${manifest.target}`,
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} — ${conflict.consequence}`)
236
+ lines.push(` ✗ ${conflict.pattern} - ${conflict.consequence}`)
237
237
  }
238
238
  }
239
239
  return lines.join("\n")