Situation sketch
At Showpad, we created quite a few libraries for internal use (a lot of them are Angular libraries). Most libraries have their own repository and get published separately to our private NPM registry. Angular's short release cycle is great for the ecosystem, but it can be challenging to keep all your applications and libraries at the same, latest version. While a major release every 6 months doesn't seem like a lot, keeping up can be a big challenge if you have multiple applications.
Recently, we decided to try moving some libraries to a monorepo to see if it would benefit us. Setting up the repo was not very straightforward and advanced Lerna documentation is pretty scarce (at the time of writing) so I decided to share our set up.
Creating the monorepo
We considered some different monorepo tools and eventually dediced to use Lerna. Lerna is a tool that optimizes the workflow around managing multi-package repositories with git an npm. It's created an used by the people behind the Babel project.
Getting started with Lerna is pretty straight forward; for basic set-ups, the documentation on the website is sufficient. That's also how we started our monorepo:
$ git clone https://github.com/showpad/angular-packages
$ cd angular-packages
$ lerna init
Most of the repositories we wanted to add to the monorepo had a build step; inlining templates and styles and generating an
AOT compiled library. We had a separate package.json
file in our src
folder only declaring
name
, version
and peerDependencies
, which we copied over to the dist folder. Results were published with npm publish dist
,
thus only publishing the
generated files.
Importing those repositories into Lerna like that was a bit of a problem; Lerna does not have a build step, you can't specify a subfolder to be published and there would be quite some duplicated build code.
The problem could be solved by just copying over the src
folder and package.json
file from the original repos
(so not using the lerna import
function), and adding an index.js
and index.d.ts
file which export everything from the src
folder
and then publishing the whole folder (with source files instead of compiled files).
// index.js and index.d.ts
export * from './src';
Publishing TypeScript source files was no problem while using Angular <= 4.x, but when Angular version 5 was released, a stricter version of tsconfig was introduced, which basically disallowed having uncompiled files in your node modules. This Typescript configuration change forced us to add a build step again.
Adding a build step
It took us a while to figure how to do it, but apparently Lerna has pre
and post
publish scripts. It's not very well documented.
This was the only mention we could find about it.
The word synchronous was also not very obvious in that comment.
By adding a script named prepublish.js
(or postpublish.js
) in the script
folder of any package, you can run synchronous
scripts.
We ended up extracting the code for executing the script and showing some info into a file in the root directory,
and adding a small script/prepublish.js
file into every package that needs to be built:
// ./packages/package-name/scripts/prepublish.js
const path = require('path');
const prePublish = require('../../../bundling/prepublish');
prePublish('search', path.join(__dirname, '../'));
// ./bundling/prepublish.js
const ora = require('ora');
const timeout = 1000 * 60 * 2;
const buildCommand = `npm run build`;
function prePublish(packageAlias, packagePath) {
const spinner = ora({
text: `Building "${packageAlias}" library`,
spinner: { interval: 0, frames: ['…'] },
}).start();
try {
const result = require('child_process').execSync(buildCommand, {
timeout,
cwd: packagePath,
});
} catch (error) {
spinner.fail(`Could not finish building "${packageAlias}" library`);
console.log(error.stdout.toString());
process.exit();
}
spinner.succeed(`Finished building "${packageAlias}" library`);
}
module.exports = prePublish;
The bundling script runs the npm build script in the specified folder synchronously (with a timeout) and
shows some information in the terminal so we can see what step the lerna publish
command is in.
We're using ora for that, just for the ease of use. We replaced the default spinner
with a static one, as it does not really spins because the command is ran synchronously.
The npm build script runs a Gulp script that inlines the templates and styles and then runs ngc
on the generated files.
Controlling the package contents
The last part of adding the build step was controlling the contents of the npm published package. We don't want to ship the
source and script folders.
You can do this by specifying the files
in the package.json
. This is how a package.json
file looks in most of our packages:
// package.json
{
"name": "@showpad/ng-package-name",
"version": "x.x.x",
"description": "package description",
"scripts": {
"lint": "tslint -c tslint.json './src/**/*.ts'",
"unit": "karma start karma.conf.js",
"test": "npm run lint && npm run unit",
"build": "gulp build:esm"
},
"main": "index.js",
"files": ["index.js", "index.d.ts", "dist", "README"],
"peerDendencies": {
// required deps for the package
},
"devDependencies": {
// required deps for development
},
"publishConfig": {
"registry": "url to showpad npm repository"
}
}
Of course we also had to update the index.js
file to point to the dist
folder.
// index.js and index.d.ts
export * from './dist';
Conclusion
While it was not very obvious how to move multiple existing repositories including a build step into a monorepo, we were able to figure it out with some digging in the code and quite some trial and error.
Hopefully it might help you out as well and feel free to point out any improvements to our set-up.
Special thanks
A special thanks to the people reviewing this post:
- Brecht Billiet @brechtbilliet
- Thibaut Nguyen @teebot