Files
RustUI/CONVENTIONS.md
T
2026-05-28 14:18:56 +08:00

616 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 项目的"宪法",所有代码生成以此为据。**