Replies: 10 comments 12 replies
-
Excellent writeup, and very onboard with the proposal! The only thing I’m not sure about is the
Nit: |
Beta Was this translation helpful? Give feedback.
-
I've been waiting so long for this and the waiting paid off! Thanks for the great work on the RFC @Conaclos ! My only reservation is that I think explicit scoped specifiers ( For example, I have a company monorepo with several internal packages. So if I specify a pattern to match
Not sure if that made sense? 😅 |
Beta Was this translation helpful? Give feedback.
This comment was marked as off-topic.
This comment was marked as off-topic.
-
Excellent. I use the trivago prettier plugin which is real close to the prettier example. One thing that might be beneficial is to have a setting to include blank lines between groups so groups can remain pure string based groups. Aside from that excellent! Waiting for it with great anticipation! 💯🎉🥰 |
Beta Was this translation helpful? Give feedback.
-
I opened the issue #3177 to fund the implementation of this RFC. |
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
Any movement on this? I'm sure a lot of people are waiting on this final piece before they can put your beautiful tool to good use :) |
Beta Was this translation helpful? Give feedback.
-
For anyone whom wants something in the meantime before this feature is built, just use prettier with defaults similar to biome.json and run the sorted imports check first. Then run biome immediately after, but have it ignore import sorting. When you do a format check in the CI, just have it only run biome check (the imports will be ignored). This solution isn't perfect, but it's still quite fast. You can also then just turn off prettier once Biome gets its funding and gets the feature built. |
Beta Was this translation helpful? Give feedback.
-
This may be more related to the unused imports rule, but will there be an option to remove empty imports? By empty I mean imports like this:
For stability, it makes sense to not remove this import. But usually this kind of code is unintended. It is an artifact of the unused imports rule fix. If user wants the side effects from module
Any thoughts about this? |
Beta Was this translation helpful? Give feedback.
-
Any update? I'm still using the biome + prettier trick in my projects. It would be nice to have biome handle this. |
Beta Was this translation helpful? Give feedback.
-
Biome users have reported several shortcomings with the current import sorter.
This document first reviews the state of the art, including the Biome import sorter and its shortcomings.
It then outlines a proposal that attempts to address these shortcomings.
Note: This is an RFC. It is not the final design.
State of the art
prettier-plugin-sort-imports
This plugin allows its users to sort imports while formatting with Prettier.
If no configuration is provided, the plugin sorts imports using natural ordering of the import sources.
The plugin allows its users to customize the order by specifying import groups.
In the following examples, 2 groups are created:
All imports that don't match at least one of its groups are third party imports.
The plugin places the third party imports at the start.
This can be changed using the
<THIRD_PARTY_MODULES>
identifier insideimportOrder
.The plugin orders imports inside a group using natural order.
The option
importOrderSeparation
controls if a blank line is inserted between groups.The plugin doesn't ensure a total order between imports of different kinds.
For example the two following examples are considered sorted:
If the option
importOrderCaseInsensitive
is enabled, the order between imports of a group becomes partial.This means that the following examples are considered sorted:
I think it is not desirable.
A user who enables
importOrderCaseInsensitive
will surely expect a single (total) order between imports.By enabling
importOrderCaseInsensitive
, I think a user wants that for any pair of letters, the lowercase, and uppercase variants of the former come before or after the uppercase and lowercase variants of the latter.By assuming that an uppercase variant comes before its lowercase variants,
a user could thus expect the following result:
By default, specifiers in named imports are not sorted.
A user has to enable
importOrderSortSpecifiers
to sort them.eslint-plugin-import/order
Similarly to the Prettier Plugin, this ESLint plugin allows customizing import order using groups.
However, it defines a set of predefined groups:
built-in
fs
,path
, ...)import/core-modules
external
import/internal-regex
, and@
(scoped package)import/external-module-folders
parent
../**/*
sibling
./**/*
index
.
./
./index
./index.js
internal
import/internal-regex
object
TypeScript's import assignment
import x = y
type
import type
unknown
By default, the plugin recognizes only
js
.This can be modified using
import/extensions
CJS imports
const x = require("")
are always ordered after ESM imports.Thus, ESM and CJS are kind-of super-groups.
Unassigned CJS imports and side effect imports are ignored.
Additionally, the plugin supports custom resolvers to resolve a module.
This allows to support TypeScript's
tsconfig
paths for example.The option
groups
is an array of predefined groups and/or subarray of mixed predefined groups.Imports of mixed groups may be interleaved according to their alphabetic order.
Omitted predefined groups forms an implicit mixed group at the end.
The current default is
["built-in", "external", "parent", "sibling", "index"]
.Example of a custom config:
The option
pathGroups
allows using a glob pattern to categorize the path in a predefined group.It also allows indicating that the path should be ordered just before or after the predefined group.
The option
newlines-between
controls the insertion of blank lines between groups.The option takes one of the following value:
ignore
: blank lines are allowed everywhere (default)always
: require at least one blank line between groups and forbid blank lines inside a groupalways-and-inside-groups
: require at least one blank line between groupsnever
: no blank line between importsThe option
distinctGroup
indicates ifpathGroups
positioned after/before apredefined group should be considered as distinct groups.
The current default is
true
.However, in the future the option should be disabled by default.
The option
pathGroupsExcludedImportTypes
defines import types that are nothandled by
pathGroups
.The option
alphabetize
allows controlling how to order the imports of the groups.The order can be ascending, descending, or either (default).
The order can be case-insensitive (by default it is case-sensitive).
ESLint Plugin Simple Import Sort
This plugin sorts each chunks of imports separately.
A chunk is a sequence of lines that contains only imports, comments, and blank lines.
Each chunk is organized into groups (sections in the Plugin terminology) with a blank line between each.
Groups are themselves organized into subgroups.
Also, the plugin sorts re-exports, i.e.
export ... from "mod"
.Unlike, imports, a comment on its own line starts a new chunk of re-exports.
The default groups (each one with a single subgroup) is as follows:
node:
.node:
).@/foo
.The plugin allows you to define your own groups and subgroups.
A regex defines a subgroup.
To determine the subgroup of a given import, the import source is matched against every regular expression.
The import ends up in the subgroup that have the longest regex match.
Imports that don’t match any regex are put together last.
Regexes are extended in a way that allows matching side effect imports and
import type
.This allows users to group them.
Import statements in every subgroup are sorted by their import source.
The order is:
This leads to the following order:
../../utils
<../utils
<.
In short,
.
and/
sort before any other (non-whitespace, non-control) character...
and similar sort like../,
to avoid the "shorter prefix comes first".If both
import type
and regularimport
are used for the same source,then the type imports come first. Same thing for
export type
.import type
can be placed in a dedicated group/subgroup by using a custom group.Named Import and named re-export specifiers are sorted by their external names.
This choice is motivated by stability, because the external name is unlikely to change.
For instance, the following code is sorted:
Leading and trailing comments are moved with the statement,
except for leading and trailing comments of a chunk on their own line.
Similarly, leading and training comments on their own line inside a named import are not moved.
This plugin don't handle CJS.
TypeScript import sorting
TypeScript's LSP provides a
sort imports
action.TypeScript 5.0 added several options to customize the sorting algorithm.
It also changed its default behavior for automatically detecting if the sorting is case-sensitive or case-insensitive.
By default, it uses an ordinal order that uses a lexicographic binary order.
This can be changed by setting
organizeImportsCollation
tounicode
.When set to
unicode
, TypeScript uses the collation order of a given locale.The locale can be set with
organizeImportsLocale
and isen-US
by default.Under
unicode
, natural sorting is used between numbers.This can be disabled with
organizeImportsNumericCollation
.TypeScript places inline-type imports last.
For example, the following code is sorted:
However, some TypeScript member expressed
the wish of ignoring the
type
qualifier and more globally to match the behavior of the ESLint plugins.Ignoring the
type
qualifier improve stability by avoiding moving the specifier whentype
is added or removed.Biome's current design
Imports separated by at least one blank lines form distinct chunks.
Biome groups imports by their "distance" from a user POV.
The groups (in order) are:
bun:
protocol.node:
protocol and common Node built-in modules such asassert
;npm:
protocol.:
.These are usually considered "virtual modules", modules that are injected by your working environment, e.g.
vite
;@scoped
specifier (libraries);#
.This is applicable when using Node's sub-path imports;
Imports of a group are ordered using a natural order.
Named specifiers are also ordered in natural order.
If they are aliased (
name as alias
), we use their aliases to order them.This is different from the ESLint Plugin Simple Import Sort that uses the imported name, and not the aliases.
A
type
qualifier of a named specifier doesn't affect the order.The following example is correctly ordered:
Biome users reported several shortcomings with the current design:
I didn't find the related comment/issue that points it.
This could also cover prefer top-level type imports #1660
export from
Sort exports #467Proposal
All import sorters presented in the state-of-the-art rely on a multi-layered sorting approach to determine the order between import statements.
They organize import statements into groups that are ordered between them, and then use a string-wise comparison of their import source.
This proposal is based on the same approach: first we define import chunks, then we group imports within a chunk before sorting them.
Imports are ranked according to the order of import sources.
Unlike other approaches, we didn't compare the import sources in a string-wise way.
Instead, we parse them into a structured form and compare this form.
Import chunks
An import chunk is either:
In other words, side effect imports, non-sticky comments, and any statement which is not an import acts as a boundary of an import chunk.
Unlike the current Biome import organizer, a blank line doesn't end an import chunk.
A sticky comment is a comment that precedes an import statement.
They must not be separated by any blank lines.
Import source sorting
Import sources are first parsed into a structured form and then compared.
Import sources have many structures:
Regular paths that start with
/
,../
, or./
, or is the paths..
or.
.Bare specifiers that generally are NPM packages or aliases.
They can be separated between scoped specifiers such as
@biomejs/biome
and non-scoped specifiers such asjest
.URLs.
Moreover, some URLs respect a precise format.
For instance, an
esm.sh
URL follows this pattern:https://esm.sh/<pkg>@<smver>/<path>
.Here, an example:
Bare specifiers prefixed by a protocol
Internal aliases.
@/
is a convention used by Vue (Webpack aliases)And more?
I propose to parse them in a unified way.
We could parse import sources as URI with optional scheme (aka protocol).
The following diagram shows an overview of the unified structure.
It flows from left to right and top to bottom.
#intera\nal
and@/
could be handled as a path.The comparison of import sources relies on the structure of the import sources.
A Missing component is placed after a present component.
For instance, an import source without protocol is placed after an import source with a protocol.
A bare specifier (
pkg
) is placed before a regular path.Paths are also parsed and ordered in a structural way:
@/
or#
/
..
or.
Path components could be ordered such as
..
is placed before.
that is itself placed beforetext
.This allows to use an order analog to ESLint Simple Sort which orders paths in terms of location proximity.
Component are compared in a string-wise way.
It seems that the community is slowly going towards Unicode collation with numerical ordering.
Most of the time, import source are expected to be in ASCII and Rust has not yet mature libraries for internationalization, including collation.
Thus, I propose using case-insensitive natural sorting with code point order as tiebreaker.
Examples of an ordered list of strings:
Note that this diverges from the current Biome import sorter.
In the current Biome import sorter, all uppercase characters come before all lowercase characters.
This solves #2162 and is more in line with the direction taken by other import sorters.
Specifiers of a named import statement are also sorted using this order.
Let's take an example of ordered import sources:
Although the way of comparing import sources is different from the current Biome import sorter, this should marginally change the order that users currently observe.
I think the resulting order is good enough.
If users are not satisfied, they can use import groups, as described in the following section.
Import groups
Import statements within an import chunk are grouped before being sorted within each group.
A group can de defined by a glob pattern.
Groups are defined inside a list.
Groups are ordered in a chunk in the order that they appear in the list.
An import belongs to the first group that it matches in the list.
For example the following list:
["node:**", "#**", "**"]
results in this organization:The last group
**
can be omitted, because Biome has an implicit group that collects all remaining imports.Thus, an import always matches at least the (last) implicit group.
Some special identifier are mapped to predefined groups:
<bun>
: all imports that start withbun:
and alsobun
<node>
: all imports that start withnode:
and also node built-in modules that are not a dependency inpackage.json
<scoped-bare-specifier>
: e.g.@biomejs/biome
<bare-specifier>
: e.g.ava
, and also scoped<relative>
: all imports that start with../
or./
, or are exactly..
or.
<blank-line>
: allows separating two groups with a blank line.<types>
: allimport type
For example, the following code is sorted according to
["<node>", "<blank-line>", "<bare-specifier>", "<blank-line>", "<relative>"]
.Note that requesting for two consecutive blank lines is not forbidden, however the Biome formatter will merge them into a single blank line.
Also, a group (except blank line) should not appear twice in the list because the second apparition will never be matched.
Note: I proposed the syntax
<predefined-group>
.However, I am open to suggestion. I also thought about
:predefined-group:
. Any opinion?Also, instead of using plain globs, we could use a structured matcher.
For example
{ protocol: "node" }
could match anything with thenode
protocol.Globs could still be used to match several protocols or a specific path:
{ protocol: "{node,bun}", package: "assert{,/strict}" }
.Order between import kinds with identical source
Distinct import kind with an identical source are always ordered in the following order:
import type defaultType from "x"
import defaultName from "x"
import defaultName, * as ns from "x"
import defaultName, { Name } from "x"
import type * as ns from "x"
import * as ns from "x"
import type { Name } from "x"
import { Name } from "x"
The logic of this ordering is:
This order is induced by the order between NameSpace Imports and Named Imports.
making it more "gross" than named imports
Note that if the predefined
<types>
group is in the group list, then allimport type
are grouped together.Order between imports with identical kind and source
Imports can have the same kind and source.
This is for example the case of two default imports:
or when import attributes are present:
In this case, imports are sorted using the first imported name.
Import and export merging with identical source
The import organizer merges the import statements when it is possible.
Because this conflicts with our stability goal, we should certainly provide an option to disable/enable import statement merging.
First, the import organizer merges imports with same kind, source, and attributes.
For example:
Note that Default Imports (and NameSpace Imports) from a same source cannot be merged.
This kind of duplication should be reported by a linter rule.
Moreover, we should not merge two side effect imports with a same source.
Second, it merges imports of different kinds into grouped imports.
Default NameSpace Imports are preferred over Default Named Imports (this follows their kind order).
Some examples:
Also, if
import type
doesn't belong to a dedicated group, thenimport type {}
is merged withimport {}
, but if the predefined<types>
group is used in the group list.If the predefined
<types>
group is in the group list, then all inline types are extracted to a dedicatedimport type
.Grouped
import type
andexport type
When all specifiers of a named import or of a named export have a
type
qualifier, thentype
is factorized into a groupedimport type
orexport type
.Organize
export from
We should also order
export from
because they are kind of imports that directly re-export.Export chunks could follow the same logic as import chunks.
Exports could be directly sorted inside a chunk without grouping capabilities.
Beta Was this translation helpful? Give feedback.
All reactions