带你了解并实践monorepo和pnpm,绝对干货!熬夜总结!

2023-11-12

大厂技术  高级前端  Node进阶

点击上方 程序员成长指北,关注公众号

回复1,加入高级Node交流群

03f7045447be6cddd579b2a3b4be0cbf.png

为什么使用monorepo

什么是monorepo

简单来说就是,将多个项目或包文件放到一个git仓库来管理。 目前比较广泛应用的是yarn+lerna的方式实现monorepo的管理。 一个简单的monorepo的目录结构类似这样:

js
复制代码
├── packages
|   ├── pkg1
|   |   ├── package.json
|   ├── pkg2
|   |   ├── package.json
├── package.json
├── lerna.json

之所以应用monorepo,主要是解决以下问题:

  • 代码复用的问题

  • 开发流程统一

  • 高效管理多项目/包

pnpm的使用

为什么用pnpm

关于为什么越来越多的人推荐使用pnpm,可以参考这篇文章[1] 这里简单列一下pnpm相对于yarn/npm的优势:

  1. 安装速度最快(非扁平的包结构,没有yarn/npm的复杂的扁平算法,且只更新变化的文件)

  2. 节省磁盘空间 (统一安装包到磁盘的某个位置,项目中的node_modules通过hard-link的方式链接到实际的安装地址)

pnpm安装包有何不同

目前,使用npm/yarn安装包是扁平结构(以前是嵌套结构,npm3之后改为扁平结构)

扁平结构 就是安装一个包,那么这个包依赖的包将一起被安装到与这个包同级的目录下。比如安装一个express包,打开目录下的node_modules会发现除了express之外,多出很多其他的包。如图:

58bbe1c01391378304d10d698f9c5ae0.jpeg
image.png

嵌套结构 就是一个包的依赖包会安装在这个包文件下的node_modules下,而依赖的依赖会安装到依赖包文件的node_modules下。依此类推。如下所示:

js
复制代码
node_modules
├─ foo
  ├─ node_modules
     ├─ bar
       ├─ index.js
       └─ package.json
  ├─ index.js
  └─ package.json

嵌套结构的问题在于:

  • 包文件的目录可能会非常长

  • 重复安装包

  • 相同包的实例不能共享

而扁平结构也同样存在问题:

  • 依赖结构的不确定性(不同包依赖某个包的不同版本 最终安装的版本具有不确定性)可通过lock文件确定安装版本

  • 扁平化算法复杂,耗时

  • 非法访问未声明的包

现在,我们使用pnpm来安装express,然后打开node_modules

98e81040fe984b71a95cf543b7574a02.jpeg
image.png

从上图可以发现:

  1. node_modules下只有express一个包,且这个被软链到了其他的地方。

  2. .modlues.yaml包含了一些pnpm包管理的配置信息。如下图:

8f3232dabe0a06f42f2bdffe8ee010eb.jpeg
image.png

可以看到 .pnpm目录的实际指向的pnpm store的路径pnpm包的版本等信息

  1. .pnpm目录可以看到所有安装了的依赖包。如下图:

2b8c24a0168bebf8a9f5c66e28897d5d.jpeg
image.png

观察之后,发现安装结构和官方发布的图是完全一致的:7394d4fee6f0c1182fac2d2491ce9b10.jpeg

由官方图我们可以了解到:

  • 当我们安装bar包时,根目录下只包含安装的包bar

  • node_modules目录下的bar包会软链接.pnpm/bar/node_modules/bar@*.*.*

  • bar的依赖包foo会被提升到.pnpm的根目录下,其他包依赖foo时也会软链接到这里

  • 而bar和foo实际通过硬链接.pnpm store

软链接可以理解成快捷方式。 它和windows下的快捷方式的作用是一样的。 硬链接等于cp -p 加 同步更新。即文件大小和创建时间与源文件相同,源文件修改,硬链接的文件会同步更新。应用:可以防止别人误删你的源文件

软链接解决了磁盘空间占用的问题,而硬链接解决了包的同步更新和统一管理问题。 还有一个巧妙的设计就是:将安装包和依赖包放在同一级目录下,即.pnpm/依赖包/node_modules下。这个设计也就防止了 **依赖包间的非法访问**,根据Node模块路径解析规则[2]可知,不在安装包同级的依赖包无法被访问,即只能访问安装包依赖的包。

现在应该没理由不升级你的包管理工具了吧!

如果你还有使用npm/yarn的场景,那么,可以推荐使用 **ni**[3] 这个工具,它可以帮你自动识别项目使用的包管理工具,你只需要一行命名就搞定了。

比如: 执行命令ni安装依赖包,如果当前项目包含pnpm-lock.yaml,那么会使用 pnpm install执行安装命令,否则判断是否包含package-lock.json/yarn.lock/bun.lockb,来确定使用哪个包管理工具去执行安装命令。

pnpm workspace实践

1. 新建仓库并初始化

