Skip to main content

NPM Workspaces Monorepo Setup (JavaScript)

In this article, I show you how to set up a JavaScript monorepo using npm workspaces. With the latest versions of npm you no longer need third-party tools to manage your packages in one repo.

These instructions were written for a Mac.  

What is an npm workspace?

From the online documentation (see references at the end of this article):

Workspaces is a generic term that refers to the set of features in the npm cli that provides support to managing multiple packages from your local file system from within a singular top-level, root package.

This set of features makes up for a much more streamlined workflow handling linked packages from the local file system. Automating the linking process as part of npm install and avoiding manually having to use npm link in order to add references to packages that should be symlinked into the current node_modules folder.

We also refer to these packages being auto-symlinked during npm install as a single workspace, meaning it's a nested package within the current local file system that is explicitly defined in the package.json workspaces configuration.

If you've ever wrestled with npm link to test one local package that depends on another local package, you may appreciate workspaces as an alternative.

Step 1. Install the latest npm and mocha globally

You must ensure that you have the latest version of npm installed in order to use workspaces.

If you don’t have npm installed, click here.

Upgrade to the latest version of npm and install the mocha testing framework globally:

  • Open up a terminal window
  • Run the following (only use sudo in the front if you have rights issues):
sudo npm install --location=global npm@latest mocha

That command installs the latest version of npm and mocha - a test automation utility.

Because the packages are installed globally, you won’t need to do this again for new projects.  Though on occasion you might want to upgrade them to the latest version.

Step 2. Create a monorepo project

Create a new folder for your project:

  • Change to a parent folder for all of your projects
  • Create a new folder for a new monorepo / workspace project:
mkdir workspace-js
cd workspace-js

Step 3. Create a .gitignore file

To prevent build folders and files from being checked in:

  • Create a new file called .gitignore
  • On a Mac, you can use this command:
touch .gitignore
  • Open .gitignore in a code editor
  • Paste the following into the file and save it:
node_modules/
npm-debug.log
.DS_Store

Step 4. Initialize the project

To initialize the project with defaults, run this command in the project folder:

npm init -y

That will create a new package.json file that will look something like this:

{
"name": "workspace",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}

Step 5. Add a package to the workspace

  • Create a new package:
npm init -w ./packages/alpha
tip

Notice the use of the -w flag - that's the workspace flag added in npm 7.

When prompted, add your scope to the package name. In my case, the package would be @mitchallen/alpha - change @mitchallen to whatever scope you want.

Respond to the prompts so the resulting package.json file looks like this:

{
"name": "@mitchallen/alpha",
"version": "0.0.1",
"description": "Test package alpha",
"main": "index.js",
"scripts": {
"mocha test/*.test.js"
},
"author": "Mitch Allen",
"license": "MIT"
}

This does the following:

  • creates a new packages folder
  • creates a new packages/alpha folder
  • creates a new packages/alpha/package.json file and populates it with your answers
  • adds a workspaces property in the root package.json file, referencing the alpha folder

You can see the results for yourself on a Mac with this command:

cat packages/alpha/package.json

If you open the root package.json you will see the new workspaces property:

"workspaces": [ "packages/alpha" ]

On a Mac, you can view the newly created symlink with this command (substitute YOUR_SCOPE):

ls -ls node_modules/@YOUR_SCOPE 

You should see a linked entry like this:

alpha -> ../../packages/alpha

Alternate way

If you prefer a more streamlined approach, you could instead have done this (replace YOUR_SCOPE with your scope):

npm init --scope=@YOUR_SCOPE -w ./packages/alpha -y
  • The scope parameter adds your scope automatically
  • The -y flag at the end skips the questions

Then you could edit the ./packages/alpha/package.json file later.

Step 6. Create a library

  • Create a new index.js file in the package folder:
touch packages/alpha/index.js
  • Replace the code in packages/alpha/index.js with this code and save it:
"use strict";

module.exports = {
add: (a,b) => a + b,
subtract: (a,b) => a - b
};

It’s a simple module that has two functions:

  • add – returns the addition of two arguments
  • subtract – returns the subtraction of one argument from another

Step 7. Public access

