refactor(router): make v2RouteName the single authority for the v2 name rule

Moves the `v2-` de-dup (needed because getPascalCaseRouteName folds the
v2/ URL segment into the base) into the unit-tested v2RouteName helper
and simplifies the vite.config.ts call site to v2RouteName(raw, nodePath).
Removes the duplicated isV2 detection. No behavioural change: /v2/dashboard
still resolves to route name v2-dashboard; v1 names unchanged. Addresses
the Task 3 code-review Important finding.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 09:37:07 +02:00
parent 714abd7178
commit 9d5398e0a2
3 changed files with 37 additions and 25 deletions

View File

@@ -12,11 +12,23 @@ describe('v2RouteName', () => {
expect(v2RouteName('index', 'v2/')).toBe('v2-index')
})
it('de-dups when the raw name already folded in the v2 segment', () => {
// getPascalCaseRouteName folds the /v2/ URL prefix into the base, so
// the raw name arrives as `v2-dashboard`; the result must still be a
// single `v2-dashboard`, never `v2-v2-dashboard`.
expect(v2RouteName('v2-dashboard', '/v2/dashboard')).toBe('v2-dashboard')
expect(v2RouteName('v2-events-id', '/v2/events/:id')).toBe('v2-events-id')
})
it('leaves v1 route names untouched', () => {
expect(v2RouteName('dashboard', '/dashboard')).toBe('dashboard')
expect(v2RouteName('events', '/events')).toBe('events')
})
it('does not strip a v1 name that starts with v2- when the path is not under /v2', () => {
expect(v2RouteName('v2-legacy', '/v2-legacy')).toBe('v2-legacy')
})
it('does not match a v1 path that merely starts with the letters v2', () => {
expect(v2RouteName('v2x-thing', '/v2x-thing')).toBe('v2x-thing')
})

View File

@@ -1,20 +1,30 @@
/**
* Route-NAME collision guard for the parallel /v2/* tree.
*
* unplugin-vue-router derives the route name from the file path relative
* to its routesFolder, so `src/pages/events/index.vue` and
* `src/pages-v2/events/index.vue` would BOTH yield name `events` — a
* silent runtime collision (router.push({ name: 'events' }) becomes
* ambiguous). The pages-v2 routesFolder carries `path: 'v2/'`, so every
* v2 route's URL path is under `/v2`. Prefix the NAME with `v2-` for
* those, leaving v1 names untouched. Stripped at final cutover.
* unplugin-vue-router derives the route name from the file path. With a
* second routesFolder mounting `src/pages-v2` under URL prefix `/v2/`,
* `getPascalCaseRouteName` folds that `v2` segment into the base name
* (e.g. `/v2/dashboard` → `v2-dashboard`), while a same-named v1 page
* under `src/pages` yields a bare `dashboard` — a silent runtime name
* collision. This helper is the SINGLE authority for the v2 name rule:
* for any route whose path is under `/v2`, normalise to exactly one
* canonical `v2-` prefix (stripping the prefix the pascal name already
* folded in, then re-adding it); v1 names pass through untouched.
* Reverted at final cutover.
*
* @param baseName the kebab name already computed by getRouteName
* @param rawName the kebab name computed by getRouteName
* @param routePath the node's URL path (leading slash optional)
*/
export function v2RouteName(baseName: string, routePath: string): string {
export function v2RouteName(rawName: string, routePath: string): string {
const normalized = routePath.replace(/^\//, '')
const isV2 = normalized === 'v2' || normalized.startsWith('v2/')
return isV2 ? `v2-${baseName}` : baseName
if (!isV2)
return rawName
const base = rawName.startsWith('v2-')
? rawName.slice('v2-'.length)
: rawName
return `v2-${base}`
}

View File

@@ -38,26 +38,16 @@ export default defineConfig({
// Defensive path read: unplugin-vue-router 0.8.8 TreeNode exposes
// `.fullPath`; fall back to `.value.path` then '' so a future
// plugin bump can't silently drop the v2- prefix. Step 5 below
// empirically verifies the emitted name.
// plugin bump can't silently drop the v2- prefix.
const nodePath
= (routeNode.fullPath
?? routeNode.value?.path
?? '') as string
// getPascalCaseRouteName includes the 'v2' segment from the URL
// path prefix set by routesFolder (e.g. 'v2/dashboard' → 'v2-dashboard').
// Strip that prefix before v2RouteName re-adds the canonical `v2-`
// so we get 'v2-dashboard' not 'v2-v2-dashboard'.
const isV2
= nodePath === '/v2'
|| nodePath === 'v2'
|| nodePath.startsWith('/v2/')
|| nodePath.startsWith('v2/')
const base = isV2 && raw.startsWith('v2-') ? raw.slice(3) : raw
return v2RouteName(base, nodePath)
// v2RouteName is the single authority for the /v2 name rule,
// including de-duping the `v2-` that getPascalCaseRouteName
// already folds in from the routesFolder URL prefix.
return v2RouteName(raw, nodePath)
},
}),
vue(),