Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: volar plugins #609

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@
"import": "./dist/data-loaders/pinia-colada.js",
"require": "./dist/data-loaders/pinia-colada.cjs"
},
"./volar/sfc-route-blocks": {
"require": "./dist/volar/sfc-route-blocks.cjs"
},
"./volar/sfc-typed-router": {
"require": "./dist/volar/sfc-typed-router.cjs"
},
"./client": {
"types": "./client.d.ts"
}
Expand Down Expand Up @@ -141,6 +147,7 @@
"local-pkg": "^1.0.0",
"magic-string": "^0.30.17",
"mlly": "^1.7.4",
"muggle-string": "^0.4.1",
"pathe": "^2.0.2",
"picomatch": "^4.0.2",
"scule": "^1.3.0",
Expand Down
4 changes: 1 addition & 3 deletions playground/src/pages/[...path].vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
<template>
<main>
<h1>Not Found</h1>
<p>
{{ $route.name === '/[...path]' && $route.params.path }} does not exist.
</p>
<p>{{ $route.params.path }} does not exist.</p>
</main>
</template>

Expand Down
7 changes: 4 additions & 3 deletions playground/src/pages/[name].vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
onBeforeRouteUpdate,
type RouteLocationNormalized,
} from 'vue-router'
import type { RouteNamedMap } from 'vue-router/auto-routes'

const thing = 'THING'

Expand Down Expand Up @@ -90,8 +91,8 @@ if (routeLocation.name === '/[name]') {
routeLocation.params.id
}

