@nifrajs/web 5.5.0 → 5.6.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/CHANGELOG.md +162 -0
- package/dist/boundary.d.ts +67 -7
- package/dist/boundary.d.ts.map +1 -1
- package/dist/boundary.js +6 -4
- package/dist/boundary.js.map +1 -1
- package/dist/build-plan.d.ts +3 -0
- package/dist/build-plan.d.ts.map +1 -1
- package/dist/build-plan.js.map +1 -1
- package/dist/build-vite.d.ts.map +1 -1
- package/dist/build-vite.js +2 -0
- package/dist/build-vite.js.map +1 -1
- package/dist/build.d.ts +9 -2
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +9 -0
- package/dist/build.js.map +1 -1
- package/dist/client.d.ts +2 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +140 -12
- package/dist/client.js.map +1 -1
- package/dist/csp.d.ts +8 -0
- package/dist/csp.d.ts.map +1 -1
- package/dist/csp.js +11 -2
- package/dist/csp.js.map +1 -1
- package/dist/deferred.d.ts +6 -4
- package/dist/deferred.d.ts.map +1 -1
- package/dist/deferred.js +10 -6
- package/dist/deferred.js.map +1 -1
- package/dist/dev-feed.d.ts.map +1 -1
- package/dist/dev-feed.js +11 -2
- package/dist/dev-feed.js.map +1 -1
- package/dist/dev-session.d.ts.map +1 -1
- package/dist/dev-session.js +13 -6
- package/dist/dev-session.js.map +1 -1
- package/dist/dev.d.ts +4 -1
- package/dist/dev.d.ts.map +1 -1
- package/dist/dev.js +12 -5
- package/dist/dev.js.map +1 -1
- package/dist/diagnostic-catalog.d.ts +4 -0
- package/dist/diagnostic-catalog.d.ts.map +1 -1
- package/dist/diagnostic-catalog.js +48 -0
- package/dist/diagnostic-catalog.js.map +1 -1
- package/dist/diagnostic.d.ts +1 -1
- package/dist/diagnostic.d.ts.map +1 -1
- package/dist/diagnostic.js +1 -1
- package/dist/diagnostic.js.map +1 -1
- package/dist/fn.d.ts +5 -3
- package/dist/fn.d.ts.map +1 -1
- package/dist/fn.js +20 -5
- package/dist/fn.js.map +1 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +13 -3
- package/dist/fs.js.map +1 -1
- package/dist/identity.d.ts +22 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +60 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/internal/cdn/cloudflare.d.ts +2 -1
- package/dist/internal/cdn/cloudflare.d.ts.map +1 -1
- package/dist/internal/cdn/cloudflare.js +2 -1
- package/dist/internal/cdn/cloudflare.js.map +1 -1
- package/dist/internal/certified-components.d.ts +1 -1
- package/dist/internal/certified-components.js +1 -1
- package/dist/internal/codegen.d.ts +11 -10
- package/dist/internal/codegen.d.ts.map +1 -1
- package/dist/internal/codegen.js +65 -8
- package/dist/internal/codegen.js.map +1 -1
- package/dist/internal/dev-websocket.d.ts +73 -0
- package/dist/internal/dev-websocket.d.ts.map +1 -0
- package/dist/internal/dev-websocket.js +145 -0
- package/dist/internal/dev-websocket.js.map +1 -0
- package/dist/internal/hole-runtime.d.ts +66 -0
- package/dist/internal/hole-runtime.d.ts.map +1 -0
- package/dist/internal/hole-runtime.js +211 -0
- package/dist/internal/hole-runtime.js.map +1 -0
- package/dist/internal/identity-epoch.d.ts +50 -0
- package/dist/internal/identity-epoch.d.ts.map +1 -0
- package/dist/internal/identity-epoch.js +179 -0
- package/dist/internal/identity-epoch.js.map +1 -0
- package/dist/internal/invalidation-contract.d.ts +23 -0
- package/dist/internal/invalidation-contract.d.ts.map +1 -0
- package/dist/internal/invalidation-contract.js +11 -0
- package/dist/internal/invalidation-contract.js.map +1 -0
- package/dist/internal/invalidation.d.ts +62 -0
- package/dist/internal/invalidation.d.ts.map +1 -0
- package/dist/internal/invalidation.js +149 -0
- package/dist/internal/invalidation.js.map +1 -0
- package/dist/internal/island-bindings.d.ts +51 -0
- package/dist/internal/island-bindings.d.ts.map +1 -0
- package/dist/internal/island-bindings.js +147 -0
- package/dist/internal/island-bindings.js.map +1 -0
- package/dist/internal/island-list.d.ts +26 -0
- package/dist/internal/island-list.d.ts.map +1 -0
- package/dist/internal/island-list.js +78 -0
- package/dist/internal/island-list.js.map +1 -0
- package/dist/internal/island-mount.d.ts +4 -0
- package/dist/internal/island-mount.d.ts.map +1 -0
- package/dist/internal/island-mount.js +66 -0
- package/dist/internal/island-mount.js.map +1 -0
- package/dist/internal/island-owner.d.ts +5 -0
- package/dist/internal/island-owner.d.ts.map +1 -0
- package/dist/internal/island-owner.js +20 -0
- package/dist/internal/island-owner.js.map +1 -0
- package/dist/internal/island-resource.d.ts +30 -0
- package/dist/internal/island-resource.d.ts.map +1 -0
- package/dist/internal/island-resource.js +57 -0
- package/dist/internal/island-resource.js.map +1 -0
- package/dist/internal/island-signals.d.ts +51 -0
- package/dist/internal/island-signals.d.ts.map +1 -0
- package/dist/internal/island-signals.js +220 -0
- package/dist/internal/island-signals.js.map +1 -0
- package/dist/internal/keep-alive-table.d.ts +23 -0
- package/dist/internal/keep-alive-table.d.ts.map +1 -0
- package/dist/internal/keep-alive-table.js +53 -0
- package/dist/internal/keep-alive-table.js.map +1 -0
- package/dist/internal/live-contract.d.ts +96 -0
- package/dist/internal/live-contract.d.ts.map +1 -0
- package/dist/internal/live-contract.js +5 -0
- package/dist/internal/live-contract.js.map +1 -0
- package/dist/internal/live-queries.d.ts +22 -0
- package/dist/internal/live-queries.d.ts.map +1 -0
- package/dist/internal/live-queries.js +54 -0
- package/dist/internal/live-queries.js.map +1 -0
- package/dist/internal/page-events.d.ts +10 -0
- package/dist/internal/page-events.d.ts.map +1 -0
- package/dist/internal/page-events.js +26 -0
- package/dist/internal/page-events.js.map +1 -0
- package/dist/internal/page-execution.d.ts +14 -1
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +415 -31
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/ppr/admission.d.ts +83 -0
- package/dist/internal/ppr/admission.d.ts.map +1 -0
- package/dist/internal/ppr/admission.js +221 -0
- package/dist/internal/ppr/admission.js.map +1 -0
- package/dist/internal/ppr/canary.d.ts +24 -0
- package/dist/internal/ppr/canary.d.ts.map +1 -0
- package/dist/internal/ppr/canary.js +102 -0
- package/dist/internal/ppr/canary.js.map +1 -0
- package/dist/internal/ppr/compile-shell.d.ts +39 -0
- package/dist/internal/ppr/compile-shell.d.ts.map +1 -0
- package/dist/internal/ppr/compile-shell.js +298 -0
- package/dist/internal/ppr/compile-shell.js.map +1 -0
- package/dist/internal/ppr/contract.d.ts +177 -0
- package/dist/internal/ppr/contract.d.ts.map +1 -0
- package/dist/internal/ppr/contract.js +80 -0
- package/dist/internal/ppr/contract.js.map +1 -0
- package/dist/internal/ppr/frames.d.ts +71 -0
- package/dist/internal/ppr/frames.d.ts.map +1 -0
- package/dist/internal/ppr/frames.js +119 -0
- package/dist/internal/ppr/frames.js.map +1 -0
- package/dist/internal/ppr/integration.d.ts +19 -0
- package/dist/internal/ppr/integration.d.ts.map +1 -0
- package/dist/internal/ppr/integration.js +106 -0
- package/dist/internal/ppr/integration.js.map +1 -0
- package/dist/internal/ppr/options.d.ts +96 -0
- package/dist/internal/ppr/options.d.ts.map +1 -0
- package/dist/internal/ppr/options.js +125 -0
- package/dist/internal/ppr/options.js.map +1 -0
- package/dist/internal/ppr/route-rules.d.ts +12 -0
- package/dist/internal/ppr/route-rules.d.ts.map +1 -0
- package/dist/internal/ppr/route-rules.js +53 -0
- package/dist/internal/ppr/route-rules.js.map +1 -0
- package/dist/internal/ppr/runtime.d.ts +29 -0
- package/dist/internal/ppr/runtime.d.ts.map +1 -0
- package/dist/internal/ppr/runtime.js +32 -0
- package/dist/internal/ppr/runtime.js.map +1 -0
- package/dist/internal/ppr/server.d.ts +29 -0
- package/dist/internal/ppr/server.d.ts.map +1 -0
- package/dist/internal/ppr/server.js +1407 -0
- package/dist/internal/ppr/server.js.map +1 -0
- package/dist/internal/ppr/shell-store.d.ts +38 -0
- package/dist/internal/ppr/shell-store.d.ts.map +1 -0
- package/dist/internal/ppr/shell-store.js +90 -0
- package/dist/internal/ppr/shell-store.js.map +1 -0
- package/dist/internal/prerendered-pages.d.ts +1 -1
- package/dist/internal/prerendered-pages.d.ts.map +1 -1
- package/dist/internal/prerendered-pages.js +7 -4
- package/dist/internal/prerendered-pages.js.map +1 -1
- package/dist/internal/query-digest.d.ts +9 -0
- package/dist/internal/query-digest.d.ts.map +1 -0
- package/dist/internal/query-digest.js +23 -0
- package/dist/internal/query-digest.js.map +1 -0
- package/dist/internal/query-runtime.d.ts +6 -1
- package/dist/internal/query-runtime.d.ts.map +1 -1
- package/dist/internal/query-runtime.js +9 -0
- package/dist/internal/query-runtime.js.map +1 -1
- package/dist/internal/query-seam.d.ts +29 -0
- package/dist/internal/query-seam.d.ts.map +1 -0
- package/dist/internal/query-seam.js +23 -0
- package/dist/internal/query-seam.js.map +1 -0
- package/dist/internal/render-document.d.ts +28 -1
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +83 -18
- package/dist/internal/render-document.js.map +1 -1
- package/dist/internal/request-abort.d.ts +8 -0
- package/dist/internal/request-abort.d.ts.map +1 -0
- package/dist/internal/request-abort.js +22 -0
- package/dist/internal/request-abort.js.map +1 -0
- package/dist/internal/server-boundary.d.ts +2 -0
- package/dist/internal/server-boundary.d.ts.map +1 -1
- package/dist/internal/server-boundary.js +8 -1
- package/dist/internal/server-boundary.js.map +1 -1
- package/dist/internal/share-data.d.ts +10 -0
- package/dist/internal/share-data.d.ts.map +1 -0
- package/dist/internal/share-data.js +135 -0
- package/dist/internal/share-data.js.map +1 -0
- package/dist/internal/store-signature.d.ts +15 -0
- package/dist/internal/store-signature.d.ts.map +1 -0
- package/dist/internal/store-signature.js +114 -0
- package/dist/internal/store-signature.js.map +1 -0
- package/dist/internal/vite-plugin-compat.d.ts +2 -2
- package/dist/internal/vite-plugin-compat.js +2 -2
- package/dist/internal/web-app.d.ts +25 -0
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +78 -3
- package/dist/internal/web-app.js.map +1 -1
- package/dist/islands-swap.d.ts +41 -0
- package/dist/islands-swap.d.ts.map +1 -0
- package/dist/islands-swap.js +270 -0
- package/dist/islands-swap.js.map +1 -0
- package/dist/islands.d.ts +86 -24
- package/dist/islands.d.ts.map +1 -1
- package/dist/islands.js +138 -78
- package/dist/islands.js.map +1 -1
- package/dist/isr.d.ts +67 -13
- package/dist/isr.d.ts.map +1 -1
- package/dist/isr.js +247 -99
- package/dist/isr.js.map +1 -1
- package/dist/keep-alive.d.ts +147 -0
- package/dist/keep-alive.d.ts.map +1 -0
- package/dist/keep-alive.js +1069 -0
- package/dist/keep-alive.js.map +1 -0
- package/dist/live/bus.d.ts +56 -0
- package/dist/live/bus.d.ts.map +1 -0
- package/dist/live/bus.js +32 -0
- package/dist/live/bus.js.map +1 -0
- package/dist/live/core.d.ts +25 -0
- package/dist/live/core.d.ts.map +1 -0
- package/dist/live/core.js +29 -0
- package/dist/live/core.js.map +1 -0
- package/dist/live/history.d.ts +38 -0
- package/dist/live/history.d.ts.map +1 -0
- package/dist/live/history.js +104 -0
- package/dist/live/history.js.map +1 -0
- package/dist/live/hub-protocol.d.ts +46 -0
- package/dist/live/hub-protocol.d.ts.map +1 -0
- package/dist/live/hub-protocol.js +108 -0
- package/dist/live/hub-protocol.js.map +1 -0
- package/dist/live/hub.d.ts +181 -0
- package/dist/live/hub.d.ts.map +1 -0
- package/dist/live/hub.js +539 -0
- package/dist/live/hub.js.map +1 -0
- package/dist/live/keyring.d.ts +87 -0
- package/dist/live/keyring.d.ts.map +1 -0
- package/dist/live/keyring.js +260 -0
- package/dist/live/keyring.js.map +1 -0
- package/dist/live/read-capped.d.ts +6 -0
- package/dist/live/read-capped.d.ts.map +1 -0
- package/dist/live/read-capped.js +42 -0
- package/dist/live/read-capped.js.map +1 -0
- package/dist/live/revocations.d.ts +52 -0
- package/dist/live/revocations.d.ts.map +1 -0
- package/dist/live/revocations.js +116 -0
- package/dist/live/revocations.js.map +1 -0
- package/dist/live/settings.d.ts +14 -0
- package/dist/live/settings.d.ts.map +1 -0
- package/dist/live/settings.js +38 -0
- package/dist/live/settings.js.map +1 -0
- package/dist/live-client.d.ts +34 -0
- package/dist/live-client.d.ts.map +1 -0
- package/dist/live-client.js +840 -0
- package/dist/live-client.js.map +1 -0
- package/dist/live-hub.d.ts +130 -0
- package/dist/live-hub.d.ts.map +1 -0
- package/dist/live-hub.js +507 -0
- package/dist/live-hub.js.map +1 -0
- package/dist/live-redis.d.ts +48 -0
- package/dist/live-redis.d.ts.map +1 -0
- package/dist/live-redis.js +201 -0
- package/dist/live-redis.js.map +1 -0
- package/dist/live.d.ts +94 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +869 -0
- package/dist/live.js.map +1 -0
- package/dist/manifest.d.ts +113 -2
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +109 -5
- package/dist/manifest.js.map +1 -1
- package/dist/navigation.d.ts +9 -3
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +13 -3
- package/dist/navigation.js.map +1 -1
- package/dist/plugins/vite-zone-guard.d.ts +6 -0
- package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
- package/dist/plugins/vite-zone-guard.js +8 -2
- package/dist/plugins/vite-zone-guard.js.map +1 -1
- package/dist/ppr.d.ts +73 -0
- package/dist/ppr.d.ts.map +1 -0
- package/dist/ppr.js +76 -0
- package/dist/ppr.js.map +1 -0
- package/dist/query.d.ts +8 -13
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +112 -18
- package/dist/query.js.map +1 -1
- package/dist/render-seam.d.ts +30 -0
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +5 -0
- package/dist/render-seam.js.map +1 -1
- package/dist/route-types.d.ts.map +1 -1
- package/dist/route-types.js +2 -2
- package/dist/route-types.js.map +1 -1
- package/dist/router.d.ts +72 -4
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +374 -99
- package/dist/router.js.map +1 -1
- package/dist/server-components/build.d.ts.map +1 -1
- package/dist/server-components/build.js +3 -3
- package/dist/server-components/build.js.map +1 -1
- package/dist/server-components/client.d.ts +23 -1
- package/dist/server-components/client.d.ts.map +1 -1
- package/dist/server-components/client.js +92 -5
- package/dist/server-components/client.js.map +1 -1
- package/dist/server-components/hole.d.ts +8 -0
- package/dist/server-components/hole.d.ts.map +1 -0
- package/dist/server-components/hole.js +20 -0
- package/dist/server-components/hole.js.map +1 -0
- package/dist/server-components/html-policy.d.ts.map +1 -1
- package/dist/server-components/html-policy.js +62 -4
- package/dist/server-components/html-policy.js.map +1 -1
- package/dist/server-components/html-scan.d.ts +17 -0
- package/dist/server-components/html-scan.d.ts.map +1 -0
- package/dist/server-components/html-scan.js +180 -0
- package/dist/server-components/html-scan.js.map +1 -0
- package/dist/server-components/html.d.ts.map +1 -1
- package/dist/server-components/html.js +19 -4
- package/dist/server-components/html.js.map +1 -1
- package/dist/server-components/router-integration.d.ts +4 -1
- package/dist/server-components/router-integration.d.ts.map +1 -1
- package/dist/server-components/router-integration.js +23 -2
- package/dist/server-components/router-integration.js.map +1 -1
- package/dist/server-components/runtime.d.ts +6 -1
- package/dist/server-components/runtime.d.ts.map +1 -1
- package/dist/server-components/runtime.js +86 -28
- package/dist/server-components/runtime.js.map +1 -1
- package/dist/server-components/state.d.ts +3 -1
- package/dist/server-components/state.d.ts.map +1 -1
- package/dist/server-components/state.js +50 -38
- package/dist/server-components/state.js.map +1 -1
- package/dist/server-components/types.d.ts +9 -0
- package/dist/server-components/types.d.ts.map +1 -1
- package/dist/vite.d.ts +5 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +58 -4
- package/dist/vite.js.map +1 -1
- package/package.json +48 -8
- package/src/boundary.ts +77 -5
- package/src/build-plan.ts +3 -0
- package/src/build-vite.ts +2 -0
- package/src/build.ts +19 -1
- package/src/client.ts +147 -10
- package/src/csp.ts +16 -2
- package/src/deferred.ts +11 -5
- package/src/dev-feed.ts +11 -2
- package/src/dev-session.ts +22 -11
- package/src/dev.ts +29 -8
- package/src/diagnostic-catalog.ts +53 -0
- package/src/diagnostic.ts +1 -0
- package/src/fn.ts +24 -7
- package/src/fs.ts +22 -8
- package/src/identity.ts +61 -0
- package/src/index.ts +19 -0
- package/src/internal/cdn/cloudflare.ts +2 -1
- package/src/internal/certified-components.ts +1 -1
- package/src/internal/codegen.ts +87 -8
- package/src/internal/dev-websocket.ts +230 -0
- package/src/internal/hole-runtime.ts +286 -0
- package/src/internal/identity-epoch.ts +235 -0
- package/src/internal/invalidation-contract.ts +35 -0
- package/src/internal/invalidation.ts +213 -0
- package/src/internal/island-bindings.ts +191 -0
- package/src/internal/island-list.ts +92 -0
- package/src/internal/island-mount.ts +68 -0
- package/src/internal/island-owner.ts +20 -0
- package/src/internal/island-resource.ts +76 -0
- package/src/internal/island-signals.ts +249 -0
- package/src/internal/keep-alive-table.ts +72 -0
- package/src/internal/live-contract.ts +106 -0
- package/src/internal/live-queries.ts +60 -0
- package/src/internal/page-events.ts +27 -0
- package/src/internal/page-execution.ts +579 -40
- package/src/internal/ppr/admission.ts +300 -0
- package/src/internal/ppr/canary.ts +122 -0
- package/src/internal/ppr/compile-shell.ts +379 -0
- package/src/internal/ppr/contract.ts +265 -0
- package/src/internal/ppr/frames.ts +170 -0
- package/src/internal/ppr/integration.ts +137 -0
- package/src/internal/ppr/options.ts +221 -0
- package/src/internal/ppr/route-rules.ts +77 -0
- package/src/internal/ppr/runtime.ts +36 -0
- package/src/internal/ppr/server.ts +1764 -0
- package/src/internal/ppr/shell-store.ts +139 -0
- package/src/internal/prerendered-pages.ts +7 -4
- package/src/internal/query-digest.ts +34 -0
- package/src/internal/query-runtime.ts +14 -1
- package/src/internal/query-seam.ts +48 -0
- package/src/internal/render-document.ts +149 -22
- package/src/internal/request-abort.ts +26 -0
- package/src/internal/server-boundary.ts +10 -1
- package/src/internal/share-data.ts +136 -0
- package/src/internal/store-signature.ts +149 -0
- package/src/internal/vite-plugin-compat.ts +2 -2
- package/src/internal/web-app.ts +121 -3
- package/src/islands-swap.ts +283 -0
- package/src/islands.ts +169 -72
- package/src/isr.ts +348 -106
- package/src/keep-alive.ts +1315 -0
- package/src/live/bus.ts +77 -0
- package/src/live/core.ts +52 -0
- package/src/live/history.ts +109 -0
- package/src/live/hub-protocol.ts +133 -0
- package/src/live/hub.ts +615 -0
- package/src/live/keyring.ts +379 -0
- package/src/live/read-capped.ts +36 -0
- package/src/live/revocations.ts +155 -0
- package/src/live/settings.ts +46 -0
- package/src/live-client.ts +973 -0
- package/src/live-hub.ts +658 -0
- package/src/live-redis.ts +259 -0
- package/src/live.ts +1096 -0
- package/src/manifest.ts +274 -6
- package/src/navigation.ts +14 -2
- package/src/plugins/vite-zone-guard.ts +15 -2
- package/src/ppr.ts +153 -0
- package/src/query.ts +147 -20
- package/src/render-seam.ts +35 -0
- package/src/route-types.ts +3 -1
- package/src/router.ts +494 -101
- package/src/server-components/build.ts +9 -3
- package/src/server-components/client.ts +89 -5
- package/src/server-components/hole.ts +23 -0
- package/src/server-components/html-policy.ts +63 -4
- package/src/server-components/html-scan.ts +159 -0
- package/src/server-components/html.ts +18 -4
- package/src/server-components/router-integration.ts +21 -2
- package/src/server-components/runtime.ts +112 -30
- package/src/server-components/state.ts +66 -44
- package/src/server-components/types.ts +16 -0
- package/src/vite.ts +75 -6
- package/dist/nano.d.ts +0 -125
- package/dist/nano.d.ts.map +0 -1
- package/dist/nano.js +0 -190
- package/dist/nano.js.map +0 -1
- package/src/nano.ts +0 -246
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { createRequire } from "node:module"
|
|
2
|
+
import { basename, join, resolve } from "node:path"
|
|
3
|
+
import type * as TS from "typescript"
|
|
4
|
+
import type { Manifest, RouteEntry } from "../../manifest.ts"
|
|
5
|
+
import { type BunMetafileLike, fromBunMetafile } from "../../module-graph.ts"
|
|
6
|
+
import { isTypeScriptCompiler } from "../typescript-api.ts"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Which routes passed the shell check for one deployment. The server shares a shell only for a route
|
|
10
|
+
* listed here under its own deployment; anything else renders blocking (fail closed).
|
|
11
|
+
*/
|
|
12
|
+
export interface ShellCheckRecord {
|
|
13
|
+
readonly v: 1
|
|
14
|
+
readonly deployment: string
|
|
15
|
+
/** Route id to the sha256 of the checked shell bundle. */
|
|
16
|
+
readonly routes: Readonly<Record<string, string>>
|
|
17
|
+
/** Route id to the shared holes whose loaders were checked in that bundle: only these are stored. */
|
|
18
|
+
readonly sharedHoles?: Readonly<Record<string, readonly string[]>>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class ShellCheckError extends Error {
|
|
22
|
+
override readonly name = "ShellCheckError"
|
|
23
|
+
constructor(
|
|
24
|
+
readonly routeId: string,
|
|
25
|
+
readonly chain: readonly string[],
|
|
26
|
+
) {
|
|
27
|
+
super(
|
|
28
|
+
`[nifra/web] ppr: the shell of route "${routeId}" reaches request-scoped module "${chain.at(-1)}" through ${chain.join(" -> ")}. Move that read into a dynamic boundary's loader: a shared boundary's loader is part of the shell`,
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CompileShellsOptions {
|
|
34
|
+
readonly routesDir: string
|
|
35
|
+
readonly manifest: Manifest
|
|
36
|
+
/** The deployment the record is for: the server refuses a record made for another one. */
|
|
37
|
+
readonly deployment: string
|
|
38
|
+
/** True for a module (absolute path, `/` separators) whose code reads the visitor's request. */
|
|
39
|
+
readonly requestScoped: (moduleId: string) => boolean
|
|
40
|
+
/** Called for each route that fails; the route is left out of the record. Default: rethrow. */
|
|
41
|
+
readonly onFailure?: (routeId: string, error: unknown) => void
|
|
42
|
+
/** Bun plugins the server build uses for route files, such as `svelteBunPlugin("ssr")`. */
|
|
43
|
+
readonly plugins?: readonly import("bun").BunPlugin[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const NAMESPACE = "nifra-ppr-shell"
|
|
47
|
+
|
|
48
|
+
const toPosix = (path: string): string => path.replaceAll("\\", "/")
|
|
49
|
+
|
|
50
|
+
const escapeRegExp = (text: string): string => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* nifra's own modules that read the visitor's request from async context. Packages stay external in
|
|
54
|
+
* the shell bundle, so these are bundled in instead: the output graph then shows whether shell code
|
|
55
|
+
* calls them, not merely whether a module beside it imports them.
|
|
56
|
+
*/
|
|
57
|
+
const NIFRA_REQUEST_SCOPED: readonly string[] = ["@nifrajs/middleware/context-storage"]
|
|
58
|
+
const NIFRA_REQUEST_SCOPED_FILTER = new RegExp(
|
|
59
|
+
`^(?:${NIFRA_REQUEST_SCOPED.map(escapeRegExp).join("|")})$`,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
/** Hands any module the Worker's `env`, its secrets included, with no request in sight. */
|
|
63
|
+
const PLATFORM_ENV = /^cloudflare:workers$/
|
|
64
|
+
|
|
65
|
+
/** The import chain from `root` to the first request-scoped module it reaches, breadth first. */
|
|
66
|
+
function requestScopedChain(
|
|
67
|
+
root: string,
|
|
68
|
+
next: ReadonlyMap<string, readonly string[]>,
|
|
69
|
+
scoped: ReadonlySet<string>,
|
|
70
|
+
): string[] | undefined {
|
|
71
|
+
const parent = new Map<string, string | undefined>([[root, undefined]])
|
|
72
|
+
const queue = [root]
|
|
73
|
+
for (let cursor = 0; cursor < queue.length; cursor++) {
|
|
74
|
+
const module = queue[cursor]
|
|
75
|
+
if (module === undefined) break
|
|
76
|
+
if (scoped.has(module)) {
|
|
77
|
+
const chain: string[] = []
|
|
78
|
+
for (let at: string | undefined = module; at !== undefined; at = parent.get(at))
|
|
79
|
+
chain.push(at)
|
|
80
|
+
return chain.reverse()
|
|
81
|
+
}
|
|
82
|
+
for (const child of next.get(module) ?? []) {
|
|
83
|
+
if (!parent.has(child)) {
|
|
84
|
+
parent.set(child, module)
|
|
85
|
+
queue.push(child)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return undefined
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Bundle each route's shell (its page and layout modules, its backend `shellLoader`, and nothing else)
|
|
94
|
+
* and refuse it when the emitted code reaches a request-scoped module. A module counts only
|
|
95
|
+
* when its code reached the output, so an import only a hole loader uses is not a leak. Routes whose
|
|
96
|
+
* backend half has no `shellLoader` are skipped. Bun only.
|
|
97
|
+
*/
|
|
98
|
+
export async function compileShells(options: CompileShellsOptions): Promise<ShellCheckRecord> {
|
|
99
|
+
const dir = resolve(options.routesDir)
|
|
100
|
+
const routes: Record<string, string> = {}
|
|
101
|
+
const sharedHoles: Record<string, readonly string[]> = {}
|
|
102
|
+
const parser = typeScriptFor(dir)
|
|
103
|
+
for (const route of options.manifest.routes) {
|
|
104
|
+
if (route.backend === undefined) continue
|
|
105
|
+
try {
|
|
106
|
+
const checked = await compileShell(
|
|
107
|
+
dir,
|
|
108
|
+
options.manifest,
|
|
109
|
+
route,
|
|
110
|
+
route.backend,
|
|
111
|
+
options.requestScoped,
|
|
112
|
+
options.plugins ?? [],
|
|
113
|
+
parser,
|
|
114
|
+
)
|
|
115
|
+
if (checked === undefined) continue
|
|
116
|
+
routes[route.id] = checked.hash
|
|
117
|
+
if (checked.sharedHoles.length > 0) sharedHoles[route.id] = checked.sharedHoles
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (options.onFailure === undefined) throw error
|
|
120
|
+
options.onFailure(route.id, error)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return Object.keys(sharedHoles).length === 0
|
|
124
|
+
? { v: 1, deployment: options.deployment, routes }
|
|
125
|
+
: { v: 1, deployment: options.deployment, routes, sharedHoles }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The app's TypeScript compiler, read only when a backend half has boundary loaders to sort. */
|
|
129
|
+
function typeScriptFor(dir: string): () => typeof TS | undefined {
|
|
130
|
+
let loaded: typeof TS | undefined
|
|
131
|
+
let tried = false
|
|
132
|
+
return () => {
|
|
133
|
+
if (tried) return loaded
|
|
134
|
+
tried = true
|
|
135
|
+
let ts: unknown
|
|
136
|
+
try {
|
|
137
|
+
ts = createRequire(join(dir, "package.json"))("typescript")
|
|
138
|
+
} catch {
|
|
139
|
+
ts = undefined
|
|
140
|
+
}
|
|
141
|
+
if (isTypeScriptCompiler(ts)) loaded = ts
|
|
142
|
+
else {
|
|
143
|
+
console.warn(
|
|
144
|
+
"[nifra/web] ppr: no shared hole's loader is checked, so each loads for every visitor: finding them reads boundaryLoaders with the TypeScript compiler API, which the app's TypeScript does not have (TypeScript 7 ships none; install typescript@^6 as a dev dependency)",
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
return loaded
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** `node` without the `satisfies`, `as` and parentheses around it. */
|
|
152
|
+
function unwrapped(ts: typeof TS, node: TS.Expression): TS.Expression {
|
|
153
|
+
let at = node
|
|
154
|
+
while (ts.isSatisfiesExpression(at) || ts.isAsExpression(at) || ts.isParenthesizedExpression(at))
|
|
155
|
+
at = at.expression
|
|
156
|
+
return at
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The backend half with `boundaryLoaders` cut down to its shared entries, and their names. A shared
|
|
161
|
+
* entry is one written inline with `revalidate`, which only a shared boundary's entry may have; one the
|
|
162
|
+
* source does not spell out that way is left out, so its hole is not stored (fail closed). A per-visitor
|
|
163
|
+
* entry's loader may read the request, so it never joins the shell bundle.
|
|
164
|
+
*/
|
|
165
|
+
function sharedLoaders(
|
|
166
|
+
ts: typeof TS,
|
|
167
|
+
module: SourceModule,
|
|
168
|
+
): { readonly source: string; readonly names: readonly string[] } | undefined {
|
|
169
|
+
const file = ts.createSourceFile(
|
|
170
|
+
module.path,
|
|
171
|
+
module.source,
|
|
172
|
+
ts.ScriptTarget.Latest,
|
|
173
|
+
true,
|
|
174
|
+
module.loader === "tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
175
|
+
)
|
|
176
|
+
for (const statement of file.statements) {
|
|
177
|
+
if (!ts.isVariableStatement(statement)) continue
|
|
178
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
179
|
+
if (!ts.isIdentifier(declaration.name) || declaration.name.text !== "boundaryLoaders")
|
|
180
|
+
continue
|
|
181
|
+
const initializer = declaration.initializer && unwrapped(ts, declaration.initializer)
|
|
182
|
+
if (initializer === undefined || !ts.isObjectLiteralExpression(initializer)) return undefined
|
|
183
|
+
const names: string[] = []
|
|
184
|
+
const kept: string[] = []
|
|
185
|
+
for (const property of initializer.properties) {
|
|
186
|
+
if (!ts.isPropertyAssignment(property)) continue
|
|
187
|
+
const { name } = property
|
|
188
|
+
if (!ts.isIdentifier(name) && !ts.isStringLiteral(name)) continue
|
|
189
|
+
const entry = unwrapped(ts, property.initializer)
|
|
190
|
+
if (!ts.isObjectLiteralExpression(entry)) continue
|
|
191
|
+
const shared = entry.properties.some(
|
|
192
|
+
(field) =>
|
|
193
|
+
field.name !== undefined &&
|
|
194
|
+
(ts.isIdentifier(field.name) || ts.isStringLiteral(field.name)) &&
|
|
195
|
+
field.name.text === "revalidate",
|
|
196
|
+
)
|
|
197
|
+
if (!shared) continue
|
|
198
|
+
names.push(name.text)
|
|
199
|
+
kept.push(module.source.slice(property.getStart(file), property.getEnd()))
|
|
200
|
+
}
|
|
201
|
+
if (names.length === 0) return undefined
|
|
202
|
+
const source = `${module.source.slice(0, initializer.getStart(file))}{ ${kept.join(", ")} }${module.source.slice(initializer.getEnd())}`
|
|
203
|
+
return { source, names }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return undefined
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
interface SourceModule {
|
|
210
|
+
readonly path: string
|
|
211
|
+
readonly loader: "ts" | "tsx"
|
|
212
|
+
readonly source: string
|
|
213
|
+
/** The names it exports, read from its source without running it. */
|
|
214
|
+
readonly exports: readonly string[]
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function readModule(dir: string, file: string): Promise<SourceModule> {
|
|
218
|
+
const path = toPosix(resolve(dir, file))
|
|
219
|
+
const loader = /\.[cm]?tsx$|\.jsx$/.test(path) ? "tsx" : "ts"
|
|
220
|
+
const source = await Bun.file(path).text()
|
|
221
|
+
return { path, loader, source, exports: new Bun.Transpiler({ loader }).scan(source).exports }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function compileShell(
|
|
225
|
+
dir: string,
|
|
226
|
+
manifest: Manifest,
|
|
227
|
+
route: RouteEntry,
|
|
228
|
+
backend: string,
|
|
229
|
+
requestScoped: (moduleId: string) => boolean,
|
|
230
|
+
plugins: readonly import("bun").BunPlugin[],
|
|
231
|
+
parser: () => typeof TS | undefined,
|
|
232
|
+
): Promise<{ readonly hash: string; readonly sharedHoles: readonly string[] } | undefined> {
|
|
233
|
+
const backendModule = await readModule(dir, backend)
|
|
234
|
+
if (!backendModule.exports.includes("shellLoader")) return undefined
|
|
235
|
+
const ts = backendModule.exports.includes("boundaryLoaders") ? parser() : undefined
|
|
236
|
+
const shared = ts === undefined ? undefined : sharedLoaders(ts, backendModule)
|
|
237
|
+
const kept = shared === undefined ? ["shellLoader"] : ["shellLoader", "boundaryLoaders"]
|
|
238
|
+
// A gate's loader runs for its redirect or refusal and its data never reaches the shell; any other
|
|
239
|
+
// layout loader reads the request for data the shell would show every visitor.
|
|
240
|
+
for (const id of route.layoutIds) {
|
|
241
|
+
const layoutBackend = manifest.layouts[id]?.backend
|
|
242
|
+
if (layoutBackend === undefined) continue
|
|
243
|
+
const names = (await readModule(dir, layoutBackend)).exports
|
|
244
|
+
if (names.includes("loader") && !names.includes("gate")) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
`[nifra/web] ppr: route "${route.id}" has a shellLoader, but its layout "${id}" has a loader that is not a gate. Its output would be in a shell shared by every visitor; move that data into a dynamic boundary, or mark the layout \`gate\` if it loads only to redirect or refuse`,
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const at = (file: string): string => JSON.stringify(toPosix(resolve(dir, file)))
|
|
251
|
+
// A page's and each layout's module whole: the shell runs `meta`, `handle` and `searchSchema` as
|
|
252
|
+
// well as the component, and an import only one of them uses must not escape the check.
|
|
253
|
+
const lines = [
|
|
254
|
+
`export { ${kept.join(", ")} } from ${at(backend)}`,
|
|
255
|
+
`export * as page from ${at(route.file)}`,
|
|
256
|
+
]
|
|
257
|
+
route.layoutIds.forEach((id, index) => {
|
|
258
|
+
const layout = manifest.layouts[id]
|
|
259
|
+
if (layout !== undefined) lines.push(`export * as layout${index} from ${at(layout.file)}`)
|
|
260
|
+
})
|
|
261
|
+
const nifraScoped = new Set<string>()
|
|
262
|
+
const envImporters = new Set<string>()
|
|
263
|
+
const result = await Bun.build({
|
|
264
|
+
// Bun reads a `/` in an entry name as a directory, and route ids have them.
|
|
265
|
+
entrypoints: [`${NAMESPACE}:${encodeURIComponent(route.id)}`],
|
|
266
|
+
target: "bun",
|
|
267
|
+
packages: "external",
|
|
268
|
+
metafile: true,
|
|
269
|
+
throw: false,
|
|
270
|
+
plugins: [
|
|
271
|
+
// Ours first: its resolve and load hooks claim the entry and the backend half before theirs.
|
|
272
|
+
{
|
|
273
|
+
name: NAMESPACE,
|
|
274
|
+
setup(build) {
|
|
275
|
+
build.onResolve({ filter: new RegExp(`^${NAMESPACE}:`) }, (args) => ({
|
|
276
|
+
path: args.path,
|
|
277
|
+
namespace: NAMESPACE,
|
|
278
|
+
}))
|
|
279
|
+
build.onLoad({ filter: /.*/, namespace: NAMESPACE }, () => ({
|
|
280
|
+
loader: "ts",
|
|
281
|
+
contents: lines.join("\n"),
|
|
282
|
+
}))
|
|
283
|
+
build.onResolve({ filter: NIFRA_REQUEST_SCOPED_FILTER }, (args) => {
|
|
284
|
+
let path: string
|
|
285
|
+
try {
|
|
286
|
+
path = toPosix(Bun.resolveSync(args.path, args.resolveDir))
|
|
287
|
+
} catch {
|
|
288
|
+
// Not installed, so no shell code can call it.
|
|
289
|
+
return undefined
|
|
290
|
+
}
|
|
291
|
+
nifraScoped.add(path)
|
|
292
|
+
return { path }
|
|
293
|
+
})
|
|
294
|
+
build.onResolve({ filter: PLATFORM_ENV }, (args) => {
|
|
295
|
+
envImporters.add(toPosix(resolve(args.importer)))
|
|
296
|
+
return { path: args.path, external: true }
|
|
297
|
+
})
|
|
298
|
+
// The backend half's other exports are not shell code. A hole loader's `output: t.object()`
|
|
299
|
+
// is a call into a package, which the bundler must keep, and with it every import the
|
|
300
|
+
// hole uses. Eliminating them first scopes the check to `shellLoader` and the shared
|
|
301
|
+
// holes' loaders; their imports stay as bare imports, so a module with top-level effects
|
|
302
|
+
// still counts. The file is matched by its `/` form: Bun hands a plugin `\` on Windows.
|
|
303
|
+
build.onLoad(
|
|
304
|
+
{ filter: new RegExp(`${escapeRegExp(basename(backendModule.path))}$`) },
|
|
305
|
+
(args) =>
|
|
306
|
+
toPosix(args.path) !== backendModule.path
|
|
307
|
+
? undefined
|
|
308
|
+
: {
|
|
309
|
+
loader: "js",
|
|
310
|
+
contents: new Bun.Transpiler({
|
|
311
|
+
loader: backendModule.loader,
|
|
312
|
+
trimUnusedImports: true,
|
|
313
|
+
exports: {
|
|
314
|
+
eliminate: backendModule.exports.filter((name) => !kept.includes(name)),
|
|
315
|
+
},
|
|
316
|
+
}).transformSync(shared?.source ?? backendModule.source),
|
|
317
|
+
},
|
|
318
|
+
)
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
...plugins,
|
|
322
|
+
],
|
|
323
|
+
})
|
|
324
|
+
if (!result.success) {
|
|
325
|
+
throw new AggregateError(
|
|
326
|
+
result.logs,
|
|
327
|
+
`[nifra/web] ppr: the shell of "${route.id}" failed to build`,
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
// biome-ignore lint/plugin/noChainedTypeAssertions: Bun returns the metafile when `metafile: true`; @types/bun does not name it yet, and fromBunMetafile fails closed on a missing one.
|
|
331
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: Bun returns the metafile when `metafile: true`; @types/bun does not name it yet, and fromBunMetafile fails closed on a missing one.
|
|
332
|
+
const meta = (result as unknown as { readonly metafile?: BunMetafileLike }).metafile
|
|
333
|
+
const graph = fromBunMetafile(meta)
|
|
334
|
+
const entry = Object.values(meta?.outputs ?? {}).find((output) =>
|
|
335
|
+
output.entryPoint?.startsWith(`${NAMESPACE}:`),
|
|
336
|
+
)
|
|
337
|
+
if (entry === undefined)
|
|
338
|
+
throw new Error(`[nifra/web] ppr: no output for the shell of "${route.id}"`)
|
|
339
|
+
// Re-exports and the virtual entry have zero bytes too, so every edge stays and only the
|
|
340
|
+
// request-scoped test reads bytes.
|
|
341
|
+
const emitted = (id: string): boolean => {
|
|
342
|
+
const input = entry.inputs?.[id]
|
|
343
|
+
return (
|
|
344
|
+
typeof input === "object" &&
|
|
345
|
+
input !== null &&
|
|
346
|
+
"bytesInOutput" in input &&
|
|
347
|
+
typeof input.bytesInOutput === "number" &&
|
|
348
|
+
input.bytesInOutput > 0
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
const absolute = (id: string): string =>
|
|
352
|
+
id.startsWith(`${NAMESPACE}:`) ? id : toPosix(resolve(id))
|
|
353
|
+
const next = new Map<string, string[]>()
|
|
354
|
+
const scoped = new Set<string>()
|
|
355
|
+
let root: string | undefined
|
|
356
|
+
for (const [id, module] of Object.entries(graph.modules)) {
|
|
357
|
+
const from = absolute(id)
|
|
358
|
+
if (id.startsWith(`${NAMESPACE}:`)) root = from
|
|
359
|
+
if (emitted(id) && (nifraScoped.has(from) || envImporters.has(from) || requestScoped(from)))
|
|
360
|
+
scoped.add(from)
|
|
361
|
+
next.set(
|
|
362
|
+
from,
|
|
363
|
+
module.imports.flatMap((edge) => (edge.path === undefined ? [] : [absolute(edge.path)])),
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
if (root === undefined) throw new Error(`[nifra/web] ppr: no entry module for "${route.id}"`)
|
|
367
|
+
const chain = requestScopedChain(root, next, scoped)
|
|
368
|
+
if (chain !== undefined) throw new ShellCheckError(route.id, chain.slice(1))
|
|
369
|
+
const output = result.outputs.find((artifact) => artifact.kind === "entry-point")
|
|
370
|
+
if (output === undefined)
|
|
371
|
+
throw new Error(`[nifra/web] ppr: no bundle for the shell of "${route.id}"`)
|
|
372
|
+
const digest = new Uint8Array(
|
|
373
|
+
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(await output.text())),
|
|
374
|
+
)
|
|
375
|
+
return {
|
|
376
|
+
hash: Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join(""),
|
|
377
|
+
sharedHoles: shared?.names ?? [],
|
|
378
|
+
}
|
|
379
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the page executor and `createWebApp` know of partial prerendering. Everything here is small and
|
|
3
|
+
* imports no runtime code: the server, its options and the hole HTML validator load only through
|
|
4
|
+
* `partialPrerendering` (`@nifrajs/web/ppr`), so an app without it carries none of them.
|
|
5
|
+
*/
|
|
6
|
+
import type {
|
|
7
|
+
BoundaryRegistration,
|
|
8
|
+
BoundaryRequestCtx,
|
|
9
|
+
BoundaryStates,
|
|
10
|
+
ShellCtx,
|
|
11
|
+
} from "../../boundary.ts"
|
|
12
|
+
import type { CspPolicy } from "../../csp.ts"
|
|
13
|
+
import type { RevalidateTags } from "../../isr.ts"
|
|
14
|
+
import type { RouteEntry, RouteModule } from "../../manifest.ts"
|
|
15
|
+
import type { RenderAdapter } from "../../render-seam.ts"
|
|
16
|
+
import type { LoadedLayoutModules } from "../render-document.ts"
|
|
17
|
+
import type { PprAdmissionStats } from "./admission.ts"
|
|
18
|
+
import type { ResolvedPprOptions } from "./options.ts"
|
|
19
|
+
|
|
20
|
+
/** An app built with `experimental.ppr` carries its {@link PprServer} under this key. */
|
|
21
|
+
export const PPR_SERVER: unique symbol = Symbol.for("nifra.web.ppr")
|
|
22
|
+
|
|
23
|
+
/** The key of a {@link PprSetup}'s factory. */
|
|
24
|
+
export const PPR_SETUP: unique symbol = Symbol.for("nifra.web.ppr.setup")
|
|
25
|
+
|
|
26
|
+
/** Where the runtime's cancel beacon goes, under the app's base path. */
|
|
27
|
+
export const PPR_CANCEL_PATH = "/__nifra/ppr/cancel"
|
|
28
|
+
|
|
29
|
+
interface HoleFields {
|
|
30
|
+
readonly interactive?: boolean
|
|
31
|
+
readonly timeout?: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A hole filled per visitor. */
|
|
35
|
+
export type DynamicHole = Extract<BoundaryRegistration, { readonly mode: "dynamic" }> & HoleFields
|
|
36
|
+
|
|
37
|
+
/** A hole built once for every visitor, with its loader entry's freshness merged in. */
|
|
38
|
+
export type SharedHole = Extract<BoundaryRegistration, { readonly mode: "shared" }> &
|
|
39
|
+
HoleFields & {
|
|
40
|
+
readonly revalidate: number
|
|
41
|
+
readonly revalidateTags?: RevalidateTags | undefined
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The boundary fields partial prerendering reads beyond the neutral registration. */
|
|
45
|
+
export type HoleRegistration = DynamicHole | SharedHole
|
|
46
|
+
|
|
47
|
+
export const isHole = (boundary: BoundaryRegistration): boundary is HoleRegistration =>
|
|
48
|
+
boundary.mode === "dynamic" || boundary.mode === "shared"
|
|
49
|
+
|
|
50
|
+
/** The key `serverHole()` stores a hole's server component under, on the hole's `render`. */
|
|
51
|
+
export const SERVER_HOLE: unique symbol = Symbol.for("nifra.web.server-hole")
|
|
52
|
+
|
|
53
|
+
/** The server component a hole's `render` names, or `undefined` for an ordinary `render`. */
|
|
54
|
+
export const serverHoleComponent = (render: unknown): object | undefined => {
|
|
55
|
+
if (typeof render !== "function") return undefined
|
|
56
|
+
const component: unknown = Reflect.get(render, SERVER_HOLE)
|
|
57
|
+
return typeof component === "object" && component !== null ? component : undefined
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** What the page executor hands over for one PPR request. */
|
|
61
|
+
export interface PprRequest {
|
|
62
|
+
readonly routeId: string
|
|
63
|
+
/** The request's pathname, without search: the shell renders it, so it is part of the key. */
|
|
64
|
+
readonly path: string
|
|
65
|
+
readonly locale: string | undefined
|
|
66
|
+
/** The declared variations this request selected, by name; part of the shell key. */
|
|
67
|
+
readonly variations: Readonly<Record<string, string>>
|
|
68
|
+
readonly request: Request
|
|
69
|
+
/** The route's dynamic and shared boundaries, in declaration order, with their merged (guarded)
|
|
70
|
+
* loaders. */
|
|
71
|
+
readonly holes: readonly HoleRegistration[]
|
|
72
|
+
readonly holeContext: Omit<BoundaryRequestCtx, "signal" | "live">
|
|
73
|
+
/** What the shell loader and every shared hole loader get, but the build's signal. */
|
|
74
|
+
readonly shellContext: Omit<ShellCtx, "signal">
|
|
75
|
+
readonly loadShell: (signal: AbortSignal) => Promise<unknown>
|
|
76
|
+
/** Render the full document for `shellData` with every hole pending, under no nonce. */
|
|
77
|
+
readonly renderShellDocument: (shellData: unknown, states: BoundaryStates) => Promise<string>
|
|
78
|
+
/** The route's `revalidate`, in seconds: an older shell is served while one rebuild runs. */
|
|
79
|
+
readonly revalidate: number | undefined
|
|
80
|
+
/** The route's `revalidateTags` for this request, read only when a shell is built. */
|
|
81
|
+
readonly tags: () => readonly string[]
|
|
82
|
+
/** A shared hole's `revalidateTags` for this request, read only when its entry is built. */
|
|
83
|
+
readonly holeTags: (hole: SharedHole) => readonly string[]
|
|
84
|
+
/** Keeps the platform alive for work that outlives the response, where the platform needs it. */
|
|
85
|
+
readonly waitUntil?: ((promise: Promise<unknown>) => void) | undefined
|
|
86
|
+
/** The holes the client holds as hosts: a data request sends each one's fill. A hole that shows a
|
|
87
|
+
* server component always gets its fill. */
|
|
88
|
+
readonly fill?: ReadonlySet<string>
|
|
89
|
+
/** Render a hole's server component for this visitor, when the app's server components list it. */
|
|
90
|
+
readonly component?: (
|
|
91
|
+
hole: string,
|
|
92
|
+
component: object,
|
|
93
|
+
input: unknown,
|
|
94
|
+
signal: AbortSignal,
|
|
95
|
+
) => Promise<{ readonly html: string }>
|
|
96
|
+
/** Holes the client keeps: none of them loads, and each state says it was kept. */
|
|
97
|
+
readonly kept?: ReadonlySet<string>
|
|
98
|
+
/** A live load: each hole's keys, and which holes loaded. */
|
|
99
|
+
readonly live?: HoleLive
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A live load's view of one response's holes. Owners are named by hole. */
|
|
103
|
+
export interface HoleLive {
|
|
104
|
+
/** `ctx.live` for one hole's loader. */
|
|
105
|
+
collector(hole: string): (...keys: string[]) => void
|
|
106
|
+
/** Close one hole's collector once its loader settled. */
|
|
107
|
+
seal(hole: string): void
|
|
108
|
+
/** Called once, after every hole that ran settled, with the holes that loaded. */
|
|
109
|
+
done(loaded: readonly string[]): void
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The shell cannot be served now; the caller renders the page whole for this request. */
|
|
113
|
+
export class PprShellUnavailable extends Error {
|
|
114
|
+
constructor(message: string) {
|
|
115
|
+
super(message)
|
|
116
|
+
this.name = "PprShellUnavailable"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Thrown when a route's shell has no passing import-check record. */
|
|
121
|
+
export class PprShellUnchecked extends PprShellUnavailable {
|
|
122
|
+
constructor(routeId: string) {
|
|
123
|
+
super(
|
|
124
|
+
`[nifra/web] ppr: route "${routeId}" has no passing shell import check; rendered blocking`,
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Thrown when a route already runs `shellBuildConcurrency` shell builds: no request waits on one. */
|
|
130
|
+
export class PprShellBusy extends PprShellUnavailable {
|
|
131
|
+
constructor(routeId: string) {
|
|
132
|
+
super(`[nifra/web] ppr: route "${routeId}" is at its shell build limit; rendered blocking`)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Thrown for a key seen fewer than `shellAfterRequests` times in a full store: the page streams whole. */
|
|
137
|
+
export class PprShellCold extends PprShellUnavailable {
|
|
138
|
+
constructor(routeId: string) {
|
|
139
|
+
super(`[nifra/web] ppr: route "${routeId}" has no shell for this key yet; rendered whole`)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* How a response that is not a streamed document gets its holes. `"deferred"`: each hole runs under
|
|
145
|
+
* admission and its own deadline, and its state carries the result as a deferred value (a soft
|
|
146
|
+
* navigation's data request). `"settled"`: the same, awaited (the blocking render). `"omitted"`: no
|
|
147
|
+
* hole runs and every state is an empty host (a prefetch, which must not start work its navigation
|
|
148
|
+
* cannot cancel).
|
|
149
|
+
*/
|
|
150
|
+
export type HoleStatesMode = "deferred" | "settled" | "omitted"
|
|
151
|
+
|
|
152
|
+
export interface PprServerStats extends PprAdmissionStats {
|
|
153
|
+
/** Responses aborted because unsent bytes went over `maxUnsentBytes` (`ppr.slow_aborted`). */
|
|
154
|
+
readonly slowAborted: number
|
|
155
|
+
/** Frame bytes queued for clients across every live PPR stream. */
|
|
156
|
+
readonly unsentBytes: number
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface PprServer {
|
|
160
|
+
/** Stream one PPR document: the shared shell at once, then each hole as it settles. */
|
|
161
|
+
respond(request: PprRequest): Promise<Response>
|
|
162
|
+
/** The shell data a soft navigation's data request carries. */
|
|
163
|
+
shellData(request: PprRequest): Promise<unknown>
|
|
164
|
+
/** The hole states of a data request or a blocking render; `signal` is the request's. */
|
|
165
|
+
holeStates(
|
|
166
|
+
request: PprRequest,
|
|
167
|
+
mode: HoleStatesMode,
|
|
168
|
+
signal: AbortSignal,
|
|
169
|
+
): Promise<BoundaryStates>
|
|
170
|
+
/** Abort the response that printed `token`. Unknown tokens do nothing. */
|
|
171
|
+
cancel(token: string): boolean
|
|
172
|
+
/**
|
|
173
|
+
* Drop every shell and shared hole frame carrying `tag`. A build of either that started before the
|
|
174
|
+
* call is not stored when it finishes, so an entry built from data read before the purge never
|
|
175
|
+
* outlives it.
|
|
176
|
+
*/
|
|
177
|
+
invalidateTag(tag: string): Promise<void>
|
|
178
|
+
stats(): PprServerStats
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** What `createWebApp` gives a {@link PprSetup}: the parts of the app a PPR server depends on. */
|
|
182
|
+
export interface PprEnvironment {
|
|
183
|
+
readonly adapter: RenderAdapter
|
|
184
|
+
readonly csp?: CspPolicy
|
|
185
|
+
/** Part of every shell key, so a shell built by one deployment is never served by another. */
|
|
186
|
+
readonly deployment: string
|
|
187
|
+
/** The app authorizes scripts with a per-request nonce, which would make every shell unique. */
|
|
188
|
+
readonly perRequestNonce: boolean
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** One app's partial prerendering, as the page executor uses it. */
|
|
192
|
+
export interface PprIntegration {
|
|
193
|
+
readonly server: PprServer
|
|
194
|
+
/** The configured public origin a shell may use; never a visitor's Host. */
|
|
195
|
+
readonly origin?: string
|
|
196
|
+
/** Where the runtime's cancel beacon posts, and its handler. */
|
|
197
|
+
readonly cancelPath: string
|
|
198
|
+
cancel(request: Request): Promise<Response>
|
|
199
|
+
/**
|
|
200
|
+
* The streamed document, with the headers middleware and gates wrote, or how this request renders
|
|
201
|
+
* whole instead: `"settled"` for a listed bot or a shell that is unchecked or at its build limit,
|
|
202
|
+
* `"deferred"`, each hole streamed as it settles, for a key with no shell yet.
|
|
203
|
+
*/
|
|
204
|
+
stream(
|
|
205
|
+
request: PprRequest,
|
|
206
|
+
written: () => Readonly<Record<string, string>> | undefined,
|
|
207
|
+
): Promise<Response | "settled" | "deferred">
|
|
208
|
+
/** The shell data, or the shell loader's own output when the shell cannot be served now. */
|
|
209
|
+
shellData(request: PprRequest, signal: AbortSignal): Promise<unknown>
|
|
210
|
+
/** `variations` as the shell's own query string. */
|
|
211
|
+
variationQuery(variations: Readonly<Record<string, string>>): string
|
|
212
|
+
/** Throws for a route the PPR rules keep off the PPR path: its layouts, holes and their limits. */
|
|
213
|
+
assertRoute(
|
|
214
|
+
route: RouteEntry,
|
|
215
|
+
layouts: LoadedLayoutModules,
|
|
216
|
+
boundaries: readonly BoundaryRegistration[],
|
|
217
|
+
): void
|
|
218
|
+
/** The declared variations `search` selects. */
|
|
219
|
+
variationsOf(
|
|
220
|
+
declared: RouteModule["shellVariations"],
|
|
221
|
+
search: string,
|
|
222
|
+
): Readonly<Record<string, string>>
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** What `partialPrerendering()` returns for `createWebApp({ experimental: { ppr } })`. */
|
|
226
|
+
export interface PprSetup {
|
|
227
|
+
readonly [PPR_SETUP]: (environment: PprEnvironment) => PprIntegration
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** `variations` as a query string, sorted by name: the shell key part and the shell's own search. */
|
|
231
|
+
export const variationQuery = (variations: Readonly<Record<string, string>>): string => {
|
|
232
|
+
let query = ""
|
|
233
|
+
for (const name of Object.keys(variations).sort()) {
|
|
234
|
+
query += `${query === "" ? "" : "&"}${encodeURIComponent(name)}=${encodeURIComponent(variations[name] ?? "")}`
|
|
235
|
+
}
|
|
236
|
+
return query
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// A browser's agent took the default rule about 0.5 us to test, and agents repeat. The bounds keep a
|
|
240
|
+
// client that sends a new agent per request from growing the map.
|
|
241
|
+
const MAX_REMEMBERED_AGENT = 256
|
|
242
|
+
const MAX_REMEMBERED_AGENTS = 1024
|
|
243
|
+
const agentAnswers = new WeakMap<RegExp, Map<string, boolean>>()
|
|
244
|
+
|
|
245
|
+
/** Whether `request` gets the blocking render under `options`. */
|
|
246
|
+
export function wantsBlockingRender(options: ResolvedPprOptions, request: Request): boolean {
|
|
247
|
+
const rule = options.blockingFor
|
|
248
|
+
if (typeof rule === "function") return rule(request)
|
|
249
|
+
const agent = request.headers.get("user-agent")
|
|
250
|
+
if (agent === null) return false
|
|
251
|
+
if (agent.length > MAX_REMEMBERED_AGENT) return rule.test(agent)
|
|
252
|
+
let answers = agentAnswers.get(rule)
|
|
253
|
+
if (answers === undefined) {
|
|
254
|
+
answers = new Map()
|
|
255
|
+
agentAnswers.set(rule, answers)
|
|
256
|
+
}
|
|
257
|
+
let answer = answers.get(agent)
|
|
258
|
+
if (answer === undefined) {
|
|
259
|
+
if (answers.size >= MAX_REMEMBERED_AGENTS) answers.clear()
|
|
260
|
+
// No `g` or `y` flag (options refuse them), so a test reads no state and an answer holds.
|
|
261
|
+
answer = rule.test(agent)
|
|
262
|
+
answers.set(agent, answer)
|
|
263
|
+
}
|
|
264
|
+
return answer
|
|
265
|
+
}
|