@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/diagnostic.ts
CHANGED
|
@@ -14,6 +14,22 @@
|
|
|
14
14
|
import { readFileSync, realpathSync } from "node:fs"
|
|
15
15
|
import { isAbsolute, relative, resolve } from "node:path"
|
|
16
16
|
import { fileURLToPath } from "node:url"
|
|
17
|
+
import { classify, type FixOption } from "./diagnostic-catalog.ts"
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
classify,
|
|
21
|
+
DIAGNOSTIC_CATALOG,
|
|
22
|
+
type FixOption,
|
|
23
|
+
isHydrationMismatch,
|
|
24
|
+
} from "./diagnostic-catalog.ts"
|
|
25
|
+
export {
|
|
26
|
+
buildFixPrompt,
|
|
27
|
+
type FixPrompt,
|
|
28
|
+
type FixPromptContext,
|
|
29
|
+
type FixPromptSurface,
|
|
30
|
+
fixPrompts,
|
|
31
|
+
promptPath,
|
|
32
|
+
} from "./diagnostic-prompt.ts"
|
|
17
33
|
|
|
18
34
|
/** Shared endpoint name used by both dev pipelines and the agent-facing MCP tools. */
|
|
19
35
|
export const LAST_ERROR_PATH = "/__nifra/last-error"
|
|
@@ -40,7 +56,7 @@ export interface Codeframe {
|
|
|
40
56
|
|
|
41
57
|
/** The structured failure. Serialisable as-is to JSON for the agent surfaces. */
|
|
42
58
|
export interface Diagnostic {
|
|
43
|
-
/** Stable, greppable identifier, e.g. `
|
|
59
|
+
/** Stable, greppable identifier, e.g. `NIFRA_BACKEND_IN_CLIENT`. `NIFRA_UNHANDLED` when unrecognised. */
|
|
44
60
|
readonly code: string
|
|
45
61
|
readonly name: string
|
|
46
62
|
readonly message: string
|
|
@@ -51,8 +67,10 @@ export interface Diagnostic {
|
|
|
51
67
|
readonly cause?: string | undefined
|
|
52
68
|
/** Plain-language "do this", when the failure is recognised. */
|
|
53
69
|
readonly fix?: string | undefined
|
|
54
|
-
/** Docs section anchor for the code, e.g. `errors#
|
|
70
|
+
/** Docs section anchor for the code, e.g. `errors#backend-in-client`. */
|
|
55
71
|
readonly docsAnchor?: string | undefined
|
|
72
|
+
/** The distinct ways to fix it, when more than one is right; `fix` covers them all in one line. */
|
|
73
|
+
readonly fixOptions?: readonly FixOption[] | undefined
|
|
56
74
|
}
|
|
57
75
|
|
|
58
76
|
/** Reads a source file's text, or returns undefined if it cannot (missing, binary, permission). */
|
|
@@ -178,61 +196,6 @@ export function buildCodeframe(
|
|
|
178
196
|
return { file, line, column, lines }
|
|
179
197
|
}
|
|
180
198
|
|
|
181
|
-
/** A recognised failure shape: a stable code plus the plain-language cause/fix/anchor to attach. */
|
|
182
|
-
interface CatalogEntry {
|
|
183
|
-
readonly code: string
|
|
184
|
-
readonly match: (name: string, message: string) => boolean
|
|
185
|
-
readonly cause: string
|
|
186
|
-
readonly fix: string
|
|
187
|
-
readonly docsAnchor: string
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* The recognised-failure catalog. Seeded with the highest-signal nifra failures; extend it as new
|
|
192
|
-
* classes of error earn a stable code. Order matters only in that the first match wins.
|
|
193
|
-
*/
|
|
194
|
-
export const DIAGNOSTIC_CATALOG: readonly CatalogEntry[] = [
|
|
195
|
-
{
|
|
196
|
-
code: "NIFRA_SERVER_ONLY_IN_CLIENT",
|
|
197
|
-
match: (_n, m) => m.includes("server-only module(s) in the client bundle"),
|
|
198
|
-
cause:
|
|
199
|
-
"A module marked server-only was reachable from a client entry, so it would ship to the browser.",
|
|
200
|
-
fix: "Follow the import chain in the message and move the server-only use behind a loader/action or a `*.server.ts` boundary, so it never enters a client component.",
|
|
201
|
-
docsAnchor: "errors#server-only-in-client",
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
code: "NIFRA_NODE_BUILTIN_IN_CLIENT",
|
|
205
|
-
match: (_n, m) => m.includes("Node built-in(s) in the client bundle"),
|
|
206
|
-
cause: "A `node:` built-in was reached from a client entry; it has no browser implementation.",
|
|
207
|
-
fix: "Move the code using the built-in to the server (loader/action or `*.server.ts`); the message lists the import chain that pulled it in.",
|
|
208
|
-
docsAnchor: "errors#node-builtin-in-client",
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
code: "NIFRA_SCHEMA_PARSE",
|
|
212
|
-
match: (n, m) =>
|
|
213
|
-
n === "SchemaError" ||
|
|
214
|
-
/failed to (parse|validate)|invalid_type|expected .* received/i.test(m),
|
|
215
|
-
cause: "Data crossing a boundary did not match its declared schema.",
|
|
216
|
-
fix: "Check the value against the schema at the failing boundary (loader input, search params, or request body); parse-don't-cast means the shape must match exactly.",
|
|
217
|
-
docsAnchor: "errors#schema-parse",
|
|
218
|
-
},
|
|
219
|
-
]
|
|
220
|
-
|
|
221
|
-
/** Classify an error name+message against the catalog; falls back to the generic unhandled code. */
|
|
222
|
-
export function classify(
|
|
223
|
-
name: string,
|
|
224
|
-
message: string,
|
|
225
|
-
): {
|
|
226
|
-
code: string
|
|
227
|
-
cause?: string
|
|
228
|
-
fix?: string
|
|
229
|
-
docsAnchor?: string
|
|
230
|
-
} {
|
|
231
|
-
const hit = DIAGNOSTIC_CATALOG.find((e) => e.match(name, message))
|
|
232
|
-
if (hit === undefined) return { code: "NIFRA_UNHANDLED" }
|
|
233
|
-
return { code: hit.code, cause: hit.cause, fix: hit.fix, docsAnchor: hit.docsAnchor }
|
|
234
|
-
}
|
|
235
|
-
|
|
236
199
|
/** Split an Error's `stack` into its leading message block and its frame lines. */
|
|
237
200
|
function messageAndStack(err: Error): { message: string; stack: string } {
|
|
238
201
|
const stack = err.stack ?? `${err.name}: ${err.message}`
|
|
@@ -251,6 +214,37 @@ export interface BuildDiagnosticOptions {
|
|
|
251
214
|
readonly root?: string
|
|
252
215
|
/** Injectable source reader (tests pass a fake; production reads the filesystem). */
|
|
253
216
|
readonly read?: SourceReader
|
|
217
|
+
/** Whether a file's source may appear in the codeframe, given the path the codeframe would read. The
|
|
218
|
+
* overlay is served to a browser, so a dev server passes the zone check here and backend source never
|
|
219
|
+
* renders in it. */
|
|
220
|
+
readonly showSource?: (file: string) => boolean
|
|
221
|
+
/** A frame's authored position when its file was compiled by a plugin whose map the runtime did not
|
|
222
|
+
* apply (Bun ignores a plugin's inline map), so a compiled line never lands in the codeframe. */
|
|
223
|
+
readonly remap?: (
|
|
224
|
+
file: string,
|
|
225
|
+
line: number,
|
|
226
|
+
column: number,
|
|
227
|
+
) => { readonly line: number; readonly column: number } | undefined
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function remapFrames(
|
|
231
|
+
frames: DiagnosticFrame[],
|
|
232
|
+
remap: BuildDiagnosticOptions["remap"],
|
|
233
|
+
): DiagnosticFrame[] {
|
|
234
|
+
if (remap === undefined) return frames
|
|
235
|
+
return frames.map((frame) => {
|
|
236
|
+
if (frame.file === undefined || frame.line === undefined || frame.column === undefined) {
|
|
237
|
+
return frame
|
|
238
|
+
}
|
|
239
|
+
const authored = remap(frame.file, frame.line, frame.column)
|
|
240
|
+
if (authored === undefined) return frame
|
|
241
|
+
const at = frame.raw.lastIndexOf(`:${frame.line}:${frame.column}`)
|
|
242
|
+
const raw =
|
|
243
|
+
at === -1
|
|
244
|
+
? frame.raw
|
|
245
|
+
: `${frame.raw.slice(0, at)}:${authored.line}:${authored.column}${frame.raw.slice(at + `:${frame.line}:${frame.column}`.length)}`
|
|
246
|
+
return { raw, file: frame.file, line: authored.line, column: authored.column }
|
|
247
|
+
})
|
|
254
248
|
}
|
|
255
249
|
|
|
256
250
|
/**
|
|
@@ -262,18 +256,20 @@ export function buildDiagnostic(err: unknown, options: BuildDiagnosticOptions =
|
|
|
262
256
|
const error = err instanceof Error ? err : new Error(String(err))
|
|
263
257
|
const root = options.root ?? safeCwd()
|
|
264
258
|
const { message } = messageAndStack(error)
|
|
265
|
-
const frames = parseFrames(error.stack ?? "")
|
|
259
|
+
const frames = remapFrames(parseFrames(error.stack ?? ""), options.remap)
|
|
266
260
|
const top = topUserFrame(frames, root)
|
|
261
|
+
// The gate sees the path that will be read: a frame's text can come from a browser.
|
|
262
|
+
const source =
|
|
263
|
+
top?.file === undefined
|
|
264
|
+
? undefined
|
|
265
|
+
: options.read === undefined
|
|
266
|
+
? canonicalPath(top.file)
|
|
267
|
+
: top.file
|
|
267
268
|
const codeframe =
|
|
268
|
-
|
|
269
|
-
? buildCodeframe(
|
|
270
|
-
options.read === undefined ? canonicalPath(top.file) : top.file,
|
|
271
|
-
top.line,
|
|
272
|
-
top.column,
|
|
273
|
-
options.read,
|
|
274
|
-
)
|
|
269
|
+
source !== undefined && top?.line !== undefined && (options.showSource?.(source) ?? true)
|
|
270
|
+
? buildCodeframe(source, top.line, top.column, options.read)
|
|
275
271
|
: undefined
|
|
276
|
-
const { code, cause, fix, docsAnchor } = classify(error.name || "Error", message)
|
|
272
|
+
const { code, cause, fix, docsAnchor, fixOptions } = classify(error.name || "Error", message)
|
|
277
273
|
return {
|
|
278
274
|
code,
|
|
279
275
|
name: error.name || "Error",
|
|
@@ -284,6 +280,7 @@ export function buildDiagnostic(err: unknown, options: BuildDiagnosticOptions =
|
|
|
284
280
|
cause,
|
|
285
281
|
fix,
|
|
286
282
|
docsAnchor,
|
|
283
|
+
fixOptions,
|
|
287
284
|
}
|
|
288
285
|
}
|
|
289
286
|
|
package/src/draft.ts
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
signValue,
|
|
22
22
|
unsignValue,
|
|
23
23
|
} from "@nifrajs/core/server"
|
|
24
|
-
import { timingSafeEqual } from "./internal/timing-safe-equal.ts"
|
|
24
|
+
import { assertTokenSecret, timingSafeEqual } from "./internal/timing-safe-equal.ts"
|
|
25
25
|
|
|
26
26
|
/** The cookie name nifra uses for draft/preview mode. */
|
|
27
27
|
export const DRAFT_COOKIE = "__nifra_draft"
|
|
@@ -160,6 +160,9 @@ export interface PreviewEndpointOptions {
|
|
|
160
160
|
export function previewEndpoint(
|
|
161
161
|
options: PreviewEndpointOptions,
|
|
162
162
|
): (request: Request) => Promise<Response> {
|
|
163
|
+
// An empty token matches a link with no `?token=` at all, which would hand draft mode (and every
|
|
164
|
+
// unpublished page) to anyone. Refuse it at construction, where an unset env var surfaces at boot.
|
|
165
|
+
assertTokenSecret(options.secret, "previewEndpoint")
|
|
163
166
|
const tokenParam = options.tokenParam ?? "token"
|
|
164
167
|
const redirectParam = options.redirectParam ?? "to"
|
|
165
168
|
const fallbackPath = options.fallbackPath ?? "/"
|
package/src/fn.ts
CHANGED
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
type RouteSchema,
|
|
54
54
|
type StandardSchemaV1,
|
|
55
55
|
} from "@nifrajs/core/server"
|
|
56
|
+
import { guardChannel, outputGuard } from "./internal/output-guard.ts"
|
|
56
57
|
|
|
57
58
|
/** The URL prefix every mounted function lives under. Namespaced per mount, then by export name. */
|
|
58
59
|
export const SERVER_FN_PREFIX = "/_nifra/fn"
|
|
@@ -66,6 +67,11 @@ export interface ServerFnConfig<Input> {
|
|
|
66
67
|
* the caller controls this value completely, so an unvalidated one is an open door.
|
|
67
68
|
*/
|
|
68
69
|
readonly input?: StandardSchemaV1<unknown, Input>
|
|
70
|
+
/**
|
|
71
|
+
* Describes what the function returns to the browser. Required whenever it returns data: keys the
|
|
72
|
+
* schema does not declare are dropped, and a declared field of the wrong shape fails the call.
|
|
73
|
+
*/
|
|
74
|
+
readonly output?: StandardSchemaV1
|
|
69
75
|
/** Effect tokens, forwarded to the route so `nifra assure` and the effect ledger see them. */
|
|
70
76
|
readonly capabilities?: readonly string[]
|
|
71
77
|
}
|
|
@@ -172,6 +178,13 @@ export function serverFunctions(namespace: string, module: ServerFnModule): Iden
|
|
|
172
178
|
)
|
|
173
179
|
}
|
|
174
180
|
const config = fn[SERVER_FN]
|
|
181
|
+
const label = `server function "${namespace}.${name}"`
|
|
182
|
+
const guarded = guardChannel(fn, {
|
|
183
|
+
label,
|
|
184
|
+
guard:
|
|
185
|
+
config.output === undefined ? undefined : outputGuard(config.output, label, "output"),
|
|
186
|
+
missing: "an output schema in its serverFn({ output }) config",
|
|
187
|
+
})
|
|
175
188
|
const schema: RouteSchema = {
|
|
176
189
|
...(config.input !== undefined ? { body: config.input } : {}),
|
|
177
190
|
...(config.capabilities !== undefined ? { capabilities: config.capabilities } : {}),
|
|
@@ -182,17 +195,20 @@ export function serverFunctions(namespace: string, module: ServerFnModule): Iden
|
|
|
182
195
|
// has happened yet, so rejecting here is a complete refusal.
|
|
183
196
|
const request = c.req
|
|
184
197
|
const contentType = request.headers.get("content-type") ?? ""
|
|
185
|
-
|
|
198
|
+
const semi = contentType.indexOf(";")
|
|
199
|
+
const mediaType = (semi === -1 ? contentType : contentType.slice(0, semi)).trim()
|
|
200
|
+
if (mediaType.toLowerCase() !== "application/json") {
|
|
186
201
|
// A form cannot send this content type cross-origin without a preflight. Rejecting anything
|
|
187
202
|
// else is what makes form-driven CSRF against a server function impossible rather than
|
|
188
|
-
// merely unlikely.
|
|
203
|
+
// merely unlikely. Compare the media type, never search the header: `text/plain;
|
|
204
|
+
// x=application/json` contains the string but is a no-preflight `text/plain` request.
|
|
189
205
|
return c.json({ ok: false, error: "unsupported_media_type" }, 415)
|
|
190
206
|
}
|
|
191
207
|
const origin = request.headers.get("origin")
|
|
192
208
|
if (origin !== null && !sameOrigin(origin, request)) {
|
|
193
209
|
return c.json({ ok: false, error: "forbidden_origin" }, 403)
|
|
194
210
|
}
|
|
195
|
-
return
|
|
211
|
+
return guarded(config.input === undefined ? undefined : c.body, c)
|
|
196
212
|
}) as (context: never) => unknown)
|
|
197
213
|
}
|
|
198
214
|
return app
|
package/src/fs.ts
CHANGED
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { readdirSync } from "node:fs"
|
|
7
7
|
import { join } from "node:path"
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
buildManifest,
|
|
10
|
+
isMiddlewareFile,
|
|
11
|
+
isRouteBackendFile,
|
|
12
|
+
type Manifest,
|
|
13
|
+
type RouteModule,
|
|
14
|
+
} from "./manifest.ts"
|
|
9
15
|
|
|
10
16
|
/** Options for {@link discoverRoutes}. */
|
|
11
17
|
export interface DiscoverRoutesOptions {
|
|
@@ -31,7 +37,12 @@ export interface DiscoverRoutesOptions {
|
|
|
31
37
|
export function discoverRoutes(dir: string, options: DiscoverRoutesOptions = {}): Manifest {
|
|
32
38
|
const files = (readdirSync(dir, { recursive: true }) as string[])
|
|
33
39
|
.map((file) => file.replaceAll("\\", "/")) // normalize Windows separators
|
|
34
|
-
.filter(
|
|
40
|
+
.filter(
|
|
41
|
+
(file) =>
|
|
42
|
+
/\.(tsx|jsx|svelte|vue|mdx)$/.test(file) ||
|
|
43
|
+
isRouteBackendFile(file) ||
|
|
44
|
+
isMiddlewareFile(file),
|
|
45
|
+
)
|
|
35
46
|
const query = options.importQuery ? `?${options.importQuery}` : ""
|
|
36
47
|
const load = options.load
|
|
37
48
|
return buildManifest(files, (file) =>
|
package/src/index.ts
CHANGED
|
@@ -41,6 +41,20 @@ export {
|
|
|
41
41
|
RenderAdapterConformanceError,
|
|
42
42
|
type RenderAdapterConformanceFixture,
|
|
43
43
|
} from "./conformance.ts"
|
|
44
|
+
// Hash-based CSP: a document carries a nonce only when it needs one, so a CSP page stays cacheable.
|
|
45
|
+
export {
|
|
46
|
+
type CreateCspPolicyOptions,
|
|
47
|
+
type CspHeaderContext,
|
|
48
|
+
type CspPolicy,
|
|
49
|
+
createCspPolicy,
|
|
50
|
+
nifraScriptHashes,
|
|
51
|
+
} from "./csp.ts"
|
|
52
|
+
export {
|
|
53
|
+
assertCssLoadingCompatible,
|
|
54
|
+
type CssLoadingMode,
|
|
55
|
+
DEFAULT_CSS_LOADING,
|
|
56
|
+
normalizeCssLoading,
|
|
57
|
+
} from "./css-contract.ts"
|
|
44
58
|
// Deferred loader data (`defer()` + the `Deferred<T>` type) - consumed by the adapter's `<Await>`.
|
|
45
59
|
export { type Deferred, defer } from "./deferred.ts"
|
|
46
60
|
export {
|
|
@@ -96,42 +110,46 @@ export {
|
|
|
96
110
|
unsafeInlineScript,
|
|
97
111
|
} from "./internal/render-document.ts"
|
|
98
112
|
export {
|
|
113
|
+
type BackendOnly,
|
|
99
114
|
DEFAULT_DEV_PORT,
|
|
100
115
|
PRE_HYDRATION_GUARD,
|
|
101
|
-
type ServerOnly,
|
|
102
116
|
} from "./internal/runtime-contract.ts"
|
|
103
117
|
/**
|
|
104
|
-
* The
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* bundler takes no plugins) so it must REFUSE instead, and a refusal driven by its own hand-written
|
|
109
|
-
* glob drifts from the transform - which is how `.fn.mts` came to be stubbed by both build pipelines
|
|
110
|
-
* and waved through by the guard that exists to stop it leaking. Anything deciding "is this module
|
|
111
|
-
* server-only" should import the matcher rather than re-encode it.
|
|
118
|
+
* The `*.fn` server-function convention: the browser build REPLACES such a module with client stubs.
|
|
119
|
+
* Exported so anything deciding "is this a server-function module" imports the matcher instead of
|
|
120
|
+
* re-encoding it - a hand-written glob drifts from the transform, which is how `.fn.mts` was once
|
|
121
|
+
* stubbed by both build pipelines and waved through by the guard meant to stop it leaking.
|
|
112
122
|
*/
|
|
113
|
-
export { SERVER_FN_MODULE
|
|
123
|
+
export { SERVER_FN_MODULE } from "./internal/server-boundary.ts"
|
|
114
124
|
export {
|
|
115
125
|
type CreateWebAppOptions,
|
|
116
126
|
createWebApp,
|
|
127
|
+
type NonceResolver,
|
|
128
|
+
webProjectEvidence,
|
|
117
129
|
} from "./internal/web-app.ts"
|
|
118
130
|
// ISR (incremental static regeneration): a pluggable cache store + the `withISR` stale-while-revalidate
|
|
119
131
|
// wrapper for rendered SSR responses.
|
|
120
132
|
export {
|
|
121
133
|
type CachedResponse,
|
|
122
134
|
type CacheStore,
|
|
135
|
+
type CdnPurgeOutcome,
|
|
136
|
+
type CdnPurgeTarget,
|
|
123
137
|
ISR_REVALIDATE_HEADER,
|
|
124
138
|
ISR_REVALIDATE_TAGS_HEADER,
|
|
125
139
|
ISR_STATUS_HEADER,
|
|
126
140
|
type ISRApp,
|
|
127
141
|
type ISROptions,
|
|
128
142
|
type ISRPlatform,
|
|
143
|
+
type ISRQuery,
|
|
144
|
+
isCacheablePage,
|
|
129
145
|
KVCacheStore,
|
|
130
146
|
type KVCacheStoreOptions,
|
|
131
147
|
type KVNamespaceLike,
|
|
132
148
|
MemoryCacheStore,
|
|
133
149
|
type MemoryCacheStoreOptions,
|
|
134
150
|
type RevalidateEndpointOptions,
|
|
151
|
+
type RevalidateTags,
|
|
152
|
+
type RevalidateTagsInput,
|
|
135
153
|
revalidateEndpoint,
|
|
136
154
|
withISR,
|
|
137
155
|
} from "./isr.ts"
|
|
@@ -155,14 +173,22 @@ export {
|
|
|
155
173
|
type LinkDescriptor,
|
|
156
174
|
type Loader,
|
|
157
175
|
type LoaderContext,
|
|
176
|
+
type LoaderResponseControls,
|
|
177
|
+
type LoadingEntry,
|
|
158
178
|
type Manifest,
|
|
159
179
|
type Meta,
|
|
160
180
|
type MetaArgs,
|
|
161
181
|
type MetaDescriptor,
|
|
162
182
|
type MetaInput,
|
|
183
|
+
type MiddlewareOutcome,
|
|
184
|
+
type NotFoundEntry,
|
|
185
|
+
type NotFoundScope,
|
|
163
186
|
type RouteEntry,
|
|
187
|
+
type RouteMiddleware,
|
|
164
188
|
type RouteModule,
|
|
165
189
|
type ScriptDescriptor,
|
|
190
|
+
type ShouldRevalidate,
|
|
191
|
+
type ShouldRevalidateArgs,
|
|
166
192
|
type StaticPath,
|
|
167
193
|
type StaticPaths,
|
|
168
194
|
type StaticRoutes,
|
|
@@ -183,12 +209,21 @@ export {
|
|
|
183
209
|
type NavigateOptions,
|
|
184
210
|
type NavigateSearchOf,
|
|
185
211
|
type NavigateTargetInput,
|
|
212
|
+
type PrefetchMode,
|
|
186
213
|
type RouteSearch,
|
|
187
214
|
registerBlocker,
|
|
188
215
|
resolveNavigate,
|
|
189
216
|
setBlockerController,
|
|
190
217
|
setBrowserNavigate,
|
|
191
218
|
} from "./navigation.ts"
|
|
219
|
+
export {
|
|
220
|
+
type CreateNonceResolverOptions,
|
|
221
|
+
createNonceResolver,
|
|
222
|
+
type NonceContext,
|
|
223
|
+
type NonceGenerator,
|
|
224
|
+
type NonceHeader,
|
|
225
|
+
type NonceHeaderContext,
|
|
226
|
+
} from "./nonce.ts"
|
|
192
227
|
// public/ - user-authored static files, served identically in dev and production by one handler.
|
|
193
228
|
export {
|
|
194
229
|
type PublicDirCache,
|
|
@@ -221,14 +256,18 @@ export {
|
|
|
221
256
|
ACTION_GLOBAL,
|
|
222
257
|
BOUNDARY_GLOBAL,
|
|
223
258
|
DATA_GLOBAL,
|
|
259
|
+
HANDOVER_ID,
|
|
224
260
|
LAYOUT_DATA_GLOBAL,
|
|
261
|
+
type MatchChain,
|
|
225
262
|
type RenderAdapter,
|
|
226
263
|
type RenderProps,
|
|
264
|
+
type RenderStreamOptions,
|
|
227
265
|
ROOT_ATTRIBUTE,
|
|
228
266
|
ROUTE_GLOBAL,
|
|
229
267
|
type SsrModuleLoader,
|
|
230
268
|
setSsrModuleLoader,
|
|
231
269
|
ssrModuleLoader,
|
|
270
|
+
type UIMatch,
|
|
232
271
|
} from "./render-seam.ts"
|
|
233
272
|
// Agnostic client-side router core (pure + DOM-free) - consumed by per-adapter Router bindings.
|
|
234
273
|
// `DATA_HEADER` marks a navigation's data-only GET; `createWebApp` answers it with loader JSON.
|
|
@@ -258,3 +297,11 @@ export {
|
|
|
258
297
|
// ctx + `renderPage`) and a client adapter mount call this with the same URL + schema, so the two sides
|
|
259
298
|
// produce the identical value by construction. An adapter's `useSearch` binding reads its result.
|
|
260
299
|
export { type SearchOf, searchOf, searchOfChain, serializeSearch } from "./search.ts"
|
|
300
|
+
export {
|
|
301
|
+
type HtmlSanitizer,
|
|
302
|
+
type SanitizedHtml,
|
|
303
|
+
sanitizedHtml,
|
|
304
|
+
sanitizeHtml,
|
|
305
|
+
type TrustedHtml,
|
|
306
|
+
trustHtml,
|
|
307
|
+
} from "./trusted-html.ts"
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare: a proxied zone in front of the origin, and Workers Cache in front of a Worker. Both read
|
|
3
|
+
* `Cache-Tag` and `Cloudflare-CDN-Cache-Control` and strip them before the browser; the zone purges
|
|
4
|
+
* through the API (at most 100 tags a call; the Free plan allows 5 calls a minute), Workers Cache
|
|
5
|
+
* through the Worker's own `cache.purge` (always Free-plan limits).
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
attemptFromStatus,
|
|
9
|
+
type CdnCacheInput,
|
|
10
|
+
type CdnProvider,
|
|
11
|
+
cdnDirectives,
|
|
12
|
+
defineCdnProvider,
|
|
13
|
+
type PurgeQueueOptions,
|
|
14
|
+
} from "./core.ts"
|
|
15
|
+
|
|
16
|
+
const headersFor = (input: CdnCacheInput): Record<string, string> => ({
|
|
17
|
+
"cache-tag": input.tags.join(","),
|
|
18
|
+
"cloudflare-cdn-cache-control": cdnDirectives(input),
|
|
19
|
+
})
|
|
20
|
+
const NO_STORE = { "cloudflare-cdn-cache-control": "no-store" }
|
|
21
|
+
|
|
22
|
+
export interface CloudflareZoneOptions {
|
|
23
|
+
/** The zone id (32 hex characters, on the zone's Overview page). */
|
|
24
|
+
readonly zoneId: string
|
|
25
|
+
/** An API token scoped to Zone > Cache Purge for this zone. Keep it in an env binding. */
|
|
26
|
+
readonly apiToken: string
|
|
27
|
+
readonly queue?: PurgeQueueOptions
|
|
28
|
+
/** The fetch purge calls go through; tests pass their own. */
|
|
29
|
+
readonly fetch?: typeof fetch
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A Cloudflare zone proxying the origin. HTML is only cached by a Cache Rule that makes it eligible,
|
|
34
|
+
* and the zone's cache ignores `Vary`: the rule must also bypass the cache when the request carries
|
|
35
|
+
* `x-nifra-data` (soft navigations fetch the page URL with that header). `nifra cdn-check` tests both.
|
|
36
|
+
*/
|
|
37
|
+
export function cloudflareZone(options: CloudflareZoneOptions): CdnProvider {
|
|
38
|
+
if (!/^[a-f0-9]{32}$/.test(options.zoneId)) {
|
|
39
|
+
throw new TypeError(
|
|
40
|
+
"[nifra/web/cdn] cloudflareZone: zoneId must be the 32-character hex zone id",
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
if (typeof options.apiToken !== "string" || options.apiToken === "") {
|
|
44
|
+
throw new TypeError(
|
|
45
|
+
"[nifra/web/cdn] cloudflareZone: apiToken is empty (is the env binding set?)",
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
const send = options.fetch ?? fetch
|
|
49
|
+
const endpoint = `https://api.cloudflare.com/client/v4/zones/${options.zoneId}/purge_cache`
|
|
50
|
+
return defineCdnProvider(
|
|
51
|
+
{
|
|
52
|
+
name: "cloudflare",
|
|
53
|
+
tagsPerCall: 100,
|
|
54
|
+
maxResponseTags: 1000,
|
|
55
|
+
cacheHeaders: headersFor,
|
|
56
|
+
noStoreHeaders: () => NO_STORE,
|
|
57
|
+
async purgeTags(tags) {
|
|
58
|
+
const res = await send(endpoint, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: {
|
|
61
|
+
authorization: `Bearer ${options.apiToken}`,
|
|
62
|
+
"content-type": "application/json",
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify({ tags }),
|
|
65
|
+
})
|
|
66
|
+
const attempt = attemptFromStatus(res, "cloudflare")
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
await res.body?.cancel()
|
|
69
|
+
return attempt
|
|
70
|
+
}
|
|
71
|
+
// A 200 can still carry `success: false`; only `true` means the purge was received.
|
|
72
|
+
const body: unknown = await res.json().catch(() => null)
|
|
73
|
+
const field = (from: unknown, name: string): unknown =>
|
|
74
|
+
typeof from === "object" && from !== null ? Reflect.get(from, name) : undefined
|
|
75
|
+
if (field(body, "success") === true) return attempt
|
|
76
|
+
const errors = field(body, "errors")
|
|
77
|
+
const code = field(Array.isArray(errors) ? errors[0] : undefined, "code")
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
retryable: false,
|
|
81
|
+
reason: typeof code === "number" ? `cloudflare_error_${code}` : "cloudflare_unsuccessful",
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
options.queue,
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The Worker's cache binding: `import { cache } from "cloudflare:workers"`, or `ctx.cache`. */
|
|
90
|
+
export interface WorkersCacheLike {
|
|
91
|
+
purge(options: { tags?: string[]; pathPrefixes?: string[] }): Promise<{
|
|
92
|
+
success: boolean
|
|
93
|
+
errors?: readonly { code?: number; message?: string }[]
|
|
94
|
+
}>
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface CloudflareWorkersCacheOptions {
|
|
98
|
+
/** `import { cache } from "cloudflare:workers"` (needs `[cache] enabled = true` in wrangler). */
|
|
99
|
+
readonly cache: WorkersCacheLike
|
|
100
|
+
/**
|
|
101
|
+
* Whether the Worker serves different content per hostname. Workers Cache keys entries by path, not
|
|
102
|
+
* host, so such an app would serve one host's page to another: `true` is refused.
|
|
103
|
+
*/
|
|
104
|
+
readonly hostRouted?: boolean
|
|
105
|
+
readonly queue?: PurgeQueueOptions
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Workers Cache in front of the Worker serving the app. Purges are scoped to the calling entrypoint. */
|
|
109
|
+
export function cloudflareWorkersCache(options: CloudflareWorkersCacheOptions): CdnProvider {
|
|
110
|
+
if (options.hostRouted === true) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
"[nifra/web/cdn] NIFRA_CDN_HOST_ROUTED: Workers Cache keys pages by path, not host, so a Worker that serves different content per hostname would serve one host's page to another. Put a Cloudflare zone (cloudflareZone) in front instead, or serve each host from its own Worker.",
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
if (typeof options.cache?.purge !== "function") {
|
|
116
|
+
throw new TypeError(
|
|
117
|
+
'[nifra/web/cdn] cloudflareWorkersCache: pass `cache` from `import { cache } from "cloudflare:workers"`',
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
return defineCdnProvider(
|
|
121
|
+
{
|
|
122
|
+
name: "cloudflare-workers-cache",
|
|
123
|
+
tagsPerCall: 100,
|
|
124
|
+
maxResponseTags: 1000,
|
|
125
|
+
cacheHeaders: headersFor,
|
|
126
|
+
noStoreHeaders: () => NO_STORE,
|
|
127
|
+
async purgeTags(tags) {
|
|
128
|
+
const result = await options.cache.purge({ tags: [...tags] })
|
|
129
|
+
if (result.success) return { ok: true }
|
|
130
|
+
const first = result.errors?.[0]
|
|
131
|
+
// A rate-limit refusal is the one worth retrying; anything else is configuration.
|
|
132
|
+
const limited = /rate/i.test(first?.message ?? "")
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
retryable: limited,
|
|
136
|
+
rateLimited: limited,
|
|
137
|
+
reason:
|
|
138
|
+
typeof first?.code === "number"
|
|
139
|
+
? `workers_cache_${first.code}`
|
|
140
|
+
: "workers_cache_refused",
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
options.queue,
|
|
145
|
+
)
|
|
146
|
+
}
|