Skip to content

使用 Pnpm + Vite + Typescript 搭建组件库

初衷

最近在看 element-plus、vant 这类适配 vue3 的组件库的时候,发现这些第三方的组件库都在用 pnpm 来管理整个项目,而恰巧 pnpm 是我之前未接触过的,于是就有了想法为何不像 element-plus 一样用 pnpm 搭建一套属于自己的组件库呢?

提示

项目结构部分代码参考 element-plus

参考 element-plus 项目结构

element-plus
├─ docs      # 文档
├─ packages  # 子包
├─ internal  # 构建
└─ types     # 类型
└─ package.json

在研究 element 项目代码的时候发现,element 是将多个项目放在同一个模块下开发的,而如果看过 vant、vue3 等项目会发现都是用的类似的结构,经过一番查找发现这种项目结构叫做 Monorepo,那么什么是 Monorepo 呢?

Monorepo

Monorepo 是一种将多个项目存放在同一个代码库中的开发策略

element、vant、vue3 等我们熟悉的项目都是基于 Monorepo 打造的,目前流行的 pnpm 的 workspace 可以支持 Monorepo。

项目结构

pnpm-ui
├─ docs      # 文档
├─ packages  # 子包
├─ internal  # 构建
└─ package.json

Pnpm

安装 pnpm

sh
npm install pnpm -g

创建项目目录

sh
mkdir pnpm-ui

进入项目

sh
cd pnpm-ui

初始化项目

该步骤会在 pnpm-ui 根目录下生成 package.json

sh
pnpm init

创建 pnpm-workspace.yaml

pnpm-workspace.yaml 是定义了工作空间的根目录,可以通过通配符来包含和排除不需要的目录

yaml
packages:
  - packages/*
  - docs
  - play
  - internal/*

创建 .npmrc

pnpm 可以从 .npmrc 获取配置

ini
shamefully-hoist=true
strict-peer-dependencies=false
shell-emulator=true
fetch-retries=5
fetch-retry-mintimeout=200000
fetch-retry-maxtimeout=1200000
fetch-timeout=1800000
registry=https://registry.npm.taobao.org

很多时候网络原因包下载不下来,这里的 fetch-retriesfetch-retry-maxtimeoutfetch-timeoutregistry 可以根据自己的网络情况决定是否配置。

安装 vue

sh
pnpm install vue -D -w

配置 typescript

安装

pnpm install typescript -D -w

配置

在阅读 element-plus 源码的时候,发现 element 将 tsconfig 根据项目分成了几个部分:

  • tsconfig.json
  • tsconfig.base.json
  • tsconfig.web.json
  • tsconfig.play.json
  • tsconfig.node.json
  • tsconfig.vite-config.json
  • tsconfig.vitest.json

占时用不到 tsconfig.vitest.json,所以删除 tsconfig.vitest.json 并且保留其他配置。

于是下面是详细的代码配置(几个文件都在根目录下):

tsconfig.json
json
{
  "files": [],
  "references": [
    { "path": "./tsconfig.web.json" },
    { "path": "./tsconfig.play.json" },
    { "path": "./tsconfig.node.json" },
    { "path": "./tsconfig.vite-config.json" }
  ]
}
tsconfig.base.json
json
{
  "compilerOptions": {
    "outDir": "dist",
    "target": "es2018",
    "module": "esnext",
    "baseUrl": ".",
    "sourceMap": false,
    "moduleResolution": "node",
    "allowJs": false,
    "strict": true,
    "noUnusedLocals": true,
    "resolveJsonModule": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "removeComments": false,
    "rootDir": ".",
    "types": [],
    "paths": {
      "@pnpm-ui/*": ["packages/*"]
    }
  }
}
tsconfig.web.json
json
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "composite": true,
    "jsx": "preserve",
    "lib": ["ES2018", "DOM", "DOM.Iterable"],
    "types": ["unplugin-vue-macros/macros-global"],
    "skipLibCheck": true
  },
  "include": ["packages", "typings/components.d.ts", "typings/env.d.ts"],
  "exclude": [
    "node_modules",
    "**/dist"
  ]
}
tsconfig.play.json
json
{
  "extends": "./tsconfig.web.json",
  "compilerOptions": {
    "allowJs": true,
    "lib": ["ESNext", "DOM", "DOM.Iterable"]
  },
  "include": [
    "packages",
    "typings/components.d.ts",
    "typings/env.d.ts",

    // playground
    "play/main.ts",
    "play/env.d.ts",
    "play/src/**/*"
  ]
}
tsconfig.node.json
json
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "composite": true,
    "lib": ["ESNext"],
    "types": ["node"],
    "skipLibCheck": true
  },
  "include": [
    "internal/**/*",
    "internal/**/*.json",
  ],
  "exclude": []
}
tsconfig.vite-config.json
json
{
  "extends": "./tsconfig.node.json",
  "compilerOptions": {
    "composite": true,
    "types": ["node"]
  },
  "include": ["**/vite.config.*", "**/vite.init.*"],
  "exclude": []
}

