English | 简体中文
This guide is written to provide at-a-glance suggestions that most libraries should follow, while also giving additional information if you want to understand why a suggestion is made or if you feel like you need to deviate from these suggestions. This guide is meant for libraries and not for applications (apps).
To emphasize, this is a list of suggestions and is not meant to be definitive list that all libraries must follow, or to provide flame bait for libraries that do not follow these suggestions. Each library is unique and there are probably good reasons a library may chose to not implement any given suggestion.
Finally, this guide is not meant to be specific to any particular bundler - there already exist many guides on how to set configs in specific bundlers. Instead, we want to focus on things that apply to every library and bundler (or lack of bundler).
Use tools to validate important settings
- publint.dev validates important settings with your
package.json
and suggests improvements if it finds them - arethetypeswrong validates that your TypeScript types are output and configured correctly
Supporting the whole ecosystem
esm
is short for "EcmaScript module."
cjs
is short for "CommonJS module."
umd
is short for "Universal Module Definition," and can be run by a raw <script>
tag, or in CommonJS module loaders, or by AMD
module loaders.
Without getting into the flame wars that generally happen around esm
and cjs
formats, esm
is considered "the future" but cjs
still has a strong hold on the community and ecosystem. esm
is easier for bundlers to correctly tree shake, so it is especially important for libraries to have this format. It's also possible that some day in the future your library only needs to output to esm
.
You may have noticed that umd
is already compatible with CommonJS module loaders - so why would you want to have both cjs
and umd
output? One reason is that CommonJS files generally perform better when conditionally depended on compared to umd
files; for example:
if (process.env.NODE_ENV === "production") {
module.exports = require("my-lib.production.js");
} else {
module.exports = require("my-lib.development.js");
}
The above example, when used with CommonJS modules, will only end up with either the production
or development
bundle. However, with a UMD module, it may be the case that a developer would end up with both bundles. Refer to this discussion for more information.
Finally, if your library is stateful, be aware that this does open the possibility of your library running into the dual package hazard, which can occur in situations where a developer ends up with both a cjs
and esm
version of your library in their application. The "dual package hazard" article linked above describes some ways to mitigate this issue, and the module
condition in package.json#exports
can also help prevent this from happening.
Better tree shaking by maintaining the file structure
If you use a bundler or transpilier in your library, it can be configured to output files in the same way that they were authored. This makes it easier to mark specific files as having side effects, which helps the developer's bundler with tree shaking. Refer to this article for more details.
An exception is if you are making a bundle meant to be used directly in the browser without any bundler (commonly, these are umd
bundles but could also be modern esm
bundles as well). In this case, it is better to have the browser request a single large file than need to request multiple smaller ones. Additionally, you should minify the bundle and create sourcemaps for it.
Determine your preferred level of minification
There are certain levels of minification you can apply to your library, and depending on how aggressive you want to be will determine how small your code will be once it's finally through a developer's bundler.
For example, most bundlers are already configured to remove whitespace and other easy optimizations, even from an NPM module (in this case, your library). According to Terser - a popular JavaScript mangler/compressor - that type of compression can reduce your bundle's final size by up to 95%. In some cases, you may be happy with those savings with no effort on your part.
However, there are additional savings that can occur if you were to run a minifier on your library before publishing, but doing so requires deeply understanding the settings and side-effects of your minifer. These type of compressions are generally not run by minifiers on NPM modules, and therefore you will miss out on those savings unless you do it yourself. Refer to this issue for additional information.
Finally, if you are creating a bundle intended to be used directly in the browser without a bundler (commonly, these are umd
bundles but could also be modern esm
bundles as well), you should always minify your bundle, create sourcemaps for it, and output to a single file.
When using a bundler or transpiler, generate sourcemaps
Any sort of transformation of your source code to a bundle will produce errors that point at the wrong location in your code. Help your future self out and create sourcemaps, even if your transformations are small.
Types improve the developer experience
As the number of developers using TypeScript continues to grow, having types built-in to your library will help improve the developer experience (DX). Additionally, devs who are not using TypeScript also get a better DX when they use an editor that understands types (such as VSCode, which uses the types to power its Intellisense feature).
However, creating types does NOT mean you must author your library in TypeScript.
One option is to continue using JavaScript in your source code and then also supplement it with JSDoc comments. You would then configure TypeScript to only emit the declaration files from your JavaScript source code.
Another option is to write the TypeScript type files directly, in an index.d.ts
file.
Once you have the types file, make sure you set your package.json#exports
and package.json#types
fields.
Don't include a copy of React, Vue, etc. in your bundle
When building a library that relies on a framework (such as React, Vue, etc.) or is a plugin for another library, you'll want to add that framework to your bundler's "externals" (or equivalent) configuration. This will make it so that your library will reference the framework but will not include it in the bundle. This will prevent bugs and also reduce the size of your library's package.
You should also add that framework to your library's package.json
's peer dependencies, which will help developers discover that you rely on that framework.
Use modern features and let devs support older browsers if needed
This article on web.dev makes a great case for your library to target modern features, and offers guidelines on how to:
- Enable developers to support older browsers when using your library
- Output multiple bundles that support various levels of browser support
As one example, if you're transpiling from TypeScript, you could create two versions of your package's code:
- An
esm
version with modern JavaScript generated by setting"target"="esnext"
in yourtsconfig.json
- A
umd
version with more broadly-compatible JavaScript generated by setting"target"="es5"
in yourtsconfig.json
With these settings, most users will get the modern code, but those using older bundler configurations or loading the code using a <script>
tag will get the version with additional transpilation for older browser support.
Turn TypeScript or JSX into function calls
If your library's source code in in a format that requires transpilation, such as TypeScript, React or Vue components, etc., then your output should be transpiled. For example:- Your TypeScript library should output JavaScript bundles
- Your React components like
<Example />
should output bundles that usejsx()
orcreateElement()
instead of JSX syntax.
When transpiling this way, make sure you create sourcemaps as well.
Track updates and changes
It doesn't matter whether it's through automatic tooling or through manual process, as long as developers have a way to see what has changed and how it affects them. Ideally, every change to your library's version has a corresponding update in your changelog.
Enable devs to only include the CSS they need
If you are creating a CSS library (like Bootstrap, Tailwind, etc.), it may be easier to provide a single CSS bundle that includes all the functionality that your library provides. However, even in that situation, your CSS bundle may end up becoming large enough that it affects the performance of the devs' sites. To help prevent that, libraries generally provide methods of generating a CSS bundle that only includes the necessary CSS for what the developer is using (for example, see how Bootstrap and Tailwind do it).
If CSS is only a part of what your library exposes (for example, a component library that has default styles), then it is ideal if you separate out your CSS into individual bundles per component that are imported when the corresponding component is used. One example of this is react-component.
There are a lot of important settings and fields to talk about in package.json
; I will highlight the most important ones here, but be aware that there are additional fields that you can set as well.
Give a name to your library
The name
field will determine the name of your package on npm
, and therefore the name that developers will use to install your library.
Note that there are restrictions on what you can name your library, and additionally you can add a "scope" if your library is part of an organization. Refer to the name docs on npm for more details.
The name
and the version fields combine to create a unique identifier for each iteration of your library.
Publish updates to your library by changing the version
As noted in the name section, the name and the version combine to create a unique identifier for your library on npm. When you make updates to the code in your library, you can then update the version
field and publish to allow developers to get that new code.
Libraries are encouraged to use a versioning strategy called semver, but note that some libraries choose to calver or their own unique versioning strategy. Whichever strategy you choose to use, you should document it so that developers understand how your library's versioning works.
You should also keep track of your changes in a changelog.
exports
define the public API for your library
The exports
field on package.json
- sometimes called "package exports" - is an incredibly useful addition, though it does add some complexity. The two most important things that it does is:
- Defines what can and cannot be imported from your library, and what the name of it is. If it's not listed in
exports
, then developers cannotimport
/require
it. In other words, it acts like a public API for users of your library and helps define what is public and what is internal. - Allows you to change which file is imported based on conditions (that you can define), such as "Was the file
import
ed orrequire
d? Does the developer want adevelopment
orproduction
version of my library?" etc.
There are some good docs from the NodeJS team and the Webpack team on the possibilities here. I'll provide one example that covers the most common use-cases:
{
"exports": {
".": {
"module": "./dist/index.mjs",
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
},
"default": "./dist/index.mjs"
},
"./package.json": "./package.json"
},
"types": "./dist/index.d.ts"
}
Let us dive into the meaning of these fields and why I chose this specific shape:
"."
indicates the default entry for your package- The resolution happens from top to bottom and stops as soon as a matching field is found; the order of entries is very important
- The
module
field is an "unofficial" field that is supported by bundlers like Webpack and Rollup. It should come beforeimport
andrequire
, and point to anesm
-only bundle -- which can be the same as your originalesm
bundle if it's purelyesm
. As noted in the formats section, it is meant to help bundlers only include one copy of your library, no matter if it wasimport
ed orrequire
ed. For a deeper dive and the reasoning behind this decision, you can read more here, here, and here. - The
import
field is for when someoneimport
s your library. - The
require
field is for when someonerequire
s your library. - The
default
field is used as a fallback for if none of the conditions match. While it may not be used at the moment, it's good to have it for "unknown future situations".
If a bundler or environment understands the exports
field, then the package.json
's top-level main, types, module, and browser fields are ignored, as exports
supersedes those fields. However, it's still important to set those fields, for tools or runtimes that do not yet understand the exports
field.
If you have a "development" and a "production" bundle (for example, you have warnings in the development bundle that don't exist in the production bundle), then you can also set them in the exports
field with "development"
and "production"
. Note that some bundlers like webpack
and vite
will recognize these conditions automatically; however, while Rollup can be configured to recognize them, that is something that you would have to instruct developers to do in their own bundler config.
(Note that while the "types" field is covered in a different section, it is included in the above snippet for people who are copy-pasting the example. Even though we have two separate "types" fields inside of the "export" field, the "types" field is also required at the root of the object for full backwards compatibility, as tools like arethetypeswrong will fail your package otherwise.)
files
defines which files are included in your NPM package
The files
field indicates to the npm
CLI which files and folders to include when you package your library to be put on NPM's package registry.
For example, if you transform your code from TypeScript into JavaScript, you probably don't want to include the TypeScript source code in your NPM package. (Instead, you should include sourcemaps)
Files can take an array of strings (and those strings can include glob-like syntax if needed), so generally it will look like:
{
"files": ["dist"]
}
Be aware that the files array doesn't accept a relative specifier; writing "files": ["./dist"]
will not work as expected.
One great way to verify you have set the files field up correctly is by running npm publish --dry-run
, which will list off the files that will be included based on this setting.
type
dictates which module system your .js
files use
Runtimes and bundlers need a way to determine what type of module system your .js
files are using - ESM or CommonJS. Because CommonJS came first, that is the what bundlers will assume by default, but you can control it by adding "type"
your package.json
.
Your options are either "type":"module"
or "type":"commonjs"
, and though you can leave it blank (to default to CommonJS) it's highly recommended that you set it to one or the other to explicity declare which one you're using.
Note that you can have a mix of module types in the project, through a couple of tricks:
.mjs
files will always be ESM modules, even if yourpackage.json
has"type": "commonjs"
(or nothing fortype
).cjs
files will always be CommonJS modules, even if yourpackage.json
has"type": "module"
- You can add additional
package.json
files that are nested inside of folders; runtimes and bundlers look for the nearestpackage.json
and will traverse the folder path upwards until they find it. This means you could have two different folders, both using.js
files, but each with their ownpackage.json
set to a differenttype
to get both a CommonJS- and ESM-based folder.
Refer to the excellent NodeJS documentation here and here for more information.
Setting the sideEffects
field enables tree shaking
Much a like creating a pure function can bring benefits, creating a "pure module" enables certain benefits as well; bundlers can do a much better job of tree shaking your library.
The way to communicate to bundlers which of your modules are "pure" or not is by setting the sideEffects
field in package.json
- without this field, bundlers have to assume that all of your modules are impure.
sideEffects
can either be set to false
to indicate that none of your modules have side effects, or an array of strings to list which files have side effects. For example:
{
// all modules are "pure"
"sideEffects": false
}
or
{
// all modules are "pure" except "module.js"
"sideEffects": ["module.js"]
}
So, what make a module "impure?" Some examples are modifying a global variable, sending an API request, or importing CSS, without the developer doing anything to invoke that action. For example:
// a module with side effects
export const myVar = "hello";
window.example = "testing";
By importing myVar
, your module sets window.example
automatically! For example:
import { myVar } from "library";
console.log(window.example);
// logs "testing"
In some cases, like polyfills, that behavior is intentional. However, if we wanted to make this module "pure", we could move the assignment to window.example
into a function. For example:
// a "pure module"
export const myVar = "hello";
export function setExample() {
window.example = "testing";
}
This is now a "pure module." Also note the difference in how things look on the developer's side of things:
import { myVar, setExample } from "library";
console.log(window.example);
// logs "undefined"
setExample();
console.log(window.example);
// logs "testing"
Refer to this article for more details.
main
defines the CommonJS entry
main
is a fallback for bundlers or runtimes that don't yet understand package.json#exports
; if a bundler/environment does understand package exports, then main
is not used.
main
should point to a CommonJS-compatible bundle; it should probably match the same file as your package export's require
field.
module
defines the ESM entry
module
is a fallback for bundlers or runtimes that don't yet understand package.json#exports
; if a bundler/environment does understand package exports, then module
is not used.
module
should point to a ESM-compatible bundle; it should probably match the same file as your package export's module
and/or import
field.
Support CDNs like unpkg
and jsdelivr
To enable your library to "work by default" on CDNs like unpkg and jsdelivr, you can set their specific fields to point to your umd
bundle. For example:
{
"unpkg": "./dist/index.umd.js",
"jsdelivr": "./dist/index.umd.js"
}
browser
points to a bundle that works in the browser
browser
is a fallback for bundlers or runtimes that don't yet understand package.json#exports
; if a bundler/environment does understand package exports, then browser
is not used.
browser
should point to an esm
bundle that works in the browser. However, you'll only need to set this field if you are creating different bundles for browsers and servers (and/or other non-browser environments). If you're not creating multiple bundles for multiple environments, or if your bundles are "pure JavaScript" / "universal" and can be run in any JavaScript environment, then you don't need to to set the browser
field.
If you do need to set this field, here's an excellent guide on the different ways you can configure it.
Note that the browser
field shouldn't point to a umd
bundle, as that would make it so that your library isn't tree shaked by bundlers (like Webpack) that prioritize this field over the others such as module and main.
types
defines the TypeScript types
types
is a fallback for bundlers or runtimes that don't yet understand package.json#exports
; if a bundler/environment does understand package exports, then types
is not used.
types
should point to your TypeScript entry file, such as index.d.ts
; it should probably match the same file as your package export's types
field.
If you rely on another framework or library, set it as a peer dependency
You should externalize any frameworks you rely on. However, in doing so, your library will only work if the developer installs the framework you need on their own. One way to help them know that they need to install the framework is by setting peerDependencies
- for example, if you were building a React library, it would potentially look like this:
{
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
Refer to this article for more details.
You should also document your reliance on these dependencies; for example, npm v3-v6
does not install peer dependencies, while npm v7+
will automatically install peer dependencies.
Protect yourself and other contributors
An open source license protects contributors and users. Businesses and savvy developers won’t touch a project without this protection.
That quote comes from Choose a License, which is also a great resource for deciding which license is right for your project.
Once you have decided on a license, the npm Docs for the license describe the format that the license field takes. An example:
{
"license": "MIT"
}
Additionally, you can create a LICENSE.txt
file in the root of your project and copy the license text there.
A big thank you to the people who took the time out of their busy schedules to review and suggest improvements to the first draft of this document (ordered by last name):
- Joel Denning @joeldenning
- Fran Dios @frandiox
- Kent C. Dodds @kentcdodds
- Carlos Filoteo @filoxo
- Jason Miller @developit
- Konnor Rogers @paramagicdev
- Matt Seccafien @cartogram
- Nate Silva @natessilva
- Cong-Cong Pan @SyMind