@remix-run/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 (41) hide show
  1. package/LICENSE.md +22 -0
  2. package/README.md +155 -0
  3. package/__tests__/TestSequences/EncodedReservedCharacters.d.ts +2 -0
  4. package/__tests__/TestSequences/GoBack.d.ts +2 -0
  5. package/__tests__/TestSequences/GoForward.d.ts +2 -0
  6. package/__tests__/TestSequences/InitialLocationDefaultKey.d.ts +2 -0
  7. package/__tests__/TestSequences/InitialLocationHasKey.d.ts +2 -0
  8. package/__tests__/TestSequences/Listen.d.ts +2 -0
  9. package/__tests__/TestSequences/ListenPopOnly.d.ts +2 -0
  10. package/__tests__/TestSequences/PushMissingPathname.d.ts +2 -0
  11. package/__tests__/TestSequences/PushNewLocation.d.ts +2 -0
  12. package/__tests__/TestSequences/PushRelativePathname.d.ts +2 -0
  13. package/__tests__/TestSequences/PushRelativePathnameWarning.d.ts +2 -0
  14. package/__tests__/TestSequences/PushSamePath.d.ts +2 -0
  15. package/__tests__/TestSequences/PushState.d.ts +2 -0
  16. package/__tests__/TestSequences/ReplaceNewLocation.d.ts +2 -0
  17. package/__tests__/TestSequences/ReplaceSamePath.d.ts +2 -0
  18. package/__tests__/TestSequences/ReplaceState.d.ts +2 -0
  19. package/__tests__/browser-test.d.ts +4 -0
  20. package/__tests__/create-path-test.d.ts +1 -0
  21. package/__tests__/hash-base-test.d.ts +4 -0
  22. package/__tests__/hash-test.d.ts +4 -0
  23. package/__tests__/memory-test.d.ts +1 -0
  24. package/__tests__/router-test.d.ts +2 -0
  25. package/__tests__/setup.d.ts +1 -0
  26. package/history.d.ts +226 -0
  27. package/index.d.ts +11 -0
  28. package/index.js +2392 -0
  29. package/index.js.map +1 -0
  30. package/main.js +19 -0
  31. package/package.json +22 -0
  32. package/router.d.ts +298 -0
  33. package/router.development.js +2226 -0
  34. package/router.development.js.map +1 -0
  35. package/router.production.min.js +12 -0
  36. package/router.production.min.js.map +1 -0
  37. package/umd/router.development.js +2418 -0
  38. package/umd/router.development.js.map +1 -0
  39. package/umd/router.production.min.js +12 -0
  40. package/umd/router.production.min.js.map +1 -0
  41. package/utils.d.ts +248 -0
