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.

constraints.pro
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.

constraints.pro
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:

  1. This constraint will also be enforced for the root workspace which we don’t want.
  2. 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.

constraints.pro
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”.

constraints.pro
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.

constraints.pro
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.

constraints.pro
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.

constraints.pro
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.

constraints.pro
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!