Using Yarn Constraints
Yarn 2 (aka berry) introduced a new feature referred to as “constraints” which allow developers to enforce rules for dependencies or manifest fields for some or all workspaces in a project. While constraints are still experimental and community examples of using them are a bit sparse, they are a very powerful tool to help you more easily manage your project.
What are Yarn constraints?
As previously mentioned, Yarn constraints allow you to enforce rules for your dependencies or manifest fields in your project. While they could be used for a project with a single workspace, their value comes in when used on projects with multiple workspaces such as a component library. Constraints are written in Prolog which, although it has a learning curve, allows writing powerful rules in just a few short lines.
Let’s see an example
Alright, let’s get down to business and look at an example of a constraint.
For the rest of this article, let’s assume that we are working on an
open-source component library containing 10 packages. This would thus have
11 total “workspaces” as each package is a workspace plus the root
workspace which contains the workspaces
key in the package.json file.
Since our example project is open source, we want to make sure all of our
packages have a license
field in their manifest files (package.json).
This would look something like this.
gen_enforced_field(WorkspaceCwd, 'license', 'ISC').
All the examples in this article can be viewed on the accompanying GitHub repo I created for this article.
This will instruct Yarn that we want to enforce a constraint on all of our
workspaces (including the root) where each workspace should contain a
license
field with the value of ISC
. We can check to see if our
constraints are met by running the yarn constraints
command.
$ yarn constraints
➤ YN0037: package-a must have a field license set to "ISC", but doesn't
➤ YN0038: package-b must have a field license set to "ISC", but is set to "MIT" instead
➤ YN0000: Failed with errors in 0s 29ms
As you can see from the output, Yarn warned us that package-a does not have
a license
field in it’s manifest and package-b has an incorrect value for
the license
field. To fix these issues, we can add the --fix
argument
to the command which will instruct Yarn to fix the issues for us if
possible.
$ yarn constraints --fix
➤ YN0000: Done in 0s 43ms
The errors are now gone, and if we look at the changed files in Git, we will see that the manifest file for package-a and package-b have been updated.
Now that we’ve seen what Yarn constraints look like and how to check/fix them, let’s look at a number of real world use cases where you might want to use constraints to enforce rules for your workspaces.
Use case: Enforce homepage URL
Continuing with our hypothetical component library, we probably want to
include a homepage
field in the manifest of each workspace so that users
can use the npm docs
command to open the readme for each of our packages.
This constraint isn’t quite as simple as the last since the homepage of
each workspace will be different.
When writing more complex constraints like this, I like to start with a basic constraint and then add the complexity bit by bit. So, I’ll start with something similar to our license constraint.
gen_enforced_field(WorkspaceCwd, 'homepage', 'something').
Okay, good start. Now I have a constraint that will add a homepage
field
to each workspace. There are two problems that I’ll need to address:
- This constraint will also be enforced for the root workspace which we don’t want.
- The homepage URL isn’t correct.
Let’s start with the first issue. We need to instruct Yarn not to enforce the constraint for the root workspace. There are a number of ways you could do this, but the approach I like to take looks something like this.
gen_enforced_field(WorkspaceCwd, 'homepage', 'something') :-
workspace_field(WorkspaceCwd, 'version', _).
Aside about syntax
Before I explain the code block above, I should pause for a moment to explain some of the syntax in the previous code block and the mental model you should use when reading and writing Yarn constraints.
In Prolog, we use “rules” to create conditional statements that will either
be true or false. Rules contain a “head” section, and a “body” section
where the head will be true if the body is true. For example, the following
rule would be read as “X is a mortal if X is human” with mortal(X)
as the
“head” and human(X)
as the “body”.
mortal(X) :- human(X).
When writing Yarn constraints, we create rules which when true instruct Yarn to enforce the constraint for the given workspace. This does not necessarily mean that the constraint is met, simply that Yarn should enforce it when we run yarn constraints. This point is important to remember.
The rules we create don’t indicate whether constraint has been met, rather it indicates that the constraint should be enforced.
The last thing to note about Prolog syntax is that rules without a body are always true. Prolog calls these rules “facts” and our previous example of enforcing the license field is an example of a fact. Facts are just syntactic sugar meaning that the following two lines give the same result.
gen_enforced_field(WorkspaceCwd, 'license', 'ISC') :- true.
gen_enforced_field(WorkspaceCwd, 'license', 'ISC').
Enough syntax, back to work
Okay, back to our homepage constraint example. We left off with the
following block of code, but I didn’t explain why I added the
workspace_field
portion to the body of the rule.
gen_enforced_field(WorkspaceCwd, 'homepage', 'something') :-
workspace_field(WorkspaceCwd, 'version', _).
This change resolved the first of the two issues where the constraint was
being enforced for the root workspace. By using the workspace_field
rule
provided by Yarn, our rule will now return true for all workspaces whose
manifest has a version
property set to any value (underscores in Prolog
indicate any value). Since our root workspace does not have a version
property in its manifest, the constraint will not be enforced.
Next, we need to set the correct homepage URL and to do that, we can change our constraint to the following.
gen_enforced_field(WorkspaceCwd, 'homepage', Homepage) :-
workspace_field(WorkspaceCwd, 'version', _),
atom_concat('https://github.com/mskelton/yarn-constraints-example/tree/main/', WorkspaceCwd, Homepage).
To get the correct homepage URL for each workspace, we use Prolog’s
built-in atom_concat
predicate to concatenate the GitHub URL and the
workspace directory into a variable called Homepage
. We then can use this
variable as the third argument to gen_enforced_field
so Yarn will enforce
the correct homepage URL in each workspace. If we check our constraints, we
will likely see something like this.
$ yarn constraints
➤ YN0037: package-a must have a field homepage set to "https://github.com/mskelton/yarn-constraints-example/tree/main/packages/package-a", but doesn't
➤ YN0037: package-b must have a field homepage set to "https://github.com/mskelton/yarn-constraints-example/tree/main/packages/package-b", but doesn't
➤ YN0000: Failed with errors in 0s 22ms
This constraint is also auto-fixable when you run
yarn constraints --fix
.
Use case: Ensuring a dependency is a peer dependency
So, we’ve been using the example of a component library to demonstrate
constraints, so let’s extend the example to say that we are building a
React component library. When building React component packages, the
react
dependency should be a peer dependency, not a regular dependency.
This is a perfect opportunity to add a constraint to enforce this rule in
all of our packages. This constraint would look like this.
gen_enforced_dependency(WorkspaceCwd, 'react', null, dependencies) :-
workspace_field(WorkspaceCwd, 'version', _).
As you can see, instead of creating a gen_enforced_field
rule, we are
creating a gen_enforced_dependency
rule. Similar to our previous rules,
we only want to run this for published packages, so we ignore the root
workspace using our version check trick. For the workspaces where we
enforce this constraint, Yarn will ensure the react
is not present in the
dependencies
block of the manifest. If it is, we will get the following
error.
$ yarn constraints
➤ YN0025: package-a has an extraneous dependency on react (in dependencies)
➤ YN0000: Failed with errors in 0s 20ms
As we can see in the message, our newly created constraint prevents us from
using react
as a regular dependency.
Wrapping it up
That was a lot for a single article! I hope this article has helped you to understand what constraints are, when you might want to use them, and how to build them. There is definitely a learning curve to constraints, but my best advice is to just try it out for a while and see what you come up with! Whether you pull inspiration from the constraints I wrote here or come up with your own ideas of what constraints to create, I encourage you to give constraints a try on one of your projects.
Goodbye and God bless!