首页
/ Project AIRI 中的 tsdown unbundle 模式实战:如何保留源码目录结构构建 monorepo 库

Project AIRI 中的 tsdown unbundle 模式实战:如何保留源码目录结构构建 monorepo 库

2026-09-08 14:17:27作者:咎岭娴Homer

导读

在构建组件库、多语言包这类"由多个可独立导入模块组成"的 npm 包时,把全部源码打包成一个单文件往往并不是最优解。tsdown 的 Unbundle 模式(unbundle,也称 bundleless 或 transpile-only) 会按照与源码一一对应的关系逐文件编译输出,让 dist 目录完美复刻 src 目录结构,从而支持用户按子路径精确导入单个工具函数或组件。本文以 .agents/skills/tsdown/references/option-unbundle.md 为骨架,结合 Project AIRI 仓库中真实的 tsdown 配置(音频工具包、i18n 语言包、字体包等),完整讲解 unbundle 模式的配置方法、运行原理、适用场景、输出控制与 package.json 联动,帮助你在 monorepo 中落地"开发期毫秒级重建、发布后按需导入"的库构建方案。

什么是 tsdown 的 unbundle 模式

Unbundle 模式(又称 "bundleless" 或 "transpile-only")的核心思想是:输出文件与源文件结构一一镜像,而不是把全部代码打包进少数几个文件。开启后,每个源文件都会被单独编译,形成一对一的映射关系。

tsdown 本身是基于 Rolldown 与 Oxc 的库构建工具(见 .agents/skills/tsdown/SKILL.md),unbundle 模式正是它面向"以源码结构为发布形态"的库所提供的能力开关。

开启方式

CLI 方式:

tsdown --unbundle

