Skip to main content

Constraints

This page documents the new JavaScript-based constraints. The older constraints, based on Prolog, are still supported but should be considered deprecated. Their documentation can be found here.

Overview

Constraints are a solution to a very basic need: you have a lot of workspaces, and you need to make sure they use the same version of their dependencies. Or that they don't depend on a specific package. Or that they use a specific type of dependency. Whatever is the exact logic, your goal is the same: you want to automatically enforce some kind of rule across all your workspaces. That's exactly what constraints are for.

What can we enforce?

Our constraint engine currently supports two main targets:

  • Workspace dependencies
  • Arbitrary package.json fields

It currently doesn't support the following, but might in the future (PRs welcome!):

  • Transitive dependencies
  • Project structure

Creating a constraint

Constraints are created by adding a yarn.config.cjs file at the root of your project (repository). This file should export an object with a constraints method. This method will be called by the constraints engine, and must define the rules to enforce on the project, using the provided API.

For example, the following yarn.config.cjs will enforce that all react dependencies are set to 18.0.0.

module.exports = {
  async constraints({Yarn}) {
    for (const dep of Yarn.dependencies({ ident: 'react' })) {
      dep.update(`18.0.0`);
    }
  },
};

And the following will enforce that the engines.node field is properly set in all workspaces:

module.exports = {
  async constraints({Yarn}) {
    for (const workspace of Yarn.workspaces()) {
      workspace.set('engines.node', `20.0.0`);
    }
  },
};

Declarative model

As much as possible, constraints are defined using a declarative model: you declare what the expected state should be, and Yarn checks whether it matches the reality or not. If it doesn't, Yarn will either throw an error (when calling yarn constraints without arguments), or attempt to automatically fix the issue (when calling yarn constraints --fix).

Because of this declarative model, you don't need to check the actual values yourself. For instance, the if condition here is extraneous and should be removed:

module.exports = {
  async constraints({Yarn}) {
    for (const dep of Yarn.dependencies({ ident: 'ts-node' })) {
      // No need to check for the actual value! Just always call `update`.
      if (dep.range !== `18.0.0`) {
        dep.update(`18.0.0`);
      }
    }
  },
};

TypeScript support

Yarn ships types that make it easier to write constraints. To use them, add the dependency to your project:

$ yarn add @yarnpkg/types

Then, in your yarn.config.cjs file, import the types, in particular the defineConfig function which automatically type the configuration methods:

/** @type {import('@yarnpkg/types')} */
const { defineConfig } = require('@yarnpkg/types');

module.exports = defineConfig({
  async constraints({Yarn}) {
    // `Yarn` is now well-typed ✨
  },
});

You can also retrieve the types manually, which can be useful if you extract some rules into helper functions:

/** @param {import('@yarnpkg/types').Yarn.Constraints.Workspace} dependency */
function expectMyCustomRule(dependency) {
  // ...
}

You can alias the types to make them a little easier to use:

/**
 * @typedef {import('@yarnpkg/types').Yarn.Constraints.Workspace} Workspace
 * @typedef {import('@yarnpkg/types').Yarn.Constraints.Dependency} Dependency
 */

/** @param {Workspace} dependency */
function expectMyCustomRule(dependency) {
  // ...
}

Putting it all together

This section regroups a couple of constraint examples. We are thinking to provide some of them as builtin helpers later on, although they tend to often contain some logic unique to each team / company.

Restrict dependencies between workspaces

This code ensures that no two workspaces in your project can list the same packages in their dependencies or devDependencies fields but with different associated references.

// @ts-check

/** @type {import('@yarnpkg/types')} */
const {defineConfig} = require(`@yarnpkg/types`);

/**
 * This rule will enforce that a workspace MUST depend on the same version of
 * a dependency as the one used by the other workspaces.
 * 
 * @param {Context} context
 */
function enforceConsistentDependenciesAcrossTheProject({Yarn}) {
  for (const dependency of Yarn.dependencies()) {
    if (dependency.type === `peerDependencies`)
      continue;

    for (const otherDependency of Yarn.dependencies({ident: dependency.ident})) {
      if (otherDependency.type === `peerDependencies`)
        continue;

      dependency.update(otherDependency.range);
    }
  }
}

module.exports = defineConfig({
  constraints: async ctx => {
    enforceConsistentDependenciesAcrossTheProject(ctx);
  },
});