@@ -0,0 +1,2226 @@
1
+ /**
2
+ * @remix-run/router v0.1.0
3
+ *
4
+ * Copyright (c) Remix Software Inc.
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE.md file in the root directory of this source tree.
8
+ *
9
+ * @license MIT
10
+ */
11
+ ////////////////////////////////////////////////////////////////////////////////
12
+ //#region Types and Constants
13
+ ////////////////////////////////////////////////////////////////////////////////
14
+
15
+ /**
16
+ * Actions represent the type of change to a location value.
17
+ */
18
+ let Action;
19
+ /**
20
+ * The pathname, search, and hash values of a URL.
21
+ */
22
+
23
+ (function (Action) {
24
+ Action["Pop"] = "POP";
25
+ Action["Push"] = "PUSH";
26
+ Action["Replace"] = "REPLACE";
27
+ })(Action || (Action = {}));
28
+
29
+ const PopStateEventType = "popstate"; //#endregion
30
+ ////////////////////////////////////////////////////////////////////////////////
31
+ //#region Memory History
32
+ ////////////////////////////////////////////////////////////////////////////////
33
+
34
+ /**
35
+ * A user-supplied object that describes a location. Used when providing
36
+ * entries to `createMemoryHistory` via its `initialEntries` option.
37
+ */
38
+
39
+ /**
40
+ * Memory history stores the current location in memory. It is designed for use
41
+ * in stateful non-browser environments like tests and React Native.
42
+ */
43
+ function createMemoryHistory(options = {}) {
44
+ let {
45
+ initialEntries = ["/"],
46
+ initialIndex,
47
+ v5Compat = false
48
+ } = options;
49
+ let entries; // Declare so we can access from createMemoryLocation
50
+
51
+ entries = initialEntries.map((entry, index) => createMemoryLocation(entry, null, index === 0 ? "default" : undefined));
52
+ let index = clampIndex(initialIndex == null ? entries.length - 1 : initialIndex);
53
+ let action = Action.Pop;
54
+ let listener = null;
55
+
56
+ function clampIndex(n) {
57
+ return Math.min(Math.max(n, 0), entries.length - 1);
58
+ }
59
+
60
+ function getCurrentLocation() {
61
+ return entries[index];
62
+ }
63
+
64
+ function createMemoryLocation(to, state = null, key) {
65
+ let location = createLocation(entries ? getCurrentLocation().pathname : "/", to, state, key);
66
+ warning(location.pathname.charAt(0) === "/", `relative pathnames are not supported in memory history: ${JSON.stringify(to)}`) ;
67
+ return location;
68
+ }
69
+
70
+ let history = {
71
+ get index() {
72
+ return index;
73
+ },
74
+
75
+ get action() {
76
+ return action;
77
+ },
78
+
79
+ get location() {
80
+ return getCurrentLocation();
81
+ },
82
+
83
+ createHref(to) {
84
+ return typeof to === "string" ? to : createPath(to);
85
+ },
86
+
87
+ push(to, state) {
88
+ action = Action.Push;
89
+ let nextLocation = createMemoryLocation(to, state);
90
+ index += 1;
91
+ entries.splice(index, entries.length, nextLocation);
92
+
93
+ if (v5Compat && listener) {
94
+ listener({
95
+ action,
96
+ location: nextLocation
97
+ });
98
+ }
99
+ },
100
+
101
+ replace(to, state) {
102
+ action = Action.Replace;
103
+ let nextLocation = createMemoryLocation(to, state);
104
+ entries[index] = nextLocation;
105
+
106
+ if (v5Compat && listener) {
107
+ listener({
108
+ action,
109
+ location: nextLocation
110
+ });
111
+ }
112
+ },
113
+
114
+ go(delta) {
115
+ action = Action.Pop;
116
+ index = clampIndex(index + delta);
117
+
118
+ if (listener) {
119
+ listener({
120
+ action,
121
+ location: getCurrentLocation()
122
+ });
123
+ }
124
+ },
125
+
126
+ listen(fn) {
127
+ listener = fn;
128
+ return () => {
129
+ listener = null;
130
+ };
131
+ }
132
+
133
+ };
134
+ return history;
135
+ } //#endregion
136
+ ////////////////////////////////////////////////////////////////////////////////
137
+ //#region Browser History
138
+ ////////////////////////////////////////////////////////////////////////////////
139
+
140
+ /**
141
+ * A browser history stores the current location in regular URLs in a web
142
+ * browser environment. This is the standard for most web apps and provides the
143
+ * cleanest URLs the browser's address bar.
144
+ *
145
+ * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#browserhistory
146
+ */
147
+
148
+ /**
149
+ * Browser history stores the location in regular URLs. This is the standard for
150
+ * most web apps, but it requires some configuration on the server to ensure you
151
+ * serve the same app at multiple URLs.
152
+ *
153
+ * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createbrowserhistory
154
+ */
155
+ function createBrowserHistory(options = {}) {
156
+ function createBrowserLocation(window, globalHistory) {
157
+ let {
158
+ pathname,
159
+ search,
160
+ hash
161
+ } = window.location;
162
+ return createLocation("", {
163
+ pathname,
164
+ search,
165
+ hash
166
+ }, // state defaults to `null` because `window.history.state` does
167
+ globalHistory.state?.usr || null, globalHistory.state?.key || "default");
168
+ }
169
+
170
+ function createBrowserHref(window, to) {
171
+ return typeof to === "string" ? to : createPath(to);
172
+ }
173
+
174
+ return getUrlBasedHistory(createBrowserLocation, createBrowserHref, null, options);
175
+ } //#endregion
176
+ ////////////////////////////////////////////////////////////////////////////////
177
+ //#region Hash History
178
+ ////////////////////////////////////////////////////////////////////////////////
179
+
180
+ /**
181
+ * A hash history stores the current location in the fragment identifier portion
182
+ * of the URL in a web browser environment.
183
+ *
184
+ * This is ideal for apps that do not control the server for some reason
185
+ * (because the fragment identifier is never sent to the server), including some
186
+ * shared hosting environments that do not provide fine-grained controls over
187
+ * which pages are served at which URLs.
188
+ *
189
+ * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#hashhistory
190
+ */
191
+
192
+ /**
193
+ * Hash history stores the location in window.location.hash. This makes it ideal
194
+ * for situations where you don't want to send the location to the server for
195
+ * some reason, either because you do cannot configure it or the URL space is
196
+ * reserved for something else.
197
+ *
198
+ * @see https://github.com/remix-run/history/tree/main/docs/api-reference.md#createhashhistory
199
+ */
200
+ function createHashHistory(options = {}) {
201
+ function createHashLocation(window, globalHistory) {
202
+ let {
203
+ pathname = "/",
204
+ search = "",
205
+ hash = ""
206
+ } = parsePath(window.location.hash.substr(1));
207
+ return createLocation("", {
208
+ pathname,
209
+ search,
210
+ hash
211
+ }, // state defaults to `null` because `window.history.state` does
212
+ globalHistory.state?.usr || null, globalHistory.state?.key || "default");
213
+ }
214
+
215
+ function createHashHref(window, to) {
216
+ let base = window.document.querySelector("base");
217
+ let href = "";
218
+
219
+ if (base && base.getAttribute("href")) {
220
+ let url = window.location.href;
221
+ let hashIndex = url.indexOf("#");
222
+ href = hashIndex === -1 ? url : url.slice(0, hashIndex);
223
+ }
224
+
225
+ return href + "#" + (typeof to === "string" ? to : createPath(to));
226
+ }
227
+
228
+ function validateHashLocation(location, to) {
229
+ warning(location.pathname.charAt(0) === "/", `relative pathnames are not supported in hash history.push(${JSON.stringify(to)})`) ;
230
+ }
231
+
232
+ return getUrlBasedHistory(createHashLocation, createHashHref, validateHashLocation, options);
233
+ } //#endregion
234
+ ////////////////////////////////////////////////////////////////////////////////
235
+ //#region UTILS
236
+ ////////////////////////////////////////////////////////////////////////////////
237
+
238
+ const readOnly = obj => Object.freeze(obj) ;
239
+
240
+ function warning(cond, message) {
241
+ if (!cond) {
242
+ // eslint-disable-next-line no-console
243
+ if (typeof console !== "undefined") console.warn(message);
244
+
245
+ try {
246
+ // Welcome to debugging history!
247
+ //
248
+ // This error is thrown as a convenience so you can more easily
249
+ // find the source for a warning that appears in the console by
250
+ // enabling "pause on exceptions" in your JavaScript debugger.
251
+ throw new Error(message); // eslint-disable-next-line no-empty
252
+ } catch (e) {}
253
+ }
254
+ }
255
+
256
+ function createKey() {
257
+ return Math.random().toString(36).substr(2, 8);
258
+ }
259
+ /**
260
+ * For browser-based histories, we combine the state and key into an object
261
+ */
262
+
263
+
264
+ function getHistoryState(location) {
265
+ return {
266
+ usr: location.state,
267
+ key: location.key
268
+ };
269
+ }
270
+ /**
271
+ * Creates a Location object with a unique key from the given Path
272
+ */
273
+
274
+
275
+ function createLocation(current, to, state = null, key) {
276
+ return readOnly({
277
+ pathname: typeof current === "string" ? current : current.pathname,
278
+ search: "",
279
+ hash: "",
280
+ ...(typeof to === "string" ? parsePath(to) : to),
281
+ state,
282
+ // TODO: This could be cleaned up. push/replace should probably just take
283
+ // full Locations now and avoid the need to run through this flow at all
284
+ // But that's a pretty big refactor to the current test suite so going to
285
+ // keep as is for the time being and just let any incoming keys take precedence
286
+ key: to?.key || key || createKey()
287
+ });
288
+ }
289
+ /**
290
+ * Creates a string URL path from the given pathname, search, and hash components.
291
+ */
292
+
293
+ function createPath({
294
+ pathname = "/",
295
+ search = "",
296
+ hash = ""
297
+ }) {
298
+ if (search && search !== "?") pathname += search.charAt(0) === "?" ? search : "?" + search;
299
+ if (hash && hash !== "#") pathname += hash.charAt(0) === "#" ? hash : "#" + hash;
300
+ return pathname;
301
+ }
302
+ /**
303
+ * Parses a string URL path into its separate pathname, search, and hash components.
304
+ */
305
+
306
+ function parsePath(path) {
307
+ let parsedPath = {};
308
+
309
+ if (path) {
310
+ let hashIndex = path.indexOf("#");
311
+
312
+ if (hashIndex >= 0) {
313
+ parsedPath.hash = path.substr(hashIndex);
314
+ path = path.substr(0, hashIndex);
315
+ }
316
+
317
+ let searchIndex = path.indexOf("?");
318
+
319
+ if (searchIndex >= 0) {
320
+ parsedPath.search = path.substr(searchIndex);
321
+ path = path.substr(0, searchIndex);
322
+ }
323
+
324
+ if (path) {
325
+ parsedPath.pathname = path;
326
+ }
327
+ }
328
+
329
+ return parsedPath;
330
+ }
331
+
332
+ function getUrlBasedHistory(getLocation, createHref, validateLocation, options = {}) {
333
+ let {
334
+ window = document.defaultView,
335
+ v5Compat = false
336
+ } = options;
337
+ let globalHistory = window.history;
338
+ let action = Action.Pop;
339
+ let listener = null;
340
+
341
+ function handlePop() {
342
+ action = Action.Pop;
343
+
344
+ if (listener) {
345
+ listener({
346
+ action,
347
+ location: history.location
348
+ });
349
+ }
350
+ }
351
+
352
+ function push(to, state) {
353
+ action = Action.Push;
354
+ let location = createLocation(history.location, to, state);
355
+ validateLocation?.(location, to);
356
+ let historyState = getHistoryState(location);
357
+ let url = history.createHref(location); // try...catch because iOS limits us to 100 pushState calls :/
358
+
359
+ try {
360
+ globalHistory.pushState(historyState, "", url);
361
+ } catch (error) {
362
+ // They are going to lose state here, but there is no real
363
+ // way to warn them about it since the page will refresh...
364
+ window.location.assign(url);
365
+ }
366
+
367
+ if (v5Compat && listener) {
368
+ listener({
369
+ action,
370
+ location
371
+ });
372
+ }
373
+ }
374
+
375
+ function replace(to, state) {
376
+ action = Action.Replace;
377
+ let location = createLocation(history.location, to, state);
378
+ validateLocation?.(location, to);
379
+ let historyState = getHistoryState(location);
380
+ let url = history.createHref(location);
381
+ globalHistory.replaceState(historyState, "", url);
382
+
383
+ if (v5Compat && listener) {
384
+ listener({
385
+ action,
386
+ location: location
387
+ });
388
+ }
389
+ }
390
+
391
+ let history = {
392
+ get action() {
393
+ return action;
394
+ },
395
+
396
+ get location() {
397
+ return getLocation(window, globalHistory);
398
+ },
399
+
400
+ listen(fn) {
401
+ if (listener) {
402
+ throw new Error("A history only accepts one active listener");
403
+ }
404
+
405
+ window.addEventListener(PopStateEventType, handlePop);
406
+ listener = fn;
407
+ return () => {
408
+ window.removeEventListener(PopStateEventType, handlePop);
409
+ listener = null;
410
+ };
411
+ },
412
+
413
+ createHref(to) {
414
+ return createHref(window, to);
415
+ },
416
+
417
+ push,
418
+ replace,
419
+
420
+ go(n) {
421
+ return globalHistory.go(n);
422
+ }
423
+
424
+ };
425
+ return history;
426
+ } //#endregion
427
+
428
+ /**
429
+ * Matches the given routes to a location and returns the match data.
430
+ *
431
+ * @see https://reactrouter.com/docs/en/v6/utils/match-routes
432
+ */
433
+ function matchRoutes(routes, locationArg, basename = "/") {
434
+ let location = typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
435
+ let pathname = stripBasename(location.pathname || "/", basename);
436
+
437
+ if (pathname == null) {
438
+ return null;
439
+ }
440
+
441
+ let branches = flattenRoutes(routes);
442
+ rankRouteBranches(branches);
443
+ let matches = null;
444
+
445
+ for (let i = 0; matches == null && i < branches.length; ++i) {
446
+ matches = matchRouteBranch(branches[i], pathname);
447
+ }
448
+
449
+ return matches;
450
+ }
451
+
452
+ function flattenRoutes(routes, branches = [], parentsMeta = [], parentPath = "") {
453
+ routes.forEach((route, index) => {
454
+ let meta = {
455
+ relativePath: route.path || "",
456
+ caseSensitive: route.caseSensitive === true,
457
+ childrenIndex: index,
458
+ route
459
+ };
460
+
461
+ if (meta.relativePath.startsWith("/")) {
462
+ !meta.relativePath.startsWith(parentPath) ? invariant(false, `Absolute route path "${meta.relativePath}" nested under path ` + `"${parentPath}" is not valid. An absolute child route path ` + `must start with the combined path of all its parent routes.`) : void 0;
463
+ meta.relativePath = meta.relativePath.slice(parentPath.length);
464
+ }
465
+
466
+ let path = joinPaths([parentPath, meta.relativePath]);
467
+ let routesMeta = parentsMeta.concat(meta); // Add the children before adding this route to the array so we traverse the
468
+ // route tree depth-first and child routes appear before their parents in
469
+ // the "flattened" version.
470
+
471
+ if (route.children && route.children.length > 0) {
472
+ !(route.index !== true) ? invariant(false, `Index routes must not have child routes. Please remove ` + `all child routes from route path "${path}".`) : void 0;
473
+ flattenRoutes(route.children, branches, routesMeta, path);
474
+ } // Routes without a path shouldn't ever match by themselves unless they are
475
+ // index routes, so don't add them to the list of possible branches.
476
+
477
+
478
+ if (route.path == null && !route.index) {
479
+ return;
480
+ }
481
+
482
+ branches.push({
483
+ path,
484
+ score: computeScore(path, route.index),
485
+ routesMeta
486
+ });
487
+ });
488
+ return branches;
489
+ }
490
+
491
+ function rankRouteBranches(branches) {
492
+ branches.sort((a, b) => a.score !== b.score ? b.score - a.score // Higher score first
493
+ : compareIndexes(a.routesMeta.map(meta => meta.childrenIndex), b.routesMeta.map(meta => meta.childrenIndex)));
494
+ }
495
+
496
+ const paramRe = /^:\w+$/;
497
+ const dynamicSegmentValue = 3;
498
+ const indexRouteValue = 2;
499
+ const emptySegmentValue = 1;
500
+ const staticSegmentValue = 10;
501
+ const splatPenalty = -2;
502
+
503
+ const isSplat = s => s === "*";
504
+
505
+ function computeScore(path, index) {
506
+ let segments = path.split("/");
507
+ let initialScore = segments.length;
508
+
509
+ if (segments.some(isSplat)) {
510
+ initialScore += splatPenalty;
511
+ }
512
+
513
+ if (index) {
514
+ initialScore += indexRouteValue;
515
+ }
516
+
517
+ return segments.filter(s => !isSplat(s)).reduce((score, segment) => score + (paramRe.test(segment) ? dynamicSegmentValue : segment === "" ? emptySegmentValue : staticSegmentValue), initialScore);
518
+ }
519
+
520
+ function compareIndexes(a, b) {
521
+ let siblings = a.length === b.length && a.slice(0, -1).every((n, i) => n === b[i]);
522
+ return siblings ? // If two routes are siblings, we should try to match the earlier sibling
523
+ // first. This allows people to have fine-grained control over the matching
524
+ // behavior by simply putting routes with identical paths in the order they
525
+ // want them tried.
526
+ a[a.length - 1] - b[b.length - 1] : // Otherwise, it doesn't really make sense to rank non-siblings by index,
527
+ // so they sort equally.
528
+ 0;
529
+ }
530
+
531
+ function matchRouteBranch(branch, pathname) {
532
+ let {
533
+ routesMeta
534
+ } = branch;
535
+ let matchedParams = {};
536
+ let matchedPathname = "/";
537
+ let matches = [];
538
+
539
+ for (let i = 0; i < routesMeta.length; ++i) {
540
+ let meta = routesMeta[i];
541
+ let end = i === routesMeta.length - 1;
542
+ let remainingPathname = matchedPathname === "/" ? pathname : pathname.slice(matchedPathname.length) || "/";
543
+ let match = matchPath({
544
+ path: meta.relativePath,
545
+ caseSensitive: meta.caseSensitive,
546
+ end
547
+ }, remainingPathname);
548
+ if (!match) return null;
549
+ Object.assign(matchedParams, match.params);
550
+ let route = meta.route;
551
+ matches.push({
552
+ // TODO: Can this as be avoided?
553
+ params: matchedParams,
554
+ pathname: joinPaths([matchedPathname, match.pathname]),
555
+ pathnameBase: normalizePathname(joinPaths([matchedPathname, match.pathnameBase])),
556
+ route
557
+ });
558
+
559
+ if (match.pathnameBase !== "/") {
560
+ matchedPathname = joinPaths([matchedPathname, match.pathnameBase]);
561
+ }
562
+ }
563
+
564
+ return matches;
565
+ }
566
+ /**
567
+ * Returns a path with params interpolated.
568
+ *
569
+ * @see https://reactrouter.com/docs/en/v6/utils/generate-path
570
+ */
571
+
572
+
573
+ function generatePath(path, params = {}) {
574
+ return path.replace(/:(\w+)/g, (_, key) => {
575
+ !(params[key] != null) ? invariant(false, `Missing ":${key}" param`) : void 0;
576
+ return params[key];
577
+ }).replace(/\/*\*$/, _ => params["*"] == null ? "" : params["*"].replace(/^\/*/, "/"));
578
+ }
579
+ /**
580
+ * A PathPattern is used to match on some portion of a URL pathname.
581
+ */
582
+
583
+ /**
584
+ * Performs pattern matching on a URL pathname and returns information about
585
+ * the match.
586
+ *
587
+ * @see https://reactrouter.com/docs/en/v6/utils/match-path
588
+ */
589
+ function matchPath(pattern, pathname) {
590
+ if (typeof pattern === "string") {
591
+ pattern = {
592
+ path: pattern,
593
+ caseSensitive: false,
594
+ end: true
595
+ };
596
+ }
597
+
598
+ let [matcher, paramNames] = compilePath(pattern.path, pattern.caseSensitive, pattern.end);
599
+ let match = pathname.match(matcher);
600
+ if (!match) return null;
601
+ let matchedPathname = match[0];
602
+ let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1");
603
+ let captureGroups = match.slice(1);
604
+ let params = paramNames.reduce((memo, paramName, index) => {
605
+ // We need to compute the pathnameBase here using the raw splat value
606
+ // instead of using params["*"] later because it will be decoded then
607
+ if (paramName === "*") {
608
+ let splatValue = captureGroups[index] || "";
609
+ pathnameBase = matchedPathname.slice(0, matchedPathname.length - splatValue.length).replace(/(.)\/+$/, "$1");
610
+ }
611
+
612
+ memo[paramName] = safelyDecodeURIComponent(captureGroups[index] || "", paramName);
613
+ return memo;
614
+ }, {});
615
+ return {
616
+ params,
617
+ pathname: matchedPathname,
618
+ pathnameBase,
619
+ pattern
620
+ };
621
+ }
622
+
623
+ function compilePath(path, caseSensitive = false, end = true) {
624
+ warning$1(path === "*" || !path.endsWith("*") || path.endsWith("/*"), `Route path "${path}" will be treated as if it were ` + `"${path.replace(/\*$/, "/*")}" because the \`*\` character must ` + `always follow a \`/\` in the pattern. To get rid of this warning, ` + `please change the route path to "${path.replace(/\*$/, "/*")}".`) ;
625
+ let paramNames = [];
626
+ let regexpSource = "^" + path.replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
627
+ .replace(/^\/*/, "/") // Make sure it has a leading /
628
+ .replace(/[\\.*+^$?{}|()[\]]/g, "\\$&") // Escape special regex chars
629
+ .replace(/:(\w+)/g, (_, paramName) => {
630
+ paramNames.push(paramName);
631
+ return "([^\\/]+)";
632
+ });
633
+
634
+ if (path.endsWith("*")) {
635
+ paramNames.push("*");
636
+ regexpSource += path === "*" || path === "/*" ? "(.*)$" // Already matched the initial /, just match the rest
637
+ : "(?:\\/(.+)|\\/*)$"; // Don't include the / in params["*"]
638
+ } else {
639
+ regexpSource += end ? "\\/*$" // When matching to the end, ignore trailing slashes
640
+ : // Otherwise, match a word boundary or a proceeding /. The word boundary restricts
641
+ // parent routes to matching only their own words and nothing more, e.g. parent
642
+ // route "/home" should not match "/home2".
643
+ // Additionally, allow paths starting with `.`, `-`, `~`, and url-encoded entities,
644
+ // but do not consume the character in the matched path so they can match against
645
+ // nested paths.
646
+ "(?:(?=[@.~-]|%[0-9A-F]{2})|\\b|\\/|$)";
647
+ }
648
+
649
+ let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");
650
+ return [matcher, paramNames];
651
+ }
652
+
653
+ function safelyDecodeURIComponent(value, paramName) {
654
+ try {
655
+ return decodeURIComponent(value);
656
+ } catch (error) {
657
+ warning$1(false, `The value for the URL param "${paramName}" will not be decoded because` + ` the string "${value}" is a malformed URL segment. This is probably` + ` due to a bad percent encoding (${error}).`) ;
658
+ return value;
659
+ }
660
+ }
661
+ /**
662
+ * @private
663
+ */
664
+
665
+
666
+ function stripBasename(pathname, basename) {
667
+ if (basename === "/") return pathname;
668
+
669
+ if (!pathname.toLowerCase().startsWith(basename.toLowerCase())) {
670
+ return null;
671
+ }
672
+
673
+ let nextChar = pathname.charAt(basename.length);
674
+
675
+ if (nextChar && nextChar !== "/") {
676
+ // pathname does not start with basename/
677
+ return null;
678
+ }
679
+
680
+ return pathname.slice(basename.length) || "/";
681
+ }
682
+ /**
683
+ * @private
684
+ */
685
+
686
+ function invariant(value, message) {
687
+ if (value === false || value === null || typeof value === "undefined") {
688
+ throw new Error(message);
689
+ }
690
+ }
691
+ /**
692
+ * @private
693
+ */
694
+
695
+ function warning$1(cond, message) {
696
+ if (!cond) {
697
+ // eslint-disable-next-line no-console
698
+ if (typeof console !== "undefined") console.warn(message);
699
+
700
+ try {
701
+ // Welcome to debugging React Router!
702
+ //
703
+ // This error is thrown as a convenience so you can more easily
704
+ // find the source for a warning that appears in the console by
705
+ // enabling "pause on exceptions" in your JavaScript debugger.
706
+ throw new Error(message); // eslint-disable-next-line no-empty
707
+ } catch (e) {}
708
+ }
709
+ }
710
+ /**
711
+ * Returns a resolved path object relative to the given pathname.
712
+ *
713
+ * @see https://reactrouter.com/docs/en/v6/utils/resolve-path
714
+ */
715
+
716
+ function resolvePath(to, fromPathname = "/") {
717
+ let {
718
+ pathname: toPathname,
719
+ search = "",
720
+ hash = ""
721
+ } = typeof to === "string" ? parsePath(to) : to;
722
+ let pathname = toPathname ? toPathname.startsWith("/") ? toPathname : resolvePathname(toPathname, fromPathname) : fromPathname;
723
+ return {
724
+ pathname,
725
+ search: normalizeSearch(search),
726
+ hash: normalizeHash(hash)
727
+ };
728
+ }
729
+
730
+ function resolvePathname(relativePath, fromPathname) {
731
+ let segments = fromPathname.replace(/\/+$/, "").split("/");
732
+ let relativeSegments = relativePath.split("/");
733
+ relativeSegments.forEach(segment => {
734
+ if (segment === "..") {
735
+ // Keep the root "" segment so the pathname starts at /
736
+ if (segments.length > 1) segments.pop();
737
+ } else if (segment !== ".") {
738
+ segments.push(segment);
739
+ }
740
+ });
741
+ return segments.length > 1 ? segments.join("/") : "/";
742
+ }
743
+ /**
744
+ * @private
745
+ */
746
+
747
+
748
+ function resolveTo(toArg, routePathnames, locationPathname) {
749
+ let to = typeof toArg === "string" ? parsePath(toArg) : toArg;
750
+ let toPathname = toArg === "" || to.pathname === "" ? "/" : to.pathname; // If a pathname is explicitly provided in `to`, it should be relative to the
751
+ // route context. This is explained in `Note on `<Link to>` values` in our
752
+ // migration guide from v5 as a means of disambiguation between `to` values
753
+ // that begin with `/` and those that do not. However, this is problematic for
754
+ // `to` values that do not provide a pathname. `to` can simply be a search or
755
+ // hash string, in which case we should assume that the navigation is relative
756
+ // to the current location's pathname and *not* the route pathname.
757
+
758
+ let from;
759
+
760
+ if (toPathname == null) {
761
+ from = locationPathname;
762
+ } else {
763
+ let routePathnameIndex = routePathnames.length - 1;
764
+
765
+ if (toPathname.startsWith("..")) {
766
+ let toSegments = toPathname.split("/"); // Each leading .. segment means "go up one route" instead of "go up one
767
+ // URL segment". This is a key difference from how <a href> works and a
768
+ // major reason we call this a "to" value instead of a "href".
769
+
770
+ while (toSegments[0] === "..") {
771
+ toSegments.shift();
772
+ routePathnameIndex -= 1;
773
+ }
774
+
775
+ to.pathname = toSegments.join("/");
776
+ } // If there are more ".." segments than parent routes, resolve relative to
777
+ // the root / URL.
778
+
779
+
780
+ from = routePathnameIndex >= 0 ? routePathnames[routePathnameIndex] : "/";
781
+ }
782
+
783
+ let path = resolvePath(to, from); // Ensure the pathname has a trailing slash if the original to value had one.
784
+
785
+ if (toPathname && toPathname !== "/" && toPathname.endsWith("/") && !path.pathname.endsWith("/")) {
786
+ path.pathname += "/";
787
+ }
788
+
789
+ return path;
790
+ }
791
+ /**
792
+ * @private
793
+ */
794
+
795
+ function getToPathname(to) {
796
+ // Empty strings should be treated the same as / paths
797
+ return to === "" || to.pathname === "" ? "/" : typeof to === "string" ? parsePath(to).pathname : to.pathname;
798
+ }
799
+ /**
800
+ * @private
801
+ */
802
+
803
+ const joinPaths = paths => paths.join("/").replace(/\/\/+/g, "/");
804
+ /**
805
+ * @private
806
+ */
807
+
808
+ const normalizePathname = pathname => pathname.replace(/\/+$/, "").replace(/^\/*/, "/");
809
+ /**
810
+ * @private
811
+ */
812
+
813
+ const normalizeSearch = search => !search || search === "?" ? "" : search.startsWith("?") ? search : "?" + search;
814
+ /**
815
+ * @private
816
+ */
817
+
818
+ const normalizeHash = hash => !hash || hash === "#" ? "" : hash.startsWith("#") ? hash : "#" + hash;
819
+
820
+ /**
821
+ * This is a shortcut for creating `application/json` responses. Converts `data`
822
+ * to JSON and sets the `Content-Type` header.
823
+ */
824
+ const json = (data, init = {}) => {
825
+ let responseInit = typeof init === "number" ? {
826
+ status: init
827
+ } : init;
828
+ let headers = new Headers(responseInit.headers);
829
+
830
+ if (!headers.has("Content-Type")) {
831
+ headers.set("Content-Type", "application/json; charset=utf-8");
832
+ }
833
+
834
+ return new Response(JSON.stringify(data), { ...responseInit,
835
+ headers
836
+ });
837
+ };
838
+
839
+ /**
840
+ * A redirect response. Sets the status code and the `Location` header.
841
+ * Defaults to "302 Found".
842
+ */
843
+ const redirect = (url, init = 302) => {
844
+ let responseInit = init;
845
+
846
+ if (typeof responseInit === "number") {
847
+ responseInit = {
848
+ status: responseInit
849
+ };
850
+ } else if (typeof responseInit.status === "undefined") {
851
+ responseInit.status = 302;
852
+ }
853
+
854
+ let headers = new Headers(responseInit.headers);
855
+ headers.set("Location", url);
856
+ return new Response(null, { ...responseInit,
857
+ headers
858
+ });
859
+ };
860
+ /**
861
+ * @private
862
+ * Utility class we use to hold auto-unwrapped 4xx/5xx Response bodies
863
+ */
864
+
865
+ class ErrorResponse {
866
+ constructor(status, statusText, data) {
867
+ this.status = status;
868
+ this.statusText = statusText || "";
869
+ this.data = data;
870
+ }
871
+
872
+ }
873
+ /**
874
+ * Check if the given error is an ErrorResponse generated from a 4xx/5xx
875
+ * Response throw from an action/loader
876
+ */
877
+
878
+ function isRouteErrorResponse(e) {
879
+ return e instanceof ErrorResponse;
880
+ }
881
+
882
+ //#region Types and Constants
883
+ ////////////////////////////////////////////////////////////////////////////////
884
+
885
+ /**
886
+ * Map of routeId -> data returned from a loader/action/error
887
+ */
888
+
889
+ var ResultType;
890
+ /**
891
+ * Successful result from a loader or action
892
+ */
893
+
894
+ (function (ResultType) {
895
+ ResultType["data"] = "data";
896
+ ResultType["redirect"] = "redirect";
897
+ ResultType["error"] = "error";
898
+ })(ResultType || (ResultType = {}));
899
+
900
+ const IDLE_NAVIGATION = {
901
+ state: "idle",
902
+ location: undefined,
903
+ formMethod: undefined,
904
+ formAction: undefined,
905
+ formEncType: undefined,
906
+ formData: undefined
907
+ };
908
+ const IDLE_FETCHER = {
909
+ state: "idle",
910
+ data: undefined,
911
+ formMethod: undefined,
912
+ formAction: undefined,
913
+ formEncType: undefined,
914
+ formData: undefined
915
+ }; //#endregion
916
+ ////////////////////////////////////////////////////////////////////////////////
917
+ //#region createRouter
918
+ ////////////////////////////////////////////////////////////////////////////////
919
+
920
+ /**
921
+ * Create a router and listen to history POP navigations
922
+ */
923
+
924
+ function createRouter(init) {
925
+ !(init.routes.length > 0) ? invariant(false, "You must provide a non-empty routes array to use Data Routers") : void 0;
926
+ let dataRoutes = convertRoutesToDataRoutes(init.routes); // Cleanup function for history
927
+
928
+ let unlistenHistory = null; // Externally-provided function to call on all state changes
929
+
930
+ let subscriber = null; // Externally-provided object to hold scroll restoration locations during routing
931
+
932
+ let savedScrollPositions = null; // Externally-provided function to get scroll restoration keys
933
+
934
+ let getScrollRestorationKey = null; // Externally-provided function to get current scroll position
935
+
936
+ let getScrollPosition = null; // One-time flag to control the initial hydration scroll restoration. Because
937
+ // we don't get the saved positions from <ScrollRestoration /> until _after_
938
+ // the initial render, we need to manually trigger a separate updateState to
939
+ // send along the restoreScrollPosition
940
+
941
+ let initialScrollRestored = false;
942
+ let initialMatches = matchRoutes(dataRoutes, init.history.location) || getNotFoundMatches(dataRoutes); // If we received hydration data without errors - detect if any matched
943
+ // routes with loaders did not get provided loaderData, and if so launch an
944
+ // initial data re-load to fetch everything
945
+
946
+ let foundMissingHydrationData = init.hydrationData?.errors == null && init.hydrationData?.loaderData != null && initialMatches.filter(m => m.route.loader).some(m => init.hydrationData?.loaderData?.[m.route.id] === undefined);
947
+
948
+ if (foundMissingHydrationData) {
949
+ console.warn(`The provided hydration data did not find loaderData for all matched ` + `routes with loaders. Performing a full initial data load`);
950
+ }
951
+
952
+ let router;
953
+ let state = {
954
+ historyAction: init.history.action,
955
+ location: init.history.location,
956
+ // If we do not match a user-provided-route, fall back to the root
957
+ // to allow the errorElement to take over
958
+ matches: initialMatches,
959
+ initialized: init.hydrationData != null && !foundMissingHydrationData,
960
+ navigation: IDLE_NAVIGATION,
961
+ restoreScrollPosition: null,
962
+ resetScrollPosition: true,
963
+ revalidation: "idle",
964
+ loaderData: foundMissingHydrationData ? {} : init.hydrationData?.loaderData || {},
965
+ actionData: init.hydrationData?.actionData || null,
966
+ errors: init.hydrationData?.errors || null,
967
+ fetchers: new Map()
968
+ }; // -- Stateful internal variables to manage navigations --
969
+ // Current navigation in progress (to be committed in completeNavigation)
970
+
971
+ let pendingAction = null; // AbortController for the active navigation
972
+
973
+ let pendingNavigationController; // We use this to avoid touching history in completeNavigation if a
974
+ // revalidation is entirely uninterrupted
975
+
976
+ let isUninterruptedRevalidation = false; // Use this internal flag to force revalidation of all loaders:
977
+ // - submissions (completed or interrupted)
978
+ // - useRevalidate()
979
+ // - X-Remix-Revalidate (from redirect)
980
+
981
+ let isRevalidationRequired = false; // AbortControllers for any in-flight fetchers
982
+
983
+ let fetchControllers = new Map(); // Track loads based on the order in which they started
984
+
985
+ let incrementingLoadId = 0; // Track the outstanding pending navigation data load to be compared against
986
+ // the globally incrementing load when a fetcher load lands after a completed
987
+ // navigation
988
+
989
+ let pendingNavigationLoadId = -1; // Fetchers that triggered data reloads as a result of their actions
990
+
991
+ let fetchReloadIds = new Map(); // Fetchers that triggered redirect navigations from their actions
992
+
993
+ let fetchRedirectIds = new Set(); // Most recent href/match for fetcher.load calls for fetchers
994
+
995
+ let fetchLoadMatches = new Map(); // Initialize the router, all side effects should be kicked off from here.
996
+ // Implemented as a Fluent API for ease of:
997
+ // let router = createRouter(init).initialize();
998
+
999
+ function initialize() {
1000
+ // If history informs us of a POP navigation, start the navigation but do not update
1001
+ // state. We'll update our own state once the navigation completes
1002
+ unlistenHistory = init.history.listen(({
1003
+ action: historyAction,
1004
+ location
1005
+ }) => startNavigation(historyAction, location)); // Kick off initial data load if needed. Use Pop to avoid modifying history
1006
+
1007
+ if (!state.initialized) {
1008
+ startNavigation(Action.Pop, state.location);
1009
+ }
1010
+
1011
+ return router;
1012
+ } // Clean up a router and it's side effects
1013
+
1014
+
1015
+ function dispose() {
1016
+ if (unlistenHistory) {
1017
+ unlistenHistory();
1018
+ }
1019
+
1020
+ subscriber = null;
1021
+ pendingNavigationController?.abort();
1022
+
1023
+ for (let [, controller] of fetchControllers) {
1024
+ controller.abort();
1025
+ }
1026
+ } // Subscribe to state updates for the router
1027
+
1028
+
1029
+ function subscribe(fn) {
1030
+ if (subscriber) {
1031
+ throw new Error("A router only accepts one active subscriber");
1032
+ }
1033
+
1034
+ subscriber = fn;
1035
+ return () => {
1036
+ subscriber = null;
1037
+ };
1038
+ } // Update our state and notify the calling context of the change
1039
+
1040
+
1041
+ function updateState(newState) {
1042
+ state = { ...state,
1043
+ ...newState
1044
+ };
1045
+ subscriber?.(state);
1046
+ } // Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION
1047
+ // and setting state.[historyAction/location/matches] to the new route.
1048
+ // - HistoryAction and Location are required params
1049
+ // - Navigation will always be set to IDLE_NAVIGATION
1050
+ // - Can pass any other state in newState
1051
+
1052
+
1053
+ function completeNavigation(historyAction, location, newState) {
1054
+ // Deduce if we're in a loading/actionReload state:
1055
+ // - We have committed actionData in the store
1056
+ // - The current navigation was a submission
1057
+ // - We're past the submitting state and into the loading state
1058
+ // - This should not be susceptible to false positives for
1059
+ // loading/submissionRedirect since there would not be actionData in the
1060
+ // state since the prior action would have returned a redirect response
1061
+ // and short circuited
1062
+ let isActionReload = state.actionData != null && state.navigation.formMethod != null && state.navigation.state === "loading";
1063
+ updateState({ // Clear existing actionData on any completed navigation beyond the original
1064
+ // action, unless we're currently finishing the loading/actionReload state.
1065
+ // Do this prior to spreading in newState in case we got back to back actions
1066
+ ...(isActionReload ? {} : {
1067
+ actionData: null
1068
+ }),
1069
+ ...newState,
1070
+ historyAction,
1071
+ location,
1072
+ initialized: true,
1073
+ navigation: IDLE_NAVIGATION,
1074
+ revalidation: "idle",
1075
+ // Always preserve any existing loaderData from re-used routes
1076
+ loaderData: mergeLoaderData(state, newState),
1077
+ // Don't restore on submission navigations
1078
+ restoreScrollPosition: state.navigation.formData ? false : getSavedScrollPosition(location, newState.matches || state.matches),
1079
+ // Always reset scroll unless explicitly told not to
1080
+ resetScrollPosition: location.state?.__resetScrollPosition !== false
1081
+ });
1082
+
1083
+ if (isUninterruptedRevalidation) ; else if (historyAction === Action.Pop) ; else if (historyAction === Action.Push) {
1084
+ init.history.push(location, location.state);
1085
+ } else if (historyAction === Action.Replace) {
1086
+ init.history.replace(location, location.state);
1087
+ } // Reset stateful navigation vars
1088
+
1089
+
1090
+ pendingAction = null;
1091
+ isUninterruptedRevalidation = false;
1092
+ isRevalidationRequired = false;
1093
+ } // Trigger a navigation event, which can either be a numerical POP or a PUSH
1094
+ // replace with an optional submission
1095
+
1096
+
1097
+ async function navigate(path, opts) {
1098
+ if (typeof path === "number") {
1099
+ init.history.go(path);
1100
+ return;
1101
+ }
1102
+
1103
+ let location = createLocation(state.location, path, opts?.state);
1104
+ let historyAction = opts?.replace ? Action.Replace : Action.Push;
1105
+
1106
+ if (isSubmissionNavigation(opts)) {
1107
+ return await startNavigation(historyAction, location, {
1108
+ submission: {
1109
+ formMethod: opts.formMethod || "get",
1110
+ formAction: createHref(location),
1111
+ formEncType: opts?.formEncType || "application/x-www-form-urlencoded",
1112
+ formData: opts.formData
1113
+ }
1114
+ });
1115
+ }
1116
+
1117
+ return await startNavigation(historyAction, location);
1118
+ } // Revalidate all current loaders. If a navigation is in progress or if this
1119
+ // is interrupted by a navigation, allow this to "succeed" by calling all
1120
+ // loaders during the next loader round
1121
+
1122
+
1123
+ function revalidate() {
1124
+ // Toggle isRevalidationRequired so the next data load will call all loaders,
1125
+ // and mark us in a revalidating state
1126
+ isRevalidationRequired = true;
1127
+ updateState({
1128
+ revalidation: "loading"
1129
+ }); // If we're currently submitting an action, we don't need to start a new
1130
+ // navigation, we'll just let the follow up loader execution call all loaders
1131
+
1132
+ if (state.navigation.state === "submitting" && state.navigation.formMethod !== "get") {
1133
+ return;
1134
+ } // If we're currently in an idle state, start a new navigation for the current
1135
+ // action/location and mark it as uninterrupted, which will skip the history
1136
+ // update in completeNavigation
1137
+
1138
+
1139
+ if (state.navigation.state === "idle") {
1140
+ startNavigation(state.historyAction, state.location, {
1141
+ startUninterruptedRevalidation: true
1142
+ });
1143
+ return;
1144
+ } // Otherwise, if we're currently in a loading state, just start a new
1145
+ // navigation to the navigation.location but do not trigger an uninterrupted
1146
+ // revalidation so that history correctly updates once the navigation completes
1147
+
1148
+
1149
+ startNavigation(pendingAction || state.historyAction, state.navigation.location, {
1150
+ overrideNavigation: state.navigation
1151
+ });
1152
+ } // Start a navigation to the given action/location. Can optionally provide a
1153
+ // overrideNavigation which will override the normalLoad in the case of a redirect
1154
+ // navigation
1155
+
1156
+
1157
+ async function startNavigation(historyAction, location, opts) {
1158
+ // Abort any in-progress navigations and start a new one
1159
+ pendingNavigationController?.abort();
1160
+ pendingAction = historyAction; // Unset any ongoing uninterrupted revalidations (unless told otherwise),
1161
+ // since we want this new navigation to update history normally
1162
+
1163
+ isUninterruptedRevalidation = opts?.startUninterruptedRevalidation === true; // Save the current scroll position every time we start a new navigation
1164
+
1165
+ saveScrollPosition(state.location, state.matches);
1166
+ let loadingNavigation = opts?.overrideNavigation;
1167
+ let matches = matchRoutes(dataRoutes, location); // Short circuit with a 404 on the root error boundary if we match nothing
1168
+
1169
+ if (!matches) {
1170
+ completeNavigation(historyAction, location, {
1171
+ matches: getNotFoundMatches(dataRoutes),
1172
+ errors: {
1173
+ [dataRoutes[0].id]: new Response(null, {
1174
+ status: 404
1175
+ })
1176
+ }
1177
+ });
1178
+ return;
1179
+ } // Short circuit if it's only a hash change
1180
+
1181
+
1182
+ if (isHashChangeOnly(state.location, location)) {
1183
+ completeNavigation(historyAction, location, {
1184
+ matches
1185
+ });
1186
+ return;
1187
+ } // Call action if we received an action submission
1188
+
1189
+
1190
+ let pendingActionData = null;
1191
+ let pendingActionError = null;
1192
+
1193
+ if (opts?.submission && isActionSubmission(opts.submission)) {
1194
+ let actionOutput = await handleAction(historyAction, location, opts.submission, matches);
1195
+
1196
+ if (actionOutput.shortCircuited) {
1197
+ return;
1198
+ }
1199
+
1200
+ pendingActionData = actionOutput.pendingActionData || null;
1201
+ pendingActionError = actionOutput.pendingActionError || null;
1202
+ let navigation = {
1203
+ state: "loading",
1204
+ location,
1205
+ ...opts.submission
1206
+ };
1207
+ loadingNavigation = navigation;
1208
+ } // Call loaders
1209
+
1210
+
1211
+ let {
1212
+ shortCircuited,
1213
+ loaderData,
1214
+ errors
1215
+ } = await handleLoaders(historyAction, location, opts?.submission, matches, loadingNavigation, pendingActionData, pendingActionError);
1216
+
1217
+ if (shortCircuited) {
1218
+ return;
1219
+ }
1220
+
1221
+ completeNavigation(historyAction, location, {
1222
+ matches,
1223
+ loaderData,
1224
+ errors
1225
+ });
1226
+ } // Call the action matched by the leaf route for this navigation and handle
1227
+ // redirects/errors
1228
+
1229
+
1230
+ async function handleAction(historyAction, location, submission, matches) {
1231
+ isRevalidationRequired = true;
1232
+
1233
+ if (matches[matches.length - 1].route.index && !hasNakedIndexQuery(location.search)) {
1234
+ // Note: OK to mutate this in-place since it's a scoped var inside
1235
+ // handleAction and mutation will not impact the startNavigation matches
1236
+ // variable that we use for revalidation
1237
+ matches = matches.slice(0, -1);
1238
+ } // Put us in a submitting state
1239
+
1240
+
1241
+ let navigation = {
1242
+ state: "submitting",
1243
+ location,
1244
+ ...submission
1245
+ };
1246
+ updateState({
1247
+ navigation
1248
+ }); // Call our action and get the result
1249
+
1250
+ let result;
1251
+ let actionMatch = matches.slice(-1)[0];
1252
+
1253
+ if (!actionMatch.route.action) {
1254
+ {
1255
+ console.warn("You're trying to submit to a route that does not have an action. To " + "fix this, please add an `action` function to the route for " + `[${createHref(location)}]`);
1256
+ }
1257
+
1258
+ result = {
1259
+ type: ResultType.error,
1260
+ error: new Response(null, {
1261
+ status: 405
1262
+ })
1263
+ };
1264
+ } else {
1265
+ // Create a controller for this data load
1266
+ let actionAbortController = new AbortController();
1267
+ pendingNavigationController = actionAbortController;
1268
+ result = await callLoaderOrAction(actionMatch, location, actionAbortController.signal, submission);
1269
+
1270
+ if (actionAbortController.signal.aborted) {
1271
+ return {
1272
+ shortCircuited: true
1273
+ };
1274
+ } // Clean up now that the loaders have completed. We do do not clean up if
1275
+ // we short circuited because pendingNavigationController will have already
1276
+ // been assigned to a new controller for the next navigation
1277
+
1278
+
1279
+ pendingNavigationController = null;
1280
+ } // If the action threw a redirect Response, start a new REPLACE navigation
1281
+
1282
+
1283
+ if (isRedirectResult(result)) {
1284
+ let redirectNavigation = {
1285
+ state: "loading",
1286
+ location: createLocation(state.location, result.location),
1287
+ ...submission
1288
+ };
1289
+ await startRedirectNavigation(result, redirectNavigation);
1290
+ return {
1291
+ shortCircuited: true
1292
+ };
1293
+ }
1294
+
1295
+ if (isErrorResult(result)) {
1296
+ // Store off the pending error - we use it to determine which loaders
1297
+ // to call and will commit it when we complete the navigation
1298
+ let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
1299
+ return {
1300
+ pendingActionError: {
1301
+ [boundaryMatch.route.id]: result.error
1302
+ }
1303
+ };
1304
+ }
1305
+
1306
+ return {
1307
+ pendingActionData: {
1308
+ [actionMatch.route.id]: result.data
1309
+ }
1310
+ };
1311
+ } // Call all applicable loaders for the given matches, handling redirects,
1312
+ // errors, etc.
1313
+
1314
+
1315
+ async function handleLoaders(historyAction, location, submission, matches, overrideNavigation, pendingActionData, pendingActionError) {
1316
+ // Figure out the right navigation we want to use for data loading
1317
+ let loadingNavigation;
1318
+
1319
+ if (overrideNavigation) {
1320
+ loadingNavigation = overrideNavigation;
1321
+ } else if (submission?.formMethod === "get") {
1322
+ let navigation = {
1323
+ state: "submitting",
1324
+ location,
1325
+ ...submission
1326
+ };
1327
+ loadingNavigation = navigation;
1328
+ } else {
1329
+ let navigation = {
1330
+ state: "loading",
1331
+ location,
1332
+ formMethod: undefined,
1333
+ formAction: undefined,
1334
+ formEncType: undefined,
1335
+ formData: undefined
1336
+ };
1337
+ loadingNavigation = navigation;
1338
+ }
1339
+
1340
+ let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(state, matches, submission, location, isRevalidationRequired, pendingActionData, pendingActionError, fetchLoadMatches); // Short circuit if we have no loaders to run
1341
+
1342
+ if (matchesToLoad.length === 0 && revalidatingFetchers.length === 0) {
1343
+ completeNavigation(historyAction, location, {
1344
+ matches,
1345
+ // Commit pending action error if we're short circuiting
1346
+ errors: pendingActionError || null,
1347
+ actionData: pendingActionData || null
1348
+ });
1349
+ return {
1350
+ shortCircuited: true
1351
+ };
1352
+ } // If this is an uninterrupted revalidation, remain in our current idle state.
1353
+ // Otherwise, switch to our loading state and load data, preserving any
1354
+ // new action data or existing action data (in the case of a revalidation
1355
+ // interrupting an actionReload)
1356
+
1357
+
1358
+ if (!isUninterruptedRevalidation) {
1359
+ revalidatingFetchers.forEach(([key]) => {
1360
+ let revalidatingFetcher = {
1361
+ state: "loading",
1362
+ data: state.fetchers.get(key)?.data,
1363
+ formMethod: undefined,
1364
+ formAction: undefined,
1365
+ formEncType: undefined,
1366
+ formData: undefined
1367
+ };
1368
+ state.fetchers.set(key, revalidatingFetcher);
1369
+ });
1370
+ updateState({
1371
+ navigation: loadingNavigation,
1372
+ actionData: pendingActionData || state.actionData || null,
1373
+ ...(revalidatingFetchers.length > 0 ? {
1374
+ fetchers: new Map(state.fetchers)
1375
+ } : {})
1376
+ });
1377
+ } // Start the data load
1378
+
1379
+
1380
+ let abortController = new AbortController();
1381
+ pendingNavigationController = abortController;
1382
+ pendingNavigationLoadId = ++incrementingLoadId;
1383
+ revalidatingFetchers.forEach(([key]) => fetchControllers.set(key, abortController)); // Call all navigation loaders and revalidating fetcher loaders in parallel,
1384
+ // then slice off the results into separate arrays so we can handle them
1385
+ // accordingly
1386
+
1387
+ let results = await Promise.all([...matchesToLoad.map(m => callLoaderOrAction(m, location, abortController.signal)), ...revalidatingFetchers.map(([, href, match]) => callLoaderOrAction(match, href, abortController.signal))]);
1388
+ let navigationResults = results.slice(0, matchesToLoad.length);
1389
+ let fetcherResults = results.slice(matchesToLoad.length);
1390
+
1391
+ if (abortController.signal.aborted) {
1392
+ return {
1393
+ shortCircuited: true
1394
+ };
1395
+ } // Clean up now that the loaders have completed. We do do not clean up if
1396
+ // we short circuited because pendingNavigationController will have already
1397
+ // been assigned to a new controller for the next navigation
1398
+
1399
+
1400
+ pendingNavigationController = null;
1401
+ revalidatingFetchers.forEach(key => fetchControllers.delete(key)); // If any loaders returned a redirect Response, start a new REPLACE navigation
1402
+
1403
+ let redirect = findRedirect(results);
1404
+
1405
+ if (redirect) {
1406
+ let redirectNavigation = getLoaderRedirect(state, redirect);
1407
+ await startRedirectNavigation(redirect, redirectNavigation);
1408
+ return {
1409
+ shortCircuited: true
1410
+ };
1411
+ } // Process and commit output from loaders
1412
+
1413
+
1414
+ let {
1415
+ loaderData,
1416
+ errors
1417
+ } = processLoaderData(state, matches, matchesToLoad, navigationResults, pendingActionError, revalidatingFetchers, fetcherResults);
1418
+ markFetchRedirectsDone();
1419
+ let didAbortFetchLoads = abortStaleFetchLoads(pendingNavigationLoadId);
1420
+ return {
1421
+ loaderData,
1422
+ errors,
1423
+ ...(didAbortFetchLoads || revalidatingFetchers.length > 0 ? {
1424
+ fetchers: new Map(state.fetchers)
1425
+ } : {})
1426
+ };
1427
+ }
1428
+
1429
+ function getFetcher(key) {
1430
+ return state.fetchers.get(key) || IDLE_FETCHER;
1431
+ } // Trigger a fetcher load/submit for the given fetcher key
1432
+
1433
+
1434
+ function fetch(key, href, opts) {
1435
+ if (typeof AbortController === "undefined") {
1436
+ throw new Error("router.fetch() was called during the server render, but it shouldn't be. " + "You are likely calling a useFetcher() method in the body of your component. " + "Try moving it to a useEffect or a callback.");
1437
+ }
1438
+
1439
+ let matches = matchRoutes(dataRoutes, href);
1440
+ !matches ? invariant(false, `No matches found for fetch url: ${href}`) : void 0;
1441
+ if (fetchControllers.has(key)) abortFetcher(key);
1442
+ let match = matches[matches.length - 1].route.index && !hasNakedIndexQuery(parsePath(href).search || "") ? matches.slice(-2)[0] : matches.slice(-1)[0];
1443
+
1444
+ if (isSubmissionNavigation(opts)) {
1445
+ let submission = {
1446
+ formMethod: opts.formMethod || "get",
1447
+ formAction: href,
1448
+ formEncType: opts.formEncType || "application/x-www-form-urlencoded",
1449
+ formData: opts.formData
1450
+ };
1451
+
1452
+ if (isActionSubmission(submission)) {
1453
+ handleFetcherAction(key, href, match, submission);
1454
+ return;
1455
+ }
1456
+
1457
+ let _loadingFetcher = {
1458
+ state: "submitting",
1459
+ ...submission,
1460
+ data: state.fetchers.get(key)?.data || undefined
1461
+ };
1462
+ handleFetcherLoader(key, href, match, _loadingFetcher);
1463
+ return;
1464
+ }
1465
+
1466
+ let loadingFetcher = {
1467
+ state: "loading",
1468
+ formMethod: undefined,
1469
+ formAction: undefined,
1470
+ formEncType: undefined,
1471
+ formData: undefined,
1472
+ data: state.fetchers.get(key)?.data || undefined
1473
+ };
1474
+ handleFetcherLoader(key, href, match, loadingFetcher);
1475
+ } // Call the action for the matched fetcher.submit(), and then handle redirects,
1476
+ // errors, and revalidation
1477
+
1478
+
1479
+ async function handleFetcherAction(key, href, match, submission) {
1480
+ isRevalidationRequired = true;
1481
+ fetchLoadMatches.delete(key); // Put this fetcher into it's submitting state
1482
+
1483
+ let fetcher = {
1484
+ state: "submitting",
1485
+ ...submission,
1486
+ data: state.fetchers.get(key)?.data || undefined
1487
+ };
1488
+ state.fetchers.set(key, fetcher);
1489
+ updateState({
1490
+ fetchers: new Map(state.fetchers)
1491
+ }); // Call the action for the fetcher
1492
+
1493
+ let abortController = new AbortController();
1494
+ fetchControllers.set(key, abortController);
1495
+ let actionResult = await callLoaderOrAction(match, href, abortController.signal, submission);
1496
+
1497
+ if (abortController.signal.aborted) {
1498
+ return;
1499
+ }
1500
+
1501
+ if (isRedirectResult(actionResult)) {
1502
+ fetchRedirectIds.add(key);
1503
+ let loadingFetcher = {
1504
+ state: "loading",
1505
+ ...submission,
1506
+ data: undefined
1507
+ };
1508
+ state.fetchers.set(key, loadingFetcher);
1509
+ updateState({
1510
+ fetchers: new Map(state.fetchers)
1511
+ });
1512
+ let redirectNavigation = {
1513
+ state: "loading",
1514
+ location: createLocation(state.location, actionResult.location),
1515
+ ...submission
1516
+ };
1517
+ await startRedirectNavigation(actionResult, redirectNavigation);
1518
+ return;
1519
+ } // Process any non-redirect errors thrown
1520
+
1521
+
1522
+ if (isErrorResult(actionResult)) {
1523
+ let boundaryMatch = findNearestBoundary(state.matches, match.route.id);
1524
+ state.fetchers.delete(key);
1525
+ updateState({
1526
+ fetchers: new Map(state.fetchers),
1527
+ errors: {
1528
+ [boundaryMatch.route.id]: actionResult.error
1529
+ }
1530
+ });
1531
+ return;
1532
+ } // Start the data load for current matches, or the next location if we're
1533
+ // in the middle of a navigation
1534
+
1535
+
1536
+ let nextLocation = state.navigation.location || state.location;
1537
+ let matches = state.navigation.state !== "idle" ? matchRoutes(dataRoutes, state.navigation.location) : state.matches;
1538
+ !matches ? invariant(false, "Didn't find any matches after fetcher action") : void 0;
1539
+ let loadId = ++incrementingLoadId;
1540
+ fetchReloadIds.set(key, loadId);
1541
+ let loadFetcher = {
1542
+ state: "loading",
1543
+ data: actionResult.data,
1544
+ ...submission
1545
+ };
1546
+ state.fetchers.set(key, loadFetcher);
1547
+ let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(state, matches, submission, nextLocation, isRevalidationRequired, null, null, fetchLoadMatches); // Put all revalidating fetchers into the loading state, except for the
1548
+ // current fetcher which we want to keep in it's current loading state which
1549
+ // contains it's action submission info + action data
1550
+
1551
+ revalidatingFetchers.filter(([staleKey]) => staleKey !== key).forEach(([staleKey]) => {
1552
+ let revalidatingFetcher = {
1553
+ state: "loading",
1554
+ data: state.fetchers.get(key)?.data,
1555
+ formMethod: undefined,
1556
+ formAction: undefined,
1557
+ formEncType: undefined,
1558
+ formData: undefined
1559
+ };
1560
+ state.fetchers.set(staleKey, revalidatingFetcher);
1561
+ fetchControllers.set(staleKey, abortController);
1562
+ });
1563
+ updateState({
1564
+ fetchers: new Map(state.fetchers)
1565
+ }); // Call all navigation loaders and revalidating fetcher loaders in parallel,
1566
+ // then slice off the results into separate arrays so we can handle them
1567
+ // accordingly
1568
+
1569
+ let results = await Promise.all([...matchesToLoad.map(m => callLoaderOrAction(m, nextLocation, abortController.signal)), ...revalidatingFetchers.map(([, href, match]) => callLoaderOrAction(match, href, abortController.signal))]);
1570
+ let loaderResults = results.slice(0, matchesToLoad.length);
1571
+ let fetcherResults = results.slice(matchesToLoad.length);
1572
+
1573
+ if (abortController.signal.aborted) {
1574
+ return;
1575
+ }
1576
+
1577
+ fetchReloadIds.delete(key);
1578
+ fetchControllers.delete(key);
1579
+ revalidatingFetchers.forEach(staleKey => fetchControllers.delete(staleKey));
1580
+ let loaderRedirect = findRedirect(loaderResults);
1581
+
1582
+ if (loaderRedirect) {
1583
+ let redirectNavigation = getLoaderRedirect(state, loaderRedirect);
1584
+ await startRedirectNavigation(loaderRedirect, redirectNavigation);
1585
+ return;
1586
+ } // Process and commit output from loaders
1587
+
1588
+
1589
+ let {
1590
+ loaderData,
1591
+ errors
1592
+ } = processLoaderData(state, state.matches, matchesToLoad, loaderResults, null, revalidatingFetchers, fetcherResults);
1593
+ let doneFetcher = {
1594
+ state: "idle",
1595
+ data: actionResult.data,
1596
+ formMethod: undefined,
1597
+ formAction: undefined,
1598
+ formEncType: undefined,
1599
+ formData: undefined
1600
+ };
1601
+ state.fetchers.set(key, doneFetcher);
1602
+ let didAbortFetchLoads = abortStaleFetchLoads(loadId); // If we are currently in a navigation loading state and this fetcher is
1603
+ // more recent than the navigation, we want the newer data so abort the
1604
+ // navigation and complete it with the fetcher data
1605
+
1606
+ if (state.navigation.state === "loading" && loadId > pendingNavigationLoadId) {
1607
+ !pendingAction ? invariant(false, "Expected pending action") : void 0;
1608
+ pendingNavigationController?.abort();
1609
+ completeNavigation(pendingAction, state.navigation.location, {
1610
+ matches,
1611
+ loaderData,
1612
+ errors,
1613
+ fetchers: new Map(state.fetchers)
1614
+ });
1615
+ } else {
1616
+ // otherwise just update with the fetcher data
1617
+ updateState({
1618
+ errors,
1619
+ loaderData,
1620
+ ...(didAbortFetchLoads ? {
1621
+ fetchers: new Map(state.fetchers)
1622
+ } : {})
1623
+ });
1624
+ isRevalidationRequired = false;
1625
+ }
1626
+ } // Call the matched loader for fetcher.load(), handling redirects, errors, etc.
1627
+
1628
+
1629
+ async function handleFetcherLoader(key, href, match, loadingFetcher) {
1630
+ // Put this fetcher into it's loading state
1631
+ state.fetchers.set(key, loadingFetcher);
1632
+ updateState({
1633
+ fetchers: new Map(state.fetchers)
1634
+ }); // Store off the match so we can call it's shouldRevalidate on subsequent
1635
+ // revalidations
1636
+
1637
+ fetchLoadMatches.set(key, [href, match]); // Call the loader for this fetcher route match
1638
+
1639
+ let abortController = new AbortController();
1640
+ fetchControllers.set(key, abortController);
1641
+ let result = await callLoaderOrAction(match, href, abortController.signal);
1642
+ if (abortController.signal.aborted) return;
1643
+ fetchControllers.delete(key); // If the loader threw a redirect Response, start a new REPLACE navigation
1644
+
1645
+ if (isRedirectResult(result)) {
1646
+ let redirectNavigation = getLoaderRedirect(state, result);
1647
+ await startRedirectNavigation(result, redirectNavigation);
1648
+ return;
1649
+ } // Process any non-redirect errors thrown
1650
+
1651
+
1652
+ if (isErrorResult(result)) {
1653
+ let boundaryMatch = findNearestBoundary(state.matches, match.route.id);
1654
+ state.fetchers.delete(key); // TODO: In remix, this would reset to IDLE_NAVIGATION if it was a catch -
1655
+ // do we need to behave any differently with our non-redirect errors?
1656
+ // What if it was a non-redirect Response?
1657
+
1658
+ updateState({
1659
+ fetchers: new Map(state.fetchers),
1660
+ errors: {
1661
+ [boundaryMatch.route.id]: result.error
1662
+ }
1663
+ });
1664
+ return;
1665
+ } // Put the fetcher back into an idle state
1666
+
1667
+
1668
+ let doneFetcher = {
1669
+ state: "idle",
1670
+ data: result.data,
1671
+ formMethod: undefined,
1672
+ formAction: undefined,
1673
+ formEncType: undefined,
1674
+ formData: undefined
1675
+ };
1676
+ state.fetchers.set(key, doneFetcher);
1677
+ updateState({
1678
+ fetchers: new Map(state.fetchers)
1679
+ });
1680
+ } // Utility function to handle redirects returned from an action or loader
1681
+
1682
+
1683
+ async function startRedirectNavigation(redirect, navigation) {
1684
+ if (redirect.revalidate) {
1685
+ isRevalidationRequired = true;
1686
+ }
1687
+
1688
+ !navigation.location ? invariant(false, "Expected a location on the redirect navigation") : void 0;
1689
+ await startNavigation(Action.Replace, navigation.location, {
1690
+ overrideNavigation: navigation
1691
+ });
1692
+ }
1693
+
1694
+ function deleteFetcher(key) {
1695
+ if (fetchControllers.has(key)) abortFetcher(key);
1696
+ fetchLoadMatches.delete(key);
1697
+ fetchReloadIds.delete(key);
1698
+ fetchRedirectIds.delete(key);
1699
+ state.fetchers.delete(key);
1700
+ }
1701
+
1702
+ function abortFetcher(key) {
1703
+ let controller = fetchControllers.get(key);
1704
+ !controller ? invariant(false, `Expected fetch controller: ${key}`) : void 0;
1705
+ controller.abort();
1706
+ fetchControllers.delete(key);
1707
+ }
1708
+
1709
+ function markFetchersDone(keys) {
1710
+ for (let key of keys) {
1711
+ let fetcher = getFetcher(key);
1712
+ let doneFetcher = {
1713
+ state: "idle",
1714
+ data: fetcher.data,
1715
+ formMethod: undefined,
1716
+ formAction: undefined,
1717
+ formEncType: undefined,
1718
+ formData: undefined
1719
+ };
1720
+ state.fetchers.set(key, doneFetcher);
1721
+ }
1722
+ }
1723
+
1724
+ function markFetchRedirectsDone() {
1725
+ let doneKeys = [];
1726
+
1727
+ for (let key of fetchRedirectIds) {
1728
+ let fetcher = state.fetchers.get(key);
1729
+ !fetcher ? invariant(false, `Expected fetcher: ${key}`) : void 0;
1730
+
1731
+ if (fetcher.state === "loading") {
1732
+ fetchRedirectIds.delete(key);
1733
+ doneKeys.push(key);
1734
+ }
1735
+ }
1736
+
1737
+ markFetchersDone(doneKeys);
1738
+ }
1739
+
1740
+ function abortStaleFetchLoads(landedId) {
1741
+ let yeetedKeys = [];
1742
+
1743
+ for (let [key, id] of fetchReloadIds) {
1744
+ if (id < landedId) {
1745
+ let fetcher = state.fetchers.get(key);
1746
+ !fetcher ? invariant(false, `Expected fetcher: ${key}`) : void 0;
1747
+
1748
+ if (fetcher.state === "loading") {
1749
+ abortFetcher(key);
1750
+ fetchReloadIds.delete(key);
1751
+ yeetedKeys.push(key);
1752
+ }
1753
+ }
1754
+ }
1755
+
1756
+ markFetchersDone(yeetedKeys);
1757
+ return yeetedKeys.length > 0;
1758
+ } // Opt in to capturing and reporting scroll positions during navigations,
1759
+ // used by the <ScrollRestoration> component
1760
+
1761
+
1762
+ function enableScrollRestoration(positions, getPosition, getKey) {
1763
+ savedScrollPositions = positions;
1764
+ getScrollPosition = getPosition;
1765
+
1766
+ getScrollRestorationKey = getKey || (location => location.key); // Perform initial hydration scroll restoration, since we miss the boat on
1767
+ // the initial updateState() because we've not yet rendered <ScrollRestoration/>
1768
+ // and therefore have no savedScrollPositions available
1769
+
1770
+
1771
+ if (!initialScrollRestored && state.navigation === IDLE_NAVIGATION) {
1772
+ initialScrollRestored = true;
1773
+ let y = getSavedScrollPosition(state.location, state.matches);
1774
+
1775
+ if (y != null) {
1776
+ updateState({
1777
+ restoreScrollPosition: y
1778
+ });
1779
+ }
1780
+ }
1781
+
1782
+ return () => {
1783
+ savedScrollPositions = null;
1784
+ getScrollPosition = null;
1785
+ getScrollRestorationKey = null;
1786
+ };
1787
+ }
1788
+
1789
+ function saveScrollPosition(location, matches) {
1790
+ if (savedScrollPositions && getScrollRestorationKey && getScrollPosition) {
1791
+ let key = getScrollRestorationKey(location, matches) || location.key;
1792
+ savedScrollPositions[key] = getScrollPosition();
1793
+ }
1794
+ }
1795
+
1796
+ function getSavedScrollPosition(location, matches) {
1797
+ if (savedScrollPositions && getScrollRestorationKey && getScrollPosition) {
1798
+ let key = getScrollRestorationKey(location, matches) || location.key;
1799
+ let y = savedScrollPositions[key];
1800
+
1801
+ if (typeof y === "number") {
1802
+ return y;
1803
+ }
1804
+ }
1805
+
1806
+ return null;
1807
+ }
1808
+
1809
+ router = {
1810
+ get state() {
1811
+ return state;
1812
+ },
1813
+
1814
+ initialize,
1815
+ subscribe,
1816
+ enableScrollRestoration,
1817
+ navigate,
1818
+ fetch,
1819
+ revalidate,
1820
+ createHref,
1821
+ getFetcher,
1822
+ deleteFetcher,
1823
+ dispose,
1824
+ _internalFetchControllers: fetchControllers
1825
+ };
1826
+ return router;
1827
+ } //#endregion
1828
+ ////////////////////////////////////////////////////////////////////////////////
1829
+ //#region Helpers
1830
+ ////////////////////////////////////////////////////////////////////////////////
1831
+
1832
+ function convertRoutesToDataRoutes(routes, parentPath = [], allIds = new Set()) {
1833
+ return routes.map((route, index) => {
1834
+ let treePath = [...parentPath, index];
1835
+ let id = typeof route.id === "string" ? route.id : treePath.join("-");
1836
+ !!allIds.has(id) ? invariant(false, `Found a route id collision on id "${id}". Route ` + "id's must be globally unique within Data Router usages") : void 0;
1837
+ allIds.add(id);
1838
+ let dataRoute = { ...route,
1839
+ id,
1840
+ children: route.children ? convertRoutesToDataRoutes(route.children, treePath, allIds) : undefined
1841
+ };
1842
+ return dataRoute;
1843
+ });
1844
+ }
1845
+
1846
+ function getLoaderRedirect(state, redirect) {
1847
+ let {
1848
+ formMethod,
1849
+ formAction,
1850
+ formEncType,
1851
+ formData
1852
+ } = state.navigation;
1853
+ let navigation = {
1854
+ state: "loading",
1855
+ location: createLocation(state.location, redirect.location),
1856
+ formMethod: formMethod || undefined,
1857
+ formAction: formAction || undefined,
1858
+ formEncType: formEncType || undefined,
1859
+ formData: formData || undefined
1860
+ };
1861
+ return navigation;
1862
+ }
1863
+
1864
+ function getMatchesToLoad(state, matches, submission, location, isRevalidationRequired, pendingActionData, pendingActionError, revalidatingFetcherMatches) {
1865
+ // Determine which routes to run loaders for, filter out all routes below
1866
+ // any caught action error as they aren't going to render so we don't
1867
+ // need to load them
1868
+ let deepestRenderableMatchIndex = pendingActionError ? matches.findIndex(m => m.route.id === Object.keys(pendingActionError)[0]) : matches.length;
1869
+ let actionResult = pendingActionError ? Object.values(pendingActionError)[0] : pendingActionData ? Object.values(pendingActionData)[0] : null; // Pick navigation matches that are net-new or qualify for revalidation
1870
+
1871
+ let navigationMatches = matches.filter((match, index) => {
1872
+ if (!match.route.loader || index >= deepestRenderableMatchIndex) {
1873
+ return false;
1874
+ }
1875
+
1876
+ return isNewLoader(state.loaderData, state.matches[index], match) || shouldRevalidateLoader(state.location, state.matches[index], submission, location, match, isRevalidationRequired, actionResult);
1877
+ }); // If revalidation is required, pick fetchers that qualify
1878
+
1879
+ let revalidatingFetchers = [];
1880
+
1881
+ if (isRevalidationRequired) {
1882
+ for (let entry of revalidatingFetcherMatches.entries()) {
1883
+ let [key, [href, match]] = entry;
1884
+ let shouldRevalidate = shouldRevalidateLoader(href, match, submission, href, match, isRevalidationRequired, actionResult);
1885
+
1886
+ if (shouldRevalidate) {
1887
+ revalidatingFetchers.push([key, href, match]);
1888
+ }
1889
+ }
1890
+ }
1891
+
1892
+ return [navigationMatches, revalidatingFetchers];
1893
+ }
1894
+
1895
+ function isNewLoader(currentLoaderData, currentMatch, match) {
1896
+ let isNew = // [a] -> [a, b]
1897
+ !currentMatch || // [a, b] -> [a, c]
1898
+ match.route.id !== currentMatch.route.id; // Handle the case that we don't have data for a re-used route, potentially
1899
+ // from a prior error
1900
+
1901
+ let isMissingData = currentLoaderData[match.route.id] === undefined; // Always load if this is a net-new route or we don't yet have data
1902
+
1903
+ return isNew || isMissingData;
1904
+ }
1905
+
1906
+ function shouldRevalidateLoader(currentLocation, currentMatch, submission, location, match, isRevalidationRequired, actionResult) {
1907
+ let currentUrl = createURL(currentLocation);
1908
+ let currentParams = currentMatch.params;
1909
+ let nextUrl = createURL(location);
1910
+ let nextParams = match.params; // This is the default implementation as to when we revalidate. If the route
1911
+ // provides it's own implementation, then we give them full control but
1912
+ // provide this value so they can leverage it if needed after they check
1913
+ // their own specific use cases
1914
+ // Note that fetchers always provide the same current/next locations so the
1915
+ // URL-based checks here don't apply to fetcher shouldRevalidate calls
1916
+
1917
+ let defaultShouldRevalidate = // param change for this match, /users/123 -> /users/456
1918
+ currentMatch.pathname !== match.pathname || // splat param changed, which is not present in match.path
1919
+ // e.g. /files/images/avatar.jpg -> files/finances.xls
1920
+ currentMatch.route.path?.endsWith("*") && currentMatch.params["*"] !== match.params["*"] || // Clicked the same link, resubmitted a GET form
1921
+ currentUrl.toString() === nextUrl.toString() || // Search params affect all loaders
1922
+ currentUrl.search !== nextUrl.search || // Forced revalidation due to submission, useRevalidate, or X-Remix-Revalidate
1923
+ isRevalidationRequired;
1924
+
1925
+ if (match.route.shouldRevalidate) {
1926
+ return match.route.shouldRevalidate({
1927
+ currentUrl,
1928
+ currentParams,
1929
+ nextUrl,
1930
+ nextParams,
1931
+ ...submission,
1932
+ actionResult,
1933
+ defaultShouldRevalidate
1934
+ });
1935
+ }
1936
+
1937
+ return defaultShouldRevalidate;
1938
+ }
1939
+
1940
+ async function callLoaderOrAction(match, location, signal, actionSubmission) {
1941
+ let resultType = ResultType.data;
1942
+ let result;
1943
+
1944
+ try {
1945
+ let type = actionSubmission ? "action" : "loader";
1946
+ let handler = match.route[type];
1947
+ !handler ? "development" !== "production" ? invariant(false, `Could not find the ${type} to run on the "${match.route.id}" route`) : invariant(false) : void 0;
1948
+ result = await handler({
1949
+ params: match.params,
1950
+ request: createRequest(location, actionSubmission),
1951
+ signal
1952
+ });
1953
+ } catch (e) {
1954
+ resultType = ResultType.error;
1955
+ result = e;
1956
+ }
1957
+
1958
+ if (result instanceof Response) {
1959
+ // Process redirects
1960
+ let status = result.status;
1961
+
1962
+ let _location = result.headers.get("Location");
1963
+
1964
+ if (status >= 300 && status <= 399 && _location != null) {
1965
+ return {
1966
+ type: ResultType.redirect,
1967
+ status,
1968
+ location: _location,
1969
+ revalidate: result.headers.get("X-Remix-Revalidate") !== null
1970
+ };
1971
+ }
1972
+
1973
+ let data;
1974
+
1975
+ if (result.headers.get("Content-Type")?.startsWith("application/json")) {
1976
+ data = await result.json();
1977
+ } else {
1978
+ data = await result.text();
1979
+ }
1980
+
1981
+ if (resultType === ResultType.error) {
1982
+ return {
1983
+ type: resultType,
1984
+ error: new ErrorResponse(status, result.statusText, data)
1985
+ };
1986
+ }
1987
+
1988
+ return {
1989
+ type: resultType,
1990
+ data
1991
+ };
1992
+ }
1993
+
1994
+ if (resultType === ResultType.error) {
1995
+ return {
1996
+ type: resultType,
1997
+ error: result
1998
+ };
1999
+ }
2000
+
2001
+ return {
2002
+ type: resultType,
2003
+ data: result
2004
+ };
2005
+ }
2006
+
2007
+ function createRequest(location, actionSubmission) {
2008
+ let init = undefined;
2009
+
2010
+ if (actionSubmission) {
2011
+ let {
2012
+ formMethod,
2013
+ formEncType,
2014
+ formData
2015
+ } = actionSubmission;
2016
+ let body = formData; // If we're submitting application/x-www-form-urlencoded, then body should
2017
+ // be of type URLSearchParams
2018
+
2019
+ if (formEncType === "application/x-www-form-urlencoded") {
2020
+ body = new URLSearchParams();
2021
+
2022
+ for (let [key, value] of formData.entries()) {
2023
+ !(typeof value === "string") ? invariant(false, 'File inputs are not supported with encType "application/x-www-form-urlencoded", ' + 'please use "multipart/form-data" instead.') : void 0;
2024
+ body.append(key, value);
2025
+ }
2026
+ }
2027
+
2028
+ init = {
2029
+ method: formMethod.toUpperCase(),
2030
+ headers: {
2031
+ "Content-Type": formEncType
2032
+ },
2033
+ body
2034
+ };
2035
+ }
2036
+
2037
+ let url = createURL(location).toString();
2038
+ return new Request(url, init);
2039
+ }
2040
+
2041
+ function processLoaderData(state, matches, matchesToLoad, results, pendingActionError, revalidatingFetchers, fetcherResults) {
2042
+ // Fill in loaderData/errors from our loaders
2043
+ let loaderData = {};
2044
+ let errors = null; // Process loader results into state.loaderData/state.errors
2045
+
2046
+ results.forEach((result, index) => {
2047
+ let id = matchesToLoad[index].route.id;
2048
+ !!isRedirectResult(result) ? invariant(false, "Cannot handle redirect results in processLoaderData") : void 0;
2049
+
2050
+ if (isErrorResult(result)) {
2051
+ // Look upwards from the matched route for the closest ancestor
2052
+ // errorElement, defaulting to the root match
2053
+ let boundaryMatch = findNearestBoundary(matches, id);
2054
+ let error = result.error; // If we have a pending action error, we report it at the highest-route
2055
+ // that throws a loader error, and then clear it out to indicate that
2056
+ // it was consumed
2057
+
2058
+ if (pendingActionError) {
2059
+ error = Object.values(pendingActionError)[0];
2060
+ pendingActionError = null;
2061
+ }
2062
+
2063
+ errors = Object.assign(errors || {}, {
2064
+ [boundaryMatch.route.id]: error
2065
+ });
2066
+ } else {
2067
+ loaderData[id] = result.data;
2068
+ }
2069
+ }); // If we didn't consume the pending action error (i.e., all loaders
2070
+ // resolved), then consume it here
2071
+
2072
+ if (pendingActionError) {
2073
+ errors = pendingActionError;
2074
+ } // Process results from our revalidating fetchers
2075
+
2076
+
2077
+ revalidatingFetchers.forEach(([key, href, match], index) => {
2078
+ let result = fetcherResults[index]; // Process fetcher non-redirect errors
2079
+
2080
+ if (isErrorResult(result)) {
2081
+ let boundaryMatch = findNearestBoundary(state.matches, match.route.id);
2082
+
2083
+ if (!errors?.[boundaryMatch.route.id]) {
2084
+ errors = { ...errors,
2085
+ [boundaryMatch.route.id]: result.error
2086
+ };
2087
+ }
2088
+
2089
+ state.fetchers.delete(key);
2090
+ } else if (isRedirectResult(result)) {
2091
+ // Should never get here, redirects should get processed above, but we
2092
+ // keep this to type narrow to a success result in the else
2093
+ invariant(false, "Unhandled fetcher revalidation redirect") ;
2094
+ } else {
2095
+ let doneFetcher = {
2096
+ state: "idle",
2097
+ data: result.data,
2098
+ formMethod: undefined,
2099
+ formAction: undefined,
2100
+ formEncType: undefined,
2101
+ formData: undefined
2102
+ };
2103
+ state.fetchers.set(key, doneFetcher);
2104
+ }
2105
+ });
2106
+ return {
2107
+ loaderData,
2108
+ errors
2109
+ };
2110
+ }
2111
+
2112
+ function mergeLoaderData(state, newState) {
2113
+ // Identify active routes that have current loaderData and didn't receive new
2114
+ // loaderData
2115
+ let reusedRoutesWithData = (newState.matches || state.matches).filter(match => state.loaderData[match.route.id] !== undefined && newState.loaderData?.[match.route.id] === undefined);
2116
+ return { ...newState.loaderData,
2117
+ ...reusedRoutesWithData.reduce((acc, match) => Object.assign(acc, {
2118
+ [match.route.id]: state.loaderData[match.route.id]
2119
+ }), {})
2120
+ };
2121
+ } // Find the nearest error boundary, looking upwards from the matched route
2122
+ // for the closest ancestor errorElement, defaulting to the root match
2123
+
2124
+
2125
+ function findNearestBoundary(matches, routeId) {
2126
+ return matches.slice(0, matches.findIndex(m => m.route.id === routeId) + 1).reverse().find(m => m.route.errorElement) || matches[0];
2127
+ }
2128
+
2129
+ function getNotFoundMatches(routes) {
2130
+ return [{
2131
+ params: {},
2132
+ pathname: "",
2133
+ pathnameBase: "",
2134
+ route: routes[0]
2135
+ }];
2136
+ } // Find any returned redirect errors, starting from the lowest match
2137
+
2138
+
2139
+ function findRedirect(results) {
2140
+ for (let i = results.length - 1; i >= 0; i--) {
2141
+ let result = results[i];
2142
+
2143
+ if (isRedirectResult(result)) {
2144
+ return result;
2145
+ }
2146
+ }
2147
+ } // Create an href to represent a "server" URL without the hash
2148
+
2149
+
2150
+ function createHref(location) {
2151
+ return location.pathname + location.search;
2152
+ }
2153
+
2154
+ function isHashChangeOnly(a, b) {
2155
+ return a.pathname === b.pathname && a.search === b.search && a.hash !== b.hash;
2156
+ }
2157
+
2158
+ function isErrorResult(result) {
2159
+ return result.type === ResultType.error;
2160
+ }
2161
+
2162
+ function isRedirectResult(result) {
2163
+ return result?.type === ResultType.redirect;
2164
+ }
2165
+
2166
+ function isSubmissionNavigation(opts) {
2167
+ return opts != null && "formData" in opts && opts.formData != null;
2168
+ }
2169
+
2170
+ function isActionSubmission(submission) {
2171
+ return submission && submission.formMethod !== "get";
2172
+ }
2173
+
2174
+ function hasNakedIndexQuery(search) {
2175
+ return new URLSearchParams(search).getAll("index").some(v => v === "");
2176
+ }
2177
+
2178
+ function createURL(location) {
2179
+ let base = typeof window !== "undefined" && typeof window.location !== "undefined" ? window.location.origin : "unknown://unknown";
2180
+ let href = typeof location === "string" ? location : createHref(location);
2181
+ return new URL(href, base);
2182
+ } //#endregion
2183
+
2184
+ function createMemoryRouter({
2185
+ initialEntries,
2186
+ initialIndex,
2187
+ ...routerInit
2188
+ }) {
2189
+ let history = createMemoryHistory({
2190
+ initialEntries,
2191
+ initialIndex
2192
+ });
2193
+ return createRouter({
2194
+ history,
2195
+ ...routerInit
2196
+ });
2197
+ }
2198
+
2199
+ function createBrowserRouter({
2200
+ window,
2201
+ ...routerInit
2202
+ }) {
2203
+ let history = createBrowserHistory({
2204
+ window
2205
+ });
2206
+ return createRouter({
2207
+ history,
2208
+ ...routerInit
2209
+ });
2210
+ }
2211
+
2212
+ function createHashRouter({
2213
+ window,
2214
+ ...routerInit
2215
+ }) {
2216
+ let history = createHashHistory({
2217
+ window
2218
+ });
2219
+ return createRouter({
2220
+ history,
2221
+ ...routerInit
2222
+ });
2223
+ }
2224
+
2225
+ export { Action, IDLE_FETCHER, IDLE_NAVIGATION, createBrowserHistory, createBrowserRouter, createHashHistory, createHashRouter, createMemoryHistory, createMemoryRouter, createPath, createRouter, generatePath, getToPathname, invariant, isRouteErrorResponse, joinPaths, json, matchPath, matchRoutes, normalizePathname, parsePath, redirect, resolvePath, resolveTo, stripBasename, warning$1 as warning };
2226
+ //# sourceMappingURL=router.development.js.map