新建目录pnpm-workspace-demo,执行npm init / pnpm init初始化项目,生成 package.json

2. 指定项目运行的Node、pnpm版本

为了减少因nodepnpm的版本的差异而产生开发环境错误,我们在package.json中增加engines字段来限制版本。

js
复制代码
{
    "engines": {
        "node": ">=16",
        "pnpm": ">=7"
    }
}

3. 安全性设置

为了防止我们的根目录被当作包发布,我们需要在package.json加入如下设置:

js
复制代码
{
    "private": true
}

pnpm本身支持monorepo,不用额外安装包,真是太棒了! 但是每个monorepo的根目录下必须包含pnpm-workspace.yaml文件。 目录下新建pnpm-workspace.yaml文件,内容如下:

yaml
复制代码
packages:  
# all packages in direct subdirs of packages/  
- 'packages/*'

4. 安装包

4.1 安装全局依赖包

有些依赖包需要全局安装,也就是安装到根目录,比如我们常用的编译依赖包rollup、execa、chalk、enquirer、fs-extra、minimist、npm-run-all、typescript等 运行如下命令:

-w 表示在workspace的根目录下安装而不是当前的目录

sql
复制代码
pnpm add rollup chalk minimist npm-run-all typescript -Dw

与安装命令pnpm add pkgname相反的的删除依赖包pnpm rm/remove pkgnamepnpm un/uninstall pkgname

4.2 安装子包的依赖

除了进入子包目录直接安装pnpm add pkgname之外,还可以通过过滤参数 --filter-F指定命令作用范围。格式如下:

pnpm --filter/-F 具体包目录名/包的name/正则匹配包名/匹配目录 command

比如:我在packages目录下新建两个子包,分别为toolsmini-cli,假如我要在min-cli包下安装react,那么,我们可以执行以下命令:

js
复制代码
pnpm --filter mini-cli add react

更多的过滤配置可参考:www.pnpm.cn/filtering[4]

4.3 打包输出包内容

这里选用rollup[5]作为打包工具,由于其打包具有更小的体积tree-shaking的特性,可以说是作为工具库打包的最佳选择。

先安装打包常用的一些插件:

sql
复制代码
pnpm add rollup-plugin-typescript2 @rollup/plugin-json @rollup/plugin-terser -Dw
基础编译配置

目录下新建rollup的配置文件rollup.config.mjs,考虑到多个包同时打包的情况,预留input为通过rollup通过参数传入。这里用process.env.TARGET表示不同包目录。

以下为编译的基础配置,主要包括:

  • 支持的输出包格式,即format种类,预定义好输出配置,方便后面使用

  • 根据rollup动态传入包名获取input

  • 对浏览器端使用的format进行压缩处理

  • rollup配置导出为数组,每种format都有一组配置,每个包可能需要导出多种format

js
复制代码
import { createRequire } from 'module'
import { fileURLToPath } from 'url'
import path from 'path'
import json from '@rollup/plugin-json'
import terser from '@rollup/plugin-terser'

const require = createRequire(import.meta.url)
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const packagesDir = path.resolve(__dirname, 'packages')
const packageDir = path.resolve(packagesDir, process.env.TARGET)

const resolve = p => path.resolve(packageDir, p)
const pkg = require(resolve(`package.json`))
const packageOptions = pkg.buildOptions || {}
const name = packageOptions.filename || path.basename(packageDir)

// 定义输出类型对应的编译项
const outputConfigs = {
'esm-bundler': {
    file: resolve(`dist/${name}.esm-bundler.js`),
    format: `es`
  },
  'esm-browser': {
    file: resolve(`dist/${name}.esm-browser.js`),
    format: `es`
  },
  cjs: {
    file: resolve(`dist/${name}.cjs.js`),
    format: `cjs`
  },
  global: {
    name: name,
    file: resolve(`dist/${name}.global.js`),
    format: `iife`
  }
}

const packageFormats = ['esm-bundler', 'cjs']
const packageConfigs = packageFormats.map(format => createConfig(format, outputConfigs[format]))

export default packageConfigs

function createConfig(format, output, plugins = []) {
  // 是否输出声明文件
  const shouldEmitDeclarations = !!pkg.types
  
  const minifyPlugin = format === 'global' && format === 'esm-browser' ? [terser()] : []
  return {
      input: resolve('src/index.ts'),
  // Global and Browser ESM builds inlines everything so that they can be
  // used alone.
  external: [
      ...['path', 'fs', 'os', 'http'],
      ...Object.keys(pkg.dependencies||{}),
      ...Object.keys(pkg.peerDependencies || {}),
      ...Object.keys(pkg.devDependencies||{}),
    ],
  plugins: [
    json({
      namedExports: false
    }),
    ...minifyPlugin,
    ...plugins
  ],
  output,
  onwarn: (msg, warn) => {
    if (!/Circular/.test(msg)) {
      warn(msg)
    }
  },
  treeshake: {
    moduleSideEffects: false
  }
  }
}
多包同时编译

