NPM Workspaces Tutorial

In this tutorial, you'll learn how to add Nx to a repository with an existing NPM workspaces setup.

What will you learn?

  • how to add Nx to the repository with a single command
  • how to configure caching for your tasks
  • how to configure a task pipeline
  • how to configure projects automatically with Nx Plugins
  • how to manage your releases with nx release
  • how to speed up CI with Nx Cloud ⚡

Starting Repository

To get started, fork the sample repository and clone it on your local machine:

git clone https://github.com/<your-username>/tuskydesign.git

The repository has two React packages (under packages/buttons and packages/forms) that are used in a demo application (located in apps/demo) that was designed to be used with the Vite CLI. The root package.json has a workspaces property that tells NPM how to find the projects in the repository.

package.json
1{ 2 "workspaces": ["packages/*", "apps/*"] 3} 4

Because of this setting, when the install command is run at the root, the correct packages are installed for each project. NPM will create dedicated node_modules folders inside of each project folder where necessary.

npm install

Now let's try running some tasks. To lint the demo app, use the lint npm script:

~/tuskydesigns

npm run lint -w @tuskdesign/demo

1> @tuskdesign/demo@0.0.0 lint 2> eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0 3

If you try to build the demo app, it will fail.

1Error: Failed to resolve entry for package "@tuskdesign/buttons". The package may have incorrect main/module/exports specified in its package.json. 2

The build script fails because it needs the buttons and forms projects to be built first in order to work correctly. To do this, lets return to the root of the repository and run the build task for every project in the repo:

npm run build --ws

When the buttons and forms projects are built first, the demo app can build successfully.

Now that you have a basic understanding of the repository we're working with, let's see how Nx can help us.

Smart Monorepo

Nx offers many features, but at its core, it is a task runner. Out of the box, it can cache your tasks and ensure those tasks are run in the correct order. After the initial set up, you can incrementally add on other features that would be helpful in your organization.

Add Nx

To enable Nx in your repository, run a single command:

npx nx@latest init

This command will download the latest version of Nx and help set up your repository to take advantage of it.

First, the script will propose installing some plugins based on the packages that are being used in your repository.

  • Deselect both proposed plugins so that we can explore what Nx provides without any plugins.

Second, the script asks a series of questions to help set up caching for you.

  • Which scripts need to be run in order? - Choose build
  • Which scripts are cacheable? - Choose typecheck, build and lint
  • Does the "typecheck" script create any outputs? - Enter nothing
  • Does the "build" script create any outputs? - Enter dist
  • Does the "lint" script create any outputs? - Enter nothing
  • Would you like remote caching to make your build faster? - Choose Skip for now

Explore Your Workspace

If you run nx graph as instructed, you'll see the dependencies between your projects.

npx nx graph --focus=@tuskdesign/demo

Loading...

Nx uses this graph to determine the order tasks are run and enforce module boundaries. You can also leverage this graph to gain an accurate understanding of the architecture of your codebase. Part of what makes this graph invaluable is that it is derived directly from your codebase, so it will never become out of date.

Caching Pre-configured

Nx has been configured to run your build, typecheck and lint tasks. You can run a single task like this:

npx nx build @tuskdesign/demo

Or all tasks with a certain name like this:

npx nx run-many -t typecheck

During the init script, Nx also configured caching for these tasks. You can see in the nx.json file that the build, typecheck and lint targets have the cache property set to true and the build target specifies that its output goes to the project's dist folder.

