Improving on TypeScript package build processes

Ted Spence
tedspence.com
Published in
6 min readOct 15, 2023

--

Simplifying the compile, package, and publish process for a TypeScript SDK

Many years ago I wrote Bundling a TypeScript library with RollupJS. At the time I was building the Lockstep (now Sage) software development kit for TypeScript/JavaScript. The results were great, but I had a nagging feeling in the back of my mind — was there a simpler way to do it?

Now that I’m launching the ProjectManager SDK for TypeScript/JavaScript, it’s time to see what I can do to clean up the process. My goal is to compile the code quickly, remove every unnecessary step, and to preserve type information so that developers can use autocomplete and hover-docs from within Visual Studio Code.

Can you construct a TypeScript bundled package for NPMJS more easily than with RollupJS? (NZ Herald)

Using a console test project to validate your bundle

How can we test a Node Package Manager library locally? I wanted to be able to iterate on this process until I was perfectly happy with the results, and I didn’t want to wait for files to upload and download from some remote server. My plan was as follows:

  • Define my project’s folder structure and run npm pack locally.
  • After npm pack finishes, there will be a myproject.tar.gz file in my local folder.
  • I can then write a sample application that imports my .tar.gz file locally — without going through npmjs — so I can test the package to see if it works.

So I started a small typescript test project that would just print out some results to the console. For a minimal test project, you can boil it down to three files:

root/
package.json
tsconfig.json
src/
index.ts

The critical part of the test program is the package.json file, which uses a file dependency. I will tell it to retrieve my .tar.gz file off my other TypeScript project’s output folder. Here’s what that looks like:

Anytime I want to validate my project, I can import a new version of my .tar.gz file and re-run my test project to see if it works correctly. If you’d like to look at the rest of my test project, see its github page.

So the project works — but does it have types?

Using this methodology I could remove most of my RollupJS code and strip the project down to the bare minimum. But unfortunately doing that caused the project to lose all type information.

Of course, JavaScript doesn’t require type information, and TypeScript can survive without it. But the benefits of type information are significant when you’re trying to help someone use your code!

Since I expect to help answer questions to developers using my SDK, I want them to have a good experience working with my tools in Visual Studio Code. When they hover over a method call, what do they see?

Hovering over a method call without type information does not provide much help

Yikes! The word “any” isn’t super useful. I discovered that the critical aspect of my build process was to have the following values in sync:

  • The package.json files element specifies what files to put into the eventual .tar.gz package. If your types files aren’t specified here, you’re out of luck!
  • Even if your types files are included in the files element, you need a types element in package.json that tells TypeScript where to find the types for your bundle.
  • It’s also a good practice to ensure that your files element includes a license file and a readme.md file for usability. Don’t forget to ensure the files element includes a package.json file as well! Without that, you’ll get weird errors like “no package found” or “no module found.”

Combining all of these tips together, I made sure my package.json file included itself, along with a license, readme, and my distribution folder. I then specified dist/index.js and dist/index.d.ts for my main and types respectively. Here are the key lines from package.json:

It’s important to note that your main and types pointers aren’t relative to your source folders! They are consumed by someone using your .tar.gz file, so they represent paths within the distribution package.

The process works as follows:

  • Your source file starts its life as src/index.ts.
  • It gets compiled to dist/index.js, and type information gets saved to dist/index.d.ts.
  • Your package will only pick up the files within the dist/**/* path, so the source files won’t be included.
  • Therefore your main and types elements should point to the distribution versions.

Once I realized this, it became clear that just running tsc (the TypeScript compiler) alone produces all the necessary file structure. I wouldn’t need RollupJS at all! Here’s what my tsconfig.json file looks like:

After this change, TypeScript puts all the files in their correct location without any additional compilation steps. When I run npm pack and then import this file into my test program, I can see type information:

Full hover-docs and autocomplete in the finished package

Looks great! Definitely much more usable than any.

Would ESBuild help minify the output?

I decided to investigate ESBuild to see if I could improve on this solution, with mixed results. Although ESBuild is indeed a fast and elegant solution for building JavaScript, it is an opinionated product that intentionally disregards TypeScript type information.

I attempted to construct a second tier of compilation for ESBuild. First, I would have TypeScript export all code into a dist folder. Then I would have ESBuild compile and minify this into a bundle folder, and I would attempt to do the same with the TypeScript types information.

package.json
README.md
LICENSE
src/
index.ts
... other files ...
dist/
index.js
index.d.ts
... other files ...
bundle/
bundle.js
bundle.d.ts

The tutorial I used to attempt this approach suggested I create a build.js and settings.js file to specify my ESBuild options. This worked, and I was eventually able to export a single, bundled, minified JavaScript file.

It was then possible to use the discussions on this GitHub issue to learn how to export a single unified type file with TypeScript. Using the outFile option for tsbuild, I could create a second tsbuild.bundle.json file which would generate just a single unified types file:

Unfortunately, in the end the TypeScript bundle file and the ESBuild bundle file didn’t play well together. After a few fruitless hours, I was unable to make the unified type information load correctly; I received odd errors about module structure. I’m sure there’s a trick to making this work, and if you know more about this I’d be happy to discuss with you.

But did we need to use ESBuild? In the end, I found a different way to save space and improve my build sizes — a much siller improvement indeed.

The importance of rimraf for your development

Over the course of my experiments I had attempted dozens of different approaches and tried a lot of different tutorials. As it turned out, my compilation folders were littered with many different versions of my output files.

It was time to add rimraf to my project and get in the habit of cleaning up. I ran npm install rimraf and added a new target to my package.json file:

"clear": "rimraf ./dist && rimraf *.tgz && rimraf ./bundle"

Now I just needed to get into the habit of running npm run clear from time to time as I ran experiments. My packaged bundles shrank in size quickly as soon as all the half-baked files were cleaned out.

The end result is that a project using only TypeScript for compilation — with no ESBuild and no RollupJS — is acceptable for the purposes of this project. So I brought the project to a close and launched the ProjectManager TypeScript SDK using just straight TypeScript compilation.

In the end, the differences were minor:

  • Fully bundled and minified JavaScript and TypeScript: 520KB
  • TypeScript compilation only: 820KB

I’m still eager to make minification happen, but this project was a significant improvement over my earlier attempt to use RollupJS.

Ted Spence heads engineering at ProjectManager.com and teaches at Bellevue College. If you’re interested in software engineering and business analysis, I’d love to hear from you on Mastodon or LinkedIn.

--

--

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