@nifrajs/web 4.0.2 → 5.0.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 +11 -0
- package/dist/build-vite.d.ts +19 -0
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +31 -10
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +8 -10
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +90 -31
- package/dist/build.js.map +1 -1
- package/dist/cdn.d.ts +8 -0
- package/dist/cdn.d.ts.map +1 -1
- package/dist/cdn.js +14 -15
- package/dist/cdn.js.map +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +25 -7
- package/dist/client.js.map +1 -1
- package/dist/deferred.d.ts +2 -2
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +28 -7
- package/dist/deferred.js.map +1 -1
- package/dist/dev-feed.d.ts.map +1 -1
- package/dist/dev-feed.js +24 -5
- package/dist/dev-feed.js.map +1 -1
- package/dist/dev-session.d.ts.map +1 -1
- package/dist/dev-session.js +4 -14
- package/dist/dev-session.js.map +1 -1
- package/dist/dev.d.ts +7 -17
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +13 -1
- package/dist/dev.js.map +1 -1
- package/dist/draft.d.ts +8 -5
- package/dist/draft.d.ts.map +1 -1
- package/dist/draft.js +30 -13
- package/dist/draft.js.map +1 -1
- package/dist/fonts-google.d.ts.map +1 -1
- package/dist/fonts-google.js +7 -0
- package/dist/fonts-google.js.map +1 -1
- package/dist/index.d.ts +2 -2
- 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 +1 -0
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +56 -29
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/disposable-dir.d.ts +8 -0
- package/dist/internal/disposable-dir.d.ts.map +1 -0
- package/dist/internal/disposable-dir.js +25 -0
- package/dist/internal/disposable-dir.js.map +1 -0
- package/dist/internal/entry-chunks.d.ts +9 -0
- package/dist/internal/entry-chunks.d.ts.map +1 -0
- package/dist/internal/entry-chunks.js +15 -0
- package/dist/internal/entry-chunks.js.map +1 -0
- package/dist/internal/evidence-traversal.d.ts +12 -0
- package/dist/internal/evidence-traversal.d.ts.map +1 -0
- package/dist/internal/evidence-traversal.js +48 -0
- package/dist/internal/evidence-traversal.js.map +1 -0
- package/dist/internal/head-merge.d.ts +6 -0
- package/dist/internal/head-merge.d.ts.map +1 -1
- package/dist/internal/head-merge.js +9 -0
- package/dist/internal/head-merge.js.map +1 -1
- package/dist/internal/js-string.d.ts +2 -0
- package/dist/internal/js-string.d.ts.map +1 -1
- package/dist/internal/js-string.js +5 -1
- package/dist/internal/js-string.js.map +1 -1
- package/dist/internal/mount-shadow.js +1 -1
- package/dist/internal/mount-shadow.js.map +1 -1
- package/dist/internal/output-guard.d.ts +7 -0
- package/dist/internal/output-guard.d.ts.map +1 -1
- package/dist/internal/output-guard.js +80 -8
- package/dist/internal/output-guard.js.map +1 -1
- package/dist/internal/page-execution.d.ts +6 -0
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +377 -108
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/progressive-cookies.d.ts +14 -0
- package/dist/internal/progressive-cookies.d.ts.map +1 -0
- package/dist/internal/progressive-cookies.js +43 -0
- package/dist/internal/progressive-cookies.js.map +1 -0
- package/dist/internal/public-env.d.ts +2 -0
- package/dist/internal/public-env.d.ts.map +1 -0
- package/dist/internal/public-env.js +13 -0
- package/dist/internal/public-env.js.map +1 -0
- package/dist/internal/render-document.d.ts +1 -0
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +28 -5
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/response-controls.d.ts +2 -0
- package/dist/internal/response-controls.d.ts.map +1 -1
- package/dist/internal/response-controls.js +26 -16
- package/dist/internal/response-controls.js.map +1 -1
- package/dist/internal/revive-dates.d.ts +16 -0
- package/dist/internal/revive-dates.d.ts.map +1 -0
- package/dist/internal/revive-dates.js +32 -0
- package/dist/internal/revive-dates.js.map +1 -0
- package/dist/internal/route-path.d.ts +8 -0
- package/dist/internal/route-path.d.ts.map +1 -0
- package/dist/internal/route-path.js +8 -0
- package/dist/internal/route-path.js.map +1 -0
- package/dist/internal/secret-scan.d.ts.map +1 -1
- package/dist/internal/secret-scan.js +88 -20
- package/dist/internal/secret-scan.js.map +1 -1
- package/dist/internal/server-boundary.d.ts.map +1 -1
- package/dist/internal/server-boundary.js +33 -5
- package/dist/internal/server-boundary.js.map +1 -1
- package/dist/internal/web-app.d.ts +21 -16
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +73 -21
- package/dist/internal/web-app.js.map +1 -1
- package/dist/internal/wire-date.d.ts +3 -0
- package/dist/internal/wire-date.d.ts.map +1 -0
- package/dist/internal/wire-date.js +14 -0
- package/dist/internal/wire-date.js.map +1 -0
- package/dist/internal/zone-graph.d.ts +4 -0
- package/dist/internal/zone-graph.d.ts.map +1 -1
- package/dist/internal/zone-graph.js +54 -14
- package/dist/internal/zone-graph.js.map +1 -1
- package/dist/isr.d.ts +58 -2
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +296 -39
- package/dist/isr.js.map +1 -1
- package/dist/llms-txt.d.ts +2 -0
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +29 -3
- package/dist/llms-txt.js.map +1 -1
- package/dist/manifest.d.ts +28 -4
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +3 -0
- package/dist/manifest.js.map +1 -1
- package/dist/open-resolved.d.ts +18 -0
- package/dist/open-resolved.d.ts.map +1 -0
- package/dist/open-resolved.js +62 -0
- package/dist/open-resolved.js.map +1 -0
- package/dist/plugins/kit.d.ts +1 -0
- package/dist/plugins/kit.d.ts.map +1 -1
- package/dist/plugins/kit.js +26 -11
- package/dist/plugins/kit.js.map +1 -1
- package/dist/plugins/vite-leak-guard.d.ts +3 -1
- package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
- package/dist/plugins/vite-leak-guard.js +4 -0
- package/dist/plugins/vite-leak-guard.js.map +1 -1
- package/dist/public-dir.d.ts +8 -1
- package/dist/public-dir.d.ts.map +1 -1
- package/dist/public-dir.js +17 -6
- package/dist/public-dir.js.map +1 -1
- package/dist/query.d.ts +13 -4
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +89 -26
- package/dist/query.js.map +1 -1
- package/dist/render-seam.d.ts +2 -0
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +1 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/route-types.d.ts.map +1 -1
- package/dist/route-types.js +20 -9
- package/dist/route-types.js.map +1 -1
- package/dist/router.d.ts +9 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +39 -46
- package/dist/router.js.map +1 -1
- package/dist/server-components/build-info.d.ts +3 -0
- package/dist/server-components/build-info.d.ts.map +1 -0
- package/dist/server-components/build-info.js +7 -0
- package/dist/server-components/build-info.js.map +1 -0
- package/dist/server-components/build-target.d.ts +12 -0
- package/dist/server-components/build-target.d.ts.map +1 -0
- package/dist/server-components/build-target.js +153 -0
- package/dist/server-components/build-target.js.map +1 -0
- package/dist/server-components/build.d.ts +24 -0
- package/dist/server-components/build.d.ts.map +1 -0
- package/dist/server-components/build.js +366 -0
- package/dist/server-components/build.js.map +1 -0
- package/dist/server-components/bundle-proof.d.ts +3 -0
- package/dist/server-components/bundle-proof.d.ts.map +1 -0
- package/dist/server-components/bundle-proof.js +23 -0
- package/dist/server-components/bundle-proof.js.map +1 -0
- package/dist/server-components/client.d.ts +46 -0
- package/dist/server-components/client.d.ts.map +1 -0
- package/dist/server-components/client.js +594 -0
- package/dist/server-components/client.js.map +1 -0
- package/dist/server-components/definition.d.ts +11 -0
- package/dist/server-components/definition.d.ts.map +1 -0
- package/dist/server-components/definition.js +94 -0
- package/dist/server-components/definition.js.map +1 -0
- package/dist/server-components/dev-http.d.ts +5 -0
- package/dist/server-components/dev-http.d.ts.map +1 -0
- package/dist/server-components/dev-http.js +71 -0
- package/dist/server-components/dev-http.js.map +1 -0
- package/dist/server-components/error.d.ts +9 -0
- package/dist/server-components/error.d.ts.map +1 -0
- package/dist/server-components/error.js +24 -0
- package/dist/server-components/error.js.map +1 -0
- package/dist/server-components/frames.d.ts +7 -0
- package/dist/server-components/frames.d.ts.map +1 -0
- package/dist/server-components/frames.js +169 -0
- package/dist/server-components/frames.js.map +1 -0
- package/dist/server-components/html-policy.d.ts +6 -0
- package/dist/server-components/html-policy.d.ts.map +1 -0
- package/dist/server-components/html-policy.js +75 -0
- package/dist/server-components/html-policy.js.map +1 -0
- package/dist/server-components/html.d.ts +4 -0
- package/dist/server-components/html.d.ts.map +1 -0
- package/dist/server-components/html.js +67 -0
- package/dist/server-components/html.js.map +1 -0
- package/dist/server-components/islands.d.ts +44 -0
- package/dist/server-components/islands.d.ts.map +1 -0
- package/dist/server-components/islands.js +42 -0
- package/dist/server-components/islands.js.map +1 -0
- package/dist/server-components/native.d.ts +4 -0
- package/dist/server-components/native.d.ts.map +1 -0
- package/dist/server-components/native.js +81 -0
- package/dist/server-components/native.js.map +1 -0
- package/dist/server-components/portal.d.ts +6 -0
- package/dist/server-components/portal.d.ts.map +1 -0
- package/dist/server-components/portal.js +35 -0
- package/dist/server-components/portal.js.map +1 -0
- package/dist/server-components/props.d.ts +3 -0
- package/dist/server-components/props.d.ts.map +1 -0
- package/dist/server-components/props.js +14 -0
- package/dist/server-components/props.js.map +1 -0
- package/dist/server-components/protocol.d.ts +44 -0
- package/dist/server-components/protocol.d.ts.map +1 -0
- package/dist/server-components/protocol.js +139 -0
- package/dist/server-components/protocol.js.map +1 -0
- package/dist/server-components/range.d.ts +24 -0
- package/dist/server-components/range.d.ts.map +1 -0
- package/dist/server-components/range.js +263 -0
- package/dist/server-components/range.js.map +1 -0
- package/dist/server-components/registry.d.ts +6 -0
- package/dist/server-components/registry.d.ts.map +1 -0
- package/dist/server-components/registry.js +30 -0
- package/dist/server-components/registry.js.map +1 -0
- package/dist/server-components/response.d.ts +4 -0
- package/dist/server-components/response.d.ts.map +1 -0
- package/dist/server-components/response.js +32 -0
- package/dist/server-components/response.js.map +1 -0
- package/dist/server-components/router-integration.d.ts +9 -0
- package/dist/server-components/router-integration.d.ts.map +1 -0
- package/dist/server-components/router-integration.js +29 -0
- package/dist/server-components/router-integration.js.map +1 -0
- package/dist/server-components/runtime.d.ts +14 -0
- package/dist/server-components/runtime.d.ts.map +1 -0
- package/dist/server-components/runtime.js +276 -0
- package/dist/server-components/runtime.js.map +1 -0
- package/dist/server-components/scheduler.d.ts +14 -0
- package/dist/server-components/scheduler.d.ts.map +1 -0
- package/dist/server-components/scheduler.js +74 -0
- package/dist/server-components/scheduler.js.map +1 -0
- package/dist/server-components/schema.d.ts +4 -0
- package/dist/server-components/schema.d.ts.map +1 -0
- package/dist/server-components/schema.js +77 -0
- package/dist/server-components/schema.js.map +1 -0
- package/dist/server-components/server-entry.d.ts +12 -0
- package/dist/server-components/server-entry.d.ts.map +1 -0
- package/dist/server-components/server-entry.js +90 -0
- package/dist/server-components/server-entry.js.map +1 -0
- package/dist/server-components/source.d.ts +15 -0
- package/dist/server-components/source.d.ts.map +1 -0
- package/dist/server-components/source.js +269 -0
- package/dist/server-components/source.js.map +1 -0
- package/dist/server-components/state.d.ts +4 -0
- package/dist/server-components/state.d.ts.map +1 -0
- package/dist/server-components/state.js +123 -0
- package/dist/server-components/state.js.map +1 -0
- package/dist/server-components/stream.d.ts +6 -0
- package/dist/server-components/stream.d.ts.map +1 -0
- package/dist/server-components/stream.js +70 -0
- package/dist/server-components/stream.js.map +1 -0
- package/dist/server-components/trusted-html.d.ts +3 -0
- package/dist/server-components/trusted-html.d.ts.map +1 -0
- package/dist/server-components/trusted-html.js +14 -0
- package/dist/server-components/trusted-html.js.map +1 -0
- package/dist/server-components/types.d.ts +107 -0
- package/dist/server-components/types.d.ts.map +1 -0
- package/dist/server-components/types.js +2 -0
- package/dist/server-components/types.js.map +1 -0
- package/dist/server-components/typescript.d.ts +11 -0
- package/dist/server-components/typescript.d.ts.map +1 -0
- package/dist/server-components/typescript.js +28 -0
- package/dist/server-components/typescript.js.map +1 -0
- package/dist/server-components/value.d.ts +5 -0
- package/dist/server-components/value.d.ts.map +1 -0
- package/dist/server-components/value.js +77 -0
- package/dist/server-components/value.js.map +1 -0
- package/dist/server-components/vite.d.ts +12 -0
- package/dist/server-components/vite.d.ts.map +1 -0
- package/dist/server-components/vite.js +197 -0
- package/dist/server-components/vite.js.map +1 -0
- package/dist/server-components.d.ts +5 -0
- package/dist/server-components.d.ts.map +1 -0
- package/dist/server-components.js +4 -0
- package/dist/server-components.js.map +1 -0
- package/dist/service-worker.d.ts +8 -1
- package/dist/service-worker.d.ts.map +1 -1
- package/dist/service-worker.js +20 -6
- package/dist/service-worker.js.map +1 -1
- package/dist/vite.d.ts +12 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +7 -1
- package/dist/vite.js.map +1 -1
- package/dist/zones.d.ts +40 -0
- package/dist/zones.d.ts.map +1 -1
- package/dist/zones.js +170 -6
- package/dist/zones.js.map +1 -1
- package/package.json +97 -4
- package/src/build-vite.ts +31 -9
- package/src/build.ts +109 -31
- package/src/cdn.ts +18 -12
- package/src/client.ts +25 -6
- package/src/deferred.ts +27 -9
- package/src/dev-feed.ts +24 -5
- package/src/dev-session.ts +4 -13
- package/src/dev.ts +29 -1
- package/src/draft.ts +35 -14
- package/src/fonts-google.ts +10 -0
- package/src/index.ts +3 -2
- package/src/internal/codegen.ts +60 -29
- package/src/internal/disposable-dir.ts +24 -0
- package/src/internal/entry-chunks.ts +18 -0
- package/src/internal/evidence-traversal.ts +57 -0
- package/src/internal/head-merge.ts +16 -0
- package/src/internal/js-string.ts +6 -1
- package/src/internal/mount-shadow.ts +1 -1
- package/src/internal/output-guard.ts +88 -7
- package/src/internal/page-execution.ts +506 -108
- package/src/internal/progressive-cookies.ts +60 -0
- package/src/internal/public-env.ts +14 -0
- package/src/internal/render-document.ts +26 -11
- package/src/internal/response-controls.ts +31 -14
- package/src/internal/revive-dates.ts +35 -0
- package/src/internal/route-path.ts +8 -0
- package/src/internal/secret-scan.ts +90 -18
- package/src/internal/server-boundary.ts +29 -5
- package/src/internal/web-app.ts +116 -37
- package/src/internal/wire-date.ts +16 -0
- package/src/internal/zone-graph.ts +59 -19
- package/src/isr.ts +351 -43
- package/src/llms-txt.ts +32 -3
- package/src/manifest.ts +28 -4
- package/src/open-resolved.ts +64 -0
- package/src/plugins/kit.ts +31 -11
- package/src/plugins/vite-leak-guard.ts +5 -1
- package/src/public-dir.ts +27 -8
- package/src/query.ts +107 -26
- package/src/render-seam.ts +2 -0
- package/src/route-types.ts +34 -7
- package/src/router.ts +58 -55
- package/src/server-components/build-info.ts +11 -0
- package/src/server-components/build-target.ts +194 -0
- package/src/server-components/build.ts +419 -0
- package/src/server-components/bundle-proof.ts +33 -0
- package/src/server-components/client.ts +663 -0
- package/src/server-components/definition.ts +125 -0
- package/src/server-components/dev-http.ts +75 -0
- package/src/server-components/error.ts +24 -0
- package/src/server-components/frames.ts +181 -0
- package/src/server-components/html-policy.ts +94 -0
- package/src/server-components/html.ts +81 -0
- package/src/server-components/islands.ts +111 -0
- package/src/server-components/native.ts +101 -0
- package/src/server-components/portal.ts +42 -0
- package/src/server-components/props.ts +19 -0
- package/src/server-components/protocol.ts +167 -0
- package/src/server-components/range.ts +313 -0
- package/src/server-components/registry.ts +34 -0
- package/src/server-components/response.ts +44 -0
- package/src/server-components/router-integration.ts +40 -0
- package/src/server-components/runtime.ts +376 -0
- package/src/server-components/scheduler.ts +84 -0
- package/src/server-components/schema.ts +69 -0
- package/src/server-components/server-entry.ts +98 -0
- package/src/server-components/source.ts +324 -0
- package/src/server-components/state.ts +143 -0
- package/src/server-components/stream.ts +71 -0
- package/src/server-components/trusted-html.ts +21 -0
- package/src/server-components/types.ts +138 -0
- package/src/server-components/typescript.ts +39 -0
- package/src/server-components/value.ts +82 -0
- package/src/server-components/vite.ts +242 -0
- package/src/server-components.ts +32 -0
- package/src/service-worker.ts +24 -7
- package/src/vite.ts +8 -1
- package/src/zones.ts +208 -7
package/src/llms-txt.ts
CHANGED
|
@@ -3,9 +3,12 @@ import {
|
|
|
3
3
|
reflectedRoutesFromEvidence,
|
|
4
4
|
snapshotProjectEvidence,
|
|
5
5
|
} from "@nifrajs/core/evidence"
|
|
6
|
+
import { type BackendEvidenceProvider, NIFRA_BACKEND_EVIDENCE } from "@nifrajs/core/mount"
|
|
6
7
|
import { compileRoutePattern, type RoutePatternSegment } from "@nifrajs/core/pattern"
|
|
7
8
|
|
|
8
9
|
const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
|
10
|
+
const RESERVED_CLIENT_SEGMENT = /^(?:get|post|put|patch|delete|head|options)$/i
|
|
11
|
+
const RESERVED_EXACT_SEGMENTS = new Set(["subscribe", "ws", "index", "then"])
|
|
9
12
|
|
|
10
13
|
/** The subset of JSON Schema fields {@link tsTypeOf} reads to render a TypeScript type string. */
|
|
11
14
|
interface JsonSchemaNode {
|
|
@@ -93,7 +96,13 @@ function clientCall(method: string, path: string, schema: unknown): string {
|
|
|
93
96
|
if (segs.length === 0) chain += ".index"
|
|
94
97
|
else
|
|
95
98
|
for (const seg of segs) {
|
|
96
|
-
chain +=
|
|
99
|
+
chain +=
|
|
100
|
+
paramCall(seg) ??
|
|
101
|
+
(RESERVED_CLIENT_SEGMENT.test(seg) || RESERVED_EXACT_SEGMENTS.has(seg)
|
|
102
|
+
? `(${JSON.stringify(seg)})`
|
|
103
|
+
: IDENT.test(seg)
|
|
104
|
+
? `.${seg}`
|
|
105
|
+
: `[${JSON.stringify(seg)}]`)
|
|
97
106
|
}
|
|
98
107
|
const isBodyVerb = verb === "post" || verb === "put" || verb === "patch"
|
|
99
108
|
let call: string
|
|
@@ -101,12 +110,27 @@ function clientCall(method: string, path: string, schema: unknown): string {
|
|
|
101
110
|
const bodyArg = s?.body ? "body" : s?.query ? "undefined" : ""
|
|
102
111
|
const opts = s?.query ? (bodyArg ? ", { query }" : "{ query }") : ""
|
|
103
112
|
call = `.${verb}(${bodyArg}${opts})`
|
|
113
|
+
} else if (verb === "delete") {
|
|
114
|
+
const fields = [...(s?.body ? ["body"] : []), ...(s?.query ? ["query"] : [])]
|
|
115
|
+
call = `.delete(${fields.length === 0 ? "" : `{ ${fields.join(", ")} }`})`
|
|
104
116
|
} else {
|
|
105
117
|
call = `.${verb}(${s?.query ? "{ query }" : ""})`
|
|
106
118
|
}
|
|
107
119
|
return `await ${chain}${call}`
|
|
108
120
|
}
|
|
109
121
|
|
|
122
|
+
async function backendEvidence(backend: unknown): Promise<ProjectEvidenceSnapshot> {
|
|
123
|
+
if (backend !== null && (typeof backend === "object" || typeof backend === "function")) {
|
|
124
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: Object/function check allows reading the optional symbol-keyed provider without touching the client path proxy.
|
|
125
|
+
const provider = (backend as { readonly [NIFRA_BACKEND_EVIDENCE]?: unknown })[
|
|
126
|
+
NIFRA_BACKEND_EVIDENCE
|
|
127
|
+
]
|
|
128
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: A callable at NIFRA_BACKEND_EVIDENCE implements the framework's trusted evidence-provider seam.
|
|
129
|
+
if (typeof provider === "function") return (provider as BackendEvidenceProvider).call(backend)
|
|
130
|
+
}
|
|
131
|
+
return snapshotProjectEvidence(backend)
|
|
132
|
+
}
|
|
133
|
+
|
|
110
134
|
/** Read the project's `AGENTS.md`, or `""` if there isn't one (or we're on an edge runtime with no
|
|
111
135
|
* filesystem). Never throws - a missing file just means no guidelines section. */
|
|
112
136
|
async function readAgentsMd(): Promise<string> {
|
|
@@ -134,6 +158,8 @@ export async function generateLlmsTxt(
|
|
|
134
158
|
readonly includeLocalGuidelines?: boolean
|
|
135
159
|
/** Reuse the project's canonical route-evidence pass when one is already available. */
|
|
136
160
|
readonly evidence?: ProjectEvidenceSnapshot
|
|
161
|
+
/** The mount prefix a visitor's URL carries but the backend's own routes do not (`apiStrip`). */
|
|
162
|
+
readonly apiPathPrefix?: string
|
|
137
163
|
} = {},
|
|
138
164
|
): Promise<string> {
|
|
139
165
|
let output = ""
|
|
@@ -160,14 +186,17 @@ export async function generateLlmsTxt(
|
|
|
160
186
|
// 3. API Routes
|
|
161
187
|
output += `## API Routes\n\n`
|
|
162
188
|
const apiRoutes = reflectedRoutesFromEvidence(
|
|
163
|
-
options.evidence ??
|
|
189
|
+
options.evidence ?? (await backendEvidence(backend)),
|
|
164
190
|
)
|
|
165
191
|
|
|
166
192
|
if (apiRoutes.length === 0) {
|
|
167
193
|
output += `No API routes registered.\n`
|
|
168
194
|
} else {
|
|
195
|
+
const prefix = options.apiPathPrefix ?? ""
|
|
169
196
|
for (const route of apiRoutes) {
|
|
170
|
-
|
|
197
|
+
// The stripped backend's root is the bare prefix, as the mount matches it.
|
|
198
|
+
const path = prefix !== "" && route.path === "/" ? prefix : prefix + route.path
|
|
199
|
+
output += `- **${route.method}** \`${path}\`\n`
|
|
171
200
|
if (full) {
|
|
172
201
|
output += ` - Client Call: \`${clientCall(route.method, route.path, route.schema)}\`\n`
|
|
173
202
|
const s = route.schema
|
package/src/manifest.ts
CHANGED
|
@@ -271,13 +271,24 @@ export interface Meta {
|
|
|
271
271
|
}
|
|
272
272
|
|
|
273
273
|
/**
|
|
274
|
-
* Args for a route's `meta` function:
|
|
275
|
-
* `meta()` runs in BOTH SSR and client navigation, so it has
|
|
276
|
-
* `origin` is the only server-resolved fact it gets (so you
|
|
277
|
-
* for absolute `og:url`/`canonical`/`og:image` URLs). See
|
|
274
|
+
* Args for a route's or layout's `meta` function: its own loader's `data` + the page's `pageData` + the
|
|
275
|
+
* route `params` + the request `origin`. `meta()` runs in BOTH SSR and client navigation, so it has
|
|
276
|
+
* **no `request`/`process.env`/server access** - `origin` is the only server-resolved fact it gets (so you
|
|
277
|
+
* needn't thread `siteUrl` through loader data for absolute `og:url`/`canonical`/`og:image` URLs). See
|
|
278
|
+
* {@link origin}.
|
|
278
279
|
*/
|
|
279
280
|
export interface MetaArgs<Data = unknown> {
|
|
281
|
+
/**
|
|
282
|
+
* The loader data of the module that exports this `meta`: the page's for a page, the layout's own for a
|
|
283
|
+
* layout (`null` when the layout has no loader), the same value its component receives. A layout that
|
|
284
|
+
* loads the locale sets `lang` from here, so a soft navigation keeps it.
|
|
285
|
+
*/
|
|
280
286
|
readonly data: Data
|
|
287
|
+
/**
|
|
288
|
+
* The page's loader data, for a layout's `meta` that builds on the page (a title template). In a page's
|
|
289
|
+
* own `meta` it is the same value as {@link data}. Always set by the framework.
|
|
290
|
+
*/
|
|
291
|
+
readonly pageData?: unknown
|
|
281
292
|
readonly params: Record<string, string>
|
|
282
293
|
/** The resolved document CSP nonce, when this render uses per-request nonce protection. */
|
|
283
294
|
readonly nonce?: string
|
|
@@ -323,6 +334,8 @@ export type GetStaticPaths = () => StaticPaths | Promise<StaticPaths>
|
|
|
323
334
|
export interface RouteModule {
|
|
324
335
|
readonly default: unknown
|
|
325
336
|
readonly loader?: Loader
|
|
337
|
+
/** Layout loader that must run before actions and is not retained across the action's render. */
|
|
338
|
+
readonly gate?: boolean
|
|
326
339
|
readonly action?: Action
|
|
327
340
|
/**
|
|
328
341
|
* Backend half only: the schema of what {@link loader} sends to the browser. Required whenever the
|
|
@@ -384,6 +397,14 @@ export interface RouteModule {
|
|
|
384
397
|
* the module on the server and in the browser, so it must be the same value on both.
|
|
385
398
|
*/
|
|
386
399
|
readonly handle?: unknown
|
|
400
|
+
/**
|
|
401
|
+
* On a root status page (`_404.tsx`, `_<status>.tsx`) or an `_error.tsx`. By default (`true`) the
|
|
402
|
+
* page renders inside the layouts above it with their loader data and `meta`, as a nested `_404`
|
|
403
|
+
* renders. When those layouts cannot render it - a loader throws, a gate or the middleware refuses -
|
|
404
|
+
* a root status page renders alone and an `_error` inside its layouts without their data, with the
|
|
405
|
+
* same status. `false` always renders them that way, and runs no layout loader for them.
|
|
406
|
+
*/
|
|
407
|
+
readonly inLayouts?: boolean
|
|
387
408
|
/**
|
|
388
409
|
* Opt a **static** route (no `:param`/`*`) into build-time prerendering (SSG): `prerenderRoutes`
|
|
389
410
|
* (from `@nifrajs/web/build`) renders it to a static `index.html` at build. The loader runs at build
|
|
@@ -998,6 +1019,9 @@ const ancestorDirs = (file: string): string[] => {
|
|
|
998
1019
|
* turns a path into a lazy module loader. Pure - no fs. Throws at boot (the loud-and-early
|
|
999
1020
|
* RouteConfigError ethos) on duplicate patterns. `_layout`/`_404`/`_error`/`_loading`/`_middleware`
|
|
1000
1021
|
* files are special; other `_`-prefixed files are ignored (private/colocated, never routed).
|
|
1022
|
+
* A page pairs with its `.backend.*` half only when both are in `files`, so a hand-written list that
|
|
1023
|
+
* omits the half serves the page without its loader and guards; `discoverRoutes` from
|
|
1024
|
+
* `@nifrajs/web/fs` scans a routes dir and never does.
|
|
1001
1025
|
*/
|
|
1002
1026
|
export function buildManifest(
|
|
1003
1027
|
files: readonly string[],
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { constants as FS } from "node:fs"
|
|
2
|
+
import { type FileHandle, open, readlink, realpath, stat } from "node:fs/promises"
|
|
3
|
+
|
|
4
|
+
// Darwin's open(2) flag refusing a symlink in ANY component; `O_NOFOLLOW` covers only the last one.
|
|
5
|
+
const O_NOFOLLOW_ANY = 0x20000000
|
|
6
|
+
// Deno's node:fs rejects flags it does not know with EINVAL; the first refusal turns the flag off.
|
|
7
|
+
let noFollowAny: boolean | undefined
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Open a name `realpath` already resolved, refusing it when any component became a symlink since.
|
|
11
|
+
* A local writer inside the served tree can swap an intermediate directory for a link after the
|
|
12
|
+
* containment check; `O_NOFOLLOW` alone would follow it. Darwin refuses such a path in the kernel;
|
|
13
|
+
* Linux reads back the name the descriptor holds. Elsewhere, Windows included, no file API can open
|
|
14
|
+
* without following a middle link or name an open descriptor's path, so the check in
|
|
15
|
+
* {@link opensAt} is a platform limit: it narrows the race to repeated swaps but cannot close it.
|
|
16
|
+
*/
|
|
17
|
+
export async function openResolved(realFile: string): Promise<FileHandle> {
|
|
18
|
+
// Read per open, not at load: this module can sit in a client bundle graph, where `process` is
|
|
19
|
+
// undefined.
|
|
20
|
+
if (noFollowAny ?? process.platform === "darwin") {
|
|
21
|
+
try {
|
|
22
|
+
return await open(realFile, FS.O_RDONLY | O_NOFOLLOW_ANY)
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (!(error instanceof Error && "code" in error && error.code === "EINVAL")) throw error
|
|
25
|
+
noFollowAny = false
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const handle = await open(realFile, FS.O_RDONLY | FS.O_NOFOLLOW)
|
|
29
|
+
try {
|
|
30
|
+
if (await opensAt(handle, realFile)) return handle
|
|
31
|
+
} catch {}
|
|
32
|
+
await handle.close()
|
|
33
|
+
throw Object.assign(new Error(`${realFile} moved while it was opened`), { code: "ELOOP" })
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Whether `handle` is the file at `realFile` with no symlink in its path. */
|
|
37
|
+
export async function opensAt(
|
|
38
|
+
handle: Pick<FileHandle, "fd" | "stat">,
|
|
39
|
+
realFile: string,
|
|
40
|
+
): Promise<boolean> {
|
|
41
|
+
try {
|
|
42
|
+
return (await readlink(`/proc/self/fd/${handle.fd}`)) === realFile
|
|
43
|
+
} catch {
|
|
44
|
+
return sameFileAt(handle, realFile)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* {@link opensAt} where no descriptor's path can be read. Exported for its test, which runs it on
|
|
50
|
+
* every host.
|
|
51
|
+
*/
|
|
52
|
+
export async function sameFileAt(
|
|
53
|
+
handle: Pick<FileHandle, "stat">,
|
|
54
|
+
realFile: string,
|
|
55
|
+
): Promise<boolean> {
|
|
56
|
+
// One step after another, so passing takes a swap back and a swap again between steps: the name
|
|
57
|
+
// resolves to itself, then to the opened file, then to itself again. Identities are compared as
|
|
58
|
+
// bigints because a Windows file index can exceed 2^53.
|
|
59
|
+
if ((await realpath(realFile)) !== realFile) return false
|
|
60
|
+
const opened = await handle.stat({ bigint: true })
|
|
61
|
+
const named = await stat(realFile, { bigint: true })
|
|
62
|
+
if (opened.dev !== named.dev || opened.ino !== named.ino) return false
|
|
63
|
+
return (await realpath(realFile)) === realFile
|
|
64
|
+
}
|
package/src/plugins/kit.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* CSS→client-bundle wiring, the deterministic hash, or the optional-peer guard. Internal to
|
|
5
5
|
* `@nifrajs/web` (imported by the sibling plugin modules); not a public subpath.
|
|
6
6
|
*/
|
|
7
|
-
import { existsSync } from "node:fs"
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
8
8
|
import { dirname, join, relative } from "node:path"
|
|
9
9
|
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
10
10
|
import type { BunPlugin } from "bun"
|
|
@@ -23,7 +23,7 @@ export type PluginBuilder = Parameters<BunPlugin["setup"]>[0]
|
|
|
23
23
|
*/
|
|
24
24
|
export { rewriteSsrImports } from "../dev-ssr-graph.ts"
|
|
25
25
|
|
|
26
|
-
const
|
|
26
|
+
const packageCache = new Map<string, PackageOf>()
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* Convert a plugin/runtime path into a path the host filesystem can open.
|
|
@@ -57,14 +57,31 @@ export function portablePath(path: string): string {
|
|
|
57
57
|
return normalizeFilePath(path).replaceAll("\\", "/")
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
interface PackageOf {
|
|
61
|
+
readonly root: string
|
|
62
|
+
readonly name: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The nearest ancestor directory of `startDir` that holds a `package.json` (the file's package root)
|
|
66
|
+
* and that package's `name` (`""` when it has none), cached. Falls back to `startDir` if none is found,
|
|
67
|
+
* so it never throws. */
|
|
68
|
+
function packageOf(startDir: string): PackageOf {
|
|
69
|
+
const cached = packageCache.get(startDir)
|
|
64
70
|
if (cached !== undefined) return cached
|
|
65
71
|
let dir = startDir
|
|
72
|
+
let name = ""
|
|
66
73
|
for (;;) {
|
|
67
|
-
|
|
74
|
+
const manifest = join(dir, "package.json")
|
|
75
|
+
if (existsSync(manifest)) {
|
|
76
|
+
try {
|
|
77
|
+
const parsed: unknown = JSON.parse(readFileSync(manifest, "utf8"))
|
|
78
|
+
if (typeof parsed === "object" && parsed !== null && "name" in parsed)
|
|
79
|
+
name = typeof parsed.name === "string" ? parsed.name : ""
|
|
80
|
+
} catch {
|
|
81
|
+
// An unreadable manifest still marks the package root; the name is only a disambiguator.
|
|
82
|
+
}
|
|
83
|
+
break
|
|
84
|
+
}
|
|
68
85
|
const parent = dirname(dir)
|
|
69
86
|
if (parent === dir) {
|
|
70
87
|
dir = startDir // reached the filesystem root with no package.json - anchor on the file's own dir
|
|
@@ -72,8 +89,9 @@ function packageRootOf(startDir: string): string {
|
|
|
72
89
|
}
|
|
73
90
|
dir = parent
|
|
74
91
|
}
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
const found = { root: dir, name }
|
|
93
|
+
packageCache.set(startDir, found)
|
|
94
|
+
return found
|
|
77
95
|
}
|
|
78
96
|
|
|
79
97
|
/**
|
|
@@ -84,11 +102,13 @@ function packageRootOf(startDir: string): string {
|
|
|
84
102
|
* different machines or from different cwds - derive the SAME relative path for a given file, so their
|
|
85
103
|
* scoped class maps always agree. (Hashing the absolute path would differ across CI/host; a
|
|
86
104
|
* cwd-relative path would silently desync if the SSR server started from a non-project-root cwd.)
|
|
105
|
+
* The package's `name` leads the path, so `src/Card.vue` in two workspace packages hashes apart.
|
|
87
106
|
*/
|
|
88
107
|
export function reproduciblePath(absolutePath: string): string {
|
|
89
108
|
const filePath = normalizeFilePath(absolutePath)
|
|
90
|
-
const root =
|
|
91
|
-
|
|
109
|
+
const { root, name } = packageOf(dirname(filePath))
|
|
110
|
+
const path = relative(root, filePath).replaceAll("\\", "/")
|
|
111
|
+
return name === "" ? path : `${name}/${path}`
|
|
92
112
|
}
|
|
93
113
|
|
|
94
114
|
/**
|
|
@@ -251,6 +251,7 @@ function secretLeak(
|
|
|
251
251
|
artifacts,
|
|
252
252
|
env: options.secrets?.env ?? process.env,
|
|
253
253
|
publicEnvPrefix: options.publicEnvPrefix ?? "PUBLIC_",
|
|
254
|
+
// biome-ignore lint/plugin/noConditionalEmptyObjectSpread: Explicitly configured secret-scan exemptions preserve their absence by default.
|
|
254
255
|
...(options.secrets?.exemptions ? { exemptions: options.secrets.exemptions } : {}),
|
|
255
256
|
}),
|
|
256
257
|
)
|
|
@@ -260,7 +261,7 @@ function secretLeak(
|
|
|
260
261
|
export type ServerZoneGuardOptions = Pick<
|
|
261
262
|
LeakGuardOptions,
|
|
262
263
|
"appRoot" | "routesDir" | "generatedFiles" | "publicEnvPrefix"
|
|
263
|
-
>
|
|
264
|
+
> & { readonly componentModules?: ReadonlySet<string> }
|
|
264
265
|
|
|
265
266
|
/**
|
|
266
267
|
* The server build's half of the zone rules, with the same checks and message as nifra's Bun server
|
|
@@ -285,6 +286,9 @@ export function viteServerZoneGuard(options: ServerZoneGuardOptions = {}): LeakG
|
|
|
285
286
|
classifier,
|
|
286
287
|
sourceOf: rollupModuleSource,
|
|
287
288
|
privateEnv: privateEnvCheck(options.publicEnvPrefix ?? "PUBLIC_"),
|
|
289
|
+
...(options.componentModules === undefined
|
|
290
|
+
? {}
|
|
291
|
+
: { componentModules: options.componentModules }),
|
|
288
292
|
}),
|
|
289
293
|
)
|
|
290
294
|
if (leak === undefined) return
|
package/src/public-dir.ts
CHANGED
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
* Distinct from `publicPath` in `build.ts`, which is the URL prefix for content-hashed bundle chunks.
|
|
13
13
|
* The names collide and the concepts do not: `publicPath` never covers user-authored files.
|
|
14
14
|
*/
|
|
15
|
-
import {
|
|
16
|
-
import { open, realpath } from "node:fs/promises"
|
|
15
|
+
import { realpath } from "node:fs/promises"
|
|
17
16
|
import { extname, normalize, resolve, sep } from "node:path"
|
|
18
17
|
import { Readable } from "node:stream"
|
|
19
18
|
import { parseByteRange } from "@nifrajs/core/range"
|
|
20
19
|
import { pathnameOf } from "@nifrajs/core/server"
|
|
20
|
+
import { openResolved } from "./open-resolved.ts"
|
|
21
21
|
|
|
22
22
|
/** How long each subtree may be cached. Content-hashed bundle output can be immutable; a
|
|
23
23
|
* user-authored file keeps its name across deploys, so it gets a day and a revalidation. */
|
|
@@ -31,8 +31,15 @@ export interface PublicDirCache {
|
|
|
31
31
|
export interface ServePublicDirOptions {
|
|
32
32
|
/** Absolute path of the directory to serve. */
|
|
33
33
|
readonly dir: string
|
|
34
|
-
/** URL prefix whose files are content-hashed and may be cached immutably (default `"/assets/"`).
|
|
34
|
+
/** URL prefix whose files are content-hashed and may be cached immutably (default `"/assets/"`).
|
|
35
|
+
* Ignored when `hashedFiles` is set. */
|
|
35
36
|
readonly hashedPrefix?: string
|
|
37
|
+
/**
|
|
38
|
+
* Encoded URL paths of the content-hashed files, such as a build manifest's `assets`. When set, only
|
|
39
|
+
* these are cached immutably and every other file gets the `assets` policy, so a user-authored file
|
|
40
|
+
* that happens to sit under the hashed prefix still revalidates.
|
|
41
|
+
*/
|
|
42
|
+
readonly hashedFiles?: ReadonlySet<string>
|
|
36
43
|
readonly cache?: PublicDirCache
|
|
37
44
|
/** Optional encoded URL-path allowlist. When present, route misses avoid a filesystem probe. */
|
|
38
45
|
readonly files?: ReadonlySet<string>
|
|
@@ -166,6 +173,7 @@ export function servePublicDir(
|
|
|
166
173
|
const root = resolve(options.dir)
|
|
167
174
|
const rootReal = realpath(root).catch(() => undefined)
|
|
168
175
|
const hashedPrefix = options.hashedPrefix ?? "/assets/"
|
|
176
|
+
const hashedFiles = options.hashedFiles
|
|
169
177
|
const hashed = options.cache?.hashed ?? IMMUTABLE
|
|
170
178
|
const assets = options.cache?.assets ?? ONE_DAY
|
|
171
179
|
|
|
@@ -192,16 +200,27 @@ export function servePublicDir(
|
|
|
192
200
|
}
|
|
193
201
|
// Open the canonical path once and stream from that descriptor. Reopening the pathname after
|
|
194
202
|
// realpath containment checks would let a writable public tree swap a symlink between validation
|
|
195
|
-
// and the actual read.
|
|
196
|
-
//
|
|
197
|
-
let handle: Awaited<ReturnType<typeof
|
|
203
|
+
// and the actual read. openResolved also refuses the name once any component of it became a
|
|
204
|
+
// symlink; the descriptor then pins the bytes for the lifetime of the response.
|
|
205
|
+
let handle: Awaited<ReturnType<typeof openResolved>> | undefined
|
|
198
206
|
try {
|
|
199
|
-
|
|
207
|
+
try {
|
|
208
|
+
handle = await openResolved(resolvedFile)
|
|
209
|
+
} catch {
|
|
210
|
+
// A name refused or gone since realpath falls through, as a failed realpath does.
|
|
211
|
+
return undefined
|
|
212
|
+
}
|
|
200
213
|
const stat = await handle.stat()
|
|
201
214
|
if (!stat.isFile()) return undefined
|
|
202
215
|
const size = stat.size
|
|
203
216
|
const headers = new Headers({
|
|
204
|
-
"cache-control":
|
|
217
|
+
"cache-control": (
|
|
218
|
+
hashedFiles === undefined
|
|
219
|
+
? pathname.startsWith(hashedPrefix)
|
|
220
|
+
: hashedFiles.has(pathname)
|
|
221
|
+
)
|
|
222
|
+
? hashed
|
|
223
|
+
: assets,
|
|
205
224
|
// Advertised unconditionally: a client that never sees `accept-ranges` will not attempt a seek,
|
|
206
225
|
// so a video or audio file under `public/` is scrubbable only once this header is present.
|
|
207
226
|
"accept-ranges": "bytes",
|
package/src/query.ts
CHANGED
|
@@ -111,6 +111,12 @@ export interface DehydratedState {
|
|
|
111
111
|
readonly data: unknown
|
|
112
112
|
readonly updatedAt: number
|
|
113
113
|
}>
|
|
114
|
+
/** The successful infinite queries, pages and page params. */
|
|
115
|
+
readonly infiniteQueries?: ReadonlyArray<{
|
|
116
|
+
readonly key: unknown
|
|
117
|
+
readonly data: InfiniteData<unknown, unknown>
|
|
118
|
+
readonly updatedAt: number
|
|
119
|
+
}>
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
/** The keyed query cache. One per app (a binding registers it like the router). */
|
|
@@ -125,8 +131,9 @@ export interface QueryClient {
|
|
|
125
131
|
fn: (pageParam: P) => Promise<T>,
|
|
126
132
|
options: InfiniteQueryOptions<T, P>,
|
|
127
133
|
) => InfiniteQueryHandle<T, P>
|
|
128
|
-
/** Mark matching cached queries stale and refetch the **mounted** ones
|
|
129
|
-
* keys match by **prefix** (`["todo"]` ⇒ every `["todo", …]`); other keys
|
|
134
|
+
/** Mark matching cached queries, infinite ones included, stale and refetch the **mounted** ones
|
|
135
|
+
* (subscribers > 0). Array keys match by **prefix** (`["todo"]` ⇒ every `["todo", …]`); other keys
|
|
136
|
+
* match exactly. */
|
|
130
137
|
invalidateQueries: (keyOrPrefix: unknown) => void
|
|
131
138
|
/** Read a cached query's data (`undefined` if absent or not yet successful). Synchronous, no fetch. */
|
|
132
139
|
getQueryData: <T>(key: unknown) => T | undefined
|
|
@@ -218,17 +225,24 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
|
|
|
218
225
|
|
|
219
226
|
// Lazily evict entries whose GC timer has elapsed (no subscribers), then enforce the hard cap by
|
|
220
227
|
// dropping the oldest-inserted unsubscribed entries. Called on every `query()` - no background timer.
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
228
|
+
const sweepEntries = (
|
|
229
|
+
entries: Map<string, { readonly subscribers: number; readonly gcAt: number | undefined }>,
|
|
230
|
+
t: number,
|
|
231
|
+
): void => {
|
|
232
|
+
for (const [h, e] of entries) {
|
|
233
|
+
if (e.subscribers === 0 && e.gcAt !== undefined && t >= e.gcAt) entries.delete(h)
|
|
225
234
|
}
|
|
226
|
-
if (
|
|
227
|
-
for (const [h, e] of
|
|
228
|
-
if (
|
|
229
|
-
if (e.subscribers === 0)
|
|
235
|
+
if (entries.size <= max) return
|
|
236
|
+
for (const [h, e] of entries) {
|
|
237
|
+
if (entries.size <= max) break
|
|
238
|
+
if (e.subscribers === 0) entries.delete(h) // oldest-inserted unsubscribed first (Map order)
|
|
230
239
|
}
|
|
231
240
|
}
|
|
241
|
+
const sweep = (): void => {
|
|
242
|
+
const t = now()
|
|
243
|
+
sweepEntries(cache, t)
|
|
244
|
+
sweepEntries(infinite, t)
|
|
245
|
+
}
|
|
232
246
|
|
|
233
247
|
// Build a cache entry + its handle. `fn`/`staleTime` are mutated in place on re-bind (closures change
|
|
234
248
|
// between renders). All the fetch/dedup/generation machinery lives here so query(), prefetchQuery(),
|
|
@@ -348,6 +362,9 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
|
|
|
348
362
|
entry.generation++
|
|
349
363
|
if (entry.subscribers > 0) void entry.handle.refetch().catch(() => {}) // refresh mounted now (best-effort)
|
|
350
364
|
}
|
|
365
|
+
for (const entry of infinite.values()) {
|
|
366
|
+
if (keyMatchesPrefix(entry.key, keyOrPrefix)) entry.invalidate()
|
|
367
|
+
}
|
|
351
368
|
}
|
|
352
369
|
|
|
353
370
|
const getQueryData = <T>(key: unknown): T | undefined => {
|
|
@@ -392,7 +409,17 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
|
|
|
392
409
|
if (entry.state.status === "success")
|
|
393
410
|
queries.push({ key: entry.key, data: entry.state.data, updatedAt: entry.state.updatedAt })
|
|
394
411
|
}
|
|
395
|
-
|
|
412
|
+
const infiniteQueries: Array<{
|
|
413
|
+
key: unknown
|
|
414
|
+
data: InfiniteData<unknown, unknown>
|
|
415
|
+
updatedAt: number
|
|
416
|
+
}> = []
|
|
417
|
+
for (const entry of infinite.values()) {
|
|
418
|
+
const { status, data, updatedAt } = entry.handle.snapshot()
|
|
419
|
+
if (status === "success" && data !== undefined)
|
|
420
|
+
infiniteQueries.push({ key: entry.key, data, updatedAt })
|
|
421
|
+
}
|
|
422
|
+
return { queries, infiniteQueries }
|
|
396
423
|
}
|
|
397
424
|
|
|
398
425
|
const hydrate = (state: DehydratedState): void => {
|
|
@@ -409,6 +436,14 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
|
|
|
409
436
|
if (entry.state.status !== "success" || q.updatedAt > entry.state.updatedAt)
|
|
410
437
|
entry.setState(successState(q.data, q.updatedAt))
|
|
411
438
|
}
|
|
439
|
+
// An infinite entry needs its page options before it can exist, so a snapshot for a key not yet
|
|
440
|
+
// queried waits here until `infiniteQuery` creates it.
|
|
441
|
+
for (const q of state.infiniteQueries ?? []) {
|
|
442
|
+
const h = hashQueryKey(q.key)
|
|
443
|
+
const existing = infinite.get(h)
|
|
444
|
+
if (existing === undefined) seeded.set(h, q)
|
|
445
|
+
else existing.seed(q.data, q.updatedAt)
|
|
446
|
+
}
|
|
412
447
|
}
|
|
413
448
|
|
|
414
449
|
// --- Infinite (paged) queries -----------------------------------------------------------------
|
|
@@ -427,19 +462,33 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
|
|
|
427
462
|
)
|
|
428
463
|
return existing.handle as InfiniteQueryHandle<T, P>
|
|
429
464
|
}
|
|
430
|
-
const created = makeInfinite<T, P>(fn, opts)
|
|
465
|
+
const created = makeInfinite<T, P>(key, fn, opts)
|
|
431
466
|
infinite.set(h, created as InfiniteEntry<unknown, unknown>)
|
|
467
|
+
const snapshot = seeded.get(h)
|
|
468
|
+
if (snapshot !== undefined) {
|
|
469
|
+
seeded.delete(h)
|
|
470
|
+
created.seed(snapshot.data, snapshot.updatedAt)
|
|
471
|
+
}
|
|
432
472
|
return created.handle
|
|
433
473
|
}
|
|
434
474
|
|
|
435
475
|
// Infinite entries live in their own map (their data shape + fetch model differ from plain queries).
|
|
436
476
|
interface InfiniteEntry<T, P> {
|
|
477
|
+
readonly key: unknown
|
|
437
478
|
handle: InfiniteQueryHandle<T, P>
|
|
438
479
|
rebind: (fn: (p: P) => Promise<T>, opts: InfiniteQueryOptions<T, P>) => void
|
|
480
|
+
/** Mark stale, drop an in-flight fetch's result, and refetch when mounted. */
|
|
481
|
+
invalidate: () => void
|
|
482
|
+
/** Take a dehydrated snapshot unless the entry already holds fresher data. */
|
|
483
|
+
seed: (data: InfiniteData<unknown, unknown>, updatedAt: number) => void
|
|
484
|
+
subscribers: number
|
|
485
|
+
gcAt: number | undefined
|
|
439
486
|
}
|
|
440
487
|
const infinite = new Map<string, InfiniteEntry<unknown, unknown>>()
|
|
488
|
+
const seeded = new Map<string, NonNullable<DehydratedState["infiniteQueries"]>[number]>()
|
|
441
489
|
|
|
442
490
|
const makeInfinite = <T, P>(
|
|
491
|
+
key: unknown,
|
|
443
492
|
fetchPage: (pageParam: P) => Promise<T>,
|
|
444
493
|
initialOpts: InfiniteQueryOptions<T, P>,
|
|
445
494
|
): InfiniteEntry<T, P> => {
|
|
@@ -447,6 +496,7 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
|
|
|
447
496
|
let opts = initialOpts
|
|
448
497
|
let state: QueryState<InfiniteData<T, P>> = PENDING as QueryState<InfiniteData<T, P>>
|
|
449
498
|
let generation = 0
|
|
499
|
+
let invalidated = false
|
|
450
500
|
let promise: Promise<InfiniteData<T, P>> | undefined
|
|
451
501
|
const listeners = new Set<() => void>()
|
|
452
502
|
const emit = (): void => {
|
|
@@ -475,7 +525,8 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
|
|
|
475
525
|
data.pageParams,
|
|
476
526
|
)
|
|
477
527
|
}
|
|
478
|
-
const isStale = (): boolean =>
|
|
528
|
+
const isStale = (): boolean =>
|
|
529
|
+
invalidated || now() - state.updatedAt >= (opts.staleTime ?? defaultStaleTime)
|
|
479
530
|
|
|
480
531
|
// Run a paged operation under a fresh generation, guarding against a superseding op (like `run`).
|
|
481
532
|
const runPaged = (build: () => Promise<InfiniteData<T, P>>): Promise<InfiniteData<T, P>> => {
|
|
@@ -486,6 +537,7 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
|
|
|
486
537
|
try {
|
|
487
538
|
const data = await build()
|
|
488
539
|
if (gen !== generation) return data
|
|
540
|
+
invalidated = false
|
|
489
541
|
setState(successState(data, now()))
|
|
490
542
|
return data
|
|
491
543
|
} catch (error) {
|
|
@@ -511,9 +563,13 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
|
|
|
511
563
|
const handle: InfiniteQueryHandle<T, P> = {
|
|
512
564
|
snapshot: () => state,
|
|
513
565
|
subscribe: (listener) => {
|
|
566
|
+
entry.subscribers++
|
|
567
|
+
entry.gcAt = undefined
|
|
514
568
|
listeners.add(listener)
|
|
515
569
|
return () => {
|
|
516
570
|
listeners.delete(listener)
|
|
571
|
+
entry.subscribers--
|
|
572
|
+
if (entry.subscribers === 0) entry.gcAt = now() + gcTime
|
|
517
573
|
}
|
|
518
574
|
},
|
|
519
575
|
fetch: () => fetchFirst(false),
|
|
@@ -558,13 +614,30 @@ export function createQueryClient(options: QueryClientOptions): QueryClient {
|
|
|
558
614
|
return p !== undefined && p !== null
|
|
559
615
|
},
|
|
560
616
|
}
|
|
561
|
-
|
|
562
|
-
|
|
617
|
+
const entry: InfiniteEntry<T, P> = {
|
|
618
|
+
key,
|
|
619
|
+
handle,
|
|
563
620
|
rebind: (nextFn, nextOpts) => {
|
|
564
621
|
fn = nextFn
|
|
565
622
|
opts = nextOpts
|
|
566
623
|
},
|
|
567
|
-
|
|
624
|
+
invalidate: () => {
|
|
625
|
+
invalidated = true
|
|
626
|
+
// A fetch begun before this point read pre-mutation data: it may not publish, and a new fetch
|
|
627
|
+
// must not join it.
|
|
628
|
+
generation++
|
|
629
|
+
promise = undefined
|
|
630
|
+
if (entry.subscribers > 0) void handle.refetch().catch(() => {})
|
|
631
|
+
},
|
|
632
|
+
seed: (data, updatedAt) => {
|
|
633
|
+
if (state.status === "success" && state.updatedAt >= updatedAt) return
|
|
634
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: the snapshot was dehydrated from an entry under this key, whose pages and params are T and P
|
|
635
|
+
setState(successState(data as InfiniteData<T, P>, updatedAt))
|
|
636
|
+
},
|
|
637
|
+
subscribers: 0,
|
|
638
|
+
gcAt: undefined,
|
|
639
|
+
}
|
|
640
|
+
return entry
|
|
568
641
|
}
|
|
569
642
|
|
|
570
643
|
return {
|
|
@@ -609,8 +682,10 @@ export interface MutationCallbacks<TData, TVariables> {
|
|
|
609
682
|
export interface MutationHandle<TData, TVariables> {
|
|
610
683
|
snapshot: () => MutationState<TData, TVariables>
|
|
611
684
|
subscribe: (listener: () => void) => () => void
|
|
612
|
-
/** Run the mutation. Resolves to the data (and runs onSuccess/onSettled) or rejects (onError/onSettled)
|
|
613
|
-
*
|
|
685
|
+
/** Run the mutation. Resolves to the data (and runs onSuccess/onSettled) or rejects (onError/onSettled);
|
|
686
|
+
* a throwing `onMutate` fails it the same way. A throwing `onSuccess` or `onSettled` after the write
|
|
687
|
+
* rejects the call but leaves the state `success` and never calls `onError`. The latest concurrent
|
|
688
|
+
* call wins the published state (older results are dropped). */
|
|
614
689
|
mutate: (variables: TVariables) => Promise<TData>
|
|
615
690
|
/** Rebind the latest `fn`/callbacks (closures change between renders) without losing state. */
|
|
616
691
|
rebind: (
|
|
@@ -667,21 +742,27 @@ export function createMutation<TData, TVariables>(
|
|
|
667
742
|
mutate: async (variables) => {
|
|
668
743
|
const mine = ++latest
|
|
669
744
|
setState({ status: "pending", data: undefined, error: undefined, variables })
|
|
670
|
-
|
|
671
|
-
// needless microtask before the mutationFn runs (and complicates single-flight timing).
|
|
672
|
-
if (cbs.onMutate !== undefined) await cbs.onMutate(variables)
|
|
745
|
+
let data: TData
|
|
673
746
|
try {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
if (cbs.
|
|
677
|
-
|
|
678
|
-
return data
|
|
747
|
+
// Only await a callback when one is actually supplied - an `await undefined` would insert a
|
|
748
|
+
// needless microtask before the mutationFn runs (and complicates single-flight timing).
|
|
749
|
+
if (cbs.onMutate !== undefined) await cbs.onMutate(variables)
|
|
750
|
+
data = await mutationFn(variables)
|
|
679
751
|
} catch (error) {
|
|
680
752
|
if (mine === latest) setState({ status: "error", data: undefined, error, variables })
|
|
681
753
|
if (cbs.onError !== undefined) await cbs.onError(error, variables)
|
|
682
754
|
if (cbs.onSettled !== undefined) await cbs.onSettled(undefined, error, variables)
|
|
683
755
|
throw error
|
|
684
756
|
}
|
|
757
|
+
if (mine === latest) setState({ status: "success", data, error: undefined, variables })
|
|
758
|
+
// The write happened: a success callback that throws rejects the call, but is no failure of the
|
|
759
|
+
// write, so it never reaches onError (which would roll back what the server kept).
|
|
760
|
+
try {
|
|
761
|
+
if (cbs.onSuccess !== undefined) await cbs.onSuccess(data, variables)
|
|
762
|
+
} finally {
|
|
763
|
+
if (cbs.onSettled !== undefined) await cbs.onSettled(data, undefined, variables)
|
|
764
|
+
}
|
|
765
|
+
return data
|
|
685
766
|
},
|
|
686
767
|
}
|
|
687
768
|
}
|