@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/public-dir.ts
CHANGED
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
* Distinct from `publicPath` in `build.ts`, which is the URL prefix for content-hashed bundle chunks.
|
|
13
13
|
* The names collide and the concepts do not: `publicPath` never covers user-authored files.
|
|
14
14
|
*/
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { constants as FS } from "node:fs"
|
|
16
|
+
import { open, realpath } from "node:fs/promises"
|
|
17
|
+
import { extname, normalize, resolve, sep } from "node:path"
|
|
18
|
+
import { Readable } from "node:stream"
|
|
17
19
|
import { parseByteRange } from "@nifrajs/core/range"
|
|
18
20
|
import { pathnameOf } from "@nifrajs/core/server"
|
|
19
21
|
|
|
@@ -36,8 +38,67 @@ export interface ServePublicDirOptions {
|
|
|
36
38
|
readonly files?: ReadonlySet<string>
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
const IMMUTABLE = "public, max-age=31536000, immutable"
|
|
40
|
-
const ONE_DAY = "public, max-age=86400"
|
|
41
|
+
export const IMMUTABLE = "public, max-age=31536000, immutable"
|
|
42
|
+
export const ONE_DAY = "public, max-age=86400"
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Media types by extension. A table rather than `Bun.file(path).type`: this handler also runs on the
|
|
46
|
+
* Node and Deno adapters, where `Bun` does not exist and every hit would throw. Unknown extensions
|
|
47
|
+
* (and extensionless files such as ACME tokens) are served as `application/octet-stream`, which
|
|
48
|
+
* together with `nosniff` keeps the browser from guessing a more dangerous type.
|
|
49
|
+
*/
|
|
50
|
+
export const CONTENT_TYPES: Readonly<Record<string, string>> = {
|
|
51
|
+
".html": "text/html; charset=utf-8",
|
|
52
|
+
".htm": "text/html; charset=utf-8",
|
|
53
|
+
".css": "text/css; charset=utf-8",
|
|
54
|
+
".js": "text/javascript; charset=utf-8",
|
|
55
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
56
|
+
".cjs": "text/javascript; charset=utf-8",
|
|
57
|
+
".json": "application/json; charset=utf-8",
|
|
58
|
+
".map": "application/json; charset=utf-8",
|
|
59
|
+
".webmanifest": "application/manifest+json",
|
|
60
|
+
".txt": "text/plain; charset=utf-8",
|
|
61
|
+
".md": "text/markdown; charset=utf-8",
|
|
62
|
+
".csv": "text/csv; charset=utf-8",
|
|
63
|
+
".xml": "application/xml",
|
|
64
|
+
".rss": "application/rss+xml",
|
|
65
|
+
".atom": "application/atom+xml",
|
|
66
|
+
".svg": "image/svg+xml",
|
|
67
|
+
".png": "image/png",
|
|
68
|
+
".jpg": "image/jpeg",
|
|
69
|
+
".jpeg": "image/jpeg",
|
|
70
|
+
".gif": "image/gif",
|
|
71
|
+
".webp": "image/webp",
|
|
72
|
+
".avif": "image/avif",
|
|
73
|
+
".ico": "image/x-icon",
|
|
74
|
+
".bmp": "image/bmp",
|
|
75
|
+
".woff": "font/woff",
|
|
76
|
+
".woff2": "font/woff2",
|
|
77
|
+
".ttf": "font/ttf",
|
|
78
|
+
".otf": "font/otf",
|
|
79
|
+
".wasm": "application/wasm",
|
|
80
|
+
".pdf": "application/pdf",
|
|
81
|
+
".zip": "application/zip",
|
|
82
|
+
".mp4": "video/mp4",
|
|
83
|
+
".webm": "video/webm",
|
|
84
|
+
".ogg": "audio/ogg",
|
|
85
|
+
".mp3": "audio/mpeg",
|
|
86
|
+
".wav": "audio/wav",
|
|
87
|
+
".m4a": "audio/mp4",
|
|
88
|
+
".vtt": "text/vtt; charset=utf-8",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Whether a confined path crosses a dot-segment other than `.well-known`. The build copies `public/`
|
|
93
|
+
* with dotfiles included, so an editor or VCS artifact (`.env`, `.git/`, `.DS_Store`) dropped there
|
|
94
|
+
* would otherwise be downloadable. Checked on the resolved path, so `%2e` encodings are covered.
|
|
95
|
+
*/
|
|
96
|
+
function isHiddenPath(root: string, abs: string): boolean {
|
|
97
|
+
for (const segment of abs.slice(root.length + 1).split(sep)) {
|
|
98
|
+
if (segment.startsWith(".") && segment !== ".well-known") return true
|
|
99
|
+
}
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
41
102
|
|
|
42
103
|
/** HTTP dates carry second precision; comparing raw milliseconds makes every file look stale. */
|
|
43
104
|
function seconds(time: number): number {
|
|
@@ -117,7 +178,7 @@ export function servePublicDir(
|
|
|
117
178
|
// A production manifest can reject page routes without touching the filesystem.
|
|
118
179
|
if (options.files !== undefined && !options.files.has(pathname)) return undefined
|
|
119
180
|
const abs = resolvePublicPath(root, pathname)
|
|
120
|
-
if (abs === undefined) return undefined
|
|
181
|
+
if (abs === undefined || isHiddenPath(root, abs)) return undefined
|
|
121
182
|
const [resolvedRoot, resolvedFile] = await Promise.all([
|
|
122
183
|
rootReal,
|
|
123
184
|
realpath(abs).catch(() => undefined),
|
|
@@ -129,54 +190,79 @@ export function servePublicDir(
|
|
|
129
190
|
) {
|
|
130
191
|
return undefined
|
|
131
192
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
193
|
+
// Open the canonical path once and stream from that descriptor. Reopening the pathname after
|
|
194
|
+
// realpath containment checks would let a writable public tree swap a symlink between validation
|
|
195
|
+
// and the actual read. O_NOFOLLOW also rejects a final-component symlink if one appears before
|
|
196
|
+
// the open; the descriptor then pins the bytes for the lifetime of the response.
|
|
197
|
+
let handle: Awaited<ReturnType<typeof open>> | undefined
|
|
198
|
+
try {
|
|
199
|
+
handle = await open(resolvedFile, FS.O_RDONLY | FS.O_NOFOLLOW)
|
|
200
|
+
const stat = await handle.stat()
|
|
201
|
+
if (!stat.isFile()) return undefined
|
|
202
|
+
const size = stat.size
|
|
203
|
+
const headers = new Headers({
|
|
204
|
+
"cache-control": pathname.startsWith(hashedPrefix) ? hashed : assets,
|
|
205
|
+
// Advertised unconditionally: a client that never sees `accept-ranges` will not attempt a seek,
|
|
206
|
+
// so a video or audio file under `public/` is scrubbable only once this header is present.
|
|
207
|
+
"accept-ranges": "bytes",
|
|
208
|
+
// Never let a client sniff a served file into a more dangerous type (an upload-like `.txt`
|
|
209
|
+
// rendered as HTML, say). The declared type below is authoritative.
|
|
210
|
+
"x-content-type-options": "nosniff",
|
|
211
|
+
// Set explicitly rather than inferred from the body, so HEAD (a null body) carries it too.
|
|
212
|
+
"content-type":
|
|
213
|
+
CONTENT_TYPES[extname(resolvedFile).toLowerCase()] ?? "application/octet-stream",
|
|
214
|
+
})
|
|
215
|
+
const lastModified =
|
|
216
|
+
Number.isFinite(stat.mtimeMs) && stat.mtimeMs > 0 ? stat.mtimeMs : undefined
|
|
217
|
+
if (lastModified !== undefined) {
|
|
218
|
+
headers.set("last-modified", new Date(lastModified).toUTCString())
|
|
219
|
+
}
|
|
220
|
+
const head = request.method === "HEAD"
|
|
150
221
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
222
|
+
if (isNotModified(request, lastModified)) {
|
|
223
|
+
headers.delete("content-type")
|
|
224
|
+
return new Response(null, { status: 304, headers })
|
|
225
|
+
}
|
|
155
226
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
227
|
+
const rangeHeader = request.headers.get("range")
|
|
228
|
+
const range =
|
|
229
|
+
rangeHeader !== null && ifRangeMatches(request.headers.get("if-range"), lastModified)
|
|
230
|
+
? parseByteRange(rangeHeader, size)
|
|
231
|
+
: ({ kind: "none" } as const)
|
|
161
232
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
233
|
+
if (range.kind === "unsatisfiable") {
|
|
234
|
+
headers.delete("content-type")
|
|
235
|
+
headers.set("content-range", `bytes */${size}`)
|
|
236
|
+
return new Response(null, { status: 416, headers })
|
|
237
|
+
}
|
|
167
238
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
239
|
+
const body = (start?: number, end?: number): ReadableStream<Uint8Array> => {
|
|
240
|
+
const stream = handle!.createReadStream({ start, end })
|
|
241
|
+
// FileHandle.createReadStream owns closing the descriptor after EOF/abort.
|
|
242
|
+
handle = undefined
|
|
243
|
+
return Readable.toWeb(stream) as unknown as ReadableStream<Uint8Array>
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (range.kind === "satisfiable" && range.ranges.length === 1) {
|
|
247
|
+
const { start, end } = range.ranges[0]!
|
|
248
|
+
headers.set("content-range", `bytes ${start}-${end}/${size}`)
|
|
249
|
+
headers.set("content-length", String(end - start + 1))
|
|
250
|
+
return new Response(head ? null : body(start, end), { status: 206, headers })
|
|
251
|
+
}
|
|
175
252
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
253
|
+
// Multiple ranges would require assembling `multipart/byteranges`, and doing that for a file on
|
|
254
|
+
// disk means buffering the whole representation to serve a request that asked for less of it.
|
|
255
|
+
// RFC 9110 lets a server ignore Range entirely, so the full body is the conformant answer here.
|
|
256
|
+
headers.set("content-length", String(size))
|
|
257
|
+
return new Response(head ? null : body(), { headers })
|
|
258
|
+
} finally {
|
|
259
|
+
if (handle !== undefined) {
|
|
260
|
+
try {
|
|
261
|
+
await handle.close()
|
|
262
|
+
} catch {
|
|
263
|
+
// Closing a descriptor is best-effort after the response decision is made.
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
181
267
|
}
|
|
182
268
|
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nifrajs/web/pwa-manifest` - a Web App Manifest builder, the declarative half of PWA next to
|
|
3
|
+
* `@nifrajs/web/service-worker`'s generated worker. Pure + runtime-agnostic: describe the app and
|
|
4
|
+
* get the `manifest.json` bytes to serve (typically `app.get("/manifest.webmanifest", ...)` with
|
|
5
|
+
* `content-type: application/manifest+json`), plus the `<link rel="manifest">` tag for the
|
|
6
|
+
* document head. Follows the [Web App Manifest spec](https://www.w3.org/TR/appmanifest/) field
|
|
7
|
+
* names exactly, so MDN is the reference for anything not documented here.
|
|
8
|
+
*
|
|
9
|
+
* Invalid input throws loudly at build/startup time (a missing install name, a malformed icon
|
|
10
|
+
* size, an unknown display mode) - a manifest the browser silently ignores is worse than no
|
|
11
|
+
* manifest, and these are all developer mistakes, never request input.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type ManifestDisplay = "fullscreen" | "standalone" | "minimal-ui" | "browser"
|
|
15
|
+
export type ManifestDir = "ltr" | "rtl" | "auto"
|
|
16
|
+
export type ManifestPurpose = "any" | "maskable" | "monochrome"
|
|
17
|
+
|
|
18
|
+
export interface ManifestIcon {
|
|
19
|
+
/** Icon URL, resolved relative to `start_url` by the browser when relative. */
|
|
20
|
+
readonly src: string
|
|
21
|
+
/** Space-separated sizes, e.g. `"192x192 512x512"`, or `"any"`. Default `"any"`. */
|
|
22
|
+
readonly sizes?: string
|
|
23
|
+
/** MIME type, e.g. `"image/png"`. */
|
|
24
|
+
readonly type?: string
|
|
25
|
+
/** Space-separated purposes. Default `"any"`. */
|
|
26
|
+
readonly purpose?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ManifestScreenshot {
|
|
30
|
+
readonly src: string
|
|
31
|
+
readonly sizes?: string
|
|
32
|
+
readonly type?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ManifestShortcut {
|
|
36
|
+
readonly name: string
|
|
37
|
+
readonly url: string
|
|
38
|
+
readonly short_name?: string
|
|
39
|
+
readonly description?: string
|
|
40
|
+
readonly icons?: readonly ManifestIcon[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface PwaManifestOptions {
|
|
44
|
+
/** Full install name (`"Nifra News"`). One of `name` / `short_name` is required. */
|
|
45
|
+
readonly name?: string
|
|
46
|
+
/** Short install name for launchers (`"News"`). One of `name` / `short_name` is required. */
|
|
47
|
+
readonly short_name?: string
|
|
48
|
+
readonly description?: string
|
|
49
|
+
/** Navigation scope root. Default `"/"`. */
|
|
50
|
+
readonly start_url?: string
|
|
51
|
+
/** URL scope the manifest (and its service worker) controls. Default `start_url`'s directory. */
|
|
52
|
+
readonly scope?: string
|
|
53
|
+
/** Preferred display mode. Default `"standalone"` (the installable PWA shape). */
|
|
54
|
+
readonly display?: ManifestDisplay
|
|
55
|
+
readonly orientation?: string
|
|
56
|
+
readonly background_color?: string
|
|
57
|
+
readonly theme_color?: string
|
|
58
|
+
readonly lang?: string
|
|
59
|
+
readonly dir?: ManifestDir
|
|
60
|
+
/** Opaque id distinguishing this install (defaults to `start_url` per spec). */
|
|
61
|
+
readonly id?: string
|
|
62
|
+
readonly categories?: readonly string[]
|
|
63
|
+
readonly icons?: readonly ManifestIcon[]
|
|
64
|
+
readonly screenshots?: readonly ManifestScreenshot[]
|
|
65
|
+
readonly shortcuts?: readonly ManifestShortcut[]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const SIZE_LIST = /^(?:any|\d+x\d+)(?:\s+(?:any|\d+x\d+))*$/
|
|
69
|
+
const PURPOSE_TOKENS: ReadonlySet<string> = new Set(["any", "maskable", "monochrome"])
|
|
70
|
+
|
|
71
|
+
/** The start URL's directory, origin-preserving for absolute URLs. Query strings and hashes
|
|
72
|
+
* never belong to a scope - it is a URL prefix the browser matches against. */
|
|
73
|
+
function defaultScope(start_url: string): string {
|
|
74
|
+
if (start_url.startsWith("/")) {
|
|
75
|
+
const bare = start_url.split(/[?#]/)[0] as string
|
|
76
|
+
return bare.endsWith("/") ? bare : bare.slice(0, bare.lastIndexOf("/") + 1)
|
|
77
|
+
}
|
|
78
|
+
let url: URL
|
|
79
|
+
try {
|
|
80
|
+
url = new URL(start_url)
|
|
81
|
+
} catch {
|
|
82
|
+
throw new Error(`pwaManifest: start_url ${JSON.stringify(start_url)} is not a path or URL`)
|
|
83
|
+
}
|
|
84
|
+
if (url.origin === "null" || (url.protocol !== "http:" && url.protocol !== "https:")) {
|
|
85
|
+
throw new Error(`pwaManifest: start_url ${JSON.stringify(start_url)} is not a path or URL`)
|
|
86
|
+
}
|
|
87
|
+
const dir = url.pathname.endsWith("/")
|
|
88
|
+
? url.pathname
|
|
89
|
+
: url.pathname.slice(0, url.pathname.lastIndexOf("/") + 1)
|
|
90
|
+
return `${url.origin}${dir}`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function purposeOf(purpose: string | undefined, what: string): string | undefined {
|
|
94
|
+
if (purpose === undefined) return undefined
|
|
95
|
+
const tokens = purpose.split(/\s+/).filter((t) => t !== "")
|
|
96
|
+
if (tokens.length === 0) throw new Error(`pwaManifest: ${what} purpose must not be empty`)
|
|
97
|
+
for (const token of tokens) {
|
|
98
|
+
if (!PURPOSE_TOKENS.has(token)) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`pwaManifest: ${what} purpose ${JSON.stringify(token)} must be any/maskable/monochrome`,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return tokens.join(" ")
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Build the manifest document object (pass through `JSON.stringify` to serve it). */
|
|
108
|
+
export function pwaManifest(options: PwaManifestOptions): Record<string, unknown> {
|
|
109
|
+
const { name, short_name } = options
|
|
110
|
+
if (name === undefined && short_name === undefined) {
|
|
111
|
+
throw new Error("pwaManifest: one of name / short_name is required")
|
|
112
|
+
}
|
|
113
|
+
if (name !== undefined && name.trim() === "") {
|
|
114
|
+
throw new Error("pwaManifest: name must not be empty")
|
|
115
|
+
}
|
|
116
|
+
if (short_name !== undefined && short_name.trim() === "") {
|
|
117
|
+
throw new Error("pwaManifest: short_name must not be empty")
|
|
118
|
+
}
|
|
119
|
+
const display = options.display ?? "standalone"
|
|
120
|
+
if (
|
|
121
|
+
display !== "fullscreen" &&
|
|
122
|
+
display !== "minimal-ui" &&
|
|
123
|
+
display !== "browser" &&
|
|
124
|
+
display !== "standalone"
|
|
125
|
+
) {
|
|
126
|
+
throw new Error(`pwaManifest: unknown display ${JSON.stringify(display)}`)
|
|
127
|
+
}
|
|
128
|
+
if (
|
|
129
|
+
options.dir !== undefined &&
|
|
130
|
+
options.dir !== "ltr" &&
|
|
131
|
+
options.dir !== "rtl" &&
|
|
132
|
+
options.dir !== "auto"
|
|
133
|
+
) {
|
|
134
|
+
throw new Error(`pwaManifest: unknown dir ${JSON.stringify(options.dir)}`)
|
|
135
|
+
}
|
|
136
|
+
const start_url = options.start_url ?? "/"
|
|
137
|
+
if (start_url === "" || start_url.startsWith("//")) {
|
|
138
|
+
throw new Error("pwaManifest: start_url must be a relative path or absolute URL")
|
|
139
|
+
}
|
|
140
|
+
const manifest: Record<string, unknown> = {}
|
|
141
|
+
if (name !== undefined) manifest.name = name
|
|
142
|
+
if (short_name !== undefined) manifest.short_name = short_name
|
|
143
|
+
if (options.description !== undefined) manifest.description = options.description
|
|
144
|
+
manifest.start_url = start_url
|
|
145
|
+
// The default scope is the start URL's directory ("https://x.com/app/home" → "…/app/"),
|
|
146
|
+
// query/hash excluded; a same-origin absolute URL keeps its origin, a relative one stays
|
|
147
|
+
// relative. Anything unparseable is a developer mistake, and fails loud.
|
|
148
|
+
manifest.scope = options.scope ?? defaultScope(start_url)
|
|
149
|
+
manifest.display = display
|
|
150
|
+
if (options.orientation !== undefined) manifest.orientation = options.orientation
|
|
151
|
+
if (options.background_color !== undefined) manifest.background_color = options.background_color
|
|
152
|
+
if (options.theme_color !== undefined) manifest.theme_color = options.theme_color
|
|
153
|
+
if (options.lang !== undefined) manifest.lang = options.lang
|
|
154
|
+
if (options.dir !== undefined) manifest.dir = options.dir
|
|
155
|
+
if (options.id !== undefined) manifest.id = options.id
|
|
156
|
+
if (options.categories !== undefined) manifest.categories = [...options.categories]
|
|
157
|
+
if (options.icons !== undefined) {
|
|
158
|
+
manifest.icons = options.icons.map((icon, i) => {
|
|
159
|
+
if (icon.src === "") throw new Error(`pwaManifest: icons[${i}].src must not be empty`)
|
|
160
|
+
const sizes = icon.sizes ?? "any"
|
|
161
|
+
if (!SIZE_LIST.test(sizes)) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`pwaManifest: icons[${i}].sizes ${JSON.stringify(icon.sizes)} must be "any" or space-separated WxH`,
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
const entry: Record<string, unknown> = { src: icon.src, sizes }
|
|
167
|
+
if (icon.type !== undefined) entry.type = icon.type
|
|
168
|
+
const purpose = purposeOf(icon.purpose, `icons[${i}]`)
|
|
169
|
+
if (purpose !== undefined) entry.purpose = purpose
|
|
170
|
+
return entry
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
if (options.screenshots !== undefined) {
|
|
174
|
+
manifest.screenshots = options.screenshots.map((shot, i) => {
|
|
175
|
+
if (shot.src === "") throw new Error(`pwaManifest: screenshots[${i}].src must not be empty`)
|
|
176
|
+
const entry: Record<string, unknown> = { src: shot.src }
|
|
177
|
+
if (shot.sizes !== undefined) {
|
|
178
|
+
if (!SIZE_LIST.test(shot.sizes)) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`pwaManifest: screenshots[${i}].sizes ${JSON.stringify(shot.sizes)} must be "any" or space-separated WxH`,
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
entry.sizes = shot.sizes
|
|
184
|
+
}
|
|
185
|
+
if (shot.type !== undefined) entry.type = shot.type
|
|
186
|
+
return entry
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
if (options.shortcuts !== undefined) {
|
|
190
|
+
manifest.shortcuts = options.shortcuts.map((cut, i) => {
|
|
191
|
+
if (cut.name === "") throw new Error(`pwaManifest: shortcuts[${i}].name must not be empty`)
|
|
192
|
+
if (cut.url === "") throw new Error(`pwaManifest: shortcuts[${i}].url must not be empty`)
|
|
193
|
+
const entry: Record<string, unknown> = { name: cut.name, url: cut.url }
|
|
194
|
+
if (cut.short_name !== undefined) entry.short_name = cut.short_name
|
|
195
|
+
if (cut.description !== undefined) entry.description = cut.description
|
|
196
|
+
if (cut.icons !== undefined) {
|
|
197
|
+
entry.icons = cut.icons.map((icon, j) => {
|
|
198
|
+
if (icon.src === "") {
|
|
199
|
+
throw new Error(`pwaManifest: shortcuts[${i}].icons[${j}].src must not be empty`)
|
|
200
|
+
}
|
|
201
|
+
const sizes = icon.sizes ?? "any"
|
|
202
|
+
if (!SIZE_LIST.test(sizes)) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`pwaManifest: shortcuts[${i}].icons[${j}].sizes must be "any" or space-separated WxH`,
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
return { src: icon.src, sizes }
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
return entry
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
return manifest
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Serialize a manifest document to servable JSON (stable key order, no whitespace). */
|
|
217
|
+
export function serializeManifest(manifest: Record<string, unknown>): string {
|
|
218
|
+
return JSON.stringify(manifest)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The `<link rel="manifest">` tag for the document head. */
|
|
222
|
+
export function manifestLink(href = "/manifest.webmanifest"): string {
|
|
223
|
+
if (href === "") throw new Error("manifestLink: href must not be empty")
|
|
224
|
+
const quoted = href.replace(/"/g, """)
|
|
225
|
+
return `<link rel="manifest" href="${quoted}">`
|
|
226
|
+
}
|
package/src/query.ts
CHANGED
|
@@ -642,7 +642,7 @@ export function createMutation<TData, TVariables>(
|
|
|
642
642
|
let mutationFn = fn
|
|
643
643
|
let cbs = callbacks
|
|
644
644
|
let state = IDLE_MUTATION as MutationState<TData, TVariables>
|
|
645
|
-
let
|
|
645
|
+
let latest = 0
|
|
646
646
|
const listeners = new Set<() => void>()
|
|
647
647
|
const emit = (): void => {
|
|
648
648
|
for (const l of listeners) l()
|
|
@@ -665,19 +665,19 @@ export function createMutation<TData, TVariables>(
|
|
|
665
665
|
},
|
|
666
666
|
reset: () => setState(IDLE_MUTATION as MutationState<TData, TVariables>),
|
|
667
667
|
mutate: async (variables) => {
|
|
668
|
-
const mine = ++
|
|
668
|
+
const mine = ++latest
|
|
669
669
|
setState({ status: "pending", data: undefined, error: undefined, variables })
|
|
670
670
|
// Only await a callback when one is actually supplied - an `await undefined` would insert a
|
|
671
671
|
// needless microtask before the mutationFn runs (and complicates single-flight timing).
|
|
672
672
|
if (cbs.onMutate !== undefined) await cbs.onMutate(variables)
|
|
673
673
|
try {
|
|
674
674
|
const data = await mutationFn(variables)
|
|
675
|
-
if (mine ===
|
|
675
|
+
if (mine === latest) setState({ status: "success", data, error: undefined, variables })
|
|
676
676
|
if (cbs.onSuccess !== undefined) await cbs.onSuccess(data, variables)
|
|
677
677
|
if (cbs.onSettled !== undefined) await cbs.onSettled(data, undefined, variables)
|
|
678
678
|
return data
|
|
679
679
|
} catch (error) {
|
|
680
|
-
if (mine ===
|
|
680
|
+
if (mine === latest) setState({ status: "error", data: undefined, error, variables })
|
|
681
681
|
if (cbs.onError !== undefined) await cbs.onError(error, variables)
|
|
682
682
|
if (cbs.onSettled !== undefined) await cbs.onSettled(undefined, error, variables)
|
|
683
683
|
throw error
|
package/src/render-seam.ts
CHANGED
|
@@ -30,6 +30,41 @@ export interface RenderProps {
|
|
|
30
30
|
readonly search?: Record<string, unknown>
|
|
31
31
|
/** Neutral named-boundary states; adapters choose how each boundary's `render` UI is mounted. */
|
|
32
32
|
readonly boundaries?: BoundaryStates
|
|
33
|
+
/** The rendered chain's module ids and `handle` exports, for the adapter's `useMatches`. */
|
|
34
|
+
readonly matchChain?: MatchChain
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The modules a render stacks, as `useMatches` needs them: each one's id and `handle` export,
|
|
39
|
+
* outermost layout first and the page (or status page) last. Built by the server for SSR and by the
|
|
40
|
+
* generated client entry per route, from the same manifest, so both sides report the same matches.
|
|
41
|
+
*/
|
|
42
|
+
export interface MatchChain {
|
|
43
|
+
/** Layout ids (`orgs/[org]/_layout`), then the page's route id. */
|
|
44
|
+
readonly ids: readonly string[]
|
|
45
|
+
/** Each module's `handle` export, index-aligned with {@link ids}. */
|
|
46
|
+
readonly handles: readonly unknown[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One module of the rendered chain, as `useMatches` returns it. */
|
|
50
|
+
export interface UIMatch {
|
|
51
|
+
/** The module's id: a layout's (`orgs/[org]/_layout`), or the page's route id last. */
|
|
52
|
+
readonly id: string
|
|
53
|
+
/** The part of the URL path this module wraps: a layout's directory prefix, the whole path for the
|
|
54
|
+
* page. Raw, as it appears in the URL. */
|
|
55
|
+
readonly pathname: string
|
|
56
|
+
/** The params this module's loader receives: a layout's own, every route param for the page. */
|
|
57
|
+
readonly params: Readonly<Record<string, string>>
|
|
58
|
+
/** This module's loader data: `null` for a layout without a loader. */
|
|
59
|
+
readonly data: unknown
|
|
60
|
+
/** The module's `handle` export - any value, commonly a breadcrumb label or a render function. */
|
|
61
|
+
readonly handle: unknown
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Per-document options for {@link RenderAdapter.renderToStream}. */
|
|
65
|
+
export interface RenderStreamOptions {
|
|
66
|
+
/** The document's CSP nonce, for every executable `<script>` the framework streams. */
|
|
67
|
+
readonly nonce?: string
|
|
33
68
|
}
|
|
34
69
|
|
|
35
70
|
/**
|
|
@@ -39,11 +74,14 @@ export interface RenderProps {
|
|
|
39
74
|
export interface RenderAdapter {
|
|
40
75
|
/**
|
|
41
76
|
* Render a route's layout `chain` (outermost layout → page) to a Web stream of HTML bytes,
|
|
42
|
-
* including the framework's hydration markers.
|
|
77
|
+
* including the framework's hydration markers. When `options.nonce` is set, every executable
|
|
78
|
+
* `<script>` the framework streams (out-of-order Suspense runtimes, serialized resources) must
|
|
79
|
+
* carry it, or a nonce-based Content-Security-Policy blocks it and the boundary never resolves.
|
|
43
80
|
*/
|
|
44
81
|
renderToStream(
|
|
45
82
|
chain: readonly unknown[],
|
|
46
83
|
props: RenderProps,
|
|
84
|
+
options?: RenderStreamOptions,
|
|
47
85
|
): ReadableStream<Uint8Array> | Promise<ReadableStream<Uint8Array>>
|
|
48
86
|
/** Render the chain to a complete HTML string when no content is deferred. */
|
|
49
87
|
renderToString?(chain: readonly unknown[], props: RenderProps): string | Promise<string>
|
|
@@ -67,6 +105,13 @@ export function ssrModuleLoader(): SsrModuleLoader | undefined {
|
|
|
67
105
|
return loaderSlot[SSR_MODULE_LOADER_SLOT]
|
|
68
106
|
}
|
|
69
107
|
|
|
108
|
+
/**
|
|
109
|
+
* `id` of the inert `<script type="application/json">` a hydrating document hands its page state over
|
|
110
|
+
* in: one JSON object keyed by the `*_GLOBAL` names below, which the client entry assigns onto
|
|
111
|
+
* `window` before anything reads them. Data the browser never executes needs no CSP nonce or hash.
|
|
112
|
+
*/
|
|
113
|
+
export const HANDOVER_ID = "__nifra-handover"
|
|
114
|
+
|
|
70
115
|
/** Global the server serializes loader data into; the client reads it to hydrate. */
|
|
71
116
|
export const DATA_GLOBAL = "__NIFRA_DATA__"
|
|
72
117
|
/** Per-layout loader data for hydration. */
|
package/src/route-manifest.ts
CHANGED
|
@@ -16,8 +16,19 @@
|
|
|
16
16
|
* target, and reports what the target cannot honour. Pure and fs-free: it takes an already-built
|
|
17
17
|
* `Manifest` so it runs at build time, in `nifra routes`, and in a test with a hand-built manifest.
|
|
18
18
|
*/
|
|
19
|
+
|
|
20
|
+
import { codeUnitOrder } from "./internal/code-unit-order.ts"
|
|
19
21
|
import type { Manifest, RouteModule } from "./manifest.ts"
|
|
20
22
|
|
|
23
|
+
export {
|
|
24
|
+
formatShadowedPages,
|
|
25
|
+
normalizeMountPath,
|
|
26
|
+
type ShadowedPage,
|
|
27
|
+
shadowedPages,
|
|
28
|
+
} from "./internal/mount-shadow.ts"
|
|
29
|
+
/** Which half of a route each export belongs in: the frontend file or its `x.backend.ts`. */
|
|
30
|
+
export { BACKEND_ROUTE_EXPORTS, FRONTEND_ROUTE_EXPORTS } from "./manifest.ts"
|
|
31
|
+
|
|
21
32
|
/**
|
|
22
33
|
* How a route produces its HTML.
|
|
23
34
|
*
|
|
@@ -115,7 +126,8 @@ export function deriveRouteEntry(
|
|
|
115
126
|
requires: readonly RouteCapability[]
|
|
116
127
|
} = { id, pattern, mode, hydrate: module.hydrate !== false, requires }
|
|
117
128
|
if (mode === "isr" && module.revalidate !== undefined) entry.revalidate = module.revalidate
|
|
118
|
-
|
|
129
|
+
// A function's tags exist per request; the manifest records only a fixed list.
|
|
130
|
+
if (mode === "isr" && Array.isArray(module.revalidateTags)) {
|
|
119
131
|
entry.revalidateTags = module.revalidateTags
|
|
120
132
|
}
|
|
121
133
|
if (prerenderedPaths !== undefined) entry.prerenderedPaths = prerenderedPaths
|
|
@@ -134,7 +146,7 @@ const TARGET_CAPABILITIES: Readonly<Record<string, readonly RouteCapability[]>>
|
|
|
134
146
|
bun: ["server"],
|
|
135
147
|
node: ["server"],
|
|
136
148
|
deno: ["server"],
|
|
137
|
-
|
|
149
|
+
cloudflare: ["server"],
|
|
138
150
|
vercel: ["server"],
|
|
139
151
|
static: [],
|
|
140
152
|
}
|
|
@@ -169,7 +181,7 @@ export async function buildRouteManifest(
|
|
|
169
181
|
const module = (await route.load()) as RouteModule
|
|
170
182
|
entries.push(deriveRouteEntry(route.id, route.pattern, module, options.prerendered?.[route.id]))
|
|
171
183
|
}
|
|
172
|
-
entries.sort((a, b) => a.pattern
|
|
184
|
+
entries.sort((a, b) => codeUnitOrder(a.pattern, b.pattern))
|
|
173
185
|
|
|
174
186
|
const conflicts: RouteManifestConflict[] = []
|
|
175
187
|
const capabilities =
|