首页
/ 基于 tsdown 构建 Vue 组件库:从 SFC 编译、类型声明到 npm 发布的完整方案

基于 tsdown 构建 Vue 组件库:从 SFC 编译、类型声明到 npm 发布的完整方案

2026-09-08 11:19:17作者:晏闻田Solitary

本篇技术指南讲解如何用 tsdown(基于 Rolldown/Oxc 的高性能 JS/TS 库打包器)构建可发布的 Vue 组件库:从 unplugin-vue 编译单文件组件、到 vue-tsc 生成组件级 .d.ts 类型声明,再到 package.json 的现代 exports 配置。它面向在当前项目中维护共享 Vue 组件/Composable 的开发者,读完后你将掌握一套可直接复制运行的 tsdown + Vue 配置模板,并理解 SFC、JSX、多入口、Monorepo 等场景下的底层原理与常见坑。该指南对应仓库中 tsdown 技能包(.agents/skills/tsdown)里的 Vue 配方文档,正文会同时结合仓库内其他 tsdown 配置(如 packages/stream-kit/tsdown.config.tspackages/plugin-sdk/tsdown.config.ts)佐证说明。

概述:tsdown 为何能一等公民式支持 Vue

tsdown 是面向 npm 库开发的"优雅打包器",其 Vue 支持通过两件事实现一等公民体验:

  • unplugin-vue:负责把 .vue 单文件组件(SFC)编译为可在浏览器/Node 中运行的 JavaScript;
  • rolldown-plugin-dts(dts 选项):负责类型声明的生成与打包,其中针对 Vue 有专门的 vue: true 开关。

在 airi 仓库中,tsdown 被大量用于库类包(通过根 package.json 的 pnpm catalog 锁定 tsdown: ^0.22.14,可查看 pnpm-workspace.yaml),例如 core-agentplugin-sdkstream-kit 都直接依赖 tsdown 出 ESM 产物与 .d.ts。而本仓库大量 Vue 组件库(如 packages/stage-uipackages/ui)则使用 @vitejs/plugin-vue + vue-tsc 这一同源工具链。tsdown 提供的就是把"Vite 开发体验"与"库打包 + 类型发布"统一进一份配置的方案。

需要注意的运行时前提:根据 tsdown 技能说明,tsdown 本体需要 Node.js 22.18.0+ 才能运行(仅构建期),但通过 target 选项可以让产物面向更低版本 Node,构建出的库并不会被锁定在 Node 22。

快速开始

使用官方 Starter 模板

最省事的方式是直接以 Vue 模板初始化项目:

npx create-tsdown@latest -t vue

安装依赖

在已有工程中手动接入则需要安装编译与类型生成所需的两个依赖:

pnpm add -D unplugin-vue vue-tsc

最小化配置

// tsdown.config.ts
import { defineConfig } from 'tsdown'
import Vue from 'unplugin-vue/rolldown'

export default defineConfig({
  entry: ['./src/index.ts'],
  format: ['esm', 'cjs'],
  platform: 'neutral',
  deps: {
    neverBundle: ['vue'],
  },
  plugins: [
    Vue({ isProduction: true }),
  ],
  dts: {
    vue: true,  // Enable Vue type generation
  },
})

逐项说明其含义:

  • entry: ['./src/index.ts']:入口指向聚合导出组件的入口文件,而不是直接指 .vue 文件(多入口场景见下文);
  • format: ['esm', 'cjs']:同时产出 ESM(.mjs)与 CJS(.cjs)双格式,兼顾现代打包器与老式 require 环境;
  • platform: 'neutral':产物不绑定特定运行时平台,保证最大兼容性;
  • deps: { neverBundle: ['vue'] }强制不打包 vue,让它作为外部依赖交给使用方的应用提供;
  • plugins: [Vue({ isProduction: true })]:挂载 unplugin-vue 的 Rolldown 适配,isProduction: true 走生产级 SFC 编译;
  • dts: { vue: true }:开启 Vue 类型生成,这也是与普通 TS 库配置最大的不同点。

原理剖析:SFC 如何被编译并生成类型

unplugin-vue:SFC 编译阶段

