eslint-plugin-simple-import-sort
简单易用的自动修复导入排序。
- ✅️ 通过 [
eslint --fix
][eslint-fix] 运行 – 无需新工具 - ✅️ 也可以在可能的情况下排序导出
- ✅️ 处理注释
- ✅️ 处理类型导入/导出
- ✅️ [TypeScript] 友好(通过 [@typescript-eslint/parser])
- ✅️ [Prettier] 友好
- ✅️ [dprint] 友好([需要配置][dprint-configuration])
- ✅️ [eslint-plugin-import] 友好
- ✅️
git diff
友好 - ✅️ 100% 代码覆盖率
- ✅️ 无依赖
- ❌ [不支持
require
][no-require]
这适用于经常使用 [eslint --fix
][eslint-fix](自动修复)并想完全忘记导入排序的人!
示例
import React from "react";
import Button from "../Button";
import styles from "./styles.css";
import type { User } from "../../types";
import { getUser } from "../../api";
import PropTypes from "prop-types";
import classnames from "classnames";
import { truncate, formatNumber } from "../../utils";
⬇️
import classnames from "classnames";
import PropTypes from "prop-types";
import React from "react";
import { getUser } from "../../api";
import type { User } from "../../types";
import { formatNumber, truncate } from "../../utils";
import Button from "../Button";
import styles from "./styles.css";
安装
npm install --save-dev eslint-plugin-simple-import-sort
ℹ️ 这是一个 ESLint 插件。👉 ESLint 入门指南
使用方法
-
eslintrc: 在
.eslintrc.*
文件的 "plugins" 数组中添加"simple-import-sort"
,并添加用于排序导入和导出的规则。默认情况下,ESLint 不解析import
语法 – "parserOptions" 是启用它的示例。{ "plugins": ["simple-import-sort"], "rules": { "simple-import-sort/imports": "error", "simple-import-sort/exports": "error" }, "parserOptions": { "sourceType": "module", "ecmaVersion": "latest" } }
-
[eslint.config.js (平铺配置)]: 导入 eslint-plugin-simple-import-sort,将其放入
plugins
对象中,并添加用于排序导入和导出的规则。使用平铺配置时,默认启用import
语法。import simpleImportSort from "eslint-plugin-simple-import-sort"; export default [ { plugins: { "simple-import-sort": simpleImportSort, }, rules: { "simple-import-sort/imports": "error", "simple-import-sort/exports": "error", }, }, ];
确保不要同时使用其他排序规则:
ℹ️ 注意:曾经有一个名为
"simple-import-sort/sort"
的规则。从 6.0.0 版本开始,它被称为"simple-import-sort/imports"
。
配置示例
这个示例使用了 [eslint-plugin-import],这是可选的。
建议同时设置 [Prettier],以帮助格式化你的导入(以及所有其他代码)。
{
"parserOptions": {
"sourceType": "module",
"ecmaVersion": "latest"
},
"plugins": ["simple-import-sort", "import"],
"rules": {
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"import/first": "error",
"import/newline-after-import": "error",
"import/no-duplicates": "error"
}
}
"sourceType": "module"
和"ecmaVersion": "latest"
是必需的,这样 ESLint 就不会将import
和export
报告为语法错误。- 对所有文件启用
simple-import-sort/imports
和simple-import-sort/exports
。 - import/first 确保所有导入都在文件顶部。(可自动修复)
- import/newline-after-import 确保导入后有一个空行。(可自动修复)
- import/no-duplicates 合并相同文件的导入语句。(大部分可自动修复)
不适合所有人
这个插件并不适合所有人。让我解释一下。
长期以来,这个插件没有任何选项,这有助于保持它的简单性。
虽然人工字母排序和注释处理似乎适用于很多人,但导入分组更加困难。项目之间的差异太大,无法有一个通用的分组方式。
我决定只提供这一个选项,不再增加其他。以下是一些你无法配置的内容:
- 每个组内的排序。它就是这样。参见[排序]。
- 副作用导入的排序(它们始终保持原始顺序)。
如果你想要更多选项,我建议使用 import/order 规则(来自 [eslint-plugin-import])。它有很多选项,维护者似乎有兴趣在合理的情况下扩展功能。
那么为什么这个插件存在呢?参见这个规则与 import/order
有何不同?。
如果我们开始为这个插件添加更多选项,它就不再是 eslint-plugin-simple-import-sort 了。最终它将没有存在的理由 – 最好将精力用于为 import/order 贡献。
我为自己制作了这个插件。我在许多小项目中使用它,我喜欢它。如果你也喜欢 – 我很高兴听到!但并非每个人都会喜欢它。这没关系。
排序顺序
这个插件应该与自动修复一起使用,最好是直接在你的编辑器中通过 ESLint 扩展使用,或者使用 [eslint --fix
][eslint-fix]。
本节是为了了解排序的工作原理,而不是如何手动修复错误。请使用自动修复!
总结: 先分组,然后按字母顺序排序。
分组
导入
首先,插件查找所有导入的块。一个"块"是一系列导入语句,之间只有注释和空白。每个块单独排序。如果你想确保所有导入都在同一个块中,可以使用 import/first。
然后,每个块被分组成几个部分,每个部分之间有一个空行。
import "./setup"
: 副作用导入。(这些内部不排序。)import * as fs from "node:fs"
: 带有node:
前缀的 Node.js 内置模块。import react from "react"
: 包(npm 包和不带node:
的 Node.js 内置模块)。import a from "/a"
: 绝对导入和其他导入,如 Vue 风格的@/foo
。import a from "./a"
: 相对导入。
注意:上述分组的定义非常宽松。更多信息请参见[自定义分组]。
导出
重新导出(带有 from
的导出)序列会被排序。其他类型的导出不会重新排序。
与导入不同,导出没有自动分组。相反,单独一行的注释会开始一个新组。这将分组留给你手动完成。
以下示例有 3 个组(一个包含 "x" 和 "y",一个包含 "a" 和 "b",一个包含 "./"):
export * from "x";
export * from "y";
// 这个注释开始一个新组。
/* 这个不会。 */ export * from "a"; // 这个也不会。
/* 这个
也不会 */ export * from "b";
/* 但这个会。 */
export * from "./";
每个组单独排序,组本身不排序 – 它们保持在你写它们的位置。
没有分组注释的话,上面的示例最终会变成这样:
export * from "./";
/* 这个不会。 */ export * from "a"; // 这个也不会。
/* 这个
也不会 */ export * from "b";
export * from "x";
export * from "y";
排序
在每个部分内,导入/导出按 from
字符串的字母顺序排序(另见"为什么按 from
排序?")。保持简单!看看这里的代码会有帮助:
const collator = new Intl.Collator("en", {
sensitivity: "base",
numeric: true,
});
function compare(a, b) {
return collator.compare(a, b) || (a < b ? -1 : a > b ? 1 : 0);
}
换句话说,组内的导入/导出按字母顺序排序,不区分大小写,并像人类那样处理数字,在出现平局的情况下回退到传统的字符代码排序。更多信息请参阅Intl.Collator。注意:Intl.Collator
以某种定义的顺序对标点符号进行排序。我不知道标点符号的排序顺序是什么,也不在乎。据我所知,标点符号没有有序的"字母表"。
对字母顺序规则有一个补充:目录结构。目录结构中较高层级文件的相对导入/导出排在较近层级之前——"../../utils"
排在"../utils"
之前,后者又排在"."
之前。(简而言之,.
和/
排在任何其他(非空白、非控制)字符之前。".."
和类似的排序如同"../,"
(以避免"较短前缀排在前面"的排序概念)。)
如果对同一来源同时使用了import type
和常规导入,类型导入排在前面。export type
也是如此。(你可以将类型导入移到它们自己的组,如[自定义分组]中所述。)
示例
// 副作用导入。(这些内部不进行排序。)
import "./setup";
import "some-polyfill";
import "./global.css";
// 带有`node:`前缀的Node.js内置模块。
import * as fs from "node:fs";
// 包。
import type A from "an-npm-package";
import a from "an-npm-package";
import fs2 from "fs";
import b from "https://example.com/script.js";
// 绝对导入和其他导入。
import c from "/";
import d from "/home/user/foo";
import Error from "@/components/error.vue";
// 相对导入。
import e from "../..";
import type { B } from "../types";
import f from "../Utils"; // 不区分大小写。
import g from ".";
import h from "./constants";
import i from "./styles";
// 不同类型的导出:
export { a } from "../..";
export { b } from "/";
export { Error } from "@/components/error.vue";
export * from "an-npm-package";
export { readFile } from "fs";
export * as ns from "https://example.com/script.js";
// 这个注释分组了一些更多的导出:
export { e } from "../..";
export { f } from "../Utils";
export { g } from ".";
export { h } from "./constants";
export { i } from "./styles";
// 其他导出 – 插件不会触及这些,除了对大括号内的命名导出进行排序。
export var one = 1;
export let two = 2;
export const three = 3;
export function func() {}
export class Class {}
export type Type = string;
export { named, other as renamed };
export type { T, U as V };
export default whatever;
无论在哪个组中,导入的项目都按以下方式排序:
import {
// 数字按其数值排序:
img1,
img2,
img10,
// 然后是其他所有内容,按字母顺序:
k,
L, // 不区分大小写。
m as anotherName, // 按"外部接口"名称"m"排序,而不是"anotherName"。
m as tie, // 但在出现平局时使用文件本地名称。
// 类型的排序就像`type`关键字不存在一样。
type x,
y,
} from "./x";
即使对于没有from
的导出,导出项也会被排序(尽管导出语句本身不会相对于其他导出进行排序):
export {
k,
L, // 不区分大小写。
anotherName as m, // 按"外部接口"名称"m"排序,而不是"anotherName"。
// tie as m, // 对于导出,不可能有平局 – 所有导出必须是唯一的。
// 类型的排序就像`type`关键字不存在一样。
type x,
y,
};
export type { A, B, A as C };
乍一听,导入时a as b
按a
排序,而导出时按b
排序可能听起来有悖常理。这样做的原因是选择最"稳定"的名称。在import { a as b } from "./some-file.js"
中,as b
部分是为了避免文件中的名称冲突,而不必更改some-file.js
。在export { b as a }
中,b as
部分是为了避免文件中的名称冲突,而不必更改文件的导出接口。
自定义分组
有一个选项(参见[不适合所有人])称为groups
,它对许多不同的用例都很有用。
groups
是一个字符串数组的数组:
type Options = {
groups: Array<Array<string>>;
};
每个字符串都是一个正则表达式(带有[u
标志])。这些正则表达式决定哪些导入去往何处。(记得转义反斜杠 – 是"\\w"
,而不是"\w"
,例如。)
内部数组用一个换行符连接;外部数组用两个换行符连接 – 创建一个空行。这就是为什么有两级数组 – 它让你选择在哪里有空行。
以下是一些你可以做的事情:
- 将非标准导入路径如
src/Button
和@company/Button
从(第三方)"包"组移出,放入它们自己的组。 - 将
react
移到最前面。 - 通过使用单个内部数组来避免导入之间的空行。
- 为样式导入创建单独的组。
- 分开
./
和../
导入。 - 完全不使用分组,只按字母顺序排序。
如果你在考虑自定义分组是因为想移动非标准导入路径,如
src/Button
(没有前导的./
或../
)和@company/Button
– 考虑使用不像npm包的名称,如@/Button
和~company/Button
。这样你就不需要自定义分组,而且作为额外好处,对其他在代码库上工作的人来说可能会不那么混淮。如果你有非常复杂的要求,请参见issue #31获取一些提示。
注意:对于导出,分组是通过注释手动完成的 – 参见[导出]。
每个import
都会根据from
字符串与所有正则表达式进行匹配。导入最终会出现在匹配最长的正则表达式处。在平局的情况下,第一个匹配的正则表达式胜出。
如果一个导入最终出现在错误的位置 – 尝试让所需的正则表达式匹配
from
字符串的更多部分,或使用否定前瞻((?!x)
)来排除其他组中的内容。
不匹配任何正则表达式的导入会被放在最后。
副作用导入的from
字符串前面会添加\u0000
(以\u0000
开头)。你可以用"^\\u0000"
来匹配它们。
类型导入的from
字符串后面会添加\u0000
(以\u0000
结尾)。你可以用"\\u0000$"
来匹配它们 – 但你可能需要更多内容来避免它们也被其他正则表达式匹配。
匹配同一正则表达式的所有导入会按照[排序顺序]中提到的方式内部排序。
这是groups
选项的默认值:
[
// 副作用导入。
["^\\u0000"],
// 带有`node:`前缀的Node.js内置模块。
["^node:"],
// 包。
// 以字母(或数字或下划线)开头的内容,或者`@`后跟一个字母。
["^@?\\w"],
// 绝对导入和其他导入,如Vue风格的`@/foo`。
// 任何未在其他组中匹配的内容。
["^"],
// 相对导入。
// 任何以点开头的内容。
["^\\."],
];
细心的读者可能会注意到,上述正则表达式匹配的内容比它们的注释所说的要多。例如,"@config"
和"_internal"
被匹配为包,但它们都不是有效的npm包名。".foo"
被匹配为相对导入,但".foo"
到底是什么意思?不过,使用更具体的规则并没有太多好处。所以保持简单!
参见[示例]以获取灵感。
注释和空白处理
当通过排序移动导入/导出时,它们的注释也会随之移动。注释可以放在导入/导出的上方(除了第一个 – 稍后会详细说明),或者在其行的开头或结尾。
示例:
// 导入块之前的注释
/* c1 */ import c from "c"; // c2
// b1
import b from "b"; // b2
// a1
/* a2
*/ import a /* a3 */ from "a"; /* a4 */ /* 非a
*/ // 导入块之后的注释
⬇️
// 导入块之前的注释
// a1
/* a2
*/ import a /* a3 */ from "a"; /* a4 */
// b1
import b from "b"; // b2
/* c1 */ import c from "c"; // c2
/* 非a
*/ // 导入块之后的注释
现在比较这两个例子:
// @flow
import b from "b";
// a
import a from "a";
// eslint-disable-next-line import/no-extraneous-dependencies
import b from "b";
// a
import a from "a";
// @flow
注释应该位于文件顶部(它为该文件启用 Flow 类型检查),与 "b"
导入无关。另一方面,// eslint-disable-next-line
注释却与 "b"
导入相关。即使是文档注释也可能是针对整个文件或第一个导入。因此,这个插件无法确定是否应该将注释移到第一个导入之上(但它知道 //a
注释属于 "a"
导入)。
基于这个原因,导入/导出块上下的注释永远不会被移动。如有需要,你需要自己手动移动。
围绕导入/导出项的注释遵循类似的规则 - 它们可以放在项目上方,或者在其行的开头或结尾。第一个项目或换行符之前的注释保留在开头,最后一个项目之后的注释保留在结尾。
import { // 开头的注释
/* c1 */ c /* c2 */, // c3
// b1
b as /* b2 */ renamed
, /* b3 */ /* a1
*/ a /* not-a
*/ // 结尾的注释
} from "wherever";
import {
e,
d, /* d */ /* not-d
*/ // 尾随逗号后的结尾注释
} from "wherever2";
import {/* 开头的注释 */ g, /* g */ f /* f */} from "wherever3";
⬇️
import { // 开头的注释
/* a1
*/ a,
// b1
b as /* b2 */ renamed
, /* b3 */
/* c1 */ c /* c2 */// c3
/* not-a
*/ // 结尾的注释
} from "wherever";
import {
d, /* d */ e,
/* not-d
*/ // 尾随逗号后的结尾注释
} from "wherever2";
import {/* 开头的注释 */ f, /* f */g/* g */ } from "wherever3";
如果你对奇怪的空白感到疑惑 - 请参阅 "排序自动修复导致了一些奇怪的空白!"
说到空白 - 空行怎么处理?就像注释一样,很难知道排序后空行应该放在哪里。这个插件采用了一种简单的方法 - 导入/导出块中的所有空行都被移除,除了 /**/
注释中的空行和在 [排序顺序] 中提到的组之间添加的空行。(注意:对于导出,组之间的空行完全由你决定 - 如果你在分组注释周围有空行,它们会被保留。)
(由于空行被移除,你可能会遇到与 lines-around-comment 和 padding-line-between-statements 规则略有不兼容的情况 - 我自己不使用这些规则,但我认为应该有解决方法。)
最后一条空白规则是,这个插件每行只放一个导入/导出。我从未见过有意将多个导入/导出放在同一行的真实项目。
常见问题
它支持 require
吗?
不支持。这是有意为之,以保持简单。对于 require
的排序,请使用其他排序规则,比如 import/order。或者考虑将使用 require
的代码迁移到 import
。现在 import
已经得到很好的支持。
为什么按 from
排序?
一些其他的导入排序规则是根据 import
后的第一个名称排序,而不是 from
后的字符串。本插件有意按 from
字符串排序,以便于 git diff
。
看看这个例子:
import { productType } from "./constants";
import { truncate } from "./utils";
现在假设你还需要 arraySplit
工具:
import { productType } from "./constants";
import { arraySplit, truncate } from "./utils";
如果按 import
后的第一个名称排序(在这种情况下是 "productType" 和 "arraySplit"),这两个导入现在会交换顺序:
import { arraySplit, truncate } from "./utils";
import { productType } from "./constants";
另一方面,如果按 from
字符串排序(就像本插件所做的那样),导入会保持相同的顺序。这可以防止导入在你添加和删除内容时跳来跳去,保持你的 git 历史清晰,并减少合并冲突的风险。
排序导入/导出安全吗?
大部分情况下是安全的。
在 JavaScript 中,导入和重新导出可能会有副作用,因此改变它们的顺序可能会改变这些副作用执行的顺序。最佳实践是要么导入一个模块以获得其副作用,要么导入它导出的内容(并且绝不依赖重新导出的副作用)。
// 运行副作用的 `import`:
import "some-polyfill";
// 获取 `someUtil` 的 `import`:
import { someUtil } from "some-library";
仅用于副作用的导入会保持输入顺序。这些不会被排序:
import "b";
import "a";
既导出内容又运行副作用的导入很少见。如果你遇到这种情况 - 试着修复它,因为它会让所有使用这段代码的人感到困惑。如果无法修复,可以**忽略(部分)排序。**
另一个小问题是你有时需要手动移动注释 - 请参阅 注释和空白处理。
为了完整起见,对导入的导入/导出项进行排序始终是安全的:
import { c, b, a } from "wherever";
// 等同于:
import { a, b, c } from "wherever";
注意:import {} from "wherever"
不被视为副作用导入。
最后,关于导出还有一点需要知道。考虑这种情况:
one.js:
export const title = "One";
export const one = 1;
two.js:
export const title = "Two";
export const two = 2;
reexport.js:
export * from "./one.js";
export * from "./two.js";
main.js:
import * as reexport from "./rexport.js";
console.log(reexport);
如果你运行 main.js 会发生什么?在 Node.js 和浏览器中,结果是:
{
one: 1,
two: 2,
}
注意 title
甚至不在对象中!这对排序来说是好事,因为这意味着重新排序 reexport.js 中的两个 export * from
导出是安全的 - 并不是最后一个导入"胜出",你也不会因为排序意外改变 title
的值。
然而,根据你使用的打包工具,这可能仍然会导致问题。以下是一些打包工具在编写时处理重复名称 title
的方式:
- ✅ Webpack:编译时错误 - 安全。
- ✅ Parcel:运行时错误 - 安全。
- ⚠️ Rollup:编译时警告,但使用它们中的第一个,所以可能不安全。不过,可以将 Rollup 配置为将警告视为错误。
- ✅ TypeScript:编译时错误 - 安全。
排序自动修复导致了一些奇怪的空白!
你可能会遇到一些奇怪的间距,例如逗号后缺少空格:
import {bar, baz,foo} from "example";
排序是这个插件中简单的部分。处理空白和注释是困难的部分。自动修复有时可能会在导入/导出周围产生一些奇怪的间距。我建议使用 [Prettier] 或启用其他可自动修复的 ESLint 空白规则,而不是手动修复这些空格。更多信息请参见 [示例]。
空白可能会变得奇怪的原因是,这个插件重用并移动已存在的空白,而不是删除和添加新的空白。这是为了与其他处理空白的 ESLint 规则保持兼容。
我可以在不使用自动修复的情况下使用这个吗?
不太可能。这个规则的错误消息就是 "运行自动修复来排序这些导入!"为什么?为了积极鼓励你使用 [eslint --fix
][eslint-fix](自动修复),而不是浪费时间手动做计算机能更好完成的事情。我见过有人痛苦地一个个修复其他规则产生的晦涩(且烦人的!)排序错误,却没意识到这些错误可以被自动修复。最后,不试图制作更详细的消息使得这个插件的代码更容易处理。
如何为这个规则使用 eslint-ignore?
寻找这个规则的 /* eslint-disable */
?请阅读所有关于**忽略(部分)排序的内容。**
这个规则与 import/order
有何不同?
import/order 规则以前不支持字母顺序排序,但现在支持了。那么 eslint-plugin-simple-import-sort
还能带来什么呢?
- 对导入/导出项进行排序(
import { a, b, c } from "."
):eslint-plugin-import#1787 - 对重新导出进行排序:eslint-plugin-import#1888
- 支持注释:eslint-plugin-import#1450,eslint-plugin-import#1723
- 支持类型导入:eslint-plugin-import#645
- 支持绝对导入:eslint-plugin-import#512
- 允许选择副作用导入的位置:eslint-plugin-import#970
- 允许组内自定义排序:eslint-plugin-import#1378
- 按数字顺序排序(
"./img10.jpg"
排在"./img2.jpg"
之后,而不是之前) - 开放的
import/order
问题:import/export ordering
一些其他区别:
- 本插件为每组导入/导出提供一个错误,而
import/order
可能会提供多个(详见我可以在不使用自动修复的情况下使用吗?)。换句话说,本插件在编辑器中显示的下划线更多,而import/order
在错误数量上更多。 - 本插件有一个单一(但非常强大)的选项,由一组正则表达式组成,而
import/order
有多个不同的选项。目前还不清楚哪个更容易配置。但eslint-plugin-simple-import-sort
尝试开箱即用地实现最大功能。
如何将此插件与 dprint
一起使用?
[dprint] 也会对导入和导出进行排序,但不会对它们进行分组。相反,它会保留你自己的分组方式。
首先要问自己的是 dprint 是否足够好。如果是,那你就少了一个需要担心的工具!
但是,如果你想强制分组,你仍然可以使用 eslint-plugin-simple-import-sort
。然而,这两者在某些排序边缘情况下可能会略有分歧。因此,最好在你的 dprint 配置文件中关闭排序:
{
"typescript": {
"module.sortImportDeclarations": "maintain"
}
}
来源:https://dprint.dev/plugins/typescript/config/
如何移除导入之间的所有空行?
使用[自定义分组],将 groups
选项设置为只有一个内部数组。
例如,这是默认值但改为单个内部数组:
[["^\\u0000", "^node:", "^@?\\w", "^", "^\\."]];
(默认情况下,每个字符串都在自己的数组中(总共 5 个内部数组)– 这会在每个之间造成一个空行。)