diff --git a/.changeset/tidy-crews-sit.md b/.changeset/tidy-crews-sit.md new file mode 100644 index 000000000..a32fbbf0f --- /dev/null +++ b/.changeset/tidy-crews-sit.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-import-x": minor +--- + +Add new rule option `checkTypedImports` for `extensions`, backports https://github.com/import-js/eslint-plugin-import/pull/2817 diff --git a/docs/rules/extensions.md b/docs/rules/extensions.md index 2242a57c6..097006f88 100644 --- a/docs/rules/extensions.md +++ b/docs/rules/extensions.md @@ -56,6 +56,8 @@ For example, `["error", "never", { "svg": "always" }]` would require that all ex In that case, if you still want to specify extensions, you can do so inside the **pattern** property. Default value of `ignorePackages` is `false`. +By default, `import type` and `export type` style imports/exports are ignored. If you want to check them as well, you can set the `checkTypeImports` option to `true`. + ### Exception When disallowing the use of certain extensions this rule makes an exception and allows the use of extension when the file would not be resolvable without extension. @@ -104,6 +106,13 @@ import express from 'express/index' import * as path from 'path' ``` +The following patterns are considered problems when the configuration is set to "never" and the option "checkTypeImports" is set to `true`: + +```js +import type { Foo } from './foo.ts'; +export type { Foo } from './foo.ts'; +``` + The following patterns are considered problems when configuration set to "always": ```js @@ -166,6 +175,13 @@ import express from 'express' import foo from '@/foo' ``` +The following patterns are considered problems when the configuration is set to "always" and the option "checkTypeImports" is set to `true`: + +```js +import type { Foo } from './foo'; +export type { Foo } from './foo'; +``` + ## When Not To Use It If you are not concerned about a consistent usage of file extension. diff --git a/src/rules/extensions.ts b/src/rules/extensions.ts index a1450d067..4a3d7ad1c 100644 --- a/src/rules/extensions.ts +++ b/src/rules/extensions.ts @@ -27,6 +27,9 @@ const properties = { ignorePackages: { type: 'boolean' as const, }, + checkTypeImports: { + type: 'boolean' as const, + }, }, } @@ -38,6 +41,7 @@ type NormalizedOptions = { defaultConfig?: DefaultConfig pattern?: Record ignorePackages?: boolean + checkTypeImports?: boolean } type Options = DefaultConfig | NormalizedOptions @@ -47,6 +51,7 @@ function buildProperties(context: RuleContext) { defaultConfig: 'never', pattern: {}, ignorePackages: false, + checkTypeImports: false, } for (const obj of context.options) { @@ -57,7 +62,11 @@ function buildProperties(context: RuleContext) { } // If this is not the new structure, transfer all props to result.pattern - if (obj.pattern === undefined && obj.ignorePackages === undefined) { + if ( + obj.pattern === undefined && + obj.ignorePackages === undefined && + obj.checkTypeImports === undefined + ) { Object.assign(result.pattern, obj) continue } @@ -71,6 +80,10 @@ function buildProperties(context: RuleContext) { if (obj.ignorePackages !== undefined) { result.ignorePackages = obj.ignorePackages } + + if (obj.checkTypeImports !== undefined) { + result.checkTypeImports = obj.checkTypeImports + } } if (result.defaultConfig === 'ignorePackages') { @@ -213,8 +226,9 @@ export = createRule({ if (!extension || !importPath.endsWith(`.${extension}`)) { // ignore type-only imports and exports if ( - ('importKind' in node && node.importKind === 'type') || - ('exportKind' in node && node.exportKind === 'type') + !props.checkTypeImports && + (('importKind' in node && node.importKind === 'type') || + ('exportKind' in node && node.exportKind === 'type')) ) { return } diff --git a/test/rules/extensions.spec.ts b/test/rules/extensions.spec.ts index 871c1c5a1..81b172c02 100644 --- a/test/rules/extensions.spec.ts +++ b/test/rules/extensions.spec.ts @@ -5,6 +5,15 @@ import { test, testFilePath } from '../utils' import rule from 'eslint-plugin-import-x/rules/extensions' const ruleTester = new TSESLintRuleTester() +const ruleTesterWithTypeScriptImports = new TSESLintRuleTester({ + settings: { + 'import/resolver': { + typescript: { + alwaysTryTypes: true, + }, + }, + }, +}) ruleTester.run('extensions', rule, { valid: [ @@ -694,6 +703,58 @@ describe('TypeScript', () => { { ts: 'never', tsx: 'never', js: 'never', jsx: 'never' }, ], }), + test({ + code: 'import type T from "./typescript-declare";', + errors: ['Missing file extension for "./typescript-declare"'], + options: [ + 'always', + { + ts: 'never', + tsx: 'never', + js: 'never', + jsx: 'never', + checkTypeImports: true, + }, + ], + }), + test({ + code: 'export type { MyType } from "./typescript-declare";', + errors: ['Missing file extension for "./typescript-declare"'], + options: [ + 'always', + { + ts: 'never', + tsx: 'never', + js: 'never', + jsx: 'never', + checkTypeImports: true, + }, + ], + }), + ], + }) + ruleTesterWithTypeScriptImports.run('extensions', rule, { + valid: [ + test({ + code: 'import type { MyType } from "./typescript-declare.ts";', + options: ['always', { checkTypeImports: true }], + }), + test({ + code: 'export type { MyType } from "./typescript-declare.ts";', + options: ['always', { checkTypeImports: true }], + }), + ], + invalid: [ + test({ + code: 'import type { MyType } from "./typescript-declare";', + errors: ['Missing file extension for "./typescript-declare"'], + options: ['always', { checkTypeImports: true }], + }), + test({ + code: 'export type { MyType } from "./typescript-declare";', + errors: ['Missing file extension for "./typescript-declare"'], + options: ['always', { checkTypeImports: true }], + }), ], }) })