Some time ago I tried to combine several small projects, but the results were not satisfactory. Recently I read an article about Monorepo and finally realized what my problem was. This article takes the opportunity to talk about monorepo’s simple practices and share some common practices based on big front-end code management.

Monorepo

Mono- means monomer, so monorepo refers to monomer repository management, which is generally a Git repository containing the source code for all applications in a project.

Monorepo projects are usually composed of multiple apps, such as the web, mobile, small programs and other apps together; Or break complex product lines into microservices, but still share the same code repository.

Mono-repo vs. Multi-repo

In addition to monorepo, there is a term in large project management called multirepo. As the name suggests, the former is a single warehouse; The latter is multi-warehousing — dividing applications by module into different code repositories. Colloquially, they are the difference between a single Git repo and multiple Git Repo.

Let’s take a look at how Monorepo compares to Multirepo:

  • pros of Monorepo

    all in one Using a common set of code configuration, easier to unify management
    Code reuse Code reuse is high, and it is easy to extract common modules
    transparency Source code are visible, easy to view in the IDE
    The smallest change When the dependency code changes, all dependencies take effect immediately
  • cons of Monorepo

    Access constraints Code in one place, there is a risk of overstepping your authority
    The Build time Mono tends to be a lot of code and takes a long time to build
    Git performance Git is designed to be a small repository of code, and too much code will drag Git down

Monorepo tools

There are many mainstream mono management tools, such as:

  • Lerna: the old JS multi-package management tool
  • Baze: Google’s multilingual Mono build tool
  • RushJs: Mono lifecycle tools from Mega Hardware
  • NX: Extensible Mono development tool

These tools are very powerful, some with integrated CI features, others with built-in scaffolding. But it’s easy to add to the cognitive costs of learning new tools. In fact, if you want to start Monorepo, you only need yarn. Other advanced functions can be learned slowly, and can also be used with other traditional tools. The following section briefly describes how to configure Monorepo based on YARN.

Yarn workspaces

Yarn has a multi-package management function, Yarn Workspaces, since version 1.0. The main focus is on three requirements:

  • For each project, Yarn will use a separateyarn.lockFiles instead of using a different lock file for each project, which means fewer conflicts and easier review
  • All project dependencies will be installed together, giving Yarn more freedom to optimize them better
  • Dependencies can be linked together, meaning that workspaces can depend on each other and always use the latest available code

To enable the workspace

To try opening the Yarn workspace, start by typing the following command on the console:

yarn config set workspaces-experimental true
Copy the code

This will write workspaces-experimental true to the.yarnrc file in your system directory.

Next we initialize the package.json file in the project root directory as follows:

{
  "private": true."workspaces": {
    "packages": ["packages/*"]}}Copy the code

Here “private”: true is required, primarily to add security; “Workspaces. Packages” is used to specify the folder in which the subproject is located.

Initialize the project

For simplicity, let’s create two child apps — foo and bar — with the following structure:

. ├ ─ ─ packages │ ├ ─ ─ foo │ │ └ ─ ─ package. The json │ └ ─ ─ bar │ └ ─ ─ package. The json └ ─ ─ package. The jsonCopy the code

Specify the package name for foo and bar,