配置文件方式(所有包通常位于 packages/* 下的 tsdown.config.ts):

export default defineConfig({
  entry: ['src/**/*.ts', '!**/*.test.ts'],
  unbundle: true,
})

工作原理:从源码树到输出树

以一个典型的源码目录为例:

src/
├── index.ts
├── utils/
│   ├── helper.ts
│   └── format.ts
└── components/
    └── button.ts

开启 unbundle:结构与源码一一对应

配置只指定 src/index.ts 作为入口:

export default defineConfig({
  entry: ['src/index.ts'],
  unbundle: true,
})

输出结果为(默认 ESM 输出扩展名为 .mjs):

dist/
├── index.mjs
├── utils/
│   ├── helper.mjs
│   └── format.mjs
└── components/
    └── button.mjs

所有被 index.ts 间接导入的文件都会各自输出一份,目录层次保持不变。这正是本仓库 packages/* 各包输出形态的直接来源——例如 packages/i18n/package.jsonpackages/audio/package.jsonexports 子路径都精确指向 dist 下带目录前缀的 .mjs 文件。

默认关闭(标准打包):全部收敛为一个文件

不配置 unbundle(默认值),结果只有一个文件:

dist/
└── index.mjs  (all code bundled together)

依赖关系被打包器静态分析后合并,运行时会互相内联,源码的目录边界在产物中消失。

何时使用 unbundle、何时使用标准打包

原文档给出的决策清单如下。

建议使用 unbundle:

  • 构建 monorepo 内共享的公共工具包
  • 用户需要按需导入单个模块(my-lib/utils/helper
  • 希望产物到源码有清晰的映射关系,便于调试定位
  • 提供大量相互独立的工具函数/组件的库
  • 调试时需要追踪具体文件
  • 需要增量构建、追求更快的开发期重建

建议保留标准打包:

  • 只有一个入口点的应用
  • 希望最小化整体产物体积
  • 需要激进的 tree shaking
  • 需要产出 IIFE / UMD 形态
  • 面向浏览器直接部署

仓库中的真实用例:三种典型 unbundle 配置

Project AIRI 仓库的 packages/ 目录提供了多份可直接对照学习的真实配置。

用例一:结构化子入口的音频工具包

packages/audio/tsdown.config.ts 采用"对象形式入口 + unbundle":

import { defineConfig } from 'tsdown'

export default defineConfig({
  entry: {
    'index': 'src/index.ts',
    'audio-context/index': 'src/audio-context/index.ts',
    'audio-context/processor.worklet': 'src/audio-context/processor.worklet.ts',
    'encoding/index': 'src/encoding/index.ts',
  },
  unbundle: true,
  external: [
    '@alexanderolsen/libsamplerate-js/dist/libsamplerate.worklet.js?worker&url',
    './processor.worklet?worker&url',
  ],
})

对象形式的键名直接决定产物的子路径前缀,因此其 package.json 中(packages/audio/package.json)的 exports 可以声明 ./audio-context./audio-context/processor.worklet./encoding 等深度子路径,且 .d.mts 类型文件一一对应:

{
  "exports": {
    ".": {
      "types": "./dist/index.d.mts",
      "default": "./dist/index.mjs"
    },
    "./audio-context/processor.worklet": {
      "types": "./dist/audio-context/processor.worklet.d.mts",
      "default": "./dist/audio-context/processor.worklet.mjs"
    }
  }
}

值得注意,这里通过 external 把 AudioWorklet 相关的 ?worker&url 资源排除在打包之外,保证 worklet 文件以独立 URL 形式保留引用——unbundle 与 external 常常需要组合使用。

用例二:多语言 i18n 包的 glob 入口与插件协作

packages/i18n/tsdown.config.ts 把 unbundle、copy 和 rolldown 插件组合在一起:

import Yaml from 'unplugin-yaml/rolldown'

import { defineConfig } from 'tsdown'

export default defineConfig({
  entry: {
    'index': 'src/index.ts',
    'locales/index': 'src/locales/index.ts',
    'locales/en/index': 'src/locales/en/index.ts',
    'locales/zh-Hans/index': 'src/locales/zh-Hans/index.ts',
  },
  copy: [
    { from: 'src/locales', to: 'dist/locales' },
  ],
  unbundle: true,
  plugins: [
    Yaml(),
  ],
})

语言资源(.yaml)并不适合逐模块编译,因此通过 copy 原样落到 dist/locales;TS 入口则在 unbundle 模式下被编译成各自的 index.mjs,最终在 packages/i18n/package.json 中以 ./locales/en./locales/zh-Hans、甚至 ./locales/*.yaml 通配子路径暴露给上层应用按需加载。这验证了 unbundle 特别适合"结构化子模块 + 独立资源"混合型包。

用例三:字体包的纯透传

字体包通常只是把字体二进制与对应的 CSS 声明暴露出去,packages/font-xiaolai/tsdown.config.ts 展示了极简形态:

import { defineConfig } from 'tsdown'

export default defineConfig({
  entry: ['src/index.ts'],
  external: ['./index.css'],
  copy: [
    { from: 'src/files', to: 'dist' },
    { from: 'src/index.css', to: 'dist' },
  ],
  unbundle: true,
})

常见配置模式

工具函数库:ESM + CJS + DTS 全量输出

export default defineConfig({
  entry: ['src/**/*.ts', '!**/*.test.ts'],
  format: ['esm', 'cjs'],
  unbundle: true,
  dts: true,
})

收益包括:用户只导入他们需要的模块;tree shaking 由用户侧的打包器在构建期完成;模块边界清晰。使用端可以这样按子路径导入:

// Users can import specific utilities
import { helper } from 'my-lib/utils/helper'
import { Button } from 'my-lib/components/button'

monorepo 共享包

export default defineConfig({
  entry: ['src/index.ts'],
  format: ['esm'],
  unbundle: true,
  outDir: 'dist',
})

纯 TypeScript 转译(不做优化)

export default defineConfig({
  entry: ['src/**/*.ts'],
  format: ['esm'],
  unbundle: true,
  minify: false,
  treeshake: false,
  dts: true,
})

这相当于把 tsdown 当作纯 TS→JS 转换器使用,输出与源码同构。

开发/生产差异化配置

利用 defineConfig 的函数重载,让 watch 时走 unbundle(秒级重建),发布时走打包优化

export default defineConfig((options) => ({
  entry: ['src/**/*.ts'],
  unbundle: options.watch,  // Unbundle in dev only
  minify: !options.watch,
}))

开发期快速重建、生产期优化产物,这与 .agents/skills/tsdown/SKILL.mdoptions.watch 条件配置的用法一脉相承。

与 entry 模式配合

包含 / 排除

export default defineConfig({
  entry: [
    'src/**/*.ts',
    '!**/*.test.ts',
    '!**/*.spec.ts',
    '!**/fixtures/**',
  ],
  unbundle: true,
})

被排除的文件不会进入产物目录。

多个入口点

export default defineConfig({
  entry: {
    index: 'src/index.ts',
    cli: 'src/cli.ts',
  },
  unbundle: true,
})

所有入口文件以及它们导入的文件都会被保留并独立输出。

输出控制

自定义扩展名

默认 ESM 输出为 .mjs;需要与 package.json"type": "commonjs" 等约定对齐时,可用 outExtensions 改回 .js

export default defineConfig({
  entry: ['src/**/*.ts'],
  unbundle: true,
  outExtensions: () => ({ js: '.js' }),
})