根目录下新建scripts目录,并新建build.js用于打包编译执行。为了实现多包同时进行打包操作,我们首先需要获取packages下的所有子包

js
复制代码
const fs = require('fs')
const {rm} = require('fs/promises')
const path = require('path')
const allTargets = (fs.readdirSync('packages').filter(f => {
    // 过滤掉非目录文件
    if (!fs.statSync(`packages/${f}`).isDirectory()) {
      return false
    }
    const pkg = require(`../packages/${f}/package.json`)
    // 过滤掉私有包和不带编译配置的包
    if (pkg.private && !pkg.buildOptions) {
      return false
    }
    return true
  }))

获取到子包之后就可以执行build操作,这里我们借助 execa[6] 来执行rollup命令。代码如下:

js
复制代码
const build = async function (target) { 
    const pkgDir = path.resolve(`packages/${target}`)
    const pkg = require(`${pkgDir}/package.json`)

    // 编译前移除之前生成的产物
    await rm(`${pkgDir}/dist`,{ recursive: true, force: true })
    
    // -c 指使用配置文件 默认为rollup.config.js
    // --environment 向配置文件传递环境变量 配置文件通过proccess.env.获取
    await execa(
        'rollup',
        [
          '-c',
          '--environment',
          [
            `TARGET:${target}`
          ]
            .filter(Boolean)
            .join(',')
        ],
        { stdio: 'inherit' }
    )
}

同步编译多个包时,为了不影响编译性能,我们需要控制并发的个数,这里我们暂定并发数为4,编译入口大概长这样:

js
复制代码
const targets = allTargets // 上面的获取的子包
const maxConcurrency = 4 // 并发编译个数

const buildAll = async function () {
  const ret = []
  const executing = []
  for (const item of targets) {
  // 依次对子包执行build()操作
    const p = Promise.resolve().then(() => build(item))
    ret.push(p)

    if (maxConcurrency <= targets.length) {
      const e = p.then(() => executing.splice(executing.indexOf(e), 1))
      executing.push(e)
      if (executing.length >= maxConcurrency) {
        await Promise.race(executing)
      }
    }
  }
  return Promise.all(ret)
}
// 执行编译操作
buildAll()

最后,我们将脚本添加到根目录的package.json中即可。

json
复制代码
{
    "scripts": {
    "build": "node scripts/build.js"
  },
}

现在我们简单运行pnpm run build即可完成所有包的编译工作。(注:还需要添加后面的TS插件才能工作)。

此时,在每个包下面会生成dist目录,因为我们默认的是esm-bundlercjs两种format,所以目录下生成的文件是这样的

6ed376078b1f05554a86063f706e7f35.jpeg
image.png

那么,如果我们想自定义生成文件的格式该怎么办呢?

子包自定义编译输出格式

最简单的方法其实就是在package.json里做配置,在打包的时候我们直接取这里的配置即可,比如我们在包tools里做如下配置:

json
复制代码
{
"buildOptions": {
    "name": "tools", // 定义global时全局变量的名称
    "filename": "tools", // 定义输出的文件名 比如tools.esm-browser.js 生成的文件为[filename].[format].js
    "formats": [ // 定义输出
      "esm-bundler",
      "esm-browser",
      "cjs",
      "global"
    ]
  },
}

这里我们只需要在基础配置文件rollup.config.mjs里去做些改动即可:

js
复制代码
const defaultFormats = ['esm-bundler', 'cjs']
const packageFormats = packageOptions.formats || defaultFormats // 优先使用每个包里自定义的formats
const packageConfigs = packageFormats.map(format => createConfig(format, outputConfigs[format]))
命令行自定义打包并指定其格式

比如我想单独打包tools并指定输出的文件为global类型,大概可以这么写:

arduino
复制代码
pnpm run build tools --formats global

这里其实就是将命令行参数接入到打包脚本里即可。 大概分为以下几步:

  1. 使用minimist[7]取得命令行参数

js
复制代码
const args = require('minimist')(process.argv.slice(2))
const targets = args._.length ? args._ : allTargets
const formats = args.formats || args.f
  1. 将取得的参数传递到rollup的环境变量中,修改execa部分

js
复制代码
await execa(
        'rollup',
        [
          '-c',
          '--environment', // 传递环境变量  配置文件可通过proccess.env.获取
          [
            `TARGET:${target}`,
            formats ? `FORMATS:${formats}` : `` // 将参数继续传递 
          ]
            .filter(Boolean)
            .join(',')
        ],
        { stdio: 'inherit' }
    )
  1. rollup.config.mjs中获取环境变量并应用

js
复制代码
const defaultFormats = ['esm-bundler', 'cjs']
const inlineFormats = process.env.FORMATS && process.env.FORMATS.split(',') // 获取rollup传递过来的环境变量process.env.FORMATS
const packageFormats = inlineFormats || packageOptions.formats || defaultFormats
TS打包

