Configuring pnpm peer dependencies and hoisting

This lesson preview is part of the Bundling and Automation in Monorepos course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.

This video is available to students only
Unlock This Course

Get unlimited access to Bundling and Automation in Monorepos, plus 90+ \newline books, guides and courses with the \newline Pro subscription.

Thumbnail for the \newline course Bundling and Automation in Monorepos
  • [00:00 - 00:23] pnpm has some surprising behavior in how it handles peer dependencies, and it also treats ESLint and Prettier plugin packages with a special case. We're going to configure pnpm to remove its special casing, as well as have a default behavior that is the least surprising and most transparent.

    [00:24 - 00:32] We're going to start with an example of the default pnpm behavior. To do this, we're going to install a package called eslint-plugin-prettier.

    [00:33 - 00:50] And we can immediately see weird behavior, because even though we just added a single package, pnpm ended up installing over 90 packages. If we inspect our node_modules folder, we can see that 16 packages have been made available at the top level.

    [00:51 - 00:59] This means that those packages can be directly required by our monorepo code. The reason for that is twofold.

    [01:00 - 01:17] One is to make those packages like Prettier and ESLint available as command-line tools so you can run Prettier or ESLint. And the other is to deal with limitations of how ESLint's old config format, as well as Prettier's old config format, load plugins.

    [01:18 - 01:33] So the first thing we're going to tackle is automatic peer dependency installation. I have prepared a small table that shows the different behavior of package managers and of package manager versions.

    [01:34 - 02:01] As you can see, the behavior has changed over time. For example, pnpm version one, two and three automatically installed peer dependencies four, five, and six didn't automatically install peer dependencies, and now seven, eight, and nine again automatically install peer dependencies. Yarn, on the other hand, never installed peer dependencies automatically and pnpm changed behavior in version eight.

    [02:02 - 02:20] Before version eight, it would not install peer dependencies automatically, and now version eight and nine do install them automatically. Let's look at the documentation for pnpm's .npmrc file, which configures the behavior of pnpm.

    [02:21 - 02:36] The configuration option that we're interested in is called auto-install-peers. We can change this in our project and force the behavior of pnpm even if a future version decides to change the default behavior.

    [02:37 - 02:49] We're going to set it to false. And to do that, let's use echo, and we can just echo "auto-install-peers=false" into a file called .npmrc.

    [02:50 - 03:01] Now when we run pnpm install, it's going to remove all of these extra dependencies that we didn't expect. But it's also going to give us an error that we're missing some peer dependencies.

    [03:02 - 03:09] And that's a good thing. We want to be explicit in all dependencies that are being used in our monorepo.

    [03:10 - 03:23] So we don't want the old behavior where packages would be installed opaquely. Where we would get like a version of Prettier or a version of ESLint installed just because we have a dependency on a plugin.

    [03:24 - 03:36] If you look at our node_moduless right now, we're going to see our one expected dependency plus one extra. The extra one is again pnpm trying to be helpful, and we're going to fix that separately.

    [03:37 - 03:49] But first let's re-add ESLint and Prettier as dependencies to our project. Now this gave us ESLint version nine, and we actually want to be using version eight for its better compatibility right now.

    [03:50 - 03:58] So let's just set that and let's look at our package.json file. Because we used the --save-exact option.

    [03:59 - 04:25] Our version specification is exactly Prettier 3.3.3 and ESLint 8.57.0. I highly recommend using exact specifications for packages that have wide impact on the repo. So anything like TypeScript, ESLint or React versions and Next.js versions should be always an exact specification.

    [04:26 - 04:41] And if we look at our node_modules right now, it's much more reasonable than it used to be, but it still has more packages than what we expected. Why is that? The reason is again an option of pnpm.

    [04:42 - 05:02] By default, pnpm will try to fix bugs in Prettier and ESLint plugins. The way that it does that is by forcing those to be hoisted and requirable to the whole project, and that's specified with the public-hoist-pattern option.

    [05:03 - 05:15] It has default value of any package that has ESLint or Prettier in its name. It's worth noting that this applies to dependencies of dependencies.

    [05:16 - 05:25] So if we have a plugin that requires another plugin that matches this pattern, everything would get hoisted up. The fix for this behavior is straightforward enough.

    [05:26 - 05:44] We're going to set public hoist pattern in .npmrc, and we're going to just set it to an empty array. When we run pnpm install, because of the changes we made to hoisting patterns, it's going to ask us to recreate our root node_modules folder.

    [05:45 - 05:52] So let's proceed. And let's look at what it looks like now. Now that's what we expected.

    [05:53 - 06:01] We only have the packages that we have specified and nothing extra. And let me just bring up package.json and our .npmrc.

    [06:02 - 06:28] As you can see now we don't have any surprises. Everything that we have in node_moduless is exactly what we have defined as our dependencies. We don't have automatic packages being installed, and we don't have pnpm trying to be helpful and hoist packages for us that are problematic in older versions of ESLint or Prettier.