@octanejs/remix-router 0.1.0

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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -0
  3. package/package.json +53 -0
  4. package/src/dom.ts +16 -0
  5. package/src/index.ts +267 -0
  6. package/src/internal.ts +37 -0
  7. package/src/lib/Await.tsrx +145 -0
  8. package/src/lib/Await.tsrx.d.ts +6 -0
  9. package/src/lib/DefaultErrorComponent.tsrx +47 -0
  10. package/src/lib/DefaultErrorComponent.tsrx.d.ts +2 -0
  11. package/src/lib/RenderErrorBoundary.tsrx +117 -0
  12. package/src/lib/RenderErrorBoundary.tsrx.d.ts +14 -0
  13. package/src/lib/actions.ts +90 -0
  14. package/src/lib/components/MemoryRouter.tsrx +42 -0
  15. package/src/lib/components/MemoryRouter.tsrx.d.ts +10 -0
  16. package/src/lib/components/Navigate.ts +60 -0
  17. package/src/lib/components/Router.tsrx +88 -0
  18. package/src/lib/components/Router.tsrx.d.ts +13 -0
  19. package/src/lib/components/RouterProvider.tsrx +320 -0
  20. package/src/lib/components/RouterProvider.tsrx.d.ts +21 -0
  21. package/src/lib/components/Routes.tsrx +53 -0
  22. package/src/lib/components/Routes.tsrx.d.ts +7 -0
  23. package/src/lib/components/routes-collector.ts +317 -0
  24. package/src/lib/components/utils.ts +171 -0
  25. package/src/lib/components/with-props.ts +72 -0
  26. package/src/lib/context.ts +129 -0
  27. package/src/lib/dom/Form.tsrx +76 -0
  28. package/src/lib/dom/Form.tsrx.d.ts +22 -0
  29. package/src/lib/dom/Link.tsrx +91 -0
  30. package/src/lib/dom/Link.tsrx.d.ts +22 -0
  31. package/src/lib/dom/NavLink.tsrx +118 -0
  32. package/src/lib/dom/NavLink.tsrx.d.ts +33 -0
  33. package/src/lib/dom/dom.ts +344 -0
  34. package/src/lib/dom/hooks.ts +93 -0
  35. package/src/lib/dom/lib.ts +822 -0
  36. package/src/lib/dom/routers.tsrx +104 -0
  37. package/src/lib/dom/routers.tsrx.d.ts +23 -0
  38. package/src/lib/dom/server.tsrx +350 -0
  39. package/src/lib/dom/server.tsrx.d.ts +25 -0
  40. package/src/lib/errors.ts +84 -0
  41. package/src/lib/framework-stubs.ts +103 -0
  42. package/src/lib/hooks.ts +1797 -0
  43. package/src/lib/href.ts +71 -0
  44. package/src/lib/react-types.ts +10 -0
  45. package/src/lib/router/history.ts +763 -0
  46. package/src/lib/router/instrumentation.ts +496 -0
  47. package/src/lib/router/links.ts +192 -0
  48. package/src/lib/router/router.ts +7165 -0
  49. package/src/lib/router/server-runtime-types.ts +12 -0
  50. package/src/lib/router/url.ts +8 -0
  51. package/src/lib/router/utils.ts +2179 -0
  52. package/src/lib/server-runtime/cookies.ts +240 -0
  53. package/src/lib/server-runtime/crypto.ts +55 -0
  54. package/src/lib/server-runtime/mode.ts +16 -0
  55. package/src/lib/server-runtime/sessions/cookieStorage.ts +54 -0
  56. package/src/lib/server-runtime/sessions/memoryStorage.ts +59 -0
  57. package/src/lib/server-runtime/sessions.ts +291 -0
  58. package/src/lib/server-runtime/warnings.ts +10 -0
  59. package/src/lib/types/future.ts +16 -0
  60. package/src/lib/types/params.ts +8 -0
  61. package/src/lib/types/register.ts +39 -0
  62. package/src/lib/types/route-module.ts +17 -0
  63. package/src/lib/types/utils.ts +37 -0
