Joplin 编码规范实战:CLAUDE.md 指南、ESLint 自动化与源码级工程约定
【免费下载链接】joplinJoplin - the privacy-focused note taking app with sync capabilities for Windows, macOS, Linux, Android and iOS.项目地址: https://gitcode.com/GitHub_Trending/jo/joplin
Joplin 仓库根目录的CLAUDE.md是一份面向 AI 编码助手与人类贡献者的简明工程守则(Joplin Guidelines),它把项目最核心的代码风格、测试、样式与工具链约定浓缩为一个“快速参考”。本篇以该文档为骨架,逐条解读其规则,并结合仓库中的eslint配置、pre-commit 钩子、cspell拼写检查与 coding_style.md 等配套文档,说明每条规则背后的自动化支撑与源码级落地方式,帮助你在提交任何修改前做到与项目惯例完全一致。
快速参考(Quick Reference)总览
CLAUDE.md 的第一部分列出了 18 条高频规则,覆盖格式、类型、注释、测试、工具链与文档六大方面。下面按主题分组逐条展开。
格式与 TypeScript 类型
- 制表符缩进:整个仓库统一使用 Tab 缩进,而非空格。
- 字符串使用单引号:与多数 JS 项目偏好双引号不同,Joplin 规定字符串一律使用单引号。
- 使用规范的 TypeScript 类型,避免
any:这是硬性要求,any会绕过类型检查,削弱整个 monorepo 的类型安全。 - 不要标注可推断的类型:当 TypeScript 能从上下文推断出类型时(例如
const x = 'foo'推断为string),不要显式写出返回类型或const类型;只有在 TS 会推断出any时才需要显式标注。这一点与 coding_style.md 中“Don't set the type when it can be inferred”一节完全对应,后者指出 ESLint 的no-inferable-types规则只覆盖简单类型,函数调用等场景仍需自觉避免冗余标注。 - Markdown 文件不要硬换行:段落内不手动折行,交给渲染器自动换行;只有在真正的段落或列表分隔处才插入换行符。
注释与代码复用
- 注释只用
//,不要写 JSDoc 语法:项目不采用/** ... */块注释风格。 - 默认不写注释:仅在“为什么这样做”不明显时才加注释(例如 workaround、隐藏约束、微妙不变量),且尽量控制在一两行;永远不要解释代码“做了什么”——变量名和函数名已经承担了这一职责。
- 复制大段代码时必须声明:如果你复制了一块可观的代码,需在复制处上方加注释,注明这是重复代码,并给出原始位置的文件/路径引用,以便日后同步维护。
Jest 测试约定
- 每个测试文件只允许一个顶层
describe():保持文件结构单一,便于浏览与定位。 - 聚焦核心行为与边界情况:不要为每个细枝末节添加琐碎测试,测试应覆盖关键行为与 edge case。
- 避免测试中的重复代码:当同一逻辑用不同输入测试时,使用
test.each或共享 helper,而不是复制相似的测试块。
这与 coding_style.md 的“Test units”一节互为补充:该项目还主张避免 mock 对象与 spy——优先测试真实输入输出(例如真实写临时文件、真实建库写行),只在别无选择时才使用jest.spyOn/mockImplementation,因为过度打桩实际上是在测“假实现”。
工具链与架构约束
- 新增 TypeScript 文件后运行
yarn updateIgnored(在仓库根目录执行):这条规则背后是一个自动生成机制,下文“自动化落地”一节会展开。 - 遇到 cSpell 未知单词时按 readme/dev/spellcheck.md 的规定处理:加入词表或使用
cSpell:disable注释块,详见后文。 - 编译 TypeScript 使用
yarn tsc;只做类型检查不产出文件时用yarn tsc --noEmit:package.json 中tsc脚本实际是yarn workspaces foreach --worktree --parallel --verbose --interlaced run tsc,即按 workspace 并行分发到各个packages/*子包中执行各自的tsc,这与项目的 monorepo 结构("workspaces": ["packages/*"])一致。 - SQL 查询只允许出现在 models 中(
packages/lib/models目录):这是 Joplin 的核心架构约束——packages/lib是可被 CLI、桌面端(Electron)、移动端(React Native)三端共用的数据层,所有数据访问必须收敛到packages/lib/models/下的模型类(如BaseItem.ts、Note.ts等),禁止在 service、命令或其他任意位置直接拼接 SQL,从而保证三端数据库驱动(SQLite / better-sqlite / 移动端存储)下的行为一致。
桌面端样式规范(Styling)
CLAUDE.md 的“Styling (desktop app)”一节规定:
- 桌面端只使用 RSCSS + SCSS 文件组织样式,禁止内联
style={{...}}和 styled-components; - 例外:内联
style只用于“真正的逐实例动态值”(例如计算出来的宽度、坐标位置); - 主题色一律使用
var(--joplin-*)CSS 变量,而不是在 JS 中读取主题对象再传颜色值。
这些规则的完整依据在 readme/dev/spec/desktop_styling.md。该文档交代了桌面端样式方案的三次演进:直接内联 style(缺乏灵活性、无法被自定义样式表覆盖)→ styled-components(需要为每种样式建组件、且存在若干样式失效的 bug)→当前的 SCSS + 普通 CSS 类。
工程上,应用样式表由构建packages/app-desktop/style.scss生成:新建组件时应同时新建一个样式文件(如MyComponent/style.scss)并在根style.scss中引入,全局样式放根 main.scss;每次构建会把style.scss编译为最终的style.css打进应用。
类名组织遵循RSCSS 约定,需要记住的要点:
- 组件类名至少两个词,如
.search-form; - 组件内元素类名只用一个词,如
.field、textinput; - 始终使用
>子代选择器,防止样式在嵌套组件间“渗透”。
例如:
<form class="order-form"> <input class="firstname" type="text"/> <div class="slider-box"> Select quantity: <div class="knob"></div> </div> </form>.order-form > firstname, .order-form > lastname { padding: 10px; }若某个元素(如slider-box)既是独立组件又是父组件的子元素,不要写.order-form > .slider-box,而是给它额外命名(如quantityslider)后选择.order-form > .quantityslider,目标是“精准命中标定的元素,不多不少”。
自动化落地:pre-commit 钩子、lint-staged 与 cSpell
CLAUDE.md 末尾指向 readme/dev/coding_style.md,其开篇说明了这些规范的执行机制:编码风格主要由运行eslint的 pre-commit 钩子强制,该钩子在任一应用目录执行yarn install时自动安装;若钩子缺失,在仓库根目录重新执行yarn install即可恢复。
仓库中可验证的对应物:
- .husky/pre-commit 的内容只有一行
corepack yarn lint-staged,即提交前对暂存文件运行 lint-staged; - 根目录 lint-staged.config.js 定义了对待提交文件执行检查的具体命令;
- package.json 的
postinstall脚本为husky && gulp build,解释了“yarn install之后钩子即生效”的原因; - 手动运行 lint 的方式为
yarn linter ./(即eslint --fix --quiet);CI 使用yarn linter-ci(eslint --quiet,不带--fix,保证失败即报红)。
coding_style.md 还给出了新增 ESLint 规则的标准流程:把规则加进 ESLint 配置后,老代码往往会大面积报错,此时二选一——文件不多且改动简单就直接逐一修复(首选);或者运行yarn linter-interactive ./,交互式为存量违规行逐条添加eslint-disable-next-line注释,并统一标注 “Old code before rule was applied” 以便日后检索。这样存量代码保持不动、新代码强制遵守规则。
yarn updateIgnored背后的生成机制
CLAUDE.md 要求“新增.ts文件后运行yarn updateIgnored”,其实现是 packages/tools/gulp/tasks/updateIgnoredTypeScriptBuild.js:
- 以 glob 扫描全仓库的
**/*.ts与**/*.tsx(排除node_modules、各包构建产物、测试夹具、packages/server、packages/utils等目录,并过滤掉.d.ts); - 为每个
.ts文件推导对应编译产物.js的文件名; - 用正则定位
.gitignore与.ignore.eslint中由# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD标记的自动生成分区,把最新列表整块替换进去。
原理是:TypeScript 编译会产出.js文件,这些本地产物既不能进版本库也不能参与 lint,所以每次源码文件增删后都要重新生成这两处忽略清单——yarn updateIgnored(对应node packages/tools/gulp/tasks/updateIgnoredTypeScriptBuildRun.js)就是把这一机械步骤自动化。
拼写检查(cSpell)
CLAUDE.md 引用了 readme/dev/spellcheck.md:仓库中的 Markdown 与 TypeScript 文件会被 cspell.json 配置的 CSpell 自动检查。实操要点:
- 全量检查:
yarn spellcheck --all;单文件:yarn spellcheck /path/to/file; - pre-commit 钩子会自动检查新提交文件中的拼写;
- 忽略词两种途径:追加到
packages/tools/cspell/下的词表文件(每个词表不超过约 400 词,超出会导致 CSpell 加载失败,需另建词表并在cspell.json中注册),或对含大量不可忽略词的代码块使用// cSpell:disable/// cSpell:enable(Markdown 中为<!-- cSpell:disable -->);更推荐优先用词表以免污染代码; - 也可在
cspell.json中用ignore属性按路径或正则整体跳过(例如忽略 changelog 中的 GitHub 用户名)。
深入配套文档:coding_style.md 的关键细则
CLAUDE.md 的快速参考是“摘要层”,readme/dev/coding_style.md 是“细则层”,两者必须结合阅读。以下几组细则对实际写代码影响最大。
文件命名与导入
- 导出多个东西的文件用
camelCase.ts;仅当文件包含单一类且为默认导出时才用PascalCase.ts;共享类型定义放types.ts或fooTypes.ts; - 导出成员与导入成员保持相同的命名大小写(文件、函数、导入名一致);
- 只导入需要的成员(利于 tree shaking):
import { writeFile } from 'fs-extra'优于import * as fs; - TypeScript 文件中优先
import而非require,以便享受类型检查;老包没有类型声明时才退回require(); - 避免内联类型,类型应单独定义以便复用:
type Config = Record<string, Knex.Config>; const config: Config = { /* ... */ };变量与函数
- 新代码中的常量用
camelCase(不用全大写SNAKE_CASE); - 变量声明尽量贴近其首次使用处(避免“声明在最顶部却隔了很远才用”);
- 优先
const而非let;优先箭头函数() => {}而非function() {}——不用this便于日后把类组件重构为 React Hooks,且对this的误用会被 TypeScript 直接报错; - 尽量避免默认参数与可选字段:所有参数都必填时,重构代码编译器会自动帮你发现漏传。
安全:转义用户内容(Escape variables)
这是细则文档中篇幅最大的安全章节,核心原则:永远不要假设输入安全,即使你认为自己控制了输入;且尽可能晚地转义——应用内部保持原始数据,只在边界处解码/编码,避免双重转义。按插入位置选择手段:
| 插入位置 | 手段 |
|---|---|
| JS 脚本字符串 | JSON.stringify(data) |
| HTML 字符串 | htmlentities(来自 packages/utils/html.ts),如htmlentities(content) |
| URL 查询参数 | encodeURIComponent;完整 URL 用encodeURI |
| Markdown | packages/lib/markdownUtils.ts 提供escapeTableCell()、escapeInlineCode()、escapeTitleText()、escapeLinkUrl() |
文档还提出了“Make wrong code look wrong”命名法:给已转义变量加后缀(如userContentHtml),让“把未转义变量插进 HTML”这件事在代码审阅中一眼可见、一眼可疑。
数据库约定
- 表名与列名一律
snake_case; - 所有列
NOT NULL,可带默认值,避免查询中同时处理NULL/0/ 空串; - 默认值要克制——多数情况应要求调用方显式传值;
- 枚举值用 integer 列 + TypeScript enum表达,不用数据库内置 enum(迁移困难);
- 布尔语义优先
tinyint(1)而非bool(SQLite/MySQL 中布尔本就不是独立类型)。
React 与 GitHub Actions
- 新组件一律函数组件 + Hooks,不写
extends Component类组件;长逻辑抽成自定义 hook(注意 hook 必须以use开头,否则 eslint 会报 “called outside of a component”); - GitHub Actions 的
run块内不要内嵌${{ }}(存在脚本注入风险),应通过env传入后再引用环境变量。
文档引用与延伸阅读
CLAUDE.md 末尾的“Full Documentation”给出了两个权威入口:
- 编码风格全文:readme/dev/coding_style.md(本文已大量引用);
- 贡献指南:CONTRIBUTING 文件,其内容指向 readme/dev/index.md 这一开发者指南总入口,该目录下还有构建(
BUILD.md)、部署(DEPLOY.md)、技术规格(technical_spec.md)、本地化、构建排错 以及 readme/dev/spec/ 下数十篇子系统规格文档(同步、服务器、编辑器、插件等)。
适用前提小结
本文所述约定均以当前仓库实际内容为准:Node 引擎要求>=22.12、Yarn 版本锁定(engines 声明4.12.0,packageManager声明yarn@4.16.0,见 package.json)。如果你要在 Joplin 仓库内提交代码,最短执行路径是:写完代码后先跑yarn tsc --noEmit做类型检查、yarn linter ./修风格问题、新增.ts文件后执行yarn updateIgnored、遇到拼写告警按 readme/dev/spellcheck.md 处理词表——pre-commit 钩子会在提交时兜底拦截不符合 CLAUDE.md 约定与 ESLint 规则的代码。
【免费下载链接】joplinJoplin - the privacy-focused note taking app with sync capabilities for Windows, macOS, Linux, Android and iOS.项目地址: https://gitcode.com/GitHub_Trending/jo/joplin
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考