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): support defineSlots for vue2 #525

Merged
merged 7 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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