Nx and Yarn/Lerna (Workspaces for Publishing NPM Packages)
Nx has more in common with the build tools used at Google or Facebook (just made a lot more easily accessible for other companies) than with tools like Yarn Workspaces or Lerna. When using the word "monorepo" in the context of say Google, we imagine a much richer dev experience, not simply collocating a few projects side-by-side.
Lerna/Yarn/PNPM are package managers. When it comes to monorepos, they mainly perform node_modules
deduping. So the choice isn't between Nx or Yarn Workspaces. It's between whether you want to have multiple node_modules
folders (in this case use Nx with Yarn Workspaces) or not (use Nx without Yarn Workspaces).
In this guide we will look at setting up an Nx Workspaces for publishing NPM packages.
- We will look at how to create a new workspace for publishing packages.
- We will look at adding Nx to an existing workspace.
New Workspace
Running yarn create nx-workspace --preset=npm
creates an empty workspace set up to publish npm packages.
This is what is generated:
packages/
nx.json
workspace.json
tsconfig.base.json
package.json
package.json
contains Nx packages.
1{
2 "name": "myorg",
3 "version": "0.0.0",
4 "license": "MIT",
5 "scripts": {},
6 "private": true,
7 "devDependencies": {
8 "@nrwl/cli": "12.8.0",
9 "@nrwl/tao": "12.8.0",
10 "@nrwl/workspace": "12.8.0",
11 "@types/node": "14.14.33",
12 "typescript": "~4.3.5"
13 }
14}
nx.json
contains the Nx CLI configuration.
1{
2 "extends": "@nrwl/workspace/presets/npm.json",
3 "npmScope": "myorg",
4 "tasksRunnerOptions": {
5 "default": {
6 "runner": "@nrwl/workspace/tasks-runners/default",
7 "options": {
8 "cacheableOperations": ["build", "lint", "test", "e2e"]
9 }
10 }
11 }
12}
Finally, workspace.json
lists the workspace projects, and since we have none, it is empty.
Creating an NPM Package
Running nx g npm-package simple
results in:
packages/
simple/
index.js
package.json
nx.json
workspace.json
tsconfig.base.json
package.json
The generated simple/package.json
:
1{
2 "name": "@myorg/simple",
3 "version": "1.0.0",
4 "scripts": {
5 "test": "node index.js"
6 }
7}
And workspace.json
gets updated to:
1{
2 "version": 2,
3 "projects": {
4 "simple": {
5 "root": "packages/simple"
6 }
7 }
8}
With this you can invoke any script defined in simple/package.json
via Nx. For instance, you can invoke the test
script by running yarn nx test simple
. And if you invoke this command a second time, the results are retrieved from cache.
In this example, we used a generator to create the package, but you could also have created it by hand or have copied it from another project.
The change in workspace.json
is the only thing required to make Nx aware of the simple
package. As long as you include the project into workspace.json
, Nx will include that project source into its graph computation and source code analysis. It will, for instance, analyze the project's source code, and it will know when it can reuse the computation from the cache and when it has to recompute it from scratch.
Creating Second NPM Package and Enabling Yarn Workspaces
Running nx g npm-package complex
results in:
packages/
simple/
index.js
package.json
complex/
index.js
package.json
nx.json
workspace.json
tsconfig.base.json
package.json
Now let's modify packages/complex/index.js
to include require('@myorg/simple')
. If you run npx nx test complex
, you will see an error saying that @myorg/simple
cannot be resolved.
This is expected. Nx analyzes your source to enable computation caching, it knows what projects are affected by your PR, but it does not change how your npm scripts run. Whatever tools you use in your npm scripts will run exactly as they would without Nx. Nx doesn't replace your tools and doesn't change how they work.
To make it work add a dependency from complex
to simple
in package.json
:
1{
2 "name": "@myorg/complex",
3 "version": "1.0.0",
4 "scripts": {
5 "test": "node index.js"
6 },
7 "dependencies": {
8 "@myorg/simple": "*"
9 }
10}
Then add the following to the root package.json
(which enables Yarn Workspaces).
1{
2 "workspaces": ["packages/*"]
3}
Finally, run yarn
.
npx nx test complex
works now.
What Nx Provides
If you run npx nx dep-graph
you will see that complex
has a dependency on simple
. Any change to simple
will invalidate the computation cache for complex
, but changes to complex
won't invalidate the cache for simple
.
In contrast to more basic JS monorepo tools, Nx doesn't just analyze package.json
files. It does much more. Nx also knows that adding a require()
creates a dependency and that some dependencies cannot even be expressed in the source code. This is crucial for the following reasons:
- Often, configuration files aren't packages. They aren't built and aren't published. Your packages can depend on them. Nx can recognize this dependency and take it into consideration for
affected:*
and cache computations. - Many workspaces have non-JS projects, so you cannot define the deps between them and your JS projects using
package.json
. - Aux packages (e.g., e2e tests or demo applications) aren't publishable. You could create fake
package.json
files for them, but with Nx you don't have to. - You may have a de-facto dependency (where Project A depends on B without a dependency in
package.json
). This would break other monorepo tools because they would not know that changing B changes the behavior of A. This isn’t the case with Nx.
Some Things You Can Do
npx nx run-many --target --test --all --parallel
tests all projects in parallel.npx nx affected --target --test --all
tests all the projects affected by the current PR. To see this in action, check in all your changes, and create a new branch.
Adding Plugins
As you can see, the core of Nx is generic, simple, and unobtrusive. Nx Plugins are completely optional, but they can really level up your developer experience.
Unfortunately, the example we have here is too basic and won't benefit from using plugins, so please watch the video on Adding Nx to Existing Workspaces to see how you can transition from this core Nx workspace to a plugin-powered one.
Adding Nx to Existing Workspaces
The following 10-min video walks you through the steps of adding Nx to a Lerna repo and showing many affordances Nx offers. Although the video uses Lerna, everything said applies to Yarn Workspaces or PNPM. Basically, any time you hear "Lerna" you can substitute it for Yarn or PNPM.
Clarifying Misconceptions
Misconception: You have to choose between Nx and Yarn Workspaces/Lerna.
Lerna, Yarn workspaces, pnpm workspaces offer the following affordances for developing multiple projects in the same repo:
- Deduping node_modules. If I use the same version of say Next.js in all the projects, the package is installed once.
- Task orchestration. If I want to test all the projects, I can use a single command to do it.
- Publishing (Lerna only). I can run one command to publish packages to NPM.
This is what Nx offers:
- Smart rebuilds of affected projects
- Distributed task execution & computation caching
- Code sharing and ownership management
- High-quality editor plugins & GitHub apps
- Powerful code generators
- Workspace visualizations
- Rich plugin ecosystem
- Consistent dev experience for any framework
- Automatic upgrade to the latest versions of all frameworks and tools
As you can see, there is basically no overlap. Nx is notisn't a package manager nor does it (it's not a JS-only tool), so deduping node_modules
isn't in that list. Nx doesn't care whether your repo has multiple node_modules folders or not, and whether you choose to dedupe them or not. In fact, many companies use Nx and yarn-workspaces together to get the benefits of both. If you want to use Yarn Workspaces to dedupe node_modules
in your Nx workspace, you can do it. Many companies do.
What often happens though is when folks adopt Nx, they have better affordances for implementing a single-version policy (why this is a good idea is beyond the scope of this post, but you can read more about why Google does here). But it's important to stress that this isn't required by Nx. It's simply something that Nx can enable you to do at scale.
Misconception: Nx is only for apps
If you do something well, folks assume that the only thing you can do. Nx is equally suited for publishable npm packages as it is for applications.
For instance, the Nx repo itself is built with Nx. It has 2 applications and a few dozen libraries. Those libraries are published to NPM.
Nx even has a @nrwl/node
pluginAlthough it's true to say that the Nx core doesn't care whether you publish your packages or not, there are Nx plugins (e.g., @nrwl/node
) that helps you bundle and package your modules for publishing.
For instance, the Nx repo itself is built with Nx. It has 2 applications and a few dozen libraries. Those libraries are published to NPM. Many larger organizations using Nx publish a subset of their libraries to their registry.
Misconception: Nx is "all-in"
While Nx does have many plugins, each of them is optional. As you saw in the example above, Nx at its core is very minimal. Much like VS Code, Nx is very minimal but can easily be extended by adding plugins.Saying this is akin to saying that VS Code is "all in". The fullness and richness of the experience depends on how many plugins you choose to use. You could install a lot of Nx Plugins that will do a lot of the heavy lifting in, for instance, connecting your Next.js, Storybook and Cypress. You could but you don't have to.
As you saw in the example above, Nx core itself isn't more all-in than Yarn Workspaces is.
Misconception: Nx is configuration over convention
As you saw in the example above and in the video, the amount of the generated configuration is small.
"simple": { "root": "packages/simple" }
After just one line, Nx automatically detects the npm scripts of your projects.That is it. If you have 200 projects in your workspace, you will see 200 lines specifying projects' roots. Practically everything else you see is optional. You can choose to configure your executors instead of using npm scripts, or configure generator defaults, and so forth. When you configure the @nrwl/web:dev-server
executor, though, you aren't just adding a chunk of json config into workspace.json
, you are also removing the configuration files you used to implement the same functionality (start scripts, Webpack config files etc.) So the total amount of configuration is decreasing, and, often, by a lot.
We came from Google, so we take the toolability very seriously. Nx plugins provide metadata to be understood by the Nx CLI and editors. The configuration of your workspace is also statically analyzable (in opposite to say the Webpack config files). In addition to enabling good VSCode and WebStorm support, it allows us to update your configuration automatically as you upgrade your version of Nx. Other than the toolability aspect we try to keep the config as short as possible and rely on conventions.