preface
Let’s say we have four projects
electron
Use:Electron
Create a desktop project with UI and functionality largely consistent with the Webweb
Use:React
One that was createdweb
projectservice
Use:Nest.js
Create a back-end service that is responsible for givingweb
andelectron
providebff
supportssr
Use:Next.js
Create a back-end SSR service that is responsible for givingweb
andelectron
providessr
support
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
- Or back and forth by developers
Copy and paste
- Or find multiple warehouses to independently maintain multiple packages, and then
publish
Go to the NPM repository or open multiple projects locally and thennpm link
- 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
- Put projects and reusable packages in the same repository
- After the project walk, change the code of the package, and the project can be hot updated
- 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
- Yarn Workspaces: Dependency management mechanism of Monorepo provided by YARN
- 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
- new
applications
Folder containing specific end items packages
Create multiple sub-packages in the folder, and each package corresponds to usprefaceThe reusable code package mentioned- Change each
package
As well asapplications
thepackage.json
In thename
- in
lerna.json
Packages 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
cjs
esm
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 workspaces
Cooperate 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
- Create alias. Js
- Create modules. Js
- 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