@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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { isSameOriginPath, type ResponseResult, status as statusResult } from "@nifrajs/core/server"
|
|
2
2
|
import type { BoundaryRegistration, BoundaryStates } from "../boundary.ts"
|
|
3
|
+
import { type CspPolicy, cspDocumentNonce, cspHeaderValue, prepareCspPolicy } from "../csp.ts"
|
|
4
|
+
import { type CssLoadingMode, DEFAULT_CSS_LOADING, normalizeCssLoading } from "../css-contract.ts"
|
|
3
5
|
import { DEFERRED_ERROR_CODE, DEFERRED_RUNTIME, prepareDeferred } from "../deferred.ts"
|
|
4
6
|
import { ISR_REVALIDATE_HEADER, ISR_REVALIDATE_TAGS_HEADER, serializeISRTags } from "../isr.ts"
|
|
5
7
|
import type {
|
|
@@ -12,16 +14,17 @@ import type {
|
|
|
12
14
|
ScriptDescriptor,
|
|
13
15
|
UnsafeScriptDescriptor,
|
|
14
16
|
} from "../manifest.ts"
|
|
15
|
-
import type { RenderAdapter, RenderProps } from "../render-seam.ts"
|
|
17
|
+
import type { MatchChain, RenderAdapter, RenderProps } from "../render-seam.ts"
|
|
16
18
|
import {
|
|
17
19
|
ACTION_GLOBAL,
|
|
18
20
|
BOUNDARY_GLOBAL,
|
|
19
21
|
DATA_GLOBAL,
|
|
22
|
+
HANDOVER_ID,
|
|
20
23
|
LAYOUT_DATA_GLOBAL,
|
|
21
24
|
ROOT_ATTRIBUTE,
|
|
22
25
|
ROUTE_GLOBAL,
|
|
23
26
|
} from "../render-seam.ts"
|
|
24
|
-
import { PRERENDERED_GLOBAL
|
|
27
|
+
import { PRERENDERED_GLOBAL } from "../router.ts"
|
|
25
28
|
import { trustedHeadAttributes } from "./head-attributes.ts"
|
|
26
29
|
import { isStaticMeta, mergeHeads } from "./head-merge.ts"
|
|
27
30
|
import { PRE_HYDRATION_GUARD } from "./runtime-contract.ts"
|
|
@@ -52,7 +55,7 @@ const RESPONSE_RESULT = Symbol.for("nifra.response.result")
|
|
|
52
55
|
* `status(...)` returns. Recognized here rather than imported: core keeps its own predicate internal,
|
|
53
56
|
* and the registry symbol is the contract between them (two copies of core must still agree).
|
|
54
57
|
*/
|
|
55
|
-
const isResponseResult = (value: unknown): value is ResponseResult =>
|
|
58
|
+
export const isResponseResult = (value: unknown): value is ResponseResult =>
|
|
56
59
|
typeof value === "object" &&
|
|
57
60
|
value !== null &&
|
|
58
61
|
(value as { readonly [RESPONSE_RESULT]?: unknown })[RESPONSE_RESULT] === true &&
|
|
@@ -97,9 +100,19 @@ export interface RenderPageOptions {
|
|
|
97
100
|
* `BuildManifest.css`) - injected as `<link rel="stylesheet">` in `<head>` so styles arrive with the
|
|
98
101
|
* first paint (no FOUC). Rendered even on non-hydrated pages. Empty/omitted ⇒ none (unchanged). */
|
|
99
102
|
readonly styles?: readonly string[]
|
|
103
|
+
/**
|
|
104
|
+
* Activation policy for these framework-owned stylesheet links (default `"blocking"`). In
|
|
105
|
+
* `"deferred"` mode, hydrating pages fetch with `media="print"` and the generated client promotes
|
|
106
|
+
* them to `media="all"` after load/error/timeout. Non-hydrated pages remain blocking so JS-off pages
|
|
107
|
+
* and terminal documents are styled.
|
|
108
|
+
*/
|
|
109
|
+
readonly cssLoading?: CssLoadingMode
|
|
100
110
|
/** SSG: the prerendered-path set, serialized to `window.__NIFRA_PRERENDERED__` so the client fetches
|
|
101
111
|
* a static `_data.json` on soft-nav into a prerendered route. Empty/omitted ⇒ not injected. */
|
|
102
112
|
readonly prerenderedPaths?: readonly string[]
|
|
113
|
+
/** SSG: the URL of a JSON array of the prerendered paths, handed over in place of
|
|
114
|
+
* {@link prerenderedPaths} so a large set is fetched once instead of riding in every page. */
|
|
115
|
+
readonly prerenderedListUrl?: string
|
|
103
116
|
/** ISR: route freshness in seconds, emitted as the `x-nifra-isr-revalidate` header for a `withISR`
|
|
104
117
|
* wrapper to read. Omit ⇒ no header (the wrapper's default TTL applies). */
|
|
105
118
|
readonly revalidate?: number
|
|
@@ -124,6 +137,8 @@ export interface RenderPageOptions {
|
|
|
124
137
|
readonly layoutData?: readonly unknown[]
|
|
125
138
|
/** Dynamic-boundary states, forwarded to the adapter and serialized for hydration when present. */
|
|
126
139
|
readonly boundaries?: BoundaryStates
|
|
140
|
+
/** The chain's ids and `handle` exports, forwarded as `RenderProps.matchChain` for `useMatches`. */
|
|
141
|
+
readonly matchChain?: MatchChain
|
|
127
142
|
/** HTTP status for the response (default 200; e.g. 404 for a not-found page). */
|
|
128
143
|
readonly status?: number
|
|
129
144
|
/** Extra response headers - e.g. the `cache-control` a terminal status page wants. `content-type`
|
|
@@ -157,15 +172,23 @@ export interface RenderPageOptions {
|
|
|
157
172
|
* tail - emitted **regardless of `hydrate`**, so a static (`hydrate: false`) page can still mount
|
|
158
173
|
* no-framework islands. URLs are attribute-escaped. Empty/omitted ⇒ none (unchanged output). */
|
|
159
174
|
readonly islandScripts?: readonly string[]
|
|
160
|
-
/** CSP nonce applied to every framework-owned executable script in this document.
|
|
175
|
+
/** CSP nonce applied to every framework-owned executable script in this document. Nonce-bearing
|
|
176
|
+
* documents are marked `private, no-store` so a request-specific nonce is never replayed. */
|
|
161
177
|
readonly nonce?: string
|
|
178
|
+
/**
|
|
179
|
+
* A hash-based Content-Security-Policy from `createCspPolicy`. The document then carries `nonce`
|
|
180
|
+
* only when it needs one (it defers a value, or a head tag names the nonce); otherwise it renders
|
|
181
|
+
* nonce-free and cacheable. The policy's header is set on the response either way.
|
|
182
|
+
*/
|
|
183
|
+
readonly csp?: CspPolicy
|
|
162
184
|
/**
|
|
163
185
|
* Advanced: a per-route slot the renderer fills with the request-invariant document pieces (shell
|
|
164
186
|
* prefix/suffix, tail statics) on the first render and reuses afterwards, skipping their re-assembly.
|
|
165
187
|
*
|
|
166
188
|
* Only pass a cache when every shell-shaping input is IDENTICAL across the requests sharing it:
|
|
167
189
|
* same `head` content (a static meta chain - never a `meta(data)` function), `title`, `styles`,
|
|
168
|
-
* `preload`, `islandScripts`, `clientEntry`, `rootId`, `hydrate`, and `prerenderedPaths`.
|
|
190
|
+
* `cssLoading`, `preload`, `islandScripts`, `clientEntry`, `rootId`, `hydrate`, and `prerenderedPaths`.
|
|
191
|
+
* Per-request
|
|
169
192
|
* values (`data`, `params`, `search`, `actionData`, `layoutData`, deferred state) are always assembled
|
|
170
193
|
* fresh and safe. A per-request `nonce` disables the cache automatically. `createWebApp` wires this
|
|
171
194
|
* per route, gated on the route + layout metas being static.
|
|
@@ -179,13 +202,10 @@ export interface RenderAssemblyCache {
|
|
|
179
202
|
shellPre?: string
|
|
180
203
|
/** Shell from after the deferred-runtime insertion point through the open `#root` container. */
|
|
181
204
|
shellPost?: string
|
|
182
|
-
/** Tail opener: the
|
|
205
|
+
/** Tail opener: the handover script open tag + the data member's key. */
|
|
183
206
|
tailPre?: string
|
|
184
|
-
/** Tail
|
|
185
|
-
|
|
186
|
-
/** Tail data prefix: the `window.<data global>=` assignment head. */
|
|
187
|
-
tailData?: string
|
|
188
|
-
/** Tail from after the serialized data through `</html>`. */
|
|
207
|
+
/** Tail from after the per-request handover members (the route id, prerendered paths, the handover
|
|
208
|
+
* close) through `</html>`. */
|
|
189
209
|
tailPost?: string
|
|
190
210
|
}
|
|
191
211
|
|
|
@@ -211,6 +231,12 @@ export function renderPage(options: RenderPageInput): MaybePromise<Response> {
|
|
|
211
231
|
}
|
|
212
232
|
|
|
213
233
|
export function renderPageResult(options: RenderPageInput): MaybePromise<RenderedPage> {
|
|
234
|
+
const { csp } = options
|
|
235
|
+
if (csp !== undefined) {
|
|
236
|
+
// The policy hashes the adapter's constant scripts once; only the first document waits for it.
|
|
237
|
+
const ready = prepareCspPolicy(csp, options.adapter)
|
|
238
|
+
if (ready !== undefined) return ready.then(() => renderPageResult(options))
|
|
239
|
+
}
|
|
214
240
|
const {
|
|
215
241
|
adapter,
|
|
216
242
|
chain,
|
|
@@ -219,7 +245,9 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
219
245
|
clientEntry,
|
|
220
246
|
preload = [],
|
|
221
247
|
styles = [],
|
|
248
|
+
cssLoading: requestedCssLoading,
|
|
222
249
|
prerenderedPaths = [],
|
|
250
|
+
prerenderedListUrl,
|
|
223
251
|
revalidate,
|
|
224
252
|
revalidateTags = [],
|
|
225
253
|
routeId,
|
|
@@ -229,21 +257,25 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
229
257
|
rootId = "root",
|
|
230
258
|
hydrate = true,
|
|
231
259
|
islandScripts = [],
|
|
232
|
-
nonce,
|
|
260
|
+
nonce: requestedNonce,
|
|
233
261
|
boundaries,
|
|
234
262
|
headers: extraHeaders,
|
|
235
263
|
} = options
|
|
236
|
-
|
|
264
|
+
const cssLoading = normalizeCssLoading(requestedCssLoading ?? DEFAULT_CSS_LOADING)
|
|
265
|
+
if (requestedNonce !== undefined && requestedNonce.trim() === "") {
|
|
237
266
|
throw new TypeError("[nifra/web] renderPage nonce must be non-empty when provided")
|
|
238
267
|
}
|
|
239
|
-
|
|
240
|
-
|
|
268
|
+
// Each handover entry after the data is `,"<global>":<json>`; the data opens the object, so it always
|
|
269
|
+
// has a first member and every other entry is a plain suffix.
|
|
270
|
+
const route = routeId === undefined ? "" : `,"${ROUTE_GLOBAL}":${serializeData(routeId)}`
|
|
241
271
|
// The SSG prerendered-path set (when an app declares it) - the client reads it to fetch a static
|
|
242
272
|
// `_data.json` on soft-nav into a prerendered route instead of hitting the worker. Empty ⇒ omitted.
|
|
243
273
|
const prerendered =
|
|
244
|
-
|
|
245
|
-
? ""
|
|
246
|
-
:
|
|
274
|
+
prerenderedListUrl !== undefined
|
|
275
|
+
? `,"${PRERENDERED_GLOBAL}":${serializeData(prerenderedListUrl)}`
|
|
276
|
+
: prerenderedPaths.length === 0
|
|
277
|
+
? ""
|
|
278
|
+
: `,"${PRERENDERED_GLOBAL}":${serializeData(prerenderedPaths)}`
|
|
247
279
|
// Split deferred values: the component sees markers (id + promise) to `<Await>`; the serialized
|
|
248
280
|
// data carries `{__nifra_deferred: id}` placeholders (promises don't serialize). `actionData` may
|
|
249
281
|
// also `defer()` - split it too, continuing the id space so a single registry settles both. The
|
|
@@ -283,21 +315,34 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
283
315
|
...(actionSplit ? actionSplit.deferred : []),
|
|
284
316
|
...(boundarySplit ? boundarySplit.deferred : []),
|
|
285
317
|
]
|
|
318
|
+
// Under a CSP policy a document carries a nonce only when it has a script specific to this request:
|
|
319
|
+
// each settled deferred value streams one, and an executable head tag names the nonce. Every other
|
|
320
|
+
// document is nonce-free - its inline scripts are the constant ones the policy lists by hash - so its
|
|
321
|
+
// CSP header is the same on every request and it stays cacheable.
|
|
322
|
+
const nonce =
|
|
323
|
+
csp === undefined
|
|
324
|
+
? requestedNonce
|
|
325
|
+
: cspDocumentNonce(
|
|
326
|
+
csp,
|
|
327
|
+
adapter,
|
|
328
|
+
hydrate,
|
|
329
|
+
allDeferred.length > 0 || headNamesNonce(head),
|
|
330
|
+
requestedNonce,
|
|
331
|
+
)
|
|
332
|
+
const nonceAttr = nonce === undefined ? "" : ` nonce="${escapeAttr(nonce)}"`
|
|
286
333
|
// Omitted entirely when no layout has a loader - a page-only app emits exactly what it did before.
|
|
287
334
|
const layoutTail =
|
|
288
335
|
options.layoutData === undefined
|
|
289
336
|
? ""
|
|
290
|
-
:
|
|
337
|
+
: `,"${LAYOUT_DATA_GLOBAL}":${serializeData(layoutSplits.map((split) => split.forClient))}`
|
|
291
338
|
|
|
292
339
|
// Only emit the action global when an action actually ran, so plain GET output is unchanged.
|
|
293
340
|
const action =
|
|
294
|
-
actionSplit === undefined
|
|
295
|
-
? ""
|
|
296
|
-
: `window.${ACTION_GLOBAL}=${serializeData(actionSplit.forClient)};`
|
|
341
|
+
actionSplit === undefined ? "" : `,"${ACTION_GLOBAL}":${serializeData(actionSplit.forClient)}`
|
|
297
342
|
const boundaryTail =
|
|
298
343
|
boundarySplit === undefined
|
|
299
344
|
? ""
|
|
300
|
-
:
|
|
345
|
+
: `,"${BOUNDARY_GLOBAL}":${serializeData(boundarySplit.forClient)}`
|
|
301
346
|
const deferredRuntime =
|
|
302
347
|
allDeferred.length > 0 ? `<script${nonceAttr}>${DEFERRED_RUNTIME}</script>` : ""
|
|
303
348
|
// The regex only injects the CSP nonce into the (constant) hydration head; with no nonce it's a
|
|
@@ -309,6 +354,14 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
309
354
|
const slot: RenderAssemblyCache =
|
|
310
355
|
nonce === undefined && options.assemblyCache !== undefined ? options.assemblyCache : {}
|
|
311
356
|
if (slot.shellPre === undefined) {
|
|
357
|
+
// An empty entry is not "no client": `<script type="module" src="">` resolves to the page URL, so
|
|
358
|
+
// the browser would load the document itself as a module. Checked here, where the shell is built,
|
|
359
|
+
// so a cached route pays it once.
|
|
360
|
+
if (hydrate && (clientEntry === undefined || clientEntry.trim() === "")) {
|
|
361
|
+
throw new TypeError(
|
|
362
|
+
"[nifra/web] a hydrating page needs a non-empty clientEntry (the built client entry URL); pass hydrate: false for a page with no client",
|
|
363
|
+
)
|
|
364
|
+
}
|
|
312
365
|
// Matched-route chunk preloads, concatenated directly. De-duped against the entry, which is
|
|
313
366
|
// preloaded separately below.
|
|
314
367
|
let preloadLinks = ""
|
|
@@ -317,10 +370,25 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
317
370
|
preloadLinks += `<link rel="modulepreload" href="${escapeAttr(url)}">`
|
|
318
371
|
}
|
|
319
372
|
// The matched route's stylesheets - `<link rel="stylesheet">` in `<head>` so CSS arrives with the
|
|
320
|
-
// first paint (no FOUC).
|
|
321
|
-
//
|
|
373
|
+
// first paint (no FOUC). Deferred mode is deliberately opt-in and only applies to hydrating pages:
|
|
374
|
+
// a JS-off page must retain a normal stylesheet link, and a terminal/static page has no generated
|
|
375
|
+
// client to promote `media="print"` to `media="all"`.
|
|
322
376
|
let styleLinks = ""
|
|
323
|
-
|
|
377
|
+
const deferStyles = hydrate && cssLoading === "deferred"
|
|
378
|
+
const styleUrls = [...new Set(styles)]
|
|
379
|
+
for (const url of styleUrls) {
|
|
380
|
+
const href = escapeAttr(url)
|
|
381
|
+
if (!deferStyles) {
|
|
382
|
+
styleLinks += `<link rel="stylesheet" href="${href}">`
|
|
383
|
+
continue
|
|
384
|
+
}
|
|
385
|
+
// `media="print"` starts the fetch without making the CSS render-blocking. The marker is scoped
|
|
386
|
+
// to framework-owned links so the generated client never rewrites an author's head link. The
|
|
387
|
+
// noscript copy preserves progressive enhancement when JavaScript is disabled.
|
|
388
|
+
styleLinks +=
|
|
389
|
+
`<link rel="stylesheet" href="${href}" media="print" data-nifra-css="deferred">` +
|
|
390
|
+
`<noscript><link rel="stylesheet" href="${href}"></noscript>`
|
|
391
|
+
}
|
|
324
392
|
// Island bundles are referenced only by a `<script type="module">` at the END of `<body>`, so the
|
|
325
393
|
// browser doesn't discover them until the whole page is parsed. `modulepreload` them in `<head>`
|
|
326
394
|
// so the fetch starts immediately, in parallel with parsing - regardless of `hydrate`.
|
|
@@ -349,7 +417,7 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
349
417
|
const entryPreloads = hydrate
|
|
350
418
|
? `<link rel="modulepreload" href="${escapeAttr(clientEntry ?? "")}">${preloadLinks}`
|
|
351
419
|
: ""
|
|
352
|
-
slot.shellPre = `<!doctype html><html${htmlAttrs}><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">${hydrationGuard}<title>${escapeHtml(head?.title ?? title)}</title>${headTags(head)}${styleLinks}${entryPreloads}`
|
|
420
|
+
slot.shellPre = `<!doctype html><html${htmlAttrs}><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">${hydrationGuard}<title>${escapeHtml(head?.title ?? title)}</title>${headTags(head, nonce)}${styleLinks}${entryPreloads}`
|
|
353
421
|
slot.shellPost = `${islandPreloads}${hydrationHead}</head><body><div id="${escapeAttr(rootId)}"${rootMarker}>`
|
|
354
422
|
// Island bundles load regardless of `hydrate` - a static page (hydrate:false) ships no framework
|
|
355
423
|
// client but can still mount no-framework islands (`@nifrajs/web/islands`).
|
|
@@ -357,14 +425,11 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
357
425
|
for (const src of islandScripts)
|
|
358
426
|
islandTags += `<script type="module" src="${escapeAttr(src)}"${nonceAttr}></script>`
|
|
359
427
|
if (hydrate) {
|
|
360
|
-
|
|
361
|
-
slot.
|
|
362
|
-
slot.
|
|
363
|
-
slot.tailPost = `</script><script type="module" src="${escapeAttr(clientEntry ?? "")}"${nonceAttr}></script>${islandTags}</body></html>`
|
|
428
|
+
// Inert JSON, never executed: the page state needs no nonce and no hash under any CSP.
|
|
429
|
+
slot.tailPre = `<script type="application/json" id="${HANDOVER_ID}">{"${DATA_GLOBAL}":`
|
|
430
|
+
slot.tailPost = `${route}${prerendered}}</script><script type="module" src="${escapeAttr(clientEntry ?? "")}"${nonceAttr}></script>${islandTags}</body></html>`
|
|
364
431
|
} else {
|
|
365
432
|
slot.tailPre = ""
|
|
366
|
-
slot.tailMid = ""
|
|
367
|
-
slot.tailData = ""
|
|
368
433
|
slot.tailPost = `${islandTags}</body></html>`
|
|
369
434
|
}
|
|
370
435
|
}
|
|
@@ -375,10 +440,10 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
375
440
|
// Closes the hydration container; deferred resolve scripts go AFTER it (outside `#root`) so they
|
|
376
441
|
// aren't part of the adapter's hydrated tree (an inline script inside it breaks hydration).
|
|
377
442
|
const closeRootHtml = "</div>"
|
|
378
|
-
// Tail - the
|
|
379
|
-
// the
|
|
443
|
+
// Tail - the page-state handover + the client module. Module scripts defer (run after parse), so
|
|
444
|
+
// the handover + every streamed deferred resolution are in the document before the entry hydrates.
|
|
380
445
|
const tailHtml = hydrate
|
|
381
|
-
? `${slot.tailPre}${
|
|
446
|
+
? `${slot.tailPre}${serializeData(forClient)}${action}${layoutTail}${boundaryTail}${slot.tailPost}`
|
|
382
447
|
: (slot.tailPost as string)
|
|
383
448
|
const headers: Record<string, string> = { "content-type": "text/html; charset=utf-8" }
|
|
384
449
|
// Caller-supplied headers (a terminal status page's `cache-control`, say). Applied before the
|
|
@@ -395,6 +460,14 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
395
460
|
if (revalidate !== undefined) headers[ISR_REVALIDATE_HEADER] = String(revalidate)
|
|
396
461
|
const serializedTags = serializeISRTags(revalidateTags)
|
|
397
462
|
if (serializedTags !== undefined) headers[ISR_REVALIDATE_TAGS_HEADER] = serializedTags
|
|
463
|
+
// A per-request nonce must not be replayed from a browser, CDN, or ISR cache. The same policy
|
|
464
|
+
// also keeps `withISR` from storing a document whose executable scripts carry request-specific
|
|
465
|
+
// authorization to run under the caller's CSP.
|
|
466
|
+
if (nonce !== undefined) headers["cache-control"] = "private, no-store"
|
|
467
|
+
if (csp !== undefined) {
|
|
468
|
+
const policy = cspHeaderValue(csp, adapter, nonce)
|
|
469
|
+
if (policy !== undefined) headers["content-security-policy"] = policy
|
|
470
|
+
}
|
|
398
471
|
const renderProps: RenderProps = {
|
|
399
472
|
data: forComponent,
|
|
400
473
|
actionData: actionSplit?.forComponent,
|
|
@@ -413,6 +486,7 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
413
486
|
...(boundarySplit !== undefined
|
|
414
487
|
? { boundaries: boundarySplit.forComponent as BoundaryStates }
|
|
415
488
|
: {}),
|
|
489
|
+
...(options.matchChain !== undefined ? { matchChain: options.matchChain } : {}),
|
|
416
490
|
}
|
|
417
491
|
|
|
418
492
|
// Fast path: nothing `defer()`s and the adapter can render synchronously to a string → buffer the
|
|
@@ -445,6 +519,7 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
445
519
|
tailHtml,
|
|
446
520
|
status,
|
|
447
521
|
headers,
|
|
522
|
+
nonce,
|
|
448
523
|
nonceAttr,
|
|
449
524
|
).then((response) => new ResponseRenderedPage(response))
|
|
450
525
|
}
|
|
@@ -459,6 +534,7 @@ async function renderStreamedPage(
|
|
|
459
534
|
tailHtml: string,
|
|
460
535
|
status: number,
|
|
461
536
|
headers: Record<string, string>,
|
|
537
|
+
nonce: string | undefined,
|
|
462
538
|
nonceAttr: string,
|
|
463
539
|
): Promise<Response> {
|
|
464
540
|
// Streaming path - required for `defer()` (progressive `<Await>` resolution) and used by any adapter
|
|
@@ -469,7 +545,10 @@ async function renderStreamedPage(
|
|
|
469
545
|
const shell = enc.encode(shellHtml)
|
|
470
546
|
const closeRoot = enc.encode(closeRootHtml)
|
|
471
547
|
const tail = enc.encode(tailHtml)
|
|
472
|
-
const appStream =
|
|
548
|
+
const appStream =
|
|
549
|
+
nonce === undefined
|
|
550
|
+
? await adapter.renderToStream(chain, renderProps)
|
|
551
|
+
: await adapter.renderToStream(chain, renderProps, { nonce })
|
|
473
552
|
const body = streamDocument(shell, appStream, closeRoot, allDeferred, tail, enc, nonceAttr)
|
|
474
553
|
return new Response(body, { status, headers })
|
|
475
554
|
}
|
|
@@ -554,8 +633,10 @@ function streamDocument(
|
|
|
554
633
|
deferred.map(async (d) => {
|
|
555
634
|
try {
|
|
556
635
|
const value = serializeData(await d.promise)
|
|
636
|
+
// `serializeData` JSON-serializes the value and escapes `<`, `>`, and Unicode line
|
|
637
|
+
// separators before this framework-owned script is emitted.
|
|
557
638
|
controller.enqueue(
|
|
558
|
-
enc.encode(`<script${nonceAttr}>window.__nifraResolve(${d.id},${value})</script>`),
|
|
639
|
+
enc.encode(`<script${nonceAttr}>window.__nifraResolve(${d.id},${value})</script>`), // nosemgrep: javascript.lang.security.audit.unknown-value-with-script-tag.unknown-value-with-script-tag
|
|
559
640
|
)
|
|
560
641
|
} catch (err) {
|
|
561
642
|
// A rejected deferred streams __nifraReject (the client `<Await>` surfaces it) - it must
|
|
@@ -590,7 +671,7 @@ export interface RedirectOptions {
|
|
|
590
671
|
* attacker-controlled input straight through. Set `true` for a deliberate external redirect. */
|
|
591
672
|
readonly external?: boolean
|
|
592
673
|
/** Extra response headers. A redirect is no longer a `Response`, so there is no `.headers` to
|
|
593
|
-
* mutate after the fact - name them here. Cookies
|
|
674
|
+
* mutate after the fact - name them here. Cookies queued with `ctx.set.cookie()` still ride it. */
|
|
594
675
|
readonly headers?: Readonly<Record<string, string>>
|
|
595
676
|
}
|
|
596
677
|
|
|
@@ -694,37 +775,6 @@ export function withDuplicateInstanceHint(err: unknown): unknown {
|
|
|
694
775
|
return augmented
|
|
695
776
|
}
|
|
696
777
|
|
|
697
|
-
/**
|
|
698
|
-
* An action's control-flow value passes straight through - except a redirect on a client-submit data
|
|
699
|
-
* request: fetch would follow the 3xx into HTML the client can't use, so the redirect rides the
|
|
700
|
-
* X-Nifra-Redirect header on a 204 and the client navigates. One conversion shared by the returned-
|
|
701
|
-
* and thrown- paths, so `return redirect()` and `throw redirect()` agree.
|
|
702
|
-
*
|
|
703
|
-
* Takes either shape: `redirect()` is a plain render, while a hand-rolled `new Response(...)` from an
|
|
704
|
-
* action still arrives as a `Response`. The rewrite stays on the lane its input was on - a plain
|
|
705
|
-
* redirect converts to a plain 204, and never materializes the `Response` it is replacing.
|
|
706
|
-
*/
|
|
707
|
-
export function actionResponse(
|
|
708
|
-
result: Response | ResponseResult,
|
|
709
|
-
isDataRequest: boolean,
|
|
710
|
-
): Response | ResponseResult {
|
|
711
|
-
if (!isDataRequest) return result
|
|
712
|
-
if (isResponseResult(result)) {
|
|
713
|
-
const plain = result.plain
|
|
714
|
-
// No `plain` means a carrier that only knows how to build a `Response` (not one of ours) - fall
|
|
715
|
-
// back rather than guess at its status.
|
|
716
|
-
if (plain === undefined) return actionResponse(result.toResponse(), isDataRequest)
|
|
717
|
-
if (plain.status < 300 || plain.status >= 400) return result
|
|
718
|
-
const location = plain.headers?.location ?? "/"
|
|
719
|
-
return statusResult(204, undefined, { headers: { [REDIRECT_HEADER]: location } })
|
|
720
|
-
}
|
|
721
|
-
if (result.status >= 300 && result.status < 400) {
|
|
722
|
-
const location = result.headers.get("location") ?? "/"
|
|
723
|
-
return statusResult(204, undefined, { headers: { [REDIRECT_HEADER]: location } })
|
|
724
|
-
}
|
|
725
|
-
return result
|
|
726
|
-
}
|
|
727
|
-
|
|
728
778
|
/** A loaded layout module. `loader`/`gate` are the layout-loader surface; `meta` predates it. */
|
|
729
779
|
export type LoadedLayoutModules = ReadonlyArray<{
|
|
730
780
|
default: unknown
|
|
@@ -732,6 +782,10 @@ export type LoadedLayoutModules = ReadonlyArray<{
|
|
|
732
782
|
loader?: LayoutLoader
|
|
733
783
|
action?: unknown
|
|
734
784
|
gate?: boolean
|
|
785
|
+
// Read only when it is a function - see `ShouldRevalidate` in manifest.ts.
|
|
786
|
+
shouldRevalidate?: unknown
|
|
787
|
+
// Reported by `useMatches` - see `RouteModule.handle`.
|
|
788
|
+
handle?: unknown
|
|
735
789
|
// A layout may declare its own `searchSchema`; the route's effective search merges the layout chain's
|
|
736
790
|
// schemas with the page's (page-wins). Present on the raw module already - typed here so it is readable.
|
|
737
791
|
searchSchema?: RouteModule["searchSchema"]
|
|
@@ -965,6 +1019,19 @@ function tagAttrs(tag: "meta" | "link", attrs: Readonly<object>): string | null
|
|
|
965
1019
|
// so a route module that's GC'd takes its entry with it.
|
|
966
1020
|
const headTagsCache = new WeakMap<Meta, string>()
|
|
967
1021
|
|
|
1022
|
+
// An executable script runs as written, so it is checked rather than escaped: `\u003c` is valid only
|
|
1023
|
+
// inside a JS string, and escaping turned `if (a < b)` into a syntax error. What the HTML tokenizer
|
|
1024
|
+
// reads as the element's end, or as the start of an escaped section, is refused instead.
|
|
1025
|
+
const SCRIPT_BREAKOUT = /<\/script|<!--/i
|
|
1026
|
+
|
|
1027
|
+
function assertExecutableScriptContent(content: string): void {
|
|
1028
|
+
if (SCRIPT_BREAKOUT.test(content)) {
|
|
1029
|
+
throw new TypeError(
|
|
1030
|
+
'[nifra/web] an executable inline script cannot contain "</script" or "<!--"; write "<\\/script" or split the string',
|
|
1031
|
+
)
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
968
1035
|
function assertExecutableScriptType(type: string): void {
|
|
969
1036
|
if (!EXECUTABLE_SCRIPT_TYPES.has(type)) {
|
|
970
1037
|
throw new TypeError(
|
|
@@ -980,9 +1047,15 @@ function assertExecutableScriptType(type: string): void {
|
|
|
980
1047
|
* or close the tag early. String concatenation (no intermediate `.map()` arrays + spread) - parity with
|
|
981
1048
|
* the already concat-based preloadLinks/styleLinks/islandPreloads loops; byte-identical output. Result
|
|
982
1049
|
* is memoized only for objects observed as static meta exports (serialized once per route). */
|
|
983
|
-
|
|
1050
|
+
/** Whether a document head names the CSP nonce: an executable script or a `<link nonce>`. */
|
|
1051
|
+
const headNamesNonce = (head: Meta | undefined): boolean =>
|
|
1052
|
+
(head?.unsafeScript?.length ?? 0) > 0 || (head?.link?.some((l) => l.nonce !== undefined) ?? false)
|
|
1053
|
+
|
|
1054
|
+
function headTags(head: Meta | undefined, documentNonce?: string): string {
|
|
984
1055
|
if (head === undefined) return ""
|
|
985
|
-
|
|
1056
|
+
// Executable head metadata carries a caller-chosen nonce, so never cache it by object identity. A
|
|
1057
|
+
// static route object may otherwise retain one request's nonce and replay it on another document.
|
|
1058
|
+
const cacheable = isStaticMeta(head) && (head.unsafeScript?.length ?? 0) === 0
|
|
986
1059
|
if (cacheable) {
|
|
987
1060
|
const cached = headTagsCache.get(head)
|
|
988
1061
|
if (cached !== undefined) return cached
|
|
@@ -1023,7 +1096,13 @@ function headTags(head: Meta | undefined): string {
|
|
|
1023
1096
|
// attribute and inject markup. Measured: `type: '"><img src=x onerror=alert(1)>'` reached the
|
|
1024
1097
|
// document. An injection in the slot whose whole purpose is to make script emission trustworthy.
|
|
1025
1098
|
assertExecutableScriptType(s.type)
|
|
1026
|
-
|
|
1099
|
+
if (documentNonce !== undefined && s.nonce !== documentNonce) {
|
|
1100
|
+
throw new TypeError(
|
|
1101
|
+
"[nifra/web] executable head scripts must use the same CSP nonce as the document",
|
|
1102
|
+
)
|
|
1103
|
+
}
|
|
1104
|
+
assertExecutableScriptContent(s.content)
|
|
1105
|
+
out += `<script type="${s.type}" nonce="${escapeAttr(s.nonce)}" data-nifra>${s.content}</script>`
|
|
1027
1106
|
}
|
|
1028
1107
|
if (cacheable) headTagsCache.set(head, out)
|
|
1029
1108
|
return out
|
|
@@ -1107,6 +1186,7 @@ export function unsafeInlineScript(
|
|
|
1107
1186
|
// place a caller is told what it may pass. Failing at the call site names the argument; failing at
|
|
1108
1187
|
// render names a document.
|
|
1109
1188
|
assertExecutableScriptType(type)
|
|
1189
|
+
assertExecutableScriptContent(content)
|
|
1110
1190
|
return {
|
|
1111
1191
|
unsafe: true,
|
|
1112
1192
|
type,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type UrlParts, urlPartsOf } from "@nifrajs/core/server"
|
|
2
|
+
|
|
3
|
+
// A page request reaches the same Request object through mounts, fallback checks, search parsing,
|
|
4
|
+
// and rendering. Cache the allocation-light URL split once for that request. WeakMap keeps the cache
|
|
5
|
+
// bounded to requests that are still alive and makes the helper safe for every runtime adapter.
|
|
6
|
+
const REQUEST_URL_PARTS = new WeakMap<Request, UrlParts>()
|
|
7
|
+
|
|
8
|
+
export function urlPartsFor(request: Request): UrlParts {
|
|
9
|
+
const cached = REQUEST_URL_PARTS.get(request)
|
|
10
|
+
if (cached !== undefined) return cached
|
|
11
|
+
const parts = urlPartsOf(request.url)
|
|
12
|
+
REQUEST_URL_PARTS.set(request, parts)
|
|
13
|
+
return parts
|
|
14
|
+
}
|