Skip to content

Commit

Permalink
Fix: allow nested splat routes to begin with "special" url-safe chara…
Browse files Browse the repository at this point in the history
…cters (#8563)

* allow nested splat routes to include `.`, `-`, `~`, or a url-encoded entity as the first character

* sign cla

* expand url-encoded entity matching to include any two hex characters

* add more exhaustive descendant splat route tests

* remove extra non-capturing group
  • Loading branch information
shamsup committed Feb 28, 2022
1 parent 3f6bc1b commit b6e712e
Show file tree
Hide file tree
Showing 4 changed files with 367 additions and 2 deletions.
1 change: 1 addition & 0 deletions contributors.yml
Expand Up @@ -19,6 +19,7 @@
- petersendidit
- RobHannay
- sergiodxa
- shamsup
- shivamsinghchahar
- thisiskartik
- timdorr
Expand Down
@@ -1,6 +1,7 @@
import * as React from "react";
import * as TestRenderer from "react-test-renderer";
import { MemoryRouter, Outlet, Routes, Route } from "react-router";
import { MemoryRouter, Outlet, Routes, Route, useParams } from "react-router";
import type { InitialEntry } from "history";

describe("Descendant <Routes> splat matching", () => {
describe("when the parent route path ends with /*", () => {
Expand Down Expand Up @@ -57,5 +58,158 @@ describe("Descendant <Routes> splat matching", () => {
</div>
`);
});
describe("works with paths beginning with special characters", () => {
function PrintParams() {
return <p>The params are {JSON.stringify(useParams())}</p>;
}
function ReactCourses() {
return (
<div>
<h1>React</h1>
<Routes>
<Route
path=":splat"
element={
<div>
<h1>React Fundamentals</h1>
<PrintParams />
</div>
}
/>
</Routes>
</div>
);
}

function Courses() {
return (
<div>
<h1>Courses</h1>
<Outlet />
</div>
);
}

function renderNestedSplatRoute(initialEntries: InitialEntry[]) {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
<MemoryRouter initialEntries={initialEntries}>
<Routes>
<Route path="courses" element={<Courses />}>
<Route path="react/*" element={<ReactCourses />} />
</Route>
</Routes>
</MemoryRouter>
);
});
return renderer;
}

it("allows `-` to appear at the beginning", () => {
let renderer = renderNestedSplatRoute([
"/courses/react/-react-fundamentals"
]);
expect(renderer.toJSON()).toMatchInlineSnapshot(`
<div>
<h1>
Courses
</h1>
<div>
<h1>
React
</h1>
<div>
<h1>
React Fundamentals
</h1>
<p>
The params are
{"*":"-react-fundamentals","splat":"-react-fundamentals"}
</p>
</div>
</div>
</div>
`);
});
it("allows `.` to appear at the beginning", () => {
let renderer = renderNestedSplatRoute([
"/courses/react/.react-fundamentals"
]);
expect(renderer.toJSON()).toMatchInlineSnapshot(`
<div>
<h1>
Courses
</h1>
<div>
<h1>
React
</h1>
<div>
<h1>
React Fundamentals
</h1>
<p>
The params are
{"*":".react-fundamentals","splat":".react-fundamentals"}
</p>
</div>
</div>
</div>
`);
});
it("allows `~` to appear at the beginning", () => {
let renderer = renderNestedSplatRoute([
"/courses/react/~react-fundamentals"
]);
expect(renderer.toJSON()).toMatchInlineSnapshot(`
<div>
<h1>
Courses
</h1>
<div>
<h1>
React
</h1>
<div>
<h1>
React Fundamentals
</h1>
<p>
The params are
{"*":"~react-fundamentals","splat":"~react-fundamentals"}
</p>
</div>
</div>
</div>
`);
});
it("allows url-encoded entities to appear at the beginning", () => {
let renderer = renderNestedSplatRoute([
"/courses/react/%20react-fundamentals"
]);
expect(renderer.toJSON()).toMatchInlineSnapshot(`
<div>
<h1>
Courses
</h1>
<div>
<h1>
React
</h1>
<div>
<h1>
React Fundamentals
</h1>
<p>
The params are
{"*":" react-fundamentals","splat":" react-fundamentals"}
</p>
</div>
</div>
</div>
`);
});
});
});
});
207 changes: 207 additions & 0 deletions packages/react-router/__tests__/layout-routes-test.tsx
Expand Up @@ -31,4 +31,211 @@ describe("A layout route", () => {
</h1>
`);
});
describe("matches when a nested splat route begins with a special character", () => {
it("allows routes starting with `-`", () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
<MemoryRouter initialEntries={["/-splat"]}>
<Routes>
<Route
element={
<div>
<h1>Layout</h1>
<Outlet />
</div>
}
>
<Route
path="*"
element={
<div>
<h1>Splat</h1>
</div>
}
/>
</Route>
</Routes>
</MemoryRouter>
);
});

expect(renderer.toJSON()).toMatchInlineSnapshot(`
<div>
<h1>
Layout
</h1>
<div>
<h1>
Splat
</h1>
</div>
</div>
`);
});
it("allows routes starting with `~`", () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
<MemoryRouter initialEntries={["/~splat"]}>
<Routes>
<Route
element={
<div>
<h1>Layout</h1>
<Outlet />
</div>
}
>
<Route
path="*"
element={
<div>
<h1>Splat</h1>
</div>
}
/>
</Route>
</Routes>
</MemoryRouter>
);
});