错误处理

找不到“node”的类型定义文件

配置完上述配置后会发现 tsconfig.node.json 报了以下错误:

找不到“node”的类型定义文件。
  程序包含该文件是因为:
    在 compilerOptions 中指定的类型库 "node" 的入口点

这时候需要全局安装 @types/node

sh
pnpm install @types/node -D -w

这时候就会发现上面的问题解决了~

include、exclude 报错

这是因为读取不到相关的文件,把报错的地方注释掉就好了~

配置 eslint + prettier

配置 eslint 和 prettier 是为了统一代码的质量风格,在阅读 element-plus 项目的时候,我们发现 element 把 eslint 主要规则放在根目录的 internal 文件下,这种做法让整个 eslint 配置更直观且易于维护,因此我们就直接参照 element 就好了。

项目安装 eslint + prettier

sh
pnpm install eslint prettier -D -w

配置 eslint-config 包

创建 eslint-config

sh
mkdir internal/eslint-config

init

然后再定位到 internal/eslint-config 文件下执行 pnpm init 生成 package.json

sh
pnpm init

把修改下 internal/eslint-config 文件下 package.json 的包名改成 @pnpm-ui/eslint-config

package.json
json
{
  "name": "@pnpm-ui/eslint-config",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

安装包

internal/eslint-config 执行以下命令:

sh
pnpm install eslint prettier typescript @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier eslint-define-config eslint-plugin-eslint-comments eslint-plugin-import eslint-plugin-jsonc eslint-plugin-markdown eslint-plugin-prettier eslint-plugin-unicorn eslint-plugin-vue jsonc-eslint-parser yaml-eslint-parser -D -F @pnpm-ui/eslint-config

pnpm install -F packagename 的作用是在 packagename 下安装包,所有上述命令的作用就是在 internal/eslint-config 目录下安装上面的这些包。

简单描述下一些包的作用:

  • eslint-define-config:eslint 语法提示
  • prettier、eslint-config-prettier、eslint-plugin-prettier: 兼容 eslint 配置,防止规则冲突报错
  • eslint-plugin-eslint-comments:用来检查eslint 的特殊注解
  • eslint-plugin-import:让 import 导入自动排序,让代码更简洁,更加清晰
  • eslint-plugin-jsonc:为 JSON, JSONC and JSON5 提供验证规则
  • eslint-plugin-markdown:为 markdown 提供规则
  • eslint-plugin-unicorn:内置很多有用的 eslint 规则

创建 eslint-config 规则

internal/eslint-config/index.js
js
const { defineConfig } = require('eslint-define-config')

module.exports = defineConfig({
  env: {
    es6: true,
    browser: true,
    node: true,
  },
  plugins: ['@typescript-eslint', 'prettier', 'unicorn'],
  extends: [
    'eslint:recommended',
    'plugin:import/recommended',
    'plugin:eslint-comments/recommended',
    'plugin:jsonc/recommended-with-jsonc',
    'plugin:markdown/recommended',
    'plugin:vue/vue3-recommended',
    'plugin:@typescript-eslint/recommended',
    'prettier',
  ],
  settings: {
    'import/resolver': {
      node: { extensions: ['.js', '.mjs', '.ts', '.d.ts', '.tsx'] },
    },
  },
  overrides: [
    {
      files: ['*.json', '*.json5', '*.jsonc'],
      parser: 'jsonc-eslint-parser',
    },
    {
      files: ['*.ts', '*.vue'],
      rules: {
        'no-undef': 'off',
      },
    },
    {
      files: ['package.json'],
      parser: 'jsonc-eslint-parser',
      rules: {
        'jsonc/sort-keys': [
          'error',
          {
            pathPattern: '^$',
            order: [
              'name',
              'version',
              'private',
              'packageManager',
              'description',
              'type',
              'keywords',
              'homepage',
              'bugs',
              'license',
              'author',
              'contributors',
              'funding',
              'files',
              'main',
              'module',
              'exports',
              'unpkg',
              'jsdelivr',
              'browser',
              'bin',
              'man',
              'directories',
              'repository',
              'publishConfig',
              'scripts',
              'peerDependencies',
              'peerDependenciesMeta',
              'optionalDependencies',
              'dependencies',
              'devDependencies',
              'engines',
              'config',
              'overrides',
              'pnpm',
              'husky',
              'lint-staged',
              'eslintConfig',
            ],
          },
          {
            pathPattern: '^(?:dev|peer|optional|bundled)?[Dd]ependencies$',
            order: { type: 'asc' },
          },
        ],
      },
    },
    {
      files: ['*.d.ts'],
      rules: {
        'import/no-duplicates': 'off',
      },
    },
    {
      files: ['*.js'],
      rules: {
        '@typescript-eslint/no-var-requires': 'off',
      },
    },
    {
      files: ['*.vue'],
      parser: 'vue-eslint-parser',
      parserOptions: {
        parser: '@typescript-eslint/parser',
        extraFileExtensions: ['.vue'],
        ecmaVersion: 'latest',
        ecmaFeatures: {
          jsx: true,
        },
      },
      rules: {
        'no-undef': 'off',
      },
    },

    {
      files: ['**/*.md/*.js', '**/*.md/*.ts'],
      rules: {
        'no-console': 'off',
        'import/no-unresolved': 'off',
        '@typescript-eslint/no-unused-vars': 'off',
      },
    },
  ],
  rules: {
    // js/ts
    camelcase: ['error', { properties: 'never' }],
    'no-console': ['warn', { allow: ['error'] }],
    'no-debugger': 'warn',
    'no-constant-condition': ['error', { checkLoops: false }],
    'no-restricted-syntax': ['error', 'LabeledStatement', 'WithStatement'],
    'no-return-await': 'error',
    'no-var': 'error',
    'no-empty': ['error', { allowEmptyCatch: true }],
    'prefer-const': [
      'warn',
      { destructuring: 'all', ignoreReadBeforeAssign: true },
    ],
    'prefer-arrow-callback': [
      'error',
      { allowNamedFunctions: false, allowUnboundThis: true },
    ],
    'object-shorthand': [
      'error',
      'always',
      { ignoreConstructors: false, avoidQuotes: true },
    ],
    'prefer-rest-params': 'error',
    'prefer-spread': 'error',
    'prefer-template': 'error',

    'no-redeclare': 'off',
    '@typescript-eslint/no-redeclare': 'error',

    // best-practice
    'array-callback-return': 'error',
    'block-scoped-var': 'error',
    'no-alert': 'warn',
    'no-case-declarations': 'error',
    'no-multi-str': 'error',
    'no-with': 'error',
    'no-void': 'error',

    'sort-imports': [
      'warn',
      {
        ignoreCase: false,
        ignoreDeclarationSort: true,
        ignoreMemberSort: false,
        memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'],
        allowSeparatedGroups: false,
      },
    ],

    // stylistic-issues
    'prefer-exponentiation-operator': 'error',

    // ts
    '@typescript-eslint/explicit-module-boundary-types': 'off',
    '@typescript-eslint/no-explicit-any': 'off',
    '@typescript-eslint/no-non-null-assertion': 'off',
    '@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
    '@typescript-eslint/consistent-type-imports': [
      'error',
      { disallowTypeAnnotations: false },
    ],
    '@typescript-eslint/ban-ts-comment': ['off', { 'ts-ignore': false }],

    // vue
    'vue/no-v-html': 'off',
    'vue/require-default-prop': 'off',
    'vue/require-explicit-emits': 'off',
    'vue/multi-word-component-names': 'off',
    'vue/prefer-import-from-vue': 'off',
    'vue/no-v-text-v-html-on-component': 'off',
    'vue/html-self-closing': [
      'error',
      {
        html: {
          void: 'always',
          normal: 'always',
          component: 'always',
        },
        svg: 'always',
        math: 'always',
      },
    ],

    // prettier
    'prettier/prettier': 'error',

    // import
    'import/first': 'error',
    'import/no-duplicates': 'error',
    'import/order': [
      'error',
      {
        groups: [
          'builtin',
          'external',
          'internal',
          'parent',
          'sibling',
          'index',
          'object',
          'type',
        ],

        pathGroups: [
          {
            pattern: 'vue',
            group: 'external',
            position: 'before',
          },
          {
            pattern: '@vue/**',
            group: 'external',
            position: 'before',
          },
          {
            pattern: '@w-ui/**',
            group: 'internal',
          },
        ],
        pathGroupsExcludedImportTypes: ['type'],
      },
    ],
    'import/no-unresolved': 'off',
    'import/namespace': 'off',
    'import/default': 'off',
    'import/no-named-as-default': 'off',
    'import/no-named-as-default-member': 'off',
    'import/named': 'off',

    // eslint-plugin-eslint-comments
    'eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }],
  },
})

引入 eslint-config

项目根目录安装 @pnpm-ui/eslint-config

项目根目录执行命令

sh
pnpm install @pnpm-ui/eslint-config -D -w

创建 eslintrc

在项目根目录下创建 .eslintrc.json

sh
touch .eslintrc.json

.eslintrc.json 内容为:

json
{
  "root": true,
  "extends": ["@pnpm-ui/eslint-config"]
}

配置 prettier

创建 .prettierrc

json
{
  "semi": false,
  "singleQuote": true,
  "overrides": [
    {
      "files": ".prettierrc",
      "options": {
        "parser": "json"
      }
    }
  ]
}

创建 .prettierignore

json
dist
node_modules
pnpm-lock.yaml

配置 settings

到这里你会发现我们已经配置了 eslint 和 prettier 相关的内容,但是修改文件还是不会生效!

这是因为我们没有配置相关的规则让我们在保存的时候生效

在根目录创建 .vscode 文件夹

sh
mkdir .vscode

.vscode 下创建 settings.json

json
{
  "cSpell.words": ["Pnpm Ui", "pnpm-ui"],
  "typescript.tsdk": "node_modules/typescript/lib",
  "editor.formatOnSave": true,
  "npm.packageManager": "pnpm",
  "eslint.probe": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact",
    "html",
    "vue",
    "markdown",
    "json",
    "jsonc"
  ],
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact",
    "html",
    "vue",
    "markdown",
    "json",
    "jsonc"
  ],
  "vite.devCommand": "pnpm run dev -- --",
}

