@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.80
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 +9 -0
- package/README.md +942 -4
- package/dist/bin/rango.js +1689 -0
- package/dist/vite/index.js +4960 -935
- package/package.json +70 -60
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +294 -0
- package/skills/caching/SKILL.md +93 -23
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +18 -16
- package/skills/fonts/SKILL.md +167 -0
- package/skills/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +334 -72
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +151 -8
- package/skills/layout/SKILL.md +122 -3
- package/skills/links/SKILL.md +92 -31
- package/skills/loader/SKILL.md +404 -44
- package/skills/middleware/SKILL.md +205 -37
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +764 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +263 -1
- package/skills/prerender/SKILL.md +685 -0
- package/skills/rango/SKILL.md +87 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +281 -14
- package/skills/router-setup/SKILL.md +210 -32
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +328 -89
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +102 -4
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +92 -64
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +317 -560
- package/src/browser/navigation-client.ts +206 -68
- package/src/browser/navigation-store.ts +73 -55
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +343 -316
- package/src/browser/prefetch/cache.ts +216 -0
- package/src/browser/prefetch/fetch.ts +206 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +160 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +253 -74
- package/src/browser/react/NavigationProvider.tsx +87 -11
- package/src/browser/react/context.ts +11 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +12 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- package/src/browser/react/mount-context.ts +6 -1
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +30 -126
- package/src/browser/react/use-href.tsx +2 -2
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +44 -65
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +76 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +80 -97
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +214 -58
- package/src/browser/scroll-restoration.ts +127 -52
- package/src/browser/segment-reconciler.ts +243 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +510 -603
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +141 -48
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +235 -24
- package/src/build/generate-route-types.ts +39 -0
- package/src/build/index.ts +13 -0
- package/src/build/route-trie.ts +291 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +418 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +618 -0
- package/src/build/route-types/scan-filter.ts +85 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +342 -0
- package/src/cache/cache-scope.ts +167 -309
- package/src/cache/cf/cf-cache-store.ts +571 -17
- package/src/cache/cf/index.ts +13 -3
- package/src/cache/document-cache.ts +116 -77
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +1 -15
- package/src/cache/memory-segment-store.ts +191 -13
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +153 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +3 -1
- package/src/client.tsx +135 -301
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +156 -0
- package/src/debug.ts +19 -9
- package/src/errors.ts +108 -2
- package/src/handle.ts +55 -29
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +21 -15
- package/src/host/errors.ts +8 -8
- package/src/host/index.ts +4 -7
- package/src/host/pattern-matcher.ts +27 -27
- package/src/host/router.ts +61 -39
- package/src/host/testing.ts +8 -8
- package/src/host/types.ts +15 -7
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +119 -29
- package/src/index.rsc.ts +155 -19
- package/src/index.ts +251 -30
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +26 -157
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +186 -0
- package/src/prerender.ts +524 -0
- package/src/reverse.ts +354 -0
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +1121 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +478 -0
- package/src/route-definition/index.ts +55 -0
- package/src/route-definition/redirect.ts +101 -0
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-definition.ts +1 -1428
- package/src/route-map-builder.ts +217 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +77 -8
- package/src/router/content-negotiation.ts +215 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +438 -86
- package/src/router/intercept-resolution.ts +402 -0
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +356 -128
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +163 -35
- package/src/router/match-api.ts +555 -0
- package/src/router/match-context.ts +5 -3
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +108 -93
- package/src/router/match-middleware/cache-lookup.ts +460 -10
- package/src/router/match-middleware/cache-store.ts +98 -26
- package/src/router/match-middleware/intercept-resolution.ts +57 -17
- package/src/router/match-middleware/segment-resolution.ts +80 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +135 -35
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +220 -0
- package/src/router/middleware.ts +324 -369
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +211 -43
- package/src/router/prerender-match.ts +502 -0
- package/src/router/preview-match.ts +98 -0
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +137 -38
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +41 -21
- package/src/router/router-interfaces.ts +484 -0
- package/src/router/router-options.ts +618 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +748 -0
- package/src/router/segment-resolution/helpers.ts +268 -0
- package/src/router/segment-resolution/loader-cache.ts +199 -0
- package/src/router/segment-resolution/revalidation.ts +1379 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +239 -0
- package/src/router/types.ts +78 -3
- package/src/router.ts +740 -4252
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +907 -797
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +229 -0
- package/src/rsc/manifest-init.ts +90 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +391 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +246 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +356 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +46 -11
- package/src/search-params.ts +230 -0
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +134 -36
- package/src/server/context.ts +341 -61
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +113 -15
- package/src/server/loader-registry.ts +24 -64
- package/src/server/request-context.ts +607 -81
- package/src/server.ts +35 -130
- package/src/ssr/index.tsx +103 -30
- package/src/static-handler.ts +126 -0
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +791 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +210 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +120 -0
- package/src/types/segments.ts +150 -0
- package/src/types.ts +1 -1623
- package/src/urls/include-helper.ts +207 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +372 -0
- package/src/urls/path-helper.ts +364 -0
- package/src/urls/pattern-types.ts +107 -0
- package/src/urls/response-types.ts +116 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -802
- package/src/use-loader.tsx +161 -81
- package/src/vite/discovery/bundle-postprocess.ts +181 -0
- package/src/vite/discovery/discover-routers.ts +348 -0
- package/src/vite/discovery/prerender-collection.ts +439 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +117 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +15 -1133
- package/src/vite/plugin-types.ts +103 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
- package/src/vite/plugins/expose-id-utils.ts +299 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +786 -0
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +266 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +462 -0
- package/src/vite/router-discovery.ts +918 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
- package/src/vite/utils/prerender-utils.ts +221 -0
- package/src/vite/utils/shared-utils.ts +170 -0
- package/CLAUDE.md +0 -43
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/href.ts +0 -255
- package/src/server/route-manifest-cache.ts +0 -173
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -426
- package/src/vite/expose-location-state-id.ts +0 -177
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
package/src/reverse.ts
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import type { ExtractParams } from "./types.js";
|
|
2
|
+
import type { SearchSchema, ResolveSearchSchema } from "./search-params.js";
|
|
3
|
+
import { serializeSearchParams } from "./search-params.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Sanitize prefix string by removing leading slash
|
|
7
|
+
* "/shop" -> "shop", "blog" -> "blog", "" -> ""
|
|
8
|
+
*/
|
|
9
|
+
export type SanitizePrefix<T extends string> = T extends `/${infer P}` ? P : T;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Helper type to merge multiple route definitions into a single accumulated type.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* type AppRoutes = MergeRoutes<[typeof siteRoutes, typeof apiRoutes]>;
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export type MergeRoutes<T extends unknown[]> = T extends [
|
|
20
|
+
infer First,
|
|
21
|
+
...infer Rest,
|
|
22
|
+
]
|
|
23
|
+
? First & MergeRoutes<Rest>
|
|
24
|
+
: {};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Helper to safely extract route patterns from a routes object
|
|
28
|
+
* Handles string values, { path, response } objects, and interface types (like RegisteredRoutes)
|
|
29
|
+
*/
|
|
30
|
+
type RoutePatternFor<
|
|
31
|
+
TRoutes,
|
|
32
|
+
TName extends keyof TRoutes,
|
|
33
|
+
> = TRoutes[TName] extends string
|
|
34
|
+
? TRoutes[TName]
|
|
35
|
+
: TRoutes[TName] extends { readonly path: infer P extends string }
|
|
36
|
+
? P
|
|
37
|
+
: string;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extract params type for a route
|
|
41
|
+
*/
|
|
42
|
+
export type ParamsFor<TRoutes, TName extends keyof TRoutes> = ExtractParams<
|
|
43
|
+
RoutePatternFor<TRoutes, TName>
|
|
44
|
+
>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if an object type has any keys
|
|
48
|
+
*/
|
|
49
|
+
type IsEmptyObject<T> = keyof T extends never ? true : false;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Extract search schema from a route entry.
|
|
53
|
+
* Returns {} if no search schema is defined.
|
|
54
|
+
*/
|
|
55
|
+
type ExtractSearchSchema<
|
|
56
|
+
TRoutes,
|
|
57
|
+
TName extends keyof TRoutes,
|
|
58
|
+
> = TRoutes[TName] extends { readonly search: infer S extends SearchSchema }
|
|
59
|
+
? S
|
|
60
|
+
: {};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Type-safe reverse function signature (Django-style URL reversal)
|
|
64
|
+
*
|
|
65
|
+
* Validates route names and params at compile time.
|
|
66
|
+
* Use route names instead of raw paths for full type safety.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* reverse("cart") // ✓ Validates route exists
|
|
71
|
+
* reverse("product.detail", { id: "123" }) // ✓ Validates route + params
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export type ReverseFunction<TRoutes> = {
|
|
75
|
+
/**
|
|
76
|
+
* Route without params - validates route name exists
|
|
77
|
+
*/
|
|
78
|
+
<TName extends keyof TRoutes & string>(
|
|
79
|
+
name: IsEmptyObject<
|
|
80
|
+
ExtractParams<RoutePatternFor<TRoutes, TName>>
|
|
81
|
+
> extends true
|
|
82
|
+
? TName
|
|
83
|
+
: never,
|
|
84
|
+
): string;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Route with params - validates both route name and params
|
|
88
|
+
*/
|
|
89
|
+
<TName extends keyof TRoutes & string>(
|
|
90
|
+
name: TName,
|
|
91
|
+
params: ExtractParams<RoutePatternFor<TRoutes, TName>>,
|
|
92
|
+
): string;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Route with params and search - validates route name, params, and search
|
|
96
|
+
*/
|
|
97
|
+
<TName extends keyof TRoutes & string>(
|
|
98
|
+
name: TName,
|
|
99
|
+
params: ExtractParams<RoutePatternFor<TRoutes, TName>>,
|
|
100
|
+
search: ResolveSearchSchema<ExtractSearchSchema<TRoutes, TName>>,
|
|
101
|
+
): string;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Dot-prefixed route without params - strictly local resolution
|
|
105
|
+
*/
|
|
106
|
+
<TName extends keyof TRoutes & string>(
|
|
107
|
+
name: IsEmptyObject<
|
|
108
|
+
ExtractParams<RoutePatternFor<TRoutes, TName>>
|
|
109
|
+
> extends true
|
|
110
|
+
? `.${TName}`
|
|
111
|
+
: never,
|
|
112
|
+
): string;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Dot-prefixed route with params - strictly local resolution
|
|
116
|
+
*/
|
|
117
|
+
<TName extends keyof TRoutes & string>(
|
|
118
|
+
name: `.${TName}`,
|
|
119
|
+
params: ExtractParams<RoutePatternFor<TRoutes, TName>>,
|
|
120
|
+
): string;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Dot-prefixed route with params and search - strictly local resolution
|
|
124
|
+
*/
|
|
125
|
+
<TName extends keyof TRoutes & string>(
|
|
126
|
+
name: `.${TName}`,
|
|
127
|
+
params: ExtractParams<RoutePatternFor<TRoutes, TName>>,
|
|
128
|
+
search: ResolveSearchSchema<ExtractSearchSchema<TRoutes, TName>>,
|
|
129
|
+
): string;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Type-safe scoped reverse function with separate local and global namespaces.
|
|
134
|
+
*
|
|
135
|
+
* - `.name` — local resolution within the current include() scope
|
|
136
|
+
* - `name` — global resolution against the named-routes definition
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```typescript
|
|
140
|
+
* reverse(".article", { slug: "hello" }) // ✓ Local route (resolves with mount prefix)
|
|
141
|
+
* reverse(".index") // ✓ Local route (no params)
|
|
142
|
+
* reverse("magazine.index") // ✓ Global route (fully qualified)
|
|
143
|
+
* reverse("blog.post", { slug: "hello" }) // ✓ Global route + params
|
|
144
|
+
* reverse(".typo") // ✗ Compile error (not in local routes)
|
|
145
|
+
* reverse("typo") // ✗ Compile error (not in global routes)
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
export type ScopedReverseFunction<
|
|
149
|
+
TLocalRoutes,
|
|
150
|
+
TGlobalRoutes = TLocalRoutes,
|
|
151
|
+
> = {
|
|
152
|
+
/**
|
|
153
|
+
* Global route without params
|
|
154
|
+
*/
|
|
155
|
+
<TName extends keyof TGlobalRoutes & string>(
|
|
156
|
+
name: IsEmptyObject<
|
|
157
|
+
ExtractParams<RoutePatternFor<TGlobalRoutes, TName>>
|
|
158
|
+
> extends true
|
|
159
|
+
? TName
|
|
160
|
+
: never,
|
|
161
|
+
): string;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Global route with params
|
|
165
|
+
*/
|
|
166
|
+
<TName extends keyof TGlobalRoutes & string>(
|
|
167
|
+
name: TName,
|
|
168
|
+
params: ExtractParams<RoutePatternFor<TGlobalRoutes, TName>>,
|
|
169
|
+
): string;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Global route with params and search
|
|
173
|
+
*/
|
|
174
|
+
<TName extends keyof TGlobalRoutes & string>(
|
|
175
|
+
name: TName,
|
|
176
|
+
params: ExtractParams<RoutePatternFor<TGlobalRoutes, TName>>,
|
|
177
|
+
search: ResolveSearchSchema<ExtractSearchSchema<TGlobalRoutes, TName>>,
|
|
178
|
+
): string;
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Dot-prefixed local route without params
|
|
182
|
+
*/
|
|
183
|
+
<TName extends keyof TLocalRoutes & string>(
|
|
184
|
+
name: IsEmptyObject<
|
|
185
|
+
ExtractParams<RoutePatternFor<TLocalRoutes, TName>>
|
|
186
|
+
> extends true
|
|
187
|
+
? `.${TName}`
|
|
188
|
+
: never,
|
|
189
|
+
): string;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Dot-prefixed local route with params
|
|
193
|
+
*/
|
|
194
|
+
<TName extends keyof TLocalRoutes & string>(
|
|
195
|
+
name: `.${TName}`,
|
|
196
|
+
params: ExtractParams<RoutePatternFor<TLocalRoutes, TName>>,
|
|
197
|
+
): string;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Dot-prefixed local route with params and search
|
|
201
|
+
*/
|
|
202
|
+
<TName extends keyof TLocalRoutes & string>(
|
|
203
|
+
name: `.${TName}`,
|
|
204
|
+
params: ExtractParams<RoutePatternFor<TLocalRoutes, TName>>,
|
|
205
|
+
search: ResolveSearchSchema<ExtractSearchSchema<TLocalRoutes, TName>>,
|
|
206
|
+
): string;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Extract local routes type from UrlPatterns
|
|
211
|
+
* Used with scopedReverse() to get the routes type from patterns
|
|
212
|
+
*/
|
|
213
|
+
export type ExtractLocalRoutes<TPatterns> = TPatterns extends {
|
|
214
|
+
readonly _routes?: infer TRoutes;
|
|
215
|
+
}
|
|
216
|
+
? TRoutes
|
|
217
|
+
: TPatterns extends Record<string, string>
|
|
218
|
+
? TPatterns
|
|
219
|
+
: Record<string, string>;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Extract the response data type for a named route from a UrlPatterns instance.
|
|
223
|
+
* Re-exported from urls.ts for consumer convenience.
|
|
224
|
+
*/
|
|
225
|
+
export type { RouteResponse } from "./urls.js";
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get a locally-typed reverse function from ctx.reverse for composable modules.
|
|
229
|
+
*
|
|
230
|
+
* This is a type-only cast - ctx.reverse already resolves names at runtime.
|
|
231
|
+
* Provides type safety: `.name` validates against local routes,
|
|
232
|
+
* `name` validates against global named-routes.
|
|
233
|
+
*
|
|
234
|
+
* @param reverse - The ctx.reverse function from HandlerContext
|
|
235
|
+
* @returns The same reverse function, typed with local + global routes
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* ```typescript
|
|
239
|
+
* // urls/blog.tsx
|
|
240
|
+
* export const blogPatterns = urls(({ path }) => [
|
|
241
|
+
* path("/", (ctx) => {
|
|
242
|
+
* const reverse = scopedReverse<typeof blogPatterns>(ctx.reverse);
|
|
243
|
+
*
|
|
244
|
+
* reverse(".index"); // ✓ Local route
|
|
245
|
+
* reverse(".post", { slug: "x" }); // ✓ Local with params
|
|
246
|
+
* reverse("shop.cart"); // ✓ Global route
|
|
247
|
+
*
|
|
248
|
+
* return <BlogIndex />;
|
|
249
|
+
* }, { name: "index" }),
|
|
250
|
+
*
|
|
251
|
+
* path("/:slug", BlogPost, { name: "post" }),
|
|
252
|
+
* ]);
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
export function scopedReverse<TPatterns>(
|
|
256
|
+
reverse: (...args: any[]) => string,
|
|
257
|
+
): ScopedReverseFunction<ExtractLocalRoutes<TPatterns>> {
|
|
258
|
+
return reverse as ScopedReverseFunction<ExtractLocalRoutes<TPatterns>>;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Create a type-safe reverse function for URL generation
|
|
263
|
+
*
|
|
264
|
+
* @param routeMap - Flattened route map with all registered routes
|
|
265
|
+
* @returns Type-safe reverse function
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* ```typescript
|
|
269
|
+
* // Given routes: { cart: "/shop/cart", detail: "/shop/product/:slug" }
|
|
270
|
+
* const reverse = createReverse(routeMap);
|
|
271
|
+
* reverse("cart"); // "/shop/cart"
|
|
272
|
+
* reverse("detail", { slug: "my-product" }); // "/shop/product/my-product"
|
|
273
|
+
* ```
|
|
274
|
+
*/
|
|
275
|
+
type RouteMapEntry = string | { path: string; search?: Record<string, string> };
|
|
276
|
+
|
|
277
|
+
function resolveRoutePattern(
|
|
278
|
+
entry: RouteMapEntry | undefined,
|
|
279
|
+
): string | undefined {
|
|
280
|
+
if (!entry) return undefined;
|
|
281
|
+
return typeof entry === "string" ? entry : entry.path;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function createReverse<TRoutes extends Record<string, string>>(
|
|
285
|
+
routeMap: TRoutes,
|
|
286
|
+
): ReverseFunction<TRoutes & Record<string, string>> {
|
|
287
|
+
return ((
|
|
288
|
+
name: string,
|
|
289
|
+
params?: Record<string, string>,
|
|
290
|
+
search?: Record<string, unknown>,
|
|
291
|
+
) => {
|
|
292
|
+
const pattern = resolveRoutePattern(
|
|
293
|
+
routeMap[name] as unknown as RouteMapEntry,
|
|
294
|
+
);
|
|
295
|
+
if (!pattern) {
|
|
296
|
+
// During build-time discovery, lazy includes haven't resolved yet.
|
|
297
|
+
// Return a placeholder instead of crashing the build.
|
|
298
|
+
if ((globalThis as any).__rscRouterDiscoveryActive) {
|
|
299
|
+
return `/__unresolved_reverse/${name}`;
|
|
300
|
+
}
|
|
301
|
+
throw new Error(`Unknown route: ${name}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
let result = pattern;
|
|
305
|
+
if (params) {
|
|
306
|
+
// Replace :param placeholders with actual values
|
|
307
|
+
// Strip constraint syntax: :param(a|b) -> use "param" as key
|
|
308
|
+
// Optional params (:param?) are omitted when not provided
|
|
309
|
+
let hadOmittedOptional = false;
|
|
310
|
+
result = result.replace(
|
|
311
|
+
/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(\?)/g,
|
|
312
|
+
(_, key, _constraint, optional) => {
|
|
313
|
+
const value = params[key];
|
|
314
|
+
// Empty string is treated as omitted — the trie matcher fills
|
|
315
|
+
// unmatched optional params with "" (not undefined), so reverse
|
|
316
|
+
// must collapse those segments instead of leaving empty slots.
|
|
317
|
+
if (value === undefined || value === "") {
|
|
318
|
+
hadOmittedOptional = true;
|
|
319
|
+
return "";
|
|
320
|
+
}
|
|
321
|
+
return encodeURIComponent(value);
|
|
322
|
+
},
|
|
323
|
+
);
|
|
324
|
+
// Second pass: required params (no trailing ?)
|
|
325
|
+
result = result.replace(
|
|
326
|
+
/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(?!\?)/g,
|
|
327
|
+
(_, key) => {
|
|
328
|
+
const value = params[key];
|
|
329
|
+
if (value === undefined) {
|
|
330
|
+
throw new Error(`Missing param "${key}" for route "${name}"`);
|
|
331
|
+
}
|
|
332
|
+
return encodeURIComponent(value);
|
|
333
|
+
},
|
|
334
|
+
);
|
|
335
|
+
// Clean up slashes only when an optional param was actually omitted,
|
|
336
|
+
// so intentional trailing-slash patterns like "/blog/" are preserved.
|
|
337
|
+
if (hadOmittedOptional) {
|
|
338
|
+
const hadTrailingSlash = pattern.length > 1 && pattern.endsWith("/");
|
|
339
|
+
result = result.replace(/\/\/+/g, "/").replace(/\/+$/, "") || "/";
|
|
340
|
+
if (hadTrailingSlash && !result.endsWith("/")) result += "/";
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Append search params as query string
|
|
345
|
+
if (search) {
|
|
346
|
+
const qs = serializeSearchParams(search);
|
|
347
|
+
if (qs) {
|
|
348
|
+
result += `?${qs}`;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return result;
|
|
353
|
+
}) as ReverseFunction<TRoutes>;
|
|
354
|
+
}
|
|
@@ -60,7 +60,8 @@ function NetworkErrorFallback({
|
|
|
60
60
|
marginBottom: "1.5rem",
|
|
61
61
|
}}
|
|
62
62
|
>
|
|
63
|
-
{error.message ||
|
|
63
|
+
{error.message ||
|
|
64
|
+
"Unable to connect to the server. Please check your internet connection."}
|
|
64
65
|
</p>
|
|
65
66
|
<div style={{ display: "flex", gap: "1rem", justifyContent: "center" }}>
|
|
66
67
|
<button
|
|
@@ -104,7 +105,12 @@ function NetworkErrorFallback({
|
|
|
104
105
|
* Default fallback UI for root error boundary
|
|
105
106
|
* This is shown when an unhandled error bubbles up to the root
|
|
106
107
|
*/
|
|
107
|
-
function RootErrorFallback({
|
|
108
|
+
function RootErrorFallback({
|
|
109
|
+
error,
|
|
110
|
+
reset,
|
|
111
|
+
}: ClientErrorBoundaryFallbackProps): ReactNode {
|
|
112
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
113
|
+
|
|
108
114
|
return (
|
|
109
115
|
<div
|
|
110
116
|
style={{
|
|
@@ -131,38 +137,40 @@ function RootErrorFallback({ error, reset }: ClientErrorBoundaryFallbackProps):
|
|
|
131
137
|
>
|
|
132
138
|
An unexpected error occurred while processing your request.
|
|
133
139
|
</p>
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
background: "#fef2f2",
|
|
137
|
-
border: "1px solid #fecaca",
|
|
138
|
-
borderRadius: "0.5rem",
|
|
139
|
-
padding: "1rem",
|
|
140
|
-
marginBottom: "1rem",
|
|
141
|
-
}}
|
|
142
|
-
>
|
|
143
|
-
<p
|
|
140
|
+
{isDev && (
|
|
141
|
+
<div
|
|
144
142
|
style={{
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
143
|
+
background: "#fef2f2",
|
|
144
|
+
border: "1px solid #fecaca",
|
|
145
|
+
borderRadius: "0.5rem",
|
|
146
|
+
padding: "1rem",
|
|
147
|
+
marginBottom: "1rem",
|
|
148
148
|
}}
|
|
149
149
|
>
|
|
150
|
-
|
|
151
|
-
</p>
|
|
152
|
-
{error.stack && (
|
|
153
|
-
<pre
|
|
150
|
+
<p
|
|
154
151
|
style={{
|
|
155
|
-
|
|
156
|
-
color: "#
|
|
157
|
-
|
|
158
|
-
whiteSpace: "pre-wrap",
|
|
159
|
-
wordBreak: "break-word",
|
|
152
|
+
fontWeight: 600,
|
|
153
|
+
color: "#991b1b",
|
|
154
|
+
marginBottom: "0.5rem",
|
|
160
155
|
}}
|
|
161
156
|
>
|
|
162
|
-
{error.
|
|
163
|
-
</
|
|
164
|
-
|
|
165
|
-
|
|
157
|
+
{error.name}: {error.message}
|
|
158
|
+
</p>
|
|
159
|
+
{error.stack && (
|
|
160
|
+
<pre
|
|
161
|
+
style={{
|
|
162
|
+
fontSize: "0.75rem",
|
|
163
|
+
color: "#6b7280",
|
|
164
|
+
overflow: "auto",
|
|
165
|
+
whiteSpace: "pre-wrap",
|
|
166
|
+
wordBreak: "break-word",
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
{error.stack}
|
|
170
|
+
</pre>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
166
174
|
<div style={{ display: "flex", gap: "1rem" }}>
|
|
167
175
|
<button
|
|
168
176
|
type="button"
|
|
@@ -231,7 +239,11 @@ export class RootErrorBoundary extends Component<
|
|
|
231
239
|
}
|
|
232
240
|
|
|
233
241
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
|
234
|
-
console.error(
|
|
242
|
+
console.error(
|
|
243
|
+
"[RootErrorBoundary] Unhandled error caught:",
|
|
244
|
+
error,
|
|
245
|
+
errorInfo,
|
|
246
|
+
);
|
|
235
247
|
}
|
|
236
248
|
|
|
237
249
|
componentDidUpdate(prevProps: { children: ReactNode }): void {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
3
|
import { Suspense, use, useId } from "react";
|
|
4
4
|
import { invariant } from "./errors";
|
|
5
|
-
import { OutletProvider } from "./
|
|
5
|
+
import { OutletProvider } from "./outlet-provider.js";
|
|
6
6
|
import type { ResolvedSegment } from "./types.js";
|
|
7
7
|
import { isLoaderDataResult } from "./types.js";
|
|
8
8
|
|
|
@@ -31,7 +31,10 @@ export function RouteContentWrapper({
|
|
|
31
31
|
return content as ReactNode;
|
|
32
32
|
}
|
|
33
33
|
return (
|
|
34
|
-
<Suspense
|
|
34
|
+
<Suspense
|
|
35
|
+
fallback={fallback ?? null}
|
|
36
|
+
key={segmentId ? "route-content-suspense-" + segmentId : undefined}
|
|
37
|
+
>
|
|
35
38
|
<Suspender content={content} key={segmentId} />
|
|
36
39
|
</Suspense>
|
|
37
40
|
);
|
|
@@ -50,11 +53,11 @@ export function RouteContentWrapperCallback<T>({
|
|
|
50
53
|
invariant(children, "RouteContentWrapperCallback requires children");
|
|
51
54
|
invariant(
|
|
52
55
|
typeof children === "function",
|
|
53
|
-
"RouteContentWrapperCallback requires children to be a function"
|
|
56
|
+
"RouteContentWrapperCallback requires children to be a function",
|
|
54
57
|
);
|
|
55
58
|
invariant(
|
|
56
59
|
resolve !== undefined,
|
|
57
|
-
"RouteContentWrapperCallback requires resolve"
|
|
60
|
+
"RouteContentWrapperCallback requires resolve",
|
|
58
61
|
);
|
|
59
62
|
return (
|
|
60
63
|
<Suspense
|