Mastering Yarn's lifecycle with hooks
Yarn is a powerful package manager for Node.js projects that comes with a rich set of features, including support for plugins (starting with Yarn 2.x).
As your project grows in size and complexity, you might find that you need to extend the functionality of Yarn to cater to your specific requirements. This is where custom Yarn plugins can help you. By writing a custom Yarn plugin, you can add new commands, modify existing ones, and integrate external tools and services. In this blog post, we'll take a look at the process of creating custom Yarn plugins.
Whether you are a beginner or an experienced developer, this post will equip you with the knowledge you need to create your own Yarn plugins and elevate your package management game.
Imagine you need to ensure that all contributors to your project have properly set up their local environment before
they can make any changes. This is commonly referred to as a "project doctor". In this case, you need to check whether
they have configured the maximum Node memory by setting up --max-old-space-size
in the NODE_OPTIONS
environment
variable.
To achieve this, we will create a custom yarn hook that runs every time a developer runs yarn install
and validates
that the environment is correctly set up.
Register the plugin
First, let's start by telling Yarn the location of our plugin. Open .yarnrc.yml
and add a new entry:
plugins:
- ./my-plugin.js
Plugins skeleton
This is the basic set up of an empty plugin. Save this as my-plugin.js
:
module.exports = {
name: 'my-plugin',
factory: () => {
return {
hooks: {},
};
},
};
hooks
is an object that allows us to hook into the Yarn lifecycle steps. The
official documentation is not very comprehensive, but is
a good starting point to know what hooks are available. Checking the
source code of the Hooks implementation
may be helpful to have a better understanding of the API.
Validating a project
There is a hook called validateProject
that does exactly what we need: runs before the install
method and checks
that the project is valid. Because it runs before install
, it means we can't rely on any external dependency being
installed. Yarn provides a bundler to overcome
this limitation, but for our example we don't need to worry about it.
Let's start with our project validation. Change my-plugin.js
to:
module.exports = {
name: 'yarn-doctor',
factory: () => {
return {
hooks: {
validateProject(project, report) {
console.log("Hello world");
}
},
};
},
};
Now, when you run yarn install
, you should see the plugin in action:
$ yarn install
Hello world
➤ YN0000: ┌ Resolution step
➤ YN0000: └ Completed
➤ YN0000: ┌ Fetch step
➤ YN0000: └ Completed
➤ YN0000: ┌ Link step
➤ YN0000: └ Completed
➤ YN0000: Done in 0s 31ms
Doing the actual check
We are almost there. All we need to do is change our hook implementation to check the value of NODE_OPTIONS
, and
report an error if it doesn't match our expectations. Change the implementation to:
module.exports = {
name: `yarn-doctor`,
factory: () => {
return {
hooks: {
validateProject(project, report) {
if (!process.env.NODE_OPTIONS) {
// reportError is specified in
// https://github.com/yarnpkg/berry/blob/master/packages/yarnpkg-core/sources/Plugin.ts#L144
//
// After some trial an error, I found that MessageName is _mandatory_ and a it must be a Number.
// It will be used to generate an error code like YN0099. As far as I know there is no
// recommendation about what error codes we should use for our custom errors.
report.reportError(99, "NODE_OPTIONS is not set");
return;
}
const match = process.env.NODE_OPTIONS.match( /--max-old-space-size=([0-9]+)/ );
if (!match) {
report.reportError(99, "max-old-space-size not set");
}
},
},
};
},
};
See it in action
We can run a few commands to verify it works.
This is what happens when NODE_OPTIONS is not set:
$ NODE_OPTIONS="" yarn install
➤ YN0000: ┌ Project validation
➤ YN0099: │ NODE_OPTIONS is not set
➤ YN0000: └ Completed
...
➤ YN0000: Failed with errors in 0s 30ms
And this is the error when set to an invalid value
$ NODE_OPTIONS="foo" yarn install
➤ YN0000: ┌ Project validation
➤ YN0099: │ max-old-space-size not set
➤ YN0000: └ Completed
...
➤ YN0000: Failed with errors in 0s 30ms
And finally, this is what happens when the env var is correctly set:
# Simulate that NODE_OPTIONS is not set
$ NODE_OPTIONS="--max-old-space-size=1024" yarn install
...
➤ YN0000: Done in 0s 31ms
Taking it from here
This example of checking an environment variable is just a small glimpse into the potential of Yarn hooks. For medium-sized or larger teams, this concept of a "project doctor" can be taken even further to validate various aspects of contributors' environments, such as environment variables, IDE configurations, authentication credentials, network settings, and more.
In general, tapping into the Yarn lifecycle has tremendous potential to improve the developer experience for your contributors. Just by browsing the list of available hooks, it's easy to imagine many use cases that can help your developers:
- Use the
afterWorkspaceDependencyAddition
hook to ensure new dependencies meet your standards. - Use
cleanGlobalArtifacts
to clear up other caches besides Yarn's. - Use
fetchPackageInfo
to enhance package information with known vulnerabilities. - Use
getNpmAuthenticationHeader
to ensure devs can access your internal NPM registry. - Use
setupScriptEnvironment
to set up environment variables before running any npm script. - Use
wrapScriptExecution
to monitor which scripts are run by your contributors.
With the flexibility and power of Yarn hooks, the possibilities are endless.