Output Format

There are four supported output formats for the generated JavaScript files in Rslib: esm, cjs, umd, and mf. In this chapter, we will introduce the differences between these formats and how to choose the right one for your library.

ESM / CJS

Library authors need to carefully consider which module formats to support. Let's understand ESM (ECMAScript Modules) and CJS (CommonJS) and when to use them.

What are ESM and CJS?

  • ESM: ESM is a modern module system introduced in ES2015 that allows JavaScript code to be organized into reusable, self-contained modules. ESM is now the standard for both browser and Node.js environments, replacing older module systems like CommonJS (CJS) and AMD.

  • CommonJS: CommonJS is a module system used in JavaScript, particularly in server-side environments like Node.js. It was created to allow JavaScript to be used outside of the browser by providing a way to manage modules and dependencies.

What is the dilemma of ESM / CJS

The following references are from Node Modules at War: Why CommonJS and ES Modules Can't Get Along.

  1. You can't require() ESM scripts; you can only import ESM scripts, like this: import {foo} from 'foo'
  2. CJS scripts can't use static import statements like the one above.
  3. ESM scripts can import CJS scripts, but only by using the default import syntax import _ from 'lodash', not the named import syntax import {shuffle} from 'lodash', which is a hassle if the CJS script uses named exports. (Except, sometimes, unpredictibly, Node can figure out what you meant!)
  4. ESM scripts can require() CJS scripts, even with named exports, but it's typically not worth the trouble, because it requires even more boilerplate, and, worst of all, bundlers like Webpack and Rollup don't/won't know how to work with ESM scripts that use require().
  5. CJS is the default; you have to opt-in to ESM mode. You can opt-in to ESM mode by renaming your script from .js to .mjs. Alternately, you can set "type": "module" in package.json, and then you can opt-out of ESM by renaming scripts from .js to .cjs. (You can even tweak just an individual subdirectory by putting a one-line {"type": "module"} package.json file in there.)

When to support which format?

For different shapes of libraries, the choice of module format may vary. Here are two common scenarios:

ship pure ESM package

shipping only ESM is the best choice for libraries that are intended to be used in modern environments, such as browser applications or Node.js applications that support ESM. However, if the upstream library is in format of CJS, they only can import pure ESM by using dynamic import like const pureEsmLib = await import('pure-esm-lib').

  • Pros:
    • ESM is the official JavaScript standard, making it more future-proof and widely supported across environments.
    • ESM enables static analysis, which facilitates optimizations like tree-shaking to remove unused code.
    • The syntax is cleaner and more intuitive, with import and export statements that are easier to read compared to CommonJS.
    • ESM allows for better compatibility across both browser and server environments, making it ideal for isomorphic or universal JavaScript applications.
  • Cons:
    • ESM modules are loaded asynchronously, which can complicate conditional imports and lazy loading in some cases.
    • Some Node.js tools and libraries still have limited or incomplete support for ESM, requiring workarounds or additional configuration.
    • You must explicitly include file extensions in import paths, which can be cumbersome, especially when working with TypeScript or other transpiled languages.

ship ESM & CJS (dual) package

The community is migrating to ESM, but there are still many projects using CJS. If you want to support both ESM and CJS, you can publish a dual package. For most library authors, offering dual formats is a safer and smoother way to access the best of both worlds. You could read antfu' blog post Publish ESM and CJS packages for more details.

  • Pros:

    • Wider compatibility: Dual packages support both modern ESM environments and legacy CJS environments, ensuring broader usage across different ecosystems.
    • Gradual migration: Developers can gradually transition from CJS to ESM without breaking existing projects, allowing for smoother adoption of the new standard.
    • Flexibility for consumers: Users of the package can choose which module system best fits their project, providing flexibility in different build tools and environments.
    • Cross-runtime support: Dual packages can work in multiple runtimes, such as Node.js and browsers, without requiring additional bundling or transpilation.
  • Cons:

    • Increased complexity: Maintaining two module formats adds complexity to the build process, requiring additional configuration and testing to ensure both versions work correctly.
    • Dual package hazard: Mixing ESM and CJS can lead to issues such as broken instanceof checks or unexpected behavior when dependencies are loaded in different formats.

UMD

What is UMD?

UMD stands for Universal Module Definition, a pattern for writing JavaScript modules that can work universally across different environments, such as both the browser and Node.js. Its primary goal is to ensure compatibility with the most popular module systems, including AMD (Asynchronous Module Definition), CommonJS (CJS), and browser globals.

When to use UMD?

If you are building a library that needs to be used in both the browser and Node.js environments, UMD is a good choice. UMD can be used as a standalone script tag in the browser or as a CommonJS module in Node.js.

A detailed answer from StackOverflow: What is the Universal Module Definition (UMD)?

However, for frontend libraries, you still offer a single file for convenience, that users can download (from a CDN) and directly embed in their web pages. This still commonly employs a UMD pattern, it's just no longer written/copied by the library author into their source code, but added automatically by the transpiler/bundler.

And similarly, for backend/universal libraries that are supposed to work in Node.js, you still also distribute a commonjs module build via npm to support all the users who still use a legacy version of Node.js (and don't want/need to employ a transpiler themselves). This is less common nowadays for new libraries, but existing ones try hard to stay backwards-compatible and not cause applications to break.

How to build a UMD library?

  • Set the lib.format to umd in the Rslib configuration file.
  • If the library need to be exported with a name, set lib.umdName to the name of the UMD library.
  • Use output.externals to specify the external dependencies that the UMD library depends on, lib.autoExtension is enabled by default for UMD.

Examples

The following Rslib config is an example to build a UMD library.

  • lib.format: 'umd': instruct Rslib to build in UMD format.
  • lib.umdName: 'RslibUmdExample': set the export name of the UMD library.
  • output.externals.react: 'React': specify the external dependency react could be accessed by window.React.
  • runtime: 'classic': use the classic runtime of React to support applications that using React version under 18.
rslib.config.ts
1import { pluginReact } from '@rsbuild/plugin-react';
2import { defineConfig } from '@rslib/core';
3
4export default defineConfig({
5  lib: [
6    {
7      format: 'umd',
8      umdName: 'RslibUmdExample',
9      output: {
10        externals: {
11          react: 'React',
12        },
13        distPath: {
14          root: './dist/umd',
15        },
16      },
17    },
18  ],
19  output: {
20    target: 'web',
21  },
22  plugins: [
23    pluginReact({
24      swcReactOptions: {
25        runtime: 'classic',
26      },
27    }),
28  ],
29});

MF

What is MF?

MF stands for Module Federation. Module Federation is an architectural pattern for JavaScript application decomposition (similar to microservices on the server-side), allowing you to share code and resources between multiple JavaScript applications (or micro-frontends).

See Module Federation for more details.