unplugin-vue 在 Rolldown 构建管线中拦截 .vue 请求并完成三件核心事:

  • 将 template 编译为 render 函数:模板在构建期被静态分析并生成为 _createElementVNode 等渲染代码,浏览器端无需再带运行时编译器;
  • 处理 scoped 样式:为 <style scoped> 中的选择器注入 data-v-xxx 属性哈希,同时负责 v-bind 的 CSS 变量注入等编译期工作;
  • 处理 script setup:将 <script setup> 中声明的顶层绑定提升为组件实例属性,编译 macro(defineProps/defineEmits/defineModel 等)。

vue-tsc:类型生成阶段

类型声明交给 vue-tsc 完成,当 dts.vue: true 开启时它负责:

  • 对 Vue 组件做类型检查(等价于 vue-tsc --noEmit 的库场景子集);
  • 生成 .d.ts 声明文件:把每个 .vue 文件视为一个模块输出声明;
  • 保留组件 props 类型defineProps<Props>() 中以泛型传入的接口会原样进入声明,消费方按字面类型获得智能提示;
  • 导出组件类型:让 export type { ButtonProps } from './Button.vue' 这类"从 .vue 文件导出纯类型"的写法成立。

关于类型生成的更多背景可参考技能包中的 类型声明文档:dts 生成由 rolldown-plugin-dts 负责,且只要 package.json 中存在 typestypings 字段,dts 就会自动开启;开启 vue: true 的前提是项目里安装并配置了 vue-tsc。此外,tsdown 从配置到产物还遵循两条管线规则:ESM 格式下 .js.d.ts 同批生成,CJS 格式下 .d.ts 走独立生成流程。

组件示例

一个标准 SFC 组件

以下是一个典型的 Vue 3 <script setup lang="ts"> 组件,覆盖了 Props 泛型、Emits 声明、动态 class、插槽与 scoped 样式:

<!-- src/Button.vue -->
<script setup lang="ts">
interface Props {
  type?: 'primary' | 'secondary'
  disabled?: boolean
}

defineProps<Props>()
defineEmits<{
  click: []
}>()
</script>

<template>
  <button
    :class="['btn', `btn-${type}`]"
    :disabled="disabled"
    @click="$emit('click')"
  >
    <slot />
  </button>
</template>

<style scoped>
.btn {
  padding: 8px 16px;
  border-radius: 4px;
}

.btn-primary {
  background: blue;
  color: white;
}
</style>

要点:defineProps<Props>() 使用本地 interface Props 而非 type 别名 + defineProps,可保证 vue-tsc 能在 .d.ts 中原样保留联合字面量类型;具名导出组件类型时要求 props 接口可被类型生成器解析到。

从入口导出组件

// src/index.ts
export { default as Button } from './Button.vue'
export { default as Input } from './Input.vue'
export { default as Modal } from './Modal.vue'

// Re-export types
export type { ButtonProps } from './Button.vue'

注意最后一行:在 dts.vue: truevue-tsc 的共同作用下,'./Button.vue' 模块既能提供默认组件值导出,也能提供以文件命名的 Props 类型导出。若你习惯将 Props 类型单独定义在 src/types.ts 并在组件内 import type 复用,同样可行,但务必保证所有导出成员都有显式类型——因为推荐开启的 isolatedDeclarations(见下文 tsconfig 一节)要求每个导出都有可推断的显式标注。

常用配置模式

组件库标准模板

面向真实组件库的推荐配置,在最小化配置基础上补了样式压缩与构建前清理:

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['esm', 'cjs'],
  platform: 'neutral',
  deps: {
    neverBundle: ['vue'],
  },
  plugins: [
    Vue({
      isProduction: true,
      style: {
        trim: true,
      },
    }),
  ],
  dts: {
    vue: true,
  },
  clean: true,
})
  • style.trim: true:产物中的样式会移除多余的空白与换行,压缩体积;
  • clean: true:每次构建前自动清空输出目录(dist),避免历史产物残留。

多入口:让每个组件可单独深链导入

若希望使用方既能 import { Button } from 'my-lib',也能 import Button from 'my-lib/Button' 实现按需引入与更好的 tree-shaking,可以用对象形式声明多入口:

export default defineConfig({
  entry: {
    index: 'src/index.ts',
    Button: 'src/Button.vue',
    Input: 'src/Input.vue',
    Modal: 'src/Modal.vue',
  },
  format: ['esm', 'cjs'],
  deps: {
    neverBundle: ['vue'],
  },
  plugins: [Vue({ isProduction: true })],
  dts: { vue: true },
})