注意

注意这里配置后需要重启下编辑器!

这个时候发现当修改项目下的文件后保存会自动格式化啦~

提醒

以上就是 pnpm + monorepo 基本的项目配置了,下面我们开始创建 play 测试项目吧!

play

安装相关配置

首先要在项目根目录创建 play 文件夹

sh
mkdir play

然后进入 play

sh
cd play

执行 pnpm init 创建 package.json

sh
pnpm init

修改 paly 下 package.json 名称为:@pnpm-ui/play

然后安装相关包:

sh
pnpm install vite @vitejs/plugin-vue unplugin-vue-components unplugin-vue-components unplugin-vue-macros -D -F @pnpm-ui/play

在 play 下的 package.json 添加相关运行的 scripts:

"scripts": {
  "dev": "vite",
  "build": "vue-tsc && vite build",
  "preview": "vite preview"
}

创建 index.html

/paly/index.html
html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>W UI App</title>
    <!-- element css cdn -->
  </head>

  <body>
    <div id="play"></div>
    <script type="module" src="/main.ts"></script>
  </body>
</html>

创建入口 vue 文件

首先在 /play 文件夹下创建 src 文件夹,并在 src 文件夹下创建 App.vue 文件和 .gitignore 文件

mkdir /play/src
cd /play/src
touch App.vue
touch .gitignore

