项目:react-ui | 版本:v1.1 | 生成日期:2026-10-06 | 依据:源码分析
本篇是配置层的「全量清单」:根与各包 package.json 的 scripts 逐条注释、三个 Vite 构建配置逐段解读、ESLint/Stylelint/cspell/commitlint/Playwright 五套工具配置走读、CI 工作流逐 job 解析,最后给一张本地开发环境矩阵。命令的使用时机与门禁哲学见《06-部署运行》,本篇只负责「每一行配置是什么、为什么这么写」。所有引用均来自本检出文件原文(含注释——这个仓库把决策理由写进配置注释的惯例,是配置可读性的第一来源)。
根脚本分六组(package.json 原文核对):开发——dev(pnpm -r --parallel dev,全 workspace 并行)、dev:packages(仅 ./packages/** 的 watch 构建)、dev:apps(仅 ./apps/**);构建——build(pnpm -r build 递归全部含文档站)、build:packages、build:apps;测试——test、test:packages(--filter './packages/**');质量——lint(= lint:es + lint:style + spellcheck 三连)、lint:es(NODE_OPTIONS=--max-old-space-size=8192 eslint "{packages,apps}/**/*.{js,jsx,ts,tsx}" --max-warnings=57,8192MB 与棘轮值是两个必须原样理解的魔法数)、lint:style(stylelint 扫 {packages,apps}/**/*.{css,ts}——注意扫 ts 文件里的内联样式对象)、spellcheck(cspell lint,--dot --gitignore --cache --show-suggestions, glob 限 packages|apps 下十类文本文件)、typecheck(= typecheck:ui + typecheck:hooks + typecheck:demos 三套 tsconfig)、docs:lint(三个 Node 脚本按序串联)、check:exports;e2e——e2e:screenshots(playwright test 走默认配置)、e2e:interactions(-c playwright.interactions.config.ts)、e2e:update-snapshots;发布——release(build:packages + 递归包级 release)、release:preflight / release:check / release:publish(三档对应 scripts/release.mjs 的 --preflight / 缺省 / --publish)、release:tag(内联 node 读 ui 版本打附注 tag)、publish:hooks/ui/pro 与 publish:all(手工路径:链式 prepublishOnly:* 的 build + test + pnpm publish)、prepublishOnly:hooks/ui/pro(cd 到包内 build + test);协作——commit(git-cz 交互生成,commitizen 配置指向 cz-git)。
值得点名的三个脚本细节:其一,docs:lint 与 check:exports 是纯 Node 脚本而非测试,所以能进任何阶段,但语义上必须在 build 之后(依赖产物,见《09-测试与质量保障》第 4 节);其二,lint:es 的 glob 明确不含 e2e/ 与 scripts/ 之外根级文件(e2e 用例代码不受 eslint 棘轮约束,属已知豁免口径);其三,pnpm.overrides 把 react/react-dom 钉在 19.2.7、types 钉在 19.2.3(对齐仓库根 devDeps 的 react 19.1.1 与 CI 安装行为的版本差由 pnpm 统一收敛),pnpm.patchedDependencies 挂 dumi 补丁(实义:删除 dumi 产物中 DumiDemo 的 React.memo 包装——其「JSON.stringify 长度相等」的 props 判等会在 demo 数据异步就位后命中 Suspense 重试渲染而 bailout,骨架屏永久停留;全貌见《17-依赖全景》第 8 节)。
packages/ui(packages/ui/package.json):dev = vite build --watch(库模式的 dev 就是 watch 构建);build = vite build && npm run check:published-types(产物类型二次编译是 build 的一部分,不是独立门禁);check:published-types = tsc --noEmit --skipLibCheck false --module esnext --moduleResolution bundler --target ES2020 --jsx react-jsx es/index.d.ts——注意 --skipLibCheck false 是刻意收紧(默认 skipLibCheck true 会放过依赖里的类型错误);test / test:watch(vitest);prepublishOnly = npm run build && node ../../scripts/validate-package-exports.mjs @xiaoye-react/ui(ui 是唯一把 check:exports 挂进 prepublish 的包);typecheck = tsc --noEmit -p tsconfig.typecheck.json;prebuild = pnpm --filter @xiaoye-react/hooks build(保证干净克隆先有 hooks 产物)。packages/hooks:精简版——dev(watch)、build(无 check:published-types 与 check:exports)、test/test:watch、prepublishOnly = build、release = pnpm publish。packages/@xiaoye-react/pro:同 hooks 精简形态,prepublishOnly 仅 build(测试由 release.mjs 显式补,见《13-技术债务与演进建议》D8)。apps/docs 的 scripts(apps/docs/package.json)含 predev/prebuild 钩子(ensure-ui-build.mjs + docs:gen)与 docs:gen(tsx 跑 scripts/docgen.ts + generate-css-exports.ts)。
三个 vite.config.ts 共享骨架(@vitejs/plugin-react + vite-plugin-dts + lib 模式双格式),差异在细节。packages/ui/vite.config.ts 的独有部分:dts.include 显式包含 ../../@types/**/*.d.ts(仓库级环境声明);dts.exclude 排除测试与 story 文件(注释:story 只用于本地预览,进了 .d.ts 等于把 52 个无关声明发布出去);afterDiagnostic 把 dts 类型错误升级为构建失败(注释:声明发射有错时对应文件被静默跳过,产物 import 变 TS2307,必须失败);beforeWriteFile 正则把三下划线标识符归一回双下划线(vite-plugin-dts 对 __ 前缀 re-export 标识符的已知上游 bug,注释给出 __BaseInputProps → ___BaseInputProps 实例)。build 段:formats: ['es','cjs']、fileName 映射 .js/.cjs(type: module 包内 .cjs 才被 Node 当 CommonJS);minify 开启(注释实测 12,425 vs 12,423 B gzip 与 CSS 前缀链);rollupOptions.external 用 isExternal 函数按包名段判定(注释解释 rollup external 只精确匹配 id 而源码有 dayjs/plugin/utc 子路径);output 双份都 preserveModules: true + preserveModulesRoot: 'src',ESM 份加 banner: "'use client'"(注释:rollup 不保留入口指令序言,guides 向 Next.js 用户承诺过;CJS 份不加,防挤掉 'use strict'),CJS 份 exports: 'named'。packages/hooks/vite.config.ts:dts 无 afterDiagnostic/beforeWriteFile;external 简单数组 ['react','react-dom','react/jsx-runtime'];同样双格式 preserveModules + banner。packages/@xiaoye-react/pro/vite.config.ts:formats: ['es'] 单格式(exports 的 require 条件也指向同一 index.js);external 含两个 peer 包;仅 ESM 份加 banner。
TypeScript 配置:根 tsconfig.json(strict、target ES2015、jsx react-jsx、moduleResolution bundler,include packages/**/*,exclude 产物目录)是基础;packages/ui/tsconfig.json 供构建与 docs:lint 的 ts-morph 使用;packages/ui/tsconfig.typecheck.json 是类型门禁专用(extends 基础配置 + paths 把 @xiaoye-react/hooks 指回 ../hooks/src/index.ts,头注释用大段文字警告:不要把 paths 加回构建用 tsconfig.json——2026-09-21 实测会让 rootDir 抬升、vite-plugin-dts 把 1013 个 .d.ts 发射到 es/ui/src/ 下、入口声明文件消失);apps/docs/tsconfig.demos.json 让 demo 代码参与 typecheck。
PostCSS 与样式的配置面(postcss.config.cjs):全库 CSS 走 postcss-nested(嵌套语法),这是「CSS Modules 只消费变量与属性选择器」写法得以成立的前提——没有嵌套,每个变体/状态选择器都要全称拼写,组件样式体积与可读性都会崩。与它配套的两条约束散落在别处:stylelint 用 postcss-styled-syntax 后处理器扫 ts 文件里的内联样式对象(lint:style 的 glob 里有 ts),让 style props 的书写也受规则约束;组件样式文件遵循「变量消费 + data 属性选择器」的规约本身不被机器强制(stylelint 管不了「该用变量却写死值」),靠 review 与《02-组件设计》第 3 节的共性约定表兜底——这是配置体系中少数「机器没管、纪律在管」的点,改动样式写法前先确认要改的是规约还是配置。
ESLint(eslint.config.mjs,flat config):ignores 排构建产物、dist、.next、story 文件(注释:story 不参与 lint 防 monorepo 范围过大导致 OOM)以及 Menubar/Tree 两个目录(债务项,见《13》D5);规则层继承 js.configs.recommended + tseslint.configs.recommended(注释:非 type-aware,避免为每个 tsconfig 加载 TS Program 导致 OOM)+ reactHooks recommended-latest;rules-of-hooks: error,exhaustive-deps: warn(由棘轮卡总量),其余 TypeScript 严格规则成组关闭;文件中段的长注释是 exhaustive-deps 57 条存量的完整分类台账(《09-测试与质量保障》第 6 节已详述)。Stylelint(stylelint.config.mjs):standard 配置 + postcss-styled-syntax(配合 lint:style 扫 ts 内联样式)。cspell(cspell.json):caseSensitive: false,词典为 lorem-ipsum + 自定义 ./.cspell/custom-words.txt,ignorePaths 覆盖产物目录与 language/shiki-themes 等生成性文件。commitlint(commitlint.config.cjs):extends config-conventional + conventional-changelog-conventionalcommits parserPreset;prompt 段是 cz-git 的交互配置——11 个 type 带 emoji,questions 段定义 scope/subject/body/breaking/issues 的交互文案;尾部有约 80 行注释掉的汉化配置(债务轻项 D10)。Playwright 双配置:playwright.config.ts(截图)——testIgnore: '**/interactions/**' 与交互配置互斥、toHaveScreenshot.maxDiffPixelRatio: 0.02、use.animations: 'disabled'、webServer 托管 scripts/serve-docs-dist.mjs(4173 端口,reuseExistingServer);playwright.interactions.config.ts——globalSetup 挂 assert-harness 验明正身、webServer 直接起 vite --config e2e/harness/vite.config.ts --host 127.0.0.1 --port 4175 --strictPort、timeout 120s。
触发:push 到 main、全部 PR、手动 workflow_dispatch;concurrency.group: ci-${{ github.ref }} + cancel-in-progress 按引用取消过期运行。quality job(ubuntu-latest,30 分钟超时):checkout → pnpm/action-setup@v4(版本由 packageManager 字段定)→ setup-node@v4(node 22 + cache: pnpm)→ pnpm install --frozen-lockfile → pnpm lint(注释点名含棘轮)→ pnpm typecheck → pnpm build:packages → pnpm docs:lint → pnpm check:exports → pnpm test:packages。e2e job(20 分钟):同样装依赖后,actions/cache@v4 按 lockfile 哈希缓存 ~/.cache/ms-playwright,pnpm exec playwright install --with-deps chromium,跑交互 e2e。文件头注释交代了两个顺序约束的由来:docs:lint 与 check:exports 必须在 build:packages 之后(跨包类型解析依赖产物),单测同理(workspace 协议解析 es/ 产物);截图回归暂不入 CI 的原因也写在头注释。设计上最值得记录的一条:CI 步骤与本地门禁清单一字不差——没有任何只在 CI 存在的隐藏步骤,这是 CONTRIBUTING 敢写「本地跑过的 CI 一定过」的机制基础。
scripts/ 下两个辅助脚本的机制值得单独记录,因为它们各自修复过一类静默错误。scripts/ensure-ui-build.mjs(73 行):递归扫描 packages/ui/src 下最新源文件 mtime(跳过 node_modules 与测试),与 packages/ui/es/index.js 的 mtime 比对,源码更新而产物陈旧时自动重建——它修复的是「文档站与截图回归静默跑旧代码」这一类没有报错的错误(症状是 demo 行为与源码对不上,极易误判为组件回归);退出通道是环境变量 DOCS_SKIP_UI_BUILD=1(改纯文档内容时省一次构建)。scripts/serve-docs-dist.mjs(66 行):零依赖 Node http 静态服务器,MIME 表手写十四类,托管 apps/docs/dist(端口 4173,可用 PORT 覆盖);注释解释了为什么不需要 SPA fallback——dumi 构建产物是目录型静态站点(每路由一个 index.html)。它的存在让截图 e2e 的 webServer 不依赖任何第三方 serve 包,代价是 MIME 表需要随产物类型维护。两个脚本共同的风格是「可独立运行、有明确退出码、注释解释为什么存在」,与 release.mjs/validate-package-exports.mjs 一致,这也是 scripts/ 目录整体的准入标准——新增脚本时按这个形状写。
| 维度 | 要求 | 校验方式 | 来源 |
|---|---|---|---|
| Node | 22 | node -v;本机 nvm 需先 export PATH | CONTRIBUTING、CI setup-node |
| pnpm | 9.12.3 | pnpm --version;不符时 packageManager 字段拦截 | 根 package.json |
| 浏览器(e2e) | Chromium | pnpm exec playwright install chromium | CONTRIBUTING |
| npm 账号(发布) | @xiaoye-react scope + 2FA | npm whoami;npm login --scope=@xiaoye-react --auth-type=interactive | README 前置 |
| 内存(lint) | 建议 ≥8GB 可用 | eslint 由 NODE_OPTIONS 兜底 | 根 scripts |
| git | 工作树干净、main、与 origin 对齐 | git status --porcelain | release 预检 |
另有两个环境细节易忽略:文档站启动的前置钩子(ensure-ui-build.mjs 比对 src 与 es 的 mtime、陈旧自动重建,可用 DOCS_SKIP_UI_BUILD=1 跳过;docs:gen 生成 API 表素材)——直接 dumi dev 会绕过它们;以及 vitest 的 worker 钉死(4 个线程)与 testTimeout 20s 都写在 packages/ui/vitest.config.ts,本地调参前先读注释里的 105 分钟实测。
这套配置最有价值的部分不在单文件内容,而在文件之间的耦合关系——动一处可能连锁失效,逐对列清:其一,packageManager 字段与 CI 的 pnpm/action-setup 是同一事实源(action 读字段装对应版本),本地手工升级 pnpm 而不改字段会让 CI 与本地用不同版本解析 lockfile;其二,pnpm.overrides 的 react 钉版与各包 devDependencies 里的 react 版本号并存,改 overrides 必须同步核对三包 devDeps,否则类型与运行时版本错位;其三,pnpm.patchedDependencies 与 lockfile 哈希联动——手工改 patch 文件内容而不重装,--frozen-lockfile 的 CI 会直接红;其四,files: ["es"] 与 check:exports/check:published-types 构成闭环:exports 指向的路径必须在 es/ 内,任何把新产物放 es/ 外的配置(如新增 css 出口)都要三处同步(exports 字段、validate-package-exports 的校验逻辑、dts/build 输出);其五,根 tsconfig.json 的 include 范围与 packages/ui/tsconfig.typecheck.json 的 paths 是「一宽一窄」的互补关系(构建程序宽、typecheck 程序窄),头注释警告的 rootDir 陷阱就源于把窄配置的 paths 挪进宽配置;其六,eslint.config.mjs 的 ignores 与 lint:es 的 glob 共同决定实际覆盖面——在 ignores 里新增目录等于同时从「警告总量」里移除其贡献,棘轮数值需要复核;其七,playwright.config.ts 的 testIgnore: '**/interactions/**' 与交互配置的 testDir 互斥分工,新增 e2e 用例时选错配置文件会导致用例被静默忽略。修改配置的通用守则一句话:每个配置文件的注释都写明它与其他文件的契约,动手前把这些注释读完(这个仓库的配置注释密度是刻意为之,见《05-核心实现流程》手法三)。
把第 5 节与 7 节串成一个具体例子,演示「改配置」在本仓的标准走法。假设要给 ui 包新增一个子路径导出 @xiaoye-react/ui/hooks.css(假设未来拆出 hooks 专属样式):第一步读 packages/ui/vite.config.ts,确认新 css 会进 es/ 输出(Vite 库模式的 CSS 产物自动落 es/style.css 同级,需要额外配置才能拆两份——若构建链不支持,第一步就终止立项);第二步改 packages/ui/package.json 的 exports 加 "./hooks.css" 条目,并核对 sideEffects 仍含 *.css(check:exports 会拦「导出 CSS 却 sideEffects: false」);第三步跑 pnpm build:packages && pnpm check:exports 确认磁盘文件与导出映射对齐;第四步查 scripts/validate-package-exports.mjs 的逻辑是否天然覆盖新子路径(显式子路径无需改脚本,通配符则会被拒绝);第五步在文档侧核对 apps/docs/scripts/lint-docs-imports.mjs 的样式子路径白名单逻辑(它校验「样式副作用导入的子路径存在于目标包 exports」,新子路径自动纳入);第六步全量门禁。这个例子的价值不在 css 拆分本身,而在演示配置变更的固定动作序列:先证明构建链能产出,再声明导出,再让三个校验器(check:exports、消费者复验、docs:lint)背书,最后全量门禁——任何一个跳步都可能复现「类型解析通过、运行时 404」或「本地绿、干净克隆红」的历史事故。
全部配置文件与脚本的一句话索引:根 package.json/pnpm-workspace.yaml(工作区)、tsconfig.json(TS 基座)、eslint.config.mjs/stylelint.config.mjs/cspell.json/commitlint.config.cjs/postcss.config.cjs(五件工具)、playwright*.config.ts(e2e 双配置)、.github/workflows/ci.yml(CI)、scripts/*.mjs(release/preflight/check:exports/serve/ensure 五个脚本)、apps/docs/scripts/*(文档门禁与素材生成)、packages/*/vite.config.ts + vitest.config.ts + tsconfig.*.json(包级构建与测试)。配置之外的行为语义(每条命令何时跑、顺序为什么)统一见《06-部署运行》。