Skip to content

organizeImports

~/.config/zed/settings.json
{
"code_actions_on_format": {
"source.organizeImports.biome": true,
}
}

Provides a code action to sort the imports and exports in the file using a built-in or custom order.

Imports and exports are first separated into chunks, before being sorted. Imports or exports of a chunk are then grouped according to the user-defined groups. Within a group, imports are sorted using a built-in order that depends on the import/export kind, whether the import/export has attributes and the source being imported from.

import A from "@my/lib" with { "attribute1": "value" };
^^^^^^^^ ^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
kind source attributes
export * from "@my/lib" with { "attribute1": "value" };
^^^^^^^^ ^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^
kind source attributes

Note that source is also often called specifier in the JavaScript ecosystem.

Chunk of imports and chunks of exports

Section titled Chunk of imports and chunks of exports

A chunk is a sequence of adjacent imports or exports. A chunk contains only imports or exports, not both at the same time. The following example includes two chunks. The first chunk consists of the three imports and the second chunk consists of the three exports. The first export that appears ends the first chunk and starts a new one.

import A from "a";
import * as B from "b";
import { C } from "c";
export * from "d";
export * as F from "e";
export { F } from "f";

Chunks also end as soon as a statement or a side-effect import (also called bare import) is encountered. Every side-effect import forms an independent chunk. The following example contains six chunks:

// chunk 1
import A from "a";
import * as B from "b";
// chunk 2
import "x";
// chunk 3
import "y";
// chunk 4
import { C } from "c";
// chunk 5
export * from "d";
function f() {}
// chunk 6
export * as E from "e";
export { F } from "f";
  1. The first chunk contains the two first import and ends with the appearance of the first side-effect import import "x".
  2. The second chunk contains only the side-effect import import "x".
  3. The third chunk contains only the side-effect import import "y".
  4. The fourth chunk contains a single import; The first export ends it.
  5. The fifth chunk contains the first export; The function declaration ends it.
  6. The sixth chunk contains the last two export.

Chunks are also delimited by detached comments. A detached comment is a comment followed by a blank line. Comments not followed by a blank line are attached comments. Note that blank lines alone are not taken into account when chunking imports and exports. The following example contains a detached comment that splits the imports into two chunks:

// Attached comment 1
import A from "a";
// Attached comment 2
import * as B from "b";
// Detached comment
import { C } from "c";

The line import { C } from "c" forms the second chunk. The blank line between the first two imports is ignored so they form a single chunk.

The sorter ensures that chunks are separated from each other with a blank lines. Only side-effect imports adjacent to a chunk of imports are not separated by a blank line. The following code…

import A from "a";
import * as B from "b";
import "x";
import { C } from "c";
export * from "d";
// Detached comment
export * as F from "e";
// Attached comment
export { F } from "f";

is sorted as:

import A from "a";
import * as B from "b";
import "x";
import { C } from "c";
export * from "d";
// Detached comment
export * as F from "e";
// Attached comment
export { F } from "f";

Also, note that blank lines inside a chunk are ignored and preserved. They can be removed by explicitly defining groups as demonstrated in the next section.

Once chunks are formed, imports and exports of each chunk are sorted. Imports and exports are sorted by their source. Sources are ordered by “distance”. Sources that are “farther” from the current module are put on the top, sources “closer” to the user are put on the bottom. This leads to the following order:

  1. URLs such as https://example.org.
  2. Packages with a protocol such as node:path, bun:test, jsr:@my?lib, or npm:lib.
  3. Packages such as mylib or @my/lib.
  4. Aliases: sources starting with @/, #, ~, or %. They usually are Node.js subpath imports or TypeScript path aliases.
  5. Absolute and relative paths.

Two imports/exports with the same source category are sorted using a natural sort order tailored to URLs, packages, and paths. Notably, the order ensures that A < a < B < b. The order takes also numbers into account, e.g. a9 < a10.

For example, the following code…

import sibling from "./file.js";
import internal from "#alias";
import fs from "fs";
import { test } from "node:test";
import path from "node:path";
import parent from "../parent.js";
import scopedLibUsingJsr from "jsr:@scoped/lib";
import data from "https://example.org";
import lib from "lib";
import scopedLib from "@scoped/lib";

…is sorted as follows:

