DEV Community

Dávid Szabó
Dávid Szabó

Posted on • Originally published at daveiscoding.com

Node.js TypeScript monorepo via NPM workspaces

Monorepos are all the rage right now. Modern projects are all using NX to set up a monorepo. But why would you introduce such a complex tool into your tech stack when something simple is often enough?

Both Yarn and NPM include workspace management in their feature-set. Thus you can manage multiple projects in one workspace. In addition, one of the tools is always available in your project, so why not use it?

The fantastic project

You are working on a fantastic project that you happened to name fantastic. How creative, isn't it?

fantastic is a command line application that will showcase how to set up a TypeScript monorepo using NPM workspaces. The fantastic project was a massive success as a CLI application, and many users wanted to have a graphical user interface to use it. So you decided to create a web interface. Your code currently lives in a single module containing the core logic and the CLI entry-point.

Therefore you decided to separate the project into three separate packages:

  • core - this package contains the core logic of your fantastic project
  • web - provides a web interface that interacts with the core package
  • cli - provides a command line interface that interacts with the core package

Initialize the project

Let's create an empty directory and initialize an NPM package:

mkdir fantastic
cd fantastic
npm init -y
Enter fullscreen mode Exit fullscreen mode

Now create the packages:

npm init -y --scope @fantastic -w packages/core
npm init -y --scope @fantastic -w packages/web
npm init -y --scope @fantastic -w packages/cli
Enter fullscreen mode Exit fullscreen mode

Define the dependencies between the packages:

npm install @fantastic/core -w @fantastic/web
npm install @fantastic/core -w @fantastic/cli
Enter fullscreen mode Exit fullscreen mode

Test it!

Now that we have the foundation in place, let's add some code to test it:

packages/core/index.js

console.log("Hello from Core!");
Enter fullscreen mode Exit fullscreen mode

packages/web/index.js

require("@fantastic/core");
console.log("Hello from Web!");
Enter fullscreen mode Exit fullscreen mode

packages/cli/index.js

require("@fantastic/core");
console.log("Hello from CLI!");
Enter fullscreen mode Exit fullscreen mode

Running the CLI outputs the following:

node packages/cli/index.js
Enter fullscreen mode Exit fullscreen mode
Hello from Core!
Hello from CLI!
Enter fullscreen mode Exit fullscreen mode

This confirms that the setup is working fine.

Here comes TypeScript

Time to turn this project from JavaScript to TypeScript!

First of all, install typescript as a dev dependency in the workspace project:

npm install -D typescript
Enter fullscreen mode Exit fullscreen mode

Every package requires its own tsconfig.json file. Since the fantastic project uses the same configuration for all the three packages, create a common tsconfig.base.json file in the root directory.

tsconfig.base.json

{
  "compilerOptions": {
    "incremental": true,
    "target": "es2020",
    "module": "commonjs",
    "declaration": true,
    "sourceMap": true,
    "strict": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,

    "composite": true
  }
}
Enter fullscreen mode Exit fullscreen mode

This is just a typical tsconfig.json file, except for one compiler option: composite. This option makes it possible for TypeScript to determine if a project has been built yet quickly.

Now you have a common TS config file, but you aren't using it yet. Create a tsconfig.json file in each package's root directory:

packages/core/tsconfig.json

{
  "extends": "../../tsconfig.base.json"
}
Enter fullscreen mode Exit fullscreen mode

The cli and web package is a bit different. You need to list out all your dependencies in the references property:

packages/cli/tsconfig.json and packages/web/tsconfig.json

{
  "extends": "../../tsconfig.base.json",
  "references": [{ "path": "../core" }]
}
Enter fullscreen mode Exit fullscreen mode

Now that independent packages are setup, create the tsconfig.json in the root directory:

tsconfig.json

{
  "files": [],
  "references": [
    { "path": "packages/core" },
    { "path": "packages/cli" },
    { "path": "packages/web" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Empty files array tells TypeScript to ignore all files except those in the references.

Rename all your .js files to .ts and replace require with import:

require("@fantastic/core");
Enter fullscreen mode Exit fullscreen mode

to

import "@fantastic/core";
Enter fullscreen mode Exit fullscreen mode

You are ready to compile:

npx tsc --build
Enter fullscreen mode Exit fullscreen mode

--build flag is required because the project contains multiple projects.

Now that you are done with all these changes, test your app again:

$ node packages/cli/index.js
Hello from Core!
Hello from CLI!

$ node packages/web/index.js
Hello from Core!
Hello from Web!
Enter fullscreen mode Exit fullscreen mode

Separate source code and build output

First of all, remove all the build outputs from the previous step. The following command will delete all .js, .js.map, .d.ts and .tsbuildinfo files in the packages directory.

rm packages/**/{*.js,*.js.map,*.d.ts,*.tsbuildinfo}
Enter fullscreen mode Exit fullscreen mode

Having your source code and build output in different directories is a good practice. Therefore, move each package's source code into an src directory, and change the build output directory to dist.

Extend your packages/*/tsconfig.json files with the following snippet:

  "compilerOptions": {
    "rootDir": "src",
    "outDir": "dist"
  },
Enter fullscreen mode Exit fullscreen mode

As an example, this is how the packages/web/tsconfig.json looks now:

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "rootDir": "src",
    "outDir": "dist"
  },
  "references": [{ "path": "../core" }]
}
Enter fullscreen mode Exit fullscreen mode

This tells TypeScript that your source code files are in the src directory, and the build output should go into the dist directory. These are relative to your tsconfig.json file.

Move your index.ts files into the respective src directory. At this point, you should have the following directory tree:

├── package-lock.json
├── package.json
├── packages
│   ├── cli
│   │   ├── package.json
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── core
│   │   ├── package.json
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   └── web
│       ├── package.json
│       ├── src
│       │   └── index.ts
│       └── tsconfig.json
├── tsconfig.base.json
└── tsconfig.json
Enter fullscreen mode Exit fullscreen mode

Before building your project, adjust the main property in the package.json of each package. Change index.js to dist/index.js since that's where it lives now.

Now build your project and run your CLI app:

npx tsc --build
node packages/cli/dist/index.js
Enter fullscreen mode Exit fullscreen mode

You should see the usual output:

Hello from Core!
Hello from CLI!
Enter fullscreen mode Exit fullscreen mode

You've done it! Good Job!

Now that you have laid the foundation for your project, go on and create something extraordinary!

The project's source code is available on GitHub. The repository contains a few little changes. Feel free to explore!

If you would like to learn more about NPM workspaces and TypeScript, check out these links:

PS. NX is an awesome tool! But sometimes, it's better to use the tools you already have at your disposal. Please take your time to decide whether to use NX or NPM / Yarn workspaces.

Top comments (0)