@rangojs/router 0.0.0-experimental.79 → 0.0.0-experimental.7c7e4327
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/AGENTS.md +8 -4
- package/README.md +301 -797
- package/dist/bin/rango.js +603 -145
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3750 -1160
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +96 -24
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +85 -6
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +228 -33
- package/skills/caching/SKILL.md +336 -19
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +110 -4
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +5 -3
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +87 -56
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +12 -10
- package/skills/hooks/SKILL.md +73 -691
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +129 -27
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +75 -19
- package/skills/layout/SKILL.md +40 -19
- package/skills/links/SKILL.md +247 -17
- package/skills/loader/SKILL.md +248 -10
- package/skills/middleware/SKILL.md +25 -13
- package/skills/migrate-nextjs/SKILL.md +205 -20
- package/skills/migrate-react-router/SKILL.md +59 -670
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +29 -2
- package/skills/observability/SKILL.md +202 -0
- package/skills/parallel/SKILL.md +40 -10
- package/skills/ppr/SKILL.md +616 -0
- package/skills/prerender/SKILL.md +72 -60
- package/skills/rango/SKILL.md +318 -26
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +138 -49
- package/skills/route/SKILL.md +117 -9
- package/skills/router-setup/SKILL.md +44 -9
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +776 -0
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +28 -4
- package/skills/testing/SKILL.md +130 -0
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -0
- package/skills/testing/handles.md +131 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +122 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +85 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +123 -0
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -626
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +74 -15
- package/skills/vercel/SKILL.md +128 -0
- package/skills/view-transitions/SKILL.md +337 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +252 -158
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +94 -25
- package/src/browser/navigation-client.ts +144 -79
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +161 -73
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +183 -144
- package/src/browser/prefetch/cache.ts +242 -77
- package/src/browser/prefetch/fetch.ts +325 -69
- package/src/browser/prefetch/queue.ts +61 -12
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +58 -20
- package/src/browser/react/NavigationProvider.tsx +202 -120
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +66 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +178 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +17 -14
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +32 -7
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +25 -3
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +99 -8
- package/src/browser/rsc-router.tsx +145 -28
- package/src/browser/scroll-restoration.ts +37 -22
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +236 -65
- package/src/browser/types.ts +102 -9
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +203 -154
- package/src/build/generate-route-types.ts +3 -1
- package/src/build/index.ts +11 -3
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +152 -21
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +456 -62
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +128 -51
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +13 -21
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +58 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +421 -58
- package/src/cache/cache-scope.ts +187 -96
- package/src/cache/cache-tag.ts +149 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +2202 -372
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +126 -41
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +23 -20
- package/src/cache/memory-segment-store.ts +243 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +13 -21
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +180 -99
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +1127 -0
- package/src/client.rsc.tsx +41 -21
- package/src/client.tsx +33 -61
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +108 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +18 -6
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +185 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +67 -37
- package/src/handles/MetaTags.tsx +24 -53
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +35 -8
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +18 -0
- package/src/handles/meta.ts +14 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +13 -22
- package/src/host/index.ts +9 -2
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +97 -12
- package/src/index.ts +98 -14
- package/src/internal-debug.ts +11 -10
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +78 -10
- package/src/redirect-origin.ts +114 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +62 -0
- package/src/reverse.ts +65 -39
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +19 -77
- package/src/route-definition/dsl-helpers.ts +304 -309
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +87 -59
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/redirect.ts +44 -11
- package/src/route-definition/resolve-handler-use.ts +12 -1
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +41 -20
- package/src/route-types.ts +19 -46
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +73 -25
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +129 -30
- package/src/router/handler-context.ts +27 -42
- package/src/router/instrument.ts +355 -0
- package/src/router/intercept-resolution.ts +39 -20
- package/src/router/lazy-includes.ts +82 -59
- package/src/router/loader-resolution.ts +167 -72
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +74 -40
- package/src/router/match-api.ts +80 -55
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +211 -165
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +159 -285
- package/src/router/match-middleware/cache-store.ts +64 -52
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +69 -79
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +7 -134
- package/src/router/middleware.ts +298 -172
- package/src/router/navigation-snapshot.ts +7 -56
- package/src/router/params-util.ts +23 -0
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +181 -150
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +112 -67
- package/src/router/preview-match.ts +6 -2
- package/src/router/request-classification.ts +50 -69
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +14 -3
- package/src/router/router-context.ts +6 -29
- package/src/router/router-interfaces.ts +115 -36
- package/src/router/router-options.ts +166 -5
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +131 -86
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +139 -39
- package/src/router/segment-resolution/loader-mask.ts +67 -0
- package/src/router/segment-resolution/loader-snapshot.ts +251 -0
- package/src/router/segment-resolution/revalidation.ts +272 -320
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +56 -0
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +6 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +75 -0
- package/src/router/telemetry-otel.ts +160 -200
- package/src/router/telemetry.ts +105 -20
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +215 -0
- package/src/router/trie-matching.ts +171 -59
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +57 -0
- package/src/router.ts +157 -71
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +291 -217
- package/src/rsc/helpers.ts +168 -46
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +114 -38
- package/src/rsc/manifest-init.ts +29 -42
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +124 -13
- package/src/rsc/redirect-guard.ts +100 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +99 -189
- package/src/rsc/rsc-rendering.ts +421 -76
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +282 -116
- package/src/rsc/shell-capture.ts +1158 -0
- package/src/rsc/shell-serve.ts +150 -0
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +53 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-loader-promise.ts +49 -4
- package/src/segment-system.tsx +350 -149
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +208 -51
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +152 -5
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +33 -42
- package/src/server/request-context.ts +395 -176
- package/src/ssr/index.tsx +458 -178
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +46 -0
- package/src/testing/dispatch.ts +813 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +257 -0
- package/src/testing/generated-routes.ts +199 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +371 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +357 -0
- package/src/testing/render-route.tsx +584 -0
- package/src/testing/run-loader.ts +385 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/run-transition-when.ts +164 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +56 -84
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +110 -62
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +11 -9
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +20 -52
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +135 -14
- package/src/urls/include-helper.ts +19 -64
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +2 -11
- package/src/urls/path-helper-types.ts +63 -17
- package/src/urls/path-helper.ts +22 -106
- package/src/urls/pattern-types.ts +72 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +98 -154
- package/src/urls/urls-function.ts +1 -19
- package/src/use-loader.tsx +292 -107
- package/src/vercel/index.ts +11 -0
- package/src/vercel/tracing.ts +88 -0
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +127 -86
- package/src/vite/discovery/discovery-errors.ts +255 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +96 -68
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +45 -1
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +4 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugin-types.ts +210 -10
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
- package/src/vite/plugins/expose-action-id.ts +48 -95
- package/src/vite/plugins/expose-id-utils.ts +88 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
- package/src/vite/plugins/expose-internal-ids.ts +505 -486
- package/src/vite/plugins/performance-tracks.ts +26 -25
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/vercel-output.ts +384 -0
- package/src/vite/plugins/version-injector.ts +40 -29
- package/src/vite/plugins/version-plugin.ts +37 -40
- package/src/vite/plugins/virtual-entries.ts +138 -27
- package/src/vite/rango.ts +236 -138
- package/src/vite/router-discovery.ts +927 -136
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +71 -43
- package/src/vite/utils/shared-utils.ts +142 -43
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
package/src/handles/MetaTags.tsx
CHANGED
|
@@ -3,11 +3,16 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Component to render collected meta descriptors in the document head.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* (
|
|
6
|
+
* Deferred (Promise) meta descriptors are resolved before MetaTags renders
|
|
7
|
+
* (server-side on the full render, client-side before apply on navigation), so
|
|
8
|
+
* it only ever receives resolved descriptors and never suspends.
|
|
8
9
|
*
|
|
9
10
|
* When theme is enabled in the router config, MetaTags also renders
|
|
10
11
|
* the theme initialization script to prevent FOUC (flash of unstyled content).
|
|
12
|
+
* This makes MetaTags the sole FOUC-script injector for apps that render it;
|
|
13
|
+
* the standalone `<ThemeScript />` is only needed when MetaTags is not used.
|
|
14
|
+
* Rendering both is safe (the inline script guards listener registration) but
|
|
15
|
+
* redundant.
|
|
11
16
|
*
|
|
12
17
|
* @example
|
|
13
18
|
* ```tsx
|
|
@@ -24,13 +29,13 @@
|
|
|
24
29
|
* ```
|
|
25
30
|
*/
|
|
26
31
|
|
|
27
|
-
import { use } from "react";
|
|
28
32
|
import { useHandle } from "../browser/react/use-handle.js";
|
|
29
33
|
import { Meta } from "./meta.js";
|
|
30
|
-
import type {
|
|
34
|
+
import type { MetaDescriptorBase } from "../router/types.js";
|
|
31
35
|
import { useThemeContext } from "../theme/theme-context.js";
|
|
32
36
|
import { generateThemeScript } from "../theme/theme-script.js";
|
|
33
37
|
import { useNonce } from "../browser/react/nonce-context.js";
|
|
38
|
+
import { escapeJsonForScript } from "../escape-script.js";
|
|
34
39
|
|
|
35
40
|
// Type guards for MetaDescriptorBase variants
|
|
36
41
|
function hasCharSet(d: MetaDescriptorBase): d is { charSet: "utf-8" } {
|
|
@@ -90,31 +95,18 @@ function hasTagName(
|
|
|
90
95
|
);
|
|
91
96
|
}
|
|
92
97
|
|
|
93
|
-
|
|
94
|
-
* Check if a value is a Promise.
|
|
95
|
-
*/
|
|
96
|
-
function isPromise(value: unknown): value is Promise<unknown> {
|
|
97
|
-
return value !== null && typeof value === "object" && "then" in value;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Render a single meta descriptor as a React element.
|
|
102
|
-
*/
|
|
103
|
-
function renderMetaDescriptor(
|
|
98
|
+
export function renderMetaDescriptor(
|
|
104
99
|
descriptor: MetaDescriptorBase,
|
|
105
100
|
index: number,
|
|
106
101
|
): React.ReactNode {
|
|
107
|
-
// charset
|
|
108
102
|
if (hasCharSet(descriptor)) {
|
|
109
103
|
return <meta key="charSet" charSet={descriptor.charSet} />;
|
|
110
104
|
}
|
|
111
105
|
|
|
112
|
-
// title
|
|
113
106
|
if (hasTitle(descriptor)) {
|
|
114
107
|
return <title key="title">{descriptor.title}</title>;
|
|
115
108
|
}
|
|
116
109
|
|
|
117
|
-
// name + content (description, viewport, etc.)
|
|
118
110
|
if (hasNameContent(descriptor)) {
|
|
119
111
|
return (
|
|
120
112
|
<meta
|
|
@@ -125,7 +117,6 @@ function renderMetaDescriptor(
|
|
|
125
117
|
);
|
|
126
118
|
}
|
|
127
119
|
|
|
128
|
-
// property + content (Open Graph, etc.)
|
|
129
120
|
if (hasPropertyContent(descriptor)) {
|
|
130
121
|
return (
|
|
131
122
|
<meta
|
|
@@ -136,7 +127,6 @@ function renderMetaDescriptor(
|
|
|
136
127
|
);
|
|
137
128
|
}
|
|
138
129
|
|
|
139
|
-
// http-equiv + content
|
|
140
130
|
if (hasHttpEquivContent(descriptor)) {
|
|
141
131
|
return (
|
|
142
132
|
<meta
|
|
@@ -147,9 +137,10 @@ function renderMetaDescriptor(
|
|
|
147
137
|
);
|
|
148
138
|
}
|
|
149
139
|
|
|
150
|
-
// JSON-LD structured data
|
|
151
140
|
if (hasScriptLdJson(descriptor)) {
|
|
152
|
-
const json =
|
|
141
|
+
const json = escapeJsonForScript(
|
|
142
|
+
JSON.stringify(descriptor["script:ld+json"]),
|
|
143
|
+
);
|
|
153
144
|
return (
|
|
154
145
|
<script
|
|
155
146
|
key={`ld-json-${index}`}
|
|
@@ -159,7 +150,6 @@ function renderMetaDescriptor(
|
|
|
159
150
|
);
|
|
160
151
|
}
|
|
161
152
|
|
|
162
|
-
// Custom tagName (meta or link with arbitrary attributes)
|
|
163
153
|
if (hasTagName(descriptor)) {
|
|
164
154
|
const { tagName, ...rest } = descriptor;
|
|
165
155
|
if (tagName === "link") {
|
|
@@ -180,7 +170,6 @@ function renderMetaDescriptor(
|
|
|
180
170
|
}
|
|
181
171
|
}
|
|
182
172
|
|
|
183
|
-
// Fallback: treat as meta attributes
|
|
184
173
|
return (
|
|
185
174
|
<meta
|
|
186
175
|
key={`meta-fallback-${index}`}
|
|
@@ -189,20 +178,6 @@ function renderMetaDescriptor(
|
|
|
189
178
|
);
|
|
190
179
|
}
|
|
191
180
|
|
|
192
|
-
/**
|
|
193
|
-
* Wrapper component to resolve a Promise<MetaDescriptorBase> using use().
|
|
194
|
-
*/
|
|
195
|
-
function AsyncMetaTag({
|
|
196
|
-
promise,
|
|
197
|
-
index,
|
|
198
|
-
}: {
|
|
199
|
-
promise: Promise<MetaDescriptorBase>;
|
|
200
|
-
index: number;
|
|
201
|
-
}): React.ReactNode {
|
|
202
|
-
const resolved = use(promise);
|
|
203
|
-
return renderMetaDescriptor(resolved, index);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
181
|
/**
|
|
207
182
|
* Renders all collected meta descriptors from route handlers.
|
|
208
183
|
*
|
|
@@ -212,11 +187,16 @@ function AsyncMetaTag({
|
|
|
212
187
|
* When theme is enabled in router config, also renders the theme initialization
|
|
213
188
|
* script to prevent FOUC (flash of unstyled content).
|
|
214
189
|
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
190
|
+
* Deferred (Promise) meta descriptors are resolved BEFORE MetaTags renders —
|
|
191
|
+
* server-side on the full/SSR render, client-side before apply on navigation
|
|
192
|
+
* (resolve-by-default) — so MetaTags only ever receives resolved descriptors and
|
|
193
|
+
* never suspends.
|
|
217
194
|
*/
|
|
218
195
|
export function MetaTags(): React.ReactNode {
|
|
219
|
-
|
|
196
|
+
// Deferred descriptors are resolved BEFORE collect runs (resolve-by-default),
|
|
197
|
+
// and collectMeta strips unset markers, so the collected output is always
|
|
198
|
+
// resolved base descriptors (never a Promise).
|
|
199
|
+
const descriptors = useHandle(Meta) as MetaDescriptorBase[];
|
|
220
200
|
const themeConfig = useThemeContext()?.config ?? null;
|
|
221
201
|
const nonce = useNonce();
|
|
222
202
|
|
|
@@ -229,18 +209,9 @@ export function MetaTags(): React.ReactNode {
|
|
|
229
209
|
dangerouslySetInnerHTML={{ __html: generateThemeScript(themeConfig) }}
|
|
230
210
|
/>
|
|
231
211
|
)}
|
|
232
|
-
{descriptors.map((descriptor, index) =>
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
<AsyncMetaTag
|
|
236
|
-
key={`async-${index}`}
|
|
237
|
-
promise={descriptor}
|
|
238
|
-
index={index}
|
|
239
|
-
/>
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
return renderMetaDescriptor(descriptor, index);
|
|
243
|
-
})}
|
|
212
|
+
{descriptors.map((descriptor, index) =>
|
|
213
|
+
renderMetaDescriptor(descriptor, index),
|
|
214
|
+
)}
|
|
244
215
|
</>
|
|
245
216
|
);
|
|
246
217
|
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Renders the scripts collected by the Script handle into the document.
|
|
5
|
+
*
|
|
6
|
+
* Place `<Scripts />` inside `<head>` (default) and, if you push body scripts,
|
|
7
|
+
* `<Scripts position="body" />` at the top of `<body>`. Each site renders the
|
|
8
|
+
* configs whose `position` matches; the request CSP nonce is applied
|
|
9
|
+
* automatically to every DOCUMENT-RENDERED <script> (consumers never pass it). An
|
|
10
|
+
* async script first encountered on a soft navigation is injected client-side
|
|
11
|
+
* where the nonce is unavailable, so it carries no nonce and relies on
|
|
12
|
+
* 'strict-dynamic' (or a host allowance) — see the nonce caveat in the /scripts
|
|
13
|
+
* skill.
|
|
14
|
+
*
|
|
15
|
+
* EXECUTION CONTRACT — see the Script handle's docs. Inline + ordered (defer)
|
|
16
|
+
* scripts are document-load: they execute only when present in the initial HTML,
|
|
17
|
+
* so this component FREEZES that set after hydration (the initializer below runs
|
|
18
|
+
* once) — a later soft navigation never inserts an inert <script> (React creates
|
|
19
|
+
* client-mounted scripts via innerHTML, which the HTML spec makes non-executing).
|
|
20
|
+
* Async external scripts are React resources and stay reactive: React loads them
|
|
21
|
+
* on first encounter, including after navigation, deduped by src.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```tsx
|
|
25
|
+
* <html>
|
|
26
|
+
* <head>
|
|
27
|
+
* <MetaTags />
|
|
28
|
+
* <Scripts />
|
|
29
|
+
* </head>
|
|
30
|
+
* <body>
|
|
31
|
+
* <Scripts position="body" />
|
|
32
|
+
* {children}
|
|
33
|
+
* </body>
|
|
34
|
+
* </html>
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { useState, type ReactNode } from "react";
|
|
39
|
+
import { useHandle } from "../browser/react/use-handle.js";
|
|
40
|
+
import { useNonce } from "../browser/react/nonce-context.js";
|
|
41
|
+
import { escapeScriptBody } from "../escape-script.js";
|
|
42
|
+
import { Script, type ScriptAttributes, type ScriptConfig } from "./script.js";
|
|
43
|
+
|
|
44
|
+
/** An external async script is a React-managed resource (reactive on nav). */
|
|
45
|
+
function isAsyncResource(config: ScriptConfig): boolean {
|
|
46
|
+
return config.src != null && config.async === true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Fields the Script handle owns (set via the ScriptConfig fields, applied as
|
|
50
|
+
// explicit props by renderScript) plus the inline-content props. Dropped from the
|
|
51
|
+
// attributes bag so untyped/serialized input cannot smuggle them in — e.g.
|
|
52
|
+
// `children`/`dangerouslySetInnerHTML` alongside an inline body makes React throw,
|
|
53
|
+
// or `src` on an inline script. The discriminated type already excludes these;
|
|
54
|
+
// this is the runtime guard.
|
|
55
|
+
const MANAGED_ATTRS = new Set([
|
|
56
|
+
"id",
|
|
57
|
+
"src",
|
|
58
|
+
"async",
|
|
59
|
+
"defer",
|
|
60
|
+
"type",
|
|
61
|
+
"children",
|
|
62
|
+
"nonce",
|
|
63
|
+
"dangerouslySetInnerHTML",
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
// Drop managed fields + any `on*` event handlers (a config serializes across the
|
|
67
|
+
// server -> client boundary, so a function cannot survive it) from the passthrough
|
|
68
|
+
// attributes, warning in dev.
|
|
69
|
+
function passthroughAttributes(
|
|
70
|
+
attributes: ScriptAttributes | undefined,
|
|
71
|
+
): Record<string, unknown> {
|
|
72
|
+
if (!attributes) return {};
|
|
73
|
+
const out: Record<string, unknown> = {};
|
|
74
|
+
const dev = process.env.NODE_ENV !== "production";
|
|
75
|
+
for (const [key, value] of Object.entries(
|
|
76
|
+
attributes as Record<string, unknown>,
|
|
77
|
+
)) {
|
|
78
|
+
const isHandler = key.startsWith("on");
|
|
79
|
+
if (isHandler || MANAGED_ATTRS.has(key)) {
|
|
80
|
+
if (dev) {
|
|
81
|
+
console.warn(
|
|
82
|
+
isHandler
|
|
83
|
+
? `[Scripts] event handler "${key}" in a script's attributes is ` +
|
|
84
|
+
`dropped; callbacks cannot cross the server -> client handle ` +
|
|
85
|
+
`boundary. Use a "use client" component for load/error handling.`
|
|
86
|
+
: `[Scripts] managed field "${key}" in a script's attributes is ` +
|
|
87
|
+
`dropped; set it via the ScriptConfig fields (the request nonce ` +
|
|
88
|
+
`is applied automatically).`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
out[key] = value;
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function renderScript(
|
|
99
|
+
config: ScriptConfig,
|
|
100
|
+
nonce: string | undefined,
|
|
101
|
+
index: number,
|
|
102
|
+
): ReactNode {
|
|
103
|
+
const { id, src, children, async, defer, type, attributes } = config;
|
|
104
|
+
const key = id ?? src ?? `rango-script-${index}`;
|
|
105
|
+
const attrs = passthroughAttributes(attributes);
|
|
106
|
+
|
|
107
|
+
// Inline: rendered in place (never hoisted), escaped against </script> breakout.
|
|
108
|
+
// The server-only nonce makes the attribute differ from the (undefined) client
|
|
109
|
+
// value, so suppressHydrationWarning is required — the same sanctioned pattern
|
|
110
|
+
// as the theme/Meta inline scripts.
|
|
111
|
+
if (src == null) {
|
|
112
|
+
if (children == null) return null;
|
|
113
|
+
return (
|
|
114
|
+
<script
|
|
115
|
+
key={key}
|
|
116
|
+
{...attrs}
|
|
117
|
+
id={id}
|
|
118
|
+
type={type}
|
|
119
|
+
nonce={nonce}
|
|
120
|
+
suppressHydrationWarning
|
|
121
|
+
dangerouslySetInnerHTML={{ __html: escapeScriptBody(children) }}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (
|
|
127
|
+
process.env.NODE_ENV !== "production" &&
|
|
128
|
+
config.position === "body" &&
|
|
129
|
+
async
|
|
130
|
+
) {
|
|
131
|
+
console.warn(
|
|
132
|
+
`[Scripts] An async external script (src="${src}") is hoisted into ` +
|
|
133
|
+
`<head> by React; position: "body" is ignored for it.`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// External: async => React-hoisted, src-deduped resource; otherwise in place
|
|
138
|
+
// (defer or plain), preserving authoring order.
|
|
139
|
+
return (
|
|
140
|
+
<script
|
|
141
|
+
key={key}
|
|
142
|
+
{...attrs}
|
|
143
|
+
id={id}
|
|
144
|
+
type={type}
|
|
145
|
+
src={src}
|
|
146
|
+
async={async}
|
|
147
|
+
defer={defer}
|
|
148
|
+
nonce={nonce}
|
|
149
|
+
suppressHydrationWarning
|
|
150
|
+
/>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function Scripts({
|
|
155
|
+
position = "head",
|
|
156
|
+
}: { position?: "head" | "body" } = {}): ReactNode {
|
|
157
|
+
const all = useHandle(Script) as ScriptConfig[];
|
|
158
|
+
const nonce = useNonce();
|
|
159
|
+
|
|
160
|
+
const forPosition = all.filter(
|
|
161
|
+
(config) => (config.position ?? "head") === position,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Document-load scripts (inline + ordered external) execute only from the
|
|
165
|
+
// initial HTML, so freeze them to the first-render set. The initializer runs
|
|
166
|
+
// during SSR and again at hydration with the same handle data, so the output
|
|
167
|
+
// matches; afterwards a navigation cannot add an inert <script>.
|
|
168
|
+
const [documentLoad] = useState(() =>
|
|
169
|
+
forPosition.filter((config) => !isAsyncResource(config)),
|
|
170
|
+
);
|
|
171
|
+
// Async external scripts are resources React loads on first encounter; keep
|
|
172
|
+
// them reactive so a script first reached via navigation still loads.
|
|
173
|
+
const asyncResources = forPosition.filter(isAsyncResource);
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<>
|
|
177
|
+
{documentLoad.map((config, index) => renderScript(config, nonce, index))}
|
|
178
|
+
{asyncResources.map((config, index) =>
|
|
179
|
+
renderScript(config, nonce, index),
|
|
180
|
+
)}
|
|
181
|
+
</>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Each layout/route pushes breadcrumb items via `ctx.use(Breadcrumbs)`.
|
|
5
5
|
* Items are collected in parent-to-child order with automatic deduplication
|
|
6
|
-
* by `href
|
|
6
|
+
* by `href`: each href keeps its FIRST position but takes the LAST value, so a
|
|
7
|
+
* child re-pushing a parent href refreshes the label without reordering the trail.
|
|
7
8
|
*
|
|
8
9
|
* @example
|
|
9
10
|
* ```tsx
|
|
@@ -38,19 +39,45 @@ export interface BreadcrumbItem {
|
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* Collect function for Breadcrumbs handle.
|
|
41
|
-
* Flattens segments in parent-to-child order with deduplication by href
|
|
42
|
-
*
|
|
42
|
+
* Flattens segments in parent-to-child order with deduplication by href: each
|
|
43
|
+
* href keeps its FIRST position but takes the LAST value (re-pushing a parent
|
|
44
|
+
* href refreshes the label in place without reordering the trail). Deferred
|
|
45
|
+
* crumbs (a pushed Promise or `ctx.use(Breadcrumbs).defer()`) are resolved
|
|
46
|
+
* BEFORE collect runs (resolve-by-default), so collect only ever sees resolved
|
|
47
|
+
* items. An item without a string `href` is passed through by identity and
|
|
48
|
+
* excluded from the href dedup.
|
|
43
49
|
*/
|
|
44
50
|
function collectBreadcrumbs(segments: BreadcrumbItem[][]): BreadcrumbItem[] {
|
|
45
51
|
const all = segments.flat();
|
|
46
|
-
const seen = new Map<string, number>();
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
const hasHref = (item: unknown): item is BreadcrumbItem =>
|
|
54
|
+
item != null &&
|
|
55
|
+
typeof item === "object" &&
|
|
56
|
+
typeof (item as { href?: unknown }).href === "string";
|
|
57
|
+
|
|
58
|
+
// Dedup crumbs by href: keep the FIRST position (preserving parent->child
|
|
59
|
+
// order) but the LAST value (a child re-pushing a parent's href can refresh
|
|
60
|
+
// its label). Items without an href pass through by identity at their original
|
|
61
|
+
// position.
|
|
62
|
+
const valueByHref = new Map<string, BreadcrumbItem>();
|
|
63
|
+
for (const item of all) {
|
|
64
|
+
if (hasHref(item)) valueByHref.set(item.href, item);
|
|
50
65
|
}
|
|
51
66
|
|
|
52
|
-
|
|
53
|
-
|
|
67
|
+
const result: BreadcrumbItem[] = [];
|
|
68
|
+
const emitted = new Set<string>();
|
|
69
|
+
for (const item of all) {
|
|
70
|
+
if (!hasHref(item)) {
|
|
71
|
+
result.push(item);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// Emit each href once, at its first occurrence, with the final value.
|
|
75
|
+
if (!emitted.has(item.href)) {
|
|
76
|
+
emitted.add(item.href);
|
|
77
|
+
result.push(valueByHref.get(item.href)!);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
54
81
|
}
|
|
55
82
|
|
|
56
83
|
/**
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { HandleData, HandleStore } from "../server/handle-store.js";
|
|
2
|
+
import { isThenable } from "./is-thenable.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Runtime-neutral resolution of deferred (Promise) handle values. Shared by the
|
|
6
|
+
* server full-render path (resolve before the payload is finalized so SSR sees
|
|
7
|
+
* resolved values) and the client soft-nav path (NavigationProvider resolves
|
|
8
|
+
* each yield before applying it). Pure — imports only `isThenable` — so it is
|
|
9
|
+
* safe in both the RSC server environment AND the browser.
|
|
10
|
+
*
|
|
11
|
+
* Resolution is SHALLOW: only an entry that is ITSELF a thenable is awaited. A
|
|
12
|
+
* value that merely contains a promise (e.g. `{ data: somePromise }`) is a
|
|
13
|
+
* non-thenable, so the `isThenable` check skips it and it passes through verbatim
|
|
14
|
+
* — the nested promise is the consumer's responsibility. Resolution runs each
|
|
15
|
+
* entry through `Promise.all` with a per-entry `try/catch` (NOT `allSettled`), so
|
|
16
|
+
* one rejecting entry is dropped without rejecting the batch or aborting siblings.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Handle names ($$id) whose snapshot carries a deferred (top-level Promise)
|
|
21
|
+
* value. Used by the client to decide which buckets to hold-and-resolve, and
|
|
22
|
+
* `.size > 0` answers "does this snapshot have any deferred value".
|
|
23
|
+
*/
|
|
24
|
+
export function deferredHandleNames(data: HandleData): Set<string> {
|
|
25
|
+
const names = new Set<string>();
|
|
26
|
+
for (const [handleName, segments] of Object.entries(data)) {
|
|
27
|
+
for (const values of Object.values(segments)) {
|
|
28
|
+
if (values.some(isThenable)) {
|
|
29
|
+
names.add(handleName);
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return names;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Snapshot with deferred (Promise) values awaited (shallow — only top-level
|
|
39
|
+
* thenables). A rejected deferred is dropped, and a deferred that resolves to
|
|
40
|
+
* `null`/`undefined` is dropped (the `.defer({ else })` skip semantics), so a
|
|
41
|
+
* nullish deferred slot never reaches a collector. Sync (non-thenable) values —
|
|
42
|
+
* including a legitimate sync `null` push — pass through unchanged.
|
|
43
|
+
*/
|
|
44
|
+
export async function resolveDeferredHandleValues(
|
|
45
|
+
data: HandleData,
|
|
46
|
+
): Promise<HandleData> {
|
|
47
|
+
const out: HandleData = {};
|
|
48
|
+
await Promise.all(
|
|
49
|
+
Object.entries(data).flatMap(([handleName, segments]) => {
|
|
50
|
+
out[handleName] = {};
|
|
51
|
+
return Object.entries(segments).map(async ([segmentId, values]) => {
|
|
52
|
+
out[handleName][segmentId] = await resolveValues(values);
|
|
53
|
+
});
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Resolve each entry IN ORDER. A non-thenable passes through verbatim (including
|
|
60
|
+
// a legitimate sync `null`/`undefined` push — the "await iff thenable" contract).
|
|
61
|
+
// A thenable is awaited and dropped when it rejects OR resolves to null/undefined
|
|
62
|
+
// (the `.defer({ else })` skip semantics: a forgotten resolver times out to
|
|
63
|
+
// `else ?? undefined`), so a nullish deferred slot never reaches a collector.
|
|
64
|
+
async function resolveValues(values: unknown[]): Promise<unknown[]> {
|
|
65
|
+
const resolved = await Promise.all(
|
|
66
|
+
values.map(async (v) => {
|
|
67
|
+
if (!isThenable(v)) return { keep: true, value: v };
|
|
68
|
+
try {
|
|
69
|
+
const value = await v;
|
|
70
|
+
return { keep: value != null, value };
|
|
71
|
+
} catch {
|
|
72
|
+
return { keep: false, value: undefined };
|
|
73
|
+
}
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
return resolved.filter((r) => r.keep).map((r) => r.value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Full-render handle stream: a one-shot async generator that yields the FINAL
|
|
81
|
+
* handle snapshot with every top-level deferred value resolved. Drop-in for
|
|
82
|
+
* `handleStore.stream()` on full / HTML renders (initial load, 404, progressive
|
|
83
|
+
* enhancement), where the consumer drains the generator to completion before
|
|
84
|
+
* rendering anyway — so the single final yield is equivalent to the streamed
|
|
85
|
+
* yields' converged state, and the SSR markup plus the first sync `useHandle`
|
|
86
|
+
* read see resolved values. `getData()` seals the store and awaits the handler
|
|
87
|
+
* barrier; `resolveDeferredHandleValues` then awaits the pushed-value promises.
|
|
88
|
+
* A hung handle promise blocks the full render like any unresolved await (no
|
|
89
|
+
* handle-specific timeout). Partial / action payloads keep streaming via
|
|
90
|
+
* `handleStore.stream()`.
|
|
91
|
+
*/
|
|
92
|
+
export async function* resolvedHandleStream(
|
|
93
|
+
handleStore: HandleStore,
|
|
94
|
+
): AsyncGenerator<HandleData, void, unknown> {
|
|
95
|
+
// Drain stream() (NOT getData()) for the converged snapshot: stream() sets the
|
|
96
|
+
// store's `completed` flag on seal+settle, and that flag is what makes a LATE
|
|
97
|
+
// push throw LateHandlePushError — an async JSX subtree that suspended and later
|
|
98
|
+
// calls ctx.use(Handle)(...) after collection. getData() never sets `completed`,
|
|
99
|
+
// so the late push would silently land. Both wait for the same settle barrier, so
|
|
100
|
+
// the final yielded value is identical; we just keep the late-push guard.
|
|
101
|
+
let snapshot: HandleData = {};
|
|
102
|
+
for await (const data of handleStore.stream()) {
|
|
103
|
+
snapshot = data;
|
|
104
|
+
}
|
|
105
|
+
yield await resolveDeferredHandleValues(snapshot);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resolve the deferred (Promise) values in ONE segment's handle bucket
|
|
110
|
+
* (`handleName -> entries[]`). Same shallow + drop-rejections rule as
|
|
111
|
+
* {@link resolveDeferredHandleValues}, but for the segment-keyed shape the
|
|
112
|
+
* prerender/static collection paths use (`getDataForSegment`). Prerender is a
|
|
113
|
+
* build-time cache, so the baked artifact must hold resolved values — a hit
|
|
114
|
+
* replays them into the segment system, where collect/useHandle expect resolved
|
|
115
|
+
* data.
|
|
116
|
+
*/
|
|
117
|
+
export async function resolveSegmentHandleValues(
|
|
118
|
+
segHandles: Record<string, unknown[]>,
|
|
119
|
+
): Promise<Record<string, unknown[]>> {
|
|
120
|
+
const out: Record<string, unknown[]> = {};
|
|
121
|
+
await Promise.all(
|
|
122
|
+
Object.entries(segHandles).map(async ([handleName, values]) => {
|
|
123
|
+
out[handleName] = await resolveValues(values);
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single thenable predicate for the resolve-by-default handle machinery
|
|
3
|
+
* (`handles/deferred-resolution.ts`, its sole owner) — it decides which top-level
|
|
4
|
+
* handle entries are deferred (`Promise`) values to await before any consumer
|
|
5
|
+
* sees them, and which pass through verbatim.
|
|
6
|
+
*
|
|
7
|
+
* Requires a CALLABLE `then` (`typeof obj.then === "function"`), not merely a
|
|
8
|
+
* `"then" in obj` membership check, so a non-promise carrying a `then` field
|
|
9
|
+
* (e.g. a serialized shape `{ then: 5 }`) is treated as a plain sync value rather
|
|
10
|
+
* than awaited. A single predicate keeps that classification consistent.
|
|
11
|
+
*/
|
|
12
|
+
export function isThenable(value: unknown): value is PromiseLike<unknown> {
|
|
13
|
+
return (
|
|
14
|
+
value !== null &&
|
|
15
|
+
typeof value === "object" &&
|
|
16
|
+
typeof (value as { then?: unknown }).then === "function"
|
|
17
|
+
);
|
|
18
|
+
}
|