@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/defer.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deferred handle values — "decide synchronously, resolve late".
|
|
3
|
+
*
|
|
4
|
+
* A handle is pushed from code that holds `ctx` (a route/layout handler), so the
|
|
5
|
+
* decision to push lands before the handles stream seals. But the value often
|
|
6
|
+
* isn't known there — it may come from a deep async component far from the
|
|
7
|
+
* handler. `ctx.use(Handle).defer()` reserves the handle's slot now (synchronous,
|
|
8
|
+
* so ordering and the pre-seal timing hold) and returns a resolver with the SAME
|
|
9
|
+
* signature as the push: you call it later, anywhere in the render, with the same
|
|
10
|
+
* value you would have passed to the push.
|
|
11
|
+
*
|
|
12
|
+
* const breadcrumb = ctx.use(Breadcrumbs); // (item) => void & .defer()
|
|
13
|
+
* const resolve = breadcrumb.defer({ timeoutMs: 5000, else: null });
|
|
14
|
+
* // deep async component, far from ctx:
|
|
15
|
+
* resolve({ label, href, content }); // identical call, just deferred
|
|
16
|
+
*
|
|
17
|
+
* Under the hood the reserved slot is a Promise. Handle values are resolved
|
|
18
|
+
* before any consumer sees them (resolve-by-default: the full render resolves
|
|
19
|
+
* server-side, navigation resolves client-side before apply), so `useHandle`
|
|
20
|
+
* receives the resolved value, never the Promise. The hazard that guards against
|
|
21
|
+
* bugs: a deferred slot whose resolver is never called would keep the render —
|
|
22
|
+
* and the HTTP response — waiting forever. So a deferred auto-resolves to `else`
|
|
23
|
+
* after `timeoutMs` (default {@link DEFAULT_DEFER_TIMEOUT_MS}) if the resolver is
|
|
24
|
+
* never called, degrading gracefully (and warning in dev) instead of hanging.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Default auto-resolve window. Long enough for genuine deep async work, short
|
|
28
|
+
* enough that a forgotten resolve does not hang the response indefinitely. */
|
|
29
|
+
export const DEFAULT_DEFER_TIMEOUT_MS = 10_000;
|
|
30
|
+
|
|
31
|
+
/** Options for `ctx.use(Handle).defer()`. */
|
|
32
|
+
export interface DeferOptions<TData> {
|
|
33
|
+
/**
|
|
34
|
+
* Auto-resolve to `else` after this many ms if the resolver is never called,
|
|
35
|
+
* so a forgotten resolve cannot hold the Flight stream — and thus the HTTP
|
|
36
|
+
* response — open. Defaults to {@link DEFAULT_DEFER_TIMEOUT_MS}. `0` or
|
|
37
|
+
* `Infinity` disable the timeout intentionally (not recommended on a request
|
|
38
|
+
* path). Any other non-finite or negative value is treated as a mistake and
|
|
39
|
+
* falls back to the default rather than silently disabling the safety net.
|
|
40
|
+
* Named `timeoutMs` to match the router's `*Ms` duration convention.
|
|
41
|
+
*/
|
|
42
|
+
timeoutMs?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Value the slot resolves to if the timeout fires before the resolver is
|
|
45
|
+
* called. Defaults to `undefined` (the deferred item is skipped/empty). For
|
|
46
|
+
* renderable handle content, `null` is the usual graceful fallback, so the
|
|
47
|
+
* type admits `null` even when `TData` does not.
|
|
48
|
+
*/
|
|
49
|
+
else?: TData | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The call signature shared by a handle push and the resolver returned by
|
|
54
|
+
* `.defer()`: a concrete value, a `Promise` of the value (Flight streams it as a
|
|
55
|
+
* late row), or a thunk returning a `Promise` (called immediately).
|
|
56
|
+
*/
|
|
57
|
+
export type HandlePushFn<TData> = (
|
|
58
|
+
data: TData | Promise<TData> | (() => Promise<TData>),
|
|
59
|
+
) => void;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The push function returned by `ctx.use(Handle)`. Call it to push a value now,
|
|
63
|
+
* or call `.defer()` to reserve the slot now and resolve the value later (e.g.
|
|
64
|
+
* from a deep async component) with a timeout safety net.
|
|
65
|
+
*/
|
|
66
|
+
export type HandlePush<TData> = HandlePushFn<TData> & {
|
|
67
|
+
/**
|
|
68
|
+
* Reserve this handle's slot synchronously and return a resolver that is
|
|
69
|
+
* push-equal: it takes the same argument shapes as the push (value, Promise, or
|
|
70
|
+
* thunk) and behaves identically. Two things the resolver adds over a direct
|
|
71
|
+
* push: a timeout (if the resolver is never called, the slot auto-resolves to
|
|
72
|
+
* `options.else` after `options.timeoutMs`; calling the resolver cancels it),
|
|
73
|
+
* and — on the action/revalidation path only — a thunk it runs does NOT
|
|
74
|
+
* re-enter the deadlock-guard push-callback scope a direct push thunk gets,
|
|
75
|
+
* because a deferred resolver fires after the handler phase has closed.
|
|
76
|
+
*
|
|
77
|
+
* The reserved slot is resolved before any consumer reads it
|
|
78
|
+
* (resolve-by-default), so `useHandle` receives the resolved value (or the
|
|
79
|
+
* `else` fallback on timeout), never a Promise.
|
|
80
|
+
*/
|
|
81
|
+
defer(options?: DeferOptions<TData>): HandlePushFn<TData>;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Internal: a timeout-bounded { promise, resolve }. Not part of the public API
|
|
85
|
+
// (the public surface is `ctx.use(Handle).defer()`); exported for `withDefer`
|
|
86
|
+
// and unit tests only. Resolves to `T`, the `else` fallback, or `undefined`.
|
|
87
|
+
export function createDeferred<T>(options?: {
|
|
88
|
+
timeoutMs?: number;
|
|
89
|
+
fallback?: T | null;
|
|
90
|
+
}): {
|
|
91
|
+
promise: Promise<T | null | undefined>;
|
|
92
|
+
resolve: (value: T | null | undefined) => void;
|
|
93
|
+
} {
|
|
94
|
+
let resolveInner!: (value: T | null | undefined) => void;
|
|
95
|
+
let settled = false;
|
|
96
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
97
|
+
|
|
98
|
+
const promise = new Promise<T | null | undefined>((resolve) => {
|
|
99
|
+
resolveInner = resolve;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const finish = (value: T | null | undefined): void => {
|
|
103
|
+
if (settled) return;
|
|
104
|
+
settled = true;
|
|
105
|
+
if (timer !== undefined) {
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
timer = undefined;
|
|
108
|
+
}
|
|
109
|
+
resolveInner(value);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// 0 and Infinity are documented intentional disables. Any other non-finite or
|
|
113
|
+
// negative value (NaN, -1, a bad parsed config/env) is a mistake — fall back to
|
|
114
|
+
// the default rather than SILENTLY disabling the safety net, which would let a
|
|
115
|
+
// forgotten resolve hang the Flight stream and the response forever.
|
|
116
|
+
const requested = options?.timeoutMs ?? DEFAULT_DEFER_TIMEOUT_MS;
|
|
117
|
+
let ms: number;
|
|
118
|
+
if (requested === 0 || requested === Infinity) {
|
|
119
|
+
ms = requested;
|
|
120
|
+
} else if (Number.isFinite(requested) && requested > 0) {
|
|
121
|
+
ms = requested;
|
|
122
|
+
} else {
|
|
123
|
+
if (process.env.NODE_ENV !== "production") {
|
|
124
|
+
console.warn(
|
|
125
|
+
`[rango] defer(): invalid timeout ${String(requested)}; using the ` +
|
|
126
|
+
`${DEFAULT_DEFER_TIMEOUT_MS}ms default so the safety net stays on. ` +
|
|
127
|
+
`Use 0 or Infinity to disable the timeout intentionally.`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
ms = DEFAULT_DEFER_TIMEOUT_MS;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (ms > 0 && ms !== Infinity) {
|
|
134
|
+
timer = setTimeout(() => {
|
|
135
|
+
if (process.env.NODE_ENV !== "production") {
|
|
136
|
+
console.warn(
|
|
137
|
+
`[rango] A deferred handle value was not resolved within ${ms}ms; ` +
|
|
138
|
+
`resolving to the fallback so the response can flush. Call the ` +
|
|
139
|
+
`resolver from the component that produces the value, or raise timeoutMs.`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
finish(options?.fallback);
|
|
143
|
+
}, ms);
|
|
144
|
+
// Don't let a pending timer alone keep a Node process alive (no-op on workerd).
|
|
145
|
+
(timer as { unref?: () => void }).unref?.();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { promise, resolve: finish };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Attach `.defer()` to a handle push function. The deferred slot is reserved by
|
|
153
|
+
* pushing the deferred promise through the same push (so ordering, sealing, and
|
|
154
|
+
* Flight streaming all reuse the existing path); the returned resolver settles it.
|
|
155
|
+
*/
|
|
156
|
+
export function withDefer<TData>(push: HandlePushFn<TData>): HandlePush<TData> {
|
|
157
|
+
const handlePush = push as HandlePush<TData>;
|
|
158
|
+
// Safe to mutate push in place: each ctx.use(Handle) call (request-context.ts,
|
|
159
|
+
// loader-resolution.ts) builds a fresh closure, so .defer never leaks across
|
|
160
|
+
// handles or requests.
|
|
161
|
+
handlePush.defer = (options) => {
|
|
162
|
+
const deferred = createDeferred<TData>({
|
|
163
|
+
timeoutMs: options?.timeoutMs,
|
|
164
|
+
fallback: options?.else,
|
|
165
|
+
});
|
|
166
|
+
// Reserve the slot now by pushing the pending promise (the renderer use()s it).
|
|
167
|
+
push(deferred.promise as Promise<TData>);
|
|
168
|
+
// The resolver is push-equal: a thunk is invoked immediately (as push does)
|
|
169
|
+
// and a Promise is adopted by the reserved slot. Calling it settles the slot
|
|
170
|
+
// and cancels the timeout — the timeout only fires if it is never called.
|
|
171
|
+
const resolveSlot = deferred.resolve as (
|
|
172
|
+
value: TData | Promise<TData>,
|
|
173
|
+
) => void;
|
|
174
|
+
return (data) => {
|
|
175
|
+
// The thunk runs without re-entering the push-callback scope a direct push
|
|
176
|
+
// thunk gets on the action/revalidation path (loader-resolution.ts): a
|
|
177
|
+
// deferred resolver fires from a deep component after the handler phase has
|
|
178
|
+
// closed, so there is no live deadlock-guard window to exempt.
|
|
179
|
+
resolveSlot(
|
|
180
|
+
typeof data === "function" ? (data as () => Promise<TData>)() : data,
|
|
181
|
+
);
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
return handlePush;
|
|
185
|
+
}
|
package/src/deps/ssr.ts
CHANGED
package/src/encode-kv.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared key=value serialization core.
|
|
3
|
+
*
|
|
4
|
+
* One encode+join rule shared by every deterministic query-string serializer
|
|
5
|
+
* in the codebase (cache keys, route-param strings, search-param strings,
|
|
6
|
+
* prerender param hashes). Each call site differs only in how it pre-filters
|
|
7
|
+
* and orders the pairs it hands in, plus whether it sorts; the encoding itself
|
|
8
|
+
* (`encodeURIComponent(k)=encodeURIComponent(v)` joined by `&`) is identical.
|
|
9
|
+
*
|
|
10
|
+
* Dependency-free leaf module so any layer (build-time prerender hashing,
|
|
11
|
+
* runtime cache keys, client search-param serialization) can import it without
|
|
12
|
+
* pulling in cache/router internals.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface EncodeKVOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Sort pairs by key in codepoint (byte) order before joining. Byte order via
|
|
18
|
+
* the `<` operator, NOT localeCompare, so the result is identical across
|
|
19
|
+
* Node, Workers, and browsers regardless of runtime locale.
|
|
20
|
+
*
|
|
21
|
+
* Defaults to false (insertion order preserved).
|
|
22
|
+
*/
|
|
23
|
+
sort?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Serialize an array of [key, value] pairs to a deterministic query string
|
|
28
|
+
* (no leading `?`). Both key and value are passed through encodeURIComponent
|
|
29
|
+
* so a key/value containing `&` or `=` cannot collide with a structurally
|
|
30
|
+
* different pair set.
|
|
31
|
+
*
|
|
32
|
+
* Callers are responsible for any filtering (e.g. dropping reserved params) and
|
|
33
|
+
* any value coercion (e.g. String(v), skipping null/undefined) BEFORE calling.
|
|
34
|
+
* This function never inspects or skips values; it encodes exactly what it is
|
|
35
|
+
* given so each call site keeps its own existing output.
|
|
36
|
+
*/
|
|
37
|
+
export function encodeKV(
|
|
38
|
+
pairs: Iterable<readonly [string, string]>,
|
|
39
|
+
options: EncodeKVOptions = {},
|
|
40
|
+
): string {
|
|
41
|
+
const list = [...pairs];
|
|
42
|
+
if (list.length === 0) return "";
|
|
43
|
+
if (options.sort) {
|
|
44
|
+
list.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
45
|
+
}
|
|
46
|
+
return list
|
|
47
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
48
|
+
.join("&");
|
|
49
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Custom error classes for
|
|
2
|
+
* Custom error classes for Rango
|
|
3
3
|
*
|
|
4
4
|
* All errors include:
|
|
5
5
|
* - Descriptive names for easy identification
|
|
@@ -27,6 +27,17 @@ export class RouteNotFoundError extends Error {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// name fallback covers cross-realm errors (Vite dev dupes, RSC serialization)
|
|
31
|
+
// where instanceof fails.
|
|
32
|
+
export function isRouteNotFoundError(
|
|
33
|
+
error: unknown,
|
|
34
|
+
): error is RouteNotFoundError {
|
|
35
|
+
return (
|
|
36
|
+
error instanceof RouteNotFoundError ||
|
|
37
|
+
(error instanceof Error && error.name === "RouteNotFoundError")
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
/**
|
|
31
42
|
* Thrown when data is not found (e.g., product with ID doesn't exist)
|
|
32
43
|
* Use this in handlers/loaders to trigger the nearest notFoundBoundary
|
|
@@ -109,6 +120,24 @@ export class BuildError extends Error {
|
|
|
109
120
|
}
|
|
110
121
|
}
|
|
111
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Thrown when a route-definition DSL helper (route/layout/loader/cache/…) is
|
|
125
|
+
* called outside an active urls()/map() builder, so there is no
|
|
126
|
+
* AsyncLocalStorage build context to attach to. The message names the specific
|
|
127
|
+
* helper and how to fix it; the `cause` records the mechanical reason so the
|
|
128
|
+
* failure mode is identifiable (not conflated with an unrelated throw).
|
|
129
|
+
*/
|
|
130
|
+
export class DslContextError extends Error {
|
|
131
|
+
name = "DslContextError" as const;
|
|
132
|
+
cause?: unknown;
|
|
133
|
+
|
|
134
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
135
|
+
super(message);
|
|
136
|
+
Object.setPrototypeOf(this, DslContextError.prototype);
|
|
137
|
+
this.cause = options?.cause;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
112
141
|
/**
|
|
113
142
|
* Thrown when a network request fails (server unreachable, no internet, etc.)
|
|
114
143
|
* This error triggers the root error boundary with retry capability.
|
|
@@ -196,7 +225,6 @@ export function isNetworkError(error: unknown): boolean {
|
|
|
196
225
|
export class RouterError extends Error {
|
|
197
226
|
name = "RouterError" as const;
|
|
198
227
|
code: string;
|
|
199
|
-
type?: string;
|
|
200
228
|
status: number;
|
|
201
229
|
cause?: unknown;
|
|
202
230
|
|
|
@@ -205,7 +233,6 @@ export class RouterError extends Error {
|
|
|
205
233
|
message: string,
|
|
206
234
|
options?: {
|
|
207
235
|
status?: number;
|
|
208
|
-
type?: string;
|
|
209
236
|
cause?: unknown;
|
|
210
237
|
},
|
|
211
238
|
) {
|
|
@@ -213,7 +240,6 @@ export class RouterError extends Error {
|
|
|
213
240
|
Object.setPrototypeOf(this, RouterError.prototype);
|
|
214
241
|
this.code = code;
|
|
215
242
|
this.status = options?.status ?? 500;
|
|
216
|
-
this.type = options?.type;
|
|
217
243
|
this.cause = options?.cause;
|
|
218
244
|
}
|
|
219
245
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escape a JSON (or JSON-derived) string for safe embedding inside an HTML
|
|
3
|
+
* <script> element via dangerouslySetInnerHTML. Without this a value containing
|
|
4
|
+
* "</script>" closes the tag early — the rest of the page leaks as raw HTML, and
|
|
5
|
+
* in an executable script the trailing content runs. Escaping "<" defeats the
|
|
6
|
+
* early close; ">" and "&" are escaped for completeness so the serialized payload
|
|
7
|
+
* can never form HTML syntax. The result is still valid JSON and a valid JS
|
|
8
|
+
* string literal (\uXXXX escapes are legal in both) and re-parses identically.
|
|
9
|
+
*
|
|
10
|
+
* Used by every site that interpolates JSON.stringify(...) into inline <script>
|
|
11
|
+
* content: the JSON-LD meta descriptors (handles/MetaTags) and the FOUC theme
|
|
12
|
+
* init script (theme/theme-script).
|
|
13
|
+
*/
|
|
14
|
+
export function escapeJsonForScript(json: string): string {
|
|
15
|
+
return json
|
|
16
|
+
.replace(/</g, "\\u003c")
|
|
17
|
+
.replace(/>/g, "\\u003e")
|
|
18
|
+
.replace(/&/g, "\\u0026");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Escape an inline <script> body so it cannot terminate or corrupt the document.
|
|
23
|
+
* Two sequences are rewritten, each via a JS escape that is valid in string,
|
|
24
|
+
* template, regex (including the `u`/`v` flags), and JSON contexts — so the body
|
|
25
|
+
* still parses identically as code AND as JSON (application/json, ld+json):
|
|
26
|
+
* - "</script" -> "<\/script": stops a literal close tag inside the body from
|
|
27
|
+
* ending the element early. `\/` is a valid JSON escape and a valid regex escape.
|
|
28
|
+
* - "<!--": the "!" (U+0021) is emitted as a unicode escape (see the replacement
|
|
29
|
+
* string below), so the literal "<!--" token never reaches the HTML parser. A
|
|
30
|
+
* literal "<!--" puts the parser into the "script data escaped" state and a
|
|
31
|
+
* following "<script" into "script data DOUBLE escaped", where the real
|
|
32
|
+
* "</script>" no longer closes the element — `var x = "<!--<script>"` would
|
|
33
|
+
* swallow the rest of the document. The unicode-escape form decodes back to "!"
|
|
34
|
+
* in string/template/JSON/regex contexts, unlike "\!" (invalid JSON, invalid
|
|
35
|
+
* /u-regex escape).
|
|
36
|
+
* Real operators such as `a < b` and `a && b` are untouched (unlike
|
|
37
|
+
* escapeJsonForScript, which \u-escapes every "<", "&", ">").
|
|
38
|
+
*
|
|
39
|
+
* GUARANTEE / LIMITATION: value-preserving for the contexts where these sequences
|
|
40
|
+
* legitimately appear — string/template literals, regexes (incl. `u`/`v`), and
|
|
41
|
+
* JSON. It is NOT source-text-preserving (e.g. String.raw`</script>` sees the
|
|
42
|
+
* extra backslash), and it cannot rewrite "</script"/"<!--" that appear as bare
|
|
43
|
+
* code (a legacy `<!--` line comment, or `</script` outside any literal) — neither
|
|
44
|
+
* occurs in valid script payloads. Not a general sanitizer for arbitrary UNTRUSTED
|
|
45
|
+
* source; for untrusted dynamic data, JSON-encode it and read it back, rather than
|
|
46
|
+
* inlining it as code.
|
|
47
|
+
*
|
|
48
|
+
* Used by the Script handle's <Scripts> renderer for inline `children`.
|
|
49
|
+
*/
|
|
50
|
+
export function escapeScriptBody(js: string): string {
|
|
51
|
+
return js.replace(/<!--/g, "<\\u0021--").replace(/<\/(script)/gi, "<\\/$1");
|
|
52
|
+
}
|
package/src/handle.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { missingInjectedIdError } from "./missing-id-error.js";
|
|
2
|
+
import { isUnderTestRunner } from "./runtime-env.js";
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Handle definition for accumulating data across route segments.
|
|
3
6
|
*
|
|
@@ -7,8 +10,11 @@
|
|
|
7
10
|
*
|
|
8
11
|
* @example
|
|
9
12
|
* ```ts
|
|
10
|
-
* // Define a handle (name auto-generated from file + export)
|
|
11
|
-
*
|
|
13
|
+
* // Define a handle (name auto-generated from file + export).
|
|
14
|
+
* // Default collect is the identity: one array per segment that pushed.
|
|
15
|
+
* export const Breadcrumbs = createHandle<BreadcrumbItem, BreadcrumbItem[]>(
|
|
16
|
+
* (segments) => segments.flat(), // opt into a single flat list
|
|
17
|
+
* );
|
|
12
18
|
*
|
|
13
19
|
* // Use in handler
|
|
14
20
|
* const push = ctx.use(Breadcrumbs);
|
|
@@ -18,7 +24,7 @@
|
|
|
18
24
|
* const crumbs = useHandle(Breadcrumbs);
|
|
19
25
|
* ```
|
|
20
26
|
*/
|
|
21
|
-
export interface Handle<TData, TAccumulated = TData[]> {
|
|
27
|
+
export interface Handle<TData, TAccumulated = TData[][]> {
|
|
22
28
|
/**
|
|
23
29
|
* Brand to distinguish handles from loaders in ctx.use()
|
|
24
30
|
*/
|
|
@@ -32,10 +38,13 @@ export interface Handle<TData, TAccumulated = TData[]> {
|
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
/**
|
|
35
|
-
* Default collect
|
|
41
|
+
* Default collect: pass the per-segment data through as-is — one array per segment
|
|
42
|
+
* that pushed, in segment order. Lossless, so a consumer can tell which/how-many
|
|
43
|
+
* segments contributed. Callers that want a single flat list opt in with
|
|
44
|
+
* `createHandle((segments) => segments.flat())`.
|
|
36
45
|
*/
|
|
37
|
-
function defaultCollect<T>(segments: T[][]): T[] {
|
|
38
|
-
return segments
|
|
46
|
+
function defaultCollect<T>(segments: T[][]): T[][] {
|
|
47
|
+
return segments;
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
// Module-level registry mapping $$id to collect functions.
|
|
@@ -43,10 +52,10 @@ function defaultCollect<T>(segments: T[][]): T[] {
|
|
|
43
52
|
// Used by useHandle() to recover collect when handle is deserialized from RSC prop.
|
|
44
53
|
const collectRegistry = new Map<string, (segments: unknown[][]) => unknown>();
|
|
45
54
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
// Monotonic counter for runtime fallback ids (see createHandle). Only used
|
|
56
|
+
// when no build id was injected (a bare unit test).
|
|
57
|
+
let runtimeHandleIdCounter = 0;
|
|
58
|
+
|
|
50
59
|
export function getCollectFn(
|
|
51
60
|
id: string,
|
|
52
61
|
): ((segments: unknown[][]) => unknown) | undefined {
|
|
@@ -59,13 +68,22 @@ export function getCollectFn(
|
|
|
59
68
|
* The $$id is auto-generated by the Vite exposeInternalIds plugin based on
|
|
60
69
|
* file path and export name. No manual naming required.
|
|
61
70
|
*
|
|
62
|
-
* @param collect - Optional collect function
|
|
71
|
+
* @param collect - Optional collect function. Default: pass the per-segment data
|
|
72
|
+
* through as-is (one array per segment that pushed, in segment order). Lossless,
|
|
73
|
+
* so a consumer can tell which/how-many segments contributed. Opt into a single
|
|
74
|
+
* flat list with `(segments) => segments.flat()`.
|
|
63
75
|
* @param __injectedId - Auto-injected by Vite plugin, do not provide manually
|
|
64
76
|
*
|
|
65
77
|
* @example
|
|
66
78
|
* ```ts
|
|
67
|
-
* // Default:
|
|
68
|
-
* export const
|
|
79
|
+
* // Default: per-segment grouping, as-is
|
|
80
|
+
* export const Pushed = createHandle<string>();
|
|
81
|
+
* // Result type: string[][] (e.g. [["a"], ["b", "c"]])
|
|
82
|
+
*
|
|
83
|
+
* // Opt into a single flat list
|
|
84
|
+
* export const Breadcrumbs = createHandle<BreadcrumbItem, BreadcrumbItem[]>(
|
|
85
|
+
* (segments) => segments.flat()
|
|
86
|
+
* );
|
|
69
87
|
* // Result type: BreadcrumbItem[]
|
|
70
88
|
*
|
|
71
89
|
* // Custom: last value wins
|
|
@@ -80,8 +98,9 @@ export function getCollectFn(
|
|
|
80
98
|
* );
|
|
81
99
|
* // Result type: MetaTags
|
|
82
100
|
*
|
|
83
|
-
* // Custom: dedupe by href
|
|
84
|
-
*
|
|
101
|
+
* // Custom: dedupe by href (TAccumulated must be given — a custom collect that
|
|
102
|
+
* // returns a flat array no longer matches the default TData[][])
|
|
103
|
+
* export const Breadcrumbs = createHandle<BreadcrumbItem, BreadcrumbItem[]>(
|
|
85
104
|
* (segments) => {
|
|
86
105
|
* const all = segments.flat();
|
|
87
106
|
* return all.filter((item, i) => all.findIndex(x => x.href === item.href) === i);
|
|
@@ -89,32 +108,40 @@ export function getCollectFn(
|
|
|
89
108
|
* );
|
|
90
109
|
* ```
|
|
91
110
|
*/
|
|
92
|
-
export function createHandle<TData, TAccumulated = TData[]>(
|
|
111
|
+
export function createHandle<TData, TAccumulated = TData[][]>(
|
|
93
112
|
collect?: (segments: TData[][]) => TAccumulated,
|
|
94
113
|
__injectedId?: string,
|
|
95
114
|
): Handle<TData, TAccumulated> {
|
|
96
|
-
|
|
115
|
+
let handleId = __injectedId ?? "";
|
|
97
116
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
117
|
+
// No build-injected id. Under a test runner: fall back to a synthetic id so the
|
|
118
|
+
// collect registers below and the handle is exercisable in tests (useHandle,
|
|
119
|
+
// collectHandle, renderRoute's `handles` run the REAL collect). Otherwise (dev
|
|
120
|
+
// or a real build) it means an UNSUPPORTED handler shape the plugin skipped —
|
|
121
|
+
// fail loud. The rich, stack-parsing diagnostic stays behind the NODE_ENV check
|
|
122
|
+
// so a production build folds it away and tree-shakes missing-id-error.ts out,
|
|
123
|
+
// shipping the small throw instead. isUnderTestRunner() is runtime-safe.
|
|
124
|
+
if (!handleId) {
|
|
125
|
+
if (isUnderTestRunner()) {
|
|
126
|
+
handleId = `__rango_runtime_handle_${runtimeHandleIdCounter++}`;
|
|
127
|
+
} else if (process.env.NODE_ENV !== "production") {
|
|
128
|
+
throw missingInjectedIdError("Handle", "createHandle");
|
|
129
|
+
} else {
|
|
130
|
+
throw new Error(
|
|
131
|
+
"[rango] Handle is missing $$id — the build plugin did not inject one. " +
|
|
132
|
+
"Export it as `export const X = createHandle(...)`.",
|
|
133
|
+
);
|
|
134
|
+
}
|
|
104
135
|
}
|
|
105
136
|
|
|
106
137
|
const collectFn =
|
|
107
138
|
collect ??
|
|
108
139
|
(defaultCollect as unknown as (segments: TData[][]) => TAccumulated);
|
|
109
140
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
handleId,
|
|
115
|
-
collectFn as (segments: unknown[][]) => unknown,
|
|
116
|
-
);
|
|
117
|
-
}
|
|
141
|
+
collectRegistry.set(
|
|
142
|
+
handleId,
|
|
143
|
+
collectFn as (segments: unknown[][]) => unknown,
|
|
144
|
+
);
|
|
118
145
|
|
|
119
146
|
return {
|
|
120
147
|
__brand: "handle" as const,
|
|
@@ -122,9 +149,6 @@ export function createHandle<TData, TAccumulated = TData[]>(
|
|
|
122
149
|
};
|
|
123
150
|
}
|
|
124
151
|
|
|
125
|
-
/**
|
|
126
|
-
* Type guard to check if a value is a Handle.
|
|
127
|
-
*/
|
|
128
152
|
export function isHandle(value: unknown): value is Handle<unknown, unknown> {
|
|
129
153
|
return (
|
|
130
154
|
typeof value === "object" &&
|
|
@@ -148,12 +172,18 @@ export function collectHandleData<TData, TAccumulated>(
|
|
|
148
172
|
data: Record<string, Record<string, unknown[]>>,
|
|
149
173
|
segmentOrder: string[],
|
|
150
174
|
): TAccumulated {
|
|
175
|
+
// Fall back to the default (identity, pass-through) collect when none is
|
|
176
|
+
// registered — e.g. the handle's module was not imported so createHandle() never
|
|
177
|
+
// ran. This is harmless for a handle that wanted the default, but a handle with a
|
|
178
|
+
// CUSTOM collect that failed to register silently gets the wrong shape (identity
|
|
179
|
+
// TData[][]) cast as its declared TAccumulated. The runtime can't tell the two
|
|
180
|
+
// apart (a Handle only carries $$id), so warn in dev (folded out of production).
|
|
151
181
|
const collectFn = getCollectFn(handle.$$id);
|
|
152
182
|
if (!collectFn && process.env.NODE_ENV !== "production") {
|
|
153
183
|
console.warn(
|
|
154
|
-
`[
|
|
155
|
-
`
|
|
156
|
-
`createHandle() runs
|
|
184
|
+
`[rango] Handle "${handle.$$id}" has no registered collect — falling back ` +
|
|
185
|
+
`to the identity (per-segment data as-is). If this handle declares a ` +
|
|
186
|
+
`CUSTOM collect, import its module so createHandle() runs where it is read.`,
|
|
157
187
|
);
|
|
158
188
|
}
|
|
159
189
|
const collect = (collectFn ??
|