From ed9297c92d886b6d43580b09e5b5da4d7e11a90f Mon Sep 17 00:00:00 2001 From: Alex Mitchell Date: Thu, 6 Jan 2022 00:40:42 -0800 Subject: [PATCH 1/5] allow nested splat routes to include `.`, `-`, `~`, or a url-encoded entity as the first character --- .../descendant-routes-splat-matching-test.tsx | 69 +++++- .../__tests__/layout-routes-test.tsx | 207 ++++++++++++++++++ packages/react-router/index.tsx | 5 +- 3 files changed, 279 insertions(+), 2 deletions(-) diff --git a/packages/react-router/__tests__/descendant-routes-splat-matching-test.tsx b/packages/react-router/__tests__/descendant-routes-splat-matching-test.tsx index 5c94dd9023..dccca680ad 100644 --- a/packages/react-router/__tests__/descendant-routes-splat-matching-test.tsx +++ b/packages/react-router/__tests__/descendant-routes-splat-matching-test.tsx @@ -1,6 +1,6 @@ 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"; describe("Descendant splat matching", () => { describe("when the parent route path ends with /*", () => { @@ -57,5 +57,72 @@ describe("Descendant splat matching", () => { `); }); + it("works with paths beginning with special characters", () => { + function PrintParams() { + return

The params are {JSON.stringify(useParams())}

; + } + function ReactCourses() { + return ( +
+

React

+ + +

React Fundamentals

+ +
+ } + /> +
+ + ); + } + + function Courses() { + return ( +
+

Courses

+ +
+ ); + } + + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create( + + + }> + } /> + + + + ); + }); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+

+ Courses +

+
+

+ React +

+
+

+ React Fundamentals +

+

+ The params are + {"*":"-react-fundamentals","splat":"-react-fundamentals"} +

+
+
+
+ `); + }); }); }); diff --git a/packages/react-router/__tests__/layout-routes-test.tsx b/packages/react-router/__tests__/layout-routes-test.tsx index ae0f5b9ab5..0884451520 100644 --- a/packages/react-router/__tests__/layout-routes-test.tsx +++ b/packages/react-router/__tests__/layout-routes-test.tsx @@ -31,4 +31,211 @@ describe("A layout route", () => { `); }); + 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( + + + +

Layout

+ + + } + > + +

Splat

+ + } + /> +
+
+
+ ); + }); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+

+ Layout +

+
+

+ Splat +

+
+
+ `); + }); + it("allows routes starting with `~`", () => { + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create( + + + +

Layout

+ + + } + > + +

Splat

+ + } + /> +
+
+
+ ); + }); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+

+ Layout +

+
+

+ Splat +

+
+
+ `); + }); + it("allows routes starting with `_`", () => { + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create( + + + +

Layout

+ + + } + > + +

Splat

+ + } + /> +
+
+
+ ); + }); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+

+ Layout +

+
+

+ Splat +

+
+
+ `); + }); + it("allows routes starting with `.`", () => { + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create( + + + +

Layout

+ + + } + > + +

Splat

+ + } + /> +
+
+
+ ); + }); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+

+ Layout +

+
+

+ Splat +

+
+
+ `); + }); + it("allows routes starting with url-encoded entities", () => { + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create( + + + +

Layout

+ + + } + > + +

Splat

+ + } + /> +
+
+
+ ); + }); + + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+

+ Layout +

+
+

+ Splat +

+
+
+ `); + }); + }); }); diff --git a/packages/react-router/index.tsx b/packages/react-router/index.tsx index 135d2e6bae..5aef0e5c5e 100644 --- a/packages/react-router/index.tsx +++ b/packages/react-router/index.tsx @@ -1222,7 +1222,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-7][0-9A-F])|(?:\\b|\\/|$))"; } let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i"); From 8925d33cb71a0a1ec5021752318fbc76abdaa611 Mon Sep 17 00:00:00 2001 From: Alex Mitchell Date: Thu, 6 Jan 2022 00:58:06 -0800 Subject: [PATCH 2/5] sign cla --- contributors.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/contributors.yml b/contributors.yml index 70d633a861..c5aa159a28 100644 --- a/contributors.yml +++ b/contributors.yml @@ -16,6 +16,7 @@ - petersendidit - RobHannay - sergiodxa +- shamsup - shivamsinghchahar - thisiskartik - timdorr From 6e6aeb7a74b2b753ab314d523340998d0ddc8e17 Mon Sep 17 00:00:00 2001 From: Alex Mitchell Date: Thu, 6 Jan 2022 09:24:30 -0800 Subject: [PATCH 3/5] expand url-encoded entity matching to include any two hex characters --- packages/react-router/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-router/index.tsx b/packages/react-router/index.tsx index 5aef0e5c5e..9b27731ee8 100644 --- a/packages/react-router/index.tsx +++ b/packages/react-router/index.tsx @@ -1225,7 +1225,7 @@ function compilePath( // 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-7][0-9A-F])|(?:\\b|\\/|$))"; + "(?:(?=[.~-]|%[0-9A-F]{2})|(?:\\b|\\/|$))"; } let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i"); From 87539baa5e12182298771eb0481d74d5bcdbfedf Mon Sep 17 00:00:00 2001 From: Alex Mitchell Date: Thu, 6 Jan 2022 09:25:57 -0800 Subject: [PATCH 4/5] add more exhaustive descendant splat route tests --- .../descendant-routes-splat-matching-test.tsx | 139 ++++++++++++++---- 1 file changed, 113 insertions(+), 26 deletions(-) diff --git a/packages/react-router/__tests__/descendant-routes-splat-matching-test.tsx b/packages/react-router/__tests__/descendant-routes-splat-matching-test.tsx index dccca680ad..33d2b3a675 100644 --- a/packages/react-router/__tests__/descendant-routes-splat-matching-test.tsx +++ b/packages/react-router/__tests__/descendant-routes-splat-matching-test.tsx @@ -1,6 +1,7 @@ import * as React from "react"; import * as TestRenderer from "react-test-renderer"; import { MemoryRouter, Outlet, Routes, Route, useParams } from "react-router"; +import type { InitialEntry } from "history"; describe("Descendant splat matching", () => { describe("when the parent route path ends with /*", () => { @@ -57,7 +58,7 @@ describe("Descendant splat matching", () => { `); }); - it("works with paths beginning with special characters", () => { + describe("works with paths beginning with special characters", () => { function PrintParams() { return

The params are {JSON.stringify(useParams())}

; } @@ -89,40 +90,126 @@ describe("Descendant splat matching", () => { ); } - let renderer: TestRenderer.ReactTestRenderer; - TestRenderer.act(() => { - renderer = TestRenderer.create( - - - }> - } /> - - - - ); - }); + function renderNestedSplatRoute(initialEntries: InitialEntry[]) { + let renderer: TestRenderer.ReactTestRenderer; + TestRenderer.act(() => { + renderer = TestRenderer.create( + + + }> + } /> + + + + ); + }); + return renderer; + } - expect(renderer.toJSON()).toMatchInlineSnapshot(` -
-

- Courses -

+ it("allows `-` to appear at the beginning", () => { + let renderer = renderNestedSplatRoute([ + "/courses/react/-react-fundamentals" + ]); + expect(renderer.toJSON()).toMatchInlineSnapshot(`

- React + Courses

- React Fundamentals + React

-

- The params are - {"*":"-react-fundamentals","splat":"-react-fundamentals"} -

+
+

+ React Fundamentals +

+

+ The params are + {"*":"-react-fundamentals","splat":"-react-fundamentals"} +

+
-
- `); + `); + }); + it("allows `.` to appear at the beginning", () => { + let renderer = renderNestedSplatRoute([ + "/courses/react/.react-fundamentals" + ]); + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+

+ Courses +

+
+

+ React +

+
+

+ React Fundamentals +

+

+ The params are + {"*":".react-fundamentals","splat":".react-fundamentals"} +

+
+
+
+ `); + }); + it("allows `~` to appear at the beginning", () => { + let renderer = renderNestedSplatRoute([ + "/courses/react/~react-fundamentals" + ]); + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+

+ Courses +

+
+

+ React +

+
+

+ React Fundamentals +

+

+ The params are + {"*":"~react-fundamentals","splat":"~react-fundamentals"} +

+
+
+
+ `); + }); + it("allows url-encoded entities to appear at the beginning", () => { + let renderer = renderNestedSplatRoute([ + "/courses/react/%20react-fundamentals" + ]); + expect(renderer.toJSON()).toMatchInlineSnapshot(` +
+

+ Courses +

+
+

+ React +

+
+

+ React Fundamentals +

+

+ The params are + {"*":" react-fundamentals","splat":" react-fundamentals"} +

+
+
+
+ `); + }); }); }); }); From da5435dac5555c0740f50e0999fd85cea3a28c04 Mon Sep 17 00:00:00 2001 From: Alex Mitchell Date: Thu, 6 Jan 2022 09:30:17 -0800 Subject: [PATCH 5/5] remove extra non-capturing group --- packages/react-router/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-router/index.tsx b/packages/react-router/index.tsx index 9b27731ee8..edbee188d4 100644 --- a/packages/react-router/index.tsx +++ b/packages/react-router/index.tsx @@ -1225,7 +1225,7 @@ function compilePath( // 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|\\/|$))"; + "(?:(?=[.~-]|%[0-9A-F]{2})|\\b|\\/|$)"; } let matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");