This guide will walk you through the easiest process of building and releasing your NPM package, from start to finish, using a microbundle.
Let’s talk a bit about it microbundle
. I find it particularly effective for simple libraries because you don’t have to worry about configuration, allowing you to focus on developing your package.
Here is a short list of its features:
package.json
tsconfig.json
Basically, microbundle
is built on top of rollup.js. If you have more complex libraries to build than I will mention in this article, you might consider using a pure rollup.js
configuration.
As an example, let’s create a simple library for summing two numbers, which will export only one functionsum
.
Create a folder for the project, and run npm init
with default values to generate package.json
Create index.ts
in src
folder
// src/index.ts
export function sum(a: number, b: number) {
return a + b;
}
Install microbundle
npm i -D microbundle
Updatepackage.json
with the following values:
// package.json
...
"type": "module",
"source": "src/index.ts", // your source code
"exports": {
"types": "./dist/index.d.ts", // TypeScript declaration file
"require": "./dist/index.cjs", // CommonJS entry point
"default": "./dist/index.esm.js" // ES Module entry point
},
"main": "./dist/index.cjs", // where to generate the CommonJS bundle
"module": "./dist/index.esm.js", // where to generate the ESM bundle
"unpkg": "./dist/index.umd.js", // where to generate the UMD bundle
"types": "./dist/index.d.ts", // TypeScript declaration file for the package
"scripts": {
"build": "microbundle", // compiles "source" to "main", "module", "unpkg"
"dev": "microbundle watch" // re-build when source files change
}
...
Run the build script
npm run build
The output should contain exactly the files that we declared in package.json
And voilà, we made our first package. Let’s take a look at more complex scenarios.
If you want to bring React to your library, you can still use itmicrobundle
, but now, your build command should look like this:
microbundle --jsx React.createElement --jsxFragment React.Fragment --jsxImportSource react --globals react/jsx-runtime=jsx
Add the command to package.json
into build
script for future convenience:
// package.json
...
"scripts": {
"build": "microbundle --jsx React.createElement --jsxFragment React.Fragment --jsxImportSource react --globals react/jsx-runtime=jsx"
}
...
While building a UI library, you might need a sandbox where you can develop, visualize components, and provide demo components for your documentation.
Here comes the storybook. It’s a sandbox with its own convenient interface and bundler, where you can easily describe various states of your components. Each capture of your component state is called a "story."
This picture, taken from their documentation, shows what it looks like:
Installing Storybook is quite simple; just run the command inside your library:
npx storybook@latest init
This command will install all required dependencies for Storybook, add scripts to run, and build Storybook into package.json
, create a folder .storybook
with default configuration, and add some examples of stories to the foldersrc/stories
.
You can add styling in one of two ways: CSS file or CSS-in-JS. The CSS file allows easy customization but requires separate inclusion, whereas the CSS-in-JS simplifies styling but increases bundle size.
CSS file
Create a CSS file in the src directory, and import it into the root react component file:
// src/index.tsx
import './styles.css';
export const MySuperComponent = () => {
return (
<h1 className="title">Hi there!</h1>
)
};
So, let’s run the build command again.
npm run build
And your importedstyles.css
file will be created in the dist
folder:
Great! We have obtained a CSS file with the necessary styles. However, there is a slight inconvenience with this solution. The CSS file needs to be added separately after the package is installed.
Therefore, users of your library will need to use a CSS loader (e.g., a CSS-loader for the webpack) to handle your CSS file, and their usage will look like this:
import { MySuperComponent } from 'my-super-library';
import 'my-super-library/dist/styles.css';
export const App = () => {
return (
<MySuperComponent />
);
}
CSS-in-JS
You can use libraries like styled-components for this purpose. And it will look like this:
import styled from 'styled-components';
const Title = styled.h1`
font-size: 30px;
font-weight: bold;
`;
export const MySuperComponent = () => {
return (
<Title>Hi there!</Title>
)
};
With this solution, users won’t need to import a CSS file and add a special loader for their project. After installing the library, the component will come with its own styling. However, this will increase the bundle size and make it more difficult for users to customize elements using CSS selectors.
Choose the option that suits you best. I prefer to use the CSS file because it allows users to customize any element with CSS selectors, doesn’t affect the bundle size, and works faster.
A README.md file provides information about your library, how to install it, its basic usage, and the features it has. This is often the first file that developers read when they encounter your repository or NPM package, so it should be concise and informative.
I like to create a structure in the following order:
You can refer to examples ofREADME.md
files from my packages, such as dot-path-value and react-nested-dropdown, for inspiration.
This is an important part because if you do it wrong, users may face version conflicts or other problems, and they will have to remove your library. So, let's take a look at the main differences between dependency types.
microbundle
package installed, which users don’t need to use, keep it in devDependencies, and it won’t appear in the bundle.
“peerDependencies” are dependencies that your package uses but won’t include in your bundle. Your package will utilize the version of the dependency that the user has in their project.
Basically, we should specify a peer dependency if we are creating a library for its ecosystem. For example, if you are creating a React component, set React as a peer dependency. If developing a React component with a calendar, add React and a date calculation library (e.g., date-fns) as peerDependencies.
If the user doesn’t have the specified peer dependency in their project, the npm install
command will display a warning, but it will not automatically install the dependency.
Just a small example of how it looks:
// package.json
...
"dependencies": { // libraries which will be installed along with your library
"clsx": "^1.2.1" // just library for className combining
},
"peerDependencies": { // user should have these packages installed
"react": "^16.8.0 || ^17.0.0 || ^18.0.0" // user should have react 16.8+ version
},
"devDependencies": { // won't be in user's bundle, these libraries just for your developing needs
"@types/react": "^18.2.33",
"react": "^18.2.0", // in case if we are using storybook, we need actual react library to render our components there
"microbundle": "^0.15.1",
},
...
If you are publishing an NPM package, it means it will be publicly accessible (if you have a free account). To gather feedback from users, you can create a GitHub repository for your original code. People can create issues and communicate with you about your package there. You can also describe your releases and get some stars!
You can certainly skip this step, but it is an integral part of the developer culture and can be a valuable contribution to open-source.
Before you can publish your package, it's essential to ensure that your package.json
file is properly configured. Here are some important steps to follow:
Name and try to describe the core functionality of your library. For example:
"name": "react-color-picker"
Add GitHub repository information (if it exists):
...
"homepage": "https://github.com/{your_repository}",
"repository": {
"type": "git",
"url": "https://github.com/{your_repository}"
},
"bugs": {
"url": "https://github.com/{your_repository}/issues"
},
...
Configure the files
:
...
"files": [
"dist",
],
...
You should specify the files that will be included in node_modules
, when your library is installed. Usually, including the dist
folder is sufficient.
Add keywords
:
Keywords are an array of strings that describe your package and are used by NPM for searches and recommendations. Choose words relevant to your package that you anticipate people will use when searching. Let’s create keywords for our sum
library:
...
"keywords": ["typescript", "math", "sum", "numbers", "arithmetic", "calculator", "calculation"]
...
It’s important to specify your technologies because users often search for terms like “typescript library for math” or “react calendar picker.”
Create an NPM account if you haven’t already, and run npm login
in your terminal; follow the prompts to authenticate your account. By default, the version of your package will be 1.0.0
; you can check it in the package.json
file. I recommend changing it to 0.0.1
.
Run npm publish
, and you're done! To update the version in the future, use the command npm version patch
to increment the version, and then publish the updated package with npm publish
.
As you can see, creating your own library is very easy! Essentially, this is all you need for creating and maintaining the package. If you struggle with limiting your library with microbundle
, I recommend using rollup.js with a more specific configuration.
Creating NPM packages and contributing to open-source is a rewarding and valuable experience for developers of all skill levels. It allows you to share your code with the community, gain a lot of experience, and build a portfolio of your work.