const route = useRoute('/[name]')
const anyRoute = useRoute()
const route = useRoute()
const anyRoute = useRoute<keyof RouteNamedMap>()
if (anyRoute.name == '/articles/[id]') {
console.log('anyRoute.params', anyRoute.params.id)
}
Expand Down Expand Up @@ -133,7 +134,7 @@ definePage({

<template>
<main>
<h1>Param: {{ $route.name === '/[name]' && $route.params.name }}</h1>
<h1>Param: {{ $route.params.name }}</h1>
<h2>Param: {{ route.params.name }}</h2>
<p v-show="false">{{ thing }}</p>
<p v-if="isLoading">Loading user...</p>
Expand Down
5 changes: 4 additions & 1 deletion playground/src/pages/custom-name-and-path.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@

<!-- For some reason, a local link doesn't work... -->

<template>custom names</template>
<template>
custom names
{{ $route.name satisfies 'the most rebel' }}
</template>
3 changes: 2 additions & 1 deletion playground/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
},
"vueCompilerOptions": {
"plugins": [
"../volar/index.cjs"
"unplugin-vue-router/volar/sfc-route-blocks",
"unplugin-vue-router/volar/sfc-typed-router"
]
},
"references": [
Expand Down
68 changes: 68 additions & 0 deletions playground/typed-router.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,72 @@ declare module 'vue-router/auto-routes' {
'/vuefire-tests/get-doc': RouteRecordInfo<'/vuefire-tests/get-doc', '/vuefire-tests/get-doc', Record<never, never>, Record<never, never>>,
'/with-extension': RouteRecordInfo<'/with-extension', '/with-extension', Record<never, never>, Record<never, never>>,
}

/**
* File path to route names map by unplugin-vue-router
*/
export interface FilePathToRouteNamesMap {
'src/pages/(test-group).vue': '/(test-group)' | '/(test-group)/test-group-child',
'src/pages/(test-group)/test-group-child.vue': '/(test-group)/test-group-child',
'src/pages/index.vue': 'home',
'src/pages/[email protected]': 'home',
'src/pages/[name].vue': '/[name]',
'src/pages/[...path].vue': '/[...path]',
'src/pages/[...path]+.vue': '/[...path]+',
'src/pages/@[profileId].vue': '/@[profileId]',
'src/pages/about.vue': '/about',
'src/pages/about.extra.nested.vue': '/about.extra.nested',
'src/pages/articles.vue': '/articles' | '/articles/' | '/articles/[id]' | '/articles/[id]+',
'src/pages/articles/index.vue': '/articles/',
'src/pages/articles/[id].vue': '/articles/[id]',
'src/pages/articles/[id]+.vue': '/articles/[id]+',
'src/pages/custom-definePage.vue': '/custom-definePage',
'src/pages/custom-name.vue': 'a rebel',
'src/pages/deep/nesting/works/too.vue': '/custom/page',
'src/pages/deep/nesting/works/[[files]]+.vue': '/deep/nesting/works/[[files]]+',
'src/pages/deep/nesting/works/too.vue': '/deep/nesting/works/at-root-but-from-nested',
'src/pages/deep/nesting/works/custom-name-and-path.vue': 'deep the most rebel',
'src/pages/deep/nesting/works/custom-path.vue': '/deep/nesting/works/custom-path',
'src/pages/deep/nesting/works/custom-name.vue': 'deep a rebel',
'src/docs/real/index.md': '/docs/[lang]/real/',
'src/features/feature-1/pages/index.vue': '/feature-1/',
'src/features/feature-1/pages/about.vue': '/feature-1/about',
'src/features/feature-2/pages/index.vue': '/feature-2/',
'src/features/feature-2/pages/about.vue': '/feature-2/about',
'src/features/feature-3/pages/index.vue': '/feature-3/',
'src/features/feature-3/pages/about.vue': '/feature-3/about',
'src/pages/file(ignored-parentheses).vue': '/file(ignored-parentheses)',
'src/pages/index.vue': '/from-root',
'src/pages/group/(thing).vue': '/group/(thing)',
'src/pages/custom-name-and-path.vue': 'the most rebel',
'src/pages/multiple-[a]-[b]-params.vue': '/multiple-[a]-[b]-params',
'src/pages/my-optional-[[slug]].vue': '/my-optional-[[slug]]',
'src/pages/n-[[n]]/index.vue': '/n-[[n]]/',
'src/pages/n-[[n]]/[[more]]+/index.vue': '/n-[[n]]/[[more]]+/',
'src/pages/n-[[n]]/[[more]]+/[final].vue': '/n-[[n]]/[[more]]+/[final]',
'src/pages/nested-group/(group).vue': '/nested-group/(group)',
'src/pages/nested-group/(nested-group-first-level)/(nested-group-deep)/nested-group-deep-child.vue': '/nested-group/(nested-group-first-level)/(nested-group-deep)/nested-group-deep-child',
'src/pages/nested-group/(nested-group-first-level)/nested-group-first-level-child.vue': '/nested-group/(nested-group-first-level)/nested-group-first-level-child',
'src/pages/partial-[name].vue': '/partial-[name]',
'src/pages/custom-path.vue': '/custom-path',
'src/pages/test-[a-id].vue': '/test-[a-id]',
'src/pages/todos/index.vue': '/todos/',
'src/pages/users/index.vue': '/users/',
'src/pages/users/[id].vue': '/users/[id]',
'src/pages/users/[id].edit.vue': '/users/[id].edit',
'src/pages/users/colada-loader.[id].vue': '/users/colada-loader.[id]',
'src/pages/users/nested.route.deep.vue': '/users/nested.route.deep',
'src/pages/users/pinia-colada.[id].vue': '/users/pinia-colada.[id]',
'src/pages/users/query.[id].vue': '/users/query.[id]',
'src/pages/users/tq-query.[id].vue': '/users/tq-query.[id]',
'src/pages/vuefire-tests/get-doc.vue': '/vuefire-tests/get-doc',
'src/pages/with-extension.page.vue': '/with-extension',
}

/**
* Get a route's name by file path
*/
export type GetRouteNameByPath<T extends string> = T extends keyof FilePathToRouteNamesMap
? FilePathToRouteNamesMap[T]
: keyof import('vue-router/auto-routes').RouteNamedMap
}
9 changes: 6 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 26 additions & 6 deletions src/codegen/generateDTS.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
import { ts } from '../utils'

/**
* Removes empty lines and indent by two spaces to match the rest of the file.
*/
function normalizeLines(code: string) {
return code
.split('\n')
.filter((line) => line.length !== 0)
.map((line) => ' ' + line)
.join('\n')
}

export function generateDTS({
routesModule,
routeNamedMap,
filePathToRouteNamesMap,
}: {
vueRouterModule: string
routesModule: string
routeNamedMap: string
filePathToRouteNamesMap: string
}) {
return ts`
/* eslint-disable */
Expand All @@ -28,12 +41,19 @@ declare module '${routesModule}' {
/**
* Route name map generated by unplugin-vue-router
*/
${routeNamedMap
// remove empty lines and indent by two spaces to match the rest of the file
.split('\n')
.filter((line) => line.length !== 0)
.map((line) => ' ' + line)
.join('\n')}
${normalizeLines(routeNamedMap)}

/**
* File path to route names map by unplugin-vue-router
*/
${normalizeLines(filePathToRouteNamesMap)}

/**
* Get a route's name by file path
*/
export type GetRouteNameByPath<T extends string> = T extends keyof FilePathToRouteNamesMap
? FilePathToRouteNamesMap[T]
: keyof import('vue-router/auto-routes').RouteNamedMap
}
`.trimStart()
}
55 changes: 55 additions & 0 deletions src/codegen/generateFilePathToRouteNamesMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { relative } from 'node:path'
import type { TreeNode } from '../core/tree'

export function generateFilePathToRouteNamesMap(
node: TreeNode,
options: { root: string }
): string {
if (node.isRoot()) {
return `export interface FilePathToRouteNamesMap {
${node
.getSortedChildren()
.map((child) => generateFilePathToRouteNamesMap(child, options))
.join('')}}`
}

function getRelativeFilePath(file: string) {
return relative(options.root, file)
}

const routeNamesUnion = recursiveGetRouteNames(node)
.map((name) => `'${name}'`)
.join(' | ')

return (
// if the node has a filePath, it's a component, it has a routeName and it should be
// referenced in the FilePathToRouteNamesMap otherwise it should be skipped
// TODO: can we use `RouteNameWithChildren` from https://github.com/vuejs/router/pull/2475 here if merged?
Array.from(
node.value.components
.values()
.map(
(file) => ` '${getRelativeFilePath(file)}': ${routeNamesUnion},\n`
)
).join('') +
(node.children.size > 0
? node
.getSortedChildren()
.map((child) => generateFilePathToRouteNamesMap(child, options))
.join('\n')
: '')
)
}

/**
* Gets the name of the provided node and all of its children
*/
function recursiveGetRouteNames(node: TreeNode): TreeNode['name'][] {
return [
node.name,
...node
.getSortedChildren()
.values()
.map((child) => recursiveGetRouteNames(child)),
].flat()
}
4 changes: 4 additions & 0 deletions src/core/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { TreeNode, PrefixTree } from './tree'
import { promises as fs } from 'fs'
import { asRoutePath, ImportsMap, logTree, throttle } from './utils'
import { generateRouteNamedMap } from '../codegen/generateRouteMap'
import { generateFilePathToRouteNamesMap } from '../codegen/generateFilePathToRouteNamesMap'
import { MODULE_ROUTES_PATH, MODULE_VUE_ROUTER_AUTO } from './moduleConstants'
import { generateRouteRecord } from '../codegen/generateRouteRecords'
import fg from 'fast-glob'
Expand Down Expand Up @@ -240,6 +241,9 @@ if (import.meta.hot) {
vueRouterModule: MODULE_VUE_ROUTER_AUTO,
routesModule: MODULE_ROUTES_PATH,
routeNamedMap: generateRouteNamedMap(routeTree),
filePathToRouteNamesMap: generateFilePathToRouteNamesMap(routeTree, {
root,
}),
})
}

