Comprehensive Guide to Packaging JavaScript Libraries: ESM, CJS, UMD and Best Practices
This guide provides clear, practical recommendations for packaging JavaScript libraries—including outputting ESM, CJS and UMD formats, handling multi‑file builds, code compression, sourcemaps, TypeScript typings, external frameworks, modern browser support, and essential package.json fields—so developers can create robust, tree‑shakable, and well‑documented npm packages.
This guide offers a concise set of recommendations that most JavaScript libraries should follow, explaining the rationale behind each suggestion and helping you decide when a recommendation may be unnecessary. It applies to libraries, not applications, and is tool‑agnostic.
Output esm , cjs and umd formats
esm stands for “EcmaScript module”.
cjs stands for “CommonJS module”.
umd stands for “Universal Module Definition”; it can run in a <script> tag, be loaded by a CommonJS loader, or be loaded by an AMD loader.
esm is considered the “future”, but cjs still holds an important place in the ecosystem. esm enables better tree‑shaking for bundlers, making it crucial for libraries, and someday you may only need to output esm .
Although umd is compatible with CommonJS loaders, you may still want both cjs and umd outputs because CommonJS files usually handle conditional imports better. Example:
if (process.env.NODE_ENV === "production") {
module.exports = require("my-lib.production.js");
} else {
module.exports = require("my-lib.development.js");
}When using a CommonJS module, only the production or development bundle is imported, whereas a UMD module may end up pulling in both bundles.
Be aware of the dual‑package hazard: developers might import both cjs and esm versions. Using package.json#exports can mitigate this issue.
Multi‑file output
Preserving the original file structure improves tree‑shaking. If you use a bundler or compiler, configure it to keep the source directory layout so that side‑effects can be marked per file.
When producing a bundle that should run directly in browsers (usually umd , sometimes modern esm ), it is better to serve a single large file rather than many small ones, and you should compress the code and generate a sourcemap.
Should you compress the code?
You can apply various levels of compression depending on how much you care about the final bundle size after developers’ own bundling.
Most compilers already strip whitespace and perform simple optimisations. Using a tool like terser can reduce the final size by up to 95 % without any extra effort.
If you compress the library before publishing, you gain additional size savings but must understand the compressor’s configuration and side‑effects. Many publishing pipelines skip this step, so you may miss out on the savings unless you do it yourself.
Create a sourcemap
Any form of compilation disconnects runtime errors from the original source. Generating a sourcemap helps future developers trace issues back to the original code.
Create TypeScript typings
Adding typings improves the developer experience for TypeScript users and also benefits editors like VSCode for non‑TypeScript projects.
You do not have to write the library in TypeScript; you can keep the source in JavaScript and add JSDoc comments, then let TypeScript generate .d.ts files, or you can write a separate index.d.ts file.
After generating the typings, ensure that package.json#exports and package.json#types point to the correct files.
External frameworks
Do not bundle frameworks such as React or Vue into your library. Mark them as externals so that they are not included in the final output, reducing bundle size and avoiding duplicate copies.
Also list those frameworks in peerDependencies so that developers know they must install them.
Target modern browsers
Provide multiple outputs to support both modern and legacy browsers. For example, with TypeScript you can generate an esm build targeting esnext and a umd build targeting es5 .
Modern browsers will use the esm build, while environments that rely on <script> tags will fall back to the compiled umd version.
Necessary compilation
If the source is TypeScript, JSX, or Vue components, the library must output compiled JavaScript. JSX should be transformed to jsx() or createElement() calls.
Always generate a sourcemap alongside the compiled output.
Maintain a changelog
Record every version change and its impact on users, regardless of whether you automate the process or edit it manually.
Split out your CSS
Allow developers to import CSS on demand. For pure CSS libraries, provide a single file; for component libraries, ship per‑component CSS files so that only the needed styles are loaded.
Configure package.json
Key fields to configure:
name : Determines the package name on npm.
version : Combined with name to uniquely identify each release. Follow semver or another documented strategy.
exports : Defines the public API and conditional entry points (e.g., "import", "require", "development", "production").
files : Lists the files/directories to include in the published npm package (e.g., ["dist"] ).
type : Sets the default module system for .js files ("module" for ESM, "commonjs" for CJS).
sideEffects : Helps bundlers with tree‑shaking; set to false for pure modules or list files that have side effects.
main : CommonJS entry point (fallback when exports is unsupported).
module : ESM entry point (fallback when exports is unsupported).
browser : Points to a browser‑compatible build (usually esm ), but should not point to umd if you want optimal tree‑shaking.
types : Path to the TypeScript declaration file (e.g., index.d.ts ).
peerDependencies : Lists external frameworks that the consumer must install (e.g., React).
license : Specifies the open‑source license (e.g., "MIT"). Include a LICENSE.txt file with the full text.
Example exports configuration:
{
"exports": {
".": {
"types": "index.d.ts",
"module": "index.js",
"import": "index.js",
"require": "index.cjs",
"default": "index.js"
},
"./package.json": "./package.json"
}
}Example files field:
{
"files": ["dist"]
}Example sideEffects set to false:
{
"sideEffects": false
}Example peerDependencies for a React library:
{
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}By following these guidelines, you can produce a library that is easy to consume, works across environments, and integrates smoothly with modern bundlers.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.