Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/framework/react/api/router/RouterOptionsType.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,10 @@ const router = createRouter({
- Type: [`RouterTransformer`](../RouterTransformerType)
- Optional
- The transformer that will be used when sending data between the server and the client during SSR.

### `trailingSlash` property

- Type: `'always' | 'never' | 'preserve'`
- Optional
- Defaults to `never`
- Configures how trailing slashes are treated. `'always'` will add a trailing slash if not present, `'never'` will remove the trailing slash if present and `'preserve'` will not modify the trailing slash.
26 changes: 23 additions & 3 deletions packages/react-router/src/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,18 @@ export function trimPath(path: string) {
// /a/b/c + d = /a/b/c/d
// /a/b/c + d/ = /a/b/c/d
// /a/b/c + d/e = /a/b/c/d/e
export function resolvePath(basepath: string, base: string, to: string) {
interface ResolvePathOptions {
basepath: string
base: string
to: string
trailingSlash?: 'always' | 'never' | 'preserve'
}
export function resolvePath({
basepath,
base,
to,
trailingSlash = 'never',
}: ResolvePathOptions) {
base = base.replace(new RegExp(`^${basepath}`), '/')
to = to.replace(new RegExp(`^${basepath}`), '/')

Expand Down Expand Up @@ -85,9 +96,18 @@ export function resolvePath(basepath: string, base: string, to: string) {
}
})

const joined = joinPaths([basepath, ...baseSegments.map((d) => d.value)])
if (baseSegments.length > 1) {
if (last(baseSegments)?.value === '/') {
if (trailingSlash === 'never') {
baseSegments.pop()
}
} else if (trailingSlash === 'always') {
baseSegments.push({ type: 'pathname', value: '/' })
}
}

return cleanPath(trimPathRight(joined))
const joined = joinPaths([basepath, ...baseSegments.map((d) => d.value)])
return cleanPath(joined)
}

export function parsePathname(pathname?: string): Array<Segment> {
Expand Down
9 changes: 8 additions & 1 deletion packages/react-router/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export interface RouterOptions<
defaultNotFoundComponent?: NotFoundRouteComponent
transformer?: RouterTransformer
errorSerializer?: RouterErrorSerializer<TSerializedError>
trailingSlash?: 'always' | 'never' | 'preserve'
}

export interface RouterTransformer {
Expand Down Expand Up @@ -591,7 +592,13 @@ export class Router<
}

resolvePathWithBase = (from: string, path: string) => {
return resolvePath(this.basepath, from, cleanPath(path))
const resolvedPath = resolvePath({
basepath: this.basepath,
base: from,
to: cleanPath(path),
trailingSlash: this.options.trailingSlash,
})
return resolvedPath
}

get looseRoutesById() {
Expand Down
76 changes: 73 additions & 3 deletions packages/react-router/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -572,13 +572,83 @@ describe('resolvePath', () => {
] as const
).forEach(([base, a, b, eq]) => {
test(`Base: ${base} - ${a} to ${b} === ${eq}`, () => {
expect(resolvePath(base, a, b)).toEqual(eq)
expect(resolvePath({ basepath: base, base: a, to: b })).toEqual(eq)
})
test(`Base: ${base} - ${a}/ to ${b} === ${eq} (trailing slash)`, () => {
expect(resolvePath(base, a + '/', b)).toEqual(eq)
expect(resolvePath({ basepath: base, base: a + '/', to: b })).toEqual(eq)
})
test(`Base: ${base} - ${a}/ to ${b}/ === ${eq} (trailing slash + trailing slash)`, () => {
expect(resolvePath(base, a + '/', b + '/')).toEqual(eq)
expect(
resolvePath({ basepath: base, base: a + '/', to: b + '/' }),
).toEqual(eq)
})
})
describe('trailingSlash', () => {
describe(`'always'`, () => {
test('keeps trailing slash', () => {
expect(
resolvePath({
basepath: '/',
base: '/a/b/c',
to: 'd/',
trailingSlash: 'always',
}),
).toBe('/a/b/c/d/')
})
test('adds trailing slash', () => {
expect(
resolvePath({
basepath: '/',
base: '/a/b/c',
to: 'd',
trailingSlash: 'always',
}),
).toBe('/a/b/c/d/')
})
})
describe(`'never'`, () => {
test('removes trailing slash', () => {
expect(
resolvePath({
basepath: '/',
base: '/a/b/c',
to: 'd/',
trailingSlash: 'never',
}),
).toBe('/a/b/c/d')
})
test('does not add trailing slash', () => {
expect(
resolvePath({
basepath: '/',
base: '/a/b/c',
to: 'd',
trailingSlash: 'never',
}),
).toBe('/a/b/c/d')
})
})
describe(`'preserve'`, () => {
test('keeps trailing slash', () => {
expect(
resolvePath({
basepath: '/',
base: '/a/b/c',
to: 'd/',
trailingSlash: 'preserve',
}),
).toBe('/a/b/c/d/')
})
test('does not add trailing slash', () => {
expect(
resolvePath({
basepath: '/',
base: '/a/b/c',
to: 'd',
trailingSlash: 'preserve',
}),
).toBe('/a/b/c/d')
})
})
})
})