对于ts编写的项目通常也会发布声明文件,只需要在package.json添加types字段来指定声明文件即可。那么,我们其实在做打包时就可以利用这个字段来判断是否要生成声明文件。对于rollup,我们利用其插件rollup-plugin-typescript2来解析ts文件并生成声明文件。 在rollup.config.mjs中添加如下配置:

js
复制代码
// 是否输出声明文件 取每个包的package.json的types字段
  const shouldEmitDeclarations = !!pkg.types

  const tsPlugin = ts({
    tsconfig: path.resolve(__dirname, 'tsconfig.json'),
    tsconfigOverride: {
      compilerOptions: {
        target: format === 'cjs' ? 'es2019' : 'es2015',
        sourceMap: true,
        declaration: shouldEmitDeclarations,
        declarationMap: shouldEmitDeclarations
      }
    }
  })
  
  return {
      ...
      plugins: [
        json({
          namedExports: false
        }),
        tsPlugin,
        ...minifyPlugin,
        ...plugins
      ],
    }
将生成的声明文件整理到指定文件

以上配置运行后会在每个包下面生成所有包的声明文件,如图:

6b44951a95ee524a9b9957fdb67cea36.jpeg
image.png

这并不是我们想要的,我们期望在dist目录下仅生成一个 .d.ts文件就好了,使用起来也方便。这里我们借助api-extractor[8]来做这个工作。这个工具主要有三大功能,我们要使用的是红框部分的功能,如图:

73e513f04d157f5cf4e076a5567d3b26.jpeg关键实现步骤:

  1. 根目录下生成api-extractor.json并将dtsRollup设置为开启

  2. 子包下添加api-extractor.json并定义声明文件入口及导出项,如下所示:

json
复制代码
{
  "extends": "../../api-extractor.json",
  "mainEntryPointFilePath": "./dist/packages/<unscopedPackageName>/src/index.d.ts", // rollup生成的声明文件
  "dtsRollup": {
    "publicTrimmedFilePath": "./dist/<unscopedPackageName>.d.ts" // 抽离为一个声明文件到dist目录下
  }
}
  1. 在rollup执行完成后做触发API Extractor操作,在build方法中增加以下操作:

js
复制代码
build(target) {
    await execa('rollup')
    // 执行完rollup生成声明文件后
    // package.json中定义此字段时执行
    if (pkg.types) { 
        console.log(
          chalk.bold(chalk.yellow(`Rolling up type definitions for ${target}...`))
        )
        // 执行API Extractor操作 重新生成声明文件
        const { Extractor, ExtractorConfig } = require('@microsoft/api-extractor')
        const extractorConfigPath = path.resolve(pkgDir, `api-extractor.json`)
        const extractorConfig =
         ExtractorConfig.loadFileAndPrepare(extractorConfigPath)
         const extractorResult = Extractor.invoke(extractorConfig, {
          localBuild: true,
          showVerboseMessages: true
        })
        if (extractorResult.succeeded) {
          console.log(`API Extractor completed successfully`);
          process.exitCode = 0;
        } else {
          console.error(`API Extractor completed with ${extractorResult.errorCount} errors`
            + ` and ${extractorResult.warningCount} warnings`);
          process.exitCode = 1;
        }
        
        // 删除ts生成的声明文件
        await rm(`${pkgDir}/dist/packages`,{ recursive: true, force: true })
      }
}
  1. 删除rollup生成的声明文件

那么,到这里,整个打包流程就比较完备了。

changesets的使用

对于pnpm workspace实现的monorepo,如果要管理包版本并发布,需要借助一些工具,官方推荐使用如下工具:

  • changesets[9]

  • rush[10]

我们这里主要学习一下changesets的使用,它的主要作用有两个:

  • 管理包版本

  • 生成changelog

对于monorepo项目使用它会更加方便,当然单包也可以使用。主要区别在于项目下有没有pnpm-workspace.yaml,如果未指定多包,那么会当作普通包进行处理。 那么,我们来看一下具体的步骤:

1. 安装

sql
复制代码
pnpm add @changesets/cli -Dw

2. 初始化changeset配置

csharp
复制代码
npx changeset init

这个命令会在根目录下生成.changeset文件夹,文件夹下包含一个config文件和一个readme文件。生成的config文件长这样:

json
复制代码
{
  "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false, // 是否提交因changeset和changeset version引起的文件修改
  "fixed": [], // 设置一组共享版本的包 一个组里的包,无论有没有修改、是否有依赖,都会同步修改到相同的版本
  "linked": [], // 设置一组需要关联版本的包 有依赖关系或有修改的包会同步更新到相同版本 未修改且无依赖关系的包则版本不做变化
  "access": "public", // 发布为私有包/公共包
  "baseBranch": "main",
  "updateInternalDependencies": "patch", // 确保依赖包是否更新、更新版本的衡量单位
  "ignore": [] // 忽略掉的不需要发布的包
}

