@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.
package/dist/router.js CHANGED
@@ -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
  *
@@ -486,7 +486,7 @@ function convertRoutesToDataRoutes(routes, mapRouteProperties, parentPath, manif
486
486
  manifest = {};
487
487
  }
488
488
  return routes.map((route, index) => {
489
- let treePath = [...parentPath, index];
489
+ let treePath = [...parentPath, String(index)];
490
490
  let id = typeof route.id === "string" ? route.id : treePath.join("-");
491
491
  invariant(route.index !== true || !route.children, "Cannot specify children on an index route");
492
492
  invariant(!manifest[id], "Found a route id collision on id \"" + id + "\". Route " + "id's must be globally unique within Data Router usages");
@@ -518,6 +518,9 @@ function matchRoutes(routes, locationArg, basename) {
518
518
  if (basename === void 0) {
519
519
  basename = "/";
520
520
  }
521
+ return matchRoutesImpl(routes, locationArg, basename, false);
522
+ }
523
+ function matchRoutesImpl(routes, locationArg, basename, allowPartial) {
521
524
  let location = typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
522
525
  let pathname = stripBasename(location.pathname || "/", basename);
523
526
  if (pathname == null) {
@@ -534,7 +537,7 @@ function matchRoutes(routes, locationArg, basename) {
534
537
  // should be a safe operation. This avoids needing matchRoutes to be
535
538
  // history-aware.
536
539
  let decoded = decodePath(pathname);
537
- matches = matchRouteBranch(branches[i], decoded);
540
+ matches = matchRouteBranch(branches[i], decoded, allowPartial);
538
541
  }
539
542
  return matches;
540
543
  }
@@ -687,7 +690,10 @@ function compareIndexes(a, b) {
687
690
  // so they sort equally.
688
691
  0;
689
692
  }
690
- function matchRouteBranch(branch, pathname) {
693
+ function matchRouteBranch(branch, pathname, allowPartial) {
694
+ if (allowPartial === void 0) {
695
+ allowPartial = false;
696
+ }
691
697
  let {
692
698
  routesMeta
693
699
  } = branch;
@@ -703,9 +709,18 @@ function matchRouteBranch(branch, pathname) {
703
709
  caseSensitive: meta.caseSensitive,
704
710
  end
705
711
  }, remainingPathname);
706
- if (!match) return null;
707
- Object.assign(matchedParams, match.params);
708
712
  let route = meta.route;
713
+ if (!match && end && allowPartial && !routesMeta[routesMeta.length - 1].route.index) {
714
+ match = matchPath({
715
+ path: meta.relativePath,
716
+ caseSensitive: meta.caseSensitive,
717
+ end: false
718
+ }, remainingPathname);
719
+ }
720
+ if (!match) {
721
+ return null;
722
+ }
723
+ Object.assign(matchedParams, match.params);
709
724
  matches.push({
710
725
  // TODO: Can this as be avoided?
711
726
  params: matchedParams,
@@ -1321,6 +1336,7 @@ function createRouter(init) {
1321
1336
  let inFlightDataRoutes;
1322
1337
  let basename = init.basename || "/";
1323
1338
  let dataStrategyImpl = init.unstable_dataStrategy || defaultDataStrategy;
1339
+ let patchRoutesOnMissImpl = init.unstable_patchRoutesOnMiss;
1324
1340
  // Config driven behavior flags
1325
1341
  let future = _extends({
1326
1342
  v7_fetcherPersist: false,
@@ -1349,7 +1365,7 @@ function createRouter(init) {
1349
1365
  let initialScrollRestored = init.hydrationData != null;
1350
1366
  let initialMatches = matchRoutes(dataRoutes, init.history.location, basename);
1351
1367
  let initialErrors = null;
1352
- if (initialMatches == null) {
1368
+ if (initialMatches == null && !patchRoutesOnMissImpl) {
1353
1369
  // If we do not match a user-provided-route, fall back to the root
1354
1370
  // to allow the error boundary to take over
1355
1371
  let error = getInternalRouterError(404, {
@@ -1365,13 +1381,15 @@ function createRouter(init) {
1365
1381
  };
1366
1382
  }
1367
1383
  let initialized;
1368
- let hasLazyRoutes = initialMatches.some(m => m.route.lazy);
1369
- let hasLoaders = initialMatches.some(m => m.route.loader);
1370
- if (hasLazyRoutes) {
1384
+ if (!initialMatches) {
1385
+ // We need to run patchRoutesOnMiss in initialize()
1386
+ initialized = false;
1387
+ initialMatches = [];
1388
+ } else if (initialMatches.some(m => m.route.lazy)) {
1371
1389
  // All initialMatches need to be loaded before we're ready. If we have lazy
1372
1390
  // functions around still then we'll need to run them in initialize()
1373
1391
  initialized = false;
1374
- } else if (!hasLoaders) {
1392
+ } else if (!initialMatches.some(m => m.route.loader)) {
1375
1393
  // If we've got no loaders to run, then we're good to go
1376
1394
  initialized = true;
1377
1395
  } else if (future.v7_partialHydration) {
@@ -1476,6 +1494,9 @@ function createRouter(init) {
1476
1494
  // Store blocker functions in a separate Map outside of router state since
1477
1495
  // we don't need to update UI state if they change
1478
1496
  let blockerFunctions = new Map();
1497
+ // Map of pending patchRoutesOnMiss() promises (keyed by path/matches) so
1498
+ // that we only kick them off once for a given combo
1499
+ let pendingPatchRoutes = new Map();
1479
1500
  // Flag to ignore the next history update, so we can revert the URL change on
1480
1501
  // a POP navigation that was blocked by the user without touching router state
1481
1502
  let ignoreNextHistoryUpdate = false;
@@ -1841,17 +1862,17 @@ function createRouter(init) {
1841
1862
  let loadingNavigation = opts && opts.overrideNavigation;
1842
1863
  let matches = matchRoutes(routesToUse, location, basename);
1843
1864
  let flushSync = (opts && opts.flushSync) === true;
1865
+ let fogOfWar = checkFogOfWar(matches, routesToUse, location.pathname);
1866
+ if (fogOfWar.active && fogOfWar.matches) {
1867
+ matches = fogOfWar.matches;
1868
+ }
1844
1869
  // Short circuit with a 404 on the root error boundary if we match nothing
1845
1870
  if (!matches) {
1846
- let error = getInternalRouterError(404, {
1847
- pathname: location.pathname
1848
- });
1849
1871
  let {
1850
- matches: notFoundMatches,
1872
+ error,
1873
+ notFoundMatches,
1851
1874
  route
1852
- } = getShortCircuitMatches(routesToUse);
1853
- // Cancel all pending deferred on 404s since we don't keep any routes
1854
- cancelActiveDeferreds();
1875
+ } = handleNavigational404(location.pathname);
1855
1876
  completeNavigation(location, {
1856
1877
  matches: notFoundMatches,
1857
1878
  loaderData: {},
@@ -1892,25 +1913,45 @@ function createRouter(init) {
1892
1913
  }];
1893
1914
  } else if (opts && opts.submission && isMutationMethod(opts.submission.formMethod)) {
1894
1915
  // Call action if we received an action submission
1895
- let actionResult = await handleAction(request, location, opts.submission, matches, {
1916
+ let actionResult = await handleAction(request, location, opts.submission, matches, fogOfWar.active, {
1896
1917
  replace: opts.replace,
1897
1918
  flushSync
1898
1919
  });
1899
1920
  if (actionResult.shortCircuited) {
1900
1921
  return;
1901
1922
  }
1923
+ // If we received a 404 from handleAction, it's because we couldn't lazily
1924
+ // discover the destination route so we don't want to call loaders
1925
+ if (actionResult.pendingActionResult) {
1926
+ let [routeId, result] = actionResult.pendingActionResult;
1927
+ if (isErrorResult(result) && isRouteErrorResponse(result.error) && result.error.status === 404) {
1928
+ pendingNavigationController = null;
1929
+ completeNavigation(location, {
1930
+ matches: actionResult.matches,
1931
+ loaderData: {},
1932
+ errors: {
1933
+ [routeId]: result.error
1934
+ }
1935
+ });
1936
+ return;
1937
+ }
1938
+ }
1939
+ matches = actionResult.matches || matches;
1902
1940
  pendingActionResult = actionResult.pendingActionResult;
1903
1941
  loadingNavigation = getLoadingNavigation(location, opts.submission);
1904
1942
  flushSync = false;
1943
+ // No need to do fog of war matching again on loader execution
1944
+ fogOfWar.active = false;
1905
1945
  // Create a GET request for the loaders
1906
1946
  request = createClientSideRequest(init.history, request.url, request.signal);
1907
1947
  }
1908
1948
  // Call loaders
1909
1949
  let {
1910
1950
  shortCircuited,
1951
+ matches: updatedMatches,
1911
1952
  loaderData,
1912
1953
  errors
1913
- } = await handleLoaders(request, location, matches, loadingNavigation, opts && opts.submission, opts && opts.fetcherSubmission, opts && opts.replace, opts && opts.initialHydration === true, flushSync, pendingActionResult);
1954
+ } = await handleLoaders(request, location, matches, fogOfWar.active, loadingNavigation, opts && opts.submission, opts && opts.fetcherSubmission, opts && opts.replace, opts && opts.initialHydration === true, flushSync, pendingActionResult);
1914
1955
  if (shortCircuited) {
1915
1956
  return;
1916
1957
  }
@@ -1919,7 +1960,7 @@ function createRouter(init) {
1919
1960
  // been assigned to a new controller for the next navigation
1920
1961
  pendingNavigationController = null;
1921
1962
  completeNavigation(location, _extends({
1922
- matches
1963
+ matches: updatedMatches || matches
1923
1964
  }, getActionDataForCommit(pendingActionResult), {
1924
1965
  loaderData,
1925
1966
  errors
@@ -1927,7 +1968,7 @@ function createRouter(init) {
1927
1968
  }
1928
1969
  // Call the action matched by the leaf route for this navigation and handle
1929
1970
  // redirects/errors
1930
- async function handleAction(request, location, submission, matches, opts) {
1971
+ async function handleAction(request, location, submission, matches, isFogOfWar, opts) {
1931
1972
  if (opts === void 0) {
1932
1973
  opts = {};
1933
1974
  }
@@ -1939,6 +1980,42 @@ function createRouter(init) {
1939
1980
  }, {
1940
1981
  flushSync: opts.flushSync === true
1941
1982
  });
1983
+ if (isFogOfWar) {
1984
+ let discoverResult = await discoverRoutes(matches, location.pathname, request.signal);
1985
+ if (discoverResult.type === "aborted") {
1986
+ return {
1987
+ shortCircuited: true
1988
+ };
1989
+ } else if (discoverResult.type === "error") {
1990
+ let {
1991
+ error,
1992
+ notFoundMatches,
1993
+ route
1994
+ } = handleDiscoverRouteError(location.pathname, discoverResult);
1995
+ return {
1996
+ matches: notFoundMatches,
1997
+ pendingActionResult: [route.id, {
1998
+ type: ResultType.error,
1999
+ error
2000
+ }]
2001
+ };
2002
+ } else if (!discoverResult.matches) {
2003
+ let {
2004
+ notFoundMatches,
2005
+ error,
2006
+ route
2007
+ } = handleNavigational404(location.pathname);
2008
+ return {
2009
+ matches: notFoundMatches,
2010
+ pendingActionResult: [route.id, {
2011
+ type: ResultType.error,
2012
+ error
2013
+ }]
2014
+ };
2015
+ } else {
2016
+ matches = discoverResult.matches;
2017
+ }
2018
+ }
1942
2019
  // Call our action and get the result
1943
2020
  let result;
1944
2021
  let actionMatch = getTargetMatch(matches, location);
@@ -1988,29 +2065,90 @@ function createRouter(init) {
1988
2065
  // Store off the pending error - we use it to determine which loaders
1989
2066
  // to call and will commit it when we complete the navigation
1990
2067
  let boundaryMatch = findNearestBoundary(matches, actionMatch.route.id);
1991
- // By default, all submissions are REPLACE navigations, but if the
1992
- // action threw an error that'll be rendered in an errorElement, we fall
1993
- // back to PUSH so that the user can use the back button to get back to
1994
- // the pre-submission form location to try again
2068
+ // By default, all submissions to the current location are REPLACE
2069
+ // navigations, but if the action threw an error that'll be rendered in
2070
+ // an errorElement, we fall back to PUSH so that the user can use the
2071
+ // back button to get back to the pre-submission form location to try
2072
+ // again
1995
2073
  if ((opts && opts.replace) !== true) {
1996
2074
  pendingAction = Action.Push;
1997
2075
  }
1998
2076
  return {
2077
+ matches,
1999
2078
  pendingActionResult: [boundaryMatch.route.id, result]
2000
2079
  };
2001
2080
  }
2002
2081
  return {
2082
+ matches,
2003
2083
  pendingActionResult: [actionMatch.route.id, result]
2004
2084
  };
2005
2085
  }
2006
2086
  // Call all applicable loaders for the given matches, handling redirects,
2007
2087
  // errors, etc.
2008
- async function handleLoaders(request, location, matches, overrideNavigation, submission, fetcherSubmission, replace, initialHydration, flushSync, pendingActionResult) {
2088
+ async function handleLoaders(request, location, matches, isFogOfWar, overrideNavigation, submission, fetcherSubmission, replace, initialHydration, flushSync, pendingActionResult) {
2009
2089
  // Figure out the right navigation we want to use for data loading
2010
2090
  let loadingNavigation = overrideNavigation || getLoadingNavigation(location, submission);
2011
2091
  // If this was a redirect from an action we don't have a "submission" but
2012
2092
  // we have it on the loading navigation so use that if available
2013
2093
  let activeSubmission = submission || fetcherSubmission || getSubmissionFromNavigation(loadingNavigation);
2094
+ // If this is an uninterrupted revalidation, we remain in our current idle
2095
+ // state. If not, we need to switch to our loading state and load data,
2096
+ // preserving any new action data or existing action data (in the case of
2097
+ // a revalidation interrupting an actionReload)
2098
+ // If we have partialHydration enabled, then don't update the state for the
2099
+ // initial data load since it's not a "navigation"
2100
+ let shouldUpdateNavigationState = !isUninterruptedRevalidation && (!future.v7_partialHydration || !initialHydration);
2101
+ // When fog of war is enabled, we enter our `loading` state earlier so we
2102
+ // can discover new routes during the `loading` state. We skip this if
2103
+ // we've already run actions since we would have done our matching already.
2104
+ // If the children() function threw then, we want to proceed with the
2105
+ // partial matches it discovered.
2106
+ if (isFogOfWar) {
2107
+ if (shouldUpdateNavigationState) {
2108
+ let actionData = getUpdatedActionData(pendingActionResult);
2109
+ updateState(_extends({
2110
+ navigation: loadingNavigation
2111
+ }, actionData !== undefined ? {
2112
+ actionData
2113
+ } : {}), {
2114
+ flushSync
2115
+ });
2116
+ }
2117
+ let discoverResult = await discoverRoutes(matches, location.pathname, request.signal);
2118
+ if (discoverResult.type === "aborted") {
2119
+ return {
2120
+ shortCircuited: true
2121
+ };
2122
+ } else if (discoverResult.type === "error") {
2123
+ let {
2124
+ error,
2125
+ notFoundMatches,
2126
+ route
2127
+ } = handleDiscoverRouteError(location.pathname, discoverResult);
2128
+ return {
2129
+ matches: notFoundMatches,
2130
+ loaderData: {},
2131
+ errors: {
2132
+ [route.id]: error
2133
+ }
2134
+ };
2135
+ } else if (!discoverResult.matches) {
2136
+ let {
2137
+ error,
2138
+ notFoundMatches,
2139
+ route
2140
+ } = handleNavigational404(location.pathname);
2141
+ return {
2142
+ matches: notFoundMatches,
2143
+ loaderData: {},
2144
+ errors: {
2145
+ [route.id]: error
2146
+ }
2147
+ };
2148
+ } else {
2149
+ matches = discoverResult.matches;
2150
+ }
2151
+ }
2014
2152
  let routesToUse = inFlightDataRoutes || dataRoutes;
2015
2153
  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);
2016
2154
  // Cancel pending deferreds for no-longer-matched routes or routes we're
@@ -2037,40 +2175,20 @@ function createRouter(init) {
2037
2175
  shortCircuited: true
2038
2176
  };
2039
2177
  }
2040
- // If this is an uninterrupted revalidation, we remain in our current idle
2041
- // state. If not, we need to switch to our loading state and load data,
2042
- // preserving any new action data or existing action data (in the case of
2043
- // a revalidation interrupting an actionReload)
2044
- // If we have partialHydration enabled, then don't update the state for the
2045
- // initial data load since it's not a "navigation"
2046
- if (!isUninterruptedRevalidation && (!future.v7_partialHydration || !initialHydration)) {
2047
- revalidatingFetchers.forEach(rf => {
2048
- let fetcher = state.fetchers.get(rf.key);
2049
- let revalidatingFetcher = getLoadingFetcher(undefined, fetcher ? fetcher.data : undefined);
2050
- state.fetchers.set(rf.key, revalidatingFetcher);
2051
- });
2052
- let actionData;
2053
- if (pendingActionResult && !isErrorResult(pendingActionResult[1])) {
2054
- // This is cast to `any` currently because `RouteData`uses any and it
2055
- // would be a breaking change to use any.
2056
- // TODO: v7 - change `RouteData` to use `unknown` instead of `any`
2057
- actionData = {
2058
- [pendingActionResult[0]]: pendingActionResult[1].data
2059
- };
2060
- } else if (state.actionData) {
2061
- if (Object.keys(state.actionData).length === 0) {
2062
- actionData = null;
2063
- } else {
2064
- actionData = state.actionData;
2178
+ if (shouldUpdateNavigationState) {
2179
+ let updates = {};
2180
+ if (!isFogOfWar) {
2181
+ // Only update navigation/actionNData if we didn't already do it above
2182
+ updates.navigation = loadingNavigation;
2183
+ let actionData = getUpdatedActionData(pendingActionResult);
2184
+ if (actionData !== undefined) {
2185
+ updates.actionData = actionData;
2065
2186
  }
2066
2187
  }
2067
- updateState(_extends({
2068
- navigation: loadingNavigation
2069
- }, actionData !== undefined ? {
2070
- actionData
2071
- } : {}, revalidatingFetchers.length > 0 ? {
2072
- fetchers: new Map(state.fetchers)
2073
- } : {}), {
2188
+ if (revalidatingFetchers.length > 0) {
2189
+ updates.fetchers = getUpdatedRevalidatingFetchers(revalidatingFetchers);
2190
+ }
2191
+ updateState(updates, {
2074
2192
  flushSync
2075
2193
  });
2076
2194
  }
@@ -2155,12 +2273,37 @@ function createRouter(init) {
2155
2273
  let didAbortFetchLoads = abortStaleFetchLoads(pendingNavigationLoadId);
2156
2274
  let shouldUpdateFetchers = updatedFetchers || didAbortFetchLoads || revalidatingFetchers.length > 0;
2157
2275
  return _extends({
2276
+ matches,
2158
2277
  loaderData,
2159
2278
  errors
2160
2279
  }, shouldUpdateFetchers ? {
2161
2280
  fetchers: new Map(state.fetchers)
2162
2281
  } : {});
2163
2282
  }
2283
+ function getUpdatedActionData(pendingActionResult) {
2284
+ if (pendingActionResult && !isErrorResult(pendingActionResult[1])) {
2285
+ // This is cast to `any` currently because `RouteData`uses any and it
2286
+ // would be a breaking change to use any.
2287
+ // TODO: v7 - change `RouteData` to use `unknown` instead of `any`
2288
+ return {
2289
+ [pendingActionResult[0]]: pendingActionResult[1].data
2290
+ };
2291
+ } else if (state.actionData) {
2292
+ if (Object.keys(state.actionData).length === 0) {
2293
+ return null;
2294
+ } else {
2295
+ return state.actionData;
2296
+ }
2297
+ }
2298
+ }
2299
+ function getUpdatedRevalidatingFetchers(revalidatingFetchers) {
2300
+ revalidatingFetchers.forEach(rf => {
2301
+ let fetcher = state.fetchers.get(rf.key);
2302
+ let revalidatingFetcher = getLoadingFetcher(undefined, fetcher ? fetcher.data : undefined);
2303
+ state.fetchers.set(rf.key, revalidatingFetcher);
2304
+ });
2305
+ return new Map(state.fetchers);
2306
+ }
2164
2307
  // Trigger a fetcher load/submit for the given fetcher key
2165
2308
  function fetch(key, routeId, href, opts) {
2166
2309
  if (isServer) {
@@ -2171,6 +2314,10 @@ function createRouter(init) {
2171
2314
  let routesToUse = inFlightDataRoutes || dataRoutes;
2172
2315
  let normalizedPath = normalizeTo(state.location, state.matches, basename, future.v7_prependBasename, href, future.v7_relativeSplatPath, routeId, opts == null ? void 0 : opts.relative);
2173
2316
  let matches = matchRoutes(routesToUse, normalizedPath, basename);
2317
+ let fogOfWar = checkFogOfWar(matches, routesToUse, normalizedPath);
2318
+ if (fogOfWar.active && fogOfWar.matches) {
2319
+ matches = fogOfWar.matches;
2320
+ }
2174
2321
  if (!matches) {
2175
2322
  setFetcherError(key, routeId, getInternalRouterError(404, {
2176
2323
  pathname: normalizedPath
@@ -2193,7 +2340,7 @@ function createRouter(init) {
2193
2340
  let match = getTargetMatch(matches, path);
2194
2341
  pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
2195
2342
  if (submission && isMutationMethod(submission.formMethod)) {
2196
- handleFetcherAction(key, routeId, path, match, matches, flushSync, submission);
2343
+ handleFetcherAction(key, routeId, path, match, matches, fogOfWar.active, flushSync, submission);
2197
2344
  return;
2198
2345
  }
2199
2346
  // Store off the match so we can call it's shouldRevalidate on subsequent
@@ -2202,22 +2349,28 @@ function createRouter(init) {
2202
2349
  routeId,
2203
2350
  path
2204
2351
  });
2205
- handleFetcherLoader(key, routeId, path, match, matches, flushSync, submission);
2352
+ handleFetcherLoader(key, routeId, path, match, matches, fogOfWar.active, flushSync, submission);
2206
2353
  }
2207
2354
  // Call the action for the matched fetcher.submit(), and then handle redirects,
2208
2355
  // errors, and revalidation
2209
- async function handleFetcherAction(key, routeId, path, match, requestMatches, flushSync, submission) {
2356
+ async function handleFetcherAction(key, routeId, path, match, requestMatches, isFogOfWar, flushSync, submission) {
2210
2357
  interruptActiveLoads();
2211
2358
  fetchLoadMatches.delete(key);
2212
- if (!match.route.action && !match.route.lazy) {
2213
- let error = getInternalRouterError(405, {
2214
- method: submission.formMethod,
2215
- pathname: path,
2216
- routeId: routeId
2217
- });
2218
- setFetcherError(key, routeId, error, {
2219
- flushSync
2220
- });
2359
+ function detectAndHandle405Error(m) {
2360
+ if (!m.route.action && !m.route.lazy) {
2361
+ let error = getInternalRouterError(405, {
2362
+ method: submission.formMethod,
2363
+ pathname: path,
2364
+ routeId: routeId
2365
+ });
2366
+ setFetcherError(key, routeId, error, {
2367
+ flushSync
2368
+ });
2369
+ return true;
2370
+ }
2371
+ return false;
2372
+ }
2373
+ if (!isFogOfWar && detectAndHandle405Error(match)) {
2221
2374
  return;
2222
2375
  }
2223
2376
  // Put this fetcher into it's submitting state
@@ -2225,9 +2378,36 @@ function createRouter(init) {
2225
2378
  updateFetcherState(key, getSubmittingFetcher(submission, existingFetcher), {
2226
2379
  flushSync
2227
2380
  });
2228
- // Call the action for the fetcher
2229
2381
  let abortController = new AbortController();
2230
2382
  let fetchRequest = createClientSideRequest(init.history, path, abortController.signal, submission);
2383
+ if (isFogOfWar) {
2384
+ let discoverResult = await discoverRoutes(requestMatches, path, fetchRequest.signal);
2385
+ if (discoverResult.type === "aborted") {
2386
+ return;
2387
+ } else if (discoverResult.type === "error") {
2388
+ let {
2389
+ error
2390
+ } = handleDiscoverRouteError(path, discoverResult);
2391
+ setFetcherError(key, routeId, error, {
2392
+ flushSync
2393
+ });
2394
+ return;
2395
+ } else if (!discoverResult.matches) {
2396
+ setFetcherError(key, routeId, getInternalRouterError(404, {
2397
+ pathname: path
2398
+ }), {
2399
+ flushSync
2400
+ });
2401
+ return;
2402
+ } else {
2403
+ requestMatches = discoverResult.matches;
2404
+ match = getTargetMatch(requestMatches, path);
2405
+ if (detectAndHandle405Error(match)) {
2406
+ return;
2407
+ }
2408
+ }
2409
+ }
2410
+ // Call the action for the fetcher
2231
2411
  fetchControllers.set(key, abortController);
2232
2412
  let originatingLoadId = incrementingLoadId;
2233
2413
  let actionResults = await callDataStrategy("action", fetchRequest, [match], requestMatches);
@@ -2369,14 +2549,38 @@ function createRouter(init) {
2369
2549
  }
2370
2550
  }
2371
2551
  // Call the matched loader for fetcher.load(), handling redirects, errors, etc.
2372
- async function handleFetcherLoader(key, routeId, path, match, matches, flushSync, submission) {
2552
+ async function handleFetcherLoader(key, routeId, path, match, matches, isFogOfWar, flushSync, submission) {
2373
2553
  let existingFetcher = state.fetchers.get(key);
2374
2554
  updateFetcherState(key, getLoadingFetcher(submission, existingFetcher ? existingFetcher.data : undefined), {
2375
2555
  flushSync
2376
2556
  });
2377
- // Call the loader for this fetcher route match
2378
2557
  let abortController = new AbortController();
2379
2558
  let fetchRequest = createClientSideRequest(init.history, path, abortController.signal);
2559
+ if (isFogOfWar) {
2560
+ let discoverResult = await discoverRoutes(matches, path, fetchRequest.signal);
2561
+ if (discoverResult.type === "aborted") {
2562
+ return;
2563
+ } else if (discoverResult.type === "error") {
2564
+ let {
2565
+ error
2566
+ } = handleDiscoverRouteError(path, discoverResult);
2567
+ setFetcherError(key, routeId, error, {
2568
+ flushSync
2569
+ });
2570
+ return;
2571
+ } else if (!discoverResult.matches) {
2572
+ setFetcherError(key, routeId, getInternalRouterError(404, {
2573
+ pathname: path
2574
+ }), {
2575
+ flushSync
2576
+ });
2577
+ return;
2578
+ } else {
2579
+ matches = discoverResult.matches;
2580
+ match = getTargetMatch(matches, path);
2581
+ }
2582
+ }
2583
+ // Call the loader for this fetcher route match
2380
2584
  fetchControllers.set(key, abortController);
2381
2585
  let originatingLoadId = incrementingLoadId;
2382
2586
  let results = await callDataStrategy("loader", fetchRequest, [match], matches);
@@ -2743,6 +2947,38 @@ function createRouter(init) {
2743
2947
  return blockerKey;
2744
2948
  }
2745
2949
  }
2950
+ function handleNavigational404(pathname) {
2951
+ let error = getInternalRouterError(404, {
2952
+ pathname
2953
+ });
2954
+ let routesToUse = inFlightDataRoutes || dataRoutes;
2955
+ let {
2956
+ matches,
2957
+ route
2958
+ } = getShortCircuitMatches(routesToUse);
2959
+ // Cancel all pending deferred on 404s since we don't keep any routes
2960
+ cancelActiveDeferreds();
2961
+ return {
2962
+ notFoundMatches: matches,
2963
+ route,
2964
+ error
2965
+ };
2966
+ }
2967
+ function handleDiscoverRouteError(pathname, discoverResult) {
2968
+ let matches = discoverResult.partialMatches;
2969
+ let route = matches[matches.length - 1].route;
2970
+ let error = getInternalRouterError(400, {
2971
+ type: "route-discovery",
2972
+ routeId: route.id,
2973
+ pathname,
2974
+ message: discoverResult.error != null && "message" in discoverResult.error ? discoverResult.error : String(discoverResult.error)
2975
+ });
2976
+ return {
2977
+ notFoundMatches: matches,
2978
+ route,
2979
+ error
2980
+ };
2981
+ }
2746
2982
  function cancelActiveDeferreds(predicate) {
2747
2983
  let cancelledRouteIds = [];
2748
2984
  activeDeferreds.forEach((dfd, routeId) => {
@@ -2804,6 +3040,99 @@ function createRouter(init) {
2804
3040
  }
2805
3041
  return null;
2806
3042
  }
3043
+ function checkFogOfWar(matches, routesToUse, pathname) {
3044
+ if (patchRoutesOnMissImpl) {
3045
+ if (!matches) {
3046
+ let fogMatches = matchRoutesImpl(routesToUse, pathname, basename, true);
3047
+ return {
3048
+ active: true,
3049
+ matches: fogMatches || []
3050
+ };
3051
+ } else {
3052
+ let leafRoute = matches[matches.length - 1].route;
3053
+ if (leafRoute.path === "*") {
3054
+ // If we matched a splat, it might only be because we haven't yet fetched
3055
+ // the children that would match with a higher score, so let's fetch
3056
+ // around and find out
3057
+ let partialMatches = matchRoutesImpl(routesToUse, pathname, basename, true);
3058
+ return {
3059
+ active: true,
3060
+ matches: partialMatches
3061
+ };
3062
+ }
3063
+ }
3064
+ }
3065
+ return {
3066
+ active: false,
3067
+ matches: null
3068
+ };
3069
+ }
3070
+ async function discoverRoutes(matches, pathname, signal) {
3071
+ let partialMatches = matches;
3072
+ let route = partialMatches.length > 0 ? partialMatches[partialMatches.length - 1].route : null;
3073
+ while (true) {
3074
+ try {
3075
+ await loadLazyRouteChildren(patchRoutesOnMissImpl, pathname, partialMatches, dataRoutes || inFlightDataRoutes, manifest, mapRouteProperties, pendingPatchRoutes, signal);
3076
+ } catch (e) {
3077
+ return {
3078
+ type: "error",
3079
+ error: e,
3080
+ partialMatches
3081
+ };
3082
+ }
3083
+ if (signal.aborted) {
3084
+ return {
3085
+ type: "aborted"
3086
+ };
3087
+ }
3088
+ let routesToUse = inFlightDataRoutes || dataRoutes;
3089
+ let newMatches = matchRoutes(routesToUse, pathname, basename);
3090
+ let matchedSplat = false;
3091
+ if (newMatches) {
3092
+ let leafRoute = newMatches[newMatches.length - 1].route;
3093
+ if (leafRoute.index) {
3094
+ // If we found an index route, we can stop
3095
+ return {
3096
+ type: "success",
3097
+ matches: newMatches
3098
+ };
3099
+ }
3100
+ if (leafRoute.path && leafRoute.path.length > 0) {
3101
+ if (leafRoute.path === "*") {
3102
+ // If we found a splat route, we can't be sure there's not a
3103
+ // higher-scoring route down some partial matches trail so we need
3104
+ // to check that out
3105
+ matchedSplat = true;
3106
+ } else {
3107
+ // If we found a non-splat route, we can stop
3108
+ return {
3109
+ type: "success",
3110
+ matches: newMatches
3111
+ };
3112
+ }
3113
+ }
3114
+ }
3115
+ let newPartialMatches = matchRoutesImpl(routesToUse, pathname, basename, true);
3116
+ // If we are no longer partially matching anything, this was either a
3117
+ // legit splat match above, or it's a 404. Also avoid loops if the
3118
+ // second pass results in the same partial matches
3119
+ if (!newPartialMatches || partialMatches.map(m => m.route.id).join("-") === newPartialMatches.map(m => m.route.id).join("-")) {
3120
+ return {
3121
+ type: "success",
3122
+ matches: matchedSplat ? newMatches : null
3123
+ };
3124
+ }
3125
+ partialMatches = newPartialMatches;
3126
+ route = partialMatches[partialMatches.length - 1].route;
3127
+ if (route.path === "*") {
3128
+ // The splat is still our most accurate partial, so run with it
3129
+ return {
3130
+ type: "success",
3131
+ matches: partialMatches
3132
+ };
3133
+ }
3134
+ }
3135
+ }
2807
3136
  function _internalSetRoutes(newRoutes) {
2808
3137
  manifest = {};
2809
3138
  inFlightDataRoutes = convertRoutesToDataRoutes(newRoutes, mapRouteProperties, undefined, manifest);
@@ -2839,6 +3168,9 @@ function createRouter(init) {
2839
3168
  dispose,
2840
3169
  getBlocker,
2841
3170
  deleteBlocker,
3171
+ patchRoutes(routeId, children) {
3172
+ return patchRoutes(routeId, children, dataRoutes || inFlightDataRoutes, manifest, mapRouteProperties);
3173
+ },
2842
3174
  _internalFetchControllers: fetchControllers,
2843
3175
  _internalActiveDeferreds: activeDeferreds,
2844
3176
  // TODO: Remove setRoutes, it's temporary to avoid dealing with
@@ -3614,6 +3946,49 @@ function shouldRevalidateLoader(loaderMatch, arg) {
3614
3946
  }
3615
3947
  return arg.defaultShouldRevalidate;
3616
3948
  }
3949
+ /**
3950
+ * Idempotent utility to execute route.children() method to lazily load route
3951
+ * definitions and update the routes/routeManifest
3952
+ */
3953
+ async function loadLazyRouteChildren(patchRoutesOnMissImpl, path, matches, routes, manifest, mapRouteProperties, pendingRouteChildren, signal) {
3954
+ let key = [path, ...matches.map(m => m.route.id)].join("-");
3955
+ try {
3956
+ let pending = pendingRouteChildren.get(key);
3957
+ if (!pending) {
3958
+ pending = patchRoutesOnMissImpl({
3959
+ path,
3960
+ matches,
3961
+ patch: (routeId, children) => {
3962
+ if (!signal.aborted) {
3963
+ patchRoutes(routeId, children, routes, manifest, mapRouteProperties);
3964
+ }
3965
+ }
3966
+ });
3967
+ pendingRouteChildren.set(key, pending);
3968
+ }
3969
+ if (pending && isPromise(pending)) {
3970
+ await pending;
3971
+ }
3972
+ } finally {
3973
+ pendingRouteChildren.delete(key);
3974
+ }
3975
+ }
3976
+ function patchRoutes(routeId, children, routes, manifest, mapRouteProperties) {
3977
+ if (routeId) {
3978
+ var _route$children;
3979
+ let route = manifest[routeId];
3980
+ invariant(route, "No route found to patch children into: routeId = " + routeId);
3981
+ let dataChildren = convertRoutesToDataRoutes(children, mapRouteProperties, [routeId, "patch", String(((_route$children = route.children) == null ? void 0 : _route$children.length) || "0")], manifest);
3982
+ if (route.children) {
3983
+ route.children.push(...dataChildren);
3984
+ } else {
3985
+ route.children = dataChildren;
3986
+ }
3987
+ } else {
3988
+ let dataChildren = convertRoutesToDataRoutes(children, mapRouteProperties, ["patch", String(routes.length || "0")], manifest);
3989
+ routes.push(...dataChildren);
3990
+ }
3991
+ }
3617
3992
  /**
3618
3993
  * Execute route.lazy() methods to lazily load route modules (loader, action,
3619
3994
  * shouldRevalidate) and update the routeManifest in place which shares objects
@@ -4139,13 +4514,16 @@ function getInternalRouterError(status, _temp5) {
4139
4514
  pathname,
4140
4515
  routeId,
4141
4516
  method,
4142
- type
4517
+ type,
4518
+ message
4143
4519
  } = _temp5 === void 0 ? {} : _temp5;
4144
4520
  let statusText = "Unknown Server Error";
4145
4521
  let errorMessage = "Unknown @remix-run/router error";
4146
4522
  if (status === 400) {
4147
4523
  statusText = "Bad Request";
4148
- if (method && pathname && routeId) {
4524
+ if (type === "route-discovery") {
4525
+ errorMessage = "Unable to match URL \"" + pathname + "\" - the `children()` function for " + ("route `" + routeId + "` threw the following error:\n" + message);
4526
+ } else if (method && pathname && routeId) {
4149
4527
  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.";
4150
4528
  } else if (type === "defer-action") {
4151
4529
  errorMessage = "defer() is not supported in actions";
@@ -4204,6 +4582,9 @@ function isHashChangeOnly(a, b) {
4204
4582
  // /page#hash -> /page
4205
4583
  return false;
4206
4584
  }
4585
+ function isPromise(val) {
4586
+ return typeof val === "object" && val != null && "then" in val;
4587
+ }
4207
4588
  function isHandlerResult(result) {
4208
4589
  return result != null && typeof result === "object" && "type" in result && "result" in result && (result.type === ResultType.data || result.type === ResultType.error);
4209
4590
  }