Skip to content

Commit cd42cc5

Browse files
authored
fix: generatePath incorrectly applying parameters #9051 (#10078)
1 parent 0823407 commit cd42cc5

File tree

5 files changed

+67
-41
lines changed

5 files changed

+67
-41
lines changed

.changeset/old-camels-accept.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@remix-run/router": patch
3+
"react-router": patch
4+
---
5+
6+
Fix `generatePath` incorrectly applying parameters in some cases

contributors.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@
135135
- ms10596
136136
- ned-park
137137
- noisypigeon
138+
- Obi-Dann
138139
- omar-moquete
139140
- p13i
140141
- parched

packages/react-router/__tests__/generatePath-test.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,20 @@ describe("generatePath", () => {
2727
);
2828
expect(generatePath("/*", {})).toBe("/");
2929
});
30+
it("handles * in parameter values", () => {
31+
expect(generatePath("/courses/:name", { name: "foo*" })).toBe(
32+
"/courses/foo*"
33+
);
34+
expect(generatePath("/courses/:name", { name: "*foo" })).toBe(
35+
"/courses/*foo"
36+
);
37+
expect(generatePath("/courses/:name", { name: "*f*oo*" })).toBe(
38+
"/courses/*f*oo*"
39+
);
40+
expect(generatePath("/courses/:name", { name: "foo*", "*": "splat_should_not_be_added" })).toBe(
41+
"/courses/foo*"
42+
);
43+
});
3044
});
3145

3246
describe("with extraneous params", () => {

packages/react-router/__tests__/matchPath-test.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,14 @@ describe("matchPath *", () => {
300300
pathnameBase: "/",
301301
});
302302
});
303+
304+
it("resolves params containing '*' correctly", () => {
305+
expect(matchPath("/users/:name/*", "/users/foo*/splat")).toMatchObject({
306+
params: { name: "foo*", "*": "splat" },
307+
pathname: "/users/foo*/splat",
308+
pathnameBase: "/users/foo*",
309+
});
310+
});
303311
});
304312

305313
describe("matchPath warnings", () => {

packages/router/utils.ts

Lines changed: 38 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ type _PathParam<Path extends string> =
217217
*/
218218
type PathParam<Path extends string> =
219219
// check if path is just a wildcard
220-
Path extends "*"
220+
Path extends "*" | "/*"
221221
? "*"
222222
: // look for wildcard at the end of the path
223223
Path extends `${infer Rest}/*`
@@ -621,7 +621,7 @@ export function generatePath<Path extends string>(
621621
[key in PathParam<Path>]: string | null;
622622
} = {} as any
623623
): string {
624-
let path = originalPath;
624+
let path: string = originalPath;
625625
if (path.endsWith("*") && path !== "*" && !path.endsWith("/*")) {
626626
warning(
627627
false,
@@ -633,49 +633,46 @@ export function generatePath<Path extends string>(
633633
path = path.replace(/\*$/, "/*") as Path;
634634
}
635635

636-
return (
637-
path
638-
.replace(
639-
/^:(\w+)(\??)/g,
640-
(_, key: PathParam<Path>, optional: string | undefined) => {
641-
let param = params[key];
642-
if (optional === "?") {
643-
return param == null ? "" : param;
644-
}
645-
if (param == null) {
646-
invariant(false, `Missing ":${key}" param`);
647-
}
648-
return param;
649-
}
650-
)
651-
.replace(
652-
/\/:(\w+)(\??)/g,
653-
(_, key: PathParam<Path>, optional: string | undefined) => {
654-
let param = params[key];
655-
if (optional === "?") {
656-
return param == null ? "" : `/${param}`;
657-
}
658-
if (param == null) {
659-
invariant(false, `Missing ":${key}" param`);
660-
}
661-
return `/${param}`;
662-
}
663-
)
664-
// Remove any optional markers from optional static segments
665-
.replace(/\?/g, "")
666-
.replace(/(\/?)\*/, (_, prefix, __, str) => {
636+
// ensure `/` is added at the beginning if the path is absolute
637+
const prefix = path.startsWith("/") ? "/" : "";
638+
639+
const segments = path
640+
.split(/\/+/)
641+
.map((segment, index, array) => {
642+
const isLastSegment = index === array.length - 1;
643+
644+
// only apply the splat if it's the last segment
645+
if (isLastSegment && segment === "*") {
667646
const star = "*" as PathParam<Path>;
647+
const starParam = params[star];
668648

669-
if (params[star] == null) {
670-
// If no splat was provided, trim the trailing slash _unless_ it's
671-
// the entire path
672-
return str === "/*" ? "/" : "";
649+
// Apply the splat
650+
return starParam;
651+
}
652+
653+
const keyMatch = segment.match(/^:(\w+)(\??)$/);
654+
if (keyMatch) {
655+
const [, key, optional] = keyMatch;
656+
let param = params[key as PathParam<Path>];
657+
658+
if (optional === "?") {
659+
return param == null ? "" : param;
673660
}
674661

675-
// Apply the splat
676-
return `${prefix}${params[star]}`;
677-
})
678-
);
662+
if (param == null) {
663+
invariant(false, `Missing ":${key}" param`);
664+
}
665+
666+
return param;
667+
}
668+
669+
// Remove any optional markers from optional static segments
670+
return segment.replace(/\?$/g, "");
671+
})
672+
// Remove empty segments
673+
.filter((segment) => !!segment);
674+
675+
return prefix + segments.join("/");
679676
}
680677

681678
/**

0 commit comments

Comments
 (0)