Skip to content

Commit

Permalink
feat(types/slots): support slot presence / props type checks via `def…
Browse files Browse the repository at this point in the history
…ineSlots` macro and `slots` option (#7982)
  • Loading branch information
sxzz authored Apr 3, 2023
1 parent 59e8284 commit 5a2f5d5
Show file tree
Hide file tree
Showing 16 changed files with 380 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1785,6 +1785,51 @@ return { props, emit }
})"
`;

exports[`SFC compile <script setup> > with TypeScript > defineSlots() > basic usage 1`] = `
"import { useSlots as _useSlots, defineComponent as _defineComponent } from 'vue'

export default /*#__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) {
__expose();

const slots = _useSlots()

return { slots }
}

})"
`;

exports[`SFC compile <script setup> > with TypeScript > defineSlots() > w/o generic params 1`] = `
"import { useSlots as _useSlots } from 'vue'

export default {
setup(__props, { expose: __expose }) {
__expose();

const slots = _useSlots()

return { slots }
}

}"
`;

exports[`SFC compile <script setup> > with TypeScript > defineSlots() > w/o return value 1`] = `
"import { defineComponent as _defineComponent } from 'vue'

export default /*#__PURE__*/_defineComponent({
setup(__props, { expose: __expose }) {
__expose();



return { }
}

})"
`;

exports[`SFC compile <script setup> > with TypeScript > hoist type declarations 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export interface Foo {}
Expand Down
39 changes: 39 additions & 0 deletions packages/compiler-sfc/__tests__/compileScript.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1585,6 +1585,45 @@ const emit = defineEmits(['a', 'b'])
assertCode(content)
})

describe('defineSlots()', () => {
test('basic usage', () => {
const { content } = compile(`
<script setup lang="ts">
const slots = defineSlots<{
default: { msg: string }
}>()
</script>
`)
assertCode(content)
expect(content).toMatch(`const slots = _useSlots()`)
expect(content).not.toMatch('defineSlots')
})

test('w/o return value', () => {
const { content } = compile(`
<script setup lang="ts">
defineSlots<{
default: { msg: string }
}>()
</script>
`)
assertCode(content)
expect(content).not.toMatch('defineSlots')
expect(content).not.toMatch(`_useSlots`)
})

test('w/o generic params', () => {
const { content } = compile(`
<script setup>
const slots = defineSlots()
</script>
`)
assertCode(content)
expect(content).toMatch(`const slots = _useSlots()`)
expect(content).not.toMatch('defineSlots')
})
})

