How to bundle a tree-shakable typescript library with tsup and publish with npm
Tags: typescript, esbuild, tsup, treeshaking
Published: 2022-11-02 13:15:41

Header image is generated by [Night Cafe's AI](https://creator.nightcafe.studio/) btw.

Thinking of the vast frontend ecosystem, for the most part, I don’t need to implement stuff on my own. The majority of the time, I google a particular problem and most of the time I find a library or stack-overflow answer, Bingo. But there are times when I find myself lacking something unique, whether it is a company task or a hackathon project, a holistic approach, a whole new sexy vibe is needed indeed.



<img width="100%" style="width:100%" src="https://media.giphy.com/media/SgLc1OBmibrqBBSJJW/giphy.gif">



Recently I was trying to get my head around sports analytics and realized that like the majority of the data science ecosystem, all tools are written in python. While I was re-studying to sharpen my rusted python skills (no pun intended 🦀), I also tried to do some stuff with JS for fun as well, which turned into to a separate library based on D3. In this blog post, I won’t explain the library itself because that's another story (P.S watch this space). But this time I'll share a very basic setup to enable you to come up with your own with a minimum of fuss. **Github link at the end 👇** 

For a JS/TS library you would need a transpiler/bundler, a module strategy, a published version of your lib, and optionally a demonstration app.

## Bundler (esbuild & tsup)

esbuild claimed to be the fastest in the town when it comes to bundlers, and it seems so according to benchmarks as well. Similar to `Parcel`, tsup is a zero-config library(in case you don't want it) based on esbuild. Parcel is based on `Rust` based [swc](https://swc.rs/) compiler meanwhile esbuild is based on [`Go`](https://esbuild.github.io/faq/#why-is-esbuild-fast). Tsup is tailored to work seamlessly with TS. Even though it is zero config you can change the configs if need be, so we do. Here is the config I used for this article.


`tsup.config.ts`

```
import type { Options } from 'tsup';

const env = process.env.NODE_ENV;

export const tsup: Options = {
  splitting: true,
  clean: true, // clean up the dist folder
  dts: true, // generate dts files
  format: ['cjs', 'esm'], // generate cjs and esm files
  minify: env === 'production',
  bundle: env === 'production',
  skipNodeModulesBundle: true,
  entryPoints: ['src/index.ts'],
  watch: env === 'development',
  target: 'es2020',
  outDir: env === 'production' ? 'dist' : 'lib',
  entry: ['src/**/*.ts'], //include all files under src
};
```

Here we are transpiling all our TS files into JS with the help of `tsup`'s config. Afterwards we will publish it with `npm` in JS format with type declarations so that both JS and TS apps can use it. 

I need to clarify a few things though. 


### `splitting` 
This is flagged an experimental feature that only works for `esm` right now. You need to specify `outDir` as well. With this feature, we are enabling tree-shaking so that users don't have to download shared bits of code again and again when they switch between different pages for example. Look [here](https://esbuild.github.io/api/#splitting) for further info. 

### `skipNodeModulesBundle` 
Skips building dependencies for node modules, and use them as they are imported. As a library builder, you can even bundle and serve your dependencies but this is way beyond the scope of this article.

### `dts` 
Generates typescript declaration files (e.g. `index.d.ts`), useful when you consume libraries with typescript.

### `entry` 
See how we enable all files to be parsed by tsup using a simple regex pattern `['src/**/*.ts']`

Rest of the options pretty much does the stuff, as the names imply


## Module Strategy

Handling modules has been a pain in the neck for JS community for a long while. Therefore, there are some different strategies when you want to distribute your JS bundles. 4 major modules around are:

1. CommonJS
2. AMD: Async Module Definition
3. UMD: Universal Module Definition
4. ES modules

I won't get into the details of AMD and UMD in this post. But I'd like to talk about CJS and ESM which are the most common ones. There is a distinct difference between CommonJS and ES modules which is `Tree shaking`. CommonJS modules are the original way to package JavaScript code for Node.js. Up-to-date versions of node.js support both. The major difference here is in a CommonJS module all of your pretty js files are bundled into a single file. You'll remember the syntax if you've worked on both ends.


ESM (ES modules)
```
import {john} from "doe";

export jane;
```

CJS (CommonJS)
 
```
const john = require("doe")

module.exports = jane;
 
```

## Tree-shaking

Tree-shaking is eliminating dead code and unused exports from your bundle. With `CJS` even if you only going to use a simple `Card` component let's say, if it is not tree-shakable library, you need to download the whole library and add it to your vendor chunk eventually. For example one of the most used utility libraries around is lodash and there has been a lot of problem because of its huge bundle size in the [past](https://stackoverflow.com/questions/35250500/correct-way-to-import-lodash/50656694#50656694) If you use es modules import/exports which are tree-shakable by its nature, bundlers can detect unused exports. Dead codes are eliminated that way. 


You can output different module formats with `tsup` but `cjs` file won't be tree-shakable. Don't forget you are just making the library tree-shakable. It is the app that uses the library actually shakes and eliminates the dead code. But in this article, I want to output two different formats for two different platforms (web & node) the former one would be tree-shakeable. The main motivation behind it was creating a UI library for me but you never know which platform your library is going to end with if it is not a UI library particularly. So why not provide it in both formats just for sake of doing it 🃏 Below line will handle that for us.

```
format: ['cjs', 'esm']
```

We will also change the package.json a bit. 

```js
  "main": "lib/index.cjs",
  "module": "lib/index.js",
  "types": "lib/index.d.ts",
```
The module field is used to determine the ES version of the package. Also, we want to mark our library as an `es` module. To do so, we'll add the below line to package.json.  ``` "type": "module", ``` which affects tsup compilation as well in terms of file extensions.


 ```js
├── index.js          # esm
└── index.cjs         # cjs
```

index.js is an es module now. The reason behind all this is for a UI library we want its components/classes to be importable separately so that the user can import a Component like the one below in an async manner.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nuqx3d2ki6ga483p3x8c.png)


`script type="module"` lets the browser treat this piece of import as a module



### Output Directories 

If you ever investigated libraries like Ant Design you'll recognize that there will be a separate lib folder inside of the package, from which you can import particular components from. We would have both `lib` and `dist` libraries. `dist` would stand for bundle version of your files and possibly could be used when you want to distribute your library over a CDN for example.


#### Used and Unused Exports 🤔

Now try to look at this implementation the other way around.

<img width="100%" style="width:100%" src="https://media.giphy.com/media/hbN2kKXzFqQuyEsFb6/giphy.gif">

 Let's assume an app has our library as a dependency and this app use webpack or a similar bundler that is ready to tree-shake our library. It can detect unused exported modules thanks to a feature called [usedExports](https://webpack.js.org/configuration/optimization/#optimizationusedexports) 


But if you bundle all your js into a single whole bundle will be imported anyway. There is another concept called `sideEffect` pops out at this stage. According to webpack 
> A "side effect" is defined as code that performs a special behavior when imported, other than exposing one or more exports. An example of this are polyfills, which affect the global scope and usually do not provide an export.

Webpack can identify if it should run this optimization by checking a custom field called `sideEffects` inside `package.json`. Preserving module structure and serving library with tiny modules enable bundlers to eliminate the dead code. 
If you have a particular module that you want to be bundled whether it is used or not, you should add it to the sideEffect list. This includes scss or other files as well. A possible sideEffects configuration that you can add to your package.json is  

```js
  "sideEffects": [
    "dist/*",
    "lib/**/style/*",
    "*.scss"
  ],
```

In the above example, you're telling webpack not to clean modules considered to be dead code and this makes sense because under a dist folder possibly you have a single minified bundle including all needed utility functions, etc. It can be even a CJS module in which tree-shaking is not possible. The same is true for your style files, you possibly need all of them, it might be misleading to tell webpack to skip the whole module/subtree of scss. 

### How to test if `sideEffects`actually works
Basically looking at the bundle. Let's assume a very basic library 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c5keb5kl49ulilt6dzi5.png)



`index.ts`

```js
import { sum } from './Sum';

const sampleJson = [
  {
    id: 'f5e7457f-467d-4e37-9652-f2fb1b51c712',
    first_name: 'John',
    last_name: 'Doe',
  },
];

export { sampleJson, sum };
``` 
and inside `Sum` we are just exporting a basic sum method
`Sum/index.ts`

```js
const sum = (a: number, b: number) => a + b;

export { sum };
```

After we transpile with `tsup` all the files will be added to the bundle. Note that we are preserving folder structure under lib.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p4k52qo06b3ff84l2rqq.png)



