@nifrajs/web 3.5.0 → 4.0.2
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/dist/backend-only.d.ts +15 -0
- package/dist/backend-only.d.ts.map +1 -0
- package/dist/backend-only.js +16 -0
- package/dist/backend-only.js.map +1 -0
- package/dist/boundary.d.ts +5 -0
- package/dist/boundary.d.ts.map +1 -1
- package/dist/boundary.js +11 -2
- package/dist/boundary.js.map +1 -1
- package/dist/build-plan.d.ts +18 -6
- package/dist/build-plan.d.ts.map +1 -1
- package/dist/build-plan.js +82 -34
- package/dist/build-plan.js.map +1 -1
- package/dist/build-vite.d.ts +4 -0
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +68 -13
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +57 -23
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +356 -85
- 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/cdn.d.ts +74 -0
- package/dist/cdn.d.ts.map +1 -0
- package/dist/cdn.js +132 -0
- package/dist/cdn.js.map +1 -0
- package/dist/client.d.ts +3 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +136 -35
- package/dist/client.js.map +1 -1
- package/dist/conformance.d.ts +4 -3
- package/dist/conformance.d.ts.map +1 -1
- package/dist/conformance.js +12 -3
- package/dist/conformance.js.map +1 -1
- package/dist/csp.d.ts +93 -0
- package/dist/csp.d.ts.map +1 -0
- package/dist/csp.js +122 -0
- package/dist/csp.js.map +1 -0
- package/dist/dev-client.d.ts +76 -0
- package/dist/dev-client.d.ts.map +1 -0
- package/dist/dev-client.js +280 -0
- package/dist/dev-client.js.map +1 -0
- package/dist/dev-diagnostics.d.ts +26 -2
- package/dist/dev-diagnostics.d.ts.map +1 -1
- package/dist/dev-diagnostics.js +38 -7
- package/dist/dev-diagnostics.js.map +1 -1
- package/dist/dev-error.d.ts +4 -13
- package/dist/dev-error.d.ts.map +1 -1
- package/dist/dev-error.js +46 -4
- package/dist/dev-error.js.map +1 -1
- package/dist/dev-feed.d.ts +298 -0
- package/dist/dev-feed.d.ts.map +1 -0
- package/dist/dev-feed.js +797 -0
- package/dist/dev-feed.js.map +1 -0
- package/dist/dev-indicator.d.ts +38 -0
- package/dist/dev-indicator.d.ts.map +1 -0
- package/dist/dev-indicator.js +93 -0
- package/dist/dev-indicator.js.map +1 -0
- package/dist/dev-session.d.ts +77 -0
- package/dist/dev-session.d.ts.map +1 -0
- package/dist/dev-session.js +580 -0
- package/dist/dev-session.js.map +1 -0
- package/dist/dev-sourcemap.d.ts +27 -0
- package/dist/dev-sourcemap.d.ts.map +1 -0
- package/dist/dev-sourcemap.js +235 -0
- package/dist/dev-sourcemap.js.map +1 -0
- package/dist/dev-ssr-graph.d.ts.map +1 -1
- package/dist/dev-ssr-graph.js +13 -5
- package/dist/dev-ssr-graph.js.map +1 -1
- package/dist/dev.d.ts +15 -2
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +69 -44
- package/dist/dev.js.map +1 -1
- package/dist/diagnostic-catalog.d.ts +35 -0
- package/dist/diagnostic-catalog.d.ts.map +1 -0
- package/dist/diagnostic-catalog.js +203 -0
- package/dist/diagnostic-catalog.js.map +1 -0
- package/dist/diagnostic-prompt.d.ts +49 -0
- package/dist/diagnostic-prompt.d.ts.map +1 -0
- package/dist/diagnostic-prompt.js +303 -0
- package/dist/diagnostic-prompt.js.map +1 -0
- package/dist/diagnostic.d.ts +17 -23
- package/dist/diagnostic.d.ts.map +1 -1
- package/dist/diagnostic.js +31 -39
- package/dist/diagnostic.js.map +1 -1
- package/dist/draft.d.ts.map +1 -1
- package/dist/draft.js +4 -1
- package/dist/draft.js.map +1 -1
- package/dist/fn.d.ts +5 -0
- package/dist/fn.d.ts.map +1 -1
- package/dist/fn.js +13 -3
- package/dist/fn.js.map +1 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +4 -2
- package/dist/fs.js.map +1 -1
- package/dist/index.d.ts +13 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -12
- package/dist/index.js.map +1 -1
- package/dist/internal/cdn/cloudflare.d.ts +48 -0
- package/dist/internal/cdn/cloudflare.d.ts.map +1 -0
- package/dist/internal/cdn/cloudflare.js +94 -0
- package/dist/internal/cdn/cloudflare.js.map +1 -0
- package/dist/internal/cdn/core.d.ts +91 -0
- package/dist/internal/cdn/core.d.ts.map +1 -0
- package/dist/internal/cdn/core.js +220 -0
- package/dist/internal/cdn/core.js.map +1 -0
- package/dist/internal/cdn/fastly.d.ts +17 -0
- package/dist/internal/cdn/fastly.d.ts.map +1 -0
- package/dist/internal/cdn/fastly.js +43 -0
- package/dist/internal/cdn/fastly.js.map +1 -0
- package/dist/internal/cdn/vercel.d.ts +31 -0
- package/dist/internal/cdn/vercel.d.ts.map +1 -0
- package/dist/internal/cdn/vercel.js +56 -0
- package/dist/internal/cdn/vercel.js.map +1 -0
- package/dist/internal/code-unit-order.d.ts +4 -0
- package/dist/internal/code-unit-order.d.ts.map +1 -0
- package/dist/internal/code-unit-order.js +6 -0
- package/dist/internal/code-unit-order.js.map +1 -0
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +141 -25
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/dev-csp.d.ts +24 -0
- package/dist/internal/dev-csp.d.ts.map +1 -0
- package/dist/internal/dev-csp.js +160 -0
- package/dist/internal/dev-csp.js.map +1 -0
- package/dist/internal/dev-reserved.d.ts +25 -0
- package/dist/internal/dev-reserved.d.ts.map +1 -0
- package/dist/internal/dev-reserved.js +39 -0
- package/dist/internal/dev-reserved.js.map +1 -0
- package/dist/internal/html-spans.d.ts +10 -0
- package/dist/internal/html-spans.d.ts.map +1 -0
- package/dist/internal/html-spans.js +46 -0
- package/dist/internal/html-spans.js.map +1 -0
- package/dist/internal/loading-runtime.d.ts +44 -0
- package/dist/internal/loading-runtime.d.ts.map +1 -0
- package/dist/internal/loading-runtime.js +206 -0
- package/dist/internal/loading-runtime.js.map +1 -0
- package/dist/internal/matches-runtime.d.ts +8 -0
- package/dist/internal/matches-runtime.d.ts.map +1 -0
- package/dist/internal/matches-runtime.js +61 -0
- package/dist/internal/matches-runtime.js.map +1 -0
- package/dist/internal/mount-shadow.d.ts +36 -0
- package/dist/internal/mount-shadow.d.ts.map +1 -0
- package/dist/internal/mount-shadow.js +65 -0
- package/dist/internal/mount-shadow.js.map +1 -0
- package/dist/internal/node-builtins.d.ts +3 -0
- package/dist/internal/node-builtins.d.ts.map +1 -0
- package/dist/internal/node-builtins.js +79 -0
- package/dist/internal/node-builtins.js.map +1 -0
- package/dist/internal/output-guard.d.ts +65 -0
- package/dist/internal/output-guard.d.ts.map +1 -0
- package/dist/internal/output-guard.js +630 -0
- package/dist/internal/output-guard.js.map +1 -0
- package/dist/internal/page-execution.d.ts +20 -8
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +571 -105
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/parity.d.ts +21 -0
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +104 -22
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/private-env.d.ts +12 -0
- package/dist/internal/private-env.d.ts.map +1 -0
- package/dist/internal/private-env.js +335 -0
- package/dist/internal/private-env.js.map +1 -0
- package/dist/internal/public-url.d.ts +3 -0
- package/dist/internal/public-url.d.ts.map +1 -0
- package/dist/internal/public-url.js +18 -0
- package/dist/internal/public-url.js.map +1 -0
- package/dist/internal/render-document.d.ts +25 -19
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +77 -61
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/response-controls.d.ts +75 -0
- package/dist/internal/response-controls.d.ts.map +1 -0
- package/dist/internal/response-controls.js +256 -0
- package/dist/internal/response-controls.js.map +1 -0
- package/dist/internal/runtime-contract.d.ts +11 -16
- package/dist/internal/runtime-contract.d.ts.map +1 -1
- package/dist/internal/runtime-contract.js.map +1 -1
- package/dist/internal/secret-scan.d.ts +104 -0
- package/dist/internal/secret-scan.d.ts.map +1 -0
- package/dist/internal/secret-scan.js +503 -0
- package/dist/internal/secret-scan.js.map +1 -0
- package/dist/internal/server-boundary.d.ts +6 -29
- package/dist/internal/server-boundary.d.ts.map +1 -1
- package/dist/internal/server-boundary.js +6 -66
- package/dist/internal/server-boundary.js.map +1 -1
- package/dist/internal/source-map.d.ts +47 -0
- package/dist/internal/source-map.d.ts.map +1 -0
- package/dist/internal/source-map.js +225 -0
- package/dist/internal/source-map.js.map +1 -0
- package/dist/internal/target-compat.d.ts +27 -0
- package/dist/internal/target-compat.d.ts.map +1 -0
- package/dist/internal/target-compat.js +77 -0
- package/dist/internal/target-compat.js.map +1 -0
- package/dist/internal/timing-safe-equal.d.ts +6 -0
- package/dist/internal/timing-safe-equal.d.ts.map +1 -1
- package/dist/internal/timing-safe-equal.js +10 -0
- package/dist/internal/timing-safe-equal.js.map +1 -1
- package/dist/internal/vitals-runtime.d.ts +18 -0
- package/dist/internal/vitals-runtime.d.ts.map +1 -0
- package/dist/internal/vitals-runtime.js +38 -0
- package/dist/internal/vitals-runtime.js.map +1 -0
- package/dist/internal/web-app.d.ts +38 -9
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +199 -59
- package/dist/internal/web-app.js.map +1 -1
- package/dist/internal/zone-graph.d.ts +89 -0
- package/dist/internal/zone-graph.d.ts.map +1 -0
- package/dist/internal/zone-graph.js +492 -0
- package/dist/internal/zone-graph.js.map +1 -0
- package/dist/isr.d.ts +83 -6
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +263 -36
- package/dist/isr.js.map +1 -1
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +31 -6
- package/dist/llms-txt.js.map +1 -1
- package/dist/manifest.d.ts +220 -13
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +394 -9
- package/dist/manifest.js.map +1 -1
- package/dist/module-graph.d.ts +26 -5
- package/dist/module-graph.d.ts.map +1 -1
- package/dist/module-graph.js +17 -7
- package/dist/module-graph.js.map +1 -1
- package/dist/navigation.d.ts +17 -3
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +11 -5
- package/dist/navigation.js.map +1 -1
- package/dist/nonce.d.ts +47 -0
- package/dist/nonce.d.ts.map +1 -0
- package/dist/nonce.js +63 -0
- package/dist/nonce.js.map +1 -0
- package/dist/plugins/css-modules.d.ts.map +1 -1
- package/dist/plugins/css-modules.js +4 -3
- package/dist/plugins/css-modules.js.map +1 -1
- package/dist/plugins/kit.d.ts +9 -0
- package/dist/plugins/kit.d.ts.map +1 -1
- package/dist/plugins/kit.js +18 -0
- package/dist/plugins/kit.js.map +1 -1
- package/dist/plugins/svg.d.ts +22 -3
- package/dist/plugins/svg.d.ts.map +1 -1
- package/dist/plugins/svg.js +112 -1
- package/dist/plugins/svg.js.map +1 -1
- package/dist/plugins/vite-leak-guard.d.ts +107 -3
- package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
- package/dist/plugins/vite-leak-guard.js +378 -17
- package/dist/plugins/vite-leak-guard.js.map +1 -1
- package/dist/plugins/vite-zone-guard.d.ts +61 -0
- package/dist/plugins/vite-zone-guard.d.ts.map +1 -0
- package/dist/plugins/vite-zone-guard.js +208 -0
- package/dist/plugins/vite-zone-guard.js.map +1 -0
- package/dist/plugins/zone-guard.d.ts +27 -0
- package/dist/plugins/zone-guard.d.ts.map +1 -0
- package/dist/plugins/zone-guard.js +80 -0
- package/dist/plugins/zone-guard.js.map +1 -0
- package/dist/prerender.d.ts +10 -0
- package/dist/prerender.d.ts.map +1 -1
- package/dist/prerender.js +35 -3
- package/dist/prerender.js.map +1 -1
- package/dist/public-dir.d.ts +9 -0
- package/dist/public-dir.d.ts.map +1 -1
- package/dist/public-dir.js +67 -9
- package/dist/public-dir.js.map +1 -1
- package/dist/pwa-manifest.d.ts +68 -0
- package/dist/pwa-manifest.d.ts.map +1 -0
- package/dist/pwa-manifest.js +178 -0
- package/dist/pwa-manifest.js.map +1 -0
- package/dist/query.js +4 -4
- package/dist/query.js.map +1 -1
- package/dist/render-seam.d.ts +42 -2
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +6 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/route-manifest.d.ts +3 -0
- package/dist/route-manifest.d.ts.map +1 -1
- package/dist/route-manifest.js +26 -3
- package/dist/route-manifest.js.map +1 -1
- package/dist/route-types.d.ts +24 -0
- package/dist/route-types.d.ts.map +1 -0
- package/dist/route-types.js +195 -0
- package/dist/route-types.js.map +1 -0
- package/dist/router.d.ts +28 -6
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +129 -40
- package/dist/router.js.map +1 -1
- package/dist/vitals.d.ts +33 -0
- package/dist/vitals.d.ts.map +1 -0
- package/dist/vitals.js +16 -0
- package/dist/vitals.js.map +1 -0
- package/dist/vite.d.ts +16 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +248 -43
- package/dist/vite.js.map +1 -1
- package/dist/zones.d.ts +70 -0
- package/dist/zones.d.ts.map +1 -0
- package/dist/zones.js +365 -0
- package/dist/zones.js.map +1 -0
- package/package.json +67 -14
- package/src/backend-only.ts +16 -0
- package/src/boundary.ts +18 -5
- package/src/build-plan.ts +90 -35
- package/src/build-vite.ts +77 -11
- package/src/build.ts +466 -96
- package/src/bun-dev-entry.ts +1 -1
- package/src/cdn.ts +216 -0
- package/src/client.ts +135 -36
- package/src/conformance.ts +16 -3
- package/src/csp.ts +194 -0
- package/src/dev-client.ts +349 -0
- package/src/dev-diagnostics.ts +60 -7
- package/src/dev-error.ts +49 -5
- package/src/dev-feed.ts +1140 -0
- package/src/dev-indicator.ts +113 -0
- package/src/dev-session.ts +729 -0
- package/src/dev-sourcemap.ts +281 -0
- package/src/dev-ssr-graph.ts +16 -5
- package/src/dev.ts +86 -43
- package/src/diagnostic-catalog.ts +243 -0
- package/src/diagnostic-prompt.ts +355 -0
- package/src/diagnostic.ts +63 -66
- package/src/draft.ts +4 -1
- package/src/fn.ts +19 -3
- package/src/fs.ts +13 -2
- package/src/index.ts +42 -10
- package/src/internal/cdn/cloudflare.ts +146 -0
- package/src/internal/cdn/core.ts +316 -0
- package/src/internal/cdn/fastly.ts +62 -0
- package/src/internal/cdn/vercel.ts +95 -0
- package/src/internal/code-unit-order.ts +5 -0
- package/src/internal/codegen.ts +161 -24
- package/src/internal/dev-csp.ts +176 -0
- package/src/internal/dev-reserved.ts +52 -0
- package/src/internal/html-spans.ts +50 -0
- package/src/internal/loading-runtime.ts +244 -0
- package/src/internal/matches-runtime.ts +62 -0
- package/src/internal/mount-shadow.ts +88 -0
- package/src/internal/node-builtins.ts +79 -0
- package/src/internal/output-guard.ts +701 -0
- package/src/internal/page-execution.ts +795 -185
- package/src/internal/parity.ts +150 -36
- package/src/internal/private-env.ts +334 -0
- package/src/internal/public-url.ts +22 -0
- package/src/internal/render-document.ts +114 -67
- package/src/internal/response-controls.ts +284 -0
- package/src/internal/runtime-contract.ts +10 -15
- package/src/internal/secret-scan.ts +638 -0
- package/src/internal/server-boundary.ts +6 -69
- package/src/internal/source-map.ts +256 -0
- package/src/internal/target-compat.ts +92 -0
- package/src/internal/timing-safe-equal.ts +13 -0
- package/src/internal/vitals-runtime.ts +53 -0
- package/src/internal/web-app.ts +262 -84
- package/src/internal/zone-graph.ts +593 -0
- package/src/isr.ts +325 -37
- package/src/llms-txt.ts +28 -4
- package/src/manifest.ts +682 -16
- package/src/module-graph.ts +53 -9
- package/src/navigation.ts +29 -6
- package/src/nonce.ts +104 -0
- package/src/plugins/css-modules.ts +5 -3
- package/src/plugins/kit.ts +24 -0
- package/src/plugins/svg.ts +135 -4
- package/src/plugins/vite-leak-guard.ts +529 -16
- package/src/plugins/vite-zone-guard.ts +263 -0
- package/src/plugins/zone-guard.ts +119 -0
- package/src/prerender.ts +49 -3
- package/src/public-dir.ts +69 -8
- package/src/pwa-manifest.ts +226 -0
- package/src/query.ts +4 -4
- package/src/render-seam.ts +46 -1
- package/src/route-manifest.ts +15 -3
- package/src/route-types.ts +264 -0
- package/src/router.ts +163 -63
- package/src/vitals.ts +44 -0
- package/src/vite.ts +257 -42
- package/src/zones.ts +430 -0
- package/dist/plugins/vite-server-only.d.ts +0 -49
- package/dist/plugins/vite-server-only.d.ts.map +0 -1
- package/dist/plugins/vite-server-only.js +0 -57
- package/dist/plugins/vite-server-only.js.map +0 -1
- package/dist/server-only.d.ts +0 -21
- package/dist/server-only.d.ts.map +0 -1
- package/dist/server-only.js +0 -25
- package/dist/server-only.js.map +0 -1
- package/src/plugins/vite-server-only.ts +0 -75
- package/src/server-only.ts +0 -25
package/src/router.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { StandardSchemaV1 } from "@nifrajs/core/server"
|
|
|
10
10
|
import type { BoundaryDescriptor, BoundaryStates } from "./boundary.ts"
|
|
11
11
|
import { parseNdjsonData } from "./deferred.ts"
|
|
12
12
|
import type { ClientActionResult, ClientRequestBody, ClientRouteHooks } from "./manifest.ts"
|
|
13
|
+
import type { MatchChain } from "./render-seam.ts"
|
|
13
14
|
import { isClientOnlySearchChange } from "./search.ts"
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -25,6 +26,10 @@ export const DATA_HEADER = "x-nifra-data"
|
|
|
25
26
|
*/
|
|
26
27
|
export const PRERENDERED_GLOBAL = "__NIFRA_PRERENDERED__"
|
|
27
28
|
|
|
29
|
+
/** Where `createWebApp` serves a prerendered set too large to inline; the global then holds this URL
|
|
30
|
+
* plus a `?v=` content version, so the browser may cache it for good. */
|
|
31
|
+
export const PRERENDERED_LIST_PATH = "/__nifra/prerendered.json"
|
|
32
|
+
|
|
28
33
|
/**
|
|
29
34
|
* Response header a data-mode action POST uses to convey a redirect (`redirect(...)`) to the
|
|
30
35
|
* client - fetch would otherwise silently follow a 3xx to its HTML, losing the target. The
|
|
@@ -138,8 +143,9 @@ export interface RouterState {
|
|
|
138
143
|
readonly actionData?: unknown
|
|
139
144
|
/** True while a navigation or submit is in flight (drives loading UI). */
|
|
140
145
|
readonly pending: boolean
|
|
141
|
-
/** The path a navigation is
|
|
142
|
-
* `NavLink` know whether its own `to` is the one loading;
|
|
146
|
+
/** The path a navigation or a submit's redirect is loading while `pending`; a redirect the router
|
|
147
|
+
* follows moves it to the target. Lets a `NavLink` know whether its own `to` is the one loading;
|
|
148
|
+
* `undefined` when idle. */
|
|
143
149
|
readonly pendingPath?: string | undefined
|
|
144
150
|
/** The in-flight submit (set during a `submit`, cleared when it settles) - for optimistic UI. */
|
|
145
151
|
readonly submission?: Submission
|
|
@@ -197,8 +203,10 @@ export type FetchRouteData = (
|
|
|
197
203
|
|
|
198
204
|
/** Per-submit options. `revalidate: false` opts out of the post-action loader re-fetch. */
|
|
199
205
|
export interface SubmitOptions {
|
|
200
|
-
/** Re-run the active route's
|
|
201
|
-
* keep the current `data` and rely on the action's `actionData` alone.
|
|
206
|
+
/** Re-run the active route's loaders after the action settles (default `true`). Set `false` to
|
|
207
|
+
* keep the current `data` and rely on the action's `actionData` alone. When the action redirects,
|
|
208
|
+
* the target's layouts all re-run by default; with `false` it keeps the layout data an ordinary
|
|
209
|
+
* navigation would. */
|
|
202
210
|
readonly revalidate?: boolean
|
|
203
211
|
}
|
|
204
212
|
|
|
@@ -238,7 +246,11 @@ export interface ClientRouter {
|
|
|
238
246
|
snapshot: () => RouterState
|
|
239
247
|
/** Subscribe to transitions; returns an unsubscribe fn. */
|
|
240
248
|
subscribe: (listener: () => void) => () => void
|
|
241
|
-
/**
|
|
249
|
+
/**
|
|
250
|
+
* Navigate to a path: match → fetch loader data → publish. No-op for an unmatched path. A loader
|
|
251
|
+
* redirect to another route is followed; one that leaves the app (another origin, a `#fragment`, no
|
|
252
|
+
* route) rejects with its target as `redirectTo`.
|
|
253
|
+
*/
|
|
242
254
|
navigate: (path: string) => Promise<void>
|
|
243
255
|
/**
|
|
244
256
|
* Submit an action (POST `body` to `action` in data mode): a redirect becomes a client
|
|
@@ -246,7 +258,8 @@ export interface ClientRouter {
|
|
|
246
258
|
* loader is revalidated so the mutation is reflected. Pass `{ revalidate: false }` to SKIP that
|
|
247
259
|
* revalidation - keep the current `data` and just publish the action's `actionData` (useful when
|
|
248
260
|
* the action already returned everything that changed, saving the extra round-trip). A redirect
|
|
249
|
-
* always loads its target regardless. Rejects on failure (caller falls back)
|
|
261
|
+
* always loads its target regardless. Rejects on failure (caller falls back); once the action has
|
|
262
|
+
* run, the rejection's `redirectTo` names the page to load instead of posting again.
|
|
250
263
|
*/
|
|
251
264
|
submit: (
|
|
252
265
|
action: string,
|
|
@@ -319,6 +332,10 @@ export interface MountRouterOptions {
|
|
|
319
332
|
* `ctx.search`/`RenderProps.search`. Omitted by callers with no typed search (tests, a hand-built mount)
|
|
320
333
|
* ⇒ every route sees the raw parsed query. */
|
|
321
334
|
readonly searchSchemas?: Readonly<Record<string, readonly (StandardSchemaV1 | undefined)[]>>
|
|
335
|
+
/** routeId → the ids and `handle` exports `useMatches` reports for that chain; built by
|
|
336
|
+
* `generateClientEntry` as each route's modules load. The mount hands the current route's entry to
|
|
337
|
+
* the render as `RenderProps.matchChain`. Omitted ⇒ `useMatches` returns an empty list. */
|
|
338
|
+
readonly matchChains?: Readonly<Record<string, MatchChain>>
|
|
322
339
|
/** Hydration container (opaque - the adapter casts it to its DOM element type). */
|
|
323
340
|
readonly container: unknown
|
|
324
341
|
}
|
|
@@ -356,11 +373,45 @@ const prerenderedSetOf = (paths: object): ReadonlySet<unknown> => {
|
|
|
356
373
|
return set
|
|
357
374
|
}
|
|
358
375
|
|
|
376
|
+
// A handed-over URL is fetched once: the global then holds the pending list every navigation shares.
|
|
377
|
+
// A failed fetch leaves navigations on the dynamic path, which is always correct.
|
|
378
|
+
const prerenderedOf = (): unknown => {
|
|
379
|
+
const handed = Reflect.get(globalThis, PRERENDERED_GLOBAL)
|
|
380
|
+
if (typeof handed !== "string") return handed
|
|
381
|
+
const list = fetch(handed)
|
|
382
|
+
.then((res) => (res.ok ? res.json() : undefined))
|
|
383
|
+
.catch(() => undefined)
|
|
384
|
+
Reflect.set(globalThis, PRERENDERED_GLOBAL, list)
|
|
385
|
+
return list
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* The page to load as a document, when that is why a navigation or submit rejected. Only an `http:` or
|
|
390
|
+
* `https:` target: the caller hands it to `location`, which runs a `javascript:` URL in this page,
|
|
391
|
+
* where the same target in a real redirect's `Location` never runs.
|
|
392
|
+
*/
|
|
393
|
+
export const redirectOf = (error: unknown): string | undefined => {
|
|
394
|
+
const to = (error as { redirectTo?: unknown } | null | undefined)?.redirectTo
|
|
395
|
+
return typeof to === "string" && /^https?:$/.test(targetUrl(to)?.protocol ?? "") ? to : undefined
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** A redirect target resolved against a placeholder origin, or `undefined` when it does not parse. */
|
|
399
|
+
const targetUrl = (to: string): URL | undefined => {
|
|
400
|
+
try {
|
|
401
|
+
return new URL(to, "http://n.invalid")
|
|
402
|
+
} catch {
|
|
403
|
+
return undefined
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const redirected = (to: string): Error =>
|
|
408
|
+
Object.assign(new Error(`[nifra/web] redirected to ${to}`), { redirectTo: to })
|
|
409
|
+
|
|
359
410
|
const defaultFetchData: FetchRouteData = async (path, _match, signal, navigation) => {
|
|
360
411
|
// SSG fast path: if this path was prerendered, its loader data is a static file - fetch that (no
|
|
361
412
|
// worker). Falls through to the dynamic header-GET on any miss (file absent, e.g. a deferred route,
|
|
362
413
|
// or a stale set), so it's always safe. Non-SSG apps have no global → the dynamic path, unchanged.
|
|
363
|
-
const prerendered = (
|
|
414
|
+
const prerendered = await prerenderedOf()
|
|
364
415
|
if (Array.isArray(prerendered)) {
|
|
365
416
|
const query = path.indexOf("?")
|
|
366
417
|
const hash = path.indexOf("#")
|
|
@@ -379,6 +430,8 @@ const defaultFetchData: FetchRouteData = async (path, _match, signal, navigation
|
|
|
379
430
|
headers.set(NAV_FROM_HEADER, navigation.from)
|
|
380
431
|
}
|
|
381
432
|
const res = await fetch(path, { headers, signal: signal ?? null })
|
|
433
|
+
const redirectTo = res.headers.get(REDIRECT_HEADER)
|
|
434
|
+
if (redirectTo !== null) throw redirected(redirectTo)
|
|
382
435
|
const status = Number(res.headers.get(STATUS_HEADER))
|
|
383
436
|
if (Number.isInteger(status) && status >= 400 && status <= 599) {
|
|
384
437
|
return { v: 1, data: null, status } satisfies RouteDataEnvelope
|
|
@@ -411,6 +464,8 @@ const isClientActionResult = (value: unknown): value is ClientActionResult =>
|
|
|
411
464
|
export function createClientRouter(options: ClientRouterOptions): ClientRouter {
|
|
412
465
|
const match = createMatcher(options.patterns)
|
|
413
466
|
const fetchData = options.fetchData ?? defaultFetchData
|
|
467
|
+
// Start a handed-over list's fetch now, so the first navigation rarely waits for it.
|
|
468
|
+
if (fetchData === defaultFetchData) prerenderedOf()
|
|
414
469
|
// routeId → the route's client-only search keys. Read by reference (the generated entry keeps writing
|
|
415
470
|
// to it as routes load), so a same-route nav always sees the current route's keys.
|
|
416
471
|
const searchClientKeys = options.searchClientKeys ?? {}
|
|
@@ -426,7 +481,7 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
|
|
|
426
481
|
sourceRouteId: string,
|
|
427
482
|
path: string,
|
|
428
483
|
target: RouteMatch,
|
|
429
|
-
): BoundaryDescriptor | undefined => {
|
|
484
|
+
): Omit<BoundaryDescriptor, "hasLoad"> | undefined => {
|
|
430
485
|
const boundaries = routeHooks[sourceRouteId]?.boundaries ?? []
|
|
431
486
|
const pathname = pathnameOf(path)
|
|
432
487
|
for (const boundary of boundaries) {
|
|
@@ -473,8 +528,12 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
|
|
|
473
528
|
path: string,
|
|
474
529
|
match: { routeId: string; params: Record<string, string> },
|
|
475
530
|
signal?: AbortSignal,
|
|
476
|
-
|
|
531
|
+
// Offer the server the layout data on screen: it re-runs whichever of those loaders it must.
|
|
532
|
+
keepLayouts?: boolean,
|
|
477
533
|
): Promise<LoadedRouteData> => {
|
|
534
|
+
const retainFrom = keepLayouts
|
|
535
|
+
? state.layoutData && { path: state.path, layoutData: state.layoutData }
|
|
536
|
+
: undefined
|
|
478
537
|
const retain = retainFrom?.layoutData.map((_, index) => index) ?? []
|
|
479
538
|
const payload = await fetchData(
|
|
480
539
|
path,
|
|
@@ -621,11 +680,17 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
|
|
|
621
680
|
if (listeners.size === 0) return
|
|
622
681
|
for (const listener of [...listeners]) listener()
|
|
623
682
|
}
|
|
624
|
-
// Bounded one-shot prefetch cache (path → loader data) + an in-flight guard so
|
|
625
|
-
// doesn't double-fetch. Consumed (and dropped) by the next navigate to that path
|
|
683
|
+
// Bounded one-shot prefetch cache (path → loader data, and when it arrived) + an in-flight guard so
|
|
684
|
+
// hover spam doesn't double-fetch. Consumed (and dropped) by the next navigate to that path - while
|
|
685
|
+
// fresh: a link warmed as it scrolled into view can be clicked minutes later.
|
|
626
686
|
const MAX_PREFETCH = 10
|
|
627
|
-
const
|
|
687
|
+
const PREFETCH_MAX_AGE_MS = 30_000
|
|
688
|
+
const prefetched = new Map<string, readonly [LoadedRouteData, number]>()
|
|
628
689
|
const inflight = new Set<string>()
|
|
690
|
+
const freshPrefetch = (path: string): LoadedRouteData | undefined => {
|
|
691
|
+
const held = prefetched.get(path)
|
|
692
|
+
return held !== undefined && Date.now() - held[1] < PREFETCH_MAX_AGE_MS ? held[0] : undefined
|
|
693
|
+
}
|
|
629
694
|
// Keyed data cache (path → latest loader data + freshness). Written on every published data
|
|
630
695
|
// (navigate/submit), read by `invalidate` (+ targeted revalidation and fetchers in later F16
|
|
631
696
|
// increments). Bounded - evict the oldest-inserted past the cap (route data is small; this just
|
|
@@ -684,6 +749,56 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
|
|
|
684
749
|
// (so a superseded route's NDJSON stream stops reading instead of draining in the background).
|
|
685
750
|
let navAbort: AbortController | undefined
|
|
686
751
|
|
|
752
|
+
// The route path an in-app redirect target leads to. Undefined when the browser has to load it: another
|
|
753
|
+
// origin, a `#fragment` to scroll to, or no route.
|
|
754
|
+
const appRedirect = (to: string | undefined): readonly [string, RouteMatch] | undefined => {
|
|
755
|
+
if (to === undefined || !to.startsWith("/") || to.includes("#")) return undefined
|
|
756
|
+
const url = targetUrl(to)
|
|
757
|
+
if (url?.host !== "n.invalid") return undefined // `//host`, `/\host`, unparseable
|
|
758
|
+
const path = url.pathname + url.search
|
|
759
|
+
const target = match(path)
|
|
760
|
+
return target === null ? undefined : [path, target]
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Load `path` for the navigation or submit `mine`, following the redirects its loaders answer with
|
|
764
|
+
// while they stay in the app (20 at most, as a browser allows).
|
|
765
|
+
const loadFollowing = async (
|
|
766
|
+
mine: number,
|
|
767
|
+
path: string,
|
|
768
|
+
target: RouteMatch,
|
|
769
|
+
signal: AbortSignal,
|
|
770
|
+
keepLayouts: boolean,
|
|
771
|
+
usePrefetch: boolean,
|
|
772
|
+
): Promise<readonly [string, RouteMatch, LoadedRouteData]> => {
|
|
773
|
+
let current: readonly [string, RouteMatch] = [path, target]
|
|
774
|
+
for (let hops = 0; ; hops++) {
|
|
775
|
+
const [at, route] = current
|
|
776
|
+
if (mine === generation && state.pendingPath !== at) {
|
|
777
|
+
state = { ...state, pendingPath: at }
|
|
778
|
+
emit()
|
|
779
|
+
}
|
|
780
|
+
await loadModule?.(route.routeId)
|
|
781
|
+
// Prefetched server data is one-shot; a clientLoader still runs over it without another request.
|
|
782
|
+
const prefetchedData = usePrefetch ? freshPrefetch(at) : undefined
|
|
783
|
+
if (usePrefetch) prefetched.delete(at)
|
|
784
|
+
try {
|
|
785
|
+
const loaded = await applyClientLoader(
|
|
786
|
+
at,
|
|
787
|
+
route,
|
|
788
|
+
signal,
|
|
789
|
+
() => loadRouteData(at, route, signal, keepLayouts),
|
|
790
|
+
prefetchedData,
|
|
791
|
+
)
|
|
792
|
+
if (loaded.terminalRouteId !== undefined) await loadModule?.(loaded.terminalRouteId)
|
|
793
|
+
return [at, route, loaded]
|
|
794
|
+
} catch (error) {
|
|
795
|
+
const next = hops < 20 ? appRedirect(redirectOf(error)) : undefined
|
|
796
|
+
if (next === undefined) throw error
|
|
797
|
+
current = next
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
687
802
|
// Refetch the active route + republish, single-flight via the shared `generation`. Shared by `invalidate`
|
|
688
803
|
// and targeted revalidation. Rejects if the fetch fails (clearing its own `pending` first).
|
|
689
804
|
const refetchActive = async (): Promise<void> => {
|
|
@@ -890,35 +1005,12 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
|
|
|
890
1005
|
state = { ...state, pending: true, pendingPath: path }
|
|
891
1006
|
emit()
|
|
892
1007
|
try {
|
|
893
|
-
|
|
894
|
-
// uses the same server-data path; a route with one gets a lazy `serverLoader()` thunk.
|
|
895
|
-
await loadModule?.(matched.routeId)
|
|
896
|
-
// Use prefetched server data when present (one-shot - drop it); clientLoader still runs over
|
|
897
|
-
// that cached result without causing another network request.
|
|
898
|
-
const hit = prefetched.has(path)
|
|
899
|
-
const prefetchedData = hit ? (prefetched.get(path) as LoadedRouteData) : undefined
|
|
900
|
-
if (hit) prefetched.delete(path)
|
|
901
|
-
const loaded = await applyClientLoader(
|
|
902
|
-
path,
|
|
903
|
-
matched,
|
|
904
|
-
ac.signal,
|
|
905
|
-
() =>
|
|
906
|
-
loadRouteData(
|
|
907
|
-
path,
|
|
908
|
-
matched,
|
|
909
|
-
ac.signal,
|
|
910
|
-
state.layoutData === undefined
|
|
911
|
-
? undefined
|
|
912
|
-
: { path: state.path, layoutData: state.layoutData },
|
|
913
|
-
),
|
|
914
|
-
prefetchedData,
|
|
915
|
-
)
|
|
916
|
-
if (loaded.terminalRouteId !== undefined) await loadModule?.(loaded.terminalRouteId)
|
|
1008
|
+
const [to, target, loaded] = await loadFollowing(mine, path, matched, ac.signal, true, true)
|
|
917
1009
|
if (mine !== generation) return // a newer navigation superseded this one - drop the stale result
|
|
918
|
-
cachePut(
|
|
1010
|
+
cachePut(to, loaded) // keep the keyed cache coherent with what we publish
|
|
919
1011
|
const interception =
|
|
920
1012
|
loaded.terminalRouteId === undefined
|
|
921
|
-
? interceptionFor(state.routeId,
|
|
1013
|
+
? interceptionFor(state.routeId, to, target)
|
|
922
1014
|
: undefined
|
|
923
1015
|
if (interception !== undefined) {
|
|
924
1016
|
// The target's ordinary data request has already completed (including its server authz and
|
|
@@ -927,7 +1019,7 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
|
|
|
927
1019
|
// this branch, so an unauthorized/missing target cannot become a modal.
|
|
928
1020
|
state = {
|
|
929
1021
|
...state,
|
|
930
|
-
path,
|
|
1022
|
+
path: to,
|
|
931
1023
|
boundaries: {
|
|
932
1024
|
...(state.boundaries ?? {}),
|
|
933
1025
|
[interception.name]: {
|
|
@@ -946,9 +1038,9 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
|
|
|
946
1038
|
return
|
|
947
1039
|
}
|
|
948
1040
|
state = {
|
|
949
|
-
routeId: loaded.terminalRouteId ??
|
|
950
|
-
params: loaded.terminalRouteId === undefined ?
|
|
951
|
-
path,
|
|
1041
|
+
routeId: loaded.terminalRouteId ?? target.routeId,
|
|
1042
|
+
params: loaded.terminalRouteId === undefined ? target.params : {},
|
|
1043
|
+
path: to,
|
|
952
1044
|
data: loaded.data,
|
|
953
1045
|
layoutData: loaded.layoutData,
|
|
954
1046
|
...(loaded.boundaries !== undefined ? { boundaries: loaded.boundaries } : {}),
|
|
@@ -983,6 +1075,9 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
|
|
|
983
1075
|
...(body instanceof FormData ? { submission: { action, formData: body } } : {}),
|
|
984
1076
|
}
|
|
985
1077
|
emit()
|
|
1078
|
+
// Once the action has run, falling back to posting the form again would run it twice.
|
|
1079
|
+
let posted = false
|
|
1080
|
+
let landing: string | undefined
|
|
986
1081
|
try {
|
|
987
1082
|
const prepared = await prepareClientAction(action, body, ac.signal)
|
|
988
1083
|
if (Object.hasOwn(prepared, "optimisticData")) {
|
|
@@ -995,30 +1090,30 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
|
|
|
995
1090
|
headers: { [DATA_HEADER]: "1" },
|
|
996
1091
|
})
|
|
997
1092
|
if (!res.ok) throw new Error(`[nifra/web] action failed (${res.status}): ${action}`)
|
|
1093
|
+
posted = true
|
|
998
1094
|
const redirectTo = res.headers.get(REDIRECT_HEADER)
|
|
999
1095
|
if (redirectTo !== null) {
|
|
1000
1096
|
// The action redirected (Post/Redirect/Get) - treat it as a client navigation.
|
|
1001
|
-
const
|
|
1002
|
-
if (
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1097
|
+
const next = appRedirect(redirectTo)
|
|
1098
|
+
if (next === undefined) throw redirected(redirectTo)
|
|
1099
|
+
landing = next[0]
|
|
1100
|
+
// The action may have changed what any layout shows, so the target loads its whole chain,
|
|
1101
|
+
// the same as the non-redirect revalidation below. `revalidate: false` keeps layout data
|
|
1102
|
+
// the server agrees is unchanged, like an ordinary navigation.
|
|
1103
|
+
const [to, target, loaded] = await loadFollowing(
|
|
1104
|
+
mine,
|
|
1105
|
+
next[0],
|
|
1106
|
+
next[1],
|
|
1107
|
+
ac.signal,
|
|
1108
|
+
opts?.revalidate === false,
|
|
1109
|
+
false,
|
|
1014
1110
|
)
|
|
1015
|
-
if (loaded.terminalRouteId !== undefined) await loadModule?.(loaded.terminalRouteId)
|
|
1016
1111
|
if (mine !== generation) return
|
|
1017
|
-
cachePut(
|
|
1112
|
+
cachePut(to, loaded)
|
|
1018
1113
|
state = {
|
|
1019
1114
|
routeId: loaded.terminalRouteId ?? target.routeId,
|
|
1020
1115
|
params: loaded.terminalRouteId === undefined ? target.params : {},
|
|
1021
|
-
path:
|
|
1116
|
+
path: to,
|
|
1022
1117
|
data: loaded.data,
|
|
1023
1118
|
layoutData: loaded.layoutData,
|
|
1024
1119
|
...(loaded.boundaries !== undefined ? { boundaries: loaded.boundaries } : {}),
|
|
@@ -1071,19 +1166,23 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
|
|
|
1071
1166
|
await refreshMounted(changed, true)
|
|
1072
1167
|
} catch (err) {
|
|
1073
1168
|
if (mine === generation) {
|
|
1074
|
-
// Revert: clear `submission` (the optimistic view vanishes) leaving
|
|
1075
|
-
// pre-submit
|
|
1169
|
+
// Revert: clear `submission` (the optimistic view vanishes) leaving the data untouched, so the
|
|
1170
|
+
// pre-submit page shows through.
|
|
1076
1171
|
state = {
|
|
1077
1172
|
routeId: state.routeId,
|
|
1078
1173
|
params: state.params,
|
|
1079
1174
|
path: state.path,
|
|
1080
1175
|
data: state.data,
|
|
1176
|
+
layoutData: state.layoutData,
|
|
1177
|
+
...(state.boundaries !== undefined ? { boundaries: state.boundaries } : {}),
|
|
1081
1178
|
actionData: previousActionData,
|
|
1082
1179
|
pending: false,
|
|
1083
1180
|
}
|
|
1084
1181
|
emit()
|
|
1085
1182
|
}
|
|
1086
|
-
throw err
|
|
1183
|
+
if (!posted) throw err
|
|
1184
|
+
if (mine !== generation) return // a newer navigation or submit owns the page
|
|
1185
|
+
throw redirectOf(err) === undefined ? redirected(landing ?? state.path) : err
|
|
1087
1186
|
}
|
|
1088
1187
|
},
|
|
1089
1188
|
hydrate,
|
|
@@ -1101,7 +1200,7 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
|
|
|
1101
1200
|
await Promise.all(jobs)
|
|
1102
1201
|
},
|
|
1103
1202
|
prefetch: async (path) => {
|
|
1104
|
-
if (
|
|
1203
|
+
if (inflight.has(path) || freshPrefetch(path) !== undefined) return
|
|
1105
1204
|
const matched = match(path)
|
|
1106
1205
|
if (matched === null) return
|
|
1107
1206
|
inflight.add(path)
|
|
@@ -1112,11 +1211,12 @@ export function createClientRouter(options: ClientRouterOptions): ClientRouter {
|
|
|
1112
1211
|
loadModule?.(matched.routeId),
|
|
1113
1212
|
loadRouteData(path, matched),
|
|
1114
1213
|
])
|
|
1214
|
+
prefetched.delete(path) // a stale entry re-enters as the newest
|
|
1115
1215
|
if (prefetched.size >= MAX_PREFETCH) {
|
|
1116
1216
|
const oldest = prefetched.keys().next().value
|
|
1117
1217
|
if (oldest !== undefined) prefetched.delete(oldest)
|
|
1118
1218
|
}
|
|
1119
|
-
prefetched.set(path, loaded)
|
|
1219
|
+
prefetched.set(path, [loaded, Date.now()])
|
|
1120
1220
|
} catch {
|
|
1121
1221
|
// Best-effort: a failed prefetch just means the eventual navigate fetches normally.
|
|
1122
1222
|
} finally {
|
package/src/vitals.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Core Web Vitals from Google's `web-vitals` (an optional peer), each tagged with its route. */
|
|
2
|
+
import type { MetricType } from "web-vitals"
|
|
3
|
+
import { onCLS, onFCP, onINP, onLCP, onTTFB } from "web-vitals"
|
|
4
|
+
import { reportWith } from "./internal/vitals-runtime.ts"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One measurement from `web-vitals` - `name` is `"LCP"`, `"INP"`, `"CLS"`, `"FCP"` or `"TTFB"`, with its
|
|
8
|
+
* `value`, `rating` (`"good"`, `"needs-improvement"` or `"poor"`), `delta` since the last report and an
|
|
9
|
+
* `id` unique to the measurement - plus the route it belongs to.
|
|
10
|
+
*/
|
|
11
|
+
export type WebVitalsMetric = MetricType & {
|
|
12
|
+
/** The id of the route the measured page matched (`users/[id]`, the id `useMatches` reports).
|
|
13
|
+
* `undefined` before the router mounts, for a page no route matches, or for another origin. */
|
|
14
|
+
readonly route: string | undefined
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** How {@link reportWebVitals} reports. */
|
|
18
|
+
export interface ReportWebVitalsOptions {
|
|
19
|
+
/** Report every change to a metric (LCP, CLS and INP grow while the page is open), not only the
|
|
20
|
+
* value it settles on. Use `delta` or `id` to aggregate. Default `false`. */
|
|
21
|
+
readonly reportAllChanges?: boolean
|
|
22
|
+
/** Measure each client-side navigation as a page view of its own where the browser can (Chromium
|
|
23
|
+
* 151 and later), so its metrics carry the route it navigated to. The first page's metrics then
|
|
24
|
+
* settle at the first client navigation; other browsers report as if this were off. Default `false`. */
|
|
25
|
+
readonly softNavigations?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const library = { onCLS, onFCP, onINP, onLCP, onTTFB }
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Report the page's Core Web Vitals to `report`, each with the route it belongs to. A metric is
|
|
32
|
+
* reported once its value is final - FCP and TTFB as soon as they are known, LCP at the first
|
|
33
|
+
* interaction or when the page is hidden, CLS and INP when the page is hidden - unless
|
|
34
|
+
* `reportAllChanges` asks for every change. The browser buffers the load metrics recorded before the
|
|
35
|
+
* call, so a mount effect after hydration is early enough.
|
|
36
|
+
*
|
|
37
|
+
* Returns a function that stops reporting. On the server it does nothing.
|
|
38
|
+
*/
|
|
39
|
+
export function reportWebVitals(
|
|
40
|
+
report: (metric: WebVitalsMetric) => void,
|
|
41
|
+
options: ReportWebVitalsOptions = {},
|
|
42
|
+
): () => void {
|
|
43
|
+
return reportWith(library, report, options)
|
|
44
|
+
}
|