diff --git a/apps/app/src/plugins/1.router/__tests__/v2RouteName.spec.ts b/apps/app/src/plugins/1.router/__tests__/v2RouteName.spec.ts index ae33dab1..202261a0 100644 --- a/apps/app/src/plugins/1.router/__tests__/v2RouteName.spec.ts +++ b/apps/app/src/plugins/1.router/__tests__/v2RouteName.spec.ts @@ -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') }) diff --git a/apps/app/src/plugins/1.router/v2RouteName.ts b/apps/app/src/plugins/1.router/v2RouteName.ts index 062e0580..f1390bf7 100644 --- a/apps/app/src/plugins/1.router/v2RouteName.ts +++ b/apps/app/src/plugins/1.router/v2RouteName.ts @@ -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}` } diff --git a/apps/app/vite.config.ts b/apps/app/vite.config.ts index 9030c602..8a8a9ec3 100644 --- a/apps/app/vite.config.ts +++ b/apps/app/vite.config.ts @@ -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(),