Using Lerna to Manage a JavaScript Monorepo: Workflow, Configuration, and Best Practices
Using Lerna, the article shows how to convert a multi‑package JavaScript project into a production‑ready monorepo by initializing Lerna, hoisting dependencies, automating versioning and publishing, integrating commit‑linting, code‑style tools, Babel builds, and VSCode debugging for streamlined development.
For developers who maintain multiple npm packages, a common dilemma is whether to keep all packages in a single repository or to manage each in its own repository. This article demonstrates, through a concrete example, how to use Lerna to manage multiple packages, integrate it with other tools, and establish an efficient, production‑ready workflow.
Background
The project maintains a CLI that is published to npm. The repository contains three sub‑folders (pkg‑a, pkg‑b, pkg‑main) each representing a package. The build and release process involves compiling each package with webpack, Babel, and UglifyJS, then copying the compiled files into a final pkg‑npm folder for publishing.
Pain points
Debugging is difficult because the final bundle is assembled from compressed files.
Package dependencies are unclear; pkg‑a and pkg‑b have no versioning, while pkg‑main’s package.json is copied into pkg‑npm and still depends on pkg‑a and pkg‑b.
Redundant dependencies: each package installs its own copy of Babel, webpack, etc.
Version numbers must be updated manually for the combined package.
No CHANGELOG.md because pkg‑a and pkg‑b lack proper version management.
These issues indicate that the project is effectively a monorepo that has not been managed as such.
Monorepo vs. Multirepo
A monorepo (monolithic repository) stores all related packages in a single repository, allowing each package to be published independently. A multirepo uses a separate repository for each package. Monorepos enable unified tooling, easier cross‑package debugging, and reduced coordination overhead.
Lerna Overview
Lerna is a widely adopted tool for managing JavaScript projects with multiple packages. It optimizes workflows around git and npm, handling inter‑package dependencies and automating versioning and publishing.
Installation
npm i -g lernaProject Initialization
lerna initThe generated package.json and lerna.json look like:
{
"name": "root",
"private": true,
"devDependencies": {
"lerna": "^3.15.0"
}
}
{
"packages": ["packages/*"],
"version": "0.0.0"
}Adding Packages
lerna create @mo-demo/cli lerna create @mo-demo/cli-shared-utilsAdding Dependencies
lerna add chalk // add to all packages lerna add semver --scope @mo-demo/cli-shared-utils // add to a specific package lerna add @mo-demo/cli-shared-utils --scope @mo-demo/cli // internal package dependencyPublishing
lerna publishLerna will prompt for the version (e.g., 0.0.1‑alpha.0 ) and handle version bumps, tag creation, and npm publishing.
Bootstrap with Hoisting
To avoid duplicated node_modules across packages, use hoisting:
lerna bootstrap --hoistHoisting can be made default by adding to lerna.json :
{
"packages": ["packages/*"],
"command": {
"bootstrap": {"hoist": true},
"version": {"conventionalCommits": true}
},
"ignoreChanges": ["**/*.md"],
"version": "0.0.1-alpha.1"
}Commit Workflow
Use commitizen and cz-lerna-changelog to enforce conventional commit messages:
npm i -D commitizen cz-lerna-changelogConfigure in package.json :
{
"scripts": {"c": "git-cz"},
"config": {"commitizen": {"path": "./node_modules/cz-lerna-changelog"}}
}Validate commits with commitlint and husky :
npm i -D @commitlint/cli @commitlint/config-conventional husky // commitlint.config.js
module.exports = {extends: ['@commitlint/config-conventional']}; // package.json snippet
"husky": {"hooks": {"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"}}Enforce code style with standardjs and lint‑staged :
npm i -D standard lint-staged // package.json snippet
"lint-staged": {"*.js": ["standard --fix", "git add"]},
"husky": {"hooks": {"pre-commit": "lint-staged"}}Babel Configuration
// babel.config.js
module.exports = function (api) {
api.cache(true);
const presets = [
['@babel/env', {targets: {node: '8.9'}}]
];
if (!process.env['LOCAL_DEBUG']) {
presets.push(['minify']);
}
return {presets, plugins: [], ignore: ['node_modules']};
};Package Scripts
{
"scripts": {
"c": "git-cz",
"i": "lerna bootstrap",
"u": "lerna clean",
"p": "npm run b && lerna publish",
"b": "lerna exec -- babel src -d dist --config-file ../../babel.config.js"
}
}Debugging
Use VSCode launch configuration to debug the CLI in source mode:
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [{
"type": "node",
"request": "launch",
"name": "debug cli",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/babel-node",
"runtimeArgs": ["${workspaceRoot}/packages/cli/src/index.js"],
"env": {"LOCAL_DEBUG": "true"},
"console": "integratedTerminal"
}]
}With the LOCAL_DEBUG flag, the entry files load the source code ( src ) instead of the compiled dist , enabling step‑by‑step debugging across package boundaries.
Conclusion
The described setup provides a complete, production‑ready monorepo workflow using Lerna, covering package management, unified build configuration, automated versioning, conventional commits, code style enforcement, and seamless debugging. It dramatically improves development efficiency and reduces maintenance overhead.
vivo Internet Technology
Sharing practical vivo Internet technology insights and salon events, plus the latest industry news and hot conferences.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.