12k
All articles

Publishing a JavaScript Library With Rslib

Publish JavaScript libraries with Rslib: configure ESM and CJS builds, generate TypeScript declarations, ship component styles, and catch package errors in CI.

OpenReplay Team
OpenReplay Team
Publishing a JavaScript Library With Rslib

Rslib is a library build tool built on Rsbuild and Rspack that produces ESM and CommonJS output, TypeScript declarations, framework-compiled components and processed assets from one config, for packages you publish to npm rather than apps you deploy.

That distinction matters. An app bundle only has to run in one browser tab. A library gets imported by other people’s bundlers, Node.js versions and TypeScript settings, and it breaks in ways your own test app never shows you: a require() that fails, types that resolve to any, a component that ships without its styles. This guide takes one package from a first build to a pre-publish check in CI using Rslib 1.0, released September 3, 2026.

Key Takeaways

  • Rslib 1.0 has a stable config and JavaScript API that follow Semantic Versioning, and it requires Node.js 20.19+ or 22.12+.
  • The Rslib docs recommend pure ESM for new packages and a dual ESM/CJS build only when a specific consumer cannot load ESM.
  • On Node.js ^20.19.0 or >=22.12.0, CommonJS code can require() a pure ESM package, as long as no module it loads, the entry included, uses top-level await.
  • Bundle mode (bundle: true) suits SDKs, CLIs and Node.js utilities. Bundleless mode (bundle: false) keeps the source file structure and suits component libraries.
  • rsbuild-plugin-publint and rsbuild-plugin-arethetypeswrong can run inside rslib build in CI only, so packaging mistakes fail the build before npm publish.

Why Is Publishing a Library Harder Than Building an App?

Publishing a library is harder than building an app because a publishable package has to get four things right at once. It needs ESM and possibly CJS output that load in every runtime you support. It needs declaration files that resolve under each consumer’s moduleResolution setting. Components have to ship with their CSS. And the package.json exports field has to point each condition at a file that actually exists.

Each of these usually comes from a different tool: a bundler for the JavaScript, tsc for the declarations, a PostCSS step for the styles, and a hand-written exports map holding it all together. Whenever one of those tools changes an output path, the package breaks.

What Is Rslib?

Rslib is a library build tool that covers all four of those jobs in one build. A lib array defines each output format. The dts option generates declarations, and in bundleless mode it also fixes path aliases and import extensions inside them to match the JavaScript files it writes. CSS handling depends on the build mode. Publint catches exports mistakes. Rslib does not write your exports field for you. It checks the one you wrote.

A Minimal Rslib Build, End to End

The fastest way to start is to scaffold a project or run a single CLI command. For an existing package, add a config file with one lib entry per format. According to the quick start, Rslib v1 needs Node.js 20.19+ or 22.12+.

npm create rslib@latest
# or, in an existing package
npm add @rslib/core -D

A single-format build doesn’t need a config file:

npx rslib build --entry src/index.ts --format esm --dts

--entry sets the source entry, --format esm picks the module format, and --dts emits declarations. For dual output, create 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',
});

Anything set at the top level applies to every output. Rslib merges it with each lib item, so each entry only lists what differs. Rslib 1.0 also lets you leave out lib completely when you only need one default ESM output. Add a build script:

{ "scripts": { "build": "rslib build" } }

After the build, the output directory defaults to dist. It contains an ESM build, a CJS build and declaration files. File extensions follow lib.autoExtension. With "type": "module", ESM output gets .js and CJS output gets .cjs. Without it, CJS gets .js and ESM gets .mjs. By default, both formats can emit declarations with the same .d.ts name. The dts options control per-format declaration extensions. In your exports map, point the import condition at the ESM entry and the require condition at the CJS entry, and give each one matching declarations. Copy the real paths from your own dist rather than guessing them.

For ESM and CJS output, Rslib does not bundle dependencies, optionalDependencies or peerDependencies by default, so a peer such as React stays external. devDependencies do get bundled.

Should You Publish ESM-Only or Dual ESM/CJS?

Rslib’s output format guide says to publish new packages as pure ESM and add CJS only when a consumer clearly needs it. Dual output costs more than a second build step. You have two sets of exports entries to keep right and twice as many setups to test. A consumer that loads both formats also gets two instances of your package, which can break shared state and instanceof-style identity checks.

Many CommonJS consumers no longer need a CJS build at all. On Node.js ^20.19.0 or >=22.12.0, they can load a pure ESM package directly. Two things must hold: your exports must let a require() call reach the ESM entry, and no module in the loaded graph can use top-level await:

const utils = require('my-esm-only-package');

Only publish dual output when some of your CommonJS users are on a Node version, tool or runtime that can’t load ESM through require(), or when a user asks for a separate CJS file. The Node.js package examples cover the package.json side of either choice.

Checks Before You Publish

