616 lines
14 KiB
Markdown
616 lines
14 KiB
Markdown
# Nova 项目约定 (Project Conventions)
|
||
|
||
> dumi + father 技术栈的 AI-Native 组件库工程规范。
|
||
> 所有配置在此预先约定,AI 生成代码时按此执行。
|
||
|
||
---
|
||
|
||
## 一、技术栈确认
|
||
|
||
| 领域 | 方案 | 版本 |
|
||
|------|------|------|
|
||
| 包管理 | pnpm | >= 9 |
|
||
| Monorepo | pnpm workspace | — |
|
||
| 文档站点 | dumi | 2.x |
|
||
| 组件构建 | father | 4.x |
|
||
| 测试 | Vitest + @testing-library/react | — |
|
||
| Lint | ESLint + Prettier | — |
|
||
| Git Hooks | husky + lint-staged | — |
|
||
| 版本管理 | Changesets | — |
|
||
| 样式方案 | **CSS Modules** | — |
|
||
| 语言 | TypeScript (strict) | 5.x |
|
||
|
||
### 为什么选 CSS Modules?
|
||
|
||
| 方案 | dumi 兼容 | father 兼容 | AI 友好度 | 运行时开销 |
|
||
|------|:---------:|:-----------:|:---------:|:---------:|
|
||
| CSS Modules | ✅ 原生 | ✅ 原生 | ★★★ (import styles 清晰) | 0 |
|
||
| vanilla-extract | ⚠️ 需插件 | ⚠️ 需插件 | ★★ (编译时,调试困难) | 0 |
|
||
| styled-components | ✅ | ⚠️ 需配置 | ★ (模板字符串 AI 常出错) | 有 |
|
||
| Tailwind | ✅ | ✅ | ★ (类名太长 AI 难控制) | 0 |
|
||
|
||
**结论:CSS Modules 零配置、零运行时、AI 最可控。**
|
||
|
||
---
|
||
|
||
## 二、项目结构
|
||
|
||
```
|
||
novaui/
|
||
├── .husky/
|
||
│ ├── pre-commit # lint-staged
|
||
│ └── commit-msg # commitlint
|
||
├── .github/
|
||
│ └── workflows/
|
||
│ ├── ci.yml # lint + test + build
|
||
│ └── release.yml # 发布 npm
|
||
├── docs/ # dumi 站点文档
|
||
│ ├── index.md # 首页
|
||
│ ├── guide/
|
||
│ │ ├── getting-started.md
|
||
│ │ ├── theme.md
|
||
│ │ └── faq.md
|
||
│ └── components/ # 组件概览页
|
||
│ └── index.md
|
||
├── src/
|
||
│ ├── components/
|
||
│ │ ├── Button/
|
||
│ │ │ ├── index.ts
|
||
│ │ │ ├── Button.tsx
|
||
│ │ │ ├── Button.module.css # ← CSS Modules
|
||
│ │ │ ├── Button.test.tsx
|
||
│ │ │ └── index.md # ← dumi 文档(关键)
|
||
│ │ └── ...
|
||
│ ├── hooks/
|
||
│ ├── theme/
|
||
│ │ ├── tokens.ts
|
||
│ │ ├── dark.css
|
||
│ │ └── global.css
|
||
│ ├── charts/ # 可视化组件
|
||
│ │ ├── LineChart/
|
||
│ │ └── ...
|
||
│ └── index.ts # 按需导出不做 barrel export
|
||
├── .dumirc.ts # dumi 配置
|
||
├── .fatherrc.ts # father 配置
|
||
├── vitest.config.ts
|
||
├── tsconfig.json
|
||
├── .eslintrc.cjs
|
||
├── .prettierrc
|
||
├── .commitlintrc.cjs
|
||
├── package.json
|
||
├── pnpm-workspace.yaml
|
||
├── .npmrc
|
||
└── README.md
|
||
```
|
||
|
||
---
|
||
|
||
## 三、dumi 配置约定
|
||
|
||
### 3.1 `.dumirc.ts`
|
||
|
||
```ts
|
||
import { defineConfig } from 'dumi'
|
||
|
||
export default defineConfig({
|
||
themeConfig: {
|
||
name: 'Nova',
|
||
logo: '/logo.png',
|
||
nav: [
|
||
{ title: '指南', link: '/guide' },
|
||
{ title: '组件', link: '/components' },
|
||
{ title: '图表', link: '/charts' },
|
||
],
|
||
socialLinks: {
|
||
github: 'https://github.com/user/novaui',
|
||
},
|
||
},
|
||
// 自动 API 表格(必须,AI 生成组件时直接能看到类型)
|
||
apiParser: {},
|
||
resolve: {
|
||
// doc 目录下的 .md 作为文档路由
|
||
docDirs: ['docs'],
|
||
// 组件目录下的 index.md 作为组件文档
|
||
atomDirs: [
|
||
{ type: 'component', dir: 'src/components' },
|
||
{ type: 'chart', dir: 'src/charts' },
|
||
],
|
||
},
|
||
// 约定输出
|
||
outputPath: 'dist-docs',
|
||
})
|
||
```
|
||
|
||
### 3.2 组件文档 (`index.md`) 模板
|
||
|
||
```md
|
||
---
|
||
nav: 组件
|
||
group: 通用
|
||
title: Button 按钮
|
||
description: 用于触发操作的按钮组件。
|
||
---
|
||
|
||
## 代码演示
|
||
|
||
### 基础用法
|
||
|
||
```tsx | pure
|
||
/**
|
||
* title: 基础按钮
|
||
* description: 使用 `variant` 切换按钮风格。
|
||
*/
|
||
import { Button } from '@nova/ui/button'
|
||
|
||
export default () => (
|
||
<Button variant="solid" size="md">
|
||
确认
|
||
</Button>
|
||
)
|
||
```
|
||
|
||
### 变体
|
||
|
||
```tsx | pure
|
||
<Button variant="solid">Solid</Button>
|
||
<Button variant="outline">Outline</Button>
|
||
<Button variant="ghost">Ghost</Button>
|
||
<Button variant="glass">Glass</Button>
|
||
```
|
||
|
||
## API
|
||
|
||
<API id="Button" />
|
||
```
|
||
|
||
### 3.3 dumi 文档规则
|
||
|
||
```
|
||
1. 每个组件目录必须包含 index.md
|
||
2. 每个 demo 必须用 ```tsx | pure 声明(禁止用 react,避免 dumi 额外包裹)
|
||
3. demo 必须有 title,可选 description
|
||
4. 必须用 <API id="ComponentName" /> 生成 API 表格(依赖 apiParser 插件)
|
||
5. 组件分类:通用 / 录入 / 展示 / 反馈 / 导航 / 图表
|
||
```
|
||
|
||
---
|
||
|
||
## 四、father 配置约定
|
||
|
||
### 4.1 `.fatherrc.ts`
|
||
|
||
```ts
|
||
import { defineConfig } from 'father'
|
||
|
||
export default defineConfig({
|
||
// Bundless 模式:保持源码目录结构,每个组件独立产物(Tree-shaking 最佳)
|
||
cjs: {
|
||
output: 'dist/cjs',
|
||
platform: 'browser',
|
||
},
|
||
esm: {
|
||
output: 'dist/esm',
|
||
platform: 'browser',
|
||
// 保留 .module.css 作为 CSS Modules
|
||
extraBabelPlugins: [
|
||
// 确保 .module.css 被正确处理
|
||
],
|
||
},
|
||
// 自动生成 .d.ts
|
||
typescript: {},
|
||
// UMD 构建(可选)
|
||
umd: {
|
||
output: 'dist/umd',
|
||
name: 'NovaUI',
|
||
},
|
||
})
|
||
```
|
||
|
||
### 4.2 father 构建规则
|
||
|
||
```
|
||
1. 使用 Bundless 模式(非 Bundle),保持每个组件独立文件
|
||
→ 用户可 import { Button } from '@nova/ui/button'
|
||
2. 产物格式:esm + cjs(双格式)
|
||
3. CSS Modules 文件 (.module.css) 自动保留,build 后生成对应的 .css
|
||
4. TypeScript 生成 .d.ts 类型定义
|
||
5. 外部依赖:react、react-dom 标记为 peerDependencies,不打包
|
||
```
|
||
|
||
### 4.3 package.json 导出配置
|
||
|
||
```json
|
||
{
|
||
"name": "@nova/ui",
|
||
"type": "module",
|
||
"main": "dist/cjs/index.js",
|
||
"module": "dist/esm/index.js",
|
||
"types": "dist/esm/index.d.ts",
|
||
"exports": {
|
||
".": {
|
||
"import": "./dist/esm/index.js",
|
||
"require": "./dist/cjs/index.js"
|
||
},
|
||
"./*": {
|
||
"import": "./dist/esm/*/index.js",
|
||
"require": "./dist/cjs/*/index.js"
|
||
}
|
||
},
|
||
"sideEffects": [
|
||
"**/*.css"
|
||
],
|
||
"files": [
|
||
"dist"
|
||
],
|
||
"peerDependencies": {
|
||
"react": ">=18",
|
||
"react-dom": ">=18"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 五、CSS Modules 约定
|
||
|
||
### 5.1 命名规则
|
||
|
||
```css
|
||
/* Button.module.css */
|
||
|
||
/* ✅ 用 camelCase,在 JS 中 import 后直接 styles.xxx */
|
||
.wrapper { }
|
||
.iconLeft { }
|
||
.iconRight { }
|
||
|
||
/* ✅ 全局覆盖用 :global */
|
||
:global(.nv-theme-dark) .wrapper {
|
||
background: #1a1a3e;
|
||
}
|
||
|
||
/* ✅ data-* 状态样式 */
|
||
.wrapper[data-variant='solid'] { }
|
||
.wrapper[data-variant='outline'] { }
|
||
.wrapper[data-size='lg'] { }
|
||
.wrapper[data-loading] { }
|
||
```
|
||
|
||
### 5.2 组件引用
|
||
|
||
```tsx
|
||
// ✅ AI 正确用法
|
||
import styles from './Button.module.css'
|
||
|
||
<div className={styles.wrapper}>
|
||
<span className={styles.iconLeft}>{icon}</span>
|
||
</div>
|
||
|
||
// ❌ 禁止:styles['wrapper'](AI 容易拼错字符串)
|
||
```
|
||
|
||
### 5.3 全局 CSS
|
||
|
||
```ts
|
||
// theme/global.css — 引入 normalize + CSS 变量
|
||
// 在 dumi 的 .dumirc.ts 中通过 extraBabelPlugins 或手动 import
|
||
|
||
// theme/dark.css — 暗色模式变量
|
||
// 通过 html[data-theme='dark'] 选择器作用
|
||
```
|
||
|
||
---
|
||
|
||
## 六、Monorepo 约定
|
||
|
||
### 6.1 pnpm-workspace.yaml
|
||
|
||
```yaml
|
||
packages:
|
||
- 'packages/*'
|
||
- 'apps/*'
|
||
```
|
||
|
||
### 6.2 当前是单包,但结构保留扩展性
|
||
|
||
```
|
||
novaui/
|
||
├── packages/
|
||
│ ├── core/ # UI 组件(@nova/ui)
|
||
│ ├── charts/ # 图表组件(@nova/charts)
|
||
│ ├── theme/ # 主题包(@nova/theme)
|
||
│ └── utils/ # 工具(@nova/utils)
|
||
└── apps/
|
||
└── docs/ # dumi 文档站点
|
||
```
|
||
|
||
> 初期可扁平,但目录名和 npm scope 预先约定好。
|
||
|
||
---
|
||
|
||
## 七、代码质量工具约定
|
||
|
||
### 7.1 ESLint + Prettier
|
||
|
||
```jsonc
|
||
// .eslintrc.cjs
|
||
module.exports = {
|
||
extends: [
|
||
'eslint:recommended',
|
||
'plugin:@typescript-eslint/recommended',
|
||
'plugin:react/recommended',
|
||
'plugin:react-hooks/recommended',
|
||
'prettier', // 关闭冲突规则
|
||
],
|
||
rules: {
|
||
'react/react-in-jsx-scope': 'off', // React 18+
|
||
'react/prop-types': 'off', // TypeScript 已覆盖
|
||
'@typescript-eslint/no-explicit-any': 'error',
|
||
'@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
|
||
},
|
||
}
|
||
```
|
||
|
||
```jsonc
|
||
// .prettierrc
|
||
{
|
||
"semi": false,
|
||
"singleQuote": true,
|
||
"trailingComma": "all",
|
||
"printWidth": 100,
|
||
"arrowParens": "avoid"
|
||
}
|
||
```
|
||
|
||
### 7.2 commitlint
|
||
|
||
```
|
||
// .commitlintrc.cjs
|
||
module.exports = {
|
||
extends: ['@commitlint/config-conventional'],
|
||
rules: {
|
||
'type-enum': [2, 'always', [
|
||
'feat', 'fix', 'docs', 'style', 'refactor',
|
||
'test', 'chore', 'ci',
|
||
]],
|
||
},
|
||
}
|
||
|
||
// 格式: <type>(<scope>): <subject>
|
||
// 示例: feat(Button): add glass variant
|
||
```
|
||
|
||
### 7.3 lint-staged
|
||
|
||
```jsonc
|
||
// package.json
|
||
{
|
||
"lint-staged": {
|
||
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
|
||
"*.{css,json,md}": ["prettier --write"]
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 八、测试约定
|
||
|
||
### 8.1 vitest.config.ts
|
||
|
||
```ts
|
||
import { defineConfig } from 'vitest/config'
|
||
|
||
export default defineConfig({
|
||
test: {
|
||
environment: 'jsdom',
|
||
globals: true,
|
||
setupFiles: ['./test-setup.ts'],
|
||
include: ['src/**/*.test.{ts,tsx}'],
|
||
coverage: {
|
||
provider: 'v8',
|
||
include: ['src/components/**/*.tsx', 'src/charts/**/*.tsx'],
|
||
thresholds: {
|
||
functions: 80,
|
||
},
|
||
},
|
||
},
|
||
})
|
||
```
|
||
|
||
### 8.2 测试规范
|
||
|
||
```tsx
|
||
// 每个组件至少覆盖:
|
||
// 1. 默认渲染
|
||
import { render, screen } from '@testing-library/react'
|
||
|
||
it('renders with default props', () => {
|
||
render(<Button>Click</Button>)
|
||
expect(screen.getByRole('button')).toHaveTextContent('Click')
|
||
})
|
||
|
||
// 2. Props 组合
|
||
it.each(['solid', 'outline', 'ghost'] as const)(
|
||
'renders variant="%s" correctly',
|
||
variant => {
|
||
const { container } = render(<Button variant={variant} />)
|
||
expect(container.querySelector('button')).toHaveAttribute('data-variant', variant)
|
||
}
|
||
)
|
||
|
||
// 3. 事件回调
|
||
it('fires onClick when clicked', () => {
|
||
const onClick = vi.fn()
|
||
render(<Button onClick={onClick} />)
|
||
screen.getByRole('button').click()
|
||
expect(onClick).toHaveBeenCalledTimes(1)
|
||
})
|
||
|
||
// 4. 边界(disabled、loading、空 children)
|
||
it('does not fire onClick when disabled', () => {
|
||
const onClick = vi.fn()
|
||
render(<Button onClick={onClick} disabled />)
|
||
screen.getByRole('button').click()
|
||
expect(onClick).not.toHaveBeenCalled()
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
## 九、发布流程约定
|
||
|
||
### 9.1 Changesets
|
||
|
||
```bash
|
||
# 版本变更三步走
|
||
|
||
# 1. 开发完,记变更
|
||
pnpm changeset
|
||
# → 选择变更类型 (patch / minor / major)
|
||
# → 填写 changelog 描述
|
||
|
||
# 2. 统一版本
|
||
pnpm changeset version
|
||
# → 自动更新版本号 + 生成 CHANGELOG.md
|
||
|
||
# 3. 发布
|
||
pnpm changeset publish
|
||
```
|
||
|
||
### 9.2 GitHub Actions
|
||
|
||
```yaml
|
||
# .github/workflows/ci.yml
|
||
name: CI
|
||
on: [push, pull_request]
|
||
jobs:
|
||
lint:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- uses: pnpm/action-setup@v4
|
||
- run: pnpm install
|
||
- run: pnpm lint
|
||
|
||
test:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- uses: pnpm/action-setup@v4
|
||
- run: pnpm install
|
||
- run: pnpm test -- --coverage
|
||
|
||
build:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- uses: pnpm/action-setup@v4
|
||
- run: pnpm install
|
||
- run: pnpm build
|
||
- run: pnpm docs:build
|
||
```
|
||
|
||
```yaml
|
||
# .github/workflows/release.yml
|
||
name: Release
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||
jobs:
|
||
release:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- uses: pnpm/action-setup@v4
|
||
- run: pnpm install
|
||
- run: pnpm build
|
||
- name: Publish
|
||
uses: changesets/action@v1
|
||
with:
|
||
publish: pnpm changeset publish
|
||
env:
|
||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
```
|
||
|
||
---
|
||
|
||
## 十、Git 分支与工作流
|
||
|
||
| 分支 | 用途 |
|
||
|------|------|
|
||
| `main` | 稳定版,只接受 PR merge |
|
||
| `feat/xxx` | 新组件/功能 |
|
||
| `fix/xxx` | Bug 修复 |
|
||
| `docs/xxx` | 文档改进 |
|
||
| `chore/xxx` | 工程配置 |
|
||
|
||
```
|
||
工作流:
|
||
1. 从 main 创建 feat/xxx
|
||
2. 开发 + 测试
|
||
3. PR → main(自动 CI 检查)
|
||
4. Merge → 触发 changesets → 发布
|
||
```
|
||
|
||
---
|
||
|
||
## 十一、提前准备清单(初始项目搭建步骤)
|
||
|
||
```bash
|
||
# 1. 初始化项目
|
||
mkdir novaui && cd novaui
|
||
pnpm init
|
||
|
||
# 2. 安装核心依赖
|
||
pnpm add -D dumi father vitest @testing-library/react \
|
||
@testing-library/jest-dom jsdom \
|
||
eslint prettier eslint-config-prettier \
|
||
@typescript-eslint/eslint-plugin @typescript-eslint/parser \
|
||
husky lint-staged @commitlint/cli @commitlint/config-conventional \
|
||
@changesets/cli
|
||
|
||
# 3. 安装运行时依赖
|
||
pnpm add react react-dom
|
||
pnpm add -D @types/react @types/react-dom
|
||
|
||
# 4. 创建核心配置文件
|
||
# .dumirc.ts, .fatherrc.ts, vitest.config.ts,
|
||
# tsconfig.json, .eslintrc.cjs, .prettierrc,
|
||
# .commitlintrc.cjs, pnpm-workspace.yaml
|
||
|
||
# 5. 初始化 git + husky
|
||
git init
|
||
pnpm husky init
|
||
|
||
# 6. 初始化 changesets
|
||
pnpm changeset init
|
||
|
||
# 7. 创建目录骨架
|
||
mkdir -p src/components src/theme src/hooks src/shared docs/guide docs/components
|
||
```
|
||
|
||
---
|
||
|
||
## 十二、AI 生成检查清单
|
||
|
||
每次 AI 生成代码后,按此检查:
|
||
|
||
```
|
||
[ ] import 路径正确:@nova/ui/组件名(全小写 kebab-case)
|
||
[ ] 样式引用正确:import styles from './Component.module.css'
|
||
[ ] 使用 data-* 属性标记状态,而非 class 拼接
|
||
[ ] 所有 prop 有类型定义(联合类型或 interface)
|
||
[ ] 所有 prop 有默认值(非必填的)
|
||
[ ] 测试覆盖渲染 + 事件 + 边界
|
||
[ ] 文档 index.md 有 demo + API 表格
|
||
[ ] 无 any 类型
|
||
[ ] 无 switch-case(代之以对象映射)
|
||
[ ] 无魔法字符串
|
||
```
|
||
|
||
---
|
||
|
||
> **提前约定越细,AI 犯错越少。此文档即 Nova 项目的"宪法",所有代码生成以此为据。**
|