import data from "https://example.org";
import scopedLibUsingJsr from "jsr:@scoped/lib";
import path from "node:path";
import { test } from "node:test";
import scopedLib from "@scoped/lib";
import fs from "fs";
import lib from "lib";
import internal from "#alias";
import parent from "../parent.js";
import sibling from "./file.js";

If two imports or exports share the same source and are in the same chunk, then they are ordered according to their kind as follows:

  1. Default type import
  2. Default import
  3. Combined default and namespace import
  4. Combined default and named import
  5. Namespace type import / Namespace type export
  6. Namespace import / Namespace export
  7. Named type import / Named type export
  8. Named import / Named export

Also, import and exports with attributes are always placed first. For example, the following code…

import * as namespaceImport from "same-source";
import type * as namespaceTypeImport from "same-source";
import { namedImport } from "same-source";
import type { namedTypeImport } from "same-source";
import defaultNamespaceCombined, * as namespaceCombined from "same-source";
import defaultNamedCombined, { namedCombined } from "same-source";
import defaultImport from "same-source";
import type defaultTypeImport from "same-source";
import { importWithAttribute } from "same-source" with { "attribute": "value" } ;

is sorted as follows:

import { importWithAttribute } from "same-source" with { "attribute": "value" } ;
import type defaultTypeImport from "same-source";
import defaultImport from "same-source";
import defaultNamespaceCombined, * as namespaceCombined from "same-source";
import defaultNamedCombined, { namedCombined } from "same-source";
import type * as namespaceTypeImport from "same-source";
import * as namespaceImport from "same-source";
import type { namedTypeImport } from "same-source";
import { namedImport } from "same-source";

This default order cannot be changed. However, users can still customize how imports and exports are sorted using the concept of groups.

Imports or exports of a chunk are divided into groups before being sorted with the built-in order described in the previous section. By default every chunk consists of a single group. These default groups and their order may not be to your taste. The sorter provides a groups option that allows you to customize how the chunks are divided into groups. The groups option is a list of group matchers. A group matcher is:

  • A predefined group matcher, or
  • A glob pattern, or
  • A list of glob patterns.

Predefined group matchers are strings in CONSTANT_CASE prefixed and suffixed by :. The sorter provides several predefined group matchers:

  • :ALIAS:: sources starting with #, @/, ~, or %.
  • :BUN:: sources starting with the protocol bun: or that correspond to a built-in Bun module such as bun.
  • :NODE:: sources starting with the protocol node: or that correspond to a built-in Node.js module such as fs or path.
  • :PACKAGE:: Scoped and bare packages.
  • :PACKAGE_WITH_PROTOCOL:: Scoped and bare packages with a protocol.
  • :PATH:: absolute and relative paths.
  • :URL:

Let’s take an example. In the default configuration, Node.js modules without the node: protocols are separated from those with a protocol. To groups them together, you can use the predefined group :NODE:. Given the following configuration…

{
"assist": {
"actions": {
"source": {
"organizeImports": {
"level": "on",
"options": {
"groups": [
":URL:",
":NODE:"
]
}
}
}
}
}
}

…and the following code…

import sibling from "./file.js";
import internal from "#alias";
import fs from "fs";
import { test } from "node:test";
import path from "node:path";
import parent from "../parent.js";
import scopedLibUsingJsr from "jsr:@scoped/lib";
import data from "https://example.org";
import lib from "lib";
import scopedLib from "@scoped/lib";

…we end up with the following sorted result where the imports of node:path and the fs Node.js module are grouped together:

import data from "https://example.org";
import scopedLibUsingJsr from "jsr:@scoped/lib";
import path from "node:path";
import fs from "fs";
import { test } from "node:test";
import scopedLib from "@scoped/lib";
import lib from "lib";
import internal from "#alias";
import parent from "../parent.js";
import sibling from "./file.js";

Note that all imports that don’t match a group matcher are always placed at the end.

Groups matchers can also be glob patterns and list of glob patterns. Glob patterns selects imports and exports with a source that matches the pattern. In the following example, we create two groups: one that gathers imports/exports with a source starting with @my/lib except @my/lib/speciaal and the other that gathers imports/exports starting with @/.

{
"options": {
"groups": [
["@my/lib", "@my/lib/**", "!@my/lib/special", "!@my/lib/special/**"],
"@/**"
]
}
}

By applying this configuration to the following code…

import lib from "@my/lib";
import aliased from "@/alias";
import path from "@my/lib/special";
import test from "@my/lib/path";

