@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/internal/parity.ts
CHANGED
|
@@ -586,6 +586,17 @@ const linkedRepoBoundary = async (packageRoot: string): Promise<string> => {
|
|
|
586
586
|
return packageRoot
|
|
587
587
|
}
|
|
588
588
|
|
|
589
|
+
/** Where a real package folder lives when only its files are links: Bun installs a `file:` folder
|
|
590
|
+
* dependency as a directory of per-file symlinks, so its imports resolve beside the linked files. */
|
|
591
|
+
const fileLinkedDir = async (dir: string): Promise<string | undefined> => {
|
|
592
|
+
const manifest = join(dir, "package.json")
|
|
593
|
+
try {
|
|
594
|
+
return (await lstat(manifest)).isSymbolicLink() ? dirname(await realpath(manifest)) : undefined
|
|
595
|
+
} catch {
|
|
596
|
+
return undefined
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
589
600
|
const linkedPackageRoots = async (
|
|
590
601
|
importerRoots: readonly string[],
|
|
591
602
|
manifests: readonly Record<string, unknown>[],
|
|
@@ -605,8 +616,10 @@ const linkedPackageRoots = async (
|
|
|
605
616
|
}
|
|
606
617
|
const candidate = join(importerRoot, "node_modules", ...name.split("/"))
|
|
607
618
|
const link = await lstat(candidate).catch(() => undefined)
|
|
608
|
-
if (link === undefined
|
|
609
|
-
const resolved =
|
|
619
|
+
if (link === undefined) continue
|
|
620
|
+
const resolved = link.isSymbolicLink()
|
|
621
|
+
? await realpath(candidate).catch(() => undefined)
|
|
622
|
+
: await fileLinkedDir(candidate)
|
|
610
623
|
if (resolved === undefined || pathInside(realScanRoot, resolved)) continue
|
|
611
624
|
found.set(resolved, true)
|
|
612
625
|
}
|
|
@@ -32,12 +32,12 @@ const BEFORE_EXPRESSION = new Set([
|
|
|
32
32
|
"instanceof",
|
|
33
33
|
])
|
|
34
34
|
|
|
35
|
-
type Token =
|
|
35
|
+
export type Token =
|
|
36
36
|
| { readonly kind: "id" | "punct" | "string"; readonly value: string }
|
|
37
37
|
| { readonly kind: "other" }
|
|
38
38
|
|
|
39
39
|
/** JavaScript tokens, with strings, template text, comments and regular expressions skipped. */
|
|
40
|
-
function tokenize(code: string): Token[] {
|
|
40
|
+
export function tokenize(code: string): Token[] {
|
|
41
41
|
const tokens: Token[] = []
|
|
42
42
|
// One entry per open `${`: how many `{` are open inside it.
|
|
43
43
|
const templates: number[] = []
|
|
@@ -152,24 +152,37 @@ function envReads(tokens: readonly Token[]): EnvRead[] {
|
|
|
152
152
|
const token = tokens[at]
|
|
153
153
|
return token?.kind === "id" ? token.value : undefined
|
|
154
154
|
}
|
|
155
|
-
|
|
155
|
+
// The tokenizer reads `?.` as `.`, so `?.[` and `?.(` arrive as `.` before the bracket.
|
|
156
|
+
const open = (at: number, bracket: "[" | "("): number =>
|
|
157
|
+
is(at, "punct", ".") && is(at + 1, "punct", bracket) ? at + 1 : at
|
|
158
|
+
/** The string key of `["X"]` (or `?.["X"]`) at `at`, and the index after it. */
|
|
159
|
+
const keyAt = (at: number): { readonly key: string; readonly next: number } | undefined => {
|
|
160
|
+
const bracket = open(at, "[")
|
|
161
|
+
const key =
|
|
162
|
+
is(bracket, "punct", "[") && is(bracket + 2, "punct", "]") ? stringAt(bracket + 1) : undefined
|
|
163
|
+
return key === undefined ? undefined : { key, next: bracket + 3 }
|
|
164
|
+
}
|
|
165
|
+
/** A property read after an env object that ends at `at`: `.X`, `["X"]` or `?.["X"]`. */
|
|
156
166
|
const member = (base: string, at: number, importMeta: boolean): EnvRead => {
|
|
157
167
|
const property = is(at, "punct", ".") ? idAt(at + 1) : undefined
|
|
158
168
|
if (property !== undefined)
|
|
159
169
|
return { expression: `${base}.${property}`, name: property, importMeta }
|
|
160
|
-
const key =
|
|
170
|
+
const key = keyAt(at)?.key
|
|
161
171
|
if (key !== undefined)
|
|
162
172
|
return { expression: `${base}[${JSON.stringify(key)}]`, name: key, importMeta }
|
|
163
173
|
return { expression: base, importMeta }
|
|
164
174
|
}
|
|
175
|
+
/** The index after `.env`, `["env"]` or `?.["env"]` at `at`, or `-1`. */
|
|
176
|
+
const envAt = (at: number): number => {
|
|
177
|
+
if (is(at, "punct", ".") && is(at + 1, "id", "env")) return at + 2
|
|
178
|
+
const bracket = keyAt(at)
|
|
179
|
+
return bracket?.key === "env" ? bracket.next : -1
|
|
180
|
+
}
|
|
165
181
|
for (let i = 0; i < tokens.length; i++) {
|
|
166
182
|
const head = idAt(i)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
is(i + 2, "id", "env")
|
|
171
|
-
) {
|
|
172
|
-
reads.push(member(`${head}.env`, i + 3, false))
|
|
183
|
+
const env = head === "process" || head === "Bun" ? envAt(i + 1) : -1
|
|
184
|
+
if (env !== -1) {
|
|
185
|
+
reads.push(member(`${head}.env`, env, false))
|
|
173
186
|
} else if (
|
|
174
187
|
head === "import" &&
|
|
175
188
|
is(i + 1, "punct", ".") &&
|
|
@@ -180,12 +193,13 @@ function envReads(tokens: readonly Token[]): EnvRead[] {
|
|
|
180
193
|
reads.push(member("import.meta.env", i + 5, true))
|
|
181
194
|
} else if (head === "Deno" && is(i + 1, "punct", ".") && is(i + 2, "id", "env")) {
|
|
182
195
|
const method = is(i + 3, "punct", ".") ? idAt(i + 4) : undefined
|
|
196
|
+
const call = open(i + 5, "(")
|
|
183
197
|
const name =
|
|
184
|
-
(method === "get" || method === "has") && is(
|
|
185
|
-
? stringAt(
|
|
198
|
+
(method === "get" || method === "has") && is(call, "punct", "(")
|
|
199
|
+
? stringAt(call + 1)
|
|
186
200
|
: undefined
|
|
187
201
|
reads.push(
|
|
188
|
-
name !== undefined && is(
|
|
202
|
+
name !== undefined && is(call + 2, "punct", ")")
|
|
189
203
|
? { expression: `Deno.env.${method}(${JSON.stringify(name)})`, name, importMeta: false }
|
|
190
204
|
: {
|
|
191
205
|
expression: method === undefined ? "Deno.env" : `Deno.env.${method}()`,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { CookieOptions } from "@nifrajs/core/server"
|
|
2
|
+
|
|
3
|
+
export interface ProgressiveCookie {
|
|
4
|
+
readonly name: string
|
|
5
|
+
readonly value: string
|
|
6
|
+
readonly options?: CookieOptions | undefined
|
|
7
|
+
readonly deleted?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Parent-domain acceptance belongs to the browser's cookie jar and public-suffix policy. A Secure
|
|
11
|
+
* cookie on a request seen as `http:` does too: behind a TLS terminator only the browser knows its
|
|
12
|
+
* scheme, so it decides whether the cookie is kept. */
|
|
13
|
+
export function progressiveCookieRedirect(
|
|
14
|
+
request: Request,
|
|
15
|
+
changes: readonly ProgressiveCookie[],
|
|
16
|
+
): boolean {
|
|
17
|
+
const url = new URL(request.url)
|
|
18
|
+
const host = url.hostname.toLowerCase()
|
|
19
|
+
return changes.some((change) => {
|
|
20
|
+
const domain = change.options?.domain?.replace(/^\./, "").toLowerCase()
|
|
21
|
+
if (domain !== undefined && domain !== host && host.endsWith("." + domain)) return true
|
|
22
|
+
return secureOnPlainHttp(url, change)
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const secureOnPlainHttp = (url: URL, change: ProgressiveCookie): boolean =>
|
|
27
|
+
(change.options?.secure ?? !change.deleted) && url.protocol !== "https:"
|
|
28
|
+
|
|
29
|
+
/** Reauthorize the internal GET with action cookies that are valid for this exact owner URL. */
|
|
30
|
+
export function progressiveCookieHeader(
|
|
31
|
+
request: Request,
|
|
32
|
+
changes: readonly ProgressiveCookie[],
|
|
33
|
+
): string {
|
|
34
|
+
const url = new URL(request.url)
|
|
35
|
+
let pairs = (request.headers.get("cookie") ?? "")
|
|
36
|
+
.split(";")
|
|
37
|
+
.map((part) => part.trim())
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
for (const change of changes) {
|
|
40
|
+
const options = change.options
|
|
41
|
+
const domain = options?.domain?.replace(/^\./, "").toLowerCase()
|
|
42
|
+
// Parent-domain cookies need a browser cookie jar/public-suffix decision; never synthesize them.
|
|
43
|
+
if (domain !== undefined && domain !== url.hostname.toLowerCase()) continue
|
|
44
|
+
if (secureOnPlainHttp(url, change)) continue
|
|
45
|
+
const path = options?.path ?? "/"
|
|
46
|
+
if (
|
|
47
|
+
url.pathname !== path &&
|
|
48
|
+
!(url.pathname.startsWith(path) && (path.endsWith("/") || url.pathname[path.length] === "/"))
|
|
49
|
+
)
|
|
50
|
+
continue
|
|
51
|
+
pairs = pairs.filter((pair) => pair.slice(0, pair.indexOf("=")).trim() !== change.name)
|
|
52
|
+
const expired =
|
|
53
|
+
options?.maxAge !== undefined
|
|
54
|
+
? options.maxAge <= 0
|
|
55
|
+
: options?.expires !== undefined && options.expires.getTime() <= Date.now()
|
|
56
|
+
if (!change.deleted && !expired)
|
|
57
|
+
pairs.push(change.name + "=" + encodeURIComponent(change.value))
|
|
58
|
+
}
|
|
59
|
+
return pairs.join("; ")
|
|
60
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function publicEnvDefines(
|
|
2
|
+
prefix: string,
|
|
3
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
4
|
+
): Record<string, string> {
|
|
5
|
+
const defines: Record<string, string> = {}
|
|
6
|
+
if (prefix === "") return defines // opt-out: bake in nothing
|
|
7
|
+
for (const [name, value] of Object.entries(env)) {
|
|
8
|
+
// Skip `undefined` (a deleted/unset key can still enumerate) so we never bake in `"undefined"`.
|
|
9
|
+
if (name.startsWith(prefix) && value !== undefined) {
|
|
10
|
+
defines[`process.env.${name}`] = JSON.stringify(value)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return defines
|
|
14
|
+
}
|
|
@@ -6,15 +6,12 @@ import { DEFERRED_ERROR_CODE, DEFERRED_RUNTIME, prepareDeferred } from "../defer
|
|
|
6
6
|
import { ISR_REVALIDATE_HEADER, ISR_REVALIDATE_TAGS_HEADER, serializeISRTags } from "../isr.ts"
|
|
7
7
|
import type {
|
|
8
8
|
Loader as LayoutLoader,
|
|
9
|
-
LinkDescriptor,
|
|
10
9
|
Meta,
|
|
11
|
-
MetaDescriptor,
|
|
12
10
|
MetaInput,
|
|
13
11
|
RouteModule,
|
|
14
|
-
ScriptDescriptor,
|
|
15
12
|
UnsafeScriptDescriptor,
|
|
16
13
|
} from "../manifest.ts"
|
|
17
|
-
import type { MatchChain, RenderAdapter, RenderProps } from "../render-seam.ts"
|
|
14
|
+
import type { DocumentParts, MatchChain, RenderAdapter, RenderProps } from "../render-seam.ts"
|
|
18
15
|
import {
|
|
19
16
|
ACTION_GLOBAL,
|
|
20
17
|
BOUNDARY_GLOBAL,
|
|
@@ -76,6 +73,7 @@ export interface RenderedPage {
|
|
|
76
73
|
}
|
|
77
74
|
|
|
78
75
|
export interface RenderPageOptions {
|
|
76
|
+
readonly serverComponents?: import("../server-components/types.ts").ComponentState
|
|
79
77
|
readonly adapter: RenderAdapter
|
|
80
78
|
/** The layout chain to render - outermost layout → page (opaque; the adapter renders it). */
|
|
81
79
|
readonly chain: readonly unknown[]
|
|
@@ -200,8 +198,10 @@ export interface RenderPageOptions {
|
|
|
200
198
|
export interface RenderAssemblyCache {
|
|
201
199
|
/** Shell up to (not including) the deferred-runtime insertion point. */
|
|
202
200
|
shellPre?: string
|
|
203
|
-
/** Shell from after the deferred-runtime insertion point through
|
|
201
|
+
/** Shell from after the deferred-runtime insertion point through `<body>`. */
|
|
204
202
|
shellPost?: string
|
|
203
|
+
/** The open `#root` container. */
|
|
204
|
+
rootOpen?: string
|
|
205
205
|
/** Tail opener: the handover script open tag + the data member's key. */
|
|
206
206
|
tailPre?: string
|
|
207
207
|
/** Tail from after the per-request handover members (the route id, prerendered paths, the handover
|
|
@@ -343,6 +343,10 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
343
343
|
boundarySplit === undefined
|
|
344
344
|
? ""
|
|
345
345
|
: `,"${BOUNDARY_GLOBAL}":${serializeData(boundarySplit.forClient)}`
|
|
346
|
+
const componentTail =
|
|
347
|
+
options.serverComponents === undefined
|
|
348
|
+
? ""
|
|
349
|
+
: `,"__NIFRA_COMPONENTS__":${serializeData(options.serverComponents)}`
|
|
346
350
|
const deferredRuntime =
|
|
347
351
|
allDeferred.length > 0 ? `<script${nonceAttr}>${DEFERRED_RUNTIME}</script>` : ""
|
|
348
352
|
// The regex only injects the CSP nonce into the (constant) hydration head; with no nonce it's a
|
|
@@ -418,7 +422,8 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
418
422
|
? `<link rel="modulepreload" href="${escapeAttr(clientEntry ?? "")}">${preloadLinks}`
|
|
419
423
|
: ""
|
|
420
424
|
slot.shellPre = `<!doctype html><html${htmlAttrs}><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">${hydrationGuard}<title>${escapeHtml(head?.title ?? title)}</title>${headTags(head, nonce)}${styleLinks}${entryPreloads}`
|
|
421
|
-
slot.shellPost = `${islandPreloads}${hydrationHead}</head><body
|
|
425
|
+
slot.shellPost = `${islandPreloads}${hydrationHead}</head><body>`
|
|
426
|
+
slot.rootOpen = `<div id="${escapeAttr(rootId)}"${rootMarker}>`
|
|
422
427
|
// Island bundles load regardless of `hydrate` - a static page (hydrate:false) ships no framework
|
|
423
428
|
// client but can still mount no-framework islands (`@nifrajs/web/islands`).
|
|
424
429
|
let islandTags = ""
|
|
@@ -436,14 +441,20 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
436
441
|
// The deferred runtime rides only on a hydrating page (a static page has no client takeover), and
|
|
437
442
|
// only when something actually deferred - matching the pre-cache emission exactly.
|
|
438
443
|
const runtimeSeam = hydrate && allDeferred.length > 0 ? deferredRuntime : ""
|
|
439
|
-
const shellHtml = `${slot.shellPre}${runtimeSeam}${slot.shellPost}`
|
|
444
|
+
const shellHtml = `${slot.shellPre}${runtimeSeam}${slot.shellPost}${slot.rootOpen}`
|
|
445
|
+
const parts: DocumentParts | undefined =
|
|
446
|
+
adapter.documentParts === true ? { head: "", body: "" } : undefined
|
|
447
|
+
const shellOf = (): string =>
|
|
448
|
+
parts === undefined || (parts.head === "" && parts.body === "")
|
|
449
|
+
? shellHtml
|
|
450
|
+
: `${slot.shellPre}${runtimeSeam}${parts.head}${slot.shellPost}${parts.body}${slot.rootOpen}`
|
|
440
451
|
// Closes the hydration container; deferred resolve scripts go AFTER it (outside `#root`) so they
|
|
441
452
|
// aren't part of the adapter's hydrated tree (an inline script inside it breaks hydration).
|
|
442
453
|
const closeRootHtml = "</div>"
|
|
443
454
|
// Tail - the page-state handover + the client module. Module scripts defer (run after parse), so
|
|
444
455
|
// the handover + every streamed deferred resolution are in the document before the entry hydrates.
|
|
445
456
|
const tailHtml = hydrate
|
|
446
|
-
? `${slot.tailPre}${serializeData(forClient)}${action}${layoutTail}${boundaryTail}${slot.tailPost}`
|
|
457
|
+
? `${slot.tailPre}${serializeData(forClient)}${action}${layoutTail}${boundaryTail}${componentTail}${slot.tailPost}`
|
|
447
458
|
: (slot.tailPost as string)
|
|
448
459
|
const headers: Record<string, string> = { "content-type": "text/html; charset=utf-8" }
|
|
449
460
|
// Caller-supplied headers (a terminal status page's `cache-control`, say). Applied before the
|
|
@@ -469,6 +480,9 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
469
480
|
if (policy !== undefined) headers["content-security-policy"] = policy
|
|
470
481
|
}
|
|
471
482
|
const renderProps: RenderProps = {
|
|
483
|
+
...(options.serverComponents === undefined
|
|
484
|
+
? {}
|
|
485
|
+
: { serverComponents: options.serverComponents }),
|
|
472
486
|
data: forComponent,
|
|
473
487
|
actionData: actionSplit?.forComponent,
|
|
474
488
|
// `params`/`path` thread the matched route + URL to the adapter's `useParams`/`useLocation` so they
|
|
@@ -496,13 +510,22 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
496
510
|
// gets an automatic Content-Length. A render throw surfaces here exactly as the streaming path's
|
|
497
511
|
// shell-readiness `await` does, so the `_error` boundary still maps it to a status.
|
|
498
512
|
if (allDeferred.length === 0 && adapter.renderToString !== undefined) {
|
|
499
|
-
const out =
|
|
513
|
+
const out =
|
|
514
|
+
parts !== undefined
|
|
515
|
+
? adapter.renderToString(
|
|
516
|
+
chain,
|
|
517
|
+
renderProps,
|
|
518
|
+
nonce === undefined ? { parts } : { nonce, parts },
|
|
519
|
+
)
|
|
520
|
+
: nonce === undefined
|
|
521
|
+
? adapter.renderToString(chain, renderProps)
|
|
522
|
+
: adapter.renderToString(chain, renderProps, { nonce })
|
|
500
523
|
return typeof out === "string"
|
|
501
|
-
? new BufferedRenderedPage(
|
|
524
|
+
? new BufferedRenderedPage(shellOf() + out + closeRootHtml + tailHtml, status, headers)
|
|
502
525
|
: out.then(
|
|
503
526
|
(bodyHtml) =>
|
|
504
527
|
new BufferedRenderedPage(
|
|
505
|
-
|
|
528
|
+
shellOf() + bodyHtml + closeRootHtml + tailHtml,
|
|
506
529
|
status,
|
|
507
530
|
headers,
|
|
508
531
|
),
|
|
@@ -513,7 +536,8 @@ export function renderPageResult(options: RenderPageInput): MaybePromise<Rendere
|
|
|
513
536
|
adapter,
|
|
514
537
|
chain,
|
|
515
538
|
renderProps,
|
|
516
|
-
|
|
539
|
+
shellOf,
|
|
540
|
+
parts,
|
|
517
541
|
closeRootHtml,
|
|
518
542
|
allDeferred,
|
|
519
543
|
tailHtml,
|
|
@@ -528,7 +552,8 @@ async function renderStreamedPage(
|
|
|
528
552
|
adapter: RenderAdapter,
|
|
529
553
|
chain: readonly unknown[],
|
|
530
554
|
renderProps: RenderProps,
|
|
531
|
-
|
|
555
|
+
shellOf: () => string,
|
|
556
|
+
parts: DocumentParts | undefined,
|
|
532
557
|
closeRootHtml: string,
|
|
533
558
|
allDeferred: ReadonlyArray<{ readonly id: number; readonly promise: Promise<unknown> }>,
|
|
534
559
|
tailHtml: string,
|
|
@@ -542,13 +567,20 @@ async function renderStreamedPage(
|
|
|
542
567
|
// (React: on-shell-ready; Solid: synchronously), so a shell-render throw surfaces before any byte is
|
|
543
568
|
// sent. A *mid*-stream failure errors the body instead.
|
|
544
569
|
const enc = TEXT_ENCODER
|
|
545
|
-
const shell = enc.encode(shellHtml)
|
|
546
570
|
const closeRoot = enc.encode(closeRootHtml)
|
|
547
571
|
const tail = enc.encode(tailHtml)
|
|
548
572
|
const appStream =
|
|
549
|
-
|
|
550
|
-
? await adapter.renderToStream(
|
|
551
|
-
|
|
573
|
+
parts !== undefined
|
|
574
|
+
? await adapter.renderToStream(
|
|
575
|
+
chain,
|
|
576
|
+
renderProps,
|
|
577
|
+
nonce === undefined ? { parts } : { nonce, parts },
|
|
578
|
+
)
|
|
579
|
+
: nonce === undefined
|
|
580
|
+
? await adapter.renderToStream(chain, renderProps)
|
|
581
|
+
: await adapter.renderToStream(chain, renderProps, { nonce })
|
|
582
|
+
// After the render: an adapter fills its document parts before handing back the stream.
|
|
583
|
+
const shell = enc.encode(shellOf())
|
|
552
584
|
const body = streamDocument(shell, appStream, closeRoot, allDeferred, tail, enc, nonceAttr)
|
|
553
585
|
return new Response(body, { status, headers })
|
|
554
586
|
}
|
|
@@ -616,13 +648,17 @@ function streamDocument(
|
|
|
616
648
|
enc: TextEncoder,
|
|
617
649
|
nonceAttr: string,
|
|
618
650
|
): ReadableStream<Uint8Array> {
|
|
651
|
+
const reader = stream.getReader()
|
|
652
|
+
// A client that goes away cancels the body: the renderer stops, and nothing more is written.
|
|
653
|
+
let cancelled = false
|
|
654
|
+
let released = false
|
|
619
655
|
return new ReadableStream({
|
|
620
656
|
async start(controller) {
|
|
621
|
-
const reader = stream.getReader()
|
|
622
657
|
try {
|
|
623
658
|
controller.enqueue(shell)
|
|
624
659
|
for (;;) {
|
|
625
660
|
const { done, value } = await reader.read()
|
|
661
|
+
if (cancelled) return
|
|
626
662
|
if (done) break
|
|
627
663
|
controller.enqueue(value)
|
|
628
664
|
}
|
|
@@ -631,34 +667,37 @@ function streamDocument(
|
|
|
631
667
|
// defer() must not block a faster one; each script self-addresses by id, so order is irrelevant.
|
|
632
668
|
await Promise.all(
|
|
633
669
|
deferred.map(async (d) => {
|
|
670
|
+
let script: string
|
|
634
671
|
try {
|
|
635
672
|
const value = serializeData(await d.promise)
|
|
636
673
|
// `serializeData` JSON-serializes the value and escapes `<`, `>`, and Unicode line
|
|
637
674
|
// separators before this framework-owned script is emitted.
|
|
638
|
-
|
|
639
|
-
enc.encode(`<script${nonceAttr}>window.__nifraResolve(${d.id},${value})</script>`), // nosemgrep: javascript.lang.security.audit.unknown-value-with-script-tag.unknown-value-with-script-tag
|
|
640
|
-
)
|
|
675
|
+
script = `<script${nonceAttr}>window.__nifraResolve(${d.id},${value})</script>` // nosemgrep: javascript.lang.security.audit.unknown-value-with-script-tag.unknown-value-with-script-tag
|
|
641
676
|
} catch (err) {
|
|
642
677
|
// A rejected deferred streams __nifraReject (the client `<Await>` surfaces it) - it must
|
|
643
678
|
// not break the whole body. Redact: stream a stable opaque code, never the raw error
|
|
644
679
|
// text; log the real reason server-side.
|
|
645
680
|
console.error("[nifra/web] deferred value rejected:", err)
|
|
646
|
-
|
|
647
|
-
enc.encode(
|
|
648
|
-
`<script${nonceAttr}>window.__nifraReject(${d.id},${serializeData(DEFERRED_ERROR_CODE)})</script>`,
|
|
649
|
-
),
|
|
650
|
-
)
|
|
681
|
+
script = `<script${nonceAttr}>window.__nifraReject(${d.id},${serializeData(DEFERRED_ERROR_CODE)})</script>`
|
|
651
682
|
}
|
|
683
|
+
if (!cancelled) controller.enqueue(enc.encode(script))
|
|
652
684
|
}),
|
|
653
685
|
)
|
|
686
|
+
if (cancelled) return
|
|
654
687
|
controller.enqueue(tail)
|
|
655
688
|
controller.close()
|
|
656
689
|
} catch (err) {
|
|
657
|
-
controller.error(err)
|
|
690
|
+
if (!cancelled) controller.error(err)
|
|
658
691
|
} finally {
|
|
692
|
+
released = true
|
|
659
693
|
reader.releaseLock()
|
|
660
694
|
}
|
|
661
695
|
},
|
|
696
|
+
cancel(reason) {
|
|
697
|
+
cancelled = true
|
|
698
|
+
// Once the renderer is drained the reader is released, and there is nothing left to stop.
|
|
699
|
+
return released ? undefined : reader.cancel(reason)
|
|
700
|
+
},
|
|
662
701
|
})
|
|
663
702
|
}
|
|
664
703
|
|
|
@@ -1108,68 +1147,6 @@ function headTags(head: Meta | undefined, documentNonce?: string): string {
|
|
|
1108
1147
|
return out
|
|
1109
1148
|
}
|
|
1110
1149
|
|
|
1111
|
-
/**
|
|
1112
|
-
* A `<link rel="canonical">` descriptor for a route's `meta.link`. The canonical URL tells search
|
|
1113
|
-
* engines which URL is authoritative for a page (deduping query-string / tracking variants).
|
|
1114
|
-
*
|
|
1115
|
-
* ```ts
|
|
1116
|
-
* export const meta = (a) => ({ link: [canonical(`https://site.com/posts/${a.params.slug}`)] })
|
|
1117
|
-
* ```
|
|
1118
|
-
*/
|
|
1119
|
-
export function canonical(href: string): LinkDescriptor {
|
|
1120
|
-
return { rel: "canonical", href }
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
/** Inputs for {@link openGraph} - the common Open Graph properties. All optional; only the provided
|
|
1124
|
-
* ones become tags. `type` defaults to `"website"`. */
|
|
1125
|
-
export interface OpenGraphInput {
|
|
1126
|
-
readonly title?: string
|
|
1127
|
-
readonly description?: string
|
|
1128
|
-
/** Absolute URL of the share image (`og:image`). */
|
|
1129
|
-
readonly image?: string
|
|
1130
|
-
/** Canonical URL of the page (`og:url`). */
|
|
1131
|
-
readonly url?: string
|
|
1132
|
-
/** Object type (`og:type`) - `"website"`, `"article"`, … Default `"website"`. */
|
|
1133
|
-
readonly type?: string
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
/**
|
|
1137
|
-
* Build the Open Graph `<meta property="og:*">` entries for a route's `meta.meta`. Returns only the
|
|
1138
|
-
* properties you supplied (plus `og:type`, defaulting to `"website"`), so it composes with other meta.
|
|
1139
|
-
*
|
|
1140
|
-
* ```ts
|
|
1141
|
-
* export const meta = { meta: [...openGraph({ title: "Nifra", image: "https://site.com/og.png" })] }
|
|
1142
|
-
* ```
|
|
1143
|
-
*/
|
|
1144
|
-
export function openGraph(input: OpenGraphInput): MetaDescriptor[] {
|
|
1145
|
-
const tags: MetaDescriptor[] = []
|
|
1146
|
-
const add = (property: string, content: string | undefined): void => {
|
|
1147
|
-
if (content !== undefined) tags.push({ property, content })
|
|
1148
|
-
}
|
|
1149
|
-
add("og:title", input.title)
|
|
1150
|
-
add("og:description", input.description)
|
|
1151
|
-
add("og:image", input.image)
|
|
1152
|
-
add("og:url", input.url)
|
|
1153
|
-
// `og:type` always present (the spec's required property) - default "website" when unset.
|
|
1154
|
-
tags.push({ property: "og:type", content: input.type ?? "website" })
|
|
1155
|
-
return tags
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
/**
|
|
1159
|
-
* Build a JSON-LD `<script type="application/ld+json">` entry for a route's `meta.script` from a plain
|
|
1160
|
-
* object. `JSON.stringify` produces the body; the head renderer breakout-escapes it (see
|
|
1161
|
-
* `escapeScriptContent`), so a string field containing `</script>` is embedded safely.
|
|
1162
|
-
*
|
|
1163
|
-
* ```ts
|
|
1164
|
-
* export const meta = {
|
|
1165
|
-
* script: [jsonLd({ "@context": "https://schema.org", "@type": "Article", headline: "Hi" })],
|
|
1166
|
-
* }
|
|
1167
|
-
* ```
|
|
1168
|
-
*/
|
|
1169
|
-
export function jsonLd(data: Record<string, unknown>): ScriptDescriptor {
|
|
1170
|
-
return { type: "application/ld+json", content: JSON.stringify(data) }
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
1150
|
/**
|
|
1174
1151
|
* Deliberately unsafe escape hatch for executable inline code. The required nonce keeps the result
|
|
1175
1152
|
* compatible with a strict CSP and makes the security-sensitive choice visible at the call site.
|
|
@@ -2,9 +2,15 @@ import {
|
|
|
2
2
|
type CookieOptions,
|
|
3
3
|
type ResponseResult,
|
|
4
4
|
status as statusResult,
|
|
5
|
+
writableResponse,
|
|
5
6
|
} from "@nifrajs/core/server"
|
|
6
7
|
import type { LoaderResponseControls } from "../manifest.ts"
|
|
7
8
|
import { DATA_HEADER } from "../router.ts"
|
|
9
|
+
import {
|
|
10
|
+
type ProgressiveCookie,
|
|
11
|
+
progressiveCookieHeader,
|
|
12
|
+
progressiveCookieRedirect,
|
|
13
|
+
} from "./progressive-cookies.ts"
|
|
8
14
|
import { isResponseResult } from "./render-document.ts"
|
|
9
15
|
|
|
10
16
|
/** The serving app's response controls - the structural subset of a route handler's `c.set` used here. */
|
|
@@ -107,19 +113,9 @@ export function privateOutcome(outcome: unknown): unknown {
|
|
|
107
113
|
|
|
108
114
|
function privateResponse(response: Response): Response {
|
|
109
115
|
if (response.headers.get(CACHE_CONTROL) === PRIVATE_NO_STORE) return response
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
} catch {
|
|
114
|
-
// Immutable headers (`Response.redirect()`, a `fetch()` result): answer with a copy.
|
|
115
|
-
const headers = new Headers(response.headers)
|
|
116
|
-
headers.set(CACHE_CONTROL, PRIVATE_NO_STORE)
|
|
117
|
-
return new Response(response.body, {
|
|
118
|
-
status: response.status,
|
|
119
|
-
statusText: response.statusText,
|
|
120
|
-
headers,
|
|
121
|
-
})
|
|
122
|
-
}
|
|
116
|
+
const target = writableResponse(response)
|
|
117
|
+
target.headers.set(CACHE_CONTROL, PRIVATE_NO_STORE)
|
|
118
|
+
return target
|
|
123
119
|
}
|
|
124
120
|
|
|
125
121
|
interface WrittenHeaders {
|
|
@@ -140,6 +136,7 @@ export class PageResponseControls {
|
|
|
140
136
|
#written: WrittenHeaders[] | undefined
|
|
141
137
|
#sealed = false
|
|
142
138
|
#personalized = false
|
|
139
|
+
#cookieChanges: ProgressiveCookie[] | undefined
|
|
143
140
|
|
|
144
141
|
constructor(c: { readonly set: CoreResponseControls }) {
|
|
145
142
|
this.#c = c
|
|
@@ -167,7 +164,27 @@ export class PageResponseControls {
|
|
|
167
164
|
cookies(member: string): CoreResponseControls {
|
|
168
165
|
this.assertOpen(member)
|
|
169
166
|
this.personalize()
|
|
170
|
-
|
|
167
|
+
const controls = this.#c.set
|
|
168
|
+
return {
|
|
169
|
+
headers: controls.headers,
|
|
170
|
+
cookie: (name, value, options) => {
|
|
171
|
+
controls.cookie(name, value, options)
|
|
172
|
+
this.#cookieChanges ??= []
|
|
173
|
+
this.#cookieChanges.push({ name, value, options })
|
|
174
|
+
},
|
|
175
|
+
deleteCookie: (name, options) => {
|
|
176
|
+
controls.deleteCookie(name, options)
|
|
177
|
+
this.#cookieChanges ??= []
|
|
178
|
+
this.#cookieChanges.push({ name, value: "", options, deleted: true })
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
progressiveCookies(request: Request): string {
|
|
184
|
+
return progressiveCookieHeader(request, this.#cookieChanges ?? [])
|
|
185
|
+
}
|
|
186
|
+
progressiveRedirect(request: Request): boolean {
|
|
187
|
+
return progressiveCookieRedirect(request, this.#cookieChanges ?? [])
|
|
171
188
|
}
|
|
172
189
|
|
|
173
190
|
/** Mark the response as one visitor's - a queued cookie, or a draft render - so no cache stores it. */
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `t.date()` across the wire, browser half: every place the browser parses page data rebuilds the
|
|
3
|
+
* `{ "__nifra_date": iso }` tags the server's `wireDate()` wrote into `Date`s. Kept apart from
|
|
4
|
+
* `wire-date.ts` so client bundles never carry the server class.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export const DATE_TAG = "__nifra_date"
|
|
8
|
+
const TAGGED = `"${DATE_TAG}"`
|
|
9
|
+
|
|
10
|
+
// A JSON.parse reviver runs leaves first and defines each result as an own property, so a key named
|
|
11
|
+
// `__proto__` never becomes the prototype.
|
|
12
|
+
const revive = (_key: string, value: unknown): unknown => {
|
|
13
|
+
if (typeof value !== "object" || value === null) return value
|
|
14
|
+
const tagged: unknown = Reflect.get(value, DATE_TAG)
|
|
15
|
+
return typeof tagged === "string" && Object.keys(value).length === 1 ? new Date(tagged) : value
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** `JSON.parse` that rebuilds every `{ "__nifra_date": iso }` into a `Date`, when the text has one. */
|
|
19
|
+
export const parsePageData = (text: string): unknown =>
|
|
20
|
+
text.includes(TAGGED) ? JSON.parse(text, revive) : JSON.parse(text)
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* {@link parsePageData}'s reviver as source declaring `__nifraDates`, for client code nifra generates rather than
|
|
24
|
+
* bundles (server-function stubs; `DEFERRED_RUNTIME` carries the same text). A literal with nothing
|
|
25
|
+
* interpolated: Bun keeps a template that interpolates an import, and with it every string built from
|
|
26
|
+
* it, in client bundles that never use them.
|
|
27
|
+
*/
|
|
28
|
+
export const REVIVE_DATES_SOURCE = `const __nifraDates = (d) => {
|
|
29
|
+
if (!d || typeof d !== "object") return d
|
|
30
|
+
if (Array.isArray(d)) { for (let i = 0; i < d.length; i++) d[i] = __nifraDates(d[i]); return d }
|
|
31
|
+
const k = Object.keys(d)
|
|
32
|
+
if (k.length === 1 && k[0] === "__nifra_date" && typeof d.__nifra_date === "string") return new Date(d.__nifra_date)
|
|
33
|
+
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 }) }
|
|
34
|
+
return d
|
|
35
|
+
}`
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A route pattern's literal text as a request path spells it. A route file named `café.tsx` is
|
|
3
|
+
* requested as `/caf%C3%A9`, so the routers, which compare the raw request path, are given the pattern
|
|
4
|
+
* that way. Only controls, space and non-ASCII are encoded (what a browser encodes in a path), so ASCII
|
|
5
|
+
* punctuation such as a param constraint keeps its meaning. Runs once per pattern, never per request.
|
|
6
|
+
*/
|
|
7
|
+
export const requestPattern = (pattern: string): string =>
|
|
8
|
+
pattern.normalize("NFC").replace(/[^!-~]/gu, encodeURIComponent)
|
|
9
|
+
|
|
10
|
+
// This module also compiles without the DOM lib; at runtime the name is the browser's own global.
|
|
11
|
+
declare const location: { readonly origin: string } | undefined
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* An app path as a URL `fetch`, `history` and `location` resolve on this document's own origin. A path
|
|
15
|
+
* starting `//` or `/\` is the path of a page here (`https://shop.example//evil.example/x`), but resolved
|
|
16
|
+
* as written it is a protocol-relative URL to another host, so it is anchored to this origin.
|
|
17
|
+
*/
|
|
18
|
+
export const appUrl = (path: string): string =>
|
|
19
|
+
path.charCodeAt(0) === 47 /* / */ &&
|
|
20
|
+
(path.charCodeAt(1) === 47 || path.charCodeAt(1) === 92) /* / or \ */ &&
|
|
21
|
+
typeof location !== "undefined" &&
|
|
22
|
+
location.origin !== "null"
|
|
23
|
+
? location.origin + path
|
|
24
|
+
: path
|