Skip to content

Commit ac0b761

Browse files
committed
Support trailing slashes, not extraneous ones (#3285)
* Support trailing slashes, not extraneous ones * Fix unintentional regressions from removing extraneous slash support
1 parent 4d66469 commit ac0b761

File tree

7 files changed

+172
-44
lines changed

7 files changed

+172
-44
lines changed

modules/PatternUtils.js

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@ function escapeRegExp(string) {
44
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
55
}
66

7-
function escapeSource(string) {
8-
return escapeRegExp(string).replace(/\/+/g, '/+')
9-
}
10-
117
function _compilePattern(pattern) {
128
let regexpSource = ''
139
const paramNames = []
@@ -17,7 +13,7 @@ function _compilePattern(pattern) {
1713
while ((match = matcher.exec(pattern))) {
1814
if (match.index !== lastIndex) {
1915
tokens.push(pattern.slice(lastIndex, match.index))
20-
regexpSource += escapeSource(pattern.slice(lastIndex, match.index))
16+
regexpSource += escapeRegExp(pattern.slice(lastIndex, match.index))
2117
}
2218

2319
if (match[1]) {
@@ -42,7 +38,7 @@ function _compilePattern(pattern) {
4238

4339
if (lastIndex !== pattern.length) {
4440
tokens.push(pattern.slice(lastIndex, pattern.length))
45-
regexpSource += escapeSource(pattern.slice(lastIndex, pattern.length))
41+
regexpSource += escapeRegExp(pattern.slice(lastIndex, pattern.length))
4642
}
4743

4844
return {
@@ -82,17 +78,15 @@ export function compilePattern(pattern) {
8278
* - paramValues
8379
*/
8480
export function matchPattern(pattern, pathname) {
85-
// Make leading slashes consistent between pattern and pathname.
81+
// Ensure pattern starts with leading slash for consistency with pathname.
8682
if (pattern.charAt(0) !== '/') {
8783
pattern = `/${pattern}`
8884
}
89-
if (pathname.charAt(0) !== '/') {
90-
pathname = `/${pathname}`
91-
}
92-
9385
let { regexpSource, paramNames, tokens } = compilePattern(pattern)
9486

95-
regexpSource += '/*' // Capture path separators
87+
if (pattern.charAt(pattern.length - 1) !== '/') {
88+
regexpSource += '/?' // Allow optional path separator at end.
89+
}
9690

9791
// Special-case patterns like '*' for catch-all routes.
9892
if (tokens[tokens.length - 1] === '*') {
@@ -106,18 +100,20 @@ export function matchPattern(pattern, pathname) {
106100
const matchedPath = match[0]
107101
remainingPathname = pathname.substr(matchedPath.length)
108102

109-
// If we didn't match the entire pathname, then make sure that the match we
110-
// did get ends at a path separator (potentially the one we added above at
111-
// the beginning of the path, if the actual match was empty).
112-
if (
113-
remainingPathname &&
114-
matchedPath.charAt(matchedPath.length - 1) !== '/'
115-
) {
116-
return {
117-
remainingPathname: null,
118-
paramNames,
119-
paramValues: null
103+
if (remainingPathname) {
104+
// Require that the match ends at a path separator, if we didn't match
105+
// the full path, so any remaining pathname is a new path segment.
106+
if (matchedPath.charAt(matchedPath.length - 1) !== '/') {
107+
return {
108+
remainingPathname: null,
109+
paramNames,
110+
paramValues: null
111+
}
120112
}
113+
114+
// If there is a remaining pathname, treat the path separator as part of
115+
// the remaining pathname for properly continuing the match.
116+
remainingPathname = `/${remainingPathname}`
121117
}
122118

123119
paramValues = match.slice(1).map(v => v && decodeURIComponent(v))

modules/__tests__/_bc-isActive-test.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,15 +95,15 @@ describe('v1 isActive', function () {
9595
})
9696
})
9797

98-
it('is active with extraneous slashes', function (done) {
98+
it('is not active with extraneous slashes', function (done) {
9999
render((
100100
<Router history={createHistory('/parent/child')}>
101101
<Route path="/parent">
102102
<Route path="child" />
103103
</Route>
104104
</Router>
105105
), node, function () {
106-
expect(this.history.isActive('/parent////child////')).toBe(true)
106+
expect(this.history.isActive('/parent////child////')).toBe(false)
107107
done()
108108
})
109109
})
@@ -293,7 +293,7 @@ describe('v1 isActive', function () {
293293
})
294294
})
295295

296-
it('is active with extraneous slashes', function (done) {
296+
it('is not active with extraneous slashes', function (done) {
297297
render((
298298
<Router history={createHistory('/parent/child')}>
299299
<Route path="/parent">
@@ -303,8 +303,8 @@ describe('v1 isActive', function () {
303303
</Route>
304304
</Router>
305305
), node, function () {
306-
expect(this.history.isActive('/parent///child///', null)).toBe(true)
307-
expect(this.history.isActive('/parent///child///', null, true)).toBe(true)
306+
expect(this.history.isActive('/parent///child///', null)).toBe(false)
307+
expect(this.history.isActive('/parent///child///', null, true)).toBe(false)
308308
done()
309309
})
310310
})
@@ -329,7 +329,7 @@ describe('v1 isActive', function () {
329329
})
330330
})
331331

332-
it('is active with extraneous slashes', function (done) {
332+
it('is not active with extraneous slashes', function (done) {
333333
render((
334334
<Router history={createHistory('/parent/child')}>
335335
<Route path="/parent">
@@ -341,8 +341,8 @@ describe('v1 isActive', function () {
341341
</Route>
342342
</Router>
343343
), node, function () {
344-
expect(this.history.isActive('/parent///child///', null)).toBe(true)
345-
expect(this.history.isActive('/parent///child///', null, true)).toBe(true)
344+
expect(this.history.isActive('/parent///child///', null)).toBe(false)
345+
expect(this.history.isActive('/parent///child///', null, true)).toBe(false)
346346
done()
347347
})
348348
})

modules/__tests__/isActive-test.js

Lines changed: 112 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,15 +114,43 @@ describe('isActive', function () {
114114
})
115115
})
116116

117-
it('is active with extraneous slashes', function (done) {
117+
it('is active with trailing slash on pattern', function (done) {
118118
render((
119119
<Router history={createHistory('/parent/child')}>
120120
<Route path="/parent">
121121
<Route path="child" />
122122
</Route>
123123
</Router>
124124
), node, function () {
125-
expect(this.router.isActive('/parent////child////')).toBe(true)
125+
expect(this.router.isActive('/parent/child/')).toBe(true)
126+
done()
127+
})
128+
})
129+
130+
it('is active with trailing slash on location', function (done) {
131+
render((
132+
<Router history={createHistory('/parent/child/')}>
133+
<Route path="/parent">
134+
<Route path="child" />
135+
</Route>
136+
</Router>
137+
), node, function () {
138+
expect(this.router.isActive('/parent/child')).toBe(true)
139+
expect(this.router.isActive('/parent/child/')).toBe(true)
140+
done()
141+
})
142+
})
143+
144+
it('is not active with extraneous slashes', function (done) {
145+
render((
146+
<Router history={createHistory('/parent/child')}>
147+
<Route path="/parent">
148+
<Route path="child" />
149+
</Route>
150+
</Router>
151+
), node, function () {
152+
expect(this.router.isActive('/parent//child')).toBe(false)
153+
expect(this.router.isActive('/parent/child//')).toBe(false)
126154
done()
127155
})
128156
})
@@ -336,7 +364,41 @@ describe('isActive', function () {
336364
})
337365
})
338366

339-
it('is active with extraneous slashes', function (done) {
367+
it('is active with trailing slash on pattern', function (done) {
368+
render((
369+
<Router history={createHistory('/parent/child')}>
370+
<Route path="/parent">
371+
<Route path="child">
372+
<IndexRoute />
373+
</Route>
374+
</Route>
375+
</Router>
376+
), node, function () {
377+
expect(this.router.isActive('/parent/child/')).toBe(true)
378+
expect(this.router.isActive('/parent/child/', true)).toBe(true)
379+
done()
380+
})
381+
})
382+
383+
it('is active with trailing slash on location', function (done) {
384+
render((
385+
<Router history={createHistory('/parent/child/')}>
386+
<Route path="/parent">
387+
<Route path="child">
388+
<IndexRoute />
389+
</Route>
390+
</Route>
391+
</Router>
392+
), node, function () {
393+
expect(this.router.isActive('/parent/child')).toBe(true)
394+
expect(this.router.isActive('/parent/child', true)).toBe(true)
395+
expect(this.router.isActive('/parent/child/')).toBe(true)
396+
expect(this.router.isActive('/parent/child/', true)).toBe(true)
397+
done()
398+
})
399+
})
400+
401+
it('is not active with extraneous slashes', function (done) {
340402
render((
341403
<Router history={createHistory('/parent/child')}>
342404
<Route path="/parent">
@@ -346,8 +408,10 @@ describe('isActive', function () {
346408
</Route>
347409
</Router>
348410
), node, function () {
349-
expect(this.router.isActive('/parent///child///')).toBe(true)
350-
expect(this.router.isActive('/parent///child///', true)).toBe(true)
411+
expect(this.router.isActive('/parent//child')).toBe(false)
412+
expect(this.router.isActive('/parent/child//')).toBe(false)
413+
expect(this.router.isActive('/parent//child', true)).toBe(false)
414+
expect(this.router.isActive('/parent/child//', true)).toBe(false)
351415
done()
352416
})
353417
})
@@ -372,7 +436,45 @@ describe('isActive', function () {
372436
})
373437
})
374438

375-
it('is active with extraneous slashes', function (done) {
439+
it('is active with trailing slash on pattern', function (done) {
440+
render((
441+
<Router history={createHistory('/parent/child')}>
442+
<Route path="/parent">
443+
<Route path="child">
444+
<Route>
445+
<IndexRoute />
446+
</Route>
447+
</Route>
448+
</Route>
449+
</Router>
450+
), node, function () {
451+
expect(this.router.isActive('/parent/child/')).toBe(true)
452+
expect(this.router.isActive('/parent/child/', true)).toBe(true)
453+
done()
454+
})
455+
})
456+
457+
it('is active with trailing slash on location', function (done) {
458+
render((
459+
<Router history={createHistory('/parent/child/')}>
460+
<Route path="/parent">
461+
<Route path="child">
462+
<Route>
463+
<IndexRoute />
464+
</Route>
465+
</Route>
466+
</Route>
467+
</Router>
468+
), node, function () {
469+
expect(this.router.isActive('/parent/child')).toBe(true)
470+
expect(this.router.isActive('/parent/child', true)).toBe(true)
471+
expect(this.router.isActive('/parent/child/')).toBe(true)
472+
expect(this.router.isActive('/parent/child/', true)).toBe(true)
473+
done()
474+
})
475+
})
476+
477+
it('is not active with extraneous slashes', function (done) {
376478
render((
377479
<Router history={createHistory('/parent/child')}>
378480
<Route path="/parent">
@@ -384,8 +486,10 @@ describe('isActive', function () {
384486
</Route>
385487
</Router>
386488
), node, function () {
387-
expect(this.router.isActive('/parent///child///')).toBe(true)
388-
expect(this.router.isActive('/parent///child///', true)).toBe(true)
489+
expect(this.router.isActive('/parent//child')).toBe(false)
490+
expect(this.router.isActive('/parent/child//')).toBe(false)
491+
expect(this.router.isActive('/parent//child', true)).toBe(false)
492+
expect(this.router.isActive('/parent/child//', true)).toBe(false)
389493
done()
390494
})
391495
})

modules/__tests__/matchPattern-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ describe('matchPattern', function () {
1212
}
1313

1414
it('works without params', function () {
15-
assertMatch('/', '/path', 'path', [], [])
15+
assertMatch('/', '/path', '/path', [], [])
1616
})
1717

1818
it('works with named params', function () {

modules/__tests__/serverRendering-test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,16 @@ describe('server rendering', function () {
162162
})
163163
})
164164

165+
it('supports basenames with trailing slash', function (done) {
166+
match({ routes, location: '/dashboard', basename: '/nasebame/' }, function (error, redirectLocation, renderProps) {
167+
const string = renderToString(
168+
<RouterContext {...renderProps} />
169+
)
170+
expect(string).toMatch(/\/nasebame/)
171+
done()
172+
})
173+
})
174+
165175
describe('server/client consistency', function () {
166176
// Just render to static markup here to avoid having to normalize markup.
167177

modules/isActive.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ function getMatchingRouteIndex(pathname, activeRoutes, activeParams) {
8181
* and params.
8282
*/
8383
function routeIsActive(pathname, routes, params, indexOnly) {
84+
// TODO: This is a bit ugly. It keeps around support for treating pathnames
85+
// without preceding slashes as absolute paths, but possibly also works
86+
// around the same quirks with basenames as in matchRoutes.
87+
if (pathname.charAt(0) !== '/') {
88+
pathname = `/${pathname}`
89+
}
90+
8491
const i = getMatchingRouteIndex(pathname, routes, params)
8592

8693
if (i === null) {

modules/matchRoutes.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,23 @@ function matchRouteDeep(
167167
* Note: This operation may finish synchronously if no routes have an
168168
* asynchronous getChildRoutes method.
169169
*/
170-
function matchRoutes(
170+
export default function matchRoutes(
171171
routes, location, callback,
172-
remainingPathname=location.pathname, paramNames=[], paramValues=[]
172+
remainingPathname, paramNames=[], paramValues=[]
173173
) {
174+
if (remainingPathname === undefined) {
175+
// TODO: This is a little bit ugly, but it works around a quirk in history
176+
// that strips the leading slash from pathnames when using basenames with
177+
// trailing slashes.
178+
if (location.pathname.charAt(0) !== '/') {
179+
location = {
180+
...location,
181+
pathname: `/${location.pathname}`
182+
}
183+
}
184+
remainingPathname = location.pathname
185+
}
186+
174187
loopAsync(routes.length, function (index, next, done) {
175188
matchRouteDeep(
176189
routes[index], location, remainingPathname, paramNames, paramValues,
@@ -184,5 +197,3 @@ function matchRoutes(
184197
)
185198
}, callback)
186199
}
187-
188-
export default matchRoutes

0 commit comments

Comments
 (0)