test('runtime Enum', () => {
const { content, bindings } = compile(
`<script setup lang="ts">
Expand Down
42 changes: 40 additions & 2 deletions packages/compiler-sfc/src/compileScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ const DEFINE_EMITS = 'defineEmits'
const DEFINE_EXPOSE = 'defineExpose'
const WITH_DEFAULTS = 'withDefaults'
const DEFINE_OPTIONS = 'defineOptions'
const DEFINE_SLOTS = 'defineSlots'

const isBuiltInDir = makeMap(
`once,memo,if,for,else,else-if,slot,text,html,on,bind,model,show,cloak,is`
Expand Down Expand Up @@ -312,6 +313,7 @@ export function compileScript(
let hasDefaultExportName = false
let hasDefaultExportRender = false
let hasDefineOptionsCall = false
let hasDefineSlotsCall = false
let propsRuntimeDecl: Node | undefined
let propsRuntimeDefaults: Node | undefined
let propsDestructureDecl: Node | undefined
Expand Down Expand Up @@ -590,6 +592,30 @@ export function compileScript(
return true
}

function processDefineSlots(node: Node, declId?: LVal): boolean {
if (!isCallOf(node, DEFINE_SLOTS)) {
return false
}
if (hasDefineSlotsCall) {
error(`duplicate ${DEFINE_SLOTS}() call`, node)
}
hasDefineSlotsCall = true

if (node.arguments.length > 0) {
error(`${DEFINE_SLOTS}() cannot accept arguments`, node)
}

if (declId) {
s.overwrite(
startOffset + node.start!,
startOffset + node.end!,
`${helper('useSlots')}()`
)
}

return true
}

function getAstBody(): Statement[] {
return scriptAst
? [...scriptSetupAst.body, ...scriptAst.body]
Expand Down Expand Up @@ -683,6 +709,7 @@ export function compileScript(
let propsOption = undefined
let emitsOption = undefined
let exposeOption = undefined
let slotsOption = undefined
if (optionsRuntimeDecl.type === 'ObjectExpression') {
for (const prop of optionsRuntimeDecl.properties) {
if (
Expand All @@ -692,6 +719,7 @@ export function compileScript(
if (prop.key.name === 'props') propsOption = prop
if (prop.key.name === 'emits') emitsOption = prop
if (prop.key.name === 'expose') exposeOption = prop
if (prop.key.name === 'slots') slotsOption = prop
}
}
}
Expand All @@ -714,6 +742,12 @@ export function compileScript(
exposeOption
)
}
if (slotsOption) {
error(
`${DEFINE_OPTIONS}() cannot be used to declare slots. Use ${DEFINE_SLOTS}() instead.`,
slotsOption
)
}

return true
}
Expand Down Expand Up @@ -1286,7 +1320,8 @@ export function compileScript(
processDefineProps(expr) ||
processDefineEmits(expr) ||
processDefineOptions(expr) ||
processWithDefaults(expr)
processWithDefaults(expr) ||
processDefineSlots(expr)
) {
s.remove(node.start! + startOffset, node.end! + startOffset)
} else if (processDefineExpose(expr)) {
Expand Down Expand Up @@ -1320,7 +1355,10 @@ export function compileScript(
const isDefineProps =
processDefineProps(init, decl.id) ||
processWithDefaults(init, decl.id)
const isDefineEmits = processDefineEmits(init, decl.id)
const isDefineEmits =
!isDefineProps && processDefineEmits(init, decl.id)
!isDefineEmits && processDefineSlots(init, decl.id)

if (isDefineProps || isDefineEmits) {
if (left === 1) {
s.remove(node.start! + startOffset, node.end! + startOffset)
Expand Down
69 changes: 68 additions & 1 deletion packages/dts-test/defineComponent.test-d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import {
ComponentPublicInstance,
ComponentOptions,
SetupContext,
h
h,
SlotsType,
Slots,
VNode
} from 'vue'
import { describe, expectType, IsUnion } from './utils'

Expand Down Expand Up @@ -1406,6 +1409,69 @@ export default {
})
}

describe('slots', () => {
const comp1 = defineComponent({
slots: Object as SlotsType<{
default: { foo: string; bar: number }
optional?: { data: string }
undefinedScope: undefined | { data: string }
optionalUndefinedScope?: undefined | { data: string }
}>,
setup(props, { slots }) {
expectType<(scope: { foo: string; bar: number }) => VNode[]>(
slots.default
)
expectType<((scope: { data: string }) => VNode[]) | undefined>(
slots.optional
)

slots.default({ foo: 'foo', bar: 1 })

// @ts-expect-error it's optional
slots.optional({ data: 'foo' })
slots.optional?.({ data: 'foo' })

expectType<{
(): VNode[]
(scope: undefined | { data: string }): VNode[]
}>(slots.undefinedScope)

expectType<
| { (): VNode[]; (scope: undefined | { data: string }): VNode[] }
| undefined
>(slots.optionalUndefinedScope)

slots.default({ foo: 'foo', bar: 1 })
// @ts-expect-error it's optional
slots.optional({ data: 'foo' })
slots.optional?.({ data: 'foo' })
slots.undefinedScope()
slots.undefinedScope(undefined)
// @ts-expect-error
slots.undefinedScope('foo')

slots.optionalUndefinedScope?.()
slots.optionalUndefinedScope?.(undefined)
slots.optionalUndefinedScope?.({ data: 'foo' })
// @ts-expect-error
slots.optionalUndefinedScope()
// @ts-expect-error
slots.optionalUndefinedScope?.('foo')

expectType<typeof slots | undefined>(new comp1().$slots)
}
})

const comp2 = defineComponent({
setup(props, { slots }) {
// unknown slots
expectType<Slots>(slots)
expectType<((...args: any[]) => VNode[]) | undefined>(slots.default)
}
})
expectType<Slots | undefined>(new comp2().$slots)
})

import {
DefineComponent,
ComponentOptionsMixin,
Expand All @@ -1428,6 +1494,7 @@ declare const MyButton: DefineComponent<
ComponentOptionsMixin,
EmitsOptions,
string,
{},
VNodeProps & AllowedComponentProps & ComponentCustomProps,
Readonly<ExtractPropTypes<{}>>,
{}
Expand Down
28 changes: 27 additions & 1 deletion packages/dts-test/functionalComponent.test-d.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { h, Text, FunctionalComponent, Component } from 'vue'
import { h, Text, FunctionalComponent, Component, VNode } from 'vue'
import { expectType } from './utils'

// simple function signature
Expand Down Expand Up @@ -68,3 +68,29 @@ const Qux: FunctionalComponent<{}, ['foo', 'bar']> = (props, { emit }) => {
}

expectType<Component>(Qux)

const Quux: FunctionalComponent<
{},
{},
{
default: { foo: number }
optional?: { foo: number }
}
> = (props, { emit, slots }) => {
expectType<{
default: (scope: { foo: number }) => VNode[]
optional?: (scope: { foo: number }) => VNode[]
}>(slots)

slots.default({ foo: 123 })
// @ts-expect-error
slots.default({ foo: 'fesf' })

slots.optional?.({ foo: 123 })
// @ts-expect-error
slots.optional?.({ foo: 'fesf' })
// @ts-expect-error
slots.optional({ foo: 123 })
}
expectType<Component>(Quux)
;<Quux />
25 changes: 24 additions & 1 deletion packages/dts-test/setupHelpers.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import {
useAttrs,
useSlots,
withDefaults,
Slots
Slots,
defineSlots,
VNode
} from 'vue'
import { describe, expectType } from './utils'

Expand Down Expand Up @@ -179,6 +181,27 @@ describe('defineEmits w/ runtime declaration', () => {
emit2('baz')
})

describe('defineSlots', () => {
// short syntax
const slots = defineSlots<{
default: { foo: string; bar: number }
optional?: string
}>()
expectType<(scope: { foo: string; bar: number }) => VNode[]>(slots.default)
expectType<undefined | ((scope: string) => VNode[])>(slots.optional)

// literal fn syntax (allow for specifying return type)
const fnSlots = defineSlots<{
default(props: { foo: string; bar: number }): any
optional?(props: string): any
}>()
expectType<(scope: { foo: string; bar: number }) => VNode[]>(fnSlots.default)
expectType<undefined | ((scope: string) => VNode[])>(fnSlots.optional)

const slotsUntype = defineSlots()
expectType<Slots>(slotsUntype)
})

describe('useAttrs', () => {
const attrs = useAttrs()
expectType<Record<string, unknown>>(attrs)
Expand Down
Loading

0 comments on commit 5a2f5d5

Please sign in to comment.