@remix-run/router 1.0.5-pre.2 → 1.1.0-pre.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/utils.d.ts CHANGED
@@ -48,8 +48,8 @@ export interface ErrorResult {
48
48
  * Result from a loader or action - potentially successful or unsuccessful
49
49
  */
50
50
  export declare type DataResult = SuccessResult | DeferredResult | RedirectResult | ErrorResult;
51
- export declare type SubmissionFormMethod = "post" | "put" | "patch" | "delete";
52
- export declare type FormMethod = "get" | SubmissionFormMethod;
51
+ export declare type MutationFormMethod = "post" | "put" | "patch" | "delete";
52
+ export declare type FormMethod = "get" | MutationFormMethod;
53
53
  export declare type FormEncType = "application/x-www-form-urlencoded" | "multipart/form-data";
54
54
  /**
55
55
  * @private
@@ -57,7 +57,7 @@ export declare type FormEncType = "application/x-www-form-urlencoded" | "multipa
57
57
  * external consumption
58
58
  */
59
59
  export interface Submission {
60
- formMethod: SubmissionFormMethod;
60
+ formMethod: FormMethod;
61
61
  formAction: string;
62
62
  formEncType: FormEncType;
63
63
  formData: FormData;
@@ -158,7 +158,7 @@ export declare type AgnosticDataNonIndexRouteObject = AgnosticNonIndexRouteObjec
158
158
  * A data route object, which is just a RouteObject with a required unique ID
159
159
  */
160
160
  export declare type AgnosticDataRouteObject = AgnosticDataIndexRouteObject | AgnosticDataNonIndexRouteObject;
161
- declare type _PathParam<Path extends string> = Path extends `${infer L}/${infer R}` ? _PathParam<L> | _PathParam<R> : Path extends `${string}:${infer Param}` ? Param : never;
161
+ declare type _PathParam<Path extends string> = Path extends `${infer L}/${infer R}` ? _PathParam<L> | _PathParam<R> : Path extends `:${infer Param}` ? Param : never;
162
162
  /**
163
163
  * Examples:
164
164
  * "/a/b/*" -> "*"
@@ -213,7 +213,7 @@ export declare function matchRoutes<RouteObjectType extends AgnosticRouteObject
213
213
  *
214
214
  * @see https://reactrouter.com/utils/generate-path
215
215
  */
216
- export declare function generatePath<Path extends string>(path: Path, params?: {
216
+ export declare function generatePath<Path extends string>(originalPath: Path, params?: {
217
217
  [key in PathParam<Path>]: string;
218
218
  }): string;
219
219
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remix-run/router",
3
- "version": "1.0.5-pre.2",
3
+ "version": "1.1.0-pre.0",
4
4
  "description": "Nested/Data-driven/Framework-agnostic Routing",
5
5
  "keywords": [
6
6
  "remix",
package/router.ts CHANGED
@@ -21,7 +21,7 @@ import type {
21
21
  Submission,
22
22
  SuccessResult,
23
23
  AgnosticRouteMatch,
24
- SubmissionFormMethod,
24
+ MutationFormMethod,
25
25
  } from "./utils";
26
26
  import {
27
27
  DeferredData,
@@ -521,15 +521,20 @@ interface QueryRouteResponse {
521
521
  response: Response;
522
522
  }
523
523
 
524
- const validActionMethodsArr: SubmissionFormMethod[] = [
524
+ const validMutationMethodsArr: MutationFormMethod[] = [
525
525
  "post",
526
526
  "put",
527
527
  "patch",
528
528
  "delete",
529
529
  ];
530
- const validActionMethods = new Set<SubmissionFormMethod>(validActionMethodsArr);
530
+ const validMutationMethods = new Set<MutationFormMethod>(
531
+ validMutationMethodsArr
532
+ );
531
533
 
532
- const validRequestMethodsArr: FormMethod[] = ["get", ...validActionMethodsArr];
534
+ const validRequestMethodsArr: FormMethod[] = [
535
+ "get",
536
+ ...validMutationMethodsArr,
537
+ ];
533
538
  const validRequestMethods = new Set<FormMethod>(validRequestMethodsArr);
534
539
 
535
540
  const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
@@ -811,7 +816,8 @@ export function createRouter(init: RouterInit): Router {
811
816
  };
812
817
 
813
818
  let historyAction =
814
- (opts && opts.replace) === true || submission != null
819
+ (opts && opts.replace) === true ||
820
+ (submission != null && isMutationMethod(submission.formMethod))
815
821
  ? HistoryAction.Replace
816
822
  : HistoryAction.Push;
817
823
  let preventScrollReset =
@@ -935,7 +941,11 @@ export function createRouter(init: RouterInit): Router {
935
941
  pendingError = {
936
942
  [findNearestBoundary(matches).route.id]: opts.pendingError,
937
943
  };
938
- } else if (opts && opts.submission) {
944
+ } else if (
945
+ opts &&
946
+ opts.submission &&
947
+ isMutationMethod(opts.submission.formMethod)
948
+ ) {
939
949
  // Call action if we received an action submission
940
950
  let actionOutput = await handleAction(
941
951
  request,
@@ -1095,6 +1105,7 @@ export function createRouter(init: RouterInit): Router {
1095
1105
  formAction: undefined,
1096
1106
  formEncType: undefined,
1097
1107
  formData: undefined,
1108
+ ...submission,
1098
1109
  };
1099
1110
  loadingNavigation = navigation;
1100
1111
  }
@@ -1259,7 +1270,7 @@ export function createRouter(init: RouterInit): Router {
1259
1270
  let { path, submission } = normalizeNavigateOptions(href, opts, true);
1260
1271
  let match = getTargetMatch(matches, path);
1261
1272
 
1262
- if (submission) {
1273
+ if (submission && isMutationMethod(submission.formMethod)) {
1263
1274
  handleFetcherAction(key, routeId, path, match, matches, submission);
1264
1275
  return;
1265
1276
  }
@@ -1267,7 +1278,7 @@ export function createRouter(init: RouterInit): Router {
1267
1278
  // Store off the match so we can call it's shouldRevalidate on subsequent
1268
1279
  // revalidations
1269
1280
  fetchLoadMatches.set(key, [path, match, matches]);
1270
- handleFetcherLoader(key, routeId, path, match, matches);
1281
+ handleFetcherLoader(key, routeId, path, match, matches, submission);
1271
1282
  }
1272
1283
 
1273
1284
  // Call the action for the matched fetcher.submit(), and then handle redirects,
@@ -1494,7 +1505,8 @@ export function createRouter(init: RouterInit): Router {
1494
1505
  routeId: string,
1495
1506
  path: string,
1496
1507
  match: AgnosticDataRouteMatch,
1497
- matches: AgnosticDataRouteMatch[]
1508
+ matches: AgnosticDataRouteMatch[],
1509
+ submission?: Submission
1498
1510
  ) {
1499
1511
  let existingFetcher = state.fetchers.get(key);
1500
1512
  // Put this fetcher into it's loading state
@@ -1504,6 +1516,7 @@ export function createRouter(init: RouterInit): Router {
1504
1516
  formAction: undefined,
1505
1517
  formEncType: undefined,
1506
1518
  formData: undefined,
1519
+ ...submission,
1507
1520
  data: existingFetcher && existingFetcher.data,
1508
1521
  };
1509
1522
  state.fetchers.set(key, loadingFetcher);
@@ -1635,12 +1648,12 @@ export function createRouter(init: RouterInit): Router {
1635
1648
  let { formMethod, formAction, formEncType, formData } = state.navigation;
1636
1649
 
1637
1650
  // If this was a 307/308 submission we want to preserve the HTTP method and
1638
- // re-submit the POST/PUT/PATCH/DELETE as a submission navigation to the
1651
+ // re-submit the GET/POST/PUT/PATCH/DELETE as a submission navigation to the
1639
1652
  // redirected location
1640
1653
  if (
1641
1654
  redirectPreserveMethodStatusCodes.has(redirect.status) &&
1642
1655
  formMethod &&
1643
- isSubmissionMethod(formMethod) &&
1656
+ isMutationMethod(formMethod) &&
1644
1657
  formEncType &&
1645
1658
  formData
1646
1659
  ) {
@@ -2096,7 +2109,7 @@ export function unstable_createStaticHandler(
2096
2109
  );
2097
2110
 
2098
2111
  try {
2099
- if (isSubmissionMethod(request.method.toLowerCase())) {
2112
+ if (isMutationMethod(request.method.toLowerCase())) {
2100
2113
  let result = await submit(
2101
2114
  request,
2102
2115
  matches,
@@ -2244,7 +2257,11 @@ export function unstable_createStaticHandler(
2244
2257
  }
2245
2258
 
2246
2259
  // Create a GET request for the loaders
2247
- let loaderRequest = new Request(request.url, { signal: request.signal });
2260
+ let loaderRequest = new Request(request.url, {
2261
+ headers: request.headers,
2262
+ redirect: request.redirect,
2263
+ signal: request.signal,
2264
+ });
2248
2265
  let context = await loadRouteData(loaderRequest, matches, requestContext);
2249
2266
 
2250
2267
  return {
@@ -2409,17 +2426,19 @@ function normalizeNavigateOptions(
2409
2426
  }
2410
2427
 
2411
2428
  // Create a Submission on non-GET navigations
2412
- if (opts.formMethod && isSubmissionMethod(opts.formMethod)) {
2413
- return {
2414
- path,
2415
- submission: {
2416
- formMethod: opts.formMethod,
2417
- formAction: stripHashFromPath(path),
2418
- formEncType:
2419
- (opts && opts.formEncType) || "application/x-www-form-urlencoded",
2420
- formData: opts.formData,
2421
- },
2429
+ let submission: Submission | undefined;
2430
+ if (opts.formData) {
2431
+ submission = {
2432
+ formMethod: opts.formMethod || "get",
2433
+ formAction: stripHashFromPath(path),
2434
+ formEncType:
2435
+ (opts && opts.formEncType) || "application/x-www-form-urlencoded",
2436
+ formData: opts.formData,
2422
2437
  };
2438
+
2439
+ if (isMutationMethod(submission.formMethod)) {
2440
+ return { path, submission };
2441
+ }
2423
2442
  }
2424
2443
 
2425
2444
  // Flatten submission onto URLSearchParams for GET submissions
@@ -2444,7 +2463,7 @@ function normalizeNavigateOptions(
2444
2463
  };
2445
2464
  }
2446
2465
 
2447
- return { path: createPath(parsedPath) };
2466
+ return { path: createPath(parsedPath), submission };
2448
2467
  }
2449
2468
 
2450
2469
  // Filter out all routes below any caught error as they aren't going to
@@ -2767,7 +2786,7 @@ function createClientSideRequest(
2767
2786
  let url = createClientSideURL(stripHashFromPath(location)).toString();
2768
2787
  let init: RequestInit = { signal };
2769
2788
 
2770
- if (submission) {
2789
+ if (submission && isMutationMethod(submission.formMethod)) {
2771
2790
  let { formMethod, formEncType, formData } = submission;
2772
2791
  init.method = formMethod.toUpperCase();
2773
2792
  init.body =
@@ -2833,9 +2852,14 @@ function processRouteLoaderData(
2833
2852
  error = Object.values(pendingError)[0];
2834
2853
  pendingError = undefined;
2835
2854
  }
2836
- errors = Object.assign(errors || {}, {
2837
- [boundaryMatch.route.id]: error,
2838
- });
2855
+
2856
+ errors = errors || {};
2857
+
2858
+ // Prefer higher error values if lower errors bubble to the same boundary
2859
+ if (errors[boundaryMatch.route.id] == null) {
2860
+ errors[boundaryMatch.route.id] = error;
2861
+ }
2862
+
2839
2863
  // Once we find our first (highest) error, we set the status code and
2840
2864
  // prevent deeper status codes from overriding
2841
2865
  if (!foundError) {
@@ -3115,8 +3139,8 @@ function isValidMethod(method: string): method is FormMethod {
3115
3139
  return validRequestMethods.has(method as FormMethod);
3116
3140
  }
3117
3141
 
3118
- function isSubmissionMethod(method: string): method is SubmissionFormMethod {
3119
- return validActionMethods.has(method as SubmissionFormMethod);
3142
+ function isMutationMethod(method?: string): method is MutationFormMethod {
3143
+ return validMutationMethods.has(method as MutationFormMethod);
3120
3144
  }
3121
3145
 
3122
3146
  async function resolveDeferredResults(
package/utils.ts CHANGED
@@ -61,8 +61,8 @@ export type DataResult =
61
61
  | RedirectResult
62
62
  | ErrorResult;
63
63
 
64
- export type SubmissionFormMethod = "post" | "put" | "patch" | "delete";
65
- export type FormMethod = "get" | SubmissionFormMethod;
64
+ export type MutationFormMethod = "post" | "put" | "patch" | "delete";
65
+ export type FormMethod = "get" | MutationFormMethod;
66
66
 
67
67
  export type FormEncType =
68
68
  | "application/x-www-form-urlencoded"
@@ -74,7 +74,7 @@ export type FormEncType =
74
74
  * external consumption
75
75
  */
76
76
  export interface Submission {
77
- formMethod: SubmissionFormMethod;
77
+ formMethod: FormMethod;
78
78
  formAction: string;
79
79
  formEncType: FormEncType;
80
80
  formData: FormData;
@@ -197,7 +197,7 @@ type _PathParam<Path extends string> =
197
197
  Path extends `${infer L}/${infer R}`
198
198
  ? _PathParam<L> | _PathParam<R>
199
199
  : // find params after `:`
200
- Path extends `${string}:${infer Param}`
200
+ Path extends `:${infer Param}`
201
201
  ? Param
202
202
  : // otherwise, there aren't any params present
203
203
  never;
@@ -372,9 +372,14 @@ function flattenRoutes<
372
372
  parentsMeta: RouteMeta<RouteObjectType>[] = [],
373
373
  parentPath = ""
374
374
  ): RouteBranch<RouteObjectType>[] {
375
- routes.forEach((route, index) => {
375
+ let flattenRoute = (
376
+ route: RouteObjectType,
377
+ index: number,
378
+ relativePath?: string
379
+ ) => {
376
380
  let meta: RouteMeta<RouteObjectType> = {
377
- relativePath: route.path || "",
381
+ relativePath:
382
+ relativePath === undefined ? route.path || "" : relativePath,
378
383
  caseSensitive: route.caseSensitive === true,
379
384
  childrenIndex: index,
380
385
  route,
@@ -415,12 +420,75 @@ function flattenRoutes<
415
420
  return;
416
421
  }
417
422
 
418
- branches.push({ path, score: computeScore(path, route.index), routesMeta });
423
+ branches.push({
424
+ path,
425
+ score: computeScore(path, route.index),
426
+ routesMeta,
427
+ });
428
+ };
429
+ routes.forEach((route, index) => {
430
+ // coarse-grain check for optional params
431
+ if (route.path === "" || !route.path?.includes("?")) {
432
+ flattenRoute(route, index);
433
+ } else {
434
+ for (let exploded of explodeOptionalSegments(route.path)) {
435
+ flattenRoute(route, index, exploded);
436
+ }
437
+ }
419
438
  });
420
439
 
421
440
  return branches;
422
441
  }
423
442
 
443
+ /**
444
+ * Computes all combinations of optional path segments for a given path,
445
+ * excluding combinations that are ambiguous and of lower priority.
446
+ *
447
+ * For example, `/one/:two?/three/:four?/:five?` explodes to:
448
+ * - `/one/three`
449
+ * - `/one/:two/three`
450
+ * - `/one/three/:four`
451
+ * - `/one/three/:five`
452
+ * - `/one/:two/three/:four`
453
+ * - `/one/:two/three/:five`
454
+ * - `/one/three/:four/:five`
455
+ * - `/one/:two/three/:four/:five`
456
+ */
457
+ function explodeOptionalSegments(path: string): string[] {
458
+ let segments = path.split("/");
459
+ if (segments.length === 0) return [];
460
+
461
+ let [first, ...rest] = segments;
462
+
463
+ // Optional path segments are denoted by a trailing `?`
464
+ let isOptional = first.endsWith("?");
465
+ // Compute the corresponding required segment: `foo?` -> `foo`
466
+ let required = first.replace(/\?$/, "");
467
+
468
+ if (rest.length === 0) {
469
+ // Intepret empty string as omitting an optional segment
470
+ // `["one", "", "three"]` corresponds to omitting `:two` from `/one/:two?/three` -> `/one/three`
471
+ return isOptional ? ["", required] : [required];
472
+ }
473
+
474
+ let restExploded = explodeOptionalSegments(rest.join("/"));
475
+ return restExploded
476
+ .flatMap((subpath) => {
477
+ // /one + / + :two/three -> /one/:two/three
478
+ let requiredExploded =
479
+ subpath === "" ? required : required + "/" + subpath;
480
+ // For optional segments, return the exploded path _without_ current segment first (`subpath`)
481
+ // and exploded path _with_ current segment later (`subpath`)
482
+ // This ensures that exploded paths are emitted in priority order
483
+ // `/one/three/:four` will come before `/one/three/:five`
484
+ return isOptional ? [subpath, requiredExploded] : [requiredExploded];
485
+ })
486
+ .map((exploded) => {
487
+ // for absolute paths, ensure `/` instead of empty segment
488
+ return path.startsWith("/") && exploded === "" ? "/" : exploded;
489
+ });
490
+ }
491
+
424
492
  function rankRouteBranches(branches: RouteBranch[]): void {
425
493
  branches.sort((a, b) =>
426
494
  a.score !== b.score
@@ -534,16 +602,32 @@ function matchRouteBranch<
534
602
  * @see https://reactrouter.com/utils/generate-path
535
603
  */
536
604
  export function generatePath<Path extends string>(
537
- path: Path,
605
+ originalPath: Path,
538
606
  params: {
539
607
  [key in PathParam<Path>]: string;
540
608
  } = {} as any
541
609
  ): string {
610
+ let path = originalPath;
611
+ if (path.endsWith("*") && path !== "*" && !path.endsWith("/*")) {
612
+ warning(
613
+ false,
614
+ `Route path "${path}" will be treated as if it were ` +
615
+ `"${path.replace(/\*$/, "/*")}" because the \`*\` character must ` +
616
+ `always follow a \`/\` in the pattern. To get rid of this warning, ` +
617
+ `please change the route path to "${path.replace(/\*$/, "/*")}".`
618
+ );
619
+ path = path.replace(/\*$/, "/*") as Path;
620
+ }
621
+
542
622
  return path
543
- .replace(/:(\w+)/g, (_, key: PathParam<Path>) => {
623
+ .replace(/^:(\w+)/g, (_, key: PathParam<Path>) => {
544
624
  invariant(params[key] != null, `Missing ":${key}" param`);
545
625
  return params[key]!;
546
626
  })
627
+ .replace(/\/:(\w+)/g, (_, key: PathParam<Path>) => {
628
+ invariant(params[key] != null, `Missing ":${key}" param`);
629
+ return `/${params[key]!}`;
630
+ })
547
631
  .replace(/(\/?)\*/, (_, prefix, __, str) => {
548
632
  const star = "*" as PathParam<Path>;
549
633
 
@@ -682,9 +766,9 @@ function compilePath(
682
766
  .replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
683
767
  .replace(/^\/*/, "/") // Make sure it has a leading /
684
768
  .replace(/[\\.*+^$?{}|()[\]]/g, "\\$&") // Escape special regex chars
685
- .replace(/:(\w+)/g, (_: string, paramName: string) => {
769
+ .replace(/\/:(\w+)/g, (_: string, paramName: string) => {
686
770
  paramNames.push(paramName);
687
- return "([^\\/]+)";
771
+ return "/([^\\/]+)";
688
772
  });
689
773
 
690
774
  if (path.endsWith("*")) {