Skip to content

Commit

Permalink
feat(volar/jsx-directive): add type to the functional template-ref (#674
Browse files Browse the repository at this point in the history
)

* feat(volar/jsx-directive): add type to the functional template-ref

* chore: add changeset
  • Loading branch information
zhiyuanzmj committed May 4, 2024
1 parent 3227507 commit b3c9022
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 60 deletions.
5 changes: 5 additions & 0 deletions .changeset/perfect-trains-design.md
@@ -0,0 +1,5 @@
---
"@vue-macros/volar": patch
---

add type to the functional template-ref
Expand Up @@ -39,7 +39,8 @@ defineRender(() => (
{expectTypeOf<string>(foo)}
</span>,}}></Child>
<Child scopedSlots={{...(show) ? {'title': ({ foo }) => <span>
<Child<number> scopedSlots={{...(show) ? {'title': ({ foo }) => <span>
{expectTypeOf<number>(foo)}
{show}
</span>,} : (show === false) ? {'center': ({ foo }) => <span>
{foo}
Expand Down Expand Up @@ -96,7 +97,8 @@ defineRender(() => (
{expectTypeOf<string>(foo)}
</>,}}></Child>
<Child v-slots={{...(show) ? {'title': ({ foo }) => <>
<Child<number> v-slots={{...(show) ? {'title': ({ foo }) => <>
{expectTypeOf<number>(foo)}
{show}
</>,} : (show === false) ? {'center': ({ foo }) => <>
{foo}
Expand Down
3 changes: 2 additions & 1 deletion packages/jsx-directive/tests/fixtures/v-slot/index.vue
Expand Up @@ -35,9 +35,10 @@ defineRender(() => (
{expectTypeOf<string>(foo)}
</Child>
<Child>
<Child<number>>
default
<template v-if={show} v-slot:title={{ foo }}>
{expectTypeOf<number>(foo)}
{show}
</template>
<template v-else-if={show === false} v-slot:center={{ foo }}>
Expand Down
53 changes: 53 additions & 0 deletions packages/volar/src/jsx-directive/context.ts
@@ -0,0 +1,53 @@
import { getText, isJsxExpression } from '../common'
import {
type JsxDirective,
type TransformOptions,
getOpeningElement,
getTagName,
} from './index'

export function transformCtx(
node: JsxDirective['node'],
index: number,
options: TransformOptions,
) {
const { ts, codes, sfc } = options

const openingElement = getOpeningElement(node, options)
if (!openingElement) return ''

let props = ''
for (const prop of openingElement.attributes.properties) {
if (!ts.isJsxAttribute(prop)) continue
const name = getText(prop.name, options)
if (name.startsWith('v-')) continue

const value = isJsxExpression(prop.initializer)
? getText(prop.initializer.expression!, options)
: 'true'
props += `'${name}': ${value},`
}

const tagName = getTagName(node, { ...options, withTypes: true })
const ctxName = `__VLS_ctx${index}`
if (!codes.toString().includes('function __VLS_getFunctionalComponentCtx')) {
codes.push(
`function __VLS_getFunctionalComponentCtx<T, K>(comp: T, compInstance: K): __VLS_PickNotAny<
'__ctx' extends keyof __VLS_PickNotAny<K, {}> ? K extends { __ctx?: infer Ctx } ? Ctx : never : any
, T extends (props: infer P, ctx: infer Ctx) => any ? Ctx & { props: P } : any>;
`,
)
}

const code = `const ${ctxName} = __VLS_getFunctionalComponentCtx(${tagName}, __VLS_asFunctionalComponent(${tagName})({${props}}));\n`
if (sfc.scriptSetup?.generic) {
const index = codes.findIndex((code) =>
code.includes('__VLS_setup = (async () => {'),
)
codes.splice(index + 1, 0, code)
} else {
codes.push(code)
}

return ctxName
}
80 changes: 25 additions & 55 deletions packages/volar/src/jsx-directive/index.ts
@@ -1,10 +1,12 @@
import { getText, isJsxExpression } from '../common'
import { getText } from '../common'
import { type VSlotMap, transformVSlot } from './v-slot'
import { transformVFor } from './v-for'
import { transformVIf } from './v-if'
import { transformVModel } from './v-model'
import { transformVOn, transformVOnWithModifiers } from './v-on'
import { transformVBind } from './v-bind'
import { transformRef } from './ref'
import { transformCtx } from './context'
import type { Code, Sfc } from '@vue/language-core'

export type JsxDirective = {
Expand Down Expand Up @@ -34,6 +36,7 @@ export function transformJsxDirective(options: TransformOptions) {
const vOnNodes: JsxDirective[] = []
const vOnWithModifiers: JsxDirective[] = []
const vBindNodes: JsxDirective[] = []
const refNodes: JsxDirective[] = []

const ctxNodeSet = new Set<JsxDirective['node']>()

Expand All @@ -42,15 +45,11 @@ export function transformJsxDirective(options: TransformOptions) {
parent?: import('typescript').Node,
) {
const tagName = getTagName(node, options)
const properties = ts.isJsxElement(node)
? node.openingElement.attributes.properties
: ts.isJsxSelfClosingElement(node)
? node.attributes.properties
: []
const properties = getOpeningElement(node, options)
let vIfAttribute
let vForAttribute
let vSlotAttribute
for (const attribute of properties) {
for (const attribute of properties?.attributes.properties || []) {
if (!ts.isJsxAttribute(attribute)) continue
const attributeName = getText(attribute.name, options)

Expand All @@ -76,6 +75,8 @@ export function transformJsxDirective(options: TransformOptions) {
vOnWithModifiers.push({ node, attribute })
} else if (/^(?!v-)\S+[_|-]\S+/.test(attributeName)) {
vBindNodes.push({ node, attribute })
} else if (attributeName === 'ref') {
refNodes.push({ node, attribute })
}
}

Expand Down Expand Up @@ -169,66 +170,35 @@ export function transformJsxDirective(options: TransformOptions) {
transformVOn(vOnNodes, ctxMap, options)
transformVOnWithModifiers(vOnWithModifiers, options)
transformVBind(vBindNodes, options)
transformRef(refNodes, ctxMap, options)
}

export function getTagName(
export function getOpeningElement(
node: JsxDirective['node'],
options: TransformOptions,
) {
const { ts } = options

return ts.isJsxSelfClosingElement(node)
? getText(node.tagName, options)
? node
: ts.isJsxElement(node)
? getText(node.openingElement.tagName, options)
: ''
? node.openingElement
: undefined
}

function transformCtx(
export function getTagName(
node: JsxDirective['node'],
index: number,
options: TransformOptions,
options: TransformOptions & { withTypes?: boolean },
) {
const { ts, codes, sfc } = options

const tag = ts.isJsxSelfClosingElement(node)
? node
: ts.isJsxElement(node)
? node.openingElement
: null
if (!tag) return ''

let props = ''
for (const prop of tag.attributes.properties) {
if (!ts.isJsxAttribute(prop)) continue
const name = getText(prop.name, options)
if (name.startsWith('v-')) continue

const value = isJsxExpression(prop.initializer)
? getText(prop.initializer.expression!, options)
: 'true'
props += `'${name}': ${value},`
}

const tagName = getTagName(node, options)
const ctxName = `__VLS_ctx${index}`
if (!codes.toString().includes('function __VLS_getFunctionalComponentCtx')) {
codes.push(
`function __VLS_getFunctionalComponentCtx<T, K>(comp: T, compInstance: K): __VLS_PickNotAny<
'__ctx' extends keyof __VLS_PickNotAny<K, {}> ? K extends { __ctx?: infer Ctx } ? Ctx : never : any
, T extends (props: infer P, ctx: infer Ctx) => any ? Ctx & { props: P } : any>;
`,
)
}

const code = `const ${ctxName} = __VLS_getFunctionalComponentCtx(${tagName}, __VLS_asFunctionalComponent(${tagName})({${props}}));\n`
if (sfc.scriptSetup?.generic) {
const index = codes.findIndex((code) =>
code.includes('__VLS_setup = (async () => {'),
)
codes.splice(index + 1, 0, code)
} else {
codes.push(code)
const openingElement = getOpeningElement(node, options)
if (!openingElement) return ''

let types = ''
if (options.withTypes && openingElement.typeArguments?.length) {
types = `<${openingElement.typeArguments
.map((argument) => getText(argument, options))
.join(', ')}>`
}

return ctxName
return getText(openingElement.tagName, options) + types
}
38 changes: 38 additions & 0 deletions packages/volar/src/jsx-directive/ref.ts
@@ -0,0 +1,38 @@
import { allCodeFeatures, replaceSourceRange } from '@vue/language-core'
import { getStart, getText, isJsxExpression } from '../common'
import type { JsxDirective, TransformOptions } from './index'

export function transformRef(
nodes: JsxDirective[],
ctxMap: Map<JsxDirective['node'], string>,
options: TransformOptions,
) {
const { codes, source, ts } = options

for (const { node, attribute } of nodes) {
if (
attribute.initializer &&
isJsxExpression(attribute.initializer) &&
attribute.initializer.expression &&
(ts.isFunctionExpression(attribute.initializer.expression) ||
ts.isArrowFunction(attribute.initializer.expression))
) {
replaceSourceRange(
codes,
source,
getStart(attribute, options),
attribute.end,
'{...({ ',
['ref', source, getStart(attribute.name, options), allCodeFeatures],
': ',
[
getText(attribute.initializer.expression, options),
source,
getStart(attribute.initializer.expression, options),
allCodeFeatures,
],
`} satisfies { ref: (e: Parameters<typeof ${ctxMap.get(node)}.expose>[0]) => any }) as any}`,
)
}
}
}
10 changes: 8 additions & 2 deletions playground/vue3/src/examples/jsx-directive/v-slot/index.vue
@@ -1,6 +1,6 @@
<script setup lang="tsx" generic="T">
import { type FunctionalComponent, shallowRef } from 'vue'
import Child from './child.vue'
import type { FunctionalComponent } from 'vue'
const Comp: FunctionalComponent<
{},
Expand All @@ -18,11 +18,17 @@ const Comp: FunctionalComponent<
)
}
const childRef = shallowRef<InstanceType<typeof Child>>()
defineRender(() => (
<fieldset>
<legend>v-slot</legend>
<Child>
<Child
ref={(e) => {
childRef.value = e
}}
>
<Comp v-slot={{ foo }}>
<div>default: {foo}</div>
</Comp>
Expand Down

0 comments on commit b3c9022

Please sign in to comment.