@rangojs/router 0.0.0-experimental.c873df95 → 0.0.0-experimental.c9471f9b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -4
- package/README.md +303 -741
- package/dist/bin/rango.js +724 -183
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +4276 -1206
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +86 -15
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +85 -6
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +251 -24
- package/skills/caching/SKILL.md +338 -13
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +110 -4
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +5 -3
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +87 -56
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +73 -691
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +129 -27
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +94 -18
- package/skills/layout/SKILL.md +62 -19
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +302 -54
- package/skills/middleware/SKILL.md +59 -16
- package/skills/migrate-nextjs/SKILL.md +745 -0
- package/skills/migrate-react-router/SKILL.md +153 -0
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +29 -2
- package/skills/observability/SKILL.md +202 -0
- package/skills/parallel/SKILL.md +166 -10
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +178 -124
- package/skills/rango/SKILL.md +318 -24
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +138 -49
- package/skills/route/SKILL.md +172 -9
- package/skills/router-setup/SKILL.md +131 -11
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +776 -0
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +28 -4
- package/skills/testing/SKILL.md +130 -0
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -0
- package/skills/testing/handles.md +131 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +122 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +85 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +123 -0
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -616
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +74 -15
- package/skills/vercel/SKILL.md +128 -0
- package/skills/view-transitions/SKILL.md +337 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +1 -66
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +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 +131 -30
- package/src/browser/navigation-client.ts +204 -87
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +203 -80
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +198 -125
- package/src/browser/prefetch/cache.ts +246 -71
- package/src/browser/prefetch/fetch.ts +358 -40
- package/src/browser/prefetch/queue.ts +113 -32
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +112 -15
- package/src/browser/react/NavigationProvider.tsx +213 -122
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +66 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +178 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +23 -69
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +32 -7
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +46 -11
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +99 -8
- package/src/browser/rsc-router.tsx +168 -28
- package/src/browser/scroll-restoration.ts +44 -27
- package/src/browser/segment-reconciler.ts +44 -12
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +244 -71
- package/src/browser/types.ts +127 -12
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +207 -158
- package/src/build/generate-route-types.ts +6 -1
- package/src/build/index.ts +11 -3
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +198 -41
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +464 -63
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +333 -117
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +13 -21
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +58 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +454 -101
- package/src/cache/cache-scope.ts +233 -101
- 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 +2206 -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/taint.ts +55 -0
- package/src/cache/types.ts +194 -99
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +1132 -0
- package/src/client.rsc.tsx +41 -21
- package/src/client.tsx +116 -290
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +108 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +84 -2
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +185 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +104 -34
- package/src/handles/MetaTags.tsx +24 -53
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +35 -8
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +18 -0
- package/src/handles/meta.ts +14 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +13 -22
- package/src/host/index.ts +9 -2
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +100 -13
- package/src/index.ts +143 -19
- package/src/internal-debug.ts +11 -10
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +215 -86
- package/src/redirect-origin.ts +114 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +62 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +19 -77
- package/src/route-definition/dsl-helpers.ts +461 -304
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +153 -77
- package/src/route-definition/index.ts +4 -2
- package/src/route-definition/redirect.ts +51 -10
- package/src/route-definition/resolve-handler-use.ts +160 -0
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +41 -20
- package/src/route-types.ts +37 -46
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +164 -17
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +129 -30
- package/src/router/handler-context.ts +83 -39
- package/src/router/instrument.ts +355 -0
- package/src/router/intercept-resolution.ts +48 -24
- package/src/router/lazy-includes.ts +85 -62
- package/src/router/loader-resolution.ts +286 -56
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +96 -53
- package/src/router/match-api.ts +178 -218
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +211 -165
- package/src/router/match-middleware/background-revalidation.ts +48 -21
- package/src/router/match-middleware/cache-lookup.ts +178 -277
- package/src/router/match-middleware/cache-store.ts +74 -45
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +45 -14
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +124 -50
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +300 -177
- package/src/router/navigation-snapshot.ts +133 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +181 -150
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +203 -58
- package/src/router/preview-match.ts +35 -103
- package/src/router/request-classification.ts +291 -0
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +256 -0
- package/src/router/router-context.ts +6 -28
- package/src/router/router-interfaces.ts +146 -35
- package/src/router/router-options.ts +202 -15
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +204 -88
- package/src/router/segment-resolution/helpers.ts +115 -30
- 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 +354 -319
- 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 +10 -63
- package/src/router/url-params.ts +57 -0
- package/src/router.ts +205 -70
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +682 -508
- package/src/rsc/helpers.ts +168 -46
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +127 -31
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +138 -15
- package/src/rsc/redirect-guard.ts +100 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +99 -189
- package/src/rsc/rsc-rendering.ts +509 -73
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +287 -113
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +62 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +167 -0
- package/src/segment-system.tsx +386 -205
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +280 -51
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +152 -5
- package/src/server/handle-store.ts +40 -38
- package/src/server/loader-registry.ts +38 -46
- package/src/server/request-context.ts +550 -171
- package/src/ssr/index.tsx +467 -175
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/static-handler.ts +27 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +46 -0
- package/src/testing/dispatch.ts +813 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +257 -0
- package/src/testing/generated-routes.ts +199 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +371 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +357 -0
- package/src/testing/render-route.tsx +584 -0
- package/src/testing/run-loader.ts +385 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/run-transition-when.ts +164 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +56 -84
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +145 -79
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +20 -52
- package/src/types/route-entry.ts +12 -7
- package/src/types/segments.ts +136 -15
- package/src/urls/include-helper.ts +40 -75
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +2 -11
- package/src/urls/path-helper-types.ts +102 -23
- package/src/urls/path-helper.ts +62 -111
- package/src/urls/pattern-types.ts +84 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +98 -154
- package/src/urls/urls-function.ts +1 -19
- package/src/use-loader.tsx +346 -89
- package/src/vercel/index.ts +11 -0
- package/src/vercel/tracing.ts +88 -0
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +130 -85
- package/src/vite/discovery/discovery-errors.ts +255 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +214 -132
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +57 -4
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +15 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugin-types.ts +261 -6
- 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 +96 -51
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
- package/src/vite/plugins/expose-internal-ids.ts +553 -317
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/vercel-output.ts +384 -0
- package/src/vite/plugins/version-injector.ts +40 -29
- package/src/vite/plugins/version-plugin.ts +37 -40
- package/src/vite/plugins/virtual-entries.ts +138 -27
- package/src/vite/rango.ts +247 -132
- package/src/vite/router-discovery.ts +1091 -159
- 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 +98 -38
- package/src/vite/utils/shared-utils.ts +144 -44
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Node ESM loader hook that resolves `cloudflare:*` imports to the same
|
|
2
|
+
// stub ESM the Vite transform produces for rewritten specifiers.
|
|
3
|
+
//
|
|
4
|
+
// Why both? The Vite transform (cloudflare-protocol-stub.ts) catches
|
|
5
|
+
// imports in modules that flow through Vite's plugin pipeline — covers
|
|
6
|
+
// user source and any node_modules package Vite fetches and transforms.
|
|
7
|
+
// But Vite/Rollup externalize certain packages (e.g. `partyserver`,
|
|
8
|
+
// which has `import { DurableObject, env } from "cloudflare:workers"`
|
|
9
|
+
// at its top level, and similar "workerd-native" libraries). Externalized
|
|
10
|
+
// modules bypass the transform: Rollup hands their resolution to Node's
|
|
11
|
+
// native ESM loader, which rejects URL-scheme specifiers. This loader
|
|
12
|
+
// hook registers via `module.register()` from `createTempRscServer` and
|
|
13
|
+
// intercepts `cloudflare:*` at Node's resolve layer — before the default
|
|
14
|
+
// loader throws ERR_UNSUPPORTED_ESM_URL_SCHEME.
|
|
15
|
+
//
|
|
16
|
+
// Lifecycle: the hook runs in a dedicated worker thread (Node ESM loader
|
|
17
|
+
// architecture) with its own globalThis. It cannot see the main thread's
|
|
18
|
+
// `__rango_build_env__` bridge, so the `env` export here is always `{}`.
|
|
19
|
+
// That's fine in practice — externalized libraries don't typically touch
|
|
20
|
+
// `env` at module top level; they read it at request time in workerd
|
|
21
|
+
// where the real module exists. Build-time prerender handlers in user
|
|
22
|
+
// source DO read `env`, but they flow through the Vite transform (which
|
|
23
|
+
// does bridge `env` from `getPlatformProxy()`), not through this loader.
|
|
24
|
+
//
|
|
25
|
+
// Keep STUBS in sync with cloudflare-protocol-stub.ts — both paths need
|
|
26
|
+
// to hand out the same base classes.
|
|
27
|
+
|
|
28
|
+
const CF_PREFIX = "cloudflare:";
|
|
29
|
+
|
|
30
|
+
const STUBS = {
|
|
31
|
+
"cloudflare:workers": `
|
|
32
|
+
export class DurableObject { constructor(_ctx, _env) {} }
|
|
33
|
+
export class WorkerEntrypoint { constructor(_ctx, _env) {} }
|
|
34
|
+
export class WorkflowEntrypoint { constructor(_ctx, _env) {} }
|
|
35
|
+
export class RpcTarget {}
|
|
36
|
+
export const env = {};
|
|
37
|
+
export default {};
|
|
38
|
+
`,
|
|
39
|
+
"cloudflare:email": `
|
|
40
|
+
export class EmailMessage { constructor(_from, _to, _raw) {} }
|
|
41
|
+
export default {};
|
|
42
|
+
`,
|
|
43
|
+
"cloudflare:sockets": `
|
|
44
|
+
export function connect() { return {}; }
|
|
45
|
+
export default {};
|
|
46
|
+
`,
|
|
47
|
+
"cloudflare:workflows": `
|
|
48
|
+
export class NonRetryableError extends Error {
|
|
49
|
+
constructor(message, name) { super(message); this.name = name ?? "NonRetryableError"; }
|
|
50
|
+
}
|
|
51
|
+
export default {};
|
|
52
|
+
`,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Policy: unknown `cloudflare:*` specifiers resolve permissively to an
|
|
56
|
+
// empty default export rather than throwing. Same reasoning as
|
|
57
|
+
// cloudflare-protocol-stub.ts's FALLBACK_STUB — we prioritize
|
|
58
|
+
// dependency-graph resilience over strict validation, because third-party
|
|
59
|
+
// packages can pull `cloudflare:*` modules we haven't curated.
|
|
60
|
+
const FALLBACK_STUB = `export default {};\n`;
|
|
61
|
+
|
|
62
|
+
function dataUrlFor(specifier) {
|
|
63
|
+
const body = STUBS[specifier] ?? FALLBACK_STUB;
|
|
64
|
+
return "data:text/javascript;base64," + Buffer.from(body).toString("base64");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
68
|
+
if (specifier.startsWith(CF_PREFIX)) {
|
|
69
|
+
return {
|
|
70
|
+
shortCircuit: true,
|
|
71
|
+
url: dataUrlFor(specifier),
|
|
72
|
+
format: "module",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return nextResolve(specifier, context);
|
|
76
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.0.0-experimental.
|
|
3
|
+
"version": "0.0.0-experimental.c9471f9b",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -110,6 +110,16 @@
|
|
|
110
110
|
"react-server": "./src/cache/cache-runtime.ts",
|
|
111
111
|
"default": "./src/cache/cache-runtime.ts"
|
|
112
112
|
},
|
|
113
|
+
"./cloudflare": {
|
|
114
|
+
"types": "./src/cloudflare/index.ts",
|
|
115
|
+
"react-server": "./src/cloudflare/index.ts",
|
|
116
|
+
"default": "./src/cloudflare/index.ts"
|
|
117
|
+
},
|
|
118
|
+
"./vercel": {
|
|
119
|
+
"types": "./src/vercel/index.ts",
|
|
120
|
+
"react-server": "./src/vercel/index.ts",
|
|
121
|
+
"default": "./src/vercel/index.ts"
|
|
122
|
+
},
|
|
113
123
|
"./theme": {
|
|
114
124
|
"types": "./src/theme/index.ts",
|
|
115
125
|
"default": "./src/theme/index.ts"
|
|
@@ -126,6 +136,31 @@
|
|
|
126
136
|
"./host/testing": {
|
|
127
137
|
"types": "./src/host/testing.ts",
|
|
128
138
|
"default": "./src/host/testing.ts"
|
|
139
|
+
},
|
|
140
|
+
"./testing": {
|
|
141
|
+
"types": "./src/testing/index.ts",
|
|
142
|
+
"default": "./src/testing/index.ts"
|
|
143
|
+
},
|
|
144
|
+
"./testing/vitest": {
|
|
145
|
+
"types": "./src/testing/vitest.ts",
|
|
146
|
+
"default": "./dist/testing/vitest.js"
|
|
147
|
+
},
|
|
148
|
+
"./testing/dom": {
|
|
149
|
+
"types": "./src/testing/dom.entry.ts",
|
|
150
|
+
"default": "./src/testing/dom.entry.ts"
|
|
151
|
+
},
|
|
152
|
+
"./testing/e2e": {
|
|
153
|
+
"types": "./src/testing/e2e/index.ts",
|
|
154
|
+
"default": "./src/testing/e2e/index.ts"
|
|
155
|
+
},
|
|
156
|
+
"./testing/flight": {
|
|
157
|
+
"types": "./src/testing/flight.entry.ts",
|
|
158
|
+
"react-server": "./src/testing/flight.entry.ts",
|
|
159
|
+
"default": "./src/testing/flight.entry.ts"
|
|
160
|
+
},
|
|
161
|
+
"./testing/flight-matchers": {
|
|
162
|
+
"types": "./src/testing/flight-matchers.ts",
|
|
163
|
+
"default": "./src/testing/flight-matchers.ts"
|
|
129
164
|
}
|
|
130
165
|
},
|
|
131
166
|
"publishConfig": {
|
|
@@ -133,45 +168,81 @@
|
|
|
133
168
|
"tag": "experimental"
|
|
134
169
|
},
|
|
135
170
|
"scripts": {
|
|
136
|
-
"build": "pnpm
|
|
171
|
+
"build": "pnpm exec esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm exec esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && pnpm exec esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
|
|
137
172
|
"prepublishOnly": "pnpm build",
|
|
138
|
-
"typecheck": "tsc --noEmit",
|
|
173
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
139
174
|
"test": "playwright test",
|
|
140
175
|
"test:ui": "playwright test --ui",
|
|
176
|
+
"test:hmr-local": "playwright test --project=dev-warmup --project=hmr-routes --project=hmr-basename --project=hmr-prerender --no-deps --workers=1",
|
|
141
177
|
"test:unit": "vitest run",
|
|
142
|
-
"test:unit:watch": "vitest"
|
|
178
|
+
"test:unit:watch": "vitest",
|
|
179
|
+
"test:unit:rsc": "vitest run --config vitest.rsc.config.ts"
|
|
143
180
|
},
|
|
144
181
|
"dependencies": {
|
|
145
|
-
"@
|
|
182
|
+
"@types/debug": "^4.1.12",
|
|
183
|
+
"@vitejs/plugin-rsc": "^0.5.27",
|
|
184
|
+
"debug": "^4.4.1",
|
|
146
185
|
"magic-string": "^0.30.17",
|
|
147
|
-
"picomatch": "^4.0.
|
|
148
|
-
"rsc-html-stream": "^0.0.7"
|
|
186
|
+
"picomatch": "^4.0.4",
|
|
187
|
+
"rsc-html-stream": "^0.0.7",
|
|
188
|
+
"srvx": "^0.11.15",
|
|
189
|
+
"tinyexec": "^0.3.2"
|
|
149
190
|
},
|
|
150
191
|
"devDependencies": {
|
|
192
|
+
"@opentelemetry/api": "^1.9.0",
|
|
193
|
+
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
194
|
+
"@opentelemetry/sdk-trace-base": "^2.9.0",
|
|
151
195
|
"@playwright/test": "^1.49.1",
|
|
196
|
+
"@shared/e2e": "workspace:*",
|
|
197
|
+
"@testing-library/dom": "^10.4.1",
|
|
198
|
+
"@testing-library/react": "^16.3.2",
|
|
152
199
|
"@types/node": "^24.10.1",
|
|
153
200
|
"@types/react": "catalog:",
|
|
154
201
|
"@types/react-dom": "catalog:",
|
|
155
|
-
"esbuild": "^0.
|
|
156
|
-
"
|
|
202
|
+
"esbuild": "^0.28.1",
|
|
203
|
+
"happy-dom": "^20.10.1",
|
|
204
|
+
"jiti": "^2.7.0",
|
|
157
205
|
"react": "catalog:",
|
|
158
206
|
"react-dom": "catalog:",
|
|
159
|
-
"tinyexec": "^0.3.2",
|
|
160
207
|
"typescript": "^5.3.0",
|
|
161
|
-
"vitest": "^4.
|
|
208
|
+
"vitest": "^4.1.9"
|
|
162
209
|
},
|
|
163
210
|
"peerDependencies": {
|
|
164
|
-
"@cloudflare/vite-plugin": "^1.
|
|
165
|
-
"@
|
|
166
|
-
"
|
|
167
|
-
"
|
|
211
|
+
"@cloudflare/vite-plugin": "^1.42.1",
|
|
212
|
+
"@opentelemetry/api": "^1.9.0",
|
|
213
|
+
"@playwright/test": "^1.49.1",
|
|
214
|
+
"@testing-library/react": ">=16",
|
|
215
|
+
"@vercel/functions": "^3.0.0",
|
|
216
|
+
"@vitejs/plugin-rsc": "^0.5.27",
|
|
217
|
+
"react": ">=19.2.6 <20",
|
|
218
|
+
"react-dom": ">=19.2.6 <20",
|
|
219
|
+
"vite": "^8.0.16",
|
|
220
|
+
"vitest": ">=3"
|
|
168
221
|
},
|
|
169
222
|
"peerDependenciesMeta": {
|
|
170
223
|
"@cloudflare/vite-plugin": {
|
|
171
224
|
"optional": true
|
|
172
225
|
},
|
|
226
|
+
"@opentelemetry/api": {
|
|
227
|
+
"optional": true
|
|
228
|
+
},
|
|
229
|
+
"@playwright/test": {
|
|
230
|
+
"optional": true
|
|
231
|
+
},
|
|
232
|
+
"@testing-library/react": {
|
|
233
|
+
"optional": true
|
|
234
|
+
},
|
|
235
|
+
"@vercel/functions": {
|
|
236
|
+
"optional": true
|
|
237
|
+
},
|
|
173
238
|
"vite": {
|
|
174
239
|
"optional": true
|
|
240
|
+
},
|
|
241
|
+
"vitest": {
|
|
242
|
+
"optional": true
|
|
175
243
|
}
|
|
244
|
+
},
|
|
245
|
+
"engines": {
|
|
246
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
176
247
|
}
|
|
177
248
|
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: api-client
|
|
3
|
+
description: Build a typed client for consuming your own response-route JSON APIs (no codegen). Use when calling your own JSON endpoints from another service or script, or you want typed fetch calls without a codegen step.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Typed API Client
|
|
7
|
+
|
|
8
|
+
Response routes (`path.json()`) already ship typed responses — `RouteResponse<typeof patterns, "name">` resolves to the **bare payload**, inferred from your handler with no codegen. This skill wraps that inference in a small **typed client** so first-party TypeScript code calls your endpoints like functions instead of hand-writing `fetch` + URL building per call site.
|
|
9
|
+
|
|
10
|
+
This is a **recipe, not a framework feature** — copy the helper below into your app. It depends only on **type-only** imports from `@rangojs/router` (`RouteResponse`, `ExtractParams`, `ProblemDetails`), which are erased at build time, so it runs anywhere a `fetch` does — **browser, worker, or server**. Nothing new to install or version.
|
|
11
|
+
|
|
12
|
+
> **Scope:** the typed client is a **first-party TypeScript** convenience. External/third-party consumers use the plain wire directly — bare JSON on success, RFC 9457 `application/problem+json` on error — which needs no client. (Language-agnostic OpenAPI generation is a separate, future feature.)
|
|
13
|
+
|
|
14
|
+
## What you get
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
const api = createApiClient(apiShopPatterns, routes, { baseUrl });
|
|
18
|
+
|
|
19
|
+
await api.health.get(); // no params → callable bare
|
|
20
|
+
await api.product.get({ params: { productId } }); // params typed + required
|
|
21
|
+
await api.cart.post({ body: { productId, qty: 2 } }); // body sent as JSON
|
|
22
|
+
// ^ result is the bare payload type (RouteResponse), not `any`, no `.data`
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- **Output typed** from the handler's return (`RouteResponse`), zero codegen.
|
|
26
|
+
- **Params required + typed** from the route pattern (`/catalog/:productId` → `{ productId: string }`); a missing or misspelled param is a **compile error**, not a runtime 404.
|
|
27
|
+
- **Autocomplete** over every route name; rename-safe.
|
|
28
|
+
- **Errors throw a typed `ApiError`** carrying the `ProblemDetails` body.
|
|
29
|
+
|
|
30
|
+
(`search` and `body` are _not_ route-typed — see Notes.)
|
|
31
|
+
|
|
32
|
+
## The two inputs
|
|
33
|
+
|
|
34
|
+
1. **The `urls()` patterns value** — the type source. `typeof apiShopPatterns` carries the per-route response payloads (`_responses`) and patterns (`_routes`).
|
|
35
|
+
2. **The generated route map** — the name → pattern source. `rango generate` emits a per-module `<name>.gen.ts` exporting `routes`:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
// api-shop.gen.ts (generated — do not edit)
|
|
39
|
+
export const routes = {
|
|
40
|
+
catalog: "/catalog",
|
|
41
|
+
product: "/catalog/:productId",
|
|
42
|
+
cart: "/cart",
|
|
43
|
+
// ...
|
|
44
|
+
} as const;
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Routes that declare a **search schema** are generated as objects instead — `index: { path: "/", search: { q: "string" } }`. The helper accepts both the string and `{ path }` forms. If a `urls()` block is mounted under a name prefix, build a local-keyed map from your global `NamedRoutes` so the keys match the block's route names (e.g. `{ catalog: NamedRoutes["apiShop.catalog"], ... } as const`).
|
|
48
|
+
|
|
49
|
+
## The helper (copy into your app)
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
// lib/api-client.ts
|
|
53
|
+
import type {
|
|
54
|
+
RouteResponse,
|
|
55
|
+
ExtractParams,
|
|
56
|
+
ProblemDetails,
|
|
57
|
+
} from "@rangojs/router";
|
|
58
|
+
|
|
59
|
+
type SearchParams = Record<string, string | number | boolean>;
|
|
60
|
+
|
|
61
|
+
// A generated route-map entry is a pattern string, or an object with `path`
|
|
62
|
+
// (routes that declare a search schema generate the object form).
|
|
63
|
+
type RouteMapEntry = string | { readonly path: string };
|
|
64
|
+
type PatternOf<E> = E extends string
|
|
65
|
+
? E
|
|
66
|
+
: E extends { readonly path: infer P extends string }
|
|
67
|
+
? P
|
|
68
|
+
: never;
|
|
69
|
+
|
|
70
|
+
// `params` is optional when the route has no *required* params (incl.
|
|
71
|
+
// optional-only routes like `/:locale?`), required otherwise. Typed as
|
|
72
|
+
// `ExtractParams` (not `undefined`) so optional params can still be passed.
|
|
73
|
+
type Args<TPattern extends string> =
|
|
74
|
+
{} extends ExtractParams<TPattern>
|
|
75
|
+
? {
|
|
76
|
+
params?: ExtractParams<TPattern>;
|
|
77
|
+
search?: SearchParams;
|
|
78
|
+
body?: unknown;
|
|
79
|
+
}
|
|
80
|
+
: {
|
|
81
|
+
params: ExtractParams<TPattern>;
|
|
82
|
+
search?: SearchParams;
|
|
83
|
+
body?: unknown;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
type Method<TPatterns, K extends string, TEntry> =
|
|
87
|
+
{} extends ExtractParams<PatternOf<TEntry>>
|
|
88
|
+
? (args?: Args<PatternOf<TEntry>>) => Promise<RouteResponse<TPatterns, K>>
|
|
89
|
+
: (args: Args<PatternOf<TEntry>>) => Promise<RouteResponse<TPatterns, K>>;
|
|
90
|
+
|
|
91
|
+
type ApiClient<TPatterns, TRouteMap extends Record<string, RouteMapEntry>> = {
|
|
92
|
+
[K in keyof TRouteMap & string]: {
|
|
93
|
+
get: Method<TPatterns, K, TRouteMap[K]>;
|
|
94
|
+
post: Method<TPatterns, K, TRouteMap[K]>;
|
|
95
|
+
put: Method<TPatterns, K, TRouteMap[K]>;
|
|
96
|
+
patch: Method<TPatterns, K, TRouteMap[K]>;
|
|
97
|
+
delete: Method<TPatterns, K, TRouteMap[K]>;
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** Thrown on a non-2xx response; carries the RFC 9457 problem body. */
|
|
102
|
+
export class ApiError extends Error {
|
|
103
|
+
status: number;
|
|
104
|
+
problem: ProblemDetails;
|
|
105
|
+
constructor(status: number, problem: ProblemDetails) {
|
|
106
|
+
super(problem.detail || `HTTP ${status}`);
|
|
107
|
+
this.name = "ApiError";
|
|
108
|
+
this.status = status;
|
|
109
|
+
this.problem = problem;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Client-safe path builder: substitutes :params (incl. optional/constrained
|
|
114
|
+
// forms) into the pattern. No dependency on the server-only createReverse.
|
|
115
|
+
function fillPath(pattern: string, params?: Record<string, string>): string {
|
|
116
|
+
return pattern
|
|
117
|
+
.replace(/:([A-Za-z0-9_]+)(?:\([^)]*\))?\??/g, (_m, name: string) => {
|
|
118
|
+
const v = params?.[name];
|
|
119
|
+
return v == null ? "" : encodeURIComponent(String(v));
|
|
120
|
+
})
|
|
121
|
+
.replace(/\/{2,}/g, "/");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function createApiClient<
|
|
125
|
+
TPatterns,
|
|
126
|
+
const TRouteMap extends Record<string, RouteMapEntry>,
|
|
127
|
+
>(
|
|
128
|
+
_patterns: TPatterns,
|
|
129
|
+
routeMap: TRouteMap,
|
|
130
|
+
opts: { baseUrl?: string; fetch?: typeof fetch } = {},
|
|
131
|
+
): ApiClient<TPatterns, TRouteMap> {
|
|
132
|
+
const doFetch = opts.fetch ?? fetch;
|
|
133
|
+
const baseUrl = opts.baseUrl ?? "";
|
|
134
|
+
const call =
|
|
135
|
+
(name: string, method: string) =>
|
|
136
|
+
async (args?: {
|
|
137
|
+
params?: Record<string, string>;
|
|
138
|
+
search?: SearchParams;
|
|
139
|
+
body?: unknown;
|
|
140
|
+
}) => {
|
|
141
|
+
const entry = routeMap[name];
|
|
142
|
+
const pattern = typeof entry === "string" ? entry : entry.path;
|
|
143
|
+
let url = baseUrl + fillPath(pattern, args?.params);
|
|
144
|
+
if (args?.search) {
|
|
145
|
+
const qs = new URLSearchParams();
|
|
146
|
+
for (const [k, v] of Object.entries(args.search)) {
|
|
147
|
+
if (v != null) qs.append(k, String(v));
|
|
148
|
+
}
|
|
149
|
+
const s = qs.toString();
|
|
150
|
+
if (s) url += (url.includes("?") ? "&" : "?") + s;
|
|
151
|
+
}
|
|
152
|
+
const res = await doFetch(url, {
|
|
153
|
+
method,
|
|
154
|
+
...(args?.body !== undefined
|
|
155
|
+
? {
|
|
156
|
+
body: JSON.stringify(args.body),
|
|
157
|
+
headers: { "content-type": "application/json" },
|
|
158
|
+
}
|
|
159
|
+
: {}),
|
|
160
|
+
});
|
|
161
|
+
if (!res.ok) {
|
|
162
|
+
const problem = (await res.json().catch(() => ({}))) as ProblemDetails;
|
|
163
|
+
throw new ApiError(res.status, problem);
|
|
164
|
+
}
|
|
165
|
+
return res.json();
|
|
166
|
+
};
|
|
167
|
+
return new Proxy({} as any, {
|
|
168
|
+
get: (_t, name: string) => ({
|
|
169
|
+
get: call(name, "GET"),
|
|
170
|
+
post: call(name, "POST"),
|
|
171
|
+
put: call(name, "PUT"),
|
|
172
|
+
patch: call(name, "PATCH"),
|
|
173
|
+
delete: call(name, "DELETE"),
|
|
174
|
+
}),
|
|
175
|
+
}) as ApiClient<TPatterns, TRouteMap>;
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Using it
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import { apiShopPatterns } from "./urls/api-shop";
|
|
183
|
+
import { routes } from "./urls/api-shop.gen";
|
|
184
|
+
import { createApiClient, ApiError } from "./lib/api-client";
|
|
185
|
+
|
|
186
|
+
const api = createApiClient(apiShopPatterns, routes, {
|
|
187
|
+
baseUrl: import.meta.env.VITE_API_URL ?? "",
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const product = await api.product.get({ params: { productId: "42" } });
|
|
192
|
+
// `product` is the handler's bare return type — e.g. `product.name` is typed.
|
|
193
|
+
} catch (err) {
|
|
194
|
+
if (err instanceof ApiError && err.status === 404) {
|
|
195
|
+
console.warn(err.problem.code, err.problem.detail); // typed ProblemDetails
|
|
196
|
+
} else {
|
|
197
|
+
throw err;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Notes
|
|
203
|
+
|
|
204
|
+
- **Client-safe by construction.** The helper imports only **types** from `@rangojs/router` (erased at build) and builds URLs itself by substituting `:params` into the pattern — it does **not** use `createReverse`, which is a server/RSC-only export that throws in the browser. So `createApiClient` works in client components, workers, and on the server alike.
|
|
205
|
+
- **Params are route-typed; search and body are not.** Path params come from the route pattern (`ExtractParams`), so they are precise and required. `search` is generically typed (`Record<string, string | number | boolean>`), and `body` is `unknown` (serialized to JSON). Typed request **input** needs a declared schema layer, which is intentionally out of scope here — thread per-route schemas in yourself if you want typed search/body.
|
|
206
|
+
- **Verb-agnostic wire.** Rango response routes do not dispatch on HTTP method — `.get`/`.post`/etc. set the request method but hit the same handler. Use whichever verb reads best for the operation.
|
|
207
|
+
- **Path building.** `fillPath` handles standard `:param`, optional `:param?`, and constrained `:param(a|b)` forms. For exotic patterns or strict trailing-slash policies, swap in your own builder (or the router's `reverse` on the server).
|
|
208
|
+
- **Want a return-based style instead of throwing?** Branch on `res.ok` yourself: the wire is the bare value on 2xx and `ProblemDetails` on non-2xx (see `/response-routes`). Wrapping the calls in a `{ ok, data } | { ok: false, error }` result type is a small variation on the same helper.
|
|
209
|
+
- **Third parties.** The typed client is TypeScript-only and needs your route types. External consumers in any language use the plain wire as-is (bare JSON + problem+json); no client required.
|
|
210
|
+
|
|
211
|
+
See `/response-routes` for the endpoint side and `/typesafety` for how `RouteResponse` / `PathResponse` inference works.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: breadcrumbs
|
|
3
|
-
description: Built-in Breadcrumbs handle for accumulating breadcrumb navigation across route segments
|
|
4
|
-
argument-hint: [setup]
|
|
3
|
+
description: Built-in Breadcrumbs handle for accumulating breadcrumb navigation across route segments. Use when building a breadcrumb trail for nested routes, or asking how to show the current navigation path in a layout.
|
|
4
|
+
argument-hint: "[setup]"
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Breadcrumbs
|
|
@@ -81,6 +81,70 @@ path("/product/:id", async (ctx) => {
|
|
|
81
81
|
Async content is a `Promise<ReactNode>`. Resolve it in your component
|
|
82
82
|
with React's `use()` hook wrapped in `<Suspense>`.
|
|
83
83
|
|
|
84
|
+
### Deferred content (decide now, resolve from a deep component)
|
|
85
|
+
|
|
86
|
+
When the handler should DECIDE to push a crumb (it holds `ctx`, so the decision
|
|
87
|
+
must land before the handles stream seals) but the value is produced far away — by
|
|
88
|
+
a deep async component, not the handler — call `.defer()` on the push function.
|
|
89
|
+
`ctx.use(Handle)` returns the push function; `.defer(options)` reserves the crumb's
|
|
90
|
+
slot synchronously and returns a **resolver that is push-equal** — you call it
|
|
91
|
+
later, anywhere in the render, with the same argument you'd have passed to the
|
|
92
|
+
push (a value, a `Promise`, or a thunk). The only added behavior is a timeout, so a
|
|
93
|
+
forgotten resolve can't hang the render (and the HTTP response): resolve-by-default
|
|
94
|
+
awaits the reserved slot before any consumer reads it, and the timeout guarantees it
|
|
95
|
+
settles to `else` instead of blocking forever.
|
|
96
|
+
|
|
97
|
+
Reserve the slot in the handler, then resolve it from a nested async component
|
|
98
|
+
that closes over the resolver — no extra wiring (the resolver is a plain closure,
|
|
99
|
+
not outlet context):
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
import { Breadcrumbs } from "@rangojs/router";
|
|
103
|
+
import { Outlet } from "@rangojs/router/client";
|
|
104
|
+
import { Suspense } from "react";
|
|
105
|
+
|
|
106
|
+
function DocsLayout(ctx) {
|
|
107
|
+
const breadcrumb = ctx.use(Breadcrumbs);
|
|
108
|
+
// Decide now (the slot is reserved before the stream seals); resolve later.
|
|
109
|
+
const resolveCrumb = breadcrumb.defer({ timeoutMs: 5000, else: null });
|
|
110
|
+
|
|
111
|
+
// Deep, async, far from the handler — closes over the resolver, never touches ctx.
|
|
112
|
+
// Same call shape as breadcrumb({ ... }), just deferred:
|
|
113
|
+
async function LiveCrumb() {
|
|
114
|
+
const n = await countOpenIssues();
|
|
115
|
+
resolveCrumb({ label: "Docs", href: "/docs", content: <span>{n}</span> });
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<>
|
|
121
|
+
<Suspense>
|
|
122
|
+
<LiveCrumb />
|
|
123
|
+
</Suspense>
|
|
124
|
+
<Outlet />
|
|
125
|
+
</>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
If the resolver is never called, the slot auto-resolves to `else` after
|
|
131
|
+
`timeoutMs` (default 10s) and warns in dev — graceful degradation instead of a
|
|
132
|
+
hung request. `timeoutMs: 0` or `Infinity` disable the timeout intentionally; any
|
|
133
|
+
other non-finite or negative value falls back to the default rather than silently
|
|
134
|
+
disabling the safety net.
|
|
135
|
+
|
|
136
|
+
**Consumer note (resolve-by-default):** a deferred crumb is RESOLVED before any
|
|
137
|
+
consumer sees it — `useHandle(Breadcrumbs)` returns the resolved item, never a
|
|
138
|
+
`Promise`, so you read it like any sync crumb (no `use()`, no thenable narrowing).
|
|
139
|
+
On a full/SSR load the value is resolved server-side; on a soft navigation the
|
|
140
|
+
breadcrumbs HOLD the previous resolved value until the deferred value lands, then
|
|
141
|
+
swap in — no blank, no pending entry. If the slot times out to `else: null`/
|
|
142
|
+
undefined, the entry is simply dropped. Use `.defer()` only when even
|
|
143
|
+
`label`/`href` are unknown at handler time — if you know them and only the
|
|
144
|
+
`content` is async, push a concrete item with a `Promise` `content` field instead
|
|
145
|
+
(the `content` field is a nested promise you resolve with `use()` in your
|
|
146
|
+
component; no `.defer()` needed).
|
|
147
|
+
|
|
84
148
|
## Consuming Breadcrumbs (Client)
|
|
85
149
|
|
|
86
150
|
Use `useHandle(Breadcrumbs)` in a client component to read the accumulated items:
|
|
@@ -141,10 +205,12 @@ path("/dashboard", (ctx) => {
|
|
|
141
205
|
breadcrumb({ label: "Dashboard", href: "/dashboard" });
|
|
142
206
|
return <DashboardNav handle={Breadcrumbs} />;
|
|
143
207
|
});
|
|
208
|
+
```
|
|
144
209
|
|
|
210
|
+
```tsx
|
|
145
211
|
// Client component
|
|
146
|
-
|
|
147
|
-
import { useHandle,
|
|
212
|
+
"use client";
|
|
213
|
+
import { useHandle, Breadcrumbs } from "@rangojs/router/client";
|
|
148
214
|
|
|
149
215
|
function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
|
|
150
216
|
const crumbs = useHandle(handle);
|
|
@@ -212,15 +278,28 @@ Create your own handle with `createHandle()`:
|
|
|
212
278
|
```typescript
|
|
213
279
|
import { createHandle } from "@rangojs/router";
|
|
214
280
|
|
|
215
|
-
//
|
|
281
|
+
// Custom collect: last value wins.
|
|
216
282
|
export const PageTitle = createHandle<string, string>(
|
|
217
283
|
(segments) => segments.flat().at(-1) ?? "Default Title",
|
|
218
284
|
);
|
|
219
285
|
|
|
220
|
-
// No collect
|
|
286
|
+
// No collect: the DEFAULT is the identity (lossless) — `collect` receives the
|
|
287
|
+
// per-segment data (TData[][], one array per segment that pushed, in segment
|
|
288
|
+
// order) and passes it through as-is. `useHandle(Warnings)` is `string[][]`, so a
|
|
289
|
+
// consumer can tell which/how-many segments contributed.
|
|
221
290
|
export const Warnings = createHandle<string>();
|
|
291
|
+
|
|
292
|
+
// Want a single flat list instead? Opt in:
|
|
293
|
+
export const FlatWarnings = createHandle<string, string[]>((segments) =>
|
|
294
|
+
segments.flat(),
|
|
295
|
+
);
|
|
222
296
|
```
|
|
223
297
|
|
|
298
|
+
A handle whose module is never imported (so `createHandle()` never ran to register
|
|
299
|
+
its collect) falls back to this same identity default and **warns in dev** — a
|
|
300
|
+
handle with a custom collect that failed to register would otherwise return the
|
|
301
|
+
wrong shape silently, and the runtime can't tell it from one that wanted the default.
|
|
302
|
+
|
|
224
303
|
The Vite `exposeInternalIds` plugin auto-injects a stable `$$id` based on
|
|
225
304
|
file path and export name. No manual naming required for project-local code.
|
|
226
305
|
|