Skip to content

Commit

Permalink
feat(volar): support defineSlots for vue2 (#525)
Browse files Browse the repository at this point in the history
  • Loading branch information
zhiyuanzmj committed Oct 24, 2023
1 parent 6c44c67 commit e425c90
Show file tree
Hide file tree
Showing 11 changed files with 94 additions and 65 deletions.
5 changes: 5 additions & 0 deletions .changeset/many-parrots-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@vue-macros/volar': patch
---

support defineSlots for vue2
15 changes: 7 additions & 8 deletions docs/macros/define-slots.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@ Declaring type of SFC slots in `<script setup>` using the `defineSlots`.

For Vue >= 3.3, this feature will be turned off by default.

| Features | Supported |
| :------------------: | :----------------: |
| Vue 3 | :white_check_mark: |
| Nuxt 3 | :white_check_mark: |
| Vue 2 | :white_check_mark: |
| Volar Plugin + Vue 3 | :white_check_mark: |
| Volar Plugin + Vue 2 | :x: |
| Features | Supported |
| :----------: | :----------------: |
| Vue 3 | :white_check_mark: |
| Nuxt 3 | :white_check_mark: |
| Vue 2 | :white_check_mark: |
| Volar Plugin | :white_check_mark: |

## Basic Usage

Expand Down Expand Up @@ -46,7 +45,7 @@ defineSlots<{
// tsconfig.json
{
"vueCompilerOptions": {
"target": 3, // or 2.7 is not supported by Volar.
"target": 3, // or 2.7 for Vue 2
"plugins": [
"@vue-macros/volar/define-slots"
// ...more feature
Expand Down
15 changes: 7 additions & 8 deletions docs/zh-CN/macros/define-slots.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@

在 Vue >= 3.3 中,此功能将默认关闭。

| 特性 | 支持 |
| :------------------: | :----------------: |
| Vue 3 | :white_check_mark: |
| Nuxt 3 | :white_check_mark: |
| Vue 2 | :white_check_mark: |
| Volar Plugin + Vue 3 | :white_check_mark: |
| Volar Plugin + Vue 2 | :x: |
| 特性 | 支持 |
| :----------: | :----------------: |
| Vue 3 | :white_check_mark: |
| Nuxt 3 | :white_check_mark: |
| Vue 2 | :white_check_mark: |
| Volar Plugin | :white_check_mark: |

## 基本用法

Expand Down Expand Up @@ -46,7 +45,7 @@ defineSlots<{
// tsconfig.json
{
"vueCompilerOptions": {
"target": 3, // Volar 暂不支持 2.7 版本
"target": 3, // 2.7 用于 Vue 2
"plugins": [
"@vue-macros/volar/define-slots"
// ...更多功能
Expand Down
78 changes: 46 additions & 32 deletions packages/volar/src/define-slots.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,45 @@
import { FileKind, FileRangeCapabilities } from '@volar/language-core'
import { FileKind } from '@volar/language-core'
import { DEFINE_SLOTS } from '@vue-macros/common'
import {
type Segment,
type Sfc,
type VueLanguagePlugin,
replace,
toString,
replaceSourceRange,
} from '@vue/language-core'
import type { VueEmbeddedFile } from '@vue/language-core/out/virtualFile/embeddedFile'

function transform({
embeddedFile,
typeArg,
sfc,
vueVersion,
}: {
embeddedFile: VueEmbeddedFile
typeArg: import('typescript/lib/tsserverlibrary').TypeNode
sfc: Sfc
vueVersion: number
}) {
if (embeddedFile.kind !== FileKind.TypeScriptHostFile) return
const textContent = toString(embeddedFile.content)
if (
!textContent.includes(DEFINE_SLOTS) ||
!textContent.includes('return __VLS_slots')
replaceSourceRange(
embeddedFile.content,
'scriptSetup',
typeArg.pos,
typeArg.pos,
'__VLS_DefineSlots<'
)
return

replace(
replaceSourceRange(
embeddedFile.content,
/var __VLS_slots!: [\S\s]*?;/,
'var __VLS_slots!: __VLS_DefineSlots<',
(): Segment<FileRangeCapabilities> => [
// slots type
sfc.scriptSetup!.content.slice(typeArg.pos, typeArg.end),
'scriptSetup',
typeArg!.pos,
FileRangeCapabilities.full,
],
'>;'
'scriptSetup',
typeArg.end,
typeArg.end,
'>'
)

embeddedFile.content.push(
`type __VLS_DefineSlots<T> = { [SlotName in keyof T]: T[SlotName] extends Function ? T[SlotName] : (_: T[SlotName]) => any }`
`type __VLS_DefineSlots<T> = { [SlotName in keyof T]: T[SlotName] extends Function ? T[SlotName] : (_: T[SlotName]) => any };\n`
)

if (vueVersion < 3) {
embeddedFile.content.push(
`declare function defineSlots<S extends Record<string, any> = Record<string, any>>(): S;\n`
)
}
}

function getTypeArg(
Expand All @@ -53,32 +51,48 @@ function getTypeArg(
!(
ts.isCallExpression(node) &&
ts.isIdentifier(node.expression) &&
node.expression.text === DEFINE_SLOTS &&
node.expression.escapedText === DEFINE_SLOTS &&
node.typeArguments?.length === 1
)
)
return undefined
return node.typeArguments[0]
}

const sourceFile = sfc.scriptSetupAst
return sourceFile?.forEachChild((node) => {
if (!ts.isExpressionStatement(node)) return
return getCallArg(node.expression)
return sfc.scriptSetupAst?.forEachChild((node) => {
if (ts.isExpressionStatement(node)) {
return getCallArg(node.expression)
} else if (ts.isVariableStatement(node)) {
return node.declarationList.forEachChild((decl) => {
if (ts.isVariableDeclaration(decl) && decl.initializer)
return getCallArg(decl.initializer)
})
}
})
}

const plugin: VueLanguagePlugin = ({ modules: { typescript: ts } }) => {
const plugin: VueLanguagePlugin = ({
modules: { typescript: ts },
vueCompilerOptions,
}) => {
return {
name: 'vue-macros-define-slots',
version: 1,
resolveEmbeddedFile(fileName, sfc, embeddedFile) {
if (
embeddedFile.kind !== FileKind.TypeScriptHostFile ||
!sfc.scriptSetup ||
!sfc.scriptSetupAst
)
return

const typeArg = getTypeArg(ts, sfc)
if (!typeArg) return

transform({
embeddedFile,
typeArg,
sfc,
vueVersion: vueCompilerOptions.target,
})
},
}
Expand Down
37 changes: 29 additions & 8 deletions packages/volar/src/jsx-directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
type Segment,
type Sfc,
type VueLanguagePlugin,
getSlotsPropertyName,
replaceSourceRange,
} from '@vue/language-core'

Expand All @@ -17,6 +18,7 @@ type TransformOptions = {
sfc: Sfc
ts: typeof import('typescript/lib/tsserverlibrary')
source: 'script' | 'scriptSetup'
vueVersion?: number
}

function transformVIf({
Expand Down Expand Up @@ -172,15 +174,17 @@ function transformVSlot({
ts,
sfc,
source,
vueVersion,
}: TransformOptions & {
nodes: import('typescript/lib/tsserverlibrary').JsxElement[]
}) {
if (nodes.length === 0) return
codes.push(`type __VLS_getSlots<T> = T extends new (...args: any) => any
? InstanceType<T>['$slots']
: T extends (props: any, ctx: infer Ctx) => any
? Ctx['slots']
: any`)
codes.push(`type __VLS_getSlots<T> = T extends new () => { '${getSlotsPropertyName(
vueVersion || 3
)}': infer S } ? NonNullable<S>
: T extends (props: any, ctx: infer Ctx extends { slots: any }) => any
? NonNullable<Ctx['slots']>
: {}`)

nodes.forEach((node) => {
if (!ts.isIdentifier(node.openingElement.tagName)) return
Expand Down Expand Up @@ -341,7 +345,13 @@ function transformVModel({
})
}

function transformJsxDirective({ codes, sfc, ts, source }: TransformOptions) {
function transformJsxDirective({
codes,
sfc,
ts,
source,
vueVersion,
}: TransformOptions) {
const vIfAttributeMap = new Map<any, JsxAttributeNode[]>()
const vForAttributes: JsxAttributeNode[] = []
const vSlotNodeSet = new Set<
Expand Down Expand Up @@ -429,15 +439,25 @@ function transformJsxDirective({ codes, sfc, ts, source }: TransformOptions) {
}
sfc[`${source}Ast`]!.forEachChild(walkJsxDirective)

transformVSlot({ nodes: Array.from(vSlotNodeSet), codes, sfc, ts, source })
transformVSlot({
nodes: Array.from(vSlotNodeSet),
codes,
sfc,
ts,
source,
vueVersion,
})
transformVFor({ nodes: vForAttributes, codes, sfc, ts, source })
vIfAttributeMap.forEach((nodes) =>
transformVIf({ nodes, codes, sfc, ts, source })
)
transformVModel({ nodes: vModelAttributes, codes, sfc, ts, source })
}

const plugin: VueLanguagePlugin = ({ modules: { typescript: ts } }) => {
const plugin: VueLanguagePlugin = ({
modules: { typescript: ts },
vueCompilerOptions,
}) => {
return {
name: 'vue-macros-jsx-directive',
version: 1,
Expand All @@ -452,6 +472,7 @@ const plugin: VueLanguagePlugin = ({ modules: { typescript: ts } }) => {
sfc,
ts,
source,
vueVersion: vueCompilerOptions.target,
})
}
},
Expand Down
1 change: 0 additions & 1 deletion playground/vue2/src/examples/define-slots/child.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<script setup lang="ts">
// @ts-nocheck
import { Fail, Ok } from '../../assert'
export type TitleScope = { foo: boolean | 'foo' }
Expand Down
2 changes: 0 additions & 2 deletions playground/vue2/src/examples/define-slots/parent.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
<script setup lang="ts">
// @ts-nocheck
import { expectTypeOf } from 'expect-type'
import { Assert } from '../../assert'
import Child, { type DefaultScope, type TitleScope } from './child.vue'
Expand Down
2 changes: 0 additions & 2 deletions playground/vue2/src/examples/jsx-directive/v-slot/child.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
<script setup lang="tsx">
// @ts-nocheck
defineSlots<{
default: () => any
bottom: (props: { foo: 1 }) => any
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<script setup lang="tsx">
// @ts-nocheck
import Child from './child.vue'
defineRender(() => (
Expand Down
1 change: 0 additions & 1 deletion playground/vue3/src/examples/define-slots/child.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<script setup lang="ts">
// @ts-nocheck
import { Fail, Ok } from '../../assert'
export type TitleScope = { foo: boolean | 'foo' }
Expand Down
2 changes: 0 additions & 2 deletions playground/vue3/src/examples/define-slots/parent.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
<script setup lang="ts">
// @ts-nocheck
import { expectTypeOf } from 'expect-type'
import { Assert } from '../../assert'
import Child, { type DefaultScope, type TitleScope } from './child.vue'
Expand Down

0 comments on commit e425c90

Please sign in to comment.