@remix-run/router 0.0.0-experimental-e7e9ce6e → 0.0.0-experimental-91f2bf54

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/utils.d.ts CHANGED
@@ -117,6 +117,27 @@ export interface ShouldRevalidateFunction {
117
117
  defaultShouldRevalidate: boolean;
118
118
  }): boolean;
119
119
  }
120
+ /**
121
+ * Function provided by the framework-aware layers to set `hasErrorBoundary`
122
+ * from the framework-aware `errorElement` prop
123
+ */
124
+ export interface DetectErrorBoundaryFunction {
125
+ (route: AgnosticRouteObject): boolean;
126
+ }
127
+ /**
128
+ * Keys we cannot change from within a lazy() function. We spread all other keys
129
+ * onto the route. Either they're meaningful to the router, or they'll get
130
+ * ignored.
131
+ */
132
+ export declare type ImmutableRouteKey = "lazy" | "caseSensitive" | "path" | "id" | "index" | "children";
133
+ export declare const immutableRouteKeys: Set<ImmutableRouteKey>;
134
+ /**
135
+ * lazy() function to load a route definition, which can add non-matching
136
+ * related properties to a route
137
+ */
138
+ export interface LazyRouteFunction<R extends AgnosticRouteObject> {
139
+ (): Promise<Omit<R, ImmutableRouteKey>>;
140
+ }
120
141
  /**
121
142
  * Base RouteObject with common props shared by all types of routes
122
143
  */
@@ -129,6 +150,7 @@ declare type AgnosticBaseRouteObject = {
129
150
  hasErrorBoundary?: boolean;
130
151
  shouldRevalidate?: ShouldRevalidateFunction;
131
152
  handle?: any;
153
+ lazy?: LazyRouteFunction<AgnosticBaseRouteObject>;
132
154
  };
133
155
  /**
134
156
  * Index routes must not have children
@@ -160,6 +182,7 @@ export declare type AgnosticDataNonIndexRouteObject = AgnosticNonIndexRouteObjec
160
182
  * A data route object, which is just a RouteObject with a required unique ID
161
183
  */
162
184
  export declare type AgnosticDataRouteObject = AgnosticDataIndexRouteObject | AgnosticDataNonIndexRouteObject;
185
+ export declare type RouteManifest = Record<string, AgnosticDataRouteObject | undefined>;
163
186
  declare type _PathParam<Path extends string> = Path extends `${infer L}/${infer R}` ? _PathParam<L> | _PathParam<R> : Path extends `:${infer Param}` ? Param extends `${infer Optional}?` ? Optional : Param : never;
164
187
  /**
165
188
  * Examples:
@@ -170,7 +193,7 @@ declare type _PathParam<Path extends string> = Path extends `${infer L}/${infer
170
193
  * "/:a/:b" -> "a" | "b"
171
194
  * "/:a/b/:c/*" -> "a" | "c" | "*"
172
195
  */
173
- declare type PathParam<Path extends string> = Path extends "*" ? "*" : Path extends `${infer Rest}/*` ? "*" | _PathParam<Rest> : _PathParam<Path>;
196
+ declare type PathParam<Path extends string> = Path extends "*" | "/*" ? "*" : Path extends `${infer Rest}/*` ? "*" | _PathParam<Rest> : _PathParam<Path>;
174
197
  export declare type ParamParseKey<Segment extends string> = [
175
198
  PathParam<Segment>
176
199
  ] extends [never] ? string : PathParam<Segment>;
@@ -203,7 +226,7 @@ export interface AgnosticRouteMatch<ParamKey extends string = string, RouteObjec
203
226
  }
204
227
  export interface AgnosticDataRouteMatch extends AgnosticRouteMatch<string, AgnosticDataRouteObject> {
205
228
  }
