eslint-plugin-logical-imports
v0.3.2
Published
ESLint plugin that enforces one specifier per import statement and alphabetical sort by local symbol name, with configurable block grouping.
Maintainers
Readme
eslint-plugin-logical-imports
Sort import declarations as an index of local names.
Autofix in one pass with --fix.
- What's this?
- Why?
- Show me an example
- How do I install it?
- How do I configure it?
- How do I use it?
- Can I control how it handles import blocks?
- Can I use a prefix for internal paths?
- Single imports is crazy, what about performance?
- Single imports is still crazy, can I opt out of that part?
- What alternatives exist?
- Change log
- License
What's this?
An ESLint plugin that enforces an unconventional, highly opinionated but also very logical import style:
Declarations are sorted alphabetically by the local name. The thing that is actually referenced in the rest of the codebase is the thing that drives the ordering of imports.
One import per declaration. If you need multiple imports from a module, there must be equally many declarations.
Applied in combination, these principles ensure imports are ordered like a dictionary, or the index of a book.
Why?
Because you're sorting imports wrong!
No I'm not.
I think you are! Allow me to explain why. If you disagree with any of these points, you can stop reading and move on with your life. It's not for everyone. 👍
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.
Show me an example
Before:
'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { format } from 'date-fns';
import { z } from 'zod';
import { useQuery } from '@tanstack/react-query';
import { Logo } from './components/logo';
import { Spinner as Loader } from './components/spinner';
import { Calendar } from 'lucide-react';
import { useUser } from './hooks/use-user';After, with recommended config:
'use client';
import { Calendar } from 'lucide-react';
import { format } from 'date-fns';
import Link from 'next/link';
import { useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useState } from 'react';
import { z } from 'zod';
import { Spinner as Loader } from './components/spinner';
import { Logo } from './components/logo';
import { useUser } from './hooks/use-user';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.Comments stay attached to their import when line-adjacent. A leading line or block comment immediately above an import moves with that import, and a trailing same-line comment stays on the right.
Comments at the very start of a file, e.g. license headers, are the exception. They always stay rooted at the top of the file and never move with imports.
Comments separated from the following import by a blank line act as fences. Imports either side of a fence are sorted independently.
Side-effect imports (
import 'foo') also act as fences. They're never reordered and imports either side are sorted independently.Line endings (LF or CRLF) are detected per file.
How do I install it?
npm install --save-dev eslint-plugin-logical-importsTested with ESLint versions 9 and 10, and Oxlint version 1.
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 to your .oxlintrc.json:
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"jsPlugins": [{
"name": "logical-imports",
"specifier": "eslint-plugin-logical-imports"
}],
"rules": {
"logical-imports/order": "error"
}
}To 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'],
}],
},
},
];Options
Option|Type|Default|Description
------|----|-------|-----------
allowMultipleSpecifiers|boolean|false|Set to true to allow multi-imports like { a, b }
blocks|string[]|['builtin', 'external', 'internal']|Change the handling of import blocks, or use [] for a single block.
internalPrefixes|string[]|[]|Identify path prefixes as internal, e.g. ['@/', '~/'].
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, or matching a prefix in internalPrefixes.
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.
How do I use it?
Just run eslint or oxlint as you normally would.
The plugin implements a single, atomic fix
so eslint --fix and oxlint --fix converge in one pass
(i.e. they're guaranteed to work).
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 together in a single block:
// ...
import logicalImports from 'eslint-plugin-logical-imports';
export default [
// ...
{
plugins: { 'logical-imports': logicalImports },
rules: {
'logical-imports/order': ['error', {
blocks: [],
}],
},
},
];Can I use a prefix for internal paths?
Yes, you can these with the internalPrefixes option:
// ...
import logicalImports from 'eslint-plugin-logical-imports';
export default [
// ...
{
plugins: { 'logical-imports': logicalImports },
rules: {
'logical-imports/order': ['error', {
internalPrefixes: ['@/', '~/'],
}],
},
},
];Single imports is crazy, 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.
Single imports is still 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.
