@nifrajs/web 3.5.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend-only.d.ts +15 -0
- package/dist/backend-only.d.ts.map +1 -0
- package/dist/backend-only.js +16 -0
- package/dist/backend-only.js.map +1 -0
- package/dist/boundary.d.ts +5 -0
- package/dist/boundary.d.ts.map +1 -1
- package/dist/boundary.js +11 -2
- package/dist/boundary.js.map +1 -1
- package/dist/build-plan.d.ts +18 -6
- package/dist/build-plan.d.ts.map +1 -1
- package/dist/build-plan.js +82 -34
- package/dist/build-plan.js.map +1 -1
- package/dist/build-vite.d.ts +4 -0
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +68 -13
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +57 -23
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +356 -85
- package/dist/build.js.map +1 -1
- package/dist/bun-dev-entry.d.ts +1 -1
- package/dist/bun-dev-entry.js +1 -1
- package/dist/cdn.d.ts +74 -0
- package/dist/cdn.d.ts.map +1 -0
- package/dist/cdn.js +132 -0
- package/dist/cdn.js.map +1 -0
- package/dist/client.d.ts +3 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +111 -15
- package/dist/client.js.map +1 -1
- package/dist/conformance.d.ts +4 -3
- package/dist/conformance.d.ts.map +1 -1
- package/dist/conformance.js +12 -3
- package/dist/conformance.js.map +1 -1
- package/dist/csp.d.ts +93 -0
- package/dist/csp.d.ts.map +1 -0
- package/dist/csp.js +122 -0
- package/dist/csp.js.map +1 -0
- package/dist/dev-client.d.ts +76 -0
- package/dist/dev-client.d.ts.map +1 -0
- package/dist/dev-client.js +280 -0
- package/dist/dev-client.js.map +1 -0
- package/dist/dev-diagnostics.d.ts +26 -2
- package/dist/dev-diagnostics.d.ts.map +1 -1
- package/dist/dev-diagnostics.js +38 -7
- package/dist/dev-diagnostics.js.map +1 -1
- package/dist/dev-error.d.ts +4 -13
- package/dist/dev-error.d.ts.map +1 -1
- package/dist/dev-error.js +46 -4
- package/dist/dev-error.js.map +1 -1
- package/dist/dev-feed.d.ts +298 -0
- package/dist/dev-feed.d.ts.map +1 -0
- package/dist/dev-feed.js +797 -0
- package/dist/dev-feed.js.map +1 -0
- package/dist/dev-indicator.d.ts +38 -0
- package/dist/dev-indicator.d.ts.map +1 -0
- package/dist/dev-indicator.js +93 -0
- package/dist/dev-indicator.js.map +1 -0
- package/dist/dev-session.d.ts +77 -0
- package/dist/dev-session.d.ts.map +1 -0
- package/dist/dev-session.js +580 -0
- package/dist/dev-session.js.map +1 -0
- package/dist/dev-sourcemap.d.ts +27 -0
- package/dist/dev-sourcemap.d.ts.map +1 -0
- package/dist/dev-sourcemap.js +235 -0
- package/dist/dev-sourcemap.js.map +1 -0
- package/dist/dev-ssr-graph.d.ts.map +1 -1
- package/dist/dev-ssr-graph.js +13 -5
- package/dist/dev-ssr-graph.js.map +1 -1
- package/dist/dev.d.ts +15 -2
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +69 -44
- package/dist/dev.js.map +1 -1
- package/dist/diagnostic-catalog.d.ts +35 -0
- package/dist/diagnostic-catalog.d.ts.map +1 -0
- package/dist/diagnostic-catalog.js +203 -0
- package/dist/diagnostic-catalog.js.map +1 -0
- package/dist/diagnostic-prompt.d.ts +49 -0
- package/dist/diagnostic-prompt.d.ts.map +1 -0
- package/dist/diagnostic-prompt.js +303 -0
- package/dist/diagnostic-prompt.js.map +1 -0
- package/dist/diagnostic.d.ts +17 -23
- package/dist/diagnostic.d.ts.map +1 -1
- package/dist/diagnostic.js +31 -39
- package/dist/diagnostic.js.map +1 -1
- package/dist/draft.d.ts.map +1 -1
- package/dist/draft.js +4 -1
- package/dist/draft.js.map +1 -1
- package/dist/fn.d.ts +5 -0
- package/dist/fn.d.ts.map +1 -1
- package/dist/fn.js +13 -3
- package/dist/fn.js.map +1 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +4 -2
- package/dist/fs.js.map +1 -1
- package/dist/index.d.ts +13 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -12
- package/dist/index.js.map +1 -1
- package/dist/internal/cdn/cloudflare.d.ts +48 -0
- package/dist/internal/cdn/cloudflare.d.ts.map +1 -0
- package/dist/internal/cdn/cloudflare.js +94 -0
- package/dist/internal/cdn/cloudflare.js.map +1 -0
- package/dist/internal/cdn/core.d.ts +91 -0
- package/dist/internal/cdn/core.d.ts.map +1 -0
- package/dist/internal/cdn/core.js +220 -0
- package/dist/internal/cdn/core.js.map +1 -0
- package/dist/internal/cdn/fastly.d.ts +17 -0
- package/dist/internal/cdn/fastly.d.ts.map +1 -0
- package/dist/internal/cdn/fastly.js +43 -0
- package/dist/internal/cdn/fastly.js.map +1 -0
- package/dist/internal/cdn/vercel.d.ts +31 -0
- package/dist/internal/cdn/vercel.d.ts.map +1 -0
- package/dist/internal/cdn/vercel.js +56 -0
- package/dist/internal/cdn/vercel.js.map +1 -0
- package/dist/internal/code-unit-order.d.ts +4 -0
- package/dist/internal/code-unit-order.d.ts.map +1 -0
- package/dist/internal/code-unit-order.js +6 -0
- package/dist/internal/code-unit-order.js.map +1 -0
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +141 -25
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/dev-csp.d.ts +24 -0
- package/dist/internal/dev-csp.d.ts.map +1 -0
- package/dist/internal/dev-csp.js +160 -0
- package/dist/internal/dev-csp.js.map +1 -0
- package/dist/internal/dev-reserved.d.ts +25 -0
- package/dist/internal/dev-reserved.d.ts.map +1 -0
- package/dist/internal/dev-reserved.js +39 -0
- package/dist/internal/dev-reserved.js.map +1 -0
- package/dist/internal/html-spans.d.ts +10 -0
- package/dist/internal/html-spans.d.ts.map +1 -0
- package/dist/internal/html-spans.js +46 -0
- package/dist/internal/html-spans.js.map +1 -0
- package/dist/internal/loading-runtime.d.ts +44 -0
- package/dist/internal/loading-runtime.d.ts.map +1 -0
- package/dist/internal/loading-runtime.js +206 -0
- package/dist/internal/loading-runtime.js.map +1 -0
- package/dist/internal/matches-runtime.d.ts +8 -0
- package/dist/internal/matches-runtime.d.ts.map +1 -0
- package/dist/internal/matches-runtime.js +61 -0
- package/dist/internal/matches-runtime.js.map +1 -0
- package/dist/internal/mount-shadow.d.ts +36 -0
- package/dist/internal/mount-shadow.d.ts.map +1 -0
- package/dist/internal/mount-shadow.js +65 -0
- package/dist/internal/mount-shadow.js.map +1 -0
- package/dist/internal/node-builtins.d.ts +3 -0
- package/dist/internal/node-builtins.d.ts.map +1 -0
- package/dist/internal/node-builtins.js +79 -0
- package/dist/internal/node-builtins.js.map +1 -0
- package/dist/internal/output-guard.d.ts +65 -0
- package/dist/internal/output-guard.d.ts.map +1 -0
- package/dist/internal/output-guard.js +630 -0
- package/dist/internal/output-guard.js.map +1 -0
- package/dist/internal/page-execution.d.ts +20 -8
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +571 -105
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/parity.d.ts +21 -0
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +104 -22
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/private-env.d.ts +12 -0
- package/dist/internal/private-env.d.ts.map +1 -0
- package/dist/internal/private-env.js +335 -0
- package/dist/internal/private-env.js.map +1 -0
- package/dist/internal/public-url.d.ts +3 -0
- package/dist/internal/public-url.d.ts.map +1 -0
- package/dist/internal/public-url.js +18 -0
- package/dist/internal/public-url.js.map +1 -0
- package/dist/internal/render-document.d.ts +25 -19
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +77 -61
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/response-controls.d.ts +75 -0
- package/dist/internal/response-controls.d.ts.map +1 -0
- package/dist/internal/response-controls.js +256 -0
- package/dist/internal/response-controls.js.map +1 -0
- package/dist/internal/runtime-contract.d.ts +11 -16
- package/dist/internal/runtime-contract.d.ts.map +1 -1
- package/dist/internal/runtime-contract.js.map +1 -1
- package/dist/internal/secret-scan.d.ts +104 -0
- package/dist/internal/secret-scan.d.ts.map +1 -0
- package/dist/internal/secret-scan.js +485 -0
- package/dist/internal/secret-scan.js.map +1 -0
- package/dist/internal/server-boundary.d.ts +6 -29
- package/dist/internal/server-boundary.d.ts.map +1 -1
- package/dist/internal/server-boundary.js +6 -66
- package/dist/internal/server-boundary.js.map +1 -1
- package/dist/internal/source-map.d.ts +47 -0
- package/dist/internal/source-map.d.ts.map +1 -0
- package/dist/internal/source-map.js +225 -0
- package/dist/internal/source-map.js.map +1 -0
- package/dist/internal/target-compat.d.ts +27 -0
- package/dist/internal/target-compat.d.ts.map +1 -0
- package/dist/internal/target-compat.js +77 -0
- package/dist/internal/target-compat.js.map +1 -0
- package/dist/internal/timing-safe-equal.d.ts +6 -0
- package/dist/internal/timing-safe-equal.d.ts.map +1 -1
- package/dist/internal/timing-safe-equal.js +10 -0
- package/dist/internal/timing-safe-equal.js.map +1 -1
- package/dist/internal/vitals-runtime.d.ts +18 -0
- package/dist/internal/vitals-runtime.d.ts.map +1 -0
- package/dist/internal/vitals-runtime.js +38 -0
- package/dist/internal/vitals-runtime.js.map +1 -0
- package/dist/internal/web-app.d.ts +38 -9
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +199 -59
- package/dist/internal/web-app.js.map +1 -1
- package/dist/internal/zone-graph.d.ts +89 -0
- package/dist/internal/zone-graph.d.ts.map +1 -0
- package/dist/internal/zone-graph.js +492 -0
- package/dist/internal/zone-graph.js.map +1 -0
- package/dist/isr.d.ts +83 -6
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +263 -36
- package/dist/isr.js.map +1 -1
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +31 -6
- package/dist/llms-txt.js.map +1 -1
- package/dist/manifest.d.ts +220 -13
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +386 -9
- package/dist/manifest.js.map +1 -1
- package/dist/module-graph.d.ts +26 -5
- package/dist/module-graph.d.ts.map +1 -1
- package/dist/module-graph.js +17 -7
- package/dist/module-graph.js.map +1 -1
- package/dist/navigation.d.ts +17 -3
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +11 -5
- package/dist/navigation.js.map +1 -1
- package/dist/nonce.d.ts +47 -0
- package/dist/nonce.d.ts.map +1 -0
- package/dist/nonce.js +63 -0
- package/dist/nonce.js.map +1 -0
- package/dist/plugins/css-modules.d.ts.map +1 -1
- package/dist/plugins/css-modules.js +4 -3
- package/dist/plugins/css-modules.js.map +1 -1
- package/dist/plugins/kit.d.ts +9 -0
- package/dist/plugins/kit.d.ts.map +1 -1
- package/dist/plugins/kit.js +18 -0
- package/dist/plugins/kit.js.map +1 -1
- package/dist/plugins/svg.d.ts +22 -3
- package/dist/plugins/svg.d.ts.map +1 -1
- package/dist/plugins/svg.js +112 -1
- package/dist/plugins/svg.js.map +1 -1
- package/dist/plugins/vite-leak-guard.d.ts +107 -3
- package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
- package/dist/plugins/vite-leak-guard.js +378 -17
- package/dist/plugins/vite-leak-guard.js.map +1 -1
- package/dist/plugins/vite-zone-guard.d.ts +61 -0
- package/dist/plugins/vite-zone-guard.d.ts.map +1 -0
- package/dist/plugins/vite-zone-guard.js +208 -0
- package/dist/plugins/vite-zone-guard.js.map +1 -0
- package/dist/plugins/zone-guard.d.ts +27 -0
- package/dist/plugins/zone-guard.d.ts.map +1 -0
- package/dist/plugins/zone-guard.js +72 -0
- package/dist/plugins/zone-guard.js.map +1 -0
- package/dist/prerender.d.ts +10 -0
- package/dist/prerender.d.ts.map +1 -1
- package/dist/prerender.js +35 -3
- package/dist/prerender.js.map +1 -1
- package/dist/public-dir.d.ts +9 -0
- package/dist/public-dir.d.ts.map +1 -1
- package/dist/public-dir.js +67 -9
- package/dist/public-dir.js.map +1 -1
- package/dist/pwa-manifest.d.ts +68 -0
- package/dist/pwa-manifest.d.ts.map +1 -0
- package/dist/pwa-manifest.js +178 -0
- package/dist/pwa-manifest.js.map +1 -0
- package/dist/query.js +4 -4
- package/dist/query.js.map +1 -1
- package/dist/render-seam.d.ts +42 -2
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +6 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/route-manifest.d.ts +3 -0
- package/dist/route-manifest.d.ts.map +1 -1
- package/dist/route-manifest.js +26 -3
- package/dist/route-manifest.js.map +1 -1
- package/dist/route-types.d.ts +24 -0
- package/dist/route-types.d.ts.map +1 -0
- package/dist/route-types.js +195 -0
- package/dist/route-types.js.map +1 -0
- package/dist/router.d.ts +28 -6
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +129 -40
- package/dist/router.js.map +1 -1
- package/dist/vitals.d.ts +33 -0
- package/dist/vitals.d.ts.map +1 -0
- package/dist/vitals.js +16 -0
- package/dist/vitals.js.map +1 -0
- package/dist/vite.d.ts +16 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +187 -36
- package/dist/vite.js.map +1 -1
- package/dist/zones.d.ts +70 -0
- package/dist/zones.d.ts.map +1 -0
- package/dist/zones.js +365 -0
- package/dist/zones.js.map +1 -0
- package/package.json +67 -14
- package/src/backend-only.ts +16 -0
- package/src/boundary.ts +18 -5
- package/src/build-plan.ts +90 -35
- package/src/build-vite.ts +77 -11
- package/src/build.ts +466 -96
- package/src/bun-dev-entry.ts +1 -1
- package/src/cdn.ts +216 -0
- package/src/client.ts +111 -18
- package/src/conformance.ts +16 -3
- package/src/csp.ts +194 -0
- package/src/dev-client.ts +349 -0
- package/src/dev-diagnostics.ts +60 -7
- package/src/dev-error.ts +49 -5
- package/src/dev-feed.ts +1140 -0
- package/src/dev-indicator.ts +113 -0
- package/src/dev-session.ts +729 -0
- package/src/dev-sourcemap.ts +281 -0
- package/src/dev-ssr-graph.ts +16 -5
- package/src/dev.ts +86 -43
- package/src/diagnostic-catalog.ts +243 -0
- package/src/diagnostic-prompt.ts +355 -0
- package/src/diagnostic.ts +63 -66
- package/src/draft.ts +4 -1
- package/src/fn.ts +19 -3
- package/src/fs.ts +13 -2
- package/src/index.ts +42 -10
- package/src/internal/cdn/cloudflare.ts +146 -0
- package/src/internal/cdn/core.ts +316 -0
- package/src/internal/cdn/fastly.ts +62 -0
- package/src/internal/cdn/vercel.ts +95 -0
- package/src/internal/code-unit-order.ts +5 -0
- package/src/internal/codegen.ts +161 -24
- package/src/internal/dev-csp.ts +176 -0
- package/src/internal/dev-reserved.ts +52 -0
- package/src/internal/html-spans.ts +50 -0
- package/src/internal/loading-runtime.ts +244 -0
- package/src/internal/matches-runtime.ts +62 -0
- package/src/internal/mount-shadow.ts +88 -0
- package/src/internal/node-builtins.ts +79 -0
- package/src/internal/output-guard.ts +701 -0
- package/src/internal/page-execution.ts +795 -185
- package/src/internal/parity.ts +150 -36
- package/src/internal/private-env.ts +334 -0
- package/src/internal/public-url.ts +22 -0
- package/src/internal/render-document.ts +114 -67
- package/src/internal/response-controls.ts +284 -0
- package/src/internal/runtime-contract.ts +10 -15
- package/src/internal/secret-scan.ts +623 -0
- package/src/internal/server-boundary.ts +6 -69
- package/src/internal/source-map.ts +256 -0
- package/src/internal/target-compat.ts +92 -0
- package/src/internal/timing-safe-equal.ts +13 -0
- package/src/internal/vitals-runtime.ts +53 -0
- package/src/internal/web-app.ts +262 -84
- package/src/internal/zone-graph.ts +593 -0
- package/src/isr.ts +325 -37
- package/src/llms-txt.ts +28 -4
- package/src/manifest.ts +670 -16
- package/src/module-graph.ts +53 -9
- package/src/navigation.ts +29 -6
- package/src/nonce.ts +104 -0
- package/src/plugins/css-modules.ts +5 -3
- package/src/plugins/kit.ts +24 -0
- package/src/plugins/svg.ts +135 -4
- package/src/plugins/vite-leak-guard.ts +529 -16
- package/src/plugins/vite-zone-guard.ts +263 -0
- package/src/plugins/zone-guard.ts +110 -0
- package/src/prerender.ts +49 -3
- package/src/public-dir.ts +69 -8
- package/src/pwa-manifest.ts +226 -0
- package/src/query.ts +4 -4
- package/src/render-seam.ts +46 -1
- package/src/route-manifest.ts +15 -3
- package/src/route-types.ts +264 -0
- package/src/router.ts +163 -63
- package/src/vitals.ts +44 -0
- package/src/vite.ts +207 -36
- package/src/zones.ts +430 -0
- package/dist/plugins/vite-server-only.d.ts +0 -49
- package/dist/plugins/vite-server-only.d.ts.map +0 -1
- package/dist/plugins/vite-server-only.js +0 -57
- package/dist/plugins/vite-server-only.js.map +0 -1
- package/dist/server-only.d.ts +0 -21
- package/dist/server-only.d.ts.map +0 -1
- package/dist/server-only.js +0 -25
- package/dist/server-only.js.map +0 -1
- package/src/plugins/vite-server-only.ts +0 -75
- package/src/server-only.ts +0 -25
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,14 @@ 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"
|
|
44
52
|
export {
|
|
45
53
|
assertCssLoadingCompatible,
|
|
46
54
|
type CssLoadingMode,
|
|
@@ -102,43 +110,46 @@ export {
|
|
|
102
110
|
unsafeInlineScript,
|
|
103
111
|
} from "./internal/render-document.ts"
|
|
104
112
|
export {
|
|
113
|
+
type BackendOnly,
|
|
105
114
|
DEFAULT_DEV_PORT,
|
|
106
115
|
PRE_HYDRATION_GUARD,
|
|
107
|
-
type ServerOnly,
|
|
108
116
|
} from "./internal/runtime-contract.ts"
|
|
109
117
|
/**
|
|
110
|
-
* The
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* bundler takes no plugins) so it must REFUSE instead, and a refusal driven by its own hand-written
|
|
115
|
-
* glob drifts from the transform - which is how `.fn.mts` came to be stubbed by both build pipelines
|
|
116
|
-
* and waved through by the guard that exists to stop it leaking. Anything deciding "is this module
|
|
117
|
-
* 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.
|
|
118
122
|
*/
|
|
119
|
-
export { SERVER_FN_MODULE
|
|
123
|
+
export { SERVER_FN_MODULE } from "./internal/server-boundary.ts"
|
|
120
124
|
export {
|
|
121
125
|
type CreateWebAppOptions,
|
|
122
126
|
createWebApp,
|
|
123
127
|
type NonceResolver,
|
|
128
|
+
webProjectEvidence,
|
|
124
129
|
} from "./internal/web-app.ts"
|
|
125
130
|
// ISR (incremental static regeneration): a pluggable cache store + the `withISR` stale-while-revalidate
|
|
126
131
|
// wrapper for rendered SSR responses.
|
|
127
132
|
export {
|
|
128
133
|
type CachedResponse,
|
|
129
134
|
type CacheStore,
|
|
135
|
+
type CdnPurgeOutcome,
|
|
136
|
+
type CdnPurgeTarget,
|
|
130
137
|
ISR_REVALIDATE_HEADER,
|
|
131
138
|
ISR_REVALIDATE_TAGS_HEADER,
|
|
132
139
|
ISR_STATUS_HEADER,
|
|
133
140
|
type ISRApp,
|
|
134
141
|
type ISROptions,
|
|
135
142
|
type ISRPlatform,
|
|
143
|
+
type ISRQuery,
|
|
144
|
+
isCacheablePage,
|
|
136
145
|
KVCacheStore,
|
|
137
146
|
type KVCacheStoreOptions,
|
|
138
147
|
type KVNamespaceLike,
|
|
139
148
|
MemoryCacheStore,
|
|
140
149
|
type MemoryCacheStoreOptions,
|
|
141
150
|
type RevalidateEndpointOptions,
|
|
151
|
+
type RevalidateTags,
|
|
152
|
+
type RevalidateTagsInput,
|
|
142
153
|
revalidateEndpoint,
|
|
143
154
|
withISR,
|
|
144
155
|
} from "./isr.ts"
|
|
@@ -162,14 +173,22 @@ export {
|
|
|
162
173
|
type LinkDescriptor,
|
|
163
174
|
type Loader,
|
|
164
175
|
type LoaderContext,
|
|
176
|
+
type LoaderResponseControls,
|
|
177
|
+
type LoadingEntry,
|
|
165
178
|
type Manifest,
|
|
166
179
|
type Meta,
|
|
167
180
|
type MetaArgs,
|
|
168
181
|
type MetaDescriptor,
|
|
169
182
|
type MetaInput,
|
|
183
|
+
type MiddlewareOutcome,
|
|
184
|
+
type NotFoundEntry,
|
|
185
|
+
type NotFoundScope,
|
|
170
186
|
type RouteEntry,
|
|
187
|
+
type RouteMiddleware,
|
|
171
188
|
type RouteModule,
|
|
172
189
|
type ScriptDescriptor,
|
|
190
|
+
type ShouldRevalidate,
|
|
191
|
+
type ShouldRevalidateArgs,
|
|
173
192
|
type StaticPath,
|
|
174
193
|
type StaticPaths,
|
|
175
194
|
type StaticRoutes,
|
|
@@ -190,12 +209,21 @@ export {
|
|
|
190
209
|
type NavigateOptions,
|
|
191
210
|
type NavigateSearchOf,
|
|
192
211
|
type NavigateTargetInput,
|
|
212
|
+
type PrefetchMode,
|
|
193
213
|
type RouteSearch,
|
|
194
214
|
registerBlocker,
|
|
195
215
|
resolveNavigate,
|
|
196
216
|
setBlockerController,
|
|
197
217
|
setBrowserNavigate,
|
|
198
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"
|
|
199
227
|
// public/ - user-authored static files, served identically in dev and production by one handler.
|
|
200
228
|
export {
|
|
201
229
|
type PublicDirCache,
|
|
@@ -228,14 +256,18 @@ export {
|
|
|
228
256
|
ACTION_GLOBAL,
|
|
229
257
|
BOUNDARY_GLOBAL,
|
|
230
258
|
DATA_GLOBAL,
|
|
259
|
+
HANDOVER_ID,
|
|
231
260
|
LAYOUT_DATA_GLOBAL,
|
|
261
|
+
type MatchChain,
|
|
232
262
|
type RenderAdapter,
|
|
233
263
|
type RenderProps,
|
|
264
|
+
type RenderStreamOptions,
|
|
234
265
|
ROOT_ATTRIBUTE,
|
|
235
266
|
ROUTE_GLOBAL,
|
|
236
267
|
type SsrModuleLoader,
|
|
237
268
|
setSsrModuleLoader,
|
|
238
269
|
ssrModuleLoader,
|
|
270
|
+
type UIMatch,
|
|
239
271
|
} from "./render-seam.ts"
|
|
240
272
|
// Agnostic client-side router core (pure + DOM-free) - consumed by per-adapter Router bindings.
|
|
241
273
|
// `DATA_HEADER` marks a navigation's data-only GET; `createWebApp` answers it with loader JSON.
|
|
@@ -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
|
+
}
|