I was planning on releasing a longer post about Nx, but I got lost in my line of reasoning. So here goes a shorter note, just to share some thoughts.

TypeScript project references

Before I get to the Nx part, I wanted to talk about a useful TypeScript’s feature: project references.

It allows you to divide your project into separate sub-projects that are understood by TypeScript, as long as you reference the root directories of used projects (or their tsconfig.json files) inside the “using” project’s tsconfig.json#references. What’s neat about this is that whenever you want to build the app that relies on libA and libB, and you run tsc --build, the TypeScript compiler will first run type-checking and transpilation for those dependencies, before builing the app itself. If everything is configured properly with the .tsbuildinfo files, then the sub-build will be skipped (because cached).

Enter Nx

So what’s the deal with Nx? One of it’s core features is the fact that it manages the dependencies between the tasks, including “build” tasks. Why would it matter to me, since TS handles that out of the box?

Well, I just realized that when you change the transpiler (to, for example, swc), it turns out you have to transpile all the projects one by one yourself.

So this is where Nx is showing its colors – if the sub-projects are properly defined as dependencies in the package’s manifest (=package.json), and the Nx is set up to run “build” target (=“command”) for project’s dependencies whenever the project’s “build” target is run, then you’re all set. You run “build” for the app, and all the dependencies (libA, libB) are “built” first, regardless of the used transpiler, because Nx knows what to run in what order.

The benefit? Your CI YAMLs/bash scripts/Dockefiles do not need to contain:

yarn run $YOUR_TRANSPILER packages/libA
yarn run $YOUR_TRANSPILER packages/libB
yarn run $YOUR_TRANSPILER packages/app

which would otherwise increase the complexity of the projet and would mean that the YAML/bash/Dockerfile would need to be in sync whenever you decide to add/remove/modify internal dependency of the app.

With Nx, you just need to make sure that Nx is available in the runner context, and then run:

nx app:build

All the relationships between the packages is already defined in the source code and/or the manifests. No need to duplicate the responsiblity.

And when you are using the cache properly (the easiest scenario is the local one – practically no setup required), then the projects that were already built once and were not modified are not built again.

But that’s the benefit I understood only when I decided to migrate to swc, since tsc took way to long to build the project. If not the build times, I’d still orchestrate the builds with tsc.