在 airi 仓库工程中基于 tsdown 构建 SolidJS 组件库:unplugin-solid 集成与配置实战
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
本文基于 airi 仓库内置的 tsdown 技能参考文档 recipe-solid.md,完整讲解如何用 tsdown(由 Rolldown 驱动的高性能库打包器)配合
unplugin-solid构建 Solid 组件库。airi 是一个以 pnpm workspace 组织的巨型 monorepo,仓库内大量子包(如 packages/plugin-sdk/tsdown.config.ts)都通过 tsdown 完成发布物打包,本食谱正是该技能体系中面向 Solid 框架的库工程范式。读完本文,你将掌握 Solid 组件库的脚手架初始化、platform: 'neutral'跨运行时产物配置、类型声明生成、以及把solid-js正确外置为 peer dependency 的完整发布链路。
为什么 Solid 组件库需要专门的构建配方
与 React、Vue、Svelte 一样,Solid 是 tsdown 技能文档中明确列出的框架支持对象。在 SKILL.md 的 "Framework & Runtime Support" 表中,Solid 一栏的说明是 "SolidJS JSX transform",即 Solid 库的构建核心是把 Solid 的 JSX 语法编译为面向其细粒度响应式系统(createSignal、createEffect等)的运行时调用。
关键在于:Solid 的 JSX 语义与标准 JSX 并不相同。Solid 组件里{count()}这类表达式、<Show>、<For>等控制流组件,都需要在编译期被特殊处理,JSX 编译器会为每个动态表达式生成细粒度的响应式订阅代码。因此打包 Solid 组件库时,不能像 React 那样依赖运行时自带的 JSX 转换,而必须接入专门的插件——这就是unplugin-solid。
tsdown 构建在 Rolldown 之上,天然支持 JSX/TSX 语法解析,但把 JSX 正确编译成 Solid 语义代码需要 unplugin 这样的框架级插件参与 transform 阶段(见 advanced-plugins.md 中关于 unplugin 生态兼容性的说明)。
快速开始:脚手架初始化
最快捷的方式是直接使用create-tsdown官方脚手架创建带有 Solid 模板的项目:
npx create-tsdown@latest -t solid执行后即得到一套预配置好的工程骨架,包含tsdown.config.ts、package.json、tsconfig.json与源码目录,后续只需在src/中编写.tsx组件即可。
版本前提:运行 tsdown 的构建环境需要 Node.js 22.18.0 或更高(来自 SKILL.md 的 Runtime Requirement)。不过这只是"运行构建工具"的要求——通过
target选项可以把打包产物降到 ES2020 甚至node18/node20,产物本身并不被锁定在 Node 22+。对于需要发布到浏览器或低版本 Node 环境的库,请按此前提安排 CI。
如果你更习惯手动搭建,参考 guide-getting-started.md 的做法:先安装 tsdown 本体与 TypeScript,再添加下述配置文件即可:
pnpm add -D tsdown pnpm add -D typescript最小可运行配置逐项拆解
原文档给出的 Solid 配方配置如下(这也是-t solid模板生成的tsdown.config.ts的核心形态):
import solid from 'unplugin-solid/rolldown' import { defineConfig } from 'tsdown' export default defineConfig({ entry: ['./src/index.ts'], platform: 'neutral', dts: true, plugins: [solid()], })我们逐项拆解每个字段在 Solid 库场景下的含义:
| 配置项 | 本食谱取值 | 作用与说明 |
|---|---|---|
entry | './src/index.ts' | 库的打包入口。通常入口文件负责export所有公共组件、类型与工具函数。也支持多入口对象({ index: 'src/index.ts', Button: 'src/Button.tsx' })与 glob 模式,详见 option-entry.md |
platform | 'neutral' | 声明产物与运行时无关(见下文"neutral 平台"专项解析)。对于要同时被浏览器、SSR、测试环境消费的组件库,这是推荐取值,详见 option-platform.md |
dts | true | 生成并打包.d.ts类型声明文件(见下文"DTS 生成"专项解析),详见 option-dts.md |
plugins | [solid()] | 挂载unplugin-solid的 Rolldown 适配器,负责把.tsx/.jsx中的 JSX 编译为 Solid 响应式调用 |
一个典型的组件库源码布局是:
// src/Button.tsx import type { JSX } from 'solid-js' interface ButtonProps { type?: 'primary' | 'secondary' onClick?: () => void children?: JSX.Element } export function Button(props: ButtonProps) { return ( <button class={`btn btn-${props.type ?? 'primary'}`} onClick={props.onClick}> {props.children} </button> ) }// src/index.ts export { Button } from './Button' export type { ButtonProps } from './Button'在index.ts中集中 re-export,保证entry只需指向一个入口;props 相关的 interface 记得一并导出,以便dts: true生成的类型声明对消费方完整可用。
安装依赖:unplugin-solid 与它的/rolldown入口
原文档明确要求安装unplugin-solid:
npm install -D unplugin-solid在 airi 这类使用 pnpm workspace 的 monorepo 中,等价写法是
pnpm add -D unplugin-solid(仓库根目录有 pnpm-workspace.yaml),但无论用哪个包管理器,它都必须是devDependency——构建期插件不应进入最终产物的运行时依赖。
这里有几个值得展开的工程细节:
- unplugin 是"一个插件,多打包器通用"的统一抽象。正如 advanced-plugins.md 所总结,unplugin 生态的插件(如
unplugin-auto-import、unplugin-vue-components)可以按目标打包器从不同子路径导入。unplugin-solid同样暴露多个入口,其中最核心的形态包括面向 Vite、面向 Rollup/Rolldown 的适配层。 - 本食谱导入的是
unplugin-solid/rolldown,也就是专门为 Rolldown 准备的适配器。tsdown 底层由 Rolldown 驱动,因此应使用/rolldown入口,而不是把 Vite 或 Rollup 的入口强塞进plugins数组(那会导致类型不匹配,通常需要// @ts-expect-error或as any处理,详见 advanced-plugins.md 的 Troubleshooting 一节)。 - 与之对照,advanced-plugins.md 中还给出过一种基于
vite-plugin-solid的写法(面向 Vite 生态);而在 tsdown 语境下,Solid 的推荐路径就是本食谱的unplugin-solid/rolldown。
如果你的solid-js需要从依赖/peer 角度避免被误打包,见下文"依赖外置"一节。
关键要点一:为什么用platform: 'neutral'
原文档把platform: 'neutral'列为第一条关键点,理由在于Solid 组件库通常是"通用库"(universal library):它既可能在浏览器被import,也可能在 Vitest/jsdom、SSR、甚至 Electron renderer 等环境中运行。若把平台写死为node或browser,产物的模块解析策略与内置模块处理就会带上平台假设。
参考 option-platform.md,tsdown 提供三种平台:
| 平台 | 运行时假设 | 内置模块处理 | 适用场景 |
|---|---|---|---|
node(默认) | Node.js | 自动解析 Node 内置模块(fs、path等) | 服务端、CLI、工具链 |
browser | Web 浏览器 | 使用 Node 内置模块时告警 | 前端应用 |
neutral | 平台无关 | 不做任何假设 | 通用库(组件库、工具库) |
neutral的具体表现(引自 option-platform.md):
- 不对运行时做任何假设,不做内置模块的自动解析;
- 模块解析只信任
exports字段(默认mainFields: []),对运行时行为拥有完全控制; - 产物本身不能携带平台绑定代码,从而保证同一份 dist 在各类宿主中行为一致。
需要注意的配套要求:
- 依赖方 package 必须提供
exports字段,否则neutral下解析会失败并提示The "main" field here was ignored. Main fields must be configured explicitly when using the "neutral" platform.此时需要在inputOptions.resolve.mainFields中显式声明:export default defineConfig({ platform: 'neutral', inputOptions: { resolve: { mainFields: ['module', 'main'], }, }, }) - CJS 格式的固有限制:tsdown 中
cjs格式总是使用node平台且无法更改(参见 option-platform.md 的 "CJS Format Limitation")。因此如果需要同时产出 ESM + CJS,且追求最大兼容,常规做法是默认输出 ESM 走neutral,需要 CJS 消费方时另行说明或采用多配置分别构建。
关键要点二:dts: true生成类型声明
原文档第二条关键点是类型声明生成。tsdown 底层使用 rolldown-plugin-dts 来生成并打包.d.ts(详见 option-dts.md)。前提是项目内安装了 TypeScript。
对组件库而言.d.ts是"库的可交付物"之一——消费方在 IDE 中补全 props、获得组件类型约束,都依赖产物中的声明文件。可用的增强配置包括:
| dts 选项 | 类型 | 说明 |
|---|---|---|
sourcemap | boolean | 生成声明文件的 source map(monorepo 场景定位源码很有用) |
compilerOptions | object | 覆盖 TS 编译器选项(如removeComments: false) |
oxc | boolean | 强制使用 oxc-transform 加速声明生成(需配合isolatedDeclarations) |
tsconfig | string | 指定独立的 tsconfig(如./tsconfig.build.json) |
resolver | 'oxc' \| 'tsc' | 模块解析器,默认'oxc'(快);复杂第三方类型解析失败时切'tsc'(更兼容) |
cjsDefault/sideEffects | boolean | CJS 默认导出处理 / 声明中的副作用保留 |
性能加速建议:在tsconfig.json开启isolatedDeclarations,声明生成会走 oxc-transform 的极快路径;若个别导出缺少显式类型标注,则无法使用该路径(isolated declarations 要求所有导出显式标注类型),此时 tsdown 会回退到 TypeScript 编译器。
{ "compilerOptions": { "isolatedDeclarations": true, "declaration": true, "declarationMap": true } }在 airi 仓库的真实实践中,dts: true是最常见的标配。例如 packages/plugin-sdk/tsdown.config.ts 中,SDK 包同时配置了多入口entry、dts: true与format: 'esm'。这也印证了技能文档 README.md 中的核心建议:"Always generate type declarations for TypeScript libraries"。
关键要点三:Solid 插件与 JSX 编译的职责边界
原文档第三条关键点是:"The Solid plugin handles JSX compilation for Solid's reactive system"。
展开讲,unplugin-solid在 transform 阶段接管.tsx/.jsx的编译,完成两件关键工作:
- JSX → Solid 语义编译:把 JSX 表达式树编译成
solid-js的运行时指令(组件实例化、createMemo/createSignal的细粒度订阅、<Show>/<For>/<Switch>等控制流的展开)。这正是 Solid 区别于 React 的核心——React 的响应式靠整体重渲染,Solid 靠编译期静态分析出动态边界并生成精确更新代码,所以编译这一步不可省略、也不可由通用 JSX 转换替代。 - HMR 与开发体验相关的处理:在 watch/dev 场景下保证组件热更新与类型推导正确。
由此产生的实践推论:
- 若你的
.tsx源码没有经过该插件就直接产出,结果将是无法在 Solid 运行时工作的无效代码或语义错误,因此请始终把solid()放进plugins。 - 组件文件中请从
'solid-js'显式导入组件类型与控制流所需 API,避免依赖隐式全局。 - 插件在
plugins数组中的顺序即执行顺序(参考 advanced-plugins.md);当存在多个 transform 插件时注意先后次序。
让库真正可发布:依赖外置、exports 与 peerDependencies
原文档聚焦"如何构建",但一个 Solid 组件库要能发给消费方,还必须处理好依赖边界。结合 option-dependencies.md 与 recipe-react.md 中的同类范式,推荐做法如下。
1. 把solid-js外置(neverBundle)
tsdown 默认会外置dependencies、peerDependencies、optionalDependencies,但为了把意图写清楚并防止误打包(例如把运行时打入产物导致双份 solid-js、hooks/响应式状态割裂),应显式声明:
export default defineConfig({ entry: ['./src/index.ts'], platform: 'neutral', dts: true, plugins: [solid()], deps: { neverBundle: ['solid-js', /^solid-js\//], }, })在 option-dependencies.md 的 "Framework Component" 示例中,solid-js与vue、react、svelte被并列列为典型的框架外置目标,支持字符串与正则两种写法,字符串精确匹配包名,正则用于命中命名空间下的子路径(如solid-js/web、solid-js/html)。
2. 用peerDependencies声明宿主框架
组件库本身不携带框架运行时,应要求消费方自行安装solid-js:
{ "name": "my-solid-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": { "solid-js": "^1.0.0" }, "devDependencies": { "solid-js": "^1.0.0", "tsdown": "^0.9.0", "typescript": "^5.0.0", "unplugin-solid": "^0.0.0" } }说明:上述
package.json的结构模式(exports分环境入口、files: ["dist"]、框架走peerDependencies)与同技能文档 recipe-react.md 中针对 React 的完整示例同构,此处将框架替换为solid-js即得。实际开发中若开启 tsdown 的exports: true,tsdown 会基于产物自动生成exports字段(见 SKILL.md 最佳实践第 6 条)。
3. 可选的产物优化项
结合技能库其他文档,以下选项可按需叠加(详见 option-output-format.md 与 option-dts.md):
export default defineConfig({ entry: ['./src/index.ts'], format: ['esm', 'cjs'], // 需要 CJS 时注意:cjs 恒为 node 平台 platform: 'neutral', dts: true, clean: true, // 构建前清空 outDir treeshake: true, // 摇树优化,减少包体积 minify: true, // 生产构建压缩 plugins: [solid()], deps: { neverBundle: ['solid-js', /^solid-js\//], }, })故障排查速查表
综合原文档、advanced-plugins.md、option-platform.md 与 option-dts.md,将常见问题归纳如下:
| 现象 | 原因与对策 |
|---|---|
| 插入了非 rolldown 入口的 Solid 插件后 TS 报类型错误 | 优先改用unplugin-solid/rolldown;确需兼容时用// @ts-expect-error或as any标注 |
neutral平台下第三方依赖解析失败,提示 main field ignored | 上游包没有exports字段;用inputOptions.resolve.mainFields显式声明(如['module', 'main']) |
.d.ts生成很慢 | 在 tsconfig 开启isolatedDeclarations(前提:所有导出显式标注类型),否则回退 TS 编译器 |
| 声明文件中缺类型 | 检查dts: true已开启、TypeScript 已安装为 devDependency,且公共 API 全部有显式类型 |
运行时出现两份solid-js(响应式状态割裂) | 在deps.neverBundle中加入'solid-js'及/^solid-js\//,并在package.json声明peerDependencies |
需要cjs但设置platform: 'browser'/'neutral'未生效 | CJS 恒使用 node 平台(option-platform.md 的固有限制),按需改用 ESM 或接受该行为 |
| 构建机 Node 版本过低启动失败 | tsdown 运行要求 Node.js 22.18.0+;产物目标版本用target选项另行指定 |
在 airi 仓库中的工程上下文
airi 是一个规模庞大、以 pnpm workspace + turbo 组织的 monorepo(根目录可见 pnpm-workspace.yaml、turbo.json、package.json)。在本仓库中:
- tsdown 被广泛用作 TypeScript 子包的标准打包器,仓库内存在大量
tsdown.config.ts。例如 packages/plugin-sdk/tsdown.config.ts 采用"多入口 +dts: true+format: 'esm'"的配置;SKILL.md 中亦整理了完整的选项索引与最佳实践清单。 - tsdown 技能参考集(.agents/skills/tsdown/references/)将框架类打包配方按
recipe-*.md组织,包含 React、Vue、Solid、Svelte、WASM 五份文档,本文聚焦的 recipe-solid.md 是其中面向 Solid 的一册。 - 需要说明的是:本文论述的 Solid 打包配方面向构建"框架无关、可被 Solid 消费方复用"的通用组件库这一场景,其价值在于你无论把产物发给浏览器、SSR 还是 Electron renderer,都能保证一份 dist 直接可用。若你正在 airi 内新建一个需要被多种宿主消费的纯 TS 子包,其打包形态与 packages/plugin-sdk/tsdown.config.ts 类似;而一旦需要支撑 Solid 消费方,则在上述模式上叠加本食谱的
plugins: [solid()]与platform: 'neutral'即可。
相关阅读
- advanced-plugins.md:Rolldown / Rollup / Unplugin / Vite 四类插件的兼容性与使用方式(含 framework-specific plugin 示例)
- option-platform.md:
node/browser/neutral平台语义、mainFields 解析规则与 CJS 平台限制 - option-dts.md:类型声明生成的完整选项表、
isolatedDeclarations加速与 declaration maps - option-dependencies.md:
neverBundle/alwaysBundle/onlyBundle与默认外置规则 - option-output-format.md:ESM / CJS / IIFE / UMD 产物格式
- recipe-react.md:React 组件库的同构范式(JSX transform、React Compiler 与 peer 外置),可与本文对照学习
- guide-getting-started.md:安装、首个产物与 CLI 基础
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考