EUI ESLint 插件 v3.0.0 全解析:@elastic/eslint-plugin-eui 新规则、图标别名迁移与 EuiButtonGroup 强校验
【免费下载链接】euiElastic UI Framework 🙌项目地址: https://gitcode.com/GitHub_Trending/eu/eui
本文以packages/eslint-plugin/changelogs/CHANGELOG_2026.md为主线,完整梳理@elastic/eslint-plugin-eui在 2026 年度版本线(v2.7.0 → v3.0.0)中的全部规则演进:包括EuiButtonGroup两条强校验规则(button-group-no-invalid-children、button-group-selection-require-id)的分 variant 校验逻辑与嵌套遍历策略,no-deprecated-icon-aliases的三层图标映射体系(可自动修复别名 / 仅告警指导 / 无替代项),以及no-nested-copy-tooltip等新规则的设计动机。读完后你可以理解 v3.0.0 的 breaking change 影响面,并能在自己的项目中正确配置和排查这些 ESLint 规则。
版本线总览:从 v2.7.0 到 v3.0.0
该插件位于仓库的 packages/eslint-plugin 目录,当前版本为 3.0.0(见 package.json)。peerDependencies要求eslint: ^8.57.0 || ^9.0.0、typescript: >=4.8.4 <6.0.0,即同时兼容 ESLint 8 与 ESLint 9 两种配置体系。2026 年度的 CHANGELOG_2026.md 记录了以下版本节奏:
- v2.7.0:新增
no-static-z-index规则; - v2.8.0:新增
icon-accessibility-rules与badge-accessibility-rules两条可访问性规则; - v2.9.0:修复
badge-accessibility-rules自动修复会重复添加aria-hidden的问题,并在组件带展开运算符(spread)时跳过校验; - v2.10.0:
require-aria-label-for-modals将EuiPopover、EuiWrappingPopover纳入检查范围,同样要求aria-label或aria-labelledby;并在组件带 spread 属性时跳过 ARIA 校验; - v2.11.0 / v2.11.1:
no-unnamed-interactive-element纳入EuiColorPicker;修复no-css-color在分析返回undefined或非对象值的函数时崩溃的 bug; - v2.12.0:新增
prefer-tooltip-trigger-focus-test-utility,标记it/test块中同时查询 tooltip 却又使用fireEvent.focus()的写法,并自动修复为 EUI RTL 测试工具中的focusEuiToolTipTrigger; - v2.13.0:新增
tooltip-button-icon-wrap、tooltip-no-interactive-content、require-href-for-link三条规则; - v2.14.0:移除
tooltip-button-icon-wrap的自动修复能力; - v2.14.1:修复插件无法被 ESLint 加载的问题——把运行时依赖(
cssstyle、micromatch、@typescript-eslint/utils、@typescript-eslint/typescript-estree)从devDependencies移入dependencies; - v2.15.0:新增
callout-prefer-props-for-content规则; - v2.16.0:新增
no-nested-copy-tooltip规则,并为tooltip-button-icon-wrap增加EuiCopy例外;新增button-group-no-invalid-children与no-deprecated-icon-aliases两条规则; - v3.0.0:
button-group-no-invalid-children支持variant="selection"与 segmented variant 检查;新增button-group-selection-require-id规则;no-deprecated-icon-aliases大幅扩充映射表(见下文)。
从源码结构看,v3.0.0 的两条EuiButtonGroup规则被注册为error级别,其余规则均为warn级别(见 src/index.ts 中configs.recommended的配置):
'@elastic/eui/button-group-no-invalid-children': 'error', '@elastic/eui/button-group-selection-require-id': 'error',这一级别差异本身就传递了一个信号:ButtonGroup 的结构性错误(非法子元素、selection 模式缺失id)会在运行时直接破坏组件功能,因此按 error 处理;而可访问性、样式类问题多为渐进式迁移项,按 warn 处理。
button-group-no-invalid-children:分 variant 的合法子元素校验
该规则要求EuiButtonGroup(使用 Children API 时)的直接子元素必须是合法的按钮组件。合法集合按 variant 区分,定义在 src/utils/button_group_constants.ts 中:
| variant | 合法直接子元素 | 附加约束 |
|---|---|---|
default(或不写 variant) | EuiButton、EuiButtonEmpty、EuiButtonIcon | 无 |
segmented | 仅EuiButton、EuiButtonIcon | 所有子按钮必须是同一类型 |
selection | 仅EuiButton、EuiButtonIcon | 所有子按钮必须是同一类型 |
所有 variant 下均允许三个包装组件:EuiToolTip、EuiPopover、EuiCopy。
规则演进路径
CHANGELOG 记录了这个规则的完整成长轨迹:
- v2.16.0 首次引入(CHANGELOG_2026.md),初始只覆盖 default variant 的非法子元素检测;
- v3.0.0 支持 segmented variant:
variant="segmented"下EuiButtonEmpty不再被允许,且EuiButton与EuiButtonIcon混用会被报告; - v3.0.0 支持 selection variant:同样禁止
EuiButtonEmpty、禁止混用按钮类型。
源码级实现剖析
规则实现位于 src/rules/button_group_no_invalid_children.ts。几个关键实现细节值得注意:
1. 保守跳过动态 variant。规则通过findAttrValue静态读取variant属性值(L85-L96):只有值是undefined(未写)、default、segmented、selection时才继续校验;variant={myVar}这类动态值会被保守跳过,避免误报。
2. 三种包装组件各有一套解析路径。主循环(L117-L245)对三类包装的处理并不对称:
EuiToolTip:展开其 JSX children(包括 fragment、条件表达式),对内部按钮校验并收集按钮类型;EuiPopover:触发器不是 children 而是buttonprop,规则提取该 prop 的值并收集其中的 JSX 元素;若触发器本身被EuiToolTip包裹,还会再展开一层去校验 tooltip 内部的按钮;EuiCopy:children 是 render prop({(copy) => <EuiButton />}),由工具函数collectJsxChildren展开箭头函数表达式体后按同一路径校验。
3. 混用类型检查只遍历一次。对 segmented/selection variant,规则用seenButtonTypes集合在主循环中顺带收集出现过的按钮类型(L114-L115 注释明确说"Populated during the main loop to avoid a second traversal"),循环结束后若同时包含EuiButton和EuiButtonIcon就报告invalidMixedTypes(L250-L259)。
4. spread 属性一律保守放行。无论是包装组件内部还是顶层子元素,只要带{...props},规则就跳过该节点——因为无法静态确定最终属性集合。
5. 无法静态解析的自定义组件单独提示。对首字母大写的未知组件(如<SaveButton />),报告invalidUnresolvableChild,报错信息内联给出抑制建议(L275-L283):
// eslint-disable-next-line @elastic/eui/button-group-no-invalid-children -- SaveButton only wraps EuiButton <SaveButton />典型正误示例(完整示例见 README.md):
// ✗ 非法:非按钮元素 <EuiButtonGroup legend="Actions"> <div>Not a button</div> </EuiButtonGroup> // ✗ 非法:segmented 混用 EuiButton 与 EuiButtonIcon <EuiButtonGroup legend="Actions" variant="segmented"> <EuiButton>Save</EuiButton> <EuiButtonIcon iconType="trash" aria-label="Delete" /> </EuiButtonGroup> // ✓ 合法:Popover 触发器为 ToolTip 包裹的图标按钮 <EuiButtonGroup legend="Actions"> <EuiPopover button={ <EuiToolTip content="More options"> <EuiButtonIcon iconType="boxesVertical" aria-label="More options" /> </EuiToolTip> } isOpen={isOpen} closePopover={closePopover} > Panel content </EuiPopover> </EuiButtonGroup>button-group-selection-require-id:selection 变体的 id 强约束
这是 v3.0.0 新增的第二条规则(src/rules/button_group_selection_require_id.ts),与上一条规则配合:variant="selection"的EuiButtonGroup用每个按钮的id来追踪选中状态,缺失id会导致选中态错乱或完全失效,因此这是运行时功能正确性问题,而非风格问题——这也是它被列为error的原因。
实现要点:
- 只在静态
variant="selection"时触发(L57-L63),动态 variant 直接跳过; - 与 no-invalid-children 相同的嵌套遍历深度:fragment、条件渲染、
.map()、以及EuiToolTip/EuiPopover触发器 /EuiCopyrender prop 三层包装; hasMeaningfulAttr判定 id:带 spread 的子按钮同样被跳过,因为id可能来自展开的 props;- 非按钮非包装的子元素不在此规则职责内(L136-L137 注释明确"the other rule handles those"),两条规则职责正交、互不重复报告。
// ✗ 缺失 id <EuiButtonGroup legend="Format" variant="selection"> <EuiButton>Bold</EuiButton> </EuiButtonGroup> // ✓ 全部带 id,含嵌套在 ToolTip / Popover 触发器中的按钮 <EuiButtonGroup legend="Format" variant="selection"> <EuiButton id="bold">Bold</EuiButton> <EuiToolTip content="Italic"> <EuiButton id="italic">Italic</EuiButton> </EuiToolTip> <EuiPopover button={<EuiButton id="more">More</EuiButton>} isOpen={false} closePopover={() => {}}> Panel content </EuiPopover> </EuiButtonGroup>no-deprecated-icon-aliases:三层图标迁移体系
该规则于 v2.16.0 引入,v3.0.0 中经历了多轮映射表更新(#9884、#9923、#9974),是本次版本线中改动最密集的规则。规则本体在 src/rules/no_deprecated_icon_aliases.ts,从源码结构看,它把弃用图标分成三档,对应三种报告行为:
第一档:有明确替代项的别名(DEPRECATED_ICON_ALIASES)—— 报告deprecatedIconAlias并自动修复为替代图标(L405-L420)。fixer 会保留原字符串的首字符引号做替换。v3.0.0 新增/调整的映射(均出自 CHANGELOG):
- 新弃用:
cloudDrizzle→cloudRain、cloudStormy→cloudBolt、cloudSunny→cloudSun、kqlFunction→push、menuDown→transitionBottomOut、menuUp→transitionTopOut、namespace→kubernetesNamespace、visTimelion→productTimelion、visVisualBuilder→productTSVB(#9923); - 大量历史别名补充(#9884):如
analyzeEvent→cube、annotation→flag、apps→grid、help→question、index→table、payment→money、spaces→grid、stats→chartLine、unfold→maximize、visGoal→chartGauge、wordWrap→lineBreak等完整 30 余项,全部以 源码映射表 为准; - 星形系列统一:
starFilledSpace、starMinusFilled、starPlusFilled全部映射到starFill。
第二档:无替代项的图标(DEPRECATED_ICONS_WITHOUT_REPLACEMENT)—— 仅报告deprecatedIcon("has no direct replacement"),不 autofix。v3.0.0 在此集合中加入article、dotInCircle、kubernetesNode、scale、securitySignalDetected(#9884)。
第三档:有上下文相关替代的图标(DEPRECATED_ICONS_WITH_GUIDANCE)—— 报告deprecatedIconGuidance,给出人工判断指引,不做 autofix(#9884):
folderExclamation→ 建议linkSlash(unlink 场景)或hourglass(Kibana Cases 状态场景);stopFill/stopSlash→ 建议用EuiColorPickerSwatch表示色块,其他场景用stop。
v3.0.0 的Deprecations一节还更新了folderClosed、folderClose→folder的映射(#9974)。
规则的静态分析边界
从源码看(L270-L309),规则只检查静态字符串的图标属性值(字符串字面量、无插值的模板字面量),并且只针对从@elastic/eui导入的组件。它识别的"图标属性"分两类:
- 属性名以
IconType结尾的属性(大小写不敏感); EuiIcon、EuiIconTip的type属性;- 组件专属图标属性白名单:
EuiCommentTimeline的timelineAvatar、EuiTimelineItem的icon、EuiLoadingLogo的logo、EuiFormAppend/EuiFormAppendPrepend/EuiFormPrepend的iconLeft/iconRight(L260-L268)。
动态值(type={iconVar})会被跳过。
Breaking change:push图标别名被移除
CHANGELOG 明确标注了一条破坏性变更(#9923):push图标别名从no-deprecated-icon-aliases中移除——push重新成为受支持的EuiIconglyph,不再被自动修复为send。升级 v3.0.0 的项目中,原本依赖 autofix 把push替换成send的代码会停止收到告警。但要注意映射表中新增的kqlFunction: 'push':kqlFunction会被修复为push,而不是修复为send,两者方向相反,理解这条 breaking change 时容易混淆。
no-nested-copy-tooltip:修复EuiCopy的嵌套 tooltip 陷阱
v2.16.0 新增的规则(src/rules/a11y/no_nested_copy_tooltip.ts)。问题背景:EuiCopy内部已经会用beforeMessage作为EuiToolTip的 content 包裹其子元素;如果 render prop 返回的第一个孩子又是一个EuiToolTip,就会形成嵌套且互相冲突的 tooltip。
// ✗ 嵌套 tooltip,子 EuiToolTip 与 beforeMessage 冲突 <EuiCopy textToCopy="some text" beforeMessage="Click to copy"> {(copy) => ( <EuiToolTip content="Copy me"> <EuiButton onClick={copy}>Copy</EuiButton> </EuiToolTip> )} </EuiCopy> // ✓ 去掉 EuiToolTip 包裹,让 beforeMessage 驱动 tooltip <EuiCopy textToCopy="some text" beforeMessage="Copy me"> {(copy) => <EuiButton onClick={copy}>Copy</EuiButton>} </EuiCopy>该规则只报告不自动修复,原因是 tooltip 的content与已有beforeMessage无法总能安全合并(任一为变量或 JSX 时都不行),需要手工把消息并入beforeMessage。同时 v2.16.0 为tooltip-button-icon-wrap加入了配套例外:EuiCopy设置了非空beforeMessage时,其 render-prop 子元素中的EuiButtonIcon不再被报"缺少 ToolTip 包裹",因为EuiCopy已经提供了可见 tooltip——这个例外是条件性的,beforeMessage缺失或静态为假值时仍会报告。
配置、测试与本地联调
接入方式(摘自 README.md):安装@elastic/eslint-plugin-eui为 devDependency,然后在 ESLint 配置中扩展plugin:@elastic/eui/recommended。v3.0.0 的 recommended 配置包含 25 条规则(两条 ButtonGroup 规则为 error,其余为 warn)。
单元测试:规则测试基于@typescript-eslint/rule-tester(见 package.json 的 devDependencies),在packages/eslint-plugin目录下运行yarn test(即jest src)即可执行。每条规则都有同名.test.ts文件与实现成对存放,例如 button_group_no_invalid_children.test.ts 覆盖了 default/segmented/selection 三种 variant、三类包装组件、fragment/条件/map 嵌套、spread 跳过等边界场景。
本地联调:README 给出了基于 yalc 的完整流程——cd packages/eslint-plugin后执行yarn build,再yalc publish、在目标项目yalc add @elastic/eslint-plugin-eui,之后每次改动重复 build + publish 即可。
升级 v3.0.0 的行动清单
结合 CHANGELOG 与源码,升级@elastic/eslint-plugin-eui到 3.0.0 需要关注四件事:
- 修复
EuiButtonGroup结构性问题:两条 ButtonGroup 规则在 recommended 中是 error,CI 会直接失败。重点检查variant="segmented"/variant="selection"下的EuiButtonEmpty子元素与EuiButton/EuiButtonIcon混用; - 为 selection 组补齐
id:button-group-selection-require-id会遍历 ToolTip、Popover 触发器、Copy render prop 内的按钮,动态 variant 与带 spread 的按钮会被跳过; - 批量跑
no-deprecated-icon-aliases的 autofix:映射表已扩充到 200+ 项(完整表),--fix可一次替换所有有明确替代项的别名;对folderExclamation、stopFill、stopSlash以及无替代项的 5 个图标需人工决策; - 注意
push不再被修复:v3.0.0 移除push→send的自动修复,同时新增kqlFunction→push,升级前建议全局搜索这两个图标名确认语义。
规则实现的共享工具(src/utils/ 下的collect_jsx_children.ts、has_spread.ts、get_attr_value.ts等)是这套规则能稳定处理嵌套与保守跳过的关键,阅读具体规则行为遇到疑问时,可以直接对照对应 util 的测试文件验证边界。
【免费下载链接】euiElastic UI Framework 🙌项目地址: https://gitcode.com/GitHub_Trending/eu/eui
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考