在现代前端开发中,Monorepo(单体仓库)架构已经成为管理多个相关包的主流方案。无论是 React、Vue、还是 Angular 等知名框架,都采用了 Monorepo 的组织方式。本文将带您从零开始,一步步搭建一个功能完整的 Monorepo 开发模板,涵盖 TypeScript、Rollup 打包、Jest 测试、代码质量控制以及 CI/CD 持续集成等核心功能。
什么是 Monorepo?
Monorepo(Monolithic Repository)是一种代码组织策略,将多个相关的项目或包存储在同一个 Git 仓库中。与传统的多仓库(Multi-repo)相比,Monorepo 具有以下优势:
- 统一依赖管理:共享相同的依赖版本,避免版本冲突
- 简化跨包开发:可以同时修改多个包并保持同步
- 统一工具链:使用相同的构建、测试、代码质量工具
- 原子化提交:相关改动可以在一次提交中完成
- 更好的代码重用:包之间可以更容易地共享代码
项目初始化
1. 创建项目结构
首先创建项目根目录并初始化:
mkdir monorepo_rollup_tpl
cd monorepo_rollup_tpl
npm init -y
创建基本的目录结构:
mkdir -p packages/package1/src packages/package1/__tests__
mkdir -p packages/package2/src packages/package2/__tests__
mkdir -p .github/workflows
mkdir .changeset
最终的项目结构如下:
monorepo_rollup_tpl/
├── packages/
│ ├── package1/
│ │ ├── src/
│ │ ├── __tests__/
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ └── jest.config.ts
│ └── package2/
│ ├── src/
│ ├── __tests__/
│ ├── package.json
│ ├── tsconfig.json
│ └── jest.config.ts
├── .github/
│ └── workflows/
├── .changeset/
├── package.json
├── tsconfig.json
├── rollup.config.js
├── jest.config.ts
├── babel.config.js
└── eslint.config.mjs
2. 配置包管理器
本项目使用 pnpm
作为包管理器,它对 Monorepo 有原生支持。安装 pnpm:
npm install -g pnpm
创建 pnpm-workspace.yaml
文件来定义工作空间:
packages:
- 'packages/*'
TypeScript 配置
1. 根目录 TypeScript 配置
在根目录创建 tsconfig.json
,此处列出主要配置:
{
"compilerOptions": {
"target": "esnext",
"jsx": "react-jsx",
"module": "esnext",
"moduleResolution": "bundler",
"noEmit": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
关键配置说明:
"noEmit": true
:只做类型检查,不输出编译结果,编译交给 Rollup 处理"moduleResolution": "bundler"
:使用打包器的模块解析策略"jsx": "react-jsx"
:支持现代的 JSX 转换
2. 子包 TypeScript 配置
在每个子包目录下创建 tsconfig.json
:
{
"extends": "../../tsconfig.json", // 继承根目录配置文件
"compilerOptions": {
// 编译选项
"paths": {
// 设置应用别名
"package1": ["./src/index.ts"],
"package1/*": ["./src/*.ts"]
}
},
"include": ["src"] // 限定类型检查范围
}
Rollup 打包配置
Rollup 是一个专为库打包设计的工具,支持输出多种模块格式。创建 rollup.config.js
:
const createBabelConfig = require('./babel.config.js')
const resolve = require('@rollup/plugin-node-resolve')
const babelPlugin = require('@rollup/plugin-babel')
const commonjs = require('@rollup/plugin-commonjs')
const { dts } = require('rollup-plugin-dts')
const extensions = ['.ts', '.tsx']
const getBabelOptions = () => {
return {
...createBabelConfig,
extensions,
babelHelpers: 'bundled',
comments: false,
}
}
// TypeScript 声明文件配置
function createDeclarationConfig(input, output) {
return {
input,
output: {
file: output,
format: 'esm',
},
plugins: [dts()],
}
}
// ESM 格式配置
function createESMConfig(input, output) {
return {
input,
output: {
file: output,
format: 'esm',
},
plugins: [
resolve({ extensions }),
commonjs(),
babelPlugin(getBabelOptions()),
],
}
}
// CommonJS 格式配置
function createCJSConfig(input, output) {
return {
input,
output: {
file: output,
format: 'cjs',
},
plugins: [
resolve({ extensions }),
commonjs(),
babelPlugin(getBabelOptions()),
],
}
}
// UMD 格式配置
function createUMDConfig(input, output, name) {
return {
input,
output: {
file: output,
format: 'umd',
name,
},
plugins: [
resolve({ extensions }),
commonjs(),
babelPlugin(getBabelOptions()),
],
}
}
module.exports = (args) => {
const packageName = process.env.PACKAGE
const input = `packages/${packageName}/src/index.ts`
const output = `packages/${packageName}/dist`
return [
createDeclarationConfig(input, `${output}/index.d.ts`),
createESMConfig(input, `${output}/index.mjs`),
createCJSConfig(input, `${output}/index.cjs`),
createUMDConfig(input, `${output}/index.umd.js`, packageName),
]
}
Babel 配置
创建 babel.config.js
来配置 Babel 转换:
module.exports = {
presets: [['@babel/preset-env', { targets: { node: 14 } }]],
plugins: [
'@babel/plugin-transform-typescript',
'@babel/plugin-transform-react-jsx',
],
}
包的导出配置
在每个子包的 package.json
中配置导出字段:
{
"name": "package1",
"version": "1.0.0",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.cjs"
}
}
},
"scripts": {
"typecheck": "tsc"
}
}
Jest 测试配置
1. 根目录 Jest 配置
创建 jest.config.ts
:
export default {
projects: ['<rootDir>/packages/package1', '<rootDir>/packages/package2'],
}
创建 jest.preset.js
作为共享配置:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
testMatch: ['**/__tests__/**/*.(test|spec).(ts|tsx|js)'],
collectCoverageFrom: ['src/**/*.(ts|tsx)', '!src/**/*.d.ts'],
}
2. 子包 Jest 配置
在每个子包中创建 jest.config.ts
:
export default {
...require('../../jest.preset.js'),
displayName: 'package1',
}
代码质量控制
1. ESLint 配置
创建 eslint.config.mjs
:
import eslint from '@eslint/js'
import tseslint from '@typescript-eslint/eslint-plugin'
import tsParser from '@typescript-eslint/parser'
import prettierConfig from 'eslint-config-prettier'
export default [
eslint.configs.recommended,
{
files: ['**/*.{js,jsx,ts,tsx}'],
languageOptions: {
parser: tsParser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
},
plugins: {
'@typescript-eslint': tseslint,
},
rules: {
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/no-explicit-any': 'warn',
},
},
prettierConfig,
]
2. Prettier 配置
在 package.json
中添加 Prettier 配置:
{
"prettier": {
"semi": false,
"singleQuote": true
}
}
3. 构建脚本
在根目录 package.json
中添加构建和质量检查脚本:
{
"scripts": {
"test": "jest --passWithNoTests --config jest.config.ts",
"eslint": "eslint --fix '**/src/*.{js,jsx,ts,tsx}'",
"eslint:ci": "eslint '**/src/*.{js,jsx,ts,tsx}'",
"prettier": "prettier '**/{src,__tests__}/**/*.{js,jsx,ts,tsx,md}' --write",
"prettier:ci": "prettier '**/{src,__tests__}/**/*.{js,jsx,ts,tsx,md}' --list-different",
"typecheck": "pnpm -r --parallel run typecheck",
"build": "concurrently 'pnpm:build:*'",
"build:package1": "rollup -c --environment PACKAGE:package1",
"build:package2": "rollup -c --environment PACKAGE:package2"
}
}
版本管理和发布
1. Changesets 配置
Changesets 是一个用于管理版本和 changelog 的工具。创建 .changeset/config.json
:
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": [
"@changesets/changelog-github",
{ "repo": "your-username/monorepo_rollup_tpl" }
],
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
2. 发布脚本
添加发布相关的脚本:
{
"scripts": {
"changeset": "changeset",
"version": "changeset version",
"release": "changeset publish"
}
}
持续集成 (CI/CD)
1. 测试工作流
创建 .github/workflows/test.yml
:
name: ci
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 9
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile --prefer-offline
- name: Run Tests
run: pnpm run test
2. 代码质量检查工作流
创建 .github/workflows/lint-and-type.yml
:
name: Lint and Type Check
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize]
jobs:
lint-and-type:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 9
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint check
run: pnpm run eslint:ci
- name: Prettier check
run: pnpm run prettier:ci
- name: Type check
run: pnpm run typecheck
3. 发布工作流
创建 .github/workflows/release.yml
:
name: Release
on:
push:
branches: [main]
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 9
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Create Release Pull Request or Publish
id: changesets
uses: changesets/action@v1
with:
publish: pnpm run release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN和NPM_TOKEN生成
:详见link
依赖安装
安装项目所需的所有依赖:
# 安装开发依赖
pnpm add -Dw @babel/core @babel/plugin-transform-react-jsx @babel/plugin-transform-typescript @babel/preset-env @changesets/changelog-github @changesets/cli @eslint/compat @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-node-resolve @types/jest @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser concurrently eslint eslint-config-prettier eslint-import-resolver-typescript eslint-plugin-import eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-hooks globals jest jest-environment-jsdom prettier rollup rollup-plugin-dts ts-jest ts-node typescript
使用示例
1. 创建包内容
在 packages/package1/src/index.ts
中:
const add = (a: number, b: number) => {
return a + b
}
const greeting: string = 'Hello, Monorepo!'
export { add, greeting }
2. 添加测试
在 packages/package1/__tests__/index.test.ts
中:
import { add, greeting } from '../src'
describe('package1', () => {
test('add function', () => {
expect(add(1, 2)).toBe(3)
})
test('greeting export', () => {
expect(greeting).toBe('Hello, Monorepo!')
})
})
3. 构建和测试
# 运行测试
pnpm test
# 类型检查
pnpm typecheck
# 代码格式化
pnpm prettier
# 代码质量检查
pnpm eslint
# 构建所有包
pnpm build
最佳实践和总结
1. 命名约定
- 包名使用 kebab-case
- 文件名使用 camelCase 或 kebab-case
- 导出的函数和变量使用 camelCase
2. 版本管理
- 使用 Changesets 管理版本和 changelog
- 遵循语义化版本规范
- 每次发布前确保所有测试通过
3. 代码质量
- 配置 ESLint 和 Prettier 保证代码一致性
- 使用 TypeScript 提供类型安全
- 保持高测试覆盖率
4. 持续集成
- 自动化测试、代码质量检查
- 自动化发布流程
- 使用缓存优化 CI 速度
总结
本文介绍了如何从零开始搭建一个现代化的 Monorepo 开发模板,涵盖了:
- 项目结构设计:合理的目录组织和包管理
- TypeScript 配置:类型安全和开发体验
- Rollup 打包:支持多种模块格式的库打包
- Jest 测试:完整的测试解决方案
- 代码质量控制:ESLint + Prettier + TypeScript
- 版本管理:Changesets 自动化版本和发布
- 持续集成:GitHub Actions 自动化流程
这个模板为开发多包项目提供了坚实的基础,可以根据具体需求进行扩展和定制。通过统一的工具链和自动化流程,大大提高了开发效率和代码质量。
无论是开发组件库、工具库还是应用集合,这个 Monorepo 模板都能为您的项目提供专业级的开发体验。