nx.json
1{ 2 "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 "targetDefaults": { 4 "build": { 5 "dependsOn": ["^build"], 6 "outputs": ["{projectRoot}/dist"], 7 "cache": true 8 }, 9 "typecheck": { 10 "cache": true 11 }, 12 "lint": { 13 "cache": true 14 } 15 }, 16 "defaultBase": "main" 17} 18

Try running build for the demo app a second time:

npx nx build @tuskdesign/demo

The first time nx build was run, it took about 5 seconds - just like running npm run build. But the second time you run nx build, it completes instantly and displays this message:

1Nx read the output from the cache instead of running the command for 3 out of 3 tasks. 2

You can see the same caching behavior working when you run npx nx lint or npx nx typecheck.

Use Task Pipelines

You may be wondering why the caching message in the previous section mentioned 3 tasks when you only ran the build task from the terminal. When we said that build tasks must be run in order during the setup script, Nx created a simple task pipeline. You can see the configuration for it in the nx.json file:

nx.json
1{ 2 "targetDefaults": { 3 "build": { 4 "dependsOn": ["^build"] 5 } 6 } 7} 8

This configuration means that if you run build on any project, Nx will first run build for the dependencies of that project and then run build on the project itself. The ^build text means "the build tasks of the project's dependencies." You can visualize this in the Nx graph by selecting the Tasks dropdown in the top left and clicking Show all tasks:

npx nx graph

Alternatively, you can pass the --graph option to the run command to inspect the task graph.

npx nx run @tuskdesign/demo:build --graph

Loading...

With this pipeline in place, you will never run into the error we hit at the beginning of the tutorial where the forms and buttons packages weren't built so the demo app couldn't build. Test this out by deleting the packages/forms/dist folder and then re-running the build task for the demo app.

~/tuskydesigns

npx nx build @tuskdesign/demo

1... 2 3 NX Successfully ran target build for project @tuskdesign/demo and 2 tasks it depends on (40ms) 4 5Nx read the output from the cache instead of running the command for 3 out of 3 tasks. 6

Not only does the build complete successfully, but it finishes instantly and the packages/forms/dist folder is put back in place thanks to the caching.

Create a Task Pipeline

You may have noticed in the apps/demo/package.json file, there is a prebuild script that runs typecheck before the build script in order to catch any type errors. Let's set up this same behavior in the Nx task pipeline as well.

nx.json
1{ 2 "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 "targetDefaults": { 4 "build": { 5 "dependsOn": ["^build", "typecheck"], 6 "outputs": ["{projectRoot}/dist"], 7 "cache": true 8 }, 9 "typecheck": { 10 "cache": true 11 }, 12 "lint": { 13 "cache": true 14 } 15 }, 16 "defaultBase": "main" 17} 18

The dependsOn line makes Nx run the typecheck task for the current project and the build task for any dependencies before running the current project's build task. Now nx build will run the typecheck task just like npm run build does.

Use Nx Plugins to Enhance Vite Tasks with Caching

You may remember that we defined the outputs property in nx.json when we were answering questions in the nx init script. The value is currently hard-coded so that if you change the output path in your vite.config.ts, you have to remember to also change the outputs array in the build task configuration. This is where plugins can help. They directly infer information from the actual tooling configuration files (vite.config.ts in this case).

Nx plugins can:

  • automatically configure caching for you, including inputs and outputs based on the underlying tooling configuration
  • infer tasks that can be run on a project because of the tooling present
  • provide code generators to help scaffold out projects
  • automatically keep the tooling versions and configuration files up to date

For this tutorial, we'll just focus on the automatic caching configuration.

First, let's delete the outputs array from nx.json so that we don't override the inferred values from the plugin. Your nx.json should look like this:

nx.json
1{ 2 "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 "targetDefaults": { 4 "build": { 5 "dependsOn": ["^build", "typecheck"], 6 "cache": true 7 }, 8 "typecheck": { 9 "cache": true 10 }, 11 "lint": { 12 "cache": true 13 } 14 }, 15 "defaultBase": "main" 16} 17

Now let's add the @nx/vite plugin:

~/tuskydesign

npx nx add @nx/vite

1✔ Installing @nx/vite... 2✔ Initializing @nx/vite... 3 4 NX Package @nx/vite added successfully. 5

The nx add command installs the version of the plugin that matches your repo's Nx version and runs that plugin's initialization script. For @nx/vite, the initialization script registers the plugin in the plugins array of nx.json and updates any package.json scripts that execute Vite related tasks. Open the project details view for the demo app and look at the build task.

npx nx show project @tuskdesign/demo

Loading...

If you hover over the settings for the vite:build task, you can see where those settings come from. The inputs and outputs are defined by the @nx/vite plugin from the vite.config.ts file where as the dependsOn property we set earlier in the tutorial in the targetDefaults in the nx.json file.

Now let's change where the build results are output to in the vite.config.ts file.

1import { defineConfig } from 'vite'; 2import react from '@vitejs/plugin-react'; 3 4// https://vitejs.dev/config/ 5export default defineConfig({ 6 plugins: [react()], 7 build: { 8 outDir: '../../dist/demo', 9 }, 10}); 11

Now if you look at project details view again, you'll see that the outputs property for Nx's caching has been updated to stay in sync with the setting in the vite.config.ts file.

You can also add the @nx/eslint plugin to see how it infers lint tasks based on the ESLint configuration files.

Checkpoint

At this point, the repository is still using all the same tools to run tasks, but now Nx runs those tasks in a smarter way. The tasks are efficiently cached so that there is no repeated work and the cache configuration settings are automatically synced with your tooling configuration files by Nx plugins. Also, any task dependencies are automatically executed whenever needed because we configured task pipelines for the projects.

Open up the task graph for demo app's build task again to see the changes.

npx nx run @tuskdesign/demo:build --graph

Loading...

Manage Releases

If you decide to publish the forms or buttons packages on NPM, Nx can also help you manage the release process. Release management involves updating the version of your package, populating a changelog, and publishing the new version to the NPM registry.

First you'll need to define which projects Nx should manage releases for by setting the release.projects property in nx.json:

nx.json
1{ 2 "release": { 3 "projects": ["packages/*"] 4 } 5} 6

Now you're ready to use the nx release command to publish the forms and buttons packages. The first time you run nx release, you need to add the --first-release flag so that Nx doesn't try to find the previous version to compare against. It's also recommended to use the --dry-run flag until you're sure about the results of the nx release command, then you can run it a final time without the --dry-run flag.

To preview your first release, run:

npx nx release --first-release --dry-run

The command will ask you a series of questions and then show you what the results would be. Once you are happy with the results, run it again without the --dry-run flag:

npx nx release --first-release

After this first release, you can remove the --first-release flag and just run nx release --dry-run. There is also a dedicated feature page that goes into more detail about how to use the nx release command.

Fast CI ⚡

Forked repository with Nx

Make sure you have completed the previous sections of this tutorial before starting this one. If you want a clean starting point, you can fork the sample repository with Nx already added.

So far in this tutorial you've seen how Nx improves the local development experience, but the biggest difference Nx makes is in CI. As repositories get bigger, making sure that the CI is fast, reliable and maintainable can get very challenging. Nx provides a solution.

Connect to Nx Cloud

Nx Cloud is a companion app for your CI system that provides remote caching, task distribution, e2e tests deflaking, better DX and more.

Now that we're working on the CI pipeline, it is important for your changes to be pushed to a GitHub repository.

  1. Commit your existing changes with git add . && git commit -am "updates"
  2. Push your changes to your forked GitHub repository with git push

Now connect your repository to Nx Cloud with the following command:

npx nx connect

A browser window will open to register your repository in your Nx Cloud account. The link is also printed to the terminal if the windows does not open, or you closed it before finishing the steps. The app will guide you to create a PR to enable Nx Cloud on your repository.

Once the PR is created, merge it into your main branch.

And make sure you pull the latest changes locally:

git pull

You should now have an nxCloudAccessToken property specified in the nx.json file.

Create a CI Workflow

Use the following command to generate a CI workflow file.

npx nx generate ci-workflow --ci=github

This generator creates a .github/workflows/ci.yml file that contains a CI pipeline that will run the lint, test, build and e2e tasks for projects that are affected by any given PR. Since we are using Nx Cloud, the pipeline will also distribute tasks across multiple machines to ensure fast and reliable CI runs.

The key lines in the CI pipeline are:

.github/workflows/ci.yml
1name: CI 2# ... 3jobs: 4 main: 5 runs-on: ubuntu-latest 6 steps: 7 - uses: actions/checkout@v4 8 with: 9 fetch-depth: 0 10 # This enables task distribution via Nx Cloud 11 # Run this command as early as possible, before dependencies are installed 12 # Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun 13 # Connect your workspace by running "nx connect" and uncomment this 14 - run: npx nx-cloud start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="build" 15 - uses: actions/setup-node@v3 16 with: 17 node-version: 20 18 cache: 'npm' 19 - run: npm ci --legacy-peer-deps 20 - uses: nrwl/nx-set-shas@v4 21 # Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected 22 - run: npx nx affected -t lint test build 23

Open a Pull Request

Commit the changes and open a new PR on GitHub.

git add .

git commit -m 'add CI workflow file'

git push origin add-workflow

When you view the PR on GitHub, you will see a comment from Nx Cloud that reports on the status of the CI run.

Nx Cloud report

The See all runs link goes to a page with the progress and results of tasks that were run in the CI pipeline.

Run details

For more information about how Nx can improve your CI pipeline, check out one of these detailed tutorials:

Next Steps

Connect with the rest of the Nx community with these resources: