Description
🐛 The bug
We found another bug while working on Nuxt UI, and this one is even harder to provide a native vue-sfc-transformer
reproduction (in fact I'm not even able to make the linked one run at all).
Nuxt UI has a number of components that other than static slots (eg: label
, content
, leading
, etc..) also provides dynamic slots, meaning that you can directly access a specific item in your input array.
For example using UTabs
:
<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'
const tabs = [
{
label: 'Tab 1',
slot: 'tab1' as const,
content: 'Content 1'
},
{
label: 'Tab 2',
slot: 'tab2' as const,
content: 'Content 2'
},
{
label: 'Tab 3',
slot: 'tab3' as const,
content: 'Content 3'
}
] satisfies TabsItem[]
</script>
<template>
<UTabs :items="tabs">
<template #tab1="{ item }">
{{ item.content }}
</template>
</UTabs>
</template>
but once parsed with vue-sfc-transformer
it no longer works, and item
falls back to the default TabsItem
type (screenshot from an actual project installing @nuxt/ui
)
🛠️ To reproduce
https://stackblitz.com/edit/github-sadsa3hd?file=index.js
🌈 Expected behaviour
should provide the following auto-complete and correctly type item
(these screenshots are from within the @nuxt/ui
playground)
ℹ️ Additional context
I'm not using the latest @nuxt/ui
release as I was fixing other stuff related to dynamic slots, please use the latest CI https://pkg.pr.new/@nuxt/ui@9817465
(coming from nuxt/ui#3857)
I also tried directly accessing the types and they do seem to work (screenshot from an actual project installing @nuxt/ui
)
src
-
currently generated
Tabs.vue.d.ts
import type { VariantProps } from 'tailwind-variants'; import type { TabsRootProps, TabsRootEmits } from 'reka-ui'; import type { AvatarProps } from '../types'; import type { DynamicSlots, PartialString } from '../types/utils'; declare const tabs: import("tailwind-variants").TVReturnType<{ color: { primary: string; secondary: string; success: string; info: string; warning: string; error: string; neutral: string; }; variant: { pill: { list: string; trigger: string; indicator: string; }; link: { list: string; indicator: string; }; }; orientation: { horizontal: { root: string; list: string; indicator: string; trigger: string; }; vertical: { list: string; indicator: string; }; }; size: { xs: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; sm: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; md: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; lg: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; xl: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; }; }, { root: string; list: string; indicator: string; trigger: string[]; content: string; leadingIcon: string; leadingAvatar: string; leadingAvatarSize: string; label: string; }, undefined, { color: { primary: string; secondary: string; success: string; info: string; warning: string; error: string; neutral: string; }; variant: { pill: { list: string; trigger: string; indicator: string; }; link: { list: string; indicator: string; }; }; orientation: { horizontal: { root: string; list: string; indicator: string; trigger: string; }; vertical: { list: string; indicator: string; }; }; size: { xs: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; sm: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; md: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; lg: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; xl: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; }; }, { root: string; list: string; indicator: string; trigger: string[]; content: string; leadingIcon: string; leadingAvatar: string; leadingAvatarSize: string; label: string; }, import("tailwind-variants").TVReturnType<{ color: { primary: string; secondary: string; success: string; info: string; warning: string; error: string; neutral: string; }; variant: { pill: { list: string; trigger: string; indicator: string; }; link: { list: string; indicator: string; }; }; orientation: { horizontal: { root: string; list: string; indicator: string; trigger: string; }; vertical: { list: string; indicator: string; }; }; size: { xs: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; sm: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; md: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; lg: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; xl: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; }; }, { root: string; list: string; indicator: string; trigger: string[]; content: string; leadingIcon: string; leadingAvatar: string; leadingAvatarSize: string; label: string; }, undefined, { color: { primary: string; secondary: string; success: string; info: string; warning: string; error: string; neutral: string; }; variant: { pill: { list: string; trigger: string; indicator: string; }; link: { list: string; indicator: string; }; }; orientation: { horizontal: { root: string; list: string; indicator: string; trigger: string; }; vertical: { list: string; indicator: string; }; }; size: { xs: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; sm: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; md: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; lg: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; xl: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; }; }, { root: string; list: string; indicator: string; trigger: string[]; content: string; leadingIcon: string; leadingAvatar: string; leadingAvatarSize: string; label: string; }, import("tailwind-variants").TVReturnType<{ color: { primary: string; secondary: string; success: string; info: string; warning: string; error: string; neutral: string; }; variant: { pill: { list: string; trigger: string; indicator: string; }; link: { list: string; indicator: string; }; }; orientation: { horizontal: { root: string; list: string; indicator: string; trigger: string; }; vertical: { list: string; indicator: string; }; }; size: { xs: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; sm: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; md: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; lg: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; xl: { trigger: string; leadingIcon: string; leadingAvatarSize: string; }; }; }, { root: string; list: string; indicator: string; trigger: string[]; content: string; leadingIcon: string; leadingAvatar: string; leadingAvatarSize: string; label: string; }, undefined, unknown, unknown, undefined>>>; export interface TabsItem { label?: string; /** * @IconifyIcon */ icon?: string; avatar?: AvatarProps; slot?: string; content?: string; /** A unique value for the tab item. Defaults to the index. */ value?: string | number; disabled?: boolean; [key: string]: any; } type TabsVariants = VariantProps<typeof tabs>; export interface TabsProps<T extends TabsItem = TabsItem> extends Pick<TabsRootProps<string | number>, 'defaultValue' | 'modelValue' | 'activationMode' | 'unmountOnHide'> { /** * The element or component this component should render as. * @defaultValue 'div' */ as?: any; items?: T[]; /** * @defaultValue 'primary' */ color?: TabsVariants['color']; /** * @defaultValue 'pill' */ variant?: TabsVariants['variant']; /** * @defaultValue 'md' */ size?: TabsVariants['size']; /** * The orientation of the tabs. * @defaultValue 'horizontal' */ orientation?: TabsRootProps['orientation']; /** * The content of the tabs, can be disabled to prevent rendering the content. * @defaultValue true */ content?: boolean; /** * The key used to get the label from the item. * @defaultValue 'label' */ labelKey?: string; class?: any; ui?: PartialString<typeof tabs.slots>; } export interface TabsEmits extends TabsRootEmits<string | number> { } type SlotProps<T extends TabsItem> = (props: { item: T; index: number; }) => any; export type TabsSlots<T extends TabsItem = TabsItem> = { 'leading': SlotProps<T>; 'default': SlotProps<T>; 'trailing': SlotProps<T>; 'content': SlotProps<T>; 'list-leading': (props?: {}) => any; 'list-trailing': (props?: {}) => any; } & DynamicSlots<T, undefined, { index: number; }>; declare const _default: <T extends TabsItem>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_expose?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{ props: __VLS_PrettifyLocal<any & TabsProps<T> & Partial<{}>> & (import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps); expose(exposed: import("vue").ShallowUnwrapRef<{}>): void; attrs: any; slots: Readonly<{ leading: SlotProps<T>; default: SlotProps<T>; trailing: SlotProps<T>; content: SlotProps<T>; 'list-leading': (props?: {}) => any; 'list-trailing': (props?: {}) => any; } & { [K in NonNullable<T["slot"]> as K extends string ? K : never]: (props: { item: Extract<T, { slot: K; }>; } & { index: number; }) => any; } & { [key: string]: (props: { item: T; } & { index: number; }) => any; }> & { leading: SlotProps<T>; default: SlotProps<T>; trailing: SlotProps<T>; content: SlotProps<T>; 'list-leading': (props?: {}) => any; 'list-trailing': (props?: {}) => any; } & { [K in NonNullable<T["slot"]> as K extends string ? K : never]: (props: { item: Extract<T, { slot: K; }>; } & { index: number; }) => any; } & { [key: string]: (props: { item: T; } & { index: number; }) => any; }; emit: (evt: "update:modelValue", payload: string | number) => void; }>) => import("vue").VNode & { __ctx?: Awaited<typeof __VLS_setup>; }; export default _default; type __VLS_PrettifyLocal<T> = { [K in keyof T]: T[K]; } & {};