产物会对应生成 dist/index.*dist/Button.* 等独立文件(d.ts 同理)。若配合 exports: true 等字段生成能力,每个子路径都能获得独立的条件导出。

组件库内混入 Composition 工具函数

组件库经常同时发布可复用的组合式函数,它们与组件共享同一份 vue 外部依赖与类型管线:

// src/composables/useCounter.ts
import { ref } from 'vue'

export function useCounter(initial = 0) {
  const count = ref(initial)
  const increment = () => count.value++
  const decrement = () => count.value--
  return { count, increment, decrement }
}

配置上并不需要任何额外处理,只需在入口中导出:

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['esm', 'cjs'],
  deps: {
    neverBundle: ['vue'],
  },
  plugins: [Vue({ isProduction: true })],
  dts: { vue: true },
})

vue 依然保持在 neverBundle 名单中,因为 ref/computed 等 API 必须与使用方应用共享同一个 Vue 运行时实例,否则会出现双实例导致的响应性失效问题。

TypeScript 配置

组件库推荐使用如下 tsconfig.json。其中 isolatedDeclarations: true 是性能关键项:tsdown 会利用 oxc-transform 快速生成声明,规避完整 TypeScript 编译的耗时;未开启时则回退到 TypeScript 编译器(可靠但更慢)。

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "jsx": "preserve",
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "strict": true,
    "isolatedDeclarations": true,
    "skipLibCheck": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

字段速记:

  • jsx: "preserve":Vue 生态中 TSX 代码通常交给 unplugin-vue/JSX 编译,TS 本身不吞 JSX;
  • moduleResolution: "bundler":适配现代打包器式裸模块解析;
  • allowImportingTsExtensions: true:允许带 .ts 后缀导入(仅供开发期,产物由打包器归一化);
  • isolatedDeclarations: true:走 oxc 快速声明路径的前提。

package.json 配置

库的发布元数据遵循"双格式 + 类型 + 对等依赖"的现代规范:

{
  "name": "my-vue-library",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs"
    },
  },
  "files": ["dist"],
  "peerDependencies": {
    "vue": "^3.0.0"
  },
  "devDependencies": {
    "tsdown": "^0.9.0",
    "typescript": "^5.0.0",
    "unplugin-vue": "^5.0.0",
    "vue": "^3.4.0",
    "vue-tsc": "^2.0.0"
  }
}

三点解释:

  • types 字段会让 dts 自动开启(见 option-dts 文档 的 auto-enabled 规则),但仍建议在 tsdown.config.ts 中显式 dts: { vue: true }
  • exports 的条件导出顺序应为 types 在前、import/require 在后;
  • vue 必须放在 peerDependencies(而不是 dependencies),并在构建侧加入 neverBundle,二者共同保证"库不携带 Vue、与宿主共享一个 Vue"。

(上表版本号为文档示例值。airi 仓库当前 catalog 中实际锁定为 tsdown: ^0.22.14vue: ^3.5.41vue-tsc: ^3.3.11,见 pnpm-workspace.yaml,新项目可直接以 catalog 版本为准。)

进阶模式

接入 Vite 生态插件(unplugin 系)

得益于 unplugin 的跨构建器设计,多数 Vite Vue 插件能以同构方式接入 Rolldown。例如自动按需导入组件的 unplugin-vue-components

import Vue from 'unplugin-vue/rolldown'
import Components from 'unplugin-vue-components/rolldown'

export default defineConfig({
  entry: ['src/index.ts'],
  deps: {
    neverBundle: ['vue'],
  },
  plugins: [
    Vue({ isProduction: true }),
    Components({
      dts: 'src/components.d.ts',
    }),
  ],
  dts: { vue: true },
})

Components({ dts: 'src/components.d.ts' }) 会在开发/构建期自动扫描并注册用到的组件,同时生成供编辑器感知的类型声明文件。

JSX / TSX 支持

