├── .eslintignore
├── .eslintrc.dogfood.json
├── .eslintrc.js
├── .github
└── workflows
│ ├── check.yml
│ └── test.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── babel.config.json
├── build.js
├── examples
├── .eslintrc.js
├── 1.spaces.just-sort.js
├── 2.spaces.eslint-builtin.js
├── 3.spaces.prettier.js
├── README.md
├── eslint-plugin-import.js
├── groups.custom.js
├── groups.default-reverse.js
├── groups.no-blank-lines.js
├── groups.none.js
├── groups.type-imports-first-in-each-group.ts
├── groups.type-imports-first-sorted.ts
├── groups.type-imports-first.ts
├── groups.type-imports-last-in-each-group.ts
├── groups.type-imports-last-sorted.ts
├── groups.type-imports-last.ts
├── ignore.js
├── markdown.md
├── prettier-comments.js
├── readme-comments-items.js
├── readme-comments.js
├── readme-example.prettier.ts
├── readme-exports-grouping-less-comments.prettier.js
├── readme-exports-grouping.prettier.js
├── readme-order-items.prettier.ts
├── readme-order.prettier.ts
├── typescript.ts
└── vue.vue
├── package-lock.json
├── package-real.json
├── package.json
├── src
├── exports.js
├── imports.js
├── index.d.ts
├── index.js
└── shared.js
├── test
├── __snapshots__
│ └── examples.test.js.snap
├── examples.test.js
├── exports.test.js
├── helpers.js
└── imports.test.js
└── vitest.config.mjs
/.eslintignore:
--------------------------------------------------------------------------------
1 | !/**/.eslintrc.js
2 | !/*.js
3 | *.snap
4 | build
5 | coverage
6 | examples
7 | node_modules
8 |
--------------------------------------------------------------------------------
/.eslintrc.dogfood.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "imports": "error",
4 | "exports": "error"
5 | },
6 | "parserOptions": {
7 | "sourceType": "module",
8 | "ecmaVersion": "latest"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const error = "error";
4 | const warn = process.argv.includes("--report-unused-disable-directives")
5 | ? "error"
6 | : "warn";
7 |
8 | module.exports = {
9 | root: true,
10 | extends: ["eslint:recommended"],
11 | plugins: ["vitest"],
12 | parserOptions: {
13 | ecmaVersion: 2018,
14 | },
15 | env: { es6: true, node: true },
16 | rules: {
17 | "arrow-body-style": warn,
18 | "default-case": error,
19 | "default-case-last": warn,
20 | "dot-notation": warn,
21 | "no-caller": error,
22 | "no-console": warn,
23 | "no-eval": error,
24 | "no-labels": error,
25 | "no-octal-escape": error,
26 | "no-param-reassign": error,
27 | "no-promise-executor-return": error,
28 | "no-restricted-syntax": [
29 | error,
30 | {
31 | selector: "SequenceExpression",
32 | message:
33 | "The comma operator is confusing and a common mistake. Don’t use it!",
34 | },
35 | ],
36 | "no-self-compare": error,
37 | "no-shadow": error,
38 | "no-template-curly-in-string": error,
39 | "no-unmodified-loop-condition": error,
40 | "no-unneeded-ternary": warn,
41 | "no-useless-backreference": error,
42 | "no-useless-computed-key": warn,
43 | "no-useless-concat": warn,
44 | "no-useless-constructor": warn,
45 | "no-useless-rename": warn,
46 | "no-var": warn,
47 | "object-shorthand": warn,
48 | "one-var": [warn, "never"],
49 | "prefer-arrow-callback": warn,
50 | "prefer-const": warn,
51 | "prefer-destructuring": [warn, { object: true, array: false }],
52 | "prefer-exponentiation-operator": warn,
53 | "prefer-numeric-literals": warn,
54 | "prefer-object-spread": warn,
55 | "prefer-promise-reject-errors": error,
56 | "prefer-regex-literals": warn,
57 | "prefer-rest-params": warn,
58 | "prefer-spread": warn,
59 | "prefer-template": warn,
60 | curly: warn,
61 | eqeqeq: [error, "always", { null: "ignore" }],
62 | strict: error,
63 | yoda: warn,
64 | },
65 | overrides: [
66 | {
67 | files: ["test/*.js", "*.mjs"],
68 | parserOptions: {
69 | sourceType: "module",
70 | },
71 | },
72 | {
73 | files: ["*.test.js"],
74 | extends: ["plugin:vitest/recommended"],
75 | rules: {
76 | "vitest/no-disabled-tests": warn,
77 | "vitest/no-focused-tests": warn,
78 | },
79 | },
80 | ],
81 | };
82 |
--------------------------------------------------------------------------------
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | name: Check
2 |
3 | on:
4 | push:
5 | branches:
6 | - "main"
7 | pull_request:
8 |
9 | jobs:
10 | main:
11 | runs-on: ubuntu-latest
12 |
13 | strategy:
14 | matrix:
15 | node-version: [20.x]
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 |
20 | - uses: actions/setup-node@v4
21 | with:
22 | node-version: "${{ matrix.node-version }}"
23 |
24 | - name: Cache node_modules
25 | id: cache-node_modules
26 | uses: actions/cache@v4
27 | with:
28 | path: node_modules
29 | key: node_modules-${{ matrix.node-version }}-${{ hashFiles('package.json', 'package-lock.json') }}
30 |
31 | - if: steps.cache-node_modules.outputs.cache-hit != 'true'
32 | run: npm ci --no-audit
33 |
34 | - run: npm run build
35 |
36 | - run: npx eslint . --report-unused-disable-directives
37 |
38 | - run: npx prettier --check .
39 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | branches:
6 | - "main"
7 | pull_request:
8 |
9 | jobs:
10 | main:
11 | runs-on: ubuntu-latest
12 |
13 | strategy:
14 | matrix:
15 | node-version: [18.x, 20.x]
16 |
17 | steps:
18 | - uses: actions/checkout@v4
19 |
20 | - uses: actions/setup-node@v4
21 | with:
22 | node-version: "${{ matrix.node-version }}"
23 |
24 | - name: Cache node_modules
25 | id: cache-node_modules
26 | uses: actions/cache@v4
27 | with:
28 | path: node_modules
29 | key: node_modules-${{ matrix.node-version }}-${{ hashFiles('package.json', 'package-lock.json') }}
30 |
31 | - if: steps.cache-node_modules.outputs.cache-hit != 'true'
32 | run: npm ci --no-audit
33 |
34 | - run: npx vitest
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | coverage
3 | node_modules
4 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | build
2 | coverage
3 | examples
4 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "proseWrap": "never"
3 | }
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ### Version 12.1.1 (2024-07-02)
2 |
3 | This release adds a short `meta.docs.description` to each rule. Thanks to fisker Cheung (@fisker)!
4 |
5 | ### Version 12.1.0 (2024-04-13)
6 |
7 | This release adds TypeScript type definitions for the plugin itself. This is useful when you use TypeScript to check your ESLint configuration. It assumes that you install `@types/eslint` yourself. Thanks to @Logicer16!
8 |
9 | ### Version 12.0.0 (2024-02-10)
10 |
11 | This release removes the support for import assignments added in version 11.0.0:
12 |
13 | - Turns out it was broken in some cases.
14 | - The suggested fix went past my complexity tolerance for such an esoteric feature.
15 | - I also learned that they aren’t really imports, and that I don’t understand their semantics well enough to know how sorting them affects your program.
16 |
17 | If you miss the support for import assignments, I suggest you write your own ESLint rule which moves them out of the way from the actual imports, sorting them or not.
18 |
19 | ### Version 11.0.0 (2024-02-08)
20 |
21 | This release adds support for TypeScript import assignments (`import A = B.C` and `import A = require("module")`). Thanks to Szabolcs Kurdi (@szku01) and Svyatoslav Zaytsev (@MillerSvt)!
22 |
23 | It’s only a breaking change if you use TypeScript import assignments, and only in the form that you need to autofix your files.
24 |
25 | In other news, this release adds the `meta` plugin property in preparation for ESLint Flat Config, and avoids the deprecated `context.getSourceCode()` method (while still being backwards compatible).
26 |
27 | ### Version 10.0.0 (2023-01-27)
28 |
29 | This release might move some imported items with `type` around. This is a breaking formatting change (that only affects TypeScript and Flow), but only in the form of that you need to autofix your files.
30 |
31 | In previous versions, `type` specifiers came first:
32 |
33 | ```ts
34 | import { type B, a } from "a";
35 | export { type B, a } from "a";
36 | ```
37 |
38 | Now, all specifiers are sorted alphabetically, regardless of `type`:
39 |
40 | ```ts
41 | import { a, type B } from "a";
42 | export { a, type B } from "a";
43 | ```
44 |
45 | Motivation:
46 |
47 | You might import a class for a type annotation using:
48 |
49 |
50 | ```ts
51 | import {
52 | type MyClass,
53 | coolFunction,
54 | } from "example";
55 | ```
56 |
57 | Later, you also start instantiating that class in the same file (`new MyClass()`), so you remove `type`.
58 |
59 | Previously, this resulted in a messy diff due to the class moving:
60 |
61 | ```diff
62 | import {
63 | - type MyClass,
64 | coolFunction,
65 | + MyClass,
66 | } from "example";
67 | ```
68 |
69 | Now, the sorting with the `type` keyword would be:
70 |
71 |
72 | ```ts
73 | import {
74 | coolFunction,
75 | type MyClass,
76 | } from "example";
77 | ```
78 |
79 | Now there’s no reordering diff, just the `type` keyword being removed:
80 |
81 | ```diff
82 | import {
83 | coolFunction,
84 | - type MyClass,
85 | + MyClass,
86 | } from "example";
87 | ```
88 |
89 | This is consistent with [“Why sort on `from`?”][sort-from].
90 |
91 | Thanks to Jake Bailey (@jakebailey) for reporting and suggesting the fix!
92 |
93 | ### Version 9.0.0 (2023-01-16)
94 |
95 | This version adds support for [eslint-plugin-svelte], and for `declare module` in TypeScript.
96 |
97 | More generally, imports and exports are now supported _anywhere,_ by finding the set of parents of all imports and exports and working with those. Previously, the plugin only sorted imports and exports directly inside a `Program` node. For eslint-plugin-svelte and `declare module` that didn’t cut it.
98 |
99 | This is only a breaking change if you imports or exports in `declare module` in TypeScript, and only in the form of that you need to autofix your files.
100 |
101 | ### Version 8.0.0 (2022-09-03)
102 |
103 | Node.js builtin modules prefixed with `node:` are now in a separate group by default (regex: `^node:`), above the packages group. (Node.js builtins _without_ `node:` are still sorted together with npm packages like before.)
104 |
105 | Before:
106 |
107 | ```js
108 | import fs from "fs";
109 | import _ from "lodash-es";
110 | import { rmSync } from "node:fs";
111 | ```
112 |
113 | After:
114 |
115 | ```js
116 | import { rmSync } from "node:fs";
117 |
118 | import fs from "fs";
119 | import _ from "lodash-es";
120 | ```
121 |
122 | This is only a breaking change if you use the `node:` prefix in imports, and only in the form of that you need to autofix your files.
123 |
124 | ### Version 7.0.0 (2020-12-08)
125 |
126 | You can now customize where type imports (`import type { X } from "x"`) go, via the `groups` option. Type imports have `\u0000` at the end.
127 |
128 | This is only a breaking change if you use the `groups` option and your regexes care about what the _last_ character is. If so, you now need to account for the fact that the last character of type imports is `\u0000`.
129 |
130 | ### Version 6.0.1 (2020-11-19)
131 |
132 | - Fixed: `as default` in exports no longer results in invalid code.
133 |
134 | ### Version 6.0.0 (2020-11-15)
135 |
136 | - Renamed: `simple-import-sort/sort` is now called `simple-import-sort/imports`.
137 | - Added: `simple-import-sort/exports` for sorting (some) exports. Big thanks to Remco Haszing (@remcohaszing) for the suggestion and great feedback, and to @JCrepin for the initial implementation!
138 | - Fixed: `../..` imports are now sorted properly based on directory hierarchy.
139 | - Improved: The default regexes for the `groups` option can now be reordered freely without causing imports to unexpectedly end up in other groups than before.
140 | - Removed: Support for Node.js 8.
141 |
142 | ### Version 5.0.3 (2020-04-27)
143 |
144 | - Improved: Reduced package size by 50%.
145 |
146 | ### Version 5.0.2 (2020-03-11)
147 |
148 | - Fixed: The plugin now works with TypeScript 3.8 type imports. Thanks to Liwen Guo (@Livven) and Brandon Chinn (@brandon-leapyear)!
149 |
150 | ### Version 5.0.1 (2020-01-24)
151 |
152 | - Fixed: Side effect imports now correctly keep their original order in Node.js <12. Thanks to Irvin Zhan (@izhan)!
153 |
154 | ### Version 5.0.0 (2019-11-22)
155 |
156 | - Added: The `groups` option for [custom sorting].
157 | - Changed: Due to the new `groups` option, the default grouping is ever so slightly different. Now, not only _valid_ npm package names are placed in the “packages” group, but also things that _look_ like npm package names, such as `@ui/Section`. And anything starting with `.` is now considered to be a relative import. See [custom sorting] for more information.
158 | - Removed: Built-in support for webpack loader syntax. It didn’t fit well with the new `groups` option, and since I don’t use it myself I decided to remove it. Please open an issue if you have something to say about this!
159 |
160 | ### Version 4.0.0 (2019-06-19)
161 |
162 | - Changed: Sorting is now more human – it is case insensitive (matching the default behavior of TSLint, as well as many IDEs) and numbers are sorted by their numeric values. This might cause some churn but feels a lot nicer. See [#7] for more discussion.
163 | - Improved: `from` paths ending with dots in various ways used to be treated specially. This has now been simplified, which gives a more consistent sorting. Now, `"."` and `".."` are treated as `"./"` and `"../"` – and those are the only special cases for “dotty” paths. For example, you might see `import x from "."` now sorting before `import y from "./y"`.
164 | - Fixed: `".x"` is no longer considered to be a relative import. Only `from` paths equal to `"."` or `".."`, or that start with `"./"` or `"../"` are truly relative. This is a bit of an edge case, but if you do have “weird” imports starting with dots in unusual ways you might notice them jumping up to another group of imports.
165 | - Fixed: `import {} from "a"` is no longer considered a side-effect import. Only imports completely lacking the `{...} from` part are. Remove `{} from` if you relied on this from earlier versions.
166 | - Improved: Trailing spaces after imports are now preserved. Before, if you accidentally added some trailing spaces it would result in a “Run autofix to sort these imports!” error, but the autofix wouldn’t actually sort anything – it would only remove some spaces. That was a bit weird. Now, those spaces are preserved. It is up to other rules or [Prettier] to take care of trailing spaces.
167 |
168 | ### Version 3.1.1 (2019-05-16)
169 |
170 | - Fixed: Semicolon-free code style is now supported. The plugin now leaves a semicolon at the start of a line of code after an import alone.
171 |
172 | ### Version 3.1.0 (2019-03-30)
173 |
174 | - Added: Support for indentation in Vue `
17 |
--------------------------------------------------------------------------------
/package-real.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eslint-plugin-simple-import-sort",
3 | "version": "12.1.1",
4 | "license": "MIT",
5 | "author": "Simon Lydell",
6 | "repository": "lydell/eslint-plugin-simple-import-sort",
7 | "description": "Easy autofixable import sorting",
8 | "type": "commonjs",
9 | "keywords": [
10 | "eslint",
11 | "eslint-plugin",
12 | "eslintplugin",
13 | "import",
14 | "imports",
15 | "order",
16 | "sort",
17 | "sorter",
18 | "sorting"
19 | ],
20 | "peerDependencies": {
21 | "eslint": ">=5.0.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "type": "commonjs",
4 | "scripts": {
5 | "pretest": "prettier --check . && eslint . --report-unused-disable-directives && npm run dogfood",
6 | "test": "vitest run",
7 | "posttest": "npm run build",
8 | "build": "node build.js",
9 | "dogfood": "eslint --rulesdir src --config .eslintrc.dogfood.json test",
10 | "examples": "eslint --rulesdir src --no-ignore --fix-dry-run --format json --report-unused-disable-directives examples --ext .js,.ts,.vue,.md"
11 | },
12 | "devDependencies": {
13 | "@babel/eslint-parser": "7.23.10",
14 | "@babel/plugin-syntax-import-attributes": "7.23.3",
15 | "@babel/plugin-transform-flow-strip-types": "7.23.3",
16 | "@typescript-eslint/parser": "6.21.0",
17 | "@vitest/coverage-v8": "^1.2.2",
18 | "eslint": "8.56.0",
19 | "eslint-plugin-import": "2.29.1",
20 | "eslint-plugin-markdown": "3.0.1",
21 | "eslint-plugin-vitest": "0.3.22",
22 | "eslint-plugin-vue": "9.21.1",
23 | "prettier": "3.2.5",
24 | "typescript": "5.3.3",
25 | "vitest": "1.6.1"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/exports.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const shared = require("./shared");
4 |
5 | module.exports = {
6 | meta: {
7 | type: "layout",
8 | fixable: "code",
9 | schema: [],
10 | docs: {
11 | url: "https://github.com/lydell/eslint-plugin-simple-import-sort#sort-order",
12 | description: "Automatically sort exports.",
13 | },
14 | messages: {
15 | sort: "Run autofix to sort these exports!",
16 | },
17 | },
18 | create: (context) => {
19 | const parents = new Set();
20 |
21 | const addParent = (node) => {
22 | if (isExportFrom(node)) {
23 | parents.add(node.parent);
24 | }
25 | };
26 |
27 | return {
28 | ExportNamedDeclaration: (node) => {
29 | if (node.source == null && node.declaration == null) {
30 | maybeReportExportSpecifierSorting(node, context);
31 | } else {
32 | addParent(node);
33 | }
34 | },
35 |
36 | ExportAllDeclaration: addParent,
37 |
38 | "Program:exit": () => {
39 | const sourceCode = shared.getSourceCode(context);
40 | for (const parent of parents) {
41 | for (const chunk of shared.extractChunks(parent, (node, lastNode) =>
42 | isPartOfChunk(node, lastNode, sourceCode),
43 | )) {
44 | maybeReportChunkSorting(chunk, context);
45 | }
46 | }
47 | parents.clear();
48 | },
49 | };
50 | },
51 | };
52 |
53 | function maybeReportChunkSorting(chunk, context) {
54 | const sourceCode = shared.getSourceCode(context);
55 | const items = shared.getImportExportItems(
56 | chunk,
57 | sourceCode,
58 | () => false, // isSideEffectImport
59 | getSpecifiers,
60 | );
61 | const sortedItems = [[shared.sortImportExportItems(items)]];
62 | const sorted = shared.printSortedItems(sortedItems, items, sourceCode);
63 | const { start } = items[0];
64 | const { end } = items[items.length - 1];
65 | shared.maybeReportSorting(context, sorted, start, end);
66 | }
67 |
68 | function maybeReportExportSpecifierSorting(node, context) {
69 | const sorted = shared.printWithSortedSpecifiers(
70 | node,
71 | shared.getSourceCode(context),
72 | getSpecifiers,
73 | );
74 | const [start, end] = node.range;
75 | shared.maybeReportSorting(context, sorted, start, end);
76 | }
77 |
78 | // `export * from "a"` does not have `.specifiers`.
79 | function getSpecifiers(exportNode) {
80 | return exportNode.specifiers || [];
81 | }
82 |
83 | function isPartOfChunk(node, lastNode, sourceCode) {
84 | if (!isExportFrom(node)) {
85 | return "NotPartOfChunk";
86 | }
87 |
88 | const hasGroupingComment = sourceCode
89 | .getCommentsBefore(node)
90 | .some(
91 | (comment) =>
92 | (lastNode == null || comment.loc.start.line > lastNode.loc.end.line) &&
93 | comment.loc.end.line < node.loc.start.line,
94 | );
95 |
96 | return hasGroupingComment ? "PartOfNewChunk" : "PartOfChunk";
97 | }
98 |
99 | // Full export-from statement.
100 | // export {a, b} from "A"
101 | // export * from "A"
102 | // export * as A from "A"
103 | function isExportFrom(node) {
104 | return (
105 | (node.type === "ExportNamedDeclaration" ||
106 | node.type === "ExportAllDeclaration") &&
107 | node.source != null
108 | );
109 | }
110 |
--------------------------------------------------------------------------------
/src/imports.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const shared = require("./shared");
4 |
5 | const defaultGroups = [
6 | // Side effect imports.
7 | ["^\\u0000"],
8 | // Node.js builtins prefixed with `node:`.
9 | ["^node:"],
10 | // Packages.
11 | // Things that start with a letter (or digit or underscore), or `@` followed by a letter.
12 | ["^@?\\w"],
13 | // Absolute imports and other imports such as Vue-style `@/foo`.
14 | // Anything not matched in another group.
15 | ["^"],
16 | // Relative imports.
17 | // Anything that starts with a dot.
18 | ["^\\."],
19 | ];
20 |
21 | module.exports = {
22 | meta: {
23 | type: "layout",
24 | fixable: "code",
25 | schema: [
26 | {
27 | type: "object",
28 | properties: {
29 | groups: {
30 | type: "array",
31 | items: {
32 | type: "array",
33 | items: {
34 | type: "string",
35 | },
36 | },
37 | },
38 | },
39 | additionalProperties: false,
40 | },
41 | ],
42 | docs: {
43 | url: "https://github.com/lydell/eslint-plugin-simple-import-sort#sort-order",
44 | description: "Automatically sort imports.",
45 | },
46 | messages: {
47 | sort: "Run autofix to sort these imports!",
48 | },
49 | },
50 | create: (context) => {
51 | const { groups: rawGroups = defaultGroups } = context.options[0] || {};
52 |
53 | const outerGroups = rawGroups.map((groups) =>
54 | groups.map((item) => RegExp(item, "u")),
55 | );
56 |
57 | const parents = new Set();
58 |
59 | return {
60 | ImportDeclaration: (node) => {
61 | parents.add(node.parent);
62 | },
63 |
64 | "Program:exit": () => {
65 | for (const parent of parents) {
66 | for (const chunk of shared.extractChunks(parent, (node) =>
67 | isImport(node) ? "PartOfChunk" : "NotPartOfChunk",
68 | )) {
69 | maybeReportChunkSorting(chunk, context, outerGroups);
70 | }
71 | }
72 | parents.clear();
73 | },
74 | };
75 | },
76 | };
77 |
78 | function maybeReportChunkSorting(chunk, context, outerGroups) {
79 | const sourceCode = shared.getSourceCode(context);
80 | const items = shared.getImportExportItems(
81 | chunk,
82 | sourceCode,
83 | isSideEffectImport,
84 | getSpecifiers,
85 | );
86 | const sortedItems = makeSortedItems(items, outerGroups);
87 | const sorted = shared.printSortedItems(sortedItems, items, sourceCode);
88 | const { start } = items[0];
89 | const { end } = items[items.length - 1];
90 | shared.maybeReportSorting(context, sorted, start, end);
91 | }
92 |
93 | function makeSortedItems(items, outerGroups) {
94 | const itemGroups = outerGroups.map((groups) =>
95 | groups.map((regex) => ({ regex, items: [] })),
96 | );
97 | const rest = [];
98 |
99 | for (const item of items) {
100 | const { originalSource } = item.source;
101 | const source = item.isSideEffectImport
102 | ? `\0${originalSource}`
103 | : item.source.kind !== "value"
104 | ? `${originalSource}\0`
105 | : originalSource;
106 | const [matchedGroup] = shared
107 | .flatMap(itemGroups, (groups) =>
108 | groups.map((group) => [group, group.regex.exec(source)]),
109 | )
110 | .reduce(
111 | ([group, longestMatch], [nextGroup, nextMatch]) =>
112 | nextMatch != null &&
113 | (longestMatch == null || nextMatch[0].length > longestMatch[0].length)
114 | ? [nextGroup, nextMatch]
115 | : [group, longestMatch],
116 | [undefined, undefined],
117 | );
118 | if (matchedGroup == null) {
119 | rest.push(item);
120 | } else {
121 | matchedGroup.items.push(item);
122 | }
123 | }
124 |
125 | return itemGroups
126 | .concat([[{ regex: /^/, items: rest }]])
127 | .map((groups) => groups.filter((group) => group.items.length > 0))
128 | .filter((groups) => groups.length > 0)
129 | .map((groups) =>
130 | groups.map((group) => shared.sortImportExportItems(group.items)),
131 | );
132 | }
133 |
134 | // Exclude "ImportDefaultSpecifier" – the "def" in `import def, {a, b}`.
135 | function getSpecifiers(importNode) {
136 | return importNode.specifiers.filter((node) => isImportSpecifier(node));
137 | }
138 |
139 | // Full import statement.
140 | function isImport(node) {
141 | return node.type === "ImportDeclaration";
142 | }
143 |
144 | // import def, { a, b as c, type d } from "A"
145 | // ^ ^^^^^^ ^^^^^^
146 | function isImportSpecifier(node) {
147 | return node.type === "ImportSpecifier";
148 | }
149 |
150 | // import "setup"
151 | // But not: import {} from "setup"
152 | // And not: import type {} from "setup"
153 | function isSideEffectImport(importNode, sourceCode) {
154 | return (
155 | importNode.specifiers.length === 0 &&
156 | (!importNode.importKind || importNode.importKind === "value") &&
157 | !shared.isPunctuator(sourceCode.getFirstToken(importNode, { skip: 1 }), "{")
158 | );
159 | }
160 |
--------------------------------------------------------------------------------
/src/index.d.ts:
--------------------------------------------------------------------------------
1 | import type { ESLint } from "eslint";
2 |
3 | declare const eslintPluginSimpleImportSort: ESLint.Plugin;
4 |
5 | export = eslintPluginSimpleImportSort;
6 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const importsRule = require("./imports");
4 | const exportsRule = require("./exports");
5 |
6 | module.exports = {
7 | meta: {
8 | name: "eslint-plugin-simple-import-sort",
9 | version: "%VERSION%",
10 | },
11 | rules: {
12 | imports: importsRule,
13 | exports: exportsRule,
14 | },
15 | };
16 |
--------------------------------------------------------------------------------
/src/shared.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | // A “chunk” is a sequence of statements of a certain type with only comments
4 | // and whitespace between.
5 | function extractChunks(parentNode, isPartOfChunk) {
6 | const chunks = [];
7 | let chunk = [];
8 | let lastNode = undefined;
9 |
10 | for (const node of parentNode.body) {
11 | const result = isPartOfChunk(node, lastNode);
12 | switch (result) {
13 | case "PartOfChunk":
14 | chunk.push(node);
15 | break;
16 |
17 | case "PartOfNewChunk":
18 | if (chunk.length > 0) {
19 | chunks.push(chunk);
20 | }
21 | chunk = [node];
22 | break;
23 |
24 | case "NotPartOfChunk":
25 | if (chunk.length > 0) {
26 | chunks.push(chunk);
27 | chunk = [];
28 | }
29 | break;
30 |
31 | /* v8 ignore start */
32 | default:
33 | throw new Error(`Unknown chunk result: ${result}`);
34 | /* v8 ignore stop */
35 | }
36 |
37 | lastNode = node;
38 | }
39 |
40 | if (chunk.length > 0) {
41 | chunks.push(chunk);
42 | }
43 |
44 | return chunks;
45 | }
46 |
47 | function maybeReportSorting(context, sorted, start, end) {
48 | const sourceCode = getSourceCode(context);
49 | const original = sourceCode.getText().slice(start, end);
50 | if (original !== sorted) {
51 | context.report({
52 | messageId: "sort",
53 | loc: {
54 | start: sourceCode.getLocFromIndex(start),
55 | end: sourceCode.getLocFromIndex(end),
56 | },
57 | fix: (fixer) => fixer.replaceTextRange([start, end], sorted),
58 | });
59 | }
60 | }
61 |
62 | function printSortedItems(sortedItems, originalItems, sourceCode) {
63 | const newline = guessNewline(sourceCode);
64 |
65 | const sorted = sortedItems
66 | .map((groups) =>
67 | groups
68 | .map((groupItems) => groupItems.map((item) => item.code).join(newline))
69 | .join(newline),
70 | )
71 | .join(newline + newline);
72 |
73 | // Edge case: If the last import/export (after sorting) ends with a line
74 | // comment and there’s code (or a multiline block comment) on the same line,
75 | // add a newline so we don’t accidentally comment stuff out.
76 | const flattened = flatMap(sortedItems, (groups) => [].concat(...groups));
77 | const lastSortedItem = flattened[flattened.length - 1];
78 | const lastOriginalItem = originalItems[originalItems.length - 1];
79 | const nextToken = lastSortedItem.needsNewline
80 | ? sourceCode.getTokenAfter(lastOriginalItem.node, {
81 | includeComments: true,
82 | filter: (token) =>
83 | !isLineComment(token) &&
84 | !(
85 | isBlockComment(token) &&
86 | token.loc.end.line === lastOriginalItem.node.loc.end.line
87 | ),
88 | })
89 | : undefined;
90 | const maybeNewline =
91 | nextToken != null &&
92 | nextToken.loc.start.line === lastOriginalItem.node.loc.end.line
93 | ? newline
94 | : "";
95 |
96 | return sorted + maybeNewline;
97 | }
98 |
99 | // Wrap the import/export nodes in `passedChunk` in objects with more data about
100 | // the import/export. Most importantly there’s a `code` property that contains
101 | // the node as a string, with comments (if any). Finding the corresponding
102 | // comments is the hard part.
103 | function getImportExportItems(
104 | passedChunk,
105 | sourceCode,
106 | isSideEffectImport,
107 | getSpecifiers,
108 | ) {
109 | const chunk = handleLastSemicolon(passedChunk, sourceCode);
110 | return chunk.map((node, nodeIndex) => {
111 | const lastLine =
112 | nodeIndex === 0
113 | ? node.loc.start.line - 1
114 | : chunk[nodeIndex - 1].loc.end.line;
115 |
116 | // Get all comments before the import/export, except:
117 | //
118 | // - Comments on another line for the first import/export.
119 | // - Comments that belong to the previous import/export (if any) – that is,
120 | // comments that are on the same line as the previous import/export. But
121 | // multiline block comments always belong to this import/export, not the
122 | // previous.
123 | const commentsBefore = sourceCode
124 | .getCommentsBefore(node)
125 | .filter(
126 | (comment) =>
127 | comment.loc.start.line <= node.loc.start.line &&
128 | comment.loc.end.line > lastLine &&
129 | (nodeIndex > 0 || comment.loc.start.line > lastLine),
130 | );
131 |
132 | // Get all comments after the import/export that are on the same line.
133 | // Multiline block comments belong to the _next_ import/export (or the
134 | // following code in case of the last import/export).
135 | const commentsAfter = sourceCode
136 | .getCommentsAfter(node)
137 | .filter((comment) => comment.loc.end.line === node.loc.end.line);
138 |
139 | const before = printCommentsBefore(node, commentsBefore, sourceCode);
140 | const after = printCommentsAfter(node, commentsAfter, sourceCode);
141 |
142 | // Print the indentation before the import/export or its first comment, if
143 | // any, to support indentation in `
596 |
597 | `;
598 |
--------------------------------------------------------------------------------
/test/examples.test.js:
--------------------------------------------------------------------------------
1 | import { spawnSync } from "child_process";
2 | import { readFileSync } from "fs";
3 | import { basename } from "path";
4 | import { format } from "prettier";
5 | import { describe, expect, test } from "vitest";
6 |
7 | // Make snapshots easier to read.
8 | // Before: `"\\"string\\""`
9 | // After: `"string"`
10 | expect.addSnapshotSerializer({
11 | test: (value) => typeof value === "string",
12 | print: (value) => value,
13 | });
14 |
15 | describe("examples", () => {
16 | const result = spawnSync("npm", ["run", "examples", "--silent"], {
17 | encoding: "utf8",
18 | shell: true, // For Windows.
19 | });
20 |
21 | const output = JSON.parse(result.stdout);
22 |
23 | for (const item of output) {
24 | const name = basename(item.filePath);
25 | if (!(name.startsWith(".") || name === "README.md")) {
26 | test(`${name}`, async () => {
27 | expect(item).toMatchObject({
28 | messages: [],
29 | errorCount: 0,
30 | warningCount: 0,
31 | fixableErrorCount: 0,
32 | fixableWarningCount: 0,
33 | });
34 | const code = name.includes("prettier")
35 | ? await format(item.output || readFileSync(item.filePath, "utf8"), {
36 | parser: "babel-ts",
37 | })
38 | : item.output;
39 | expect(code).toMatchSnapshot();
40 | });
41 | }
42 | }
43 | });
44 |
--------------------------------------------------------------------------------
/test/exports.test.js:
--------------------------------------------------------------------------------
1 | import { RuleTester } from "eslint";
2 | import { describe, expect, test } from "vitest";
3 |
4 | import plugin from "../src/index.js";
5 | import { input, setup } from "./helpers.js";
6 |
7 | RuleTester.it = test;
8 | RuleTester.describe = describe;
9 |
10 | const expect2 = setup(expect);
11 |
12 | // eslint-disable-next-line no-shadow
13 | const baseTests = (expect) => ({
14 | valid: [
15 | // Simple cases.
16 | `export {a} from "a"`,
17 | `export {a,b} from "a"`,
18 | `export {} from "a"`,
19 | `export { } from "a"`,
20 | `export * as a from "a"`,
21 | `export var one = 1;`,
22 | `export let two = 2;`,
23 | `export const three = 3;`,
24 | `export function f() {}`,
25 | `export class C {}`,
26 | `export { a, b as c }; var a, b;`,
27 | `export default whatever;`,
28 |
29 | // Sorted alphabetically.
30 | input`
31 | |export {x1} from "a";
32 | |export {x2} from "b"
33 | `,
34 |
35 | // Opt-out.
36 | input`
37 | |// eslint-disable-next-line
38 | |export {x2} from "b"
39 | |export {x1} from "a";
40 | `,
41 |
42 | // Whitespace before comment at last specifier should stay.
43 | input`
44 | |export {
45 | | a, // a
46 | | b // b
47 | |} from "specifiers-comment-space"
48 | |export {
49 | | c, // c
50 | | d, // d
51 | |} from "specifiers-comment-space-2"
52 | `,
53 |
54 | // Accidental trailing spaces doesn’t produce a sorting error.
55 | input`
56 | |export {a} from "a"
57 | |export {b} from "b";
58 | |export {c} from "c"; /* comment */
59 | `,
60 |
61 | // Commenting out an export doesn’t produce a sorting error.
62 | input`
63 | |export {a} from "a"
64 | |// export {b} from "b";
65 | |export {c} from "c";
66 | `,
67 | ],
68 |
69 | invalid: [
70 | // Sorting alphabetically.
71 | {
72 | code: input`
73 | |export {x2} from "b"
74 | |export {x1} from "a";
75 | `,
76 | output: (actual) => {
77 | expect(actual).toMatchInlineSnapshot(`
78 | |export {x1} from "a";
79 | |export {x2} from "b"
80 | `);
81 | },
82 | errors: 1,
83 | },
84 |
85 | // Using comments for grouping.
86 | {
87 | code: input`
88 | |export * from "g"
89 | |export * from "f";
90 | |// Group 2
91 | |export * from "e"
92 | |export * from "d"
93 | |/* Group 3 */
94 | |
95 | |export * from "c"
96 | |
97 | |
98 | |export * from "b"
99 | |
100 | |
101 | | /* Group 4
102 | | */
103 | |
104 | | export * from "a"
105 | `,
106 | output: (actual) => {
107 | expect(actual).toMatchInlineSnapshot(`
108 | |export * from "f";
109 | |export * from "g"
110 | |// Group 2
111 | |export * from "d"
112 | |export * from "e"
113 | |/* Group 3 */
114 | |
115 | |export * from "b"
116 | |export * from "c"
117 | |
118 | |
119 | | /* Group 4
120 | | */
121 | |
122 | | export * from "a"
123 | `);
124 | },
125 | errors: 3,
126 | },
127 |
128 | // Sorting specifiers.
129 | // In `a as c`, the “c” is used since that’s the “stable” name, while the
130 | // internal `a` name can change at any time without affecting the module
131 | // interface. In other words, this is “backwards” compared to
132 | // `import {a as c} from "x"`.
133 | {
134 | code: `export { d, a as c, a as b2, b, a } from "specifiers"`,
135 | output: (actual) => {
136 | expect(actual).toMatchInlineSnapshot(
137 | `export { a,b, a as b2, a as c, d } from "specifiers"`,
138 | );
139 | },
140 | errors: 1,
141 | },
142 | {
143 | code: `export { d, a as c, a as b2, b, a, }; var d, a, b;`,
144 | output: (actual) => {
145 | expect(actual).toMatchInlineSnapshot(
146 | `export { a,b, a as b2, a as c, d, }; var d, a, b;`,
147 | );
148 | },
149 | errors: 1,
150 | },
151 |
152 | // Comments on the same line as something else don’t count for grouping.
153 | {
154 | code: input`
155 | |export * from "g"
156 | |/* f1 */export * from "f"; // f2
157 | |export * from "e" /* d
158 | | */
159 | |export * from "d"
160 | |export * from "c" /*
161 | | b */ export * from "b"
162 | | /* a
163 | | */ export * from "a"
164 | `,
165 | output: (actual) => {
166 | expect(actual).toMatchInlineSnapshot(`
167 | | /* a
168 | | */ export * from "a"
169 | |/*
170 | | b */ export * from "b"
171 | |export * from "c"
172 | |/* d
173 | | */
174 | |export * from "d"
175 | |export * from "e"
176 | |/* f1 */export * from "f"; // f2
177 | |export * from "g"
178 | `);
179 | },
180 | errors: 1,
181 | },
182 |
183 | // Sorting with lots of comments.
184 | {
185 | code: input`
186 | |/*1*//*2*/export/*3*/*/*4*/as/*as*/foo/*foo1*//*foo2*/from/*6*/"specifiers-lots-of-comments"/*7*//*8*/
187 | |export { // start
188 | | /* c1 */ c /* c2 */, // c3
189 | | // b1
190 | |
191 | | b as /* b2 */ renamed
192 | | , /* b3 */ /* a1
193 | | */ a /* not-a
194 | | */ // comment at end
195 | |} from "specifiers-lots-of-comments-multiline";
196 | |export {
197 | | e,
198 | | d, /* d */ /* not-d
199 | | */ // comment at end after trailing comma
200 | |};
201 | |var e, d;
202 | `,
203 | output: (actual) => {
204 | expect(actual).toMatchInlineSnapshot(`
205 | |/*1*//*2*/export/*3*/*/*4*/as/*as*/foo/*foo1*//*foo2*/from/*6*/"specifiers-lots-of-comments"/*7*//*8*/
206 | |export { // start
207 | |/* a1
208 | | */ a,
209 | | /* c1 */ c /* c2 */, // c3
210 | | // b1
211 | | b as /* b2 */ renamed
212 | | /* b3 */ /* not-a
213 | | */ // comment at end
214 | |} from "specifiers-lots-of-comments-multiline";
215 | |export {
216 | | d, /* d */ e,
217 | |/* not-d
218 | | */ // comment at end after trailing comma
219 | |};
220 | |var e, d;
221 | `);
222 | },
223 | errors: 2,
224 | },
225 |
226 | // Collapse blank lines inside export statements.
227 | {
228 | code: input`
229 | |export
230 | |
231 | |// export
232 | |
233 | |/* default */
234 | |
235 | |
236 | |
237 | |// default
238 | |
239 | | {
240 | |
241 | | // c
242 | |
243 | | c /*c*/,
244 | |
245 | | /* b
246 | | */
247 | |
248 | | b // b
249 | | ,
250 | |
251 | | // a1
252 | |
253 | | // a2
254 | |
255 | | a
256 | |
257 | | // a3
258 | |
259 | | as
260 | |
261 | | // a4
262 | |
263 | | d
264 | |
265 | | // a5
266 | |
267 | | , // a6
268 | |
269 | | // last
270 | |
271 | |}
272 | |
273 | |// from1
274 | |
275 | |from
276 | |
277 | |// from2
278 | |
279 | |"c"
280 | |
281 | |// final
282 | |
283 | |;
284 | `,
285 | output: (actual) => {
286 | expect(actual).toMatchInlineSnapshot(`
287 | |export
288 | |// export
289 | |/* default */
290 | |// default
291 | | {
292 | | /* b
293 | | */
294 | | b // b
295 | | ,
296 | | // c
297 | | c /*c*/,
298 | | // a1
299 | | // a2
300 | | a
301 | | // a3
302 | | as
303 | | // a4
304 | | d
305 | | // a5
306 | | , // a6
307 | | // last
308 | |}
309 | |// from1
310 | |from
311 | |// from2
312 | |"c"
313 | |// final
314 | |;
315 | `);
316 | },
317 | errors: 1,
318 | },
319 |
320 | // Collapse blank lines inside empty specifier list.
321 | {
322 | code: input`
323 | |export {
324 | |
325 | | } from "specifiers-empty"
326 | `,
327 | output: (actual) => {
328 | expect(actual).toMatchInlineSnapshot(`
329 | |export {
330 | | } from "specifiers-empty"
331 | `);
332 | },
333 | errors: 1,
334 | },
335 |
336 | // Do not collapse empty lines inside export code.
337 | {
338 | code: input`
339 | |export const options = {
340 | |
341 | | a: 1,
342 | |
343 | | b: 2
344 | | }, a = 1
345 | |export {options as options2, a as a2}
346 | `,
347 | output: (actual) => {
348 | expect(actual).toMatchInlineSnapshot(`
349 | |export const options = {
350 | |
351 | | a: 1,
352 | |
353 | | b: 2
354 | | }, a = 1
355 | |export {a as a2,options as options2}
356 | `);
357 | },
358 | errors: 1,
359 | },
360 |
361 | // Preserve indentation (for `