Bundling a TypeScript library for Node with Rollup.JS

Ted Spence
CodeX
Published in
6 min readFeb 4, 2022

Packaging a NodeJS library with DTS type information and sourcemaps

When we began building the Lockstep SDK for TypeScript in 2021, my goal was to package a single library that worked for both TypeScript and JavaScript projects. I found tons of documentation online, but much of the information was out of date, led me to deprecated packages, or just plain didn’t work.

To help the situation, I’ll write down exactly how I could build a working TypeScript and JavaScript package for my SDK for NodeJS. I won’t document all the false starts I tried, although you could look in my github history and see all my past attempts if you like. I hope this will help you make your own modern and reliable JS/TS package.

A rollup provides convenient access to the features you need (Steel Door Depot)

An Import / Export Business

I organized my TypeScript SDK project into folders so that each file would be small enough to browse easily. To package this project, I wanted a single file that exported all of the available functions, types, and classes.

I created a root file called index.ts filled with import/export statements. For each file in my project that had an exported class, type, or function from another file, I imported the file and re-exported it:

// Contents of the file /src/index.ts
export { ActionResultModel } from "./models/ActionResultModel";
export { ErrorResult } from "./models/ErrorResult";
export { FetchResult } from "./models/FetchResult";
export { LockstepApi } from "./LockstepApi";
export { LockstepResponse } from "./models/LockstepResponse";

Next, I visited all my files and replaced their cross-references with ones that used the index.ts file. I could use relative paths for these references:

// Contents of the file /src/clients/ReportsClient.ts
import { LockstepApi } from "..";
import { LockstepResponse } from "..";
import { CashflowReportModel } from "..";

I was now confident that all of my references pointed to the same declaration. This single export file appeared to be the key: without it, rollup produces exports for things it thinks are relevant, rather than everything.

Adding RollupJS to my project

The next step was to add RollupJS to my project, along with the extensions rollup-plugin-dts and rollup/plugin-typescript. My goal was to produce not only a fully compiled JavaScript version of the project, but also a TypeScript DTS file and a Sourcemap so that others could build on this project.

Remember to install these packages in developer-only mode — they aren’t runtime dependencies, just used at build time:

> npm install -D rollup
> npm install -D rollup-plugin-dts
> npm install -D @rollup/plugin-typescript

We chose not to use webpack, minify, or uglify prior to publishing the package. We wanted to make sure that the package we shipped included docblocks and user-readable comments. My expectation is that most customers will choose to use their own minification engine on their fully compiled project, so we don’t need to do that prior to packaging.

Setting up the configuration files

There are three relevant configuration files that are involved in bundling your package with rollup:

  • The TypeScript configuration file, which controls where build output goes;
  • The Rollup configuration file, which controls how the bundled files are generated; and
  • The package.json file, which provides the script tasks and chooses what files are bundled into the completed package.

Here’s what these files looked like. First, the TypeScript configuration file. The key choices here are moduleResolution and outDir . For Module Resolution, we chose to use the “node” strategy. We also desginated that our build files would go to a “build/compiled” folder.

// Contents of the file /tsconfig.json
{
"compilerOptions": {
"moduleResolution": "node",
"removeComments": false,
"sourceMap": true,
"target": "es2018",
"outDir": "build/compiled",
"declaration": true,
"strict": true,
"module": "esnext",
"noEmitOnError": true,
"lib": ["es2017"],
"esModuleInterop": false,
},
"include": ["src"]
}

Next we created the Rollup configuration file. For rollup, we wanted to produce two key outputs: the main JavaScript file, and the .d.ts file.

The first task was to build a TypeScript compile-and-consolidate task. It would take, as input, our compiled index.js file. This is the compiled version of our import-and-export file from above.

  • We used sourcemap: true to ensure that debugging output was available.
  • I had three dependencies: axios, which was a public NPMJS package; and os and url which are part of the NodeJS built-in runtime. I added all three of these to the external dependencies list. Without these, Rollup produces scary-sounding warnings.
  • I specified that this should run using the typescript() plugin that I imported from the rollup typescript project.

