id | title |
---|---|
getting-started |
Getting Started |
You can incrementally adopt Lerna for existing monorepos or create a new Lerna workspace by running npx lerna init
.
All Lerna functionality will work the same way regardless.
To show a lot of what Lerna can do, we will use this repository.
If you learn better by doing, clone the repo, check out the prelerna branch and follow along!
The repo contains three packages or projects:
header
(a library of React components)footer
(a library of React components)remixapp
(an app written using the Remix framework which depends on bothheader
andfooter
)
packages/
header/
src/
...
package.json
rollup.config.json
jest.config.js
footer/
src/
...
package.json
rollup.config.json
jest.config.js
remixapp/
app/
...
public/
package.json
remix.config.js
package.json
To add Lerna run the following command:
> npx lerna@latest init
This will generate lerna.json
and will add lerna
to the root package.json
.
{
"name": "root",
"private": true,
"devDependencies": {
"lerna": "5.1.0"
}
}
What makes Lerna 5.1+ so powerful is the task delegation and other features that come with its integration
with Nx. To opt in, install the nx
package:
> npm i nx --save-dev
You should get a package.json
as follows:
{
"name": "root",
"private": true,
"devDependencies": {
"lerna": "5.1.0",
"nx": "14.2.0"
}
}
Finally, set useNx
to true
in lerna.json
:
{
"packages": ["packages/*"],
"useNx": true,
"version": "0.0.0"
}
By having Nx installed alongside Lerna, you can use its capabilities to open an interactive visualization of the workspace project graph.
Run npx nx graph
to open the visualization:
Bootstrapping is one of the three main key features of Lerna. It enables different projects in the same repository to import each other without having to be published to a registry.
To see how it works, let for example inspect the package.json
file of remixapp
.
{
...
"dependencies": {
"@remix-run/node": "^1.5.1",
"@remix-run/react": "^1.5.1",
"@remix-run/serve": "^1.5.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"header": "*",
"footer": "*"
}
}
The "header": "*"
and "footer": "*"
tell Lerna to link the contents of the header
and footer
as if they were
published to the registry. To do that, we need to run:
> npx lerna bootstrap
Now all the projects in the workspace can properly reference each other so that they can now be built.
To build all projects, run
> npx lerna run build
This builds the three projects in the right order: header
and footer
will be built first (and in parallel),
and remixapp
will be built after. The order matters because the remixapp
uses the bundles from the compiled header
and footer
.
✔ header:build (501ms)
✔ footer:build (503ms)
✔ remixapp:build (670ms)
—————————————————————————————————————————————————————————————————————————————
> Lerna (powered by Nx) Successfully ran target build for 3 projects (1s)
Now, let's run the tests.
> npx lerna run test
You should see the following output:
✔ footer:test (1s)
✔ header:test (1s)
✔ remixapp:test (236ms)
——————————————————————————————————————————————————————————————————————————————
> Lerna (powered by Nx) Successfully ran target test for 3 projects (1s)
Note, lerna
will run the three test
npm scripts in the topological order as well. Although we had to do it when
building, it isn't necessary for tests (and it also makes the command slower). We can change this behavior by
adding --no-sort
to the command.
> npx lerna run test --no-sort
Running any command right now will execute all the tasks, every time, even when nothing changes. We can fix it by adding a bit of configuration.
First, let's run
> npx nx init
This which will generate a nx.json
at the root of your workspace:
{
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": []
}
}
}
}
Second, let's mark build
and test
to be cacheable operations.
{
"tasksRunnerOptions": {
"default": {
"runner": "nx/tasks-runners/default",
"options": {
"cacheableOperations": ["build", "test"]
}
}
}
}
Now, let's run npx lerna run test --scope=header
twice. The second time the operation will be instant:
> lerna run test --scope=header
> header:test [existing outputs match the cache, left as is]
> header@0.1.0 test
> jest
PASS src/Header.spec.tsx
✓ renders header (12 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.439 s, estimated 1 s
Ran all test suites.
———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
> Lerna (powered by Nx) Successfully ran target test for project header (4ms)
Nx read the output from the cache instead of running the command for 1 out of 1 tasks.
Lerna (powered by Nx) was able to recognize that the same command has already executed against the same relevant code and environment, so instead running it Lerna restored the necessary files and replayed the terminal output.
Most of the time Lerna (powered by Nx) is good at recognizing what files need to be cached and restored. In case of
building the Remix app we need to help it by adding the following section to packages/remixapp/package.json
.
{
"nx": {
"targets": {
"build": {
"outputs": ["{projectRoot}/build", "{projectRoot}/public/build"]
}
}
}
}
Caching not only restores the terminal output logs, but also artifacts that might have been produced.
Run lerna run build
, then remove packages/remixapp/public/build
and run the build command again. You will see all
the files restored from cache and the command executing instantly.
✔ header:build [existing outputs match the cache, left as is]
✔ footer:build [existing outputs match the cache, left as is]
✔ remixapp:build [local cache]
———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
> Lerna (powered by Nx) Successfully ran target build for 3 projects (19ms)
Nx read the output from the cache instead of running the command for 3 out of 3 tasks.
Lerna also supports distributed caching and config-free distributed task execution.
We have made good progress, but there are two problems left to be solved:
- We need to remember to use
--no-sort
when running tests. - We need to remember to build
header
andfooter
before we runlerna run dev --scope=remixapp
.
Both are the symptoms of the same issue: by default, Lerna doesn't know how different targets (npm scripts) relate to
each other. We can fix that by defining dependencies between targets (also often known as task pipelines) in
the nx.json
:
{
...
"targetDefaults": {
"build": {
"dependsOn": [
"^build"
]
},
"dev": {
"dependsOn": [
"^build"
]
}
}
}
Note, older versions of Nx used targetDependencies instead of targetDefaults. Both still work, but targetDefaults is recommended.
With this change:
npx lerna run build
will run the build targets in the right order.npx lerna run dev --scope=remixapp
will run the build targets forheader
andfooter
first and then run the dev target forremixapp
.npx lerna run test
will run all the three test targets in parallel.
If you are wondering whether it is slow to run lerna run dev --scope=remixapp
given that you have to rebuild all the
dependencies all the time, the answer is "no". The dependencies will be rebuilt only when they change. Otherwise,
their dist folders will be kept as is.
Finally, let's talk about the third key Lerna feature: publishing to npm. Lerna comes already with a publish
command
built-in. To publish our packages header
and footer
, all we need to do is to run:
> npx lerna publish --no-private
This will
- determine the current version of the packages
- detect which packages has changed since the last publishing & then update its version in
package.json
accordingly - create a commit of the changed
package.json
files, tag the commit and push the tag & commit to the remote - publish the packages to NPM
Read more about the publishing and versioning process in the corresponding docs page.