@rangojs/router 0.0.0-experimental.b9cb8739 → 0.0.0-experimental.bdaf10aa
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 +303 -741
- package/dist/bin/rango.js +730 -184
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +4344 -1335
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +86 -15
- 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 +251 -24
- package/skills/caching/SKILL.md +375 -17
- 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 +364 -0
- 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 +94 -18
- package/skills/layout/SKILL.md +62 -19
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +302 -54
- package/skills/middleware/SKILL.md +59 -16
- package/skills/migrate-nextjs/SKILL.md +745 -0
- package/skills/migrate-react-router/SKILL.md +153 -0
- 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 +225 -10
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +178 -124
- package/skills/rango/SKILL.md +318 -24
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +138 -49
- package/skills/route/SKILL.md +172 -9
- package/skills/router-setup/SKILL.md +131 -11
- 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 -616
- 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 +1 -66
- 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/app-version.ts +14 -0
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +257 -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 +132 -33
- package/src/browser/navigation-client.ts +218 -68
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +203 -80
- package/src/browser/navigation-transaction.ts +18 -66
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +241 -127
- package/src/browser/prefetch/cache.ts +271 -44
- package/src/browser/prefetch/fetch.ts +367 -40
- package/src/browser/prefetch/queue.ts +144 -23
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +121 -16
- package/src/browser/react/NavigationProvider.tsx +240 -122
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- 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 +23 -69
- 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 +46 -11
- 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 +272 -80
- package/src/browser/scroll-restoration.ts +56 -22
- package/src/browser/segment-reconciler.ts +44 -7
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +244 -71
- package/src/browser/types.ts +136 -12
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +207 -158
- package/src/build/generate-route-types.ts +6 -1
- package/src/build/index.ts +11 -3
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +198 -41
- 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 +464 -63
- 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 +336 -110
- package/src/build/route-types/scan-filter.ts +9 -2
- 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 +454 -97
- package/src/cache/cache-scope.ts +235 -103
- 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 +2446 -170
- 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 +11 -17
- package/src/cache/document-cache.ts +144 -49
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +24 -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/taint.ts +55 -0
- package/src/cache/types.ts +194 -99
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +1132 -0
- package/src/client.rsc.tsx +41 -21
- package/src/client.tsx +116 -290
- 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 +84 -2
- package/src/debug.ts +2 -2
- 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 +104 -34
- 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 +100 -13
- package/src/index.ts +143 -19
- 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 +37 -41
- package/src/prerender.ts +215 -86
- 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 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +19 -77
- package/src/route-definition/dsl-helpers.ts +485 -303
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +153 -77
- package/src/route-definition/index.ts +4 -2
- package/src/route-definition/redirect.ts +53 -12
- package/src/route-definition/resolve-handler-use.ts +160 -0
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +48 -21
- package/src/route-types.ts +37 -46
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +164 -17
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +130 -29
- package/src/router/handler-context.ts +83 -39
- package/src/router/instrument.ts +355 -0
- package/src/router/intercept-resolution.ts +50 -24
- package/src/router/lazy-includes.ts +89 -63
- package/src/router/loader-resolution.ts +286 -56
- package/src/router/logging.ts +5 -8
- package/src/router/manifest.ts +105 -56
- package/src/router/match-api.ts +178 -218
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +211 -165
- package/src/router/match-middleware/background-revalidation.ts +66 -22
- package/src/router/match-middleware/cache-lookup.ts +214 -263
- package/src/router/match-middleware/cache-store.ts +105 -50
- package/src/router/match-middleware/intercept-resolution.ts +8 -28
- package/src/router/match-middleware/segment-resolution.ts +52 -18
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +128 -44
- package/src/router/metrics.ts +5 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +301 -177
- package/src/router/navigation-snapshot.ts +133 -0
- 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 +203 -58
- package/src/router/preview-match.ts +35 -103
- package/src/router/request-classification.ts +291 -0
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +256 -0
- package/src/router/router-context.ts +11 -29
- package/src/router/router-interfaces.ts +146 -35
- package/src/router/router-options.ts +202 -15
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +301 -78
- package/src/router/segment-resolution/helpers.ts +115 -30
- package/src/router/segment-resolution/loader-cache.ts +156 -39
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +477 -385
- 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 +8 -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 +10 -63
- package/src/router/url-params.ts +57 -0
- package/src/router.ts +210 -71
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +682 -508
- 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 +127 -31
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +138 -15
- 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 +509 -73
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +287 -113
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +62 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +167 -0
- package/src/segment-system.tsx +449 -132
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +367 -61
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +152 -5
- package/src/server/handle-store.ts +40 -38
- package/src/server/loader-registry.ts +38 -46
- package/src/server/request-context.ts +558 -173
- package/src/ssr/index.tsx +491 -174
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/static-handler.ts +27 -18
- 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 +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +234 -82
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +20 -52
- package/src/types/route-entry.ts +19 -7
- package/src/types/segments.ts +137 -14
- package/src/urls/include-helper.ts +40 -75
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +2 -11
- package/src/urls/path-helper-types.ts +102 -23
- package/src/urls/path-helper.ts +62 -111
- package/src/urls/pattern-types.ts +84 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +98 -154
- package/src/urls/urls-function.ts +1 -19
- package/src/use-loader.tsx +346 -89
- 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 +36 -38
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +130 -85
- 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 +214 -132
- 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 +57 -6
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +15 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugin-types.ts +234 -62
- 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 +49 -98
- package/src/vite/plugins/expose-id-utils.ts +96 -51
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
- 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 +553 -317
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- 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 +46 -37
- package/src/vite/plugins/virtual-entries.ts +138 -27
- package/src/vite/rango.ts +353 -303
- package/src/vite/router-discovery.ts +1090 -166
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +4 -4
- 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 +98 -38
- package/src/vite/utils/shared-utils.ts +144 -44
- 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
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
generateHistoryKey,
|
|
9
9
|
} from "./navigation-store.js";
|
|
10
10
|
import { createEventController } from "./event-controller.js";
|
|
11
|
+
import { validateRedirectOrigin } from "./validate-redirect-origin.js";
|
|
11
12
|
import { createNavigationClient } from "./navigation-client.js";
|
|
12
13
|
import { createServerActionBridge } from "./server-action-bridge.js";
|
|
13
14
|
import { createNavigationBridge } from "./navigation-bridge.js";
|
|
@@ -22,11 +23,17 @@ import type {
|
|
|
22
23
|
import type { EventController } from "./event-controller.js";
|
|
23
24
|
import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
|
|
24
25
|
import { initRangoState } from "./rango-state.js";
|
|
26
|
+
import { registerNavigationStore } from "./navigation-store-handle.js";
|
|
25
27
|
import { initPrefetchCache } from "./prefetch/cache.js";
|
|
28
|
+
import { setPrefetchConcurrency } from "./prefetch/queue.js";
|
|
29
|
+
import { setPrefetchDecoder } from "./prefetch/fetch.js";
|
|
30
|
+
import { setAppVersion } from "./app-version.js";
|
|
26
31
|
import {
|
|
27
32
|
isInterceptSegment,
|
|
28
33
|
splitInterceptSegments,
|
|
29
34
|
} from "./intercept-utils.js";
|
|
35
|
+
import { createAppShellRef } from "./app-shell.js";
|
|
36
|
+
import { bootLog, IS_BROWSER_DEBUG } from "./logging.js";
|
|
30
37
|
|
|
31
38
|
// Vite HMR types are provided by vite/client
|
|
32
39
|
|
|
@@ -111,15 +118,26 @@ export interface BrowserAppContext {
|
|
|
111
118
|
initialTheme?: Theme;
|
|
112
119
|
/** Whether connection warmup is enabled */
|
|
113
120
|
warmupEnabled?: boolean;
|
|
121
|
+
/** Whether the hydrated tree should be wrapped in React.StrictMode */
|
|
122
|
+
strictMode?: boolean;
|
|
114
123
|
/** App version for prefetch version mismatch detection */
|
|
115
124
|
version?: string;
|
|
125
|
+
/**
|
|
126
|
+
* App-shell ref, read through on each render so renderSegments and the
|
|
127
|
+
* NavigationProvider see rootLayout/basename/version without closing over a
|
|
128
|
+
* stale snapshot. Set once from the initial payload and not swapped within a
|
|
129
|
+
* session: a cross-app navigation is a full document load (X-RSC-Reload), so
|
|
130
|
+
* the target app establishes its own shell on load. Theme, warmup, and
|
|
131
|
+
* prefetch TTL are document-lifetime too (see AppShell).
|
|
132
|
+
*/
|
|
133
|
+
appShellRef?: import("./app-shell.js").AppShellRef;
|
|
116
134
|
}
|
|
117
135
|
|
|
118
136
|
// Module-level state for the initialized app
|
|
119
137
|
let browserAppContext: BrowserAppContext | null = null;
|
|
120
138
|
|
|
121
139
|
/**
|
|
122
|
-
* Initialize the browser app. Must be called before rendering
|
|
140
|
+
* Initialize the browser app. Must be called before rendering Rango.
|
|
123
141
|
*
|
|
124
142
|
* This function:
|
|
125
143
|
* - Loads the initial RSC payload from the stream
|
|
@@ -139,7 +157,8 @@ export async function initBrowserApp(
|
|
|
139
157
|
initialTheme,
|
|
140
158
|
} = options;
|
|
141
159
|
|
|
142
|
-
|
|
160
|
+
bootLog("initBrowserApp start");
|
|
161
|
+
bootLog("flight decode: awaiting initial payload from document stream");
|
|
143
162
|
const initialPayload =
|
|
144
163
|
await deps.createFromReadableStream<RscPayload>(rscStream);
|
|
145
164
|
|
|
@@ -153,6 +172,14 @@ export async function initBrowserApp(
|
|
|
153
172
|
// Get initial segments and compute history key from current URL
|
|
154
173
|
const initialSegments = (initialPayload.metadata?.segments ??
|
|
155
174
|
[]) as ResolvedSegment[];
|
|
175
|
+
if (IS_BROWSER_DEBUG) {
|
|
176
|
+
bootLog("initial payload decoded", {
|
|
177
|
+
version: initialPayload.metadata?.version,
|
|
178
|
+
routerId: initialPayload.metadata?.routerId,
|
|
179
|
+
segments: initialSegments.map((s) => s.id),
|
|
180
|
+
matched: initialPayload.metadata?.matched,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
156
183
|
const initialHistoryKey = generateHistoryKey(window.location.href);
|
|
157
184
|
|
|
158
185
|
// Create navigation store with history-based caching
|
|
@@ -164,6 +191,18 @@ export async function initBrowserApp(
|
|
|
164
191
|
...(storeOptions?.cacheSize && { cacheSize: storeOptions.cacheSize }),
|
|
165
192
|
});
|
|
166
193
|
|
|
194
|
+
// Register the active store on the module-level handle and wire the
|
|
195
|
+
// jar-divergence observer before any getRangoState() read can detect a
|
|
196
|
+
// cross-tab/server rotation. There is no global store singleton, so this
|
|
197
|
+
// handle is the live reference.
|
|
198
|
+
registerNavigationStore(store);
|
|
199
|
+
|
|
200
|
+
// Seed router identity from the initial SSR payload so the first
|
|
201
|
+
// cross-app SPA navigation can detect the app switch.
|
|
202
|
+
if (initialPayload.metadata?.routerId) {
|
|
203
|
+
store.setRouterId?.(initialPayload.metadata.routerId);
|
|
204
|
+
}
|
|
205
|
+
|
|
167
206
|
// Create event controller for reactive state management
|
|
168
207
|
const eventController = createEventController({
|
|
169
208
|
initialLocation: new URL(window.location.href),
|
|
@@ -179,11 +218,24 @@ export async function initBrowserApp(
|
|
|
179
218
|
// This ensures useHandle returns correct data during hydration to avoid mismatch
|
|
180
219
|
// The handles property is an async generator that yields on each push
|
|
181
220
|
if (initialPayload.metadata?.handles) {
|
|
221
|
+
// This for-await consumes the handle generator to completion BEFORE
|
|
222
|
+
// hydrateRoot is called — on a streaming/PPR document the generator only
|
|
223
|
+
// ends when its stream side does, so the per-push logs below are the
|
|
224
|
+
// primary probe for "the document render is holding hydration".
|
|
225
|
+
bootLog("handles: consuming payload handle stream (pre-hydration await)");
|
|
182
226
|
const handlesGenerator = initialPayload.metadata.handles;
|
|
183
227
|
let lastHandleData: Record<string, Record<string, unknown[]>> = {};
|
|
228
|
+
let handlePushes = 0;
|
|
184
229
|
for await (const handleData of handlesGenerator) {
|
|
185
230
|
lastHandleData = handleData;
|
|
231
|
+
if (IS_BROWSER_DEBUG) {
|
|
232
|
+
handlePushes += 1;
|
|
233
|
+
bootLog(`handles: push #${handlePushes}`, {
|
|
234
|
+
segments: Object.keys(handleData),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
186
237
|
}
|
|
238
|
+
bootLog("handles: stream complete", { pushes: handlePushes });
|
|
187
239
|
// Initialize event controller with initial handle state before hydration.
|
|
188
240
|
eventController.setHandleData(
|
|
189
241
|
lastHandleData,
|
|
@@ -193,31 +245,62 @@ export async function initBrowserApp(
|
|
|
193
245
|
// Update the initial cache entry with the processed handleData
|
|
194
246
|
// The cache entry was created by createNavigationStore but without handleData
|
|
195
247
|
store.updateCacheHandleData(initialHistoryKey, lastHandleData);
|
|
248
|
+
} else {
|
|
249
|
+
bootLog("handles: none in payload");
|
|
196
250
|
}
|
|
197
251
|
|
|
198
252
|
// Create composable utilities
|
|
199
253
|
const client = createNavigationClient(deps);
|
|
200
254
|
|
|
201
|
-
//
|
|
202
|
-
|
|
255
|
+
// Capture the per-router app-shell. rootLayout, basename, and version live
|
|
256
|
+
// here and are read through the ref at call time rather than closed over.
|
|
257
|
+
// It is set once from the initial payload and not swapped within a session:
|
|
258
|
+
// a cross-app navigation is a full document load (X-RSC-Reload), so the
|
|
259
|
+
// target app establishes its own shell on load.
|
|
203
260
|
const version = initialPayload.metadata?.version;
|
|
261
|
+
const appShellRef = createAppShellRef({
|
|
262
|
+
routerId: initialPayload.metadata?.routerId,
|
|
263
|
+
rootLayout: initialPayload.metadata?.rootLayout,
|
|
264
|
+
basename: initialPayload.metadata?.basename,
|
|
265
|
+
version,
|
|
266
|
+
});
|
|
204
267
|
|
|
205
|
-
// Initialize the
|
|
206
|
-
//
|
|
207
|
-
|
|
268
|
+
// Initialize the rango state cookie for cache invalidation. The build version
|
|
269
|
+
// busts cached prefetches on deploy; the server-resolved cookie name
|
|
270
|
+
// namespaces the cookie so sibling apps on the same origin don't collide
|
|
271
|
+
// (falls back to the bare default prefix if metadata lacks the name).
|
|
272
|
+
initRangoState(version ?? "0", initialPayload.metadata?.stateCookieName);
|
|
273
|
+
setAppVersion(version);
|
|
208
274
|
|
|
209
|
-
// Initialize the in-memory prefetch cache TTL
|
|
210
|
-
// A
|
|
275
|
+
// Initialize the in-memory prefetch cache (TTL + max size) and the prefetch
|
|
276
|
+
// queue concurrency from server config. A TTL of 0 disables the cache;
|
|
277
|
+
// undefined values fall back to the module defaults.
|
|
211
278
|
const prefetchCacheTTL = initialPayload.metadata?.prefetchCacheTTL;
|
|
212
|
-
|
|
213
|
-
|
|
279
|
+
const prefetchCacheSize = initialPayload.metadata?.prefetchCacheSize;
|
|
280
|
+
if (prefetchCacheTTL !== undefined || prefetchCacheSize !== undefined) {
|
|
281
|
+
initPrefetchCache(prefetchCacheTTL, prefetchCacheSize);
|
|
282
|
+
}
|
|
283
|
+
const prefetchConcurrency = initialPayload.metadata?.prefetchConcurrency;
|
|
284
|
+
if (prefetchConcurrency !== undefined) {
|
|
285
|
+
setPrefetchConcurrency(prefetchConcurrency);
|
|
214
286
|
}
|
|
215
287
|
|
|
216
|
-
//
|
|
288
|
+
// Wire the RSC decoder so prefetches decode eagerly and warm the route's
|
|
289
|
+
// client chunks (same createFromFetch the navigation client uses).
|
|
290
|
+
setPrefetchDecoder((response) => deps.createFromFetch<RscPayload>(response));
|
|
291
|
+
|
|
292
|
+
// Create a bound renderSegments that reads rootLayout through the shell ref.
|
|
293
|
+
// The shell is set once at init and not swapped within a session (a cross-app
|
|
294
|
+
// navigation is a full document load), so this always renders this app's
|
|
295
|
+
// Document; reading through the ref just avoids closing over a stale value.
|
|
217
296
|
const renderSegments = (
|
|
218
297
|
segments: ResolvedSegment[],
|
|
219
298
|
options?: RenderSegmentsOptions,
|
|
220
|
-
) =>
|
|
299
|
+
) =>
|
|
300
|
+
baseRenderSegments(segments, {
|
|
301
|
+
...options,
|
|
302
|
+
rootLayout: appShellRef.get().rootLayout,
|
|
303
|
+
});
|
|
221
304
|
|
|
222
305
|
// Lazy reference for navigation bridge — the action bridge is created first
|
|
223
306
|
// but may need to trigger SPA navigation for action redirects.
|
|
@@ -231,10 +314,15 @@ export async function initBrowserApp(
|
|
|
231
314
|
deps,
|
|
232
315
|
onUpdate: (update) => store.emitUpdate(update),
|
|
233
316
|
renderSegments,
|
|
234
|
-
version,
|
|
235
317
|
onNavigate: (url, options) => {
|
|
236
318
|
if (!navigateFn) {
|
|
237
|
-
|
|
319
|
+
// Navigation bridge not wired yet: hard-navigate, but re-validate
|
|
320
|
+
// same-origin defensively so this init-window fallback cannot become an
|
|
321
|
+
// open redirect (the normal path validates inside the navigation bridge).
|
|
322
|
+
const safe = validateRedirectOrigin(url, window.location.origin);
|
|
323
|
+
if (safe) {
|
|
324
|
+
window.location.href = safe;
|
|
325
|
+
}
|
|
238
326
|
return Promise.resolve();
|
|
239
327
|
}
|
|
240
328
|
return navigateFn(url, options);
|
|
@@ -249,7 +337,7 @@ export async function initBrowserApp(
|
|
|
249
337
|
client,
|
|
250
338
|
onUpdate: (update) => store.emitUpdate(update),
|
|
251
339
|
renderSegments,
|
|
252
|
-
version,
|
|
340
|
+
version: version,
|
|
253
341
|
});
|
|
254
342
|
|
|
255
343
|
// Connect action redirect → navigation bridge (now that both are initialized)
|
|
@@ -259,79 +347,169 @@ export async function initBrowserApp(
|
|
|
259
347
|
if (linkInterception) {
|
|
260
348
|
navigationBridge.registerLinkInterception();
|
|
261
349
|
}
|
|
350
|
+
bootLog("bridges registered (action + navigation)");
|
|
262
351
|
|
|
263
352
|
// Build initial tree with rootLayout
|
|
353
|
+
bootLog("building initial segment tree (renderSegments)");
|
|
264
354
|
const initialTree = renderSegments(initialPayload.metadata!.segments);
|
|
355
|
+
if (IS_BROWSER_DEBUG && initialTree instanceof Promise) {
|
|
356
|
+
initialTree.then(
|
|
357
|
+
() => bootLog("initial segment tree settled"),
|
|
358
|
+
(err: unknown) => bootLog("initial segment tree rejected", { err }),
|
|
359
|
+
);
|
|
360
|
+
}
|
|
265
361
|
|
|
266
|
-
// Setup HMR
|
|
362
|
+
// Setup HMR with debounce — burst saves (format-on-save, rapid edits)
|
|
363
|
+
// fire many rsc:update events in quick succession. Without debouncing,
|
|
364
|
+
// each event triggers a fetchPartial() which on slow routes can pile up
|
|
365
|
+
// and overwhelm the worker (cross-request promise issues, 500s).
|
|
267
366
|
if (import.meta.hot) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
367
|
+
let hmrTimer: ReturnType<typeof setTimeout> | null = null;
|
|
368
|
+
let hmrAbort: AbortController | null = null;
|
|
369
|
+
|
|
370
|
+
import.meta.hot.on("rsc:update", () => {
|
|
371
|
+
// Cancel any pending debounce timer
|
|
372
|
+
if (hmrTimer !== null) {
|
|
373
|
+
clearTimeout(hmrTimer);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Abort any in-flight HMR fetch so it doesn't race with the next one
|
|
377
|
+
if (hmrAbort) {
|
|
378
|
+
hmrAbort.abort();
|
|
379
|
+
hmrAbort = null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Debounce: wait 200ms of quiet before fetching
|
|
383
|
+
hmrTimer = setTimeout(async () => {
|
|
384
|
+
hmrTimer = null;
|
|
385
|
+
|
|
386
|
+
// Don't interrupt an active user navigation — startNavigation()
|
|
387
|
+
// would abort it and refetch the old URL (window.location.href
|
|
388
|
+
// hasn't updated yet). The user's navigation will pick up the
|
|
389
|
+
// new server code when it completes. isNavigating covers the
|
|
390
|
+
// full lifecycle (fetching + streaming, before commit) without
|
|
391
|
+
// blocking on server actions.
|
|
392
|
+
if (eventController.getState().isNavigating) {
|
|
393
|
+
console.log("[Rango] HMR: Skipping — navigation in progress");
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
console.log("[Rango] HMR: Server update, refetching RSC");
|
|
398
|
+
|
|
399
|
+
const abort = new AbortController();
|
|
400
|
+
hmrAbort = abort;
|
|
401
|
+
|
|
402
|
+
const handle = eventController.startNavigation(window.location.href, {
|
|
403
|
+
replace: true,
|
|
285
404
|
});
|
|
405
|
+
const streamingToken = handle.startStreaming();
|
|
406
|
+
|
|
407
|
+
const interceptSourceUrl = store.getInterceptSourceUrl();
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
const { payload, streamComplete } = await client.fetchPartial({
|
|
411
|
+
targetUrl: window.location.href,
|
|
412
|
+
segmentIds: [],
|
|
413
|
+
previousUrl: store.getSegmentState().currentUrl,
|
|
414
|
+
interceptSourceUrl: interceptSourceUrl || undefined,
|
|
415
|
+
routerId: store.getRouterId?.(),
|
|
416
|
+
hmr: true,
|
|
417
|
+
signal: abort.signal,
|
|
418
|
+
});
|
|
286
419
|
|
|
287
|
-
|
|
288
|
-
const segments = payload.metadata.segments || [];
|
|
289
|
-
const matched = payload.metadata.matched || [];
|
|
420
|
+
if (abort.signal.aborted) return;
|
|
290
421
|
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
|
|
422
|
+
// If the server returned a non-RSC response (404, 500 without
|
|
423
|
+
// error boundary), the payload won't have valid metadata.
|
|
424
|
+
// Reload to recover rather than leaving the page stale.
|
|
425
|
+
if (!payload.metadata) {
|
|
426
|
+
throw new Error("HMR refetch returned invalid payload");
|
|
427
|
+
}
|
|
295
428
|
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
429
|
+
// Update version BEFORE rebuilding state so that
|
|
430
|
+
// clearHistoryCache() runs first, then the fresh segment
|
|
431
|
+
// cache entry we create below survives.
|
|
432
|
+
//
|
|
433
|
+
// Compare against the bridge's live version, not the init-time
|
|
434
|
+
// `version` const: after the first HMR bump the const is stale, so a
|
|
435
|
+
// later update with an unchanged version would otherwise re-clear the
|
|
436
|
+
// cache and re-broadcast across tabs/apps. The live read fires only
|
|
437
|
+
// on a genuine version change.
|
|
438
|
+
const newVersion = payload.metadata.version;
|
|
439
|
+
const currentVersion = navigationBridge.getVersion();
|
|
440
|
+
if (newVersion && newVersion !== currentVersion) {
|
|
441
|
+
console.log(
|
|
442
|
+
"[Rango] HMR: version changed",
|
|
443
|
+
currentVersion,
|
|
444
|
+
"→",
|
|
445
|
+
newVersion,
|
|
446
|
+
"clearing caches",
|
|
447
|
+
);
|
|
448
|
+
navigationBridge.updateVersion(newVersion);
|
|
299
449
|
}
|
|
300
450
|
|
|
301
|
-
|
|
302
|
-
|
|
451
|
+
// Apply only partial segment updates. A non-partial payload during
|
|
452
|
+
// HMR is transient: the worker route table is still rebuilding after
|
|
453
|
+
// the edit, so the URL momentarily resolves to not-found/catch-all.
|
|
454
|
+
// Skip it -- the debounced follow-up refetch returns the settled
|
|
455
|
+
// route's partial payload and renders it below. We never reload here:
|
|
456
|
+
// a paramless document GET would run the SSR path and surface the
|
|
457
|
+
// not-found page during that same transient.
|
|
458
|
+
if (payload.metadata?.isPartial) {
|
|
459
|
+
const segments = payload.metadata.segments || [];
|
|
460
|
+
const matched = payload.metadata.matched || [];
|
|
461
|
+
|
|
462
|
+
// Derive intercept state from the returned payload, not the
|
|
463
|
+
// pre-fetch store snapshot. If the HMR edit removed intercept
|
|
464
|
+
// behavior, the response won't contain intercept segments.
|
|
465
|
+
const responseIsIntercept = segments.some(isInterceptSegment);
|
|
466
|
+
|
|
467
|
+
// Sync store intercept state with what the server returned
|
|
468
|
+
if (!responseIsIntercept && interceptSourceUrl) {
|
|
469
|
+
store.setInterceptSourceUrl(null);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
store.setSegmentIds(matched);
|
|
473
|
+
store.setCurrentUrl(window.location.href);
|
|
474
|
+
|
|
475
|
+
const historyKey = generateHistoryKey(window.location.href, {
|
|
476
|
+
intercept: responseIsIntercept,
|
|
477
|
+
});
|
|
478
|
+
store.setHistoryKey(historyKey);
|
|
479
|
+
const currentHandleData = eventController.getHandleState().data;
|
|
480
|
+
store.cacheSegmentsForHistory(
|
|
481
|
+
historyKey,
|
|
482
|
+
segments,
|
|
483
|
+
currentHandleData,
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
const { main, intercept } = splitInterceptSegments(segments);
|
|
487
|
+
store.emitUpdate({
|
|
488
|
+
root: renderSegments(main, {
|
|
489
|
+
interceptSegments: intercept.length > 0 ? intercept : undefined,
|
|
490
|
+
}),
|
|
491
|
+
metadata: payload.metadata,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
303
494
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
store.emitUpdate({
|
|
317
|
-
root: renderSegments(main, {
|
|
318
|
-
interceptSegments: intercept.length > 0 ? intercept : undefined,
|
|
319
|
-
}),
|
|
320
|
-
metadata: payload.metadata,
|
|
321
|
-
});
|
|
495
|
+
await streamComplete;
|
|
496
|
+
handle.complete(new URL(window.location.href));
|
|
497
|
+
console.log("[Rango] HMR: RSC stream complete");
|
|
498
|
+
} catch (err) {
|
|
499
|
+
if (abort.signal.aborted) return;
|
|
500
|
+
console.warn("[Rango] HMR: Refetch failed, reloading page", err);
|
|
501
|
+
window.location.reload();
|
|
502
|
+
return;
|
|
503
|
+
} finally {
|
|
504
|
+
if (hmrAbort === abort) hmrAbort = null;
|
|
505
|
+
streamingToken.end();
|
|
506
|
+
handle[Symbol.dispose]();
|
|
322
507
|
}
|
|
323
|
-
|
|
324
|
-
await streamComplete;
|
|
325
|
-
handle.complete(new URL(window.location.href));
|
|
326
|
-
console.log("[RSCRouter] HMR: RSC stream complete");
|
|
327
|
-
} finally {
|
|
328
|
-
streamingToken.end();
|
|
329
|
-
handle[Symbol.dispose]();
|
|
330
|
-
}
|
|
508
|
+
}, 200);
|
|
331
509
|
});
|
|
332
510
|
}
|
|
333
511
|
|
|
334
|
-
// Store context for
|
|
512
|
+
// Store context for Rango component
|
|
335
513
|
const context: BrowserAppContext = {
|
|
336
514
|
store,
|
|
337
515
|
eventController,
|
|
@@ -341,20 +519,27 @@ export async function initBrowserApp(
|
|
|
341
519
|
themeConfig: effectiveThemeConfig,
|
|
342
520
|
initialTheme: effectiveInitialTheme,
|
|
343
521
|
warmupEnabled: initialPayload.metadata?.warmupEnabled ?? true,
|
|
522
|
+
strictMode: initialPayload.metadata?.strictMode ?? true,
|
|
344
523
|
version,
|
|
524
|
+
appShellRef,
|
|
345
525
|
};
|
|
346
526
|
browserAppContext = context;
|
|
347
527
|
|
|
528
|
+
bootLog("initBrowserApp complete -- handing off to hydrateRoot");
|
|
348
529
|
return context;
|
|
349
530
|
}
|
|
350
531
|
|
|
532
|
+
// Once-flag so the hydration-commit boot log fires a single time (StrictMode
|
|
533
|
+
// re-runs the root effect; the second flush is not a second hydration).
|
|
534
|
+
let hydrationCommitLogged = false;
|
|
535
|
+
|
|
351
536
|
/**
|
|
352
537
|
* Get the browser app context. Throws if initBrowserApp hasn't been called.
|
|
353
538
|
*/
|
|
354
539
|
export function getBrowserAppContext(): BrowserAppContext {
|
|
355
540
|
if (!browserAppContext) {
|
|
356
541
|
throw new Error(
|
|
357
|
-
"
|
|
542
|
+
"Rango: initBrowserApp() must be called before rendering Rango",
|
|
358
543
|
);
|
|
359
544
|
}
|
|
360
545
|
return browserAppContext;
|
|
@@ -368,18 +553,18 @@ export function resetBrowserAppContext(): void {
|
|
|
368
553
|
}
|
|
369
554
|
|
|
370
555
|
/**
|
|
371
|
-
* Props for the
|
|
556
|
+
* Props for the Rango component
|
|
372
557
|
*/
|
|
373
|
-
export interface
|
|
558
|
+
export interface RangoProps {}
|
|
374
559
|
|
|
375
560
|
/**
|
|
376
|
-
*
|
|
561
|
+
* Rango component - renders the RSC router with all internal wiring.
|
|
377
562
|
*
|
|
378
563
|
* Must be called after initBrowserApp() has completed.
|
|
379
564
|
*
|
|
380
565
|
* @example
|
|
381
566
|
* ```tsx
|
|
382
|
-
* import { initBrowserApp,
|
|
567
|
+
* import { initBrowserApp, Rango } from "rsc-router/browser";
|
|
383
568
|
* import { rscStream } from "rsc-html-stream/client";
|
|
384
569
|
* import * as rscBrowser from "@vitejs/plugin-rsc/browser";
|
|
385
570
|
*
|
|
@@ -389,14 +574,14 @@ export interface RSCRouterProps {}
|
|
|
389
574
|
* hydrateRoot(
|
|
390
575
|
* document,
|
|
391
576
|
* <React.StrictMode>
|
|
392
|
-
* <
|
|
577
|
+
* <Rango />
|
|
393
578
|
* </React.StrictMode>
|
|
394
579
|
* );
|
|
395
580
|
* }
|
|
396
581
|
* main();
|
|
397
582
|
* ```
|
|
398
583
|
*/
|
|
399
|
-
export function
|
|
584
|
+
export function Rango(_props: RangoProps): React.ReactElement {
|
|
400
585
|
const {
|
|
401
586
|
store,
|
|
402
587
|
eventController,
|
|
@@ -407,6 +592,7 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
|
|
|
407
592
|
initialTheme,
|
|
408
593
|
warmupEnabled,
|
|
409
594
|
version,
|
|
595
|
+
appShellRef,
|
|
410
596
|
} = getBrowserAppContext();
|
|
411
597
|
|
|
412
598
|
// Signal that the React tree has hydrated. useEffect only fires after
|
|
@@ -414,6 +600,10 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
|
|
|
414
600
|
// that does not depend on React internals like __reactFiber.
|
|
415
601
|
React.useEffect(() => {
|
|
416
602
|
document.documentElement.dataset.hydrated = "";
|
|
603
|
+
if (IS_BROWSER_DEBUG && !hydrationCommitLogged) {
|
|
604
|
+
hydrationCommitLogged = true;
|
|
605
|
+
bootLog("hydration commit (root effect flushed)");
|
|
606
|
+
}
|
|
417
607
|
}, []);
|
|
418
608
|
|
|
419
609
|
return (
|
|
@@ -426,6 +616,8 @@ export function RSCRouter(_props: RSCRouterProps): React.ReactElement {
|
|
|
426
616
|
initialTheme={initialTheme}
|
|
427
617
|
warmupEnabled={warmupEnabled}
|
|
428
618
|
version={version}
|
|
619
|
+
basename={initialPayload.metadata?.basename}
|
|
620
|
+
appShellRef={appShellRef}
|
|
429
621
|
/>
|
|
430
622
|
);
|
|
431
623
|
}
|
|
@@ -8,7 +8,16 @@
|
|
|
8
8
|
* - Supports hash link scrolling
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { debugLog } from "./logging.js";
|
|
11
|
+
import { debugLog, IS_BROWSER_DEBUG } from "./logging.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Defers a callback to the next animation frame.
|
|
15
|
+
* Falls back to setTimeout(0) in environments without requestAnimationFrame.
|
|
16
|
+
*/
|
|
17
|
+
const deferToNextPaint: (fn: () => void) => void =
|
|
18
|
+
typeof requestAnimationFrame === "function"
|
|
19
|
+
? requestAnimationFrame
|
|
20
|
+
: (fn) => setTimeout(fn, 0);
|
|
12
21
|
|
|
13
22
|
const SCROLL_STORAGE_KEY = "rsc-router-scroll-positions";
|
|
14
23
|
|
|
@@ -141,10 +150,12 @@ export function initScrollRestoration(options?: {
|
|
|
141
150
|
|
|
142
151
|
window.addEventListener("pagehide", handlePageHide);
|
|
143
152
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
153
|
+
if (IS_BROWSER_DEBUG) {
|
|
154
|
+
debugLog(
|
|
155
|
+
"[Scroll] Initialized, loaded positions:",
|
|
156
|
+
Object.keys(savedScrollPositions).length,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
148
159
|
|
|
149
160
|
return () => {
|
|
150
161
|
cancelScrollRestorationPolling();
|
|
@@ -182,10 +193,15 @@ export function saveCurrentScrollPosition(): void {
|
|
|
182
193
|
|
|
183
194
|
/**
|
|
184
195
|
* Persist scroll positions to sessionStorage.
|
|
185
|
-
* If the write fails
|
|
186
|
-
* entries and retry
|
|
196
|
+
* If the write fails (typically QuotaExceededError), evict the oldest ~1/4 of
|
|
197
|
+
* entries ONCE and retry the write a single time; if that still fails, remove
|
|
198
|
+
* our storage key entirely so we don't block other sessionStorage consumers.
|
|
199
|
+
* This is a single evict-then-retry-then-clear ladder, not a loop.
|
|
200
|
+
*
|
|
201
|
+
* Exported so that single eviction/retry/clear ladder is unit-testable directly.
|
|
202
|
+
* The browser drives it from the `pagehide` handler.
|
|
187
203
|
*/
|
|
188
|
-
function persistToSessionStorage(): void {
|
|
204
|
+
export function persistToSessionStorage(): void {
|
|
189
205
|
try {
|
|
190
206
|
sessionStorage.setItem(
|
|
191
207
|
SCROLL_STORAGE_KEY,
|
|
@@ -285,9 +301,13 @@ export function restoreScrollPosition(options?: {
|
|
|
285
301
|
return true;
|
|
286
302
|
}
|
|
287
303
|
|
|
288
|
-
// Not streaming — scroll
|
|
289
|
-
|
|
290
|
-
|
|
304
|
+
// Not streaming — scroll after React commits and browser paints.
|
|
305
|
+
// startTransition defers the DOM commit, so scrolling synchronously
|
|
306
|
+
// would be overwritten when React replaces the content.
|
|
307
|
+
deferToNextPaint(() => {
|
|
308
|
+
window.scrollTo(0, savedY);
|
|
309
|
+
debugLog("[Scroll] Restored position:", savedY, "for key:", key);
|
|
310
|
+
});
|
|
291
311
|
return true;
|
|
292
312
|
}
|
|
293
313
|
|
|
@@ -319,6 +339,8 @@ export function scrollToHash(): boolean {
|
|
|
319
339
|
* Scroll to top of page
|
|
320
340
|
*/
|
|
321
341
|
export function scrollToTop(): void {
|
|
342
|
+
if (typeof window === "undefined") return;
|
|
343
|
+
if (typeof window.scrollTo !== "function") return;
|
|
322
344
|
window.scrollTo(0, 0);
|
|
323
345
|
}
|
|
324
346
|
|
|
@@ -343,31 +365,43 @@ export function handleNavigationEnd(options: {
|
|
|
343
365
|
scroll?: boolean;
|
|
344
366
|
isStreaming?: () => boolean;
|
|
345
367
|
}): void {
|
|
346
|
-
if (!initialized) {
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
368
|
const { restore = false, scroll = true, isStreaming } = options;
|
|
351
369
|
|
|
352
|
-
// Don't scroll if explicitly disabled
|
|
353
|
-
if (scroll === false) {
|
|
370
|
+
// Don't scroll if explicitly disabled or not in a browser
|
|
371
|
+
if (scroll === false || typeof window === "undefined") {
|
|
354
372
|
return;
|
|
355
373
|
}
|
|
356
374
|
|
|
357
|
-
//
|
|
358
|
-
|
|
375
|
+
// Save/restore requires initialization (sessionStorage, history state).
|
|
376
|
+
// But basic scroll-to-top and hash scrolling work without it — this
|
|
377
|
+
// matters during cross-app navigation where ScrollRestoration unmounts
|
|
378
|
+
// and remounts, creating a brief window where initialized is false.
|
|
379
|
+
if (restore && initialized) {
|
|
359
380
|
if (restoreScrollPosition({ retryIfStreaming: true, isStreaming })) {
|
|
360
381
|
return;
|
|
361
382
|
}
|
|
362
383
|
// Fall through to hash or top if no saved position
|
|
363
384
|
}
|
|
364
385
|
|
|
365
|
-
//
|
|
386
|
+
// scrollToHash / scrollToTop run synchronously here.
|
|
387
|
+
// handleNavigationEnd is invoked from NavigationProvider's
|
|
388
|
+
// useLayoutEffect (post-commit, pre-paint), so a sync scrollTo is
|
|
389
|
+
// captured by the upcoming paint AND by startViewTransition's snapshot.
|
|
390
|
+
// Deferring via rAF here pushed the call past the snapshot capture,
|
|
391
|
+
// making forward navigations wrapped in a layout/route view transition
|
|
392
|
+
// skip scroll-to-top — the live DOM scrolled but the captured snapshot
|
|
393
|
+
// was at the previous scroll position, so the user-facing page stayed
|
|
394
|
+
// visually clamped at the source page's scrollY (often the new tree's
|
|
395
|
+
// max scroll for tall→short navs). Y=0 / a hash element are robust
|
|
396
|
+
// against unmeasured layout, so sync scroll is correct here even
|
|
397
|
+
// before the new tree's scrollHeight settles.
|
|
398
|
+
//
|
|
399
|
+
// (The restore branch above keeps deferToNextPaint because savedY
|
|
400
|
+
// depends on the new tree's max scroll; sync scrollTo against an
|
|
401
|
+
// unmeasured DOM would clamp savedY to whatever the old/zero max was.)
|
|
366
402
|
if (scrollToHash()) {
|
|
367
403
|
return;
|
|
368
404
|
}
|
|
369
|
-
|
|
370
|
-
// Default: scroll to top
|
|
371
405
|
scrollToTop();
|
|
372
406
|
}
|
|
373
407
|
|