1956 1791 1558 1704 1366 1584 1847 1028 1253 1624 1448 1139 1262 1779 1470 1217 1029 1705 1935 1968 1182 1557 1269 1784 1830 1566 1014 1247 1933 1022 1992 1343 1457 1771 1693 1788 1531 1199 1092 1694 1648 1160 1030 1764 1061 1275 1283 1902 1651 1356 1893 1211 1117 1882 1979 1238 1130 1237 1230 1091 1967 1146 1220 1187 1499 1519 1849 1885 1233 1662 1939 1581 1025 1923 1791 1558 1617 1078 1030 1846 1693 1437 1236 1506 1198 1494 1991 1376 1991 1816 1088 1937 1569 1254 1298 1523 1985 1054 1330 Setting up a TypeScript multi-package mono-repo for @scoped/packages. | PHPnews.io


Setting up a TypeScript multi-package mono-repo for @scoped/packages.

Written by Frank de Jonge / Original link on Aug. 8, 2021


Lerna and Yarn workspaces provide tools to ease the lives of multi-package mono-repo maintainers. Since version 7 NPM follows suit by adding support for workspaces, opening up the world of multi-package tooling for NPM-only users.

While I was setting up a project, I tried to search for a way to combine workspaces with scoped packages. It wasn't immediately clear how to set this up. After figuring out how to do it, I thought I'd share this for those who are looking to do the same.

In a couple of steps I'll walk through setting up a multi-package mon0-repo for a TypeScript project that uses Jest for testing. Let's get right to it.

Step 1: Setting up the top-level NPM project

The first thing we need to do is create a directory for our project and initiate a private NPM package.

mkdir -p ~/Sites/acme-corp

cd ~/Sites/acme-corp

echo '{"private": true}' >> package.json

Next we can install some dependencies that we'll use to compile and test our packages. We'll be using jest as the test framework.

npm install --save-dev typescript @types/node jest ts-jest @types/jest

The packages will be located in the packages directory. The package.json needs to modified to NPM where to find the packages will be located.

  "private": true,
  "workspaces": [
  // ...

Lastly, to prepare for our test setup we can add a test script.

  // ...
  "scripts": {
    "test": "jest"
  // ...

Step 2: Create scoped packages in workspaces

NPM's documentation shows how to create packages in a work space. To create a scoped workspace package, use the --scope option. For demonstration purposes we're going to re-create the is-even and is-odd packages with the same dependency setup they have. The is-odd will depend on the is-even package to implement its logic.

npm init --scope=@acme-corp -y -w ./packages/is-even
npm init --scope=@acme-corp -y -w ./packages/is-odd

This creates two package.json files that look something like this:

  "name": "@acme-corp/is-even",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  "author": "",
  "license": "ISC"

Although many other workspace tutorials keep the package name and directory name in sync, this does not seem to be a requirement. Scoped packages have a name that is different from the folder name.

As stated before, our is-odd clone will use the @acme-corp/is-even package. This mean we need to add the @acme-corp/is-even as a dependency.

npm install @acme-corp/is-even -w ./packages/is-odd

The install command uses the -w option to indicate which package should receive the dependency. This can be used to add any dependencies to the workspace. We can now implement the packages. For each package we create an index.ts file.

// contents of ./packages/is-even/index.ts

export function isEven(i: number): boolean {
    return i % 2 === 0;
// contents of ./packages/is-odd/index.ts

import { isEven } from '@acme-corp/is-even';

export function isOdd(i: number): boolean {
    return isEven(i) === false;

With the code in place we can start configuring everything that's needed to ship the packages.

Step 3: Configuring out TypeScript setup

This is probably the most complicated step. The majority of our TypeScript configuration will be centralised, preventing the configuration of our packages to get out of sync. First up is a top-level config file (tsconfig.build.json) that we'll use as configuration baseline.

    "exclude": [
    "compilerOptions": {
        "target": "ES2015",
        "module": "commonjs",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true,
        "declaration": true,
        "paths": {
            "@acme-corp/*": ["./packages/*/"]

The configuration baseline uses a dynamic paths configuration to resolve the packages locally. This is not strictly needed, but it allows you to try out your packages together in a local script.

Our main top-level configuration file tsconfig.json will extend our build configuration.

    "extends": "./tsconfig.build.json",
    "compilerOptions": {
        "baseUrl": "./"
    "include": [

Next, each of our packages will need their own configuration file located at ./packages/[package]/tsconfig.pkg.json.

    "extends": "../../tsconfig.build.json",
    "compilerOptions": {
        "outDir": "./dist/"
    "include": [

To compile the project, each package.json will require a compile script.

  // ... 
  "scripts": {
    "compile": "rm -rf dist/* && tsc -p tsconfig.pkg.json"
  // ...

We can now compile our code in bulk by using the npm run compile instruction with the -ws option to trigger the script on all of the packages.

npm run compile -ws

With the code compiled, the packages can be release. Before we do that, let's add some tests to ensure what we ship makes any sense.

Step 4: Testing our packages

As mentioned before, we'll be using Jest to test our code. The first thing to do is to add the tests. These are the tests for the is-odd package, located in ./packages/is-odd/index.test.ts.

import { isOdd } from "./index";

describe('isOdd', () => {
    test('it detects even numbers', () => {

    test('it detects odd numbers', () => {

To get our tests to run, we need to create a Jest configuration file located at the root of our project (jest.config.js). This setup generates a code-coverage report and uses ts-jest which allows us to use TypeScript.

module.exports = {
    preset: 'ts-jest',
    testEnvironment: 'node',
    collectCoverage: true,
    coverageDirectory: "coverage",
    collectCoverageFrom: [
    coveragePathIgnorePatterns: [
    moduleNameMapper: {
        '^@acme-corp/(.*)$': '<rootDir>/packages/$1/'

The moduleNameMapper setting provides local module resolution that is compatible with tsconfig.build.js, which is required because ts-jest does not use tsconfig.js configuration.

With our top-level test script in place, we can now run the tests for our entire project using a single command.

npm run test

Running the tests for a single package is also possible.

npm run test -- packages/is-odd

Step 5: Publishing the packages

As our last step we'll go ahead and release our packages by publishing then to NPM. Before shipping our packages we need to ensure everything is ready to go. First, we'll want to be sure that all our packages are compiled.

npm run compile -ws

Next we'll add an .npmignore file to each package to ensure the packages only contain the files intended for shipping. In the following example everything is ignored and shippable artefacts are excluded from the ignore list.


This list ensures only the dist folder, the package.json and the readme are shipped. We're now ready to publish our packages. We can do this by running the following command:

npm publish --workspaces --access public

During the publishing you may be required to authenticate against the NPM registry is you haven't done so already.

Et voila!

That's all it takes to setup a multi-package mono-repo for a TypeScript project. I've published the packages and pushed the entire setup to a github repository in case you want to see it in its entirety.

frankdejonge frankdejonge

« Why Aren't There More Programming Languages Startups? - Hold Attention on the Virtual Stage »