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/jsx-directive): add type to the functional template-ref #674

Merged
merged 2 commits into from
May 4, 2024
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/perfect-trains-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@vue-macros/volar": patch
---

add type to the functional template-ref
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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