Skip to content

Commit de3c96a

Browse files
authored
Fix up generatePath when optional params are present (#9764)
1 parent b154367 commit de3c96a

File tree

3 files changed

+108
-23
lines changed

3 files changed

+108
-23
lines changed

Diff for: .changeset/happy-ladybugs-occur.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"react-router": patch
3+
"@remix-run/router": patch
4+
---
5+
6+
Fix `generatePath` when optional params are present

Diff for: packages/react-router/__tests__/generatePath-test.tsx

+55
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ describe("generatePath", () => {
2525
expect(generatePath("*", { "*": "routing/grades" })).toBe(
2626
"routing/grades"
2727
);
28+
expect(generatePath("/*", {})).toBe("/");
2829
});
2930
});
3031

@@ -49,6 +50,60 @@ describe("generatePath", () => {
4950
});
5051
});
5152

53+
describe("with optional params", () => {
54+
it("adds optional dynamic params where appropriate", () => {
55+
let path = "/:one?/:two?/:three?";
56+
expect(generatePath(path, { one: "uno" })).toBe("/uno");
57+
expect(generatePath(path, { one: "uno", two: "dos" })).toBe("/uno/dos");
58+
expect(
59+
generatePath(path, {
60+
one: "uno",
61+
two: "dos",
62+
three: "tres",
63+
})
64+
).toBe("/uno/dos/tres");
65+
expect(generatePath(path, { one: "uno", three: "tres" })).toBe(
66+
"/uno/tres"
67+
);
68+
expect(generatePath(path, { two: "dos" })).toBe("/dos");
69+
expect(generatePath(path, { two: "dos", three: "tres" })).toBe(
70+
"/dos/tres"
71+
);
72+
});
73+
74+
it("strips optional aspects of static segments", () => {
75+
expect(generatePath("/one?/two?/:three?", {})).toBe("/one/two");
76+
expect(generatePath("/one?/two?/:three?", { three: "tres" })).toBe(
77+
"/one/two/tres"
78+
);
79+
});
80+
81+
it("handles intermixed segments", () => {
82+
let path = "/one?/:two?/three/:four/*";
83+
expect(generatePath(path, { four: "cuatro" })).toBe("/one/three/cuatro");
84+
expect(
85+
generatePath(path, {
86+
two: "dos",
87+
four: "cuatro",
88+
})
89+
).toBe("/one/dos/three/cuatro");
90+
expect(
91+
generatePath(path, {
92+
two: "dos",
93+
four: "cuatro",
94+
"*": "splat",
95+
})
96+
).toBe("/one/dos/three/cuatro/splat");
97+
expect(
98+
generatePath(path, {
99+
two: "dos",
100+
four: "cuatro",
101+
"*": "splat/and/then/some",
102+
})
103+
).toBe("/one/dos/three/cuatro/splat/and/then/some");
104+
});
105+
});
106+
52107
it("throws only on on missing named parameters, but not missing splat params", () => {
53108
expect(() => generatePath(":foo")).toThrow();
54109
expect(() => generatePath("/:foo")).toThrow();

Diff for: packages/router/utils.ts

+47-23
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,9 @@ type _PathParam<Path extends string> =
200200
? _PathParam<L> | _PathParam<R>
201201
: // find params after `:`
202202
Path extends `:${infer Param}`
203-
? Param
203+
? Param extends `${infer Optional}?`
204+
? Optional
205+
: Param
204206
: // otherwise, there aren't any params present
205207
never;
206208

@@ -616,7 +618,7 @@ function matchRouteBranch<
616618
export function generatePath<Path extends string>(
617619
originalPath: Path,
618620
params: {
619-
[key in PathParam<Path>]: string;
621+
[key in PathParam<Path>]: string | null;
620622
} = {} as any
621623
): string {
622624
let path = originalPath;
@@ -631,27 +633,49 @@ export function generatePath<Path extends string>(
631633
path = path.replace(/\*$/, "/*") as Path;
632634
}
633635

634-
return path
635-
.replace(/^:(\w+)/g, (_, key: PathParam<Path>) => {
636-
invariant(params[key] != null, `Missing ":${key}" param`);
637-
return params[key]!;
638-
})
639-
.replace(/\/:(\w+)/g, (_, key: PathParam<Path>) => {
640-
invariant(params[key] != null, `Missing ":${key}" param`);
641-
return `/${params[key]!}`;
642-
})
643-
.replace(/(\/?)\*/, (_, prefix, __, str) => {
644-
const star = "*" as PathParam<Path>;
645-
646-
if (params[star] == null) {
647-
// If no splat was provided, trim the trailing slash _unless_ it's
648-
// the entire path
649-
return str === "/*" ? "/" : "";
650-
}
651-
652-
// Apply the splat
653-
return `${prefix}${params[star]}`;
654-
});
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) => {
667+
const star = "*" as PathParam<Path>;
668+
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 === "/*" ? "/" : "";
673+
}
674+
675+
// Apply the splat
676+
return `${prefix}${params[star]}`;
677+
})
678+
);
655679
}
656680

657681
/**

0 commit comments

Comments
 (0)