Why can pnpm be used to build a useful monorepo (more efficient than yarn/lerna) at the speed of light

preface

First of all, the combination of yarn + lerna is now the general scheme of monorepo, and it is also the most functional and popular scheme. It is absolutely correct to use this scheme.

However, there is a certain threshold for it to get started. Compared with pnpm, which has its own workspace, it is not comparable in efficiency.

problem

Here are some reasons why we don't use the yarn + lerna scheme.

Recognize pnpm's small and simple

As a novel dependency management tool, pnpm is very weak in terms of compatibility, functional richness and community ecology. No matter what new card it plays, it is difficult to compete with the dominant position of yarn. As a "small" tool, pnpm also has some convenience and high efficiency.

lerna functionality is often not required

A monorepo management tool like lerna includes many functions.

For example, many times we don't need unified version release management. For a large open source library like vue, it needs unified version release management for all sub packages, but we don't need it personally. Therefore, commands like lerna version are actually redundant. Why not abandon complexity and simplify?

Complex configuration of yarn + lerna

For a newcomer to monorepo, it's enough to eat a pot just to find out the difference between using lerna alone and yarn + workspace + lerna.

Specifically, first of all, you need such a large lerna json:

// lerna.json
{
  "packages": [
    "packages/*"
  ],
  "npmClient": "yarn",
  "useWorkspaces": true,
  "command": {
    "bootstrap": {
      "hoist": true
    }
  },
  "version": "0.0.0"
}

The standard management tools like yarn let me configure and use them manually. In order to improve efficiency, I have to manually open dependency. Without opening workspace, the significance of using yarn will be lost. Without mentioning other custom configurations, these are necessary. In a series, it is really a complex template configuration!

Not only that, but also in package JSON clearly indicates the workspace location:

// package.json append
{
  "workspaces": [
    "packages/*"
  ]
}

For novices, these configurations need to receive too much information. What is a yarn workspace? Why promote dependence? packages / * should be written in two places 😅 , Directly persuaded him to retreat.

Establishing monorepo using pnpm speed of light

Driven by various problems, we realize that we need a simple and efficient monorepo management tool. Don't boast that pnpm installation depends on soft links, so it's fast and saves space, which is not the focus.

Let's look at how pnpm establishes monorepo at the speed of light.

I Create workspace

First, create a pnpm workspcae configuration file pnpm workspace in the project root directory yaml ( Official description ), which means that this is a monorepo:

packages:
  # all packages in subdirs of packages/ and components/
  - 'packages/**'
  # exclude packages that are inside test directories
  - '!**/test/**'

In one step, you can avoid all the troubles of yarn + lerna + workspace above. The specific workspace configuration is easy to use by default. If you need to customize, you can view the official configuration document: pnpm Workspace

II Clean up dependencies

If you are a react technology stack, is it too cumbersome and takes up time and space to install react for each project?

The scheme in yarn + lerna is to configure automatic lifting. This scheme will have the problem of dependency abuse, because there are no restrictions after lifting to the top layer, and a dependency may be loaded to any node on the top layer_ The existence of modules depends on whether it really depends on the package and the version of the package. However, in pnpm, whether it is elevated or not, there is a default isolation policy, which is safe.

Therefore, it is recommended to remove the common dependencies of each sub project, such as react and lodash, and then put them into the top-level package JSON:

The commands for installing global dependencies at the top level are as follows:

	pnpm add -w lodash
	pnpm add -D -w typescript

By default, in order to save time and space, pnpm will not install the global dependencies installed at the top level of the soft link in the subproject by default. If you need to turn off this behavior (not recommended), you can install them in the root directory of the project npmrc configuration:

shared-workspace-lockfile=false
III Set start command

Suppose we have two react subprojects, namely @ mono/app1 and @ mono/app2 (here refers to the name in package.json of the subproject):

Then in the project root directory package JSON configuration item startup command:

"scripts": {
    "dev:app1": "pnpm start --filter \"@mono/app1\"",
    "dev:app2": "pnpm start --filter \"@mono/app2\""
},

Here -- filter parameter is the specific sub item to be used. See the following for details: pnpm filtering

So far, all the preparations have been completed, and you can start using monorepo happily!

Enjoy using monorepo

We set a small goal:

The typescript code of app2 can be used in app1, and can be hot updated in real time. The code of app2 needs to be packaged without changing once.

Provide source

In app2, we create a component package that needs share, packages / app2 / SRC / shared / index tsx:

import React, { FC } from 'react'

// Use ts specific syntax to verify whether we have really translated
export enum EConstans {
    value = 'value'
}

export const Button: FC = () => {
  return <div>app2 button: {EConstans.value}</div>
}

After that, the package of app2 is directly The JSON entry main is configured here:

// packages/app2/package.json add
{
	"main": "./src/shared/index.tsx"
}

As a result, when other subprojects reference the package (@ mono/app2), they will directly import code from the main field.

Reference code

In the package. Of app1 The JSON dependency list indicates that app2 of workspace is referenced:

// packages/app1/package.json add
{
  "dependencies": {
    "@mono/app2": "workspace:*"
  }
}

This is the unique writing method of pnpm. To indicate that this is the dependency of the work area and prevent confusion caused by automatic search on npm, see: pnpm Workspace protocol

Then run the command in the project root directory to re chain the dependencies:

	pnpm i

After that, you can happily use the code of app2 in app1:

// packages/app1/src/App.tsx

import React from 'react';
import { Button } from '@mono/app2'

function App() {
  return (
    <div>
      <Button />
    </div>
  );
}

export default App;
Configure cross project compilation

One key is that app2 also uses the code that needs to be escaped (here is typescript), while the default configuration of Babel loader does not include it (only includes the src directory of its own project). Therefore, although the code of app2 can be referenced at this time, it cannot be used directly because it is ts code that is not escaped.

In order to solve this problem, we need to modify the construction configuration of the project. There are many methods on how to modify the configuration, which are not within the scope of this article. Here are only examples of craco:

// packages/app1/craco.config.js

const path = require('path')

module.exports = {
  webpack: {
    configure: (webpackConfig, { env, paths }) => {
      webpackConfig.module.rules.forEach(rule => {
        if (!rule.oneOf) {
          return
        }
        // Find Babel loader at this location 
        // Don't ask me why I know that Babel loader is here. You can find a new project eject or write it out and see the json file
        rule.oneOf.forEach(ruleItem => {
          const isBabelLoader = ruleItem.test?.source?.includes?.('ts')
          if (isBabelLoader) {
            ruleItem.include = [
              ...getAllWorkspaceDepPaths(),
              ruleItem.include
            ]
          }
        })
      })

      return webpackConfig
    },
  },
}

/**
 * This method can dynamically obtain all packages Other subprojects referenced in JSON
 */
function getAllWorkspaceDepPaths() {

  const SCOPE_PREFIX = '@mono'

  const pkg = require('./package.json')
  const depsObj = pkg.dependencies
  if (!depsObj) {
    return []
  }
  const depPaths = []
  Object.entries(depsObj).forEach(([name, version]) => {
    if (name.startsWith(SCOPE_PREFIX) && version.startsWith('workspace:')) {
      depPaths.push(path.resolve(`../${name.slice(SCOPE_PREFIX.length + 1)}/src`))
    }
  })
  return depPaths
}

If you still don't understand what you've done, you're actually putting the package Find the absolute paths of all other subprojects starting with @ mono referenced in JSON and put them in the include of babel loader, so that babel will escape the ts code of other subprojects referenced.

So far, our small goal has been successfully completed. At this time, the code changed in app2 can also be hot updated and reflected in app1 (only app1 is started at this time).

summary

There is really no need to blow more about the advantages of pnpm. The performance of modern computers is very surplus. These soft links and isolation are virtual in order to save time and space.

But there is no doubt that pnpm's natural advantages enable it to support workspace monorepo simply and efficiently, and it is very friendly to novices. Especially when sharing code among multiple projects, there is no need to separate the code.

Pnpm is not a silver bullet, and any new thing must be viewed from many aspects. In particular, pnpm's unique lock file does not have two-way compatibility with yarn, while yarn has two-way compatibility with npm. Yarn has been tested all over the world, but pnpm does not. In the future, due to the needs of some requirements and functions, architecture changes must occur, and migration will cause irreversible technical debt, Please think twice before using pnpm.

Keywords: Web Development npm Yarn

Added by nick1 on Thu, 20 Jan 2022 18:35:04 +0200