If you plan to publish the package at a later time, be sure to insert this into the package.json file.

"publishConfig": {
"access": "public"
},

You would need to do that for every package in the workspace.

Step 8. Verify package test script

  • Check the scripts section of the alpha package.json file.  It should look like this:
  "scripts": {
"test": "mocha test/*.test.js"
},

Step 9. Write the alpha test cases

If there isn't a test folder under the alpha package, create it with this command:

mkdir packages/alpha/test
  • Create a test file in that folder:
touch packages/alpha/test/smoke.test.js
  • Edit packages/alpha/test/smoke.test.js
  • Replace all code in the file with the following and save it:
'use strict';

var assert = require('assert');

const alpha = require('..');

describe('alpha', function () {
context('smoke test', function () {
it('add should add two numbers together', function (done) {
assert.strictEqual(alpha.add(100,200),300);
done();
});
it('subtract should subtract one number from another', function (done) {
assert.strictEqual(alpha.subtract(100,200),-100);
done();
});
});
});
  • Run the test case with this command:
npm test -ws --if-present

💡

Notice the use of the -ws flag - that's the workspaces flag (plural) as opposed to just -w (singular) used in the init command.

Step 10. Create a second package

Create a second package called beta:

npm init -w ./packages/beta
  • When prompted use your scope in the package name (like @mitchallen/beta)
  • Answer the prompts so the beta package.json file looks something like this:
{
"name": "@mitchallen/beta",
"version": "0.0.1",
"description": "Test beta package",
"main": "index.js",
"devDependencies": {},
"scripts": {
"test": "mocha test/*.test.js"
},
"author": "Mitch Allen",
"license": "MIT"
}
  • Create a new index.js file in the package folder:
touch packages/beta/index.js
  • Replace the code in packages/beta/index.js with the code below and save it
  • Replace @YOUR-SCOPE with the scope you used for the alpha package
"use strict";

var alpha = require('@YOUR-SCOPE/alpha');

module.exports = beta;

function beta(a,b,c) {
// return a + b - c
return alpha.subtract(alpha.add(a,b),c);
}

The purpose is to show how one package (beta) can depend on another package (alpha) in the same monorepo without explicit linking or publishing.

Step 11. Verify the package test script

  • Check the scripts section of the beta package.json file.  It should look like this:
"scripts": { 
"test": "mocha test/*.test.js"
}

Step 12. Write the test cases

  • Create a folder called test under the beta package folder:
mkdir packages/beta/test
  • Create a file called smoke.test.js under the test folder:
touch packages/beta/test/smoke.test.js
  • Edit packages/beta/test/smoke.test.js
  • Replace all code in the file with the following and save it:
"use strict";

var assert = require('assert');

const beta = require('..');

describe('beta', function () {
context('smoke test', function () {
it('should add first two numbers and subtract the third', function (done) {
const a = 100, b = 200, c = 50;
const expected = a + b - c;
assert.strictEqual(beta( a, b, c ), expected );
done();
});
});
})

Step 13. Run the tests

Run the tests with the following command:

npm test -ws --if-present

You should see the mocha test results for your package.

To run the tests for each package individually (substituting with your scope):

npm test -w @mitchallen/alpha

npm test -w @mitchallen/beta

Step 14. Specify a dependency

Things work fine when testing inside a monorepo. But if you publish the individual packages they will need to know where to find and install their dependencies.

Run this command to make the alpha package a dependency for the beta package (substituting for your scope):

npm install @mitchallen/alpha -w @mitchallen/beta

Run this command to see that alpha is now a dependency in the beta package:

cat packages/beta/package.json

Here is what the dependency should look like:

  "dependencies": {
"@mitchallen/alpha": "^0.0.1"
}

Whenever you make changes, you should run the tests again to make sure nothing broke:

npm test -ws --if-present

Example Repo

You can find an example of the repo created in this article here:

Conclusion

In this article, you learned how to:

  • Setup a workspaces JavaScript monorepo for testing using mocha (MochaJS)
  • Add multiple packages
  • Add mocha for testing
  • Create dependencies between packages without the need for linking or publishing

Reference

  • workspaces | npm Docs - [1]