Skip to content

Commit 530ac53

Browse files
committed
feat: add volar plugins
1 parent 8d3864d commit 530ac53

File tree

3 files changed

+220
-0
lines changed

3 files changed

+220
-0
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { allCodeFeatures, type VueLanguagePlugin } from '@vue/language-core'
2+
import { replace, toString } from 'muggle-string'
3+
4+
const plugin: VueLanguagePlugin = () => {
5+
const routeBlockIdPrefix = 'route_'
6+
const routeBlockIdRe = new RegExp(`^${routeBlockIdPrefix}(\\d+)$`)
7+
8+
return {
9+
version: 2.1,
10+
getEmbeddedCodes(_fileName, sfc) {
11+
const embeddedCodes = []
12+
13+
// we add an embedded code for every route block we find with the same index as the block
14+
for (let i = 0; i < sfc.customBlocks.length; i++) {
15+
const block = sfc.customBlocks[i]!
16+
17+
// TODO:
18+
// `<route>` blocks without `lang="json"` are still interpreted as text right now.
19+
// See: https://github.com/vuejs/language-tools/issues/185#issuecomment-1173742726
20+
// This seems to be because `custom_block_x` is still seen as txt, even though the corresponding `route_x` is json.
21+
if (block.type === 'route') {
22+
const lang = block.lang === 'txt' ? 'json' : block.lang
23+
embeddedCodes.push({ id: `${routeBlockIdPrefix}${i}`, lang })
24+
}
25+
}
26+
27+
return embeddedCodes
28+
},
29+
resolveEmbeddedCode(_fileName, sfc, embeddedCode) {
30+
const match = embeddedCode.id.match(routeBlockIdRe)
31+
32+
if (match) {
33+
const i = parseInt(match[1]!)
34+
const block = sfc.customBlocks[i]
35+
36+
// this shouldn't happen, but just in case
37+
if (!block) {
38+
return
39+
}
40+
41+
embeddedCode.content.push([
42+
block.content,
43+
block.name,
44+
0,
45+
allCodeFeatures,
46+
])
47+
48+
if (embeddedCode.lang === 'json') {
49+
const contentStr = toString(embeddedCode.content)
50+
if (
51+
contentStr.trim().startsWith('{') &&
52+
!contentStr.includes('$schema')
53+
) {
54+
replace(
55+
embeddedCode.content,
56+
'{',
57+
'{\n "$schema": "https://router.vuejs.org/schemas/route.schema.json",'
58+
)
59+
}
60+
}
61+
}
62+
},
63+
}
64+
}
65+
66+
export default plugin
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { relative } from 'pathe'
2+
import type { VueLanguagePlugin } from '@vue/language-core'
3+
import { replaceSourceRange, toString } from 'muggle-string'
4+
import { augmentVlsCtx } from '../utils/augment-vls-ctx'
5+
import type ts from 'typescript'
6+
7+
/*
8+
Future ideas:
9+
- Enhance typing of `onBeforeRouteUpdate() to and from parameters
10+
- Enhance typing of `onBeforeRouteLeave() from parameter
11+
- Enhance typing of `<RouterView>`
12+
- Typed `name` attribute for named views
13+
- Typed `route` slot prop when using `<RouterView v-slot="{route}">`
14+
- (low priority) Enhance typing of `to` route in `beforeEnter` route guards defined in `definePage`
15+
*/
16+
17+
const plugin: VueLanguagePlugin = ({
18+
compilerOptions,
19+
modules: { typescript: ts },
20+
}) => {
21+
const RE = {
22+
DOLLAR_ROUTE: {
23+
/**
24+
* When using `$route` in a template, it is referred
25+
* to as `__VLS_ctx.$route` in the virtual file.
26+
*/
27+
VLS_CTX: /\b__VLS_ctx.\$route\b/g,
28+
},
29+
}
30+
31+
return {
32+
version: 2.1,
33+
resolveEmbeddedCode(fileName, sfc, embeddedCode) {
34+
if (!embeddedCode.id.startsWith('script_')) {
35+
return
36+
}
37+
38+
// TODO: Do we want to apply this to EVERY .vue file or only to components that the user wrote themselves?
39+
40+
// NOTE: this might not work if different from the root passed to VueRouter unplugin
41+
const relativeFilePath = compilerOptions.rootDir
42+
? relative(compilerOptions.rootDir, fileName)
43+
: fileName
44+
45+
const useRouteNameType = `import('vue-router/auto-routes')._RouteNamesForFilePath<'${relativeFilePath}'>`
46+
const useRouteNameTypeParam = `<${useRouteNameType}>`
47+
48+
if (sfc.scriptSetup) {
49+
visit(sfc.scriptSetup.ast)
50+
}
51+
52+
function visit(node: ts.Node) {
53+
if (
54+
ts.isCallExpression(node) &&
55+
ts.isIdentifier(node.expression) &&
56+
node.expression.text === 'useRoute' &&
57+
!node.typeArguments &&
58+
!node.arguments.length
59+
) {
60+
if (!sfc.scriptSetup!.lang.startsWith('js')) {
61+
replaceSourceRange(
62+
embeddedCode.content,
63+
sfc.scriptSetup!.name,
64+
node.expression.end,
65+
node.expression.end,
66+
useRouteNameTypeParam
67+
)
68+
} else {
69+
const start = node.getStart(sfc.scriptSetup!.ast)
70+
replaceSourceRange(
71+
embeddedCode.content,
72+
sfc.scriptSetup!.name,
73+
start,
74+
start,
75+
`(`
76+
)
77+
replaceSourceRange(
78+
embeddedCode.content,
79+
sfc.scriptSetup!.name,
80+
node.end,
81+
node.end,
82+
` as ReturnType<typeof useRoute${useRouteNameTypeParam}>)`
83+
)
84+
}
85+
} else {
86+
ts.forEachChild(node, visit)
87+
}
88+
}
89+
90+
const contentStr = toString(embeddedCode.content)
91+
92+
const vlsCtxAugmentations: string[] = []
93+
94+
// Augment `__VLS_ctx.$route` to override the typings of `$route` in template blocks
95+
if (contentStr.match(RE.DOLLAR_ROUTE.VLS_CTX)) {
96+
vlsCtxAugmentations.push(
97+
`{} as { $route: ReturnType<typeof import('vue-router').useRoute${useRouteNameTypeParam}> }`
98+
)
99+
}
100+
101+
// We can try augmenting the types for `RouterView` below.
102+
// if (contentStr.includes(`__VLS_WithComponent<'RouterView', __VLS_LocalComponents`)) {
103+
// vlsCtxAugmentations.push(`RouterView: 'test';`)
104+
// }
105+
106+
if (vlsCtxAugmentations.length) {
107+
augmentVlsCtx(embeddedCode.content, vlsCtxAugmentations)
108+
}
109+
},
110+
}
111+
}
112+
113+
export default plugin
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { Code } from '@vue/language-core'
2+
3+
/**
4+
* Augments the VLS context (volar) with additianal type information.
5+
*
6+
* @param content - content retrieved from the volar pluign
7+
* @param codes - codes to add to the VLS context
8+
*/
9+
export function augmentVlsCtx(content: Code[], codes: Code[]) {
10+
let from = -1
11+
12+
for (let i = 0; i < content.length; i++) {
13+
const code = content[i]
14+
15+
if (typeof code !== 'string') {
16+
continue
17+
}
18+
19+
if (from === -1 && code.startsWith(`const __VLS_ctx`)) {
20+
from = i
21+
} else if (from !== -1) {
22+
if (code === `}`) {
23+
content.splice(i, 0, ...codes.map(code => `...${code},\n`))
24+
break
25+
} else if (code === `;\n`) {
26+
content.splice(
27+
from + 1,
28+
i - from,
29+
`{\n`,
30+
`...`,
31+
...content.slice(from + 1, i),
32+
`,\n`,
33+
...codes.map(code => `...${code},\n`),
34+
`}`,
35+
`;\n`
36+
)
37+
break
38+
}
39+
}
40+
}
41+
}

0 commit comments

Comments
 (0)