【实践】Monorepo 工程化:沉淀可复用的配置规则
一、背景介绍在上次完成最小可用 Vue Monorepo 之后,我们遇到一个关键问题:配置一旦被复制成 N 份,就不再是统一规范,而是会各自独立演化的副本。Monorepo 提供了更优雅的方案:把配置本身当作 npm 包发布到 workspace 内部,其他包通过继承这些配置来生效。例如 TypeScript 的 extends、ESLint 的 import flat config、函数工厂(vitest 预设)。这样可以做到:升级配置只改一个地方,所有包同步生效。这才是 Monorepo 真正的杠杆所在——不是更快地构建,而是更低成本地维护规范。二、方案分析本次主要解决“好协作”问题,让多人/多包开发在同一套规则下进行,并把这些规则沉淀成可复用的 package。将 ESLint、TypeScript、Vitest 三类配置从“每个包各复制一份”,改为“通过依赖一份共享包来继承”。2.1 整体方案新增 3 个内部基础包@repo/tsconfig:所有包的 TS 配置基线@repo/eslint-config:所有包的 ESLint 规则基线(flat config)@repo/vitest-preset:所有包的 Vitest 预设工厂为现有 3 个包接入基础设施apps/web、packages/ui、packages/utils从共享包继承配置为packages/utils、packages/ui补充单元测试提供统一脚本:pnpm lint/pnpm typecheck/pnpm test/pnpm format根目录补充提交侧守护Prettier + EditorConfig:统一代码格式husky + lint-staged:commit 前自动 lint 改动文件commitlint:约束提交信息风格三、实操步骤步骤 1:共享 TypeScript 配置(@repo/tsconfig)1.1 包结构packages/tsconfig/ ├── package.json ├── base.json # 通用基线 ├── vue.json # Vue 项目变体 └── node.json # Node 项目变体1.2 package.json{"name":"@repo/tsconfig","version":"0.0.0","private":true,"files":["base.json","vue.json","node.json"]}注意:这个包没有任何代码入口,只是一组 JSON 文件的集合。pnpm 通过 workspace 链接将它放进消费者的 node_modules,TypeScript 的 extends 通过 Node resolution 找到。⚠ 注意:files是发布/共享 npm 包时的白名单配置,用来精确指定哪些文件/文件夹会被包含在最终发布的包里,未在列表中的文件默认不会被打包发布。1.3 base.json(通用基线){"$schema":"https://json.schemastore.org/tsconfig","compilerOptions":{"target":"ES2022","module":"ESNext","moduleResolution":"Bundler","lib":["ES2022"],"strict":true,"noUncheckedIndexedAccess":true,"noImplicitOverride":true,"esModuleInterop":true,"resolveJsonModule":true,"isolatedModules":true,"skipLibCheck":true,"verbatimModuleSyntax":true,"forceConsistentCasingInFileNames":true,"useDefineForClassFields":true}}几个有意为之的选择:strict: true+noUncheckedIndexedAccess:进一步收紧类型安全moduleResolution: "Bundler":现代打包器(Vite/esbuild)通用方案verbatimModuleSyntax: true:强制 import/export 语法精确,配合 ESLint 可以发现混用 type/value 的问题isolatedModules: true:保证每个文件可被独立转译,是 esbuild/SWC 的前置条件1.4 vue.json 与 node.jsonvue.json(在 base 基础上加 DOM 相关 lib){"extends":"./base.json","compilerOptions":{"jsx":"preserve","lib":["ES2022","DOM","DOM.Iterable"]}}node.json(在 base 基础上加 Node 类型){"extends":"./base.json","compilerOptions":{"lib":["ES2022"],"types":["node"]}}步骤 2:共享 ESLint 配置(@repo/eslint-config)ESLint 9 已经全面切到 flat config,这是当前主流,新建项目应直接使用 flat。2.1 包结构packages/eslint-config/ ├── package.json ├── base.js # 通用 TS/JS 规则 └── vue.js # 在 base 之上叠加 Vue 规则2.2 package.json 关键设计{"type":"module","name":"@repo/eslint-config","exports":{"./base":"./base.js","./vue":"./vue.js"},"dependencies":{"@eslint/js":"^9.13.0","eslint-config-prettier":"^9.1.0","eslint-plugin-vue":"^9.30.0","globals":"^15.11.0","typescript-eslint":"^8.12.0","vue-eslint-parser":"^9.4.3"},"peerDependencies":{"eslint":"^9.0.0"}}关键点:把 ESLint 插件作为 dependencies:消费包不必各自安装一堆插件,只需依赖@repo/eslint-config即可ESLint 本身放 peerDependencies:让消费方决定 ESLint 版本,避免多版本共存这是“配置即依赖”的核心模式:配置包负责规则版本管理,消费包只关心继承2.3 base.jsimportjsfrom"@eslint/js";importtseslintfrom"typescript-eslint";importprettierfrom"eslint-config-prettier";