@remix-run/router 0.0.0-experimental-48058118

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/utils.ts ADDED
@@ -0,0 +1,1559 @@
1
+ import type { Location, Path, To } from "./history";
2
+ import { invariant, parsePath } from "./history";
3
+
4
+ /**
5
+ * Map of routeId -> data returned from a loader/action/error
6
+ */
7
+ export interface RouteData {
8
+ [routeId: string]: any;
9
+ }
10
+
11
+ export enum ResultType {
12
+ data = "data",
13
+ deferred = "deferred",
14
+ redirect = "redirect",
15
+ error = "error",
16
+ }
17
+
18
+ /**
19
+ * Successful result from a loader or action
20
+ */
21
+ export interface SuccessResult {
22
+ type: ResultType.data;
23
+ data: any;
24
+ statusCode?: number;
25
+ headers?: Headers;
26
+ }
27
+
28
+ /**
29
+ * Successful defer() result from a loader or action
30
+ */
31
+ export interface DeferredResult {
32
+ type: ResultType.deferred;
33
+ deferredData: DeferredData;
34
+ statusCode?: number;
35
+ headers?: Headers;
36
+ }
37
+
38
+ /**
39
+ * Redirect result from a loader or action
40
+ */
41
+ export interface RedirectResult {
42
+ type: ResultType.redirect;
43
+ status: number;
44
+ location: string;
45
+ revalidate: boolean;
46
+ }
47
+
48
+ /**
49
+ * Unsuccessful result from a loader or action
50
+ */
51
+ export interface ErrorResult {
52
+ type: ResultType.error;
53
+ error: any;
54
+ headers?: Headers;
55
+ }
56
+
57
+ /**
58
+ * Result from a loader or action - potentially successful or unsuccessful
59
+ */
60
+ export type DataResult =
61
+ | SuccessResult
62
+ | DeferredResult
63
+ | RedirectResult
64
+ | ErrorResult;
65
+
66
+ export type MutationFormMethod = "post" | "put" | "patch" | "delete";
67
+ export type FormMethod = "get" | MutationFormMethod;
68
+
69
+ export type FormEncType =
70
+ | "application/x-www-form-urlencoded"
71
+ | "multipart/form-data";
72
+
73
+ /**
74
+ * @private
75
+ * Internal interface to pass around for action submissions, not intended for
76
+ * external consumption
77
+ */
78
+ export interface Submission {
79
+ formMethod: FormMethod;
80
+ formAction: string;
81
+ formEncType: FormEncType;
82
+ formData: FormData;
83
+ }
84
+
85
+ /**
86
+ * @private
87
+ * Arguments passed to route loader/action functions. Same for now but we keep
88
+ * this as a private implementation detail in case they diverge in the future.
89
+ */
90
+ interface DataFunctionArgs {
91
+ request: Request;
92
+ params: Params;
93
+ context?: any;
94
+ }
95
+
96
+ type DataFunctionReturnValue =
97
+ | Promise<Response>
98
+ | Response
99
+ | Promise<any>
100
+ | any;
101
+
102
+ /**
103
+ * Arguments passed to loader functions
104
+ */
105
+ export interface LoaderFunctionArgs extends DataFunctionArgs {}
106
+
107
+ /**
108
+ * Arguments passed to action functions
109
+ */
110
+ export interface ActionFunctionArgs extends DataFunctionArgs {}
111
+
112
+ /**
113
+ * Route loader function signature
114
+ */
115
+ export interface LoaderFunction {
116
+ (args: LoaderFunctionArgs): DataFunctionReturnValue;
117
+ }
118
+
119
+ /**
120
+ * Route action function signature
121
+ */
122
+ export interface ActionFunction {
123
+ (args: ActionFunctionArgs): DataFunctionReturnValue;
124
+ }
125
+
126
+ /**
127
+ * @private
128
+ * Arguments passed to route loader/action functions when middleware is enabled.
129
+ */
130
+ interface DataFunctionArgsWithMiddleware {
131
+ request: Request;
132
+ params: Params;
133
+ context: MiddlewareContext;
134
+ }
135
+
136
+ /**
137
+ * Arguments passed to middleware functions when middleware is enabled
138
+ */
139
+ export interface MiddlewareFunctionArgs
140
+ extends DataFunctionArgsWithMiddleware {}
141
+
142
+ /**
143
+ * Route loader function signature when middleware is enabled
144
+ */
145
+ export interface MiddlewareFunction {
146
+ (args: MiddlewareFunctionArgs): DataFunctionReturnValue;
147
+ }
148
+
149
+ /**
150
+ * Arguments passed to loader functions when middleware is enabled
151
+ */
152
+ export interface LoaderFunctionArgsWithMiddleware
153
+ extends DataFunctionArgsWithMiddleware {}
154
+
155
+ /**
156
+ * Route loader function signature when middleware is enabled
157
+ */
158
+ export interface LoaderFunctionWithMiddleware {
159
+ (args: LoaderFunctionArgsWithMiddleware): DataFunctionReturnValue;
160
+ }
161
+
162
+ /**
163
+ * Arguments passed to action functions when middleware is enabled
164
+ */
165
+ export interface ActionFunctionArgsWithMiddleware
166
+ extends DataFunctionArgsWithMiddleware {}
167
+
168
+ /**
169
+ * Route action function signature when middleware is enabled
170
+ */
171
+ export interface ActionFunctionWithMiddleware {
172
+ (args: ActionFunctionArgsWithMiddleware): DataFunctionReturnValue;
173
+ }
174
+
175
+ /**
176
+ * Route shouldRevalidate function signature. This runs after any submission
177
+ * (navigation or fetcher), so we flatten the navigation/fetcher submission
178
+ * onto the arguments. It shouldn't matter whether it came from a navigation
179
+ * or a fetcher, what really matters is the URLs and the formData since loaders
180
+ * have to re-run based on the data models that were potentially mutated.
181
+ */
182
+ export interface ShouldRevalidateFunction {
183
+ (args: {
184
+ currentUrl: URL;
185
+ currentParams: AgnosticDataRouteMatch["params"];
186
+ nextUrl: URL;
187
+ nextParams: AgnosticDataRouteMatch["params"];
188
+ formMethod?: Submission["formMethod"];
189
+ formAction?: Submission["formAction"];
190
+ formEncType?: Submission["formEncType"];
191
+ formData?: Submission["formData"];
192
+ actionResult?: DataResult;
193
+ defaultShouldRevalidate: boolean;
194
+ }): boolean;
195
+ }
196
+
197
+ /**
198
+ * Base RouteObject with common props shared by all types of routes
199
+ */
200
+ type AgnosticBaseRouteObject = {
201
+ caseSensitive?: boolean;
202
+ path?: string;
203
+ id?: string;
204
+ middleware?: MiddlewareFunction;
205
+ loader?: LoaderFunction | LoaderFunctionWithMiddleware;
206
+ action?: ActionFunction | ActionFunctionWithMiddleware;
207
+ hasErrorBoundary?: boolean;
208
+ shouldRevalidate?: ShouldRevalidateFunction;
209
+ handle?: any;
210
+ };
211
+
212
+ /**
213
+ * Index routes must not have children
214
+ */
215
+ export type AgnosticIndexRouteObject = AgnosticBaseRouteObject & {
216
+ children?: undefined;
217
+ index: true;
218
+ };
219
+
220
+ /**
221
+ * Non-index routes may have children, but cannot have index
222
+ */
223
+ export type AgnosticNonIndexRouteObject = AgnosticBaseRouteObject & {
224
+ children?: AgnosticRouteObject[];
225
+ index?: false;
226
+ };
227
+
228
+ /**
229
+ * A route object represents a logical route, with (optionally) its child
230
+ * routes organized in a tree-like structure.
231
+ */
232
+ export type AgnosticRouteObject =
233
+ | AgnosticIndexRouteObject
234
+ | AgnosticNonIndexRouteObject;
235
+
236
+ export type AgnosticDataIndexRouteObject = AgnosticIndexRouteObject & {
237
+ id: string;
238
+ };
239
+
240
+ export type AgnosticDataNonIndexRouteObject = AgnosticNonIndexRouteObject & {
241
+ children?: AgnosticDataRouteObject[];
242
+ id: string;
243
+ };
244
+
245
+ /**
246
+ * A data route object, which is just a RouteObject with a required unique ID
247
+ */
248
+ export type AgnosticDataRouteObject =
249
+ | AgnosticDataIndexRouteObject
250
+ | AgnosticDataNonIndexRouteObject;
251
+
252
+ // Recursive helper for finding path parameters in the absence of wildcards
253
+ type _PathParam<Path extends string> =
254
+ // split path into individual path segments
255
+ Path extends `${infer L}/${infer R}`
256
+ ? _PathParam<L> | _PathParam<R>
257
+ : // find params after `:`
258
+ Path extends `:${infer Param}`
259
+ ? Param extends `${infer Optional}?`
260
+ ? Optional
261
+ : Param
262
+ : // otherwise, there aren't any params present
263
+ never;
264
+
265
+ /**
266
+ * Examples:
267
+ * "/a/b/*" -> "*"
268
+ * ":a" -> "a"
269
+ * "/a/:b" -> "b"
270
+ * "/a/blahblahblah:b" -> "b"
271
+ * "/:a/:b" -> "a" | "b"
272
+ * "/:a/b/:c/*" -> "a" | "c" | "*"
273
+ */
274
+ type PathParam<Path extends string> =
275
+ // check if path is just a wildcard
276
+ Path extends "*"
277
+ ? "*"
278
+ : // look for wildcard at the end of the path
279
+ Path extends `${infer Rest}/*`
280
+ ? "*" | _PathParam<Rest>
281
+ : // look for params in the absence of wildcards
282
+ _PathParam<Path>;
283
+
284
+ // Attempt to parse the given string segment. If it fails, then just return the
285
+ // plain string type as a default fallback. Otherwise return the union of the
286
+ // parsed string literals that were referenced as dynamic segments in the route.
287
+ export type ParamParseKey<Segment extends string> =
288
+ // if could not find path params, fallback to `string`
289
+ [PathParam<Segment>] extends [never] ? string : PathParam<Segment>;
290
+
291
+ /**
292
+ * The parameters that were parsed from the URL path.
293
+ */
294
+ export type Params<Key extends string = string> = {
295
+ readonly [key in Key]: string | undefined;
296
+ };
297
+
298
+ /**
299
+ * A RouteMatch contains info about how a route matched a URL.
300
+ */
301
+ export interface AgnosticRouteMatch<
302
+ ParamKey extends string = string,
303
+ RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
304
+ > {
305
+ /**
306
+ * The names and values of dynamic parameters in the URL.
307
+ */
308
+ params: Params<ParamKey>;
309
+ /**
310
+ * The portion of the URL pathname that was matched.
311
+ */
312
+ pathname: string;
313
+ /**
314
+ * The portion of the URL pathname that was matched before child routes.
315
+ */
316
+ pathnameBase: string;
317
+ /**
318
+ * The route object that was used to match.
319
+ */
320
+ route: RouteObjectType;
321
+ }
322
+
323
+ export interface AgnosticDataRouteMatch
324
+ extends AgnosticRouteMatch<string, AgnosticDataRouteObject> {}
325
+
326
+ function isIndexRoute(
327
+ route: AgnosticRouteObject
328
+ ): route is AgnosticIndexRouteObject {
329
+ return route.index === true;
330
+ }
331
+
332
+ // Walk the route tree generating unique IDs where necessary so we are working
333
+ // solely with AgnosticDataRouteObject's within the Router
334
+ export function convertRoutesToDataRoutes(
335
+ routes: AgnosticRouteObject[],
336
+ parentPath: number[] = [],
337
+ allIds: Set<string> = new Set<string>()
338
+ ): AgnosticDataRouteObject[] {
339
+ return routes.map((route, index) => {
340
+ let treePath = [...parentPath, index];
341
+ let id = typeof route.id === "string" ? route.id : treePath.join("-");
342
+ invariant(
343
+ route.index !== true || !route.children,
344
+ `Cannot specify children on an index route`
345
+ );
346
+ invariant(
347
+ !allIds.has(id),
348
+ `Found a route id collision on id "${id}". Route ` +
349
+ "id's must be globally unique within Data Router usages"
350
+ );
351
+ allIds.add(id);
352
+
353
+ if (isIndexRoute(route)) {
354
+ let indexRoute: AgnosticDataIndexRouteObject = { ...route, id };
355
+ return indexRoute;
356
+ } else {
357
+ let pathOrLayoutRoute: AgnosticDataNonIndexRouteObject = {
358
+ ...route,
359
+ id,
360
+ children: route.children
361
+ ? convertRoutesToDataRoutes(route.children, treePath, allIds)
362
+ : undefined,
363
+ };
364
+ return pathOrLayoutRoute;
365
+ }
366
+ });
367
+ }
368
+
369
+ /**
370
+ * Matches the given routes to a location and returns the match data.
371
+ *
372
+ * @see https://reactrouter.com/utils/match-routes
373
+ */
374
+ export function matchRoutes<
375
+ RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
376
+ >(
377
+ routes: RouteObjectType[],
378
+ locationArg: Partial<Location> | string,
379
+ basename = "/"
380
+ ): AgnosticRouteMatch<string, RouteObjectType>[] | null {
381
+ let location =
382
+ typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
383
+
384
+ let pathname = stripBasename(location.pathname || "/", basename);
385
+
386
+ if (pathname == null) {
387
+ return null;
388
+ }
389
+
390
+ let branches = flattenRoutes(routes);
391
+ rankRouteBranches(branches);
392
+
393
+ let matches = null;
394
+ for (let i = 0; matches == null && i < branches.length; ++i) {
395
+ matches = matchRouteBranch<string, RouteObjectType>(
396
+ branches[i],
397
+ // Incoming pathnames are generally encoded from either window.location
398
+ // or from router.navigate, but we want to match against the unencoded
399
+ // paths in the route definitions. Memory router locations won't be
400
+ // encoded here but there also shouldn't be anything to decode so this
401
+ // should be a safe operation. This avoids needing matchRoutes to be
402
+ // history-aware.
403
+ safelyDecodeURI(pathname)
404
+ );
405
+ }
406
+
407
+ return matches;
408
+ }
409
+
410
+ interface RouteMeta<
411
+ RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
412
+ > {
413
+ relativePath: string;
414
+ caseSensitive: boolean;
415
+ childrenIndex: number;
416
+ route: RouteObjectType;
417
+ }
418
+
419
+ interface RouteBranch<
420
+ RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
421
+ > {
422
+ path: string;
423
+ score: number;
424
+ routesMeta: RouteMeta<RouteObjectType>[];
425
+ }
426
+
427
+ function flattenRoutes<
428
+ RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
429
+ >(
430
+ routes: RouteObjectType[],
431
+ branches: RouteBranch<RouteObjectType>[] = [],
432
+ parentsMeta: RouteMeta<RouteObjectType>[] = [],
433
+ parentPath = ""
434
+ ): RouteBranch<RouteObjectType>[] {
435
+ let flattenRoute = (
436
+ route: RouteObjectType,
437
+ index: number,
438
+ relativePath?: string
439
+ ) => {
440
+ let meta: RouteMeta<RouteObjectType> = {
441
+ relativePath:
442
+ relativePath === undefined ? route.path || "" : relativePath,
443
+ caseSensitive: route.caseSensitive === true,
444
+ childrenIndex: index,
445
+ route,
446
+ };
447
+
448
+ if (meta.relativePath.startsWith("/")) {
449
+ invariant(
450
+ meta.relativePath.startsWith(parentPath),
451
+ `Absolute route path "${meta.relativePath}" nested under path ` +
452
+ `"${parentPath}" is not valid. An absolute child route path ` +
453
+ `must start with the combined path of all its parent routes.`
454
+ );
455
+
456
+ meta.relativePath = meta.relativePath.slice(parentPath.length);
457
+ }
458
+
459
+ let path = joinPaths([parentPath, meta.relativePath]);
460
+ let routesMeta = parentsMeta.concat(meta);
461
+
462
+ // Add the children before adding this route to the array so we traverse the
463
+ // route tree depth-first and child routes appear before their parents in
464
+ // the "flattened" version.
465
+ if (route.children && route.children.length > 0) {
466
+ invariant(
467
+ // Our types know better, but runtime JS may not!
468
+ // @ts-expect-error
469
+ route.index !== true,
470
+ `Index routes must not have child routes. Please remove ` +
471
+ `all child routes from route path "${path}".`
472
+ );
473
+
474
+ flattenRoutes(route.children, branches, routesMeta, path);
475
+ }
476
+
477
+ // Routes without a path shouldn't ever match by themselves unless they are
478
+ // index routes, so don't add them to the list of possible branches.
479
+ if (route.path == null && !route.index) {
480
+ return;
481
+ }
482
+
483
+ branches.push({
484
+ path,
485
+ score: computeScore(path, route.index),
486
+ routesMeta,
487
+ });
488
+ };
489
+ routes.forEach((route, index) => {
490
+ // coarse-grain check for optional params
491
+ if (route.path === "" || !route.path?.includes("?")) {
492
+ flattenRoute(route, index);
493
+ } else {
494
+ for (let exploded of explodeOptionalSegments(route.path)) {
495
+ flattenRoute(route, index, exploded);
496
+ }
497
+ }
498
+ });
499
+
500
+ return branches;
501
+ }
502
+
503
+ /**
504
+ * Computes all combinations of optional path segments for a given path,
505
+ * excluding combinations that are ambiguous and of lower priority.
506
+ *
507
+ * For example, `/one/:two?/three/:four?/:five?` explodes to:
508
+ * - `/one/three`
509
+ * - `/one/:two/three`
510
+ * - `/one/three/:four`
511
+ * - `/one/three/:five`
512
+ * - `/one/:two/three/:four`
513
+ * - `/one/:two/three/:five`
514
+ * - `/one/three/:four/:five`
515
+ * - `/one/:two/three/:four/:five`
516
+ */
517
+ function explodeOptionalSegments(path: string): string[] {
518
+ let segments = path.split("/");
519
+ if (segments.length === 0) return [];
520
+
521
+ let [first, ...rest] = segments;
522
+
523
+ // Optional path segments are denoted by a trailing `?`
524
+ let isOptional = first.endsWith("?");
525
+ // Compute the corresponding required segment: `foo?` -> `foo`
526
+ let required = first.replace(/\?$/, "");
527
+
528
+ if (rest.length === 0) {
529
+ // Intepret empty string as omitting an optional segment
530
+ // `["one", "", "three"]` corresponds to omitting `:two` from `/one/:two?/three` -> `/one/three`
531
+ return isOptional ? [required, ""] : [required];
532
+ }
533
+
534
+ let restExploded = explodeOptionalSegments(rest.join("/"));
535
+
536
+ let result: string[] = [];
537
+
538
+ // All child paths with the prefix. Do this for all children before the
539
+ // optional version for all children so we get consistent ordering where the
540
+ // parent optional aspect is preferred as required. Otherwise, we can get
541
+ // child sections interspersed where deeper optional segments are higher than
542
+ // parent optional segments, where for example, /:two would explodes _earlier_
543
+ // then /:one. By always including the parent as required _for all children_
544
+ // first, we avoid this issue
545
+ result.push(
546
+ ...restExploded.map((subpath) =>
547
+ subpath === "" ? required : [required, subpath].join("/")
548
+ )
549
+ );
550
+
551
+ // Then if this is an optional value, add all child versions without
552
+ if (isOptional) {
553
+ result.push(...restExploded);
554
+ }
555
+
556
+ // for absolute paths, ensure `/` instead of empty segment
557
+ return result.map((exploded) =>
558
+ path.startsWith("/") && exploded === "" ? "/" : exploded
559
+ );
560
+ }
561
+
562
+ function rankRouteBranches(branches: RouteBranch[]): void {
563
+ branches.sort((a, b) =>
564
+ a.score !== b.score
565
+ ? b.score - a.score // Higher score first
566
+ : compareIndexes(
567
+ a.routesMeta.map((meta) => meta.childrenIndex),
568
+ b.routesMeta.map((meta) => meta.childrenIndex)
569
+ )
570
+ );
571
+ }
572
+
573
+ const paramRe = /^:\w+$/;
574
+ const dynamicSegmentValue = 3;
575
+ const indexRouteValue = 2;
576
+ const emptySegmentValue = 1;
577
+ const staticSegmentValue = 10;
578
+ const splatPenalty = -2;
579
+ const isSplat = (s: string) => s === "*";
580
+
581
+ function computeScore(path: string, index: boolean | undefined): number {
582
+ let segments = path.split("/");
583
+ let initialScore = segments.length;
584
+ if (segments.some(isSplat)) {
585
+ initialScore += splatPenalty;
586
+ }
587
+
588
+ if (index) {
589
+ initialScore += indexRouteValue;
590
+ }
591
+
592
+ return segments
593
+ .filter((s) => !isSplat(s))
594
+ .reduce(
595
+ (score, segment) =>
596
+ score +
597
+ (paramRe.test(segment)
598
+ ? dynamicSegmentValue
599
+ : segment === ""
600
+ ? emptySegmentValue
601
+ : staticSegmentValue),
602
+ initialScore
603
+ );
604
+ }
605
+
606
+ function compareIndexes(a: number[], b: number[]): number {
607
+ let siblings =
608
+ a.length === b.length && a.slice(0, -1).every((n, i) => n === b[i]);
609
+
610
+ return siblings
611
+ ? // If two routes are siblings, we should try to match the earlier sibling
612
+ // first. This allows people to have fine-grained control over the matching
613
+ // behavior by simply putting routes with identical paths in the order they
614
+ // want them tried.
615
+ a[a.length - 1] - b[b.length - 1]
616
+ : // Otherwise, it doesn't really make sense to rank non-siblings by index,
617
+ // so they sort equally.
618
+ 0;
619
+ }
620
+
621
+ function matchRouteBranch<
622
+ ParamKey extends string = string,
623
+ RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
624
+ >(
625
+ branch: RouteBranch<RouteObjectType>,
626
+ pathname: string
627
+ ): AgnosticRouteMatch<ParamKey, RouteObjectType>[] | null {
628
+ let { routesMeta } = branch;
629
+
630
+ let matchedParams = {};
631
+ let matchedPathname = "/";
632
+ let matches: AgnosticRouteMatch<ParamKey, RouteObjectType>[] = [];
633
+ for (let i = 0; i < routesMeta.length; ++i) {
634
+ let meta = routesMeta[i];
635
+ let end = i === routesMeta.length - 1;
636
+ let remainingPathname =
637
+ matchedPathname === "/"
638
+ ? pathname
639
+ : pathname.slice(matchedPathname.length) || "/";
640
+ let match = matchPath(
641
+ { path: meta.relativePath, caseSensitive: meta.caseSensitive, end },
642
+ remainingPathname
643
+ );
644
+
645
+ if (!match) return null;
646
+
647
+ Object.assign(matchedParams, match.params);
648
+
649
+ let route = meta.route;
650
+
651
+ matches.push({
652
+ // TODO: Can this as be avoided?
653
+ params: matchedParams as Params<ParamKey>,
654
+ pathname: joinPaths([matchedPathname, match.pathname]),
655
+ pathnameBase: normalizePathname(
656
+ joinPaths([matchedPathname, match.pathnameBase])
657
+ ),
658
+ route,
659
+ });
660
+
661
+ if (match.pathnameBase !== "/") {
662
+ matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);
663
+ }
664
+ }
665
+
666
+ return matches;
667
+ }
668
+
669
+ /**
670
+ * Returns a path with params interpolated.
671
+ *
672
+ * @see https://reactrouter.com/utils/generate-path
673
+ */
674
+ export function generatePath<Path extends string>(
675
+ originalPath: Path,
676
+ params: {
677
+ [key in PathParam<Path>]: string | null;
678
+ } = {} as any
679
+ ): string {
680
+ let path = originalPath;
681
+ if (path.endsWith("*") && path !== "*" && !path.endsWith("/*")) {
682
+ warning(
683
+ false,
684
+ `Route path "${path}" will be treated as if it were ` +
685
+ `"${path.replace(/\*$/, "/*")}" because the \`*\` character must ` +
686
+ `always follow a \`/\` in the pattern. To get rid of this warning, ` +
687
+ `please change the route path to "${path.replace(/\*$/, "/*")}".`
688
+ );
689
+ path = path.replace(/\*$/, "/*") as Path;
690
+ }
691
+
692
+ return (
693
+ path
694
+ .replace(
695
+ /^:(\w+)(\??)/g,
696
+ (_, key: PathParam<Path>, optional: string | undefined) => {
697
+ let param = params[key];
698
+ if (optional === "?") {
699
+ return param == null ? "" : param;
700
+ }
701
+ if (param == null) {
702
+ invariant(false, `Missing ":${key}" param`);
703
+ }
704
+ return param;
705
+ }
706
+ )
707
+ .replace(
708
+ /\/:(\w+)(\??)/g,
709
+ (_, key: PathParam<Path>, optional: string | undefined) => {
710
+ let param = params[key];
711
+ if (optional === "?") {
712
+ return param == null ? "" : `/${param}`;
713
+ }
714
+ if (param == null) {
715
+ invariant(false, `Missing ":${key}" param`);
716
+ }
717
+ return `/${param}`;
718
+ }
719
+ )
720
+ // Remove any optional markers from optional static segments
721
+ .replace(/\?/g, "")
722
+ .replace(/(\/?)\*/, (_, prefix, __, str) => {
723
+ const star = "*" as PathParam<Path>;
724
+
725
+ if (params[star] == null) {
726
+ // If no splat was provided, trim the trailing slash _unless_ it's
727
+ // the entire path
728
+ return str === "/*" ? "/" : "";
729
+ }
730
+
731
+ // Apply the splat
732
+ return `${prefix}${params[star]}`;
733
+ })
734
+ );
735
+ }
736
+
737
+ /**
738
+ * A PathPattern is used to match on some portion of a URL pathname.
739
+ */
740
+ export interface PathPattern<Path extends string = string> {
741
+ /**
742
+ * A string to match against a URL pathname. May contain `:id`-style segments
743
+ * to indicate placeholders for dynamic parameters. May also end with `/*` to
744
+ * indicate matching the rest of the URL pathname.
745
+ */
746
+ path: Path;
747
+ /**
748
+ * Should be `true` if the static portions of the `path` should be matched in
749
+ * the same case.
750
+ */
751
+ caseSensitive?: boolean;
752
+ /**
753
+ * Should be `true` if this pattern should match the entire URL pathname.
754
+ */
755
+ end?: boolean;
756
+ }
757
+
758
+ /**
759
+ * A PathMatch contains info about how a PathPattern matched on a URL pathname.
760
+ */
761
+ export interface PathMatch<ParamKey extends string = string> {
762
+ /**
763
+ * The names and values of dynamic parameters in the URL.
764
+ */
765
+ params: Params<ParamKey>;
766
+ /**
767
+ * The portion of the URL pathname that was matched.
768
+ */
769
+ pathname: string;
770
+ /**
771
+ * The portion of the URL pathname that was matched before child routes.
772
+ */
773
+ pathnameBase: string;
774
+ /**
775
+ * The pattern that was used to match.
776
+ */
777
+ pattern: PathPattern;
778
+ }
779
+
780
+ type Mutable<T> = {
781
+ -readonly [P in keyof T]: T[P];
782
+ };
783
+
784
+ /**
785
+ * Performs pattern matching on a URL pathname and returns information about
786
+ * the match.
787
+ *
788
+ * @see https://reactrouter.com/utils/match-path
789
+ */
790
+ export function matchPath<
791
+ ParamKey extends ParamParseKey<Path>,
792
+ Path extends string
793
+ >(
794
+ pattern: PathPattern<Path> | Path,
795
+ pathname: string
796
+ ): PathMatch<ParamKey> | null {
797
+ if (typeof pattern === "string") {
798
+ pattern = { path: pattern, caseSensitive: false, end: true };
799
+ }
800
+
801
+ let [matcher, paramNames] = compilePath(
802
+ pattern.path,
803
+ pattern.caseSensitive,
804
+ pattern.end
805
+ );
806
+
807
+ let match = pathname.match(matcher);
808
+ if (!match) return null;
809
+
810
+ let matchedPathname = match[0];
811
+ let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1");
812
+ let captureGroups = match.slice(1);
813
+ let params: Params = paramNames.reduce<Mutable<Params>>(
814
+ (memo, paramName, index) => {
815
+ // We need to compute the pathnameBase here using the raw splat value
816
+ // instead of using params["*"] later because it will be decoded then
817
+ if (paramName === "*") {
818
+ let splatValue = captureGroups[index] || "";
819
+ pathnameBase = matchedPathname
820
+ .slice(0, matchedPathname.length - splatValue.length)
821
+ .replace(/(.)\/+$/, "$1");
822
+ }
823
+
824
+ memo[paramName] = safelyDecodeURIComponent(
825
+ captureGroups[index] || "",
826
+ paramName
827
+ );
828
+ return memo;
829
+ },
830
+ {}
831
+ );
832
+
833
+ return {
834
+ params,
835
+ pathname: matchedPathname,
836
+ pathnameBase,
837
+ pattern,
838
+ };
839
+ }
840
+
841
+ function compilePath(
842
+ path: string,
843
+ caseSensitive = false,
844
+ end = true
845
+ ): [RegExp, string[]] {
846
+ warning(
847
+ path === "*" || !path.endsWith("*") || path.endsWith("/*"),
848
+ `Route path "${path}" will be treated as if it were ` +
849
+ `"${path.replace(/\*$/, "/*")}" because the \`*\` character must ` +
850
+ `always follow a \`/\` in the pattern. To get rid of this warning, ` +
851
+ `please change the route path to "${path.replace(/\*$/, "/*")}".`
852
+ );
853
+
854
+ let paramNames: string[] = [];
855
+ let regexpSource =
856
+ "^" +
857
+ path
858
+ .replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
859
+ .replace(/^\/*/, "/") // Make sure it has a leading /
860
+ .replace(/[\\.*+^$?{}|()[\]]/g, "\\$&") // Escape special regex chars
861
+ .replace(/\/:(\w+)/g, (_: string, paramName: string) => {
862
+ paramNames.push(paramName);
863
+ return "/([^\\/]+)";
864
+ });
865
+
866
+ if (path.endsWith("*")) {
867
+ paramNames.push("*");
868
+ regexpSource +=
869
+ path === "*" || path === "/*"
870
+ ? "(.*)$" // Already matched the initial /, just match the rest
871
+ : "(?:\\/(.+)|\\/*)$"; // Don't include the / in params["*"]
872
+ } else if (end) {
873
+ // When matching to the end, ignore trailing slashes
874
+ regexpSource += "\\/*$";
875
+ } else if (path !== "" && path !== "/") {
876
+ // If our path is non-empty and contains anything beyond an initial slash,
877
+ // then we have _some_ form of path in our regex so we should expect to
878
+ // match only if we find the end of this path segment. Look for an optional
879
+ // non-captured trailing slash (to match a portion of the URL) or the end
880
+ // of the path (if we've matched to the end). We used to do this with a
881
+ // word boundary but that gives false positives on routes like
882
+ // /user-preferences since `-` counts as a word boundary.
883
+ regexpSource += "(?:(?=\\/|$))";
884
+ } else {
885
+ // Nothing to match for "" or "/"
886
+ }
887
+
888
+ let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");
889
+
890
+ return [matcher, paramNames];
891
+ }
892
+
893
+ function safelyDecodeURI(value: string) {
894
+ try {
895
+ return decodeURI(value);
896
+ } catch (error) {
897
+ warning(
898
+ false,
899
+ `The URL path "${value}" could not be decoded because it is is a ` +
900
+ `malformed URL segment. This is probably due to a bad percent ` +
901
+ `encoding (${error}).`
902
+ );
903
+
904
+ return value;
905
+ }
906
+ }
907
+
908
+ function safelyDecodeURIComponent(value: string, paramName: string) {
909
+ try {
910
+ return decodeURIComponent(value);
911
+ } catch (error) {
912
+ warning(
913
+ false,
914
+ `The value for the URL param "${paramName}" will not be decoded because` +
915
+ ` the string "${value}" is a malformed URL segment. This is probably` +
916
+ ` due to a bad percent encoding (${error}).`
917
+ );
918
+
919
+ return value;
920
+ }
921
+ }
922
+
923
+ /**
924
+ * @private
925
+ */
926
+ export function stripBasename(
927
+ pathname: string,
928
+ basename: string
929
+ ): string | null {
930
+ if (basename === "/") return pathname;
931
+
932
+ if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {
933
+ return null;
934
+ }
935
+
936
+ // We want to leave trailing slash behavior in the user's control, so if they
937
+ // specify a basename with a trailing slash, we should support it
938
+ let startIndex = basename.endsWith("/")
939
+ ? basename.length - 1
940
+ : basename.length;
941
+ let nextChar = pathname.charAt(startIndex);
942
+ if (nextChar && nextChar !== "/") {
943
+ // pathname does not start with basename/
944
+ return null;
945
+ }
946
+
947
+ return pathname.slice(startIndex) || "/";
948
+ }
949
+
950
+ /**
951
+ * @private
952
+ */
953
+ export function warning(cond: any, message: string): void {
954
+ if (!cond) {
955
+ // eslint-disable-next-line no-console
956
+ if (typeof console !== "undefined") console.warn(message);
957
+
958
+ try {
959
+ // Welcome to debugging @remix-run/router!
960
+ //
961
+ // This error is thrown as a convenience so you can more easily
962
+ // find the source for a warning that appears in the console by
963
+ // enabling "pause on exceptions" in your JavaScript debugger.
964
+ throw new Error(message);
965
+ // eslint-disable-next-line no-empty
966
+ } catch (e) {}
967
+ }
968
+ }
969
+
970
+ /**
971
+ * Returns a resolved path object relative to the given pathname.
972
+ *
973
+ * @see https://reactrouter.com/utils/resolve-path
974
+ */
975
+ export function resolvePath(to: To, fromPathname = "/"): Path {
976
+ let {
977
+ pathname: toPathname,
978
+ search = "",
979
+ hash = "",
980
+ } = typeof to === "string" ? parsePath(to) : to;
981
+
982
+ let pathname = toPathname
983
+ ? toPathname.startsWith("/")
984
+ ? toPathname
985
+ : resolvePathname(toPathname, fromPathname)
986
+ : fromPathname;
987
+
988
+ return {
989
+ pathname,
990
+ search: normalizeSearch(search),
991
+ hash: normalizeHash(hash),
992
+ };
993
+ }
994
+
995
+ function resolvePathname(relativePath: string, fromPathname: string): string {
996
+ let segments = fromPathname.replace(/\/+$/, "").split("/");
997
+ let relativeSegments = relativePath.split("/");
998
+
999
+ relativeSegments.forEach((segment) => {
1000
+ if (segment === "..") {
1001
+ // Keep the root "" segment so the pathname starts at /
1002
+ if (segments.length > 1) segments.pop();
1003
+ } else if (segment !== ".") {
1004
+ segments.push(segment);
1005
+ }
1006
+ });
1007
+
1008
+ return segments.length > 1 ? segments.join("/") : "/";
1009
+ }
1010
+
1011
+ function getInvalidPathError(
1012
+ char: string,
1013
+ field: string,
1014
+ dest: string,
1015
+ path: Partial<Path>
1016
+ ) {
1017
+ return (
1018
+ `Cannot include a '${char}' character in a manually specified ` +
1019
+ `\`to.${field}\` field [${JSON.stringify(
1020
+ path
1021
+ )}]. Please separate it out to the ` +
1022
+ `\`to.${dest}\` field. Alternatively you may provide the full path as ` +
1023
+ `a string in <Link to="..."> and the router will parse it for you.`
1024
+ );
1025
+ }
1026
+
1027
+ /**
1028
+ * @private
1029
+ *
1030
+ * When processing relative navigation we want to ignore ancestor routes that
1031
+ * do not contribute to the path, such that index/pathless layout routes don't
1032
+ * interfere.
1033
+ *
1034
+ * For example, when moving a route element into an index route and/or a
1035
+ * pathless layout route, relative link behavior contained within should stay
1036
+ * the same. Both of the following examples should link back to the root:
1037
+ *
1038
+ * <Route path="/">
1039
+ * <Route path="accounts" element={<Link to=".."}>
1040
+ * </Route>
1041
+ *
1042
+ * <Route path="/">
1043
+ * <Route path="accounts">
1044
+ * <Route element={<AccountsLayout />}> // <-- Does not contribute
1045
+ * <Route index element={<Link to=".."} /> // <-- Does not contribute
1046
+ * </Route
1047
+ * </Route>
1048
+ * </Route>
1049
+ */
1050
+ export function getPathContributingMatches<
1051
+ T extends AgnosticRouteMatch = AgnosticRouteMatch
1052
+ >(matches: T[]) {
1053
+ return matches.filter(
1054
+ (match, index) =>
1055
+ index === 0 || (match.route.path && match.route.path.length > 0)
1056
+ );
1057
+ }
1058
+
1059
+ /**
1060
+ * @private
1061
+ */
1062
+ export function resolveTo(
1063
+ toArg: To,
1064
+ routePathnames: string[],
1065
+ locationPathname: string,
1066
+ isPathRelative = false
1067
+ ): Path {
1068
+ let to: Partial<Path>;
1069
+ if (typeof toArg === "string") {
1070
+ to = parsePath(toArg);
1071
+ } else {
1072
+ to = { ...toArg };
1073
+
1074
+ invariant(
1075
+ !to.pathname || !to.pathname.includes("?"),
1076
+ getInvalidPathError("?", "pathname", "search", to)
1077
+ );
1078
+ invariant(
1079
+ !to.pathname || !to.pathname.includes("#"),
1080
+ getInvalidPathError("#", "pathname", "hash", to)
1081
+ );
1082
+ invariant(
1083
+ !to.search || !to.search.includes("#"),
1084
+ getInvalidPathError("#", "search", "hash", to)
1085
+ );
1086
+ }
1087
+
1088
+ let isEmptyPath = toArg === "" || to.pathname === "";
1089
+ let toPathname = isEmptyPath ? "/" : to.pathname;
1090
+
1091
+ let from: string;
1092
+
1093
+ // Routing is relative to the current pathname if explicitly requested.
1094
+ //
1095
+ // If a pathname is explicitly provided in `to`, it should be relative to the
1096
+ // route context. This is explained in `Note on `<Link to>` values` in our
1097
+ // migration guide from v5 as a means of disambiguation between `to` values
1098
+ // that begin with `/` and those that do not. However, this is problematic for
1099
+ // `to` values that do not provide a pathname. `to` can simply be a search or
1100
+ // hash string, in which case we should assume that the navigation is relative
1101
+ // to the current location's pathname and *not* the route pathname.
1102
+ if (isPathRelative || toPathname == null) {
1103
+ from = locationPathname;
1104
+ } else {
1105
+ let routePathnameIndex = routePathnames.length - 1;
1106
+
1107
+ if (toPathname.startsWith("..")) {
1108
+ let toSegments = toPathname.split("/");
1109
+
1110
+ // Each leading .. segment means "go up one route" instead of "go up one
1111
+ // URL segment". This is a key difference from how <a href> works and a
1112
+ // major reason we call this a "to" value instead of a "href".
1113
+ while (toSegments[0] === "..") {
1114
+ toSegments.shift();
1115
+ routePathnameIndex -= 1;
1116
+ }
1117
+
1118
+ to.pathname = toSegments.join("/");
1119
+ }
1120
+
1121
+ // If there are more ".." segments than parent routes, resolve relative to
1122
+ // the root / URL.
1123
+ from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : "/";
1124
+ }
1125
+
1126
+ let path = resolvePath(to, from);
1127
+
1128
+ // Ensure the pathname has a trailing slash if the original "to" had one
1129
+ let hasExplicitTrailingSlash =
1130
+ toPathname && toPathname !== "/" && toPathname.endsWith("/");
1131
+ // Or if this was a link to the current path which has a trailing slash
1132
+ let hasCurrentTrailingSlash =
1133
+ (isEmptyPath || toPathname === ".") && locationPathname.endsWith("/");
1134
+ if (
1135
+ !path.pathname.endsWith("/") &&
1136
+ (hasExplicitTrailingSlash || hasCurrentTrailingSlash)
1137
+ ) {
1138
+ path.pathname += "/";
1139
+ }
1140
+
1141
+ return path;
1142
+ }
1143
+
1144
+ /**
1145
+ * @private
1146
+ */
1147
+ export function getToPathname(to: To): string | undefined {
1148
+ // Empty strings should be treated the same as / paths
1149
+ return to === "" || (to as Path).pathname === ""
1150
+ ? "/"
1151
+ : typeof to === "string"
1152
+ ? parsePath(to).pathname
1153
+ : to.pathname;
1154
+ }
1155
+
1156
+ /**
1157
+ * @private
1158
+ */
1159
+ export const joinPaths = (paths: string[]): string =>
1160
+ paths.join("/").replace(/\/\/+/g, "/");
1161
+
1162
+ /**
1163
+ * @private
1164
+ */
1165
+ export const normalizePathname = (pathname: string): string =>
1166
+ pathname.replace(/\/+$/, "").replace(/^\/*/, "/");
1167
+
1168
+ /**
1169
+ * @private
1170
+ */
1171
+ export const normalizeSearch = (search: string): string =>
1172
+ !search || search === "?"
1173
+ ? ""
1174
+ : search.startsWith("?")
1175
+ ? search
1176
+ : "?" + search;
1177
+
1178
+ /**
1179
+ * @private
1180
+ */
1181
+ export const normalizeHash = (hash: string): string =>
1182
+ !hash || hash === "#" ? "" : hash.startsWith("#") ? hash : "#" + hash;
1183
+
1184
+ export type JsonFunction = <Data>(
1185
+ data: Data,
1186
+ init?: number | ResponseInit
1187
+ ) => Response;
1188
+
1189
+ /**
1190
+ * This is a shortcut for creating `application/json` responses. Converts `data`
1191
+ * to JSON and sets the `Content-Type` header.
1192
+ */
1193
+ export const json: JsonFunction = (data, init = {}) => {
1194
+ let responseInit = typeof init === "number" ? { status: init } : init;
1195
+
1196
+ let headers = new Headers(responseInit.headers);
1197
+ if (!headers.has("Content-Type")) {
1198
+ headers.set("Content-Type", "application/json; charset=utf-8");
1199
+ }
1200
+
1201
+ return new Response(JSON.stringify(data), {
1202
+ ...responseInit,
1203
+ headers,
1204
+ });
1205
+ };
1206
+
1207
+ export interface TrackedPromise extends Promise<any> {
1208
+ _tracked?: boolean;
1209
+ _data?: any;
1210
+ _error?: any;
1211
+ }
1212
+
1213
+ export class AbortedDeferredError extends Error {}
1214
+
1215
+ export class DeferredData {
1216
+ private pendingKeysSet: Set<string> = new Set<string>();
1217
+ private controller: AbortController;
1218
+ private abortPromise: Promise<void>;
1219
+ private unlistenAbortSignal: () => void;
1220
+ private subscribers: Set<(aborted: boolean, settledKey?: string) => void> =
1221
+ new Set();
1222
+ data: Record<string, unknown>;
1223
+ init?: ResponseInit;
1224
+ deferredKeys: string[] = [];
1225
+
1226
+ constructor(data: Record<string, unknown>, responseInit?: ResponseInit) {
1227
+ invariant(
1228
+ data && typeof data === "object" && !Array.isArray(data),
1229
+ "defer() only accepts plain objects"
1230
+ );
1231
+
1232
+ // Set up an AbortController + Promise we can race against to exit early
1233
+ // cancellation
1234
+ let reject: (e: AbortedDeferredError) => void;
1235
+ this.abortPromise = new Promise((_, r) => (reject = r));
1236
+ this.controller = new AbortController();
1237
+ let onAbort = () =>
1238
+ reject(new AbortedDeferredError("Deferred data aborted"));
1239
+ this.unlistenAbortSignal = () =>
1240
+ this.controller.signal.removeEventListener("abort", onAbort);
1241
+ this.controller.signal.addEventListener("abort", onAbort);
1242
+
1243
+ this.data = Object.entries(data).reduce(
1244
+ (acc, [key, value]) =>
1245
+ Object.assign(acc, {
1246
+ [key]: this.trackPromise(key, value),
1247
+ }),
1248
+ {}
1249
+ );
1250
+
1251
+ if (this.done) {
1252
+ // All incoming values were resolved
1253
+ this.unlistenAbortSignal();
1254
+ }
1255
+
1256
+ this.init = responseInit;
1257
+ }
1258
+
1259
+ private trackPromise(
1260
+ key: string,
1261
+ value: Promise<unknown> | unknown
1262
+ ): TrackedPromise | unknown {
1263
+ if (!(value instanceof Promise)) {
1264
+ return value;
1265
+ }
1266
+
1267
+ this.deferredKeys.push(key);
1268
+ this.pendingKeysSet.add(key);
1269
+
1270
+ // We store a little wrapper promise that will be extended with
1271
+ // _data/_error props upon resolve/reject
1272
+ let promise: TrackedPromise = Promise.race([value, this.abortPromise]).then(
1273
+ (data) => this.onSettle(promise, key, null, data as unknown),
1274
+ (error) => this.onSettle(promise, key, error as unknown)
1275
+ );
1276
+
1277
+ // Register rejection listeners to avoid uncaught promise rejections on
1278
+ // errors or aborted deferred values
1279
+ promise.catch(() => {});
1280
+
1281
+ Object.defineProperty(promise, "_tracked", { get: () => true });
1282
+ return promise;
1283
+ }
1284
+
1285
+ private onSettle(
1286
+ promise: TrackedPromise,
1287
+ key: string,
1288
+ error: unknown,
1289
+ data?: unknown
1290
+ ): unknown {
1291
+ if (
1292
+ this.controller.signal.aborted &&
1293
+ error instanceof AbortedDeferredError
1294
+ ) {
1295
+ this.unlistenAbortSignal();
1296
+ Object.defineProperty(promise, "_error", { get: () => error });
1297
+ return Promise.reject(error);
1298
+ }
1299
+
1300
+ this.pendingKeysSet.delete(key);
1301
+
1302
+ if (this.done) {
1303
+ // Nothing left to abort!
1304
+ this.unlistenAbortSignal();
1305
+ }
1306
+
1307
+ if (error) {
1308
+ Object.defineProperty(promise, "_error", { get: () => error });
1309
+ this.emit(false, key);
1310
+ return Promise.reject(error);
1311
+ }
1312
+
1313
+ Object.defineProperty(promise, "_data", { get: () => data });
1314
+ this.emit(false, key);
1315
+ return data;
1316
+ }
1317
+
1318
+ private emit(aborted: boolean, settledKey?: string) {
1319
+ this.subscribers.forEach((subscriber) => subscriber(aborted, settledKey));
1320
+ }
1321
+
1322
+ subscribe(fn: (aborted: boolean, settledKey?: string) => void) {
1323
+ this.subscribers.add(fn);
1324
+ return () => this.subscribers.delete(fn);
1325
+ }
1326
+
1327
+ cancel() {
1328
+ this.controller.abort();
1329
+ this.pendingKeysSet.forEach((v, k) => this.pendingKeysSet.delete(k));
1330
+ this.emit(true);
1331
+ }
1332
+
1333
+ async resolveData(signal: AbortSignal) {
1334
+ let aborted = false;
1335
+ if (!this.done) {
1336
+ let onAbort = () => this.cancel();
1337
+ signal.addEventListener("abort", onAbort);
1338
+ aborted = await new Promise((resolve) => {
1339
+ this.subscribe((aborted) => {
1340
+ signal.removeEventListener("abort", onAbort);
1341
+ if (aborted || this.done) {
1342
+ resolve(aborted);
1343
+ }
1344
+ });
1345
+ });
1346
+ }
1347
+ return aborted;
1348
+ }
1349
+
1350
+ get done() {
1351
+ return this.pendingKeysSet.size === 0;
1352
+ }
1353
+
1354
+ get unwrappedData() {
1355
+ invariant(
1356
+ this.data !== null && this.done,
1357
+ "Can only unwrap data on initialized and settled deferreds"
1358
+ );
1359
+
1360
+ return Object.entries(this.data).reduce(
1361
+ (acc, [key, value]) =>
1362
+ Object.assign(acc, {
1363
+ [key]: unwrapTrackedPromise(value),
1364
+ }),
1365
+ {}
1366
+ );
1367
+ }
1368
+
1369
+ get pendingKeys() {
1370
+ return Array.from(this.pendingKeysSet);
1371
+ }
1372
+ }
1373
+
1374
+ function isTrackedPromise(value: any): value is TrackedPromise {
1375
+ return (
1376
+ value instanceof Promise && (value as TrackedPromise)._tracked === true
1377
+ );
1378
+ }
1379
+
1380
+ function unwrapTrackedPromise(value: any) {
1381
+ if (!isTrackedPromise(value)) {
1382
+ return value;
1383
+ }
1384
+
1385
+ if (value._error) {
1386
+ throw value._error;
1387
+ }
1388
+ return value._data;
1389
+ }
1390
+
1391
+ export type DeferFunction = (
1392
+ data: Record<string, unknown>,
1393
+ init?: number | ResponseInit
1394
+ ) => DeferredData;
1395
+
1396
+ export const defer: DeferFunction = (data, init = {}) => {
1397
+ let responseInit = typeof init === "number" ? { status: init } : init;
1398
+
1399
+ return new DeferredData(data, responseInit);
1400
+ };
1401
+
1402
+ export type RedirectFunction = (
1403
+ url: string,
1404
+ init?: number | ResponseInit
1405
+ ) => Response;
1406
+
1407
+ /**
1408
+ * A redirect response. Sets the status code and the `Location` header.
1409
+ * Defaults to "302 Found".
1410
+ */
1411
+ export const redirect: RedirectFunction = (url, init = 302) => {
1412
+ let responseInit = init;
1413
+ if (typeof responseInit === "number") {
1414
+ responseInit = { status: responseInit };
1415
+ } else if (typeof responseInit.status === "undefined") {
1416
+ responseInit.status = 302;
1417
+ }
1418
+
1419
+ let headers = new Headers(responseInit.headers);
1420
+ headers.set("Location", url);
1421
+
1422
+ return new Response(null, {
1423
+ ...responseInit,
1424
+ headers,
1425
+ });
1426
+ };
1427
+
1428
+ /**
1429
+ * @private
1430
+ * Utility class we use to hold auto-unwrapped 4xx/5xx Response bodies
1431
+ */
1432
+ export class ErrorResponse {
1433
+ status: number;
1434
+ statusText: string;
1435
+ data: any;
1436
+ error?: Error;
1437
+ internal: boolean;
1438
+
1439
+ constructor(
1440
+ status: number,
1441
+ statusText: string | undefined,
1442
+ data: any,
1443
+ internal = false
1444
+ ) {
1445
+ this.status = status;
1446
+ this.statusText = statusText || "";
1447
+ this.internal = internal;
1448
+ if (data instanceof Error) {
1449
+ this.data = data.toString();
1450
+ this.error = data;
1451
+ } else {
1452
+ this.data = data;
1453
+ }
1454
+ }
1455
+ }
1456
+
1457
+ /**
1458
+ * Check if the given error is an ErrorResponse generated from a 4xx/5xx
1459
+ * Response thrown from an action/loader
1460
+ */
1461
+ export function isRouteErrorResponse(error: any): error is ErrorResponse {
1462
+ return (
1463
+ error != null &&
1464
+ typeof error.status === "number" &&
1465
+ typeof error.statusText === "string" &&
1466
+ typeof error.internal === "boolean" &&
1467
+ "data" in error
1468
+ );
1469
+ }
1470
+
1471
+ /**
1472
+ * Context object passed through middleware functions and into action/loaders.
1473
+ *
1474
+ * Supports only key/value for now, eventually will be enhanced
1475
+ */
1476
+ export interface MiddlewareContext {
1477
+ /**
1478
+ * Retrieve a value from context
1479
+ */
1480
+ get<T>(key: MiddlewareContextInstance<T>): T;
1481
+ /**
1482
+ * Set a value from context
1483
+ */
1484
+ set<T>(key: MiddlewareContextInstance<T>, value: T): void;
1485
+ /**
1486
+ * Call any child middlewares and the destination loader/action
1487
+ */
1488
+ next: () => DataFunctionReturnValue;
1489
+ /**
1490
+ * @internal
1491
+ * PRIVATE - DO NOT USE
1492
+ *
1493
+ * Return the entries - needed so we can copy values from the serverMiddleware
1494
+ * context into route-specific contexts
1495
+ */
1496
+ entries(): IterableIterator<[MiddlewareContextInstance<unknown>, unknown]>;
1497
+ }
1498
+
1499
+ /**
1500
+ * Generic class to "hold" a default middleware value and the generic type so
1501
+ * we can enforce typings on middleware.get/set
1502
+ */
1503
+ export class MiddlewareContextInstance<T> {
1504
+ private defaultValue: T | undefined;
1505
+
1506
+ constructor(defaultValue?: T) {
1507
+ if (typeof defaultValue !== "undefined") {
1508
+ this.defaultValue = defaultValue;
1509
+ }
1510
+ }
1511
+
1512
+ getDefaultValue(): T {
1513
+ if (typeof this.defaultValue === "undefined") {
1514
+ throw new Error("Unable to find a value in the middleware context");
1515
+ }
1516
+ return this.defaultValue;
1517
+ }
1518
+ }
1519
+
1520
+ /**
1521
+ * Create a middleware context that can be used as a "key" to set/get middleware
1522
+ * values in a strongly-typed fashion
1523
+ */
1524
+ export function createMiddlewareContext<T extends unknown>(
1525
+ defaultValue?: T
1526
+ ): MiddlewareContextInstance<T> {
1527
+ return new MiddlewareContextInstance<T>(defaultValue);
1528
+ }
1529
+
1530
+ /**
1531
+ * @internal
1532
+ * PRIVATE - DO NOT USE
1533
+ *
1534
+ * Create a middleware "context" to store values and provide a next() hook
1535
+ */
1536
+ export function createMiddlewareStore(
1537
+ initialMiddlewareContext?: MiddlewareContext
1538
+ ) {
1539
+ let store = new Map(initialMiddlewareContext?.entries());
1540
+ let middlewareContext: MiddlewareContext = {
1541
+ get<T>(k: MiddlewareContextInstance<T>) {
1542
+ if (store.has(k)) {
1543
+ return store.get(k) as T;
1544
+ }
1545
+ return k.getDefaultValue();
1546
+ },
1547
+ set<T>(k: MiddlewareContextInstance<T>, v: T) {
1548
+ if (typeof v === "undefined") {
1549
+ throw new Error(
1550
+ "You cannot set an undefined value in the middleware context"
1551
+ );
1552
+ }
1553
+ store.set(k, v);
1554
+ },
1555
+ next: () => {},
1556
+ entries: () => store.entries(),
1557
+ };
1558
+ return middlewareContext;
1559
+ }