…we obtain the following sorted result. Imports with the sources @my/lib and @my/lib/path form the first group. They match the glob patterns @my/lib and @my/lib/** respectively. The import with the source @my/lib/special is not placed in this first group because it is rejected by the exception !@my/lib/special. The import with the source @/alias is placed in a second group because it matches the glob pattern @/**. Finally, other imports are placed at the end.

import lib from "@my/lib";
import test from "@my/lib/path";
import aliased from "@/alias";
import path from "@my/lib/special";

Note that @my/lib matches @my/lib but not @my/lib/**. Conversely, @my/lib/subpath matches @my/lib/**, but not @my/lib. So, you have to specify both glob patterns if you want to accept all imports/exports that start with @my/lib. The prefix ! indicates an exception. You can create exceptions of exceptions by following an exception by a regular glob pattern. For example ["@my/lib", "@my/lib/**", "!@my/lib/special", "!@my/lib/special/**", "@my/lib/special/*/accepted/**"] allows you to accepts all sources matching @my/lib/special/*/accepted/**. For more details on the supported glob patterns, see the dedicated section.

For now, it is not possible to mix predefined group matchers and glob patterns inside a list of globs. This is a limitation that we are working to remove.

The sorter allows the separation of two groups with a blank line using the predefined string :BLANK_LINE:. Given the following configuration…

{
"options": {
"groups": [
":NODE:",
":BLANK_LINE:",
["@my/lib", "@my/lib/**", "!@my/lib/special", "!@my/lib/special/**"],
"@/**"
]
}
}

…the following code…

import path from "node:path";
import lib from "@my/lib";
import test from "@my/lib/path";
import path from "@my/lib/special";
import aliased from "@/alias";

…is sorted as:

import path from "node:path";
import lib from "@my/lib";
import test from "@my/lib/path";
import aliased from "@/alias";
import path from "@my/lib/special";

When sorting imports and exports, attached comments are moved with their import or export, and detached comments (comments followed by a blank line) are left where they are.

However, there is an exception to the rule. If a comment appears at the top of the file, it is considered as detached even if no blank line follows. This ensures that copyright notice and file header comments stay at the top of the file.

For example, the following code…

// Copyright notice and file header comment
import F from "f";
// Attached comment for `e`
import E from "e";
// Attached comment for `d`
import D from "d";
// Detached comment (new chunk)
// Attached comment for `b`
import B from "b";
// Attached comment for `a`
import A from "a";

…is sorted as follows. A blank line is automatically added after the header comment to ensure that the attached comment doesn’t merge with the header comment.

// Copyright notice and file header comment
// Attached comment for `d`
import D from "d";
// Attached comment for `e`
import E from "e";
import F from "f";
// Detached comment (new chunk)
// Attached comment for `a`
import A from "a";
// Attached comment for `b`
import B from "b";

Named imports, named exports and attributes sorting

Section titled Named imports, named exports and attributes sorting

The sorter also sorts named imports, named exports, as well as attributes. It uses a natural sort order for comparing numbers.

The following code…

import { a, b, A, B, c10, c9 } from "a";
export { a, b, A, B, c10, c9 } from "a";
import special from "special" with { "type": "ty", "metadata": "data" };

…is sorted as follows:

import { A, a, B, b, c9, c10 } from "a";
export { A, a, B, b, c9, c10 } from "a";
import special from "special" with { "metadata": "data", "type": "ty" };

You need to understand the structure of a source to understand which source matches a glob. A source is divided in source segments. Every source segment is delimited by the separator / or the start/end of the source. For instance src/file.js consists of two source segments: src and file.js.

  • star * that matches zero or more characters inside a source segment

file.js matches *.js. Conversely, src/file.js doesn’t match *.js

  • globstar ** that matches zero or more source segments ** must be enclosed by separators / or the start/end of the glob. For example, **a is not a valid glob. Also, ** must not be followed by another globstar. For example, **/** is not a valid glob.

file.js and src/file.js match ** and **/*.js Conversely, README.txt doesn’t match **/*.js because the source ends with .txt.

  • Use \* to escape *

\* matches the literal * character in a source.

  • ?, [, ], {, and } must be escaped using \. These characters are reserved for possible future use.

  • Use ! as first character to negate a glob

file.js matches !*.test.js. src/file.js matches !*.js because the source contains several segments.

biome.json
{
"assist": {
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}