Vue 组件库同样可以发布 .tsx 组件或 render 函数。关键在于两处:inputOptions.transform 中把 JSX 运行时指向 'vue',并在 Vue() 插件里开启 props 解构等新语法支持:

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['esm', 'cjs'],
  deps: {
    neverBundle: ['vue'],
  },
  plugins: [
    Vue({
      isProduction: true,
      script: {
        propsDestructure: true,
      },
    }),
  ],
  inputOptions: {
    transform: {
      jsx: 'automatic',
      jsxImportSource: 'vue',
    },
  },
  dts: { vue: true },
})
  • jsx: 'automatic' + jsxImportSource: 'vue':使用 vue/jsx-runtime 的自动导入模式,无需手动 import { h }
  • script.propsDestructure: true:允许 const { msg } = defineProps<{ msg: string }>() 式的解构语法,同时保持响应性(Vue 3.5+ 特性)。

Monorepo 内构建 Vue 包

在 workspace 中,一个配置即可构建多个 Vue 包,并且用正则把同组织的工作区包与 vue 一起外部化:

export default defineConfig({
  workspace: 'packages/*',
  entry: ['src/index.ts'],
  format: ['esm', 'cjs'],
  deps: {
    neverBundle: ['vue', /^@mycompany\//],
  },
  plugins: [Vue({ isProduction: true })],
  dts: { vue: true },
})

workspace: 'packages/*' 让 tsdown 以子包为单位分别构建;/^@mycompany\// 正则将同组织的兄弟包一律视为 external,避免把其它 workspace 源码打进每个子包。这与 airi 仓库在 pnpm-workspace.yaml 里集中 catalog 管理依赖、各 包级 tsdown.config.ts 独立声明 entry 的模式互为补充。依赖外部化 / 打包的完整决策规则(默认 dependencies/peerDependencies/optionalDependencies 全部 external,devDependencies 仅被引用时打包)见 依赖选项文档

unplugin-vue 插件选项速查

Vue({
  isProduction: true,
  script: {
    defineModel: true,
    propsDestructure: true,
  },
  style: {
    trim: true,
  },
  template: {
    compilerOptions: {
      isCustomElement: (tag) => tag.startsWith('custom-'),
    },
  },
})
选项 作用
isProduction 生产模式编译(如对模板做更积极的优化),库构建推荐为 true
script.defineModel 支持 defineModel() 宏(Vue 3.4+)
script.propsDestructure 支持 defineProps 解构赋值并保持响应性
style.trim 移除产物样式的多余空白,压缩输出
template.compilerOptions.isCustomElement 将匹配 tag 当作自定义元素,跳过组件解析

实践要点清单

  1. 始终外部化 Vue——不要打包 Vue 本身,加到 deps.neverBundle 并声明为 peerDependencies
  2. dts 开启 vue: true——没有它 .vue 模块无法产出可用的组件类型声明;
  3. 使用 platform: 'neutral'——让库同时适配浏览器与各类运行时,兼容性最大化;
  4. 安装 vue-tsc——Vue 类型生成的前提依赖,缺了它 dts.vue 会失败;
  5. Vue({ isProduction: true })——按生产标准优化 SFC 编译结果;
  6. 把 Vue 放进 peerDependencies——与使用方共享同一个 Vue 运行时,避免双实例问题。

疑难排查

类型生成失败

先确认 vue-tsc 已安装:

pnpm add -D vue-tsc

再确认配置中已开启 Vue 类型开关:

dts: { vue: true }

组件类型缺失 / Props 提示丢失

检查 tsconfig 中 JSX 与解析器相关配置是否正确(缺少时 vue-tsc 可能无法为 .vue 模块产出有效声明):

{
  "compilerOptions": {
    "jsx": "preserve",
    "moduleResolution": "bundler"
  }
}

若第三方依赖的类型较复杂导致 dts 解析失败,还可以参考 option-dts 把模块解析器切回兼容性更高的 dts: { resolver: 'tsc' },代价是生成更慢。另外请确保所有导出都带显式类型——这是启用 isolatedDeclarations 快速路径的前提。

Vue 未被外部化(被打进了产物)

deps.neverBundle 显式声明:

deps: {
  neverBundle: ['vue'],
}

同时也建议把 @vue/runtime-core@vue/reactivity 等 Vue 附属包一并外部化,并确认它们存在于 peerDependencies(参见 依赖处理文档)。

SFC 编译报错

通常是 unplugin-vue 与正在使用的 Vue 版本不匹配,升级到最新版重试:

pnpm add -D unplugin-vue@latest

延伸阅读

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.74 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.63 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
518
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389