@rangojs/router 0.0.0-experimental.bd6e11bc → 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 +296 -887
- package/dist/bin/rango.js +459 -91
- package/dist/testing/vitest.js +36 -2
- package/dist/vite/index.js +1708 -414
- package/package.json +35 -10
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +82 -5
- package/skills/bundle-analysis/SKILL.md +2 -2
- package/skills/cache-guide/SKILL.md +14 -9
- package/skills/caching/SKILL.md +221 -12
- 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 +83 -2
- 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 +11 -3
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +9 -9
- package/skills/hooks/SKILL.md +73 -900
- 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 +84 -7
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +51 -17
- package/skills/layout/SKILL.md +38 -16
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +48 -20
- package/skills/middleware/SKILL.md +11 -5
- package/skills/migrate-nextjs/SKILL.md +203 -20
- package/skills/migrate-react-router/SKILL.md +59 -675
- 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 +3 -3
- package/skills/observability/SKILL.md +70 -5
- package/skills/parallel/SKILL.md +32 -8
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +59 -28
- package/skills/rango/SKILL.md +124 -50
- package/skills/response-routes/SKILL.md +78 -46
- package/skills/route/SKILL.md +85 -6
- package/skills/router-setup/SKILL.md +41 -6
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +28 -3
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +28 -4
- package/skills/testing/SKILL.md +68 -654
- 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 -918
- 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 +47 -17
- package/skills/vercel/SKILL.md +128 -0
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/__augment-tests__/augmented.check.ts +2 -3
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +14 -27
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +178 -100
- 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 +81 -68
- package/src/browser/navigation-client.ts +115 -70
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +153 -88
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +157 -144
- package/src/browser/prefetch/cache.ts +148 -81
- package/src/browser/prefetch/fetch.ts +231 -51
- package/src/browser/prefetch/queue.ts +25 -7
- package/src/browser/rango-state.ts +157 -115
- package/src/browser/react/Link.tsx +40 -7
- package/src/browser/react/NavigationProvider.tsx +140 -99
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +17 -2
- package/src/browser/react/index.ts +0 -51
- package/src/browser/react/location-state-shared.ts +14 -15
- package/src/browser/react/location-state.ts +0 -1
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +0 -5
- 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 +10 -5
- package/src/browser/react/use-params.ts +0 -2
- package/src/browser/react/use-router.ts +6 -4
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/response-adapter.ts +74 -8
- package/src/browser/rsc-router.tsx +97 -22
- package/src/browser/scroll-restoration.ts +15 -8
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +216 -38
- package/src/browser/types.ts +94 -22
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/generate-route-types.ts +1 -1
- package/src/build/index.ts +11 -5
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +152 -22
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +12 -1
- package/src/build/route-types/include-resolution.ts +455 -61
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +15 -2
- package/src/build/route-types/router-processing.ts +77 -41
- package/src/build/route-types/source-scan.ts +105 -7
- package/src/build/runtime-discovery.ts +4 -1
- 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 -101
- package/src/cache/cache-scope.ts +159 -54
- 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 +2170 -377
- 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 +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 +39 -22
- package/src/client.tsx +28 -58
- 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 +13 -1
- package/src/decode-loader-results.ts +18 -2
- package/src/defer.ts +185 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +0 -3
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +57 -40
- 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 +7 -0
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +1 -65
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +7 -12
- package/src/index.rsc.ts +88 -8
- package/src/index.ts +90 -16
- package/src/internal-debug.ts +11 -10
- package/src/loader.rsc.ts +19 -9
- package/src/loader.ts +12 -4
- 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 +75 -7
- 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 +25 -0
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +13 -49
- package/src/route-definition/dsl-helpers.ts +60 -53
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +46 -46
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/redirect.ts +44 -11
- package/src/route-definition/resolve-handler-use.ts +6 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -20
- package/src/route-types.ts +0 -5
- package/src/router/content-negotiation.ts +58 -23
- package/src/router/error-handling.ts +44 -17
- package/src/router/find-match.ts +129 -30
- package/src/router/handler-context.ts +6 -1
- package/src/router/instrument.ts +355 -0
- package/src/router/intercept-resolution.ts +35 -2
- package/src/router/lazy-includes.ts +79 -56
- package/src/router/loader-resolution.ts +151 -73
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +74 -40
- package/src/router/match-api.ts +76 -52
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +181 -178
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +115 -194
- package/src/router/match-middleware/cache-store.ts +61 -50
- 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 +36 -67
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +0 -116
- package/src/router/middleware.ts +231 -120
- 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 +99 -152
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +111 -66
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +47 -42
- package/src/router/revalidation.ts +75 -81
- package/src/router/route-snapshot.ts +14 -3
- package/src/router/router-context.ts +6 -29
- package/src/router/router-interfaces.ts +70 -8
- package/src/router/router-options.ts +126 -4
- package/src/router/segment-resolution/fresh.ts +104 -80
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +155 -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 +215 -304
- 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 +35 -15
- 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 +54 -35
- package/src/router/telemetry-otel.ts +160 -200
- package/src/router/telemetry.ts +9 -23
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +215 -0
- package/src/router/trie-matching.ts +171 -64
- package/src/router/types.ts +1 -63
- package/src/router/url-params.ts +13 -5
- package/src/router.ts +119 -48
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler-context.ts +1 -0
- package/src/rsc/handler.ts +267 -152
- package/src/rsc/helpers.ts +78 -4
- package/src/rsc/index.ts +1 -4
- 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 +11 -15
- package/src/rsc/progressive-enhancement.ts +120 -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 +58 -141
- package/src/rsc/rsc-rendering.ts +492 -49
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +268 -82
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +45 -3
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +31 -26
- package/src/segment-loader-promise.ts +49 -4
- package/src/segment-system.tsx +260 -95
- package/src/server/context.ts +99 -9
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +125 -2
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +33 -42
- package/src/server/request-context.ts +379 -138
- package/src/ssr/index.tsx +491 -182
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +44 -48
- package/src/testing/collect-handle.ts +14 -31
- package/src/testing/dispatch.ts +533 -160
- package/src/testing/e2e/fixture.ts +45 -11
- package/src/testing/e2e/index.ts +1 -22
- package/src/testing/e2e/matchers.ts +0 -16
- package/src/testing/e2e/parity.ts +85 -4
- package/src/testing/e2e/server.ts +12 -0
- package/src/testing/flight-matchers.ts +7 -14
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +36 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +30 -0
- package/src/testing/flight.ts +145 -70
- package/src/testing/generated-routes.ts +26 -50
- package/src/testing/index.ts +18 -19
- package/src/testing/internal/context.ts +184 -68
- 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 +134 -115
- package/src/testing/run-loader.ts +140 -51
- package/src/testing/run-middleware.ts +59 -33
- package/src/testing/run-transition-when.ts +164 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +1 -1
- package/src/testing/vitest-stubs/cloudflare-workers.ts +1 -1
- package/src/testing/vitest.ts +138 -16
- 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 +15 -15
- package/src/types/handler-context.ts +45 -15
- package/src/types/index.ts +2 -10
- package/src/types/loader-types.ts +6 -3
- package/src/types/request-scope.ts +8 -22
- package/src/types/route-config.ts +20 -52
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +100 -13
- package/src/urls/include-helper.ts +10 -12
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +2 -8
- package/src/urls/path-helper-types.ts +52 -14
- package/src/urls/path-helper.ts +5 -54
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +76 -42
- package/src/urls/urls-function.ts +0 -14
- package/src/use-loader.tsx +0 -186
- package/src/vercel/index.ts +11 -0
- package/src/vercel/tracing.ts +88 -0
- package/src/vite/discovery/bundle-postprocess.ts +2 -1
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +34 -43
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/discovery/prerender-collection.ts +33 -46
- package/src/vite/discovery/state.ts +12 -1
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/index.ts +9 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugin-types.ts +143 -10
- package/src/vite/plugins/cjs-to-esm.ts +8 -12
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +0 -10
- package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
- package/src/vite/plugins/expose-action-id.ts +2 -73
- package/src/vite/plugins/expose-id-utils.ts +85 -56
- package/src/vite/plugins/expose-ids/export-analysis.ts +30 -43
- package/src/vite/plugins/expose-ids/handler-transform.ts +5 -31
- package/src/vite/plugins/expose-ids/loader-transform.ts +12 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
- package/src/vite/plugins/expose-internal-ids.ts +10 -1
- package/src/vite/plugins/performance-tracks.ts +0 -3
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +21 -46
- package/src/vite/plugins/vercel-output.ts +384 -0
- package/src/vite/plugins/version-injector.ts +22 -27
- package/src/vite/plugins/version-plugin.ts +6 -66
- package/src/vite/plugins/virtual-entries.ts +137 -26
- package/src/vite/rango.ts +146 -135
- package/src/vite/router-discovery.ts +189 -48
- package/src/vite/utils/ast-handler-extract.ts +11 -20
- package/src/vite/utils/bundle-analysis.ts +6 -13
- package/src/vite/utils/client-chunks.ts +0 -6
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +0 -22
- package/src/vite/utils/manifest-utils.ts +4 -75
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +71 -44
- package/src/vite/utils/shared-utils.ts +55 -37
- 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
|
@@ -225,7 +225,6 @@ export function isNetworkError(error: unknown): boolean {
|
|
|
225
225
|
export class RouterError extends Error {
|
|
226
226
|
name = "RouterError" as const;
|
|
227
227
|
code: string;
|
|
228
|
-
type?: string;
|
|
229
228
|
status: number;
|
|
230
229
|
cause?: unknown;
|
|
231
230
|
|
|
@@ -234,7 +233,6 @@ export class RouterError extends Error {
|
|
|
234
233
|
message: string,
|
|
235
234
|
options?: {
|
|
236
235
|
status?: number;
|
|
237
|
-
type?: string;
|
|
238
236
|
cause?: unknown;
|
|
239
237
|
},
|
|
240
238
|
) {
|
|
@@ -242,7 +240,6 @@ export class RouterError extends Error {
|
|
|
242
240
|
Object.setPrototypeOf(this, RouterError.prototype);
|
|
243
241
|
this.code = code;
|
|
244
242
|
this.status = options?.status ?? 500;
|
|
245
|
-
this.type = options?.type;
|
|
246
243
|
this.cause = options?.cause;
|
|
247
244
|
}
|
|
248
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,4 +1,5 @@
|
|
|
1
1
|
import { missingInjectedIdError } from "./missing-id-error.js";
|
|
2
|
+
import { isUnderTestRunner } from "./runtime-env.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Handle definition for accumulating data across route segments.
|
|
@@ -9,8 +10,11 @@ import { missingInjectedIdError } from "./missing-id-error.js";
|
|
|
9
10
|
*
|
|
10
11
|
* @example
|
|
11
12
|
* ```ts
|
|
12
|
-
* // Define a handle (name auto-generated from file + export)
|
|
13
|
-
*
|
|
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
|
+
* );
|
|
14
18
|
*
|
|
15
19
|
* // Use in handler
|
|
16
20
|
* const push = ctx.use(Breadcrumbs);
|
|
@@ -20,7 +24,7 @@ import { missingInjectedIdError } from "./missing-id-error.js";
|
|
|
20
24
|
* const crumbs = useHandle(Breadcrumbs);
|
|
21
25
|
* ```
|
|
22
26
|
*/
|
|
23
|
-
export interface Handle<TData, TAccumulated = TData[]> {
|
|
27
|
+
export interface Handle<TData, TAccumulated = TData[][]> {
|
|
24
28
|
/**
|
|
25
29
|
* Brand to distinguish handles from loaders in ctx.use()
|
|
26
30
|
*/
|
|
@@ -34,10 +38,13 @@ export interface Handle<TData, TAccumulated = TData[]> {
|
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
/**
|
|
37
|
-
* 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())`.
|
|
38
45
|
*/
|
|
39
|
-
function defaultCollect<T>(segments: T[][]): T[] {
|
|
40
|
-
return segments
|
|
46
|
+
function defaultCollect<T>(segments: T[][]): T[][] {
|
|
47
|
+
return segments;
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
// Module-level registry mapping $$id to collect functions.
|
|
@@ -45,15 +52,10 @@ function defaultCollect<T>(segments: T[][]): T[] {
|
|
|
45
52
|
// Used by useHandle() to recover collect when handle is deserialized from RSC prop.
|
|
46
53
|
const collectRegistry = new Map<string, (segments: unknown[][]) => unknown>();
|
|
47
54
|
|
|
48
|
-
// Monotonic counter for runtime fallback ids (see createHandle).
|
|
49
|
-
//
|
|
50
|
-
// the process. Only used when no build id was injected (a bare unit test).
|
|
55
|
+
// Monotonic counter for runtime fallback ids (see createHandle). Only used
|
|
56
|
+
// when no build id was injected (a bare unit test).
|
|
51
57
|
let runtimeHandleIdCounter = 0;
|
|
52
58
|
|
|
53
|
-
/**
|
|
54
|
-
* Look up a collect function from the registry by handle $$id.
|
|
55
|
-
* Returns undefined if not registered (falls back to defaultCollect in useHandle).
|
|
56
|
-
*/
|
|
57
59
|
export function getCollectFn(
|
|
58
60
|
id: string,
|
|
59
61
|
): ((segments: unknown[][]) => unknown) | undefined {
|
|
@@ -66,13 +68,22 @@ export function getCollectFn(
|
|
|
66
68
|
* The $$id is auto-generated by the Vite exposeInternalIds plugin based on
|
|
67
69
|
* file path and export name. No manual naming required.
|
|
68
70
|
*
|
|
69
|
-
* @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()`.
|
|
70
75
|
* @param __injectedId - Auto-injected by Vite plugin, do not provide manually
|
|
71
76
|
*
|
|
72
77
|
* @example
|
|
73
78
|
* ```ts
|
|
74
|
-
* // Default:
|
|
75
|
-
* 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
|
+
* );
|
|
76
87
|
* // Result type: BreadcrumbItem[]
|
|
77
88
|
*
|
|
78
89
|
* // Custom: last value wins
|
|
@@ -87,8 +98,9 @@ export function getCollectFn(
|
|
|
87
98
|
* );
|
|
88
99
|
* // Result type: MetaTags
|
|
89
100
|
*
|
|
90
|
-
* // Custom: dedupe by href
|
|
91
|
-
*
|
|
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[]>(
|
|
92
104
|
* (segments) => {
|
|
93
105
|
* const all = segments.flat();
|
|
94
106
|
* return all.filter((item, i) => all.findIndex(x => x.href === item.href) === i);
|
|
@@ -96,34 +108,36 @@ export function getCollectFn(
|
|
|
96
108
|
* );
|
|
97
109
|
* ```
|
|
98
110
|
*/
|
|
99
|
-
export function createHandle<TData, TAccumulated = TData[]>(
|
|
111
|
+
export function createHandle<TData, TAccumulated = TData[][]>(
|
|
100
112
|
collect?: (segments: TData[][]) => TAccumulated,
|
|
101
113
|
__injectedId?: string,
|
|
102
114
|
): Handle<TData, TAccumulated> {
|
|
103
115
|
let handleId = __injectedId ?? "";
|
|
104
116
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
// process-stable runtime id so the collect registers below and the handle is
|
|
113
|
-
// fully exercisable in tests (useHandle, collectHandle, renderRoute's `handles`
|
|
114
|
-
// seeding run the REAL collect). Provably inert in production: the fallback
|
|
115
|
-
// never triggers when the plugin injects the id, so server/client id
|
|
116
|
-
// consistency (required for RSC recovery) is unaffected.
|
|
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.
|
|
117
124
|
if (!handleId) {
|
|
118
|
-
|
|
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
|
+
}
|
|
119
135
|
}
|
|
120
136
|
|
|
121
137
|
const collectFn =
|
|
122
138
|
collect ??
|
|
123
139
|
(defaultCollect as unknown as (segments: TData[][]) => TAccumulated);
|
|
124
140
|
|
|
125
|
-
// Register collect in module-level registry so useHandle() can recover it
|
|
126
|
-
// when the handle is deserialized from RSC props (toJSON strips collect).
|
|
127
141
|
collectRegistry.set(
|
|
128
142
|
handleId,
|
|
129
143
|
collectFn as (segments: unknown[][]) => unknown,
|
|
@@ -135,9 +149,6 @@ export function createHandle<TData, TAccumulated = TData[]>(
|
|
|
135
149
|
};
|
|
136
150
|
}
|
|
137
151
|
|
|
138
|
-
/**
|
|
139
|
-
* Type guard to check if a value is a Handle.
|
|
140
|
-
*/
|
|
141
152
|
export function isHandle(value: unknown): value is Handle<unknown, unknown> {
|
|
142
153
|
return (
|
|
143
154
|
typeof value === "object" &&
|
|
@@ -161,12 +172,18 @@ export function collectHandleData<TData, TAccumulated>(
|
|
|
161
172
|
data: Record<string, Record<string, unknown[]>>,
|
|
162
173
|
segmentOrder: string[],
|
|
163
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).
|
|
164
181
|
const collectFn = getCollectFn(handle.$$id);
|
|
165
182
|
if (!collectFn && process.env.NODE_ENV !== "production") {
|
|
166
183
|
console.warn(
|
|
167
|
-
`[rango] Handle "${handle.$$id}" has no registered collect
|
|
168
|
-
`
|
|
169
|
-
`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.`,
|
|
170
187
|
);
|
|
171
188
|
}
|
|
172
189
|
const collect = (collectFn ??
|
package/src/handles/MetaTags.tsx
CHANGED
|
@@ -3,11 +3,16 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Component to render collected meta descriptors in the document head.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* (
|
|
6
|
+
* Deferred (Promise) meta descriptors are resolved before MetaTags renders
|
|
7
|
+
* (server-side on the full render, client-side before apply on navigation), so
|
|
8
|
+
* it only ever receives resolved descriptors and never suspends.
|
|
8
9
|
*
|
|
9
10
|
* When theme is enabled in the router config, MetaTags also renders
|
|
10
11
|
* the theme initialization script to prevent FOUC (flash of unstyled content).
|
|
12
|
+
* This makes MetaTags the sole FOUC-script injector for apps that render it;
|
|
13
|
+
* the standalone `<ThemeScript />` is only needed when MetaTags is not used.
|
|
14
|
+
* Rendering both is safe (the inline script guards listener registration) but
|
|
15
|
+
* redundant.
|
|
11
16
|
*
|
|
12
17
|
* @example
|
|
13
18
|
* ```tsx
|
|
@@ -24,13 +29,13 @@
|
|
|
24
29
|
* ```
|
|
25
30
|
*/
|
|
26
31
|
|
|
27
|
-
import { use } from "react";
|
|
28
32
|
import { useHandle } from "../browser/react/use-handle.js";
|
|
29
33
|
import { Meta } from "./meta.js";
|
|
30
|
-
import type {
|
|
34
|
+
import type { MetaDescriptorBase } from "../router/types.js";
|
|
31
35
|
import { useThemeContext } from "../theme/theme-context.js";
|
|
32
36
|
import { generateThemeScript } from "../theme/theme-script.js";
|
|
33
37
|
import { useNonce } from "../browser/react/nonce-context.js";
|
|
38
|
+
import { escapeJsonForScript } from "../escape-script.js";
|
|
34
39
|
|
|
35
40
|
// Type guards for MetaDescriptorBase variants
|
|
36
41
|
function hasCharSet(d: MetaDescriptorBase): d is { charSet: "utf-8" } {
|
|
@@ -90,31 +95,18 @@ function hasTagName(
|
|
|
90
95
|
);
|
|
91
96
|
}
|
|
92
97
|
|
|
93
|
-
|
|
94
|
-
* Check if a value is a Promise.
|
|
95
|
-
*/
|
|
96
|
-
function isPromise(value: unknown): value is Promise<unknown> {
|
|
97
|
-
return value !== null && typeof value === "object" && "then" in value;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Render a single meta descriptor as a React element.
|
|
102
|
-
*/
|
|
103
|
-
function renderMetaDescriptor(
|
|
98
|
+
export function renderMetaDescriptor(
|
|
104
99
|
descriptor: MetaDescriptorBase,
|
|
105
100
|
index: number,
|
|
106
101
|
): React.ReactNode {
|
|
107
|
-
// charset
|
|
108
102
|
if (hasCharSet(descriptor)) {
|
|
109
103
|
return <meta key="charSet" charSet={descriptor.charSet} />;
|
|
110
104
|
}
|
|
111
105
|
|
|
112
|
-
// title
|
|
113
106
|
if (hasTitle(descriptor)) {
|
|
114
107
|
return <title key="title">{descriptor.title}</title>;
|
|
115
108
|
}
|
|
116
109
|
|
|
117
|
-
// name + content (description, viewport, etc.)
|
|
118
110
|
if (hasNameContent(descriptor)) {
|
|
119
111
|
return (
|
|
120
112
|
<meta
|
|
@@ -125,7 +117,6 @@ function renderMetaDescriptor(
|
|
|
125
117
|
);
|
|
126
118
|
}
|
|
127
119
|
|
|
128
|
-
// property + content (Open Graph, etc.)
|
|
129
120
|
if (hasPropertyContent(descriptor)) {
|
|
130
121
|
return (
|
|
131
122
|
<meta
|
|
@@ -136,7 +127,6 @@ function renderMetaDescriptor(
|
|
|
136
127
|
);
|
|
137
128
|
}
|
|
138
129
|
|
|
139
|
-
// http-equiv + content
|
|
140
130
|
if (hasHttpEquivContent(descriptor)) {
|
|
141
131
|
return (
|
|
142
132
|
<meta
|
|
@@ -147,9 +137,10 @@ function renderMetaDescriptor(
|
|
|
147
137
|
);
|
|
148
138
|
}
|
|
149
139
|
|
|
150
|
-
// JSON-LD structured data
|
|
151
140
|
if (hasScriptLdJson(descriptor)) {
|
|
152
|
-
const json =
|
|
141
|
+
const json = escapeJsonForScript(
|
|
142
|
+
JSON.stringify(descriptor["script:ld+json"]),
|
|
143
|
+
);
|
|
153
144
|
return (
|
|
154
145
|
<script
|
|
155
146
|
key={`ld-json-${index}`}
|
|
@@ -159,7 +150,6 @@ function renderMetaDescriptor(
|
|
|
159
150
|
);
|
|
160
151
|
}
|
|
161
152
|
|
|
162
|
-
// Custom tagName (meta or link with arbitrary attributes)
|
|
163
153
|
if (hasTagName(descriptor)) {
|
|
164
154
|
const { tagName, ...rest } = descriptor;
|
|
165
155
|
if (tagName === "link") {
|
|
@@ -180,7 +170,6 @@ function renderMetaDescriptor(
|
|
|
180
170
|
}
|
|
181
171
|
}
|
|
182
172
|
|
|
183
|
-
// Fallback: treat as meta attributes
|
|
184
173
|
return (
|
|
185
174
|
<meta
|
|
186
175
|
key={`meta-fallback-${index}`}
|
|
@@ -189,20 +178,6 @@ function renderMetaDescriptor(
|
|
|
189
178
|
);
|
|
190
179
|
}
|
|
191
180
|
|
|
192
|
-
/**
|
|
193
|
-
* Wrapper component to resolve a Promise<MetaDescriptorBase> using use().
|
|
194
|
-
*/
|
|
195
|
-
function AsyncMetaTag({
|
|
196
|
-
promise,
|
|
197
|
-
index,
|
|
198
|
-
}: {
|
|
199
|
-
promise: Promise<MetaDescriptorBase>;
|
|
200
|
-
index: number;
|
|
201
|
-
}): React.ReactNode {
|
|
202
|
-
const resolved = use(promise);
|
|
203
|
-
return renderMetaDescriptor(resolved, index);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
181
|
/**
|
|
207
182
|
* Renders all collected meta descriptors from route handlers.
|
|
208
183
|
*
|
|
@@ -212,11 +187,16 @@ function AsyncMetaTag({
|
|
|
212
187
|
* When theme is enabled in router config, also renders the theme initialization
|
|
213
188
|
* script to prevent FOUC (flash of unstyled content).
|
|
214
189
|
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
190
|
+
* Deferred (Promise) meta descriptors are resolved BEFORE MetaTags renders —
|
|
191
|
+
* server-side on the full/SSR render, client-side before apply on navigation
|
|
192
|
+
* (resolve-by-default) — so MetaTags only ever receives resolved descriptors and
|
|
193
|
+
* never suspends.
|
|
217
194
|
*/
|
|
218
195
|
export function MetaTags(): React.ReactNode {
|
|
219
|
-
|
|
196
|
+
// Deferred descriptors are resolved BEFORE collect runs (resolve-by-default),
|
|
197
|
+
// and collectMeta strips unset markers, so the collected output is always
|
|
198
|
+
// resolved base descriptors (never a Promise).
|
|
199
|
+
const descriptors = useHandle(Meta) as MetaDescriptorBase[];
|
|
220
200
|
const themeConfig = useThemeContext()?.config ?? null;
|
|
221
201
|
const nonce = useNonce();
|
|
222
202
|
|
|
@@ -229,18 +209,9 @@ export function MetaTags(): React.ReactNode {
|
|
|
229
209
|
dangerouslySetInnerHTML={{ __html: generateThemeScript(themeConfig) }}
|
|
230
210
|
/>
|
|
231
211
|
)}
|
|
232
|
-
{descriptors.map((descriptor, index) =>
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
<AsyncMetaTag
|
|
236
|
-
key={`async-${index}`}
|
|
237
|
-
promise={descriptor}
|
|
238
|
-
index={index}
|
|
239
|
-
/>
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
return renderMetaDescriptor(descriptor, index);
|
|
243
|
-
})}
|
|
212
|
+
{descriptors.map((descriptor, index) =>
|
|
213
|
+
renderMetaDescriptor(descriptor, index),
|
|
214
|
+
)}
|
|
244
215
|
</>
|
|
245
216
|
);
|
|
246
217
|
}
|