@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
package/src/internal/codegen.ts
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
|
+
import { type CssLoadingMode, DEFAULT_CSS_LOADING, normalizeCssLoading } from "../css-contract.ts"
|
|
1
2
|
import { MAP_DEFERRED_SOURCE } from "../deferred.ts"
|
|
2
3
|
import type { LayoutEntry, Manifest } from "../manifest.ts"
|
|
3
4
|
import {
|
|
4
5
|
ACTION_GLOBAL,
|
|
5
6
|
BOUNDARY_GLOBAL,
|
|
6
7
|
DATA_GLOBAL,
|
|
8
|
+
HANDOVER_ID,
|
|
7
9
|
LAYOUT_DATA_GLOBAL,
|
|
8
10
|
ROOT_ATTRIBUTE,
|
|
9
11
|
ROUTE_GLOBAL,
|
|
10
12
|
} from "../render-seam.ts"
|
|
13
|
+
import { PRERENDERED_GLOBAL } from "../router.ts"
|
|
11
14
|
import { jsStringLiteral } from "./js-string.ts"
|
|
15
|
+
|
|
16
|
+
/** The globals the page-state handover may set - nothing else in it reaches `window`. */
|
|
17
|
+
const HANDOVER_GLOBALS = [
|
|
18
|
+
DATA_GLOBAL,
|
|
19
|
+
LAYOUT_DATA_GLOBAL,
|
|
20
|
+
ROUTE_GLOBAL,
|
|
21
|
+
ACTION_GLOBAL,
|
|
22
|
+
BOUNDARY_GLOBAL,
|
|
23
|
+
PRERENDERED_GLOBAL,
|
|
24
|
+
]
|
|
12
25
|
export interface GenerateClientEntryOptions {
|
|
13
26
|
/**
|
|
14
27
|
* Module specifier for the adapter's client runtime, e.g. `"@nifrajs/web-solid/client"`.
|
|
@@ -47,6 +60,8 @@ export function generateClientEntry(
|
|
|
47
60
|
// Routes whose loader appends a nearest `_error` module (LAST) - the client wraps the page in the
|
|
48
61
|
// adapter's `errorBoundary(fallback)` for these, so a client render error shows the `_error` UI.
|
|
49
62
|
const errorRouteIds: string[] = []
|
|
63
|
+
// Route id → its layout ids, for the chain `useMatches` reports. Routes without layouts are omitted.
|
|
64
|
+
const layoutIdsOf: Record<string, readonly string[]> = {}
|
|
50
65
|
// Lazy loader returns the raw modules (for both the component chain + the page's `meta` export).
|
|
51
66
|
const lazyLoader = (files: readonly string[]): string => {
|
|
52
67
|
const imports = files.map((f) => `import(${jsStringLiteral(resolve(f))})`).join(", ")
|
|
@@ -67,6 +82,7 @@ export function generateClientEntry(
|
|
|
67
82
|
errorRouteIds.push(route.id)
|
|
68
83
|
}
|
|
69
84
|
loaderRows.push(` ${JSON.stringify(route.id)}: ${lazyLoader(files)},`)
|
|
85
|
+
if (route.layoutIds.length > 0) layoutIdsOf[route.id] = route.layoutIds
|
|
70
86
|
patternRows.push(
|
|
71
87
|
` { routeId: ${JSON.stringify(route.id)}, pattern: ${JSON.stringify(route.pattern)} },`,
|
|
72
88
|
)
|
|
@@ -80,14 +96,46 @@ export function generateClientEntry(
|
|
|
80
96
|
loaderRows.push(` ${JSON.stringify(routeId)}: ${lazyLoader([entry.file])},`)
|
|
81
97
|
statusRoutes[Number(status)] = routeId
|
|
82
98
|
}
|
|
99
|
+
// `_loading` pages. Emitted only when the app has one, so an app without them imports nothing extra.
|
|
100
|
+
// Each is its own lazy chunk; a route row carries its layout ids and the loading pages above it with
|
|
101
|
+
// the number of those layouts each sits under - all the runtime needs to choose one.
|
|
102
|
+
const loadings = Object.entries(manifest.loadings ?? {})
|
|
103
|
+
const loadingLines: string[] = []
|
|
104
|
+
if (loadings.length > 0) {
|
|
105
|
+
const routeRows = new Map<string, string>()
|
|
106
|
+
for (const route of manifest.routes) {
|
|
107
|
+
const above = (route.loadingIds ?? []).map((id) => [
|
|
108
|
+
id,
|
|
109
|
+
manifest.loadings?.[id]?.layoutIds.length ?? 0,
|
|
110
|
+
])
|
|
111
|
+
routeRows.set(
|
|
112
|
+
route.id,
|
|
113
|
+
` ${JSON.stringify(route.id)}: ${JSON.stringify([route.layoutIds, above])},`,
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
loadingLines.push(
|
|
117
|
+
"const loadingModules = {",
|
|
118
|
+
...loadings.map(
|
|
119
|
+
([id, entry]) =>
|
|
120
|
+
` ${JSON.stringify(id)}: () => import(${jsStringLiteral(resolve(entry.file))}),`,
|
|
121
|
+
),
|
|
122
|
+
"}",
|
|
123
|
+
"const loadingRoutes = {",
|
|
124
|
+
...routeRows.values(),
|
|
125
|
+
"}",
|
|
126
|
+
)
|
|
127
|
+
}
|
|
83
128
|
|
|
84
129
|
return `${[
|
|
85
130
|
// `/client`, never the root: the root's graph carries the server (renderPage, the static-file
|
|
86
131
|
// server), and Vite's dev server evaluates what it is given instead of tree-shaking it.
|
|
87
132
|
'import { createClientRouter, createMatcher, mergeHeads, resolveMeta } from "@nifrajs/web/client"',
|
|
88
|
-
'import { applyHead, installForms, installHistory, signalHydrated } from "@nifrajs/web/client"',
|
|
133
|
+
'import { applyHead, currentDocumentNonce, installForms, installHistory, signalHydrated, waitForStyles } from "@nifrajs/web/client"',
|
|
89
134
|
// Namespace import: `errorBoundary` is optional (an adapter may not export it). A namespace member
|
|
90
135
|
// access yields `undefined` if absent - unlike a named import, which would be a link error.
|
|
136
|
+
...(loadings.length > 0
|
|
137
|
+
? ['import { withLoading } from "@nifrajs/web/internal/loading-runtime"']
|
|
138
|
+
: []),
|
|
91
139
|
`import * as __adapter from ${JSON.stringify(clientModule)}`,
|
|
92
140
|
"const { mountRouter } = __adapter",
|
|
93
141
|
// The assurance hook is optional: ordinary client entries pay only for the namespace lookup, while
|
|
@@ -139,6 +187,12 @@ export function generateClientEntry(
|
|
|
139
187
|
// routeId → the page's optional post-hydration client loader/action hooks. Hooks are populated only
|
|
140
188
|
// after the route chunk loads, so routes that do not declare them remain ordinary route modules.
|
|
141
189
|
"const routeHooks = {}",
|
|
190
|
+
// routeId → the chain the server rendered for an `ssr = false` page: the same layouts (and
|
|
191
|
+
// boundary), with the page's `HydrateFallback` - or an empty leaf - where the component goes.
|
|
192
|
+
"const holds = {}",
|
|
193
|
+
// routeId → the ids and `handle` exports `useMatches` reports (layouts, then the page).
|
|
194
|
+
`const layoutIdsOf = ${JSON.stringify(layoutIdsOf)}`,
|
|
195
|
+
"const matchChains = {}",
|
|
142
196
|
"const loadModule = async (id) => {",
|
|
143
197
|
" if (chains[id]) return",
|
|
144
198
|
" const mods = await loaders[id]()",
|
|
@@ -160,20 +214,34 @@ export function generateClientEntry(
|
|
|
160
214
|
// `searchSchema` merges with the page's. Client keys come from the page (second-to-last).
|
|
161
215
|
" searchSchemas[id] = mods.slice(0, mods.length - 1).map((m) => m.searchSchema)",
|
|
162
216
|
" searchClientKeys[id] = mods[mods.length - 2].searchClientKeys ?? []",
|
|
217
|
+
" matchChains[id] = { ids: [...(layoutIdsOf[id] ?? []), id], handles: mods.slice(0, mods.length - 1).map((m) => m.handle) }",
|
|
163
218
|
" } else {",
|
|
164
219
|
" chains[id] = mods.map((m) => m.default)",
|
|
165
220
|
" metas[id] = mods.map((m) => m.meta)",
|
|
166
221
|
// The chain is every module (layouts + page); client keys come from the page (the last module).
|
|
167
222
|
" searchSchemas[id] = mods.map((m) => m.searchSchema)",
|
|
168
223
|
" searchClientKeys[id] = mods[mods.length - 1].searchClientKeys ?? []",
|
|
224
|
+
" matchChains[id] = { ids: [...(layoutIdsOf[id] ?? []), id], handles: mods.map((m) => m.handle) }",
|
|
169
225
|
" }",
|
|
170
226
|
" const page = errorRouteIds.has(id) ? mods[mods.length - 2] : mods[mods.length - 1]",
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
// hook table or serialized into a browser bundle by this wiring.
|
|
227
|
+
// Only neutral name/mode metadata crosses into the router; a boundary's loader lives in the route's
|
|
228
|
+
// backend half, which no browser bundle contains.
|
|
174
229
|
" const boundaryMods = errorRouteIds.has(id) ? mods.slice(0, -1) : mods",
|
|
175
|
-
" const boundaries = boundaryMods.flatMap((m) => (m.boundaries ?? []).map((b) => ({ name: b.name, mode: b.mode,
|
|
230
|
+
" const boundaries = boundaryMods.flatMap((m) => (m.boundaries ?? []).map((b) => ({ name: b.name, mode: b.mode, ...(b.errorId === undefined ? {} : { errorId: b.errorId }) })))",
|
|
176
231
|
" routeHooks[id] = { clientLoader: page.clientLoader, clientAction: page.clientAction, boundaries }",
|
|
232
|
+
" if (page.ssr === false) holds[id] = [...chains[id].slice(0, -1), page.HydrateFallback ?? (() => null)]",
|
|
233
|
+
"}",
|
|
234
|
+
// The server hands page state over as one inert JSON script; lift it onto the globals the
|
|
235
|
+
// router, the deferred mapper and the adapters read, before any of them runs. Only the LAST
|
|
236
|
+
// matching <script> counts, and only known keys: page HTML (sanitized user content) can carry an
|
|
237
|
+
// element with the same id, and arbitrary keys copied onto `window` include `location`.
|
|
238
|
+
`const handovers = document.querySelectorAll(${JSON.stringify(`script[type="application/json"][id="${HANDOVER_ID}"]`)})`,
|
|
239
|
+
"const handover = handovers[handovers.length - 1]",
|
|
240
|
+
"if (handover !== undefined) {",
|
|
241
|
+
' const state = JSON.parse(handover.textContent || "{}")',
|
|
242
|
+
` for (const key of ${JSON.stringify(HANDOVER_GLOBALS)}) {`,
|
|
243
|
+
" if (Object.prototype.hasOwnProperty.call(state, key)) Reflect.set(window, key, state[key])",
|
|
244
|
+
" }",
|
|
177
245
|
"}",
|
|
178
246
|
"const patterns = [",
|
|
179
247
|
...patternRows,
|
|
@@ -181,12 +249,19 @@ export function generateClientEntry(
|
|
|
181
249
|
// Derive the initial route from the URL (correct on refresh/deep-link); fall back to the
|
|
182
250
|
// server-injected route id for non-pattern routes (e.g. _404, which matches nothing).
|
|
183
251
|
"const matched = createMatcher(patterns)(location.pathname)",
|
|
252
|
+
`const statusRoutes = ${JSON.stringify(statusRoutes)}`,
|
|
253
|
+
`const serverRoute = window.${ROUTE_GLOBAL} ?? ""`,
|
|
254
|
+
// A terminal status page the server rendered (`_404`, `_410`, ...) outranks the URL match: a
|
|
255
|
+
// loader that throws notFound() on `/learn/bad-slug` leaves a URL that still matches
|
|
256
|
+
// `learn/[slug]`, and hydrating that route with null data over the 404 markup is wrong. Membership
|
|
257
|
+
// in the status table, not a `_` prefix: `routes/_admin/index.tsx` is an ordinary routable id.
|
|
258
|
+
"const terminal = Object.values(statusRoutes).includes(serverRoute)",
|
|
184
259
|
// Map any `{__nifra_deferred: id}` placeholder in the SSR data to the registry's promise, so the
|
|
185
260
|
// component receives real promises to `<Await>` (a no-op when a page has no deferred data).
|
|
186
261
|
MAP_DEFERRED_SOURCE,
|
|
187
262
|
"const initial = {",
|
|
188
|
-
|
|
189
|
-
" params: matched ? matched.params : {},",
|
|
263
|
+
" routeId: matched && !terminal ? matched.routeId : serverRoute,",
|
|
264
|
+
" params: matched && !terminal ? matched.params : {},",
|
|
190
265
|
// pathname + search (NOT just pathname): the SSR render threads `pathname+search` into
|
|
191
266
|
// `useLocation`/`useSearchParams`, so the hydrating initial state must carry the query too or a
|
|
192
267
|
// page reading the search string would hydrate-mismatch. The #hash is client-only (never SSR'd).
|
|
@@ -201,8 +276,12 @@ export function generateClientEntry(
|
|
|
201
276
|
` actionData: mapDeferred(window.${ACTION_GLOBAL}),`,
|
|
202
277
|
" pending: false,",
|
|
203
278
|
"}",
|
|
204
|
-
|
|
205
|
-
|
|
279
|
+
...loadingLines,
|
|
280
|
+
// With `_loading` pages the store is wrapped: a slow navigation then names a loading chain (the
|
|
281
|
+
// shared layouts + the `_loading` component) for the mounted view to render in the meantime.
|
|
282
|
+
loadings.length > 0
|
|
283
|
+
? "const router = withLoading(createClientRouter({ patterns, initial, loadModule, statusRoutes, searchClientKeys, routeHooks }), { routes: loadingRoutes, modules: loadingModules, chains, searchSchemas, matchChains })"
|
|
284
|
+
: "const router = createClientRouter({ patterns, initial, loadModule, statusRoutes, searchClientKeys, routeHooks })",
|
|
206
285
|
"installHistory(router)",
|
|
207
286
|
"installForms(router)",
|
|
208
287
|
// The container is found in the DOM, not baked in. `rootId` is a per-render option and this entry
|
|
@@ -219,10 +298,45 @@ export function generateClientEntry(
|
|
|
219
298
|
"is set, `#root` otherwise - and this document has neither. If you emit the document yourself, " +
|
|
220
299
|
'keep `<div id="root">` around the SSR markup.',
|
|
221
300
|
)})`,
|
|
222
|
-
//
|
|
223
|
-
// is
|
|
224
|
-
"loadModule(initial.routeId).then(() => {",
|
|
225
|
-
|
|
301
|
+
// Wait for framework-owned deferred CSS before loading/mounting the initial route. The coordinator
|
|
302
|
+
// is a no-op for the default blocking links and always has a bounded fail-open terminal state.
|
|
303
|
+
"waitForStyles().then(() => loadModule(initial.routeId)).then(() => {",
|
|
304
|
+
// An `ssr = false` page: the server rendered its held chain, so the view hydrates THAT - under a
|
|
305
|
+
// chain id no route file can produce - and is handed the real route a task later, once every
|
|
306
|
+
// adapter has hydrated and subscribed. Only the first view is held; a client navigation to such a
|
|
307
|
+
// page has no server markup to match and renders the component directly.
|
|
308
|
+
" let view = router",
|
|
309
|
+
" const hold = holds[initial.routeId]",
|
|
310
|
+
" if (hold) {",
|
|
311
|
+
' const id = "\\0ssr"',
|
|
312
|
+
" chains[id] = hold",
|
|
313
|
+
" searchSchemas[id] = searchSchemas[initial.routeId]",
|
|
314
|
+
" matchChains[id] = matchChains[initial.routeId]",
|
|
315
|
+
" const listeners = new Set()",
|
|
316
|
+
" let held = true",
|
|
317
|
+
" let base",
|
|
318
|
+
" let derived",
|
|
319
|
+
" view = {",
|
|
320
|
+
" ...router,",
|
|
321
|
+
" snapshot() {",
|
|
322
|
+
" const s = router.snapshot()",
|
|
323
|
+
" if (!held) return s",
|
|
324
|
+
// One derived state per router state: the view's store must hand back a stable reference.
|
|
325
|
+
" if (base !== s) derived = { ...(base = s), routeId: id }",
|
|
326
|
+
" return derived",
|
|
327
|
+
" },",
|
|
328
|
+
" subscribe(listener) {",
|
|
329
|
+
" listeners.add(listener)",
|
|
330
|
+
" const off = router.subscribe(listener)",
|
|
331
|
+
" return () => (listeners.delete(listener), off())",
|
|
332
|
+
" },",
|
|
333
|
+
" }",
|
|
334
|
+
" setTimeout(() => {",
|
|
335
|
+
" held = false",
|
|
336
|
+
" for (const listener of [...listeners]) listener()",
|
|
337
|
+
" }, 0)",
|
|
338
|
+
" }",
|
|
339
|
+
" mountRouter({ router: view, routes: chains, searchSchemas, matchChains, container: root })",
|
|
226
340
|
// Run the optional client loader only after the adapter has mounted the SSR tree. The initial
|
|
227
341
|
// server data is already in `window.__NIFRA_DATA__`, so `serverLoader()` reuses it and cannot
|
|
228
342
|
// duplicate the first request.
|
|
@@ -236,11 +350,12 @@ export function generateClientEntry(
|
|
|
236
350
|
" if (!s.pending) {",
|
|
237
351
|
// `origin: location.origin` matches the SSR `originOf(req)` (both are `URL.origin` for the same
|
|
238
352
|
// page URL), so a soft-nav re-resolves the SAME absolute canonical/og:url - no head drift.
|
|
239
|
-
" const
|
|
353
|
+
" const nonce = currentDocumentNonce()",
|
|
354
|
+
" const args = { data: s.data, params: s.params, origin: location.origin, ...(nonce === undefined ? {} : { nonce }) }",
|
|
240
355
|
" applyHead(mergeHeads((metas[s.routeId] ?? [undefined]).map((m) => resolveMeta(m, args))))",
|
|
241
356
|
" }",
|
|
242
357
|
" })",
|
|
243
|
-
"})",
|
|
358
|
+
"}).catch((error) => console.error('[nifra/web] hydration failed:', error))",
|
|
244
359
|
].join("\n")}\n`
|
|
245
360
|
}
|
|
246
361
|
|
|
@@ -305,6 +420,8 @@ export interface GenerateServerManifestOptions {
|
|
|
305
420
|
/** Per-route stylesheet URLs (`buildClient`'s `BuildManifest.routeStyles`) - baked as
|
|
306
421
|
* `export const routeStyles` so a page links only the CSS its route chain uses. */
|
|
307
422
|
readonly routeStyles?: Readonly<Record<string, readonly string[]>> | undefined
|
|
423
|
+
/** How framework-owned stylesheet links should activate before the generated client mounts. */
|
|
424
|
+
readonly cssLoading?: CssLoadingMode
|
|
308
425
|
/** Emit **lazy** per-route loaders (`() => import("./routes/x")`, a static specifier) instead of
|
|
309
426
|
* eager `import * as`, so a bundler with code-splitting emits one chunk per route - loaded on the
|
|
310
427
|
* first request to it, not all at boot (smaller cold-start parse). Default `false` (eager). Both
|
|
@@ -316,7 +433,8 @@ export interface GenerateServerManifestOptions {
|
|
|
316
433
|
* Codegen: emit a **server manifest** module (as source) for disk-less edge runtimes (Cloudflare
|
|
317
434
|
* Workers, …) - and, with a `target`, any portable server bundle. `discoverRoutes` scans `node:fs`
|
|
318
435
|
* and dynamic-imports each route by a *runtime* path - neither exists on workerd. This instead emits
|
|
319
|
-
* **statically-analyzable** imports of every route/layout/`_error`/terminal status page
|
|
436
|
+
* **statically-analyzable** imports of every route/layout/`_error`/terminal status page and each one's
|
|
437
|
+
* backend half (so the bundler includes them) and
|
|
320
438
|
* rebuilds the manifest with `buildManifest` - the SAME pure logic `discoverRoutes` feeds, so patterns
|
|
321
439
|
* + layout chains are identical. Eager (`import * as`) by default; `lazy` emits `() => import(...)` so
|
|
322
440
|
* a code-splitting bundler chunks per route. The emitted module exports `manifest` (consumed by
|
|
@@ -335,20 +453,50 @@ function moduleSpecifier(resolved: string): string {
|
|
|
335
453
|
return resolved.replace(SOURCE_EXTENSION, "")
|
|
336
454
|
}
|
|
337
455
|
|
|
456
|
+
// A route's namespace is not assignable to `RouteModule` under a strict `tsc` (a backend half has no
|
|
457
|
+
// `default`, a typed `meta` narrows its data), so the table holds plain objects and the cast happens at
|
|
458
|
+
// `buildManifest`'s boundary - the same cast `discoverRoutes` applies to its dynamic import.
|
|
459
|
+
const LAZY_IMPORTER = "(file) => loaders[file] as () => Promise<RouteModule>"
|
|
460
|
+
const EAGER_IMPORTER = "(file) => () => Promise.resolve(modules[file] as RouteModule)"
|
|
461
|
+
|
|
338
462
|
export function generateServerManifest(
|
|
339
463
|
manifest: Manifest,
|
|
340
464
|
options: GenerateServerManifestOptions,
|
|
341
465
|
): string {
|
|
342
|
-
const {
|
|
343
|
-
|
|
466
|
+
const {
|
|
467
|
+
resolve,
|
|
468
|
+
clientEntry,
|
|
469
|
+
styles,
|
|
470
|
+
routeStyles,
|
|
471
|
+
cssLoading: requestedCssLoading,
|
|
472
|
+
lazy = false,
|
|
473
|
+
} = options
|
|
474
|
+
// A missing entry would bake `clientEntry = undefined` and the build would still succeed, leaving
|
|
475
|
+
// every hydrating page to fail at request time. `""` stays valid: an app with no client script.
|
|
476
|
+
if (typeof clientEntry !== "string") {
|
|
477
|
+
throw new TypeError(
|
|
478
|
+
`[nifra/web] the server build needs clientEntry (the built client entry URL, buildClient's manifest.entry), got ${clientEntry === null ? "null" : typeof clientEntry}; check the option is spelled clientEntry, or pass "" for an app with no client script`,
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
const cssLoading = normalizeCssLoading(requestedCssLoading ?? DEFAULT_CSS_LOADING)
|
|
482
|
+
// Every unique source file in the manifest, both halves of each, sorted for stable output. This is the
|
|
483
|
+
// one generated module that imports frontend and backend halves together: it is the server's
|
|
484
|
+
// composition, and `buildManifest` pairs the halves again from these keys.
|
|
485
|
+
const sources = [
|
|
486
|
+
...manifest.routes,
|
|
487
|
+
...Object.values(manifest.layouts),
|
|
488
|
+
...Object.values(manifest.errors ?? {}),
|
|
489
|
+
...(manifest.notFound ? [manifest.notFound] : []),
|
|
490
|
+
...Object.values(manifest.notFounds ?? {}),
|
|
491
|
+
...Object.values(manifest.statusPages ?? {}),
|
|
492
|
+
...Object.values(manifest.middlewares ?? {}),
|
|
493
|
+
]
|
|
344
494
|
const files = [
|
|
345
|
-
...new Set(
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
...Object.values(manifest.statusPages ?? {}).map((page) => page.file),
|
|
351
|
-
]),
|
|
495
|
+
...new Set(
|
|
496
|
+
sources.flatMap((entry) =>
|
|
497
|
+
entry.backend === undefined ? [entry.file] : [entry.file, entry.backend],
|
|
498
|
+
),
|
|
499
|
+
),
|
|
352
500
|
].sort()
|
|
353
501
|
const header = [
|
|
354
502
|
"// GENERATED by @nifrajs/web generateServerManifest - route manifest for the disk-less edge",
|
|
@@ -361,6 +509,7 @@ export function generateServerManifest(
|
|
|
361
509
|
// Always emitted (default empty) so consumers can `import { styles, routeStyles }` unconditionally.
|
|
362
510
|
const stylesLine = `export const styles = ${JSON.stringify(styles ?? [])}`
|
|
363
511
|
const routeStylesLine = `export const routeStyles = ${JSON.stringify(routeStyles ?? {})}`
|
|
512
|
+
const cssLoadingLine = `export const cssLoading = ${JSON.stringify(cssLoading)}`
|
|
364
513
|
if (lazy) {
|
|
365
514
|
// Lazy: `() => import("./routes/x")` per route (static specifier → one chunk per route under a
|
|
366
515
|
// code-splitting bundler, loaded on first request). The map keys are the route-relative paths
|
|
@@ -371,13 +520,14 @@ export function generateServerManifest(
|
|
|
371
520
|
)
|
|
372
521
|
return `${[
|
|
373
522
|
...header,
|
|
374
|
-
"const loaders: Record<string, () => Promise<
|
|
523
|
+
"const loaders: Record<string, () => Promise<object>> = {",
|
|
375
524
|
...loaders,
|
|
376
525
|
"}",
|
|
377
526
|
clientEntryLine,
|
|
378
527
|
stylesLine,
|
|
379
528
|
routeStylesLine,
|
|
380
|
-
|
|
529
|
+
cssLoadingLine,
|
|
530
|
+
`export const manifest = buildManifest(Object.keys(loaders), ${LAZY_IMPORTER})`,
|
|
381
531
|
].join("\n")}\n`
|
|
382
532
|
}
|
|
383
533
|
// Eager: `import * as` per route (all bundled into the entry, parsed at boot). Index-based
|
|
@@ -389,12 +539,13 @@ export function generateServerManifest(
|
|
|
389
539
|
return `${[
|
|
390
540
|
...header,
|
|
391
541
|
...imports,
|
|
392
|
-
"const modules: Record<string,
|
|
542
|
+
"const modules: Record<string, object> = {",
|
|
393
543
|
...entries,
|
|
394
544
|
"}",
|
|
395
545
|
clientEntryLine,
|
|
396
546
|
stylesLine,
|
|
397
547
|
routeStylesLine,
|
|
398
|
-
|
|
548
|
+
cssLoadingLine,
|
|
549
|
+
`export const manifest = buildManifest(Object.keys(modules), ${EAGER_IMPORTER})`,
|
|
399
550
|
].join("\n")}\n`
|
|
400
551
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vite dev adds its own tags to each SSR'd page: the HMR client, a framework's refresh preamble (an
|
|
3
|
+
* inline script), and at runtime a `<style>` per imported stylesheet. A page whose CSP admits scripts
|
|
4
|
+
* by nonce or hash refuses all of them, so the app would not boot in dev. Dev gives every such tag one
|
|
5
|
+
* nonce - the page's own, or a fresh one for a hash-only page - and names it in each directive that
|
|
6
|
+
* governs them.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const POLICY_HEADERS = ["content-security-policy", "content-security-policy-report-only"] as const
|
|
10
|
+
|
|
11
|
+
/** Directives that govern each element kind, most specific first: the first present one applies. */
|
|
12
|
+
const GOVERNING = [
|
|
13
|
+
["script-src-elem", "script-src", "default-src"],
|
|
14
|
+
["style-src-elem", "style-src", "default-src"],
|
|
15
|
+
] as const
|
|
16
|
+
|
|
17
|
+
const NONCE_SOURCE = /'nonce-([^']+)'/
|
|
18
|
+
|
|
19
|
+
/** True when a directive admits every inline element: `'unsafe-inline'`, not voided by a nonce or hash. */
|
|
20
|
+
const allowsInline = (sources: readonly string[]): boolean =>
|
|
21
|
+
sources.includes("'unsafe-inline'") &&
|
|
22
|
+
!sources.some((source) => /^'(?:nonce|sha256|sha384|sha512)-/.test(source))
|
|
23
|
+
|
|
24
|
+
/** One header value (comma-separated policies), with `'nonce-<nonce>'` added where it is needed. */
|
|
25
|
+
function admitNonce(value: string, nonce: string): string {
|
|
26
|
+
const token = `'nonce-${nonce}'`
|
|
27
|
+
return value
|
|
28
|
+
.split(",")
|
|
29
|
+
.map((policy) => {
|
|
30
|
+
const directives = policy
|
|
31
|
+
.split(";")
|
|
32
|
+
.map((directive) => directive.trim())
|
|
33
|
+
.filter((directive) => directive !== "")
|
|
34
|
+
.map((directive) => directive.split(/\s+/))
|
|
35
|
+
for (const names of GOVERNING) {
|
|
36
|
+
const governing = names
|
|
37
|
+
.map((name) => directives.find(([directive]) => directive?.toLowerCase() === name))
|
|
38
|
+
.find((directive) => directive !== undefined)
|
|
39
|
+
if (governing === undefined) continue
|
|
40
|
+
const sources = governing.slice(1)
|
|
41
|
+
if (sources.includes(token) || sources.includes("'none'") || allowsInline(sources)) continue
|
|
42
|
+
governing.push(token)
|
|
43
|
+
}
|
|
44
|
+
return directives.map((directive) => directive.join(" ")).join("; ")
|
|
45
|
+
})
|
|
46
|
+
.join(", ")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const RAW_TEXT_START = /<(script|style)\b([^>]*)>/gi
|
|
50
|
+
|
|
51
|
+
/** `html` with `nonce` on each `<script>` / `<style>` that has none; their bodies are left as written. */
|
|
52
|
+
function nonceTags(html: string, nonce: string): string {
|
|
53
|
+
let out = ""
|
|
54
|
+
let at = 0
|
|
55
|
+
const lower = html.toLowerCase()
|
|
56
|
+
RAW_TEXT_START.lastIndex = 0
|
|
57
|
+
for (let match = RAW_TEXT_START.exec(html); match !== null; match = RAW_TEXT_START.exec(html)) {
|
|
58
|
+
const [tag, name = "", attrs = ""] = match
|
|
59
|
+
out += html.slice(at, match.index)
|
|
60
|
+
out += /\snonce\s*=/i.test(attrs) ? tag : `<${name} nonce="${nonce}"${attrs}>`
|
|
61
|
+
const close = lower.indexOf(`</${name.toLowerCase()}`, RAW_TEXT_START.lastIndex)
|
|
62
|
+
at = close === -1 ? html.length : close
|
|
63
|
+
out += html.slice(RAW_TEXT_START.lastIndex, at)
|
|
64
|
+
RAW_TEXT_START.lastIndex = at
|
|
65
|
+
}
|
|
66
|
+
return out + html.slice(at)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Let a dev page's CSP admit what Vite injected. Rewrites the policy headers in `headers` and returns
|
|
71
|
+
* the page with the nonce on its script and style tags, plus the `csp-nonce` meta Vite's client reads
|
|
72
|
+
* to nonce the styles it adds at runtime. A page with no CSP is returned as it is.
|
|
73
|
+
*/
|
|
74
|
+
export function admitViteTags(
|
|
75
|
+
html: string,
|
|
76
|
+
headers: Headers,
|
|
77
|
+
generate: () => string = () => crypto.randomUUID().replaceAll("-", ""),
|
|
78
|
+
): string {
|
|
79
|
+
const policies = POLICY_HEADERS.map((name) => [name, headers.get(name)] as const)
|
|
80
|
+
if (policies.every(([, value]) => value === null)) return html
|
|
81
|
+
const nonce =
|
|
82
|
+
policies.map(([, value]) => NONCE_SOURCE.exec(value ?? "")?.[1]).find((n) => n !== undefined) ??
|
|
83
|
+
generate()
|
|
84
|
+
for (const [name, value] of policies) {
|
|
85
|
+
if (value !== null) headers.set(name, admitNonce(value, nonce))
|
|
86
|
+
}
|
|
87
|
+
const tagged = nonceTags(html, nonce)
|
|
88
|
+
if (tagged.includes(`property="csp-nonce"`)) return tagged
|
|
89
|
+
const meta = `<meta property="csp-nonce" nonce="${nonce}">`
|
|
90
|
+
const head = /<head\b[^>]*>/i.exec(tagged)
|
|
91
|
+
return head === null
|
|
92
|
+
? meta + tagged
|
|
93
|
+
: tagged.slice(0, head.index + head[0].length) +
|
|
94
|
+
meta +
|
|
95
|
+
tagged.slice(head.index + head[0].length)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const SCRIPT_DIRECTIVES = GOVERNING[0]
|
|
99
|
+
const CONNECT_DIRECTIVES = ["connect-src", "default-src"] as const
|
|
100
|
+
|
|
101
|
+
/** Whether `sources` already let the page reach `url` (a same-origin absolute URL). */
|
|
102
|
+
const reaches = (sources: readonly string[], url: URL): boolean =>
|
|
103
|
+
sources.some((source) => {
|
|
104
|
+
const lower = source.toLowerCase()
|
|
105
|
+
return (
|
|
106
|
+
lower === "'self'" ||
|
|
107
|
+
lower === "*" ||
|
|
108
|
+
lower === `${url.protocol}` ||
|
|
109
|
+
lower === url.origin ||
|
|
110
|
+
lower === `${url.origin}/` ||
|
|
111
|
+
lower === url.href
|
|
112
|
+
)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Admit nifra's own inline dev script, by `hash`, and its POSTs to `connect`, in each policy header.
|
|
117
|
+
* Without a `hash` the caller nonces the script itself. Returns false when a policy forbids scripts
|
|
118
|
+
* outright (`'none'`): such a page runs no code, so there is nothing to capture and the script stays
|
|
119
|
+
* out. A directive that already admits every inline script is left alone, because adding a hash would
|
|
120
|
+
* switch its `'unsafe-inline'` off. `module` is the one script the inline one may load; it is admitted
|
|
121
|
+
* by its exact URL, except under `'strict-dynamic'`, which ignores URLs and trusts what a trusted
|
|
122
|
+
* script loads anyway.
|
|
123
|
+
*/
|
|
124
|
+
export function admitInlineScript(
|
|
125
|
+
headers: Headers,
|
|
126
|
+
hash: string | undefined,
|
|
127
|
+
connect: URL,
|
|
128
|
+
module?: URL,
|
|
129
|
+
): boolean {
|
|
130
|
+
const updates: [string, string][] = []
|
|
131
|
+
let allowed = true
|
|
132
|
+
for (const name of POLICY_HEADERS) {
|
|
133
|
+
const value = headers.get(name)
|
|
134
|
+
if (value === null) continue
|
|
135
|
+
const next = value
|
|
136
|
+
.split(",")
|
|
137
|
+
.map((policy) => {
|
|
138
|
+
const directives = policy
|
|
139
|
+
.split(";")
|
|
140
|
+
.map((directive) => directive.trim())
|
|
141
|
+
.filter((directive) => directive !== "")
|
|
142
|
+
.map((directive) => directive.split(/\s+/))
|
|
143
|
+
const governing = (names: readonly string[]): string[] | undefined =>
|
|
144
|
+
names
|
|
145
|
+
.map((n) => directives.find(([directive]) => directive?.toLowerCase() === n))
|
|
146
|
+
.find((directive) => directive !== undefined)
|
|
147
|
+
const script = governing(SCRIPT_DIRECTIVES)
|
|
148
|
+
if (script !== undefined) {
|
|
149
|
+
const sources = script.slice(1)
|
|
150
|
+
if (sources.includes("'none'")) allowed = false
|
|
151
|
+
else {
|
|
152
|
+
if (hash !== undefined && !sources.includes(hash) && !allowsInline(sources))
|
|
153
|
+
script.push(hash)
|
|
154
|
+
if (
|
|
155
|
+
module !== undefined &&
|
|
156
|
+
!sources.includes("'strict-dynamic'") &&
|
|
157
|
+
!reaches(sources, module)
|
|
158
|
+
)
|
|
159
|
+
script.push(module.href)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const connectDirective = governing(CONNECT_DIRECTIVES)
|
|
163
|
+
if (connectDirective !== undefined && !reaches(connectDirective.slice(1), connect)) {
|
|
164
|
+
const none = connectDirective.indexOf("'none'")
|
|
165
|
+
if (none === -1) connectDirective.push(connect.href)
|
|
166
|
+
else connectDirective.splice(none, 1, connect.href)
|
|
167
|
+
}
|
|
168
|
+
return directives.map((directive) => directive.join(" ")).join("; ")
|
|
169
|
+
})
|
|
170
|
+
.join(", ")
|
|
171
|
+
updates.push([name, next])
|
|
172
|
+
}
|
|
173
|
+
if (!allowed) return false
|
|
174
|
+
for (const [name, value] of updates) headers.set(name, value)
|
|
175
|
+
return true
|
|
176
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Specifiers no plugin may match on Bun's dev-server bundler.
|
|
3
|
+
*
|
|
4
|
+
* When a `[serve.static]` plugin's `onResolve` FILTER matches an import, Bun sends it down its plugin
|
|
5
|
+
* path, and a handler that declines does not undo that: Bun then resolves the import on a fallback that
|
|
6
|
+
* breaks a nifra dev page in two ways (Bun 1.3.14 through 1.4.3, oven-sh/bun#19951):
|
|
7
|
+
*
|
|
8
|
+
* - an HTML page's `<script src>` keeps its raw specifier in the HMR module table, so the browser throws
|
|
9
|
+
* "Failed to load bundled module" and the page never hydrates;
|
|
10
|
+
* - the fallback skips the dev server's built-in modules, so the React Fast Refresh runtime Bun embeds
|
|
11
|
+
* for an app without `react-refresh` installed fails to resolve, and the dev server does not start.
|
|
12
|
+
*
|
|
13
|
+
* unplugin's Bun adapter registers `onResolve({ filter: /.*\/ })` for every plugin with a `resolveId`
|
|
14
|
+
* hook, so an app plugin can hit both without doing anything unusual. Every plugin nifra hands that
|
|
15
|
+
* bundler registers through {@link reserveDevSpecifiers}.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { PluginBuilder } from "bun"
|
|
19
|
+
|
|
20
|
+
/** The generated client entry, written beside the probe page that loads it. Named so that no import
|
|
21
|
+
* an app writes can be the reserved specifier. */
|
|
22
|
+
export const DEV_ENTRY_FILE = "nifra-dev-entry.tsx"
|
|
23
|
+
|
|
24
|
+
const RESERVED = [`./${DEV_ENTRY_FILE}`, "react-refresh/runtime/index.js"]
|
|
25
|
+
const RESERVED_SOURCE = RESERVED.map((specifier) =>
|
|
26
|
+
specifier.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&"),
|
|
27
|
+
).join("|")
|
|
28
|
+
|
|
29
|
+
/** `filter`, except that it never matches a reserved specifier. Any other string matches as before. */
|
|
30
|
+
export function withoutReservedSpecifiers(filter: RegExp): RegExp {
|
|
31
|
+
// The original runs inside a lookahead from the start, so it still matches anywhere in the string
|
|
32
|
+
// and its own anchors, lookbehinds and backreferences keep their meaning. `g` and `y` are dropped:
|
|
33
|
+
// they make `test` depend on `lastIndex`, which a filter has no use for.
|
|
34
|
+
return new RegExp(
|
|
35
|
+
`^(?!(?:${RESERVED_SOURCE})$)(?=[\\s\\S]*?(?:${filter.source}))`,
|
|
36
|
+
filter.flags.replace(/[gy]/g, ""),
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A view of `build` whose `onResolve` filters never match a reserved specifier. */
|
|
41
|
+
export function reserveDevSpecifiers(build: PluginBuilder): PluginBuilder {
|
|
42
|
+
return {
|
|
43
|
+
...build,
|
|
44
|
+
onResolve(constraints, callback) {
|
|
45
|
+
build.onResolve(
|
|
46
|
+
{ ...constraints, filter: withoutReservedSpecifiers(constraints.filter) },
|
|
47
|
+
callback,
|
|
48
|
+
)
|
|
49
|
+
return this
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// The comment and delimiter readers scan instead of matching one lazy RegExp: a pattern like
|
|
2
|
+
// `<!--[\s\S]*?-->` reads to the end of the source again from every opener that never closes.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `source` without its HTML comments. A browser ends one at `-->`, and also at `--!>`; an opener
|
|
6
|
+
* with no end after it is kept as text.
|
|
7
|
+
*/
|
|
8
|
+
export function withoutComments(source: string): string {
|
|
9
|
+
const end = /--!?>/g
|
|
10
|
+
let kept = ""
|
|
11
|
+
let from = 0
|
|
12
|
+
for (let start = source.indexOf("<!--"); start !== -1; start = source.indexOf("<!--", from)) {
|
|
13
|
+
end.lastIndex = start + 4
|
|
14
|
+
const close = end.exec(source)
|
|
15
|
+
// No later opener has an end after it either.
|
|
16
|
+
if (close === null) break
|
|
17
|
+
kept += source.slice(from, start)
|
|
18
|
+
from = close.index + close[0].length
|
|
19
|
+
}
|
|
20
|
+
return kept + source.slice(from)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The text between each `open` and the first `close` after it, read on from that `close`. */
|
|
24
|
+
export function delimitedSpans(source: string, open: string, close: string): string[] {
|
|
25
|
+
const spans: string[] = []
|
|
26
|
+
for (let start = source.indexOf(open); start !== -1; ) {
|
|
27
|
+
const end = source.indexOf(close, start + open.length)
|
|
28
|
+
if (end === -1) break
|
|
29
|
+
spans.push(source.slice(start + open.length, end))
|
|
30
|
+
start = source.indexOf(open, end + close.length)
|
|
31
|
+
}
|
|
32
|
+
return spans
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** `source` without the spans the global `pattern` matches, each handed to `onMatch` first. */
|
|
36
|
+
export function withoutMatches(
|
|
37
|
+
source: string,
|
|
38
|
+
pattern: RegExp,
|
|
39
|
+
onMatch?: (match: RegExpMatchArray) => void,
|
|
40
|
+
): string {
|
|
41
|
+
let kept = ""
|
|
42
|
+
let from = 0
|
|
43
|
+
for (const match of source.matchAll(pattern)) {
|
|
44
|
+
onMatch?.(match)
|
|
45
|
+
const start = match.index ?? 0
|
|
46
|
+
kept += source.slice(from, start)
|
|
47
|
+
from = start + match[0].length
|
|
48
|
+
}
|
|
49
|
+
return kept + source.slice(from)
|
|
50
|
+
}
|