unplugin-dts icon indicating copy to clipboard operation
unplugin-dts copied to clipboard

Plugin ignores defineExpose or generates all properties

Open KirillSolovyev opened this issue 3 years ago • 2 comments

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

KirillSolovyev avatar Aug 10 '22 12:08 KirillSolovyev

I think it's probably a issue of vue compiler, it needs further confirmation.

qmhc avatar Aug 11 '22 07:08 qmhc

<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

productdevbook avatar Aug 24 '22 07:08 productdevbook