diff --git a/.changeset/itchy-tires-mate.md b/.changeset/itchy-tires-mate.md new file mode 100644 index 00000000..1916c396 --- /dev/null +++ b/.changeset/itchy-tires-mate.md @@ -0,0 +1,5 @@ +--- +'@commencis/eslint-config': major +--- + +feat: enable consistent type imports diff --git a/packages/eslint-config/src/plugins/importSortPlugin.ts b/packages/eslint-config/src/plugins/importSortPlugin.ts index 624bd54a..a0328bfd 100644 --- a/packages/eslint-config/src/plugins/importSortPlugin.ts +++ b/packages/eslint-config/src/plugins/importSortPlugin.ts @@ -1,8 +1,7 @@ import simpleImportSortPlugin from 'eslint-plugin-simple-import-sort'; -import { FlatConfig } from '@/types'; - import { importSortRules } from '@/rules'; +import type { FlatConfig } from '@/types'; export const importSortPluginConfig: FlatConfig = { name: 'commencis/plugin:simple-import-sort', diff --git a/packages/eslint-config/src/rules/importSortRules.ts b/packages/eslint-config/src/rules/importSortRules.ts index 3c589312..233a5d0a 100644 --- a/packages/eslint-config/src/rules/importSortRules.ts +++ b/packages/eslint-config/src/rules/importSortRules.ts @@ -1,47 +1,106 @@ -import { Linter } from '@typescript-eslint/utils/ts-eslint'; +import type { Linter } from '@typescript-eslint/utils/ts-eslint'; -export const importSortRules: Linter.RulesRecord = { - 'simple-import-sort/imports': [ - 'error', - { - groups: [ - // Side effects - ['^\\u0000'], +/** + * Turn a normal "from" regex into a “type-only” variant that matches + * the same sources *when* they’re type-only imports. + * simple-import-sort appends \u0000 to type-only import sources. + */ +function asTypeOnly(pattern: string): string { + const base = pattern.endsWith('$') ? pattern.slice(0, -1) : pattern; + return `${base}\\u0000$`; +} + +// Put type-only variants first inside the same group. +function withTypeFirst(group: string[]): string[] { + return group.flatMap((pattern) => [asTypeOnly(pattern), pattern]); +} + +// Helpers to generate exact and subpath regex for @/ aliases +function exact(p: string): string { + return `^@/${p}$`; +} + +function subpath(p: string): string { + return `^@/${p}/.+$`; +} + +// Expand an array of folder names into [exact, subpath] for each +function expandFolders(folders: string[]): string[] { + return folders.flatMap((name) => [exact(name), subpath(name)]); +} - // Main frameworks & libraries - [ - '^(react(-native|-dom)?(/.*)?)$', - '^next', - '^vue', - '^nuxt', - '^@angular(/.*|$)', - '^expo', - '^node', - ], +const GROUPS: Record = { + // Side effects (simple-import-sort prefixes side-effects with \u0000 at the start) + SIDE_EFFECTS: ['^\\u0000'], - // External packages - ['^@commencis', '^@?\\w'], + // Main frameworks & libraries + FRAMEWORKS: [ + '^(react(-native|-dom)?(/.*)?)$', + '^next', + '^vue', + '^nuxt', + '^@angular(/.*|$)', + '^expo', + '^node', + ], - // Internal common directories - ['^@?/?(config|types|interfaces|constants|helpers|utils|lib)(/.*|$)'], + // External packages + EXTERNAL: ['^@commencis', '^@?\\w'], - // Internal directories - ['^@/'], + // Internal common directories + INTERNAL_COMMON: expandFolders([ + 'config', + 'types', + 'interfaces', + 'constants', + 'helpers', + 'utils', + 'lib', + ]), - // Components - ['((.*)/)?(providers|layouts|pages|modules|features|components)/?'], + // Component directories + COMPONENTS: expandFolders([ + 'providers', + 'layouts', + 'pages', + 'modules', + 'features', + 'components', + ]), - // Relative parent imports: '../' comes last - ['^\\.\\.(?!/?$)', '^\\.\\./?$'], + // Internal root alias (catch-all leftover @/ imports) + INTERNAL_ROOT: [exact(''), subpath('')], - // Relative imports: './' comes last - ['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'], + // Relative parent imports then same-dir relatives + RELATIVE_PARENT: ['^\\.\\.(?!/?$)', '^\\.\\./?$'], + RELATIVE_SAME: ['^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'], - // Styles - ['^.+\\.(s?css|(style(s)?)\\..+)$'], + // Styles + STYLES: ['^.+\\.(s?css|(style(s)?)\\..+)$'], - // Static assets - ['(asset(s?)|public|static|images)(/.*|$)', '^.+\\.svg$', '^.+\\.png$'], + // Assets + ASSETS: [ + '(asset(s?)|public|static|images)(/.*|$)', + '^.+\\.svg$', + '^.+\\.png$', + ], +}; + +export const importSortRules: Linter.RulesRecord = { + 'simple-import-sort/imports': [ + 'error', + { + groups: [ + GROUPS.SIDE_EFFECTS, + withTypeFirst(GROUPS.FRAMEWORKS), + withTypeFirst(GROUPS.EXTERNAL), + withTypeFirst(GROUPS.INTERNAL_COMMON), + withTypeFirst(GROUPS.COMPONENTS), + withTypeFirst(GROUPS.INTERNAL_ROOT), + withTypeFirst(GROUPS.RELATIVE_PARENT), + withTypeFirst(GROUPS.RELATIVE_SAME), + GROUPS.STYLES, + GROUPS.ASSETS, ], }, ], diff --git a/packages/eslint-config/src/rules/typescriptRules.ts b/packages/eslint-config/src/rules/typescriptRules.ts index 2c6e14de..3fb7a0e5 100644 --- a/packages/eslint-config/src/rules/typescriptRules.ts +++ b/packages/eslint-config/src/rules/typescriptRules.ts @@ -1,10 +1,11 @@ -import { Linter } from '@typescript-eslint/utils/ts-eslint'; +import type { Linter } from '@typescript-eslint/utils/ts-eslint'; export const typescriptRules: Linter.RulesRecord = { '@typescript-eslint/consistent-type-definitions': 'off', '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/array-type': 'off', + '@typescript-eslint/consistent-type-imports': 'error', '@typescript-eslint/explicit-function-return-type': 'error', '@typescript-eslint/no-unused-vars': [ 'error',