关于每个配置项的详细含义参考:config.json[11] 这里有几点需要注意的:

  • access 默认restricted发布为私有包,需要改为public公共包,否则发布时会报错

  • 对于依赖包版本的控制,我们需要重点理解一下 **fixed**[12] 和 **linked**[13] 的区别

  • fixedlinked的值为二维数组,元素为具体的包名或匹配表达式,但是这些包必须在pnpm-workspace.yaml添加过

3. 生成发布包版本信息

运行npx changeset,会出现一系列确认问题,包括:

  • 需要为哪些包更新版本

  • 哪些包更新为major版本

  • 哪些包更新为minor版本

  • 修改信息(会添加到最终生成的changelog.md中) 所有问题回答完成之后,会在.changeset下生成一个Markdown文件,这个文件的内容就是刚才问题的答案集合,大概长这样:

yaml
复制代码
---
'@scope/mini-cli': major
'@scope/tools': minor
---

update packages

—-- 中间为要更新版本的包列表 以及包对应的更新版本,最下面是修改信息

4. 更新包版本并生成changelog

运行npx changeset version 这个命令会做以下操作

  • 依据上一步生成的md文件和changeset的config文件更新相关包版本

  • 为版本更新的包生成CHANGELOG.md文件 填入上一步填写的修改信息

  • 删除上一步生成的Markdown文件,保证只使用一次

建议执行此操作后,pulish之前将改动合并到主分支

5. 版本发布

这个没啥好说的,直接执行命令npx changeset publish即可

为了保证发布功能,添加如下脚本:

json
复制代码
{
    "scripts": {
        "release": "run-s build releaseOnly",
        "releaseOnly": "changeset publish"
    }
}

预发布版本

changeset提供了带tag的预发布版本的模式,这个模式使用时候需要注意:

  • 通过pre enter/exit进入或退出预发布模式,在这个模式下可以执行正常模式下的所有命令,比如versionpublish

  • 为了不影响正式版本,预发布模式最好在单独分支进行操作,以免带来不好修复的问题

  • 预发布模式下,版本号为正常模式下应该生成的版本号加-<tag>.<num>结尾。tag为pre命令接的tag名,num每次发布都会递增 从0开始

  • 预发布的版本并不符合语义化版本的范围,比如我的依赖包版本为"^1.0.0",那么,预发布版本是不满足这个版本的,所以依赖包版本会保持不变

一个完整的预发布包大概要执行以下操作:

  1. changeset pre enter <tag> 进入预发布模式

  2. changeset 确认发布包版本信息

  3. changeset version 生成预发布版本号和changelog

  4. changeset publish 发布预发布版本

这里的tag可以是我们常用的几种类型:

名称 功能
alpha 是内部测试版,一般不向外部发布,会有很多Bug,一般只有测试人员使用
beta 也是测试版,这个阶段的版本会一直加入新的功能。在Alpha版之后推出
rc (Release Candidate) 发行候选版本。RC版不会再加入新的功能了,主要着重于除错

每次需要更新版本时从第二步往后再次执行即可

如果需要发布正式版本,退出预发布模式changeset pre exit,然后切换到主分支操作即可

代码格式校验

这里主要对代码风格进行校验, 校验工具为eslint (主要对js、ts等js语言的文件)和 prettier(js、css等多种类型的文件)

辅助工具为

  • **lint-stage**[14] 检查暂存区中的文件

  • **simple-git-hooks**[15] 一个git钩子管理工具,优点是使用简单,缺点是每个钩子只能执行一个命令,如果需要执行多个命令可以选择husky

配置如下:

json
复制代码
{
    "simple-git-hooks": {
        "pre-commit": "pnpm lint-staged" // 注册提交前操作 即进行代码格式校验
      },
    "lint-staged": {
        "*.{js,json}": [
          "prettier --write"
        ],
        "*.ts?(x)": [
          "eslint",
          "prettier --parser=typescript --write"
        ]
    },
}

对于钩子函数的注册通过simple-git-hooks来实现,在项目安装依赖之后触发钩子注册。可以添加以下脚本。(如果钩子操作改变,则需要重新执行安装依赖操作来更新)

json
复制代码
"scripts": {
    "postinstall": "simple-git-hooks",
  },

代码规范提交

这里主要用到以下三个工具:

  • **Commitizen**[16]:是一个命令行提示工具,它主要用于帮助我们更快地写出规范的commit message

  • **Commitlint**[17]:用于校验填写的commit message是否符合设定的规范

  • **simple-git-hooks**[18]

1. Commitizen的使用

  1. 安装Commitizen

复制代码
npm install -g commitizen
  1. 安装Commitizen的适配器,确定使用的规范,这里使用cz-conventional-changelog[19],也可以选择其他的适配器

复制代码
npm install -g cz-conventional-changelog
  1. 全局指定适配器

