preface

Let’s say we have four projects

  1. electronUse:ElectronCreate a desktop project with UI and functionality largely consistent with the Web
  2. webUse:ReactOne that was createdwebproject
  3. serviceUse:Nest.jsCreate a back-end service that is responsible for givingwebandelectronprovidebffsupport
  4. ssrUse:Next.jsCreate a back-end SSR service that is responsible for givingwebandelectronprovidessrsupport

Our task is to develop or maintain these four projects, in which some reusable code packages will be used as follows

category describe
common Constant definitions, hooks, utils, etc
controls Atomic component, responsible for UI
icons icon
openapi Axios generates ts code based on Swagger’s JSON
components The business component
apps A routing component consisting of business components

If split into multiple projects multiple warehouses, these reusable code

  1. Or back and forth by developersCopy and paste
  2. Or find multiple warehouses to independently maintain multiple packages, and thenpublishGo to the NPM repository or open multiple projects locally and thennpm link
  3. Or write the logic separately in each warehouse that could have been extracted and reused

So, is there a better way to improve the quality of our team’s development and release? The following is our appeal

  1. Put projects and reusable packages in the same repository
  2. After the project walk, change the code of the package, and the project can be hot updated
  3. If necessary, the code for the package can be published at deployment time for use by repositories other than the MonorePO

monorepo

Monorepo is a software development strategy that stores code for multiple projects in a repository

React or Vscode or Babel use monorepo to manage their code.

There are several ways to set up Monorepo

  1. Yarn Workspaces: Dependency management mechanism of Monorepo provided by YARN
  2. Lerna: An open source management tool for managing JavaScript projects that contain multiple packages

We use LerNA to initialize the project

Website document: Lerna is a management tool, used to manage the JavaScript contains more than one package (package) project | Lerna Chinese document (lernajs. Cn)

github:github.com/lerna/lerna

The installation

#First use NPM to install Lerna into the global environment:
#Lerna 2.x is recommended.
npm install --global lerna

#Next, we'll create a new Git repository:
git init monorepo && cd monorepo

#Now we convert the above warehouse into a Lerna warehouse:
lerna init
Copy the code

Your repository should currently have the following structure

Directory transformation

  1. newapplicationsFolder containing specific end items
  2. packagesCreate multiple sub-packages in the folder, and each package corresponds to usprefaceThe reusable code package mentioned
  3. Change eachpackageAs well asapplicationsthepackage.jsonIn thename
  4. inlerna.jsonPackages are added to"applications/*"

package

category name describe
common @monorepo/common Constant definitions, hooks, utils, etc
controls @monorepo/controls Atomic component, responsible for UI
icons @monorepo/icons icon
openapi @monorepo/openapi Axios generates ts code based on Swagger’s JSON
components @monorepo/components The business component
apps @monorepo/apps A routing component consisting of business components

applications

category name describe
electron @monorepo/electron The ELECTRON project, UI and functionality are basically the same as those of the Web
web @monorepo/web The web project
service @monorepo/service A back-end service that provides BFF support for Web and Electron
ssr @monorepo/ssr A back-end SSR service responsible for providing SSR support to Web and ELECTRON

lerna.json

{
  "packages": [
    "packages/*"."applications/*"]."version": "0.0.0"
}
Copy the code

After the directory transformation, your repository should now have the following structure

tsconfig

Use the Paths in Tsconfig to help with module parsing

The root directory