expect(renderer.toJSON()).toMatchInlineSnapshot(`
<div>
<h1>
Layout
</h1>
<div>
<h1>
Splat
</h1>
</div>
</div>
`);
});
it("allows routes starting with `_`", () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
<MemoryRouter initialEntries={["/_splat"]}>
<Routes>
<Route
element={
<div>
<h1>Layout</h1>
<Outlet />
</div>
}
>
<Route
path="*"
element={
<div>
<h1>Splat</h1>
</div>
}
/>
</Route>
</Routes>
</MemoryRouter>
);
});

expect(renderer.toJSON()).toMatchInlineSnapshot(`
<div>
<h1>
Layout
</h1>
<div>
<h1>
Splat
</h1>
</div>
</div>
`);
});
it("allows routes starting with `.`", () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
<MemoryRouter initialEntries={["/.splat"]}>
<Routes>
<Route
element={
<div>
<h1>Layout</h1>
<Outlet />
</div>
}
>
<Route
path="*"
element={
<div>
<h1>Splat</h1>
</div>
}
/>
</Route>
</Routes>
</MemoryRouter>
);
});

expect(renderer.toJSON()).toMatchInlineSnapshot(`
<div>
<h1>
Layout
</h1>
<div>
<h1>
Splat
</h1>
</div>
</div>
`);
});
it("allows routes starting with url-encoded entities", () => {
let renderer: TestRenderer.ReactTestRenderer;
TestRenderer.act(() => {
renderer = TestRenderer.create(
<MemoryRouter initialEntries={["/%20splat"]}>
<Routes>
<Route
element={
<div>
<h1>Layout</h1>
<Outlet />
</div>
}
>
<Route
path="*"
element={
<div>
<h1>Splat</h1>
</div>
}
/>
</Route>
</Routes>
</MemoryRouter>
);
});

expect(renderer.toJSON()).toMatchInlineSnapshot(`
<div>
<h1>
Layout
</h1>
<div>
<h1>
Splat
</h1>
</div>
</div>
`);
});
});
});
5 changes: 4 additions & 1 deletion packages/react-router/index.tsx
Expand Up @@ -1227,7 +1227,10 @@ function compilePath(
: // Otherwise, match a word boundary or a proceeding /. The word boundary restricts
// parent routes to matching only their own words and nothing more, e.g. parent
// route "/home" should not match "/home2".
"(?:\\b|\\/|$)";
// Additionally, allow paths starting with `.`, `-`, `~`, and url-encoded entities,
// but do not consume the character in the matched path so they can match against
// nested paths.
"(?:(?=[.~-]|%[0-9A-F]{2})|\\b|\\/|$)";
}

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

0 comments on commit b6e712e

Please sign in to comment.