json
复制代码
// mac用户
echo '{ "path": "cz-conventional-changelog" }' > ~/.czrc

这个时候执行命令git cz会自动进入交互式生成commit message的询问中,如图:

2. Commitlint如何配置

我们可以通过配置的git cz命令进行规范的代码提交,那么,如果其他同事依然使用的是git commit来提交代码的话,那么,提交信息就会比较乱。这时候就需要对commit mesaage进行校验了,如果不通过则中断提交。这个校验就可以通过Commitlint来完成。

对于按照何种规则来校验,我们就需要单独安装检验规则的包来进行检验,比如@commitlint/config-conventional[20]

如果想定义自己的规则可以参考cz-customizable[21]

  1. 首先安装这两个包:

sql
复制代码
pnpm add @commitlint/config-conventional @commitlint/cli -Dw
  1. 根目录下写入commitlint配置,指定规则包

arduino
复制代码
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
  1. 配置git钩子执行校验操作 (执行pnpm install更新钩子)

json
复制代码
"simple-git-hooks": {
    "commit-msg": "npx --no -- commitlint --edit ${1}"
  },

这个时候再提交会对commit message进行校验,不符合规范则会出现以下提示:

ac2ea75c21970897cadfd964da2249e7.jpeg
image.png

Node 社群

 
 

我组建了一个氛围特别好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你对Node.js学习感兴趣的话(后续有计划也可以),我们可以一起进行Node.js相关的交流、学习、共建。下方加 考拉 好友回复「Node」即可。

e534a2eaa57899e4782b31014f0afe97.png

“分享、点赞、在看” 支持一下
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

带你了解并实践monorepo和pnpm,绝对干货!熬夜总结! 的相关文章

  • ESP32调试笔记

    1 现象 上电后一直复位 rst 0x3 SW RESET boot 0x13 SPI FAST FLASH BOOT 原因 Flash烧录时 ota data和app0位置错了 解决 把ota data和app0位置烧录正确即可 位置从分
  • 【vue3总结知识点——精简一】

    vue3总结知识点 认识vue3 Composition API setup 执行时机 setup 包含的生命周期 ref获取页面数据 reactive reactive与ref的异同 比较Vue2与Vue3的响应式 vue2的响应式 Vu
  • 组合数打表模板

    组合数打表模板 组合数打表模板 适用于N lt 3000c i j 表示从i个中选j个的选法 1 2 3 4 5 6 7 8 9 10 11 12 long long C N N const int mod 1e9 5 void get C
  • MFC创建内存映射文件示例

    该实例是在程序的exe路径下 实现读取 写入内存映射文件功能 头文件 ifdef GERNERAL PRODUCTDATA EXP define GERNERAL PRODUCTDATA API declspec dllexport els
  • window中如何用命令行新建文件夹和文件

    1 新建文件夹 D gt mkdir test 通过mkdir 文件夹名 回车即可用命令行工具新建文件夹 2 新建文件 cd test文件目录下 D gt test type nul 文件名 回车即可创建新的文件
  • Element UI的table表格中实现复选框勾选

    需求 在table中实现勾选多行复选框的内容 点击提交按钮 选择的复选框与表格内容对应
  • Matlab 常用快捷键

    MATLAB Numpy函数对照表 http mathesaurus sourceforge net matlab python xref pdf 常见快捷键 Ctrl R 注释代码 Ctrl T 取消注释代码 Ctrl 或 先将光标移动到
  • stm32——PWM实现呼吸灯效果

    使用pwm点亮led 实现呼吸灯效果 led为什么可以越来越亮 越来越暗 由不同的占空比决定 占空比由CCRx决定 1 芯片手册查看引脚pwm通道 2 cubeMX sys设置串口 RCC设置时钟来源 配置时钟 配置io口的pwm输出 三
  • 华为机顶盒系统时间同步服务器,华为悦盒主时间同步服务器地址

    华为悦盒主时间同步服务器地址 内容精选 换一换 华为云存储容灾服务 简称SDRS 提供了虚拟机级别的容灾保护 当主站点故障的时候 虚拟机可以在备站点迅速恢复 以确保业务的联系性 来自 产品 边缘节点既可以是物理机 也可以是虚拟机 边缘节点需
  • Linux下C/C++语言gdb调试方法

    1 gdb参数列表 启动程序准备调试 gdb your proceduce 或者先输入gdb 然后输入 file your proceduce 然后使用run或者r命令开始程序的执行 也可以使用 run parameter将参数传递给该程序
  • 核酸检测安排

    题目描述 在系统 网络均正常的情况下组织核酸采样员和志愿者对人群进行核酸检测筛查 每名采样员的效率不同 采样效率为N人 小时 由于外界变化 采样员的效率会以M人 小时为粒度发生变化 M为采样效率浮动粒度 M N 10 输入保证N 10 的结
  • 软件测试工程师(4k~6k)的工作怎么找?转行IT人特别是应届生得好好看看这篇文章了...

    前言 作为一个入行软件测试10多年的老兵来说 最初我的工作也不是做软件测试的 只是一个偶然后机会可以转到这个行业 所以就豪不犹豫的转到这个行业 虽然前期会感觉有点压力 毕竟没有真正的做过 但是只要在工作中保持积极乐观的态度 多问 多学 多实
  • C语言数据结构篇——用栈实现四则运算

    作者名 Demo不是emo 主页面链接 主页传送门创作初心 舞台再大 你不上台 永远是观众 没人会关心你努不努力 摔的痛不痛 他们只会看你最后站在什么位置 然后羡慕或鄙夷座右铭 不要让时代的悲哀成为你的悲哀专研方向 网络安全 数据结构 每日
  • 【RocketMQ】NameServer总结

    NameServer是一个注册中心 提供服务注册和服务发现的功能 NameServer可以集群部署 集群中每个节点都是对等的关系 没有像ZooKeeper那样在集群中选举出一个Master节点 节点之间互不通信 服务注册 Broker启动的
  • “左三圈右三圈”,莫言开收割机 收割大批网友喜爱

    昨晚十点 莫言公众号如期更新 半夜就有网友在朋友圈里奔走相告 莫言为大家表演开联合收割机啦 看完莫言紧张又努力开收割机的视频 网友直呼 莫言老师抓紧方向盘使劲转动的样子太可爱了 爱了 爱了 在大家的印象中 莫言是深邃而丰腴的大作家 但这个视
  • [STM32F1]STM32上的DWT与延时实现

    对于做单片机程序开发来说 定时管理的需求非常普遍 不管是 系统滴答定时器systick计数器延时或者是通过外设定时器timer的向上向下等计数来延时 甚至在精度要求不高的地方还可以通过变量自加判断来延时 这都是一种延时管理实现的方法 但是对
  • java并发的概念

    1 并发的概念 并发 concurrency 指在同一时刻只能有一条指令执行 但多个进程指令被快速的轮换执行 使得在宏观上具有多个进程同时执行的效果 但在微观上并不是同时执行的 只是把时间分成若干段 使多个进程快速交替的执行 2 并行的概念
  • Ubuntu配置中文环境

    用了一段时间的英文开发了想起来要不换中文试试 所以闲暇之余配置了一个中文 做了一个小记录 这个是英文的环境下面的界面 在安装前简称系统的网络 和源是否正常 检查网络ping www baidu com 检查源 cat etc apt sou
  • 老板的思维模式:投资与浪费

    有人说 人生最大的投资 不是房子 不是股票 是人 是跟什么人交往 跟随什么人 交什么样的朋友 其实就是你投资什么人 而这是对人生影响最大的 钱不会给你机会 股票不会 房子也不会 只有人会给你机会 当你需要帮助的时候 只有跟你要好的人会帮你
  • 企业运维实践-如何在K8S集群环境Gitlab+Jenkins+Jmeter+Grafana技术中实现自动化分布压力测试数据展示...

    关注 WeiyiGeek 公众号 设为 特别关注 每天带你玩转网络安全运维 应用开发 物联网IOT学习 本章目录 0x00 前言简述 0x01 安装配置 在 Windows 中安装 Apache jmeter 工具 以二进制方式安装Helm

