@rangojs/router 0.0.0-experimental.0f44aca1
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 +5 -0
- package/README.md +899 -0
- package/dist/bin/rango.js +1601 -0
- package/dist/vite/index.js +5214 -0
- package/package.json +176 -0
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +220 -0
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +112 -0
- package/skills/document-cache/SKILL.md +182 -0
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +704 -0
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +313 -0
- package/skills/layout/SKILL.md +310 -0
- package/skills/links/SKILL.md +239 -0
- package/skills/loader/SKILL.md +596 -0
- package/skills/middleware/SKILL.md +339 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +305 -0
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +118 -0
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +385 -0
- package/skills/router-setup/SKILL.md +439 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +79 -0
- package/skills/typesafety/SKILL.md +623 -0
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +273 -0
- 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/event-controller.ts +899 -0
- package/src/browser/history-state.ts +80 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +141 -0
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +134 -0
- package/src/browser/navigation-bridge.ts +645 -0
- package/src/browser/navigation-client.ts +215 -0
- package/src/browser/navigation-store.ts +806 -0
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +550 -0
- package/src/browser/prefetch/cache.ts +146 -0
- package/src/browser/prefetch/fetch.ts +135 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +360 -0
- package/src/browser/react/NavigationProvider.tsx +386 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +59 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +162 -0
- package/src/browser/react/location-state.ts +107 -0
- package/src/browser/react/mount-context.ts +37 -0
- 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 +218 -0
- package/src/browser/react/use-client-cache.ts +58 -0
- package/src/browser/react/use-handle.ts +162 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +135 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +99 -0
- 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 +63 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +171 -0
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +431 -0
- package/src/browser/scroll-restoration.ts +400 -0
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +667 -0
- package/src/browser/shallow.ts +40 -0
- package/src/browser/types.ts +538 -0
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +438 -0
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +35 -0
- package/src/build/route-trie.ts +265 -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 +411 -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 +469 -0
- package/src/build/route-types/scan-filter.ts +78 -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 +338 -0
- package/src/cache/cache-scope.ts +382 -0
- package/src/cache/cf/cf-cache-store.ts +540 -0
- package/src/cache/cf/index.ts +25 -0
- package/src/cache/document-cache.ts +369 -0
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +43 -0
- package/src/cache/memory-segment-store.ts +328 -0
- 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 +98 -0
- package/src/cache/types.ts +342 -0
- package/src/client.rsc.tsx +85 -0
- package/src/client.tsx +601 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +27 -0
- package/src/context-var.ts +86 -0
- package/src/debug.ts +243 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +365 -0
- package/src/handle.ts +135 -0
- package/src/handles/MetaTags.tsx +246 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +7 -0
- package/src/handles/meta.ts +264 -0
- package/src/host/cookie-handler.ts +165 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +53 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +352 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +146 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +222 -0
- package/src/index.rsc.ts +233 -0
- package/src/index.ts +277 -0
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +89 -0
- package/src/loader.ts +64 -0
- package/src/network-error-thrower.tsx +23 -0
- package/src/outlet-context.ts +15 -0
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +185 -0
- package/src/prerender.ts +463 -0
- package/src/reverse.ts +330 -0
- package/src/root-error-boundary.tsx +289 -0
- package/src/route-content-wrapper.tsx +196 -0
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -0
- package/src/route-map-builder.ts +275 -0
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +259 -0
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/find-match.ts +158 -0
- package/src/router/handler-context.ts +451 -0
- package/src/router/intercept-resolution.ts +395 -0
- package/src/router/lazy-includes.ts +234 -0
- package/src/router/loader-resolution.ts +420 -0
- package/src/router/logging.ts +248 -0
- package/src/router/manifest.ts +267 -0
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +266 -0
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +223 -0
- package/src/router/match-middleware/cache-lookup.ts +634 -0
- package/src/router/match-middleware/cache-store.ts +295 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +306 -0
- package/src/router/match-middleware/segment-resolution.ts +192 -0
- package/src/router/match-pipelines.ts +179 -0
- package/src/router/match-result.ts +219 -0
- package/src/router/metrics.ts +282 -0
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +748 -0
- package/src/router/pattern-matching.ts +563 -0
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +289 -0
- package/src/router/router-context.ts +316 -0
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1239 -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 +289 -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 +170 -0
- package/src/router.ts +1002 -0
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +1089 -0
- package/src/rsc/helpers.ts +198 -0
- package/src/rsc/index.ts +36 -0
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +32 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +235 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +263 -0
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +454 -0
- package/src/server/context.ts +591 -0
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +308 -0
- package/src/server/loader-registry.ts +133 -0
- package/src/server/request-context.ts +914 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +51 -0
- package/src/ssr/index.tsx +365 -0
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +297 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +62 -0
- package/src/theme/index.ts +48 -0
- package/src/theme/theme-context.ts +44 -0
- package/src/theme/theme-script.ts +155 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- 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 +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +102 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -0
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -0
- package/src/use-loader.tsx +354 -0
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -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 +110 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +16 -0
- package/src/vite/plugin-types.ts +131 -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/plugins/expose-action-id.ts +365 -0
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -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 +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -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 +254 -0
- package/src/vite/plugins/version.d.ts +12 -0
- package/src/vite/plugins/virtual-entries.ts +123 -0
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +510 -0
- package/src/vite/router-discovery.ts +785 -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/utils/package-resolution.ts +121 -0
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
package/src/reverse.ts
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
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
|
+
result = result.replace(
|
|
309
|
+
/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?\??/g,
|
|
310
|
+
(_, key) => {
|
|
311
|
+
const value = params[key];
|
|
312
|
+
if (value === undefined) {
|
|
313
|
+
throw new Error(`Missing param "${key}" for route "${name}"`);
|
|
314
|
+
}
|
|
315
|
+
return encodeURIComponent(value);
|
|
316
|
+
},
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Append search params as query string
|
|
321
|
+
if (search) {
|
|
322
|
+
const qs = serializeSearchParams(search);
|
|
323
|
+
if (qs) {
|
|
324
|
+
result += `?${qs}`;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return result;
|
|
329
|
+
}) as ReverseFunction<TRoutes>;
|
|
330
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Component, useState, type ReactNode } from "react";
|
|
4
|
+
import type { ClientErrorBoundaryFallbackProps } from "./types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if an error is a network-related error
|
|
8
|
+
*/
|
|
9
|
+
function isNetworkError(error: Error): boolean {
|
|
10
|
+
return error.name === "NetworkError";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Network error fallback UI with retry functionality
|
|
15
|
+
* Shows a connection-specific message and allows retrying via page refresh
|
|
16
|
+
*/
|
|
17
|
+
function NetworkErrorFallback({
|
|
18
|
+
error,
|
|
19
|
+
reset,
|
|
20
|
+
}: ClientErrorBoundaryFallbackProps): ReactNode {
|
|
21
|
+
const [isRetrying, setIsRetrying] = useState(false);
|
|
22
|
+
|
|
23
|
+
const handleRetry = (): void => {
|
|
24
|
+
setIsRetrying(true);
|
|
25
|
+
// Refresh the page to retry the request
|
|
26
|
+
window.location.reload();
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
style={{
|
|
32
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
33
|
+
padding: "2rem",
|
|
34
|
+
maxWidth: "600px",
|
|
35
|
+
margin: "2rem auto",
|
|
36
|
+
textAlign: "center",
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
<div
|
|
40
|
+
style={{
|
|
41
|
+
fontSize: "3rem",
|
|
42
|
+
marginBottom: "1rem",
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
{/* Simple cloud with x icon using CSS */}
|
|
46
|
+
<span style={{ color: "#9ca3af" }}>☁</span>
|
|
47
|
+
</div>
|
|
48
|
+
<h1
|
|
49
|
+
style={{
|
|
50
|
+
color: "#374151",
|
|
51
|
+
fontSize: "1.5rem",
|
|
52
|
+
marginBottom: "0.5rem",
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
Connection Error
|
|
56
|
+
</h1>
|
|
57
|
+
<p
|
|
58
|
+
style={{
|
|
59
|
+
color: "#6b7280",
|
|
60
|
+
marginBottom: "1.5rem",
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
{error.message ||
|
|
64
|
+
"Unable to connect to the server. Please check your internet connection."}
|
|
65
|
+
</p>
|
|
66
|
+
<div style={{ display: "flex", gap: "1rem", justifyContent: "center" }}>
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
onClick={handleRetry}
|
|
70
|
+
disabled={isRetrying}
|
|
71
|
+
style={{
|
|
72
|
+
padding: "0.75rem 1.5rem",
|
|
73
|
+
backgroundColor: isRetrying ? "#9ca3af" : "#2563eb",
|
|
74
|
+
color: "white",
|
|
75
|
+
border: "none",
|
|
76
|
+
borderRadius: "0.375rem",
|
|
77
|
+
cursor: isRetrying ? "not-allowed" : "pointer",
|
|
78
|
+
fontSize: "1rem",
|
|
79
|
+
fontWeight: 500,
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
{isRetrying ? "Retrying..." : "Retry"}
|
|
83
|
+
</button>
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
onClick={() => window.history.back()}
|
|
87
|
+
style={{
|
|
88
|
+
padding: "0.75rem 1.5rem",
|
|
89
|
+
backgroundColor: "transparent",
|
|
90
|
+
color: "#6b7280",
|
|
91
|
+
border: "1px solid #d1d5db",
|
|
92
|
+
borderRadius: "0.375rem",
|
|
93
|
+
cursor: "pointer",
|
|
94
|
+
fontSize: "1rem",
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
Go Back
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Default fallback UI for root error boundary
|
|
106
|
+
* This is shown when an unhandled error bubbles up to the root
|
|
107
|
+
*/
|
|
108
|
+
function RootErrorFallback({
|
|
109
|
+
error,
|
|
110
|
+
reset,
|
|
111
|
+
}: ClientErrorBoundaryFallbackProps): ReactNode {
|
|
112
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div
|
|
116
|
+
style={{
|
|
117
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
118
|
+
padding: "2rem",
|
|
119
|
+
maxWidth: "600px",
|
|
120
|
+
margin: "2rem auto",
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
<h1
|
|
124
|
+
style={{
|
|
125
|
+
color: "#dc2626",
|
|
126
|
+
fontSize: "1.5rem",
|
|
127
|
+
marginBottom: "1rem",
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
Internal Server Error
|
|
131
|
+
</h1>
|
|
132
|
+
<p
|
|
133
|
+
style={{
|
|
134
|
+
color: "#374151",
|
|
135
|
+
marginBottom: "1rem",
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
An unexpected error occurred while processing your request.
|
|
139
|
+
</p>
|
|
140
|
+
{isDev && (
|
|
141
|
+
<div
|
|
142
|
+
style={{
|
|
143
|
+
background: "#fef2f2",
|
|
144
|
+
border: "1px solid #fecaca",
|
|
145
|
+
borderRadius: "0.5rem",
|
|
146
|
+
padding: "1rem",
|
|
147
|
+
marginBottom: "1rem",
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
<p
|
|
151
|
+
style={{
|
|
152
|
+
fontWeight: 600,
|
|
153
|
+
color: "#991b1b",
|
|
154
|
+
marginBottom: "0.5rem",
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
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
|
+
)}
|
|
174
|
+
<div style={{ display: "flex", gap: "1rem" }}>
|
|
175
|
+
<button
|
|
176
|
+
type="button"
|
|
177
|
+
onClick={reset}
|
|
178
|
+
style={{
|
|
179
|
+
padding: "0.5rem 1rem",
|
|
180
|
+
backgroundColor: "#2563eb",
|
|
181
|
+
color: "white",
|
|
182
|
+
border: "none",
|
|
183
|
+
borderRadius: "0.25rem",
|
|
184
|
+
cursor: "pointer",
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
187
|
+
Try Again
|
|
188
|
+
</button>
|
|
189
|
+
<a
|
|
190
|
+
href="/"
|
|
191
|
+
style={{
|
|
192
|
+
display: "inline-block",
|
|
193
|
+
padding: "0.5rem 1rem",
|
|
194
|
+
color: "#2563eb",
|
|
195
|
+
textDecoration: "underline",
|
|
196
|
+
}}
|
|
197
|
+
>
|
|
198
|
+
Go to homepage
|
|
199
|
+
</a>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
interface RootErrorBoundaryState {
|
|
206
|
+
hasError: boolean;
|
|
207
|
+
error: Error | null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Root error boundary component
|
|
212
|
+
*
|
|
213
|
+
* Wraps the entire segment tree to catch any unhandled errors that bubble up.
|
|
214
|
+
* This prevents the entire app from crashing with a white screen.
|
|
215
|
+
*
|
|
216
|
+
* This is a client component with an inline fallback to avoid the
|
|
217
|
+
* "Functions cannot be passed to Client Components" RSC error.
|
|
218
|
+
*/
|
|
219
|
+
export class RootErrorBoundary extends Component<
|
|
220
|
+
{ children: ReactNode },
|
|
221
|
+
RootErrorBoundaryState
|
|
222
|
+
> {
|
|
223
|
+
constructor(props: { children: ReactNode }) {
|
|
224
|
+
super(props);
|
|
225
|
+
this.state = { hasError: false, error: null };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
static getDerivedStateFromError(error: Error): RootErrorBoundaryState {
|
|
229
|
+
return { hasError: true, error };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
componentDidMount(): void {
|
|
233
|
+
// Listen for popstate (back/forward navigation) to reset error state
|
|
234
|
+
window.addEventListener("popstate", this.handlePopState);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
componentWillUnmount(): void {
|
|
238
|
+
window.removeEventListener("popstate", this.handlePopState);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
|
242
|
+
console.error(
|
|
243
|
+
"[RootErrorBoundary] Unhandled error caught:",
|
|
244
|
+
error,
|
|
245
|
+
errorInfo,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
componentDidUpdate(prevProps: { children: ReactNode }): void {
|
|
250
|
+
// Reset error state when children change (e.g., navigation)
|
|
251
|
+
// This allows the app to recover after navigation away from an errored route
|
|
252
|
+
if (this.state.hasError && prevProps.children !== this.props.children) {
|
|
253
|
+
this.setState({ hasError: false, error: null });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
handlePopState = (): void => {
|
|
258
|
+
// Reset error state on back/forward navigation
|
|
259
|
+
if (this.state.hasError) {
|
|
260
|
+
this.setState({ hasError: false, error: null });
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
reset = (): void => {
|
|
265
|
+
this.setState({ hasError: false, error: null });
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
render(): ReactNode {
|
|
269
|
+
if (this.state.hasError && this.state.error) {
|
|
270
|
+
const errorInfo = {
|
|
271
|
+
message: this.state.error.message,
|
|
272
|
+
name: this.state.error.name,
|
|
273
|
+
stack: this.state.error.stack,
|
|
274
|
+
cause: this.state.error.cause,
|
|
275
|
+
segmentId: "root",
|
|
276
|
+
segmentType: "route" as const,
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// Use specialized fallback for network errors
|
|
280
|
+
if (isNetworkError(this.state.error)) {
|
|
281
|
+
return <NetworkErrorFallback error={errorInfo} reset={this.reset} />;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return <RootErrorFallback error={errorInfo} reset={this.reset} />;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return this.props.children;
|
|
288
|
+
}
|
|
289
|
+
}
|