Let's jump into to the app and locally link the library for testing. I'll use this webpack starter https://github.com/wbkd/webpack-starter

### Testing with npm link

The easiest way to test your library is to register it locally with `npm link` or `yarn link`.


```
yarn link v1.22.19
success Registered "tsup-library-template".
info You can now run `npm link "tsup-library-template"` in the projects where you want to use this package and it will be used instead.
```

Then go to the app you want to use the library then `yarn link <your-library>`
```
yarn link v1.22.19
success Using linked package for "tsup-library-template".
✨  Done in 0.02s.
```

This is the quickest way to set you up. If you want to manage your lib and examples in the same monorepo have a look at the [yarn workspaces](https://yarnpkg.com/features/workspaces) 

### Let's use the library
Inside the app we will use library like this 

`someApp.js`
```
import {sum} from 'tsup-library-template';

console.log(sum);
``` 
even though library has two expected members we only used one method which is `sum` and that reflects on webpack bundler.


`someAppsBundle.js`
```
 !function(){"use strict";console.log(((o,c)=>o+c))}();
//# sourceMappingURL=app.5712c325.js.map
``` 

Now import the `sampleJson` and use it as well
```
import {sum, sampleJson} from 'tsup-library-template';

console.log(sum, sampleJson);
```

Look at the bundle generated by the webpack
 it is larger because it know includes two imported members `sum` and `sampleJson`.
```
!function(){"use strict";console.log(((e,f)=>e+f),[{id:"f5e7457f-467d-4e37-9652-f2fb1b51c712",first_name:"John",last_name:"Doe"}])}();
//# sourceMappingURL=app.b0051b82.js.map
```

We can even solely import a module inside using the below syntax 
```
import {sum} from 'tsup-library-template/lib/Sum';

console.log(sum);
```
output will be same with the first example.
```
!function(){"use strict";console.log(((o,c)=>o+c))}();
//# sourceMappingURL=app.5712c325.js.map
```
Some of you might think at this stage "isn't it just regular module import/export". Yes, you are absolutely right, the only difference here is we let the bundler to do that inside of an npm package. We're done with the building, time to send it to the npm 🚀

### Versioning and Releasing
```
npm version major  // increase major version

npm version minor  // increase minor version

npm version patch // increase patch version
```
All of the above comments set the new version, updates the package json and commits to git

```
npm login
``` 
to login your npm account, then run 

```
npm publish
``` 

on terminal. Check you npm account to see package is ready with the version you specify in`package.json`

## Summary 

> tsup is a bundler based on arguably the fastest bundler: esbuild. You don't have to modify configs, but you can compose the way you want for different outputs. ESM is the preferred module strategy for web and UI libraries. The main difference between a CJS and ES module is that the latter is tree-shakable makes it more efficient and requires less time to download, parse and execute for browsers. We provide the library with type declarations in JS format. Add `sideEffects:false` to your package.json for further optimization when other bundlers use the library. Quickly test with `npm link`


### Further Reading 
I've found all of these resources very useful. Shout out to all of them 🙌
- [How to make tree-shakable libraries](https://blog.theodo.com/2021/04/library-tree-shaking/#:~:text=ESM%20is%20a%20requirement%20for,them%20both%20through%20the%20package.)
- [JS module system](https://www.freecodecamp.org/news/anatomy-of-js-module-systems-and-building-libraries-fadcd8dbd0e/)
- [React based tree shakeable library with Rollup](https://dev.to/lukasbombach/how-to-write-a-tree-shakable-component-library-4ied)

### Github Link

In case you want to use this approach as a template 
👉 https://github.com/orabazu/tsup-library-template

#### Webpack Error
If you see this error when you use the library 

```js
ModuleNotFoundError: Module not found: Error: Can't resolve './Sum' in '/Users/hunor/workspace-zhunor/js/tsup-template.js/lib'
Did you mean 'index.js'?
BREAKING CHANGE: The request './Sum' failed to resolve only because it was resolved as fully specified
```

By default webpack expects you to provide the file extension when importing a module in .mjs files or any other .js files when their nearest parent package.json file contains a "type" field with a value of "module", otherwise webpack would fail the compiling with a Module not found error. 

In order to skip this you might need to set `fullySpecified: false` in webpack configuration similar to below.

```js
{
  test: /\.m?js/,
  resolve: {
    fullySpecified: false
  }
}
```