Expand Down
31 changes: 19 additions & 12 deletions volar/index.cjs → src/volar/entries/sfc-route-blocks.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,34 @@
// @ts-check
import type { VueLanguagePlugin } from '@vue/language-core'

/**
* @type {import('@vue/language-core').VueLanguagePlugin}
*/
const plugin = () => {
const plugin: VueLanguagePlugin = () => {
return {
version: 2.1,
getEmbeddedCodes(fileName, sfc) {
const names = [];
getEmbeddedCodes(_fileName, sfc) {
const names = []

for (let i = 0; i < sfc.customBlocks.length; i++) {
const block = sfc.customBlocks[i]
const block = sfc.customBlocks[i]!

// TODO: `<route>` block without `lang` is still interpreted as text right now, it seems
if (block.type === 'route') {
const lang = block.lang === 'txt' ? 'json' : block.lang
names.push({ id: `route_${i}`, lang })
names.push({ id: `route_${i}`, lang: lang || 'json' })
}
}

return names
},
resolveEmbeddedCode(fileName, sfc, embeddedCode) {
resolveEmbeddedCode(_fileName, sfc, embeddedCode) {
const match = embeddedCode.id.match(/^route_(\d+)$/)
if (match) {

if (match && match[1] !== undefined) {
const index = parseInt(match[1])
const block = sfc.customBlocks[index]

if (!block) {
return
}

embeddedCode.content.push([
block.content,
block.name,
Expand All @@ -40,4 +47,4 @@ const plugin = () => {
}
}

module.exports = plugin
export default plugin
Loading
Loading