eslint-config-expo 演进全解:Expo 项目 ESLint 基础配置的规则、迁移与源码实现
eslint-config-expo 是 Expo 官方为 React Native / Expo 应用提供的最小化 ESLint 基础配置包,负责开箱即用地支持 JSX、TypeScript、平台化文件后缀(.android.js、.ios.js、.web.js)以及 React Native 专属全局变量,同时承接 expo/use-dom-exports 等 DOM 组件规则。本文以 CHANGELOG.md 为骨架,逐版梳理该配置包从 2024 年 7.0.0 到 57.0.1 的功能演进与破坏性变更,并结合当前仓库中的 default.js、flat 配置、utils 规则模块 与 测试用例 展开源码级讲解。读完你将掌握:expo 配置由哪些最小规则构成、flat config 与传统 .eslintrc 两种接入方式、各版本升级时需要注意的兼容点,以及如何按需扩展属于自己的 Expo lint 规则集。
一、定位:一个"刻意保持最小"的 Base 配置
先明确 eslint-config-expo 的定位——它不是"开箱即用的全量规则大全",而是 Base 配置。README.md 开篇即说明:
This is a minimal config that supports JSX and TypeScript, platform-specific global variables, and file extensions like
.android.js,.ios.jsand.web.js. You are intended to compose this base config with the linter rules of your choice in your own ESLint configuration.
即它只负责解决 Expo 工程中通用性最强的三件事:
- 解析并识别 JSX 与 TypeScript 代码;
- 提供 React Native / Expo 运行时环境(
__DEV__、fetch、FormData、navigator等全局变量); - 理解平台化文件扩展名(
.android.*、.ios.*、.web.*、.native.*)的导入解析。
具体的编码风格、可读性、复杂度等"口味化"规则,官方建议由你(或你的团队)在 expo 配置之上自行叠加。
组成结构
当前包根目录(仓库路径 packages/eslint-config-expo)结构如下:
default.js:legacy(.eslintrc)模式的入口,聚合四份规则 + 全局变量;flat.js/flat/default.js:flat config 模式的入口(推荐),通过defineConfig导出数组;utils/:四类规则的模块化实现(core / typescript / react / expo)以及平台扩展名计算工具;eslint.config.js:包自身 CI/lint 使用的自举配置;__tests__/:针对 legacy 与 flat 两套模式的基线测试与规则测试。
二、核心规则构成:从源码看它到底 lint 什么
1. 平台化文件扩展名支持(extensions.js)
Expo 项目最常见的特性就是按平台拆分文件:Button.android.tsx、Button.ios.tsx、Button.web.tsx。若 ESLint 不认识这些后缀,导入解析与 import/no-unresolved 类规则会全部误报。
utils/extensions.js 给出了精确算法:
const jsExtensions = ['.js', '.jsx'];
const tsExtensions = ['.ts', '.tsx', '.d.ts'];
const platformSubextensions = ['.android', '.ios', '.web', '.native'];
function computeExpoExtensions(baseExtensions, platformSubextensions) {
const expoExtensions = [];
for (const platform of [...platformSubextensions, '']) { // '' 代表无平台后缀
for (const base of baseExtensions) {
expoExtensions.push(`${platform}${base}`);
}
}
return expoExtensions;
}
该函数把 4 个平台子扩展名与 5 个基础扩展名两两组合(含无平台后缀的情况),最终生成 20 个合法扩展名。default.js 与 flat/default.js 都会把它写入两份关键 settings:
settings: {
'import/extensions': allExtensions,
'import/resolver': { node: { extensions: allExtensions } },
},
这保证 eslint-plugin-import 在解析 import Foo from './Button' 时能命中任意平台变体文件。
2. TypeScript 与 React 最小规则集
utils/typescript.js 引入 @typescript-eslint parser/plugin 并提供针对 TS/TSX 的最小规则;utils/react.js 则是 JSX 相关的显式规则集合,注意它采用了逐条显式声明而非 plugin:react/recommended 全量继承(只有 react-hooks 通过 extends: ['plugin:react-hooks/recommended'] 引入),例如:
rules: {
'react/display-name': 'warn',
'react/jsx-no-duplicate-props': 'error',
'react/jsx-no-undef': 'error',
'react/jsx-uses-react': 'warn',
'react/jsx-uses-vars': 'warn',
'react/no-danger-with-children': 'warn',
'react/no-deprecated': 'warn',
'react/no-direct-mutation-state': 'warn',
'react/no-string-refs': ['warn', { noTemplateLiterals: true }],
'react/no-this-in-sfc': 'warn',
'react/no-unknown-property': 'warn',
'react/require-render-return': 'warn',
}
react/display-name等以warn收尾的规则,是为了兼容默认模板不被"报错"打断;react/require-render-return强制render()必须有返回,属于 createClass 时代遗留但仍有防护价值的规则;react/jsx-uses-vars保证 JSX 中使用的组件变量不被no-unused-vars误删。
3. React Native / DOM 全局变量
Expo 运行环境既不是纯浏览器也不是纯 Node,所以 default.js 与 flat/default.js 手工声明了一批全局:
globals: {
__DEV__: 'readonly',
ErrorUtils: false,
FormData: false,
XMLHttpRequest: false,
alert: false,
cancelAnimationFrame: false,
cancelIdleCallback: false,
clearImmediate: false,
fetch: false,
navigator: false,
process: false,
requestAnimationFrame: false,
requestIdleCallback: false,
setImmediate: false,
window: false,
'shared-node-browser': true,
}
- 值
false表示可写、readonly表示只读(如__DEV__); navigator、window、fetch、XMLHttpRequest等 Web 全局在 Expo/React Native 中确实可用,因此统一放行;- 值得注意的是 flat 版本额外
...globals.browser(来自 globals 包)合并进基础 globals,并针对*.web.*文件再用独立files块锁定浏览器环境——这与 CHANGELOG 中 9.0.2 修复的 "Define browser globals correctly for flat config" 以及 8.0.1 的 "Enable node globals formetro.config.js" 一脉相承(后者在 legacy 模式里通过*.web.*overrides +env: { browser: true }实现,见 default.js)。
4. Expo 专属规则:DOM Components 与 env 安全
utils/expo.js 引入工作区内的 eslint-plugin-expo 插件(见 package.json 中 "eslint-plugin-expo": "workspace:^"),并默认开启三条规则:
rules: {
'expo/use-dom-exports': ['error'],
'expo/no-env-var-destructuring': ['error'],
'expo/no-dynamic-env-var': ['error'],
}
这三条规则是 CHANGELOG 多条记录的落点:
expo/use-dom-exports:校验 "use dom" 指令与 Expo DOM Components 的导出写法(对应 10.0.0 "Add lint rules for Expo DOM Components and the 'use dom' directive");expo/no-dynamic-env-var与expo/no-env-var-destructuring:防止process.env被动态索引或解构,避免打包时环境变量内联失效(Expo 的 env 在构建期静态内联,动态访问无法被替换)。
该文件还包含一个经典的 ignorePatterns 示例:android/app/build——注释解释得很直白,"JS files can end up in build intermediates, eg: android/app/build/intermediates/assets/debug/EXDevMenuApp.android.js",即 Expo Go / dev 构建产物中的 JS 不应被当作源码 lint。
三、演进时间线:7.0.0 → 57.0.1 各版本解读
CHANGELOG 是本包最权威的演进记录。下面按时间正序(从早到晚)解读每条改动及其背后的动机,方便你在升级时对照检查。
起点:7.0.0(2024-04-03)
Breaking changes:Create a minimal ESLint config for Expo projects.
该版本本质上是包的"出生证明"。在此之前 Expo 生态的 lint 配置分散或依赖旧版,7.0.0 起确立设计原则:只提供基础、可组合的最小配置。此后凡是继承 expo 配置的用户,实际上就在消费这一最小集。
7.1.0(2024-04-18)
New features:Opt into explicit rules from eslint-plugin-react.
配置由"含糊地整包继承"改为逐条显式挑选 eslint-plugin-react 规则(即上文 utils/react.js 中那批手工枚举的规则)。显式规则的好处是升级插件大版本时规则名变更造成的破坏面可控,也便于读者/用户精确了解自己启用了什么。
7.1.1 / 7.1.2(2024-04-22 / 04-24)
两个 patch 版本均标注 This version does not introduce any user-facing changes.,属于纯内部构建/发布调整。
8.0.0(2024-10-22)
Breaking changes:Update @typescript-eslint dependencies to new major version, migrate rule set.
@typescript-eslint 主版本升级迫使规则集整体迁移——这也是 CHANGELOG 中首个真实的破坏性升级信号。若你在旧版本基础上自定义过基于 @typescript-eslint 老版规则名的配置,升级到 8.x 后需要按新规则名同步调整。同日发布的 8.0.1 修复了 metro.config.js 的 Node 全局变量缺失问题:Metro 配置文件跑在 Node 环境,理应可见 process、__dirname 等全局。
9.0.0(2025-04-04)
New features:Support flat config.
Expo 官方正式跟随 ESLint 生态接入 flat config(自 ESLint v9 起 flat config 已是默认),见 flat/default.js 的实现——它用 defineConfig([...]) 输出由 core / typescript / react / expo 四段展开的数组,并把全局变量与 import settings 放入独立对象。
同一版本的 Others 记录了两项依赖升级:
- Update
@typescript-eslintdependencies for better compatibility with TypeScript; - Update
eslint-plugin-react-hooksdependency to new major version。
9.0.1(2025-04-08)
Bug fixes:Wrap exported config in defineConfig.
9.0.0 刚发布 4 天即打补丁——flat 导出需要包裹在 defineConfig 中,以正确触发 ESLint 的配置校验与提示。这也解释了 flat.js 与 flat/default.js 里 const { defineConfig } = require('eslint/config') 的来源。
9.0.2(2025-04-11)
Bug fixes:Define browser globals correctly for flat config.
修复 flat 模式下浏览器全局变量定义不完整的问题——当前实现通过在 flat 配置里 ...globals.browser 展开标准浏览器全局,确保 document、window 等只在 Web 文件里按预期生效。
9.0.3(2025-04-22)
New features:use react/recommended plugin.
一个方向性反转:之前 7.1.0 刻意逐条显式挑选 react 规则,而 9.0.3 改为引入 react/recommended 作为基础。不过从当前源码看,这一"推荐集"仍被收敛为"安全子集 + 少量显式覆盖",以保证对不同 React/React Native 版本与模板的普适性。
9.1.0(2025-04-23)
New features:Disallow require() for source files and continue to allow for assets.
规则语义:源码文件禁止 require()(统一使用 ESM import),而 assets 类导入(图片、字体、音频等)仍然允许 require()——因为 Metro 资源管线普遍依赖 require('./icon.png') 这种写法。落地时通过按文件类型(.js/.ts/.tsx vs 资源扩展名)区分实现,这是 Expo 工程非常"务实"的一条规则。
9.1.1(2025-04-25)
无用户可见变更的维护版本。
9.2.0(2025-04-30)
New features:Add no-var rule to disallow var.
将 no-var 加入规则集,配合 prefer-const 等习惯推动 const/let 的现代写法。
10.0.0(2025-08-13)
New features:Add lint rules for Expo DOM Components and the "use dom" directive.
版本号从 9.x 跳到 10.0.0 标志着一次能力跃迁:为 Expo DOM Components(在原生 App 中以 Web 组件形式渲染的 React 组件)以及文件顶部的 "use dom" 指令新增 lint 规则。其实现即 utils/expo.js 中 expo/use-dom-exports 这条规则,规则集底层来自同仓库的 eslint-plugin-expo。
55.0.0(2026-01-21)
Others:Fixed check-packages error on Windows.
从 10.0.0 跳到 55.0.0 是 Expo 各 SDK 相关 npm 包统一对齐 SDK 版本号(Expo SDK 55)的结果。此次改动解决 Windows 平台上 monorepo 内部 check-packages 脚本报错的问题,属于仓库工程化修复,不影响 lint 行为。
56.0.0(2026-05-05)
Bug fixes:Disable no-useless-return for TypeScript files.
TypeScript 中常见 if (cond) return; 的守卫式写法,其函数末尾若存在隐式 return 语义时 no-useless-return 可能产生噪音,故对 TS/TSX 文件显式关闭,避免与 TS 类型收窄(narrowing)习惯冲突。
56.0.1 / 56.0.2 / 56.0.3(2026-05-06 / 05-06 / 05-13)
连续三个 patch 均为 This version does not introduce any user-facing changes.。
56.0.4(2026-05-13)
Others:Bump to eslint-plugin-react-hooks@^7.0.0.
将 React Hooks 规则插件升至 7.x 大版本,与 package.json 当前依赖 "eslint-plugin-react-hooks": "^7.0.0" 一致。该插件的 recommended 集(含 rules-of-hooks、exhaustive-deps)正是 utils/react.js 中 extends 的部分。
57.0.0(2026-06-25)与 57.0.1(2026-07-29)
两个版本均标注无用户可见变更,只是随 SDK 57 对齐版本号并做发布管道维护。
未发布区(Unpublished)
CHANGELOG 顶部还保留了 ## Unpublished 小节,其下按 Breaking changes / New features / Bug fixes / Others 四类预留空位,这是 Expo 各包统一的变更记录规范——新合入的改动会先填到对应分类,随下一次发布归并到具体版本。
四、安装与两种接入方式实战
安装
按 README.md 说明,需要同时安装配置包与 ESLint 本体:
yarn add --dev eslint-config-expo
yarn add --dev eslint
当前 package.json 声明 "peerDependencies": { "eslint": ">=8.10" },即 ESLint 8.10 及以上均可,但若使用 flat config 推荐 ESLint 9+(仓库自身 devDependencies 为 eslint: ^9.18.0,同时保留了 eslint8 别名用于测试 legacy 兼容性)。
方式一:flat config(推荐)
创建 eslint.config.js,引入 flat 入口并展开到数组中:
// eslint.config.js
const expoConfig = require("eslint-config-expo/flat");
const { defineConfig } = require("eslint/config");
module.exports = defineConfig([
expoConfig,
// 你自己的其它规则配置
{
rules: {
// 例如叠加团队风格规则
},
},
]);
flat 入口内部等价于直接展开 flat/default.js 导出的 defineConfig 数组(core → typescript → react → expo → globals/settings → *.web.*),所以你还可以用展开运算符把它嵌进已有数组:...require('eslint-config-expo/flat')。
方式二:legacy .eslintrc(package.json 或 .eslintrc.js)
package.json
{
"eslintConfig": {
"extends": ["expo"]
}
}
.eslintrc.js
module.exports = {
extends: ["expo"],
};
legacy 入口 default.js 实际是四份 util 配置的组合:extends: ['./utils/core.js', './utils/typescript.js', './utils/react.js', './utils/expo.js'],再加全局变量、import settings 和 *.web.* overrides。因此你也可以在自有配置里只挑选某一份(例如仅需要 JSX 支持时 extends: ['eslint-config-expo/utils/react'])。
组合使用:Flat + 平台 / Node 环境
Expo 自己的 eslint.config.js 就是"在 base 之上组合"的教科书示例:它先展开 expoConfig,再叠加 globals.node 与 globals.jest(给源码测试文件提供 Node/Jest 全局)、引入 eslint-plugin-prettier/recommended 并将 prettier/prettier 降为 warn,最后忽略测试 fixtures 目录。这种分层写法正是 base 配置被设计成"最小可组合"的初衷。
五、版本策略与迁移指南
为什么版本号从 10 跳到 55、57?
从 CHANGELOG 能清晰读出两条版本线:
- 功能演进线:7.x → 8.x → 9.x → 10.x,代表配置本身的实质功能迭代;
- SDK 对齐线:10.0.0 之后直接 55.0.0、56.x、57.x,因为 Expo 各官方包在 SDK 55 之后采用与 SDK 主版本一致的版本号策略。因此 55/56/57 之间的升级不意味着有新的 lint 功能,只是随 SDK 同步;真正要看功能演进需回溯到 10.0.0 及更早。
各里程碑的破坏性变更清单
结合 CHANGELOG,跨大版本时重点检查以下三点:
| 版本 | 类型 | 对用户的影响 |
|---|---|---|
| 7.0.0 | Breaking | 首次确立最小化配置,若此前沿用旧配置需整体迁移 |
| 8.0.0 | Breaking | @typescript-eslint 升主版本,规则集迁移;自定义 TS 规则名可能失效 |
| 9.0.0 | Feature(新增 flat) | legacy 模式仍可用,但推荐切换到 flat;9.0.1 又补充 defineConfig 包裹,升级时注意入口写法 |
| 9.0.3 | Feature | 引入 react/recommended,react 相关告警基线可能变化 |
| 9.1.0 | Feature | 源码 require() 被禁止(资源文件除外),存量 require 代码需改写为 import |
| 9.2.0 | Feature | 新增 no-var,var 声明会产生告警 |
| 10.0.0 | Feature | 新增 expo/use-dom-exports 等 DOM 规则,使用 "use dom" 的组件需符合新导出规范 |
| 56.0.0 | Bug fix | TS 文件中 no-useless-return 被关闭,告警数量可能减少 |
升级建议
- 保持 SDK 对齐:使用 Expo SDK 时优先跟随对应大版本(55/56/57),避免与
expo主包、eslint-plugin-expo的版本错位——三者版本同步才能确保expo/*规则加载正常; - 自定义规则只做加法:尽量在
expo之上叠加自有 rules,而不是覆写其内置告警级别,这样跨版本升级的冲突面最小; - 关注 react-hooks 与 @typescript-eslint 主版本:这两个插件的大版本升级(8.0.0、56.0.4)最容易影响自研规则,升级后跑一次
eslint --fix并 review 告警变化。
六、质量保障:测试是如何锁住行为的
配置类包的回归风险很高,因此仓库内配套了完整测试(package.json 中 "test": "jest",pretest 会先构建 eslint-plugin-expo):
- tests/baseline-test.js 与
baseline-flat-test.js:分别针对 legacy 与 flat 两种模式,用 fixtures 中的典型 Expo 源码验证"基线无致命误报"; __tests__/rules-test.js与rules-flat-test.js:逐条断言关键规则是否被正确启用/关闭(例如第 56.0.0 条对 TS 文件禁用no-useless-return的改动必然对应测试中的规则覆盖);- 快照目录
__snapshots__锁定规则配置的解析结果,任何规则集意外变更都会在 CI 中失败。
这意味着 CHANGELOG 中几乎每条规则级改动(9.1.0 的 require 限制、9.2.0 的 no-var、56.0.0 的 TS 特例等)在合并时都伴随对应测试更新,可作为你观察"某个 lint 行为到底由哪次变更引入"的可靠索引。
七、小结
eslint-config-expo 的演进史清晰展示了 Expo 团队对 lint 配置的取舍:保持最小、按需组合、随 SDK 同步版本。无论你通过 extends: ['expo'] 走 legacy 路线,还是用 require('eslint-config-expo/flat') 走 flat 路线,核心都落在这四件事上——TypeScript/JSX 支持、平台化扩展名解析、React Native 全局变量、以及随 10.0.0 引入的 Expo DOM/环境变量专属规则。升级时对照本文里程碑表,重点审计 @typescript-eslint、eslint-plugin-react-hooks 与新增/禁用规则带来的告警面变化,即可在 Expo SDK 迭代中平稳保持代码质量。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.2 K634
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown300
jforgamejforgame是一个一站式游戏服务器开发框架。包含游戏服务器开发所需要的各种组件,比如网关,socket服务端与客户端,自定义高效消息编解码,游戏热更新,游戏通用工具等等。包含游戏服,跨服,匹配服,后台管理系统等实现,同时提供大量业务案例以供学习。亦可用于其他socket应用,例如及时聊天等。Java101
fizz-gateway-nodeAn Aggregation API Gateway in Java . FizzGate 是一个基于 Java开发的微服务聚合网关,是拥有自主知识产权的应用网关国产化替代方案,能够实现热服务编排聚合、自动授权选择、线上服务脚本编码、在线测试、高性能路由、API审核管理、回调管理等目的,拥有强大的自定义插件系统可以自行扩展,并且提供友好的图形化配置界面,能够快速帮助企业进行API服务治理、减少中间层胶水代码以及降低编码投入、提高 API 服务的稳定性和安全性。Java60
certd开源SSL证书管理工具;全自动证书申请、更新、续期;通配符证书,泛域名证书申请;证书自动化部署到阿里云、腾讯云、主机、群晖、宝塔;https证书,pfx证书,der证书,TLS证书,nginx证书自动续签自动部署JavaScript60
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python280