diff --git a/.changeset/dynamic-param-dash.md b/.changeset/dynamic-param-dash.md new file mode 100644 index 0000000000..4d5cec4895 --- /dev/null +++ b/.changeset/dynamic-param-dash.md @@ -0,0 +1,5 @@ +--- +"@remix-run/router": patch +--- + +Fix bug where dashes were not picked up in dynamic parameter names diff --git a/packages/react-router/__tests__/generatePath-test.tsx b/packages/react-router/__tests__/generatePath-test.tsx index fe5e860056..70232f7d46 100644 --- a/packages/react-router/__tests__/generatePath-test.tsx +++ b/packages/react-router/__tests__/generatePath-test.tsx @@ -52,6 +52,12 @@ describe("generatePath", () => { // incorrect usage but worked in 6.3.0 so keep it to avoid the regression expect(generatePath("/courses/*", { "*": 0 })).toBe("/courses/0"); }); + + it("handles dashes in dynamic params", () => { + expect(generatePath("/courses/:foo-bar", { "foo-bar": "baz" })).toBe( + "/courses/baz" + ); + }); }); describe("with extraneous params", () => { diff --git a/packages/react-router/__tests__/path-matching-test.tsx b/packages/react-router/__tests__/path-matching-test.tsx index 1ef1c127ec..5f00c2445b 100644 --- a/packages/react-router/__tests__/path-matching-test.tsx +++ b/packages/react-router/__tests__/path-matching-test.tsx @@ -130,6 +130,44 @@ describe("path matching", () => { expect(pickPaths(routes, "/page")).toEqual(["page"]); }); + + test("dynamic segments can contain dashes", () => { + let routes = [ + { + path: ":foo-bar", + }, + { + path: "foo-bar", + }, + ]; + + expect(matchRoutes(routes, "/foo-bar")).toMatchInlineSnapshot(` + [ + { + "params": {}, + "pathname": "/foo-bar", + "pathnameBase": "/foo-bar", + "route": { + "path": "foo-bar", + }, + }, + ] + `); + expect(matchRoutes(routes, "/whatever")).toMatchInlineSnapshot(` + [ + { + "params": { + "foo-bar": "whatever", + }, + "pathname": "/whatever", + "pathnameBase": "/whatever", + "route": { + "path": ":foo-bar", + }, + }, + ] + `); + }); }); describe("path matching with a basename", () => { diff --git a/packages/router/utils.ts b/packages/router/utils.ts index a8c35d9595..c845d0285f 100644 --- a/packages/router/utils.ts +++ b/packages/router/utils.ts @@ -685,7 +685,7 @@ function rankRouteBranches(branches: RouteBranch[]): void { ); } -const paramRe = /^:\w+$/; +const paramRe = /^:[\w-]+$/; const dynamicSegmentValue = 3; const indexRouteValue = 2; const emptySegmentValue = 1; @@ -822,7 +822,7 @@ export function generatePath( return stringify(params[star]); } - const keyMatch = segment.match(/^:(\w+)(\??)$/); + const keyMatch = segment.match(/^:([\w-]+)(\??)$/); if (keyMatch) { const [, key, optional] = keyMatch; let param = params[key as PathParam]; @@ -967,10 +967,13 @@ function compilePath( .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, isOptional) => { - params.push({ paramName, isOptional: isOptional != null }); - return isOptional ? "/?([^\\/]+)?" : "/([^\\/]+)"; - }); + .replace( + /\/:([\w-]+)(\?)?/g, + (_: string, paramName: string, isOptional) => { + params.push({ paramName, isOptional: isOptional != null }); + return isOptional ? "/?([^\\/]+)?" : "/([^\\/]+)"; + } + ); if (path.endsWith("*")) { params.push({ paramName: "*" });