From 314683436f37292895486314c6f1c1b52d3cab95 Mon Sep 17 00:00:00 2001 From: Anoesj Date: Wed, 13 Aug 2025 14:16:10 +0200 Subject: [PATCH 1/5] feat: output `routes` and `views` in `_RouteFileInfoMap` over multiple lines --- src/codegen/generateRouteFileInfoMap.spec.ts | 44 ++++++++++++++------ src/codegen/generateRouteFileInfoMap.ts | 4 +- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/codegen/generateRouteFileInfoMap.spec.ts b/src/codegen/generateRouteFileInfoMap.spec.ts index 7e49ae055..1d5c612ac 100644 --- a/src/codegen/generateRouteFileInfoMap.spec.ts +++ b/src/codegen/generateRouteFileInfoMap.spec.ts @@ -23,19 +23,23 @@ describe('generateRouteFileInfoMap', () => { .toMatchInlineSnapshot(` "export interface _RouteFileInfoMap { 'src/pages/index.vue': { - routes: '/' + routes: + | '/' views: never } 'src/pages/a.vue': { - routes: '/a' + routes: + | '/a' views: never } 'src/pages/b.vue': { - routes: '/b' + routes: + | '/b' views: never } 'src/pages/c.vue': { - routes: '/c' + routes: + | '/c' views: never } }" @@ -50,11 +54,15 @@ describe('generateRouteFileInfoMap', () => { .toMatchInlineSnapshot(` "export interface _RouteFileInfoMap { 'src/pages/parent.vue': { - routes: '/parent' | '/parent/child' - views: 'default' + routes: + | '/parent' + | '/parent/child' + views: + | 'default' } 'src/pages/parent/child.vue': { - routes: '/parent/child' + routes: + | '/parent/child' views: never } }" @@ -70,15 +78,21 @@ describe('generateRouteFileInfoMap', () => { .toMatchInlineSnapshot(` "export interface _RouteFileInfoMap { 'src/pages/parent.vue': { - routes: '/parent' | '/parent/child' - views: 'default' | 'test' + routes: + | '/parent' + | '/parent/child' + views: + | 'default' + | 'test' } 'src/pages/parent/child.vue': { - routes: '/parent/child' + routes: + | '/parent/child' views: never } 'src/pages/parent/child@test.vue': { - routes: '/parent/child' + routes: + | '/parent/child' views: never } }" @@ -98,11 +112,15 @@ describe('generateRouteFileInfoMap', () => { .toMatchInlineSnapshot(` "export interface _RouteFileInfoMap { 'index.vue': { - routes: '/' | '/home' + routes: + | '/' + | '/home' views: never } 'nested/index.vue': { - routes: '/nested/path' | '/unnested' + routes: + | '/nested/path' + | '/unnested' views: never } }" diff --git a/src/codegen/generateRouteFileInfoMap.ts b/src/codegen/generateRouteFileInfoMap.ts index bb9de4204..66a3c1c06 100644 --- a/src/codegen/generateRouteFileInfoMap.ts +++ b/src/codegen/generateRouteFileInfoMap.ts @@ -42,8 +42,8 @@ export function generateRouteFileInfoMap( ([file, { routes, views }]) => ` '${file}': { - routes: ${routes.map((name) => `'${name}'`).join(' | ')} - views: ${views.length > 0 ? views.map((view) => `'${view}'`).join(' | ') : 'never'} + routes:${routes.map((name) => `\n | '${name}'`).join('\n')} + views:${views.length > 0 ? views.map((view) => `\n | '${view}'`).join('\n') : ' never'} }` ) .join('\n') From 084193966be3c81c3cd763b1c3352d74196d257a Mon Sep 17 00:00:00 2001 From: Anoesj Date: Wed, 13 Aug 2025 17:43:33 +0200 Subject: [PATCH 2/5] feat: output `RouteRecordInfo` over multiple lines --- docs/.vitepress/twoslash-files.ts | 2 +- docs/guide/typescript.md | 2 +- playground/typed-router.d.ts | 690 ++++++++++++++++++++++----- src/codegen/generateRouteMap.spec.ts | 540 ++++++++++++++++++--- src/codegen/generateRouteMap.ts | 10 +- 5 files changed, 1045 insertions(+), 199 deletions(-) diff --git a/docs/.vitepress/twoslash-files.ts b/docs/.vitepress/twoslash-files.ts index b8ed18561..73c8e2974 100644 --- a/docs/.vitepress/twoslash-files.ts +++ b/docs/.vitepress/twoslash-files.ts @@ -31,7 +31,7 @@ declare module 'vue-router/auto-routes' { '/users/:id', { id: ParamValue }, { id: ParamValue }, - '/users/[id]/edit' + | '/users/[id]/edit' > '/users/[id]/edit': RouteRecordInfo< '/users/[id]/edit', diff --git a/docs/guide/typescript.md b/docs/guide/typescript.md index a33dee239..1b11c5086 100644 --- a/docs/guide/typescript.md +++ b/docs/guide/typescript.md @@ -63,7 +63,7 @@ declare module 'vue-router/auto-routes' { { path: ParamValue }, // this is a union of all children route names // if the route does not have nested routes, pass `never` or omit this generic entirely - 'custom-dynamic-child-name' + | 'custom-dynamic-child-name' > 'custom-dynamic-child-name': RouteRecordInfo< 'custom-dynamic-child-name', diff --git a/playground/typed-router.d.ts b/playground/typed-router.d.ts index c4b6e56f2..6ac50eb17 100644 --- a/playground/typed-router.d.ts +++ b/playground/typed-router.d.ts @@ -18,66 +18,426 @@ declare module 'vue-router/auto-routes' { * Route name map generated by unplugin-vue-router */ export interface RouteNamedMap { - '/(some-layout)/app': RouteRecordInfo<'/(some-layout)/app', '/app', Record, Record>, - '/(some-layout)/home': RouteRecordInfo<'/(some-layout)/home', '/home', Record, Record>, - '/(test-group)': RouteRecordInfo<'/(test-group)', '/', Record, Record, '/(test-group)/test-group-child'>, - '/(test-group)/test-group-child': RouteRecordInfo<'/(test-group)/test-group-child', '/test-group-child', Record, Record>, - 'home': RouteRecordInfo<'home', '/', Record, Record>, - '/[name]': RouteRecordInfo<'/[name]', '/:name', { name: ParamValue }, { name: ParamValue }>, - '/[...path]': RouteRecordInfo<'/[...path]', '/:path(.*)', { path: ParamValue }, { path: ParamValue }>, - '/[...path]+': RouteRecordInfo<'/[...path]+', '/:path(.*)+', { path: ParamValueOneOrMore }, { path: ParamValueOneOrMore }>, - '/@[profileId]': RouteRecordInfo<'/@[profileId]', '/@:profileId', { profileId: ParamValue }, { profileId: ParamValue }>, - '/about': RouteRecordInfo<'/about', '/about', Record, Record>, - '/about.extra.nested': RouteRecordInfo<'/about.extra.nested', '/about/extra/nested', Record, Record>, - '/articles/': RouteRecordInfo<'/articles/', '/articles', Record, Record>, - '/articles/[id]': RouteRecordInfo<'/articles/[id]', '/articles/:id', { id: ParamValue }, { id: ParamValue }>, - '/articles/[id]+': RouteRecordInfo<'/articles/[id]+', '/articles/:id+', { id: ParamValueOneOrMore }, { id: ParamValueOneOrMore }>, - '/custom-definePage': RouteRecordInfo<'/custom-definePage', '/custom-definePage', Record, Record>, - 'a rebel': RouteRecordInfo<'a rebel', '/custom-name', Record, Record>, - '/custom/page': RouteRecordInfo<'/custom/page', '/custom/page', Record, Record>, - '/deep/nesting/works/[[files]]+': RouteRecordInfo<'/deep/nesting/works/[[files]]+', '/deep/nesting/works/:files*', { files?: ParamValueZeroOrMore }, { files?: ParamValueZeroOrMore }>, - '/deep/nesting/works/at-root-but-from-nested': RouteRecordInfo<'/deep/nesting/works/at-root-but-from-nested', '/at-root-but-from-nested', Record, Record>, - 'deep the most rebel': RouteRecordInfo<'deep the most rebel', '/deep-most-rebel', Record, Record>, - '/deep/nesting/works/custom-path': RouteRecordInfo<'/deep/nesting/works/custom-path', '/deep-surprise-:id(\d+)', Record, Record>, - 'deep a rebel': RouteRecordInfo<'deep a rebel', '/deep/nesting/works/custom-name', Record, Record>, - '/docs/[lang]/real/': RouteRecordInfo<'/docs/[lang]/real/', '/docs/:lang/real', { lang: ParamValue }, { lang: ParamValue }>, - '/feature-1/': RouteRecordInfo<'/feature-1/', '/feature-1', Record, Record>, - '/feature-1/about': RouteRecordInfo<'/feature-1/about', '/feature-1/about', Record, Record>, - '/feature-2/': RouteRecordInfo<'/feature-2/', '/feature-2', Record, Record>, - '/feature-2/about': RouteRecordInfo<'/feature-2/about', '/feature-2/about', Record, Record>, - '/feature-3/': RouteRecordInfo<'/feature-3/', '/feature-3', Record, Record>, - '/feature-3/about': RouteRecordInfo<'/feature-3/about', '/feature-3/about', Record, Record>, - '/file(ignored-parentheses)': RouteRecordInfo<'/file(ignored-parentheses)', '/file(ignored-parentheses)', Record, Record>, - '/from-root': RouteRecordInfo<'/from-root', '/from-root', Record, Record>, - '/group/(thing)': RouteRecordInfo<'/group/(thing)', '/group', Record, Record>, - 'the most rebel': RouteRecordInfo<'the most rebel', '/most-rebel', Record, Record>, - '/multiple-[a]-[b]-params': RouteRecordInfo<'/multiple-[a]-[b]-params', '/multiple-:a-:b-params', { a: ParamValue, b: ParamValue }, { a: ParamValue, b: ParamValue }>, - '/my-optional-[[slug]]': RouteRecordInfo<'/my-optional-[[slug]]', '/my-optional-:slug?', { slug?: ParamValueZeroOrOne }, { slug?: ParamValueZeroOrOne }>, - '/n-[[n]]/': RouteRecordInfo<'/n-[[n]]/', '/n-:n?', { n?: ParamValueZeroOrOne }, { n?: ParamValueZeroOrOne }>, - '/n-[[n]]/[[more]]+/': RouteRecordInfo<'/n-[[n]]/[[more]]+/', '/n-:n?/:more*', { n?: ParamValueZeroOrOne, more?: ParamValueZeroOrMore }, { n?: ParamValueZeroOrOne, more?: ParamValueZeroOrMore }>, - '/n-[[n]]/[[more]]+/[final]': RouteRecordInfo<'/n-[[n]]/[[more]]+/[final]', '/n-:n?/:more*/:final', { n?: ParamValueZeroOrOne, more?: ParamValueZeroOrMore, final: ParamValue }, { n?: ParamValueZeroOrOne, more?: ParamValueZeroOrMore, final: ParamValue }>, - '/named-views/parent': RouteRecordInfo<'/named-views/parent', '/named-views/parent', Record, Record, '/named-views/parent/'>, - '/named-views/parent/': RouteRecordInfo<'/named-views/parent/', '/named-views/parent', Record, Record>, - '/nested-group/(group)': RouteRecordInfo<'/nested-group/(group)', '/nested-group', Record, Record>, - '/nested-group/(nested-group-first-level)/(nested-group-deep)/nested-group-deep-child': RouteRecordInfo<'/nested-group/(nested-group-first-level)/(nested-group-deep)/nested-group-deep-child', '/nested-group/nested-group-deep-child', Record, Record>, - '/nested-group/(nested-group-first-level)/nested-group-first-level-child': RouteRecordInfo<'/nested-group/(nested-group-first-level)/nested-group-first-level-child', '/nested-group/nested-group-first-level-child', Record, Record>, - '/partial-[name]': RouteRecordInfo<'/partial-[name]', '/partial-:name', { name: ParamValue }, { name: ParamValue }>, - '/custom-path': RouteRecordInfo<'/custom-path', '/surprise-:id(\d+)', Record, Record>, - '/test-[a-id]': RouteRecordInfo<'/test-[a-id]', '/test-:a-id', { aId: ParamValue }, { aId: ParamValue }>, - '/todos/': RouteRecordInfo<'/todos/', '/todos', Record, Record>, - '/todos/+layout': RouteRecordInfo<'/todos/+layout', '/todos/+layout', Record, Record>, - '/users/': RouteRecordInfo<'/users/', '/users', Record, Record>, - '/users/[id]': RouteRecordInfo<'/users/[id]', '/users/:id', { id: ParamValue }, { id: ParamValue }>, - '/users/[id].edit': RouteRecordInfo<'/users/[id].edit', '/users/:id/edit', { id: ParamValue }, { id: ParamValue }>, - '/users/colada-loader.[id]': RouteRecordInfo<'/users/colada-loader.[id]', '/users/colada-loader/:id', { id: ParamValue }, { id: ParamValue }>, - '/users/nested.route.deep': RouteRecordInfo<'/users/nested.route.deep', '/users/nested/route/deep', Record, Record>, - '/users/pinia-colada.[id]': RouteRecordInfo<'/users/pinia-colada.[id]', '/users/pinia-colada/:id', { id: ParamValue }, { id: ParamValue }>, - '/users/query.[id]': RouteRecordInfo<'/users/query.[id]', '/users/query/:id', { id: ParamValue }, { id: ParamValue }>, - '/users/tq-infinite-query': RouteRecordInfo<'/users/tq-infinite-query', '/users/tq-infinite-query', Record, Record>, - '/users/tq-query-bug': RouteRecordInfo<'/users/tq-query-bug', '/users/tq-query-bug', Record, Record>, - '/users/tq-query.[id]': RouteRecordInfo<'/users/tq-query.[id]', '/users/tq-query/:id', { id: ParamValue }, { id: ParamValue }>, - '/vuefire-tests/get-doc': RouteRecordInfo<'/vuefire-tests/get-doc', '/vuefire-tests/get-doc', Record, Record>, - '/with-extension': RouteRecordInfo<'/with-extension', '/with-extension', Record, Record>, + '/(some-layout)/app': RouteRecordInfo< + '/(some-layout)/app', + '/app', + Record, + Record, + never + >, + '/(some-layout)/home': RouteRecordInfo< + '/(some-layout)/home', + '/home', + Record, + Record, + never + >, + '/(test-group)': RouteRecordInfo< + '/(test-group)', + '/', + Record, + Record, + | '/(test-group)/test-group-child' + >, + '/(test-group)/test-group-child': RouteRecordInfo< + '/(test-group)/test-group-child', + '/test-group-child', + Record, + Record, + never + >, + 'home': RouteRecordInfo< + 'home', + '/', + Record, + Record, + never + >, + '/[name]': RouteRecordInfo< + '/[name]', + '/:name', + { name: ParamValue }, + { name: ParamValue }, + never + >, + '/[...path]': RouteRecordInfo< + '/[...path]', + '/:path(.*)', + { path: ParamValue }, + { path: ParamValue }, + never + >, + '/[...path]+': RouteRecordInfo< + '/[...path]+', + '/:path(.*)+', + { path: ParamValueOneOrMore }, + { path: ParamValueOneOrMore }, + never + >, + '/@[profileId]': RouteRecordInfo< + '/@[profileId]', + '/@:profileId', + { profileId: ParamValue }, + { profileId: ParamValue }, + never + >, + '/about': RouteRecordInfo< + '/about', + '/about', + Record, + Record, + never + >, + '/about.extra.nested': RouteRecordInfo< + '/about.extra.nested', + '/about/extra/nested', + Record, + Record, + never + >, + '/articles/': RouteRecordInfo< + '/articles/', + '/articles', + Record, + Record, + never + >, + '/articles/[id]': RouteRecordInfo< + '/articles/[id]', + '/articles/:id', + { id: ParamValue }, + { id: ParamValue }, + never + >, + '/articles/[id]+': RouteRecordInfo< + '/articles/[id]+', + '/articles/:id+', + { id: ParamValueOneOrMore }, + { id: ParamValueOneOrMore }, + never + >, + '/custom-definePage': RouteRecordInfo< + '/custom-definePage', + '/custom-definePage', + Record, + Record, + never + >, + 'a rebel': RouteRecordInfo< + 'a rebel', + '/custom-name', + Record, + Record, + never + >, + '/custom/page': RouteRecordInfo< + '/custom/page', + '/custom/page', + Record, + Record, + never + >, + '/deep/nesting/works/[[files]]+': RouteRecordInfo< + '/deep/nesting/works/[[files]]+', + '/deep/nesting/works/:files*', + { files?: ParamValueZeroOrMore }, + { files?: ParamValueZeroOrMore }, + never + >, + '/deep/nesting/works/at-root-but-from-nested': RouteRecordInfo< + '/deep/nesting/works/at-root-but-from-nested', + '/at-root-but-from-nested', + Record, + Record, + never + >, + 'deep the most rebel': RouteRecordInfo< + 'deep the most rebel', + '/deep-most-rebel', + Record, + Record, + never + >, + '/deep/nesting/works/custom-path': RouteRecordInfo< + '/deep/nesting/works/custom-path', + '/deep-surprise-:id(\d+)', + Record, + Record, + never + >, + 'deep a rebel': RouteRecordInfo< + 'deep a rebel', + '/deep/nesting/works/custom-name', + Record, + Record, + never + >, + '/docs/[lang]/real/': RouteRecordInfo< + '/docs/[lang]/real/', + '/docs/:lang/real', + { lang: ParamValue }, + { lang: ParamValue }, + never + >, + '/feature-1/': RouteRecordInfo< + '/feature-1/', + '/feature-1', + Record, + Record, + never + >, + '/feature-1/about': RouteRecordInfo< + '/feature-1/about', + '/feature-1/about', + Record, + Record, + never + >, + '/feature-2/': RouteRecordInfo< + '/feature-2/', + '/feature-2', + Record, + Record, + never + >, + '/feature-2/about': RouteRecordInfo< + '/feature-2/about', + '/feature-2/about', + Record, + Record, + never + >, + '/feature-3/': RouteRecordInfo< + '/feature-3/', + '/feature-3', + Record, + Record, + never + >, + '/feature-3/about': RouteRecordInfo< + '/feature-3/about', + '/feature-3/about', + Record, + Record, + never + >, + '/file(ignored-parentheses)': RouteRecordInfo< + '/file(ignored-parentheses)', + '/file(ignored-parentheses)', + Record, + Record, + never + >, + '/from-root': RouteRecordInfo< + '/from-root', + '/from-root', + Record, + Record, + never + >, + '/group/(thing)': RouteRecordInfo< + '/group/(thing)', + '/group', + Record, + Record, + never + >, + 'the most rebel': RouteRecordInfo< + 'the most rebel', + '/most-rebel', + Record, + Record, + never + >, + '/multiple-[a]-[b]-params': RouteRecordInfo< + '/multiple-[a]-[b]-params', + '/multiple-:a-:b-params', + { a: ParamValue, b: ParamValue }, + { a: ParamValue, b: ParamValue }, + never + >, + '/my-optional-[[slug]]': RouteRecordInfo< + '/my-optional-[[slug]]', + '/my-optional-:slug?', + { slug?: ParamValueZeroOrOne }, + { slug?: ParamValueZeroOrOne }, + never + >, + '/n-[[n]]/': RouteRecordInfo< + '/n-[[n]]/', + '/n-:n?', + { n?: ParamValueZeroOrOne }, + { n?: ParamValueZeroOrOne }, + never + >, + '/n-[[n]]/[[more]]+/': RouteRecordInfo< + '/n-[[n]]/[[more]]+/', + '/n-:n?/:more*', + { n?: ParamValueZeroOrOne, more?: ParamValueZeroOrMore }, + { n?: ParamValueZeroOrOne, more?: ParamValueZeroOrMore }, + never + >, + '/n-[[n]]/[[more]]+/[final]': RouteRecordInfo< + '/n-[[n]]/[[more]]+/[final]', + '/n-:n?/:more*/:final', + { n?: ParamValueZeroOrOne, more?: ParamValueZeroOrMore, final: ParamValue }, + { n?: ParamValueZeroOrOne, more?: ParamValueZeroOrMore, final: ParamValue }, + never + >, + '/named-views/parent': RouteRecordInfo< + '/named-views/parent', + '/named-views/parent', + Record, + Record, + | '/named-views/parent/' + >, + '/named-views/parent/': RouteRecordInfo< + '/named-views/parent/', + '/named-views/parent', + Record, + Record, + never + >, + '/nested-group/(group)': RouteRecordInfo< + '/nested-group/(group)', + '/nested-group', + Record, + Record, + never + >, + '/nested-group/(nested-group-first-level)/(nested-group-deep)/nested-group-deep-child': RouteRecordInfo< + '/nested-group/(nested-group-first-level)/(nested-group-deep)/nested-group-deep-child', + '/nested-group/nested-group-deep-child', + Record, + Record, + never + >, + '/nested-group/(nested-group-first-level)/nested-group-first-level-child': RouteRecordInfo< + '/nested-group/(nested-group-first-level)/nested-group-first-level-child', + '/nested-group/nested-group-first-level-child', + Record, + Record, + never + >, + '/partial-[name]': RouteRecordInfo< + '/partial-[name]', + '/partial-:name', + { name: ParamValue }, + { name: ParamValue }, + never + >, + '/custom-path': RouteRecordInfo< + '/custom-path', + '/surprise-:id(\d+)', + Record, + Record, + never + >, + '/test-[a-id]': RouteRecordInfo< + '/test-[a-id]', + '/test-:a-id', + { aId: ParamValue }, + { aId: ParamValue }, + never + >, + '/todos/': RouteRecordInfo< + '/todos/', + '/todos', + Record, + Record, + never + >, + '/todos/+layout': RouteRecordInfo< + '/todos/+layout', + '/todos/+layout', + Record, + Record, + never + >, + '/users/': RouteRecordInfo< + '/users/', + '/users', + Record, + Record, + never + >, + '/users/[id]': RouteRecordInfo< + '/users/[id]', + '/users/:id', + { id: ParamValue }, + { id: ParamValue }, + never + >, + '/users/[id].edit': RouteRecordInfo< + '/users/[id].edit', + '/users/:id/edit', + { id: ParamValue }, + { id: ParamValue }, + never + >, + '/users/colada-loader.[id]': RouteRecordInfo< + '/users/colada-loader.[id]', + '/users/colada-loader/:id', + { id: ParamValue }, + { id: ParamValue }, + never + >, + '/users/nested.route.deep': RouteRecordInfo< + '/users/nested.route.deep', + '/users/nested/route/deep', + Record, + Record, + never + >, + '/users/pinia-colada.[id]': RouteRecordInfo< + '/users/pinia-colada.[id]', + '/users/pinia-colada/:id', + { id: ParamValue }, + { id: ParamValue }, + never + >, + '/users/query.[id]': RouteRecordInfo< + '/users/query.[id]', + '/users/query/:id', + { id: ParamValue }, + { id: ParamValue }, + never + >, + '/users/tq-infinite-query': RouteRecordInfo< + '/users/tq-infinite-query', + '/users/tq-infinite-query', + Record, + Record, + never + >, + '/users/tq-query-bug': RouteRecordInfo< + '/users/tq-query-bug', + '/users/tq-query-bug', + Record, + Record, + never + >, + '/users/tq-query.[id]': RouteRecordInfo< + '/users/tq-query.[id]', + '/users/tq-query/:id', + { id: ParamValue }, + { id: ParamValue }, + never + >, + '/vuefire-tests/get-doc': RouteRecordInfo< + '/vuefire-tests/get-doc', + '/vuefire-tests/get-doc', + Record, + Record, + never + >, + '/with-extension': RouteRecordInfo< + '/with-extension', + '/with-extension', + Record, + Record, + never + >, } /** @@ -92,255 +452,331 @@ declare module 'vue-router/auto-routes' { */ export interface _RouteFileInfoMap { 'src/pages/(some-layout).vue': { - routes: '/(some-layout)/app' | '/(some-layout)/home' - views: 'default' + routes: + | '/(some-layout)/app' + | '/(some-layout)/home' + views: + | 'default' } 'src/pages/(some-layout)/app.vue': { - routes: '/(some-layout)/app' + routes: + | '/(some-layout)/app' views: never } 'src/pages/(some-layout)/home.vue': { - routes: '/(some-layout)/home' + routes: + | '/(some-layout)/home' views: never } 'src/pages/(test-group).vue': { - routes: '/(test-group)' | '/(test-group)/test-group-child' - views: 'default' + routes: + | '/(test-group)' + | '/(test-group)/test-group-child' + views: + | 'default' } 'src/pages/(test-group)/test-group-child.vue': { - routes: '/(test-group)/test-group-child' + routes: + | '/(test-group)/test-group-child' views: never } 'src/pages/index.vue': { - routes: 'home' | '/from-root' + routes: + | 'home' + | '/from-root' views: never } 'src/pages/index@named.vue': { - routes: 'home' + routes: + | 'home' views: never } 'src/pages/[name].vue': { - routes: '/[name]' + routes: + | '/[name]' views: never } 'src/pages/[...path].vue': { - routes: '/[...path]' + routes: + | '/[...path]' views: never } 'src/pages/[...path]+.vue': { - routes: '/[...path]+' + routes: + | '/[...path]+' views: never } 'src/pages/@[profileId].vue': { - routes: '/@[profileId]' + routes: + | '/@[profileId]' views: never } 'src/pages/about.vue': { - routes: '/about' + routes: + | '/about' views: never } 'src/pages/about.extra.nested.vue': { - routes: '/about.extra.nested' + routes: + | '/about.extra.nested' views: never } 'src/pages/articles.vue': { - routes: '/articles/' | '/articles/[id]' | '/articles/[id]+' - views: 'default' + routes: + | '/articles/' + | '/articles/[id]' + | '/articles/[id]+' + views: + | 'default' } 'src/pages/articles/index.vue': { - routes: '/articles/' + routes: + | '/articles/' views: never } 'src/pages/articles/[id].vue': { - routes: '/articles/[id]' + routes: + | '/articles/[id]' views: never } 'src/pages/articles/[id]+.vue': { - routes: '/articles/[id]+' + routes: + | '/articles/[id]+' views: never } 'src/pages/custom-definePage.vue': { - routes: '/custom-definePage' + routes: + | '/custom-definePage' views: never } 'src/pages/custom-name.vue': { - routes: 'a rebel' + routes: + | 'a rebel' views: never } 'src/pages/deep/nesting/works/too.vue': { - routes: '/custom/page' | '/deep/nesting/works/at-root-but-from-nested' + routes: + | '/custom/page' + | '/deep/nesting/works/at-root-but-from-nested' views: never } 'src/pages/deep/nesting/works/[[files]]+.vue': { - routes: '/deep/nesting/works/[[files]]+' + routes: + | '/deep/nesting/works/[[files]]+' views: never } 'src/pages/deep/nesting/works/custom-name-and-path.vue': { - routes: 'deep the most rebel' + routes: + | 'deep the most rebel' views: never } 'src/pages/deep/nesting/works/custom-path.vue': { - routes: '/deep/nesting/works/custom-path' + routes: + | '/deep/nesting/works/custom-path' views: never } 'src/pages/deep/nesting/works/custom-name.vue': { - routes: 'deep a rebel' + routes: + | 'deep a rebel' views: never } 'src/docs/real/index.md': { - routes: '/docs/[lang]/real/' + routes: + | '/docs/[lang]/real/' views: never } 'src/features/feature-1/pages/index.vue': { - routes: '/feature-1/' + routes: + | '/feature-1/' views: never } 'src/features/feature-1/pages/about.vue': { - routes: '/feature-1/about' + routes: + | '/feature-1/about' views: never } 'src/features/feature-2/pages/index.vue': { - routes: '/feature-2/' + routes: + | '/feature-2/' views: never } 'src/features/feature-2/pages/about.vue': { - routes: '/feature-2/about' + routes: + | '/feature-2/about' views: never } 'src/features/feature-3/pages/index.vue': { - routes: '/feature-3/' + routes: + | '/feature-3/' views: never } 'src/features/feature-3/pages/about.vue': { - routes: '/feature-3/about' + routes: + | '/feature-3/about' views: never } 'src/pages/file(ignored-parentheses).vue': { - routes: '/file(ignored-parentheses)' + routes: + | '/file(ignored-parentheses)' views: never } 'src/pages/group/(thing).vue': { - routes: '/group/(thing)' + routes: + | '/group/(thing)' views: never } 'src/pages/custom-name-and-path.vue': { - routes: 'the most rebel' + routes: + | 'the most rebel' views: never } 'src/pages/multiple-[a]-[b]-params.vue': { - routes: '/multiple-[a]-[b]-params' + routes: + | '/multiple-[a]-[b]-params' views: never } 'src/pages/my-optional-[[slug]].vue': { - routes: '/my-optional-[[slug]]' + routes: + | '/my-optional-[[slug]]' views: never } 'src/pages/n-[[n]]/index.vue': { - routes: '/n-[[n]]/' + routes: + | '/n-[[n]]/' views: never } 'src/pages/n-[[n]]/[[more]]+/index.vue': { - routes: '/n-[[n]]/[[more]]+/' + routes: + | '/n-[[n]]/[[more]]+/' views: never } 'src/pages/n-[[n]]/[[more]]+/[final].vue': { - routes: '/n-[[n]]/[[more]]+/[final]' + routes: + | '/n-[[n]]/[[more]]+/[final]' views: never } 'src/pages/named-views/parent.vue': { - routes: '/named-views/parent' | '/named-views/parent/' - views: 'default' | 'a' | 'b' + routes: + | '/named-views/parent' + | '/named-views/parent/' + views: + | 'default' + | 'a' + | 'b' } 'src/pages/named-views/parent/index.vue': { - routes: '/named-views/parent/' + routes: + | '/named-views/parent/' views: never } 'src/pages/named-views/parent/index@a.vue': { - routes: '/named-views/parent/' + routes: + | '/named-views/parent/' views: never } 'src/pages/named-views/parent/index@b.vue': { - routes: '/named-views/parent/' + routes: + | '/named-views/parent/' views: never } 'src/pages/nested-group/(group).vue': { - routes: '/nested-group/(group)' + routes: + | '/nested-group/(group)' views: never } 'src/pages/nested-group/(nested-group-first-level)/(nested-group-deep)/nested-group-deep-child.vue': { - routes: '/nested-group/(nested-group-first-level)/(nested-group-deep)/nested-group-deep-child' + routes: + | '/nested-group/(nested-group-first-level)/(nested-group-deep)/nested-group-deep-child' views: never } 'src/pages/nested-group/(nested-group-first-level)/nested-group-first-level-child.vue': { - routes: '/nested-group/(nested-group-first-level)/nested-group-first-level-child' + routes: + | '/nested-group/(nested-group-first-level)/nested-group-first-level-child' views: never } 'src/pages/partial-[name].vue': { - routes: '/partial-[name]' + routes: + | '/partial-[name]' views: never } 'src/pages/custom-path.vue': { - routes: '/custom-path' + routes: + | '/custom-path' views: never } 'src/pages/test-[a-id].vue': { - routes: '/test-[a-id]' + routes: + | '/test-[a-id]' views: never } 'src/pages/todos/index.vue': { - routes: '/todos/' + routes: + | '/todos/' views: never } 'src/pages/todos/+layout.vue': { - routes: '/todos/+layout' + routes: + | '/todos/+layout' views: never } 'src/pages/users/index.vue': { - routes: '/users/' + routes: + | '/users/' views: never } 'src/pages/users/[id].vue': { - routes: '/users/[id]' + routes: + | '/users/[id]' views: never } 'src/pages/users/[id].edit.vue': { - routes: '/users/[id].edit' + routes: + | '/users/[id].edit' views: never } 'src/pages/users/colada-loader.[id].vue': { - routes: '/users/colada-loader.[id]' + routes: + | '/users/colada-loader.[id]' views: never } 'src/pages/users/nested.route.deep.vue': { - routes: '/users/nested.route.deep' + routes: + | '/users/nested.route.deep' views: never } 'src/pages/users/pinia-colada.[id].vue': { - routes: '/users/pinia-colada.[id]' + routes: + | '/users/pinia-colada.[id]' views: never } 'src/pages/users/query.[id].vue': { - routes: '/users/query.[id]' + routes: + | '/users/query.[id]' views: never } 'src/pages/users/tq-infinite-query.vue': { - routes: '/users/tq-infinite-query' + routes: + | '/users/tq-infinite-query' views: never } 'src/pages/users/tq-query-bug.vue': { - routes: '/users/tq-query-bug' + routes: + | '/users/tq-query-bug' views: never } 'src/pages/users/tq-query.[id].vue': { - routes: '/users/tq-query.[id]' + routes: + | '/users/tq-query.[id]' views: never } 'src/pages/vuefire-tests/get-doc.vue': { - routes: '/vuefire-tests/get-doc' + routes: + | '/vuefire-tests/get-doc' views: never } 'src/pages/with-extension.page.vue': { - routes: '/with-extension' + routes: + | '/with-extension' views: never } } diff --git a/src/codegen/generateRouteMap.spec.ts b/src/codegen/generateRouteMap.spec.ts index 99f517615..e739362a4 100644 --- a/src/codegen/generateRouteMap.spec.ts +++ b/src/codegen/generateRouteMap.spec.ts @@ -21,10 +21,34 @@ describe('generateRouteNamedMap', () => { tree.insert('c', 'c.vue') expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { - '/': RouteRecordInfo<'/', '/', Record, Record>, - '/a': RouteRecordInfo<'/a', '/a', Record, Record>, - '/b': RouteRecordInfo<'/b', '/b', Record, Record>, - '/c': RouteRecordInfo<'/c', '/c', Record, Record>, + '/': RouteRecordInfo< + '/', + '/', + Record, + Record, + never + >, + '/a': RouteRecordInfo< + '/a', + '/a', + Record, + Record, + never + >, + '/b': RouteRecordInfo< + '/b', + '/b', + Record, + Record, + never + >, + '/c': RouteRecordInfo< + '/c', + '/c', + Record, + Record, + never + >, }" `) }) @@ -42,15 +66,69 @@ describe('generateRouteNamedMap', () => { tree.insert('[[...a]]+', '[[...a]]+.vue') // splat expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { - '/[a]': RouteRecordInfo<'/[a]', '/:a', { a: ParamValue }, { a: ParamValue }>, - '/[[a]]': RouteRecordInfo<'/[[a]]', '/:a?', { a?: ParamValueZeroOrOne }, { a?: ParamValueZeroOrOne }>, - '/[...a]': RouteRecordInfo<'/[...a]', '/:a(.*)', { a: ParamValue }, { a: ParamValue }>, - '/[[...a]]': RouteRecordInfo<'/[[...a]]', '/:a(.*)?', { a?: ParamValueZeroOrOne }, { a?: ParamValueZeroOrOne }>, - '/[[...a]]+': RouteRecordInfo<'/[[...a]]+', '/:a(.*)*', { a?: ParamValueZeroOrMore }, { a?: ParamValueZeroOrMore }>, - '/[[a]]+': RouteRecordInfo<'/[[a]]+', '/:a*', { a?: ParamValueZeroOrMore }, { a?: ParamValueZeroOrMore }>, - '/[a]+': RouteRecordInfo<'/[a]+', '/:a+', { a: ParamValueOneOrMore }, { a: ParamValueOneOrMore }>, - '/partial-[a]': RouteRecordInfo<'/partial-[a]', '/partial-:a', { a: ParamValue }, { a: ParamValue }>, - '/partial-[[a]]': RouteRecordInfo<'/partial-[[a]]', '/partial-:a?', { a?: ParamValueZeroOrOne }, { a?: ParamValueZeroOrOne }>, + '/[a]': RouteRecordInfo< + '/[a]', + '/:a', + { a: ParamValue }, + { a: ParamValue }, + never + >, + '/[[a]]': RouteRecordInfo< + '/[[a]]', + '/:a?', + { a?: ParamValueZeroOrOne }, + { a?: ParamValueZeroOrOne }, + never + >, + '/[...a]': RouteRecordInfo< + '/[...a]', + '/:a(.*)', + { a: ParamValue }, + { a: ParamValue }, + never + >, + '/[[...a]]': RouteRecordInfo< + '/[[...a]]', + '/:a(.*)?', + { a?: ParamValueZeroOrOne }, + { a?: ParamValueZeroOrOne }, + never + >, + '/[[...a]]+': RouteRecordInfo< + '/[[...a]]+', + '/:a(.*)*', + { a?: ParamValueZeroOrMore }, + { a?: ParamValueZeroOrMore }, + never + >, + '/[[a]]+': RouteRecordInfo< + '/[[a]]+', + '/:a*', + { a?: ParamValueZeroOrMore }, + { a?: ParamValueZeroOrMore }, + never + >, + '/[a]+': RouteRecordInfo< + '/[a]+', + '/:a+', + { a: ParamValueOneOrMore }, + { a: ParamValueOneOrMore }, + never + >, + '/partial-[a]': RouteRecordInfo< + '/partial-[a]', + '/partial-:a', + { a: ParamValue }, + { a: ParamValue }, + never + >, + '/partial-[[a]]': RouteRecordInfo< + '/partial-[[a]]', + '/partial-:a?', + { a?: ParamValueZeroOrOne }, + { a?: ParamValueZeroOrOne }, + never + >, }" `) }) @@ -63,8 +141,20 @@ describe('generateRouteNamedMap', () => { expect(b.name).toBe('/:b()') expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { - '/:a': RouteRecordInfo<'/:a', '/:a', { a: ParamValue }, { a: ParamValue }>, - '/:b()': RouteRecordInfo<'/:b()', '/:b()', { b: ParamValue }, { b: ParamValue }>, + '/:a': RouteRecordInfo< + '/:a', + '/:a', + { a: ParamValue }, + { a: ParamValue }, + never + >, + '/:b()': RouteRecordInfo< + '/:b()', + '/:b()', + { b: ParamValue }, + { b: ParamValue }, + never + >, }" `) }) @@ -77,10 +167,34 @@ describe('generateRouteNamedMap', () => { tree.insert('n/[a]/[c]/other-[d]', 'n/[a]/[c]/other-[d].vue') expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { - '/n/[a]/': RouteRecordInfo<'/n/[a]/', '/n/:a', { a: ParamValue }, { a: ParamValue }>, - '/n/[a]/[b]': RouteRecordInfo<'/n/[a]/[b]', '/n/:a/:b', { a: ParamValue, b: ParamValue }, { a: ParamValue, b: ParamValue }>, - '/n/[a]/[c]/other-[d]': RouteRecordInfo<'/n/[a]/[c]/other-[d]', '/n/:a/:c/other-:d', { a: ParamValue, c: ParamValue, d: ParamValue }, { a: ParamValue, c: ParamValue, d: ParamValue }>, - '/n/[a]/other': RouteRecordInfo<'/n/[a]/other', '/n/:a/other', { a: ParamValue }, { a: ParamValue }>, + '/n/[a]/': RouteRecordInfo< + '/n/[a]/', + '/n/:a', + { a: ParamValue }, + { a: ParamValue }, + never + >, + '/n/[a]/[b]': RouteRecordInfo< + '/n/[a]/[b]', + '/n/:a/:b', + { a: ParamValue, b: ParamValue }, + { a: ParamValue, b: ParamValue }, + never + >, + '/n/[a]/[c]/other-[d]': RouteRecordInfo< + '/n/[a]/[c]/other-[d]', + '/n/:a/:c/other-:d', + { a: ParamValue, c: ParamValue, d: ParamValue }, + { a: ParamValue, c: ParamValue, d: ParamValue }, + never + >, + '/n/[a]/other': RouteRecordInfo< + '/n/[a]/other', + '/n/:a/other', + { a: ParamValue }, + { a: ParamValue }, + never + >, }" `) }) @@ -95,11 +209,41 @@ describe('generateRouteNamedMap', () => { tree.insert('n/[...a]', 'n/[...a].vue') // splat expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { - '/n/[a]': RouteRecordInfo<'/n/[a]', '/n/:a', { a: ParamValue }, { a: ParamValue }>, - '/n/[[a]]': RouteRecordInfo<'/n/[[a]]', '/n/:a?', { a?: ParamValueZeroOrOne }, { a?: ParamValueZeroOrOne }>, - '/n/[...a]': RouteRecordInfo<'/n/[...a]', '/n/:a(.*)', { a: ParamValue }, { a: ParamValue }>, - '/n/[[a]]+': RouteRecordInfo<'/n/[[a]]+', '/n/:a*', { a?: ParamValueZeroOrMore }, { a?: ParamValueZeroOrMore }>, - '/n/[a]+': RouteRecordInfo<'/n/[a]+', '/n/:a+', { a: ParamValueOneOrMore }, { a: ParamValueOneOrMore }>, + '/n/[a]': RouteRecordInfo< + '/n/[a]', + '/n/:a', + { a: ParamValue }, + { a: ParamValue }, + never + >, + '/n/[[a]]': RouteRecordInfo< + '/n/[[a]]', + '/n/:a?', + { a?: ParamValueZeroOrOne }, + { a?: ParamValueZeroOrOne }, + never + >, + '/n/[...a]': RouteRecordInfo< + '/n/[...a]', + '/n/:a(.*)', + { a: ParamValue }, + { a: ParamValue }, + never + >, + '/n/[[a]]+': RouteRecordInfo< + '/n/[[a]]+', + '/n/:a*', + { a?: ParamValueZeroOrMore }, + { a?: ParamValueZeroOrMore }, + never + >, + '/n/[a]+': RouteRecordInfo< + '/n/[a]+', + '/n/:a+', + { a: ParamValueOneOrMore }, + { a: ParamValueOneOrMore }, + never + >, }" `) }) @@ -117,9 +261,27 @@ describe('generateRouteNamedMap', () => { expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { - '/[lang]/': RouteRecordInfo<'/[lang]/', '/:lang', { lang: ParamValue }, { lang: ParamValue }>, - '/[lang]/[id]': RouteRecordInfo<'/[lang]/[id]', '/:lang/:id', { lang: ParamValue, id: ParamValue }, { lang: ParamValue, id: ParamValue }>, - '/[lang]/a': RouteRecordInfo<'/[lang]/a', '/:lang/a', { lang: ParamValue }, { lang: ParamValue }>, + '/[lang]/': RouteRecordInfo< + '/[lang]/', + '/:lang', + { lang: ParamValue }, + { lang: ParamValue }, + never + >, + '/[lang]/[id]': RouteRecordInfo< + '/[lang]/[id]', + '/:lang/:id', + { lang: ParamValue, id: ParamValue }, + { lang: ParamValue, id: ParamValue }, + never + >, + '/[lang]/a': RouteRecordInfo< + '/[lang]/a', + '/:lang/a', + { lang: ParamValue }, + { lang: ParamValue }, + never + >, }" `) }) @@ -136,14 +298,62 @@ describe('generateRouteNamedMap', () => { tree.insert('d', 'd.vue') expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { - '/a/a': RouteRecordInfo<'/a/a', '/a/a', Record, Record>, - '/a/b': RouteRecordInfo<'/a/b', '/a/b', Record, Record>, - '/a/c': RouteRecordInfo<'/a/c', '/a/c', Record, Record>, - '/b/b': RouteRecordInfo<'/b/b', '/b/b', Record, Record>, - '/b/c': RouteRecordInfo<'/b/c', '/b/c', Record, Record>, - '/b/d': RouteRecordInfo<'/b/d', '/b/d', Record, Record>, - '/c': RouteRecordInfo<'/c', '/c', Record, Record>, - '/d': RouteRecordInfo<'/d', '/d', Record, Record>, + '/a/a': RouteRecordInfo< + '/a/a', + '/a/a', + Record, + Record, + never + >, + '/a/b': RouteRecordInfo< + '/a/b', + '/a/b', + Record, + Record, + never + >, + '/a/c': RouteRecordInfo< + '/a/c', + '/a/c', + Record, + Record, + never + >, + '/b/b': RouteRecordInfo< + '/b/b', + '/b/b', + Record, + Record, + never + >, + '/b/c': RouteRecordInfo< + '/b/c', + '/b/c', + Record, + Record, + never + >, + '/b/d': RouteRecordInfo< + '/b/d', + '/b/d', + Record, + Record, + never + >, + '/c': RouteRecordInfo< + '/c', + '/c', + Record, + Record, + never + >, + '/d': RouteRecordInfo< + '/d', + '/d', + Record, + Record, + never + >, }" `) }) @@ -156,10 +366,36 @@ describe('generateRouteNamedMap', () => { tree.insert('a/[id]/index', 'a/[id]/index.vue') expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { - '/a': RouteRecordInfo<'/a', '/a', Record, Record, '/a/' | '/a/[id]' | '/a/[id]/'>, - '/a/': RouteRecordInfo<'/a/', '/a', Record, Record>, - '/a/[id]': RouteRecordInfo<'/a/[id]', '/a/:id', { id: ParamValue }, { id: ParamValue }, '/a/[id]/'>, - '/a/[id]/': RouteRecordInfo<'/a/[id]/', '/a/:id', { id: ParamValue }, { id: ParamValue }>, + '/a': RouteRecordInfo< + '/a', + '/a', + Record, + Record, + | '/a/' + | '/a/[id]' + | '/a/[id]/' + >, + '/a/': RouteRecordInfo< + '/a/', + '/a', + Record, + Record, + never + >, + '/a/[id]': RouteRecordInfo< + '/a/[id]', + '/a/:id', + { id: ParamValue }, + { id: ParamValue }, + | '/a/[id]/' + >, + '/a/[id]/': RouteRecordInfo< + '/a/[id]/', + '/a/:id', + { id: ParamValue }, + { id: ParamValue }, + never + >, }" `) }) @@ -172,8 +408,20 @@ describe('generateRouteNamedMap', () => { expect(child.fullPath).toBe('/child') expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { - '/parent': RouteRecordInfo<'/parent', '/', Record, Record, '/parent/child'>, - '/parent/child': RouteRecordInfo<'/parent/child', '/child', Record, Record>, + '/parent': RouteRecordInfo< + '/parent', + '/', + Record, + Record, + | '/parent/child' + >, + '/parent/child': RouteRecordInfo< + '/parent/child', + '/child', + Record, + Record, + never + >, }" `) }) @@ -190,11 +438,45 @@ describe('generateRouteNamedMap', () => { tree.insert('parent/other-child', 'parent/other-child.vue') expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { - '/parent': RouteRecordInfo<'/parent', '/parent', Record, Record, '/parent/child' | '/parent/child/subchild' | '/parent/child/subchild/grandchild' | '/parent/other-child'>, - '/parent/child': RouteRecordInfo<'/parent/child', '/parent/child', Record, Record, '/parent/child/subchild' | '/parent/child/subchild/grandchild'>, - '/parent/child/subchild': RouteRecordInfo<'/parent/child/subchild', '/parent/child/subchild', Record, Record, '/parent/child/subchild/grandchild'>, - '/parent/child/subchild/grandchild': RouteRecordInfo<'/parent/child/subchild/grandchild', '/parent/child/subchild/grandchild', Record, Record>, - '/parent/other-child': RouteRecordInfo<'/parent/other-child', '/parent/other-child', Record, Record>, + '/parent': RouteRecordInfo< + '/parent', + '/parent', + Record, + Record, + | '/parent/child' + | '/parent/child/subchild' + | '/parent/child/subchild/grandchild' + | '/parent/other-child' + >, + '/parent/child': RouteRecordInfo< + '/parent/child', + '/parent/child', + Record, + Record, + | '/parent/child/subchild' + | '/parent/child/subchild/grandchild' + >, + '/parent/child/subchild': RouteRecordInfo< + '/parent/child/subchild', + '/parent/child/subchild', + Record, + Record, + | '/parent/child/subchild/grandchild' + >, + '/parent/child/subchild/grandchild': RouteRecordInfo< + '/parent/child/subchild/grandchild', + '/parent/child/subchild/grandchild', + Record, + Record, + never + >, + '/parent/other-child': RouteRecordInfo< + '/parent/other-child', + '/parent/other-child', + Record, + Record, + never + >, }" `) }) @@ -205,8 +487,20 @@ describe('generateRouteNamedMap', () => { tree.insert('parent/child/a/b/c', 'parent/child/a/b/c.vue') expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { - '/parent': RouteRecordInfo<'/parent', '/parent', Record, Record, '/parent/child/a/b/c'>, - '/parent/child/a/b/c': RouteRecordInfo<'/parent/child/a/b/c', '/parent/child/a/b/c', Record, Record>, + '/parent': RouteRecordInfo< + '/parent', + '/parent', + Record, + Record, + | '/parent/child/a/b/c' + >, + '/parent/child/a/b/c': RouteRecordInfo< + '/parent/child/a/b/c', + '/parent/child/a/b/c', + Record, + Record, + never + >, }" `) }) @@ -217,8 +511,20 @@ describe('generateRouteNamedMap', () => { tree.insert('parent/child', 'parent/child.vue') expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { - '/parent/': RouteRecordInfo<'/parent/', '/parent', Record, Record>, - '/parent/child': RouteRecordInfo<'/parent/child', '/parent/child', Record, Record>, + '/parent/': RouteRecordInfo< + '/parent/', + '/parent', + Record, + Record, + never + >, + '/parent/child': RouteRecordInfo< + '/parent/child', + '/parent/child', + Record, + Record, + never + >, }" `) }) @@ -232,11 +538,42 @@ describe('generateRouteNamedMap', () => { tree.insert('parent/a/b/c', 'parent/a/b/c.vue') expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { - '/parent/': RouteRecordInfo<'/parent/', '/parent', Record, Record>, - '/parent/a/': RouteRecordInfo<'/parent/a/', '/parent/a', Record, Record>, - '/parent/a/b': RouteRecordInfo<'/parent/a/b', '/parent/a/b', Record, Record, '/parent/a/b/' | '/parent/a/b/c'>, - '/parent/a/b/': RouteRecordInfo<'/parent/a/b/', '/parent/a/b', Record, Record>, - '/parent/a/b/c': RouteRecordInfo<'/parent/a/b/c', '/parent/a/b/c', Record, Record>, + '/parent/': RouteRecordInfo< + '/parent/', + '/parent', + Record, + Record, + never + >, + '/parent/a/': RouteRecordInfo< + '/parent/a/', + '/parent/a', + Record, + Record, + never + >, + '/parent/a/b': RouteRecordInfo< + '/parent/a/b', + '/parent/a/b', + Record, + Record, + | '/parent/a/b/' + | '/parent/a/b/c' + >, + '/parent/a/b/': RouteRecordInfo< + '/parent/a/b/', + '/parent/a/b', + Record, + Record, + never + >, + '/parent/a/b/c': RouteRecordInfo< + '/parent/a/b/c', + '/parent/a/b/c', + Record, + Record, + never + >, }" `) }) @@ -254,9 +591,27 @@ describe('generateRouteNamedMap', () => { expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { - '/[lang]/': RouteRecordInfo<'/[lang]/', '/:lang', { lang: ParamValue }, { lang: ParamValue }>, - '/[lang]/[id]': RouteRecordInfo<'/[lang]/[id]', '/:lang/:id', { lang: ParamValue, id: ParamValue }, { lang: ParamValue, id: ParamValue }>, - '/[lang]/a': RouteRecordInfo<'/[lang]/a', '/:lang/a', { lang: ParamValue }, { lang: ParamValue }>, + '/[lang]/': RouteRecordInfo< + '/[lang]/', + '/:lang', + { lang: ParamValue }, + { lang: ParamValue }, + never + >, + '/[lang]/[id]': RouteRecordInfo< + '/[lang]/[id]', + '/:lang/:id', + { lang: ParamValue, id: ParamValue }, + { lang: ParamValue, id: ParamValue }, + never + >, + '/[lang]/a': RouteRecordInfo< + '/[lang]/a', + '/:lang/a', + { lang: ParamValue }, + { lang: ParamValue }, + never + >, }" `) }) @@ -268,7 +623,13 @@ describe('generateRouteNamedMap', () => { expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { - '/(group)/a': RouteRecordInfo<'/(group)/a', '/a', Record, Record>, + '/(group)/a': RouteRecordInfo< + '/(group)/a', + '/a', + Record, + Record, + never + >, }" `) }) @@ -280,7 +641,13 @@ describe('generateRouteNamedMap', () => { expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { - '/(group)/(subgroup)/c': RouteRecordInfo<'/(group)/(subgroup)/c', '/c', Record, Record>, + '/(group)/(subgroup)/c': RouteRecordInfo< + '/(group)/(subgroup)/c', + '/c', + Record, + Record, + never + >, }" `) }) @@ -292,7 +659,13 @@ describe('generateRouteNamedMap', () => { expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { - '/folder/(group)': RouteRecordInfo<'/folder/(group)', '/folder', Record, Record>, + '/folder/(group)': RouteRecordInfo< + '/folder/(group)', + '/folder', + Record, + Record, + never + >, }" `) }) @@ -325,8 +698,8 @@ describe('generateRouteNamedMap', () => { expect(result2).toBe(result3) // Verify the union type is alphabetically sorted - expect(result1).toContain( - "'/parent/alpha' | '/parent/beta' | '/parent/zebra'" + expect(result1.replaceAll(/\n\s+\|/g, ' |')).toContain( + "| '/parent/alpha' | '/parent/beta' | '/parent/zebra'" ) }) @@ -342,8 +715,20 @@ describe('generateRouteNamedMap', () => { expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { - '/child': RouteRecordInfo<'/child', '/child', Record, Record>, - '/parent/child': RouteRecordInfo<'/parent/child', '/parent/child', Record, Record>, + '/child': RouteRecordInfo< + '/child', + '/child', + Record, + Record, + never + >, + '/parent/child': RouteRecordInfo< + '/parent/child', + '/parent/child', + Record, + Record, + never + >, }" `) }) @@ -361,9 +746,28 @@ describe('generateRouteNamedMap', () => { expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(` "export interface RouteNamedMap { - '/parent': RouteRecordInfo<'/parent', '/parent', Record, Record, '/parent/child1' | '/parent/child3'>, - '/parent/child1': RouteRecordInfo<'/parent/child1', '/parent/child1', Record, Record>, - '/parent/child3': RouteRecordInfo<'/parent/child3', '/parent/child3', Record, Record>, + '/parent': RouteRecordInfo< + '/parent', + '/parent', + Record, + Record, + | '/parent/child1' + | '/parent/child3' + >, + '/parent/child1': RouteRecordInfo< + '/parent/child1', + '/parent/child1', + Record, + Record, + never + >, + '/parent/child3': RouteRecordInfo< + '/parent/child3', + '/parent/child3', + Record, + Record, + never + >, }" `) }) diff --git a/src/codegen/generateRouteMap.ts b/src/codegen/generateRouteMap.ts index 0454f9908..965940e76 100644 --- a/src/codegen/generateRouteMap.ts +++ b/src/codegen/generateRouteMap.ts @@ -27,6 +27,8 @@ export function generateRouteRecordInfo(node: TreeNode) { generateRouteParams(node, false), ] + let childRouteNames = 'never' + if (node.children.size > 0) { // TODO: remove Array.from() once Node 20 support is dropped const deepNamedChildren = Array.from(node.getChildrenDeep()) @@ -38,9 +40,13 @@ export function generateRouteRecordInfo(node: TreeNode) { .sort() if (deepNamedChildren.length > 0) { - typeParams.push(deepNamedChildren.join(' | ')) + childRouteNames = deepNamedChildren + .map((childRouteName) => `| ${childRouteName}`) + .join('\n ') } } - return `RouteRecordInfo<${typeParams.join(', ')}>` + typeParams.push(childRouteNames) + + return `RouteRecordInfo<${typeParams.map((line) => `\n ${line}`).join(',\n')}\n >` } From b5ca9761464c7c9eeb14e53347f47ee70e532e86 Mon Sep 17 00:00:00 2001 From: Anoesj Date: Thu, 14 Aug 2025 09:54:02 +0200 Subject: [PATCH 3/5] chore: refactor ts codegen utils and adjust auto-routes examples in docs --- docs/.vitepress/twoslash-files.ts | 6 +- docs/.vitepress/twoslash/code/typed-router.ts | 10 +- docs/guide/typescript.md | 2 +- docs/introduction.md | 6 +- playground/src/router.ts | 6 +- playground/typed-router.d.ts | 293 +++++++++++------- src/codegen/generateDTS.ts | 4 +- src/codegen/generateRouteFileInfoMap.spec.ts | 27 +- src/codegen/generateRouteFileInfoMap.ts | 13 +- src/codegen/generateRouteMap.spec.ts | 114 +++---- src/codegen/generateRouteMap.ts | 19 +- src/utils/index.ts | 20 ++ 12 files changed, 312 insertions(+), 208 deletions(-) diff --git a/docs/.vitepress/twoslash-files.ts b/docs/.vitepress/twoslash-files.ts index 73c8e2974..d5aae1c56 100644 --- a/docs/.vitepress/twoslash-files.ts +++ b/docs/.vitepress/twoslash-files.ts @@ -17,14 +17,14 @@ declare module 'vue-router/auto-routes' { '/', Record, Record, - never + | never > '/users': RouteRecordInfo< '/users', '/users', Record, Record, - never + | never > '/users/[id]': RouteRecordInfo< '/users/[id]', @@ -38,7 +38,7 @@ declare module 'vue-router/auto-routes' { '/users/:id/edit', { id: ParamValue }, { id: ParamValue }, - never + | never > } } diff --git a/docs/.vitepress/twoslash/code/typed-router.ts b/docs/.vitepress/twoslash/code/typed-router.ts index 8260a4f5a..442b70ce7 100644 --- a/docs/.vitepress/twoslash/code/typed-router.ts +++ b/docs/.vitepress/twoslash/code/typed-router.ts @@ -1,3 +1,5 @@ +/* prettier-ignore */ + declare module 'vue-router/auto-routes' { import type { RouteRecordInfo, @@ -13,28 +15,28 @@ declare module 'vue-router/auto-routes' { '/', Record, Record, - never + | never > '/users': RouteRecordInfo< '/users', '/users', Record, Record, - never + | never > '/users/[id]': RouteRecordInfo< '/users/[id]', '/users/:id', { id: ParamValue }, { id: ParamValue }, - '/users/[id]/edit' + | '/users/[id]/edit' > '/users/[id]/edit': RouteRecordInfo< '/users/[id]/edit', '/users/:id/edit', { id: ParamValue }, { id: ParamValue }, - never + | never > } } diff --git a/docs/guide/typescript.md b/docs/guide/typescript.md index 1b11c5086..88e30667a 100644 --- a/docs/guide/typescript.md +++ b/docs/guide/typescript.md @@ -70,7 +70,7 @@ declare module 'vue-router/auto-routes' { '/added-during-runtime/[...path]/child', { path: ParamValue }, { path: ParamValue }, - never + | never > } } diff --git a/docs/introduction.md b/docs/introduction.md index 77818bff9..ae705deed 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -120,21 +120,21 @@ declare module 'vue-router/auto-routes' { '/', Record, Record, - never + | never > '/about': RouteRecordInfo< '/about', '/about', Record, Record, - never + | never > '/users/[id]': RouteRecordInfo< '/users/[id]', '/users/:id', { id: ParamValue }, { id: ParamValue }, - never + | never > } } diff --git a/playground/src/router.ts b/playground/src/router.ts index 489516cc4..78dc4b389 100644 --- a/playground/src/router.ts +++ b/playground/src/router.ts @@ -24,6 +24,8 @@ if (import.meta.hot) { addRedirects() } +/* prettier-ignore */ + // manual extension of route types declare module 'vue-router/auto-routes' { export interface RouteNamedMap { @@ -32,14 +34,14 @@ declare module 'vue-router/auto-routes' { '/added-during-runtime/[...path]', { path: ParamValue }, { path: ParamValue }, - 'custom-dynamic-child-name' + | 'custom-dynamic-child-name' > 'custom-dynamic-child-name': RouteRecordInfo< 'custom-dynamic-child-name', '/added-during-runtime/[...path]/child', { path: ParamValue }, { path: ParamValue }, - never + | never > } } diff --git a/playground/typed-router.d.ts b/playground/typed-router.d.ts index 6ac50eb17..e38d53426 100644 --- a/playground/typed-router.d.ts +++ b/playground/typed-router.d.ts @@ -23,14 +23,14 @@ declare module 'vue-router/auto-routes' { '/app', Record, Record, - never + | never >, '/(some-layout)/home': RouteRecordInfo< '/(some-layout)/home', '/home', Record, Record, - never + | never >, '/(test-group)': RouteRecordInfo< '/(test-group)', @@ -44,245 +44,245 @@ declare module 'vue-router/auto-routes' { '/test-group-child', Record, Record, - never + | never >, 'home': RouteRecordInfo< 'home', '/', Record, Record, - never + | never >, '/[name]': RouteRecordInfo< '/[name]', '/:name', { name: ParamValue }, { name: ParamValue }, - never + | never >, '/[...path]': RouteRecordInfo< '/[...path]', '/:path(.*)', { path: ParamValue }, { path: ParamValue }, - never + | never >, '/[...path]+': RouteRecordInfo< '/[...path]+', '/:path(.*)+', { path: ParamValueOneOrMore }, { path: ParamValueOneOrMore }, - never + | never >, '/@[profileId]': RouteRecordInfo< '/@[profileId]', '/@:profileId', { profileId: ParamValue }, { profileId: ParamValue }, - never + | never >, '/about': RouteRecordInfo< '/about', '/about', Record, Record, - never + | never >, '/about.extra.nested': RouteRecordInfo< '/about.extra.nested', '/about/extra/nested', Record, Record, - never + | never >, '/articles/': RouteRecordInfo< '/articles/', '/articles', Record, Record, - never + | never >, '/articles/[id]': RouteRecordInfo< '/articles/[id]', '/articles/:id', { id: ParamValue }, { id: ParamValue }, - never + | never >, '/articles/[id]+': RouteRecordInfo< '/articles/[id]+', '/articles/:id+', { id: ParamValueOneOrMore }, { id: ParamValueOneOrMore }, - never + | never >, '/custom-definePage': RouteRecordInfo< '/custom-definePage', '/custom-definePage', Record, Record, - never + | never >, 'a rebel': RouteRecordInfo< 'a rebel', '/custom-name', Record, Record, - never + | never >, '/custom/page': RouteRecordInfo< '/custom/page', '/custom/page', Record, Record, - never + | never >, '/deep/nesting/works/[[files]]+': RouteRecordInfo< '/deep/nesting/works/[[files]]+', '/deep/nesting/works/:files*', { files?: ParamValueZeroOrMore }, { files?: ParamValueZeroOrMore }, - never + | never >, '/deep/nesting/works/at-root-but-from-nested': RouteRecordInfo< '/deep/nesting/works/at-root-but-from-nested', '/at-root-but-from-nested', Record, Record, - never + | never >, 'deep the most rebel': RouteRecordInfo< 'deep the most rebel', '/deep-most-rebel', Record, Record, - never + | never >, '/deep/nesting/works/custom-path': RouteRecordInfo< '/deep/nesting/works/custom-path', '/deep-surprise-:id(\d+)', Record, Record, - never + | never >, 'deep a rebel': RouteRecordInfo< 'deep a rebel', '/deep/nesting/works/custom-name', Record, Record, - never + | never >, '/docs/[lang]/real/': RouteRecordInfo< '/docs/[lang]/real/', '/docs/:lang/real', { lang: ParamValue }, { lang: ParamValue }, - never + | never >, '/feature-1/': RouteRecordInfo< '/feature-1/', '/feature-1', Record, Record, - never + | never >, '/feature-1/about': RouteRecordInfo< '/feature-1/about', '/feature-1/about', Record, Record, - never + | never >, '/feature-2/': RouteRecordInfo< '/feature-2/', '/feature-2', Record, Record, - never + | never >, '/feature-2/about': RouteRecordInfo< '/feature-2/about', '/feature-2/about', Record, Record, - never + | never >, '/feature-3/': RouteRecordInfo< '/feature-3/', '/feature-3', Record, Record, - never + | never >, '/feature-3/about': RouteRecordInfo< '/feature-3/about', '/feature-3/about', Record, Record, - never + | never >, '/file(ignored-parentheses)': RouteRecordInfo< '/file(ignored-parentheses)', '/file(ignored-parentheses)', Record, Record, - never + | never >, '/from-root': RouteRecordInfo< '/from-root', '/from-root', Record, Record, - never + | never >, '/group/(thing)': RouteRecordInfo< '/group/(thing)', '/group', Record, Record, - never + | never >, 'the most rebel': RouteRecordInfo< 'the most rebel', '/most-rebel', Record, Record, - never + | never >, '/multiple-[a]-[b]-params': RouteRecordInfo< '/multiple-[a]-[b]-params', '/multiple-:a-:b-params', { a: ParamValue, b: ParamValue }, { a: ParamValue, b: ParamValue }, - never + | never >, '/my-optional-[[slug]]': RouteRecordInfo< '/my-optional-[[slug]]', '/my-optional-:slug?', { slug?: ParamValueZeroOrOne }, { slug?: ParamValueZeroOrOne }, - never + | never >, '/n-[[n]]/': RouteRecordInfo< '/n-[[n]]/', '/n-:n?', { n?: ParamValueZeroOrOne }, { n?: ParamValueZeroOrOne }, - never + | never >, '/n-[[n]]/[[more]]+/': RouteRecordInfo< '/n-[[n]]/[[more]]+/', '/n-:n?/:more*', { n?: ParamValueZeroOrOne, more?: ParamValueZeroOrMore }, { n?: ParamValueZeroOrOne, more?: ParamValueZeroOrMore }, - never + | never >, '/n-[[n]]/[[more]]+/[final]': RouteRecordInfo< '/n-[[n]]/[[more]]+/[final]', '/n-:n?/:more*/:final', { n?: ParamValueZeroOrOne, more?: ParamValueZeroOrMore, final: ParamValue }, { n?: ParamValueZeroOrOne, more?: ParamValueZeroOrMore, final: ParamValue }, - never + | never >, '/named-views/parent': RouteRecordInfo< '/named-views/parent', @@ -296,147 +296,147 @@ declare module 'vue-router/auto-routes' { '/named-views/parent', Record, Record, - never + | never >, '/nested-group/(group)': RouteRecordInfo< '/nested-group/(group)', '/nested-group', Record, Record, - never + | never >, '/nested-group/(nested-group-first-level)/(nested-group-deep)/nested-group-deep-child': RouteRecordInfo< '/nested-group/(nested-group-first-level)/(nested-group-deep)/nested-group-deep-child', '/nested-group/nested-group-deep-child', Record, Record, - never + | never >, '/nested-group/(nested-group-first-level)/nested-group-first-level-child': RouteRecordInfo< '/nested-group/(nested-group-first-level)/nested-group-first-level-child', '/nested-group/nested-group-first-level-child', Record, Record, - never + | never >, '/partial-[name]': RouteRecordInfo< '/partial-[name]', '/partial-:name', { name: ParamValue }, { name: ParamValue }, - never + | never >, '/custom-path': RouteRecordInfo< '/custom-path', '/surprise-:id(\d+)', Record, Record, - never + | never >, '/test-[a-id]': RouteRecordInfo< '/test-[a-id]', '/test-:a-id', { aId: ParamValue }, { aId: ParamValue }, - never + | never >, '/todos/': RouteRecordInfo< '/todos/', '/todos', Record, Record, - never + | never >, '/todos/+layout': RouteRecordInfo< '/todos/+layout', '/todos/+layout', Record, Record, - never + | never >, '/users/': RouteRecordInfo< '/users/', '/users', Record, Record, - never + | never >, '/users/[id]': RouteRecordInfo< '/users/[id]', '/users/:id', { id: ParamValue }, { id: ParamValue }, - never + | never >, '/users/[id].edit': RouteRecordInfo< '/users/[id].edit', '/users/:id/edit', { id: ParamValue }, { id: ParamValue }, - never + | never >, '/users/colada-loader.[id]': RouteRecordInfo< '/users/colada-loader.[id]', '/users/colada-loader/:id', { id: ParamValue }, { id: ParamValue }, - never + | never >, '/users/nested.route.deep': RouteRecordInfo< '/users/nested.route.deep', '/users/nested/route/deep', Record, Record, - never + | never >, '/users/pinia-colada.[id]': RouteRecordInfo< '/users/pinia-colada.[id]', '/users/pinia-colada/:id', { id: ParamValue }, { id: ParamValue }, - never + | never >, '/users/query.[id]': RouteRecordInfo< '/users/query.[id]', '/users/query/:id', { id: ParamValue }, { id: ParamValue }, - never + | never >, '/users/tq-infinite-query': RouteRecordInfo< '/users/tq-infinite-query', '/users/tq-infinite-query', Record, Record, - never + | never >, '/users/tq-query-bug': RouteRecordInfo< '/users/tq-query-bug', '/users/tq-query-bug', Record, Record, - never + | never >, '/users/tq-query.[id]': RouteRecordInfo< '/users/tq-query.[id]', '/users/tq-query/:id', { id: ParamValue }, { id: ParamValue }, - never + | never >, '/vuefire-tests/get-doc': RouteRecordInfo< '/vuefire-tests/get-doc', '/vuefire-tests/get-doc', Record, Record, - never + | never >, '/with-extension': RouteRecordInfo< '/with-extension', '/with-extension', Record, Record, - never + | never >, } @@ -461,12 +461,14 @@ declare module 'vue-router/auto-routes' { 'src/pages/(some-layout)/app.vue': { routes: | '/(some-layout)/app' - views: never + views: + | never } 'src/pages/(some-layout)/home.vue': { routes: | '/(some-layout)/home' - views: never + views: + | never } 'src/pages/(test-group).vue': { routes: @@ -478,48 +480,57 @@ declare module 'vue-router/auto-routes' { 'src/pages/(test-group)/test-group-child.vue': { routes: | '/(test-group)/test-group-child' - views: never + views: + | never } 'src/pages/index.vue': { routes: | 'home' | '/from-root' - views: never + views: + | never } 'src/pages/index@named.vue': { routes: | 'home' - views: never + views: + | never } 'src/pages/[name].vue': { routes: | '/[name]' - views: never + views: + | never } 'src/pages/[...path].vue': { routes: | '/[...path]' - views: never + views: + | never } 'src/pages/[...path]+.vue': { routes: | '/[...path]+' - views: never + views: + | never } 'src/pages/@[profileId].vue': { routes: | '/@[profileId]' - views: never + views: + | never } 'src/pages/about.vue': { routes: | '/about' - views: never + views: + | never } 'src/pages/about.extra.nested.vue': { routes: | '/about.extra.nested' - views: never + views: + | never } 'src/pages/articles.vue': { routes: @@ -532,128 +543,153 @@ declare module 'vue-router/auto-routes' { 'src/pages/articles/index.vue': { routes: | '/articles/' - views: never + views: + | never } 'src/pages/articles/[id].vue': { routes: | '/articles/[id]' - views: never + views: + | never } 'src/pages/articles/[id]+.vue': { routes: | '/articles/[id]+' - views: never + views: + | never } 'src/pages/custom-definePage.vue': { routes: | '/custom-definePage' - views: never + views: + | never } 'src/pages/custom-name.vue': { routes: | 'a rebel' - views: never + views: + | never } 'src/pages/deep/nesting/works/too.vue': { routes: | '/custom/page' | '/deep/nesting/works/at-root-but-from-nested' - views: never + views: + | never } 'src/pages/deep/nesting/works/[[files]]+.vue': { routes: | '/deep/nesting/works/[[files]]+' - views: never + views: + | never } 'src/pages/deep/nesting/works/custom-name-and-path.vue': { routes: | 'deep the most rebel' - views: never + views: + | never } 'src/pages/deep/nesting/works/custom-path.vue': { routes: | '/deep/nesting/works/custom-path' - views: never + views: + | never } 'src/pages/deep/nesting/works/custom-name.vue': { routes: | 'deep a rebel' - views: never + views: + | never } 'src/docs/real/index.md': { routes: | '/docs/[lang]/real/' - views: never + views: + | never } 'src/features/feature-1/pages/index.vue': { routes: | '/feature-1/' - views: never + views: + | never } 'src/features/feature-1/pages/about.vue': { routes: | '/feature-1/about' - views: never + views: + | never } 'src/features/feature-2/pages/index.vue': { routes: | '/feature-2/' - views: never + views: + | never } 'src/features/feature-2/pages/about.vue': { routes: | '/feature-2/about' - views: never + views: + | never } 'src/features/feature-3/pages/index.vue': { routes: | '/feature-3/' - views: never + views: + | never } 'src/features/feature-3/pages/about.vue': { routes: | '/feature-3/about' - views: never + views: + | never } 'src/pages/file(ignored-parentheses).vue': { routes: | '/file(ignored-parentheses)' - views: never + views: + | never } 'src/pages/group/(thing).vue': { routes: | '/group/(thing)' - views: never + views: + | never } 'src/pages/custom-name-and-path.vue': { routes: | 'the most rebel' - views: never + views: + | never } 'src/pages/multiple-[a]-[b]-params.vue': { routes: | '/multiple-[a]-[b]-params' - views: never + views: + | never } 'src/pages/my-optional-[[slug]].vue': { routes: | '/my-optional-[[slug]]' - views: never + views: + | never } 'src/pages/n-[[n]]/index.vue': { routes: | '/n-[[n]]/' - views: never + views: + | never } 'src/pages/n-[[n]]/[[more]]+/index.vue': { routes: | '/n-[[n]]/[[more]]+/' - views: never + views: + | never } 'src/pages/n-[[n]]/[[more]]+/[final].vue': { routes: | '/n-[[n]]/[[more]]+/[final]' - views: never + views: + | never } 'src/pages/named-views/parent.vue': { routes: @@ -667,117 +703,140 @@ declare module 'vue-router/auto-routes' { 'src/pages/named-views/parent/index.vue': { routes: | '/named-views/parent/' - views: never + views: + | never } 'src/pages/named-views/parent/index@a.vue': { routes: | '/named-views/parent/' - views: never + views: + | never } 'src/pages/named-views/parent/index@b.vue': { routes: | '/named-views/parent/' - views: never + views: + | never } 'src/pages/nested-group/(group).vue': { routes: | '/nested-group/(group)' - views: never + views: + | never } 'src/pages/nested-group/(nested-group-first-level)/(nested-group-deep)/nested-group-deep-child.vue': { routes: | '/nested-group/(nested-group-first-level)/(nested-group-deep)/nested-group-deep-child' - views: never + views: + | never } 'src/pages/nested-group/(nested-group-first-level)/nested-group-first-level-child.vue': { routes: | '/nested-group/(nested-group-first-level)/nested-group-first-level-child' - views: never + views: + | never } 'src/pages/partial-[name].vue': { routes: | '/partial-[name]' - views: never + views: + | never } 'src/pages/custom-path.vue': { routes: | '/custom-path' - views: never + views: + | never } 'src/pages/test-[a-id].vue': { routes: | '/test-[a-id]' - views: never + views: + | never } 'src/pages/todos/index.vue': { routes: | '/todos/' - views: never + views: + | never } 'src/pages/todos/+layout.vue': { routes: | '/todos/+layout' - views: never + views: + | never } 'src/pages/users/index.vue': { routes: | '/users/' - views: never + views: + | never } 'src/pages/users/[id].vue': { routes: | '/users/[id]' - views: never + views: + | never } 'src/pages/users/[id].edit.vue': { routes: | '/users/[id].edit' - views: never + views: + | never } 'src/pages/users/colada-loader.[id].vue': { routes: | '/users/colada-loader.[id]' - views: never + views: + | never } 'src/pages/users/nested.route.deep.vue': { routes: | '/users/nested.route.deep' - views: never + views: + | never } 'src/pages/users/pinia-colada.[id].vue': { routes: | '/users/pinia-colada.[id]' - views: never + views: + | never } 'src/pages/users/query.[id].vue': { routes: | '/users/query.[id]' - views: never + views: + | never } 'src/pages/users/tq-infinite-query.vue': { routes: | '/users/tq-infinite-query' - views: never + views: + | never } 'src/pages/users/tq-query-bug.vue': { routes: | '/users/tq-query-bug' - views: never + views: + | never } 'src/pages/users/tq-query.[id].vue': { routes: | '/users/tq-query.[id]' - views: never + views: + | never } 'src/pages/vuefire-tests/get-doc.vue': { routes: | '/vuefire-tests/get-doc' - views: never + views: + | never } 'src/pages/with-extension.page.vue': { routes: | '/with-extension' - views: never + views: + | never } } diff --git a/src/codegen/generateDTS.ts b/src/codegen/generateDTS.ts index 09a6e2b8c..2880ed1c1 100644 --- a/src/codegen/generateDTS.ts +++ b/src/codegen/generateDTS.ts @@ -1,4 +1,4 @@ -import { ts } from '../utils' +import { ts, pad } from '../utils' /** * Removes empty lines and indent by two spaces to match the rest of the file. @@ -10,7 +10,7 @@ function normalizeLines(code: string) { // FIXME: the code should be cleaned up by the codegen functions. Removing empty lines here // reduces readability of the route file info map. .filter((line) => line.length !== 0) - .map((line) => line && ' ' + line) + .map((line) => pad(2, line)) .join('\n') ) } diff --git a/src/codegen/generateRouteFileInfoMap.spec.ts b/src/codegen/generateRouteFileInfoMap.spec.ts index 1d5c612ac..e5a6eb06e 100644 --- a/src/codegen/generateRouteFileInfoMap.spec.ts +++ b/src/codegen/generateRouteFileInfoMap.spec.ts @@ -25,22 +25,26 @@ describe('generateRouteFileInfoMap', () => { 'src/pages/index.vue': { routes: | '/' - views: never + views: + | never } 'src/pages/a.vue': { routes: | '/a' - views: never + views: + | never } 'src/pages/b.vue': { routes: | '/b' - views: never + views: + | never } 'src/pages/c.vue': { routes: | '/c' - views: never + views: + | never } }" `) @@ -63,7 +67,8 @@ describe('generateRouteFileInfoMap', () => { 'src/pages/parent/child.vue': { routes: | '/parent/child' - views: never + views: + | never } }" `) @@ -88,12 +93,14 @@ describe('generateRouteFileInfoMap', () => { 'src/pages/parent/child.vue': { routes: | '/parent/child' - views: never + views: + | never } 'src/pages/parent/child@test.vue': { routes: | '/parent/child' - views: never + views: + | never } }" `) @@ -115,13 +122,15 @@ describe('generateRouteFileInfoMap', () => { routes: | '/' | '/home' - views: never + views: + | never } 'nested/index.vue': { routes: | '/nested/path' | '/unnested' - views: never + views: + | never } }" `) diff --git a/src/codegen/generateRouteFileInfoMap.ts b/src/codegen/generateRouteFileInfoMap.ts index 66a3c1c06..c6f438d09 100644 --- a/src/codegen/generateRouteFileInfoMap.ts +++ b/src/codegen/generateRouteFileInfoMap.ts @@ -1,5 +1,6 @@ import { relative } from 'pathe' import type { PrefixTree, TreeNode } from '../core/tree' +import { formatMultilineUnion } from '../utils' export function generateRouteFileInfoMap( node: PrefixTree, @@ -42,8 +43,16 @@ export function generateRouteFileInfoMap( ([file, { routes, views }]) => ` '${file}': { - routes:${routes.map((name) => `\n | '${name}'`).join('\n')} - views:${views.length > 0 ? views.map((view) => `\n | '${view}'`).join('\n') : ' never'} + routes: + ${formatMultilineUnion( + routes.map((routeName) => `'${routeName}'`), + 6 + )} + views: + ${formatMultilineUnion( + views.length > 0 ? views.map((view) => `'${view}'`) : ['never'], + 6 + )} }` ) .join('\n') diff --git a/src/codegen/generateRouteMap.spec.ts b/src/codegen/generateRouteMap.spec.ts index e739362a4..c62e51f40 100644 --- a/src/codegen/generateRouteMap.spec.ts +++ b/src/codegen/generateRouteMap.spec.ts @@ -26,28 +26,28 @@ describe('generateRouteNamedMap', () => { '/', Record, Record, - never + | never >, '/a': RouteRecordInfo< '/a', '/a', Record, Record, - never + | never >, '/b': RouteRecordInfo< '/b', '/b', Record, Record, - never + | never >, '/c': RouteRecordInfo< '/c', '/c', Record, Record, - never + | never >, }" `) @@ -71,63 +71,63 @@ describe('generateRouteNamedMap', () => { '/:a', { a: ParamValue }, { a: ParamValue }, - never + | never >, '/[[a]]': RouteRecordInfo< '/[[a]]', '/:a?', { a?: ParamValueZeroOrOne }, { a?: ParamValueZeroOrOne }, - never + | never >, '/[...a]': RouteRecordInfo< '/[...a]', '/:a(.*)', { a: ParamValue }, { a: ParamValue }, - never + | never >, '/[[...a]]': RouteRecordInfo< '/[[...a]]', '/:a(.*)?', { a?: ParamValueZeroOrOne }, { a?: ParamValueZeroOrOne }, - never + | never >, '/[[...a]]+': RouteRecordInfo< '/[[...a]]+', '/:a(.*)*', { a?: ParamValueZeroOrMore }, { a?: ParamValueZeroOrMore }, - never + | never >, '/[[a]]+': RouteRecordInfo< '/[[a]]+', '/:a*', { a?: ParamValueZeroOrMore }, { a?: ParamValueZeroOrMore }, - never + | never >, '/[a]+': RouteRecordInfo< '/[a]+', '/:a+', { a: ParamValueOneOrMore }, { a: ParamValueOneOrMore }, - never + | never >, '/partial-[a]': RouteRecordInfo< '/partial-[a]', '/partial-:a', { a: ParamValue }, { a: ParamValue }, - never + | never >, '/partial-[[a]]': RouteRecordInfo< '/partial-[[a]]', '/partial-:a?', { a?: ParamValueZeroOrOne }, { a?: ParamValueZeroOrOne }, - never + | never >, }" `) @@ -146,14 +146,14 @@ describe('generateRouteNamedMap', () => { '/:a', { a: ParamValue }, { a: ParamValue }, - never + | never >, '/:b()': RouteRecordInfo< '/:b()', '/:b()', { b: ParamValue }, { b: ParamValue }, - never + | never >, }" `) @@ -172,28 +172,28 @@ describe('generateRouteNamedMap', () => { '/n/:a', { a: ParamValue }, { a: ParamValue }, - never + | never >, '/n/[a]/[b]': RouteRecordInfo< '/n/[a]/[b]', '/n/:a/:b', { a: ParamValue, b: ParamValue }, { a: ParamValue, b: ParamValue }, - never + | never >, '/n/[a]/[c]/other-[d]': RouteRecordInfo< '/n/[a]/[c]/other-[d]', '/n/:a/:c/other-:d', { a: ParamValue, c: ParamValue, d: ParamValue }, { a: ParamValue, c: ParamValue, d: ParamValue }, - never + | never >, '/n/[a]/other': RouteRecordInfo< '/n/[a]/other', '/n/:a/other', { a: ParamValue }, { a: ParamValue }, - never + | never >, }" `) @@ -214,35 +214,35 @@ describe('generateRouteNamedMap', () => { '/n/:a', { a: ParamValue }, { a: ParamValue }, - never + | never >, '/n/[[a]]': RouteRecordInfo< '/n/[[a]]', '/n/:a?', { a?: ParamValueZeroOrOne }, { a?: ParamValueZeroOrOne }, - never + | never >, '/n/[...a]': RouteRecordInfo< '/n/[...a]', '/n/:a(.*)', { a: ParamValue }, { a: ParamValue }, - never + | never >, '/n/[[a]]+': RouteRecordInfo< '/n/[[a]]+', '/n/:a*', { a?: ParamValueZeroOrMore }, { a?: ParamValueZeroOrMore }, - never + | never >, '/n/[a]+': RouteRecordInfo< '/n/[a]+', '/n/:a+', { a: ParamValueOneOrMore }, { a: ParamValueOneOrMore }, - never + | never >, }" `) @@ -266,21 +266,21 @@ describe('generateRouteNamedMap', () => { '/:lang', { lang: ParamValue }, { lang: ParamValue }, - never + | never >, '/[lang]/[id]': RouteRecordInfo< '/[lang]/[id]', '/:lang/:id', { lang: ParamValue, id: ParamValue }, { lang: ParamValue, id: ParamValue }, - never + | never >, '/[lang]/a': RouteRecordInfo< '/[lang]/a', '/:lang/a', { lang: ParamValue }, { lang: ParamValue }, - never + | never >, }" `) @@ -303,56 +303,56 @@ describe('generateRouteNamedMap', () => { '/a/a', Record, Record, - never + | never >, '/a/b': RouteRecordInfo< '/a/b', '/a/b', Record, Record, - never + | never >, '/a/c': RouteRecordInfo< '/a/c', '/a/c', Record, Record, - never + | never >, '/b/b': RouteRecordInfo< '/b/b', '/b/b', Record, Record, - never + | never >, '/b/c': RouteRecordInfo< '/b/c', '/b/c', Record, Record, - never + | never >, '/b/d': RouteRecordInfo< '/b/d', '/b/d', Record, Record, - never + | never >, '/c': RouteRecordInfo< '/c', '/c', Record, Record, - never + | never >, '/d': RouteRecordInfo< '/d', '/d', Record, Record, - never + | never >, }" `) @@ -380,7 +380,7 @@ describe('generateRouteNamedMap', () => { '/a', Record, Record, - never + | never >, '/a/[id]': RouteRecordInfo< '/a/[id]', @@ -394,7 +394,7 @@ describe('generateRouteNamedMap', () => { '/a/:id', { id: ParamValue }, { id: ParamValue }, - never + | never >, }" `) @@ -420,7 +420,7 @@ describe('generateRouteNamedMap', () => { '/child', Record, Record, - never + | never >, }" `) @@ -468,14 +468,14 @@ describe('generateRouteNamedMap', () => { '/parent/child/subchild/grandchild', Record, Record, - never + | never >, '/parent/other-child': RouteRecordInfo< '/parent/other-child', '/parent/other-child', Record, Record, - never + | never >, }" `) @@ -499,7 +499,7 @@ describe('generateRouteNamedMap', () => { '/parent/child/a/b/c', Record, Record, - never + | never >, }" `) @@ -516,14 +516,14 @@ describe('generateRouteNamedMap', () => { '/parent', Record, Record, - never + | never >, '/parent/child': RouteRecordInfo< '/parent/child', '/parent/child', Record, Record, - never + | never >, }" `) @@ -543,14 +543,14 @@ describe('generateRouteNamedMap', () => { '/parent', Record, Record, - never + | never >, '/parent/a/': RouteRecordInfo< '/parent/a/', '/parent/a', Record, Record, - never + | never >, '/parent/a/b': RouteRecordInfo< '/parent/a/b', @@ -565,14 +565,14 @@ describe('generateRouteNamedMap', () => { '/parent/a/b', Record, Record, - never + | never >, '/parent/a/b/c': RouteRecordInfo< '/parent/a/b/c', '/parent/a/b/c', Record, Record, - never + | never >, }" `) @@ -596,21 +596,21 @@ describe('generateRouteNamedMap', () => { '/:lang', { lang: ParamValue }, { lang: ParamValue }, - never + | never >, '/[lang]/[id]': RouteRecordInfo< '/[lang]/[id]', '/:lang/:id', { lang: ParamValue, id: ParamValue }, { lang: ParamValue, id: ParamValue }, - never + | never >, '/[lang]/a': RouteRecordInfo< '/[lang]/a', '/:lang/a', { lang: ParamValue }, { lang: ParamValue }, - never + | never >, }" `) @@ -628,7 +628,7 @@ describe('generateRouteNamedMap', () => { '/a', Record, Record, - never + | never >, }" `) @@ -646,7 +646,7 @@ describe('generateRouteNamedMap', () => { '/c', Record, Record, - never + | never >, }" `) @@ -664,7 +664,7 @@ describe('generateRouteNamedMap', () => { '/folder', Record, Record, - never + | never >, }" `) @@ -720,14 +720,14 @@ describe('generateRouteNamedMap', () => { '/child', Record, Record, - never + | never >, '/parent/child': RouteRecordInfo< '/parent/child', '/parent/child', Record, Record, - never + | never >, }" `) @@ -759,14 +759,14 @@ describe('generateRouteNamedMap', () => { '/parent/child1', Record, Record, - never + | never >, '/parent/child3': RouteRecordInfo< '/parent/child3', '/parent/child3', Record, Record, - never + | never >, }" `) diff --git a/src/codegen/generateRouteMap.ts b/src/codegen/generateRouteMap.ts index 965940e76..507daee21 100644 --- a/src/codegen/generateRouteMap.ts +++ b/src/codegen/generateRouteMap.ts @@ -1,5 +1,6 @@ import type { TreeNode } from '../core/tree' import { generateRouteParams } from './generateRouteParams' +import { pad, formatMultilineUnion } from '../utils' export function generateRouteNamedMap(node: TreeNode): string { if (node.isRoot()) { @@ -11,7 +12,7 @@ ${node.getChildrenSorted().map(generateRouteNamedMap).join('')}}` // if the node has a filePath, it's a component, it has a routeName and it should be referenced in the RouteNamedMap // otherwise it should be skipped to avoid navigating to a route that doesn't render anything (node.value.components.size > 0 && node.name - ? ` '${node.name}': ${generateRouteRecordInfo(node)},\n` + ? pad(2, `'${node.name}': ${generateRouteRecordInfo(node)},\n`) : '') + (node.children.size > 0 ? node.getChildrenSorted().map(generateRouteNamedMap).join('\n') @@ -27,7 +28,7 @@ export function generateRouteRecordInfo(node: TreeNode) { generateRouteParams(node, false), ] - let childRouteNames = 'never' + let childRouteNames = ['never'] if (node.children.size > 0) { // TODO: remove Array.from() once Node 20 support is dropped @@ -36,17 +37,19 @@ export function generateRouteRecordInfo(node: TreeNode) { .filter( (childRoute) => childRoute.value.components.size > 0 && childRoute.name ) - .map((childRoute) => `'${childRoute.name}'`) + .map((childRoute) => childRoute.name) .sort() if (deepNamedChildren.length > 0) { - childRouteNames = deepNamedChildren - .map((childRouteName) => `| ${childRouteName}`) - .join('\n ') + childRouteNames = deepNamedChildren.map( + (childRouteName) => `'${childRouteName}'` + ) } } - typeParams.push(childRouteNames) + typeParams.push(formatMultilineUnion(childRouteNames, 4)) - return `RouteRecordInfo<${typeParams.map((line) => `\n ${line}`).join(',\n')}\n >` + return `RouteRecordInfo< +${typeParams.map((line) => pad(4, line)).join(',\n')} + >` } diff --git a/src/utils/index.ts b/src/utils/index.ts index c74ccc691..73f0006a2 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -14,3 +14,23 @@ export type LiteralStringUnion = // for highlighting export const ts = String.raw + +/** + * Pads a single-line string with spaces. + * @param indent The number of spaces to pad with. + * @param str The string to pad, none if omitted. + * @returns The padded string. + */ +export function pad(indent: number, str = ''): string { + return str.padStart(str.length + indent) +} + +/** + * Formats an array of union items as a multiline union type. + * @param items The items to format. + * @param indent The number of spaces to indent each line. + * @returns The formatted multiline union type. + */ +export function formatMultilineUnion(items: string[], indent: number): string { + return items.map((s) => `| ${s}`).join(`\n${pad(indent)}`) +} From 59c3a23fcde5d88b07e3172682f8c33982a6231c Mon Sep 17 00:00:00 2001 From: Anoesj Date: Thu, 14 Aug 2025 11:33:56 +0200 Subject: [PATCH 4/5] refactor: replace `pad` with `indent` in code generation functions and add `stringToStringType` utility --- src/codegen/generateDTS.ts | 4 +-- src/codegen/generateRouteFileInfoMap.ts | 12 ++----- src/codegen/generateRouteMap.ts | 42 +++++++++++-------------- src/utils/index.ts | 25 +++++++++++---- 4 files changed, 43 insertions(+), 40 deletions(-) diff --git a/src/codegen/generateDTS.ts b/src/codegen/generateDTS.ts index 2880ed1c1..f429c9a98 100644 --- a/src/codegen/generateDTS.ts +++ b/src/codegen/generateDTS.ts @@ -1,4 +1,4 @@ -import { ts, pad } from '../utils' +import { ts, indent } from '../utils' /** * Removes empty lines and indent by two spaces to match the rest of the file. @@ -10,7 +10,7 @@ function normalizeLines(code: string) { // FIXME: the code should be cleaned up by the codegen functions. Removing empty lines here // reduces readability of the route file info map. .filter((line) => line.length !== 0) - .map((line) => pad(2, line)) + .map((line) => indent(2, line)) .join('\n') ) } diff --git a/src/codegen/generateRouteFileInfoMap.ts b/src/codegen/generateRouteFileInfoMap.ts index c6f438d09..25db74c32 100644 --- a/src/codegen/generateRouteFileInfoMap.ts +++ b/src/codegen/generateRouteFileInfoMap.ts @@ -1,6 +1,6 @@ import { relative } from 'pathe' import type { PrefixTree, TreeNode } from '../core/tree' -import { formatMultilineUnion } from '../utils' +import { formatMultilineUnion, stringToStringType } from '../utils' export function generateRouteFileInfoMap( node: PrefixTree, @@ -44,15 +44,9 @@ export function generateRouteFileInfoMap( ` '${file}': { routes: - ${formatMultilineUnion( - routes.map((routeName) => `'${routeName}'`), - 6 - )} + ${formatMultilineUnion(routes.map(stringToStringType), 6)} views: - ${formatMultilineUnion( - views.length > 0 ? views.map((view) => `'${view}'`) : ['never'], - 6 - )} + ${formatMultilineUnion(views.map(stringToStringType), 6)} }` ) .join('\n') diff --git a/src/codegen/generateRouteMap.ts b/src/codegen/generateRouteMap.ts index 507daee21..4206e4a53 100644 --- a/src/codegen/generateRouteMap.ts +++ b/src/codegen/generateRouteMap.ts @@ -1,6 +1,6 @@ import type { TreeNode } from '../core/tree' import { generateRouteParams } from './generateRouteParams' -import { pad, formatMultilineUnion } from '../utils' +import { indent, formatMultilineUnion, stringToStringType } from '../utils' export function generateRouteNamedMap(node: TreeNode): string { if (node.isRoot()) { @@ -12,7 +12,7 @@ ${node.getChildrenSorted().map(generateRouteNamedMap).join('')}}` // if the node has a filePath, it's a component, it has a routeName and it should be referenced in the RouteNamedMap // otherwise it should be skipped to avoid navigating to a route that doesn't render anything (node.value.components.size > 0 && node.name - ? pad(2, `'${node.name}': ${generateRouteRecordInfo(node)},\n`) + ? indent(2, `'${node.name}': ${generateRouteRecordInfo(node)},\n`) : '') + (node.children.size > 0 ? node.getChildrenSorted().map(generateRouteNamedMap).join('\n') @@ -28,28 +28,24 @@ export function generateRouteRecordInfo(node: TreeNode) { generateRouteParams(node, false), ] - let childRouteNames = ['never'] - - if (node.children.size > 0) { - // TODO: remove Array.from() once Node 20 support is dropped - const deepNamedChildren = Array.from(node.getChildrenDeep()) - // skip routes that are not added to the types - .filter( - (childRoute) => childRoute.value.components.size > 0 && childRoute.name - ) - .map((childRoute) => childRoute.name) - .sort() - - if (deepNamedChildren.length > 0) { - childRouteNames = deepNamedChildren.map( - (childRouteName) => `'${childRouteName}'` - ) - } - } - - typeParams.push(formatMultilineUnion(childRouteNames, 4)) + const childRouteNames: string[] = + node.children.size > 0 + ? // TODO: remove Array.from() once Node 20 support is dropped + Array.from(node.getChildrenDeep()) + // skip routes that are not added to the types + .filter( + (childRoute): childRoute is TreeNode & { name: string } => + childRoute.value.components.size > 0 && !!childRoute.name + ) + .map((childRoute) => childRoute.name) + .sort() + : [] + + typeParams.push( + formatMultilineUnion(childRouteNames.map(stringToStringType), 4) + ) return `RouteRecordInfo< -${typeParams.map((line) => pad(4, line)).join(',\n')} +${typeParams.map((line) => indent(4, line)).join(',\n')} >` } diff --git a/src/utils/index.ts b/src/utils/index.ts index 73f0006a2..a255f820d 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -17,20 +17,33 @@ export const ts = String.raw /** * Pads a single-line string with spaces. - * @param indent The number of spaces to pad with. + * @param spaces The number of spaces to pad with. * @param str The string to pad, none if omitted. * @returns The padded string. */ -export function pad(indent: number, str = ''): string { - return str.padStart(str.length + indent) +export function indent(spaces: number, str = ''): string { + return `${' '.repeat(spaces)}${str}` } /** * Formats an array of union items as a multiline union type. * @param items The items to format. - * @param indent The number of spaces to indent each line. + * @param spaces The number of spaces to indent each line. * @returns The formatted multiline union type. */ -export function formatMultilineUnion(items: string[], indent: number): string { - return items.map((s) => `| ${s}`).join(`\n${pad(indent)}`) +export function formatMultilineUnion(items: string[], spaces: number): string { + return (items.length ? items : ['never']) + .map((s) => `| ${s}`) + .join(`\n${indent(spaces)}`) +} + +/** + * Converts a string value to a TS string literal type. + * @param str the string to convert to a string type + * @returns The string wrapped in single quotes. + * @example + * stringToStringType('hello') // returns "'hello'" + */ +export function stringToStringType(str: string): string { + return `'${str}'` } From c9552a537c55a4dc486ad289de8ca6857eadb1cd Mon Sep 17 00:00:00 2001 From: Anoesj Date: Thu, 14 Aug 2025 12:40:11 +0200 Subject: [PATCH 5/5] refactor: revert renaming `pad` util, add and use `TreeNode.isNamed()` method, minor optimizations --- src/codegen/generateDTS.ts | 4 ++-- src/codegen/generateRouteFileInfoMap.ts | 8 +++++-- src/codegen/generateRouteMap.ts | 30 ++++++++++++++----------- src/codegen/generateRouteRecords.ts | 12 +++++----- src/core/tree.spec.ts | 19 ++++++++++++++++ src/core/tree.ts | 8 +++++++ src/utils/index.ts | 4 ++-- 7 files changed, 61 insertions(+), 24 deletions(-) diff --git a/src/codegen/generateDTS.ts b/src/codegen/generateDTS.ts index f429c9a98..2880ed1c1 100644 --- a/src/codegen/generateDTS.ts +++ b/src/codegen/generateDTS.ts @@ -1,4 +1,4 @@ -import { ts, indent } from '../utils' +import { ts, pad } from '../utils' /** * Removes empty lines and indent by two spaces to match the rest of the file. @@ -10,7 +10,7 @@ function normalizeLines(code: string) { // FIXME: the code should be cleaned up by the codegen functions. Removing empty lines here // reduces readability of the route file info map. .filter((line) => line.length !== 0) - .map((line) => indent(2, line)) + .map((line) => pad(2, line)) .join('\n') ) } diff --git a/src/codegen/generateRouteFileInfoMap.ts b/src/codegen/generateRouteFileInfoMap.ts index 25db74c32..fa0f56f05 100644 --- a/src/codegen/generateRouteFileInfoMap.ts +++ b/src/codegen/generateRouteFileInfoMap.ts @@ -79,8 +79,12 @@ function generateRouteFileInfoLines( const routeNames = [node, ...node.getChildrenDeepSorted()] // an unnamed route cannot be accessed in types - .filter((node): node is TreeNode & { name: string } => !!node.name) - .map((node) => node.name) + .reduce((acc, node) => { + if (node.isNamed()) { + acc.push(node.name) + } + return acc + }, []) // Most of the time we only have one view, but with named views we can have multiple. const currentRouteInfo = diff --git a/src/codegen/generateRouteMap.ts b/src/codegen/generateRouteMap.ts index 4206e4a53..f869f5818 100644 --- a/src/codegen/generateRouteMap.ts +++ b/src/codegen/generateRouteMap.ts @@ -1,6 +1,6 @@ -import type { TreeNode } from '../core/tree' +import type { TreeNode, TreeNodeNamed } from '../core/tree' import { generateRouteParams } from './generateRouteParams' -import { indent, formatMultilineUnion, stringToStringType } from '../utils' +import { pad, formatMultilineUnion, stringToStringType } from '../utils' export function generateRouteNamedMap(node: TreeNode): string { if (node.isRoot()) { @@ -11,8 +11,11 @@ ${node.getChildrenSorted().map(generateRouteNamedMap).join('')}}` return ( // if the node has a filePath, it's a component, it has a routeName and it should be referenced in the RouteNamedMap // otherwise it should be skipped to avoid navigating to a route that doesn't render anything - (node.value.components.size > 0 && node.name - ? indent(2, `'${node.name}': ${generateRouteRecordInfo(node)},\n`) + (node.value.components.size && node.isNamed() + ? pad( + 2, + `${stringToStringType(node.name)}: ${generateRouteRecordInfo(node)},\n` + ) : '') + (node.children.size > 0 ? node.getChildrenSorted().map(generateRouteNamedMap).join('\n') @@ -20,10 +23,10 @@ ${node.getChildrenSorted().map(generateRouteNamedMap).join('')}}` ) } -export function generateRouteRecordInfo(node: TreeNode) { +export function generateRouteRecordInfo(node: TreeNodeNamed): string { const typeParams = [ - `'${node.name}'`, - `'${node.fullPath}'`, + stringToStringType(node.name), + stringToStringType(node.fullPath), generateRouteParams(node, true), generateRouteParams(node, false), ] @@ -33,11 +36,12 @@ export function generateRouteRecordInfo(node: TreeNode) { ? // TODO: remove Array.from() once Node 20 support is dropped Array.from(node.getChildrenDeep()) // skip routes that are not added to the types - .filter( - (childRoute): childRoute is TreeNode & { name: string } => - childRoute.value.components.size > 0 && !!childRoute.name - ) - .map((childRoute) => childRoute.name) + .reduce((acc, childRoute) => { + if (childRoute.value.components.size && childRoute.isNamed()) { + acc.push(childRoute.name) + } + return acc + }, []) .sort() : [] @@ -46,6 +50,6 @@ export function generateRouteRecordInfo(node: TreeNode) { ) return `RouteRecordInfo< -${typeParams.map((line) => indent(4, line)).join(',\n')} +${typeParams.map((line) => pad(4, line)).join(',\n')} >` } diff --git a/src/codegen/generateRouteRecords.ts b/src/codegen/generateRouteRecords.ts index 3dca451b4..fb26b8664 100644 --- a/src/codegen/generateRouteRecords.ts +++ b/src/codegen/generateRouteRecords.ts @@ -2,6 +2,7 @@ import { getLang } from '@vue-macros/common' import type { TreeNode } from '../core/tree' import { ImportsMap } from '../core/utils' import { type ResolvedOptions } from '../options' +import { pad, stringToStringType } from '../utils' /** * Generate the route records for the given node. @@ -48,8 +49,8 @@ ${node } } - const startIndent = ' '.repeat(indent * 2) - const indentStr = ' '.repeat((indent + 1) * 2) + const startIndent = pad(indent * 2) + const indentStr = pad((indent + 1) * 2) // TODO: should meta be defined a different way to allow preserving imports? // const meta = node.value.overrides.meta @@ -62,10 +63,11 @@ ${node ${indentStr}path: '${node.path}', ${indentStr}${ node.value.components.size - ? node.name - ? `name: '${node.name}',` + ? node.isNamed() + ? `name: ${stringToStringType(node.name)},` : `/* no name */` - : `/* internal name: '${node.name}' */` + : // node.name can still be false and we don't want that to result in string literal 'false' + `/* internal name: ${typeof node.name === 'string' ? stringToStringType(node.name) : node.name} */` } ${ // component diff --git a/src/core/tree.spec.ts b/src/core/tree.spec.ts index f3f696090..8289d96e8 100644 --- a/src/core/tree.spec.ts +++ b/src/core/tree.spec.ts @@ -394,12 +394,14 @@ describe('Tree', () => { name: 'custom', }) expect(node.name).toBe('custom') + expect(node.isNamed()).toBe(true) node = tree.insert('auth/login', 'auth/login.vue') node.value.setOverride('', { name: 'custom-child', }) expect(node.name).toBe('custom-child') + expect(node.isNamed()).toBe(true) }) it('allows empty name to remove route from route map', () => { @@ -408,21 +410,38 @@ describe('Tree', () => { // Before setting empty name, it should use the default name expect(node.name).toBe('/some-route') + expect(node.isNamed()).toBe(true) // Set empty name node.value.setOverride('', { name: '', }) expect(node.name).toBe('') + expect(node.isNamed()).toBe(false) + + // Set false name + node.value.setOverride('', { + name: false, + }) + expect(node.name).toBe(false) + expect(node.isNamed()).toBe(false) // Test with nested route node = tree.insert('nested/child', 'nested/child.vue') expect(node.name).toBe('/nested/child') + expect(node.isNamed()).toBe(true) node.value.setOverride('', { name: '', }) expect(node.name).toBe('') + expect(node.isNamed()).toBe(false) + + node.value.setOverride('', { + name: false, + }) + expect(node.name).toBe(false) + expect(node.isNamed()).toBe(false) }) it('allows a custom path', () => { diff --git a/src/core/tree.ts b/src/core/tree.ts index 25fff6644..6a9de3627 100644 --- a/src/core/tree.ts +++ b/src/core/tree.ts @@ -12,6 +12,10 @@ export interface TreeNodeOptions extends ResolvedOptions { treeNodeOptions?: TreeNodeValueOptions } +export type TreeNodeNamed = TreeNode & { + name: Extract +} + export class TreeNode { /** * value of the node @@ -290,6 +294,10 @@ export class TreeNode { ) } + isNamed(): this is TreeNodeNamed { + return !!this.name + } + toString(): string { return `${this.value}${ // either we have multiple names diff --git a/src/utils/index.ts b/src/utils/index.ts index a255f820d..62d8bcc40 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -21,7 +21,7 @@ export const ts = String.raw * @param str The string to pad, none if omitted. * @returns The padded string. */ -export function indent(spaces: number, str = ''): string { +export function pad(spaces: number, str = ''): string { return `${' '.repeat(spaces)}${str}` } @@ -34,7 +34,7 @@ export function indent(spaces: number, str = ''): string { export function formatMultilineUnion(items: string[], spaces: number): string { return (items.length ? items : ['never']) .map((s) => `| ${s}`) - .join(`\n${indent(spaces)}`) + .join(`\n${pad(spaces)}`) } /**