@nifrajs/web 3.3.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 +35 -6
- package/dist/build-plan.d.ts.map +1 -1
- package/dist/build-plan.js +89 -35
- package/dist/build-plan.js.map +1 -1
- package/dist/build-vite.d.ts +18 -0
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +97 -17
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +72 -23
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +411 -98
- 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 +21 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +226 -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/css-contract.d.ts +21 -0
- package/dist/css-contract.d.ts.map +1 -0
- package/dist/css-contract.js +28 -0
- package/dist/css-contract.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 +15 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -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 +3 -0
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +151 -30
- 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 +69 -0
- package/dist/internal/page-execution.d.ts.map +1 -0
- package/dist/internal/page-execution.js +1021 -0
- package/dist/internal/page-execution.js.map +1 -0
- 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 +37 -21
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +104 -65
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/request-url.d.ts +3 -0
- package/dist/internal/request-url.d.ts.map +1 -0
- package/dist/internal/request-url.js +14 -0
- package/dist/internal/request-url.js.map +1 -0
- 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 +56 -8
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +215 -772
- 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 +301 -39
- package/dist/isr.js.map +1 -1
- package/dist/llms-txt.d.ts +3 -0
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +37 -9
- package/dist/llms-txt.js.map +1 -1
- package/dist/manifest.d.ts +221 -14
- 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 +131 -48
- 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/trusted-html.d.ts +34 -0
- package/dist/trusted-html.d.ts.map +1 -0
- package/dist/trusted-html.js +35 -0
- package/dist/trusted-html.js.map +1 -0
- 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 +113 -36
- package/src/build-vite.ts +124 -15
- package/src/build.ts +535 -102
- package/src/bun-dev-entry.ts +1 -1
- package/src/cdn.ts +216 -0
- package/src/client.ts +251 -18
- package/src/conformance.ts +16 -3
- package/src/csp.ts +194 -0
- package/src/css-contract.ts +44 -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 +57 -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 +180 -29
- 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 +1371 -0
- 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 +153 -73
- package/src/internal/request-url.ts +14 -0
- 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 +290 -879
- package/src/internal/zone-graph.ts +593 -0
- package/src/isr.ts +366 -39
- package/src/llms-txt.ts +44 -8
- package/src/manifest.ts +671 -17
- 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 +135 -49
- 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/trusted-html.ts +44 -0
- 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
|
@@ -0,0 +1,1371 @@
|
|
|
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"
|
|
7
|
+
import {
|
|
8
|
+
type BoundaryRequestCtx,
|
|
9
|
+
type BoundaryStates,
|
|
10
|
+
boundaryDescriptors,
|
|
11
|
+
resolveStaticBoundaries,
|
|
12
|
+
type StaticBoundaryCache,
|
|
13
|
+
startDynamicBoundaries,
|
|
14
|
+
} from "../boundary.ts"
|
|
15
|
+
import type { CspPolicy } from "../csp.ts"
|
|
16
|
+
import { type CssLoadingMode, DEFAULT_CSS_LOADING, normalizeCssLoading } from "../css-contract.ts"
|
|
17
|
+
import { defer, ndjsonStream, prepareDeferred } from "../deferred.ts"
|
|
18
|
+
import { isDraftEnabled } from "../draft.ts"
|
|
19
|
+
import { routeTags } from "../isr.ts"
|
|
20
|
+
import type {
|
|
21
|
+
LayoutEntry,
|
|
22
|
+
LoaderContext,
|
|
23
|
+
Manifest,
|
|
24
|
+
Meta,
|
|
25
|
+
MetaArgs,
|
|
26
|
+
NotFoundEntry,
|
|
27
|
+
RouteEntry,
|
|
28
|
+
RouteModule,
|
|
29
|
+
ShouldRevalidate,
|
|
30
|
+
} from "../manifest.ts"
|
|
31
|
+
import type { NonceResolver } from "../nonce.ts"
|
|
32
|
+
import type { MatchChain, RenderAdapter } from "../render-seam.ts"
|
|
33
|
+
import {
|
|
34
|
+
createMatcher,
|
|
35
|
+
DATA_HEADER,
|
|
36
|
+
NAV_FROM_HEADER,
|
|
37
|
+
REDIRECT_HEADER,
|
|
38
|
+
RETAIN_HEADER,
|
|
39
|
+
REVALIDATE_HEADER,
|
|
40
|
+
STATUS_HEADER,
|
|
41
|
+
} from "../router.ts"
|
|
42
|
+
import { searchOf, searchOfChain } from "../search.ts"
|
|
43
|
+
import { mergeHeads, resolveMeta } from "./head-merge.ts"
|
|
44
|
+
import {
|
|
45
|
+
EMPTY_RETAIN,
|
|
46
|
+
type HeadersLike,
|
|
47
|
+
isControlFlow,
|
|
48
|
+
isResponseResult,
|
|
49
|
+
isStatusSignal,
|
|
50
|
+
type LoadedLayoutModules,
|
|
51
|
+
layoutErrorId,
|
|
52
|
+
type RenderAssemblyCache,
|
|
53
|
+
type RenderedPage,
|
|
54
|
+
type RevalidateResult,
|
|
55
|
+
renderPageResult,
|
|
56
|
+
STATUS_SIGNAL,
|
|
57
|
+
STATUS_TEXT,
|
|
58
|
+
type StatusSignal,
|
|
59
|
+
scopeParams,
|
|
60
|
+
tagLayoutError,
|
|
61
|
+
withDuplicateInstanceHint,
|
|
62
|
+
} from "./render-document.ts"
|
|
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"
|
|
76
|
+
|
|
77
|
+
export type { NonceResolver } from "../nonce.ts"
|
|
78
|
+
|
|
79
|
+
/** Structural context supplied by the core server to one page route handler. */
|
|
80
|
+
export interface PageRouteContext<Env = unknown> {
|
|
81
|
+
readonly params: Record<string, string>
|
|
82
|
+
readonly req: Request
|
|
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
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface PageExecutionOptions<Env = unknown> {
|
|
175
|
+
readonly adapter: RenderAdapter
|
|
176
|
+
readonly manifest: Manifest
|
|
177
|
+
readonly clientEntry: string
|
|
178
|
+
readonly title?: string
|
|
179
|
+
readonly api?: unknown
|
|
180
|
+
readonly draftSecret?: string
|
|
181
|
+
readonly routePreload?: Readonly<Record<string, readonly string[]>>
|
|
182
|
+
readonly styles?: readonly string[]
|
|
183
|
+
readonly routeStyles?: Readonly<Record<string, readonly string[]>>
|
|
184
|
+
readonly cssLoading?: CssLoadingMode
|
|
185
|
+
readonly prerenderedPaths?: readonly string[]
|
|
186
|
+
/** Where pages fetch {@link prerenderedPaths} from when the list is too large to inline. */
|
|
187
|
+
readonly prerenderedListUrl?: string
|
|
188
|
+
readonly staticFallbacks?: Readonly<Record<string, "ssr" | "404">>
|
|
189
|
+
readonly staticBoundaryCache?: StaticBoundaryCache
|
|
190
|
+
/** Per-request CSP nonce for framework-owned executable document scripts. */
|
|
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 }
|
|
196
|
+
readonly onLoaderError?: (
|
|
197
|
+
error: unknown,
|
|
198
|
+
ctx: {
|
|
199
|
+
readonly request: Request
|
|
200
|
+
readonly params: Readonly<Record<string, string>>
|
|
201
|
+
readonly route: string
|
|
202
|
+
},
|
|
203
|
+
) => void
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
type PageResponse = Response | ResponseResult | RenderedPage
|
|
207
|
+
|
|
208
|
+
export interface PageRequestExecutor<Env = unknown> {
|
|
209
|
+
get(route: RouteEntry): (ctx: PageRouteContext<Env>) => Promise<PageResponse>
|
|
210
|
+
post(route: RouteEntry): (ctx: PageRouteContext<Env>) => Promise<PageResponse>
|
|
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
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Own the page request program behind one private seam.
|
|
243
|
+
*
|
|
244
|
+
* `createWebApp` remains responsible for app construction, mount ordering, and route registration.
|
|
245
|
+
* This module owns the page-specific contract: context, layout execution, control-flow precedence,
|
|
246
|
+
* data/document assembly, and error/status rendering. GET and POST retain their distinct ordering
|
|
247
|
+
* (ordinary layout data may run after an action; a gate must run before it) while sharing the same
|
|
248
|
+
* outcome and render decisions.
|
|
249
|
+
*/
|
|
250
|
+
export function createPageRequestExecutor<Env = unknown>(
|
|
251
|
+
options: PageExecutionOptions<Env>,
|
|
252
|
+
): PageRequestExecutor<Env> {
|
|
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)
|
|
260
|
+
const cssLoading = normalizeCssLoading(options.cssLoading ?? DEFAULT_CSS_LOADING)
|
|
261
|
+
const titleOption = options.title === undefined ? {} : { title: options.title }
|
|
262
|
+
const cspOption = options.csp === undefined ? {} : { csp: options.csp }
|
|
263
|
+
const prerenderedSet = new Set(options.prerenderedPaths ?? [])
|
|
264
|
+
const prerenderedHandover =
|
|
265
|
+
options.prerenderedListUrl === undefined
|
|
266
|
+
? { prerenderedPaths: options.prerenderedPaths ?? [] }
|
|
267
|
+
: { prerenderedListUrl: options.prerenderedListUrl }
|
|
268
|
+
const matchManifestRoute = createMatcher(
|
|
269
|
+
manifest.routes.map((route) => ({ routeId: route.id, pattern: route.pattern })),
|
|
270
|
+
)
|
|
271
|
+
const routeById = new Map(manifest.routes.map((route) => [route.id, route]))
|
|
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
|
+
|
|
314
|
+
const draftFlag = (req: Request): Promise<boolean> =>
|
|
315
|
+
options.draftSecret === undefined
|
|
316
|
+
? Promise.resolve(false)
|
|
317
|
+
: isDraftEnabled(req, options.draftSecret)
|
|
318
|
+
|
|
319
|
+
const preloadOf = (id: string): { preload?: readonly string[] } => {
|
|
320
|
+
const chunks = options.routePreload?.[id]
|
|
321
|
+
return chunks ? { preload: chunks } : {}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const resolveNonce = (
|
|
325
|
+
request: Request,
|
|
326
|
+
env: Env,
|
|
327
|
+
): string | undefined | Promise<string | undefined> =>
|
|
328
|
+
options.nonce === undefined ? undefined : options.nonce({ request, env })
|
|
329
|
+
|
|
330
|
+
const stylesOf = (id: string): { styles?: readonly string[]; cssLoading?: CssLoadingMode } => {
|
|
331
|
+
const perRoute = options.routeStyles?.[id]
|
|
332
|
+
const styles = perRoute ?? options.styles
|
|
333
|
+
if (styles === undefined || styles.length === 0) return {}
|
|
334
|
+
return {
|
|
335
|
+
styles,
|
|
336
|
+
...(cssLoading === "deferred" ? { cssLoading } : {}),
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Load a route's layout modules once for the current execution. The render path needs each whole
|
|
341
|
+
// module for its component + meta, while the data path also needs loader/gate/boundary declarations.
|
|
342
|
+
const loadLayoutModules = async (route: RouteEntry): Promise<LoadedLayoutModules> => {
|
|
343
|
+
const modules = await Promise.all(
|
|
344
|
+
route.layoutIds.map((id) => (manifest.layouts[id] as LayoutEntry).load()),
|
|
345
|
+
)
|
|
346
|
+
for (let i = 0; i < modules.length; i++) {
|
|
347
|
+
if ((modules[i] as { action?: unknown }).action === undefined) continue
|
|
348
|
+
const id = route.layoutIds[i] as string
|
|
349
|
+
throw new Error(
|
|
350
|
+
`[nifra/web] "${(manifest.layouts[id] as LayoutEntry).file}" exports an \`action\`, but layouts do not run one - only route files do. Move the mutation into the route that submits to it.`,
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
return modules
|
|
354
|
+
}
|
|
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
|
+
|
|
364
|
+
/** Run layout loaders, awaiting gates before starting the page loader and retaining only safe data. */
|
|
365
|
+
const runLayoutChain = async (
|
|
366
|
+
route: RouteEntry,
|
|
367
|
+
ctx: LoaderContext,
|
|
368
|
+
controls: PageResponseControls,
|
|
369
|
+
retain?: RetainContext,
|
|
370
|
+
): Promise<LayoutRun> => {
|
|
371
|
+
if (route.layoutIds.length === 0) return emptyLayoutRun
|
|
372
|
+
const modules = await loadLayoutModules(route)
|
|
373
|
+
if (!modules.some((m) => m.loader !== undefined)) {
|
|
374
|
+
return { modules, layoutData: undefined, retained: [], pending: undefined }
|
|
375
|
+
}
|
|
376
|
+
const results: unknown[] = new Array(modules.length).fill(null)
|
|
377
|
+
const retained: number[] = []
|
|
378
|
+
const pending: Array<Promise<void>> = []
|
|
379
|
+
for (let i = 0; i < modules.length; i++) {
|
|
380
|
+
const layout = modules[i] as LoadedLayoutModules[number]
|
|
381
|
+
const loader = layout.loader
|
|
382
|
+
if (loader === undefined) continue
|
|
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
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const scoped: LoaderContext = {
|
|
396
|
+
...ctx,
|
|
397
|
+
params: scopeParams(ctx.params, route.layoutParams?.[i]),
|
|
398
|
+
set: controls.scope(i),
|
|
399
|
+
}
|
|
400
|
+
if (modules[i]?.gate === true) {
|
|
401
|
+
try {
|
|
402
|
+
results[i] = await loader(scoped)
|
|
403
|
+
} catch (err) {
|
|
404
|
+
throw tagLayoutError(err, route.layoutIds[i] as string)
|
|
405
|
+
}
|
|
406
|
+
continue
|
|
407
|
+
}
|
|
408
|
+
const layoutId = route.layoutIds[i] as string
|
|
409
|
+
const index = i
|
|
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),
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** Run only authorization gates before an action; ordinary layout data runs after a native action. */
|
|
440
|
+
const runLayoutGates = async (
|
|
441
|
+
route: RouteEntry,
|
|
442
|
+
ctx: LoaderContext,
|
|
443
|
+
controls: PageResponseControls,
|
|
444
|
+
): Promise<LoadedLayoutModules> => {
|
|
445
|
+
const modules = await loadLayoutModules(route)
|
|
446
|
+
for (let i = 0; i < modules.length; i++) {
|
|
447
|
+
const mod = modules[i]
|
|
448
|
+
if (mod?.gate !== true || mod.loader === undefined) continue
|
|
449
|
+
try {
|
|
450
|
+
await mod.loader({
|
|
451
|
+
...ctx,
|
|
452
|
+
params: scopeParams(ctx.params, route.layoutParams?.[i]),
|
|
453
|
+
set: controls.scope(i),
|
|
454
|
+
})
|
|
455
|
+
} catch (err) {
|
|
456
|
+
throw tagLayoutError(err, route.layoutIds[i] as string)
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return modules
|
|
460
|
+
}
|
|
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
|
+
|
|
503
|
+
const retainedIndices = (header: string | null): ReadonlySet<number> => {
|
|
504
|
+
if (header === null || header === "") return EMPTY_RETAIN
|
|
505
|
+
const out = new Set<number>()
|
|
506
|
+
for (const part of header.split(",")) {
|
|
507
|
+
if (!/^\d+$/.test(part)) continue
|
|
508
|
+
out.add(Number(part))
|
|
509
|
+
}
|
|
510
|
+
return out
|
|
511
|
+
}
|
|
512
|
+
|
|
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
|
|
519
|
+
const requested = retainedIndices(req.headers.get(RETAIN_HEADER))
|
|
520
|
+
const from = req.headers.get(NAV_FROM_HEADER)
|
|
521
|
+
if (requested.size === 0 || from === null) return undefined
|
|
522
|
+
let toUrl: URL
|
|
523
|
+
let fromUrl: URL
|
|
524
|
+
try {
|
|
525
|
+
toUrl = new URL(req.url)
|
|
526
|
+
fromUrl = new URL(from, toUrl)
|
|
527
|
+
} catch {
|
|
528
|
+
return undefined
|
|
529
|
+
}
|
|
530
|
+
if (fromUrl.origin !== toUrl.origin) return undefined
|
|
531
|
+
const previousMatch = matchManifestRoute(fromUrl.pathname + fromUrl.search)
|
|
532
|
+
if (previousMatch === null) return undefined
|
|
533
|
+
const previous = routeById.get(previousMatch.routeId)
|
|
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
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const loaderSearch = (searchSchema: RouteModule["searchSchema"], request: Request) =>
|
|
571
|
+
searchOf(searchSchema, urlPartsFor(request).search)
|
|
572
|
+
|
|
573
|
+
const searchChainOf = (
|
|
574
|
+
layoutMods: LoadedLayoutModules,
|
|
575
|
+
mod: RouteModule,
|
|
576
|
+
request: Request,
|
|
577
|
+
): Record<string, unknown> =>
|
|
578
|
+
searchOfChain(
|
|
579
|
+
[...layoutMods.map((m) => m.searchSchema), mod.searchSchema],
|
|
580
|
+
urlPartsFor(request).search,
|
|
581
|
+
)
|
|
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
|
+
|
|
595
|
+
const resolveChainAndHead = (
|
|
596
|
+
layoutModules: LoadedLayoutModules,
|
|
597
|
+
page: RouteModule,
|
|
598
|
+
metaArgs: MetaArgs,
|
|
599
|
+
leaf: unknown = page.default,
|
|
600
|
+
): { chain: unknown[]; head: Meta } => {
|
|
601
|
+
const chain = [...layoutModules.map((m) => m.default), leaf]
|
|
602
|
+
const heads = [
|
|
603
|
+
...layoutModules.map((m) => resolveMeta(m.meta, metaArgs)),
|
|
604
|
+
resolveMeta(page.meta, metaArgs),
|
|
605
|
+
]
|
|
606
|
+
return { chain, head: mergeHeads(heads) }
|
|
607
|
+
}
|
|
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
|
+
|
|
620
|
+
const dirOfId = (id: string, suffix: string): string =>
|
|
621
|
+
id === suffix ? "" : id.slice(0, id.length - suffix.length - 1)
|
|
622
|
+
|
|
623
|
+
const boundaryFor = (route: RouteEntry, err: unknown): string | undefined => {
|
|
624
|
+
const ids = route.errorIds ?? []
|
|
625
|
+
const failing = layoutErrorId(err)
|
|
626
|
+
if (failing === undefined) return ids.at(-1)
|
|
627
|
+
const layoutDir = dirOfId(failing, "_layout")
|
|
628
|
+
for (let i = ids.length - 1; i >= 0; i--) {
|
|
629
|
+
const errDir = dirOfId(ids[i] as string, "_error")
|
|
630
|
+
if (errDir === "" || errDir === layoutDir || layoutDir.startsWith(`${errDir}/`)) return ids[i]
|
|
631
|
+
}
|
|
632
|
+
return undefined
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const originOf = (req: Request): string => {
|
|
636
|
+
try {
|
|
637
|
+
return new URL(req.url).origin
|
|
638
|
+
} catch {
|
|
639
|
+
return ""
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const pathOf = (req: Request): string => {
|
|
644
|
+
try {
|
|
645
|
+
const parts = urlPartsFor(req)
|
|
646
|
+
return parts.pathname + parts.search
|
|
647
|
+
} catch {
|
|
648
|
+
return "/"
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const renderError = async (
|
|
653
|
+
route: RouteEntry,
|
|
654
|
+
errorId: string,
|
|
655
|
+
err: unknown,
|
|
656
|
+
personalized: boolean,
|
|
657
|
+
nonce?: string,
|
|
658
|
+
): Promise<PageResponse> => {
|
|
659
|
+
const errDir = dirOfId(errorId, "_error")
|
|
660
|
+
const keptLayoutIds = route.layoutIds.filter((id) => {
|
|
661
|
+
const ld = dirOfId(id, "_layout")
|
|
662
|
+
return ld === "" || ld === errDir || errDir.startsWith(`${ld}/`)
|
|
663
|
+
})
|
|
664
|
+
const layouts = await Promise.all(
|
|
665
|
+
keptLayoutIds.map((id) =>
|
|
666
|
+
(manifest.layouts[id] as LayoutEntry).load().then((m) => m.default),
|
|
667
|
+
),
|
|
668
|
+
)
|
|
669
|
+
const { default: errComp } = await (manifest.errors?.[errorId] as LayoutEntry).load()
|
|
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"
|
|
673
|
+
return renderPageResult({
|
|
674
|
+
adapter,
|
|
675
|
+
chain: [...layouts, errComp],
|
|
676
|
+
data: production
|
|
677
|
+
? { name: "Error", message: "Internal Server Error" }
|
|
678
|
+
: { name: e.name, message: e.message },
|
|
679
|
+
clientEntry,
|
|
680
|
+
routeId: errorId,
|
|
681
|
+
status: 500,
|
|
682
|
+
hydrate: false,
|
|
683
|
+
...(personalized ? { headers: PRIVATE_PAGE_HEADERS } : {}),
|
|
684
|
+
...(nonce === undefined ? {} : { nonce }),
|
|
685
|
+
...cspOption,
|
|
686
|
+
...titleOption,
|
|
687
|
+
})
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const renderTerminalStatus = async (
|
|
691
|
+
status: number,
|
|
692
|
+
path?: string,
|
|
693
|
+
extraHeaders?: HeadersLike,
|
|
694
|
+
nonce?: string,
|
|
695
|
+
): Promise<PageResponse> => {
|
|
696
|
+
const page = manifest.statusPages?.[String(status)] ?? manifest.notFound
|
|
697
|
+
if (page === undefined) {
|
|
698
|
+
const headers = new Headers(extraHeaders)
|
|
699
|
+
headers.set("content-type", "text/plain; charset=utf-8")
|
|
700
|
+
return new Response(STATUS_TEXT[status] ?? "Error", { status, headers })
|
|
701
|
+
}
|
|
702
|
+
const routeId = manifest.statusPages?.[String(status)] !== undefined ? `_${status}` : "_404"
|
|
703
|
+
const loaded = (await page.load()) as {
|
|
704
|
+
readonly default: unknown
|
|
705
|
+
readonly searchSchema?: RouteModule["searchSchema"]
|
|
706
|
+
readonly handle?: unknown
|
|
707
|
+
}
|
|
708
|
+
return renderPageResult({
|
|
709
|
+
adapter,
|
|
710
|
+
chain: [loaded.default],
|
|
711
|
+
data: null,
|
|
712
|
+
clientEntry,
|
|
713
|
+
routeId,
|
|
714
|
+
matchChain: { ids: [routeId], handles: [loaded.handle] },
|
|
715
|
+
...(path !== undefined
|
|
716
|
+
? {
|
|
717
|
+
search: searchOf(
|
|
718
|
+
loaded.searchSchema,
|
|
719
|
+
path.includes("?") ? path.slice(path.indexOf("?")) : "",
|
|
720
|
+
),
|
|
721
|
+
}
|
|
722
|
+
: {}),
|
|
723
|
+
...(path !== undefined ? { path } : {}),
|
|
724
|
+
status,
|
|
725
|
+
...(extraHeaders !== undefined ? { headers: extraHeaders } : {}),
|
|
726
|
+
...(nonce === undefined ? {} : { nonce }),
|
|
727
|
+
...cspOption,
|
|
728
|
+
...preloadOf(routeId),
|
|
729
|
+
...stylesOf(routeId),
|
|
730
|
+
...prerenderedHandover,
|
|
731
|
+
...titleOption,
|
|
732
|
+
})
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const renderStatusSignal = async (
|
|
736
|
+
req: Request,
|
|
737
|
+
env: Env,
|
|
738
|
+
signal: StatusSignal,
|
|
739
|
+
personalized: boolean,
|
|
740
|
+
): Promise<PageResponse> => {
|
|
741
|
+
const { status, headers } = signal[STATUS_SIGNAL]
|
|
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.
|
|
745
|
+
const responseHeaders = new Headers(headers)
|
|
746
|
+
responseHeaders.set(STATUS_HEADER, String(status))
|
|
747
|
+
responseHeaders.set("cache-control", PRIVATE_NO_STORE)
|
|
748
|
+
responseHeaders.set("vary", varyOnDataHeader(responseHeaders.get("vary")))
|
|
749
|
+
return new Response(null, { status, headers: responseHeaders })
|
|
750
|
+
}
|
|
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)
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const reportLoaderError = (
|
|
765
|
+
route: RouteEntry,
|
|
766
|
+
req: Request,
|
|
767
|
+
params: Record<string, string>,
|
|
768
|
+
err: unknown,
|
|
769
|
+
): void => {
|
|
770
|
+
if (options.onLoaderError === undefined) return
|
|
771
|
+
try {
|
|
772
|
+
options.onLoaderError(err, { request: req, params, route: route.pattern })
|
|
773
|
+
} catch {
|
|
774
|
+
// A faulty reporter must never break error rendering.
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const handleDataOrDocument = async (
|
|
779
|
+
req: Request,
|
|
780
|
+
env: Env,
|
|
781
|
+
route: RouteEntry,
|
|
782
|
+
params: Readonly<Record<string, string>>,
|
|
783
|
+
mod: RouteModule,
|
|
784
|
+
data: unknown,
|
|
785
|
+
layoutData: readonly unknown[] | undefined,
|
|
786
|
+
layoutModules: LoadedLayoutModules,
|
|
787
|
+
layoutRetained: readonly number[],
|
|
788
|
+
boundaryStates: BoundaryStates | undefined,
|
|
789
|
+
assemblyCache: RenderAssemblyCache | undefined,
|
|
790
|
+
responseHeaders: Record<string, string> | undefined,
|
|
791
|
+
personalized: boolean,
|
|
792
|
+
): Promise<PageResponse> => {
|
|
793
|
+
if (isStatusSignal(data)) return renderStatusSignal(req, env, data, personalized)
|
|
794
|
+
if (isControlFlow(data)) return data
|
|
795
|
+
if (req.headers.get(DATA_HEADER) !== null) {
|
|
796
|
+
const pageSplit = prepareDeferred(data)
|
|
797
|
+
const layoutSplits: Array<ReturnType<typeof prepareDeferred>> = []
|
|
798
|
+
let offset = pageSplit.deferred.length
|
|
799
|
+
for (const entry of layoutData ?? []) {
|
|
800
|
+
const split = prepareDeferred(entry, offset)
|
|
801
|
+
layoutSplits.push(split)
|
|
802
|
+
offset += split.deferred.length
|
|
803
|
+
}
|
|
804
|
+
const deferred = [...pageSplit.deferred, ...layoutSplits.flatMap((split) => split.deferred)]
|
|
805
|
+
const boundarySplit =
|
|
806
|
+
boundaryStates === undefined ? undefined : prepareDeferred(boundaryStates, deferred.length)
|
|
807
|
+
const allDeferred = [
|
|
808
|
+
...deferred,
|
|
809
|
+
...(boundarySplit === undefined ? [] : boundarySplit.deferred),
|
|
810
|
+
]
|
|
811
|
+
const payload =
|
|
812
|
+
layoutData === undefined && boundarySplit === undefined
|
|
813
|
+
? pageSplit.forClient
|
|
814
|
+
: {
|
|
815
|
+
v: 1 as const,
|
|
816
|
+
data: pageSplit.forClient,
|
|
817
|
+
layoutData: layoutSplits.map((split) => split.forClient),
|
|
818
|
+
retained: layoutRetained,
|
|
819
|
+
...(boundarySplit === undefined ? {} : { boundaries: boundarySplit.forClient }),
|
|
820
|
+
}
|
|
821
|
+
if (allDeferred.length === 0) {
|
|
822
|
+
return Response.json(payload ?? null, { headers: DATA_RESPONSE_HEADERS })
|
|
823
|
+
}
|
|
824
|
+
return new Response(ndjsonStream(payload, allDeferred), {
|
|
825
|
+
headers: {
|
|
826
|
+
"content-type": "application/x-ndjson; charset=utf-8",
|
|
827
|
+
...DATA_RESPONSE_HEADERS,
|
|
828
|
+
},
|
|
829
|
+
})
|
|
830
|
+
}
|
|
831
|
+
|
|
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
|
+
)
|
|
844
|
+
try {
|
|
845
|
+
return await renderPageResult({
|
|
846
|
+
adapter,
|
|
847
|
+
chain,
|
|
848
|
+
data,
|
|
849
|
+
head,
|
|
850
|
+
clientEntry,
|
|
851
|
+
routeId: route.id,
|
|
852
|
+
params,
|
|
853
|
+
path: pathOf(req),
|
|
854
|
+
search: searchChainOf(layoutModules, mod, req),
|
|
855
|
+
matchChain: matchChainOf(route.layoutIds, layoutModules, route.id, mod),
|
|
856
|
+
hydrate: mod.hydrate !== false,
|
|
857
|
+
...(layoutData !== undefined ? { layoutData } : {}),
|
|
858
|
+
...(boundaryStates !== undefined ? { boundaries: boundaryStates } : {}),
|
|
859
|
+
...preloadOf(route.id),
|
|
860
|
+
...stylesOf(route.id),
|
|
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
|
+
: {}),
|
|
878
|
+
...(mod.islandScripts !== undefined ? { islandScripts: mod.islandScripts } : {}),
|
|
879
|
+
...(nonce === undefined ? {} : { nonce }),
|
|
880
|
+
...cspOption,
|
|
881
|
+
...titleOption,
|
|
882
|
+
...(assemblyCache === undefined ? {} : { assemblyCache }),
|
|
883
|
+
})
|
|
884
|
+
} catch (err) {
|
|
885
|
+
if (isStatusSignal(err)) return renderStatusSignal(req, env, err, personalized)
|
|
886
|
+
if (isControlFlow(err)) throw err
|
|
887
|
+
reportLoaderError(route, req, { ...params }, err)
|
|
888
|
+
const errorId = boundaryFor(route, err)
|
|
889
|
+
if (errorId === undefined) throw err
|
|
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
|
+
})
|
|
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
|
+
)
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const renderNotFound = async (
|
|
1021
|
+
request: Request,
|
|
1022
|
+
env: Env,
|
|
1023
|
+
path?: string,
|
|
1024
|
+
): Promise<PageResponse> => {
|
|
1025
|
+
const page = manifest.statusPages?.["404"] ?? manifest.notFound
|
|
1026
|
+
if (page === undefined) return renderTerminalStatus(404, path)
|
|
1027
|
+
return renderTerminalStatus(
|
|
1028
|
+
404,
|
|
1029
|
+
path,
|
|
1030
|
+
undefined,
|
|
1031
|
+
options.nonce === undefined ? undefined : await resolveNonce(request, env),
|
|
1032
|
+
)
|
|
1033
|
+
}
|
|
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
|
+
|
|
1062
|
+
return {
|
|
1063
|
+
get: (route) => {
|
|
1064
|
+
const is404Fallback = options.staticFallbacks?.[route.pattern] === "404"
|
|
1065
|
+
let assemblySlot: RenderAssemblyCache | undefined
|
|
1066
|
+
let assemblySlotMod: unknown
|
|
1067
|
+
let assemblySlotLayouts: LoadedLayoutModules | undefined
|
|
1068
|
+
const assemblyCacheFor = (
|
|
1069
|
+
mod: RouteModule,
|
|
1070
|
+
layouts: LoadedLayoutModules,
|
|
1071
|
+
): RenderAssemblyCache | undefined => {
|
|
1072
|
+
if (typeof mod.meta === "function") return undefined
|
|
1073
|
+
for (const layout of layouts) if (typeof layout.meta === "function") return undefined
|
|
1074
|
+
const layoutsUnchanged =
|
|
1075
|
+
assemblySlotLayouts !== undefined &&
|
|
1076
|
+
assemblySlotLayouts.length === layouts.length &&
|
|
1077
|
+
assemblySlotLayouts.every((layout, index) => layout === layouts[index])
|
|
1078
|
+
if (assemblySlotMod !== mod || !layoutsUnchanged) {
|
|
1079
|
+
assemblySlot = {}
|
|
1080
|
+
assemblySlotMod = mod
|
|
1081
|
+
assemblySlotLayouts = layouts
|
|
1082
|
+
}
|
|
1083
|
+
return assemblySlot
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
return withResponseControls(async (c, controls) => {
|
|
1087
|
+
if (is404Fallback && !prerenderedSet.has(urlPartsFor(c.req).pathname)) {
|
|
1088
|
+
return renderNotFound(c.req, c.env, pathOf(c.req))
|
|
1089
|
+
}
|
|
1090
|
+
const mod = await route.load()
|
|
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()
|
|
1094
|
+
let data: unknown
|
|
1095
|
+
let layoutData: readonly unknown[] | undefined
|
|
1096
|
+
let boundaryStates: BoundaryStates | undefined
|
|
1097
|
+
let layoutModules: LoadedLayoutModules | undefined
|
|
1098
|
+
let layoutRetained: readonly number[] = []
|
|
1099
|
+
let responseHeaders: Record<string, string> | undefined
|
|
1100
|
+
const requestApi = apiFor(c)
|
|
1101
|
+
let run: LayoutRun | undefined
|
|
1102
|
+
try {
|
|
1103
|
+
const ctx: LoaderContext = {
|
|
1104
|
+
params: c.params,
|
|
1105
|
+
request: c.req,
|
|
1106
|
+
req: c.req,
|
|
1107
|
+
api: requestApi,
|
|
1108
|
+
env: c.env,
|
|
1109
|
+
draft,
|
|
1110
|
+
search: loaderSearch(mod.searchSchema, c.req),
|
|
1111
|
+
set: controls.scope(PAGE_SCOPE),
|
|
1112
|
+
}
|
|
1113
|
+
if (route.middlewareIds !== undefined) await runMiddleware(route, ctx, controls)
|
|
1114
|
+
run = await runLayoutChain(route, ctx, controls, retainContextOf(c.req))
|
|
1115
|
+
layoutModules = run.modules
|
|
1116
|
+
layoutRetained = run.retained
|
|
1117
|
+
const boundaryDefinitions = [
|
|
1118
|
+
...run.modules.flatMap((layout) => layout.boundaries ?? []),
|
|
1119
|
+
...(mod.boundaries ?? []),
|
|
1120
|
+
]
|
|
1121
|
+
if (boundaryDefinitions.length > 0) boundaryDescriptors(boundaryDefinitions)
|
|
1122
|
+
const effectiveSearch = searchChainOf(run.modules, mod, c.req)
|
|
1123
|
+
const boundaryBatch =
|
|
1124
|
+
boundaryDefinitions.length === 0
|
|
1125
|
+
? undefined
|
|
1126
|
+
: startDynamicBoundaries(boundaryDefinitions, {
|
|
1127
|
+
request: c.req,
|
|
1128
|
+
params: c.params,
|
|
1129
|
+
api: requestApi,
|
|
1130
|
+
env: c.env,
|
|
1131
|
+
draft,
|
|
1132
|
+
search: effectiveSearch,
|
|
1133
|
+
signal: c.req.signal,
|
|
1134
|
+
} satisfies BoundaryRequestCtx)
|
|
1135
|
+
const staticBoundaryPromise =
|
|
1136
|
+
boundaryDefinitions.length === 0
|
|
1137
|
+
? undefined
|
|
1138
|
+
: resolveStaticBoundaries(
|
|
1139
|
+
boundaryDefinitions,
|
|
1140
|
+
{ phase: "build" },
|
|
1141
|
+
options.staticBoundaryCache,
|
|
1142
|
+
)
|
|
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])
|
|
1150
|
+
data = pageData
|
|
1151
|
+
layoutData = run.layoutData
|
|
1152
|
+
if (boundaryBatch !== undefined) {
|
|
1153
|
+
const streamed = { ...boundaryBatch.initial, ...(staticBoundaries ?? {}) }
|
|
1154
|
+
for (const entry of boundaryBatch.pending) {
|
|
1155
|
+
const initial = streamed[entry.name]
|
|
1156
|
+
if (initial !== undefined)
|
|
1157
|
+
streamed[entry.name] = { ...initial, data: defer(entry.promise) }
|
|
1158
|
+
}
|
|
1159
|
+
boundaryStates = streamed
|
|
1160
|
+
} else if (staticBoundaries !== undefined) {
|
|
1161
|
+
boundaryStates = staticBoundaries
|
|
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()
|
|
1166
|
+
} catch (err) {
|
|
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
|
+
)
|
|
1179
|
+
}
|
|
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,
|
|
1186
|
+
route,
|
|
1187
|
+
c.params,
|
|
1188
|
+
data,
|
|
1189
|
+
run,
|
|
1190
|
+
controls.personalized,
|
|
1191
|
+
)
|
|
1192
|
+
if (nested !== undefined) return nested
|
|
1193
|
+
}
|
|
1194
|
+
return handleDataOrDocument(
|
|
1195
|
+
c.req,
|
|
1196
|
+
c.env,
|
|
1197
|
+
route,
|
|
1198
|
+
c.params,
|
|
1199
|
+
mod,
|
|
1200
|
+
data,
|
|
1201
|
+
layoutData,
|
|
1202
|
+
layoutModules ?? (await loadLayoutModules(route)),
|
|
1203
|
+
layoutRetained,
|
|
1204
|
+
boundaryStates,
|
|
1205
|
+
layoutModules === undefined ? undefined : assemblyCacheFor(mod, layoutModules),
|
|
1206
|
+
responseHeaders,
|
|
1207
|
+
controls.personalized,
|
|
1208
|
+
)
|
|
1209
|
+
})
|
|
1210
|
+
},
|
|
1211
|
+
|
|
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,
|
|
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))
|
|
1328
|
+
}
|
|
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
|
|
1332
|
+
}
|
|
1333
|
+
const mod = await target.page.load()
|
|
1334
|
+
const draft = await draftFlag(c.req)
|
|
1335
|
+
if (draft) controls.personalize()
|
|
1336
|
+
let run: LayoutRun
|
|
1337
|
+
try {
|
|
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
|
|
1353
|
+
} catch (err) {
|
|
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)
|
|
1358
|
+
}
|
|
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
|
+
}),
|
|
1370
|
+
}
|
|
1371
|
+
}
|