随机推荐

  • STM32 定时器

    include timer h include led h 本程序只供学习使用 未经作者许可 不得用于其它任何用途 Mini STM32开发板 通用定时器 驱动代码 正点原子 ALIENTEK 技术论坛 www openedv com 修改
  • 关于char类型变量输入与输出的区别

    笔者前几天看到了一个小项目 请输入一个小写字母 输出对应的大写字母 乍一看挺简单 可实际操作却难倒了我 直到我打开看了老师的视频之后 我才恍然大悟 char的输入其实输入的永远是数字 没有单纯的字符 与此同时char的输出却有两种形式 c对
  • covariance matrix r语言_时间序列分析

    这是关于时间序列的第N篇文章 本文将介绍ARIMAX模型 简单来说就是在ARIMA的基础上增加一个外生变量 ARIMAX和ARIMA相比在理论上没有太多新的内容 所以本文直接介绍在R里怎么一步一步跑ARIMAX 在阅读这篇文章前 需要对AR
  • MySQL 的主从复制原理详解高级

    首先要明白为什么要用 mysql 的主从复制 1 在从服务器可以执行查询工作 即我们常说的读功能 降低主服务器压力 主库写 从库读 降压 2 在从主服务器进行备份 避免备份期间影响主服务器服务 确保数据安全 3 当主服务器出现问题时 可以切
  • eclipse中package,source folder和folder

    在eclipse的Package explorer中 如下图所示 Source folder 存放Java的源代码 eclipse会自动编译里面的文件 以 来进行文件夹的分级 默认为src文件夹 Package 一般位于source fol
  • 【深度学习】 Python 和 NumPy 系列教程(六):Python容器:4、字典Dictionary详解(初始化、访问元素、常用操作、常用函数、遍历、解析)

    目录 一 前言 二 实验环境 三 Python容器 Containers 0 容器介绍 4 字典 Dictionary 0 基本概念 1 初始化 a 使用 创建字典 b 使用dict 函数创建字典 2 访问字典元素 a 使用方括号 b 使用
  • nexus下载安装

    进入官网http www sonatype org 点击Jion Now 展开downloads 选择Nexus Repository Manager OSS 目前已经更新到3 X了 这里暂且还是选2 X的吧 下载完 解压 cmd打开命令提
  • 【软件工程】第五章 结构化设计

    5 1 结构化设计的概念 5 1 1 设计的定义 何谓设计 一种软件开发活动 定义实现需求规约所需的软件结构 目标 依据需求规约在一个抽象层上建立系统软件模型 包括软件体系结构 数据和程序结构 以及详细的处理算法 给出软件解决方案 产生设计
  • 欧拉回路【总结】【题解】

    题目 欧拉回路 UOJ 欧拉回路 Liuser s OJ 题目描述 有一天一位灵魂画师画了一张图 现在要你找出欧拉回路 即在图中找一个环使得每条边都在环上出现恰好一次 一共两个子任务 无向图 有向图 输入格式 第一行一个整数 t 表示子任务
  • vue项目启动后,js-base64依赖报错Cannot read properties of null (reading ‘replace’)

    cannot read properties of null reading replace 关于这种乱七八糟的问题 咱也不敢说 在哪也不敢问 项目运行之后 有一些警告 都是一些依赖版本的问题 平时也能直接给运行起来 这次就是项目可以运行起
  • rabbitmq+springboot实现幂等性操作

    文章目录 1 场景描述 1 1 场景1 1 2 场景2 2 原理 3 实战开发 3 1 建表 3 2 集成mybatis plus 3 3 集成RabbitMq 3 3 1 安装mq 3 3 2 springBoot集成mq 3 4 具体实
  • 阿里云服务器使用xshell连接

    阿里云服务器使用xshell连接 当购买了第一次阿里云服务器时 如何使用xshell连接 其实是非常简单的 1 登录阿里云控制台 1 是你的阿里云服务器所在地址 2 是公网IP 将来远程连接时需要使用 3 是设置远程连接的密码 用户名默认r
  • 在x86和arm编译libmodbus

    编译libmodbus 下载路径 1 编译准备 sudo apt get install libtool autogen sh 2 arm编译 autogen sh mkdir install configure ac cv func ma
  • 电压电流的驱动能力分析以及计算方法

    文章为笔者学习过程中看到的 感觉帮助较大 分享出来希望能帮助到大家 在电子电路中为什么有的地方电压会被拉低2 驱动能力是什么意思 如何提高驱动能力 在很多资料上看到说驱动能力不够是因为提供的电流太小 为什么不说电压呢 在很多限制的条件都是电
  • BIOS开启虚拟化技术

    什么是BIOS BIOS 是一个内置于个人计算机的程序 当您打开计算机时该程序启动操作系统 也称为系统固件 BIOS 是计算机硬件的一部分 不同于 Windows 怎么进入BIOS 电脑进入BIOS的方法各有不同 通常会在开机时 显示电脑l
  • atoi函数源代码

    atoi函数源代码 isspace int x if x x t x n x f x b x r return 1 else return 0 isdigit int x if x lt 9 x gt 0 return 1 else ret
  • CPU是如何读写内存的?

    如果你不知道CPU是如何读写内存的 那你应该好好看看这篇文章 如果你觉得这是一个非常简单的问题 那你更应该好好读读本文 这个问题绝没有你想象那么简单 一定要读完 闲话少说 让我们来看看CPU在读写内存时底层究竟发生了什么 1 谁来告诉CPU
  • Mybatis二级缓存应用场景和局限性

    二级缓存应用场景 对查询频率高 变化频率低的数据建议使用二级缓存 对于访问多的查询请求且用户对查询结果实时性要求不高 此时可采用mybatis二级缓存技术降低数据库访问量 提高访问速度 业务场景比如 耗时较高的统计分析sql 电话账单查询s
  • ChatGPT是否具有记忆能力?

    ChatGPT在某种程度上具有记忆能力 但它的记忆能力有限且不像人类的记忆那样全面和持久 以下是对ChatGPT的记忆能力的详细分析 1 上下文记忆 ChatGPT可以在对话过程中记住先前的对话历史 以便更好地理解和回应后续的问题 通过将上
  • 带你了解并实践monorepo和pnpm,绝对干货!熬夜总结!

    大厂技术 高级前端 Node进阶 点击上方 程序员成长指北 关注公众号 回复1 加入高级Node交流群 为什么使用monorepo 什么是monorepo 简单来说就是 将多个项目或包文件放到一个git仓库来管理 目前比较广泛应用的是yar