工程化
Exports 策略
package.json exports / 子入口 / 平台分支的统一规范
为什么严肃对待 exports
package.json 的 exports 字段决定:
- 下游
import '@skyroc/utils/storage'能否解析; - TypeScript 能否找到
.d.ts; - bundler tree-shaking 的边界;
- ESM / CJS / 浏览器 / RN 平台路由。
本仓库的所有发布包都遵循一份统一的 exports 模板,避免下游用法分裂。
单入口模板
{
"type": "module",
"main": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"./package.json": "./package.json"
}
}要点:
type: "module"全仓统一;types/default顺序固定(types 必须在最前,Node 16+ 要求);- 暴露
./package.json便于工具读取版本 / 依赖; - 不导出
require字段(不发 CJS)。
多入口(子路径)模板
库提供多个独立子模块时(如 @skyroc/utils、@skyroc/hooks、@skyroc/service):
"exports": {
".": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"./color": {
"types": "./dist/color.d.mts",
"default": "./dist/color.mjs"
},
"./storage": {
"types": "./dist/storage.d.mts",
"default": "./dist/storage.mjs"
},
"./package.json": "./package.json"
}每个子入口都需要 tsdown 在 entry 中列出,否则 dist 不存在对应文件。
平台分支模板
跨平台共享包(如 @skyroc/hooks)通过子入口区分平台:
"exports": {
".": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"./web": {
"types": "./dist/web.d.mts",
"default": "./dist/web.mjs"
},
"./native": {
"types": "./dist/native.d.mts",
"default": "./dist/native.mjs"
}
}不使用
"browser" / "react-native"条件字段,因为这种「同入口、按 condition 分发」的方式:
- VS Code 类型推断有时挑不出正确平台;
- bundler 可能误判 SSR 环境;
- 调试难度高。
统一约定:跨平台代码用显式子路径(
./web、./native),不用 condition 字段。
资源 / 样式 / mock 子入口
部分包还需要暴露非 TS 内容(CSS / 模板 / mock):
"exports": {
".": { "types": "./dist/index.d.mts", "default": "./dist/index.mjs" },
"./style.css": "./dist/style.css",
"./mock": { "types": "./dist/mock.d.mts", "default": "./dist/mock.mjs" }
}例子:
@skyroc/web-ui/style.css— 设计系统全局样式;@skyroc/web-admin-notification/mock— 通知中心 mock 数据;@skyroc/web-admin-devtools/jotai— Jotai devtools 子入口;@skyroc/tailwind-plugin/preset— UnoCSS preset;@skyroc/web-admin-styles/reset.css— 仅 reset 部分。
tsdown entry 与 exports 的一致性
最常见的 bug:exports 里有 ./color,但 tsdown.config.ts 没把它列入 entry,结果安装方解析失败。
正确做法:
// tsdown.config.ts
export default defineConfig({
entry: {
index: 'src/index.ts',
color: 'src/color/index.ts', // ← 必须有
storage: 'src/storage/index.ts'
},
dts: true
});"exports": {
".": { ... },
"./color": { "types": "./dist/color.d.mts", "default": "./dist/color.mjs" },
"./storage": { "types": "./dist/storage.d.mts", "default": "./dist/storage.mjs" }
}两者必须一一对应。
peerDependencies 与 external 的协同
| 项 | 含义 |
|---|---|
dependencies | 直接依赖,被 npm 安装;tsdown 默认 external |
peerDependencies | 由消费方提供(React、antd 等) |
devDependencies | 仅本包开发用,不影响下游 |
tsdown external 显式列表 | 必要时手动追加 |
约定:
react/react-dom一律peerDependencies;antd一律peerDependencies(仅在 ui-antd 等强绑包出现);- 跨包依赖
@skyroc/*一律dependencies(外部化即可); - 不要把构建工具(
tsdown/vite)写到dependencies。
sideEffects
"sideEffects": false // 纯 ESM 包
"sideEffects": ["*.css", "*.scss", "*.svg"] // 仅样式 / 资源有副作用| 包类型 | sideEffects |
|---|---|
| 纯函数 / 类型 / Hook | false |
| 注入 CSS / 全局副作用(runtime / styles / theme) | 数组列出 |
| 应用 | 不需要 |
错误的 sideEffects: false 会让 bundler 把 CSS 摇掉,导致样式丢失。
files 字段
"files": [
"dist",
"README.md"
]发布到 npm 时只包含 dist、README.md、package.json(后者自动)。不要打包 src / __tests__ / tsconfig.json。
publishConfig
公开包:
"publishConfig": {
"access": "public"
}私有包:
"private": true@skyroc scope 默认私有,需要发布的包必须显式 access: "public"。
检查清单
新建 / 修改 exports 后:
- tsdown
entry与exports一一对应; -
types/default顺序正确; - 暴露
./package.json; -
sideEffects设置正确(默认false但 CSS 包要列); -
files: ["dist"]; - 跨平台分支用显式
./web/./native; -
pnpm build产物存在; - 在 admin / playground 写一行
import验证; -
pnpm publint(如已接入)通过。
调试 exports 错误
| 错误 | 原因 |
|---|---|
ERR_PACKAGE_PATH_NOT_EXPORTED | exports 没暴露子路径 |
| TS 找不到类型 | types 字段缺失或顺序错误 |
| 产物找不到 | tsdown entry 没列 |
| 双 ESM 实例 | 该包没把 @skyroc/* 标 external |
| RN 报 DOM API 错 | 错把 web-only 代码放进主入口 |