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 yourfantastic
project -
web
- provides a web interface that interacts with thecore
package -
cli
- provides a command line interface that interacts with thecore
package
Initialize the project
Let's create an empty directory and initialize an NPM package:
mkdir fantastic
cd fantastic
npm init -y
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
Define the dependencies between the packages:
npm install @fantastic/core -w @fantastic/web
npm install @fantastic/core -w @fantastic/cli
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!");
packages/web/index.js
require("@fantastic/core");
console.log("Hello from Web!");
packages/cli/index.js
require("@fantastic/core");
console.log("Hello from CLI!");
Running the CLI outputs the following:
node packages/cli/index.js
Hello from Core!
Hello from CLI!
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
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
}
}
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"
}
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" }]
}
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" }
]
}
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");
to
import "@fantastic/core";
You are ready to compile:
npx tsc --build
--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!
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}
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"
},
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" }]
}
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
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
You should see the usual output:
Hello from Core!
Hello from CLI!
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)