.gitignore 内容:

*
!.gitignore

表示忽略 src 文件夹下面的所有文件,不做提交,这个文件夹下的文件都是本地测试用的

App.vue 内容:

vue
<template>App</template>

<script lang="ts" setup></script>

接下来在 /play 创建 app.example.vue 文件,内容为:

vue
<template>
  <div>app.example</div>
</template>

<script lang="ts" setup></script>

创建 main.ts

ts
import { createApp } from 'vue'
;(async () => {
  const apps = import.meta.glob('./src/*.vue')
  const name = location.pathname.replace(/^\//, '') || 'App'
  const file = apps[`./src/${name}.vue`]
  if (!file) {
    location.pathname = 'App'
    return
  }
  const App = ((await file()) as unknown as any).default
  const app = createApp(App)

  app.mount('#play')
})()

这时候你会发现 import.meta.glob 语法爆红了,不过没关系,我们只需要在 /play 文件夹下创建 env.d.ts 文件,并将下面内容放进去就好了:

/// <reference types="vite/client" />

或者在在 tsconfig 中增加:

json
{
  "compilerOptions": {
    "types": ["vite/client"]
  }
}

创建 vite.init.ts

ts
import { existsSync, readFileSync, writeFileSync } from 'fs'

const app = 'src/App.vue'
const example = 'app.example.vue'

if (!existsSync(app)) {
  writeFileSync(app, readFileSync(example))
}

创建 vite.config.ts

/play/vite.config.ts
ts
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import VueMacros from 'unplugin-vue-macros/vite'

import './vite.init'

export default defineConfig(async ({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '')

  return {
    server: {
      host: true,
      https: !!env.HTTPS,
    },
    plugins: [
      VueMacros({
        setupComponent: false,
        setupSFC: false,
        plugins: {
          vue: vue(),
        },
      }),
      Components({
        dirs: `${__dirname}/../packages/components/**`,
        extensions: ['vue'],
        dts: false,
        resolvers: [
          (componentName) => {
            if (componentName.startsWith('P'))
              return { name: componentName, from: '@pnpm-ui/components' }
          },
        ],
      }),
    ],
    optimizeDeps: {
      include: ['vue', '@vue/shared'],
    },
    esbuild: {
      target: 'chrome64',
    },
  }
})

在根目录下 package.json 添加 scripts

json
"scripts": {
  "dev": "pnpm -C play dev",
}

测试运行

在根目录下运行:

sh
pnpm dev

如果没什么问题的话,项目就正常跑起来啦!

提示

通过上面的步骤,我们已经把测试组件的项目搭建完成,接下来就要开始开发我们的组件了!

组件搭建

我们首先要在 /packages 目录下创建 components 文件夹,后续的 ui 组件都会放在 components 文件夹下。

sh
mkdir packages/components

components 内容如下:

/packages/components
├─ button
  ├─src
    ├─button.vue
  ├─index.ts    
├─ index.ts
└─ package.json

init

在 packages/components 执行 pnpm init 生成 package.json

sh
pnpm init

修改 package.json 的 name:

json
{
  "name": "@pnpm-ui/components",
  "version": "0.0.1",
  "description": "all components are settled here",
  "main": "index.ts",
  "module": "index.ts",
  "unpkg": "index.js",
  "jsdelivr": "index.js",
  "types": "index.d.ts",
  "sideEffects": false
}

创建 button 组件

这里只做简单的演示

在 packages/components/ 下创建 button 文件夹

sh
mkdir packages/components/button

在 packages/components/button 创建 src 文件夹用于存放 相关组件

sh
mkdir packages/components/button/src

在 packages/components/button/src 创建 button.vue 文件

sh
touch packages/components/button/src/button.vue

button.vue 中内容

vue
<template>
  <div>this is button component</div>
</template>

<script lang="ts" setup>
defineOptions({
  name: 'PButton',
})
</script>

导出 button

在 packages/components/button 下创建 index.ts

sh
touch packages/components/button/index.ts

packages/components/button/index.ts 中内容如下:

ts
import _button from './src/button.vue'

export const PButton = _button

export default PButton

在 packages/components 下创建 index.ts 引入 button

sh
touch packages/components/index.ts

packages/components/index.ts 内容如下

ts
export * from './button'

安装 @pnpm-ui/components

根目录下安装 @pnpm-ui/components

sh
pnpm install @pnpm-ui/components -D -w

到这里基本上一个简单的 ui 组件就已经搭建完成了,下面测试下组件。

测试 PButton

现在我们稍微修改下 play/src/App.vue 的文件内容,将 PButton 引入,代码如下

vue
<template>
  App
  <p-button />
</template>

<script lang="ts" setup></script>

然后在根目录运行:

sh
pnpm dev

不出意外的话,我们的按钮就展示到页面上了!

那么接下来开始做规范化提交相关的配置啦!

规范化 commit message

初始化 git

根目录运行

sh
git init

安装相关包

项目根目录安装:

sh
pnpm install @commitlint/cli @commitlint/config-conventional cz-git czg husky lint-staged @esbuild-kit/cjs-loader fast-glob -D -w

在项目根目录创建 commitlint.config.jscommitlint.config.ts

commitlint.config.js 内容如下:

js
require('@esbuild-kit/cjs-loader')
module.exports = require('./commitlint.config.ts').default

commitlint.config.ts 内容如下:

commitlint.config.ts
ts
import { execSync } from 'child_process'
import fg from 'fast-glob'

const getPackages = (packagePath: string) =>
  fg.sync('*', { cwd: packagePath, onlyDirectories: true })

const scopes = [
  ...getPackages('packages'),
  ...getPackages('internal'),
  'docs',
  'play',
  'project',
  'core',
  'style',
  'ci',
  'dev',
  'deploy',
  'other',
  'typography',
  'color',
  'border',
  'var',
  'ssr',
]

const gitStatus = execSync('git status --porcelain || true')
  .toString()
  .trim()
  .split('\n')

const scopeComplete = gitStatus
  .find((r) => ~r.indexOf('M  packages'))
  ?.replace(/\//g, '%%')
  ?.match(/packages%%((\w|-)*)/)?.[1]

const subjectComplete = gitStatus
  .find((r) => ~r.indexOf('M  packages/components'))
  ?.replace(/\//g, '%%')
  ?.match(/packages%%components%%((\w|-)*)/)?.[1]

export default {
  rules: {
    /**
     * type[scope]: [function] description
     *      ^^^^^
     */
    'scope-enum': [2, 'always', scopes],
    /**
     * type[scope]: [function] description
     *
     * ^^^^^^^^^^^^^^ empty line.
     * - Something here
     */
    'body-leading-blank': [1, 'always'],
    /**
     * type[scope]: [function] description
     *
     * - something here
     *
     * ^^^^^^^^^^^^^^
     */
    'footer-leading-blank': [1, 'always'],
    /**
     * type[scope]: [function] description [No more than 72 characters]
     *      ^^^^^
     */
    'header-max-length': [2, 'always', 72],
    'scope-case': [2, 'always', 'lower-case'],
    'subject-case': [
      1,
      'never',
      ['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
    ],
    'subject-empty': [2, 'never'],
    'subject-full-stop': [2, 'never', '.'],
    'type-case': [2, 'always', 'lower-case'],
    'type-empty': [2, 'never'],
    /**
     * type[scope]: [function] description
     * ^^^^
     */
    'type-enum': [
      2,
      'always',
      [
        'build',
        'chore',
        'ci',
        'docs',
        'feat',
        'fix',
        'perf',
        'refactor',
        'revert',
        'release',
        'style',
        'test',
        'improvement',
      ],
    ],
  },
  prompt: {
    defaultScope: scopeComplete,
    customScopesAlign: !scopeComplete ? 'top' : 'bottom',
    defaultSubject: subjectComplete && `[${subjectComplete}] `,
    allowCustomIssuePrefixs: false,
    allowEmptyIssuePrefixs: false,
  },
}

修改 package.json

json
"scripts": {
  "cz": "czg",
},
"config": {
  "commitizen": {
    "path": "./node_modules/cz-git"
  }
},
"lint-staged": {
  "*.{vue,js,ts,jsx,tsx,md,json}": "eslint --fix"
}

配置 husky

执行 init

sh
npx husky-init

添加 hooks

sh
npx husky add .husky/commit-msg 'pnpm exec commitlint --config commitlint.config.js --edit "${1}"'

这时候会发现项目根目录下创建了 .husky 文件夹,.husky 结构如下:

📦.husky
 ┣ 📂_
 ┃ ┣ 📜.gitignore
 ┃ ┗ 📜husky.sh
 ┣ 📜commit-msg
 ┗ 📜pre-commit

commit-msg 内容修改如下:

sh
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

pnpm exec commitlint --config commitlint.config.js --edit "${1}"

pre-commit 内容修改如下:

sh
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

pnpm exec lint-staged

.gitignore

# Editor directories and files
.idea

# Package Manager
node_modules
.pnpm-debug.log*

# System
.DS_Store

# Bundle
dist

# local env files
*.local
.eslintcache

到此项目的规范化 commit 就完成啦!

总结

除了组件库的项目文档外,基本上到这里就完成了 pnpm + monorepo 的 ui 组件库的项目搭建了!

后面有时间把文档搭建也加上~