// packages/foo/package.json
{
  "name": "@onion/foo"."version": "1.0.0"."devDependencies": {
    "chalk": "^ 4.1.0." "}}Copy the code

Where bar depends on foo

// packages/bar/package.json
{
  "name": "@onion/bar"."version": "1.0.0"."dependencies": {
    "@onion/foo": "1.0.0"
  },
  "devDependencies": {
    "chalk": "^ 4.1.0." "}}Copy the code

The installation

Let’s run yarn install in the root directory; @onion/bar and @onion/foo appear under node_modules, and chalk is the common dependency of both packages.

. ├ ─ node_modules │ ├ ─ @ the onion │ │ ├ ─ bar │ │ └ ─ foo │ └ ─ [email protected] │ └ ─ packages ├ ─ bar └ ─ fooCopy the code

Yarn install:

  1. Promote packages in workspaces to node_modules so that js under the project can invoke private project packages by importing ‘@onion/foo’

  2. The co-dependent three-party library — Chalk — under Workspaces has been promoted to node_modules for use by both

To load NPM modules, check the node_modules folder in the current directory. If the import module is not found, look for the node_modules folder in the previous directory. Until node_modules under the system home. So raising dependencies does not affect usage.

nohoist

Of course, if the three dependency versions of the subproject are different, YARN will selectively promote only one version and keep the other version under the node_modules folder of the specific subproject:

. ├ ─ node_modules │ ├ ─ @ the onion │ │ ├ ─ bar │ │ └ ─ foo │ └ ─ [email protected] │ └ ─ packages ├ ─ bar │ └ ─ node_modules │ └ ─ [email protected] └ ─ fooCopy the code

You can also go a step further and not let YARN increase some dependencies altogether:

//package.json
"workspaces": {
    "packages": ["packages/*"]."nohoist": ["**/chalk"]}Copy the code

Workspaces. Nohoist prevents dependency escalation so that the dependencies of the respective projects remain in the node_modules directory:

.├ ─node_modules │ ├─ @onion │ ├─bar │ ├─ foo │ ├─ packages │ ├─bar │ ├─ modules │ ├─ [email protected] ├─ foo │ ├─node_modules └ ─ [email protected]Copy the code

Run the machine-specific commands

Run the NPM script and regular operations of the subproject, first CD to a specific directory and then yarn run. You can also specify the name of the space in the root directory:

yarn workspace @onion/foo run test
Copy the code

Symlink

Finally, there is the integration of Monorepo with regular tools in the development environment. As mentioned above: the @onion/bar project depends on the @onion/foo project.

// packages/bar/package.json
{
  "name": "@onion/bar"."version": "1.0.0"."dependencies": {
    "@onion/foo": "1.0.0"}}Copy the code

We refer to the foo project within the bar project, usually not as a relative path; Instead, it is imported in the form of a Symlink (such as @onion/foo).

// packages/bar/index.js
import foo from ".. /foo/src/index"; // Bad

import foo from "@onion/foo"; // Good
Copy the code

The dependencies that Symlink points to are typically files in node_modules. As mentioned above, using the latest dependencies in local development can be a bit cumbersome because we need to install them each time, and sometimes there are version configuration issues. The bigger problem is that often the code in a dependency is compressed or escaped, making it difficult to debug. So to use the latest code, we need to add a little more configuration.

ts-node

Using typescript, for example, aliases symlinks to source files. If @onion/foo is pointed to the source code in tsconfig, the VSCode compiler will support source jump:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "./packages"."paths": {
      "@onion/foo": ["foo/src"]}}}Copy the code

Insert tsconfig-paths into NPM script:

// package.json
{
  "scripts": {
    "start": "ts-node -r tsconfig-paths/register src/index.ts"}}Copy the code

Webpack

You can also read the tsConfig configuration using the tsconfig-paths-webpack-plugin when using webpack hot loading:

//packages/foo/webpack.config.js
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");

module.exports = {
  resolve: {
    plugins: [new TsconfigPathsPlugin()],
  },
};
Copy the code

Babel

Babel, I haven’t found a plugin that works particularly well yet, but I can write a simple regex to implement aliasing:

// packages/bar/babel.config.js
module.exports = {
  plugins: [["module-resolver",
      {
        alias: {
          "^@onion/(.+)": ".. /\\1/src",},},],],};Copy the code

Other tools such as Rollup, Jest, CRA scaffolding, etc., are configured in much the same way, just changing the alias. You can try it yourself if you’re interested.

summary

This installment provides a brief overview of some common ways front-end projects manage Monorepo in conjunction with Yarn Workspaces. The advantages and disadvantages of Monorepo and Multirepo mentioned above, I wonder if you have the “empathy” experience?

I’ve worked on a large project myself that went from a few people to dozens, and then shrunk to just a few. We started with Multirepo and we had a lot of fun with it; Then, with the loss of manpower, running Multirepo with just a few people, it became extremely difficult. When I look back on it, I can see everything clearly.

Although Monorepo and Multirepo have their merits, I personally recommend using Monorepo because you can’t predict the future of a project; At the time of the handover, Monorepo is at least complete, unlike Multirepo where you have no idea how much information is missing.