Next, I wanted to export .d.ts type information for TypeScript users. I imported the rollup-plugin-dts project and created a new output for it.

The important step here is to make sure that both Rollup tasks use our import/export file as their source data. You’ll notice that the “input” values for both tasks point to the build/compiled folder.

Here’s the completed rollup.config.js file from our project:

// Contents of the file /rollup.config.js
import typescript from '@rollup/plugin-typescript';
import dts from "rollup-plugin-dts";
const config = [
{
input: 'build/compiled/index.js',
output: {
file: 'lockstep-api.js',
format: 'cjs',
sourcemap: true,
},
external: ['axios', 'os', 'url'],
plugins: [typescript()]
}, {
input: 'build/compiled/index.d.ts',
output: {
file: 'lockstep-api.d.ts',
format: 'es'
},
plugins: [dts()]
}
];
export default config;

Now I need to modify my build scripts. I want to be able to type npm run build in a terminal and have everything be produced and ready to go. This is done via the package.json file, so let’s tinker with that.

  • First, I need to specify that the final packaged contents will be only three files: the consolidated javascript file and the sourcemap, produced by step one of the rollup task above; and the .d.ts file, produced by step two.
  • I created a script called “build”, and told it to execute three steps: clear, tsc, and make-bundle.
  • To clear the build folder, I executed rimraf to remove existing files.
  • To compile, I run tsc with no parameters.
  • To execute the bundle task, I run rollup --config.
  • Finally, I need to specify where the main file of this project is and where to find the .d.ts file using the “main” and “types” values.

Here’s the appropriate segment of the package.json file:

"files": [
"lockstep-api.d.ts",
"lockstep-api.js",
"lockstep-api.js.map"
],
"scripts": {
"_clear": "rimraf build/compiled/*",
"_tsc": "tsc",
"_make-bundle": "rollup --config",
"build": "run-s _clear _tsc _make-bundle",
},
"main": "lockstep-api.js",
"types": "lockstep-api.d.ts",

Checking to see if it worked

Let’s execute npm run build and check to make sure it’s working correctly!

Testing locally, I can see three files produced in the output:

  • lockstep-api.d.ts
  • lockstep-api.js
  • lockstep-api.js.map

I added these three files to my gitignore. They are simply build artifacts; there’s no need to check them into version control.

The most critical task is to make sure that all the classes, types, and functions you exported are referenced in these files. For the .d.ts file, you should see a line at the very end of the file that looks like this:

export { ... a million things! ... };

You should see every class, every type, and every function you export in this list. If you don’t, go back and check your import/export file to make sure it includes everything.

Next we should check the .js file. This file has plain JavaScript code with all type references removed. At the bottom of the file, you should see a list of exports that looks like this:

exports.StatusClient = StatusClient;
exports.SyncClient = SyncClient;
exports.UserAccountsClient = UserAccountsClient;
exports.UserRolesClient = UserRolesClient;
... and so on ...

Check to make sure that all your classes and functions are included here. Types won’t show up because JavaScript doesn’t understand them. Again, if anything is missing, check your import/export file to make sure it references it correctly.

Testing your NPM package

The final step is to produce a beta package to make sure that customers will be able to see everything correctly. We edited the package.json file to change the version number to “-beta1”, then npm run build andnpm publish. This created a pre-release version of our software that we could test. We could then import this beta version into a new test project and make sure it works as expected.

After we were happy with this project, we edited our GitHub Actions to make sure that it ran npm run build prior to publishing our package. The end result has been solid, and we’re off to the races!

Ted Spence teaches at Bellevue College and leads engineering at Lockstep. If you’re interested in software engineering and business analysis, I’d love to hear from you on LinkedIn.

--

--

Ted Spence
CodeX
Writer for

Software development management, focusing on analytics and effective programming techniques.