@nifrajs/web 4.0.2 → 5.1.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/README.md +19 -0
- package/dist/build-vite.d.ts +19 -0
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +31 -10
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +8 -10
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +90 -31
- package/dist/build.js.map +1 -1
- package/dist/cdn.d.ts +8 -0
- package/dist/cdn.d.ts.map +1 -1
- package/dist/cdn.js +14 -15
- package/dist/cdn.js.map +1 -1
- package/dist/client.d.ts +6 -6
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +46 -18
- package/dist/client.js.map +1 -1
- package/dist/deferred.d.ts +2 -2
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +28 -7
- package/dist/deferred.js.map +1 -1
- package/dist/dev-feed.d.ts.map +1 -1
- package/dist/dev-feed.js +24 -5
- package/dist/dev-feed.js.map +1 -1
- package/dist/dev-port.d.ts +3 -2
- package/dist/dev-port.d.ts.map +1 -1
- package/dist/dev-port.js +2 -6
- package/dist/dev-port.js.map +1 -1
- package/dist/dev-session.d.ts.map +1 -1
- package/dist/dev-session.js +4 -14
- package/dist/dev-session.js.map +1 -1
- package/dist/dev.d.ts +7 -17
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +13 -1
- package/dist/dev.js.map +1 -1
- package/dist/draft.d.ts +8 -5
- package/dist/draft.d.ts.map +1 -1
- package/dist/draft.js +30 -13
- package/dist/draft.js.map +1 -1
- package/dist/fonts-google.d.ts.map +1 -1
- package/dist/fonts-google.js +7 -0
- package/dist/fonts-google.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/internal/codegen.d.ts +1 -0
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +56 -29
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/disposable-dir.d.ts +8 -0
- package/dist/internal/disposable-dir.d.ts.map +1 -0
- package/dist/internal/disposable-dir.js +25 -0
- package/dist/internal/disposable-dir.js.map +1 -0
- package/dist/internal/entry-chunks.d.ts +9 -0
- package/dist/internal/entry-chunks.d.ts.map +1 -0
- package/dist/internal/entry-chunks.js +15 -0
- package/dist/internal/entry-chunks.js.map +1 -0
- package/dist/internal/evidence-traversal.d.ts +12 -0
- package/dist/internal/evidence-traversal.d.ts.map +1 -0
- package/dist/internal/evidence-traversal.js +48 -0
- package/dist/internal/evidence-traversal.js.map +1 -0
- package/dist/internal/head-merge.d.ts +6 -0
- package/dist/internal/head-merge.d.ts.map +1 -1
- package/dist/internal/head-merge.js +9 -0
- package/dist/internal/head-merge.js.map +1 -1
- package/dist/internal/js-string.d.ts +2 -0
- package/dist/internal/js-string.d.ts.map +1 -1
- package/dist/internal/js-string.js +5 -1
- package/dist/internal/js-string.js.map +1 -1
- package/dist/internal/mount-shadow.js +1 -1
- package/dist/internal/mount-shadow.js.map +1 -1
- package/dist/internal/output-guard.d.ts +7 -0
- package/dist/internal/output-guard.d.ts.map +1 -1
- package/dist/internal/output-guard.js +80 -8
- package/dist/internal/output-guard.js.map +1 -1
- package/dist/internal/package-entries.d.ts +34 -0
- package/dist/internal/package-entries.d.ts.map +1 -0
- package/dist/internal/package-entries.js +324 -0
- package/dist/internal/package-entries.js.map +1 -0
- package/dist/internal/page-execution.d.ts +6 -0
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +437 -114
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +15 -2
- package/dist/internal/parity.js.map +1 -1
- package/dist/internal/private-env.d.ts +8 -0
- package/dist/internal/private-env.d.ts.map +1 -1
- package/dist/internal/private-env.js +25 -10
- package/dist/internal/private-env.js.map +1 -1
- package/dist/internal/progressive-cookies.d.ts +14 -0
- package/dist/internal/progressive-cookies.d.ts.map +1 -0
- package/dist/internal/progressive-cookies.js +43 -0
- package/dist/internal/progressive-cookies.js.map +1 -0
- package/dist/internal/public-env.d.ts +2 -0
- package/dist/internal/public-env.d.ts.map +1 -0
- package/dist/internal/public-env.js +13 -0
- package/dist/internal/public-env.js.map +1 -0
- package/dist/internal/render-document.d.ts +5 -44
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +51 -63
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/response-controls.d.ts +2 -0
- package/dist/internal/response-controls.d.ts.map +1 -1
- package/dist/internal/response-controls.js +26 -16
- package/dist/internal/response-controls.js.map +1 -1
- package/dist/internal/revive-dates.d.ts +16 -0
- package/dist/internal/revive-dates.d.ts.map +1 -0
- package/dist/internal/revive-dates.js +32 -0
- package/dist/internal/revive-dates.js.map +1 -0
- package/dist/internal/route-path.d.ts +14 -0
- package/dist/internal/route-path.d.ts.map +1 -0
- package/dist/internal/route-path.js +19 -0
- package/dist/internal/route-path.js.map +1 -0
- package/dist/internal/secret-scan.d.ts.map +1 -1
- package/dist/internal/secret-scan.js +190 -20
- package/dist/internal/secret-scan.js.map +1 -1
- package/dist/internal/server-boundary.d.ts.map +1 -1
- package/dist/internal/server-boundary.js +33 -5
- package/dist/internal/server-boundary.js.map +1 -1
- package/dist/internal/web-app.d.ts +25 -18
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +81 -24
- package/dist/internal/web-app.js.map +1 -1
- package/dist/internal/wire-date.d.ts +3 -0
- package/dist/internal/wire-date.d.ts.map +1 -0
- package/dist/internal/wire-date.js +14 -0
- package/dist/internal/wire-date.js.map +1 -0
- package/dist/internal/without-query.d.ts +4 -0
- package/dist/internal/without-query.d.ts.map +1 -0
- package/dist/internal/without-query.js +9 -0
- package/dist/internal/without-query.js.map +1 -0
- package/dist/internal/zone-graph.d.ts +4 -0
- package/dist/internal/zone-graph.d.ts.map +1 -1
- package/dist/internal/zone-graph.js +71 -20
- package/dist/internal/zone-graph.js.map +1 -1
- package/dist/isr.d.ts +59 -3
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +311 -41
- package/dist/isr.js.map +1 -1
- package/dist/llms-txt.d.ts +2 -0
- package/dist/llms-txt.d.ts.map +1 -1
- package/dist/llms-txt.js +29 -3
- package/dist/llms-txt.js.map +1 -1
- package/dist/manifest.d.ts +37 -4
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +4 -0
- package/dist/manifest.js.map +1 -1
- package/dist/open-resolved.d.ts +18 -0
- package/dist/open-resolved.d.ts.map +1 -0
- package/dist/open-resolved.js +62 -0
- package/dist/open-resolved.js.map +1 -0
- package/dist/plugins/kit.d.ts +1 -0
- package/dist/plugins/kit.d.ts.map +1 -1
- package/dist/plugins/kit.js +26 -11
- package/dist/plugins/kit.js.map +1 -1
- package/dist/plugins/vite-leak-guard.d.ts +9 -1
- package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
- package/dist/plugins/vite-leak-guard.js +84 -7
- package/dist/plugins/vite-leak-guard.js.map +1 -1
- package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
- package/dist/plugins/vite-zone-guard.js +17 -21
- package/dist/plugins/vite-zone-guard.js.map +1 -1
- package/dist/plugins/zone-guard.d.ts.map +1 -1
- package/dist/plugins/zone-guard.js +44 -3
- package/dist/plugins/zone-guard.js.map +1 -1
- package/dist/prerender.d.ts +5 -1
- package/dist/prerender.d.ts.map +1 -1
- package/dist/prerender.js +41 -19
- package/dist/prerender.js.map +1 -1
- package/dist/public-dir.d.ts +8 -1
- package/dist/public-dir.d.ts.map +1 -1
- package/dist/public-dir.js +17 -6
- package/dist/public-dir.js.map +1 -1
- package/dist/query.d.ts +13 -4
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +89 -26
- package/dist/query.js.map +1 -1
- package/dist/render-seam.d.ts +25 -2
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +1 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/route-types.d.ts.map +1 -1
- package/dist/route-types.js +20 -9
- package/dist/route-types.js.map +1 -1
- package/dist/router.d.ts +9 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +47 -50
- package/dist/router.js.map +1 -1
- package/dist/seo.d.ts +49 -0
- package/dist/seo.d.ts.map +1 -0
- package/dist/seo.js +48 -0
- package/dist/seo.js.map +1 -0
- package/dist/server-components/build-info.d.ts +3 -0
- package/dist/server-components/build-info.d.ts.map +1 -0
- package/dist/server-components/build-info.js +7 -0
- package/dist/server-components/build-info.js.map +1 -0
- package/dist/server-components/build-target.d.ts +12 -0
- package/dist/server-components/build-target.d.ts.map +1 -0
- package/dist/server-components/build-target.js +153 -0
- package/dist/server-components/build-target.js.map +1 -0
- package/dist/server-components/build.d.ts +24 -0
- package/dist/server-components/build.d.ts.map +1 -0
- package/dist/server-components/build.js +366 -0
- package/dist/server-components/build.js.map +1 -0
- package/dist/server-components/bundle-proof.d.ts +3 -0
- package/dist/server-components/bundle-proof.d.ts.map +1 -0
- package/dist/server-components/bundle-proof.js +23 -0
- package/dist/server-components/bundle-proof.js.map +1 -0
- package/dist/server-components/client.d.ts +46 -0
- package/dist/server-components/client.d.ts.map +1 -0
- package/dist/server-components/client.js +594 -0
- package/dist/server-components/client.js.map +1 -0
- package/dist/server-components/definition.d.ts +11 -0
- package/dist/server-components/definition.d.ts.map +1 -0
- package/dist/server-components/definition.js +94 -0
- package/dist/server-components/definition.js.map +1 -0
- package/dist/server-components/dev-http.d.ts +5 -0
- package/dist/server-components/dev-http.d.ts.map +1 -0
- package/dist/server-components/dev-http.js +71 -0
- package/dist/server-components/dev-http.js.map +1 -0
- package/dist/server-components/error.d.ts +9 -0
- package/dist/server-components/error.d.ts.map +1 -0
- package/dist/server-components/error.js +24 -0
- package/dist/server-components/error.js.map +1 -0
- package/dist/server-components/frames.d.ts +7 -0
- package/dist/server-components/frames.d.ts.map +1 -0
- package/dist/server-components/frames.js +169 -0
- package/dist/server-components/frames.js.map +1 -0
- package/dist/server-components/html-policy.d.ts +6 -0
- package/dist/server-components/html-policy.d.ts.map +1 -0
- package/dist/server-components/html-policy.js +75 -0
- package/dist/server-components/html-policy.js.map +1 -0
- package/dist/server-components/html.d.ts +4 -0
- package/dist/server-components/html.d.ts.map +1 -0
- package/dist/server-components/html.js +67 -0
- package/dist/server-components/html.js.map +1 -0
- package/dist/server-components/islands.d.ts +44 -0
- package/dist/server-components/islands.d.ts.map +1 -0
- package/dist/server-components/islands.js +42 -0
- package/dist/server-components/islands.js.map +1 -0
- package/dist/server-components/native.d.ts +4 -0
- package/dist/server-components/native.d.ts.map +1 -0
- package/dist/server-components/native.js +81 -0
- package/dist/server-components/native.js.map +1 -0
- package/dist/server-components/portal.d.ts +6 -0
- package/dist/server-components/portal.d.ts.map +1 -0
- package/dist/server-components/portal.js +35 -0
- package/dist/server-components/portal.js.map +1 -0
- package/dist/server-components/props.d.ts +3 -0
- package/dist/server-components/props.d.ts.map +1 -0
- package/dist/server-components/props.js +14 -0
- package/dist/server-components/props.js.map +1 -0
- package/dist/server-components/protocol.d.ts +44 -0
- package/dist/server-components/protocol.d.ts.map +1 -0
- package/dist/server-components/protocol.js +139 -0
- package/dist/server-components/protocol.js.map +1 -0
- package/dist/server-components/range.d.ts +24 -0
- package/dist/server-components/range.d.ts.map +1 -0
- package/dist/server-components/range.js +263 -0
- package/dist/server-components/range.js.map +1 -0
- package/dist/server-components/registry.d.ts +6 -0
- package/dist/server-components/registry.d.ts.map +1 -0
- package/dist/server-components/registry.js +30 -0
- package/dist/server-components/registry.js.map +1 -0
- package/dist/server-components/response.d.ts +4 -0
- package/dist/server-components/response.d.ts.map +1 -0
- package/dist/server-components/response.js +32 -0
- package/dist/server-components/response.js.map +1 -0
- package/dist/server-components/router-integration.d.ts +9 -0
- package/dist/server-components/router-integration.d.ts.map +1 -0
- package/dist/server-components/router-integration.js +29 -0
- package/dist/server-components/router-integration.js.map +1 -0
- package/dist/server-components/runtime.d.ts +14 -0
- package/dist/server-components/runtime.d.ts.map +1 -0
- package/dist/server-components/runtime.js +276 -0
- package/dist/server-components/runtime.js.map +1 -0
- package/dist/server-components/scheduler.d.ts +14 -0
- package/dist/server-components/scheduler.d.ts.map +1 -0
- package/dist/server-components/scheduler.js +74 -0
- package/dist/server-components/scheduler.js.map +1 -0
- package/dist/server-components/schema.d.ts +4 -0
- package/dist/server-components/schema.d.ts.map +1 -0
- package/dist/server-components/schema.js +77 -0
- package/dist/server-components/schema.js.map +1 -0
- package/dist/server-components/server-entry.d.ts +12 -0
- package/dist/server-components/server-entry.d.ts.map +1 -0
- package/dist/server-components/server-entry.js +90 -0
- package/dist/server-components/server-entry.js.map +1 -0
- package/dist/server-components/source.d.ts +15 -0
- package/dist/server-components/source.d.ts.map +1 -0
- package/dist/server-components/source.js +269 -0
- package/dist/server-components/source.js.map +1 -0
- package/dist/server-components/state.d.ts +4 -0
- package/dist/server-components/state.d.ts.map +1 -0
- package/dist/server-components/state.js +123 -0
- package/dist/server-components/state.js.map +1 -0
- package/dist/server-components/stream.d.ts +6 -0
- package/dist/server-components/stream.d.ts.map +1 -0
- package/dist/server-components/stream.js +70 -0
- package/dist/server-components/stream.js.map +1 -0
- package/dist/server-components/trusted-html.d.ts +3 -0
- package/dist/server-components/trusted-html.d.ts.map +1 -0
- package/dist/server-components/trusted-html.js +14 -0
- package/dist/server-components/trusted-html.js.map +1 -0
- package/dist/server-components/types.d.ts +107 -0
- package/dist/server-components/types.d.ts.map +1 -0
- package/dist/server-components/types.js +2 -0
- package/dist/server-components/types.js.map +1 -0
- package/dist/server-components/typescript.d.ts +11 -0
- package/dist/server-components/typescript.d.ts.map +1 -0
- package/dist/server-components/typescript.js +28 -0
- package/dist/server-components/typescript.js.map +1 -0
- package/dist/server-components/value.d.ts +5 -0
- package/dist/server-components/value.d.ts.map +1 -0
- package/dist/server-components/value.js +77 -0
- package/dist/server-components/value.js.map +1 -0
- package/dist/server-components/vite.d.ts +12 -0
- package/dist/server-components/vite.d.ts.map +1 -0
- package/dist/server-components/vite.js +197 -0
- package/dist/server-components/vite.js.map +1 -0
- package/dist/server-components.d.ts +5 -0
- package/dist/server-components.d.ts.map +1 -0
- package/dist/server-components.js +4 -0
- package/dist/server-components.js.map +1 -0
- package/dist/service-worker.d.ts +8 -1
- package/dist/service-worker.d.ts.map +1 -1
- package/dist/service-worker.js +20 -6
- package/dist/service-worker.js.map +1 -1
- package/dist/vite.d.ts +12 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +7 -1
- package/dist/vite.js.map +1 -1
- package/dist/zones.d.ts +64 -2
- package/dist/zones.d.ts.map +1 -1
- package/dist/zones.js +331 -28
- package/dist/zones.js.map +1 -1
- package/package.json +102 -4
- package/src/build-vite.ts +31 -9
- package/src/build.ts +109 -31
- package/src/cdn.ts +18 -12
- package/src/client.ts +48 -17
- package/src/deferred.ts +27 -9
- package/src/dev-feed.ts +24 -5
- package/src/dev-port.ts +4 -10
- package/src/dev-session.ts +4 -13
- package/src/dev.ts +29 -1
- package/src/draft.ts +35 -14
- package/src/fonts-google.ts +10 -0
- package/src/index.ts +5 -6
- package/src/internal/codegen.ts +60 -29
- package/src/internal/disposable-dir.ts +24 -0
- package/src/internal/entry-chunks.ts +18 -0
- package/src/internal/evidence-traversal.ts +57 -0
- package/src/internal/head-merge.ts +16 -0
- package/src/internal/js-string.ts +6 -1
- package/src/internal/mount-shadow.ts +1 -1
- package/src/internal/output-guard.ts +88 -7
- package/src/internal/package-entries.ts +331 -0
- package/src/internal/page-execution.ts +573 -114
- package/src/internal/parity.ts +15 -2
- package/src/internal/private-env.ts +27 -13
- package/src/internal/progressive-cookies.ts +60 -0
- package/src/internal/public-env.ts +14 -0
- package/src/internal/render-document.ts +66 -89
- package/src/internal/response-controls.ts +31 -14
- package/src/internal/revive-dates.ts +35 -0
- package/src/internal/route-path.ts +24 -0
- package/src/internal/secret-scan.ts +190 -18
- package/src/internal/server-boundary.ts +29 -5
- package/src/internal/web-app.ts +126 -42
- package/src/internal/wire-date.ts +16 -0
- package/src/internal/without-query.ts +8 -0
- package/src/internal/zone-graph.ts +78 -25
- package/src/isr.ts +364 -45
- package/src/llms-txt.ts +32 -3
- package/src/manifest.ts +38 -4
- package/src/open-resolved.ts +64 -0
- package/src/plugins/kit.ts +31 -11
- package/src/plugins/vite-leak-guard.ts +104 -8
- package/src/plugins/vite-zone-guard.ts +21 -19
- package/src/plugins/zone-guard.ts +41 -2
- package/src/prerender.ts +39 -18
- package/src/public-dir.ts +27 -8
- package/src/query.ts +107 -26
- package/src/render-seam.ts +30 -2
- package/src/route-types.ts +34 -7
- package/src/router.ts +66 -59
- package/src/seo.ts +68 -0
- package/src/server-components/build-info.ts +11 -0
- package/src/server-components/build-target.ts +194 -0
- package/src/server-components/build.ts +419 -0
- package/src/server-components/bundle-proof.ts +33 -0
- package/src/server-components/client.ts +663 -0
- package/src/server-components/definition.ts +125 -0
- package/src/server-components/dev-http.ts +75 -0
- package/src/server-components/error.ts +24 -0
- package/src/server-components/frames.ts +181 -0
- package/src/server-components/html-policy.ts +94 -0
- package/src/server-components/html.ts +81 -0
- package/src/server-components/islands.ts +111 -0
- package/src/server-components/native.ts +101 -0
- package/src/server-components/portal.ts +42 -0
- package/src/server-components/props.ts +19 -0
- package/src/server-components/protocol.ts +167 -0
- package/src/server-components/range.ts +313 -0
- package/src/server-components/registry.ts +34 -0
- package/src/server-components/response.ts +44 -0
- package/src/server-components/router-integration.ts +40 -0
- package/src/server-components/runtime.ts +376 -0
- package/src/server-components/scheduler.ts +84 -0
- package/src/server-components/schema.ts +69 -0
- package/src/server-components/server-entry.ts +98 -0
- package/src/server-components/source.ts +324 -0
- package/src/server-components/state.ts +143 -0
- package/src/server-components/stream.ts +71 -0
- package/src/server-components/trusted-html.ts +21 -0
- package/src/server-components/types.ts +138 -0
- package/src/server-components/typescript.ts +39 -0
- package/src/server-components/value.ts +82 -0
- package/src/server-components/vite.ts +242 -0
- package/src/server-components.ts +32 -0
- package/src/service-worker.ts +24 -7
- package/src/vite.ts +8 -1
- package/src/zones.ts +418 -32
package/src/client.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* a full page load). DOM-only - never imported on the server, so the store stays SSR-safe.
|
|
6
6
|
*/
|
|
7
7
|
import { trustedHeadAttributes } from "./internal/head-attributes.ts"
|
|
8
|
+
import { appUrl } from "./internal/route-path.ts"
|
|
8
9
|
import { EXECUTABLE_SCRIPT_TYPES, INERT_SCRIPT_TYPES } from "./internal/script-types.ts"
|
|
9
10
|
import type { Meta } from "./manifest.ts"
|
|
10
11
|
import {
|
|
@@ -40,7 +41,7 @@ export function currentDocumentNonce(): string | undefined {
|
|
|
40
41
|
* serves modules as written, so the browser evaluated `public-dir.ts` and died on `node:fs/promises`
|
|
41
42
|
* before hydrating. The fix is for the entry not to name the server module in the first place.
|
|
42
43
|
*/
|
|
43
|
-
export { mergeHeads, resolveMeta } from "./internal/head-merge.ts"
|
|
44
|
+
export { mergeHeads, resolveChainMeta, resolveMeta } from "./internal/head-merge.ts"
|
|
44
45
|
export {
|
|
45
46
|
getBrowserNavigate,
|
|
46
47
|
IDLE_BLOCKER,
|
|
@@ -232,7 +233,7 @@ export function installHistory(
|
|
|
232
233
|
router: ClientRouter,
|
|
233
234
|
options: InstallHistoryOptions = {},
|
|
234
235
|
): () => void {
|
|
235
|
-
const fallback = options.fallback ?? ((path: string) => location.assign(path))
|
|
236
|
+
const fallback = options.fallback ?? ((path: string) => location.assign(appUrl(path)))
|
|
236
237
|
|
|
237
238
|
// We manage scroll ourselves (save per history entry, restore after the navigated content
|
|
238
239
|
// renders), so disable the browser's native restoration which would otherwise fight us.
|
|
@@ -364,7 +365,7 @@ export function installHistory(
|
|
|
364
365
|
if (mode === "replace") {
|
|
365
366
|
// Replace the current entry (same index) with the new URL. The entry we're leaving is discarded, so
|
|
366
367
|
// its scroll isn't worth saving; the new route scrolls to its fragment target, else the top.
|
|
367
|
-
history.replaceState(entryState, "", path)
|
|
368
|
+
history.replaceState(entryState, "", appUrl(path))
|
|
368
369
|
pendingScroll = url.hash !== "" ? { hash: hashId(url.hash) } : { pos: [0, 0] }
|
|
369
370
|
} else {
|
|
370
371
|
// Save the leaving entry's scroll (spread keeps its nifraIndex), push a fresh higher-indexed entry
|
|
@@ -372,7 +373,7 @@ export function installHistory(
|
|
|
372
373
|
history.replaceState({ ...(history.state ?? {}), nifraScroll: [scrollX, scrollY] }, "")
|
|
373
374
|
index += 1
|
|
374
375
|
entryState.nifraIndex = index
|
|
375
|
-
history.pushState(entryState, "", path)
|
|
376
|
+
history.pushState(entryState, "", appUrl(path))
|
|
376
377
|
pendingScroll = url.hash !== "" ? { hash: hashId(url.hash) } : { pos: [0, 0] }
|
|
377
378
|
}
|
|
378
379
|
here = url.pathname + url.search + url.hash
|
|
@@ -555,9 +556,9 @@ export function installHistory(
|
|
|
555
556
|
if (expected === undefined) {
|
|
556
557
|
history.replaceState({ ...(history.state ?? {}), nifraScroll: [scrollX, scrollY] }, "")
|
|
557
558
|
index += 1
|
|
558
|
-
history.pushState({ nifraIndex: index }, "", path)
|
|
559
|
+
history.pushState({ nifraIndex: index }, "", appUrl(path))
|
|
559
560
|
} else {
|
|
560
|
-
history.replaceState(history.state, "", path + location.hash)
|
|
561
|
+
history.replaceState(history.state, "", appUrl(path) + location.hash)
|
|
561
562
|
}
|
|
562
563
|
here = location.pathname + location.search + location.hash
|
|
563
564
|
pendingScroll = location.hash !== "" ? { hash: hashId(location.hash) } : { pos: [0, 0] }
|
|
@@ -610,11 +611,6 @@ export function installHistory(
|
|
|
610
611
|
}
|
|
611
612
|
}
|
|
612
613
|
|
|
613
|
-
/**
|
|
614
|
-
* Intercept submissions of same-origin `<form method="post">` whose action targets an app route:
|
|
615
|
-
* submit via the router (no full reload - POST the action, then revalidate the active loader). On
|
|
616
|
-
* failure it falls back to a native submit, so the form still works. Returns a teardown function.
|
|
617
|
-
*/
|
|
618
614
|
/**
|
|
619
615
|
* Sync the document head to a route's resolved {@link Meta} on client navigation. Sets the title
|
|
620
616
|
* (when provided) and replaces the **managed** (`data-nifra`) `<meta>`/`<link>` tags - static head
|
|
@@ -689,24 +685,59 @@ export function applyHead(head: Meta): void {
|
|
|
689
685
|
}
|
|
690
686
|
}
|
|
691
687
|
|
|
688
|
+
// A control named like a form property shadows it (`<input name="target">` makes `form.target` that
|
|
689
|
+
// input), so the form's own state is read through the prototype accessors.
|
|
690
|
+
function formProperty(form: HTMLFormElement, name: "action" | "method" | "target"): string {
|
|
691
|
+
return String(Reflect.get(HTMLFormElement.prototype, name, form))
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Intercept submissions of same-origin `<form method="post">` whose action targets an app route:
|
|
696
|
+
* submit via the router (no full reload - POST the action, then revalidate the active loader). On
|
|
697
|
+
* failure it falls back to a native submit, so the form still works. Returns a teardown function.
|
|
698
|
+
*/
|
|
692
699
|
export function installForms(router: ClientRouter): () => void {
|
|
700
|
+
// Forms whose data submit failed before the server answered: their next submit event is the native
|
|
701
|
+
// post that replaces it.
|
|
702
|
+
const native = new WeakSet<HTMLFormElement>()
|
|
693
703
|
const onSubmit = (event: SubmitEvent): void => {
|
|
694
704
|
if (event.defaultPrevented) return
|
|
695
705
|
const form = event.target
|
|
696
706
|
if (!(form instanceof HTMLFormElement)) return
|
|
697
|
-
if (
|
|
698
|
-
|
|
707
|
+
if (native.delete(form)) return
|
|
708
|
+
// The button that submitted can override the form's method, action and target, and its
|
|
709
|
+
// name=value is part of the submission (`<button name="intent" value="delete">`).
|
|
710
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: a submit event's submitter is the submit button or input that fired it, or null
|
|
711
|
+
const submitter = event.submitter as HTMLButtonElement | null
|
|
712
|
+
const method = submitter?.hasAttribute("formmethod")
|
|
713
|
+
? submitter.formMethod
|
|
714
|
+
: formProperty(form, "method")
|
|
715
|
+
if (method.toLowerCase() !== "post") return // GET forms submit natively
|
|
716
|
+
const target = submitter?.hasAttribute("formtarget")
|
|
717
|
+
? submitter.formTarget
|
|
718
|
+
: formProperty(form, "target")
|
|
719
|
+
if (target !== "" && target !== "_self") return // it loads in another browsing context
|
|
720
|
+
// Resolved (or the current document URL if unset).
|
|
721
|
+
const url = sameDocumentUrl(
|
|
722
|
+
submitter?.hasAttribute("formaction") ? submitter.formAction : formProperty(form, "action"),
|
|
723
|
+
)
|
|
699
724
|
if (url === null) return
|
|
700
725
|
if (router.match(url.pathname) === null) return // not an app route → native submit
|
|
701
726
|
event.preventDefault()
|
|
702
727
|
// `data-nifra-revalidate="false"` opts out of the post-action loader revalidation (the action's
|
|
703
728
|
// actionData drives the update); absent or any other value keeps the default revalidation.
|
|
704
|
-
const revalidate =
|
|
705
|
-
|
|
729
|
+
const revalidate =
|
|
730
|
+
Element.prototype.getAttribute.call(form, "data-nifra-revalidate") !== "false"
|
|
731
|
+
const body = new FormData(form, submitter)
|
|
732
|
+
router.submit(url.pathname + url.search, body, { revalidate }).catch((error) => {
|
|
706
733
|
const to = redirectOf(error)
|
|
707
|
-
// The
|
|
734
|
+
// The server answered: load the page that shows the result rather than run the action again.
|
|
708
735
|
if (to !== undefined) location.assign(to)
|
|
709
|
-
else
|
|
736
|
+
else {
|
|
737
|
+
// Nothing reached the server - post natively, through the same button.
|
|
738
|
+
native.add(form)
|
|
739
|
+
HTMLFormElement.prototype.requestSubmit.call(form, submitter)
|
|
740
|
+
}
|
|
710
741
|
})
|
|
711
742
|
}
|
|
712
743
|
document.addEventListener("submit", onSubmit)
|
package/src/deferred.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* resolution scripts settle. The per-adapter `<Await>` lives in `@nifrajs/web-{solid,react}/client`.
|
|
7
7
|
*/
|
|
8
8
|
import { plainJsonCodec, type TransportCodec } from "@nifrajs/core/transport-codec"
|
|
9
|
+
import { parsePageData } from "./internal/revive-dates.ts"
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* A loader value marked to stream in after the shell. The component consumes it with the adapter's
|
|
@@ -164,25 +165,29 @@ export function ndjsonStream(
|
|
|
164
165
|
deferred: ReadonlyArray<{ readonly id: number; readonly promise: Promise<unknown> }>,
|
|
165
166
|
codec: TransportCodec = plainJsonCodec,
|
|
166
167
|
): ReadableStream<Uint8Array> {
|
|
168
|
+
// A client that goes away cancels the body; a value settling after that has nowhere to go.
|
|
169
|
+
let cancelled = false
|
|
167
170
|
return new ReadableStream({
|
|
168
171
|
async start(controller) {
|
|
169
172
|
controller.enqueue(NDJSON_ENCODER.encode(`${codec.encode(forClient)}\n`))
|
|
170
173
|
await Promise.all(
|
|
171
174
|
deferred.map(async (d) => {
|
|
175
|
+
let line: string
|
|
172
176
|
try {
|
|
173
|
-
|
|
174
|
-
controller.enqueue(NDJSON_ENCODER.encode(`${codec.encode({ i: d.id, v: value })}\n`))
|
|
177
|
+
line = codec.encode({ i: d.id, v: await d.promise })
|
|
175
178
|
} catch (err) {
|
|
176
179
|
// Redact: stream a stable opaque code, never the raw error text; log the real reason
|
|
177
180
|
// server-side. The client's <Await> errorFallback receives this code.
|
|
178
181
|
console.error("[nifra/web] deferred value rejected:", err)
|
|
179
|
-
|
|
180
|
-
NDJSON_ENCODER.encode(`${codec.encode({ i: d.id, e: DEFERRED_ERROR_CODE })}\n`),
|
|
181
|
-
)
|
|
182
|
+
line = codec.encode({ i: d.id, e: DEFERRED_ERROR_CODE })
|
|
182
183
|
}
|
|
184
|
+
if (!cancelled) controller.enqueue(NDJSON_ENCODER.encode(`${line}\n`))
|
|
183
185
|
}),
|
|
184
186
|
)
|
|
185
|
-
controller.close()
|
|
187
|
+
if (!cancelled) controller.close()
|
|
188
|
+
},
|
|
189
|
+
cancel() {
|
|
190
|
+
cancelled = true
|
|
186
191
|
},
|
|
187
192
|
})
|
|
188
193
|
}
|
|
@@ -213,7 +218,16 @@ export const DEFERRED_RUNTIME = `(() => {
|
|
|
213
218
|
// Tag the promise (status/value/reason) - React's use() reads them and returns SYNCHRONOUSLY at
|
|
214
219
|
// hydration, so it renders the resolved content directly into the <Suspense> boundary the server
|
|
215
220
|
// streamed (no re-suspend, no fallback flash). r/j also wake awaiters (Solid uses its own _$HY).
|
|
216
|
-
|
|
221
|
+
// A t.date() value arrives tagged; rebuild it before anything reads the settled value.
|
|
222
|
+
const __nifraDates = (d) => {
|
|
223
|
+
if (!d || typeof d !== "object") return d
|
|
224
|
+
if (Array.isArray(d)) { for (let i = 0; i < d.length; i++) d[i] = __nifraDates(d[i]); return d }
|
|
225
|
+
const k = Object.keys(d)
|
|
226
|
+
if (k.length === 1 && k[0] === "__nifra_date" && typeof d.__nifra_date === "string") return new Date(d.__nifra_date)
|
|
227
|
+
for (const x of k) { const v = __nifraDates(d[x]); if (v !== d[x]) Object.defineProperty(d, x, { value: v, enumerable: true, writable: true, configurable: true }) }
|
|
228
|
+
return d
|
|
229
|
+
};
|
|
230
|
+
window.__nifraResolve = (id, v) => { v = __nifraDates(v); const e = get(id); e.p.status = "fulfilled"; e.p.value = v; e.r(v); };
|
|
217
231
|
window.__nifraReject = (id, x) => { const e = get(id); e.p.status = "rejected"; e.p.reason = x; e.j(x); };
|
|
218
232
|
})();`
|
|
219
233
|
|
|
@@ -225,6 +239,7 @@ export const DEFERRED_RUNTIME = `(() => {
|
|
|
225
239
|
export const MAP_DEFERRED_SOURCE = `const mapDeferred = (d) => {
|
|
226
240
|
if (!d || typeof d !== "object") return d
|
|
227
241
|
if (typeof d.__nifra_deferred === "number") return { __nifra_deferred: true, id: d.__nifra_deferred, promise: window.__nifraDeferred(d.__nifra_deferred) }
|
|
242
|
+
if (typeof d.__nifra_date === "string" && Object.keys(d).length === 1) return new Date(d.__nifra_date)
|
|
228
243
|
if (Array.isArray(d)) return d.map(mapDeferred)
|
|
229
244
|
const out = {}
|
|
230
245
|
for (const k of Object.keys(d)) Object.defineProperty(out, k, { value: mapDeferred(d[k]), enumerable: true, writable: true, configurable: true })
|
|
@@ -326,8 +341,11 @@ export async function parseNdjsonData(
|
|
|
326
341
|
}
|
|
327
342
|
|
|
328
343
|
const pending: Pending = new Map()
|
|
344
|
+
// Only plain JSON carries t.date() tags; a richer codec decodes its own dates.
|
|
345
|
+
const decode = (line: string): unknown =>
|
|
346
|
+
codec === plainJsonCodec ? parsePageData(line) : codec.decode(line)
|
|
329
347
|
const first = await readLine()
|
|
330
|
-
const data = markersFromPlaceholders(first === null ? null :
|
|
348
|
+
const data = markersFromPlaceholders(first === null ? null : decode(first), pending)
|
|
331
349
|
|
|
332
350
|
void (async () => {
|
|
333
351
|
try {
|
|
@@ -335,7 +353,7 @@ export async function parseNdjsonData(
|
|
|
335
353
|
const line = await readLine()
|
|
336
354
|
if (line === null) break
|
|
337
355
|
if (line === "") continue
|
|
338
|
-
const msg =
|
|
356
|
+
const msg = decode(line) as { i: number; v?: unknown; e?: string }
|
|
339
357
|
const entry = pending.get(msg.i)
|
|
340
358
|
if (entry === undefined) continue
|
|
341
359
|
pending.delete(msg.i)
|
package/src/dev-feed.ts
CHANGED
|
@@ -392,6 +392,25 @@ function fnv1a(text: string): string {
|
|
|
392
392
|
return (hash >>> 0).toString(16).padStart(8, "0")
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
+
/**
|
|
396
|
+
* A query value may be a single-use ticket or a token no redaction pattern knows, so the feed keeps
|
|
397
|
+
* which keys a request sent and never their values. `/x?ticket=abc&page` -> `/x?ticket=[redacted]&[redacted]`.
|
|
398
|
+
*/
|
|
399
|
+
function queryKeysOnly(target: string): string {
|
|
400
|
+
const at = target.indexOf("?")
|
|
401
|
+
if (at === -1) return target
|
|
402
|
+
const hash = target.indexOf("#", at)
|
|
403
|
+
const parts = target
|
|
404
|
+
.slice(at + 1, hash === -1 ? undefined : hash)
|
|
405
|
+
.split("&")
|
|
406
|
+
.filter((part) => part.length > 0)
|
|
407
|
+
.map((part) => {
|
|
408
|
+
const eq = part.indexOf("=")
|
|
409
|
+
return eq === -1 ? "[redacted]" : `${part.slice(0, eq)}=[redacted]`
|
|
410
|
+
})
|
|
411
|
+
return parts.length === 0 ? target.slice(0, at) : `${target.slice(0, at)}?${parts.join("&")}`
|
|
412
|
+
}
|
|
413
|
+
|
|
395
414
|
const cap = (text: string, max: number): string =>
|
|
396
415
|
text.length <= max ? text : `${text.slice(0, max)}... [${text.length - max} more chars]`
|
|
397
416
|
|
|
@@ -477,7 +496,7 @@ export function createDevFeed(options: DevFeedOptions): DevFeed {
|
|
|
477
496
|
if (diagnostic.request !== undefined) {
|
|
478
497
|
redacted.request = {
|
|
479
498
|
method: diagnostic.request.method,
|
|
480
|
-
url: cap(redactText(diagnostic.request.url), 2048),
|
|
499
|
+
url: cap(redactText(queryKeysOnly(diagnostic.request.url)), 2048),
|
|
481
500
|
}
|
|
482
501
|
}
|
|
483
502
|
if (diagnostic.codeframe !== undefined) {
|
|
@@ -530,7 +549,7 @@ export function createDevFeed(options: DevFeedOptions): DevFeed {
|
|
|
530
549
|
record.resolvedAt = undefined
|
|
531
550
|
if (requestId !== undefined) record.requestId = requestId
|
|
532
551
|
if (meta.route !== undefined) record.route = meta.route
|
|
533
|
-
if (meta.page !== undefined) record.page = meta.page
|
|
552
|
+
if (meta.page !== undefined) record.page = cap(redactText(queryKeysOnly(meta.page)), 2048)
|
|
534
553
|
// Re-insert: Map order is recency order, which eviction relies on.
|
|
535
554
|
errors.delete(key)
|
|
536
555
|
errors.set(key, record)
|
|
@@ -557,7 +576,7 @@ export function createDevFeed(options: DevFeedOptions): DevFeed {
|
|
|
557
576
|
message: text,
|
|
558
577
|
}
|
|
559
578
|
if (requestId !== undefined) entry.requestId = requestId
|
|
560
|
-
if (meta.page !== undefined) entry.page = meta.page
|
|
579
|
+
if (meta.page !== undefined) entry.page = cap(redactText(queryKeysOnly(meta.page)), 2048)
|
|
561
580
|
logs.push(entry)
|
|
562
581
|
logBytes += text.length
|
|
563
582
|
if (logs.length > limits.logs || logBytes > limits.logBytes) {
|
|
@@ -607,7 +626,7 @@ export function createDevFeed(options: DevFeedOptions): DevFeed {
|
|
|
607
626
|
seq,
|
|
608
627
|
requestId,
|
|
609
628
|
method,
|
|
610
|
-
path: cap(redactText(path), 2048),
|
|
629
|
+
path: cap(redactText(queryKeysOnly(path)), 2048),
|
|
611
630
|
at: nowIso(),
|
|
612
631
|
status: undefined,
|
|
613
632
|
durationMs: undefined,
|
|
@@ -856,7 +875,7 @@ export function writeDevServerRecord(root: string, record: DevServerRecord): voi
|
|
|
856
875
|
|
|
857
876
|
/** Remove the record only while it is still this server's: a newer server may have replaced it. */
|
|
858
877
|
export function removeDevServerRecord(root: string, token: string): void {
|
|
859
|
-
//
|
|
878
|
+
// nifra-expect NF-S002: the server comparing its own token with a file on this machine; no request
|
|
860
879
|
// reaches this comparison.
|
|
861
880
|
if (readDevServerRecord(root)?.token !== token) return
|
|
862
881
|
rmSync(join(root, DEV_SERVER_RECORD_FILE), { force: true })
|
package/src/dev-port.ts
CHANGED
|
@@ -38,13 +38,14 @@ export const explainBindFailure = (err: unknown, port: number): Error =>
|
|
|
38
38
|
|
|
39
39
|
/** The bits of a Node server {@link listenOrExplain} touches - structural, so a test can fake it. */
|
|
40
40
|
export interface ListenTarget {
|
|
41
|
-
listen(port: number, cb: () => void): void
|
|
41
|
+
listen(port: number, hostname: string, cb: () => void): void
|
|
42
42
|
once(event: "error", cb: (err: unknown) => void): void
|
|
43
43
|
removeListener(event: "error", cb: (err: unknown) => void): void
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* `listen`, but a bind failure becomes a readable nifra error instead of Node's raw internal throw.
|
|
48
|
+
* The hostname is required: omitting it binds every interface, and a dev server is local-only.
|
|
48
49
|
*
|
|
49
50
|
* Without an `error` listener attached, Node throws from deep inside `node:events`, the new process dies
|
|
50
51
|
* in the background, and nothing connects that death to the stale page in front of you.
|
|
@@ -52,7 +53,7 @@ export interface ListenTarget {
|
|
|
52
53
|
export function listenOrExplain(
|
|
53
54
|
server: ListenTarget,
|
|
54
55
|
port: number,
|
|
55
|
-
hostname
|
|
56
|
+
hostname: string,
|
|
56
57
|
): Promise<void> {
|
|
57
58
|
return new Promise<void>((resolve, reject) => {
|
|
58
59
|
const onError = (err: unknown): void => reject(explainBindFailure(err, port))
|
|
@@ -63,13 +64,6 @@ export function listenOrExplain(
|
|
|
63
64
|
server.removeListener("error", onError)
|
|
64
65
|
resolve()
|
|
65
66
|
}
|
|
66
|
-
|
|
67
|
-
else {
|
|
68
|
-
;(
|
|
69
|
-
server as ListenTarget & {
|
|
70
|
-
listen(port: number, hostname: string, cb: () => void): void
|
|
71
|
-
}
|
|
72
|
-
).listen(port, hostname, onListening)
|
|
73
|
-
}
|
|
67
|
+
server.listen(port, hostname, onListening)
|
|
74
68
|
})
|
|
75
69
|
}
|
package/src/dev-session.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* either. `/__nifra/last-error` keeps its older, token-free contract behind the same Host check.
|
|
9
9
|
*/
|
|
10
10
|
import { performance } from "node:perf_hooks"
|
|
11
|
+
import { writableResponse } from "@nifrajs/core/server"
|
|
11
12
|
import {
|
|
12
13
|
type BrowserError,
|
|
13
14
|
CLIENT_BATCH_MAX_BYTES,
|
|
@@ -649,19 +650,9 @@ export function createDevSession(options: DevSessionOptions): DevSession {
|
|
|
649
650
|
},
|
|
650
651
|
performance.now() - started,
|
|
651
652
|
)
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
} catch {
|
|
656
|
-
// `Response.redirect()` and fetched responses carry immutable headers on some runtimes.
|
|
657
|
-
const headers = new Headers(response.headers)
|
|
658
|
-
headers.set(DEV_REQUEST_ID_HEADER, requestId)
|
|
659
|
-
return new Response(response.body, {
|
|
660
|
-
status: response.status,
|
|
661
|
-
statusText: response.statusText,
|
|
662
|
-
headers,
|
|
663
|
-
})
|
|
664
|
-
}
|
|
653
|
+
const target = writableResponse(response)
|
|
654
|
+
target.headers.set(DEV_REQUEST_ID_HEADER, requestId)
|
|
655
|
+
return target
|
|
665
656
|
}
|
|
666
657
|
|
|
667
658
|
return {
|
package/src/dev.ts
CHANGED
|
@@ -58,7 +58,12 @@ import { dirname, relative, resolve } from "node:path"
|
|
|
58
58
|
import { type BuildClientOptions, buildClient } from "./build.ts"
|
|
59
59
|
import { type DevEntryMatch, resolveDevEntry } from "./bun-dev-entry.ts"
|
|
60
60
|
import { explainBindFailure } from "./dev-port.ts"
|
|
61
|
-
import {
|
|
61
|
+
import {
|
|
62
|
+
createDevSession,
|
|
63
|
+
type DevAppHooks,
|
|
64
|
+
type DevSession,
|
|
65
|
+
isLoopbackHost,
|
|
66
|
+
} from "./dev-session.ts"
|
|
62
67
|
import { createSsrGraph, type SsrGraph } from "./dev-ssr-graph.ts"
|
|
63
68
|
import { discoverRoutes } from "./fs.ts"
|
|
64
69
|
import { DEFAULT_DEV_PORT, generateClientEntry } from "./index.ts"
|
|
@@ -112,6 +117,13 @@ export interface DevServerOptions extends Omit<BuildClientOptions, "minify"> {
|
|
|
112
117
|
* fix (default `true`). Off, the errors still reach the feed.
|
|
113
118
|
*/
|
|
114
119
|
readonly indicator?: boolean
|
|
120
|
+
/**
|
|
121
|
+
* Host names the dev server answers besides loopback ones (`localhost`, `*.localhost`, `127.0.0.1`,
|
|
122
|
+
* `[::1]`), port ignored. Every other `Host` gets a 403: a page on another site whose name a DNS
|
|
123
|
+
* rebind points at 127.0.0.1 would otherwise read the dev server's pages, loader data and error
|
|
124
|
+
* overlay. Add a name here when a hosts-file entry or tunnel reaches the server under it.
|
|
125
|
+
*/
|
|
126
|
+
readonly allowedHosts?: readonly string[]
|
|
115
127
|
}
|
|
116
128
|
|
|
117
129
|
export interface DevServer {
|
|
@@ -465,6 +477,7 @@ export async function createDevServer(options: DevServerOptions): Promise<DevSer
|
|
|
465
477
|
}
|
|
466
478
|
}
|
|
467
479
|
|
|
480
|
+
const allowedHosts = new Set((options.allowedHosts ?? []).map(hostName))
|
|
468
481
|
try {
|
|
469
482
|
server = serve({
|
|
470
483
|
port,
|
|
@@ -474,6 +487,12 @@ export async function createDevServer(options: DevServerOptions): Promise<DevSer
|
|
|
474
487
|
routes: { [PROBE_PATH]: htmlModule.default },
|
|
475
488
|
fetch: async (req: Request): Promise<Response> => {
|
|
476
489
|
const url = new URL(req.url)
|
|
490
|
+
const host = req.headers.get("host") ?? url.host
|
|
491
|
+
if (!isLoopbackHost(host) && !allowedHosts.has(hostName(host)))
|
|
492
|
+
return new Response(
|
|
493
|
+
`[nifra/web/dev] ${hostName(host)} is not a loopback host; add it to allowedHosts if this server should answer it.\n`,
|
|
494
|
+
{ status: 403, headers: { "content-type": "text/plain; charset=utf-8" } },
|
|
495
|
+
)
|
|
477
496
|
// The stable client-entry URL. Pages reference THIS; it redirects to whichever hashed chunk Bun
|
|
478
497
|
// is serving at the moment the browser asks. A redirect rather than a proxy so the module's final
|
|
479
498
|
// URL is Bun's own - its HMR client derives its socket and module identity from `import.meta.url`,
|
|
@@ -620,6 +639,15 @@ export async function createDevServer(options: DevServerOptions): Promise<DevSer
|
|
|
620
639
|
* whose message is just "Bundle failed" - the actionable part (which file, which import) lives in
|
|
621
640
|
* `.errors`. Surface it, or the guard reports a failure while hiding the reason.
|
|
622
641
|
*/
|
|
642
|
+
/** A Host header's name: lowercased, without its port or IPv6 brackets. */
|
|
643
|
+
function hostName(host: string): string {
|
|
644
|
+
const name = host.trim().toLowerCase()
|
|
645
|
+
if (name.startsWith("["))
|
|
646
|
+
return name.slice(1, name.indexOf("]") === -1 ? undefined : name.indexOf("]"))
|
|
647
|
+
const colon = name.lastIndexOf(":")
|
|
648
|
+
return colon === -1 ? name : name.slice(0, colon)
|
|
649
|
+
}
|
|
650
|
+
|
|
623
651
|
export function buildFailureDetail(err: unknown): string {
|
|
624
652
|
return err instanceof AggregateError && err.errors.length > 0
|
|
625
653
|
? err.errors.map((e) => ` ${e instanceof Error ? e.message : String(e)}`).join("\n")
|
package/src/draft.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Draft / preview mode - let an editor see unpublished content by flipping a signed cookie that
|
|
3
3
|
* loaders read (`ctx.draft`) and ISR bypasses (fresh render, never cached). The cookie is **HMAC-signed**
|
|
4
|
-
* (so a visitor can't forge it)
|
|
4
|
+
* (so a visitor can't forge it), **HttpOnly** (JS can't read it) and carries its own expiry, which the
|
|
5
|
+
* server checks (a copied cookie stops working when the browser would have dropped it). You gate
|
|
6
|
+
* `enableDraft` yourself -
|
|
5
7
|
* behind an editor login or a `?token=` check - exactly like Next's `draftMode().enable()`.
|
|
6
8
|
*
|
|
7
9
|
* // a route you've protected (e.g. checked ?token= against a secret):
|
|
@@ -26,9 +28,23 @@ import { assertTokenSecret, timingSafeEqual } from "./internal/timing-safe-equal
|
|
|
26
28
|
/** The cookie name nifra uses for draft/preview mode. */
|
|
27
29
|
export const DRAFT_COOKIE = "__nifra_draft"
|
|
28
30
|
|
|
29
|
-
// The signed
|
|
30
|
-
//
|
|
31
|
-
const
|
|
31
|
+
// The signed payload is `1:<expiry in Unix seconds>`. A valid signature says the server issued it; the
|
|
32
|
+
// expiry bounds a replayed copy, which Max-Age alone cannot, since a client can resend any cookie it holds.
|
|
33
|
+
const DRAFT_PREFIX = "1:"
|
|
34
|
+
|
|
35
|
+
function draftPayload(options: EnableDraftOptions): string {
|
|
36
|
+
return `${DRAFT_PREFIX}${Math.floor(Date.now() / 1000) + draftMaxAge(options)}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function draftMaxAge(options: EnableDraftOptions): number {
|
|
40
|
+
const seconds = options.maxAgeSeconds ?? 3600
|
|
41
|
+
if (!Number.isSafeInteger(seconds) || seconds <= 0) {
|
|
42
|
+
throw new RangeError(
|
|
43
|
+
`[nifra] draft maxAgeSeconds must be a positive whole number of seconds, got ${seconds}`,
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
return seconds
|
|
47
|
+
}
|
|
32
48
|
|
|
33
49
|
/** The response-cookie surface `enableDraft`/`disableDraft` need - nifra's `c.set`. Structural, so any
|
|
34
50
|
* nifra handler context satisfies it without importing the full `Context`. */
|
|
@@ -38,7 +54,8 @@ export interface DraftCookieControls {
|
|
|
38
54
|
}
|
|
39
55
|
|
|
40
56
|
export interface EnableDraftOptions {
|
|
41
|
-
/**
|
|
57
|
+
/** Draft session lifetime in **seconds** (default `3600` = 1h): the cookie's `Max-Age`, and the expiry
|
|
58
|
+
* signed into the cookie that the server enforces. Keep it short - draft is an editor session. */
|
|
42
59
|
readonly maxAgeSeconds?: number
|
|
43
60
|
/** Cookie `Path` (default `"/"`). */
|
|
44
61
|
readonly path?: string
|
|
@@ -57,8 +74,8 @@ export async function enableDraft(
|
|
|
57
74
|
secret: string,
|
|
58
75
|
options: EnableDraftOptions = {},
|
|
59
76
|
): Promise<void> {
|
|
60
|
-
const
|
|
61
|
-
c.set.cookie(DRAFT_COOKIE,
|
|
77
|
+
const cookieOptions = draftCookieOptions(options)
|
|
78
|
+
c.set.cookie(DRAFT_COOKIE, await signValue(draftPayload(options), secret), cookieOptions)
|
|
62
79
|
}
|
|
63
80
|
|
|
64
81
|
/**
|
|
@@ -77,7 +94,7 @@ function draftCookieOptions(options: EnableDraftOptions): CookieOptions {
|
|
|
77
94
|
httpOnly: true,
|
|
78
95
|
sameSite: "lax",
|
|
79
96
|
path: options.path ?? "/",
|
|
80
|
-
maxAge: options
|
|
97
|
+
maxAge: draftMaxAge(options),
|
|
81
98
|
secure: options.secure ?? true,
|
|
82
99
|
}
|
|
83
100
|
}
|
|
@@ -91,14 +108,17 @@ export function disableDraft(
|
|
|
91
108
|
}
|
|
92
109
|
|
|
93
110
|
/**
|
|
94
|
-
* Whether `request` carries a **valid
|
|
95
|
-
* `createWebApp` uses it to set `ctx.draft`; `withISR` uses it to bypass the cache for
|
|
96
|
-
* missing, forged, or
|
|
111
|
+
* Whether `request` carries a **valid**, unexpired signed draft cookie (constant-time verify via
|
|
112
|
+
* `unsignValue`). `createWebApp` uses it to set `ctx.draft`; `withISR` uses it to bypass the cache for
|
|
113
|
+
* editors. A missing, forged, tampered, or expired cookie returns `false`.
|
|
97
114
|
*/
|
|
98
115
|
export async function isDraftEnabled(request: Request, secret: string): Promise<boolean> {
|
|
99
116
|
const cookie = parseCookies(request.headers.get("cookie"))[DRAFT_COOKIE]
|
|
100
117
|
if (cookie === undefined) return false
|
|
101
|
-
|
|
118
|
+
const payload = await unsignValue(cookie, secret)
|
|
119
|
+
if (payload === null || !payload.startsWith(DRAFT_PREFIX)) return false
|
|
120
|
+
const expires = Number(payload.slice(DRAFT_PREFIX.length))
|
|
121
|
+
return Number.isSafeInteger(expires) && Date.now() / 1000 < expires
|
|
102
122
|
}
|
|
103
123
|
|
|
104
124
|
/**
|
|
@@ -173,7 +193,8 @@ export function previewEndpoint(
|
|
|
173
193
|
`[nifra] previewEndpoint fallbackPath must be a site-relative path, got ${JSON.stringify(fallbackPath)}`,
|
|
174
194
|
)
|
|
175
195
|
}
|
|
176
|
-
const
|
|
196
|
+
const draftOptions = options.cookie ?? {}
|
|
197
|
+
const cookieOptions = draftCookieOptions(draftOptions)
|
|
177
198
|
return async (request) => {
|
|
178
199
|
const url = new URL(request.url)
|
|
179
200
|
if (!timingSafeEqual(url.searchParams.get(tokenParam) ?? "", options.secret)) {
|
|
@@ -184,7 +205,7 @@ export function previewEndpoint(
|
|
|
184
205
|
if (destination === null) {
|
|
185
206
|
return Response.json({ ok: false, error: "invalid_redirect" }, { status: 400 })
|
|
186
207
|
}
|
|
187
|
-
const signed = await signValue(
|
|
208
|
+
const signed = await signValue(draftPayload(draftOptions), options.draftSecret)
|
|
188
209
|
return new Response(null, {
|
|
189
210
|
status: 302,
|
|
190
211
|
headers: {
|
package/src/fonts-google.ts
CHANGED
|
@@ -113,6 +113,10 @@ const DEFAULT_MAX_FONT_BYTES = 5 * 1024 * 1024
|
|
|
113
113
|
const MAX_CSS_BYTES = 512 * 1024
|
|
114
114
|
const MAX_TEXT_LEN = 2048
|
|
115
115
|
|
|
116
|
+
/** A face's weight as Google writes it: `400`, a variable `100 900`, or a keyword. It lands in the
|
|
117
|
+
* written file's name, so anything else (a tampered `400/../..`) is refused. */
|
|
118
|
+
const FACE_WEIGHT = /^(?:\d{1,4}(?: \d{1,4})?|normal|bold)$/
|
|
119
|
+
|
|
116
120
|
const VALID_DISPLAY: ReadonlySet<string> = new Set([
|
|
117
121
|
"auto",
|
|
118
122
|
"block",
|
|
@@ -420,6 +424,12 @@ export async function loadGoogleFont(
|
|
|
420
424
|
"the stylesheet may be tampered with",
|
|
421
425
|
)
|
|
422
426
|
}
|
|
427
|
+
if (!FACE_WEIGHT.test(face.weight)) {
|
|
428
|
+
throw new Error(
|
|
429
|
+
`loadGoogleFont: refusing an unexpected font-weight ${JSON.stringify(face.weight.slice(0, 40))}; ` +
|
|
430
|
+
"the stylesheet may be tampered with",
|
|
431
|
+
)
|
|
432
|
+
}
|
|
423
433
|
const bytes = await fetchBounded(fetchImpl, source.url, maxBytes)
|
|
424
434
|
const hash = await contentHash(bytes)
|
|
425
435
|
const style = face.style === "italic" ? "italic" : "normal"
|
package/src/index.ts
CHANGED
|
@@ -85,15 +85,11 @@ export {
|
|
|
85
85
|
generateServerManifest,
|
|
86
86
|
} from "./internal/codegen.ts"
|
|
87
87
|
// Generated client entries intentionally import the browser router from the client subpath:
|
|
88
|
-
// `import { createClientRouter, createMatcher, mergeHeads,
|
|
89
|
-
export { mergeHeads, resolveMeta } from "./internal/head-merge.ts"
|
|
88
|
+
// `import { createClientRouter, createMatcher, mergeHeads, resolveChainMeta } from "@nifrajs/web/client"`.
|
|
89
|
+
export { mergeHeads, resolveChainMeta, resolveMeta } from "./internal/head-merge.ts"
|
|
90
90
|
export {
|
|
91
|
-
canonical,
|
|
92
91
|
gone,
|
|
93
|
-
jsonLd,
|
|
94
92
|
notFound,
|
|
95
|
-
type OpenGraphInput,
|
|
96
|
-
openGraph,
|
|
97
93
|
type RedirectOptions,
|
|
98
94
|
type RenderAssemblyCache,
|
|
99
95
|
type RenderedPage,
|
|
@@ -141,6 +137,7 @@ export {
|
|
|
141
137
|
type ISROptions,
|
|
142
138
|
type ISRPlatform,
|
|
143
139
|
type ISRQuery,
|
|
140
|
+
type ISRStoreOp,
|
|
144
141
|
isCacheablePage,
|
|
145
142
|
KVCacheStore,
|
|
146
143
|
type KVCacheStoreOptions,
|
|
@@ -256,6 +253,7 @@ export {
|
|
|
256
253
|
ACTION_GLOBAL,
|
|
257
254
|
BOUNDARY_GLOBAL,
|
|
258
255
|
DATA_GLOBAL,
|
|
256
|
+
type DocumentParts,
|
|
259
257
|
HANDOVER_ID,
|
|
260
258
|
LAYOUT_DATA_GLOBAL,
|
|
261
259
|
type MatchChain,
|
|
@@ -297,6 +295,7 @@ export {
|
|
|
297
295
|
// ctx + `renderPage`) and a client adapter mount call this with the same URL + schema, so the two sides
|
|
298
296
|
// produce the identical value by construction. An adapter's `useSearch` binding reads its result.
|
|
299
297
|
export { type SearchOf, searchOf, searchOfChain, serializeSearch } from "./search.ts"
|
|
298
|
+
export { canonical, jsonLd, type OpenGraphInput, openGraph } from "./seo.ts"
|
|
300
299
|
export {
|
|
301
300
|
type HtmlSanitizer,
|
|
302
301
|
type SanitizedHtml,
|