Two Rsbuild plugins catch packaging bugs in an Rslib build before npm does. rsbuild-plugin-publint looks for common mistakes in your package.json, your file layout and your exports setup. rsbuild-plugin-arethetypeswrong tests whether your types still load when a consumer uses a different module resolution setting. The Rslib 1.0 announcement shows both running only when CI is set:

// 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) }),
  ],
});

Install both as dev dependencies. Local builds stay fast, and the rslib build step you already run in CI becomes the gate before publishing.

Declaration generation gets faster on large projects too. With TypeScript 7 or later, Rslib uses tsgo automatically and still type-checks. The experimental dts.isolated option is faster again, but it skips type checking, so run a separate type-check step alongside it. In the Rslib team’s own test on the Rsbuild repository, declaration generation took 9.7 s with TypeScript 6, 4.1 s with TypeScript 7 and 2.3 s with isolated declarations. Gains on your own project will differ.

Component Libraries and Bundleless Mode

Use bundleless mode for component libraries and bundle mode for SDKs and CLIs. With bundle: false, Rslib turns each source file into its own output file, in the same folder layout as your source. That leaves tree shaking and on-demand loading to the consumer’s bundler. React, Vue, Svelte and Solid compilers plug in through Rsbuild plugins, and create-rslib includes a template for each framework.

// rslib.config.ts
import { pluginReact } from '@rsbuild/plugin-react';
import { defineConfig } from '@rslib/core';

export default defineConfig({
  bundle: false,
  output: { target: 'web' },
  plugins: [pluginReact()],
});

Components import styles the usual way:

import './button.css';
import styles from './button.module.css';

CSS Modules and PostCSS work without extra setup, and the way Rslib outputs styles changes with the build mode. Sass, Less, Stylus and Tailwind CSS are added through Rsbuild plugins. Vue, Svelte and Solid libraries use the same config shape with their own plugin. In the Rslib 1.0 announcement, a benchmark of 10,000 React components shows 1.0 cutting uncached build time by about 24.3% and cached build time by about 56.7% compared with 0.7.0, with output about 32.2% smaller before gzip and 4.1% smaller after.

Module Federation Output

Rslib can also build a library as a Module Federation remote with format: 'mf', so multiple applications load it at runtime instead of installing it from npm. Use it when independent deployment of shared code matters more than versioned package releases.

Rslib vs tsup vs Vite Library Mode

Rslib, tsup and Vite library mode differ in scope more than in speed. tsup is built on esbuild and focuses on bundling TypeScript. Vite library mode takes an application build tool and points it at a library entry. Rslib is built specifically for libraries on top of Rsbuild and Rspack, and one config covers output formats, bundle and bundleless modes, declarations, framework compilation, assets and pre-publish checks. If your app already uses Rsbuild or Rspack, the library can share its plugins and configuration.

The current state of JavaScript bundlers shows where Rspack fits in the wider ecosystem. If you’d rather assemble your own pipeline from a general-purpose bundler and plugins, start with this guide to getting started with Rollup. For migrations, Rslib has a guide for moving from tsc, and its quick start links a matching guide for tsup.

Conclusion

A library is only ready to publish when its formats, declarations, styles and exports map all agree with each other, and Rslib builds and checks them together. Start with pure ESM, add a CJS entry to the lib array only when a consumer needs it, and turn on publint and arethetypeswrong in CI before your next release.

FAQs

How do I bundle a dependency into my Rslib output instead of leaving it external?

Set output.autoExternal inside the lib entry and turn off the dependency field you want bundled, for example dependencies set to false. Rslib v1 deprecates lib.autoExternal in favour of Rsbuild's output.autoExternal, but the old option still works. Automatic externalization only applies in bundle mode. In bundleless mode, dependencies are never bundled. To keep only specific packages external, list them in output.externals.

Can Rslib bundle all TypeScript declarations into a single file?

Yes. Install @microsoft/api-extractor as a dev dependency and set dts.bundle to true in the lib entry. API Extractor does the declaration bundling, and dts.bundle is false by default. Direct dependencies that are not externalized get their declarations inlined automatically. Setting dts.bundle.bundledPackages replaces that default list, which helps when you need to bundle transitive dependencies.

Can Rslib build a UMD bundle for a CDN or script tag?

Yes. Set format to 'umd' in a lib entry and set lib.umdName to the global name the bundle exposes, such as MyLibrary. Automatic externalization is off by default for UMD output, so dependencies get bundled unless you list them in output.externals. Each entry there maps a package to a browser global, for example react to React. lib.autoExtension is on by default for UMD.

How do I emit .d.cts declarations for the CommonJS build?

Enable dts.autoExtension in each lib entry. It follows the same rules as lib.autoExtension: with type set to module in package.json, ESM declarations use .d.ts and CJS declarations use .d.cts. The option is off by default, so ESM and CJS output can end up with the same .d.ts file name. Point the types for the require condition at the .d.cts file.

DevTools for the frontend

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

We use cookies to improve your experience. By using our site, you accept cookies.