@nifrajs/web 5.5.1 → 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 +154 -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,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nifrajs/web/islands/swap - refresh one region of a server-rendered page from a plain link or form,
|
|
3
|
+
* without a full reload and without a client router.
|
|
4
|
+
*
|
|
5
|
+
* <form action="/hotels" data-swap="replace:results"><input name="q"></form>
|
|
6
|
+
* <ul data-swap-slot="results">...</ul>
|
|
7
|
+
* <a href="/hotels?page=2" data-swap="append:results">More</a>
|
|
8
|
+
*
|
|
9
|
+
* The request is the one the link or form makes anyway, plus an `x-nifra-swap: <slot>` header. The
|
|
10
|
+
* server runs the page as usual and answers with that slot's HTML alone (`slot()` in
|
|
11
|
+
* `@nifrajs/web-vanilla`), so the fragment is a slice of the real page. Any other answer - a redirect,
|
|
12
|
+
* another origin, a page without that slot - loads the URL normally. Only GET requests are enhanced:
|
|
13
|
+
* a POST form submits natively, so the browser applies its response's security policy and scripts,
|
|
14
|
+
* and the form is never sent twice.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { mountIsland } from "./internal/island-mount.ts"
|
|
18
|
+
import type { IslandEnhancer } from "./islands.ts"
|
|
19
|
+
import { SWAP_HEADER } from "./render-seam.ts"
|
|
20
|
+
|
|
21
|
+
/** Where `enableSwap` looks for links, forms, slots and islands. */
|
|
22
|
+
export interface SwapOptions {
|
|
23
|
+
/** Default `document`. */
|
|
24
|
+
readonly root?: Document | Element
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const MODE = /^(replace|append):([A-Za-z][\w-]{0,63})$/
|
|
28
|
+
const SLOTS = "[data-swap-slot]:not([data-island-ignore],[data-island-ignore] *)"
|
|
29
|
+
const HISTORY_KEY = "nifraSwap"
|
|
30
|
+
|
|
31
|
+
// A control named like a DOM method shadows it on its form (`<input name="closest">`).
|
|
32
|
+
const attr = (el: Element, name: string): string | null =>
|
|
33
|
+
Element.prototype.getAttribute.call(el, name)
|
|
34
|
+
|
|
35
|
+
const here = (): string => location.pathname + location.search
|
|
36
|
+
|
|
37
|
+
/** The slot names a history entry's swaps changed, from the first one in this tab up to it. */
|
|
38
|
+
const swappedIn = (state: unknown): string[] => {
|
|
39
|
+
const names: unknown =
|
|
40
|
+
typeof state === "object" && state !== null ? Reflect.get(state, HISTORY_KEY) : undefined
|
|
41
|
+
return Array.isArray(names) ? names.filter((name) => typeof name === "string") : []
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Mount the page's islands, as `mountIslands` does, and make every `data-swap` link and GET form under
|
|
46
|
+
* `root` refresh its slot in place. Call it instead of `mountIslands`.
|
|
47
|
+
*
|
|
48
|
+
* - `data-swap="replace:<slot>"` replaces the content of the `data-swap-slot="<slot>"` element;
|
|
49
|
+
* `append:<slot>` adds to it, and a link or form inside that slot is spent: the slot's child holding
|
|
50
|
+
* it is removed when the next content arrives, so a "more" link at the end of a list gives way to
|
|
51
|
+
* the next page and its own "more" link.
|
|
52
|
+
* - A replacing GET pushes its URL, so a filtered list survives reload and can be shared; back and
|
|
53
|
+
* forward fetch the slots again for the URL they land on.
|
|
54
|
+
* - The islands inside a slot mount with it and unmount when it is replaced.
|
|
55
|
+
* - While a slot loads it carries `aria-busy="true"`; a newer swap of the same slot cancels the
|
|
56
|
+
* older one. When the request fails, the page stays as it is and the link or form gets
|
|
57
|
+
* `data-swap-error`.
|
|
58
|
+
*
|
|
59
|
+
* Nothing inside a `data-island-ignore` element swaps. Returns a disposer that removes the listeners,
|
|
60
|
+
* cancels pending swaps and unmounts every island.
|
|
61
|
+
*/
|
|
62
|
+
export function enableSwap(
|
|
63
|
+
// `never` admits an enhancer of any props type, as in `mountIslands`.
|
|
64
|
+
enhancers: Readonly<Record<string, IslandEnhancer<never>>>,
|
|
65
|
+
options: SwapOptions = {},
|
|
66
|
+
): () => void {
|
|
67
|
+
const root: ParentNode & Node = options.root ?? document
|
|
68
|
+
const stops = new Map<Node, () => void>()
|
|
69
|
+
const pending = new Map<Element, AbortController>()
|
|
70
|
+
let swapped = swappedIn(history.state)
|
|
71
|
+
let shown = here()
|
|
72
|
+
|
|
73
|
+
const islands = "[data-island]:not([data-island-ignore],[data-island-ignore] *)"
|
|
74
|
+
const attach = (scope: ParentNode & Node): void => {
|
|
75
|
+
const mount = (el: HTMLElement): void => {
|
|
76
|
+
const stop = mountIsland(el, enhancers)
|
|
77
|
+
if (stop !== undefined) stops.set(el, stop)
|
|
78
|
+
}
|
|
79
|
+
if (scope instanceof HTMLElement && scope.matches(islands)) mount(scope)
|
|
80
|
+
for (const el of scope.querySelectorAll<HTMLElement>(islands)) if (scope.contains(el)) mount(el)
|
|
81
|
+
}
|
|
82
|
+
const release = (scope: Node): void => {
|
|
83
|
+
const cleanups: Array<() => void> = []
|
|
84
|
+
for (const [node, stop] of stops)
|
|
85
|
+
if (scope.contains(node)) {
|
|
86
|
+
stops.delete(node)
|
|
87
|
+
cleanups.push(stop)
|
|
88
|
+
}
|
|
89
|
+
// Cleanup may itself detach descendants, so collect every owner before stopping any of them.
|
|
90
|
+
for (const stop of cleanups) stop()
|
|
91
|
+
}
|
|
92
|
+
// The server answers with the first slot of a name in the page, so the client takes the first too.
|
|
93
|
+
const slotNamed = (name: string): Element | undefined => {
|
|
94
|
+
for (const slot of root.querySelectorAll(SLOTS))
|
|
95
|
+
if (slot.getAttribute("data-swap-slot") === name) return slot
|
|
96
|
+
return undefined
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const put = (slot: Element, html: string, append: boolean, source: Element | undefined): void => {
|
|
100
|
+
const template = document.createElement("template")
|
|
101
|
+
// Parsed inert: scripts in it never run, as for any HTML set this way.
|
|
102
|
+
template.innerHTML = html
|
|
103
|
+
const focused = slot.contains(document.activeElement)
|
|
104
|
+
if (append) {
|
|
105
|
+
for (const child of [...slot.children])
|
|
106
|
+
if (child === source || (source !== undefined && child.contains(source))) {
|
|
107
|
+
release(child)
|
|
108
|
+
slot.removeChild(child)
|
|
109
|
+
}
|
|
110
|
+
slot.append(template.content)
|
|
111
|
+
} else {
|
|
112
|
+
release(slot)
|
|
113
|
+
slot.replaceChildren(template.content)
|
|
114
|
+
}
|
|
115
|
+
attach(slot)
|
|
116
|
+
if (focused && !slot.contains(document.activeElement) && slot instanceof HTMLElement) {
|
|
117
|
+
if (!slot.hasAttribute("tabindex")) slot.tabIndex = -1
|
|
118
|
+
slot.focus({ preventScroll: true })
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const swap = async (
|
|
123
|
+
slot: Element,
|
|
124
|
+
name: string,
|
|
125
|
+
append: boolean,
|
|
126
|
+
url: URL,
|
|
127
|
+
source?: Element,
|
|
128
|
+
): Promise<void> => {
|
|
129
|
+
pending.get(slot)?.abort()
|
|
130
|
+
const abort = new AbortController()
|
|
131
|
+
pending.set(slot, abort)
|
|
132
|
+
slot.setAttribute("aria-busy", "true")
|
|
133
|
+
try {
|
|
134
|
+
const response = await fetch(url, {
|
|
135
|
+
cache: "no-store",
|
|
136
|
+
headers: { [SWAP_HEADER]: name },
|
|
137
|
+
signal: abort.signal,
|
|
138
|
+
redirect: "manual",
|
|
139
|
+
})
|
|
140
|
+
const load = (): void => {
|
|
141
|
+
if (source === undefined) location.reload()
|
|
142
|
+
else location.assign(response.redirected ? response.url : url)
|
|
143
|
+
}
|
|
144
|
+
if (
|
|
145
|
+
response.type !== "basic" ||
|
|
146
|
+
!response.headers.get("content-type")?.startsWith("text/html")
|
|
147
|
+
) {
|
|
148
|
+
load()
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
// A slot answer joins this document, so this document's CSP governs it, whatever headers the
|
|
152
|
+
// fetch carried; anything that is not one loads as its own document under its own policy.
|
|
153
|
+
if (response.headers.get(SWAP_HEADER) !== name) {
|
|
154
|
+
load()
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
const html = await response.text()
|
|
158
|
+
if (!slot.isConnected) return
|
|
159
|
+
try {
|
|
160
|
+
put(slot, html, append, source)
|
|
161
|
+
} catch {
|
|
162
|
+
// Trusted Types without a default policy refuses the HTML before the slot changes.
|
|
163
|
+
load()
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
if (!append && source !== undefined) {
|
|
167
|
+
if (!swapped.includes(name)) swapped = [...swapped, name]
|
|
168
|
+
history.pushState({ [HISTORY_KEY]: swapped }, "", url)
|
|
169
|
+
shown = url.pathname + url.search
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
if (abort.signal.aborted) return
|
|
173
|
+
// Back or forward left the URL ahead of the page: show what the URL shows.
|
|
174
|
+
if (source === undefined) location.reload()
|
|
175
|
+
else Element.prototype.setAttribute.call(source, "data-swap-error", "")
|
|
176
|
+
} finally {
|
|
177
|
+
if (pending.get(slot) === abort) {
|
|
178
|
+
pending.delete(slot)
|
|
179
|
+
slot.removeAttribute("aria-busy")
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const start = (event: Event, source: Element, url: URL): void => {
|
|
185
|
+
const mode = MODE.exec(attr(source, "data-swap") ?? "")
|
|
186
|
+
const name = mode?.[2]
|
|
187
|
+
if (
|
|
188
|
+
name === undefined ||
|
|
189
|
+
url.origin !== location.origin ||
|
|
190
|
+
Element.prototype.closest.call(source, "[data-island-ignore]") !== null
|
|
191
|
+
)
|
|
192
|
+
return
|
|
193
|
+
const slot = slotNamed(name)
|
|
194
|
+
if (slot === undefined) return
|
|
195
|
+
event.preventDefault()
|
|
196
|
+
Element.prototype.removeAttribute.call(source, "data-swap-error")
|
|
197
|
+
void swap(slot, name, mode?.[1] === "append", url, source)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const onClick = (event: Event): void => {
|
|
201
|
+
if (!(event instanceof MouseEvent) || event.defaultPrevented || event.button !== 0) return
|
|
202
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
|
|
203
|
+
const link = event.target instanceof Element ? event.target.closest("a[data-swap]") : null
|
|
204
|
+
if (!(link instanceof HTMLAnchorElement) || !link.hasAttribute("href")) return
|
|
205
|
+
const target = link.getAttribute("target")
|
|
206
|
+
if ((target !== null && target !== "" && target !== "_self") || link.hasAttribute("download"))
|
|
207
|
+
return
|
|
208
|
+
const url = new URL(link.href)
|
|
209
|
+
// An in-page anchor keeps its native jump.
|
|
210
|
+
if (url.hash !== "" && url.pathname + url.search === here()) return
|
|
211
|
+
start(event, link, url)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const onSubmit = (event: Event): void => {
|
|
215
|
+
const form = event.target
|
|
216
|
+
if (
|
|
217
|
+
!(event instanceof SubmitEvent) ||
|
|
218
|
+
event.defaultPrevented ||
|
|
219
|
+
!(form instanceof HTMLFormElement) ||
|
|
220
|
+
attr(form, "data-swap") === null
|
|
221
|
+
)
|
|
222
|
+
return
|
|
223
|
+
const submitter = event.submitter
|
|
224
|
+
// The button that submitted can override the form's method, action, target and encoding.
|
|
225
|
+
const pick = (name: string): string | null =>
|
|
226
|
+
submitter?.hasAttribute(`form${name}`)
|
|
227
|
+
? submitter.getAttribute(`form${name}`)
|
|
228
|
+
: attr(form, name)
|
|
229
|
+
// An empty method is a GET, as for the browser.
|
|
230
|
+
const method = (pick("method") || "get").toUpperCase()
|
|
231
|
+
const target = pick("target")
|
|
232
|
+
const enctype = pick("enctype")?.toLowerCase()
|
|
233
|
+
if (
|
|
234
|
+
method !== "GET" ||
|
|
235
|
+
(target !== null && target !== "" && target !== "_self") ||
|
|
236
|
+
enctype === "text/plain"
|
|
237
|
+
)
|
|
238
|
+
return
|
|
239
|
+
const action = pick("action")
|
|
240
|
+
const url = new URL(action === null || action === "" ? location.href : action, document.baseURI)
|
|
241
|
+
const data = new FormData(form, submitter)
|
|
242
|
+
const fields = new URLSearchParams()
|
|
243
|
+
for (const [key, value] of data)
|
|
244
|
+
fields.append(key, typeof value === "string" ? value : value.name)
|
|
245
|
+
url.search = fields.toString()
|
|
246
|
+
start(event, form, url)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const onPopState = (event: PopStateEvent): void => {
|
|
250
|
+
if (here() === shown) return
|
|
251
|
+
shown = here()
|
|
252
|
+
const reached = swappedIn(event.state)
|
|
253
|
+
// Every slot a swap changed between the entry left and the one reached is in the later one's list.
|
|
254
|
+
const names = new Set([...swapped, ...reached])
|
|
255
|
+
swapped = reached
|
|
256
|
+
const targets: Array<[string, Element]> = []
|
|
257
|
+
for (const name of names) {
|
|
258
|
+
const slot = slotNamed(name)
|
|
259
|
+
if (slot === undefined) {
|
|
260
|
+
location.reload()
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
targets.push([name, slot])
|
|
264
|
+
}
|
|
265
|
+
for (const [name, slot] of targets) void swap(slot, name, false, new URL(location.href))
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const listen = (method: "addEventListener" | "removeEventListener"): void => {
|
|
269
|
+
root[method]("click", onClick)
|
|
270
|
+
root[method]("submit", onSubmit)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
attach(root)
|
|
274
|
+
listen("addEventListener")
|
|
275
|
+
window.addEventListener("popstate", onPopState)
|
|
276
|
+
return () => {
|
|
277
|
+
listen("removeEventListener")
|
|
278
|
+
window.removeEventListener("popstate", onPopState)
|
|
279
|
+
for (const abort of pending.values()) abort.abort()
|
|
280
|
+
for (const stop of stops.values()) stop()
|
|
281
|
+
stops.clear()
|
|
282
|
+
}
|
|
283
|
+
}
|
package/src/islands.ts
CHANGED
|
@@ -1,27 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @nifrajs/web/islands - the
|
|
2
|
+
* @nifrajs/web/islands - the islands client runtime: interactivity for server-rendered pages without a
|
|
3
|
+
* framework runtime.
|
|
3
4
|
*
|
|
4
|
-
* An
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* bundle (this runtime + your enhancers) loads. This variant ships ~99% less client JS than full
|
|
8
|
-
* React hydration on a mostly-static page - the framework runtime is the cost, and islands skip it
|
|
9
|
-
* entirely here.
|
|
5
|
+
* An island is a server-rendered element marked `data-island="<id>"`. Its interactivity is mounted by
|
|
6
|
+
* an enhancer instead of hydration, so on a route with `export const hydrate = false` the page ships
|
|
7
|
+
* no framework JS; only the island bundle (this runtime + your enhancers) loads via `islandScripts`.
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* (
|
|
14
|
-
*
|
|
15
|
-
*
|
|
9
|
+
* Two tiers, one mount:
|
|
10
|
+
* - `defineIsland((el, props) => cleanup)` - plain DOM code. The default.
|
|
11
|
+
* - `bindIsland(({ state, onDispose }) => handlers)` - signals the markup binds by name through a
|
|
12
|
+
* closed set of `data-bind-*` attributes. No expression language: names only.
|
|
13
|
+
* `signal`/`computed`/`effect`, a keyed `bindList` and an async `resource` work inside either tier;
|
|
14
|
+
* whatever an enhancer creates stops when its island unmounts.
|
|
15
|
+
*
|
|
16
|
+
* Server side: `<div data-island="counter" data-island-props="${islandProps({ start: 0 })}">`, or
|
|
17
|
+
* `@nifrajs/web-react`'s `<Island>`. Optional `data-island-strategy` (`load` | `idle` | `visible` |
|
|
18
|
+
* `media`, with `data-island-media`). Props travel as JSON in the attribute - no central registry, so
|
|
19
|
+
* concurrent SSR renders never share state. Nothing inside a `data-island-ignore` element mounts or
|
|
20
|
+
* binds: render user HTML there.
|
|
16
21
|
*/
|
|
17
22
|
|
|
18
|
-
import {
|
|
23
|
+
import { bindScope } from "./internal/island-bindings.ts"
|
|
24
|
+
import { mountIsland } from "./internal/island-mount.ts"
|
|
25
|
+
import { owned } from "./internal/island-owner.ts"
|
|
26
|
+
import { type Signal, signal } from "./internal/island-signals.ts"
|
|
27
|
+
import { PAGE_HIDE_EVENT, PAGE_SHOW_EVENT } from "./internal/page-events.ts"
|
|
28
|
+
|
|
29
|
+
export {
|
|
19
30
|
type IslandStrategy,
|
|
20
31
|
MAX_MEDIA_QUERY_LENGTH,
|
|
21
32
|
scheduleTrigger,
|
|
22
33
|
} from "@nifrajs/island-trigger"
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
export {
|
|
35
|
+
type BindListOptions,
|
|
36
|
+
bindList,
|
|
37
|
+
type ListContainer,
|
|
38
|
+
type ListNode,
|
|
39
|
+
} from "./internal/island-list.ts"
|
|
40
|
+
export { type Resource, type ResourceState, resource } from "./internal/island-resource.ts"
|
|
41
|
+
export {
|
|
42
|
+
batch,
|
|
43
|
+
type Computed,
|
|
44
|
+
computed,
|
|
45
|
+
effect,
|
|
46
|
+
type Signal,
|
|
47
|
+
signal,
|
|
48
|
+
untrack,
|
|
49
|
+
} from "./internal/island-signals.ts"
|
|
25
50
|
|
|
26
51
|
/** Optional teardown an enhancer returns (remove listeners/observers); run on `dispose()`. */
|
|
27
52
|
export type IslandCleanup = () => void
|
|
@@ -36,7 +61,7 @@ export type IslandEnhancer<P = unknown> = (el: HTMLElement, props: P) => IslandC
|
|
|
36
61
|
|
|
37
62
|
/**
|
|
38
63
|
* Author a typed enhancer. Pure identity at runtime (zero cost, tree-shaken away) - its only job is
|
|
39
|
-
* to pin the
|
|
64
|
+
* to pin the props shape so `el` and `props` are typed inside the body and the enhancer is
|
|
40
65
|
* assignable to `mountIslands`. Prefer this over an inline arrow whenever an island reads props:
|
|
41
66
|
*
|
|
42
67
|
* const counter = defineIsland<{ start: number }>((el, props) => {
|
|
@@ -48,78 +73,150 @@ export type IslandEnhancer<P = unknown> = (el: HTMLElement, props: P) => IslandC
|
|
|
48
73
|
* })
|
|
49
74
|
* mountIslands({ counter })
|
|
50
75
|
*
|
|
51
|
-
* The props type is a contract, not a validator - `data-props` is JSON authored by your own
|
|
52
|
-
* it is trusted framing, not user input. Keep the enhancer body imperative: read the DOM,
|
|
53
|
-
* listeners, return cleanup.
|
|
76
|
+
* The props type is a contract, not a validator - `data-island-props` is JSON authored by your own
|
|
77
|
+
* SSR, so it is trusted framing, not user input. Keep the enhancer body imperative: read the DOM,
|
|
78
|
+
* wire listeners, return cleanup.
|
|
54
79
|
*/
|
|
55
80
|
export function defineIsland<P = unknown>(enhancer: IslandEnhancer<P>): IslandEnhancer<P> {
|
|
56
81
|
return enhancer
|
|
57
82
|
}
|
|
58
83
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
84
|
+
const onPage = (type: string, el: Element, callback: () => void): (() => void) => {
|
|
85
|
+
if (typeof document === "undefined") return () => {}
|
|
86
|
+
const listener = (event: Event): void => {
|
|
87
|
+
if (event.target instanceof Node && event.target.contains(el)) callback()
|
|
88
|
+
}
|
|
89
|
+
document.addEventListener(type, listener)
|
|
90
|
+
return () => document.removeEventListener(type, listener)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Run `callback` each time the `keepAlive` page `el` renders in is hidden behind another page, just
|
|
95
|
+
* before it leaves the document. An island in a kept page stays mounted, and its timers and window
|
|
96
|
+
* listeners keep running, while the page is hidden: pause them here. Returns its removal - hand it back
|
|
97
|
+
* as part of the enhancer's cleanup. Never runs outside a kept page.
|
|
98
|
+
*
|
|
99
|
+
* const clock = defineIsland((el) => {
|
|
100
|
+
* let timer = setInterval(tick, 1000)
|
|
101
|
+
* const hide = onPageHide(el, () => clearInterval(timer))
|
|
102
|
+
* const show = onPageShow(el, () => { timer = setInterval(tick, 1000) })
|
|
103
|
+
* return () => { hide(); show(); clearInterval(timer) }
|
|
104
|
+
* })
|
|
105
|
+
*/
|
|
106
|
+
export function onPageHide(el: Element, callback: () => void): () => void {
|
|
107
|
+
return onPage(PAGE_HIDE_EVENT, el, callback)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Run `callback` each time the `keepAlive` page `el` renders in is shown again; returns its removal. */
|
|
111
|
+
export function onPageShow(el: Element, callback: () => void): () => void {
|
|
112
|
+
return onPage(PAGE_SHOW_EVENT, el, callback)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** What a `bindIsland` setup receives. */
|
|
116
|
+
export interface BindIslandContext<P> {
|
|
117
|
+
/** The island's host element. */
|
|
118
|
+
readonly root: HTMLElement
|
|
119
|
+
/** The parsed `data-island-props`, or `undefined` when absent or malformed. */
|
|
120
|
+
readonly props: P | undefined
|
|
121
|
+
/**
|
|
122
|
+
* The signal the markup binds as `key`, seeded from `props[key]` when present, else `fallback`.
|
|
123
|
+
* Each key returns the same signal across calls.
|
|
124
|
+
*/
|
|
125
|
+
state<T>(key: string, fallback: T): Signal<T>
|
|
126
|
+
/**
|
|
127
|
+
* Run `fn` when the island unmounts - for timers, requests and listeners the setup starts. Called
|
|
128
|
+
* after the island unmounted (say, once an awaited request settles), it runs `fn` at once.
|
|
129
|
+
*/
|
|
130
|
+
onDispose(fn: () => void): void
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The binding tier: `setup` creates the island's state and returns the event handlers its markup
|
|
135
|
+
* names; the island's `data-bind-*` attributes then bind them.
|
|
136
|
+
*
|
|
137
|
+
* data-bind-text="count" data-bind-show="open" data-bind-class="active:open"
|
|
138
|
+
* data-bind-attr="aria-expanded:open" data-bind-value="query" data-bind-on="click:add"
|
|
139
|
+
*
|
|
140
|
+
* Attribute values are signal and handler names, never code; an unknown name warns once and the
|
|
141
|
+
* server-rendered content stays as it is. A binding belongs to its nearest island.
|
|
142
|
+
*
|
|
143
|
+
* A setup or binding that throws stops everything the island started before the error reaches
|
|
144
|
+
* `mountIslands`. On unmount every cleanup runs; one that throws is logged and the rest still run.
|
|
145
|
+
*/
|
|
146
|
+
export function bindIsland<P = Record<string, unknown>>(
|
|
147
|
+
setup: (ctx: BindIslandContext<P>) => Record<string, (event: Event) => void> | void,
|
|
148
|
+
): IslandEnhancer<P> {
|
|
149
|
+
return (root, props) => {
|
|
150
|
+
const stops: Array<() => void> = []
|
|
151
|
+
let live = true
|
|
152
|
+
const seeded: Record<string, unknown> = typeof props === "object" && props !== null ? props : {}
|
|
153
|
+
// No prototype: a state key like `constructor` is an entry like any other.
|
|
154
|
+
const signals: Record<string, Signal<unknown>> = Object.create(null)
|
|
155
|
+
const ctx: BindIslandContext<P> = {
|
|
156
|
+
root,
|
|
157
|
+
props,
|
|
158
|
+
state<T>(key: string, fallback: T): Signal<T> {
|
|
159
|
+
signals[key] ??= signal(Object.hasOwn(seeded, key) ? seeded[key] : fallback)
|
|
160
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: each key's signal is created once, from this call's type
|
|
161
|
+
return signals[key] as Signal<T>
|
|
162
|
+
},
|
|
163
|
+
onDispose: (fn) => {
|
|
164
|
+
if (live) stops.push(fn)
|
|
165
|
+
else fn()
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
const dispose = (): void => {
|
|
169
|
+
live = false
|
|
170
|
+
for (let stop = stops.pop(); stop !== undefined; stop = stops.pop()) {
|
|
171
|
+
try {
|
|
172
|
+
stop()
|
|
173
|
+
} catch (err) {
|
|
174
|
+
console.error("[nifra/islands] cleanup failed:", err)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
// Owned, so a binding whose first run throws is registered before it throws.
|
|
180
|
+
owned(stops, () => bindScope(root, { signals, handlers: setup(ctx) ?? {} }, stops))
|
|
181
|
+
} catch (err) {
|
|
182
|
+
dispose()
|
|
183
|
+
throw err
|
|
184
|
+
}
|
|
185
|
+
return dispose
|
|
67
186
|
}
|
|
68
187
|
}
|
|
69
188
|
|
|
70
189
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
|
|
75
|
-
|
|
190
|
+
* The `data-island-props` value for `props`: JSON. Emit it through an escaping renderer into a quoted
|
|
191
|
+
* attribute (`data-island-props="${...}"`): unquoted, a space in the JSON ends the value and the rest
|
|
192
|
+
* parses as attributes.
|
|
193
|
+
*/
|
|
194
|
+
export function islandProps(props: Record<string, unknown>): string {
|
|
195
|
+
return JSON.stringify(props)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Find every `[data-island]` element under `root` (default `document`) and enhance each with the
|
|
200
|
+
* matching enhancer, honoring its `data-island-strategy`. An island whose id has no enhancer, or that
|
|
201
|
+
* is already mounted, is left alone, so calling again after a soft navigation mounts only new hosts.
|
|
202
|
+
* An enhancer that throws is isolated - it never blocks the others. Every effect, list binding and
|
|
203
|
+
* resource an enhancer creates is owned by its island. Returns a disposer that cancels pending
|
|
204
|
+
* triggers and tears down every island it mounted (call it on soft-nav teardown).
|
|
76
205
|
*/
|
|
77
206
|
export function mountIslands(
|
|
78
|
-
|
|
207
|
+
// `never` admits an enhancer of any props type: each one's props are its own island's JSON.
|
|
208
|
+
enhancers: Readonly<Record<string, IslandEnhancer<never>>>,
|
|
79
209
|
options: { readonly root?: ParentNode } = {},
|
|
80
210
|
): () => void {
|
|
81
|
-
const root = options.root ?? document
|
|
82
211
|
const disposers: Array<() => void> = []
|
|
83
|
-
for (const el of root.querySelectorAll<HTMLElement>(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
88
|
-
const props = readProps(el)
|
|
89
|
-
const strategyAttr = el.dataset.strategy
|
|
90
|
-
const strategy: IslandStrategy | undefined =
|
|
91
|
-
strategyAttr === "idle" || strategyAttr === "visible" || strategyAttr === "load"
|
|
92
|
-
? strategyAttr
|
|
93
|
-
: strategyAttr === "media" &&
|
|
94
|
-
el.dataset.media !== undefined &&
|
|
95
|
-
el.dataset.media.length > 0 &&
|
|
96
|
-
el.dataset.media.length <= MAX_MEDIA_QUERY_LENGTH
|
|
97
|
-
? { media: el.dataset.media }
|
|
98
|
-
: strategyAttr === undefined
|
|
99
|
-
? "load"
|
|
100
|
-
: undefined
|
|
101
|
-
// Invalid media markers remain inert rather than silently becoming eager. Their SSR content stays
|
|
102
|
-
// usable, and untrusted HTML cannot turn an oversized query into an unexpected trigger.
|
|
103
|
-
if (strategy === undefined) continue
|
|
104
|
-
const run = (): void => {
|
|
105
|
-
try {
|
|
106
|
-
const cleanup = enhancer(el, props)
|
|
107
|
-
if (typeof cleanup === "function") disposers.push(cleanup)
|
|
108
|
-
} catch (err) {
|
|
109
|
-
// One island's failure must not take down the others; surface it without throwing.
|
|
110
|
-
console.error(`[nifra/islands] enhancer "${id}" failed:`, err)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
disposers.push(scheduleTrigger(strategy, run, { target: el }))
|
|
212
|
+
for (const el of (options.root ?? document).querySelectorAll<HTMLElement>(
|
|
213
|
+
"[data-island]:not([data-island-ignore],[data-island-ignore] *)",
|
|
214
|
+
)) {
|
|
215
|
+
const dispose = mountIsland(el, enhancers)
|
|
216
|
+
if (dispose !== undefined) disposers.push(dispose)
|
|
114
217
|
}
|
|
115
218
|
return () => {
|
|
116
|
-
for (const dispose of disposers)
|
|
117
|
-
try {
|
|
118
|
-
dispose()
|
|
119
|
-
} catch {
|
|
120
|
-
// best-effort teardown - a failing cleanup must not block the rest
|
|
121
|
-
}
|
|
122
|
-
}
|
|
219
|
+
for (const dispose of disposers.splice(0)) dispose()
|
|
123
220
|
}
|
|
124
221
|
}
|
|
125
222
|
|