@remix-run/router 1.16.1 → 1.17.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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @remix-run/router v1.16.1
2
+ * @remix-run/router v1.17.0
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -718,7 +718,7 @@ function convertRoutesToDataRoutes(routes, mapRouteProperties, parentPath, manif
718
718
  manifest = {};
719
719
  }
720
720
  return routes.map((route, index) => {
721
- let treePath = [...parentPath, index];
721
+ let treePath = [...parentPath, String(index)];
722
722
  let id = typeof route.id === "string" ? route.id : treePath.join("-");
723
723
  invariant(route.index !== true || !route.children, "Cannot specify children on an index route");
724
724
  invariant(!manifest[id], "Found a route id collision on id \"" + id + "\". Route " + "id's must be globally unique within Data Router usages");
@@ -751,6 +751,9 @@ function matchRoutes(routes, locationArg, basename) {
751
751
  if (basename === void 0) {
752
752
  basename = "/";
753
753
  }
754
+ return matchRoutesImpl(routes, locationArg, basename, false);
755
+ }
756
+ function matchRoutesImpl(routes, locationArg, basename, allowPartial) {
754
757
  let location = typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
755
758
  let pathname = stripBasename(location.pathname || "/", basename);
756
759
  if (pathname == null) {
@@ -767,7 +770,7 @@ function matchRoutes(routes, locationArg, basename) {
767
770
  // should be a safe operation. This avoids needing matchRoutes to be
768
771
  // history-aware.
769
772
  let decoded = decodePath(pathname);
770
- matches = matchRouteBranch(branches[i], decoded);
773
+ matches = matchRouteBranch(branches[i], decoded, allowPartial);
771
774
  }
772
775
  return matches;
773
776
  }
@@ -927,7 +930,10 @@ function compareIndexes(a, b) {
927
930
  // so they sort equally.
928
931
  0;
929
932
  }
930
- function matchRouteBranch(branch, pathname) {
933
+ function matchRouteBranch(branch, pathname, allowPartial) {
934
+ if (allowPartial === void 0) {
935
+ allowPartial = false;
936
+ }
931
937
  let {
932
938
  routesMeta
933
939
  } = branch;
@@ -943,9 +949,18 @@ function matchRouteBranch(branch, pathname) {
943
949
  caseSensitive: meta.caseSensitive,
944
950
  end
945
951
  }, remainingPathname);
946
- if (!match) return null;
947
- Object.assign(matchedParams, match.params);
948
952
  let route = meta.route;
953
+ if (!match && end && allowPartial && !routesMeta[routesMeta.length - 1].route.index) {
954
+ match = matchPath({
955
+ path: meta.relativePath,
956
+ caseSensitive: meta.caseSensitive,
957
+ end: false
958
+ }, remainingPathname);
959
+ }
960
+ if (!match) {
961
+ return null;
962
+ }
963
+ Object.assign(matchedParams, match.params);
949
964
  matches.push({
950
965
  // TODO: Can this as be avoided?
951
966
  params: matchedParams,
@@ -1669,6 +1684,8 @@ function createRouter(init) {
1669
1684
  let inFlightDataRoutes;
1670
1685
  let basename = init.basename || "/";
1671
1686
  let dataStrategyImpl = init.unstable_dataStrategy || defaultDataStrategy;
1687
+ let patchRoutesOnMissImpl = init.unstable_patchRoutesOnMiss;
1688
+
1672
1689
  // Config driven behavior flags
1673
1690
  let future = _extends({
1674
1691
  v7_fetcherPersist: false,
@@ -1697,7 +1714,7 @@ function createRouter(init) {
1697
1714
  let initialScrollRestored = init.hydrationData != null;
1698
1715
  let initialMatches = matchRoutes(dataRoutes, init.history.location, basename);
1699
1716
  let initialErrors = null;
1700
- if (initialMatches == null) {
1717
+ if (initialMatches == null && !patchRoutesOnMissImpl) {
1701
1718
  // If we do not match a user-provided-route, fall back to the root
1702
1719
  // to allow the error boundary to take over
1703
1720
  let error = getInternalRouterError(404, {
@@ -1713,13 +1730,15 @@ function createRouter(init) {
1713
1730
  };
1714
1731
  }
1715
1732
  let initialized;
1716
- let hasLazyRoutes = initialMatches.some(m => m.route.lazy);
1717
- let hasLoaders = initialMatches.some(m => m.route.loader);
1718
- if (hasLazyRoutes) {
1733
+ if (!initialMatches) {
1734
+ // We need to run patchRoutesOnMiss in initialize()
1735
+ initialized = false;
1736
+ initialMatches = [];
1737
+ } else if (initialMatches.some(m => m.route.lazy)) {
1719
1738
  // All initialMatches need to be loaded before we're ready. If we have lazy
1720
1739
  // functions around still then we'll need to run them in initialize()
1721
1740
  initialized = false;
1722
- } else if (!hasLoaders) {
1741
+ } else if (!initialMatches.some(m => m.route.loader)) {
1723
1742
  // If we've got no loaders to run, then we're good to go
1724
1743
  initialized = true;
1725
1744
  } else if (future.v7_partialHydration) {
@@ -1846,6 +1865,10 @@ function createRouter(init) {
1846
1865
  // we don't need to update UI state if they change
1847
1866
  let blockerFunctions = new Map();
1848
1867
 
1868
+ // Map of pending patchRoutesOnMiss() promises (keyed by path/matches) so
1869
+ // that we only kick them off once for a given combo
1870
+ let pendingPatchRoutes = new Map();
1871
+
1849
1872
  // Flag to ignore the next history update, so we can revert the URL change on
1850
1873
  // a POP navigation that was blocked by the user without touching router state
1851
1874
  let ignoreNextHistoryUpdate = false;
@@ -2235,18 +2258,18 @@ function createRouter(init) {
2235
2258
  let loadingNavigation = opts && opts.overrideNavigation;
2236
2259
  let matches = matchRoutes(routesToUse, location, basename);
2237
2260
  let flushSync = (opts && opts.flushSync) === true;
2261
+ let fogOfWar = checkFogOfWar(matches, routesToUse, location.pathname);
2262
+ if (fogOfWar.active && fogOfWar.matches) {
2263
+ matches = fogOfWar.matches;
2264
+ }
2238
2265
 
2239
2266
  // Short circuit with a 404 on the root error boundary if we match nothing
2240
2267
  if (!matches) {
2241
- let error = getInternalRouterError(404, {
2242
- pathname: location.pathname
2243
- });
2244
2268
  let {
2245
- matches: notFoundMatches,
2269
+ error,
2270
+ notFoundMatches,
2246
2271
  route
2247
- } = getShortCircuitMatches(routesToUse);
2248
- // Cancel all pending deferred on 404s since we don't keep any routes
2249
- cancelActiveDeferreds();
2272
+ } = handleNavigational404(location.pathname);
2250
2273
  completeNavigation(location, {
2251
2274
  matches: notFoundMatches,
2252
2275
  loaderData: {},
@@ -2289,16 +2312,36 @@ function createRouter(init) {
2289
2312
  }];
2290
2313
  } else if (opts && opts.submission && isMutationMethod(opts.submission.formMethod)) {
2291
2314
  // Call action if we received an action submission
2292
- let actionResult = await handleAction(request, location, opts.submission, matches, {
2315
+ let actionResult = await handleAction(request, location, opts.submission, matches, fogOfWar.active, {
2293
2316
  replace: opts.replace,
2294
2317
  flushSync
2295
2318
  });
2296
2319
  if (actionResult.shortCircuited) {
2297
2320
  return;
2298
2321
  }
2322
+
2323
+ // If we received a 404 from handleAction, it's because we couldn't lazily
2324
+ // discover the destination route so we don't want to call loaders
2325
+ if (actionResult.pendingActionResult) {
2326
+ let [routeId, result] = actionResult.pendingActionResult;
2327
+ if (isErrorResult(result) && isRouteErrorResponse(result.error) && result.error.status === 404) {
2328
+ pendingNavigationController = null;
2329
+ completeNavigation(location, {
2330
+ matches: actionResult.matches,
2331
+ loaderData: {},
2332
+ errors: {
2333
+ [routeId]: result.error
2334
+ }
2335
+ });
2336
+ return;
2337
+ }
2338
+ }
2339
+ matches = actionResult.matches || matches;
2299
2340
  pendingActionResult = actionResult.pendingActionResult;
2300
2341
  loadingNavigation = getLoadingNavigation(location, opts.submission);
2301
2342
  flushSync = false;
2343
+ // No need to do fog of war matching again on loader execution
2344
+ fogOfWar.active = false;
2302
2345
 
2303
2346
  // Create a GET request for the loaders
2304
2347
  request = createClientSideRequest(init.history, request.url, request.signal);
@@ -2307,9 +2350,10 @@ function createRouter(init) {
2307
2350
  // Call loaders
2308
2351
  let {
2309
2352
  shortCircuited,
2353
+ matches: updatedMatches,
2310
2354
  loaderData,
2311
2355
  errors
2312
- } = await handleLoaders(request, location, matches, loadingNavigation, opts && opts.submission, opts && opts.fetcherSubmission, opts && opts.replace, opts && opts.initialHydration === true, flushSync, pendingActionResult);
2356
+ } = await handleLoaders(request, location, matches, fogOfWar.active, loadingNavigation, opts && opts.submission, opts && opts.fetcherSubmission, opts && opts.replace, opts && opts.initialHydration === true, flushSync, pendingActionResult);
2313
2357
  if (shortCircuited) {
2314
2358
  return;
2315
2359
  }
@@ -2319,7 +2363,7 @@ function createRouter(init) {
2319
2363
  // been assigned to a new controller for the next navigation
2320
2364
  pendingNavigationController = null;
2321
2365
  completeNavigation(location, _extends({
2322
- matches
2366
+ matches: updatedMatches || matches
2323
2367
  }, getActionDataForCommit(pendingActionResult), {
2324
2368
  loaderData,
2325
2369
  errors
@@ -2328,7 +2372,7 @@ function createRouter(init) {
2328
2372
 
2329
2373
  // Call the action matched by the leaf route for this navigation and handle
2330
2374
  // redirects/errors
2331
- async function handleAction(request, location, submission, matches, opts) {
2375
+ async function handleAction(request, location, submission, matches, isFogOfWar, opts) {
2332
2376
  if (opts === void 0) {
2333
2377
  opts = {};
2334
2378
  }
@@ -2341,6 +2385,42 @@ function createRouter(init) {
2341
2385
  }, {
2342
2386
  flushSync: opts.flushSync === true
2343
2387
  });
2388
+ if (isFogOfWar) {
2389
+ let discoverResult = await discoverRoutes(matches, location.pathname, request.signal);
2390
+ if (discoverResult.type === "aborted") {
2391
+ return {
2392
+ shortCircuited: true
2393
+ };
2394
+ } else if (discoverResult.type === "error") {
2395
+ let {
2396
+ error,
2397
+ notFoundMatches,
2398
+ route
2399
+ } = handleDiscoverRouteError(location.pathname, discoverResult);
2400
+ return {
2401
+ matches: notFoundMatches,
2402
+ pendingActionResult: [route.id, {
2403
+ type: ResultType.error,
2404
+ error
2405
+ }]
2406
+ };
2407
+ } else if (!discoverResult.matches) {
2408
+ let {
2409
+ notFoundMatches,
2410
+ error,
2411
+ route
2412
+ } = handleNavigational404(location.pathname);
2413
+ return {
2414
+ matches: notFoundMatches,
2415
+ pendingActionResult: [route.id, {
2416
+ type: ResultType.error,
2417
+ error
2418
+ }]
2419
+ };
2420
+ } else {
2421
+ matches = discoverResult.matches;
2422
+ }
2423
+ }
2344
2424
 
2345
2425
  // Call our action and get the result
2346
2426
  let result;
@@ -2392,31 +2472,94 @@ function createRouter(init) {
2392
2472
  // to call and will commit it when we complete the navigation
2393
2473
  let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
2394
2474
 
2395
- // By default, all submissions are REPLACE navigations, but if the
2396
- // action threw an error that'll be rendered in an errorElement, we fall
2397
- // back to PUSH so that the user can use the back button to get back to
2398
- // the pre-submission form location to try again
2475
+ // By default, all submissions to the current location are REPLACE
2476
+ // navigations, but if the action threw an error that'll be rendered in
2477
+ // an errorElement, we fall back to PUSH so that the user can use the
2478
+ // back button to get back to the pre-submission form location to try
2479
+ // again
2399
2480
  if ((opts && opts.replace) !== true) {
2400
2481
  pendingAction = Action.Push;
2401
2482
  }
2402
2483
  return {
2484
+ matches,
2403
2485
  pendingActionResult: [boundaryMatch.route.id, result]
2404
2486
  };
2405
2487
  }
2406
2488
  return {
2489
+ matches,
2407
2490
  pendingActionResult: [actionMatch.route.id, result]
2408
2491
  };
2409
2492
  }
2410
2493
 
2411
2494
  // Call all applicable loaders for the given matches, handling redirects,
2412
2495
  // errors, etc.
2413
- async function handleLoaders(request, location, matches, overrideNavigation, submission, fetcherSubmission, replace, initialHydration, flushSync, pendingActionResult) {
2496
+ async function handleLoaders(request, location, matches, isFogOfWar, overrideNavigation, submission, fetcherSubmission, replace, initialHydration, flushSync, pendingActionResult) {
2414
2497
  // Figure out the right navigation we want to use for data loading
2415
2498
  let loadingNavigation = overrideNavigation || getLoadingNavigation(location, submission);
2416
2499
 
2417
2500
  // If this was a redirect from an action we don't have a "submission" but
2418
2501
  // we have it on the loading navigation so use that if available
2419
2502
  let activeSubmission = submission || fetcherSubmission || getSubmissionFromNavigation(loadingNavigation);
2503
+
2504
+ // If this is an uninterrupted revalidation, we remain in our current idle
2505
+ // state. If not, we need to switch to our loading state and load data,
2506
+ // preserving any new action data or existing action data (in the case of
2507
+ // a revalidation interrupting an actionReload)
2508
+ // If we have partialHydration enabled, then don't update the state for the
2509
+ // initial data load since it's not a "navigation"
2510
+ let shouldUpdateNavigationState = !isUninterruptedRevalidation && (!future.v7_partialHydration || !initialHydration);
2511
+
2512
+ // When fog of war is enabled, we enter our `loading` state earlier so we
2513
+ // can discover new routes during the `loading` state. We skip this if
2514
+ // we've already run actions since we would have done our matching already.
2515
+ // If the children() function threw then, we want to proceed with the
2516
+ // partial matches it discovered.
2517
+ if (isFogOfWar) {
2518
+ if (shouldUpdateNavigationState) {
2519
+ let actionData = getUpdatedActionData(pendingActionResult);
2520
+ updateState(_extends({
2521
+ navigation: loadingNavigation
2522
+ }, actionData !== undefined ? {
2523
+ actionData
2524
+ } : {}), {
2525
+ flushSync
2526
+ });
2527
+ }
2528
+ let discoverResult = await discoverRoutes(matches, location.pathname, request.signal);
2529
+ if (discoverResult.type === "aborted") {
2530
+ return {
2531
+ shortCircuited: true
2532
+ };
2533
+ } else if (discoverResult.type === "error") {
2534
+ let {
2535
+ error,
2536
+ notFoundMatches,
2537
+ route
2538
+ } = handleDiscoverRouteError(location.pathname, discoverResult);
2539
+ return {
2540
+ matches: notFoundMatches,
2541
+ loaderData: {},
2542
+ errors: {
2543
+ [route.id]: error
2544
+ }
2545
+ };
2546
+ } else if (!discoverResult.matches) {
2547
+ let {
2548
+ error,
2549
+ notFoundMatches,
2550
+ route
2551
+ } = handleNavigational404(location.pathname);
2552
+ return {
2553
+ matches: notFoundMatches,
2554
+ loaderData: {},
2555
+ errors: {
2556
+ [route.id]: error
2557
+ }
2558
+ };
2559
+ } else {
2560
+ matches = discoverResult.matches;
2561
+ }
2562
+ }
2420
2563
  let routesToUse = inFlightDataRoutes || dataRoutes;
2421
2564
  let [matchesToLoad, revalidatingFetchers] = getMatchesToLoad(init.history, state, matches, activeSubmission, location, future.v7_partialHydration && initialHydration === true, future.unstable_skipActionErrorRevalidation, isRevalidationRequired, cancelledDeferredRoutes, cancelledFetcherLoads, deletedFetchers, fetchLoadMatches, fetchRedirectIds, routesToUse, basename, pendingActionResult);
2422
2565
 
@@ -2445,41 +2588,20 @@ function createRouter(init) {
2445
2588
  shortCircuited: true
2446
2589
  };
2447
2590
  }
2448
-
2449
- // If this is an uninterrupted revalidation, we remain in our current idle
2450
- // state. If not, we need to switch to our loading state and load data,
2451
- // preserving any new action data or existing action data (in the case of
2452
- // a revalidation interrupting an actionReload)
2453
- // If we have partialHydration enabled, then don't update the state for the
2454
- // initial data load since it's not a "navigation"
2455
- if (!isUninterruptedRevalidation && (!future.v7_partialHydration || !initialHydration)) {
2456
- revalidatingFetchers.forEach(rf => {
2457
- let fetcher = state.fetchers.get(rf.key);
2458
- let revalidatingFetcher = getLoadingFetcher(undefined, fetcher ? fetcher.data : undefined);
2459
- state.fetchers.set(rf.key, revalidatingFetcher);
2460
- });
2461
- let actionData;
2462
- if (pendingActionResult && !isErrorResult(pendingActionResult[1])) {
2463
- // This is cast to `any` currently because `RouteData`uses any and it
2464
- // would be a breaking change to use any.
2465
- // TODO: v7 - change `RouteData` to use `unknown` instead of `any`
2466
- actionData = {
2467
- [pendingActionResult[0]]: pendingActionResult[1].data
2468
- };
2469
- } else if (state.actionData) {
2470
- if (Object.keys(state.actionData).length === 0) {
2471
- actionData = null;
2472
- } else {
2473
- actionData = state.actionData;
2591
+ if (shouldUpdateNavigationState) {
2592
+ let updates = {};
2593
+ if (!isFogOfWar) {
2594
+ // Only update navigation/actionNData if we didn't already do it above
2595
+ updates.navigation = loadingNavigation;
2596
+ let actionData = getUpdatedActionData(pendingActionResult);
2597
+ if (actionData !== undefined) {
2598
+ updates.actionData = actionData;
2474
2599
  }
2475
2600
  }
2476
- updateState(_extends({
2477
- navigation: loadingNavigation
2478
- }, actionData !== undefined ? {
2479
- actionData
2480
- } : {}, revalidatingFetchers.length > 0 ? {
2481
- fetchers: new Map(state.fetchers)
2482
- } : {}), {
2601
+ if (revalidatingFetchers.length > 0) {
2602
+ updates.fetchers = getUpdatedRevalidatingFetchers(revalidatingFetchers);
2603
+ }
2604
+ updateState(updates, {
2483
2605
  flushSync
2484
2606
  });
2485
2607
  }
@@ -2570,12 +2692,37 @@ function createRouter(init) {
2570
2692
  let didAbortFetchLoads = abortStaleFetchLoads(pendingNavigationLoadId);
2571
2693
  let shouldUpdateFetchers = updatedFetchers || didAbortFetchLoads || revalidatingFetchers.length > 0;
2572
2694
  return _extends({
2695
+ matches,
2573
2696
  loaderData,
2574
2697
  errors
2575
2698
  }, shouldUpdateFetchers ? {
2576
2699
  fetchers: new Map(state.fetchers)
2577
2700
  } : {});
2578
2701
  }
2702
+ function getUpdatedActionData(pendingActionResult) {
2703
+ if (pendingActionResult && !isErrorResult(pendingActionResult[1])) {
2704
+ // This is cast to `any` currently because `RouteData`uses any and it
2705
+ // would be a breaking change to use any.
2706
+ // TODO: v7 - change `RouteData` to use `unknown` instead of `any`
2707
+ return {
2708
+ [pendingActionResult[0]]: pendingActionResult[1].data
2709
+ };
2710
+ } else if (state.actionData) {
2711
+ if (Object.keys(state.actionData).length === 0) {
2712
+ return null;
2713
+ } else {
2714
+ return state.actionData;
2715
+ }
2716
+ }
2717
+ }
2718
+ function getUpdatedRevalidatingFetchers(revalidatingFetchers) {
2719
+ revalidatingFetchers.forEach(rf => {
2720
+ let fetcher = state.fetchers.get(rf.key);
2721
+ let revalidatingFetcher = getLoadingFetcher(undefined, fetcher ? fetcher.data : undefined);
2722
+ state.fetchers.set(rf.key, revalidatingFetcher);
2723
+ });
2724
+ return new Map(state.fetchers);
2725
+ }
2579
2726
 
2580
2727
  // Trigger a fetcher load/submit for the given fetcher key
2581
2728
  function fetch(key, routeId, href, opts) {
@@ -2587,6 +2734,10 @@ function createRouter(init) {
2587
2734
  let routesToUse = inFlightDataRoutes || dataRoutes;
2588
2735
  let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, href, future.v7_relativeSplatPath, routeId, opts == null ? void 0 : opts.relative);
2589
2736
  let matches = matchRoutes(routesToUse, normalizedPath, basename);
2737
+ let fogOfWar = checkFogOfWar(matches, routesToUse, normalizedPath);
2738
+ if (fogOfWar.active && fogOfWar.matches) {
2739
+ matches = fogOfWar.matches;
2740
+ }
2590
2741
  if (!matches) {
2591
2742
  setFetcherError(key, routeId, getInternalRouterError(404, {
2592
2743
  pathname: normalizedPath
@@ -2609,7 +2760,7 @@ function createRouter(init) {
2609
2760
  let match = getTargetMatch(matches, path);
2610
2761
  pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
2611
2762
  if (submission && isMutationMethod(submission.formMethod)) {
2612
- handleFetcherAction(key, routeId, path, match, matches, flushSync, submission);
2763
+ handleFetcherAction(key, routeId, path, match, matches, fogOfWar.active, flushSync, submission);
2613
2764
  return;
2614
2765
  }
2615
2766
 
@@ -2619,23 +2770,29 @@ function createRouter(init) {
2619
2770
  routeId,
2620
2771
  path
2621
2772
  });
2622
- handleFetcherLoader(key, routeId, path, match, matches, flushSync, submission);
2773
+ handleFetcherLoader(key, routeId, path, match, matches, fogOfWar.active, flushSync, submission);
2623
2774
  }
2624
2775
 
2625
2776
  // Call the action for the matched fetcher.submit(), and then handle redirects,
2626
2777
  // errors, and revalidation
2627
- async function handleFetcherAction(key, routeId, path, match, requestMatches, flushSync, submission) {
2778
+ async function handleFetcherAction(key, routeId, path, match, requestMatches, isFogOfWar, flushSync, submission) {
2628
2779
  interruptActiveLoads();
2629
2780
  fetchLoadMatches.delete(key);
2630
- if (!match.route.action && !match.route.lazy) {
2631
- let error = getInternalRouterError(405, {
2632
- method: submission.formMethod,
2633
- pathname: path,
2634
- routeId: routeId
2635
- });
2636
- setFetcherError(key, routeId, error, {
2637
- flushSync
2638
- });
2781
+ function detectAndHandle405Error(m) {
2782
+ if (!m.route.action && !m.route.lazy) {
2783
+ let error = getInternalRouterError(405, {
2784
+ method: submission.formMethod,
2785
+ pathname: path,
2786
+ routeId: routeId
2787
+ });
2788
+ setFetcherError(key, routeId, error, {
2789
+ flushSync
2790
+ });
2791
+ return true;
2792
+ }
2793
+ return false;
2794
+ }
2795
+ if (!isFogOfWar && detectAndHandle405Error(match)) {
2639
2796
  return;
2640
2797
  }
2641
2798
 
@@ -2644,10 +2801,37 @@ function createRouter(init) {
2644
2801
  updateFetcherState(key, getSubmittingFetcher(submission, existingFetcher), {
2645
2802
  flushSync
2646
2803
  });
2647
-
2648
- // Call the action for the fetcher
2649
2804
  let abortController = new AbortController();
2650
2805
  let fetchRequest = createClientSideRequest(init.history, path, abortController.signal, submission);
2806
+ if (isFogOfWar) {
2807
+ let discoverResult = await discoverRoutes(requestMatches, path, fetchRequest.signal);
2808
+ if (discoverResult.type === "aborted") {
2809
+ return;
2810
+ } else if (discoverResult.type === "error") {
2811
+ let {
2812
+ error
2813
+ } = handleDiscoverRouteError(path, discoverResult);
2814
+ setFetcherError(key, routeId, error, {
2815
+ flushSync
2816
+ });
2817
+ return;
2818
+ } else if (!discoverResult.matches) {
2819
+ setFetcherError(key, routeId, getInternalRouterError(404, {
2820
+ pathname: path
2821
+ }), {
2822
+ flushSync
2823
+ });
2824
+ return;
2825
+ } else {
2826
+ requestMatches = discoverResult.matches;
2827
+ match = getTargetMatch(requestMatches, path);
2828
+ if (detectAndHandle405Error(match)) {
2829
+ return;
2830
+ }
2831
+ }
2832
+ }
2833
+
2834
+ // Call the action for the fetcher
2651
2835
  fetchControllers.set(key, abortController);
2652
2836
  let originatingLoadId = incrementingLoadId;
2653
2837
  let actionResults = await callDataStrategy("action", fetchRequest, [match], requestMatches);
@@ -2797,15 +2981,39 @@ function createRouter(init) {
2797
2981
  }
2798
2982
 
2799
2983
  // Call the matched loader for fetcher.load(), handling redirects, errors, etc.
2800
- async function handleFetcherLoader(key, routeId, path, match, matches, flushSync, submission) {
2984
+ async function handleFetcherLoader(key, routeId, path, match, matches, isFogOfWar, flushSync, submission) {
2801
2985
  let existingFetcher = state.fetchers.get(key);
2802
2986
  updateFetcherState(key, getLoadingFetcher(submission, existingFetcher ? existingFetcher.data : undefined), {
2803
2987
  flushSync
2804
2988
  });
2805
-
2806
- // Call the loader for this fetcher route match
2807
2989
  let abortController = new AbortController();
2808
2990
  let fetchRequest = createClientSideRequest(init.history, path, abortController.signal);
2991
+ if (isFogOfWar) {
2992
+ let discoverResult = await discoverRoutes(matches, path, fetchRequest.signal);
2993
+ if (discoverResult.type === "aborted") {
2994
+ return;
2995
+ } else if (discoverResult.type === "error") {
2996
+ let {
2997
+ error
2998
+ } = handleDiscoverRouteError(path, discoverResult);
2999
+ setFetcherError(key, routeId, error, {
3000
+ flushSync
3001
+ });
3002
+ return;
3003
+ } else if (!discoverResult.matches) {
3004
+ setFetcherError(key, routeId, getInternalRouterError(404, {
3005
+ pathname: path
3006
+ }), {
3007
+ flushSync
3008
+ });
3009
+ return;
3010
+ } else {
3011
+ matches = discoverResult.matches;
3012
+ match = getTargetMatch(matches, path);
3013
+ }
3014
+ }
3015
+
3016
+ // Call the loader for this fetcher route match
2809
3017
  fetchControllers.set(key, abortController);
2810
3018
  let originatingLoadId = incrementingLoadId;
2811
3019
  let results = await callDataStrategy("loader", fetchRequest, [match], matches);
@@ -3189,6 +3397,39 @@ function createRouter(init) {
3189
3397
  return blockerKey;
3190
3398
  }
3191
3399
  }
3400
+ function handleNavigational404(pathname) {
3401
+ let error = getInternalRouterError(404, {
3402
+ pathname
3403
+ });
3404
+ let routesToUse = inFlightDataRoutes || dataRoutes;
3405
+ let {
3406
+ matches,
3407
+ route
3408
+ } = getShortCircuitMatches(routesToUse);
3409
+
3410
+ // Cancel all pending deferred on 404s since we don't keep any routes
3411
+ cancelActiveDeferreds();
3412
+ return {
3413
+ notFoundMatches: matches,
3414
+ route,
3415
+ error
3416
+ };
3417
+ }
3418
+ function handleDiscoverRouteError(pathname, discoverResult) {
3419
+ let matches = discoverResult.partialMatches;
3420
+ let route = matches[matches.length - 1].route;
3421
+ let error = getInternalRouterError(400, {
3422
+ type: "route-discovery",
3423
+ routeId: route.id,
3424
+ pathname,
3425
+ message: discoverResult.error != null && "message" in discoverResult.error ? discoverResult.error : String(discoverResult.error)
3426
+ });
3427
+ return {
3428
+ notFoundMatches: matches,
3429
+ route,
3430
+ error
3431
+ };
3432
+ }
3192
3433
  function cancelActiveDeferreds(predicate) {
3193
3434
  let cancelledRouteIds = [];
3194
3435
  activeDeferreds.forEach((dfd, routeId) => {
@@ -3252,6 +3493,100 @@ function createRouter(init) {
3252
3493
  }
3253
3494
  return null;
3254
3495
  }
3496
+ function checkFogOfWar(matches, routesToUse, pathname) {
3497
+ if (patchRoutesOnMissImpl) {
3498
+ if (!matches) {
3499
+ let fogMatches = matchRoutesImpl(routesToUse, pathname, basename, true);
3500
+ return {
3501
+ active: true,
3502
+ matches: fogMatches || []
3503
+ };
3504
+ } else {
3505
+ let leafRoute = matches[matches.length - 1].route;
3506
+ if (leafRoute.path === "*") {
3507
+ // If we matched a splat, it might only be because we haven't yet fetched
3508
+ // the children that would match with a higher score, so let's fetch
3509
+ // around and find out
3510
+ let partialMatches = matchRoutesImpl(routesToUse, pathname, basename, true);
3511
+ return {
3512
+ active: true,
3513
+ matches: partialMatches
3514
+ };
3515
+ }
3516
+ }
3517
+ }
3518
+ return {
3519
+ active: false,
3520
+ matches: null
3521
+ };
3522
+ }
3523
+ async function discoverRoutes(matches, pathname, signal) {
3524
+ let partialMatches = matches;
3525
+ let route = partialMatches.length > 0 ? partialMatches[partialMatches.length - 1].route : null;
3526
+ while (true) {
3527
+ try {
3528
+ await loadLazyRouteChildren(patchRoutesOnMissImpl, pathname, partialMatches, dataRoutes || inFlightDataRoutes, manifest, mapRouteProperties, pendingPatchRoutes, signal);
3529
+ } catch (e) {
3530
+ return {
3531
+ type: "error",
3532
+ error: e,
3533
+ partialMatches
3534
+ };
3535
+ }
3536
+ if (signal.aborted) {
3537
+ return {
3538
+ type: "aborted"
3539
+ };
3540
+ }
3541
+ let routesToUse = inFlightDataRoutes || dataRoutes;
3542
+ let newMatches = matchRoutes(routesToUse, pathname, basename);
3543
+ let matchedSplat = false;
3544
+ if (newMatches) {
3545
+ let leafRoute = newMatches[newMatches.length - 1].route;
3546
+ if (leafRoute.index) {
3547
+ // If we found an index route, we can stop
3548
+ return {
3549
+ type: "success",
3550
+ matches: newMatches
3551
+ };
3552
+ }
3553
+ if (leafRoute.path && leafRoute.path.length > 0) {
3554
+ if (leafRoute.path === "*") {
3555
+ // If we found a splat route, we can't be sure there's not a
3556
+ // higher-scoring route down some partial matches trail so we need
3557
+ // to check that out
3558
+ matchedSplat = true;
3559
+ } else {
3560
+ // If we found a non-splat route, we can stop
3561
+ return {
3562
+ type: "success",
3563
+ matches: newMatches
3564
+ };
3565
+ }
3566
+ }
3567
+ }
3568
+ let newPartialMatches = matchRoutesImpl(routesToUse, pathname, basename, true);
3569
+
3570
+ // If we are no longer partially matching anything, this was either a
3571
+ // legit splat match above, or it's a 404. Also avoid loops if the
3572
+ // second pass results in the same partial matches
3573
+ if (!newPartialMatches || partialMatches.map(m => m.route.id).join("-") === newPartialMatches.map(m => m.route.id).join("-")) {
3574
+ return {
3575
+ type: "success",
3576
+ matches: matchedSplat ? newMatches : null
3577
+ };
3578
+ }
3579
+ partialMatches = newPartialMatches;
3580
+ route = partialMatches[partialMatches.length - 1].route;
3581
+ if (route.path === "*") {
3582
+ // The splat is still our most accurate partial, so run with it
3583
+ return {
3584
+ type: "success",
3585
+ matches: partialMatches
3586
+ };
3587
+ }
3588
+ }
3589
+ }
3255
3590
  function _internalSetRoutes(newRoutes) {
3256
3591
  manifest = {};
3257
3592
  inFlightDataRoutes = convertRoutesToDataRoutes(newRoutes, mapRouteProperties, undefined, manifest);
@@ -3287,6 +3622,9 @@ function createRouter(init) {
3287
3622
  dispose,
3288
3623
  getBlocker,
3289
3624
  deleteBlocker,
3625
+ patchRoutes(routeId, children) {
3626
+ return patchRoutes(routeId, children, dataRoutes || inFlightDataRoutes, manifest, mapRouteProperties);
3627
+ },
3290
3628
  _internalFetchControllers: fetchControllers,
3291
3629
  _internalActiveDeferreds: activeDeferreds,
3292
3630
  // TODO: Remove setRoutes, it's temporary to avoid dealing with
@@ -4103,6 +4441,50 @@ function shouldRevalidateLoader(loaderMatch, arg) {
4103
4441
  return arg.defaultShouldRevalidate;
4104
4442
  }
4105
4443
 
4444
+ /**
4445
+ * Idempotent utility to execute route.children() method to lazily load route
4446
+ * definitions and update the routes/routeManifest
4447
+ */
4448
+ async function loadLazyRouteChildren(patchRoutesOnMissImpl, path, matches, routes, manifest, mapRouteProperties, pendingRouteChildren, signal) {
4449
+ let key = [path, ...matches.map(m => m.route.id)].join("-");
4450
+ try {
4451
+ let pending = pendingRouteChildren.get(key);
4452
+ if (!pending) {
4453
+ pending = patchRoutesOnMissImpl({
4454
+ path,
4455
+ matches,
4456
+ patch: (routeId, children) => {
4457
+ if (!signal.aborted) {
4458
+ patchRoutes(routeId, children, routes, manifest, mapRouteProperties);
4459
+ }
4460
+ }
4461
+ });
4462
+ pendingRouteChildren.set(key, pending);
4463
+ }
4464
+ if (pending && isPromise(pending)) {
4465
+ await pending;
4466
+ }
4467
+ } finally {
4468
+ pendingRouteChildren.delete(key);
4469
+ }
4470
+ }
4471
+ function patchRoutes(routeId, children, routes, manifest, mapRouteProperties) {
4472
+ if (routeId) {
4473
+ var _route$children;
4474
+ let route = manifest[routeId];
4475
+ invariant(route, "No route found to patch children into: routeId = " + routeId);
4476
+ let dataChildren = convertRoutesToDataRoutes(children, mapRouteProperties, [routeId, "patch", String(((_route$children = route.children) == null ? void 0 : _route$children.length) || "0")], manifest);
4477
+ if (route.children) {
4478
+ route.children.push(...dataChildren);
4479
+ } else {
4480
+ route.children = dataChildren;
4481
+ }
4482
+ } else {
4483
+ let dataChildren = convertRoutesToDataRoutes(children, mapRouteProperties, ["patch", String(routes.length || "0")], manifest);
4484
+ routes.push(...dataChildren);
4485
+ }
4486
+ }
4487
+
4106
4488
  /**
4107
4489
  * Execute route.lazy() methods to lazily load route modules (loader, action,
4108
4490
  * shouldRevalidate) and update the routeManifest in place which shares objects
@@ -4646,13 +5028,16 @@ function getInternalRouterError(status, _temp5) {
4646
5028
  pathname,
4647
5029
  routeId,
4648
5030
  method,
4649
- type
5031
+ type,
5032
+ message
4650
5033
  } = _temp5 === void 0 ? {} : _temp5;
4651
5034
  let statusText = "Unknown Server Error";
4652
5035
  let errorMessage = "Unknown @remix-run/router error";
4653
5036
  if (status === 400) {
4654
5037
  statusText = "Bad Request";
4655
- if (method && pathname && routeId) {
5038
+ if (type === "route-discovery") {
5039
+ errorMessage = "Unable to match URL \"" + pathname + "\" - the `children()` function for " + ("route `" + routeId + "` threw the following error:\n" + message);
5040
+ } else if (method && pathname && routeId) {
4656
5041
  errorMessage = "You made a " + method + " request to \"" + pathname + "\" but " + ("did not provide a `loader` for route \"" + routeId + "\", ") + "so there is no way to handle the request.";
4657
5042
  } else if (type === "defer-action") {
4658
5043
  errorMessage = "defer() is not supported in actions";
@@ -4713,6 +5098,9 @@ function isHashChangeOnly(a, b) {
4713
5098
  // /page#hash -> /page
4714
5099
  return false;
4715
5100
  }
5101
+ function isPromise(val) {
5102
+ return typeof val === "object" && val != null && "then" in val;
5103
+ }
4716
5104
  function isHandlerResult(result) {
4717
5105
  return result != null && typeof result === "object" && "type" in result && "result" in result && (result.type === ResultType.data || result.type === ResultType.error);
4718
5106
  }
@@ -4978,7 +5366,6 @@ function persistAppliedTransitions(_window, transitions) {
4978
5366
  }
4979
5367
  }
4980
5368
  }
4981
-
4982
5369
  //#endregion
4983
5370
 
4984
5371
  exports.AbortedDeferredError = AbortedDeferredError;