diff --git a/docs/rules/sort-imports.md b/docs/rules/sort-imports.md
index 11637de..0d90cc1 100644
--- a/docs/rules/sort-imports.md
+++ b/docs/rules/sort-imports.md
@@ -196,6 +196,28 @@ If you use [one of the configs](/configs/) exported by this plugin, you get the
}
```
+### custom-groups
+
+(default: `{ value: {}, type: {} }`)
+
+You can define your own groups for importing values or types. The [minimatch](https://github.com/isaacs/minimatch) library is used for pattern matching.
+
+Example:
+
+```
+{
+ "custom-groups": {
+ "value": {
+ "react": ["react", "react-*"],
+ "lodash": "lodash"
+ },
+ "type": {
+ "react": ["react", "react-*"]
+ }
+ }
+}
+```
+
### internal-pattern
(default: `['~/**']`)
@@ -234,6 +256,8 @@ If your project is written in TypeScript, you can read `tsconfig.json` and use `
"order": "asc",
"groups": [
"type",
+ "react",
+ "nanostores",
["builtin", "external"],
"internal-type",
"internal",
@@ -244,6 +268,15 @@ If your project is written in TypeScript, you can read `tsconfig.json` and use `
"object",
"unknown"
],
+ "custom-groups": {
+ "value": {
+ "react": ["react", "react-*"],
+ "nanostores": "@nanostores/**"
+ },
+ "type": {
+ "react": "react"
+ }
+ },
"newlines-between": "always",
"internal-pattern": [
"@/components/**",
@@ -275,6 +308,8 @@ export default [
order: 'asc',
groups: [
'type',
+ 'react',
+ 'nanostores',
['builtin', 'external'],
'internal-type',
'internal',
@@ -285,6 +320,15 @@ export default [
'object',
'unknown',
],
+ 'custom-groups': {
+ value: {
+ react: ['react', 'react-*'],
+ nanostores: '@nanostores/**',
+ },
+ type: {
+ react: 'react'
+ }
+ },
'newlines-between': 'always',
'internal-pattern': [
'@/components/**',
diff --git a/index.ts b/index.ts
index 5699dc3..619deb3 100644
--- a/index.ts
+++ b/index.ts
@@ -44,6 +44,10 @@ let createConfigWithOptions = (options: {
'object',
'unknown',
],
+ 'custom-groups': {
+ value: {},
+ type: {},
+ },
'newlines-between': 'always',
'internal-pattern': ['~/**'],
'read-tsconfig': false,
diff --git a/rules/sort-imports.ts b/rules/sort-imports.ts
index ce29782..2c8aa21 100644
--- a/rules/sort-imports.ts
+++ b/rules/sort-imports.ts
@@ -29,30 +29,35 @@ export enum NewlinesBetweenValue {
'never' = 'never',
}
-type Group =
- | 'internal-type'
+type Group =
| 'external-type'
- | 'sibling-type'
+ | 'internal-type'
| 'builtin-type'
- | 'side-effect'
+ | 'sibling-type'
| 'parent-type'
+ | 'side-effect'
| 'index-type'
- | 'external'
| 'internal'
- | 'builtin'
- | 'unknown'
+ | 'external'
+ | T[number]
| 'sibling'
- | 'object'
+ | 'unknown'
+ | 'builtin'
| 'parent'
- | 'style'
+ | 'object'
| 'index'
+ | 'style'
| 'type'
-type Options = [
+type Options = [
Partial<{
+ 'custom-groups': {
+ value?: { [key in T[number]]: string[] | string }
+ type?: { [key in T[number]]: string[] | string }
+ }
'newlines-between': NewlinesBetweenValue
+ groups: (Group[] | Group)[]
'internal-pattern': string[]
- groups: (Group[] | Group)[]
'read-tsconfig': boolean
'ignore-case': boolean
order: SortOrder
@@ -66,9 +71,11 @@ type ModuleDeclaration =
| TSESTree.TSImportEqualsDeclaration
| TSESTree.ImportDeclaration
-type SortingNodeWithGroup = SortingNode & { group: Group }
+type SortingNodeWithGroup = SortingNode & {
+ group: Group
+}
-export default createEslintRule({
+export default createEslintRule, MESSAGE_ID>({
name: RULE_NAME,
meta: {
type: 'suggestion',
@@ -81,6 +88,18 @@ export default createEslintRule({
{
type: 'object',
properties: {
+ 'custom-groups': {
+ type: 'object',
+ properties: {
+ type: {
+ type: 'object',
+ },
+ value: {
+ type: 'object',
+ },
+ },
+ additionalProperties: false,
+ },
type: {
enum: [
SortType.alphabetical,
@@ -138,6 +157,7 @@ export default createEslintRule({
create: context => {
let options = complete(context.options.at(0), {
'newlines-between': NewlinesBetweenValue.always,
+ 'custom-groups': { type: {}, value: {} },
'internal-pattern': ['~/**'],
type: SortType.alphabetical,
'read-tsconfig': false,
@@ -160,10 +180,10 @@ export default createEslintRule({
let source = context.getSourceCode()
- let nodes: SortingNodeWithGroup[] = []
+ let nodes: SortingNodeWithGroup[] = []
- let computeGroup = (node: ModuleDeclaration): Group => {
- let group: undefined | Group
+ let computeGroup = (node: ModuleDeclaration): Group => {
+ let group: Group | undefined
let isStyle = (value: string) =>
['.less', '.scss', '.sass', '.styl', '.pcss', '.css', '.sss'].some(
@@ -185,7 +205,7 @@ export default createEslintRule({
let isSibling = (value: string) => value.indexOf('./') === 0
- let defineGroup = (nodeGroup: Group) => {
+ let defineGroup = (nodeGroup: Group) => {
if (!group && options.groups.flat().includes(nodeGroup)) {
group = nodeGroup
}
@@ -198,8 +218,30 @@ export default createEslintRule({
)) ||
tsPaths.some(pattern => minimatch(nodeElement.source.value, pattern))
+ let determineCustomGroup = (
+ groupType: 'value' | 'type',
+ value: string,
+ ) => {
+ Object.entries(options['custom-groups'][groupType] ?? {}).forEach(
+ ([key, pattern]) => {
+ if (
+ Array.isArray(pattern) &&
+ pattern.some(patternValue => minimatch(value, patternValue))
+ ) {
+ defineGroup(key)
+ }
+
+ if (typeof pattern === 'string' && minimatch(value, pattern)) {
+ defineGroup(key)
+ }
+ },
+ )
+ }
+
if (node.importKind === 'type') {
if (node.type === AST_NODE_TYPES.ImportDeclaration) {
+ determineCustomGroup('type', node.source.value)
+
if (isCoreModule(node.source.value)) {
defineGroup('builtin-type')
}
@@ -225,6 +267,8 @@ export default createEslintRule({
}
if (!group && node.type === AST_NODE_TYPES.ImportDeclaration) {
+ determineCustomGroup('value', node.source.value)
+
if (isCoreModule(node.source.value)) {
defineGroup('builtin')
}
@@ -288,7 +332,7 @@ export default createEslintRule({
TSImportEqualsDeclaration: registerNode,
ImportDeclaration: registerNode,
'Program:exit': () => {
- let getGroupNumber = (node: SortingNodeWithGroup): number => {
+ let getGroupNumber = (node: SortingNodeWithGroup): number => {
for (let i = 0, max = options.groups.length; i < max; i++) {
let currentGroup = options.groups[i]
@@ -328,14 +372,14 @@ export default createEslintRule({
let fix = (
fixer: TSESLint.RuleFixer,
- nodesToFix: SortingNodeWithGroup[],
+ nodesToFix: SortingNodeWithGroup[],
): TSESLint.RuleFix[] => {
let fixes: TSESLint.RuleFix[] = []
let grouped = nodesToFix.reduce(
(
accumulator: {
- [key: string]: SortingNodeWithGroup[]
+ [key: string]: SortingNodeWithGroup[]
},
node,
) => {
@@ -358,10 +402,10 @@ export default createEslintRule({
let formatted = Object.keys(grouped)
.sort()
.reduce(
- (accumulator: SortingNodeWithGroup[], group: string) => [
- ...accumulator,
- ...grouped[group],
- ],
+ (
+ accumulator: SortingNodeWithGroup[],
+ group: string,
+ ) => [...accumulator, ...grouped[group]],
[],
)
@@ -437,8 +481,8 @@ export default createEslintRule({
let splittedNodes = nodes.reduce(
(
- accumulator: SortingNodeWithGroup[][],
- node: SortingNodeWithGroup,
+ accumulator: SortingNodeWithGroup[][],
+ node: SortingNodeWithGroup,
) => {
let lastNode = accumulator.at(-1)?.at(-1)
diff --git a/test/sort-imports.test.ts b/test/sort-imports.test.ts
index af61299..3aa57cd 100644
--- a/test/sort-imports.test.ts
+++ b/test/sort-imports.test.ts
@@ -941,6 +941,122 @@ describe(RULE_NAME, () => {
],
})
})
+
+ it(`${RULE_NAME}(${type}): allows to define custom groups`, () => {
+ ruleTester.run(RULE_NAME, rule, {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ import type { Titan } from 'titans'
+
+ import Armin from '@scout-regiment/armin'
+ import Mikasa from '@scout-regiment/mikasa'
+ import Reiner from '@titans/armored-titan'
+ import Eren from '@titans/attack-titan'
+ import Zeke from '@titans/beast-titan'
+ import { KennyAckermann } from 'military-police'
+ `,
+ output: dedent`
+ import Reiner from '@titans/armored-titan'
+ import Eren from '@titans/attack-titan'
+ import Zeke from '@titans/beast-titan'
+ import type { Titan } from 'titans'
+
+ import Armin from '@scout-regiment/armin'
+ import Mikasa from '@scout-regiment/mikasa'
+
+ import { KennyAckermann } from 'military-police'
+ `,
+ options: [
+ {
+ type: SortType.alphabetical,
+ order: SortOrder.asc,
+ 'ignore-case': true,
+ 'custom-groups': {
+ type: {
+ titans: ['titans', '@titans/**'],
+ },
+ value: {
+ titans: ['titans', '@titans/**'],
+ scouts: '@scout-regiment/**',
+ },
+ },
+ groups: [
+ 'type',
+ 'titans',
+ 'scouts',
+ ['builtin', 'external'],
+ 'internal-type',
+ 'internal',
+ ['parent-type', 'sibling-type', 'index-type'],
+ ['parent', 'sibling', 'index'],
+ 'object',
+ 'unknown',
+ ],
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedImportsOrder',
+ data: {
+ left: '@scout-regiment/mikasa',
+ right: '@titans/armored-titan',
+ },
+ },
+ {
+ messageId: 'missedSpacingBetweenImports',
+ data: {
+ left: '@titans/beast-titan',
+ right: 'military-police',
+ },
+ },
+ ],
+ },
+ ],
+ })
+ })
+
+ it(`${RULE_NAME}(${type}): allows to define value only custom groups`, () => {
+ ruleTester.run(RULE_NAME, rule, {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ import type { Child } from 'giovannis-island'
+ import { Kanta, Junpei } from 'giovannis-island'
+ `,
+ output: dedent`
+ import type { Child } from 'giovannis-island'
+
+ import { Kanta, Junpei } from 'giovannis-island'
+ `,
+ options: [
+ {
+ type: SortType.alphabetical,
+ order: SortOrder.asc,
+ 'ignore-case': true,
+ 'custom-groups': {
+ value: {
+ giovanni: ['giovannis-island'],
+ },
+ },
+ groups: ['type', 'giovanni'],
+ },
+ ],
+ errors: [
+ {
+ messageId: 'missedSpacingBetweenImports',
+ data: {
+ left: 'giovannis-island',
+ right: 'giovannis-island',
+ },
+ },
+ ],
+ },
+ ],
+ })
+ })
})
describe(`${RULE_NAME}: sorting by natural order`, () => {
@@ -1874,6 +1990,122 @@ describe(RULE_NAME, () => {
],
})
})
+
+ it(`${RULE_NAME}(${type}): allows to define custom groups`, () => {
+ ruleTester.run(RULE_NAME, rule, {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ import type { Titan } from 'titans'
+
+ import Armin from '@scout-regiment/armin'
+ import Mikasa from '@scout-regiment/mikasa'
+ import Reiner from '@titans/armored-titan'
+ import Eren from '@titans/attack-titan'
+ import Zeke from '@titans/beast-titan'
+ import { KennyAckermann } from 'military-police'
+ `,
+ output: dedent`
+ import Reiner from '@titans/armored-titan'
+ import Eren from '@titans/attack-titan'
+ import Zeke from '@titans/beast-titan'
+ import type { Titan } from 'titans'
+
+ import Armin from '@scout-regiment/armin'
+ import Mikasa from '@scout-regiment/mikasa'
+
+ import { KennyAckermann } from 'military-police'
+ `,
+ options: [
+ {
+ type: SortType.natural,
+ order: SortOrder.asc,
+ 'ignore-case': true,
+ 'custom-groups': {
+ type: {
+ titans: ['titans', '@titans/**'],
+ },
+ value: {
+ titans: ['titans', '@titans/**'],
+ scouts: '@scout-regiment/**',
+ },
+ },
+ groups: [
+ 'type',
+ 'titans',
+ 'scouts',
+ ['builtin', 'external'],
+ 'internal-type',
+ 'internal',
+ ['parent-type', 'sibling-type', 'index-type'],
+ ['parent', 'sibling', 'index'],
+ 'object',
+ 'unknown',
+ ],
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedImportsOrder',
+ data: {
+ left: '@scout-regiment/mikasa',
+ right: '@titans/armored-titan',
+ },
+ },
+ {
+ messageId: 'missedSpacingBetweenImports',
+ data: {
+ left: '@titans/beast-titan',
+ right: 'military-police',
+ },
+ },
+ ],
+ },
+ ],
+ })
+ })
+
+ it(`${RULE_NAME}(${type}): allows to define value only custom groups`, () => {
+ ruleTester.run(RULE_NAME, rule, {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ import type { Child } from 'giovannis-island'
+ import { Kanta, Junpei } from 'giovannis-island'
+ `,
+ output: dedent`
+ import type { Child } from 'giovannis-island'
+
+ import { Kanta, Junpei } from 'giovannis-island'
+ `,
+ options: [
+ {
+ type: SortType.natural,
+ order: SortOrder.asc,
+ 'ignore-case': true,
+ 'custom-groups': {
+ value: {
+ giovanni: ['giovannis-island'],
+ },
+ },
+ groups: ['type', 'giovanni'],
+ },
+ ],
+ errors: [
+ {
+ messageId: 'missedSpacingBetweenImports',
+ data: {
+ left: 'giovannis-island',
+ right: 'giovannis-island',
+ },
+ },
+ ],
+ },
+ ],
+ })
+ })
})
describe(`${RULE_NAME}: sorting by line length`, () => {
@@ -2849,6 +3081,129 @@ describe(RULE_NAME, () => {
],
})
})
+
+ it(`${RULE_NAME}(${type}): allows to define custom groups`, () => {
+ ruleTester.run(RULE_NAME, rule, {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ import type { Titan } from 'titans'
+
+ import Armin from '@scout-regiment/armin'
+ import Mikasa from '@scout-regiment/mikasa'
+ import Reiner from '@titans/armored-titan'
+ import Eren from '@titans/attack-titan'
+ import Zeke from '@titans/beast-titan'
+ import { KennyAckermann } from 'military-police'
+ `,
+ output: dedent`
+ import Reiner from '@titans/armored-titan'
+ import Eren from '@titans/attack-titan'
+ import Zeke from '@titans/beast-titan'
+ import type { Titan } from 'titans'
+
+ import Mikasa from '@scout-regiment/mikasa'
+ import Armin from '@scout-regiment/armin'
+
+ import { KennyAckermann } from 'military-police'
+ `,
+ options: [
+ {
+ type: SortType['line-length'],
+ order: SortOrder.desc,
+ 'ignore-case': true,
+ 'custom-groups': {
+ type: {
+ titans: ['titans', '@titans/**'],
+ },
+ value: {
+ titans: ['titans', '@titans/**'],
+ scouts: '@scout-regiment/**',
+ },
+ },
+ groups: [
+ 'type',
+ 'titans',
+ 'scouts',
+ ['builtin', 'external'],
+ 'internal-type',
+ 'internal',
+ ['parent-type', 'sibling-type', 'index-type'],
+ ['parent', 'sibling', 'index'],
+ 'object',
+ 'unknown',
+ ],
+ },
+ ],
+ errors: [
+ {
+ messageId: 'unexpectedImportsOrder',
+ data: {
+ left: '@scout-regiment/armin',
+ right: '@scout-regiment/mikasa',
+ },
+ },
+ {
+ messageId: 'unexpectedImportsOrder',
+ data: {
+ left: '@scout-regiment/mikasa',
+ right: '@titans/armored-titan',
+ },
+ },
+ {
+ messageId: 'missedSpacingBetweenImports',
+ data: {
+ left: '@titans/beast-titan',
+ right: 'military-police',
+ },
+ },
+ ],
+ },
+ ],
+ })
+ })
+
+ it(`${RULE_NAME}(${type}): allows to define value only custom groups`, () => {
+ ruleTester.run(RULE_NAME, rule, {
+ valid: [],
+ invalid: [
+ {
+ code: dedent`
+ import type { Child } from 'giovannis-island'
+ import { Kanta, Junpei } from 'giovannis-island'
+ `,
+ output: dedent`
+ import type { Child } from 'giovannis-island'
+
+ import { Kanta, Junpei } from 'giovannis-island'
+ `,
+ options: [
+ {
+ type: SortType['line-length'],
+ order: SortOrder.desc,
+ 'ignore-case': true,
+ 'custom-groups': {
+ value: {
+ giovanni: ['giovannis-island'],
+ },
+ },
+ groups: ['type', 'giovanni'],
+ },
+ ],
+ errors: [
+ {
+ messageId: 'missedSpacingBetweenImports',
+ data: {
+ left: 'giovannis-island',
+ right: 'giovannis-island',
+ },
+ },
+ ],
+ },
+ ],
+ })
+ })
})
describe(`${RULE_NAME}: misc`, () => {