WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content
Open
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
26 changes: 26 additions & 0 deletions packages/router-core/src/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,3 +351,29 @@ function encodePathParam(value: string, decodeCharMap?: Map<string, string>) {
}
return encoded
}

/**
* Checks if a pathname is within a basepath scope.
* Used to determine if a router should process a location change.
* This enables MFE architectures where multiple routers coexist.
*
* Mirrors React Router's stripBasename behavior where paths outside
* the basename are silently ignored.
*
* @param pathname - The current pathname to check
* @param basepath - The router's configured basepath
* @returns true if pathname is within basepath scope, false otherwise
*/
export function isPathInScope(pathname: string, basepath: string): boolean {
if (basepath === '/') return true

// Case-insensitive comparison (same as React Router)
if (!pathname.toLowerCase().startsWith(basepath.toLowerCase())) {
return false
}

// Ensure basepath is followed by / or end of string
// This prevents /app from matching /application
const nextChar = pathname.charAt(basepath.length)
return !nextChar || nextChar === '/'
}
23 changes: 23 additions & 0 deletions packages/router-core/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import {
cleanPath,
interpolatePath,
isPathInScope,
resolvePath,
trimPath,
trimPathRight,
Expand Down Expand Up @@ -2098,6 +2099,28 @@ export class RouterCore<
}

load: LoadFn = async (opts?: { sync?: boolean }): Promise<void> => {
// If this router has a basepath, only respond to paths within scope.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we add support for opting out of navigation, we need to check where that should happen best.
if that's here, then i would rather add an optional callback function that can execute arbitrary user code instead of adding this logic here.

// This enables MFE architectures where multiple routers coexist.
if (this.basepath && this.basepath !== '/') {
// Get the current path from history
let pathToCheck = this.history.location.pathname

// For hash history, extract path from the hash portion
const href = this.history.location.href
if (href.includes('#')) {
const hashPart = href.split('#')[1]
if (hashPart) {
// Remove query string and nested hash from path
pathToCheck = hashPart.split('?')[0]?.split('#')[0] || '/'
}
}

// If path is outside this router's scope, silently ignore
if (!isPathInScope(pathToCheck, this.basepath)) {
return
}
}

let redirect: AnyRedirect | undefined
let notFound: NotFoundError | undefined
let loadPromise: Promise<void>
Expand Down
55 changes: 55 additions & 0 deletions packages/router-core/tests/path.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import {
exactPathTest,
interpolatePath,
isPathInScope,
removeTrailingSlash,
resolvePath,
trimPathLeft,
Expand Down Expand Up @@ -85,6 +86,60 @@ describe.each([{ basepath: '/' }, { basepath: '/app' }, { basepath: '/app/' }])(
},
)

describe('isPathInScope', () => {
it('returns true for root basepath with any path', () => {
expect(isPathInScope('/anything', '/')).toBe(true)
expect(isPathInScope('/', '/')).toBe(true)
expect(isPathInScope('/deep/nested/path', '/')).toBe(true)
})

it('returns true when pathname matches basepath exactly', () => {
expect(isPathInScope('/app', '/app')).toBe(true)
expect(isPathInScope('/user-management', '/user-management')).toBe(true)
})

it('returns true when pathname starts with basepath followed by /', () => {
expect(isPathInScope('/app/page', '/app')).toBe(true)
expect(isPathInScope('/app/nested/deep', '/app')).toBe(true)
expect(isPathInScope('/user-management/users', '/user-management')).toBe(
true,
)
})

it('returns false when pathname does not start with basepath', () => {
expect(isPathInScope('/other', '/app')).toBe(false)
expect(isPathInScope('/', '/app')).toBe(false)
expect(isPathInScope('/settings', '/app')).toBe(false)
expect(isPathInScope('/home', '/user-management')).toBe(false)
})

it('returns false when basepath is prefix but not at path boundary', () => {
// /app should not match /application
expect(isPathInScope('/application', '/app')).toBe(false)
expect(isPathInScope('/apps', '/app')).toBe(false)
expect(isPathInScope('/appstore/page', '/app')).toBe(false)
})

it('handles case-insensitive comparison', () => {
expect(isPathInScope('/APP/page', '/app')).toBe(true)
expect(isPathInScope('/App/Page', '/app')).toBe(true)
expect(isPathInScope('/app/page', '/APP')).toBe(true)
expect(isPathInScope('/USER-MANAGEMENT/users', '/user-management')).toBe(
true,
)
})

it('handles trailing slashes correctly', () => {
expect(isPathInScope('/app/', '/app')).toBe(true)
expect(isPathInScope('/app/page/', '/app')).toBe(true)
})

it('handles edge cases', () => {
expect(isPathInScope('', '/')).toBe(true)
expect(isPathInScope('/', '/')).toBe(true)
})
})

describe('resolvePath', () => {
describe.each([
['/', '/', '/'],
Expand Down