206
- export declare function convertRoutesToDataRoutes(routes: AgnosticRouteObject[], parentPath?: number[], allIds?: Set<string>): AgnosticDataRouteObject[];
229
+ export declare function convertRoutesToDataRoutes(routes: AgnosticRouteObject[], detectErrorBoundary: DetectErrorBoundaryFunction, parentPath?: number[], manifest?: RouteManifest): AgnosticDataRouteObject[];
207
230
  /**
208
231
  * Matches the given routes to a location and returns the match data.
209
232
  *
@@ -270,10 +293,6 @@ export declare function matchPath<ParamKey extends ParamParseKey<Path>, Path ext
270
293
  * @private
271
294
  */
272
295
  export declare function stripBasename(pathname: string, basename: string): string | null;
273
- /**
274
- * @private
275
- */
276
- export declare function warning(cond: any, message: string): void;
277
296
  /**
278
297
  * Returns a resolved path object relative to the given pathname.
279
298
  *
package/history.ts CHANGED
@@ -481,7 +481,7 @@ export function invariant(value: any, message?: string) {
481
481
  }
482
482
  }
483
483
 
484
- function warning(cond: any, message: string) {
484
+ export function warning(cond: any, message: string) {
485
485
  if (!cond) {
486
486
  // eslint-disable-next-line no-console
487
487
  if (typeof console !== "undefined") console.warn(message);
package/index.ts CHANGED
@@ -9,6 +9,7 @@ export type {
9
9
  AgnosticNonIndexRouteObject,
10
10
  AgnosticRouteMatch,
11
11
  AgnosticRouteObject,
12
+ LazyRouteFunction,
12
13
  TrackedPromise,
13
14
  FormEncType,
14
15
  FormMethod,
@@ -40,7 +41,6 @@ export {
40
41
  resolvePath,
41
42
  resolveTo,
42
43
  stripBasename,
43
- warning,
44
44
  } from "./utils";
45
45
 
46
46
  export type {
@@ -76,10 +76,14 @@ export * from "./router";
76
76
  ///////////////////////////////////////////////////////////////////////////////
77
77
 
78
78
  /** @internal */
79
+ export type { RouteManifest as UNSAFE_RouteManifest } from "./utils";
79
80
  export {
80
81
  DeferredData as UNSAFE_DeferredData,
81
82
  convertRoutesToDataRoutes as UNSAFE_convertRoutesToDataRoutes,
82
83
  getPathContributingMatches as UNSAFE_getPathContributingMatches,
83
84
  } from "./utils";
84
85
 
85
- export { invariant as UNSAFE_invariant } from "./history";
86
+ export {
87
+ invariant as UNSAFE_invariant,
88
+ warning as UNSAFE_warning,
89
+ } from "./history";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remix-run/router",
3
- "version": "0.0.0-experimental-e7e9ce6e",
3
+ "version": "0.0.0-experimental-91f2bf54",
4
4
  "description": "Nested/Data-driven/Framework-agnostic Routing",
5
5
  "keywords": [
6
6
  "remix",
package/router.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  createPath,
6
6
  invariant,
7
7
  parsePath,
8
+ warning,
8
9
  } from "./history";
