Skip to content

Commit

Permalink
Support optional path segments in matchPath (#10768)
Browse files Browse the repository at this point in the history
Co-authored-by: Matt Brophy <matt@brophy.org>
  • Loading branch information
imjordanxd and brophdawg11 committed Oct 20, 2023
1 parent 908a40a commit 677d6c8
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 16 deletions.
5 changes: 5 additions & 0 deletions .changeset/support-optional-path-segments-in-match-path.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/router": minor
---

Add support for optional path segments in `matchPath`
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
- ianflynnwork
- IbraRouisDev
- igniscyan
- imjordanxd
- infoxicator
- IsaiStormBlesed
- Isammoc
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
},
"filesize": {
"packages/router/dist/router.umd.min.js": {
"none": "48.3 kB"
"none": "48.7 kB"
},
"packages/react-router/dist/react-router.production.min.js": {
"none": "13.9 kB"
Expand Down
47 changes: 47 additions & 0 deletions packages/react-router/__tests__/matchPath-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,53 @@ describe("matchPath", () => {
});
});

describe("matchPath optional segments", () => {
it("should match when optional segment is provided", () => {
const match = matchPath("/:lang?/user/:id", "/en/user/123");
expect(match).toMatchObject({ params: { lang: "en", id: "123" } });
});

it("should match when optional segment is *not* provided", () => {
const match = matchPath("/:lang?/user/:id", "/user/123");
expect(match).toMatchObject({ params: { lang: undefined, id: "123" } });
});

it("should match when middle optional segment is provided", () => {
const match = matchPath("/user/:lang?/:id", "/user/en/123");
expect(match).toMatchObject({ params: { lang: "en", id: "123" } });
});

it("should match when middle optional segment is *not* provided", () => {
const match = matchPath("/user/:lang?/:id", "/user/123");
expect(match).toMatchObject({ params: { lang: undefined, id: "123" } });
});

it("should match when end optional segment is provided", () => {
const match = matchPath("/user/:id/:lang?", "/user/123/en");
expect(match).toMatchObject({ params: { lang: "en", id: "123" } });
});

it("should match when end optional segment is *not* provided", () => {
const match = matchPath("/user/:id/:lang?", "/user/123");
expect(match).toMatchObject({ params: { lang: undefined, id: "123" } });
});

it("should match multiple optional segments and none are provided", () => {
const match = matchPath("/:lang?/user/:id?", "/user");
expect(match).toMatchObject({ params: { lang: undefined, id: undefined } });
});

it("should match multiple optional segments and one is provided", () => {
const match = matchPath("/:lang?/user/:id?", "/en/user");
expect(match).toMatchObject({ params: { lang: "en", id: undefined } });
});

it("should match multiple optional segments and all are provided", () => {
const match = matchPath("/:lang?/user/:id?", "/en/user/123");
expect(match).toMatchObject({ params: { lang: "en", id: "123" } });
});
});

describe("matchPath *", () => {
it("matches the root URL", () => {
expect(matchPath("*", "/")).toMatchObject({
Expand Down
34 changes: 19 additions & 15 deletions packages/router/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -903,7 +903,7 @@ export function matchPath<
pattern = { path: pattern, caseSensitive: false, end: true };
}

let [matcher, paramNames] = compilePath(
let [matcher, compiledParams] = compilePath(
pattern.path,
pattern.caseSensitive,
pattern.end
Expand All @@ -915,8 +915,8 @@ export function matchPath<
let matchedPathname = match[0];
let pathnameBase = matchedPathname.replace(/(.)\/+$/, "$1");
let captureGroups = match.slice(1);
let params: Params = paramNames.reduce<Mutable<Params>>(
(memo, paramName, index) => {
let params: Params = compiledParams.reduce<Mutable<Params>>(
(memo, { paramName, isOptional }, index) => {
// We need to compute the pathnameBase here using the raw splat value
// instead of using params["*"] later because it will be decoded then
if (paramName === "*") {
Expand All @@ -926,10 +926,12 @@ export function matchPath<
.replace(/(.)\/+$/, "$1");
}

memo[paramName] = safelyDecodeURIComponent(
captureGroups[index] || "",
paramName
);
const value = captureGroups[index];
if (isOptional && !value) {
memo[paramName] = undefined;
} else {
memo[paramName] = safelyDecodeURIComponent(value || "", paramName);
}
return memo;
},
{}
Expand All @@ -943,11 +945,13 @@ export function matchPath<
};
}

type CompiledPathParam = { paramName: string; isOptional?: boolean };

function compilePath(
path: string,
caseSensitive = false,
end = true
): [RegExp, string[]] {
): [RegExp, CompiledPathParam[]] {
warning(
path === "*" || !path.endsWith("*") || path.endsWith("/*"),
`Route path "${path}" will be treated as if it were ` +
Expand All @@ -956,20 +960,20 @@ function compilePath(
`please change the route path to "${path.replace(/\*$/, "/*")}".`
);

let paramNames: string[] = [];
let params: CompiledPathParam[] = [];
let regexpSource =
"^" +
path
.replace(/\/*\*?$/, "") // Ignore trailing / and /*, we'll handle it below
.replace(/^\/*/, "/") // Make sure it has a leading /
.replace(/[\\.*+^$?{}|()[\]]/g, "\\$&") // Escape special regex chars
.replace(/\/:(\w+)/g, (_: string, paramName: string) => {
paramNames.push(paramName);
return "/([^\\/]+)";
.replace(/[\\.*+^${}|()[\]]/g, "\\$&") // Escape special regex chars
.replace(/\/:(\w+)(\?)?/g, (_: string, paramName: string, isOptional) => {
params.push({ paramName, isOptional: isOptional != null });
return isOptional ? "/?([^\\/]+)?" : "/([^\\/]+)";
});

if (path.endsWith("*")) {
paramNames.push("*");
params.push({ paramName: "*" });
regexpSource +=
path === "*" || path === "/*"
? "(.*)$" // Already matched the initial /, just match the rest
Expand All @@ -992,7 +996,7 @@ function compilePath(

let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");

return [matcher, paramNames];
return [matcher, params];
}

function safelyDecodeURI(value: string) {
Expand Down

0 comments on commit 677d6c8

Please sign in to comment.