使用 Rslib 发布 JavaScript 库
使用Rslib发布JavaScript库:配置ESM和CJS构建、生成TypeScript声明文件、打包组件样式,并在CI中检查包配置错误。
Rslib 是基于 Rsbuild 和 Rspack 构建的库构建工具。只需一份配置,即可生成 ESM 和 CommonJS 产物、TypeScript 类型声明、经框架编译的组件以及处理后的静态资源。它面向发布到 npm 的包,而非用于部署的应用。
这一区别至关重要。应用的 bundle 只需在一个浏览器标签页中运行即可;而库会被其他人的打包工具、不同的 Node.js 版本和各种 TypeScript 配置引入,出问题的方式往往是你自己的测试应用永远暴露不出来的:require() 调用失败、类型被解析为 any、组件发布时丢失了样式。本指南以 2026 年 9 月 3 日发布的 Rslib 1.0 为例,带你把一个包从首次构建一路推进到 CI 中的发布前检查。
核心要点
- Rslib 1.0 的配置和 JavaScript API 均已稳定并遵循语义化版本(Semantic Versioning),要求 Node.js 20.19+ 或 22.12+。
- Rslib 文档建议新包采用纯 ESM 格式,仅当特定使用方无法加载 ESM 时才构建 ESM/CJS 双格式产物。
- 在 Node.js
^20.19.0或>=22.12.0上,CommonJS 代码可以require()纯 ESM 包,前提是其加载的所有模块(包括入口模块)都没有使用顶层await。 - Bundle 模式(
bundle: true)适用于 SDK、CLI 和 Node.js 工具库;Bundleless 模式(bundle: false)会保留源码文件结构,适用于组件库。 rsbuild-plugin-publint和rsbuild-plugin-arethetypeswrong可以配置为仅在 CI 中随rslib build运行,让打包错误在npm publish之前就导致构建失败。
为什么发布库比构建应用更难?
发布库之所以比构建应用更难,是因为一个可发布的包必须同时做对四件事:需要 ESM(可能还有 CJS)产物,并能在你支持的所有运行时中正常加载;需要类型声明文件,并能在每个使用方的 moduleResolution 配置下正确解析;组件必须连同 CSS 一起发布;package.json 中的 exports 字段必须让每个条件都指向真实存在的文件。
这些工作通常分别由不同的工具完成:打包工具负责 JavaScript,tsc 负责类型声明,PostCSS 负责样式,再加上一份手写的 exports 映射把它们串起来。只要其中任何一个工具改变了输出路径,包就会出问题。
什么是 Rslib?
Rslib 是一个在单次构建中完成上述全部四项工作的库构建工具。lib 数组定义每种输出格式;dts 选项负责生成类型声明,在 bundleless 模式下还会修正声明文件中的路径别名和导入扩展名,使其与生成的 JavaScript 文件保持一致;CSS 的处理方式取决于构建模式;Publint 则负责捕获 exports 中的错误。需要注意的是,Rslib 不会替你编写 exports 字段,而是检查你写好的那一份。
从零到一:最小化的 Rslib 构建
最快的上手方式是用脚手架创建项目,或者直接运行一条 CLI 命令。对于已有的包,可以添加一个配置文件,为每种格式配置一个 lib 条目。根据快速上手文档,Rslib v1 要求 Node.js 20.19+ 或 22.12+。
npm create rslib@latest
# or, in an existing package
npm add @rslib/core -D
单一格式的构建不需要配置文件:
npx rslib build --entry src/index.ts --format esm --dts
--entry 指定源码入口,--format esm 选择模块格式,--dts 生成类型声明。如需双格式产物,可创建 rslib.config.ts:
// rslib.config.ts
import { defineConfig } from '@rslib/core';
export default defineConfig({
lib: [
{ format: 'esm', dts: true },
{ format: 'cjs', dts: true },
],
syntax: 'es2023',
});
顶层配置会作用于所有产物,Rslib 会将其与每个 lib 条目合并,因此每个条目只需列出差异部分。Rslib 1.0 还允许在只需要一个默认 ESM 产物时完全省略 lib。接下来添加构建脚本:
{ "scripts": { "build": "rslib build" } }
构建完成后,输出目录默认为 dist,其中包含 ESM 产物、CJS 产物和类型声明文件。文件扩展名由 lib.autoExtension 决定:设置了 "type": "module" 时,ESM 产物使用 .js,CJS 产物使用 .cjs;未设置时,CJS 使用 .js,ESM 使用 .mjs。默认情况下,两种格式生成的类型声明可能使用相同的 .d.ts 文件名,可通过 dts 选项按格式控制声明文件的扩展名。在 exports 映射中,将 import 条件指向 ESM 入口,require 条件指向 CJS 入口,并为每个条件配上对应的类型声明。请从你自己的 dist 目录中复制真实路径,不要凭猜测填写。
对于 ESM 和 CJS 产物,Rslib 默认不会打包 dependencies、optionalDependencies 和 peerDependencies,因此像 React 这样的 peer 依赖会保持 external。devDependencies 则会被打包进产物。
应该发布纯 ESM 还是 ESM/CJS 双格式?
Rslib 的输出格式指南建议新包以纯 ESM 格式发布,仅在使用方明确需要时才添加 CJS。双格式产物的代价远不止多一个构建步骤:你需要维护两套 exports 条目,测试的配置组合也会翻倍。如果使用方同时加载了两种格式,还会得到你的包的两个实例,这可能破坏共享状态以及基于 instanceof 的身份判断。
许多 CommonJS 使用方其实已经不再需要 CJS 产物了。在 Node.js ^20.19.0 或 >=22.12.0 上,它们可以直接加载纯 ESM 包,但需满足两个条件:你的 exports 必须允许 require() 调用访问到 ESM 入口;被加载的模块图中不能有任何模块使用顶层 await:
const utils = require('my-esm-only-package');
只有当部分 CommonJS 用户使用的 Node 版本、工具或运行时无法通过 require() 加载 ESM,或者有用户明确要求提供独立的 CJS 文件时,才需要发布双格式产物。无论选择哪种方式,Node.js 包示例都涵盖了 package.json 方面的配置。
发布前检查
有两个 Rsbuild 插件可以在 npm 发现问题之前,就在 Rslib 构建中捕获打包缺陷。rsbuild-plugin-publint 会检查 package.json、文件结构和 exports 配置中的常见错误;rsbuild-plugin-arethetypeswrong 会测试当使用方采用不同的模块解析配置时,你的类型是否仍能正常加载。Rslib 1.0 发布公告中展示了让这两个插件仅在设置了 CI 时运行的写法:
// rslib.config.ts
import { defineConfig } from '@rslib/core';
import { pluginAreTheTypesWrong } from 'rsbuild-plugin-arethetypeswrong';
import { pluginPublint } from 'rsbuild-plugin-publint';
export default defineConfig({
lib: [
{ format: 'esm', dts: true },
{ format: 'cjs', dts: true },
],
plugins: [
pluginPublint({ enable: Boolean(process.env.CI) }),
pluginAreTheTypesWrong({ enable: Boolean(process.env.CI) }),
],
});
将两者都安装为开发依赖即可。这样本地构建依然快速,而 CI 中原本就会执行的 rslib build 步骤就成了发布前的把关环节。
在大型项目中,类型声明的生成速度也有所提升。使用 TypeScript 7 或更高版本时,Rslib 会自动使用 tsgo,并且仍会执行类型检查。实验性的 dts.isolated 选项速度更快,但会跳过类型检查,因此需要搭配单独的类型检查步骤。在 Rslib 团队基于 Rsbuild 仓库进行的测试中,类型声明生成耗时在 TypeScript 6 下为 9.7 秒,TypeScript 7 下为 4.1 秒,使用 isolated declarations 时为 2.3 秒。实际提升幅度会因项目而异。
组件库与 Bundleless 模式
组件库建议使用 bundleless 模式,SDK 和 CLI 则使用 bundle 模式。设置 bundle: false 后,Rslib 会将每个源文件转换为独立的输出文件,并保持与源码相同的目录结构,从而把 tree shaking 和按需加载交给使用方的打包工具处理。React、Vue、Svelte 和 Solid 的编译器通过 Rsbuild 插件接入,create-rslib 也为每个框架都提供了对应的模板。
// rslib.config.ts
import { pluginReact } from '@rsbuild/plugin-react';
import { defineConfig } from '@rslib/core';
export default defineConfig({
bundle: false,
output: { target: 'web' },
plugins: [pluginReact()],
});
组件按常规方式引入样式:
import './button.css';
import styles from './button.module.css';
CSS Modules 和 PostCSS 开箱即用,Rslib 输出样式的方式会随构建模式而变化。Sass、Less、Stylus 和 Tailwind CSS 可通过 Rsbuild 插件添加。Vue、Svelte 和 Solid 库的配置结构相同,只需换成各自的插件。在 Rslib 1.0 发布公告中,针对 10,000 个 React 组件的基准测试显示:与 0.7.0 相比,1.0 的无缓存构建时间缩短约 24.3%,有缓存构建时间缩短约 56.7%,产物体积在 gzip 前减小约 32.2%,gzip 后减小 4.1%。
Module Federation 产物
Rslib 还可以通过 format: 'mf' 将库构建为 Module Federation remote,让多个应用在运行时加载它,而不是从 npm 安装。当共享代码的独立部署比版本化的包发布更重要时,可以采用这种方式。
Rslib、tsup 与 Vite 库模式对比
Rslib、tsup 和 Vite 库模式的区别更多在于定位,而非速度。tsup 基于 esbuild,专注于打包 TypeScript;Vite 库模式则是把一个应用构建工具指向库入口。Rslib 基于 Rsbuild 和 Rspack 专为库而打造,一份配置即可涵盖输出格式、bundle 与 bundleless 模式、类型声明、框架编译、静态资源以及发布前检查。如果你的应用已经在使用 Rsbuild 或 Rspack,库还可以与之共享插件和配置。
JavaScript 打包工具现状一文介绍了 Rspack 在整个生态中所处的位置。如果你更愿意基于通用打包工具和插件自行搭建构建流程,可以从这篇 Rollup 入门指南开始。关于迁移,Rslib 提供了从 tsc 迁移的指南,其快速上手文档中也链接了对应的 tsup 迁移指南。
总结
只有当格式、类型声明、样式和 exports 映射彼此一致时,一个库才算真正可以发布,而 Rslib 会将它们一并构建和检查。从纯 ESM 起步,仅在使用方需要时才向 lib 数组添加 CJS 条目,并在下一次发布前于 CI 中启用 publint 和 arethetypeswrong。
常见问题
如何将某个依赖打包进 Rslib 产物,而不是保持 external?
在 lib 条目中设置 output.autoExternal,并关闭你希望打包的依赖字段,例如将 dependencies 设为 false。Rslib v1 已弃用 lib.autoExternal,改用 Rsbuild 的 output.autoExternal,但旧选项仍然可用。自动 external 仅在 bundle 模式下生效;在 bundleless 模式下,依赖永远不会被打包。如果只想将特定包保持 external,可以将它们列在 output.externals 中。
Rslib 能否将所有 TypeScript 类型声明打包成单个文件?
可以。将 @microsoft/api-extractor 安装为开发依赖,并在 lib 条目中将 dts.bundle 设为 true。类型声明的打包由 API Extractor 完成,dts.bundle 默认为 false。未被 external 的直接依赖,其类型声明会被自动内联。设置 dts.bundle.bundledPackages 会替换该默认列表,在需要打包传递依赖时非常有用。
Rslib 能否为 CDN 或 script 标签构建 UMD 产物?
可以。在 lib 条目中将 format 设为 'umd',并将 lib.umdName 设为该产物暴露的全局变量名,例如 MyLibrary。UMD 产物默认关闭自动 external,因此除非将依赖列在 output.externals 中,否则它们都会被打包。output.externals 中的每个条目会将一个包映射到一个浏览器全局变量,例如将 react 映射为 React。UMD 产物默认开启 lib.autoExtension。
如何为 CommonJS 产物生成 .d.cts 类型声明?
在每个 lib 条目中启用 dts.autoExtension。它遵循与 lib.autoExtension 相同的规则:当 package.json 中 type 设为 module 时,ESM 类型声明使用 .d.ts,CJS 类型声明使用 .d.cts。该选项默认关闭,因此 ESM 和 CJS 产物可能会生成同名的 .d.ts 文件。请将 require 条件下的 types 指向 .d.cts 文件。
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k