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