Plugin ignores defineExpose or generates all properties
Hello! I was developing a Vue 3 library with TS and encountered an error with types declarations in lib mode: plugin ignores defineExpose method.
I have a .vue component that exposes methods and variables as a public API. However, when types are generated exposed properties are missed and when used in projects TS throws error.
Code snippet
<template>
<Transition name="slide-up">
<div v-if="isVisible" class="ch-alert" data-test-id="alert-content">
<div class="ch-alert__content">
<div>
<slot />
</div>
<slot name="icon" />
</div>
<div v-if="$slots.footer" class="ch-alert__footer">
<slot name="footer" />
</div>
</div>
</Transition>
</template>
<script setup lang="ts">
import { ref, readonly, onBeforeUnmount } from 'vue'
const props = defineProps({
duration: {
type: Number,
default: 5000
},
persistent: {
type: Boolean,
default: false
}
})
const timeoutId = ref()
const isVisible = ref(false)
const show = () => {
isVisible.value = true
if (!props.persistent) {
timeoutId.value = setTimeout(hide, props.duration)
}
}
const hide = () => {
isVisible.value = false
if (!props.persistent) {
clearTimeout(timeoutId.value)
}
}
defineExpose({
show,
hide,
isVisible: readonly(isVisible)
})
onBeforeUnmount(hide)
</script>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'ChAlert'
})
</script>
Generated .vue.d.ts file
declare const _sfc_main: import("vue").DefineComponent<unknown, object, {}, import("vue").ComputedOptions, import("vue").MethodOptions, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<unknown>, {}>;
export default _sfc_main;
Workaround to this problem: add defineEmit
<script setup lang="ts">
...
defineEmits() // Useless emit that is needed only as a workaround
...
</script>
Generated type
The file contains all properties even those than were not added to defineExpose
declare const _sfc_main: import("vue").DefineComponent<{}, {
props: Readonly<import("@vue/shared").LooseRequired<Readonly<import("vue").ExtractPropTypes<{}>> & {
[x: string & `on${any}`]: (...args: any[]) => any;
}>>;
timeoutId: import("vue").Ref<any>;
isVisible: import("vue").Ref<boolean>;
show: () => void;
hide: () => void;
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, any[], any, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{}>> & {
[x: string & `on${any}`]: (...args: any[]) => any;
}, {}>;
export default _sfc_main;
When the component has defineEmits all its properties are generated into types, which is not a desired behavior, since it makes more sense to generate only public properties that were defined explicitly
vite: ^2.9.5 vue-plugin-dts: ^1.2.0
Build and generate declaration command vue-tsc --declaration --emitDeclarationOnly && vite build
Link to the project https://github.com/chocofamilyme/choco-ui/blob/feature/alert/vite.config.ts
I think it's probably a issue of vue compiler, it needs further confirmation.
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { fromUnixTime, millisecondsToSeconds, parse, parseISO } from 'date-fns'
import { localizedFormat, localizedFormatDistance, localizedFormatDistanceStrict } from '@huntersofbook/core'
interface Props {
value: string
type: 'dateTime' | 'date' | 'time' | 'timestamp' | 'unixMillisecondTimestamp' | DateFormat
format?: string
relative?: boolean
strict?: boolean
round?: 'round' | 'floor' | 'ceil'
suffix?: boolean
}
const props = withDefaults(defineProps<Props>(), {
format: 'long',
relative: false,
strict: false,
round: 'round',
suffix: true,
value: '',
type: 'ISOString',
})
const EDateFormat = {
dateTimeISO: 'yyyy-MM-dd HH:mm:ss',
dateTimeJP: 'yyyy年MM月dd日 HH時mm分ss秒',
timestampISO: 'yyyy-MM-dd HH:mm:ss.SSS',
ISOString: 'yyy-MM-dd\'T\'HH:mm:ssX',
} as const
type DateFormat = keyof typeof EDateFormat
const { t } = useI18n()
const displayValue = ref<string | null>(null)
const localValue = computed(() => {
if (!props.value)
return null
if (props.type === 'unixMillisecondTimestamp')
return parseISO(fromUnixTime(millisecondsToSeconds(Number(props.value))).toISOString())
else if (props.type === 'timestamp')
return parseISO(props.value)
else if (props.type === 'dateTime')
return parse(props.value, 'yyyy-MM-dd\'T\'HH:mm:ss', new Date())
else if (props.type === 'date')
return parse(props.value, 'yyyy-MM-dd', new Date())
else if (props.type === 'time')
return parse(props.value, 'HH:mm:ss', new Date())
try {
parse(props.value, EDateFormat[props.type], new Date())
}
catch (error) {
return null
}
return null
})
const relativeFormat = (value: Date) => {
const fn = props.strict
? localizedFormatDistanceStrict(undefined, value, new Date(), {
addSuffix: props.suffix,
roundingMethod: props.round,
})
: localizedFormatDistance(undefined, value, new Date(), {
addSuffix: props.suffix,
includeSeconds: true,
})
return fn
}
watch(
localValue,
async (newValue) => {
if (newValue === null) {
displayValue.value = null
return
}
if (props.relative) {
displayValue.value = relativeFormat(newValue)
}
else {
let format
if (props.format === 'long') {
format = `${t('date-fns_date')} ${t('date-fns_time')}`
if (props.type === 'date')
format = String(t('date-fns_date'))
if (props.type === 'time')
format = String(t('date-fns_time'))
}
else if (props.format === 'short') {
format = `${t('date-fns_date_short')} ${t('date-fns_time_short')}`
if (props.type === 'date')
format = String(t('date-fns_date_short'))
if (props.type === 'time')
format = String(t('date-fns_time_short'))
}
else {
format = props.format
}
displayValue.value = localizedFormat(undefined, newValue, format)
}
},
{ immediate: true },
)
let refreshInterval: number | null = null
onMounted(async () => {
if (!props.relative)
return
refreshInterval = window.setInterval(async () => {
if (!localValue.value)
return
displayValue.value = relativeFormat(localValue.value)
}, 60000)
})
onUnmounted(() => {
if (refreshInterval)
clearInterval(refreshInterval)
})
</script>
<template>
<span v-bind="$attrs">{{ displayValue }}</span>
</template>
some problem
code: https://github.com/huntersofbook/huntersofbook/blob/main/packages/ui/src/components/date/h-date-time.vue
repo: https://github.com/huntersofbook/huntersofbook