{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true."baseUrl": "."."downlevelIteration": true."esModuleInterop": true."experimentalDecorators": true."forceConsistentCasingInFileNames": true."importHelpers": true."isolatedModules": true."jsx": "react"."module": "commonjs"."moduleResolution": "node"."newLine": "lf"."noImplicitAny": true."noImplicitThis": true."noImplicitUseStrict": true."noUnusedLocals": true."noUnusedParameters": true."paths": {
      "@monorepo/apps": ["./packages/apps/src"]."@monorepo/apps/lib/*": ["./packages/apps/src/*"]."@monorepo/apps/es/*": ["./packages/apps/src/*"]."@monorepo/common/lib/*": ["./packages/common/src/*"]."@monorepo/common/es/*": ["./packages/common/src/*"]."@monorepo/components": ["./packages/components/src"]."@monorepo/components/lib/*": ["./packages/components/src/*"]."@monorepo/components/es/*": ["./packages/components/src/*"]."@monorepo/controls": ["./packages/controls/src"]."@monorepo/controls/lib/*": ["./packages/controls/src/*"]."@monorepo/controls/es/*": ["./packages/controls/src/*"]."@monorepo/icons": ["./packages/icons/src"]."@monorepo/icons/lib/*": ["./packages/icons/src/*"]."@monorepo/icons/es/*": ["./packages/icons/src/*"]."@monorepo/openapi": ["./packages/openapi/src"]."@monorepo/openapi/dist/lib/*": ["./packages/openapi/src/*"]},"pretty": true."resolveJsonModule": true."skipLibCheck": true."sourceMap": true."strictFunctionTypes": true."strictNullChecks": true."strictPropertyInitialization": true."target": "es5"}}Copy the code

package/applications

{
    "extends": ".. /.. /tsconfig.json".// Depending on the project path
    "compilerOptions": {
        "lib": ["dom"."dom.iterable"."esnext"]}}Copy the code

Package to pack

The code in the package is more like a library than an application. So instead of using a webpack-heavy packer, you can use gulp or rollup to pack, and output as needed in the following format

  1. cjs
  2. esm
  3. umd

The use of gulp or rollup also varies from person to person. For example, if the package does not need to be published, gulp can be used. If the package needs to be distributed externally, either to NPM or to an SDK package, it can be packaged using rollup

Here is a simple rollup configuration

import typescript from "rollup-plugin-typescript2";
import common from "rollup-plugin-commonjs";
import NodePath from "path";
import autoprefixer from "autoprefixer";
import url from "rollup-plugin-url";
import RollupJson from "@rollup/plugin-json";
import RollupUrl from "@rollup/plugin-url";
import RollupBabel from "@rollup/plugin-babel";
import RollPostcss from "rollup-plugin-postcss";
import RollProgress from "rollup-plugin-progress";
import peerDepsExternal from "rollup-plugin-peer-deps-external";
import pkg from "./package.json";

console.info("EXPECTED EXTERNALS", [...Object.keys(pkg.peerDependencies || {})]);

const rollBabelConfig = {
    babelHelpers: "runtime".exclude: "node_modules/**"};const rollPostcssConfig = {
    inject: true.minimize: true.modules: true.plugins: [
        autoprefixer({
            remove: false,})]};export default {
    input: "./src/index.ts".output: [{format: "cjs".dir: "lib".sourcemap: true.preserveModules: true.preserveModulesRoot: "src"}, {dir: "es".format: "esm".sourcemap: true.preserveModules: true.preserveModulesRoot: "src"],},declaration: true.external: [...Object.keys(pkg.peerDependencies || {})],
    plugins: [
        peerDepsExternal(),
        RollPostcss(rollPostcssConfig),
        url({
            url: "inline".limit: 1000.emitFiles: true,
        }),
        RollupUrl({
            fileName: "[dirname][hash][extname]".sourceDir: NodePath.join(__dirname, ".."),
        }),
        typescript(),
        RollupBabel(rollBabelConfig),
        common({
            include: /\/node_modules\//,
        }),
        RollupJson(),
        RollProgress(),
    ],
};
Copy the code

Here is a gulp configuration

import { src, dest, parallel } from "gulp";

function copyAssets(toDir: string) {
    return function copyAssets() {
        return src(["src/**/*.less"."src/**/*.png"."src/**/*.gif"]).pipe(dest(toDir));
    };
}

export default parallel(copyAssets("lib"), copyAssets("es"));
Copy the code

Install the package in the project

Applications is our specific project, and Package is a reusable software package

Here we need to use the lerna add command to help us install the package in the project

Leran add < PackageName > : By default, all packages install dependencies at the same time. You can also receive a parameter –scope=PackageName, and you can install the corresponding dependencies for that package only by adding local or remote packages as dependencies to the current Lerna repository. Unlike YARN Add and NPM install, you can add only one software package at a time

lerna add <package>[@version] [--dev] [--exact] [--peer]
Copy the code

So we use lerna add to install @monorepo/common to @monorepo/electron and @monorepo/web

lerna add @monorepo/common --scope=@monorepo/electron --scope=@monorepo/web
Copy the code

