Building an npm package in 2023
Tags: opensource, npm, react, javascript
Published: 2023-06-14 09:14:51

Building an npm package requires a lot of decisions that need to be considered. From choosing a default bundler to selecting an appropriate transpiler to understanding the use case of the tool or package, or I dare say, a library &mdash; you're creating. 

A whole lot of thinking goes into this process than the actual process of writing the code. Yes, sometimes, this whole **"deep thinking"** charade may not even be necessary. But, it is important that you have it at the back of your mind.

## Webpack, the oldie

Years back, one approach towards building these packages was completely dependent on the use of [webpack](https://webpack.js.org/) as the go-to bundler, which also comes with a lot of plugins that can be used to bundle your JavaScript code.

But, Webpack has a lot of complexities attached to it. When I tried using it a few times, I'd always get frustrated at the number of things that flew around in the config file, [I even wrote an article about my travails](https://seven.hashnode.dev/the-travails-of-using-webpack).

> **Note:** My aim here isn't to rant about webpack, but to provide a more subtle approach towards building these packages, and publishing them.

A lot of folks may come for my opinions here, and bring up that famous quote **"Webpack is for building apps, the others are for building libraries"**. 

But, I think what a lot of folks are missing out here, is the good DX &mdash; Developer Experience &mdash; that the "other" bundling tools have when compared to webpack. 

Yes, webpack gives you a lot of functionalities. Okay, good. But at the detriment of my sanity?

As I mentioned before, the purpose of this article somehow revolves around the mistakes I made and how I bypassed them. 

For some days now, I've been working on a [React Tab component](https://github.com/kaf-lamed-beyt/react-tab) that preserves the state of each Tab (or nav items) when it is clicked upon and when you navigate away from and back to where this component is mounted.

Working on it affirmed a question that I've always pondered on, for a very long time. "Can we keep component state in the browser URL?" 

You should try using the package and let me know what you think.

I wrote a short piece on why [you might not need a state-management library](https://meje.dev/blog/keep-state-in-the-url), as it establishes the proof-of-concept for this component. Take a look in your spare time.

Now, onto the matter at hand, npm packages. 

From the things I've done in the past, I think I can conclude that you should use the typescript compiler &mdash; `tsc` &mdash; as your bundler when you decide to build an npm package that isn't in any way similar to a component library or package that depends on some sort of styling with CSS.

An example is this [helper function](https://github.com/kaf-lamed-beyt/extract-md-headings) &mdash; that extracts the heading texts from any markdown file &mdash; I worked on a while back. You can take a look at the configuration of the compiler [here](https://github.com/kaf-lamed-beyt/extract-md-headings/blob/master/tsconfig.json)

Let me walk you through the essence of this config. It is the same as the one from the repo. 

```json
{
 "include": ["src"],
 "exclude": ["node_modules", "dist"],
 "compilerOptions": {
 "lib": ["ESNext"],
 "module": "ESNext",
 "sourceMap": true,
 "importHelpers": true,
 "declaration": true,
 "rootDir": "./src",
 "outDir": "./dist/esm",
 "strict": true,
 "noImplicitReturns": true,
 "noUnusedLocals": true,
 "noUnusedParameters": true,
 "moduleResolution": "node",
 "jsx": "react",
 "esModuleInterop": true,
 "skipLibCheck": true,
 "forceConsistentCasingInFileNames": true
 }
}
```

`include` specifies the files or directories that should be included when compiling the TypeScript code. In this case, for that helper function, it includes the `src` directory. Yours could be different.

`exclude` specifies the files or directories that should be excluded from the compilation process. Here, it excludes the `node_modules` and `dist` directories.

In the `compilerOptions` section, there's the `lib` property that ensures the specific libraries that should be included in the compilation process. 

Here, it includes the "ESNext" library, which provides the latest ECMAScript features.

The `module` property determines the module system to use. In this case, it's set to "ESNext", which enables the use of modern JavaScript modules.

`sourceMap` enables the generation of source maps, which are useful for debugging TypeScript code in the browser or in development tools.

The `importHelpers` enables the import of TypeScript helper functions to assist with certain features like decorators and async/await.

`declaration` generates corresponding `.d.ts` declaration files alongside the compiled JavaScript files, allowing for type checking and code completion in other TypeScript projects that consume this code.

`rootDir` specifies the root directory for TypeScript source files. Here, it's set to the `src` directory. Again, yours can be completely different. 

In the recent package, I worked on. I set mine to `packages` because the library will have many use cases for different components, supposedly.

`outDir` determines the output directory for compiled JavaScript files. In this case, it's set to the `dist/esm` directory.

`strict` enables strict type checking and stricter compiler options.

`noImplicitReturns` reports an error when a function has a missing return statement.

`noUnusedLocals` This reports an error when a local variable is declared but not used.

`noUnusedParameters` This reports an error when a function parameter is declared but not used.

`moduleResolution` specifies how TypeScript resolves module imports. Here, it's set to "node", which uses Node.js module resolution.

This part tends to be very frustrating at times. 

`jsx` determines the syntax used for JSX. In this case, it's set to "react" to support React JSX syntax.

`esModuleInterop` enables interoperability between CommonJS and ES modules, allowing for easier importing of CommonJS modules in TypeScript.

`skipLibCheck` skips type checking of declaration files (*.d.ts) from dependencies, which can improve compilation speed.

`forceConsistentCasingInFileNames` enforces consistent casing of filenames, which can help prevent issues when working on different operating systems.

You do not have to use this config strictly if you don't want to. You are free to set up whichever one works for you.

## Enter, Rollup.js

Rollup is a bundler for JavaScript applications and it is by far the most recommended tool to use if you want to build a component library or any npm package, because of its intuitive and straightforward process of onboarding devs when compared to webpack.

The good thing here is that you can use Typescript and Rollup together when you want to build an npm package. especially one that depends on styling.

All you'd need to do is find the right plugins. When I set out to build the [react-tab](https://github.com/kaf-lamed-beyt/react-tab) package, I started with Typescript's compiler, `tsc` as my bundler. 

But, it brought a lot of limitations, because, it couldn't extract and handle CSS outputs correctly. Since Rollup has an ecosystem of plugins that I could use, doing research and finding the appropriate ones were not too strenuous.

Remember how I also talked about the "thinking" that goes into creating an ideal package? 

If the npm package you set out to build is in one way or another other dependent on using CSS, you may want to consider choosing an approach that you'll take. 

Some people like going with CSS modules, some with scss or Sass, and some with plain 'Ol CSS. I used styled-components for `react-tab` 

> "Why would you even do that? There are so many limitations with styled-components"

I'll get to that later. My point here revolves around those little edge cases. For example now, if you decided to use CSS modules for a package whose major end user(s) are likely to use it in a Next.js project, you'll have some issues to deal with.

Going with styled-components was a wise decision for me, since the Tab components are meant to be intuitive and completely customizable for developers. I needed to expose some certain style props.

And it is no new thing that you can extend the props of a component when using styled-components. 

```ts
// component.styled.ts
import styled from "styled-components"

type themeProps = {
 theme: string
}

export const Container = styled.div<themeProps>`
 background: ${({theme}) => theme ? theme : "brown"}
`
```

`Container` then becomes something similar to the snippet below. This is just the tip of the iceberg of what can be achieved with this CSS component-library

```tsx
<Container theme="purple">
 // children
</Container>
```

But, the limitations still persist. Because, and as I mentioned previously, typescript's compiler cannot properly handle the outputs of styles that'll be bundled together onto the component, Rollup swoops in to save the day.

## The source of truth &mdash; rollup.config.js

It is a common practice for most bundlers to have a config file that sorta controls how the application is chunked, crunched, compiled, and any other action that goes on behind the scenes.

An example of a config file can be seen below. This one exports the entry point &mdash; `input` &mdash; of the application and the output directory.

```ts
export default {
 input: "packages/index.ts",
 output: {
   file: "dist/index.js",
   format: "cjs"
 }
}
```

Since, react-tab uses an external dependency, `next/router` to be precise, an "unresolved dependencies" warning will be logged when I try to build the package with the `rollup -c` command.

Rollup treats dependencies differently based on their import paths. When it encounters an import that starts with `.` or `/`, it assumes it's a local file and resolves it accordingly. 

However, when the import starts with a module name, such as `next/router`, it treats it as an external dependency that should be resolved by whoever uses the package.

To bypass this warning, I had to specify that `next/router` is an external dependency in my rollup config. 

This same approach applies to you, once you've identified the external packages that your application/library depends on. 

```ts
export default {
 input: "packages/index.ts",
 output: {
   file: "dist/index.js",
   format: "cjs",
 },
 external: ["react", "react-dom", "next/router"],
}
```

Recall when I mentioned something related to the limitations of styled-components? Here's the issue with it.

When using styled-components in a shared component or library, you should keep three things in mind to ensure that the styles are applied correctly when the package is consumed by other projects.

<u>listing it as a peer dependency </u>: I had to make sure that `styled-components` is listed as a peer dependency in my package.json file. 

This informs consumers that they need to have `styled-components` installed in their own project to properly utilize your component.

If you do not have it that way in your `package.json` file, you can do so by modifying the `peerDependencies` property with the `generatePackageJSON` plugin like so: 

```ts
generatePackageJSON({
 outputFolder: "dist",
 baseContents: (pkg) => ({
   name: pkg.name,
   main: "/dist/index.js",
   peerDependencies: {
     react: "^18.2.0",
     "styled-components": "^6.0.0-rc.3",
   },
  }),
})
```
<u>CSS extraction</u>: By default, styled-components applies styles by injecting them into the DOM at runtime. 

However, when building a shared component library or package, it's best to extract the styles during the build process, so they can be bundled and used by the consuming project. 

To achieve this, I had to use a tool like `babel-plugin-styled-components` along with the Babel plugin in my rollup config

```ts
babel({
 extensions: [".ts", ".tsx"],
 exclude: "node_modules/**",
 presets: ["@babel/preset-react", "@babel/preset-typescript"],
 plugins: ["styled-components"],
}),
```
<u>CSS output</u>: there's a rollup plugin &mdash; `rollup-plugin-styles` &mdash; that should be included in your config file, so long as you're writing anything that is transpiled down to CSS.

The plugin ensures that the styles you'll be writing are properly extracted. 

```ts 
export default {
 input: "packages/index.ts",
 output: {
   file: "dist/index.js",
   format: "cjs",
 },
 external: ["react", "react-dom", "next/router"],
 styles(),
}
```

In the `peerDependencies` property, the exact version of the dependencies was hard coded. But, there is a way to ensure that the peer dependencies are automatically updated to the latest compatible versions without hardcoding them manually.

You can use the `latest` tag in the version range of the peer dependencies.

```ts
peerDependencies: {
 react: "latest",
 "styled-components": "latest",
},
```

By using the `latest` tag, it will automatically fetch the latest version of the peer dependency that satisfies the specified version range. 

This way, you don't need to manually update the versions every time there's a newer compatible version.

When users install your package or when they run `npm install` or `yarn install`, the package manager will fetch the latest compatible versions of the peer dependencies based on the version range specified in your `package.json`.

But &mdash; there's always a but &mdash; keep in mind that using the `latest` tag can also introduce potential breaking changes if a new major version of the peer dependency is released and it includes breaking changes. 

To mitigate this, it's a good practice to thoroughly test your package with the latest versions of the peer dependencies before releasing a new version.

Here's what the complete rollup config looks like: 

```ts
import babel from "rollup-plugin-babel";
import commonjs from "rollup-plugin-commonjs";
import generatePackageJSON from "rollup-plugin-generate-package-json";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import styles from "rollup-plugin-styles";
import { terser } from "rollup-plugin-terser";

const dev = process.env.NODE_ENV !== "production";

export default {
  input: "packages/index.ts",
  output: {
    file: "dist/index.js",
    format: "cjs",
  },
  external: ["react", "react-dom"],
  plugins: [
    nodeResolve({
      extensions: [".ts", ".tsx"],
    }),
    babel({
      extensions: [".ts", ".tsx"],
      exclude: "node_modules/**",
      presets: ["@babel/preset-react", "@babel/preset-typescript"],
    }),
    generatePackageJSON({
      outputFolder: "dist",
      baseContents: (pkg) => ({
        name: pkg.name,
        main: "/dist/index.js",
        peerDependencies: {
          react: "^18.2.0",
          "styled-components": "^6.0.0-rc.3",
        },
      }),
    }),
    terser({
      ecma: 2015,
      mangle: { toplevel: true },
      compress: {
        toplevel: true,
        drop_console: !dev,
        drop_debugger: !dev,
      },
      output: { quote_style: 1 },
    }),
    commonjs(),
    styles(),
  ],
};
```

And here's what the important part of the `package.json` file entails. It specifies the build configuration and script for the react-tab package. 

You can adopt it to suit your use case.

```json
{
 "main": "./dist/cjs/index.js",
 "module": "./dist/esm/index.js",
 "types": "./dist/esm/index.d.ts",
 "scripts": {
   "build:esm": "tsc",
   "build": "yarn build:esm && yarn build:cjs",
   "build:cjs": "rollup -c"
 },
}
```

Here's a breakdown of the snippet;

`"main": "./dist/cjs/index.js"` specifies the entry point for CommonJS (CJS) modules. When someone imports your package using require() or import in a CommonJS environment, this is the file that will be loaded.

`"module": "./dist/esm/index.js"` specifies the entry point for ECMAScript Modules (ESM). When someone imports your package using import in an ESM-supported environment, this is the file that will be loaded.

`"types": "./dist/esm/index.d.ts"` specifies the location of the TypeScript declaration file (`.d.ts`). 

This file provides type information for your package, allowing TypeScript users to get type-checking and autocompletion when using your package.

`"build:esm": "tsc"` runs the TypeScript compiler &mdash; `tsc` &mdash; to build the ESM version of your package. It compiles the TypeScript code into JavaScript and outputs the files in the `./dist/esm` directory.

`"build": "yarn build:esm && yarn build:cjs"` runs the `build:esm` script first, and then executes the `build:cjs` script. 

It ensures that both the ESM and CJS versions of your package are built.

`"build:cjs": "rollup -c"` runs the Rollup bundler &mdash; `rollup` &mdash; with the configuration file (`-c`). 

Rollup reads the configuration file to bundle your code, applying any specified transformations or optimizations. The output is generated in the `./dist/cjs` directory.

## testing locally before publishing.

This part is somewhat tricky as it may somehow affect the versioning process of your tool.

Instead of running `npm publish` anytime you make a significant change, you should instead link the package with `npm link` 

If you're on any Linux distro OS, you'll have to append `sudo` to `npm link` to grant permissions. This ensures that a symlink is created for the package. 

Then you can proceed to link the package in another project where you want to use it like so.

```bash
npm link package-name
```

## wrapping up.

Inasmuch as the intent of this article isn't entirely to walk you through the process of building a component library by following some steps, below are some resources that may help you accomplish your aim.

[How to Publish to NPM the Right Way](https://dev.to/apestein/how-to-publish-to-npm-the-right-way-2pd7)

[How to build a component library with React and TypeScript](https://blog.logrocket.com/how-to-build-component-library-react-typescript/)

[Component library setup with React, TypeScript and Rollup](https://dev.to/siddharthvenkatesh/component-library-setup-with-react-typescript-and-rollup-onj) 

[Easily build and publish a React / Typescript component library package to Npm using Storybook and Rollup.](https://medium.com/soulweb-academy/easily-build-and-publish-a-react-typescript-component-library-package-to-npm-using-storybook-and-4836d4f700b6)