@@ -0,0 +1,2179 @@
1
+ // Vendored from react-router@7.18.1 packages/react-router/lib/router/utils.ts — unmodified except: type-only react import → local ../react-types shim.
2
+ // Re-vendor with `node scripts/vendor-remix-router.mjs`; never hand-edit.
3
+ import type * as React from '../react-types';
4
+ import type { MiddlewareEnabled } from '../types/future';
5
+ import type { Equal, Expect } from '../types/utils';
6
+ import type { Location, Path, To } from './history';
7
+ import { invariant, parsePath, warning } from './history';
8
+ import {
9
+ ABSOLUTE_URL_REGEX,
10
+ normalizeProtocolRelativeUrl,
11
+ PROTOCOL_RELATIVE_URL_REGEX,
12
+ } from './url';
13
+
14
+ export type MaybePromise<T> = T | Promise<T>;
15
+
16
+ /**
17
+ * Map of routeId -> data returned from a loader/action/error
18
+ */
19
+ export interface RouteData {
20
+ [routeId: string]: any;
21
+ }
22
+
23
+ export enum ResultType {
24
+ data = 'data',
25
+ redirect = 'redirect',
26
+ error = 'error',
27
+ }
28
+
29
+ /**
30
+ * Successful result from a loader or action
31
+ */
32
+ export interface SuccessResult {
33
+ type: ResultType.data;
34
+ data: unknown;
35
+ statusCode?: number;
36
+ headers?: Headers;
37
+ }
38
+
39
+ /**
40
+ * Redirect result from a loader or action
41
+ */
42
+ export interface RedirectResult {
43
+ type: ResultType.redirect;
44
+ // We keep the raw Response for redirects so we can return it verbatim
45
+ response: Response;
46
+ }
47
+
48
+ /**
49
+ * Unsuccessful result from a loader or action
50
+ */
51
+ export interface ErrorResult {
52
+ type: ResultType.error;
53
+ error: unknown;
54
+ statusCode?: number;
55
+ headers?: Headers;
56
+ }
57
+
58
+ /**
59
+ * Result from a loader or action - potentially successful or unsuccessful
60
+ */
61
+ export type DataResult = SuccessResult | RedirectResult | ErrorResult;
62
+
63
+ export type LowerCaseFormMethod = 'get' | 'post' | 'put' | 'patch' | 'delete';
64
+ export type UpperCaseFormMethod = Uppercase<LowerCaseFormMethod>;
65
+
66
+ /**
67
+ * Users can specify either lowercase or uppercase form methods on `<Form>`,
68
+ * useSubmit(), `<fetcher.Form>`, etc.
69
+ */
70
+ export type HTMLFormMethod = LowerCaseFormMethod | UpperCaseFormMethod;
71
+
72
+ /**
73
+ * Active navigation/fetcher form methods are exposed in uppercase on the
74
+ * RouterState. This is to align with the normalization done via fetch().
75
+ */
76
+ export type FormMethod = UpperCaseFormMethod;
77
+ export type MutationFormMethod = Exclude<FormMethod, 'GET'>;
78
+
79
+ export type FormEncType =
80
+ | 'application/x-www-form-urlencoded'
81
+ | 'multipart/form-data'
82
+ | 'application/json'
83
+ | 'text/plain';
84
+
85
+ // Thanks https://github.com/sindresorhus/type-fest!
86
+ type JsonObject = { [Key in string]: JsonValue } & {
87
+ [Key in string]?: JsonValue | undefined;
88
+ };
89
+ type JsonArray = JsonValue[] | readonly JsonValue[];
90
+ type JsonPrimitive = string | number | boolean | null;
91
+ type JsonValue = JsonPrimitive | JsonObject | JsonArray;
92
+
93
+ /**
94
+ * @private
95
+ * Internal interface to pass around for action submissions, not intended for
96
+ * external consumption
97
+ */
98
+ export type Submission =
99
+ | {
100
+ formMethod: FormMethod;
101
+ formAction: string;
102
+ formEncType: FormEncType;
103
+ formData: FormData;
104
+ json: undefined;
105
+ text: undefined;
106
+ }
107
+ | {
108
+ formMethod: FormMethod;
109
+ formAction: string;
110
+ formEncType: FormEncType;
111
+ formData: undefined;
112
+ json: JsonValue;
113
+ text: undefined;
114
+ }
115
+ | {
116
+ formMethod: FormMethod;
117
+ formAction: string;
118
+ formEncType: FormEncType;
119
+ formData: undefined;
120
+ json: undefined;
121
+ text: string;
122
+ };
123
+
124
+ /**
125
+ * A context instance used as the key for the `get`/`set` methods of a
126
+ * {@link RouterContextProvider}. Accepts an optional default
127
+ * value to be returned if no value has been set.
128
+ */
129
+ export interface RouterContext<T = unknown> {
130
+ defaultValue?: T;
131
+ }
132
+
133
+ /**
134
+ * Creates a type-safe {@link RouterContext} object that can be used to
135
+ * store and retrieve arbitrary values in [`action`](../../start/framework/route-module#action)s,
136
+ * [`loader`](../../start/framework/route-module#loader)s, and [middleware](../../how-to/middleware).
137
+ * Similar to React's [`createContext`](https://react.dev/reference/react/createContext),
138
+ * but specifically designed for React Router's request/response lifecycle.
139
+ *
140
+ * If a `defaultValue` is provided, it will be returned from `context.get()`
141
+ * when no value has been set for the context. Otherwise, reading this context
142
+ * when no value has been set will throw an error.
143
+ *
144
+ * ```tsx filename=app/context.ts
145
+ * import { createContext } from "react-router";
146
+ *
147
+ * // Create a context for user data
148
+ * export const userContext =
149
+ * createContext<User | null>(null);
150
+ * ```
151
+ *
152
+ * ```tsx filename=app/middleware/auth.ts
153
+ * import { getUserFromSession } from "~/auth.server";
154
+ * import { userContext } from "~/context";
155
+ *
156
+ * export const authMiddleware = async ({
157
+ * context,
158
+ * request,
159
+ * }) => {
160
+ * const user = await getUserFromSession(request);
161
+ * context.set(userContext, user);
162
+ * };
163
+ * ```
164
+ *
165
+ * ```tsx filename=app/routes/profile.tsx
166
+ * import { userContext } from "~/context";
167
+ *
168
+ * export async function loader({
169
+ * context,
170
+ * }: Route.LoaderArgs) {
171
+ * const user = context.get(userContext);
172
+ *
173
+ * if (!user) {
174
+ * throw new Response("Unauthorized", { status: 401 });
175
+ * }
176
+ *
177
+ * return { user };
178
+ * }
179
+ * ```
180
+ *
181
+ * @public
182
+ * @category Utils
183
+ * @mode framework
184
+ * @mode data
185
+ * @param defaultValue An optional default value for the context. This value
186
+ * will be returned if no value has been set for this context.
187
+ * @returns A {@link RouterContext} object that can be used with
188
+ * `context.get()` and `context.set()` in [`action`](../../start/framework/route-module#action)s,
189
+ * [`loader`](../../start/framework/route-module#loader)s, and [middleware](../../how-to/middleware).
190
+ */
191
+ export function createContext<T>(defaultValue?: T): RouterContext<T> {
192
+ return { defaultValue };
193
+ }
194
+
195
+ /**
196
+ * Provides methods for writing/reading values in application context in a
197
+ * type-safe way. Primarily for usage with [middleware](../../how-to/middleware).
198
+ *
199
+ * @example
200
+ * import {
201
+ * createContext,
202
+ * RouterContextProvider
203
+ * } from "react-router";
204
+ *
205
+ * const userContext = createContext<User | null>(null);
206
+ * const contextProvider = new RouterContextProvider();
207
+ * contextProvider.set(userContext, getUser());
208
+ * // ^ Type-safe
209
+ * const user = contextProvider.get(userContext);
210
+ * // ^ User
211
+ *
212
+ * @public
213
+ * @category Utils
214
+ * @mode framework
215
+ * @mode data
216
+ */
217
+ export class RouterContextProvider {
218
+ #map = new Map<RouterContext, unknown>();
219
+
220
+ /**
221
+ * Create a new `RouterContextProvider` instance
222
+ * @param init An optional initial context map to populate the provider with
223
+ */
224
+ constructor(init?: Map<RouterContext, unknown>) {
225
+ if (init) {
226
+ for (let [context, value] of init) {
227
+ this.set(context, value);
228
+ }
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Access a value from the context. If no value has been set for the context,
234
+ * it will return the context's `defaultValue` if provided, or throw an error
235
+ * if no `defaultValue` was set.
236
+ * @param context The context to get the value for
237
+ * @returns The value for the context, or the context's `defaultValue` if no
238
+ * value was set
239
+ */
240
+ get<T>(context: RouterContext<T>): T {
241
+ if (this.#map.has(context)) {
242
+ return this.#map.get(context) as T;
243
+ }
244
+
245
+ if (context.defaultValue !== undefined) {
246
+ return context.defaultValue;
247
+ }
248
+
249
+ throw new Error('No value found for context');
250
+ }
251
+
252
+ /**
253
+ * Set a value for the context. If the context already has a value set, this
254
+ * will overwrite it.
255
+ *
256
+ * @param context The context to set the value for
257
+ * @param value The value to set for the context
258
+ * @returns {void}
259
+ */
260
+ set<C extends RouterContext>(
261
+ context: C,
262
+ value: C extends RouterContext<infer T> ? T : never,
263
+ ): void {
264
+ this.#map.set(context, value);
265
+ }
266
+ }
267
+
268
+ type DefaultContext = MiddlewareEnabled extends true ? Readonly<RouterContextProvider> : any;
269
+
270
+ /**
271
+ * @private
272
+ * Arguments passed to route loader/action functions. Same for now but we keep
273
+ * this as a private implementation detail in case they diverge in the future.
274
+ */
275
+ interface DataFunctionArgs<Context> {
276
+ /** A {@link https://developer.mozilla.org/en-US/docs/Web/API/Request Fetch Request instance} which you can use to read headers (like cookies, and {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams URLSearchParams} from the request. */
277
+ request: Request;
278
+ /**
279
+ * A URL instance representing the application location being navigated to or
280
+ * fetched. By default, this matches `request.url`.
281
+ *
282
+ * In Framework mode with `future.v8_passThroughRequests` enabled, this is a
283
+ * normalized URL with React-Router-specific implementation details removed
284
+ * (`.data` suffixes, `index`/`_routes` search params).
285
+ */
286
+ url: URL;
287
+ /**
288
+ * Matched un-interpolated route pattern for the current path (i.e., /blog/:slug).
289
+ * Mostly useful as a identifier to aggregate on for logging/tracing/etc.
290
+ */
291
+ pattern: string;
292
+ /**
293
+ * {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route.
294
+ * @example
295
+ * // app/routes.ts
296
+ * route("teams/:teamId", "./team.tsx"),
297
+ *
298
+ * // app/team.tsx
299
+ * export function loader({
300
+ * params,
301
+ * }: Route.LoaderArgs) {
302
+ * params.teamId;
303
+ * // ^ string
304
+ * }
305
+ */
306
+ params: Params;
307
+ /**
308
+ * This is the context passed in to your server adapter's getLoadContext() function.
309
+ * It's a way to bridge the gap between the adapter's request/response API with your React Router app.
310
+ * It is only applicable if you are using a custom server adapter.
311
+ */
312
+ context: Context;
313
+ }
314
+
315
+ /**
316
+ * Route middleware `next` function to call downstream handlers and then complete
317
+ * middlewares from the bottom-up
318
+ */
319
+ export interface MiddlewareNextFunction<Result = unknown> {
320
+ (): Promise<Result>;
321
+ }
322
+
323
+ /**
324
+ * Route middleware function signature. Receives the same "data" arguments as a
325
+ * `loader`/`action` (`request`, `params`, `context`) as the first parameter and
326
+ * a `next` function as the second parameter which will call downstream handlers
327
+ * and then complete middlewares from the bottom-up
328
+ */
329
+ export type MiddlewareFunction<Result = unknown> = (
330
+ args: DataFunctionArgs<Readonly<RouterContextProvider>>,
331
+ next: MiddlewareNextFunction<Result>,
332
+ ) => MaybePromise<Result | void>;
333
+
334
+ /**
335
+ * Arguments passed to loader functions
336
+ */
337
+ export interface LoaderFunctionArgs<Context = DefaultContext> extends DataFunctionArgs<Context> {}
338
+
339
+ /**
340
+ * Arguments passed to action functions
341
+ */
342
+ export interface ActionFunctionArgs<Context = DefaultContext> extends DataFunctionArgs<Context> {}
343
+
344
+ /**
345
+ * Loaders and actions can return anything
346
+ */
347
+ type DataFunctionValue = unknown;
348
+
349
+ type DataFunctionReturnValue = MaybePromise<DataFunctionValue>;
350
+
351
+ /**
352
+ * Route loader function signature
353
+ */
354
+ export type LoaderFunction<Context = DefaultContext> = {
355
+ (args: LoaderFunctionArgs<Context>, handlerCtx?: unknown): DataFunctionReturnValue;
356
+ } & { hydrate?: boolean };
357
+
358
+ /**
359
+ * Route action function signature
360
+ */
361
+ export interface ActionFunction<Context = DefaultContext> {
362
+ (args: ActionFunctionArgs<Context>, handlerCtx?: unknown): DataFunctionReturnValue;
363
+ }
364
+
365
+ /**
366
+ * Arguments passed to shouldRevalidate function
367
+ */
368
+ export interface ShouldRevalidateFunctionArgs {
369
+ /** This is the url the navigation started from. You can compare it with `nextUrl` to decide if you need to revalidate this route's data. */
370
+ currentUrl: URL;
371
+ /** These are the {@link https://reactrouter.com/start/framework/routing#dynamic-segments dynamic route params} from the URL that can be compared to the `nextParams` to decide if you need to reload or not. Perhaps you're using only a partial piece of the param for data loading, you don't need to revalidate if a superfluous part of the param changed. */
372
+ currentParams: DataRouteMatch['params'];
373
+ /** In the case of navigation, this the URL the user is requesting. Some revalidations are not navigation, so it will simply be the same as currentUrl. */
374
+ nextUrl: URL;
375
+ /** In the case of navigation, these are the {@link https://reactrouter.com/start/framework/routing#dynamic-segments dynamic route params} from the next location the user is requesting. Some revalidations are not navigation, so it will simply be the same as currentParams. */
376
+ nextParams: DataRouteMatch['params'];
377
+ /** The method (probably `"GET"` or `"POST"`) used in the form submission that triggered the revalidation. */
378
+ formMethod?: Submission['formMethod'];
379
+ /** The form action (`<Form action="/somewhere">`) that triggered the revalidation. */
380
+ formAction?: Submission['formAction'];
381
+ /** The form encType (`<Form encType="application/x-www-form-urlencoded">) used in the form submission that triggered the revalidation*/
382
+ formEncType?: Submission['formEncType'];
383
+ /** The form submission data when the form's encType is `text/plain` */
384
+ text?: Submission['text'];
385
+ /** The form submission data when the form's encType is `application/x-www-form-urlencoded` or `multipart/form-data` */
386
+ formData?: Submission['formData'];
387
+ /** The form submission data when the form's encType is `application/json` */
388
+ json?: Submission['json'];
389
+ /** The status code of the action response */
390
+ actionStatus?: number;
391
+ /**
392
+ * When a submission causes the revalidation this will be the result of the action—either action data or an error if the action failed. It's common to include some information in the action result to instruct shouldRevalidate to revalidate or not.
393
+ *
394
+ * @example
395
+ * export async function action() {
396
+ * await saveSomeStuff();
397
+ * return { ok: true };
398
+ * }
399
+ *
400
+ * export function shouldRevalidate({
401
+ * actionResult,
402
+ * }) {
403
+ * if (actionResult?.ok) {
404
+ * return false;
405
+ * }
406
+ * return true;
407
+ * }
408
+ */
409
+ actionResult?: any;
410
+ /**
411
+ * By default, React Router doesn't call every loader all the time. There are reliable optimizations it can make by default. For example, only loaders with changing params are called. Consider navigating from the following URL to the one below it:
412
+ *
413
+ * /projects/123/tasks/abc
414
+ * /projects/123/tasks/def
415
+ * React Router will only call the loader for tasks/def because the param for projects/123 didn't change.
416
+ *
417
+ * It's safest to always return defaultShouldRevalidate after you've done your specific optimizations that return false, otherwise your UI might get out of sync with your data on the server.
418
+ */
419
+ defaultShouldRevalidate: boolean;
420
+ }
421
+
422
+ /**
423
+ * Route shouldRevalidate function signature. This runs after any submission
424
+ * (navigation or fetcher), so we flatten the navigation/fetcher submission
425
+ * onto the arguments. It shouldn't matter whether it came from a navigation
426
+ * or a fetcher, what really matters is the URLs and the formData since loaders
427
+ * have to re-run based on the data models that were potentially mutated.
428
+ */
429
+ export interface ShouldRevalidateFunction {
430
+ (args: ShouldRevalidateFunctionArgs): boolean;
431
+ }
432
+
433
+ export interface DataStrategyMatch extends RouteMatch<string, DataRouteObject> {
434
+ /**
435
+ * @private
436
+ */
437
+ _lazyPromises?: {
438
+ middleware: Promise<void> | undefined;
439
+ handler: Promise<void> | undefined;
440
+ route: Promise<void> | undefined;
441
+ };
442
+ /**
443
+ * @deprecated Deprecated in favor of `shouldCallHandler`
444
+ *
445
+ * A boolean value indicating whether this route handler should be called in
446
+ * this pass.
447
+ *
448
+ * The `matches` array always includes _all_ matched routes even when only
449
+ * _some_ route handlers need to be called so that things like middleware can
450
+ * be implemented.
451
+ *
452
+ * `shouldLoad` is usually only interesting if you are skipping the route
453
+ * handler entirely and implementing custom handler logic - since it lets you
454
+ * determine if that custom logic should run for this route or not.
455
+ *
456
+ * For example:
457
+ * - If you are on `/parent/child/a` and you navigate to `/parent/child/b` -
458
+ * you'll get an array of three matches (`[parent, child, b]`), but only `b`
459
+ * will have `shouldLoad=true` because the data for `parent` and `child` is
460
+ * already loaded
461
+ * - If you are on `/parent/child/a` and you submit to `a`'s [`action`](https://reactrouter.com/docs/start/data/route-object#action),
462
+ * then only `a` will have `shouldLoad=true` for the action execution of
463
+ * `dataStrategy`
464
+ * - After the [`action`](https://reactrouter.com/docs/start/data/route-object#action),
465
+ * `dataStrategy` will be called again for the [`loader`](https://reactrouter.com/docs/start/data/route-object#loader)
466
+ * revalidation, and all matches will have `shouldLoad=true` (assuming no
467
+ * custom `shouldRevalidate` implementations)
468
+ */
469
+ shouldLoad: boolean;
470
+ /**
471
+ * Arguments passed to the `shouldRevalidate` function for this `loader` execution.
472
+ * Will be `null` if this is not a revalidating loader {@link DataStrategyMatch}.
473
+ */
474
+ shouldRevalidateArgs: ShouldRevalidateFunctionArgs | null;
475
+ /**
476
+ * Determine if this route's handler should be called during this `dataStrategy`
477
+ * execution. Calling it with no arguments will leverage the default revalidation
478
+ * behavior. You can pass your own `defaultShouldRevalidate` value if you wish
479
+ * to change the default revalidation behavior with your `dataStrategy`.
480
+ *
481
+ * @param defaultShouldRevalidate `defaultShouldRevalidate` override value (optional)
482
+ */
483
+ shouldCallHandler(defaultShouldRevalidate?: boolean): boolean;
484
+ /**
485
+ * An async function that will resolve any `route.lazy` implementations and
486
+ * execute the route's handler (if necessary), returning a {@link DataStrategyResult}
487
+ *
488
+ * - Calling `match.resolve` does not mean you're calling the
489
+ * [`action`](https://reactrouter.com/docs/start/data/route-object#action)/[`loader`](https://reactrouter.com/docs/start/data/route-object#loader)
490
+ * (the "handler") - `resolve` will only call the `handler` internally if
491
+ * needed _and_ if you don't pass your own `handlerOverride` function parameter
492
+ * - It is safe to call `match.resolve` for all matches, even if they have
493
+ * `shouldLoad=false`, and it will no-op if no loading is required
494
+ * - You should generally always call `match.resolve()` for `shouldLoad:true`
495
+ * routes to ensure that any `route.lazy` implementations are processed
496
+ * - See the examples below for how to implement custom handler execution via
497
+ * `match.resolve`
498
+ */
499
+ resolve: (
500
+ handlerOverride?: (
501
+ handler: (ctx?: unknown) => DataFunctionReturnValue,
502
+ ) => DataFunctionReturnValue,
503
+ ) => Promise<DataStrategyResult>;
504
+ }
505
+
506
+ export interface DataStrategyFunctionArgs<
507
+ Context = DefaultContext,
508
+ > extends DataFunctionArgs<Context> {
509
+ /**
510
+ * Matches for this route extended with Data strategy APIs
511
+ */
512
+ matches: DataStrategyMatch[];
513
+ runClientMiddleware: (
514
+ cb: DataStrategyFunction<Context>,
515
+ ) => Promise<Record<string, DataStrategyResult>>;
516
+ /**
517
+ * The key of the fetcher we are calling `dataStrategy` for, otherwise `null`
518
+ * for navigational executions
519
+ */
520
+ fetcherKey: string | null;
521
+ }
522
+
523
+ /**
524
+ * Result from a loader or action called via dataStrategy
525
+ */
526
+ export interface DataStrategyResult {
527
+ type: 'data' | 'error';
528
+ result: unknown; // data, Error, Response, DeferredData, DataWithResponseInit
529
+ }
530
+
531
+ export interface DataStrategyFunction<Context = DefaultContext> {
532
+ (args: DataStrategyFunctionArgs<Context>): Promise<Record<string, DataStrategyResult>>;
533
+ }
534
+
535
+ export type PatchRoutesOnNavigationFunctionArgs = {
536
+ signal: AbortSignal;
537
+ path: string;
538
+ matches: RouteMatch[];
539
+ fetcherKey: string | undefined;
540
+ patch: (routeId: string | null, children: RouteObject[]) => void;
541
+ };
542
+
543
+ export type PatchRoutesOnNavigationFunction = (
544
+ opts: PatchRoutesOnNavigationFunctionArgs,
545
+ ) => MaybePromise<void>;
546
+
547
+ /**
548
+ * Function provided to set route-specific properties from route objects
549
+ */
550
+ export interface MapRoutePropertiesFunction {
551
+ (route: DataRouteObject): {
552
+ hasErrorBoundary: boolean;
553
+ } & Record<string, any>;
554
+ }
555
+
556
+ /**
557
+ * Keys we cannot change from within a lazy object. We spread all other keys
558
+ * onto the route. Either they're meaningful to the router, or they'll get
559
+ * ignored.
560
+ */
561
+ type UnsupportedLazyRouteObjectKey =
562
+ | 'lazy'
563
+ | 'caseSensitive'
564
+ | 'path'
565
+ | 'id'
566
+ | 'index'
567
+ | 'children';
568
+ const unsupportedLazyRouteObjectKeys = new Set<UnsupportedLazyRouteObjectKey>([
569
+ 'lazy',
570
+ 'caseSensitive',
571
+ 'path',
572
+ 'id',
573
+ 'index',
574
+ 'children',
575
+ ]);
576
+ export function isUnsupportedLazyRouteObjectKey(key: string): key is UnsupportedLazyRouteObjectKey {
577
+ return unsupportedLazyRouteObjectKeys.has(key as UnsupportedLazyRouteObjectKey);
578
+ }
579
+
580
+ /**
581
+ * Keys we cannot change from within a lazy() function. We spread all other keys
582
+ * onto the route. Either they're meaningful to the router, or they'll get
583
+ * ignored.
584
+ */
585
+ type UnsupportedLazyRouteFunctionKey = UnsupportedLazyRouteObjectKey | 'middleware';
586
+ const unsupportedLazyRouteFunctionKeys = new Set<UnsupportedLazyRouteFunctionKey>([
587
+ 'lazy',
588
+ 'caseSensitive',
589
+ 'path',
590
+ 'id',
591
+ 'index',
592
+ 'middleware',
593
+ 'children',
594
+ ]);
595
+ export function isUnsupportedLazyRouteFunctionKey(
596
+ key: string,
597
+ ): key is UnsupportedLazyRouteFunctionKey {
598
+ return unsupportedLazyRouteFunctionKeys.has(key as UnsupportedLazyRouteFunctionKey);
599
+ }
600
+
601
+ /**
602
+ * lazy object to load route properties, which can add non-matching
603
+ * related properties to a route
604
+ */
605
+ export type LazyRouteObject<R extends RouteObject> = {
606
+ [K in keyof R as K extends UnsupportedLazyRouteObjectKey ? never : K]?: () => Promise<
607
+ R[K] | null | undefined
608
+ >;
609
+ };
610
+
611
+ /**
612
+ * lazy() function to load a route definition, which can add non-matching
613
+ * related properties to a route
614
+ */
615
+ export interface LazyRouteFunction<R extends RouteObject> {
616
+ (): Promise<
617
+ Omit<R, UnsupportedLazyRouteFunctionKey> &
618
+ Partial<Record<UnsupportedLazyRouteFunctionKey, never>>
619
+ >;
620
+ }
621
+
622
+ export type LazyRouteDefinition<R extends RouteObject> = LazyRouteObject<R> | LazyRouteFunction<R>;
623
+
624
+ /**
625
+ * Base RouteObject with common props shared by all types of routes
626
+ * @internal
627
+ */
628
+ export type BaseRouteObject = {
629
+ /**
630
+ * Whether the path should be case-sensitive. Defaults to `false`.
631
+ */
632
+ caseSensitive?: boolean;
633
+ /**
634
+ * The path pattern to match. If unspecified or empty, then this becomes a
635
+ * layout route.
636
+ */
637
+ path?: string;
638
+ /**
639
+ * The unique identifier for this route (for use with {@link DataRouter}s)
640
+ */
641
+ id?: string;
642
+ /**
643
+ * The route middleware.
644
+ * See [`middleware`](../../start/data/route-object#middleware).
645
+ */
646
+ middleware?: MiddlewareFunction[];
647
+ /**
648
+ * The route loader.
649
+ * See [`loader`](../../start/data/route-object#loader).
650
+ */
651
+ loader?: LoaderFunction | boolean;
652
+ /**
653
+ * The route action.
654
+ * See [`action`](../../start/data/route-object#action).
655
+ */
656
+ action?: ActionFunction | boolean;
657
+ // TODO(v8): deprecate/remove
658
+ hasErrorBoundary?: boolean;
659
+ /**
660
+ * The route shouldRevalidate function.
661
+ * See [`shouldRevalidate`](../../start/data/route-object#shouldRevalidate).
662
+ */
663
+ shouldRevalidate?: ShouldRevalidateFunction;
664
+ /**
665
+ * The route handle.
666
+ */
667
+ handle?: any;
668
+ /**
669
+ * A function that returns a promise that resolves to the route object.
670
+ * Used for code-splitting routes.
671
+ * See [`lazy`](../../start/data/route-object#lazy).
672
+ */
673
+ lazy?: LazyRouteDefinition<BaseRouteObject>;
674
+ /**
675
+ * The React Component to render when this route matches.
676
+ * Mutually exclusive with `element`.
677
+ */
678
+ Component?: React.ComponentType | null;
679
+ /**
680
+ * The React element to render when this Route matches.
681
+ * Mutually exclusive with `Component`.
682
+ */
683
+ element?: React.ReactNode | null;
684
+ /**
685
+ * The React Component to render at this route if an error occurs.
686
+ * Mutually exclusive with `errorElement`.
687
+ */
688
+ ErrorBoundary?: React.ComponentType | null;
689
+ /**
690
+ * The React element to render at this route if an error occurs.
691
+ * Mutually exclusive with `ErrorBoundary`.
692
+ */
693
+ errorElement?: React.ReactNode | null;
694
+ /**
695
+ * The React Component to render while this router is loading data.
696
+ * Mutually exclusive with `hydrateFallbackElement`.
697
+ */
698
+ HydrateFallback?: React.ComponentType | null;
699
+ /**
700
+ * The React element to render while this router is loading data.
701
+ * Mutually exclusive with `HydrateFallback`.
702
+ */
703
+ hydrateFallbackElement?: React.ReactNode | null;
704
+ };
705
+
706
+ /**
707
+ * Index routes must not have children
708
+ */
709
+ export type IndexRouteObject = BaseRouteObject & {
710
+ /**
711
+ * Child Route objects - not valid on index routes.
712
+ */
713
+ children?: undefined;
714
+ /**
715
+ * Whether this is an index route.
716
+ */
717
+ index: true;
718
+ };
719
+
720
+ /**
721
+ * Non-index routes may have children, but cannot have `index` set to `true`.
722
+ */
723
+ export type NonIndexRouteObject = BaseRouteObject & {
724
+ /**
725
+ * Child Route objects.
726
+ */
727
+ children?: RouteObject[];
728
+ /**
729
+ * Whether this is an index route - must be `false` or undefined on non-index routes.
730
+ */
731
+ index?: false;
732
+ };
733
+
734
+ /**
735
+ * A route object represents a logical route, with (optionally) its child
736
+ * routes organized in a tree-like structure.
737
+ */
738
+ export type RouteObject = IndexRouteObject | NonIndexRouteObject;
739
+
740
+ export type DataIndexRouteObject = IndexRouteObject & {
741
+ id: string;
742
+ };
743
+
744
+ export type DataNonIndexRouteObject = NonIndexRouteObject & {
745
+ children?: DataRouteObject[];
746
+ id: string;
747
+ };
748
+
749
+ /**
750
+ * A data route object, which is just a RouteObject with a required unique ID
751
+ */
752
+ export type DataRouteObject = DataIndexRouteObject | DataNonIndexRouteObject;
753
+
754
+ export type RouteManifest<R = DataRouteObject> = Record<string, R | undefined>;
755
+
756
+ // prettier-ignore
757
+ type Regex_az = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z"
758
+ type Regex_AZ = Uppercase<Regex_az>;
759
+ type Regex_09 = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
760
+ type Regex_w = Regex_az | Regex_AZ | Regex_09 | '_';
761
+
762
+ // prettier-ignore
763
+ /** Emulates Regex `+` operator */
764
+ type RegexMatchPlus<char extends string, T extends string> =
765
+ _RegexMatchPlus<char, T> extends infer result extends string ?
766
+ result extends '' ? never : result
767
+ :
768
+ never
769
+
770
+ // prettier-ignore
771
+ type _RegexMatchPlus<char extends string, T extends string> =
772
+ T extends `${infer head extends char}${infer rest}` ?
773
+ `${head}${_RegexMatchPlus<char, rest>}`
774
+ :
775
+ ''
776
+
777
+ type ParamNameChar = Regex_w | '-';
778
+
779
+ type Simplify<T> = { [K in keyof T]: T[K] } & {};
780
+
781
+ // prettier-ignore
782
+ type GeneratePathParams<path extends string> = Simplify<
783
+ & ParseParams<path>
784
+ & { [key in string]: string | null | undefined }
785
+ >
786
+
787
+ // prettier-ignore
788
+ type ParseParams<path extends string> =
789
+ // check if path is just a wildcard
790
+ path extends '*' ? { '*': string } :
791
+ // look for wildcard at the end of the path
792
+ path extends `${infer rest}/*` ? { '*': string } & ParseParams<rest> :
793
+ // look for params in the absence of wildcards
794
+ _ParseParams<path>;
795
+
796
+ // prettier-ignore
797
+ type _ParseParams<path extends string> =
798
+ // split path into individual path segments
799
+ path extends `${infer left}/${infer right}` ?
800
+ _ParseParams<left> & _ParseParams<right> :
801
+ // look for optional param in segment
802
+ path extends `:${infer param}?${string}` ?
803
+ { [key in RegexMatchPlus<ParamNameChar, param>]?: string | null | undefined } :
804
+ // look for required param in segment
805
+ path extends `:${infer param}` ?
806
+ { [key in RegexMatchPlus<ParamNameChar, param>]: string } :
807
+ {};
808
+
809
+ // prettier-ignore
810
+ export type PathParam<path extends string> = (keyof ParseParams<path>) & string;
811
+
812
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
813
+ type _tests = [
814
+ // PathParam
815
+ Expect<Equal<PathParam<'/a/b/*'>, '*'>>,
816
+ Expect<Equal<PathParam<':a'>, 'a'>>,
817
+ Expect<Equal<PathParam<'/a/:b'>, 'b'>>,
818
+ Expect<Equal<PathParam<'/a/blahblahblah:b'>, never>>,
819
+ Expect<Equal<PathParam<'/:a/:b'>, 'a' | 'b'>>,
820
+ Expect<Equal<PathParam<'/:a/b/:c/*'>, 'a' | 'c' | '*'>>,
821
+ Expect<Equal<PathParam<'/:lang.xml'>, 'lang'>>,
822
+ Expect<Equal<PathParam<'/:lang?.xml'>, 'lang'>>,
823
+
824
+ // ParseParams
825
+ Expect<Equal<ParseParams<'/a/b/*'>, { '*': string }>>,
826
+ Expect<Equal<ParseParams<':a'>, { a: string }>>,
827
+ Expect<Equal<ParseParams<'/a/:b'>, { b: string }>>,
828
+ Expect<Equal<ParseParams<'/a/blahblahblah:b'>, {}>>,
829
+ Expect<Equal<Simplify<ParseParams<'/:a/:b'>>, { a: string; b: string }>>,
830
+ Expect<Equal<Simplify<ParseParams<'/:a/b/:c/*'>>, { a: string; c: string; '*': string }>>,
831
+ Expect<Equal<ParseParams<'/:lang.xml'>, { lang: string }>>,
832
+ Expect<Equal<ParseParams<'/:lang?.xml'>, { lang?: string | null | undefined }>>,
833
+ Expect<Equal<Simplify<ParseParams<'/:a/:a'>>, { a: string }>>,
834
+ Expect<Equal<Simplify<ParseParams<'/:a/:a?'>>, { a: string }>>,
835
+ Expect<Equal<Simplify<ParseParams<'/:a?/:a?'>>, { a?: string | null | undefined }>>,
836
+ ];
837
+
838
+ // Attempt to parse the given string segment. If it fails, then just return the
839
+ // plain string type as a default fallback. Otherwise, return the union of the
840
+ // parsed string literals that were referenced as dynamic segments in the route.
841
+ export type ParamParseKey<Segment extends string> =
842
+ // if you could not find path params, fallback to `string`
843
+ [PathParam<Segment>] extends [never] ? string : PathParam<Segment>;
844
+
845
+ /**
846
+ * The parameters that were parsed from the URL path.
847
+ */
848
+ export type Params<Key extends string = string> = {
849
+ readonly [key in Key]: string | undefined;
850
+ };
851
+
852
+ /**
853
+ * A RouteMatch contains info about how a route matched a URL.
854
+ */
855
+ export interface RouteMatch<
856
+ ParamKey extends string = string,
857
+ RouteObjectType extends RouteObject = RouteObject,
858
+ > {
859
+ /**
860
+ * The names and values of dynamic parameters in the URL.
861
+ */
862
+ params: Params<ParamKey>;
863
+ /**
864
+ * The portion of the URL pathname that was matched.
865
+ */
866
+ pathname: string;
867
+ /**
868
+ * The portion of the URL pathname that was matched before child routes.
869
+ */
870
+ pathnameBase: string;
871
+ /**
872
+ * The route object that was used to match.
873
+ */
874
+ route: RouteObjectType;
875
+ }
876
+
877
+ export interface DataRouteMatch extends RouteMatch<string, DataRouteObject> {}
878
+
879
+ function isIndexRoute(route: RouteObject): route is IndexRouteObject {
880
+ return route.index === true;
881
+ }
882
+
883
+ // Walk the route tree generating unique IDs where necessary, so we are working
884
+ // solely with DataRouteObject's within the Router
885
+ export function convertRoutesToDataRoutes(
886
+ routes: RouteObject[],
887
+ mapRouteProperties: MapRoutePropertiesFunction,
888
+ parentPath: string[] = [],
889
+ manifest: RouteManifest = {},
890
+ allowInPlaceMutations = false,
891
+ ): DataRouteObject[] {
892
+ return routes.map((route, index) => {
893
+ let treePath = [...parentPath, String(index)];
894
+ let id = typeof route.id === 'string' ? route.id : treePath.join('-');
895
+ invariant(route.index !== true || !route.children, `Cannot specify children on an index route`);
896
+ invariant(
897
+ allowInPlaceMutations || !manifest[id],
898
+ `Found a route id collision on id "${id}". Route ` +
899
+ "id's must be globally unique within Data Router usages",
900
+ );
901
+
902
+ if (isIndexRoute(route)) {
903
+ let indexRoute: DataIndexRouteObject = {
904
+ ...route,
905
+ id,
906
+ };
907
+ manifest[id] = mergeRouteUpdates(indexRoute, mapRouteProperties(indexRoute));
908
+ return indexRoute;
909
+ } else {
910
+ let pathOrLayoutRoute: DataNonIndexRouteObject = {
911
+ ...route,
912
+ id,
913
+ children: undefined,
914
+ };
915
+ manifest[id] = mergeRouteUpdates(pathOrLayoutRoute, mapRouteProperties(pathOrLayoutRoute));
916
+
917
+ if (route.children) {
918
+ pathOrLayoutRoute.children = convertRoutesToDataRoutes(
919
+ route.children,
920
+ mapRouteProperties,
921
+ treePath,
922
+ manifest,
923
+ allowInPlaceMutations,
924
+ );
925
+ }
926
+
927
+ return pathOrLayoutRoute;
928
+ }
929
+ });
930
+ }
931
+
932
+ function mergeRouteUpdates<T extends DataRouteObject>(
933
+ route: T,
934
+ updates: ReturnType<MapRoutePropertiesFunction>,
935
+ ): T {
936
+ return Object.assign(route, {
937
+ ...updates,
938
+ ...(typeof updates.lazy === 'object' && updates.lazy != null
939
+ ? {
940
+ lazy: {
941
+ ...route.lazy,
942
+ ...updates.lazy,
943
+ },
944
+ }
945
+ : {}),
946
+ });
947
+ }
948
+
949
+ /**
950
+ * Matches the given routes to a location and returns the match data.
951
+ *
952
+ * @example
953
+ * import { matchRoutes } from "react-router";
954
+ *
955
+ * let routes = [{
956
+ * path: "/",
957
+ * Component: Root,
958
+ * children: [{
959
+ * path: "dashboard",
960
+ * Component: Dashboard,
961
+ * }]
962
+ * }];
963
+ *
964
+ * matchRoutes(routes, "/dashboard"); // [rootMatch, dashboardMatch]
965
+ *
966
+ * @public
967
+ * @category Utils
968
+ * @param routes The array of route objects to match against.
969
+ * @param locationArg The location to match against, either a string path or a
970
+ * partial {@link Location} object
971
+ * @param basename Optional base path to strip from the location before matching.
972
+ * Defaults to `/`.
973
+ * @returns An array of matched routes, or `null` if no matches were found.
974
+ */
975
+ export function matchRoutes<RouteObjectType extends RouteObject = RouteObject>(
976
+ routes: RouteObjectType[],
977
+ locationArg: Partial<Location> | string,
978
+ basename = '/',
979
+ ): RouteMatch<string, RouteObjectType>[] | null {
980
+ return matchRoutesImpl(routes, locationArg, basename, false);
981
+ }
982
+
983
+ export function matchRoutesImpl<RouteObjectType extends RouteObject = RouteObject>(
984
+ routes: RouteObjectType[],
985
+ locationArg: Partial<Location> | string,
986
+ basename: string,
987
+ allowPartial: boolean,
988
+ precomputedBranches?: RouteBranch<RouteObjectType>[],
989
+ ): RouteMatch<string, RouteObjectType>[] | null {
990
+ let location = typeof locationArg === 'string' ? parsePath(locationArg) : locationArg;
991
+
992
+ let pathname = stripBasename(location.pathname || '/', basename);
993
+
994
+ if (pathname == null) {
995
+ return null;
996
+ }
997
+
998
+ let branches = precomputedBranches ?? flattenAndRankRoutes(routes);
999
+
1000
+ let matches = null;
1001
+ let decoded = decodePath(pathname);
1002
+ for (let i = 0; matches == null && i < branches.length; ++i) {
1003
+ // Incoming pathnames are generally encoded from either window.location
1004
+ // or from router.navigate, but we want to match against the unencoded
1005
+ // paths in the route definitions. Memory router locations won't be
1006
+ // encoded here but there also shouldn't be anything to decode so this
1007
+ // should be a safe operation. This avoids needing matchRoutes to be
1008
+ // history-aware.
1009
+ matches = matchRouteBranch<string, RouteObjectType>(branches[i], decoded, allowPartial);
1010
+ }
1011
+
1012
+ return matches;
1013
+ }
1014
+
1015
+ export interface UIMatch<Data = unknown, Handle = unknown> {
1016
+ id: string;
1017
+ pathname: string;
1018
+ /**
1019
+ * {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the matched route.
1020
+ */
1021
+ params: RouteMatch['params'];
1022
+ /**
1023
+ * The return value from the matched route's loader or clientLoader. This might
1024
+ * be `undefined` if this route's `loader` (or a deeper route's `loader`) threw
1025
+ * an error and we're currently displaying an `ErrorBoundary`.
1026
+ *
1027
+ * @deprecated Use `UIMatch.loaderData` instead
1028
+ */
1029
+ data: Data | undefined;
1030
+ /**
1031
+ * The return value from the matched route's loader or clientLoader. This might
1032
+ * be `undefined` if this route's `loader` (or a deeper route's `loader`) threw
1033
+ * an error and we're currently displaying an `ErrorBoundary`.
1034
+ */
1035
+ loaderData: Data | undefined;
1036
+ /**
1037
+ * The {@link https://reactrouter.com/start/framework/route-module#handle handle object}
1038
+ * exported from the matched route module
1039
+ */
1040
+ handle: Handle;
1041
+ }
1042
+
1043
+ export function convertRouteMatchToUiMatch(match: DataRouteMatch, loaderData: RouteData): UIMatch {
1044
+ let { route, pathname, params } = match;
1045
+ return {
1046
+ id: route.id,
1047
+ pathname,
1048
+ params,
1049
+ data: loaderData[route.id],
1050
+ loaderData: loaderData[route.id],
1051
+ handle: route.handle,
1052
+ };
1053
+ }
1054
+
1055
+ interface RouteMeta<RouteObjectType extends RouteObject = RouteObject> {
1056
+ relativePath: string;
1057
+ caseSensitive: boolean;
1058
+ childrenIndex: number;
1059
+ route: RouteObjectType;
1060
+ matcher?: RegExp;
1061
+ compiledParams?: CompiledPathParam[];
1062
+ }
1063
+
1064
+ /**
1065
+ * @private
1066
+ * PRIVATE - DO NOT USE
1067
+ *
1068
+ * A "branch" of routes that match a given route pattern.
1069
+ * This is an internal interface not intended for direct external usage.
1070
+ */
1071
+ export interface RouteBranch<RouteObjectType extends RouteObject = RouteObject> {
1072
+ path: string;
1073
+ score: number;
1074
+ routesMeta: RouteMeta<RouteObjectType>[];
1075
+ }
1076
+
1077
+ export function flattenAndRankRoutes<RouteObjectType extends RouteObject = RouteObject>(
1078
+ routes: RouteObjectType[],
1079
+ ): RouteBranch<RouteObjectType>[] {
1080
+ let branches = flattenRoutes(routes);
1081
+ rankRouteBranches(branches);
1082
+ return branches;
1083
+ }
1084
+
1085
+ function flattenRoutes<RouteObjectType extends RouteObject = RouteObject>(
1086
+ routes: RouteObjectType[],
1087
+ branches: RouteBranch<RouteObjectType>[] = [],
1088
+ parentsMeta: RouteMeta<RouteObjectType>[] = [],
1089
+ parentPath = '',
1090
+ _hasParentOptionalSegments = false,
1091
+ ): RouteBranch<RouteObjectType>[] {
1092
+ let flattenRoute = (
1093
+ route: RouteObjectType,
1094
+ index: number,
1095
+ hasParentOptionalSegments = _hasParentOptionalSegments,
1096
+ relativePath?: string,
1097
+ ) => {
1098
+ let meta: RouteMeta<RouteObjectType> = {
1099
+ relativePath: relativePath === undefined ? route.path || '' : relativePath,
1100
+ caseSensitive: route.caseSensitive === true,
1101
+ childrenIndex: index,
1102
+ route,
1103
+ };
1104
+
1105
+ if (meta.relativePath.startsWith('/')) {
1106
+ if (!meta.relativePath.startsWith(parentPath) && hasParentOptionalSegments) {
1107
+ // If we're inside of a parent route that has optional segments, we don't
1108
+ // want to throw a hard error here because due to the route exploding
1109
+ // approach, some of the routes won't match by design and we can just
1110
+ // discard them instead.
1111
+ // https://github.com/remix-run/react-router/issues/9925#issuecomment-1387252214
1112
+ return;
1113
+ }
1114
+ invariant(
1115
+ meta.relativePath.startsWith(parentPath),
1116
+ `Absolute route path "${meta.relativePath}" nested under path ` +
1117
+ `"${parentPath}" is not valid. An absolute child route path ` +
1118
+ `must start with the combined path of all its parent routes.`,
1119
+ );
1120
+
1121
+ meta.relativePath = meta.relativePath.slice(parentPath.length);
1122
+ }
1123
+
1124
+ let path = joinPaths([parentPath, meta.relativePath]);
1125
+ let routesMeta = parentsMeta.concat(meta);
1126
+
1127
+ // Add the children before adding this route to the array, so we traverse the
1128
+ // route tree depth-first and child routes appear before their parents in
1129
+ // the "flattened" version.
1130
+ if (route.children && route.children.length > 0) {
1131
+ invariant(
1132
+ // Our types know better, but runtime JS may not!
1133
+ // @ts-expect-error
1134
+ route.index !== true,
1135
+ `Index routes must not have child routes. Please remove ` +
1136
+ `all child routes from route path "${path}".`,
1137
+ );
1138
+ flattenRoutes(route.children, branches, routesMeta, path, hasParentOptionalSegments);
1139
+ }
1140
+
1141
+ // Routes without a path shouldn't ever match by themselves unless they are
1142
+ // index routes, so don't add them to the list of possible branches.
1143
+ if (route.path == null && !route.index) {
1144
+ return;
1145
+ }
1146
+
1147
+ branches.push({
1148
+ path,
1149
+ score: computeScore(path, route.index),
1150
+ routesMeta: routesMeta.map((meta, i) => {
1151
+ let [matcher, params] = compilePath(
1152
+ meta.relativePath,
1153
+ meta.caseSensitive,
1154
+ i === routesMeta.length - 1,
1155
+ );
1156
+ return {
1157
+ ...meta,
1158
+ matcher,
1159
+ compiledParams: params,
1160
+ } satisfies RouteMeta<RouteObjectType>;
1161
+ }),
1162
+ });
1163
+ };
1164
+
1165
+ routes.forEach((route, index) => {
1166
+ // coarse-grain check for optional params
1167
+ if (route.path === '' || !route.path?.includes('?')) {
1168
+ flattenRoute(route, index);
1169
+ } else {
1170
+ for (let exploded of explodeOptionalSegments(route.path)) {
1171
+ flattenRoute(route, index, true, exploded);
1172
+ }
1173
+ }
1174
+ });
1175
+
1176
+ return branches;
1177
+ }
1178
+
1179
+ /*
1180
+ * Computes all combinations of optional path segments for a given path,
1181
+ * excluding combinations that are ambiguous and of lower priority.
1182
+ *
1183
+ * For example, `/one/:two?/three/:four?/:five?` explodes to:
1184
+ * - `/one/three`
1185
+ * - `/one/:two/three`
1186
+ * - `/one/three/:four`
1187
+ * - `/one/three/:five`
1188
+ * - `/one/:two/three/:four`
1189
+ * - `/one/:two/three/:five`
1190
+ * - `/one/three/:four/:five`
1191
+ * - `/one/:two/three/:four/:five`
1192
+ */
1193
+ function explodeOptionalSegments(path: string): string[] {
1194
+ let segments = path.split('/');
1195
+ if (segments.length === 0) return [];
1196
+
1197
+ let [first, ...rest] = segments;
1198
+
1199
+ // Optional path segments are denoted by a trailing `?`
1200
+ let isOptional = first.endsWith('?');
1201
+ // Compute the corresponding required segment: `foo?` -> `foo`
1202
+ let required = first.replace(/\?$/, '');
1203
+
1204
+ if (rest.length === 0) {
1205
+ // Interpret empty string as omitting an optional segment
1206
+ // `["one", "", "three"]` corresponds to omitting `:two` from `/one/:two?/three` -> `/one/three`
1207
+ return isOptional ? [required, ''] : [required];
1208
+ }
1209
+
1210
+ let restExploded = explodeOptionalSegments(rest.join('/'));
1211
+
1212
+ let result: string[] = [];
1213
+
1214
+ // All child paths with the prefix. Do this for all children before the
1215
+ // optional version for all children, so we get consistent ordering where the
1216
+ // parent optional aspect is preferred as required. Otherwise, we can get
1217
+ // child sections interspersed where deeper optional segments are higher than
1218
+ // parent optional segments, where for example, /:two would explode _earlier_
1219
+ // then /:one. By always including the parent as required _for all children_
1220
+ // first, we avoid this issue
1221
+ result.push(
1222
+ ...restExploded.map((subpath) => (subpath === '' ? required : [required, subpath].join('/'))),
1223
+ );
1224
+
1225
+ // Then, if this is an optional value, add all child versions without
1226
+ if (isOptional) {
1227
+ result.push(...restExploded);
1228
+ }
1229
+
1230
+ // for absolute paths, ensure `/` instead of empty segment
1231
+ return result.map((exploded) => (path.startsWith('/') && exploded === '' ? '/' : exploded));
1232
+ }
1233
+
1234
+ function rankRouteBranches(branches: RouteBranch[]): void {
1235
+ branches.sort((a, b) =>
1236
+ a.score !== b.score
1237
+ ? b.score - a.score // Higher score first
1238
+ : compareIndexes(
1239
+ a.routesMeta.map((meta) => meta.childrenIndex),
1240
+ b.routesMeta.map((meta) => meta.childrenIndex),
1241
+ ),
1242
+ );
1243
+ }
1244
+
1245
+ const paramRe = /^:[\w-]+$/;
1246
+ const dynamicSegmentValue = 3;
1247
+ const indexRouteValue = 2;
1248
+ const emptySegmentValue = 1;
1249
+ const staticSegmentValue = 10;
1250
+ const splatPenalty = -2;
1251
+ const isSplat = (s: string) => s === '*';
1252
+
1253
+ function computeScore(path: string, index: boolean | undefined): number {
1254
+ let segments = path.split('/');
1255
+ let initialScore = segments.length;
1256
+ if (segments.some(isSplat)) {
1257
+ initialScore += splatPenalty;
1258
+ }
1259
+
1260
+ if (index) {
1261
+ initialScore += indexRouteValue;
1262
+ }
1263
+
1264
+ return segments
1265
+ .filter((s) => !isSplat(s))
1266
+ .reduce(
1267
+ (score, segment) =>
1268
+ score +
1269
+ (paramRe.test(segment)
1270
+ ? dynamicSegmentValue
1271
+ : segment === ''
1272
+ ? emptySegmentValue
1273
+ : staticSegmentValue),
1274
+ initialScore,
1275
+ );
1276
+ }
1277
+
1278
+ function compareIndexes(a: number[], b: number[]): number {
1279
+ let siblings = a.length === b.length && a.slice(0, -1).every((n, i) => n === b[i]);
1280
+
1281
+ return siblings
1282
+ ? // If two routes are siblings, we should try to match the earlier sibling
1283
+ // first. This allows people to have fine-grained control over the matching
1284
+ // behavior by simply putting routes with identical paths in the order they
1285
+ // want them tried.
1286
+ a[a.length - 1] - b[b.length - 1]
1287
+ : // Otherwise, it doesn't really make sense to rank non-siblings by index,
1288
+ // so they sort equally.
1289
+ 0;
1290
+ }
1291
+
1292
+ function matchRouteBranch<
1293
+ ParamKey extends string = string,
1294
+ RouteObjectType extends RouteObject = RouteObject,
1295
+ >(
1296
+ branch: RouteBranch<RouteObjectType>,
1297
+ pathname: string,
1298
+ allowPartial = false,
1299
+ ): RouteMatch<ParamKey, RouteObjectType>[] | null {
1300
+ let { routesMeta } = branch;
1301
+
1302
+ let matchedParams = {};
1303
+ let matchedPathname = '/';
1304
+ let matches: RouteMatch<ParamKey, RouteObjectType>[] = [];
1305
+ for (let i = 0; i < routesMeta.length; ++i) {
1306
+ let meta = routesMeta[i];
1307
+ let end = i === routesMeta.length - 1;
1308
+ let remainingPathname =
1309
+ matchedPathname === '/' ? pathname : pathname.slice(matchedPathname.length) || '/';
1310
+ let pattern = {
1311
+ path: meta.relativePath,
1312
+ caseSensitive: meta.caseSensitive,
1313
+ end,
1314
+ };
1315
+ let match =
1316
+ // Use precomputed matcher if it exists
1317
+ meta.matcher && meta.compiledParams
1318
+ ? matchPathImpl(pattern, remainingPathname, meta.matcher, meta.compiledParams)
1319
+ : matchPath(pattern, remainingPathname);
1320
+
1321
+ let route = meta.route;
1322
+
1323
+ if (!match && end && allowPartial && !routesMeta[routesMeta.length - 1].route.index) {
1324
+ match = matchPath(
1325
+ {
1326
+ path: meta.relativePath,
1327
+ caseSensitive: meta.caseSensitive,
1328
+ end: false,
1329
+ },
1330
+ remainingPathname,
1331
+ );
1332
+ }
1333
+
1334
+ if (!match) {
1335
+ return null;
1336
+ }
1337
+
1338
+ Object.assign(matchedParams, match.params);
1339
+
1340
+ matches.push({
1341
+ // TODO: Can this as be avoided?
1342
+ params: matchedParams as Params<ParamKey>,
1343
+ pathname: joinPaths([matchedPathname, match.pathname]),
1344
+ pathnameBase: normalizePathname(joinPaths([matchedPathname, match.pathnameBase])),
1345
+ route,
1346
+ });
1347
+
1348
+ if (match.pathnameBase !== '/') {
1349
+ matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);
1350
+ }
1351
+ }
1352
+
1353
+ return matches;
1354
+ }
1355
+
1356
+ /**
1357
+ * Returns a path with params interpolated.
1358
+ *
1359
+ * @example
1360
+ * import { generatePath } from "react-router";
1361
+ *
1362
+ * generatePath("/users/:id", { id: "123" }); // "/users/123"
1363
+ *
1364
+ * @public
1365
+ * @category Utils
1366
+ * @param originalPath The original path to generate.
1367
+ * @param params The parameters to interpolate into the path.
1368
+ * @returns The generated path with parameters interpolated.
1369
+ */
1370
+ export function generatePath<Path extends string>(
1371
+ originalPath: Path,
1372
+ params: GeneratePathParams<Path> = {} as any,
1373
+ ): string {
1374
+ let path: string = originalPath;
1375
+ if (path.endsWith('*') && path !== '*' && !path.endsWith('/*')) {
1376
+ warning(
1377
+ false,
1378
+ `Route path "${path}" will be treated as if it were ` +
1379
+ `"${path.replace(/\*$/, '/*')}" because the \`*\` character must ` +
1380
+ `always follow a \`/\` in the pattern. To get rid of this warning, ` +
1381
+ `please change the route path to "${path.replace(/\*$/, '/*')}".`,
1382
+ );
1383
+ path = path.replace(/\*$/, '/*') as Path;
1384
+ }
1385
+
1386
+ // ensure `/` is added at the beginning if the path is absolute
1387
+ const prefix = path.startsWith('/') ? '/' : '';
1388
+
1389
+ const stringify = (p: any) => (p == null ? '' : typeof p === 'string' ? p : String(p));
1390
+
1391
+ const segments = path
1392
+ .split(/\/+/)
1393
+ .map((segment, index, array) => {
1394
+ const isLastSegment = index === array.length - 1;
1395
+
1396
+ // only apply the splat if it's the last segment
1397
+ if (isLastSegment && segment === '*') {
1398
+ // Apply the splat
1399
+ return stringify(params['*' as keyof typeof params]);
1400
+ }
1401
+
1402
+ const keyMatch = segment.match(/^:([\w-]+)(\??)(.*)/);
1403
+ if (keyMatch) {
1404
+ const [, key, optional, suffix] = keyMatch;
1405
+ let param = params[key as keyof typeof params];
1406
+ invariant(optional === '?' || param != null, `Missing ":${key}" param`);
1407
+ return encodeURIComponent(stringify(param)) + suffix;
1408
+ }
1409
+
1410
+ // Remove any optional markers from optional static segments
1411
+ return segment.replace(/\?$/g, '');
1412
+ })
1413
+ // Remove empty segments
1414
+ .filter((segment) => !!segment);
1415
+
1416
+ return prefix + segments.join('/');
1417
+ }
1418
+
1419
+ /**
1420
+ * Used to match on some portion of a URL pathname.
1421
+ */
1422
+ export interface PathPattern<Path extends string = string> {
1423
+ /**
1424
+ * A string to match against a URL pathname. May contain `:id`-style segments
1425
+ * to indicate placeholders for dynamic parameters. It May also end with `/*`
1426
+ * to indicate matching the rest of the URL pathname.
1427
+ */
1428
+ path: Path;
1429
+ /**
1430
+ * Should be `true` if the static portions of the `path` should be matched in
1431
+ * the same case.
1432
+ */
1433
+ caseSensitive?: boolean;
1434
+ /**
1435
+ * Should be `true` if this pattern should match the entire URL pathname.
1436
+ */
1437
+ end?: boolean;
1438
+ }
1439
+
1440
+ /**
1441
+ * Contains info about how a {@link PathPattern} matched on a URL pathname.
1442
+ */
1443
+ export interface PathMatch<ParamKey extends string = string> {
1444
+ /**
1445
+ * The names and values of dynamic parameters in the URL.
1446
+ */
1447
+ params: Params<ParamKey>;
1448
+ /**
1449
+ * The portion of the URL pathname that was matched.
1450
+ */
1451
+ pathname: string;
1452
+ /**
1453
+ * The portion of the URL pathname that was matched before child routes.
1454
+ */
1455
+ pathnameBase: string;
1456
+ /**
1457
+ * The pattern that was used to match.
1458
+ */
1459
+ pattern: PathPattern;
1460
+ }
1461
+
1462
+ type Mutable<T> = {
1463
+ -readonly [P in keyof T]: T[P];
1464
+ };
1465
+
1466
+ /**
1467
+ * Performs pattern matching on a URL pathname and returns information about
1468
+ * the match.
1469
+ *
1470
+ * @public
1471
+ * @category Utils
1472
+ * @param pattern The pattern to match against the URL pathname. This can be a
1473
+ * string or a {@link PathPattern} object. If a string is provided, it will be
1474
+ * treated as a pattern with `caseSensitive` set to `false` and `end` set to
1475
+ * `true`.
1476
+ * @param pathname The URL pathname to match against the pattern.
1477
+ * @returns A path match object if the pattern matches the pathname,
1478
+ * or `null` if it does not match.
1479
+ */
1480
+ export function matchPath<Path extends string>(
1481
+ pattern: PathPattern<Path> | Path,
1482
+ pathname: string,
1483
+ ): PathMatch<ParamParseKey<Path>> | null {
1484
+ if (typeof pattern === 'string') {
1485
+ pattern = { path: pattern, caseSensitive: false, end: true };
1486
+ }
1487
+
1488
+ let [matcher, compiledParams] = compilePath(pattern.path, pattern.caseSensitive, pattern.end);
1489
+
1490
+ return matchPathImpl(pattern, pathname, matcher, compiledParams);
1491
+ }
1492
+
1493
+ function matchPathImpl<Path extends string>(
1494
+ pattern: PathPattern<Path>,
1495
+ pathname: string,
1496
+ matcher: RegExp,
1497
+ compiledParams: CompiledPathParam[],
1498
+ ): PathMatch<ParamParseKey<Path>> | null {
1499
+ let match = pathname.match(matcher);
1500
+ if (!match) return null;
1501
+
1502
+ let matchedPathname = match[0];
1503
+ let pathnameBase = matchedPathname.replace(/(.)\/+$/, '$1');
1504
+ let captureGroups = match.slice(1);
1505
+ let params: Params = compiledParams.reduce<Mutable<Params>>(
1506
+ (memo, { paramName, isOptional }, index) => {
1507
+ // We need to compute the pathnameBase here using the raw splat value
1508
+ // instead of using params["*"] later because it will be decoded then
1509
+ if (paramName === '*') {
1510
+ let splatValue = captureGroups[index] || '';
1511
+ pathnameBase = matchedPathname
1512
+ .slice(0, matchedPathname.length - splatValue.length)
1513
+ .replace(/(.)\/+$/, '$1');
1514
+ }
1515
+
1516
+ const value = captureGroups[index];
1517
+ if (isOptional && !value) {
1518
+ memo[paramName] = undefined;
1519
+ } else {
1520
+ memo[paramName] = (value || '').replace(/%2F/g, '/');
1521
+ }
1522
+ return memo;
1523
+ },
1524
+ {},
1525
+ );
1526
+
1527
+ return {
1528
+ params,
1529
+ pathname: matchedPathname,
1530
+ pathnameBase,
1531
+ pattern,
1532
+ };
1533
+ }
1534
+
1535
+ type CompiledPathParam = { paramName: string; isOptional?: boolean };
1536
+
1537
+ export function compilePath(
1538
+ path: string,
1539
+ caseSensitive = false,
1540
+ end = true,
1541
+ ): [RegExp, CompiledPathParam[]] {
1542
+ warning(
1543
+ path === '*' || !path.endsWith('*') || path.endsWith('/*'),
1544
+ `Route path "${path}" will be treated as if it were ` +
1545
+ `"${path.replace(/\*$/, '/*')}" because the \`*\` character must ` +
1546
+ `always follow a \`/\` in the pattern. To get rid of this warning, ` +
1547
+ `please change the route path to "${path.replace(/\*$/, '/*')}".`,
1548
+ );
1549
+
1550
+ let params: CompiledPathParam[] = [];
1551
+ let regexpSource =
1552
+ '^' +
1553
+ path
1554
+ .replace(/\/*\*?$/, '') // Ignore trailing / and /*, we'll handle it below
1555
+ .replace(/^\/*/, '/') // Make sure it has a leading /
1556
+ .replace(/[\\.*+^${}|()[\]]/g, '\\$&') // Escape special regex chars
1557
+ .replace(
1558
+ /\/:([\w-]+)(\?)?/g,
1559
+ (
1560
+ match: string,
1561
+ paramName: string,
1562
+ isOptional: string | undefined,
1563
+ index: number,
1564
+ str: string,
1565
+ ) => {
1566
+ params.push({ paramName, isOptional: isOptional != null });
1567
+
1568
+ if (isOptional) {
1569
+ let nextChar = str.charAt(index + match.length);
1570
+ if (nextChar && nextChar !== '/') {
1571
+ return '/([^\\/]*)';
1572
+ }
1573
+
1574
+ return '(?:/([^\\/]*))?';
1575
+ }
1576
+
1577
+ return '/([^\\/]+)';
1578
+ },
1579
+ ) // Dynamic segment
1580
+ .replace(/\/([\w-]+)\?(\/|$)/g, '(/$1)?$2'); // Optional static segment
1581
+
1582
+ if (path.endsWith('*')) {
1583
+ params.push({ paramName: '*' });
1584
+ regexpSource +=
1585
+ path === '*' || path === '/*'
1586
+ ? '(.*)$' // Already matched the initial /, just match the rest
1587
+ : '(?:\\/(.+)|\\/*)$'; // Don't include the / in params["*"]
1588
+ } else if (end) {
1589
+ // When matching to the end, ignore trailing slashes
1590
+ regexpSource += '\\/*$';
1591
+ } else if (path !== '' && path !== '/') {
1592
+ // If our path is non-empty and contains anything beyond an initial slash,
1593
+ // then we have _some_ form of path in our regex, so we should expect to
1594
+ // match only if we find the end of this path segment. Look for an optional
1595
+ // non-captured trailing slash (to match a portion of the URL) or the end
1596
+ // of the path (if we've matched to the end). We used to do this with a
1597
+ // word boundary but that gives false positives on routes like
1598
+ // /user-preferences since `-` counts as a word boundary.
1599
+ regexpSource += '(?:(?=\\/|$))';
1600
+ } else {
1601
+ // Nothing to match for "" or "/"
1602
+ }
1603
+
1604
+ let matcher = new RegExp(regexpSource, caseSensitive ? undefined : 'i');
1605
+
1606
+ return [matcher, params];
1607
+ }
1608
+
1609
+ export function decodePath(value: string) {
1610
+ try {
1611
+ return value
1612
+ .split('/')
1613
+ .map((v) => decodeURIComponent(v).replace(/\//g, '%2F'))
1614
+ .join('/');
1615
+ } catch (error) {
1616
+ warning(
1617
+ false,
1618
+ `The URL path "${value}" could not be decoded because it is a ` +
1619
+ `malformed URL segment. This is probably due to a bad percent ` +
1620
+ `encoding (${error}).`,
1621
+ );
1622
+
1623
+ return value;
1624
+ }
1625
+ }
1626
+
1627
+ export function stripBasename(pathname: string, basename: string): string | null {
1628
+ if (basename === '/') return pathname;
1629
+
1630
+ if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {
1631
+ return null;
1632
+ }
1633
+
1634
+ // We want to leave trailing slash behavior in the user's control, so if they
1635
+ // specify a basename with a trailing slash, we should support it
1636
+ let startIndex = basename.endsWith('/') ? basename.length - 1 : basename.length;
1637
+ let nextChar = pathname.charAt(startIndex);
1638
+ if (nextChar && nextChar !== '/') {
1639
+ // pathname does not start with basename/
1640
+ return null;
1641
+ }
1642
+
1643
+ return pathname.slice(startIndex) || '/';
1644
+ }
1645
+
1646
+ export function prependBasename({
1647
+ basename,
1648
+ pathname,
1649
+ }: {
1650
+ basename: string;
1651
+ pathname: string;
1652
+ }): string {
1653
+ // If this is a root navigation, then just use the raw basename which allows
1654
+ // the basename to have full control over the presence of a trailing slash on
1655
+ // root actions
1656
+ return pathname === '/' ? basename : joinPaths([basename, pathname]);
1657
+ }
1658
+
1659
+ export const isAbsoluteUrl = (url: string) => ABSOLUTE_URL_REGEX.test(url);
1660
+
1661
+ /**
1662
+ * Returns a resolved {@link Path} object relative to the given pathname.
1663
+ *
1664
+ * @public
1665
+ * @category Utils
1666
+ * @param to The path to resolve, either a string or a partial {@link Path}
1667
+ * object.
1668
+ * @param fromPathname The pathname to resolve the path from. Defaults to `/`.
1669
+ * @returns A {@link Path} object with the resolved pathname, search, and hash.
1670
+ */
1671
+ export function resolvePath(to: To, fromPathname = '/'): Path {
1672
+ let {
1673
+ pathname: toPathname,
1674
+ search = '',
1675
+ hash = '',
1676
+ } = typeof to === 'string' ? parsePath(to) : to;
1677
+
1678
+ let pathname: string;
1679
+ if (toPathname) {
1680
+ toPathname = removeDoubleSlashes(toPathname);
1681
+ if (toPathname.startsWith('/')) {
1682
+ pathname = resolvePathname(toPathname.substring(1), '/');
1683
+ } else {
1684
+ pathname = resolvePathname(toPathname, fromPathname);
1685
+ }
1686
+ } else {
1687
+ pathname = fromPathname;
1688
+ }
1689
+
1690
+ return {
1691
+ pathname,
1692
+ search: normalizeSearch(search),
1693
+ hash: normalizeHash(hash),
1694
+ };
1695
+ }
1696
+
1697
+ function resolvePathname(relativePath: string, fromPathname: string): string {
1698
+ let segments = removeTrailingSlash(fromPathname).split('/');
1699
+ let relativeSegments = relativePath.split('/');
1700
+
1701
+ relativeSegments.forEach((segment) => {
1702
+ if (segment === '..') {
1703
+ // Keep the root "" segment so the pathname starts at /
1704
+ if (segments.length > 1) segments.pop();
1705
+ } else if (segment !== '.') {
1706
+ segments.push(segment);
1707
+ }
1708
+ });
1709
+
1710
+ return segments.length > 1 ? segments.join('/') : '/';
1711
+ }
1712
+
1713
+ function getInvalidPathError(char: string, field: string, dest: string, path: Partial<Path>) {
1714
+ return (
1715
+ `Cannot include a '${char}' character in a manually specified ` +
1716
+ `\`to.${field}\` field [${JSON.stringify(path)}]. Please separate it out to the ` +
1717
+ `\`to.${dest}\` field. Alternatively you may provide the full path as ` +
1718
+ `a string in <Link to="..."> and the router will parse it for you.`
1719
+ );
1720
+ }
1721
+
1722
+ // When processing relative navigation we want to ignore ancestor routes that
1723
+ // do not contribute to the path, such that index/pathless layout routes don't
1724
+ // interfere.
1725
+ //
1726
+ // For example, when moving a route element into an index route and/or a
1727
+ // pathless layout route, relative link behavior contained within should stay
1728
+ // the same. Both of the following examples should link back to the root:
1729
+ //
1730
+ // <Route path="/">
1731
+ // <Route path="accounts" element={<Link to=".."}>
1732
+ // </Route>
1733
+ //
1734
+ // <Route path="/">
1735
+ // <Route path="accounts">
1736
+ // <Route element={<AccountsLayout />}> // <-- Does not contribute
1737
+ // <Route index element={<Link to=".."} /> // <-- Does not contribute
1738
+ // </Route
1739
+ // </Route>
1740
+ // </Route>
1741
+ export function getPathContributingMatches<T extends RouteMatch = RouteMatch>(matches: T[]) {
1742
+ return matches.filter(
1743
+ (match, index) => index === 0 || (match.route.path && match.route.path.length > 0),
1744
+ );
1745
+ }
1746
+
1747
+ // Return the array of pathnames for the current route matches - used to
1748
+ // generate the routePathnames input for resolveTo()
1749
+ export function getResolveToMatches<T extends RouteMatch = RouteMatch>(matches: T[]) {
1750
+ let pathMatches = getPathContributingMatches(matches);
1751
+
1752
+ // Use the full pathname for the leaf match so we include splat values for "." links
1753
+ // https://github.com/remix-run/react-router/issues/11052#issuecomment-1836589329
1754
+ return pathMatches.map((match, idx) =>
1755
+ idx === pathMatches.length - 1 ? match.pathname : match.pathnameBase,
1756
+ );
1757
+ }
1758
+
1759
+ export function resolveTo(
1760
+ toArg: To,
1761
+ routePathnames: string[],
1762
+ locationPathname: string,
1763
+ isPathRelative = false,
1764
+ ): Path {
1765
+ let to: Partial<Path>;
1766
+ if (typeof toArg === 'string') {
1767
+ to = parsePath(toArg);
1768
+ } else {
1769
+ to = { ...toArg };
1770
+
1771
+ invariant(
1772
+ !to.pathname || !to.pathname.includes('?'),
1773
+ getInvalidPathError('?', 'pathname', 'search', to),
1774
+ );
1775
+ invariant(
1776
+ !to.pathname || !to.pathname.includes('#'),
1777
+ getInvalidPathError('#', 'pathname', 'hash', to),
1778
+ );
1779
+ invariant(
1780
+ !to.search || !to.search.includes('#'),
1781
+ getInvalidPathError('#', 'search', 'hash', to),
1782
+ );
1783
+ }
1784
+
1785
+ let isEmptyPath = toArg === '' || to.pathname === '';
1786
+ let toPathname = isEmptyPath ? '/' : to.pathname;
1787
+
1788
+ let from: string;
1789
+
1790
+ // Routing is relative to the current pathname if explicitly requested.
1791
+ //
1792
+ // If a pathname is explicitly provided in `to`, it should be relative to the
1793
+ // route context. This is explained in `Note on `<Link to>` values` in our
1794
+ // migration guide from v5 as a means of disambiguation between `to` values
1795
+ // that begin with `/` and those that do not. However, this is problematic for
1796
+ // `to` values that do not provide a pathname. `to` can simply be a search or
1797
+ // hash string, in which case we should assume that the navigation is relative
1798
+ // to the current location's pathname and *not* the route pathname.
1799
+ if (toPathname == null) {
1800
+ from = locationPathname;
1801
+ } else {
1802
+ let routePathnameIndex = routePathnames.length - 1;
1803
+
1804
+ // With relative="route" (the default), each leading .. segment means
1805
+ // "go up one route" instead of "go up one URL segment". This is a key
1806
+ // difference from how <a href> works and a major reason we call this a
1807
+ // "to" value instead of a "href".
1808
+ if (!isPathRelative && toPathname.startsWith('..')) {
1809
+ let toSegments = toPathname.split('/');
1810
+
1811
+ while (toSegments[0] === '..') {
1812
+ toSegments.shift();
1813
+ routePathnameIndex -= 1;
1814
+ }
1815
+
1816
+ to.pathname = toSegments.join('/');
1817
+ }
1818
+
1819
+ from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : '/';
1820
+ }
1821
+
1822
+ let path = resolvePath(to, from);
1823
+
1824
+ // Ensure the pathname has a trailing slash if the original "to" had one
1825
+ let hasExplicitTrailingSlash = toPathname && toPathname !== '/' && toPathname.endsWith('/');
1826
+ // Or if this was a link to the current path which has a trailing slash
1827
+ let hasCurrentTrailingSlash =
1828
+ (isEmptyPath || toPathname === '.') && locationPathname.endsWith('/');
1829
+ if (!path.pathname.endsWith('/') && (hasExplicitTrailingSlash || hasCurrentTrailingSlash)) {
1830
+ path.pathname += '/';
1831
+ }
1832
+
1833
+ return path;
1834
+ }
1835
+
1836
+ export const removeDoubleSlashes = (path: string): string => path.replace(/[\\/]{2,}/g, '/');
1837
+
1838
+ export const joinPaths = (paths: string[]): string => removeDoubleSlashes(paths.join('/'));
1839
+
1840
+ export const removeTrailingSlash = (path: string): string => path.replace(/\/+$/, '');
1841
+
1842
+ export const normalizePathname = (pathname: string): string =>
1843
+ removeTrailingSlash(pathname).replace(/^\/*/, '/');
1844
+
1845
+ /*
1846
+ lol - this comment is needed because the JSDoc parser for docs.ts gets confused
1847
+ by the star-slash in the normalizePathname regex and messes up the parsed comment
1848
+ for data() below. This comment seems to reset the parser.
1849
+ */
1850
+
1851
+ export const normalizeSearch = (search: string): string =>
1852
+ !search || search === '?' ? '' : search.startsWith('?') ? search : '?' + search;
1853
+
1854
+ export const normalizeHash = (hash: string): string =>
1855
+ !hash || hash === '#' ? '' : hash.startsWith('#') ? hash : '#' + hash;
1856
+
1857
+ export class DataWithResponseInit<D> {
1858
+ type: string = 'DataWithResponseInit';
1859
+ data: D;
1860
+ init: ResponseInit | null;
1861
+
1862
+ constructor(data: D, init?: ResponseInit) {
1863
+ this.data = data;
1864
+ this.init = init || null;
1865
+ }
1866
+ }
1867
+
1868
+ /**
1869
+ * Create "responses" that contain `headers`/`status` without forcing
1870
+ * serialization into an actual [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
1871
+ *
1872
+ * @example
1873
+ * import { data } from "react-router";
1874
+ *
1875
+ * export async function action({ request }: Route.ActionArgs) {
1876
+ * let formData = await request.formData();
1877
+ * let item = await createItem(formData);
1878
+ * return data(item, {
1879
+ * headers: { "X-Custom-Header": "value" }
1880
+ * status: 201,
1881
+ * });
1882
+ * }
1883
+ *
1884
+ * @public
1885
+ * @category Utils
1886
+ * @mode framework
1887
+ * @mode data
1888
+ * @param data The data to be included in the response.
1889
+ * @param init The status code or a `ResponseInit` object to be included in the
1890
+ * response.
1891
+ * @returns A {@link DataWithResponseInit} instance containing the data and
1892
+ * response init.
1893
+ */
1894
+ export function data<D>(data: D, init?: number | ResponseInit) {
1895
+ return new DataWithResponseInit(data, typeof init === 'number' ? { status: init } : init);
1896
+ }
1897
+
1898
+ // This is now only used by the Await component and will eventually probably
1899
+ // go away in favor of the format used by `React.use`
1900
+ export interface TrackedPromise extends Promise<any> {
1901
+ _tracked?: boolean;
1902
+ _data?: any;
1903
+ _error?: any;
1904
+ }
1905
+
1906
+ export type RedirectFunction = (url: string, init?: number | ResponseInit) => Response;
1907
+
1908
+ /**
1909
+ * A redirect [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response).
1910
+ * Sets the status code and the [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)
1911
+ * header. Defaults to [`302 Found`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302).
1912
+ *
1913
+ * This utility accepts absolute URLs and can navigate to external domains, so
1914
+ * the application should validate any user-supplied inputs to redirects.
1915
+ *
1916
+ * @example
1917
+ * import { redirect } from "react-router";
1918
+ *
1919
+ * export async function loader({ request }: Route.LoaderArgs) {
1920
+ * if (!isLoggedIn(request))
1921
+ * throw redirect("/login");
1922
+ * }
1923
+ *
1924
+ * // ...
1925
+ * }
1926
+ *
1927
+ * @public
1928
+ * @category Utils
1929
+ * @mode framework
1930
+ * @mode data
1931
+ * @param url The URL to redirect to.
1932
+ * @param init The status code or a `ResponseInit` object to be included in the
1933
+ * response.
1934
+ * @returns A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
1935
+ * object with the redirect status and [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)
1936
+ * header.
1937
+ */
1938
+ export const redirect: RedirectFunction = (url, init = 302) => {
1939
+ let responseInit = init;
1940
+ if (typeof responseInit === 'number') {
1941
+ responseInit = { status: responseInit };
1942
+ } else if (typeof responseInit.status === 'undefined') {
1943
+ responseInit.status = 302;
1944
+ }
1945
+
1946
+ let headers = new Headers(responseInit.headers);
1947
+ headers.set('Location', url);
1948
+
1949
+ return new Response(null, { ...responseInit, headers });
1950
+ };
1951
+
1952
+ /**
1953
+ * A redirect [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
1954
+ * that will force a document reload to the new location. Sets the status code
1955
+ * and the [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)
1956
+ * header. Defaults to [`302 Found`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302).
1957
+ *
1958
+ * This utility accepts absolute URLs and can navigate to external domains, so
1959
+ * the application should validate any user-supplied inputs to redirects.
1960
+ *
1961
+ * ```tsx filename=routes/logout.tsx
1962
+ * import { redirectDocument } from "react-router";
1963
+ *
1964
+ * import { destroySession } from "../sessions.server";
1965
+ *
1966
+ * export async function action({ request }: Route.ActionArgs) {
1967
+ * let session = await getSession(request.headers.get("Cookie"));
1968
+ * return redirectDocument("/", {
1969
+ * headers: { "Set-Cookie": await destroySession(session) }
1970
+ * });
1971
+ * }
1972
+ * ```
1973
+ *
1974
+ * @public
1975
+ * @category Utils
1976
+ * @mode framework
1977
+ * @mode data
1978
+ * @param url The URL to redirect to.
1979
+ * @param init The status code or a `ResponseInit` object to be included in the
1980
+ * response.
1981
+ * @returns A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
1982
+ * object with the redirect status and [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)
1983
+ * header.
1984
+ */
1985
+ export const redirectDocument: RedirectFunction = (url, init) => {
1986
+ let response = redirect(url, init);
1987
+ response.headers.set('X-Remix-Reload-Document', 'true');
1988
+ return response;
1989
+ };
1990
+
1991
+ /**
1992
+ * A redirect [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
1993
+ * that will perform a [`history.replaceState`](https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState)
1994
+ * instead of a [`history.pushState`](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState)
1995
+ * for client-side navigation redirects. Sets the status code and the [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)
1996
+ * header. Defaults to [`302 Found`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302).
1997
+ *
1998
+ * @example
1999
+ * import { replace } from "react-router";
2000
+ *
2001
+ * export async function loader() {
2002
+ * return replace("/new-location");
2003
+ * }
2004
+ *
2005
+ * @public
2006
+ * @category Utils
2007
+ * @mode framework
2008
+ * @mode data
2009
+ * @param url The URL to redirect to.
2010
+ * @param init The status code or a `ResponseInit` object to be included in the
2011
+ * response.
2012
+ * @returns A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
2013
+ * object with the redirect status and [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location)
2014
+ * header.
2015
+ */
2016
+ export const replace: RedirectFunction = (url, init) => {
2017
+ let response = redirect(url, init);
2018
+ response.headers.set('X-Remix-Replace', 'true');
2019
+ return response;
2020
+ };
2021
+
2022
+ export type ErrorResponse = {
2023
+ status: number;
2024
+ statusText: string;
2025
+ data: any;
2026
+ };
2027
+
2028
+ export const SUPPORTED_ERROR_TYPES = [
2029
+ 'EvalError',
2030
+ 'RangeError',
2031
+ 'ReferenceError',
2032
+ 'SyntaxError',
2033
+ 'TypeError',
2034
+ 'URIError',
2035
+ ];
2036
+
2037
+ /*
2038
+ * Utility class we use to hold auto-unwrapped 4xx/5xx Response bodies
2039
+ *
2040
+ * We don't export the class for public use since it's an implementation
2041
+ * detail, but we export the interface above so folks can build their own
2042
+ * abstractions around instances via isRouteErrorResponse()
2043
+ */
2044
+ export class ErrorResponseImpl implements ErrorResponse {
2045
+ status: number;
2046
+ statusText: string;
2047
+ data: any;
2048
+ private error?: Error;
2049
+ private internal: boolean;
2050
+
2051
+ constructor(status: number, statusText: string | undefined, data: any, internal = false) {
2052
+ this.status = status;
2053
+ this.statusText = statusText || '';
2054
+ this.internal = internal;
2055
+ if (data instanceof Error) {
2056
+ this.data = data.toString();
2057
+ this.error = data;
2058
+ } else {
2059
+ this.data = data;
2060
+ }
2061
+ }
2062
+ }
2063
+
2064
+ /**
2065
+ * Check if the given error is an {@link ErrorResponse} generated from a 4xx/5xx
2066
+ * [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response)
2067
+ * thrown from an [`action`](../../start/framework/route-module#action) or
2068
+ * [`loader`](../../start/framework/route-module#loader) function.
2069
+ *
2070
+ * @example
2071
+ * import { isRouteErrorResponse } from "react-router";
2072
+ *
2073
+ * export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
2074
+ * if (isRouteErrorResponse(error)) {
2075
+ * return (
2076
+ * <>
2077
+ * <p>Error: `${error.status}: ${error.statusText}`</p>
2078
+ * <p>{error.data}</p>
2079
+ * </>
2080
+ * );
2081
+ * }
2082
+ *
2083
+ * return (
2084
+ * <p>Error: {error instanceof Error ? error.message : "Unknown Error"}</p>
2085
+ * );
2086
+ * }
2087
+ *
2088
+ * @public
2089
+ * @category Utils
2090
+ * @mode framework
2091
+ * @mode data
2092
+ * @param error The error to check.
2093
+ * @returns `true` if the error is an {@link ErrorResponse}, `false` otherwise.
2094
+ */
2095
+ export function isRouteErrorResponse(error: any): error is ErrorResponse {
2096
+ return (
2097
+ error != null &&
2098
+ typeof error.status === 'number' &&
2099
+ typeof error.statusText === 'string' &&
2100
+ typeof error.internal === 'boolean' &&
2101
+ 'data' in error
2102
+ );
2103
+ }
2104
+
2105
+ /*
2106
+ lol - this comment is needed because the JSDoc parser for `docs.ts` gets confused
2107
+ by the star-slash in the `getRoutePattern` regex and messes up the parsed comment
2108
+ for `isRouteErrorResponse` above. This comment seems to reset the parser.
2109
+ */
2110
+
2111
+ export function getRoutePattern(matches: RouteMatch[]) {
2112
+ let parts = matches.map((m) => m.route.path).filter(Boolean) as string[];
2113
+ return joinPaths(parts) || '/';
2114
+ }
2115
+
2116
+ export const isBrowser =
2117
+ typeof window !== 'undefined' &&
2118
+ typeof window.document !== 'undefined' &&
2119
+ typeof window.document.createElement !== 'undefined';
2120
+
2121
+ export type ParsedLocationInfo<T extends To> =
2122
+ | {
2123
+ absoluteURL: string;
2124
+ isExternal: boolean;
2125
+ to: string;
2126
+ }
2127
+ | {
2128
+ absoluteURL: undefined;
2129
+ isExternal: false;
2130
+ to: T;
2131
+ };
2132
+ export function parseToInfo<T extends To | string>(
2133
+ _to: T,
2134
+ basename: string,
2135
+ ): ParsedLocationInfo<T | string> {
2136
+ let to = _to as string;
2137
+ if (typeof to !== 'string' || !ABSOLUTE_URL_REGEX.test(to)) {
2138
+ return {
2139
+ absoluteURL: undefined,
2140
+ isExternal: false,
2141
+ to,
2142
+ };
2143
+ }
2144
+
2145
+ let absoluteURL = to;
2146
+ let isExternal = false;
2147
+ if (isBrowser) {
2148
+ try {
2149
+ let currentUrl = new URL(window.location.href);
2150
+ let targetUrl = PROTOCOL_RELATIVE_URL_REGEX.test(to)
2151
+ ? new URL(normalizeProtocolRelativeUrl(to, currentUrl.protocol))
2152
+ : new URL(to);
2153
+ let path = stripBasename(targetUrl.pathname, basename);
2154
+
2155
+ if (targetUrl.origin === currentUrl.origin && path != null) {
2156
+ // Strip the protocol/origin/basename for same-origin absolute URLs
2157
+ to = path + targetUrl.search + targetUrl.hash;
2158
+ } else {
2159
+ isExternal = true;
2160
+ }
2161
+ } catch (
2162
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
2163
+ e
2164
+ ) {
2165
+ // We can't do external URL detection without a valid URL
2166
+ warning(
2167
+ false,
2168
+ `<Link to="${to}"> contains an invalid URL which will probably break ` +
2169
+ `when clicked - please update to a valid URL path.`,
2170
+ );
2171
+ }
2172
+ }
2173
+
2174
+ return {
2175
+ absoluteURL,
2176
+ isExternal,
2177
+ to,
2178
+ };
2179
+ }