- Notifications
You must be signed in to change notification settings - Fork74
Easy autofixable import sorting.
License
lydell/eslint-plugin-simple-import-sort
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Easy autofixable import sorting.
- ✅️ Runs via
eslint --fix
– no new tooling - ✅️ Also sorts exports where possible
- ✅️ Handles comments
- ✅️ Handles type imports/exports
- ✅️TypeScript friendly (via@typescript-eslint/parser)
- ✅️Prettier friendly
- ✅️dprint friendly (with configuration)
- ✅️eslint-plugin-import friendly
- ✅️
git diff
friendly - ✅️ 100% code coverage
- ✅️ No dependencies
- ❌Does not support
require
This is for those who useeslint --fix
(autofix) a lot and want to completely forget about sorting imports!
importReactfrom"react";importButtonfrom"../Button";importstylesfrom"./styles.css";importtype{User}from"../../types";import{getUser}from"../../api";importPropTypesfrom"prop-types";importclassnamesfrom"classnames";import{truncate,formatNumber}from"../../utils";
⬇️
importclassnamesfrom"classnames";importPropTypesfrom"prop-types";importReactfrom"react";import{getUser}from"../../api";importtype{User}from"../../types";import{formatNumber,truncate}from"../../utils";importButtonfrom"../Button";importstylesfrom"./styles.css";
npm install --save-dev eslint-plugin-simple-import-sort
ℹ️ This is anESLint plugin. 👉Getting Started with ESLint
eslintrc: Add
"simple-import-sort"
to the "plugins" array in your.eslintrc.*
file, and add the rules for sorting imports and exports. By default ESLint doesn’t parseimport
syntax – the "parserOptions" is an example of how to enable that.{"plugins": ["simple-import-sort"],"rules": {"simple-import-sort/imports":"error","simple-import-sort/exports":"error" },"parserOptions": {"sourceType":"module","ecmaVersion":"latest" }}
eslint.config.js (flat config): Import eslint-plugin-simple-import-sort, put it in the
plugins
object, and add the rules for sorting imports and exports. With flat config,import
syntax is enabled by default.importsimpleImportSortfrom"eslint-plugin-simple-import-sort";exportdefault[{plugins:{"simple-import-sort":simpleImportSort,},rules:{"simple-import-sort/imports":"error","simple-import-sort/exports":"error",},},];
Make surenot to use other sorting rules at the same time:
ℹ️ Note: There used to be a rule called
"simple-import-sort/sort"
. Since version 6.0.0 it’s called"simple-import-sort/imports"
.
This example useseslint-plugin-import, which is optional.
It is recommended to also set upPrettier, to help formatting your imports (and all other code) nicely.
{"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"
and"ecmaVersion": "latest"
are needed so ESLint doesn’t reportimport
andexport
as syntax errors.simple-import-sort/imports
andsimple-import-sort/exports
are turned on for all files.- import/first makes sure all imports are at the top of the file. (autofixable)
- import/newline-after-import makes sure there’s a newline after the imports. (autofixable)
- import/no-duplicates merges import statements of the same file. (autofixable, mostly)
This plugin is not for everyone. Let me explain.
For a long time, this plugin used to have no options, which helped keeping it simple.
While the human alphabetical sorting and comment handling seems to work for a lot of people, grouping of imports is more difficult. Projects differ too much to have a one-size-fits-all grouping.
I’ve decided to have this single option but nothing more. Here are some things you can’t configure:
- The sorting within each group. It is what it is. SeeSorting.
- Sorting ofside effect imports (they always stay in the original order).
If you want more options, I recommend using theimport/order rule (fromeslint-plugin-import) instead. It has plenty of options, and the maintainers seem interested in expanding the feature where it makes sense.
Then why does this plugin exist? SeeHow is this rule different fromimport/order
?.
If we start adding more options to this plugin, it won’t be eslint-plugin-simple-import-sort anymore. Eventually it would have no reason to exist – effort would be better spent contributing toimport/order.
I made this plugin for myself. I use it in many little projects and I like it. If you like it too – I’m very glad to hear! Buteveryone won’t like it. And that’s ok.
This plugin is supposed to be used with autofix, ideally directly in your editor via an ESLint extension, or witheslint --fix
otherwise.
This section is for learning how the sorting works, not for how to manually fix errors. Use autofix!
TL;DR: First group, then sort alphabetically.
First, the plugin finds allchunks of imports. A “chunk” is a sequence of import statements with only comments and whitespace between. Each chunk is sorted separately. Useimport/first if you want to make sure that all imports end up in the same chunk.
Then, each chunk isgrouped into sections with a blank line between each.
import "./setup"
: Side effect imports. (These are not sorted internally.)import * as fs from "node:fs"
: Node.js builtin modules prefixed withnode:
.import react from "react"
: Packages (npm packages and Node.js builtinswithoutnode:
).import a from "/a"
: Absolute imports and other imports such as Vue-style@/foo
.import a from "./a"
: Relative imports.
Note: The above groups are very loosely defined. SeeCustom grouping for more information.
Sequences of re-exports (exports withfrom
) are sorted. Other types of exports are not reordered.
Unlike imports, there’s no automatic grouping of exports. Instead a comment on its own line starts a group. This leaves the grouping up to you to do manually.
The following example has 3 groups (one with “x” and “y”, one with “a” and “b” and one with “./”):
export*from"x";export*from"y";// This comment starts a new group./* This one does not. */export*from"a";// Neither does this one./* Nor thisone */export*from"b";/* But this one does. */export*from"./";
Each group is sorted separately, and the groups themselves aren’t sorted – they stay where you wrote them.
Without the grouping comments the above example would end up like this:
export*from"./";/* This one does not. */export*from"a";// Neither does this one./* Nor thisone */export*from"b";export*from"x";export*from"y";
Within each section, the imports/exports are sorted alphabetically on thefrom
string (see also“Why sort onfrom
?”). Keep it simple! It helps looking at the code here:
constcollator=newIntl.Collator("en",{sensitivity:"base",numeric:true,});functioncompare(a,b){returncollator.compare(a,b)||(a<b ?-1 :a>b ?1 :0);}
In other words, the imports/exports within groups are sorted alphabetically, case-insensitively and treating numbers like a human would, falling back to good old character code sorting in case of ties. SeeIntl.Collator for more information. Note:Intl.Collator
sorts punctuation insome defined order. I have no idea what order punctuation sorts in, and I don’t care. There’s no ordered “alphabet” for punctuation that I know of.
There’s one addition to the alphabetical rule: Directory structure. Relative imports/exports of files higher up in the directory structure come before closer ones –"../../utils"
comes before"../utils"
, which comes before"."
. (In short,.
and/
sort before any other (non-whitespace, non-control) character.".."
and similar sort like"../,"
(to avoid the “shorter prefix comes first” sorting concept).)
If bothimport type
and regular imports are used for the same source, the type imports come first. Same thing forexport type
. (You can move type imports to their own group, as mentioned incustom grouping.)
// Side effect imports. (These are not sorted internally.)import"./setup";import"some-polyfill";import"./global.css";// Node.js builtins prefixed with `node:`.import*asfsfrom"node:fs";// Packages.importtypeAfrom"an-npm-package";importafrom"an-npm-package";importfs2from"fs";importbfrom"https://example.com/script.js";// Absolute imports and other imports.importcfrom"/";importdfrom"/home/user/foo";importErrorfrom"@/components/error.vue";// Relative imports.importefrom"../..";importtype{B}from"../types";importffrom"../Utils";// Case insensitive.importgfrom".";importhfrom"./constants";importifrom"./styles";// Different types of exports:export{a}from"../..";export{b}from"/";export{Error}from"@/components/error.vue";export*from"an-npm-package";export{readFile}from"fs";export*asnsfrom"https://example.com/script.js";// This comment groups some more exports:export{e}from"../..";export{f}from"../Utils";export{g}from".";export{h}from"./constants";export{i}from"./styles";// Other exports – the plugin does not touch these, other than sorting named// exports inside braces.exportvarone=1;exportlettwo=2;exportconstthree=3;exportfunctionfunc(){}exportclassClass{}exporttypeType=string;export{named,otherasrenamed};exporttype{T,UasV};exportdefaultwhatever;
Regardless of group, imported items are sorted like this:
import{// Numbers are sorted by their numeric value:img1,img2,img10,// Then everything else, alphabetically:k,L,// Case insensitive.masanotherName,// Sorted by the “external interface” name “m”, not “anotherName”.mastie,// But do use the file-local name in case of a tie.// Types are sorted as if the `type` keyword wasn’t there.typex,y,}from"./x";
Exported items are sorted even for exportswithoutfrom
(even though the whole export statement itself isn’t sorted in relation to other exports):
export{k,L,// Case insensitive.anotherNameasm,// Sorted by the “external interface” name “m”, not “anotherName”.// tie as m, // For exports there can’t be ties – all exports must be unique.// Types are sorted as if the `type` keyword wasn’t there.typex,y,};exporttype{A,B,AasC};
At first it might sound counter-intuitive thata as b
is sorted bya
for imports, but byb
for exports. The reason for doing it this way is to pick the most “stable” name. Inimport { a as b } from "./some-file.js"
, theas b
part is there to avoid a name collision in the file without having to changesome-file.js
. Inexport { b as a }
, theb as
part is there to avoid a name collision in the file without having to change the exported interface of the file.
There isone option (seeNot for everyone) calledgroups
that is useful for a bunch of different use cases.
groups
is an array of arrays of strings:
typeOptions={groups:Array<Array<string>>;};
Each string is a regex (with theu
flag). The regexes decide which imports go where. (Remember to escape backslashes – it’s"\\w"
, not"\w"
, for example.)
The inner arrays are joined with one newline; the outer arrays are joined with two – creating a blank line. That’s why there are two levels of arrays – it lets you choose where to have blank lines.
Here are some things you can do:
- Move non-standard import paths like
src/Button
and@company/Button
out of the (third party) “packages” group, into their own group. - Move
react
first. - Avoid blank lines between imports by using a single inner array.
- Make a separate group for style imports.
- Separate
./
and../
imports. - Not use groups at all and only sort alphabetically.
If you’re looking at custom grouping because you want to move non-standard import paths like
src/Button
(with no leading./
or../
) and@company/Button
– consider instead using names that do not look like npm packages, such as@/Button
and~company/Button
. Then you won’t need to customize the grouping at all, and as a bonus things might be less confusing for other people working on the code base.Seeissue #31 for some tips on what you can do if you have very complex requirements.
Note: For exports the grouping is manual using comments – seeexports.
Eachimport
is matched againstall regexes on thefrom
string. The import ends up at the regex withthe longest match. In case of a tie, thefirst matching regex wins.
If an import ends up in the wrong place – try making the desired regex match more of the
from
string, or use negative lookahead ((?!x)
) to exclude things from other groups.
Imports that don’t match any regex are put together last.
Side effect imports have\u0000
prepended to theirfrom
string (starts with\u0000
). You can match them with"^\\u0000"
.
Type imports have\u0000
appended to theirfrom
string (ends with\u0000
). You can match them with"\\u0000$"
– but you probably need more than that to avoid them also being matched by other regexes.
All imports that match the same regex are sorted internally as mentioned inSort order.
This is the default value for thegroups
option:
[// Side effect imports.["^\\u0000"],// Node.js builtins prefixed with `node:`.["^node:"],// Packages.// Things that start with a letter (or digit or underscore), or `@` followed by a letter.["^@?\\w"],// Absolute imports and other imports such as Vue-style `@/foo`.// Anything not matched in another group.["^"],// Relative imports.// Anything that starts with a dot.["^\\."],];
The astute reader might notice that the above regexes match more than their comments say. For example,"@config"
and"_internal"
are matched as packages, but none of them are valid npm package names.".foo"
is matched as a relative import, but what does".foo"
even mean? There’s little gain in having more specific rules, though. So keep it simple!
See theexamples for inspiration.
When an import/export is moved through sorting, its comments are moved with it. Comments can be placed above an import/export (except the first one – more on that later), or at the start or end of its line.
Example:
// comment before import chunk/* c1 */importcfrom"c";// c2// b1importbfrom"b";// b2// a1/* a2 */importa/* a3 */from"a";/* a4 *//* not-a*/// comment after import chunk
⬇️
// comment before import chunk// a1/* a2 */importa/* a3 */from"a";/* a4 */// b1importbfrom"b";// b2/* c1 */importcfrom"c";// c2/* not-a*/// comment after import chunk
Now compare these two examples:
//@flowimportbfrom"b";// aimportafrom"a";
// eslint-disable-next-line import/no-extraneous-dependenciesimportbfrom"b";// aimportafrom"a";
The// @flow
comment is supposed to be at the top of the file (it enablesFlow type checking for the file), and isn’t related to the"b"
import. On the other hand, the// eslint-disable-next-line
commentis related to the"b"
import. Even a documentation comment could be either for the whole file, or the first import. So this plugin can’t know if it should move comments above the first import or not (but it knows that the//a
comment belongs to the"a"
import).
For this reason, comments above and below chunks of imports/exports are never moved. You need to do so yourself, if needed.
Comments around imported/exported items follow similar rules – they can be placed above an item, or at the start or end of its line. Comments before the first item or newline stay at the start, and comments after the last item stay at the end.
import{// comment at start/* c1 */c/* c2 */,// c3// b1bas/* b2 */renamed,/* b3 *//* a1 */a/* not-a */// comment at end}from"wherever";import{e,d,/* d *//* not-d */// comment at end after trailing comma}from"wherever2";import{/* comment at start */g,/* g */f/* f */}from"wherever3";
⬇️
import{// comment at start/* a1 */a,// b1bas/* b2 */renamed,/* b3 *//* c1 */c/* c2 */// c3/* not-a */// comment at end}from"wherever";import{d,/* d */e,/* not-d */// comment at end after trailing comma}from"wherever2";import{/* comment at start */f,/* f */g/* g */}from"wherever3";
If you wonder what’s up with the strange whitespace – see“The sorting autofix causes some odd whitespace!”
Speaking of whitespace – what about blank lines? Just like comments, it’s difficult to know where blank lines should go after sorting. This plugin went with a simple approach – all blank lines in chunks of imports/exports are removed, except in/**/
comments and the blank lines added between the groups mentioned inSort order. (Note: For exports, blank lines between groups are completely up to you – if you have blank lines around the grouping comments they are preserved.)
(Since blank lines are removed, you might get slight incompatibilities with thelines-around-comment andpadding-line-between-statements rules – I don’t use those myself, but I think there should be workarounds.)
The final whitespace rule is that this plugin puts one import/export per line. I’ve never seen real projects that intentionally puts several imports/exports on the same line.
No. This is intentional to keep things simple. Use some other sorting rule, such asimport/order, for sortingrequire
. Or consider migrating your code usingrequire
toimport
.import
is well supported these days.
Some other import sorting rules sort based on the first name afterimport
, rather than the string afterfrom
. This plugin intentionally sorts on thefrom
string to begit diff
friendly.
Have a look at this example:
import{productType}from"./constants";import{truncate}from"./utils";
Now let’s say you need thearraySplit
util as well:
import{productType}from"./constants";import{arraySplit,truncate}from"./utils";
If the imports were sorted based on the first name afterimport
(“productType” and “arraySplit” in this case), the two imports would now swap order:
import{arraySplit,truncate}from"./utils";import{productType}from"./constants";
On the other hand, if sorting based on thefrom
string (like this plugin does), the imports stay in the same order. This prevents the imports from jumping around as you add and remove things, keeping your git history clean and reducing the risk of merge conflicts.
Mostly.
Imports and re-exports can have side effects in JavaScript, so changing the order of them can change the order that those side effects execute in. It is best practice toeither import a module for its side effectsor for the things it exports (andnever rely on side effects from re-exports).
// An `import` that runs side effects:import"some-polyfill";// An `import` that gets `someUtil`:import{someUtil}from"some-library";
Imports that are only used for side effects stay in the input order. These won’t be sorted:
import"b";import"a";
Imports thatboth export stuffand run side effects are rare. If you run into such a situation – try to fix it, since it will confuse everyone working with the code. If that’s not possible, it’s possible toignore (parts of) sorting.
Another small caveat is that you sometimes need to move comments manually – seeComment and whitespace handling.
For completeness, sorting the imported/exporteditems of an import is always safe:
import{c,b,a}from"wherever";// Equivalent to:import{a,b,c}from"wherever";
Note:import {} from "wherever"
isnot treated as a side effect import.
Finally, there’s one more thing to know about exports. Consider this case:
one.js:
exportconsttitle="One";exportconstone=1;
two.js:
exportconsttitle="Two";exportconsttwo=2;
reexport.js:
export*from"./one.js";export*from"./two.js";
main.js:
import*asreexportfrom"./reexport.js";console.log(reexport);
What happens if you runmain.js? In Node.js and browsers the result is:
{one:1,two:2,}
Note howtitle
is not even present in the object! This is good for sorting, because it means that it’s safe to reorder the twoexport * from
exports inreexport.js – it’s not like the last import “wins” and you’d accidentally change the value oftitle
by sorting.
However, thismight still cause issues depending on which bundler you use. Here’s how a few bundlers handled the duplicate nametitle
the time of this writing:
- ✅ Webpack: Compile time error – safe.
- ✅ Parcel: Run time error – safe.
⚠️ Rollup: Compile time warning, but uses the first one of them so it’s potentially unsafe. It’s possible to configure Rollup to treat warnings as errors, though.- ✅ TypeScript: Compile time error – safe.
You might end up with slightly weird spacing, for example a missing space after a comma:
import{bar,baz,foo}from"example";
Sorting is the easy part of this plugin. Handling whitespace and comments is the hard part. The autofix might end up with a little odd spacing around an import/export sometimes. Rather than fixing those spaces by hand, I recommend usingPrettier or enabling other autofixable ESLint whitespace rules. Seeexamples for more information.
The reason the whitespace can end up weird is because this plugin re-uses and moves around already existing whitespace rather than removing and adding new whitespace. This is to stay compatible with other ESLint rules that deal with whitespace.
Not really. The error message for this rule is literally “Run autofix to sort these imports!” Why? To actively encourage you to useeslint --fix
(autofix), and not waste time on manually doing something that the computer does a lot better. I’ve seen people painstakingly fixing cryptic (and annoying!) sorting errors from other rules one by one, not realizing they could have been autofixed. Finally, not trying to make more detailed messages makes the code of this pluginmuch easier to work with.
Looking for/* eslint-disable */
for this rule? Read all aboutignoring (parts of) sorting.
Theimport/order rule used to not support alphabetical sorting but now it does. So what doeseslint-plugin-simple-import-sort
bring to the table?
- Sorts imported/exported items (
import { a, b, c } from "."
):eslint-plugin-import#1787 - Sorts re-exports:eslint-plugin-import#1888
- Supports comments:eslint-plugin-import#1450,eslint-plugin-import#1723
- Supports type imports:eslint-plugin-import#645
- Supports absolute imports:eslint-plugin-import#512
- Allows choosing where side effect imports go:eslint-plugin-import#970
- Allows custom ordering within groups:eslint-plugin-import#1378
- Sorts numerically (
"./img10.jpg"
sorts after"./img2.jpg"
, not before) - Open
import/order
issues:import/export ordering
Some other differences:
- This plugin gives you a single error for each chunk of imports/exports, while
import/order
can give multiple (seeCan I use this without autofix? for details). In other words, this plugin is noisier in terms of underlined lines in your editor, whileimport/order
is noisier in terms of error count. - This plugin has a single (though very powerful) option that is a bunch of regexes, while
import/order
has bunch of different options. It’s unclear which is easier to configure. Buteslint-plugin-simple-import-sort
tries to do the maximum out of the box.
dprint also sorts imports and exports – but does not group them. Instead, it preserves your own grouping.
The first question to ask yourself is if dprint is good enough. If so, you’ve got one tool less to worry about!
If you’d like to enforce grouping, though, you could still useeslint-plugin-simple-import-sort
. However, the two might disagree slightly on some sorting edge cases. So it’s better to turn off sorting in your dprint config file:
{"typescript": {"module.sortImportDeclarations":"maintain" }}
Source:https://dprint.dev/plugins/typescript/config/
Usecustom grouping, setting thegroups
option to only have a single inner array.
For example, here’s the default value but changed to a single inner array:
[["^\\u0000","^node:","^@?\\w","^","^\\."]];
(By default, each string is in itsown array (that’s 5 inner arrays) – causing a blank line between each.)
About
Easy autofixable import sorting.
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.