@rangojs/router 0.0.0-experimental.2

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.
Files changed (155) hide show
  1. package/CLAUDE.md +7 -0
  2. package/README.md +19 -0
  3. package/dist/vite/index.js +1298 -0
  4. package/package.json +140 -0
  5. package/skills/caching/SKILL.md +319 -0
  6. package/skills/document-cache/SKILL.md +152 -0
  7. package/skills/hooks/SKILL.md +359 -0
  8. package/skills/intercept/SKILL.md +292 -0
  9. package/skills/layout/SKILL.md +216 -0
  10. package/skills/loader/SKILL.md +365 -0
  11. package/skills/middleware/SKILL.md +442 -0
  12. package/skills/parallel/SKILL.md +255 -0
  13. package/skills/route/SKILL.md +141 -0
  14. package/skills/router-setup/SKILL.md +403 -0
  15. package/skills/theme/SKILL.md +54 -0
  16. package/skills/typesafety/SKILL.md +352 -0
  17. package/src/__mocks__/version.ts +6 -0
  18. package/src/__tests__/component-utils.test.ts +76 -0
  19. package/src/__tests__/route-definition.test.ts +63 -0
  20. package/src/__tests__/urls.test.tsx +436 -0
  21. package/src/browser/event-controller.ts +876 -0
  22. package/src/browser/index.ts +18 -0
  23. package/src/browser/link-interceptor.ts +121 -0
  24. package/src/browser/lru-cache.ts +69 -0
  25. package/src/browser/merge-segment-loaders.ts +126 -0
  26. package/src/browser/navigation-bridge.ts +893 -0
  27. package/src/browser/navigation-client.ts +162 -0
  28. package/src/browser/navigation-store.ts +823 -0
  29. package/src/browser/partial-update.ts +559 -0
  30. package/src/browser/react/Link.tsx +248 -0
  31. package/src/browser/react/NavigationProvider.tsx +275 -0
  32. package/src/browser/react/ScrollRestoration.tsx +94 -0
  33. package/src/browser/react/context.ts +53 -0
  34. package/src/browser/react/index.ts +52 -0
  35. package/src/browser/react/location-state-shared.ts +120 -0
  36. package/src/browser/react/location-state.ts +62 -0
  37. package/src/browser/react/use-action.ts +240 -0
  38. package/src/browser/react/use-client-cache.ts +56 -0
  39. package/src/browser/react/use-handle.ts +178 -0
  40. package/src/browser/react/use-href.tsx +208 -0
  41. package/src/browser/react/use-link-status.ts +134 -0
  42. package/src/browser/react/use-navigation.ts +150 -0
  43. package/src/browser/react/use-segments.ts +188 -0
  44. package/src/browser/request-controller.ts +164 -0
  45. package/src/browser/rsc-router.tsx +353 -0
  46. package/src/browser/scroll-restoration.ts +324 -0
  47. package/src/browser/server-action-bridge.ts +747 -0
  48. package/src/browser/shallow.ts +35 -0
  49. package/src/browser/types.ts +464 -0
  50. package/src/cache/__tests__/document-cache.test.ts +522 -0
  51. package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
  52. package/src/cache/__tests__/memory-store.test.ts +484 -0
  53. package/src/cache/cache-scope.ts +565 -0
  54. package/src/cache/cf/__tests__/cf-cache-store.test.ts +428 -0
  55. package/src/cache/cf/cf-cache-store.ts +428 -0
  56. package/src/cache/cf/index.ts +19 -0
  57. package/src/cache/document-cache.ts +340 -0
  58. package/src/cache/index.ts +58 -0
  59. package/src/cache/memory-segment-store.ts +150 -0
  60. package/src/cache/memory-store.ts +253 -0
  61. package/src/cache/types.ts +387 -0
  62. package/src/client.rsc.tsx +88 -0
  63. package/src/client.tsx +621 -0
  64. package/src/component-utils.ts +76 -0
  65. package/src/components/DefaultDocument.tsx +23 -0
  66. package/src/default-error-boundary.tsx +88 -0
  67. package/src/deps/browser.ts +8 -0
  68. package/src/deps/html-stream-client.ts +2 -0
  69. package/src/deps/html-stream-server.ts +2 -0
  70. package/src/deps/rsc.ts +10 -0
  71. package/src/deps/ssr.ts +2 -0
  72. package/src/errors.ts +259 -0
  73. package/src/handle.ts +120 -0
  74. package/src/handles/MetaTags.tsx +193 -0
  75. package/src/handles/index.ts +6 -0
  76. package/src/handles/meta.ts +247 -0
  77. package/src/href-client.ts +128 -0
  78. package/src/href-context.ts +33 -0
  79. package/src/href.ts +177 -0
  80. package/src/index.rsc.ts +79 -0
  81. package/src/index.ts +87 -0
  82. package/src/loader.rsc.ts +204 -0
  83. package/src/loader.ts +47 -0
  84. package/src/network-error-thrower.tsx +21 -0
  85. package/src/outlet-context.ts +15 -0
  86. package/src/root-error-boundary.tsx +277 -0
  87. package/src/route-content-wrapper.tsx +198 -0
  88. package/src/route-definition.ts +1371 -0
  89. package/src/route-map-builder.ts +146 -0
  90. package/src/route-types.ts +198 -0
  91. package/src/route-utils.ts +89 -0
  92. package/src/router/__tests__/match-context.test.ts +104 -0
  93. package/src/router/__tests__/match-pipelines.test.ts +537 -0
  94. package/src/router/__tests__/match-result.test.ts +566 -0
  95. package/src/router/__tests__/on-error.test.ts +935 -0
  96. package/src/router/__tests__/pattern-matching.test.ts +577 -0
  97. package/src/router/error-handling.ts +287 -0
  98. package/src/router/handler-context.ts +158 -0
  99. package/src/router/loader-resolution.ts +326 -0
  100. package/src/router/manifest.ts +138 -0
  101. package/src/router/match-context.ts +264 -0
  102. package/src/router/match-middleware/background-revalidation.ts +236 -0
  103. package/src/router/match-middleware/cache-lookup.ts +261 -0
  104. package/src/router/match-middleware/cache-store.ts +266 -0
  105. package/src/router/match-middleware/index.ts +81 -0
  106. package/src/router/match-middleware/intercept-resolution.ts +268 -0
  107. package/src/router/match-middleware/segment-resolution.ts +174 -0
  108. package/src/router/match-pipelines.ts +214 -0
  109. package/src/router/match-result.ts +214 -0
  110. package/src/router/metrics.ts +62 -0
  111. package/src/router/middleware.test.ts +1355 -0
  112. package/src/router/middleware.ts +748 -0
  113. package/src/router/pattern-matching.ts +272 -0
  114. package/src/router/revalidation.ts +190 -0
  115. package/src/router/router-context.ts +299 -0
  116. package/src/router/types.ts +96 -0
  117. package/src/router.ts +3876 -0
  118. package/src/rsc/__tests__/helpers.test.ts +175 -0
  119. package/src/rsc/handler.ts +1060 -0
  120. package/src/rsc/helpers.ts +64 -0
  121. package/src/rsc/index.ts +56 -0
  122. package/src/rsc/nonce.ts +18 -0
  123. package/src/rsc/types.ts +237 -0
  124. package/src/segment-system.tsx +456 -0
  125. package/src/server/__tests__/request-context.test.ts +171 -0
  126. package/src/server/context.ts +417 -0
  127. package/src/server/handle-store.ts +230 -0
  128. package/src/server/loader-registry.ts +174 -0
  129. package/src/server/request-context.ts +554 -0
  130. package/src/server/root-layout.tsx +10 -0
  131. package/src/server/tsconfig.json +14 -0
  132. package/src/server.ts +146 -0
  133. package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
  134. package/src/ssr/index.tsx +234 -0
  135. package/src/theme/ThemeProvider.tsx +291 -0
  136. package/src/theme/ThemeScript.tsx +61 -0
  137. package/src/theme/__tests__/theme.test.ts +120 -0
  138. package/src/theme/constants.ts +55 -0
  139. package/src/theme/index.ts +58 -0
  140. package/src/theme/theme-context.ts +70 -0
  141. package/src/theme/theme-script.ts +152 -0
  142. package/src/theme/types.ts +182 -0
  143. package/src/theme/use-theme.ts +44 -0
  144. package/src/types.ts +1561 -0
  145. package/src/urls.ts +726 -0
  146. package/src/use-loader.tsx +346 -0
  147. package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
  148. package/src/vite/expose-action-id.ts +344 -0
  149. package/src/vite/expose-handle-id.ts +209 -0
  150. package/src/vite/expose-loader-id.ts +357 -0
  151. package/src/vite/expose-location-state-id.ts +177 -0
  152. package/src/vite/index.ts +787 -0
  153. package/src/vite/package-resolution.ts +125 -0
  154. package/src/vite/version.d.ts +12 -0
  155. package/src/vite/virtual-entries.ts +109 -0
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Router Error Handling Utilities
3
+ *
4
+ * Error boundary and not-found boundary handling for RSC Router.
5
+ * Also includes the shared invokeOnError utility for error callback invocation.
6
+ */
7
+
8
+ import type { ReactNode } from "react";
9
+ import type { EntryData } from "../server/context";
10
+ import type {
11
+ ResolvedSegment,
12
+ ErrorInfo,
13
+ ErrorBoundaryHandler,
14
+ ErrorBoundaryFallbackProps,
15
+ NotFoundInfo,
16
+ NotFoundBoundaryHandler,
17
+ NotFoundBoundaryFallbackProps,
18
+ ErrorPhase,
19
+ OnErrorCallback,
20
+ OnErrorContext,
21
+ } from "../types";
22
+
23
+ /**
24
+ * Context required to invoke the onError callback.
25
+ * This is a subset of OnErrorContext that callers must provide.
26
+ */
27
+ export interface InvokeOnErrorContext<TEnv = any> {
28
+ request: Request;
29
+ url: URL;
30
+ routeKey?: string;
31
+ params?: Record<string, string>;
32
+ segmentId?: string;
33
+ segmentType?: "layout" | "route" | "parallel" | "loader" | "middleware";
34
+ loaderName?: string;
35
+ middlewareId?: string;
36
+ actionId?: string;
37
+ env?: TEnv;
38
+ isPartial?: boolean;
39
+ handledByBoundary?: boolean;
40
+ metadata?: Record<string, unknown>;
41
+ /** Request start time from performance.now() for duration calculation */
42
+ requestStartTime?: number;
43
+ }
44
+
45
+ /**
46
+ * Invoke the onError callback with comprehensive context.
47
+ * Catches any errors in the callback itself to prevent masking the original error.
48
+ *
49
+ * This is a shared utility used by both the router and RSC handler to ensure
50
+ * consistent error callback behavior across the codebase.
51
+ *
52
+ * @param onError - The onError callback to invoke (may be undefined)
53
+ * @param error - The error that occurred
54
+ * @param phase - The phase where the error occurred
55
+ * @param context - Additional context about the error
56
+ * @param logPrefix - Prefix for console.error messages (e.g., "Router" or "RSC")
57
+ */
58
+ export function invokeOnError<TEnv = any>(
59
+ onError: OnErrorCallback<TEnv> | undefined,
60
+ error: unknown,
61
+ phase: ErrorPhase,
62
+ context: InvokeOnErrorContext<TEnv>,
63
+ logPrefix: string = "Router"
64
+ ): void {
65
+ if (!onError) return;
66
+
67
+ const errorObj = error instanceof Error ? error : new Error(String(error));
68
+ const duration = context.requestStartTime
69
+ ? performance.now() - context.requestStartTime
70
+ : undefined;
71
+
72
+ const errorContext: OnErrorContext<TEnv> = {
73
+ error: errorObj,
74
+ phase,
75
+ request: context.request,
76
+ url: context.url,
77
+ pathname: context.url.pathname,
78
+ method: context.request.method,
79
+ routeKey: context.routeKey,
80
+ params: context.params,
81
+ segmentId: context.segmentId,
82
+ segmentType: context.segmentType,
83
+ loaderName: context.loaderName,
84
+ middlewareId: context.middlewareId,
85
+ actionId: context.actionId,
86
+ env: context.env,
87
+ duration,
88
+ isPartial: context.isPartial,
89
+ handledByBoundary: context.handledByBoundary,
90
+ stack: errorObj.stack,
91
+ metadata: context.metadata,
92
+ };
93
+
94
+ try {
95
+ const result = onError(errorContext);
96
+ // If onError returns a promise, catch any rejections
97
+ if (result instanceof Promise) {
98
+ result.catch((callbackError) => {
99
+ console.error(`[${logPrefix}.onError] Callback error:`, callbackError);
100
+ });
101
+ }
102
+ } catch (callbackError) {
103
+ // Log but don't throw - we don't want callback errors to mask the original error
104
+ console.error(`[${logPrefix}.onError] Callback error:`, callbackError);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Find the nearest error boundary by walking up the entry chain
110
+ * Also checks sibling layouts (orphan layouts) for error boundaries
111
+ * Returns the first fallback found, or the default error boundary if configured
112
+ */
113
+ export function findNearestErrorBoundary(
114
+ entry: EntryData | null,
115
+ defaultErrorBoundary?: ReactNode | ErrorBoundaryHandler
116
+ ): ReactNode | ErrorBoundaryHandler | null {
117
+ let current: EntryData | null = entry;
118
+
119
+ while (current) {
120
+ // Check if this entry has error boundaries defined
121
+ if (current.errorBoundary && current.errorBoundary.length > 0) {
122
+ // Return the last error boundary (most recently defined takes precedence)
123
+ return current.errorBoundary[current.errorBoundary.length - 1];
124
+ }
125
+
126
+ // Check orphan layouts for error boundaries
127
+ // Orphan layouts are siblings that render alongside the main route chain
128
+ // They can define error boundaries that catch errors from routes in the same route group
129
+ // Check from first to last (first sibling takes precedence as the "outer" wrapper)
130
+ if (current.layout && current.layout.length > 0) {
131
+ for (const orphan of current.layout) {
132
+ if (orphan.errorBoundary && orphan.errorBoundary.length > 0) {
133
+ return orphan.errorBoundary[orphan.errorBoundary.length - 1];
134
+ }
135
+ }
136
+ }
137
+
138
+ current = current.parent;
139
+ }
140
+
141
+ // Return default error boundary if configured
142
+ return defaultErrorBoundary || null;
143
+ }
144
+
145
+ /**
146
+ * Find the nearest notFound boundary by walking up the entry chain
147
+ * Returns the first fallback found, or the default notFound boundary if configured
148
+ */
149
+ export function findNearestNotFoundBoundary(
150
+ entry: EntryData | null,
151
+ defaultNotFoundBoundary?: ReactNode | NotFoundBoundaryHandler
152
+ ): ReactNode | NotFoundBoundaryHandler | null {
153
+ let current: EntryData | null = entry;
154
+
155
+ while (current) {
156
+ // Check if this entry has notFound boundaries defined
157
+ if (current.notFoundBoundary && current.notFoundBoundary.length > 0) {
158
+ // Return the last notFound boundary (most recently defined takes precedence)
159
+ return current.notFoundBoundary[current.notFoundBoundary.length - 1];
160
+ }
161
+ current = current.parent;
162
+ }
163
+
164
+ // Return default notFound boundary if configured
165
+ return defaultNotFoundBoundary || null;
166
+ }
167
+
168
+ /**
169
+ * Create ErrorInfo from an error object
170
+ * Sanitizes error details in production
171
+ */
172
+ export function createErrorInfo(
173
+ error: unknown,
174
+ segmentId: string,
175
+ segmentType: ErrorInfo["segmentType"]
176
+ ): ErrorInfo {
177
+ const isDev = process.env.NODE_ENV !== "production";
178
+
179
+ if (error instanceof Error) {
180
+ return {
181
+ message: isDev ? error.message : "An error occurred",
182
+ name: error.name,
183
+ code: (error as any).code,
184
+ stack: isDev ? error.stack : undefined,
185
+ cause: isDev ? error.cause : undefined,
186
+ segmentId,
187
+ segmentType,
188
+ };
189
+ }
190
+
191
+ // Non-Error thrown
192
+ return {
193
+ message: isDev ? String(error) : "An error occurred",
194
+ name: "Error",
195
+ segmentId,
196
+ segmentType,
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Create an error segment with the fallback component
202
+ * Renders the fallback with error info and reset function
203
+ */
204
+ export function createErrorSegment(
205
+ errorInfo: ErrorInfo,
206
+ fallback: ReactNode | ErrorBoundaryHandler,
207
+ entry: EntryData,
208
+ params: Record<string, string>
209
+ ): ResolvedSegment {
210
+ // Determine the component to render
211
+ let component: ReactNode;
212
+
213
+ if (typeof fallback === "function") {
214
+ // ErrorBoundaryHandler - call with error info
215
+ const props: ErrorBoundaryFallbackProps = {
216
+ error: errorInfo,
217
+ };
218
+ component = fallback(props);
219
+ } else {
220
+ // Static ReactNode fallback
221
+ component = fallback;
222
+ }
223
+
224
+ // Error segment uses the same ID as the layout that has the error boundary
225
+ // The error boundary content replaces the layout's outlet content
226
+ return {
227
+ id: entry.shortCode,
228
+ namespace: entry.id,
229
+ type: "error",
230
+ index: 0,
231
+ component,
232
+ params,
233
+ error: errorInfo,
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Create NotFoundInfo from a DataNotFoundError
239
+ */
240
+ export function createNotFoundInfo(
241
+ error: { message: string },
242
+ segmentId: string,
243
+ segmentType: NotFoundInfo["segmentType"],
244
+ pathname?: string
245
+ ): NotFoundInfo {
246
+ return {
247
+ message: error.message,
248
+ segmentId,
249
+ segmentType,
250
+ pathname,
251
+ };
252
+ }
253
+
254
+ /**
255
+ * Create a notFound segment with the fallback component
256
+ * Renders the fallback with not found info
257
+ */
258
+ export function createNotFoundSegment(
259
+ notFoundInfo: NotFoundInfo,
260
+ fallback: ReactNode | NotFoundBoundaryHandler,
261
+ entry: EntryData,
262
+ params: Record<string, string>
263
+ ): ResolvedSegment {
264
+ // Determine the component to render
265
+ let component: ReactNode;
266
+
267
+ if (typeof fallback === "function") {
268
+ // NotFoundBoundaryHandler - call with props
269
+ const props: NotFoundBoundaryFallbackProps = {
270
+ notFound: notFoundInfo,
271
+ };
272
+ component = fallback(props);
273
+ } else {
274
+ // Static ReactNode fallback
275
+ component = fallback;
276
+ }
277
+
278
+ return {
279
+ id: `${entry.shortCode}.notFound`,
280
+ namespace: entry.id,
281
+ type: "notFound",
282
+ index: 0,
283
+ component,
284
+ params,
285
+ notFoundInfo,
286
+ };
287
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Router Handler Context
3
+ *
4
+ * Creates the handler context object passed to route handlers, middleware, and loaders.
5
+ */
6
+
7
+ import type { HandlerContext } from "../types";
8
+ import { getRequestContext } from "../server/request-context.js";
9
+
10
+ /**
11
+ * Resolve route name with namespace prefix support.
12
+ * Same logic as client-side useHref for consistency.
13
+ */
14
+ function resolveRouteName(
15
+ name: string,
16
+ routeMap: Record<string, string>,
17
+ currentRoutePrefix?: string
18
+ ): string | undefined {
19
+ // 1. Path-based - starts with /
20
+ if (name.startsWith("/")) {
21
+ return name;
22
+ }
23
+
24
+ // 2. Absolute name - already has a dot (e.g., "shop.cart")
25
+ if (name.includes(".")) {
26
+ return routeMap[name];
27
+ }
28
+
29
+ // 3. Local name - try with current prefix first, then fall back to direct lookup
30
+ if (currentRoutePrefix) {
31
+ // Extract the prefix from current route name
32
+ // e.g., "blog.posts.detail" → prefix is "blog.posts"
33
+ const lastDot = currentRoutePrefix.lastIndexOf(".");
34
+ const prefix = lastDot > 0 ? currentRoutePrefix.substring(0, lastDot) : currentRoutePrefix;
35
+
36
+ // Try prefixed name
37
+ const prefixedName = `${prefix}.${name}`;
38
+ if (routeMap[prefixedName] !== undefined) {
39
+ return routeMap[prefixedName];
40
+ }
41
+
42
+ // If current route is a nested include, try parent prefixes
43
+ // e.g., for "blog.posts.detail", try "blog.posts.index", then "blog.index"
44
+ let currentPrefix = prefix;
45
+ while (currentPrefix.includes(".")) {
46
+ const parentDot = currentPrefix.lastIndexOf(".");
47
+ currentPrefix = currentPrefix.substring(0, parentDot);
48
+ const parentPrefixedName = `${currentPrefix}.${name}`;
49
+ if (routeMap[parentPrefixedName] !== undefined) {
50
+ return routeMap[parentPrefixedName];
51
+ }
52
+ }
53
+ }
54
+
55
+ // Fall back to direct lookup (route without prefix)
56
+ return routeMap[name];
57
+ }
58
+
59
+ /**
60
+ * Create HandlerContext with typed env/var/get/set
61
+ */
62
+ export function createHandlerContext<TEnv>(
63
+ params: Record<string, string>,
64
+ request: Request,
65
+ searchParams: URLSearchParams,
66
+ pathname: string,
67
+ url: URL,
68
+ bindings: any = {},
69
+ routeMap: Record<string, string> = {},
70
+ routeName?: string
71
+ ): HandlerContext<any, TEnv> {
72
+ // Get variables from request context - this is the unified context
73
+ // shared between middleware and route handlers
74
+ const requestContext = getRequestContext();
75
+ const variables: any = requestContext?.var ?? {};
76
+
77
+ // Filter system parameters (starting with _rsc) from searchParams
78
+ // This ensures handlers only see user-facing query params
79
+ const cleanSearchParams = new URLSearchParams();
80
+ searchParams.forEach((value, key) => {
81
+ if (!key.startsWith("_rsc")) {
82
+ cleanSearchParams.set(key, value);
83
+ }
84
+ });
85
+
86
+ // Create clean URL without system params
87
+ const cleanUrl = new URL(url);
88
+ cleanUrl.search = cleanSearchParams.toString();
89
+
90
+ // Get stub response from request context for setting headers
91
+ const stubResponse = requestContext?.res ?? new Response(null, { status: 200 });
92
+
93
+ return {
94
+ params,
95
+ request,
96
+ searchParams: cleanSearchParams, // Filtered params
97
+ pathname,
98
+ url: cleanUrl, // Clean URL
99
+ env: bindings,
100
+ var: variables,
101
+ get: ((key: string) => variables[key]) as HandlerContext<
102
+ any,
103
+ TEnv
104
+ >["get"],
105
+ set: ((key: string, value: any) => {
106
+ variables[key] = value;
107
+ }) as HandlerContext<any, TEnv>["set"],
108
+ _originalRequest: request, // Raw request for advanced use
109
+ res: stubResponse, // Stub response for setting headers
110
+ headers: stubResponse.headers, // Shorthand for res.headers
111
+ // Placeholder use() - will be replaced with actual implementation during request
112
+ use: () => {
113
+ throw new Error("ctx.use() called before loaders were initialized");
114
+ },
115
+ // Theme support (when enabled via router config)
116
+ theme: requestContext?.theme,
117
+ setTheme: requestContext?.setTheme,
118
+ // Scoped href for URL generation
119
+ href: (name: string, hrefParams?: Record<string, string>) => {
120
+ // Path-based - return directly (optionally with param substitution)
121
+ if (name.startsWith("/")) {
122
+ if (hrefParams) {
123
+ return name.replace(/:([^/]+)/g, (_, key) => {
124
+ const value = hrefParams[key];
125
+ if (value === undefined) {
126
+ throw new Error(`Missing param "${key}" for path "${name}"`);
127
+ }
128
+ return encodeURIComponent(value);
129
+ });
130
+ }
131
+ return name;
132
+ }
133
+
134
+ // Resolve route name with namespace support
135
+ const pattern = resolveRouteName(name, routeMap, routeName);
136
+
137
+ if (pattern === undefined) {
138
+ throw new Error(
139
+ `Unknown route: "${name}"${routeName ? ` (current route: ${routeName})` : ""}`
140
+ );
141
+ }
142
+
143
+ // If no params, return pattern directly
144
+ if (!hrefParams) {
145
+ return pattern;
146
+ }
147
+
148
+ // Substitute params
149
+ return pattern.replace(/:([^/]+)/g, (_, key) => {
150
+ const value = hrefParams[key];
151
+ if (value === undefined) {
152
+ throw new Error(`Missing param "${key}" for route "${name}"`);
153
+ }
154
+ return encodeURIComponent(value);
155
+ });
156
+ },
157
+ };
158
+ }