@nifrajs/web 3.5.0 → 4.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/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 +111 -15
- 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 +485 -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 +386 -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 +72 -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 +187 -36
- 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 +111 -18
- 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 +623 -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 +670 -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 +110 -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 +207 -36
- 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
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
type BackendPlatformBinder,
|
|
3
|
+
NIFRA_BACKEND_BIND_PLATFORM,
|
|
4
|
+
NIFRA_PLATFORM_CLIENT_IP_DERIVED,
|
|
5
|
+
} from "@nifrajs/core/mount"
|
|
6
|
+
import { type Platform, type ResponseResult, status as statusResult } from "@nifrajs/core/server"
|
|
2
7
|
import {
|
|
3
8
|
type BoundaryRequestCtx,
|
|
4
9
|
type BoundaryStates,
|
|
@@ -7,23 +12,29 @@ import {
|
|
|
7
12
|
type StaticBoundaryCache,
|
|
8
13
|
startDynamicBoundaries,
|
|
9
14
|
} from "../boundary.ts"
|
|
15
|
+
import type { CspPolicy } from "../csp.ts"
|
|
10
16
|
import { type CssLoadingMode, DEFAULT_CSS_LOADING, normalizeCssLoading } from "../css-contract.ts"
|
|
11
17
|
import { defer, ndjsonStream, prepareDeferred } from "../deferred.ts"
|
|
12
18
|
import { isDraftEnabled } from "../draft.ts"
|
|
19
|
+
import { routeTags } from "../isr.ts"
|
|
13
20
|
import type {
|
|
14
21
|
LayoutEntry,
|
|
15
22
|
LoaderContext,
|
|
16
23
|
Manifest,
|
|
17
24
|
Meta,
|
|
18
25
|
MetaArgs,
|
|
26
|
+
NotFoundEntry,
|
|
19
27
|
RouteEntry,
|
|
20
28
|
RouteModule,
|
|
29
|
+
ShouldRevalidate,
|
|
21
30
|
} from "../manifest.ts"
|
|
22
|
-
import type {
|
|
31
|
+
import type { NonceResolver } from "../nonce.ts"
|
|
32
|
+
import type { MatchChain, RenderAdapter } from "../render-seam.ts"
|
|
23
33
|
import {
|
|
24
34
|
createMatcher,
|
|
25
35
|
DATA_HEADER,
|
|
26
36
|
NAV_FROM_HEADER,
|
|
37
|
+
REDIRECT_HEADER,
|
|
27
38
|
RETAIN_HEADER,
|
|
28
39
|
REVALIDATE_HEADER,
|
|
29
40
|
STATUS_HEADER,
|
|
@@ -31,10 +42,10 @@ import {
|
|
|
31
42
|
import { searchOf, searchOfChain } from "../search.ts"
|
|
32
43
|
import { mergeHeads, resolveMeta } from "./head-merge.ts"
|
|
33
44
|
import {
|
|
34
|
-
actionResponse,
|
|
35
45
|
EMPTY_RETAIN,
|
|
36
46
|
type HeadersLike,
|
|
37
47
|
isControlFlow,
|
|
48
|
+
isResponseResult,
|
|
38
49
|
isStatusSignal,
|
|
39
50
|
type LoadedLayoutModules,
|
|
40
51
|
layoutErrorId,
|
|
@@ -50,18 +61,114 @@ import {
|
|
|
50
61
|
withDuplicateInstanceHint,
|
|
51
62
|
} from "./render-document.ts"
|
|
52
63
|
import { urlPartsFor } from "./request-url.ts"
|
|
64
|
+
import {
|
|
65
|
+
ACTION_SCOPE,
|
|
66
|
+
type CoreResponseControls,
|
|
67
|
+
DATA_RESPONSE_HEADERS,
|
|
68
|
+
MIDDLEWARE_SCOPE,
|
|
69
|
+
PAGE_SCOPE,
|
|
70
|
+
PageResponseControls,
|
|
71
|
+
PRIVATE_NO_STORE,
|
|
72
|
+
PRIVATE_PAGE_HEADERS,
|
|
73
|
+
privateOutcome,
|
|
74
|
+
varyOnDataHeader,
|
|
75
|
+
} from "./response-controls.ts"
|
|
53
76
|
|
|
54
|
-
|
|
55
|
-
export type NonceResolver<Env = unknown> = (ctx: {
|
|
56
|
-
readonly request: Request
|
|
57
|
-
readonly env: Env
|
|
58
|
-
}) => string | undefined | Promise<string | undefined>
|
|
77
|
+
export type { NonceResolver } from "../nonce.ts"
|
|
59
78
|
|
|
60
79
|
/** Structural context supplied by the core server to one page route handler. */
|
|
61
80
|
export interface PageRouteContext<Env = unknown> {
|
|
62
81
|
readonly params: Record<string, string>
|
|
63
82
|
readonly req: Request
|
|
64
83
|
readonly env: Env
|
|
84
|
+
/** The serving app's response controls; a loader's `ctx.set` queues its cookies through them. */
|
|
85
|
+
readonly set: CoreResponseControls
|
|
86
|
+
/** The visitor's IP as the serving app derived it; forwarded to a request-bound `ctx.api`. */
|
|
87
|
+
readonly clientIp?: string | undefined
|
|
88
|
+
readonly waitUntil?: (promise: Promise<unknown>) => void
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The page slot of an `ssr = false` route with no `HydrateFallback`: a component that renders nothing. */
|
|
92
|
+
const EMPTY_LEAF = (): null => null
|
|
93
|
+
|
|
94
|
+
/** The request-scoping seam of an in-process `api` (see `inProcessClient`), if it carries one. */
|
|
95
|
+
function platformBinderOf(api: unknown): BackendPlatformBinder | undefined {
|
|
96
|
+
if ((typeof api !== "object" && typeof api !== "function") || api === null) return undefined
|
|
97
|
+
const bind = (api as { [NIFRA_BACKEND_BIND_PLATFORM]?: unknown })[NIFRA_BACKEND_BIND_PLATFORM]
|
|
98
|
+
return typeof bind === "function" ? (bind as BackendPlatformBinder) : undefined
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The page request's platform identity for loader calls: `clientIp` (read lazily, so a backend that
|
|
103
|
+
* never reads `c.clientIp` never pays the socket lookup), `env` and `waitUntil` - and nothing else. The
|
|
104
|
+
* page request's headers (cookie, authorization) never travel; a loader passes them explicitly if it
|
|
105
|
+
* wants an authenticated call. The prototype marker tells the backend its `clientIp` is already derived,
|
|
106
|
+
* so its own trust declaration does not re-read forwarding headers the synthesized request does not
|
|
107
|
+
* carry; a spread copy drops both the getter and the marker, so it falls back to re-deriving.
|
|
108
|
+
*/
|
|
109
|
+
class LoaderPlatform {
|
|
110
|
+
readonly env: unknown
|
|
111
|
+
readonly waitUntil: ((promise: Promise<unknown>) => void) | undefined
|
|
112
|
+
readonly #c: PageRouteContext<unknown>
|
|
113
|
+
constructor(c: PageRouteContext<unknown>) {
|
|
114
|
+
this.#c = c
|
|
115
|
+
this.env = c.env
|
|
116
|
+
this.waitUntil = c.waitUntil
|
|
117
|
+
}
|
|
118
|
+
get clientIp(): string | undefined {
|
|
119
|
+
return this.#c.clientIp
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
;(LoaderPlatform.prototype as { [NIFRA_PLATFORM_CLIENT_IP_DERIVED]?: true })[
|
|
123
|
+
NIFRA_PLATFORM_CLIENT_IP_DERIVED
|
|
124
|
+
] = true
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A redirect answering a data request (a client navigation or form submit), as the client router reads
|
|
128
|
+
* one: fetch would follow a 3xx and hand the router another page's data, so the target rides
|
|
129
|
+
* `x-nifra-redirect` on a 204 that keeps the redirect's own headers. `undefined` for any other outcome
|
|
130
|
+
* or request.
|
|
131
|
+
*/
|
|
132
|
+
function dataRedirect(req: Request, outcome: unknown): Response | ResponseResult | undefined {
|
|
133
|
+
if (req.headers.get(DATA_HEADER) === null) return undefined
|
|
134
|
+
if (isResponseResult(outcome)) {
|
|
135
|
+
const plain = outcome.plain
|
|
136
|
+
if (plain === undefined) return dataRedirect(req, outcome.toResponse())
|
|
137
|
+
if (plain.status < 300 || plain.status >= 400) return undefined
|
|
138
|
+
const { location, ...own } = plain.headers ?? {}
|
|
139
|
+
if (location === undefined) return undefined
|
|
140
|
+
return statusResult(204, undefined, {
|
|
141
|
+
headers: {
|
|
142
|
+
...own,
|
|
143
|
+
[REDIRECT_HEADER]: appTarget(location, req.url),
|
|
144
|
+
...DATA_RESPONSE_HEADERS,
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
if (!(outcome instanceof Response) || outcome.status < 300 || outcome.status >= 400) {
|
|
149
|
+
return undefined
|
|
150
|
+
}
|
|
151
|
+
const location = outcome.headers.get("location")
|
|
152
|
+
if (location === null) return undefined
|
|
153
|
+
const headers = new Headers(outcome.headers)
|
|
154
|
+
headers.delete("location")
|
|
155
|
+
headers.delete("content-length")
|
|
156
|
+
headers.set(REDIRECT_HEADER, appTarget(location, req.url))
|
|
157
|
+
for (const [name, value] of Object.entries(DATA_RESPONSE_HEADERS)) headers.set(name, value)
|
|
158
|
+
return new Response(null, { status: 204, headers })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** A same-origin target as a path, which the client router can follow without loading a document. */
|
|
162
|
+
function appTarget(location: string, requestUrl: string): string {
|
|
163
|
+
let target: URL
|
|
164
|
+
try {
|
|
165
|
+
target = new URL(location, requestUrl)
|
|
166
|
+
} catch {
|
|
167
|
+
return location // the browser fails on it the same way when it loads it
|
|
168
|
+
}
|
|
169
|
+
return target.origin === new URL(requestUrl).origin
|
|
170
|
+
? target.pathname + target.search + target.hash
|
|
171
|
+
: location
|
|
65
172
|
}
|
|
66
173
|
|
|
67
174
|
export interface PageExecutionOptions<Env = unknown> {
|
|
@@ -76,10 +183,16 @@ export interface PageExecutionOptions<Env = unknown> {
|
|
|
76
183
|
readonly routeStyles?: Readonly<Record<string, readonly string[]>>
|
|
77
184
|
readonly cssLoading?: CssLoadingMode
|
|
78
185
|
readonly prerenderedPaths?: readonly string[]
|
|
186
|
+
/** Where pages fetch {@link prerenderedPaths} from when the list is too large to inline. */
|
|
187
|
+
readonly prerenderedListUrl?: string
|
|
79
188
|
readonly staticFallbacks?: Readonly<Record<string, "ssr" | "404">>
|
|
80
189
|
readonly staticBoundaryCache?: StaticBoundaryCache
|
|
81
190
|
/** Per-request CSP nonce for framework-owned executable document scripts. */
|
|
82
191
|
readonly nonce?: NonceResolver<Env>
|
|
192
|
+
/** A hash-based CSP policy: every document is rendered under it, carrying `nonce` only when needed. */
|
|
193
|
+
readonly csp?: CspPolicy
|
|
194
|
+
/** Set once a cache wrapper attaches; until then a route's freshness and tags stay off the response. */
|
|
195
|
+
readonly cacheChannel?: { readonly enabled: boolean }
|
|
83
196
|
readonly onLoaderError?: (
|
|
84
197
|
error: unknown,
|
|
85
198
|
ctx: {
|
|
@@ -95,7 +208,34 @@ type PageResponse = Response | ResponseResult | RenderedPage
|
|
|
95
208
|
export interface PageRequestExecutor<Env = unknown> {
|
|
96
209
|
get(route: RouteEntry): (ctx: PageRouteContext<Env>) => Promise<PageResponse>
|
|
97
210
|
post(route: RouteEntry): (ctx: PageRouteContext<Env>) => Promise<PageResponse>
|
|
98
|
-
|
|
211
|
+
/** Answer a URL no route matched: the nearest nested `_404` for it, else the root one. */
|
|
212
|
+
fallback(ctx: PageRouteContext<Env>): Promise<PageResponse>
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** One URL pattern a nested `_404` answers, as the executor indexes it. */
|
|
216
|
+
interface NotFoundTarget {
|
|
217
|
+
readonly id: string
|
|
218
|
+
readonly page: NotFoundEntry
|
|
219
|
+
/** The pattern's param names in order; the index holds them by position. */
|
|
220
|
+
readonly names: readonly string[]
|
|
221
|
+
/** The page's layout chain as a route, so layouts, gates and boundaries run as they do for one. */
|
|
222
|
+
readonly route: RouteEntry
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** The browser's side of a client navigation: the page it is leaving and the layout slots it holds. */
|
|
226
|
+
type RetainContext = {
|
|
227
|
+
readonly requested: ReadonlySet<number>
|
|
228
|
+
readonly from: RouteEntry
|
|
229
|
+
readonly fromParams: Readonly<Record<string, string>>
|
|
230
|
+
readonly fromUrl: URL
|
|
231
|
+
readonly toUrl: URL
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
type LayoutRun = {
|
|
235
|
+
readonly modules: LoadedLayoutModules
|
|
236
|
+
readonly layoutData: readonly unknown[] | undefined
|
|
237
|
+
readonly retained: readonly number[]
|
|
238
|
+
readonly pending: Promise<unknown> | undefined
|
|
99
239
|
}
|
|
100
240
|
|
|
101
241
|
/**
|
|
@@ -111,14 +251,66 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
111
251
|
options: PageExecutionOptions<Env>,
|
|
112
252
|
): PageRequestExecutor<Env> {
|
|
113
253
|
const { adapter, manifest, clientEntry, api } = options
|
|
254
|
+
// Bound once per render, so every loader, action and boundary in that render shares one view of the
|
|
255
|
+
// in-process client carrying the visitor's platform. An `api` without the seam passes through as-is.
|
|
256
|
+
const bindApi = platformBinderOf(api)
|
|
257
|
+
// One view per render, shared by every loader, action and boundary it runs.
|
|
258
|
+
const apiFor = (c: PageRouteContext<Env>): unknown =>
|
|
259
|
+
bindApi === undefined ? api : bindApi.call(api, new LoaderPlatform(c) as Platform)
|
|
114
260
|
const cssLoading = normalizeCssLoading(options.cssLoading ?? DEFAULT_CSS_LOADING)
|
|
115
261
|
const titleOption = options.title === undefined ? {} : { title: options.title }
|
|
262
|
+
const cspOption = options.csp === undefined ? {} : { csp: options.csp }
|
|
116
263
|
const prerenderedSet = new Set(options.prerenderedPaths ?? [])
|
|
264
|
+
const prerenderedHandover =
|
|
265
|
+
options.prerenderedListUrl === undefined
|
|
266
|
+
? { prerenderedPaths: options.prerenderedPaths ?? [] }
|
|
267
|
+
: { prerenderedListUrl: options.prerenderedListUrl }
|
|
117
268
|
const matchManifestRoute = createMatcher(
|
|
118
269
|
manifest.routes.map((route) => ({ routeId: route.id, pattern: route.pattern })),
|
|
119
270
|
)
|
|
120
271
|
const routeById = new Map(manifest.routes.map((route) => [route.id, route]))
|
|
121
272
|
|
|
273
|
+
// A nested `_404` answers an unmatched URL from inside the catch-all, never as a route of its own:
|
|
274
|
+
// a registered `/admin/*` would take URLs from a route the router only reaches by backtracking.
|
|
275
|
+
// Param names are indexed by position, so two directories that name one segment differently share
|
|
276
|
+
// the index; each target keeps its own names to read a match back.
|
|
277
|
+
const notFoundTargets: NotFoundTarget[] = []
|
|
278
|
+
const notFoundPatterns: Array<{ routeId: string; pattern: string }> = []
|
|
279
|
+
for (const [id, page] of Object.entries(manifest.notFounds ?? {})) {
|
|
280
|
+
for (const scope of page.scopes) {
|
|
281
|
+
const names: string[] = []
|
|
282
|
+
const pattern = scope.pattern.replace(
|
|
283
|
+
/([:*])([A-Za-z_][A-Za-z0-9_]*)/g,
|
|
284
|
+
(_match, sigil: string, name: string) => `${sigil}p${names.push(name) - 1}`,
|
|
285
|
+
)
|
|
286
|
+
notFoundPatterns.push({ routeId: String(notFoundTargets.length), pattern })
|
|
287
|
+
notFoundTargets.push({
|
|
288
|
+
id,
|
|
289
|
+
page,
|
|
290
|
+
names,
|
|
291
|
+
route: {
|
|
292
|
+
id,
|
|
293
|
+
pattern: scope.pattern,
|
|
294
|
+
layoutIds: page.layoutIds,
|
|
295
|
+
layoutParams: scope.layoutParams,
|
|
296
|
+
...(page.middlewareIds === undefined
|
|
297
|
+
? {}
|
|
298
|
+
: {
|
|
299
|
+
middlewareIds: page.middlewareIds,
|
|
300
|
+
...(scope.middlewareParams === undefined
|
|
301
|
+
? {}
|
|
302
|
+
: { middlewareParams: scope.middlewareParams }),
|
|
303
|
+
}),
|
|
304
|
+
errorIds: page.errorIds,
|
|
305
|
+
file: page.file,
|
|
306
|
+
load: page.load,
|
|
307
|
+
},
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const matchNotFoundScope =
|
|
312
|
+
notFoundPatterns.length === 0 ? undefined : createMatcher(notFoundPatterns)
|
|
313
|
+
|
|
122
314
|
const draftFlag = (req: Request): Promise<boolean> =>
|
|
123
315
|
options.draftSecret === undefined
|
|
124
316
|
? Promise.resolve(false)
|
|
@@ -161,34 +353,49 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
161
353
|
return modules
|
|
162
354
|
}
|
|
163
355
|
|
|
356
|
+
// No modules, data, or per-request state: reuse the settled result for page-only routes.
|
|
357
|
+
const emptyLayoutRun: LayoutRun = {
|
|
358
|
+
modules: [],
|
|
359
|
+
layoutData: undefined,
|
|
360
|
+
retained: [],
|
|
361
|
+
pending: undefined,
|
|
362
|
+
}
|
|
363
|
+
|
|
164
364
|
/** Run layout loaders, awaiting gates before starting the page loader and retaining only safe data. */
|
|
165
365
|
const runLayoutChain = async (
|
|
166
366
|
route: RouteEntry,
|
|
167
367
|
ctx: LoaderContext,
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
readonly retained: readonly number[]
|
|
173
|
-
readonly pending: Promise<unknown>
|
|
174
|
-
}> => {
|
|
368
|
+
controls: PageResponseControls,
|
|
369
|
+
retain?: RetainContext,
|
|
370
|
+
): Promise<LayoutRun> => {
|
|
371
|
+
if (route.layoutIds.length === 0) return emptyLayoutRun
|
|
175
372
|
const modules = await loadLayoutModules(route)
|
|
176
373
|
if (!modules.some((m) => m.loader !== undefined)) {
|
|
177
|
-
return { modules, layoutData: undefined, retained: [], pending:
|
|
374
|
+
return { modules, layoutData: undefined, retained: [], pending: undefined }
|
|
178
375
|
}
|
|
179
376
|
const results: unknown[] = new Array(modules.length).fill(null)
|
|
180
377
|
const retained: number[] = []
|
|
181
378
|
const pending: Array<Promise<void>> = []
|
|
182
379
|
for (let i = 0; i < modules.length; i++) {
|
|
183
|
-
const
|
|
380
|
+
const layout = modules[i] as LoadedLayoutModules[number]
|
|
381
|
+
const loader = layout.loader
|
|
184
382
|
if (loader === undefined) continue
|
|
185
|
-
if (retain
|
|
186
|
-
|
|
187
|
-
|
|
383
|
+
if (retain !== undefined && layout.gate !== true) {
|
|
384
|
+
let keep: boolean
|
|
385
|
+
try {
|
|
386
|
+
keep = keepsLayoutData(route, i, layout, ctx.params, retain)
|
|
387
|
+
} catch (err) {
|
|
388
|
+
throw tagLayoutError(err, route.layoutIds[i] as string)
|
|
389
|
+
}
|
|
390
|
+
if (keep) {
|
|
391
|
+
retained.push(i)
|
|
392
|
+
continue
|
|
393
|
+
}
|
|
188
394
|
}
|
|
189
395
|
const scoped: LoaderContext = {
|
|
190
396
|
...ctx,
|
|
191
397
|
params: scopeParams(ctx.params, route.layoutParams?.[i]),
|
|
398
|
+
set: controls.scope(i),
|
|
192
399
|
}
|
|
193
400
|
if (modules[i]?.gate === true) {
|
|
194
401
|
try {
|
|
@@ -200,23 +407,40 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
200
407
|
}
|
|
201
408
|
const layoutId = route.layoutIds[i] as string
|
|
202
409
|
const index = i
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
410
|
+
try {
|
|
411
|
+
const value = loader(scoped)
|
|
412
|
+
// Scalars settle synchronously. Objects keep await's intrinsic promise/thenable
|
|
413
|
+
// assimilation: property-presence and prototype probes are unsafe for proxies.
|
|
414
|
+
if (value !== null && (typeof value === "object" || typeof value === "function")) {
|
|
415
|
+
pending.push(
|
|
416
|
+
(async () => {
|
|
417
|
+
try {
|
|
418
|
+
results[index] = await value
|
|
419
|
+
} catch (err) {
|
|
420
|
+
throw tagLayoutError(err, layoutId)
|
|
421
|
+
}
|
|
422
|
+
})(),
|
|
423
|
+
)
|
|
424
|
+
} else results[index] = value
|
|
425
|
+
} catch (err) {
|
|
426
|
+
// Preserve a synchronous throw as a pending layout failure, just like the async lane.
|
|
427
|
+
pending.push(Promise.reject(tagLayoutError(err, layoutId)))
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
modules,
|
|
432
|
+
layoutData: results,
|
|
433
|
+
retained,
|
|
434
|
+
pending:
|
|
435
|
+
pending.length === 0 ? undefined : pending.length === 1 ? pending[0] : Promise.all(pending),
|
|
212
436
|
}
|
|
213
|
-
return { modules, layoutData: results, retained, pending: Promise.all(pending) }
|
|
214
437
|
}
|
|
215
438
|
|
|
216
439
|
/** Run only authorization gates before an action; ordinary layout data runs after a native action. */
|
|
217
440
|
const runLayoutGates = async (
|
|
218
441
|
route: RouteEntry,
|
|
219
442
|
ctx: LoaderContext,
|
|
443
|
+
controls: PageResponseControls,
|
|
220
444
|
): Promise<LoadedLayoutModules> => {
|
|
221
445
|
const modules = await loadLayoutModules(route)
|
|
222
446
|
for (let i = 0; i < modules.length; i++) {
|
|
@@ -226,6 +450,7 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
226
450
|
await mod.loader({
|
|
227
451
|
...ctx,
|
|
228
452
|
params: scopeParams(ctx.params, route.layoutParams?.[i]),
|
|
453
|
+
set: controls.scope(i),
|
|
229
454
|
})
|
|
230
455
|
} catch (err) {
|
|
231
456
|
throw tagLayoutError(err, route.layoutIds[i] as string)
|
|
@@ -234,6 +459,47 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
234
459
|
return modules
|
|
235
460
|
}
|
|
236
461
|
|
|
462
|
+
/** Outermost first; a failure carries its directory so the nearest `_error` above it renders. */
|
|
463
|
+
const runMiddleware = async (
|
|
464
|
+
route: RouteEntry,
|
|
465
|
+
ctx: LoaderContext,
|
|
466
|
+
controls: PageResponseControls,
|
|
467
|
+
): Promise<void> => {
|
|
468
|
+
const ids = route.middlewareIds ?? []
|
|
469
|
+
for (let i = 0; i < ids.length; i++) {
|
|
470
|
+
const id = ids[i] as string
|
|
471
|
+
const entry = manifest.middlewares?.[id] as LayoutEntry
|
|
472
|
+
const dirTag = `${id.slice(0, id.length - "_middleware".length)}_layout`
|
|
473
|
+
let outcome: unknown
|
|
474
|
+
try {
|
|
475
|
+
const middleware = (await entry.load()).default
|
|
476
|
+
// A `_layout.backend.ts` that exports no middleware is a layout data module, nothing more.
|
|
477
|
+
if (middleware === undefined) continue
|
|
478
|
+
if (typeof middleware !== "function") {
|
|
479
|
+
throw new Error(
|
|
480
|
+
`[nifra/web] "${entry.file}" exports a middleware that is not a function.`,
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
outcome = await middleware({
|
|
484
|
+
...ctx,
|
|
485
|
+
params: scopeParams(ctx.params, route.middlewareParams?.[i]),
|
|
486
|
+
set: controls.scope(MIDDLEWARE_SCOPE + i),
|
|
487
|
+
})
|
|
488
|
+
} catch (err) {
|
|
489
|
+
throw tagLayoutError(err, dirTag)
|
|
490
|
+
}
|
|
491
|
+
if (outcome === undefined) continue
|
|
492
|
+
throw tagLayoutError(
|
|
493
|
+
isControlFlow(outcome)
|
|
494
|
+
? outcome
|
|
495
|
+
: new Error(
|
|
496
|
+
`[nifra/web] the middleware in "${entry.file}" returned a value. Middleware returns nothing to let the request through, or a redirect(), a status such as notFound(), or a Response to answer it.`,
|
|
497
|
+
),
|
|
498
|
+
dirTag,
|
|
499
|
+
)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
237
503
|
const retainedIndices = (header: string | null): ReadonlySet<number> => {
|
|
238
504
|
if (header === null || header === "") return EMPTY_RETAIN
|
|
239
505
|
const out = new Set<number>()
|
|
@@ -244,43 +510,61 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
244
510
|
return out
|
|
245
511
|
}
|
|
246
512
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
):
|
|
252
|
-
if (req.headers.get(DATA_HEADER) === null) return
|
|
513
|
+
/**
|
|
514
|
+
* The browser's side of a client navigation: the page it is leaving and the layout slots it asked
|
|
515
|
+
* to keep. Undefined unless this is a data request whose `from` is same-origin and routable.
|
|
516
|
+
*/
|
|
517
|
+
const retainContextOf = (req: Request): RetainContext | undefined => {
|
|
518
|
+
if (req.headers.get(DATA_HEADER) === null) return undefined
|
|
253
519
|
const requested = retainedIndices(req.headers.get(RETAIN_HEADER))
|
|
254
520
|
const from = req.headers.get(NAV_FROM_HEADER)
|
|
255
|
-
if (requested.size === 0 || from === null) return
|
|
256
|
-
let
|
|
521
|
+
if (requested.size === 0 || from === null) return undefined
|
|
522
|
+
let toUrl: URL
|
|
523
|
+
let fromUrl: URL
|
|
257
524
|
try {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (source.origin !== current.origin) return EMPTY_RETAIN
|
|
261
|
-
fromPath = source.pathname + source.search
|
|
525
|
+
toUrl = new URL(req.url)
|
|
526
|
+
fromUrl = new URL(from, toUrl)
|
|
262
527
|
} catch {
|
|
263
|
-
return
|
|
528
|
+
return undefined
|
|
264
529
|
}
|
|
265
|
-
|
|
266
|
-
|
|
530
|
+
if (fromUrl.origin !== toUrl.origin) return undefined
|
|
531
|
+
const previousMatch = matchManifestRoute(fromUrl.pathname + fromUrl.search)
|
|
532
|
+
if (previousMatch === null) return undefined
|
|
267
533
|
const previous = routeById.get(previousMatch.routeId)
|
|
268
|
-
if (previous === undefined) return
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
534
|
+
if (previous === undefined) return undefined
|
|
535
|
+
return { requested, from: previous, fromParams: previousMatch.params, fromUrl, toUrl }
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** A query change re-runs layout loaders by default: any can read it, and none declares keys. */
|
|
539
|
+
const keepsLayoutData = (
|
|
540
|
+
route: RouteEntry,
|
|
541
|
+
index: number,
|
|
542
|
+
layout: LoadedLayoutModules[number],
|
|
543
|
+
params: Readonly<Record<string, string>>,
|
|
544
|
+
retain: RetainContext,
|
|
545
|
+
): boolean => {
|
|
546
|
+
if (!retain.requested.has(index)) return false
|
|
547
|
+
const previous = retain.from
|
|
548
|
+
if (route.layoutIds[index] !== previous.layoutIds[index]) return false
|
|
549
|
+
const owned = route.layoutParams?.[index] ?? []
|
|
550
|
+
const previouslyOwned = previous.layoutParams?.[index] ?? []
|
|
551
|
+
const defaultShouldRevalidate =
|
|
552
|
+
retain.fromUrl.search !== retain.toUrl.search ||
|
|
553
|
+
owned.length !== previouslyOwned.length ||
|
|
554
|
+
owned.some((name, i) => name !== previouslyOwned[i]) ||
|
|
555
|
+
owned.some((name) => params[name] !== retain.fromParams[name])
|
|
556
|
+
const decide = layout.shouldRevalidate
|
|
557
|
+
if (typeof decide !== "function") return !defaultShouldRevalidate
|
|
558
|
+
// Copies, so a hook that mutates its arguments cannot reach the request's params or the next
|
|
559
|
+
// layout's view of the navigation.
|
|
560
|
+
const answer = (decide as ShouldRevalidate)({
|
|
561
|
+
currentUrl: new URL(retain.fromUrl),
|
|
562
|
+
nextUrl: new URL(retain.toUrl),
|
|
563
|
+
currentParams: { ...retain.fromParams },
|
|
564
|
+
nextParams: { ...params },
|
|
565
|
+
defaultShouldRevalidate,
|
|
566
|
+
})
|
|
567
|
+
return answer === false
|
|
284
568
|
}
|
|
285
569
|
|
|
286
570
|
const loaderSearch = (searchSchema: RouteModule["searchSchema"], request: Request) =>
|
|
@@ -296,12 +580,25 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
296
580
|
urlPartsFor(request).search,
|
|
297
581
|
)
|
|
298
582
|
|
|
583
|
+
// `ssr = false`: the slot holds `HydrateFallback`, which the browser hydrates before the page.
|
|
584
|
+
const serverLeafOf = (route: RouteEntry, mod: RouteModule): unknown => {
|
|
585
|
+
if (mod.ssr !== false) return mod.default
|
|
586
|
+
if (mod.hydrate === false) {
|
|
587
|
+
throw new Error(
|
|
588
|
+
`[nifra/web] route "${route.id}" sets both \`ssr = false\` and \`hydrate = false\`: the ` +
|
|
589
|
+
"server skips its component and no client would ever render it. Remove one of the two.",
|
|
590
|
+
)
|
|
591
|
+
}
|
|
592
|
+
return mod.HydrateFallback ?? EMPTY_LEAF
|
|
593
|
+
}
|
|
594
|
+
|
|
299
595
|
const resolveChainAndHead = (
|
|
300
596
|
layoutModules: LoadedLayoutModules,
|
|
301
597
|
page: RouteModule,
|
|
302
598
|
metaArgs: MetaArgs,
|
|
599
|
+
leaf: unknown = page.default,
|
|
303
600
|
): { chain: unknown[]; head: Meta } => {
|
|
304
|
-
const chain = [...layoutModules.map((m) => m.default),
|
|
601
|
+
const chain = [...layoutModules.map((m) => m.default), leaf]
|
|
305
602
|
const heads = [
|
|
306
603
|
...layoutModules.map((m) => resolveMeta(m.meta, metaArgs)),
|
|
307
604
|
resolveMeta(page.meta, metaArgs),
|
|
@@ -309,6 +606,17 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
309
606
|
return { chain, head: mergeHeads(heads) }
|
|
310
607
|
}
|
|
311
608
|
|
|
609
|
+
/** What `useMatches` reports for a chain: the layouts' ids and `handle`s, then the page's. */
|
|
610
|
+
const matchChainOf = (
|
|
611
|
+
layoutIds: readonly string[],
|
|
612
|
+
layoutModules: LoadedLayoutModules,
|
|
613
|
+
pageId: string,
|
|
614
|
+
page: { readonly handle?: unknown },
|
|
615
|
+
): MatchChain => ({
|
|
616
|
+
ids: [...layoutIds, pageId],
|
|
617
|
+
handles: [...layoutModules.map((layout) => layout.handle), page.handle],
|
|
618
|
+
})
|
|
619
|
+
|
|
312
620
|
const dirOfId = (id: string, suffix: string): string =>
|
|
313
621
|
id === suffix ? "" : id.slice(0, id.length - suffix.length - 1)
|
|
314
622
|
|
|
@@ -345,6 +653,7 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
345
653
|
route: RouteEntry,
|
|
346
654
|
errorId: string,
|
|
347
655
|
err: unknown,
|
|
656
|
+
personalized: boolean,
|
|
348
657
|
nonce?: string,
|
|
349
658
|
): Promise<PageResponse> => {
|
|
350
659
|
const errDir = dirOfId(errorId, "_error")
|
|
@@ -359,15 +668,21 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
359
668
|
)
|
|
360
669
|
const { default: errComp } = await (manifest.errors?.[errorId] as LayoutEntry).load()
|
|
361
670
|
const e = err instanceof Error ? err : new Error(String(err))
|
|
671
|
+
// An error's text can carry a connection string or a query, so a production page gets neither.
|
|
672
|
+
const production = typeof process === "undefined" || process.env.NODE_ENV === "production"
|
|
362
673
|
return renderPageResult({
|
|
363
674
|
adapter,
|
|
364
675
|
chain: [...layouts, errComp],
|
|
365
|
-
data:
|
|
676
|
+
data: production
|
|
677
|
+
? { name: "Error", message: "Internal Server Error" }
|
|
678
|
+
: { name: e.name, message: e.message },
|
|
366
679
|
clientEntry,
|
|
367
680
|
routeId: errorId,
|
|
368
681
|
status: 500,
|
|
369
682
|
hydrate: false,
|
|
683
|
+
...(personalized ? { headers: PRIVATE_PAGE_HEADERS } : {}),
|
|
370
684
|
...(nonce === undefined ? {} : { nonce }),
|
|
685
|
+
...cspOption,
|
|
371
686
|
...titleOption,
|
|
372
687
|
})
|
|
373
688
|
}
|
|
@@ -388,6 +703,7 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
388
703
|
const loaded = (await page.load()) as {
|
|
389
704
|
readonly default: unknown
|
|
390
705
|
readonly searchSchema?: RouteModule["searchSchema"]
|
|
706
|
+
readonly handle?: unknown
|
|
391
707
|
}
|
|
392
708
|
return renderPageResult({
|
|
393
709
|
adapter,
|
|
@@ -395,6 +711,7 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
395
711
|
data: null,
|
|
396
712
|
clientEntry,
|
|
397
713
|
routeId,
|
|
714
|
+
matchChain: { ids: [routeId], handles: [loaded.handle] },
|
|
398
715
|
...(path !== undefined
|
|
399
716
|
? {
|
|
400
717
|
search: searchOf(
|
|
@@ -407,9 +724,10 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
407
724
|
status,
|
|
408
725
|
...(extraHeaders !== undefined ? { headers: extraHeaders } : {}),
|
|
409
726
|
...(nonce === undefined ? {} : { nonce }),
|
|
727
|
+
...cspOption,
|
|
410
728
|
...preloadOf(routeId),
|
|
411
729
|
...stylesOf(routeId),
|
|
412
|
-
|
|
730
|
+
...prerenderedHandover,
|
|
413
731
|
...titleOption,
|
|
414
732
|
})
|
|
415
733
|
}
|
|
@@ -418,14 +736,29 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
418
736
|
req: Request,
|
|
419
737
|
env: Env,
|
|
420
738
|
signal: StatusSignal,
|
|
739
|
+
personalized: boolean,
|
|
421
740
|
): Promise<PageResponse> => {
|
|
422
741
|
const { status, headers } = signal[STATUS_SIGNAL]
|
|
423
742
|
if (req.headers.get(DATA_HEADER) !== null) {
|
|
743
|
+
// Same URL as the status page, so it is never storable: a cache keyed on the URL alone would
|
|
744
|
+
// otherwise answer a document request with this empty body.
|
|
424
745
|
const responseHeaders = new Headers(headers)
|
|
425
746
|
responseHeaders.set(STATUS_HEADER, String(status))
|
|
747
|
+
responseHeaders.set("cache-control", PRIVATE_NO_STORE)
|
|
748
|
+
responseHeaders.set("vary", varyOnDataHeader(responseHeaders.get("vary")))
|
|
426
749
|
return new Response(null, { status, headers: responseHeaders })
|
|
427
750
|
}
|
|
428
|
-
|
|
751
|
+
const nonce = await resolveNonce(req, env)
|
|
752
|
+
if (headers === undefined && !personalized) {
|
|
753
|
+
return renderTerminalStatus(status, pathOf(req), undefined, nonce)
|
|
754
|
+
}
|
|
755
|
+
const documentHeaders = new Headers(headers)
|
|
756
|
+
if (headers !== undefined) {
|
|
757
|
+
documentHeaders.set("vary", varyOnDataHeader(documentHeaders.get("vary")))
|
|
758
|
+
}
|
|
759
|
+
// A cookie queued before the signal makes the status page specific to this visitor.
|
|
760
|
+
if (personalized) documentHeaders.set("cache-control", PRIVATE_NO_STORE)
|
|
761
|
+
return renderTerminalStatus(status, pathOf(req), documentHeaders, nonce)
|
|
429
762
|
}
|
|
430
763
|
|
|
431
764
|
const reportLoaderError = (
|
|
@@ -454,8 +787,10 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
454
787
|
layoutRetained: readonly number[],
|
|
455
788
|
boundaryStates: BoundaryStates | undefined,
|
|
456
789
|
assemblyCache: RenderAssemblyCache | undefined,
|
|
790
|
+
responseHeaders: Record<string, string> | undefined,
|
|
791
|
+
personalized: boolean,
|
|
457
792
|
): Promise<PageResponse> => {
|
|
458
|
-
if (isStatusSignal(data)) return renderStatusSignal(req, env, data)
|
|
793
|
+
if (isStatusSignal(data)) return renderStatusSignal(req, env, data, personalized)
|
|
459
794
|
if (isControlFlow(data)) return data
|
|
460
795
|
if (req.headers.get(DATA_HEADER) !== null) {
|
|
461
796
|
const pageSplit = prepareDeferred(data)
|
|
@@ -483,18 +818,29 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
483
818
|
retained: layoutRetained,
|
|
484
819
|
...(boundarySplit === undefined ? {} : { boundaries: boundarySplit.forClient }),
|
|
485
820
|
}
|
|
486
|
-
if (allDeferred.length === 0)
|
|
821
|
+
if (allDeferred.length === 0) {
|
|
822
|
+
return Response.json(payload ?? null, { headers: DATA_RESPONSE_HEADERS })
|
|
823
|
+
}
|
|
487
824
|
return new Response(ndjsonStream(payload, allDeferred), {
|
|
488
|
-
headers: {
|
|
825
|
+
headers: {
|
|
826
|
+
"content-type": "application/x-ndjson; charset=utf-8",
|
|
827
|
+
...DATA_RESPONSE_HEADERS,
|
|
828
|
+
},
|
|
489
829
|
})
|
|
490
830
|
}
|
|
491
831
|
|
|
492
|
-
const { chain, head } = resolveChainAndHead(layoutModules, mod, {
|
|
493
|
-
data,
|
|
494
|
-
params,
|
|
495
|
-
origin: originOf(req),
|
|
496
|
-
})
|
|
497
832
|
const nonce = options.nonce === undefined ? undefined : await resolveNonce(req, env)
|
|
833
|
+
const { chain, head } = resolveChainAndHead(
|
|
834
|
+
layoutModules,
|
|
835
|
+
mod,
|
|
836
|
+
{
|
|
837
|
+
data,
|
|
838
|
+
params,
|
|
839
|
+
origin: originOf(req),
|
|
840
|
+
...(nonce === undefined ? {} : { nonce }),
|
|
841
|
+
},
|
|
842
|
+
serverLeafOf(route, mod),
|
|
843
|
+
)
|
|
498
844
|
try {
|
|
499
845
|
return await renderPageResult({
|
|
500
846
|
adapter,
|
|
@@ -506,27 +852,169 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
506
852
|
params,
|
|
507
853
|
path: pathOf(req),
|
|
508
854
|
search: searchChainOf(layoutModules, mod, req),
|
|
855
|
+
matchChain: matchChainOf(route.layoutIds, layoutModules, route.id, mod),
|
|
509
856
|
hydrate: mod.hydrate !== false,
|
|
510
857
|
...(layoutData !== undefined ? { layoutData } : {}),
|
|
511
858
|
...(boundaryStates !== undefined ? { boundaries: boundaryStates } : {}),
|
|
512
859
|
...preloadOf(route.id),
|
|
513
860
|
...stylesOf(route.id),
|
|
514
|
-
|
|
515
|
-
...(
|
|
516
|
-
|
|
861
|
+
...prerenderedHandover,
|
|
862
|
+
...(responseHeaders === undefined ? {} : { headers: responseHeaders }),
|
|
863
|
+
// A response that sets a cookie is one visitor's: it never advertises ISR freshness.
|
|
864
|
+
...(mod.revalidate !== undefined && !personalized && options.cacheChannel?.enabled === true
|
|
865
|
+
? { revalidate: mod.revalidate }
|
|
866
|
+
: {}),
|
|
867
|
+
...(mod.revalidateTags !== undefined &&
|
|
868
|
+
!personalized &&
|
|
869
|
+
options.cacheChannel?.enabled === true
|
|
870
|
+
? {
|
|
871
|
+
revalidateTags: routeTags(
|
|
872
|
+
mod.revalidateTags,
|
|
873
|
+
{ params, url: new URL(req.url) },
|
|
874
|
+
route.id,
|
|
875
|
+
),
|
|
876
|
+
}
|
|
877
|
+
: {}),
|
|
517
878
|
...(mod.islandScripts !== undefined ? { islandScripts: mod.islandScripts } : {}),
|
|
518
879
|
...(nonce === undefined ? {} : { nonce }),
|
|
880
|
+
...cspOption,
|
|
519
881
|
...titleOption,
|
|
520
882
|
...(assemblyCache === undefined ? {} : { assemblyCache }),
|
|
521
883
|
})
|
|
522
884
|
} catch (err) {
|
|
523
|
-
if (isStatusSignal(err)) return renderStatusSignal(req, env, err)
|
|
885
|
+
if (isStatusSignal(err)) return renderStatusSignal(req, env, err, personalized)
|
|
524
886
|
if (isControlFlow(err)) throw err
|
|
525
887
|
reportLoaderError(route, req, { ...params }, err)
|
|
526
888
|
const errorId = boundaryFor(route, err)
|
|
527
889
|
if (errorId === undefined) throw err
|
|
528
|
-
return renderError(route, errorId, withDuplicateInstanceHint(err), nonce)
|
|
890
|
+
return renderError(route, errorId, withDuplicateInstanceHint(err), personalized, nonce)
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/** What a failed loader answers with, once a status signal has been ruled out. */
|
|
895
|
+
const renderLoaderFailure = async (
|
|
896
|
+
route: RouteEntry,
|
|
897
|
+
req: Request,
|
|
898
|
+
env: Env,
|
|
899
|
+
params: Record<string, string>,
|
|
900
|
+
err: unknown,
|
|
901
|
+
personalized: boolean,
|
|
902
|
+
): Promise<PageResponse> => {
|
|
903
|
+
if (isControlFlow(err)) throw err
|
|
904
|
+
reportLoaderError(route, req, params, err)
|
|
905
|
+
const errorId = boundaryFor(route, err)
|
|
906
|
+
if (errorId === undefined) throw err
|
|
907
|
+
if (req.headers.get(DATA_HEADER) !== null) {
|
|
908
|
+
return new Response("Internal Server Error", {
|
|
909
|
+
status: 500,
|
|
910
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
911
|
+
})
|
|
529
912
|
}
|
|
913
|
+
return renderError(
|
|
914
|
+
route,
|
|
915
|
+
errorId,
|
|
916
|
+
withDuplicateInstanceHint(err),
|
|
917
|
+
personalized,
|
|
918
|
+
options.nonce === undefined ? undefined : await resolveNonce(req, env),
|
|
919
|
+
)
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* Render a nested `_404` inside its layouts. `layoutModules` and `layoutData` are exactly the
|
|
924
|
+
* layouts at or above the page, their gates already passed.
|
|
925
|
+
*/
|
|
926
|
+
const renderNestedNotFound = async (
|
|
927
|
+
req: Request,
|
|
928
|
+
env: Env,
|
|
929
|
+
id: string,
|
|
930
|
+
page: NotFoundEntry,
|
|
931
|
+
params: Readonly<Record<string, string>>,
|
|
932
|
+
layoutModules: LoadedLayoutModules,
|
|
933
|
+
layoutData: readonly unknown[] | undefined,
|
|
934
|
+
personalized: boolean,
|
|
935
|
+
signalHeaders?: HeadersLike,
|
|
936
|
+
): Promise<PageResponse> => {
|
|
937
|
+
// The page can show what a layout loaded for this visitor, and a 404 is storable by default.
|
|
938
|
+
const isPrivate = personalized || layoutData !== undefined
|
|
939
|
+
if (req.headers.get(DATA_HEADER) !== null) {
|
|
940
|
+
// No status header: the page has no client chunk, so the navigation falls back to loading the
|
|
941
|
+
// URL as a document, which renders it here.
|
|
942
|
+
const headers = new Headers(signalHeaders)
|
|
943
|
+
headers.set("cache-control", PRIVATE_NO_STORE)
|
|
944
|
+
headers.set("vary", varyOnDataHeader(headers.get("vary")))
|
|
945
|
+
return new Response(null, { status: 404, headers })
|
|
946
|
+
}
|
|
947
|
+
const mod = await page.load()
|
|
948
|
+
const nonce = options.nonce === undefined ? undefined : await resolveNonce(req, env)
|
|
949
|
+
const { chain, head } = resolveChainAndHead(layoutModules, mod, {
|
|
950
|
+
data: null,
|
|
951
|
+
params,
|
|
952
|
+
origin: originOf(req),
|
|
953
|
+
...(nonce === undefined ? {} : { nonce }),
|
|
954
|
+
})
|
|
955
|
+
let headers: Headers | undefined
|
|
956
|
+
if (signalHeaders !== undefined || isPrivate) {
|
|
957
|
+
headers = new Headers(signalHeaders)
|
|
958
|
+
if (signalHeaders !== undefined) headers.set("vary", varyOnDataHeader(headers.get("vary")))
|
|
959
|
+
if (isPrivate) headers.set("cache-control", PRIVATE_NO_STORE)
|
|
960
|
+
}
|
|
961
|
+
return renderPageResult({
|
|
962
|
+
adapter,
|
|
963
|
+
chain,
|
|
964
|
+
data: null,
|
|
965
|
+
head,
|
|
966
|
+
clientEntry,
|
|
967
|
+
routeId: id,
|
|
968
|
+
params,
|
|
969
|
+
path: pathOf(req),
|
|
970
|
+
search: searchChainOf(layoutModules, mod, req),
|
|
971
|
+
matchChain: matchChainOf(page.layoutIds, layoutModules, id, mod),
|
|
972
|
+
status: 404,
|
|
973
|
+
hydrate: false,
|
|
974
|
+
...(layoutData !== undefined ? { layoutData } : {}),
|
|
975
|
+
...stylesOf(id),
|
|
976
|
+
...(headers === undefined ? {} : { headers }),
|
|
977
|
+
...(nonce === undefined ? {} : { nonce }),
|
|
978
|
+
...cspOption,
|
|
979
|
+
...titleOption,
|
|
980
|
+
})
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/** The nearest nested `_404` for the route's own `notFound()`, else `undefined` (root page). */
|
|
984
|
+
const renderRouteNotFound = async (
|
|
985
|
+
req: Request,
|
|
986
|
+
env: Env,
|
|
987
|
+
route: RouteEntry,
|
|
988
|
+
params: Readonly<Record<string, string>>,
|
|
989
|
+
signal: StatusSignal,
|
|
990
|
+
run: LayoutRun | undefined,
|
|
991
|
+
personalized: boolean,
|
|
992
|
+
): Promise<PageResponse | undefined> => {
|
|
993
|
+
const id = route.notFoundIds?.at(-1)
|
|
994
|
+
if (id === undefined || run === undefined) return undefined
|
|
995
|
+
if (signal[STATUS_SIGNAL].status !== 404 || layoutErrorId(signal) !== undefined) {
|
|
996
|
+
return undefined
|
|
997
|
+
}
|
|
998
|
+
const page = manifest.notFounds?.[id]
|
|
999
|
+
if (page === undefined) return undefined
|
|
1000
|
+
try {
|
|
1001
|
+
await run.pending
|
|
1002
|
+
} catch {
|
|
1003
|
+
return undefined
|
|
1004
|
+
}
|
|
1005
|
+
// The page's layouts are the leading ones of the route's: its directory contains the route.
|
|
1006
|
+
const kept = page.layoutIds.length
|
|
1007
|
+
return renderNestedNotFound(
|
|
1008
|
+
req,
|
|
1009
|
+
env,
|
|
1010
|
+
id,
|
|
1011
|
+
page,
|
|
1012
|
+
params,
|
|
1013
|
+
run.modules.slice(0, kept),
|
|
1014
|
+
run.layoutData?.slice(0, kept),
|
|
1015
|
+
personalized,
|
|
1016
|
+
signal[STATUS_SIGNAL].headers,
|
|
1017
|
+
)
|
|
530
1018
|
}
|
|
531
1019
|
|
|
532
1020
|
const renderNotFound = async (
|
|
@@ -544,6 +1032,33 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
544
1032
|
)
|
|
545
1033
|
}
|
|
546
1034
|
|
|
1035
|
+
/**
|
|
1036
|
+
* Run a page handler with the request's `ctx.set`, sealed once the handler settles - whatever it
|
|
1037
|
+
* answered with - so a write that arrives after the response was decided throws. A request that
|
|
1038
|
+
* queued a cookie answers with a response no shared cache may store, returned or thrown. A redirect
|
|
1039
|
+
* answering a data request, returned or thrown, reaches the client router intact.
|
|
1040
|
+
*/
|
|
1041
|
+
const withResponseControls =
|
|
1042
|
+
(run: (c: PageRouteContext<Env>, controls: PageResponseControls) => Promise<PageResponse>) =>
|
|
1043
|
+
async (c: PageRouteContext<Env>): Promise<PageResponse> => {
|
|
1044
|
+
const controls = new PageResponseControls(c)
|
|
1045
|
+
try {
|
|
1046
|
+
const outcome = await run(c, controls)
|
|
1047
|
+
const answer = dataRedirect(c.req, outcome) ?? outcome
|
|
1048
|
+
return controls.personalized ? (privateOutcome(answer) as PageResponse) : answer
|
|
1049
|
+
} catch (err) {
|
|
1050
|
+
if (!isControlFlow(err)) throw err
|
|
1051
|
+
const answer = dataRedirect(c.req, err)
|
|
1052
|
+
// Returned rather than rethrown: the serving app sends a thrown `Response` exactly as it is,
|
|
1053
|
+
// without the cookies the request queued.
|
|
1054
|
+
if (controls.personalized) return privateOutcome(answer ?? err) as PageResponse
|
|
1055
|
+
if (answer !== undefined) return answer
|
|
1056
|
+
throw err
|
|
1057
|
+
} finally {
|
|
1058
|
+
controls.seal()
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
547
1062
|
return {
|
|
548
1063
|
get: (route) => {
|
|
549
1064
|
const is404Fallback = options.staticFallbacks?.[route.pattern] === "404"
|
|
@@ -568,32 +1083,35 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
568
1083
|
return assemblySlot
|
|
569
1084
|
}
|
|
570
1085
|
|
|
571
|
-
return async (c
|
|
1086
|
+
return withResponseControls(async (c, controls) => {
|
|
572
1087
|
if (is404Fallback && !prerenderedSet.has(urlPartsFor(c.req).pathname)) {
|
|
573
1088
|
return renderNotFound(c.req, c.env, pathOf(c.req))
|
|
574
1089
|
}
|
|
575
1090
|
const mod = await route.load()
|
|
576
1091
|
const draft = await draftFlag(c.req)
|
|
1092
|
+
// Unpublished content is the editor's alone: whatever cache-control a loader sets, no cache keeps it.
|
|
1093
|
+
if (draft) controls.personalize()
|
|
577
1094
|
let data: unknown
|
|
578
1095
|
let layoutData: readonly unknown[] | undefined
|
|
579
1096
|
let boundaryStates: BoundaryStates | undefined
|
|
580
1097
|
let layoutModules: LoadedLayoutModules | undefined
|
|
581
1098
|
let layoutRetained: readonly number[] = []
|
|
1099
|
+
let responseHeaders: Record<string, string> | undefined
|
|
1100
|
+
const requestApi = apiFor(c)
|
|
1101
|
+
let run: LayoutRun | undefined
|
|
582
1102
|
try {
|
|
583
1103
|
const ctx: LoaderContext = {
|
|
584
1104
|
params: c.params,
|
|
585
1105
|
request: c.req,
|
|
586
1106
|
req: c.req,
|
|
587
|
-
api,
|
|
1107
|
+
api: requestApi,
|
|
588
1108
|
env: c.env,
|
|
589
1109
|
draft,
|
|
590
1110
|
search: loaderSearch(mod.searchSchema, c.req),
|
|
1111
|
+
set: controls.scope(PAGE_SCOPE),
|
|
591
1112
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
ctx,
|
|
595
|
-
validatedRetainedIndices(c.req, route, c.params),
|
|
596
|
-
)
|
|
1113
|
+
if (route.middlewareIds !== undefined) await runMiddleware(route, ctx, controls)
|
|
1114
|
+
run = await runLayoutChain(route, ctx, controls, retainContextOf(c.req))
|
|
597
1115
|
layoutModules = run.modules
|
|
598
1116
|
layoutRetained = run.retained
|
|
599
1117
|
const boundaryDefinitions = [
|
|
@@ -608,7 +1126,7 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
608
1126
|
: startDynamicBoundaries(boundaryDefinitions, {
|
|
609
1127
|
request: c.req,
|
|
610
1128
|
params: c.params,
|
|
611
|
-
api,
|
|
1129
|
+
api: requestApi,
|
|
612
1130
|
env: c.env,
|
|
613
1131
|
draft,
|
|
614
1132
|
search: effectiveSearch,
|
|
@@ -616,17 +1134,19 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
616
1134
|
} satisfies BoundaryRequestCtx)
|
|
617
1135
|
const staticBoundaryPromise =
|
|
618
1136
|
boundaryDefinitions.length === 0
|
|
619
|
-
?
|
|
1137
|
+
? undefined
|
|
620
1138
|
: resolveStaticBoundaries(
|
|
621
1139
|
boundaryDefinitions,
|
|
622
|
-
{ phase: "build"
|
|
1140
|
+
{ phase: "build" },
|
|
623
1141
|
options.staticBoundaryCache,
|
|
624
1142
|
)
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
run.pending
|
|
628
|
-
|
|
629
|
-
|
|
1143
|
+
const pageResult = mod.loader ? mod.loader({ ...ctx, search: effectiveSearch }) : null
|
|
1144
|
+
const [pageData, , staticBoundaries] =
|
|
1145
|
+
run.pending === undefined && staticBoundaryPromise === undefined
|
|
1146
|
+
? ([await pageResult, undefined, undefined] as const)
|
|
1147
|
+
: staticBoundaryPromise === undefined
|
|
1148
|
+
? await Promise.all([pageResult, run.pending])
|
|
1149
|
+
: await Promise.all([pageResult, run.pending, staticBoundaryPromise])
|
|
630
1150
|
data = pageData
|
|
631
1151
|
layoutData = run.layoutData
|
|
632
1152
|
if (boundaryBatch !== undefined) {
|
|
@@ -640,24 +1160,36 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
640
1160
|
} else if (staticBoundaries !== undefined) {
|
|
641
1161
|
boundaryStates = staticBoundaries
|
|
642
1162
|
}
|
|
1163
|
+
// Inside the loader `try`: a header the response cannot carry is a loader error, rendered
|
|
1164
|
+
// by the same boundary as one.
|
|
1165
|
+
responseHeaders = controls.documentHeaders()
|
|
643
1166
|
} catch (err) {
|
|
644
|
-
if (isStatusSignal(err))
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
1167
|
+
if (isStatusSignal(err)) {
|
|
1168
|
+
return (
|
|
1169
|
+
(await renderRouteNotFound(
|
|
1170
|
+
c.req,
|
|
1171
|
+
c.env,
|
|
1172
|
+
route,
|
|
1173
|
+
c.params,
|
|
1174
|
+
err,
|
|
1175
|
+
run,
|
|
1176
|
+
controls.personalized,
|
|
1177
|
+
)) ?? renderStatusSignal(c.req, c.env, err, controls.personalized)
|
|
1178
|
+
)
|
|
654
1179
|
}
|
|
655
|
-
return
|
|
1180
|
+
return renderLoaderFailure(route, c.req, c.env, c.params, err, controls.personalized)
|
|
1181
|
+
}
|
|
1182
|
+
if (isStatusSignal(data)) {
|
|
1183
|
+
const nested = await renderRouteNotFound(
|
|
1184
|
+
c.req,
|
|
1185
|
+
c.env,
|
|
656
1186
|
route,
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
1187
|
+
c.params,
|
|
1188
|
+
data,
|
|
1189
|
+
run,
|
|
1190
|
+
controls.personalized,
|
|
660
1191
|
)
|
|
1192
|
+
if (nested !== undefined) return nested
|
|
661
1193
|
}
|
|
662
1194
|
return handleDataOrDocument(
|
|
663
1195
|
c.req,
|
|
@@ -671,91 +1203,169 @@ export function createPageRequestExecutor<Env = unknown>(
|
|
|
671
1203
|
layoutRetained,
|
|
672
1204
|
boundaryStates,
|
|
673
1205
|
layoutModules === undefined ? undefined : assemblyCacheFor(mod, layoutModules),
|
|
1206
|
+
responseHeaders,
|
|
1207
|
+
controls.personalized,
|
|
674
1208
|
)
|
|
675
|
-
}
|
|
1209
|
+
})
|
|
676
1210
|
},
|
|
677
1211
|
|
|
678
|
-
post: (route) =>
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
1212
|
+
post: (route) =>
|
|
1213
|
+
withResponseControls(async (c, controls) => {
|
|
1214
|
+
const mod = await route.load()
|
|
1215
|
+
const draft = await draftFlag(c.req)
|
|
1216
|
+
if (draft) controls.personalize()
|
|
1217
|
+
if (mod.action === undefined) {
|
|
1218
|
+
return new Response("Method Not Allowed", {
|
|
1219
|
+
status: 405,
|
|
1220
|
+
headers: { allow: "GET", "content-type": "text/plain; charset=utf-8" },
|
|
1221
|
+
})
|
|
1222
|
+
}
|
|
1223
|
+
const requestApi = apiFor(c)
|
|
1224
|
+
const actionContext: LoaderContext = {
|
|
1225
|
+
params: c.params,
|
|
1226
|
+
request: c.req,
|
|
1227
|
+
req: c.req,
|
|
1228
|
+
api: requestApi,
|
|
1229
|
+
env: c.env,
|
|
1230
|
+
draft,
|
|
1231
|
+
search: loaderSearch(mod.searchSchema, c.req),
|
|
1232
|
+
set: controls.scope(ACTION_SCOPE),
|
|
1233
|
+
}
|
|
1234
|
+
const isDataRequest = c.req.headers.get(DATA_HEADER) !== null
|
|
1235
|
+
let layoutModules: LoadedLayoutModules
|
|
1236
|
+
let result: unknown
|
|
1237
|
+
try {
|
|
1238
|
+
if (route.middlewareIds !== undefined) await runMiddleware(route, actionContext, controls)
|
|
1239
|
+
layoutModules = await runLayoutGates(route, actionContext, controls)
|
|
1240
|
+
result = await mod.action(actionContext)
|
|
1241
|
+
} catch (err) {
|
|
1242
|
+
if (isControlFlow(err)) return err
|
|
1243
|
+
throw err
|
|
1244
|
+
}
|
|
1245
|
+
const isRevalidate =
|
|
1246
|
+
result !== null && typeof result === "object" && "__nifraRevalidate" in result
|
|
1247
|
+
const actionResult = isRevalidate ? (result as RevalidateResult<unknown>).data : result
|
|
1248
|
+
const revalidateHeader: Record<string, string> = isRevalidate
|
|
1249
|
+
? {
|
|
1250
|
+
[REVALIDATE_HEADER]: (result as RevalidateResult<unknown>).__nifraRevalidate.join(
|
|
1251
|
+
",",
|
|
1252
|
+
),
|
|
1253
|
+
}
|
|
1254
|
+
: {}
|
|
1255
|
+
if (isControlFlow(actionResult)) return actionResult
|
|
1256
|
+
if (isDataRequest) {
|
|
1257
|
+
// Headers shape the document only, but a bad one fails here too, so a navigation and a full
|
|
1258
|
+
// page load agree on whether the action's response is valid.
|
|
1259
|
+
controls.commit()
|
|
1260
|
+
const { forClient, deferred } = prepareDeferred(actionResult)
|
|
1261
|
+
if (deferred.length === 0) {
|
|
1262
|
+
return Response.json(actionResult ?? null, {
|
|
1263
|
+
headers: { ...DATA_RESPONSE_HEADERS, ...revalidateHeader },
|
|
1264
|
+
})
|
|
1265
|
+
}
|
|
1266
|
+
return new Response(ndjsonStream(forClient, deferred), {
|
|
1267
|
+
headers: {
|
|
1268
|
+
"content-type": "application/x-ndjson; charset=utf-8",
|
|
1269
|
+
...DATA_RESPONSE_HEADERS,
|
|
1270
|
+
...revalidateHeader,
|
|
1271
|
+
},
|
|
1272
|
+
})
|
|
1273
|
+
}
|
|
1274
|
+
const search = searchChainOf(layoutModules, mod, c.req)
|
|
1275
|
+
const data = mod.loader
|
|
1276
|
+
? await mod.loader({
|
|
1277
|
+
params: c.params,
|
|
1278
|
+
request: c.req,
|
|
1279
|
+
req: c.req,
|
|
1280
|
+
api: requestApi,
|
|
1281
|
+
env: c.env,
|
|
1282
|
+
draft,
|
|
1283
|
+
search,
|
|
1284
|
+
set: controls.scope(PAGE_SCOPE),
|
|
1285
|
+
})
|
|
1286
|
+
: null
|
|
1287
|
+
const responseHeaders = controls.documentHeaders()
|
|
1288
|
+
const nonce = options.nonce === undefined ? undefined : await resolveNonce(c.req, c.env)
|
|
1289
|
+
const { chain, head } = resolveChainAndHead(
|
|
1290
|
+
layoutModules,
|
|
1291
|
+
mod,
|
|
1292
|
+
{
|
|
1293
|
+
data,
|
|
1294
|
+
params: c.params,
|
|
1295
|
+
origin: originOf(c.req),
|
|
1296
|
+
...(nonce === undefined ? {} : { nonce }),
|
|
1297
|
+
},
|
|
1298
|
+
serverLeafOf(route, mod),
|
|
1299
|
+
)
|
|
1300
|
+
return renderPageResult({
|
|
1301
|
+
adapter,
|
|
1302
|
+
chain,
|
|
1303
|
+
data,
|
|
1304
|
+
actionData: actionResult,
|
|
1305
|
+
head,
|
|
1306
|
+
clientEntry,
|
|
1307
|
+
routeId: route.id,
|
|
1308
|
+
params: c.params,
|
|
1309
|
+
path: pathOf(c.req),
|
|
1310
|
+
search,
|
|
1311
|
+
matchChain: matchChainOf(route.layoutIds, layoutModules, route.id, mod),
|
|
1312
|
+
hydrate: mod.hydrate !== false,
|
|
1313
|
+
...preloadOf(route.id),
|
|
1314
|
+
...stylesOf(route.id),
|
|
1315
|
+
...prerenderedHandover,
|
|
1316
|
+
...(responseHeaders === undefined ? {} : { headers: responseHeaders }),
|
|
1317
|
+
...(nonce === undefined ? {} : { nonce }),
|
|
1318
|
+
...cspOption,
|
|
1319
|
+
...titleOption,
|
|
685
1320
|
})
|
|
1321
|
+
}),
|
|
1322
|
+
|
|
1323
|
+
fallback: withResponseControls(async (c, controls) => {
|
|
1324
|
+
const match = matchNotFoundScope?.(pathOf(c.req)) ?? null
|
|
1325
|
+
const target = match === null ? undefined : notFoundTargets[Number(match.routeId)]
|
|
1326
|
+
if (match === null || target === undefined) {
|
|
1327
|
+
return renderNotFound(c.req, c.env, pathOf(c.req))
|
|
686
1328
|
}
|
|
687
|
-
const
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
req: c.req,
|
|
691
|
-
api,
|
|
692
|
-
env: c.env,
|
|
693
|
-
draft,
|
|
694
|
-
search: loaderSearch(mod.searchSchema, c.req),
|
|
1329
|
+
const params: Record<string, string> = {}
|
|
1330
|
+
for (let i = 0; i < target.names.length; i++) {
|
|
1331
|
+
params[target.names[i] as string] = match.params[`p${i}`] as string
|
|
695
1332
|
}
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
1333
|
+
const mod = await target.page.load()
|
|
1334
|
+
const draft = await draftFlag(c.req)
|
|
1335
|
+
if (draft) controls.personalize()
|
|
1336
|
+
let run: LayoutRun
|
|
699
1337
|
try {
|
|
700
|
-
|
|
701
|
-
|
|
1338
|
+
const ctx: LoaderContext = {
|
|
1339
|
+
params,
|
|
1340
|
+
request: c.req,
|
|
1341
|
+
req: c.req,
|
|
1342
|
+
api: apiFor(c),
|
|
1343
|
+
env: c.env,
|
|
1344
|
+
draft,
|
|
1345
|
+
search: loaderSearch(mod.searchSchema, c.req),
|
|
1346
|
+
set: controls.scope(PAGE_SCOPE),
|
|
1347
|
+
}
|
|
1348
|
+
if (target.route.middlewareIds !== undefined) {
|
|
1349
|
+
await runMiddleware(target.route, ctx, controls)
|
|
1350
|
+
}
|
|
1351
|
+
run = await runLayoutChain(target.route, ctx, controls)
|
|
1352
|
+
await run.pending
|
|
702
1353
|
} catch (err) {
|
|
703
|
-
if (
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
result !== null && typeof result === "object" && "__nifraRevalidate" in result
|
|
708
|
-
const actionResult = isRevalidate ? (result as RevalidateResult<unknown>).data : result
|
|
709
|
-
const revalidateHeader: Record<string, string> = isRevalidate
|
|
710
|
-
? { [REVALIDATE_HEADER]: (result as RevalidateResult<unknown>).__nifraRevalidate.join(",") }
|
|
711
|
-
: {}
|
|
712
|
-
if (isControlFlow(actionResult)) return actionResponse(actionResult, isDataRequest)
|
|
713
|
-
if (isDataRequest) {
|
|
714
|
-
const { forClient, deferred } = prepareDeferred(actionResult)
|
|
715
|
-
if (deferred.length === 0)
|
|
716
|
-
return Response.json(actionResult ?? null, { headers: revalidateHeader })
|
|
717
|
-
return new Response(ndjsonStream(forClient, deferred), {
|
|
718
|
-
headers: { "content-type": "application/x-ndjson; charset=utf-8", ...revalidateHeader },
|
|
719
|
-
})
|
|
1354
|
+
if (isStatusSignal(err)) {
|
|
1355
|
+
return renderStatusSignal(c.req, c.env, err, controls.personalized)
|
|
1356
|
+
}
|
|
1357
|
+
return renderLoaderFailure(target.route, c.req, c.env, params, err, controls.personalized)
|
|
720
1358
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
: null
|
|
733
|
-
const { chain, head } = resolveChainAndHead(layoutModules, mod, {
|
|
734
|
-
data,
|
|
735
|
-
params: c.params,
|
|
736
|
-
origin: originOf(c.req),
|
|
737
|
-
})
|
|
738
|
-
const nonce = options.nonce === undefined ? undefined : await resolveNonce(c.req, c.env)
|
|
739
|
-
return renderPageResult({
|
|
740
|
-
adapter,
|
|
741
|
-
chain,
|
|
742
|
-
data,
|
|
743
|
-
actionData: actionResult,
|
|
744
|
-
head,
|
|
745
|
-
clientEntry,
|
|
746
|
-
routeId: route.id,
|
|
747
|
-
params: c.params,
|
|
748
|
-
path: pathOf(c.req),
|
|
749
|
-
search,
|
|
750
|
-
hydrate: mod.hydrate !== false,
|
|
751
|
-
...preloadOf(route.id),
|
|
752
|
-
...stylesOf(route.id),
|
|
753
|
-
prerenderedPaths: options.prerenderedPaths ?? [],
|
|
754
|
-
...(nonce === undefined ? {} : { nonce }),
|
|
755
|
-
...titleOption,
|
|
756
|
-
})
|
|
757
|
-
},
|
|
758
|
-
|
|
759
|
-
notFound: renderNotFound,
|
|
1359
|
+
return renderNestedNotFound(
|
|
1360
|
+
c.req,
|
|
1361
|
+
c.env,
|
|
1362
|
+
target.id,
|
|
1363
|
+
target.page,
|
|
1364
|
+
params,
|
|
1365
|
+
run.modules,
|
|
1366
|
+
run.layoutData,
|
|
1367
|
+
controls.personalized,
|
|
1368
|
+
)
|
|
1369
|
+
}),
|
|
760
1370
|
}
|
|
761
1371
|
}
|