9
10
  import type {
10
11
  DataResult,
@@ -14,6 +15,7 @@ import type {
14
15
  ErrorResult,
15
16
  FormEncType,
16
17
  FormMethod,
18
+ DetectErrorBoundaryFunction,
17
19
  RedirectResult,
18
20
  RouteData,
19
21
  AgnosticRouteObject,
@@ -22,6 +24,10 @@ import type {
22
24
  AgnosticRouteMatch,
23
25
  MutationFormMethod,
24
26
  ShouldRevalidateFunction,
27
+ RouteManifest,
28
+ ImmutableRouteKey,
29
+ ActionFunction,
30
+ LoaderFunction,
25
31
  } from "./utils";
26
32
  import {
27
33
  DeferredData,
@@ -29,12 +35,12 @@ import {
29
35
  ResultType,
30
36
  convertRoutesToDataRoutes,
31
37
  getPathContributingMatches,
38
+ immutableRouteKeys,
32
39
  isRouteErrorResponse,
33
40
  joinPaths,
34
41
  matchRoutes,
35
42
  resolveTo,
36
43
  stripBasename,
37
- warning,
38
44
  } from "./utils";
39
45
 
40
46
  ////////////////////////////////////////////////////////////////////////////////
@@ -328,6 +334,7 @@ export interface RouterInit {
328
334
  routes: AgnosticRouteObject[];
329
335
  history: History;
330
336
  hydrationData?: HydrationState;
337
+ detectErrorBoundary?: DetectErrorBoundaryFunction;
331
338
  }
332
339
 
333
340
  /**
@@ -638,6 +645,9 @@ const isBrowser =
638
645
  typeof window.document !== "undefined" &&
639
646
  typeof window.document.createElement !== "undefined";
640
647
  const isServer = !isBrowser;
648
+
649
+ const defaultDetectErrorBoundary = (route: AgnosticRouteObject) =>
650
+ Boolean(route.hasErrorBoundary);
641
651
  //#endregion
642
652
 
643
653
  ////////////////////////////////////////////////////////////////////////////////
@@ -653,7 +663,18 @@ export function createRouter(init: RouterInit): Router {
653
663
  "You must provide a non-empty routes array to createRouter"
654
664
  );
655
665
 
656
- let dataRoutes = convertRoutesToDataRoutes(init.routes);
666
+ let detectErrorBoundary =
667
+ init.detectErrorBoundary || defaultDetectErrorBoundary;
668
+
669
+ // Routes keyed by ID
670
+ let manifest: RouteManifest = {};
671
+ // Routes in tree format for matching
672
+ let dataRoutes = convertRoutesToDataRoutes(
673
+ init.routes,
674
+ detectErrorBoundary,
675
+ undefined,
676
+ manifest
677
+ );
657
678
  let inFlightDataRoutes: AgnosticDataRouteObject[] | undefined;
658
679
  // Cleanup function for history
659
680
  let unlistenHistory: (() => void) | null = null;
@@ -692,7 +713,11 @@ export function createRouter(init: RouterInit): Router {
692
713
  }
693
714
 
694
715
  let initialized =
695
- !initialMatches.some((m) => m.route.loader) || init.hydrationData != null;
716
+ // All initialMatches need to be loaded before we're ready. If we have lazy
717
+ // functions around still then we'll need to run them in initialize()
718
+ !initialMatches.some((m) => m.route.lazy) &&
719
+ // And we have to either have no loaders or have been provided hydrationData
720
+ (!initialMatches.some((m) => m.route.loader) || init.hydrationData != null);
696
721
 
697
722
  let router: Router;
698
723
  let state: RouterState = {
@@ -837,11 +862,35 @@ export function createRouter(init: RouterInit): Router {
837
862
  }
838
863
  );
839
864
 
840
- // Kick off initial data load if needed. Use Pop to avoid modifying history
841
- if (!state.initialized) {
865
+ if (state.initialized) {
866
+ return router;
867
+ }
868
+
869
+ let lazyMatches = state.matches.filter((m) => m.route.lazy);
870
+
871
+ if (lazyMatches.length === 0) {
872
+ // Kick off initial data load if needed. Use Pop to avoid modifying history
842
873
  startNavigation(HistoryAction.Pop, state.location);
874
+ return router;
843
875
  }
844
876
 
877
+ // Load lazy modules, then kick off initial data load if needed
878
+ let lazyPromises = lazyMatches.map((m) =>
879
+ loadLazyRouteModule(m.route, detectErrorBoundary, manifest)
880
+ );
881
+ Promise.all(lazyPromises).then(() => {
882
+ let initialized =
883
+ !state.matches.some((m) => m.route.loader) ||
884
+ init.hydrationData != null;
885
+ if (initialized) {
886
+ // We already have required loaderData so we can just set initialized
887
+ updateState({ initialized: true });
888
+ } else {
889
+ // We still need to kick off initial data loads
890
+ startNavigation(HistoryAction.Pop, state.location);
891
+ }
892
+ });
893
+
845
894
  return router;
846
895
  }
847
896
 
@@ -871,12 +920,27 @@ export function createRouter(init: RouterInit): Router {
871
920
  subscribers.forEach((subscriber) => subscriber(state));
872
921
  }
873
922
 
923
+ function completeNavigation(
924
+ location: Location,
925
+ newState: Partial<Omit<RouterState, "action" | "location" | "navigation">>
926
+ ) {
927
+ // @ts-expect-error
928
+ if (typeof document !== "undefined" && document.startViewTransition) {
929
+ // @ts-expect-error
930
+ document.startViewTransition(() =>
931
+ completeNavigationForRealz(location, newState)
932
+ );
933
+ } else {
934
+ completeNavigationForRealz(location, newState);
935
+ }
936
+ }
937
+
874
938
  // Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION
875
939
  // and setting state.[historyAction/location/matches] to the new route.
876
940
  // - Location is a required param
877
941
  // - Navigation will always be set to IDLE_NAVIGATION
878
942
  // - Can pass any other state in newState
879
- function completeNavigation(
943
+ function completeNavigationForRealz(
880
944
  location: Location,
881
945
  newState: Partial<Omit<RouterState, "action" | "location" | "navigation">>
882
946
  ): void {
@@ -1259,7 +1323,7 @@ export function createRouter(init: RouterInit): Router {
1259
1323
  let result: DataResult;
1260
1324
  let actionMatch = getTargetMatch(matches, location);
1261
1325
 
1262
- if (!actionMatch.route.action) {
1326
+ if (!actionMatch.route.action && !actionMatch.route.lazy) {
1263
1327
  result = {
1264
1328
  type: ResultType.error,
1265
1329
  error: getInternalRouterError(405, {
@@ -1274,6 +1338,8 @@ export function createRouter(init: RouterInit): Router {
1274
1338
  request,
1275
1339
  actionMatch,
1276
1340
  matches,
1341
+ manifest,
1342
+ detectErrorBoundary,
1277
1343
  router.basename
1278
1344
  );
1279
1345
 
@@ -1566,7 +1632,7 @@ export function createRouter(init: RouterInit): Router {
1566
1632
  interruptActiveLoads();
1567
1633
  fetchLoadMatches.delete(key);
1568
1634
 
1569
- if (!match.route.action) {
1635
+ if (!match.route.action && !match.route.lazy) {
1570
1636
  let error = getInternalRouterError(405, {
1571
1637
  method: submission.formMethod,
1572
1638
  pathname: path,
@@ -1602,6 +1668,8 @@ export function createRouter(init: RouterInit): Router {
1602
1668
  fetchRequest,
1603
1669
  match,
1604
1670
  requestMatches,
1671
+ manifest,
1672
+ detectErrorBoundary,
1605
1673
  router.basename
1606
1674
  );
1607
1675
 
@@ -1821,11 +1889,14 @@ export function createRouter(init: RouterInit): Router {
1821
1889
  abortController.signal
1822
1890
  );
1823
1891
  fetchControllers.set(key, abortController);
1892
+
1824
1893
  let result: DataResult = await callLoaderOrAction(
1825
1894
  "loader",
1826
1895
  fetchRequest,
1827
1896
  match,
1828
1897
  matches,
1898
+ manifest,
1899
+ detectErrorBoundary,
1829
1900
  router.basename
1830
1901
  );
1831
1902
 
@@ -2021,7 +2092,15 @@ export function createRouter(init: RouterInit): Router {
2021
2092
  // accordingly
2022
2093
  let results = await Promise.all([
2023
2094
  ...matchesToLoad.map((match) =>
2024
- callLoaderOrAction("loader", request, match, matches, router.basename)
2095
+ callLoaderOrAction(
2096
+ "loader",
2097
+ request,
2098
+ match,
2099
+ matches,
2100
+ manifest,
2101
+ detectErrorBoundary,
2102
+ router.basename
2103
+ )
2025
2104
  ),
2026
2105
  ...fetchersToLoad.map((f) => {
2027
2106
  if (f.matches && f.match) {
@@ -2030,6 +2109,8 @@ export function createRouter(init: RouterInit): Router {
2030
2109
  createClientSideRequest(init.history, f.path, request.signal),
2031
2110
  f.match,
2032
2111
  f.matches,
2112
+ manifest,
2113
+ detectErrorBoundary,
2033
2114
  router.basename
2034
2115
  );
2035
2116
  } else {
@@ -2346,18 +2427,29 @@ export function createRouter(init: RouterInit): Router {
2346
2427
 
2347
2428
  export const UNSAFE_DEFERRED_SYMBOL = Symbol("deferred");
2348
2429
 
2430
+ export interface CreateStaticHandlerOptions {
2431
+ basename?: string;
2432
+ detectErrorBoundary?: DetectErrorBoundaryFunction;
2433
+ }
2434
+
2349
2435
  export function createStaticHandler(
2350
2436
  routes: AgnosticRouteObject[],
2351
- opts?: {
2352
- basename?: string;
2353
- }
2437
+ opts?: CreateStaticHandlerOptions
2354
2438
  ): StaticHandler {
2355
2439
  invariant(
2356
2440
  routes.length > 0,
2357
2441
  "You must provide a non-empty routes array to createStaticHandler"
2358
2442
  );
2359
2443
 
2360
- let dataRoutes = convertRoutesToDataRoutes(routes);
2444
+ let manifest: RouteManifest = {};
2445
+ let detectErrorBoundary =
2446
+ opts?.detectErrorBoundary || defaultDetectErrorBoundary;
2447
+ let dataRoutes = convertRoutesToDataRoutes(
2448
+ routes,
2449
+ detectErrorBoundary,
2450
+ undefined,
2451
+ manifest
2452
+ );
2361
2453
  let basename = (opts ? opts.basename : null) || "/";
2362
2454
 
2363
2455
  /**
@@ -2592,7 +2684,7 @@ export function createStaticHandler(
2592
2684
  ): Promise<Omit<StaticHandlerContext, "location" | "basename"> | Response> {
2593
2685
  let result: DataResult;
2594
2686
 
2595
- if (!actionMatch.route.action) {
2687
+ if (!actionMatch.route.action && !actionMatch.route.lazy) {
2596
2688
  let error = getInternalRouterError(405, {
2597
2689
  method: request.method,
2598
2690
  pathname: new URL(request.url).pathname,
@@ -2611,6 +2703,8 @@ export function createStaticHandler(
2611
2703
  request,
2612
2704
  actionMatch,
2613
2705
  matches,
2706
+ manifest,
2707
+ detectErrorBoundary,
2614
2708
  basename,
2615
2709
  true,
2616
2710
  isRouteRequest,
@@ -2732,7 +2826,11 @@ export function createStaticHandler(
2732
2826
  let isRouteRequest = routeMatch != null;
2733
2827
 
2734
2828
  // Short circuit if we have no loaders to run (queryRoute())
2735
- if (isRouteRequest && !routeMatch?.route.loader) {
2829
+ if (
2830
+ isRouteRequest &&
2831
+ !routeMatch?.route.loader &&
2832
+ !routeMatch?.route.lazy
2833
+ ) {
2736
2834
  throw getInternalRouterError(400, {
2737
2835
  method: request.method,
2738
2836
  pathname: new URL(request.url).pathname,
@@ -2746,7 +2844,9 @@ export function createStaticHandler(
2746
2844
  matches,
2747
2845
  Object.keys(pendingActionError || {})[0]
2748
2846
  );
2749
- let matchesToLoad = requestMatches.filter((m) => m.route.loader);
2847
+ let matchesToLoad = requestMatches.filter(
2848
+ (m) => m.route.loader || m.route.lazy
2849
+ );
2750
2850
 
2751
2851
  // Short circuit if we have no loaders to run (query())
2752
2852
  if (matchesToLoad.length === 0) {
@@ -2771,6 +2871,8 @@ export function createStaticHandler(
2771
2871
  request,
2772
2872
  match,
2773
2873
  matches,
2874
+ manifest,
2875
+ detectErrorBoundary,
2774
2876
  basename,
2775
2877
  true,
2776
2878
  isRouteRequest,
@@ -2960,6 +3062,10 @@ function getMatchesToLoad(
2960
3062
  let boundaryMatches = getLoaderMatchesUntilBoundary(matches, boundaryId);
2961
3063
 
2962
3064
  let navigationMatches = boundaryMatches.filter((match, index) => {
3065
+ if (match.route.lazy) {
3066
+ // We haven't loaded this route yet so we don't know if it's got a loader!
3067
+ return true;
3068
+ }
2963
3069
  if (match.route.loader == null) {
2964
3070
  return false;
2965
3071
  }
@@ -3096,11 +3202,90 @@ function shouldRevalidateLoader(
3096
3202
  return arg.defaultShouldRevalidate;
3097
3203
  }
3098
3204
 
3205
+ /**
3206
+ * Execute route.lazy() methods to lazily load route modules (loader, action,
3207
+ * shouldRevalidate) and update the routeManifest in place which shares objects
3208
+ * with dataRoutes so those get updated as well.
3209
+ */
3210
+ async function loadLazyRouteModule(
3211
+ route: AgnosticDataRouteObject,
3212
+ detectErrorBoundary: DetectErrorBoundaryFunction,
3213
+ manifest: RouteManifest
3214
+ ) {
3215
+ if (!route.lazy) {
3216
+ return;
3217
+ }
3218
+
3219
+ let lazyRoute = await route.lazy();
3220
+
3221
+ // If the lazy route function was executed and removed by another parallel
3222
+ // call then we can return - first lazy() to finish wins because the return
3223
+ // value of lazy is expected to be static
3224
+ if (!route.lazy) {
3225
+ return;
3226
+ }
3227
+
3228
+ let routeToUpdate = manifest[route.id];
3229
+ invariant(routeToUpdate, "No route found in manifest");
3230
+
3231
+ // Update the route in place. This should be safe because there's no way
3232
+ // we could yet be sitting on this route as we can't get there without
3233
+ // resolving lazy() first.
3234
+ //
3235
+ // This is different than the HMR "update" use-case where we may actively be
3236
+ // on the route being updated. The main concern boils down to "does this
3237
+ // mutation affect any ongoing navigations or any current state.matches
3238
+ // values?". If not, it should be safe to update in place.
3239
+ let routeUpdates: Record<string, any> = {};
3240
+ for (let lazyRouteProperty in lazyRoute) {
3241
+ let staticRouteValue =
3242
+ routeToUpdate[lazyRouteProperty as keyof typeof routeToUpdate];
3243
+
3244
+ let isPropertyStaticallyDefined =
3245
+ staticRouteValue !== undefined &&
3246
+ // This property isn't static since it should always be updated based
3247
+ // on the route updates
3248
+ lazyRouteProperty !== "hasErrorBoundary";
3249
+
3250
+ warning(
3251
+ !isPropertyStaticallyDefined,
3252
+ `Route "${routeToUpdate.id}" has a static property "${lazyRouteProperty}" ` +
3253
+ `defined but its lazy function is also returning a value for this property. ` +
3254
+ `The lazy route property "${lazyRouteProperty}" will be ignored.`
3255
+ );
3256
+
3257
+ if (
3258
+ !isPropertyStaticallyDefined &&
3259
+ !immutableRouteKeys.has(lazyRouteProperty as ImmutableRouteKey)
3260
+ ) {
3261
+ routeUpdates[lazyRouteProperty] =
3262
+ lazyRoute[lazyRouteProperty as keyof typeof lazyRoute];
3263
+ }
3264
+ }
3265
+
3266
+ // Mutate the route with the provided updates. Do this first so we pass
3267
+ // the updated version to detectErrorBoundary
3268
+ Object.assign(routeToUpdate, routeUpdates);
3269
+
3270
+ // Mutate the `hasErrorBoundary` property on the route based on the route
3271
+ // updates and remove the `lazy` function so we don't resolve the lazy
3272
+ // route again.
3273
+ Object.assign(routeToUpdate, {
3274
+ // To keep things framework agnostic, we use the provided
3275
+ // `detectErrorBoundary` function to set the `hasErrorBoundary` route
3276
+ // property since the logic will differ between frameworks.
3277
+ hasErrorBoundary: detectErrorBoundary({ ...routeToUpdate }),
3278
+ lazy: undefined,
3279
+ });
3280
+ }
3281
+
3099
3282
  async function callLoaderOrAction(
3100
3283
  type: "loader" | "action",
3101
3284
  request: Request,
3102
3285
  match: AgnosticDataRouteMatch,
3103
3286
  matches: AgnosticDataRouteMatch[],
3287
+ manifest: RouteManifest,
3288
+ detectErrorBoundary: DetectErrorBoundaryFunction,
3104
3289
  basename = "/",
3105
3290
  isStaticRequest: boolean = false,
3106
3291
  isRouteRequest: boolean = false,
@@ -3108,24 +3293,61 @@ async function callLoaderOrAction(
3108
3293
  ): Promise<DataResult> {
3109
3294
  let resultType;
3110
3295
  let result;
3111
-
3112
- // Setup a promise we can race against so that abort signals short circuit
3113
- let reject: () => void;
3114
- let abortPromise = new Promise((_, r) => (reject = r));
3115
- let onReject = () => reject();
3116
- request.signal.addEventListener("abort", onReject);
3296
+ let onReject: (() => void) | undefined;
3297
+
3298
+ let runHandler = (handler: ActionFunction | LoaderFunction) => {
3299
+ // Setup a promise we can race against so that abort signals short circuit
3300
+ let reject: () => void;
3301
+ let abortPromise = new Promise((_, r) => (reject = r));
3302
+ onReject = () => reject();
3303
+ request.signal.addEventListener("abort", onReject);
3304
+ return Promise.race([
3305
+ handler({ request, params: match.params, context: requestContext }),
3306
+ abortPromise,
3307
+ ]);
3308
+ };
3117
3309
 
3118
3310
  try {
3119
3311
  let handler = match.route[type];
3120
- invariant<Function>(
3121
- handler,
3122
- `Could not find the ${type} to run on the "${match.route.id}" route`
3123
- );
3124
3312
 
3125
- result = await Promise.race([
3126
- handler({ request, params: match.params, context: requestContext }),
3127
- abortPromise,
3128
- ]);
3313
+ if (match.route.lazy) {
3314
+ if (handler) {
3315
+ // Run statically defined handler in parallel with lazy()
3316
+ let values = await Promise.all([
3317
+ runHandler(handler),
3318
+ loadLazyRouteModule(match.route, detectErrorBoundary, manifest),
3319
+ ]);
3320
+ result = values[0];
3321
+ } else {
3322
+ // Load lazy route module, then run any returned handler
3323
+ await loadLazyRouteModule(match.route, detectErrorBoundary, manifest);
3324
+
3325
+ handler = match.route[type];
3326
+ if (handler) {
3327
+ // Handler still run even if we got interrupted to maintain consistency
3328
+ // with un-abortable behavior of handler execution on non-lazy or
3329
+ // previously-lazy-loaded routes
3330
+ result = await runHandler(handler);
3331
+ } else if (type === "action") {
3332
+ throw getInternalRouterError(405, {
3333
+ method: request.method,
3334
+ pathname: new URL(request.url).pathname,
3335
+ routeId: match.route.id,
3336
+ });
3337
+ } else {
3338
+ // lazy() route has no loader to run. Short circuit here so we don't
3339
+ // hit the invariant below that errors on returning undefined.
3340
+ return { type: ResultType.data, data: undefined };
3341
+ }
3342
+ }
3343
+ } else {
3344
+ invariant<Function>(
3345
+ handler,
3346
+ `Could not find the ${type} to run on the "${match.route.id}" route`
3347
+ );
3348
+
3349
+ result = await runHandler(handler);
3350
+ }
3129
3351
 
3130
3352
  invariant(
3131
3353
  result !== undefined,
@@ -3137,7 +3359,9 @@ async function callLoaderOrAction(
3137
3359
  resultType = ResultType.error;
3138
3360
  result = e;
3139
3361
  } finally {
3140
- request.signal.removeEventListener("abort", onReject);
3362
+ if (onReject) {
3363
+ request.signal.removeEventListener("abort", onReject);
3364
+ }
3141
3365
  }
3142
3366
 
3143
3367
  if (isResponse(result)) {