eslint-plugin-logical-imports
v0.2.2
Published
ESLint plugin that enforces one specifier per import statement and alphabetical sort by local symbol name, with configurable block grouping.
Readme
eslint-plugin-logical-imports
You're sorting imports wrong!
- No I'm not
- What is this?
- How do I install it?
- How do I configure it?
- How do I use it?
- What about performance?
- Can I control how it handles import blocks?
- Single imports is crazy, can I opt out of that part?
- What alternatives exist?
- Examples
- Change log
- License
No I'm not
I think you are! Let me explain why. If you disagree with any point, that's okay and you can skip the rest of the readme. This tool is not meant for you. 👍
What is the point of sorting anything?
Firstly, to make things easier to find, especially when using human eyeballs.
Secondly, to make things easier to insert, by providing a single objectively correct insertion point (which is also easy to find).
What are you most often trying to find when eyeballing a block of imports?
A local name, which could be an alias. Something that is referenced elsewhere in the current file.
If you're looking for a local name, what order should the imports be sorted by?
The local names should be sorted alphabetically. Import blocks should work like a dictionary, or the index of a book.
What happens if you sort by module path instead?
It breaks the sort order of local names. They might as well be sorted randomly.
What happens if you sort by export name instead?
It breaks the sort order of aliased local names. They might as well be sorted randomly.
What happens if you group imports from the same module into a single declaration?
It breaks the sort order of local names relative to other modules. They might as well be sorted randomly.
What is the only consistent and logical method for sorting imports?
By breaking down declarations to a single import each, then sorting them alphabetically by the local name.
What is this?
An ESLint plugin that enforces an unconventional, highly opinionated but also very logical import style:
One import per declaration. If you need multiple imports from a module, there must be equally many declarations.
Declarations are sorted alphabetically by the local name. The thing that is actually referenced in the rest of the codebase is the thing that should drive the ordering of imports.
Applied in combination, these principles ensure imports are ordered like a dictionary, or the index of a book.
Options
Option|Type|Default|Description
------|----|-------|-----------
allowMultipleSpecifiers|boolean|false|Set to true to all multi-imports like { a, b }
blocks|string[]|['builtin', 'external', 'internal']|Change the handling of import blocks, or use [] for a single block.
Block glossary
Value|Matches
-----|-------
builtin|Node/Deno/Bun builtins, e.g. node:fs, module, bun:test
builtin:types|Type-only imports of builtins
external|External dependencies (npm packages)
external:types|Type-only imports of external dependencies
internal|Local dependencies, e.g. ./foo/bar, ../baz, /qux
internal:types|Type-only imports of internal dependencies
If a :types variant is omitted from blocks,
type imports of that category fall back to the runtime-block peer,
so the default config merges types into their runtime blocks.
Behaviour
Sort key is the local name:
Import|Sort key ------|--------
import Foo from 'x'|Fooimport * as ns from 'x'|nsimport { foo } from 'x'|fooimport { foo as bar } from 'x'|barimport type { Foo } from 'x'|Fooimport { type Foo } from 'x'|FooComparison is then case-insensitive
localeCompare, with ASCII tie-break for stability.Side-effect imports (
import 'x') act as fences. They're never reordered and imports either side are sorted independently.Comments stay attached to their import. Leading line and block comments move with the import below, trailing same-line comments stay on the right.
Floating comments (a comment with blank lines on either side) attach to the next import, per ESLint's
getCommentsBeforesemantics.Line endings (LF or CRLF) are detected per file.
How do I install it?
npm install --save-dev eslint-plugin-logical-importsRequires Node >= 24 and ESLint >= 10.
How do I configure it?
Add the recommended config
to your eslint.config.js:
// ...
import logicalImports from 'eslint-plugin-logical-imports';
export default [
// ...
logicalImports.configs.recommended,
];Or configure it manually:
// ...
import logicalImports from 'eslint-plugin-logical-imports';
export default [
// ...
{
plugins: { 'logical-imports': logicalImports },
rules: {
'logical-imports/order': ['error', {
allowMultipleSpecifiers: false,
blocks: ['builtin', 'external', 'internal'],
}],
},
},
];How do I use it?
Just run eslint as you normally would.
The plugin implements a single, atomic fix
so eslint --fix converges in one pass
(i.e. is guaranteed to work).
What about performance?
There's no impact from splitting imports across multiple declarations. The spec mandates that all imports of a module must resolve to the same cached hit. This works in all runtimes and bundlers.
Can I control how it handles import blocks?
Yes, you can re-order the blocks if you wish:
// ...
import logicalImports from 'eslint-plugin-logical-imports';
export default [
// ...
{
plugins: { 'logical-imports': logicalImports },
rules: {
'logical-imports/order': ['error', {
blocks: ['internal', 'external', 'builtin'],
}],
},
},
];Or separate types from runtime imports:
// ...
import logicalImports from 'eslint-plugin-logical-imports';
export default [
// ...
{
plugins: { 'logical-imports': logicalImports },
rules: {
'logical-imports/order': ['error', {
blocks: ['builtin', 'external', 'internal', 'builtin:types', 'external:types', 'internal:types'],
}],
},
},
];Or group all imports as a single block:
// ...
import logicalImports from 'eslint-plugin-logical-imports';
export default [
// ...
{
plugins: { 'logical-imports': logicalImports },
rules: {
'logical-imports/order': ['error', {
blocks: [],
}],
},
},
];Single imports is crazy, can I opt out of that part?
Deep sigh, sad face.
Yes, you can.
Set allowMultipleSpecifiers: true in config:
// ...
import logicalImports from 'eslint-plugin-logical-imports';
export default [
// ...
{
plugins: { 'logical-imports': logicalImports },
rules: {
'logical-imports/order': ['error', {
allowMultipleSpecifiers: true,
}],
},
},
];Import declarations will be ordered by their first specifier and specifiers within a declaration will be ordered by their local name. If there is a comment in the specifier list, the specifiers will not be sorted.
What alternatives exist?
eslint-plugin-import: Sorts alphabetically by module path, which is objectively wrong.eslint-plugin-simple-import-sort: Sorts alphabetically by module path, wrong.
Examples
Before:
import { z } from 'zod';
import { format } from 'date-fns';
import { readFile } from 'node:fs/promises';
import { useState, useEffect } from 'react';
import { Component as Widget } from './app/widget';
import express from 'express';
import { format as utilFormat } from 'node:util';
import * as utils from './utils';After, with recommended config:
import { readFile } from 'node:fs/promises';
import { format as utilFormat } from 'node:util';
import express from 'express';
import { format } from 'date-fns';
import { useEffect } from 'react';
import { useState } from 'react';
import { z } from 'zod';
import * as utils from './utils';
import { Component as Widget } from './app/widget';