You can see that both the electron and Web projects have successfully installed @monorepo/common

However, both projects clearly use the same dependency, so why install it separately in both projects? To solve this problem, you need to use ityarn workspacesCooperate withlerna

yarn workspaces

Yarn workspaces allows you to easily install or upgrade all dependencies with a single YARN command, allowing multiple projects to share the same node_modules directory

Do this in the root packes. json directory

{
  "name": "root",
  "private": true,
+ "workspaces": [
+ "applications/*",
+ "packages/*"
+],"DevDependencies ": {"devDependencies": "^4.0.0"}}Copy the code

You can see that YARN Workspaces helped us do the integration

Webpack configuration

We need to use Webpack to help us implement the code to modify the package, project hot update, we at @monorepo/web according to the following configuration changes

The following is a project created by create-react-app

  1. Create alias. Js
  2. Create modules. Js
  3. Create paths. Js

alias.js

In webpack.config.js, you can configure the base path to find “commonJS/AMD modules” by setting the resolve property, set the module suffix to search for, and set the alias

Setting an alias reduces the complexity of the path where it is referenced later

function getAlias() {
    if (process.env.NODE_ENV === "development") {
        return {
            "@monorepo/apps/lib": "@monorepo/apps/src"."@monorepo/apps/es": "@monorepo/apps/src"."@monorepo/apps": "@monorepo/apps/src"."@monorepo/common/lib": "@monorepo/common/src"."@monorepo/common/es": "@monorepo/common/src"."@monorepo/components/lib": "@monorepo/components/src"."@monorepo/components/es": "@monorepo/components/src"."@monorepo/components": "@monorepo/components/src"."@monorepo/controls/lib": "@monorepo/controls/src"."@monorepo/controls/es": "@monorepo/controls/src"."@monorepo/controls": "@monorepo/controls/src"."@monorepo/icons/lib": "@monorepo/icons/src"."@monorepo/icons/es": "@monorepo/icons/src"."@monorepo/icons": "@monorepo/icons/src"."@monorepo/openapi/dist/lib": "@monorepo/openapi/src"."@monorepo/openapi": "@monorepo/openapi/src"
        };
    }
    return {};
}

module.exports = getAlias();
Copy the code

modules.js

After formatting the alias.js object in modules.js, configure it in the webpackAliases value of getModules Return

const alias = require("./alias"); .function getWebpackAliases(options = {}) {
    const baseUrl = options.baseUrl;

    if(! baseUrl) {return alias;
    }

    const baseUrlResolved = path.resolve(paths.appPath, baseUrl);

    if (path.relative(paths.appPath, baseUrlResolved) === "") {
        return {
            src: paths.appSrc, ... alias, }; }returnalias; }.../ / if there is a Jest
function getJestAliases(options = {}) {
  const baseUrl = options.baseUrl

  if(! baseUrl) {return alias
  }

  const baseUrlResolved = path.resolve(paths.appPath, baseUrl)

  if (path.relative(paths.appPath, baseUrlResolved) === ' ') {
    return {
      '^src/(.*)$': '<rootDir>/src/$1'. alias, } }return alias
}
...

return {
	webpackAliases: getWebpackAliases(options),
	...
}
Copy the code

paths.js

ProjectDirectory is exposed in paths, where the path is the root directory

module.exports = {
    projectDirectory: resolveApp(".. /.. /"),... };Copy the code

webpack.config.js

Add “isEnvDevelopment && paths.projectDirectory].filter(Boolean)” to the module of webpack.config. js. Enables the local development environment to modify the code in the package to implement hot updates

{
	test: /\.(js|mjs|jsx|ts|tsx)$/,
	include: [paths.appSrc, isEnvDevelopment && paths.projectDirectory].filter(Boolean)... }Copy the code

Afterword.

To sum up, the above mentioned scenarios use Monorepo development mode to transform independent projects into a unified engineering whole, solving the problem of improving r&d efficiency and engineering quality

As mentioned in uWyndA’s 2021 year-end summary – Nuggets (Juejin. Cn), just use the right technology for the right scenario. After all, software development has no “silver bullet”.

Also, what problems have people had when using Monorepo to solve their own project needs? Can these problems be solved? Welcome to join us in the comments section