深入解读 @lexical/eslint-plugin:用 ESLint 规则守护 Lexical 的 $function 约定
发布时间:2026/9/12 13:06:27 作者:尧图编辑部 阅读量:1,286

深入解读 lexical/eslint-plugin用 ESLint 规则守护 Lexical 的 $function 约定【免费下载链接】lexicalLexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.项目地址: https://gitcode.com/GitHub_Trending/le/lexicalLexical 是一个强调可靠性与可扩展性的文本编辑器框架其 API 的核心约定是$ 前缀函数如$getRoot、$createTextNode只能在editor.update()、editorState.read()等受限上下文中调用。本文以 packages/lexical-eslint-plugin/README.md 为骨架结合该包源码与测试系统讲解如何通过lexical/eslint-plugin在 ESLint 710 中自动强制这一约定涵盖安装、Legacy/Flat 双配置体系、rules-of-lexical规则的四个可扩展匹配器以及三种带 autofix 的违规修复场景。读完本文你将能独立完成插件的接入、自定义匹配规则并理解其底层 AST 分析原理。插件定位把约定变成可执行的规则Lexical 要求所有直接读写 EditorState 的 API 都以$开头例如$getRoot()、$createTextNode()。这一命名约定的意义在于以$为前缀的函数只能在editor.update()或editorState.read()提供的上下文中被调用否则无法保证读取到的是最新、最稳定的状态。lexical/eslint-plugin的作用就是把这条靠人脑记忆的软约定转化为构建期即可发现的硬性错误。从 插件入口 src/index.ts 的类型定义可以看出该插件目前暴露两条规则lexical/rules-of-lexical核心规则强制$function命名约定type: suggestion支持自动修复lexical/no-document-in-dom-methods禁止在节点的createDOM/updateDOM/exportDOM/$decorateDOM等 DOM 方法中直接使用全局document并提供将document自动替换为$getDocument()的修复type: problem为 Shadow DOM / iframe 场景的安全性设计。两条规则的实际注册位于 LexicalEslintPlugin.js插件的peerDependencies要求eslint 7.31.0、typescript 5.2TypeScript 为可选 peer见 package.json。安装与版本兼容文档明确指出该插件同时支持 ESLint 7、8、9 与 10且同时支持旧版.eslintrclegacy与新版eslint.config.jsflat两种配置格式。前提是项目已安装 ESLint然后安装插件npm install lexical/eslint-plugin --save-devESLint 9Flat ConfigESLint 9 起 flat config 成为默认ESLint 10 强制要求使用 flat config。在eslint.config.js中直接引入插件自带的推荐配置即可import lexical from lexical/eslint-plugin; export default [ // ... other configs lexical.configs[flat/recommended] ];ESLint 7-8Legacy ConfigESLint 7 或 8 使用传统.eslintrc格式通过extends继承推荐配置{ extends: [ // ... plugin:lexical/legacy-recommended ] }注意从 LexicalEslintPlugin.js 源码可以看到recommended与all目前都是legacy-recommended、legacy-all的别名而flat/recommended与flat/all指向同一份 flat 配置。也就是说all与recommended目前内容完全一致文档声明它们将在未来版本中迁移为独立的 flat config。无论哪种配置形态内置配置默认都将lexical/rules-of-lexical设为warn警告级别并非直接报错需要更强约束时建议手动配置为error。自定义配置ESLint 9Flat Configimport lexical from lexical/eslint-plugin; export default [ { plugins: { lexical: lexical }, rules: { lexical/rules-of-lexical: error } } ];ESLint 7-8Legacy Config{ plugins: [ // ... lexical ], rules: { // ... lexical/rules-of-lexical: error } }两种写法的本质一致在plugins中注册名为lexical的插件然后以lexical/rules-of-lexical为键配置规则级别。高级配置四个可扩展的匹配器rules-of-lexical的大多数启发式逻辑都可以通过规则选项扩展。下面这份示例展示了每个选项的默认实现仅供参考不建议直接照抄——因为当你配置这些选项时它们是与默认实现以 OR 逻辑合并的默认实现无法被覆盖。匹配器的取值规则源码见 buildMatcher.js字符串若以^或(开头则被当作正则表达式字面源使用否则按精确匹配处理内部会被转义并包裹^...$字符串也可以直接替代字符串数组传入。ESLint 9Flat Configimport lexical from lexical/eslint-plugin; export default [ { plugins: { lexical: lexical }, rules: { lexical/rules-of-lexical: [ error, { isDollarFunction: [^\\$[a-z_]], isIgnoredFunction: [], isLexicalProvider: [ parseEditorState, read, registerCommand, registerNodeTransform, update ], isSafeDollarFunction: [^\\$is] } ] } } ];ESLint 7-8Legacy Config{ plugins: [ // ... lexical ], rules: { // ... lexical/rules-of-lexical: [ error, { isDollarFunction: [^\\$[a-z_]], isIgnoredFunction: [], isLexicalProvider: [ parseEditorState, read, registerCommand, registerNodeTransform, update ], isSafeDollarFunction: [^\\$is] } ] } }isDollarFunctionBase case/^\$[a-z_]/定义$function约定默认情况下任何以$开头后跟小写拉丁字母或下划线的函数都被视为 Lexical 的$函数。如果你所在代码库有第二套约定——比如非拉丁字符开头或需要纳入内部前缀如^INTERNAL_\\$可以在此补充。注意该选项只能追加模式无法移除默认的$前缀识别。从 rules-of-lexical.js 的BaseMatchers常量可以看到源码中四个匹配器的默认值即文档所示compileMatchers会调用buildMatcher(BaseMatchers[k], parseMatcherOption(context, k))将默认值与用户配置逐项 OR 合并。isIgnoredFunctionBase case无匹配这些模式的函数将被排除在分析之外它们内部可以调用 Lexical$函数但自身不会被认定为$函数也不会因此被要求重命名。isLexicalProviderBase case/^(parseEditorState|read|registerCommand|registerNodeTransform|update)$/这类函数允许其函数参数内部使用 Lexical$函数。这正是$函数只能在update/read等上下文中调用的规则入口当分析器遇到以这些名字无论作为成员表达式editor.update、editorState.read还是独立调用传出的回调时会将该回调标记为安全上下文。isSafeDollarFunctionBase case/^\$is/这类$函数被认为是在任何地方都可以安全调用的——通常它们是无状态运行时类型检查如$isElementNode、$isTextNode不依赖任何外部状态。因此即使不在update/read上下文中调用它们也不会触发规则。在源码的CallExpression处理中matchers.isSafeDollarFunction(calleeName)命中的调用会直接进入ignoreSet跳过分析。规则的底层工作机制理解了四个匹配器之后再看 rules-of-lexical.js 的实现就能明白整条规则的执行脉络遍历函数定义对FunctionDeclaration、FunctionExpression、ArrowFunctionExpression进入时入栈、退出时出栈构成一个当前函数栈funStack。函数名通过 getFunctionName.js 与 getParentAssignmentName.js 解析后者还支持const $fun useCallback(() {}, [])这种高阶 Hook 赋值场景从useCallback/useMemo的父节点反推函数名跳过安全上下文当栈顶函数命中isDollarFunction/isIgnoredFunction/isLexicalProvider或调用命中isSafeDollarFunction时节点被加入ignoreSet跳过分析类的方法体ClassBody整体也会被跳过——这正是文档中$函数可以从类方法中调用这一合法示例成立的原因触发报告当在一个非安全上下文的函数内检测到对$函数的调用且该外层函数自身名字不满足$约定时规则以rulesOfLexicalReport消息上报{{ callee }} called from {{ caller }}, without $ prefix or read/update context并附带重命名建议suggestion与自动修复fix防重复上报通过reportedSet保证同一个函数多次调用$函数时只报告一次只需重命名一次。值得一提的细节规则对成员表达式如editor.update、editorState.read的处理只关心方法名本身getFunctionNameIdentifier会取MemberExpression的property所以文档特别说明heuristic only considers the method name。合法用法Valid Examples文档给出三类合法场景均对应上述机制1.$函数可以调用其他$函数function $namedCorrectly() { return $getRoot(); }外层函数$namedCorrectly自身满足$约定命中isDollarFunction因此整个函数体被跳过分析。2.$函数可以在下列方法提供的回调中调用启发式只考虑方法名editor.updateeditorState.readeditor.registerCommandeditor.registerNodeTransformfunction validUsesEditorOrState(editor) { editor.update(() $getRoot()); editor.getLatestState().read(() $getRoot()); }这些方法名命中isLexicalProvider其回调参数被标记为安全上下文。3.$函数可以在类方法中调用class CustomNode extends ElementNode { appendText(string) { this.appendChild($createTextNode(string)); } }类方法体ClassBody在分析时整体入ignoreSet跳过因此类方法内部调用$函数是合法的。违规场景与自动修复Invalid Examples这是rules-of-lexical最有价值的部分不仅报告错误还能通过--fix自动修复。文档提供了三种典型违规场景。场景一普通函数重命名Rename autofixfunction invalidFunction() { return $getRoot(); } function $callsInvalidFunction() { return invalidFunction(); }Autofix函数被加上$前缀重命名同时本模块内对该名字的所有引用也会一并被替换function $invalidFunction() { return $getRoot(); } function $callsInvalidFunction() { return $invalidFunction(); }底层实现中fixer 会通过getIdentifierVariable解析函数对应的作用域变量遍历variable.references将所有引用标识符统一替换为建议名见 rules-of-lexical.js 中的renameIdentifier逻辑。场景二导出函数重命名并保留旧名Rename deprecate autofixexport function exportedInvalidFunction() { return $getRoot(); }Autofix导出函数被加上$前缀重命名同时保留旧名字的导出并标记为废弃——因为自动重命名引用只限于本模块作用域模块外的引用无法被同步修改因此需要旧名作为向后兼容的过渡export function $exportedInvalidFunction() { return $getRoot(); } /** deprecated renamed to {link $exportedInvalidFunction} by lexical/eslint-plugin rules-of-lexical */ export const exportedInvalidFunction $exportedInvalidFunction;对应源码中的getExportDeclaration会识别export function与export const foo () {}两种导出形态并通过renameExportText生成带deprecatedJSDoc 注释的兼容导出。场景三重命名与作用域冲突Rename scope conflictimport {$getRoot} from lexical; function InvalidComponent() { const [editor] useLexicalComposerContext(); const getRoot useCallback(() $getRoot(), []); return (button onClick{() editor.update(() getRoot())} /); }这里getRoot包装了$getRoot()但名字不符合$约定。注意InvalidComponent是组件函数isHookFunctionIdentifier检测 Hook 命名而const getRoot useCallback(...)是典型的 Hook 赋值——规则通过getParentAssignmentName正确识别出该函数名为getRoot。Autofix建议名$getRoot已经与导入的$getRoot冲突会遮蔽已有变量因此规则自动附加下划线后缀import {$getRoot} from lexical; function InvalidComponent() { const [editor] useLexicalComposerContext(); const $getRoot_ useCallback(() $getRoot(), []); return (button onClick{() editor.update(() $getRoot_())} /); }命名冲突检测在getSuggestName中实现它从函数所在作用域向上遍历scope.upper若建议名$getRoot已被作用域中的变量占用则返回$getRoot_。另外从getFirstSuggestion可以看到更精细的命名策略小写开头直接加$前缀PascalCase名称如InvalidFunction会转为$invalidFunction首字母小写其他情况退化为$_前缀。附带规则no-document-in-dom-methods除核心的rules-of-lexical外插件还附带lexical/no-document-in-dom-methods规则。该规则禁止在以下 DOM 生命周期方法中直接引用全局documentcreateDOMupdateDOMexportDOM$decorateDOM违规时提供 autofix将document替换为$getDocument()源码见 no-document-in-dom-methods.js。$getDocument()会返回节点实际所在的文档从而保证在 Shadow DOM 或 iframe 等非主文档环境中也能正确操作避免误用顶层全局document。需要注意autofix 只替换标识符本身不会自动补充 import这是文档与源码中均明确提示的边界。测试验证与使用建议运行集成测试插件仓库内置了跨 ESLint 版本的集成测试文档命令行在仓库根目录执行node packages/lexical-eslint-plugin/__tests__/integration-test.js根据 integration-test.mjs 的实现该脚本会验证✓ ESLint 8 传统.eslintrc.json配置✓ ESLint 10 flateslint.config.js配置✓ Legacy 配置名别名recommended与legacy-recommended。测试通过pnpm dlx eslint8/pnpm dlx eslint10临时拉起不同版本的 ESLint不修改package.json或pnpm-lock.yaml对 ESLint 8 还会显式设置ESLINT_USE_FLAT_CONFIGfalse以避免 flat config 探测干扰。测试夹具位于 fixtures 目录分别对应eslint8-legacy、eslint8-legacy-deprecated与eslint10-flat三套独立环境。单元测试与文档一致性值得强调的是该插件的单元测试 rules-of-lexical.test.ts 中有一个特殊设计测试会直接读取本包 README.md从### Valid Examples与### Invalid Examples章节中解析代码块动态构造RuleTester用例来运行——也就是说文档中每个合法/违规示例都经过真实 ESLint 规则引擎的验证保证文档即测试。这使得上面列出的所有示例都具有可复现性。实践建议从 warning 起步内置recommended配置默认是warn级别适合先在 CI 中观察存量代码的违规情况再逐步过渡到error善用--fix三种 autofix重命名、重命名废弃导出、冲突加后缀都能安全自动执行建议在提交前运行 ESLint fix 让工具完成机械性改造按需扩展匹配器如果团队有INTERNAL_$之类的内部前缀或特殊 provider 方法通过isDollarFunction/isLexicalProvider等选项增量追加即可无需修改插件源码配合 Shadow DOM 场景如果你的编辑器需要跑在 iframe 或 Shadow DOM 中同时启用no-document-in-dom-methods规则并在 autofix 后手动补充$getDocument的导入。小结lexical/eslint-plugin把 Lexical 最核心、也最容易出错的$function调用约定变成了可在 CI 中强制执行的自动化检查它同时兼容 ESLint 710 的 Legacy 与 Flat 配置体系核心规则rules-of-lexical通过四个可扩展匹配器覆盖函数命名、provider 上下文与安全类型检查函数并提供重命名、导出兼容与冲突规避三种自动修复策略no-document-in-dom-methods则守护 Shadow DOM / iframe 环境下的 DOM 操作安全。结合其文档即测试的工程实践这套插件既是 Lexical 开发者日常开发的贴身 lint 工具也是一个学习如何编写高质量 ESLint 插件的绝佳范例。【免费下载链接】lexicalLexical is an extensible text editor framework that provides excellent reliability, accessibility and performance.项目地址: https://gitcode.com/GitHub_Trending/le/lexical创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考