这个项目的目标很明确:
packages/ui:UI 组件库,未来希望能发布到 npm,给外部项目直接用。apps/docs:文档站点,用来展示组件、写博客、沉淀工程化经验。为了同时兼顾“本地联调效率”和“未来发布的兼容性”,这里采用了一套很常见、也很稳的打包思路:
在 Monorepo 里开发(pnpm workspace),在 UI 包里产出标准分发物(dist:ESM + CJS + d.ts),Docs 作为真实消费者验证一切。
本项目的工作区配置是:
目录上大致是:
Docs 通过 workspace 直接依赖 UI:
这样做的好处是:UI 改动可以最快被 Docs 验证;坏处是:一旦 UI 变成“需要构建才可消费”(exports 指向 dist),就必须把构建链路/联调链路也设计好(后面会讲)。
你会在 packages/ui/dist 看到类似这些文件:
index.js:ESM 产物(给 import 用)index.cjs:CJS 产物(给 require 用)index.d.ts:类型声明(给 TS/IDE 用)index.js.map / index.cjs.map:sourcemap(调试用)现实世界里还存在很多不同的消费方式:
import { Button } from 'haiku-ui'const { Button } = require('haiku-ui')如果你只输出其中一种,就会限制用户或引入各种兼容问题;双输出是“发布到 npm 的组件库”最稳妥的默认选择之一。
.cjs,而不是也叫 .js?UI 包的 package.json 里有:
这会让 Node 把 .js 默认当作 ESM。如果你把 CJS 也输出为 .js,Node 就可能按 ESM 解释它,导致运行时错误。
所以我们用 .cjs 来明确告诉 Node:这是 CommonJS 文件。
UI 选择 tsup(底层是 esbuild)主要是因为:配置简单、速度快、很适合组件库“多格式输出 + 类型声明”。
关键配置(简化理解版):
这套配置背后的“打包思想”是:
src/index.ts 作为公共出口,内部怎么组织组件都可以。react/react-dom 作为 peer + external,避免用户项目里出现多份 React(会引起 hooks/上下文等灾难)。packages/ui/package.json 里你看到的这些字段:
可以把它理解成:我发布出去的“可见世界”只有 dist,并且不同的消费方式指向不同的文件。
files: ["dist"]:发布到 npm 时只带上 dist(干净、体积小、避免泄露源码/配置)。main/module/types:兼容老工具(历史字段,仍有价值)。exports:现代 Node/打包器的“权威入口表”,能精确区分 import 与 require,并且把类型入口也绑定到正确文件。对未来要发布到 npm 的 UI 包来说,
exports基本是必选项。
在实际构建中,我们遇到过这种错误(典型):
Cannot find module './components/Button'... Did you mean to set 'moduleResolution' to 'nodenext' ...
这类问题经常发生在:
./components/Button 依赖 index.ts)所以 packages/ui/tsconfig.json 里补上了更适合打包器场景的配置:
一句话总结:JS 产物 esbuild 能“猜对”,但 d.ts 产物必须让 TypeScript 也“看懂”。
文档站点用的是 Rspress:
Docs 页面里直接引入组件库:
这能验证几件非常关键的事:
exports 是否正确(Docs 实际会按它解析入口)Button 的 props 类型)当前项目选择的是:
UI 包里只写 className(Tailwind utility),真正生成 CSS 的工作交给消费端(Docs)。
这意味着消费端必须能“扫描到”UI 里的 class 字符串,否则就会出现:className 写了,但页面没样式。
在 Tailwind v4 里,我们用 CSS-first 的方式声明扫描来源:
这样 Docs 构建时会把 packages/ui/src 中出现的类(如 bg-blue-600)生成到最终的 CSS 里。
注意:如果未来 UI 发布到 npm,很多项目默认不会扫描
node_modules。要做到“发布即有样式”,更推荐 UI 包自己发布styles.css(后续可优化方向)。
如果你也在做类似的 UI 包,下面这份清单几乎可以直接照抄:
src/index.ts),避免用户从深层路径 import?.cjs(在 "type":"module" 下)?react/react-dom 是否 external + peerDependencies?.d.ts 并且构建能稳定通过?exports 是否为不同消费方式提供了明确映射?如果你明确要把 haiku-ui 发布到 npm,建议尽早规划:
dist/styles.css + dist/tokens.css,用户只要 import 即可。build/typecheck/lint,并保证 prepublishOnly 会强制构建出 dist。react 的主流区间(例如 ^18.2.0 || ^19.0.0)。这套打包方式的核心是:
exports 做入口治理与环境兼容当你从“内部联调”走向“对外发布”时,只需要在这个框架上补齐“样式产物与发布流程”,就能获得一个真正可用、可维护的组件库工程。