@nifrajs/web 5.5.1 → 5.7.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 +243 -0
- package/README.md +21 -11
- 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 +10 -0
- package/dist/build.js.map +1 -1
- package/dist/client.d.ts +3 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +174 -23
- 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 +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- 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 +73 -10
- 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 +180 -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 +101 -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 +16 -1
- package/dist/internal/page-execution.d.ts.map +1 -1
- package/dist/internal/page-execution.js +432 -31
- package/dist/internal/page-execution.js.map +1 -1
- package/dist/internal/parity.d.ts +3 -0
- package/dist/internal/parity.d.ts.map +1 -1
- package/dist/internal/parity.js +28 -5
- package/dist/internal/parity.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 +35 -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 +1409 -0
- package/dist/internal/ppr/server.js.map +1 -0
- package/dist/internal/ppr/shell-store.d.ts +39 -0
- package/dist/internal/ppr/shell-store.d.ts.map +1 -0
- package/dist/internal/ppr/shell-store.js +106 -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/present.d.ts +11 -0
- package/dist/internal/present.d.ts.map +1 -0
- package/dist/internal/present.js +13 -0
- package/dist/internal/present.js.map +1 -0
- 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 +34 -64
- package/dist/internal/render-document.d.ts.map +1 -1
- package/dist/internal/render-document.js +82 -64
- 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/status-page.d.ts +68 -0
- package/dist/internal/status-page.d.ts.map +1 -0
- package/dist/internal/status-page.js +66 -0
- package/dist/internal/status-page.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 +26 -0
- package/dist/internal/web-app.d.ts.map +1 -1
- package/dist/internal/web-app.js +79 -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 +1102 -0
- package/dist/keep-alive.js.map +1 -0
- package/dist/live/bus.d.ts +70 -0
- package/dist/live/bus.d.ts.map +1 -0
- package/dist/live/bus.js +55 -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 +185 -0
- package/dist/live/hub.d.ts.map +1 -0
- package/dist/live/hub.js +544 -0
- package/dist/live/hub.js.map +1 -0
- package/dist/live/keyring.d.ts +92 -0
- package/dist/live/keyring.d.ts.map +1 -0
- package/dist/live/keyring.js +265 -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 +878 -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 +60 -0
- package/dist/live-redis.d.ts.map +1 -0
- package/dist/live-redis.js +212 -0
- package/dist/live-redis.js.map +1 -0
- package/dist/live.d.ts +150 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +972 -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 +32 -0
- package/dist/render-seam.d.ts.map +1 -1
- package/dist/render-seam.js +7 -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 +416 -129
- 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/definition.d.ts.map +1 -1
- package/dist/server-components/definition.js +2 -1
- package/dist/server-components/definition.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 +88 -29
- 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/server-components/vite.d.ts.map +1 -1
- package/dist/server-components/vite.js +26 -9
- package/dist/server-components/vite.js.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 +20 -1
- package/src/client.ts +183 -23
- 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 +20 -4
- package/src/internal/cdn/cloudflare.ts +2 -1
- package/src/internal/certified-components.ts +1 -1
- package/src/internal/codegen.ts +95 -9
- package/src/internal/dev-websocket.ts +230 -0
- package/src/internal/hole-runtime.ts +286 -0
- package/src/internal/identity-epoch.ts +239 -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 +111 -0
- package/src/internal/live-queries.ts +60 -0
- package/src/internal/page-events.ts +27 -0
- package/src/internal/page-execution.ts +605 -45
- package/src/internal/parity.ts +35 -6
- 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 +39 -0
- package/src/internal/ppr/server.ts +1766 -0
- package/src/internal/ppr/shell-store.ts +152 -0
- package/src/internal/prerendered-pages.ts +7 -4
- package/src/internal/present.ts +20 -0
- 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 +158 -113
- 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/status-page.ts +95 -0
- package/src/internal/store-signature.ts +149 -0
- package/src/internal/vite-plugin-compat.ts +2 -2
- package/src/internal/web-app.ts +123 -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 +1354 -0
- package/src/live/bus.ts +104 -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 +620 -0
- package/src/live/keyring.ts +384 -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 +1011 -0
- package/src/live-hub.ts +658 -0
- package/src/live-redis.ts +284 -0
- package/src/live.ts +1257 -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 +37 -0
- package/src/route-types.ts +3 -1
- package/src/router.ts +534 -132
- package/src/server-components/build.ts +9 -3
- package/src/server-components/client.ts +89 -5
- package/src/server-components/definition.ts +2 -1
- 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 +114 -31
- package/src/server-components/state.ts +66 -44
- package/src/server-components/types.ts +16 -0
- package/src/server-components/vite.ts +33 -14
- 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
|
@@ -139,6 +139,10 @@ export async function serverComponentsConfig(
|
|
|
139
139
|
if (env.command === "serve") build = `d_${randomBytes(16).toString("hex")}`
|
|
140
140
|
const config: UserConfig = {
|
|
141
141
|
define: {
|
|
142
|
+
...publicEnvDefines(
|
|
143
|
+
options.publicEnvPrefix ?? "PUBLIC_",
|
|
144
|
+
loadEnv(env.mode, root, options.publicEnvPrefix ?? "PUBLIC_"),
|
|
145
|
+
),
|
|
142
146
|
"process.env.NODE_ENV": JSON.stringify(
|
|
143
147
|
env.command === "build" ? "production" : "development",
|
|
144
148
|
),
|
|
@@ -234,7 +238,7 @@ export async function serverComponentsConfig(
|
|
|
234
238
|
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The shared compiler guards implement Vite hooks; the shim differs only in optional hook metadata.
|
|
235
239
|
viteAssetUrlGuard(guards) as Plugin,
|
|
236
240
|
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The shared compiler guards implement Vite hooks; the shim differs only in optional hook metadata.
|
|
237
|
-
viteZoneGuard(guards) as Plugin,
|
|
241
|
+
viteZoneGuard({ ...guards, componentReferences: true }) as Plugin,
|
|
238
242
|
...(options.plugins ?? []),
|
|
239
243
|
proof,
|
|
240
244
|
]
|
|
@@ -255,7 +259,6 @@ export async function serverComponentsConfig(
|
|
|
255
259
|
// Without it Vite inlines every `VITE_*` value from the app's .env files into client code.
|
|
256
260
|
envPrefix: vitePublicEnvPrefix(options.publicEnvPrefix),
|
|
257
261
|
define: {
|
|
258
|
-
...publicEnvDefines(options.publicEnvPrefix ?? "PUBLIC_", process.env),
|
|
259
262
|
"process.env.NIFRA_SSR_BUNDLED": '"1"',
|
|
260
263
|
"globalThis.__NIFRA_EDGE_RUNTIME__": edge ? "true" : "false",
|
|
261
264
|
__NIFRA_COMPONENT_BUILD__: JSON.stringify(build),
|
|
@@ -355,7 +358,10 @@ export async function buildServerComponents(
|
|
|
355
358
|
JSON.stringify({
|
|
356
359
|
adapter: options.adapter,
|
|
357
360
|
target: options.target ?? "bun",
|
|
358
|
-
env: publicEnvDefines(
|
|
361
|
+
env: publicEnvDefines(
|
|
362
|
+
options.publicEnvPrefix ?? "PUBLIC_",
|
|
363
|
+
loadEnv("production", root, options.publicEnvPrefix ?? "PUBLIC_"),
|
|
364
|
+
),
|
|
359
365
|
}),
|
|
360
366
|
)
|
|
361
367
|
for (const [file, hash] of [...modules].sort(([a], [b]) => codeUnitOrder(a, b)))
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { setHoleComponents } from "../internal/hole-runtime.ts"
|
|
2
|
+
import { onPageRelease } from "../internal/page-events.ts"
|
|
1
3
|
import { ServerComponentError } from "./error.ts"
|
|
2
4
|
import { readComponentFrames } from "./frames.ts"
|
|
3
5
|
import {
|
|
@@ -9,7 +11,7 @@ import {
|
|
|
9
11
|
import { registerComponentController, unregisterComponentController } from "./registry.ts"
|
|
10
12
|
import { componentResponseFailure } from "./response.ts"
|
|
11
13
|
import { BoundedComponentTasks } from "./scheduler.ts"
|
|
12
|
-
import { parseComponentState } from "./state.ts"
|
|
14
|
+
import { parseComponentRecord, parseComponentState } from "./state.ts"
|
|
13
15
|
import { readComponentBytes } from "./stream.ts"
|
|
14
16
|
import type { ComponentKey, ComponentRecord, ComponentState, ServerComponent } from "./types.ts"
|
|
15
17
|
import { componentInputKey, componentValue } from "./value.ts"
|
|
@@ -64,6 +66,8 @@ interface Mounted {
|
|
|
64
66
|
abort: AbortController
|
|
65
67
|
root?: ComponentClientRoot
|
|
66
68
|
ready: Promise<void>
|
|
69
|
+
// Cancels the release a parked instance waits on from its page.
|
|
70
|
+
unwatch?: (() => void) | undefined
|
|
67
71
|
}
|
|
68
72
|
function sameRecord(a: ComponentRecord, b: ComponentRecord): boolean {
|
|
69
73
|
return (
|
|
@@ -90,6 +94,8 @@ export class ComponentController {
|
|
|
90
94
|
}
|
|
91
95
|
readonly #options: ComponentControllerOptions
|
|
92
96
|
readonly #mounted = new Map<string, Mounted>()
|
|
97
|
+
// Set aside with a `keepAlive` page while it is hidden; another page may mount the same instances.
|
|
98
|
+
readonly #parked = new Set<Mounted>()
|
|
93
99
|
// Structural outlet anchors survive logout; their private records and native roots do not.
|
|
94
100
|
readonly #restorable = new Map<string, Element>()
|
|
95
101
|
readonly #teardown = new WeakMap<Element, Promise<void>>()
|
|
@@ -122,6 +128,8 @@ export class ComponentController {
|
|
|
122
128
|
}
|
|
123
129
|
async #mount(host: Element, instance: string, fresh: boolean): Promise<void> {
|
|
124
130
|
await this.#teardown.get(host)
|
|
131
|
+
// The page that parked it takes it back when it is shown again.
|
|
132
|
+
if (this.isParked(instance, host)) return
|
|
125
133
|
if (this.#disposed || this.#mounted.has(instance))
|
|
126
134
|
throw new ServerComponentError("component_contract")
|
|
127
135
|
const record = this.state.records.find((record) => record.instance === instance)
|
|
@@ -162,6 +170,8 @@ export class ComponentController {
|
|
|
162
170
|
try {
|
|
163
171
|
await mounted.ready
|
|
164
172
|
} catch (error) {
|
|
173
|
+
// Parked while it hydrated: it mounts again when its page is shown.
|
|
174
|
+
if (this.#parked.has(mounted)) return
|
|
165
175
|
if (this.#mounted.get(instance) === mounted) await this.unmount(instance)
|
|
166
176
|
throw error
|
|
167
177
|
}
|
|
@@ -439,6 +449,64 @@ export class ComponentController {
|
|
|
439
449
|
mounted.generation++
|
|
440
450
|
}
|
|
441
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Set aside the components inside `page`, a `keepAlive` page the router hides: their pending work is
|
|
454
|
+
* cancelled, and navigation neither updates nor unmounts them until `unpark` takes them back.
|
|
455
|
+
* `bindComponentRouter` calls it on the page events.
|
|
456
|
+
*/
|
|
457
|
+
park(page: Node): void {
|
|
458
|
+
for (const mounted of this.#mounted.values())
|
|
459
|
+
if (page.contains(mounted.host)) {
|
|
460
|
+
this.#mounted.delete(mounted.record.instance)
|
|
461
|
+
this.#parked.add(mounted)
|
|
462
|
+
mounted.unwatch = onPageRelease(page, () => {
|
|
463
|
+
void this.#release(mounted).catch(() => this.reportError("component_failed"))
|
|
464
|
+
})
|
|
465
|
+
mounted.abort.abort()
|
|
466
|
+
mounted.abort = new AbortController()
|
|
467
|
+
mounted.generation++
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/** Take back the components `park` set aside inside `page`, which is shown again. */
|
|
471
|
+
unpark(page: Node): void {
|
|
472
|
+
for (const mounted of this.#parked) {
|
|
473
|
+
if (!page.contains(mounted.host)) continue
|
|
474
|
+
const id = mounted.record.instance
|
|
475
|
+
if (this.#mounted.has(id) || this.#disposed) {
|
|
476
|
+
void this.#release(mounted)
|
|
477
|
+
continue
|
|
478
|
+
}
|
|
479
|
+
this.#parked.delete(mounted)
|
|
480
|
+
mounted.unwatch?.()
|
|
481
|
+
if (mounted.root !== undefined) this.#mounted.set(id, mounted)
|
|
482
|
+
// Hidden while it hydrated, which `park` cancelled: mount it again.
|
|
483
|
+
else
|
|
484
|
+
void this.#mount(mounted.host, id, mounted.host.childNodes.length === 0).catch(() =>
|
|
485
|
+
this.reportError("component_failed"),
|
|
486
|
+
)
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Whether `instance` at `host` is parked with its hidden page. An outlet whose framework cleans up a
|
|
491
|
+
* hidden page's effects (React's `<Activity>`) leaves it parked: the page takes it back when shown
|
|
492
|
+
* again, and releases it when dropped.
|
|
493
|
+
*/
|
|
494
|
+
isParked(instance: string, host: Element): boolean {
|
|
495
|
+
for (const mounted of this.#parked)
|
|
496
|
+
if (mounted.host === host && mounted.record.instance === instance) return true
|
|
497
|
+
return false
|
|
498
|
+
}
|
|
499
|
+
async #release(mounted: Mounted): Promise<void> {
|
|
500
|
+
if (!this.#parked.delete(mounted)) return
|
|
501
|
+
mounted.unwatch?.()
|
|
502
|
+
for (const child of this.#parked)
|
|
503
|
+
if (mounted.host.contains(child.host)) await this.#release(child)
|
|
504
|
+
mounted.abort.abort()
|
|
505
|
+
await mounted.ready.catch(() => {
|
|
506
|
+
/* A parked hydration was cancelled; nothing of it can commit. */
|
|
507
|
+
})
|
|
508
|
+
await mounted.root?.dispose()
|
|
509
|
+
}
|
|
442
510
|
async adopt(state: ComponentState, ownerUrl?: string): Promise<void> {
|
|
443
511
|
if (ownerUrl !== undefined) this.#ownerUrl = ownerUrl
|
|
444
512
|
if (this.#disposed || state === this.#source) return
|
|
@@ -568,7 +636,11 @@ export class ComponentController {
|
|
|
568
636
|
}
|
|
569
637
|
}
|
|
570
638
|
}
|
|
571
|
-
|
|
639
|
+
/** Unmount `instance`; an outlet passes its `host`, which tells a parked page's instance apart. */
|
|
640
|
+
async unmount(instance: string, host?: Element): Promise<void> {
|
|
641
|
+
for (const mounted of this.#parked)
|
|
642
|
+
if (mounted.host === host && mounted.record.instance === instance)
|
|
643
|
+
return this.#release(mounted)
|
|
572
644
|
this.#restorable.delete(instance)
|
|
573
645
|
await this.#unmount(instance)
|
|
574
646
|
}
|
|
@@ -594,7 +666,7 @@ export class ComponentController {
|
|
|
594
666
|
if (this.#teardown.get(mounted.host) === teardown) this.#teardown.delete(mounted.host)
|
|
595
667
|
}
|
|
596
668
|
}
|
|
597
|
-
async #clearScope(preserveOutlets = false): Promise<void> {
|
|
669
|
+
async #clearScope(preserveOutlets = false, keepParked = false): Promise<void> {
|
|
598
670
|
this.#authEpoch++
|
|
599
671
|
if (preserveOutlets) {
|
|
600
672
|
for (const mounted of this.#mounted.values())
|
|
@@ -603,7 +675,10 @@ export class ComponentController {
|
|
|
603
675
|
} else this.#restorable.clear()
|
|
604
676
|
this.#state = { ...this.#state, records: [] }
|
|
605
677
|
this.#source = undefined
|
|
606
|
-
await Promise.all([
|
|
678
|
+
await Promise.all([
|
|
679
|
+
...[...this.#mounted.keys()].map((instance) => this.#unmount(instance)),
|
|
680
|
+
...(keepParked ? [] : [...this.#parked].map((mounted) => this.#release(mounted))),
|
|
681
|
+
])
|
|
607
682
|
}
|
|
608
683
|
async invalidateScope(): Promise<void> {
|
|
609
684
|
this.#adoption++
|
|
@@ -611,7 +686,7 @@ export class ComponentController {
|
|
|
611
686
|
}
|
|
612
687
|
async navigate(): Promise<void> {
|
|
613
688
|
this.cancelPending()
|
|
614
|
-
await this.#clearScope()
|
|
689
|
+
await this.#clearScope(false, !this.#disposed)
|
|
615
690
|
}
|
|
616
691
|
async dispose(): Promise<void> {
|
|
617
692
|
this.#disposed = true
|
|
@@ -651,6 +726,15 @@ export function resolveComponentRecord<Input>(
|
|
|
651
726
|
return matches[0]!
|
|
652
727
|
}
|
|
653
728
|
|
|
729
|
+
/**
|
|
730
|
+
* Experimental: how the client islands inside partially prerendered holes that show server
|
|
731
|
+
* components mount, as the adapter's component client, such as `vueComponentClient({ islands })`;
|
|
732
|
+
* `undefined` forgets it. Call it in the client entry before the router mounts. Without one such a
|
|
733
|
+
* hole shows its error slot.
|
|
734
|
+
*/
|
|
735
|
+
export function holeComponents(client: ComponentClientAdapter | undefined): void {
|
|
736
|
+
setHoleComponents(client === undefined ? undefined : { client, record: parseComponentRecord })
|
|
737
|
+
}
|
|
654
738
|
export { invalidateComponentScope } from "./registry.ts"
|
|
655
739
|
export { bindComponentRouter, refreshAfterComponentMutation } from "./router-integration.ts"
|
|
656
740
|
export { parseComponentState } from "./state.ts"
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { narrowCapabilityContext } from "@nifrajs/core/capabilities"
|
|
2
2
|
import { validateStandard } from "@nifrajs/core/schema"
|
|
3
3
|
import { outputGuard, runGuard } from "../internal/output-guard.ts"
|
|
4
|
-
import { isControlFlow
|
|
4
|
+
import { isControlFlow } from "../internal/render-document.ts"
|
|
5
|
+
import { isStatusSignal } from "../internal/status-page.ts"
|
|
5
6
|
import { ServerComponentError } from "./error.ts"
|
|
6
7
|
import { assertComponentSchema, componentValue } from "./schema.ts"
|
|
7
8
|
import type {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Experimental: a partially prerendered hole whose content is a server component. The hole's loader
|
|
3
|
+
* returns the component's input; the server runs the component for each visitor and sends its HTML,
|
|
4
|
+
* and the browser mounts only the client islands inside it. Under the coordinated compiler a browser
|
|
5
|
+
* import of a `*.component.server` module is an opaque handle, so the component never ships.
|
|
6
|
+
*/
|
|
7
|
+
import { SERVER_HOLE } from "../internal/ppr/contract.ts"
|
|
8
|
+
import type { ServerComponent } from "./types.ts"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A `dynamic` hole's `render`: the hole shows `component`, given the hole loader's output as its
|
|
12
|
+
* input. The page's app lists the component under `createServerComponents({ holes })` for the route.
|
|
13
|
+
* The hole cannot be `interactive`; its client islands are.
|
|
14
|
+
*/
|
|
15
|
+
export function serverHole<Input, Props, View>(
|
|
16
|
+
component: ServerComponent<Input, Props, View>,
|
|
17
|
+
): (data: Input) => never {
|
|
18
|
+
const render = (): never => {
|
|
19
|
+
throw new Error("[nifra/web] a server component hole renders on the server only")
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(render, SERVER_HOLE, { value: component })
|
|
22
|
+
return Object.freeze(render)
|
|
23
|
+
}
|
|
@@ -35,10 +35,59 @@ const urls = new Set([
|
|
|
35
35
|
"data",
|
|
36
36
|
"codebase",
|
|
37
37
|
])
|
|
38
|
+
const IMAGE_HINT = new Set([
|
|
39
|
+
"rel",
|
|
40
|
+
"as",
|
|
41
|
+
"href",
|
|
42
|
+
"imagesrcset",
|
|
43
|
+
"imagesizes",
|
|
44
|
+
"crossorigin",
|
|
45
|
+
"fetchpriority",
|
|
46
|
+
"referrerpolicy",
|
|
47
|
+
])
|
|
38
48
|
const fail = (): never => {
|
|
39
49
|
throw new ServerComponentError("component_protocol", 400)
|
|
40
50
|
}
|
|
41
51
|
|
|
52
|
+
const unsafeUrl = (value: string): boolean =>
|
|
53
|
+
/^(?:javascript|vbscript|data):/.test(
|
|
54
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: Browsers discard these controls when interpreting URL schemes.
|
|
55
|
+
value.replace(/[\u0000-\u0020\u007f]/g, "").toLowerCase(),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
const isSpace = (char: string): boolean =>
|
|
59
|
+
char === " " || char === "\t" || char === "\n" || char === "\f" || char === "\r"
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The image candidate URLs of a `srcset`, split as the HTML standard's "parse a srcset attribute"
|
|
63
|
+
* splits them, so each is the URL a browser would fetch: a URL is a run without whitespace, and its
|
|
64
|
+
* descriptors end at the first comma outside parentheses.
|
|
65
|
+
*/
|
|
66
|
+
function srcsetUrls(value: string): string[] {
|
|
67
|
+
const found: string[] = []
|
|
68
|
+
let at = 0
|
|
69
|
+
while (at < value.length) {
|
|
70
|
+
while (at < value.length && (isSpace(value[at] ?? "") || value[at] === ",")) at++
|
|
71
|
+
const start = at
|
|
72
|
+
while (at < value.length && !isSpace(value[at] ?? "")) at++
|
|
73
|
+
const url = value.slice(start, at)
|
|
74
|
+
if (url === "") break
|
|
75
|
+
if (url.endsWith(",")) {
|
|
76
|
+
found.push(url.replace(/,+$/, ""))
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
found.push(url)
|
|
80
|
+
let inParens = false
|
|
81
|
+
for (; at < value.length; at++) {
|
|
82
|
+
const char = value[at]
|
|
83
|
+
if (char === "(") inParens = true
|
|
84
|
+
else if (char === ")") inParens = false
|
|
85
|
+
else if (char === "," && !inParens) break
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return found
|
|
89
|
+
}
|
|
90
|
+
|
|
42
91
|
function css(value: string): string {
|
|
43
92
|
return (
|
|
44
93
|
value
|
|
@@ -66,6 +115,17 @@ export function validateComponentTag(
|
|
|
66
115
|
const attrs = new Map(
|
|
67
116
|
attributes.map((attribute) => [attribute.name.toLowerCase(), attribute.value]),
|
|
68
117
|
)
|
|
118
|
+
// React writes an image's preload hint ahead of each `<img>` it renders without a document. Like
|
|
119
|
+
// every hint here it is cut from the range, so it only has to be a well-formed image hint.
|
|
120
|
+
if (attrs.get("rel") === "preload" && attrs.get("as") === "image") {
|
|
121
|
+
if (
|
|
122
|
+
[...attrs.keys()].some((name) => !IMAGE_HINT.has(name)) ||
|
|
123
|
+
unsafeUrl(attrs.get("href") ?? "") ||
|
|
124
|
+
srcsetUrls(attrs.get("imagesrcset") ?? "").some(unsafeUrl)
|
|
125
|
+
)
|
|
126
|
+
fail()
|
|
127
|
+
return
|
|
128
|
+
}
|
|
69
129
|
if (
|
|
70
130
|
!["preload", "modulepreload"].includes(attrs.get("rel") ?? "") ||
|
|
71
131
|
!assets.has(attrs.get("href") ?? "") ||
|
|
@@ -79,10 +139,9 @@ export function validateComponentTag(
|
|
|
79
139
|
if (forbidden.has(tag.toLowerCase())) fail()
|
|
80
140
|
for (const attribute of attributes) {
|
|
81
141
|
const name = attribute.name.toLowerCase()
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (name
|
|
85
|
-
if (urls.has(name) && /^(?:javascript|vbscript|data):/.test(value)) fail()
|
|
142
|
+
if (name.startsWith("on") || ["srcdoc", "is", "ping"].includes(name)) fail()
|
|
143
|
+
if (urls.has(name) && unsafeUrl(attribute.value)) fail()
|
|
144
|
+
if (name === "srcset" && srcsetUrls(attribute.value).some(unsafeUrl)) fail()
|
|
86
145
|
if (
|
|
87
146
|
name === "style" &&
|
|
88
147
|
/expression\(|@import|(?:javascript|vbscript|data):|-moz-binding|behavior:/.test(
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A linear scan of the HTML a component renderer writes, for the validator's common case. It reads
|
|
3
|
+
* only a subset that parse5's tokenizer reads without a parse error and with the same start tags:
|
|
4
|
+
* ASCII tag and attribute names, double-quoted values, a few character references and plain
|
|
5
|
+
* comments. Anything else returns `undefined`, and the tokenizer decides. A differential test holds
|
|
6
|
+
* the two to the same tags (server-components-html-scan.test.ts).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface ScannedTag {
|
|
10
|
+
readonly tagName: string
|
|
11
|
+
readonly attrs: { readonly name: string; readonly value: string }[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const REFERENCES: ReadonlyArray<readonly [string, string]> = [
|
|
15
|
+
["&", "&"],
|
|
16
|
+
["<", "<"],
|
|
17
|
+
[">", ">"],
|
|
18
|
+
[""", '"'],
|
|
19
|
+
["'", "'"],
|
|
20
|
+
["'", "'"],
|
|
21
|
+
[" ", "\u00a0"],
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
const referenceAt = (html: string, at: number): readonly [string, string] | undefined => {
|
|
25
|
+
for (const reference of REFERENCES) if (html.startsWith(reference[0], at)) return reference
|
|
26
|
+
return undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const isSpace = (code: number): boolean =>
|
|
30
|
+
code === 0x20 || code === 0x0a || code === 0x09 || code === 0x0c || code === 0x0d
|
|
31
|
+
const isLetter = (code: number): boolean =>
|
|
32
|
+
(code >= 0x61 && code <= 0x7a) || (code >= 0x41 && code <= 0x5a)
|
|
33
|
+
const isTagCode = (code: number): boolean =>
|
|
34
|
+
isLetter(code) || (code >= 0x30 && code <= 0x39) || code === 0x2d
|
|
35
|
+
const isAttributeStart = (code: number): boolean => isLetter(code) || code === 0x5f || code === 0x3a
|
|
36
|
+
const isAttributeCode = (code: number): boolean =>
|
|
37
|
+
isAttributeStart(code) || (code >= 0x30 && code <= 0x39) || code === 0x2d || code === 0x2e
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The code units of the character at `at`, or 0 when parse5 reports it (NULL, a control, a
|
|
41
|
+
* noncharacter, a lone surrogate) or past the end.
|
|
42
|
+
*/
|
|
43
|
+
function unitsAt(html: string, at: number): number {
|
|
44
|
+
const code = html.charCodeAt(at)
|
|
45
|
+
if (code >= 0x20 && code < 0x7f) return 1
|
|
46
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
47
|
+
const low = html.charCodeAt(at + 1)
|
|
48
|
+
if (!(low >= 0xdc00 && low <= 0xdfff)) return 0
|
|
49
|
+
const point = ((code - 0xd800) << 10) + (low - 0xdc00) + 0x10000
|
|
50
|
+
return (point & 0xfffe) === 0xfffe ? 0 : 2
|
|
51
|
+
}
|
|
52
|
+
if (Number.isNaN(code) || (code >= 0xdc00 && code <= 0xdfff)) return 0
|
|
53
|
+
if (code < 0x20) return isSpace(code) ? 1 : 0
|
|
54
|
+
if (code <= 0x9f) return 0
|
|
55
|
+
return (code >= 0xfdd0 && code <= 0xfdef) || code === 0xfffe || code === 0xffff ? 0 : 1
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Where a start tag whose name begins at `at` ends, with `tags` given its tag, or -1. */
|
|
59
|
+
function scanStartTag(html: string, at: number, tags: ScannedTag[]): number {
|
|
60
|
+
let end = at
|
|
61
|
+
while (isTagCode(html.charCodeAt(end))) end++
|
|
62
|
+
const tagName = html.slice(at, end).toLowerCase()
|
|
63
|
+
// The validator reads these two as a browser does, which needs the text after them.
|
|
64
|
+
if (tagName === "title" || tagName === "textarea") return -1
|
|
65
|
+
let code = html.charCodeAt(end)
|
|
66
|
+
if (!(isSpace(code) || code === 0x2f || code === 0x3e)) return -1
|
|
67
|
+
const attrs: { name: string; value: string }[] = []
|
|
68
|
+
for (;;) {
|
|
69
|
+
while (isSpace(html.charCodeAt(end))) end++
|
|
70
|
+
code = html.charCodeAt(end)
|
|
71
|
+
if (code === 0x3e || (code === 0x2f && html.charCodeAt(end + 1) === 0x3e)) {
|
|
72
|
+
tags.push({ tagName, attrs })
|
|
73
|
+
return end + (code === 0x3e ? 1 : 2)
|
|
74
|
+
}
|
|
75
|
+
if (!isAttributeStart(code)) return -1
|
|
76
|
+
const nameStart = end
|
|
77
|
+
while (isAttributeCode(html.charCodeAt(end))) end++
|
|
78
|
+
const name = html.slice(nameStart, end).toLowerCase()
|
|
79
|
+
if (attrs.some((attr) => attr.name === name)) return -1
|
|
80
|
+
code = html.charCodeAt(end)
|
|
81
|
+
let value = ""
|
|
82
|
+
if (code === 0x3d) {
|
|
83
|
+
if (html.charCodeAt(end + 1) !== 0x22) return -1
|
|
84
|
+
let from = end + 2
|
|
85
|
+
end = from
|
|
86
|
+
for (;;) {
|
|
87
|
+
code = html.charCodeAt(end)
|
|
88
|
+
if (code === 0x22) break
|
|
89
|
+
if (code === 0x26) {
|
|
90
|
+
const reference = referenceAt(html, end)
|
|
91
|
+
if (reference === undefined) return -1
|
|
92
|
+
value += html.slice(from, end) + reference[1]
|
|
93
|
+
end += reference[0].length
|
|
94
|
+
from = end
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
// A CR would reach the tokenizer's value as LF.
|
|
98
|
+
const units = code === 0x0d ? 0 : unitsAt(html, end)
|
|
99
|
+
if (units === 0) return -1
|
|
100
|
+
end += units
|
|
101
|
+
}
|
|
102
|
+
value += html.slice(from, end)
|
|
103
|
+
end++
|
|
104
|
+
code = html.charCodeAt(end)
|
|
105
|
+
if (!(isSpace(code) || code === 0x2f || code === 0x3e)) return -1
|
|
106
|
+
} else if (!(isSpace(code) || code === 0x2f || code === 0x3e)) return -1
|
|
107
|
+
attrs.push({ name, value })
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The start tags of `html`, or `undefined` when it leaves the subset this scan reads. */
|
|
112
|
+
export function scanComponentHTML(html: string): ScannedTag[] | undefined {
|
|
113
|
+
const tags: ScannedTag[] = []
|
|
114
|
+
let at = 0
|
|
115
|
+
while (at < html.length) {
|
|
116
|
+
const code = html.charCodeAt(at)
|
|
117
|
+
if (code === 0x3c) {
|
|
118
|
+
const next = html.charCodeAt(at + 1)
|
|
119
|
+
if (isLetter(next)) {
|
|
120
|
+
at = scanStartTag(html, at + 1, tags)
|
|
121
|
+
if (at === -1) return undefined
|
|
122
|
+
} else if (next === 0x2f) {
|
|
123
|
+
let end = at + 2
|
|
124
|
+
if (!isLetter(html.charCodeAt(end))) return undefined
|
|
125
|
+
while (isTagCode(html.charCodeAt(end))) end++
|
|
126
|
+
while (isSpace(html.charCodeAt(end))) end++
|
|
127
|
+
if (html.charCodeAt(end) !== 0x3e) return undefined
|
|
128
|
+
at = end + 1
|
|
129
|
+
} else if (html.startsWith("!--", at + 1)) {
|
|
130
|
+
const from = at + 4
|
|
131
|
+
if (html.charCodeAt(from) === 0x3e || html.startsWith("->", from)) return undefined
|
|
132
|
+
const close = html.indexOf("-->", from)
|
|
133
|
+
if (close === -1) return undefined
|
|
134
|
+
for (let inner = from; inner < close; ) {
|
|
135
|
+
const units = unitsAt(html, inner)
|
|
136
|
+
if (units === 0) return undefined
|
|
137
|
+
// `<!` can open a nested comment and `--!` a `--!>` ending; both are parse errors.
|
|
138
|
+
if (
|
|
139
|
+
html.charCodeAt(inner) === 0x21 &&
|
|
140
|
+
((inner > from && html.charCodeAt(inner - 1) === 0x3c) ||
|
|
141
|
+
(inner - 2 >= from && html.startsWith("--", inner - 2)))
|
|
142
|
+
)
|
|
143
|
+
return undefined
|
|
144
|
+
inner += units
|
|
145
|
+
}
|
|
146
|
+
at = close + 3
|
|
147
|
+
} else return undefined
|
|
148
|
+
} else if (code === 0x26) {
|
|
149
|
+
const reference = referenceAt(html, at)
|
|
150
|
+
if (reference === undefined) return undefined
|
|
151
|
+
at += reference[0].length
|
|
152
|
+
} else {
|
|
153
|
+
const units = unitsAt(html, at)
|
|
154
|
+
if (units === 0) return undefined
|
|
155
|
+
at += units
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return tags
|
|
159
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Tokenizer } from "parse5"
|
|
2
2
|
import { ServerComponentError } from "./error.ts"
|
|
3
3
|
import { validateComponentTag } from "./html-policy.ts"
|
|
4
|
+
import { scanComponentHTML } from "./html-scan.ts"
|
|
4
5
|
import { COMPONENT_LIMITS } from "./protocol.ts"
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -19,16 +20,26 @@ function closesAsText(html: string, tag: string, from: number): boolean {
|
|
|
19
20
|
)
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
function assertOutputSize(html: string): void {
|
|
24
|
+
// UTF-8 takes at most three bytes per UTF-16 unit, so only a long string is encoded to measure it.
|
|
25
|
+
if (
|
|
26
|
+
html.length * 3 > COMPONENT_LIMITS.outputBytes &&
|
|
27
|
+
new TextEncoder().encode(html).length > COMPONENT_LIMITS.outputBytes
|
|
28
|
+
)
|
|
29
|
+
throw new ServerComponentError("component_limit", 413)
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
/** Tokenization validates attributes even in SVG/table contexts a document parser may discard. */
|
|
23
33
|
function tokenize(
|
|
24
34
|
html: string,
|
|
25
35
|
assets?: ReadonlySet<string>,
|
|
26
36
|
hints?: Array<{ start: number; end: number }>,
|
|
27
37
|
): void {
|
|
28
|
-
|
|
29
|
-
throw new ServerComponentError("component_limit", 413)
|
|
38
|
+
assertOutputSize(html)
|
|
30
39
|
const tokenizer = new Tokenizer(
|
|
31
|
-
|
|
40
|
+
// Offsets are read only for hints and for `title`/`textarea`, whose names a tokenizer lowercases
|
|
41
|
+
// from ASCII alone; tracking them costs every other string about a tenth of its check.
|
|
42
|
+
{ sourceCodeLocationInfo: hints !== undefined || /<(?:title|textarea)/i.test(html) },
|
|
32
43
|
{
|
|
33
44
|
onStartTag(token) {
|
|
34
45
|
validateComponentTag(token.tagName, token.attrs, assets)
|
|
@@ -63,7 +74,10 @@ function tokenize(
|
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
export function validateComponentHTML(html: string, assets?: ReadonlySet<string>): void {
|
|
66
|
-
|
|
77
|
+
assertOutputSize(html)
|
|
78
|
+
const tags = scanComponentHTML(html)
|
|
79
|
+
if (tags === undefined) tokenize(html, assets)
|
|
80
|
+
else for (const tag of tags) validateComponentTag(tag.tagName, tag.attrs, assets)
|
|
67
81
|
}
|
|
68
82
|
|
|
69
83
|
/** React may produce resource hints while decoding Flight. They are not owned range nodes. */
|
|
@@ -1,13 +1,27 @@
|
|
|
1
|
+
import { PAGE_HIDE_EVENT, PAGE_SHOW_EVENT } from "../internal/page-events.ts"
|
|
1
2
|
import type { ClientRouter } from "../router.ts"
|
|
2
3
|
import type { ComponentController } from "./client.ts"
|
|
3
4
|
import { componentControllers } from "./registry.ts"
|
|
4
5
|
|
|
5
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Optional router integration; imported only by opted-in application entries. The components of a
|
|
8
|
+
* `keepAlive` page are parked while it is hidden, so navigation leaves them as they are.
|
|
9
|
+
*/
|
|
6
10
|
export function bindComponentRouter(
|
|
7
11
|
router: Pick<ClientRouter, "snapshot" | "subscribe">,
|
|
8
12
|
): () => void {
|
|
9
13
|
let previous = router.snapshot()
|
|
10
|
-
|
|
14
|
+
const onPage = (event: Event): void => {
|
|
15
|
+
const page = event.target
|
|
16
|
+
if (page instanceof Node)
|
|
17
|
+
for (const controller of componentControllers)
|
|
18
|
+
if (event.type === PAGE_HIDE_EVENT) controller.park(page)
|
|
19
|
+
else controller.unpark(page)
|
|
20
|
+
}
|
|
21
|
+
const target = typeof document === "undefined" ? undefined : document
|
|
22
|
+
target?.addEventListener(PAGE_HIDE_EVENT, onPage)
|
|
23
|
+
target?.addEventListener(PAGE_SHOW_EVENT, onPage)
|
|
24
|
+
const stop = router.subscribe(() => {
|
|
11
25
|
const next = router.snapshot()
|
|
12
26
|
if (next.pending && next.pendingPath !== previous.pendingPath)
|
|
13
27
|
for (const controller of componentControllers) controller.cancelPending()
|
|
@@ -25,6 +39,11 @@ export function bindComponentRouter(
|
|
|
25
39
|
}
|
|
26
40
|
previous = next
|
|
27
41
|
})
|
|
42
|
+
return () => {
|
|
43
|
+
stop()
|
|
44
|
+
target?.removeEventListener(PAGE_HIDE_EVENT, onPage)
|
|
45
|
+
target?.removeEventListener(PAGE_SHOW_EVENT, onPage)
|
|
46
|
+
}
|
|
28
47
|
}
|
|
29
48
|
|
|
30
49
|
/** Refresh only after the existing mutation transport reports success. */
|