在本仓库的实践里,packages/audio/package.json 等包声明 "type": "module",产物统一为 .mjs / .d.mts,因此无需覆盖扩展名——选择哪种由包的类型约定决定。

修改输出目录

export default defineConfig({
  entry: ['src/**/*.ts'],
  unbundle: true,
  outDir: 'lib',
})

产物布局:

lib/
├── index.js
├── utils/
│   └── helper.js
└── components/
    └── button.js

通过 root 控制目录映射起点

默认情况下 tsdown 会把所有入口的公共目录作为输出根;当你希望保留 src/ 前缀,或让 unbundle 模式的结构映射从指定目录开始,可以用 root 显式控制(详见 Root Directory 选项)。例如:

export default defineConfig({
  entry: ['src/**/*.ts', '!**/*.test.ts'],
  root: '.',       // 保留 src/ 前缀
  unbundle: true,
})

在 unbundle 模式下,root 会作为模块结构保留的根(preserveModulesRoot)参与输出路径计算,是与 unbundle 强相关、需要一并理解的配套选项。

package.json 联动:把子路径暴露给用户

unbundle 的价值最终要靠 exports 字段兑现。以 ESM 为例,完整的手写形态如下:

{
  "name": "my-library",
  "type": "module",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": "./dist/index.js",
    "./utils/*": "./dist/utils/*.js",
    "./components/*": "./dist/components/*.js"
  },
  "files": ["dist"]
}

其中通配子路径 "./utils/*": "./dist/utils/*.js" 是配合 unbundle 产物最常用的写法——它允许用户按任意深度导入具体文件。tsdown 也支持 exports: true 由构建输出自动推断生成 exports 字段,并可用 exports: { all: true } 把所有输出文件(而不只是入口)加入导出清单;更细化的 devExportslegacy 等能力见 package exports 选项。本仓库的 packages/i18n/package.json 即展示了逐子路径手写 exports 并配合 "./locales/*.yaml" 资源通配的真实形态,可供对照。

打包 vs unbundle:特性对比

原文档给出的对比表:

特性 打包(Bundled) 非打包(Unbundled)
输出文件数
文件体积 更小 更大
构建速度 更慢 更快
Tree shaking 构建期完成 交给用户构建期
源码映射 复杂 简单
模块导入 仅限入口 任意模块
开发期重建 更慢 更快

性能特征

构建速度:unbundle 通常更快——省去打包聚合的开销、文件可并行处理、且天然支持增量构建。

产物体积:unbundle 输出整体更大——每个文件各自带有模块开销、缺少跨模块优化,最终优化需交给用户侧的打包器。

需要注意的是,原文档给出的是定性结论,具体倍率依赖项目规模、文件数量与依赖图复杂度,不应被当作普适性能承诺。

实践要点速览

  1. 配合 glob 模式处理多个源文件;
  2. 开发期开启以获得更快的重建;
  3. 让用户侧打包器完成生产环境的最终优化;
  4. 保留目录结构,适合工具函数与组件库;
  5. 务必开启 DTS,让每个模块都有独立类型声明(dts 生成细节见 DTS 选项);
  6. 结合 monorepo,作为共享代码包的标准构建形态。

常见问题排查

输出文件过多:

  • 收窄 entry 的 glob 模式
  • 排除不需要的文件(如 !**/*.test.ts!**/fixtures/**
  • 改用明确的入口点列表

产物缺失某些文件:

  • 检查 entry 模式是否覆盖该文件
  • 确认该文件确实被某个入口直接或间接 import
  • 排查是否命中排除规则

导入路径解析失败:

  • 核对产物目录结构与源码相对路径是否一致(必要时用 root 修正映射起点)
  • 核对 outDir、entry 命名
  • 更新 package.jsonexports,为新增子路径补充映射,并用 tsdown --exports --publint 校验导出声明

CLI 常用示例

# Enable unbundle
tsdown --unbundle

# With specific entry
tsdown src/**/*.ts --unbundle

# With other options
tsdown --unbundle --format esm --dts

--format--dts 的完整语义可参见 Output Format 选项DTS 选项

延伸阅读

unbundle 通常与其他输出类选项搭配使用,仓库内 references 目录下的相关文档均可继续深入:

仓库中的真实落地示例(含完整配置与对应的 exports 声明)可继续阅读 packages/audio/tsdown.config.tspackages/i18n/tsdown.config.tspackages/font-xiaolai/tsdown.config.ts,以及 .agents/skills/tsdown/SKILL.md 中的 "Preserve Directory Structure" 章节。

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

项目优选

收起
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