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
8 changes: 8 additions & 0 deletions e2e/react-start/custom-basepath/tests/navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,16 @@ test('server-side redirect', async ({ page, baseURL }) => {
expect(page.url()).toBe(`${baseURL}/posts/1`)

// do not follow redirects since we want to test the Location header
// first go to the route WITHOUT the base path, this will just add the base path
await page.request
.get('/redirect/throw-it', { maxRedirects: 0 })
.then((res) => {
const headers = new Headers(res.headers())
expect(headers.get('location')).toBe('/custom/basepath/redirect/throw-it')
})
// now go to the route WITH the base path, this will redirect to the final destination
await page.request
.get('/custom/basepath/redirect/throw-it', { maxRedirects: 0 })
.then((res) => {
const headers = new Headers(res.headers())
expect(headers.get('location')).toBe('/custom/basepath/posts/1')
Expand Down
8 changes: 8 additions & 0 deletions e2e/solid-start/custom-basepath/tests/navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,16 @@ test('server-side redirect', async ({ page, baseURL }) => {
expect(page.url()).toBe(`${baseURL}/posts/1`)

// do not follow redirects since we want to test the Location header
// first go to the route WITHOUT the base path, this will just add the base path
await page.request
.get('/redirect/throw-it', { maxRedirects: 0 })
.then((res) => {
const headers = new Headers(res.headers())
expect(headers.get('location')).toBe('/custom/basepath/redirect/throw-it')
})
// now go to the route WITH the base path, this will redirect to the final destination
await page.request
.get('/custom/basepath/redirect/throw-it', { maxRedirects: 0 })
.then((res) => {
const headers = new Headers(res.headers())
expect(headers.get('location')).toBe('/custom/basepath/posts/1')
Expand Down
4 changes: 3 additions & 1 deletion packages/react-router/src/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ export function useLinkProps<
if (disabled) {
return undefined
}
let href = next.maskedLocation ? next.maskedLocation.url : next.url
let href = next.maskedLocation
? next.maskedLocation.url.href
: next.url.href

let external = false
if (router.origin) {
Expand Down
2 changes: 1 addition & 1 deletion packages/react-router/tests/redirect.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ describe('redirect', () => {
__TSR_key: currentRedirect.options._fromLocation!.state.__TSR_key,
key: currentRedirect.options._fromLocation!.state.key,
},
url: 'http://localhost/',
url: new URL('http://localhost/'),
},
href: '/about',
to: '/about',
Expand Down
34 changes: 21 additions & 13 deletions packages/react-router/tests/router.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ describe('encoding: path params', () => {

await act(() => router.load())

expect(router.state.location.url.endsWith('/posts/tanner')).toBe(true)
expect(router.state.location.url.href.endsWith('/posts/tanner')).toBe(true)
expect(router.state.location.href).toBe('/posts/tanner')
expect(router.state.location.pathname).toBe('/posts/tanner')
})
Expand All @@ -362,7 +362,9 @@ describe('encoding: path params', () => {

await act(() => router.load())

expect(router.state.location.url.endsWith('/posts/%F0%9F%9A%80')).toBe(true)
expect(router.state.location.url.href.endsWith('/posts/%F0%9F%9A%80')).toBe(
true,
)
expect(router.state.location.href).toBe('/posts/%F0%9F%9A%80')
expect(router.state.location.pathname).toBe('/posts/🚀')
})
Expand All @@ -381,7 +383,9 @@ describe('encoding: path params', () => {
}),
)

expect(router.state.location.url.endsWith('/posts/100%2525')).toBe(true)
expect(router.state.location.url.href.endsWith('/posts/100%2525')).toBe(
true,
)
expect(router.state.location.href).toBe('/posts/100%2525')
expect(router.state.location.pathname).toBe('/posts/100%2525')
})
Expand All @@ -406,7 +410,9 @@ describe('encoding: path params', () => {
)

expect(
router.state.location.url.endsWith(`/posts/${encodedValue}jane%25`),
router.state.location.url.href.endsWith(
`/posts/${encodedValue}jane%25`,
),
).toBe(true)
expect(router.state.location.href).toBe(`/posts/${encodedValue}jane%25`)
expect(router.state.location.pathname).toBe(
Expand Down Expand Up @@ -441,7 +447,7 @@ describe('encoding: path params', () => {
)

expect(
router.state.location.url.endsWith(`/posts/${character}jane%25`),
router.state.location.url.href.endsWith(`/posts/${character}jane%25`),
).toBe(true)
expect(router.state.location.href).toBe(`/posts/${character}jane%25`)
expect(router.state.location.pathname).toBe(`/posts/${character}jane%25`)
Expand All @@ -455,7 +461,9 @@ describe('encoding: path params', () => {

await act(() => router.load())

expect(router.state.location.url.endsWith('/posts/%F0%9F%9A%80')).toBe(true)
expect(router.state.location.url.href.endsWith('/posts/%F0%9F%9A%80')).toBe(
true,
)
expect(router.state.location.href).toBe('/posts/%F0%9F%9A%80')
expect(router.state.location.pathname).toBe('/posts/🚀')
})
Expand All @@ -472,7 +480,7 @@ describe('encoding: path params', () => {
await act(() => router.load())

expect(
router.state.location.url.endsWith(
router.state.location.url.href.endsWith(
'/posts/framework%2Freact%2Fguide%2Ffile-based-routing%20tanstack',
),
).toBe(true)
Expand Down Expand Up @@ -619,7 +627,7 @@ describe('encoding/decoding: wildcard routes/params', () => {

await router.load()

expect(router.state.location.url.endsWith('/tanner')).toBe(true)
expect(router.state.location.url.href.endsWith('/tanner')).toBe(true)
expect(router.state.location.href).toBe('/tanner')
expect(router.state.location.pathname).toBe('/tanner')
})
Expand All @@ -631,7 +639,7 @@ describe('encoding/decoding: wildcard routes/params', () => {

await router.load()

expect(router.state.location.url.endsWith('/%F0%9F%9A%80')).toBe(true)
expect(router.state.location.url.href.endsWith('/%F0%9F%9A%80')).toBe(true)
expect(router.state.location.href).toBe('/%F0%9F%9A%80')
expect(router.state.location.pathname).toBe('/🚀')
})
Expand All @@ -649,7 +657,7 @@ describe('encoding/decoding: wildcard routes/params', () => {
await router.load()

expect(
router.state.location.url.endsWith(`/100${encodedValue}100`),
router.state.location.url.href.endsWith(`/100${encodedValue}100`),
).toBe(true)
expect(router.state.location.href).toBe(`/100${encodedValue}100`)
expect(router.state.location.pathname).toBe(`/100${encodedValue}100`)
Expand All @@ -664,7 +672,7 @@ describe('encoding/decoding: wildcard routes/params', () => {

await router.load()

expect(router.state.location.url.endsWith('/%F0%9F%9A%80')).toBe(true)
expect(router.state.location.url.href.endsWith('/%F0%9F%9A%80')).toBe(true)
expect(router.state.location.href).toBe('/%F0%9F%9A%80')
expect(router.state.location.pathname).toBe('/🚀')
})
Expand All @@ -681,7 +689,7 @@ describe('encoding/decoding: wildcard routes/params', () => {
await router.load()

expect(
router.state.location.url.endsWith(
router.state.location.url.href.endsWith(
'/framework%2Freact%2Fguide%2Ffile-based-routing%20tanstack',
),
).toBe(true)
Expand All @@ -703,7 +711,7 @@ describe('encoding/decoding: wildcard routes/params', () => {
await router.load()

expect(
router.state.location.url.endsWith(
router.state.location.url.href.endsWith(
'/framework/react/guide/file-based-routing%20tanstack',
),
).toBe(true)
Expand Down
6 changes: 3 additions & 3 deletions packages/router-core/src/location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,14 @@ export interface ParsedLocation<TSearchObj extends AnySchema = {}> {
unmaskOnReload?: boolean
/**
* @private
* @description The public href of the location, including the origin before any rewrites.
* @description The public href of the location.
* If a rewrite is applied, the `href` property will be the rewritten URL.
*/
publicHref: string
/**
* @private
* @description The full URL of the location, including the origin.
* @description The full URL of the location.
* @private
*/
url: string
url: URL
}
56 changes: 27 additions & 29 deletions packages/router-core/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1176,16 +1176,14 @@ export class RouterCore<

const fullPath = url.href.replace(url.origin, '')

const { pathname, hash } = url

return {
href: fullPath,
publicHref: href,
url: url.href,
pathname: decodePath(pathname),
url: url,
pathname: decodePath(url.pathname),
searchStr,
search: replaceEqualDeep(previousLocation?.search, parsedSearch) as any,
hash: hash.split('#').reverse()[0] ?? '',
hash: url.hash.split('#').reverse()[0] ?? '',
state: replaceEqualDeep(previousLocation?.state, state),
}
}
Expand Down Expand Up @@ -1765,7 +1763,7 @@ export class RouterCore<
publicHref:
rewrittenUrl.pathname + rewrittenUrl.search + rewrittenUrl.hash,
href: fullPath,
url: rewrittenUrl.href,
url: rewrittenUrl,
pathname: nextPathname,
search: nextSearch,
searchStr,
Expand Down Expand Up @@ -1878,8 +1876,16 @@ export class RouterCore<
if (isSameUrl && isSameState()) {
this.load()
} else {
// eslint-disable-next-line prefer-const
let { maskedLocation, hashScrollIntoView, ...nextHistory } = next
let {
// eslint-disable-next-line prefer-const
maskedLocation,
// eslint-disable-next-line prefer-const
hashScrollIntoView,
// don't pass url into history since it is a URL instance that cannot be serialized
// eslint-disable-next-line prefer-const
url: _url,
...nextHistory
} = next

if (maskedLocation) {
nextHistory = {
Expand Down Expand Up @@ -2000,7 +2006,7 @@ export class RouterCore<
if (reloadDocument) {
if (!href) {
const location = this.buildLocation({ to, ...rest } as any)
href = location.url
href = location.url.href
}

// Check blockers for external URLs unless ignoreBlocker is true
Expand Down Expand Up @@ -2056,24 +2062,11 @@ export class RouterCore<
_includeValidateSearch: true,
})

// Normalize URLs for comparison to handle encoding differences
// Browser history always stores encoded URLs while buildLocation may produce decoded URLs
const normalizeUrl = (url: string) => {
try {
return encodeURI(decodeURI(url))
} catch {
return url
}
}

if (
trimPath(normalizeUrl(this.latestLocation.href)) !==
trimPath(normalizeUrl(nextLocation.href))
this.latestLocation.publicHref !== nextLocation.publicHref ||
nextLocation.url.origin !== this.origin
) {
let href = nextLocation.url
if (this.origin && href.startsWith(this.origin)) {
href = href.replace(this.origin, '') || '/'
}
const href = this.getParsedLocationHref(nextLocation)

throw redirect({ href })
}
Expand Down Expand Up @@ -2395,13 +2388,18 @@ export class RouterCore<
return this.load({ sync: opts?.sync })
}

getParsedLocationHref = (location: ParsedLocation) => {
let href = location.url.href
if (this.origin && location.url.origin === this.origin) {
href = href.replace(this.origin, '') || '/'
}
return href
}

resolveRedirect = (redirect: AnyRedirect): AnyRedirect => {
if (!redirect.options.href) {
const location = this.buildLocation(redirect.options)
let href = location.url
if (this.origin && href.startsWith(this.origin)) {
href = href.replace(this.origin, '') || '/'
}
const href = this.getParsedLocationHref(location)
redirect.options.href = location.href
redirect.headers.set('Location', href)
}
Expand Down
4 changes: 2 additions & 2 deletions packages/solid-router/src/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,9 @@ export function useLinkProps<
let href
const maskedLocation = next().maskedLocation
if (maskedLocation) {
href = maskedLocation.url
href = maskedLocation.url.href
} else {
href = next().url
href = next().url.href
}
let external = false
if (router.origin) {
Expand Down
2 changes: 1 addition & 1 deletion packages/solid-router/tests/redirect.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ describe('redirect', () => {
expect(currentRedirect.options).toEqual({
_fromLocation: {
publicHref: '/',
url: 'http://localhost/',
url: new URL('http://localhost/'),
hash: '',
href: '/',
pathname: '/',
Expand Down
4 changes: 2 additions & 2 deletions packages/vue-router/src/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -431,9 +431,9 @@ export function useLinkProps<

let hrefValue: string
if (maskedLocation) {
hrefValue = maskedLocation.url
hrefValue = maskedLocation.url.href
} else {
hrefValue = nextLocation?.url
hrefValue = nextLocation?.url.href
}

// Handle origin stripping like Solid does
Expand Down
Loading