diff --git a/README.md b/README.md index 974ca8b..9f93ed4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Prettier plugin sort imports +# Prettier plugin sort imports A prettier plugin to sort import declarations by provided Regular Expression order. @@ -10,6 +10,30 @@ Since then more critical features & fixes have been added. As a result, this rep [We welcome contributions!](./CONTRIBUTING.md) +**Table of Contents** + +- [Sample](#sample) + - [Input](#input) + - [Output](#output) +- [Install](#install) +- [Usage](#usage) + - [How does import sort work?](#how-does-import-sort-work) + - [Options](#options) + - [`importOrder`](#importorder) + - [`importOrderSeparation`](#importorderseparation) + - [`importOrderSortSpecifiers`](#importordersortspecifiers) + - [`importOrderGroupNamespaceSpecifiers`](#importordergroupnamespacespecifiers) + - [`importOrderCaseInsensitive`](#importordercaseinsensitive) + - [`importOrderMergeDuplicateImports`](#importordermergeduplicateimports) + - [`importOrderCombineTypeAndValueImports`](#importordercombinetypeandvalueimports) + - [`importOrderParserPlugins`](#importorderparserplugins) + - [`importOrderBuiltinModulesToTop`](#importorderbuiltinmodulestotop) + - [Prevent imports from being sorted](#prevent-imports-from-being-sorted) +- [FAQ / Troubleshooting](#faq--troubleshooting) +- [Compatibility](#compatibility) +- [Contribution](#contribution) +- [Disclaimer](#disclaimer) + ## Sample ### Input @@ -100,32 +124,59 @@ module.exports = { "importOrderCaseInsensitive": true, "importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"], "importOrderMergeDuplicateImports": true, + "importOrderCombineTypeAndValueImports": true, "importOrderSeparation": true, "importOrderSortSpecifiers": true, } ``` -_Note: all flags are off by default, so explore your options [below](#apis)_ +_Note: all flags are off by default, so explore your options [below](#options)_ -### APIs +### How does import sort work? -#### Prevent imports from being sorted +The plugin extracts the imports which are defined in `importOrder`. These imports are considered as _local imports_. +The imports which are not part of the `importOrder` is considered as _third party imports_. -This plugin supports standard prettier ignore comments. By default, side-effect imports (like -`import "core-js/stable";`) are not sorted, so in most cases things should just work. But if you ever need to, you can -prevent an import from getting sorted like this: +First, the plugin checks for +[side effect imports](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#import_a_module_for_its_side_effects_only), +such as `import 'mock-fs'`. These imports often modify the global scope or apply some patches to the current +environment, which may affect other imports. To preserve potential side effects, these kind of side effect imports are +classified as unsortable. They also behave as a barrier that other imports may not cross during the sort. So for +example, let's say you've got these imports: ```javascript -// prettier-ignore -import { goods } from "zealand"; -import { cars } from "austria"; +import E from 'e'; +import F from 'f'; +import D from 'd'; +import 'c'; +import B from 'b'; +import A from 'a'; ``` -This will keep the `zealand` import at the top instead of moving it below the `austria` import. Note that since only -entire import statements can be ignored, line comments (`// prettier-ignore`) are recommended over inline comments -(`/* prettier-ignore */`). +Then the first three imports are sorted and the last two imports are sorted, but all imports above `c` stay above `c` +and all imports below `c` stay below `c`, resulting in: -#### **`importOrder`** +```javascript +import D from 'd'; +import E from 'e'; +import F from 'f'; +import 'c'; +import A from 'a'; +import B from 'b'; +``` + +Additionally, any import statements lines that are preceded by a `// prettier-ignore` comment are also classified as +unsortable. This can be used for edge-cases, such as when you have a named import with side-effects. + +Next, the plugin sorts the _local imports_ and _third party imports_ using [natural sort algorithm](https://en.wikipedia.org/wiki/Natural_sort_order). + +In the end, the plugin returns final imports with _third party imports_ on top and _local imports_ at the end. + +The _third party imports_ position (it's top by default) can be overridden using the `` special word in the `importOrder`. + +### Options + +#### `importOrder` **type**: `Array` @@ -218,6 +269,28 @@ import ExampleView from './ExampleView'; When `true`, multiple import statements from the same module will be combined into a single import. +#### `importOrderCombineTypeAndValueImports` + +**type**: `boolean` + +**default value:** `false` + +A boolean value to control merging `import type` expressions into `import {…}`. + +```diff +- import type { C1 } from 'c'; +- import { C2 } from 'c'; ++ import { type C1, C2 } from "c"; + +- import { D1 } from 'd'; +- import type { D2 } from 'd'; ++ import { D1, type D2 } from "d"; + +- import type { A1 } from 'a'; +- import type { A2 } from 'a'; ++ import type { A1, A2 } from "a"; +``` + #### `importOrderParserPlugins` **type**: `Array` @@ -258,47 +331,21 @@ with options as a JSON string of the plugin array: A boolean value to enable sorting of [`node builtins`](https://nodejs.org/api/module.html#modulebuiltinmodules) to the top of all import groups. -### How does import sort work? - -The plugin extracts the imports which are defined in `importOrder`. These imports are considered as _local imports_. -The imports which are not part of the `importOrder` is considered as _third party imports_. - -First, the plugin checks for -[side effect imports](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#import_a_module_for_its_side_effects_only), -such as `import 'mock-fs'`. These imports often modify the global scope or apply some patches to the current -environment, which may affect other imports. To preserve potential side effects, these kind of side effect imports are -classified as unsortable. They also behave as a barrier that other imports may not cross during the sort. So for -example, let's say you've got these imports: - -```javascript -import E from 'e'; -import F from 'f'; -import D from 'd'; -import 'c'; -import B from 'b'; -import A from 'a'; -``` +### Prevent imports from being sorted -Then the first three imports are sorted and the last two imports are sorted, but all imports above `c` stay above `c` -and all imports below `c` stay below `c`, resulting in: +This plugin supports standard prettier ignore comments. By default, side-effect imports (like +`import "core-js/stable";`) are not sorted, so in most cases things should just work. But if you ever need to, you can +prevent an import from getting sorted like this: ```javascript -import D from 'd'; -import E from 'e'; -import F from 'f'; -import 'c'; -import A from 'a'; -import B from 'b'; +// prettier-ignore +import { goods } from "zealand"; +import { cars } from "austria"; ``` -Additionally, any import statements lines that are preceded by a `// prettier-ignore` comment are also classified as -unsortable. This can be used for edge-cases, such as when you have a named import with side-effects. - -Next, the plugin sorts the _local imports_ and _third party imports_ using [natural sort algorithm](https://en.wikipedia.org/wiki/Natural_sort_order). - -In the end, the plugin returns final imports with _third party imports_ on top and _local imports_ at the end. - -The _third party imports_ position (it's top by default) can be overridden using the `` special word in the `importOrder`. +This will keep the `zealand` import at the top instead of moving it below the `austria` import. Note that since only +entire import statements can be ignored, line comments (`// prettier-ignore`) are recommended over inline comments +(`/* prettier-ignore */`). ## FAQ / Troubleshooting diff --git a/src/index.ts b/src/index.ts index 8349218..86a4edd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -72,6 +72,13 @@ const options: Record< default: false, description: 'Should duplicate imports be merged?', }, + importOrderCombineTypeAndValueImports: { + type: 'boolean', + category: 'Global', + default: false, + description: + 'Should import-type expressions be merged into import-value expressions?', + }, }; module.exports = { diff --git a/src/preprocessor.ts b/src/preprocessor.ts index 7fb07c6..778239b 100644 --- a/src/preprocessor.ts +++ b/src/preprocessor.ts @@ -15,10 +15,20 @@ export function preprocessor(code: string, options: PrettierOptions): string { importOrderCaseInsensitive, importOrderGroupNamespaceSpecifiers, importOrderMergeDuplicateImports, + importOrderCombineTypeAndValueImports, importOrderSeparation, importOrderSortSpecifiers, } = options; + if ( + importOrderCombineTypeAndValueImports && + !importOrderMergeDuplicateImports + ) { + console.warn( + '[@ianvs/prettier-plugin-sort-imports]: Enabling importOrderCombineTypeAndValueImports will have no effect unless importOrderMergeDuplicateImports is also enabled.', + ); + } + const allOriginalImportNodes: ImportDeclaration[] = []; const parserOptions: ParserOptions = { sourceType: 'module', @@ -52,6 +62,7 @@ export function preprocessor(code: string, options: PrettierOptions): string { importOrderCaseInsensitive, importOrderGroupNamespaceSpecifiers, importOrderMergeDuplicateImports, + importOrderCombineTypeAndValueImports, importOrderSeparation, importOrderSortSpecifiers, }); diff --git a/src/types.ts b/src/types.ts index 22088c8..7ee08f6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,6 +16,7 @@ export interface PrettierOptions extends RequiredOptions { importOrderBuiltinModulesToTop: boolean; importOrderGroupNamespaceSpecifiers: boolean; importOrderMergeDuplicateImports: boolean; + importOrderCombineTypeAndValueImports: boolean; importOrderSeparation: boolean; importOrderSortSpecifiers: boolean; // should be of type ParserPlugin from '@babel/parser' but prettier does not support nested arrays in options @@ -46,6 +47,7 @@ export type GetSortedNodes = ( | 'importOrderCaseInsensitive' | 'importOrderGroupNamespaceSpecifiers' | 'importOrderMergeDuplicateImports' + | 'importOrderCombineTypeAndValueImports' | 'importOrderSeparation' | 'importOrderSortSpecifiers' >, @@ -57,4 +59,5 @@ export type GetImportFlavorOfNode = (node: ImportDeclaration) => FlavorType; export type MergeNodesWithMatchingImportFlavors = ( nodes: ImportDeclaration[], + options: { importOrderCombineTypeAndValueImports: boolean }, ) => ImportDeclaration[]; diff --git a/src/utils/__tests__/get-all-comments-from-nodes.spec.ts b/src/utils/__tests__/get-all-comments-from-nodes.spec.ts index afb782e..db6fb8f 100644 --- a/src/utils/__tests__/get-all-comments-from-nodes.spec.ts +++ b/src/utils/__tests__/get-all-comments-from-nodes.spec.ts @@ -14,6 +14,7 @@ const getSortedImportNodes = (code: string, options?: ParserOptions) => { importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }); diff --git a/src/utils/__tests__/get-code-from-ast.spec.ts b/src/utils/__tests__/get-code-from-ast.spec.ts index 33e4e33..caae32e 100644 --- a/src/utils/__tests__/get-code-from-ast.spec.ts +++ b/src/utils/__tests__/get-code-from-ast.spec.ts @@ -21,6 +21,7 @@ import a from 'a'; importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }); @@ -47,14 +48,13 @@ it('merges duplicate imports correctly', () => { // second comment import z from 'z'; import c from 'c'; -import type {C} from 'c'; -import type {See} from 'c'; import g from 'g'; import t from 't'; import k from 'k'; import a from 'a'; -import {b} from 'a'; -import {type Bee} from 'a'; +import {b, type Bee} from 'a'; +import type {C} from 'c'; +import type {See} from 'c'; `; const importNodes = getImportNodes(code, { plugins: ['typescript'] }); const sortedNodes = getSortedNodes(importNodes, { @@ -63,6 +63,7 @@ import {type Bee} from 'a'; importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: true, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }); diff --git a/src/utils/__tests__/get-sorted-import-specifiers.spec.ts b/src/utils/__tests__/get-sorted-import-specifiers.spec.ts index 40675f2..2bbd8ac 100644 --- a/src/utils/__tests__/get-sorted-import-specifiers.spec.ts +++ b/src/utils/__tests__/get-sorted-import-specifiers.spec.ts @@ -28,3 +28,23 @@ test('should return correct sorted nodes with default import', () => { 'reduce', ]); }); + +test('should group type imports after value imports', () => { + const code = `import Component, { type TypeB, filter, type TypeA, reduce, eventHandler } from '@server/z';`; + const [importNode] = getImportNodes(code, { + plugins: ['typescript'], + }); + const sortedImportSpecifiers = getSortedImportSpecifiers(importNode); + const specifiersList = getSortedNodesModulesNames( + sortedImportSpecifiers.specifiers, + ); + + expect(specifiersList).toEqual([ + 'Component', + 'eventHandler', + 'filter', + 'reduce', + 'TypeA', + 'TypeB', + ]); +}); diff --git a/src/utils/__tests__/get-sorted-nodes-by-import-order.spec.ts b/src/utils/__tests__/get-sorted-nodes-by-import-order.spec.ts index 3d7fa56..a10b4c2 100644 --- a/src/utils/__tests__/get-sorted-nodes-by-import-order.spec.ts +++ b/src/utils/__tests__/get-sorted-nodes-by-import-order.spec.ts @@ -32,6 +32,7 @@ test('it returns all sorted nodes', () => { importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -86,6 +87,7 @@ test('it returns all sorted nodes case-insensitive', () => { importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -140,6 +142,7 @@ test('it returns all sorted nodes with sort order', () => { importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -194,6 +197,7 @@ test('it returns all sorted nodes with sort order case-insensitive', () => { importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -247,6 +251,7 @@ test('it returns all sorted import nodes with sorted import specifiers', () => { importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: true, }) as ImportDeclaration[]; @@ -300,6 +305,7 @@ test('it returns all sorted import nodes with sorted import specifiers with case importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: true, }) as ImportDeclaration[]; @@ -353,6 +359,7 @@ test('it returns all sorted nodes with custom third party modules', () => { importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -383,6 +390,7 @@ test('it returns all sorted nodes with namespace specifiers at the top', () => { importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: true, importOrderMergeDuplicateImports: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -414,6 +422,7 @@ test('it returns all sorted nodes with builtin specifiers at the top, ', () => { importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -445,6 +454,7 @@ test('it returns all sorted nodes with custom third party modules and builtins a importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -475,6 +485,7 @@ test('it adds newlines when importOrderSeparation is true', () => { importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: true, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -515,6 +526,7 @@ test('it returns all sorted nodes with custom separation', () => { importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -553,6 +565,7 @@ test('it allows both importOrderSeparation and custom separation (but why?)', () importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: true, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -597,6 +610,7 @@ test('it does not add multiple custom import separators', () => { importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; diff --git a/src/utils/__tests__/get-sorted-nodes.spec.ts b/src/utils/__tests__/get-sorted-nodes.spec.ts index 586ff68..403ca06 100644 --- a/src/utils/__tests__/get-sorted-nodes.spec.ts +++ b/src/utils/__tests__/get-sorted-nodes.spec.ts @@ -33,6 +33,7 @@ test('it returns all sorted nodes, preserving the order side effect nodes', () = importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; diff --git a/src/utils/__tests__/merge-nodes-with-matching-flavors.spec.ts b/src/utils/__tests__/merge-nodes-with-matching-flavors.spec.ts index f44cb0f..922fe5d 100644 --- a/src/utils/__tests__/merge-nodes-with-matching-flavors.spec.ts +++ b/src/utils/__tests__/merge-nodes-with-matching-flavors.spec.ts @@ -4,6 +4,17 @@ import { getCodeFromAst } from '../get-code-from-ast'; import { getImportNodes } from '../get-import-nodes'; import { getSortedNodes } from '../get-sorted-nodes'; +const defaultOptions = { + importOrder: [], + importOrderBuiltinModulesToTop: false, + importOrderCaseInsensitive: false, + importOrderGroupNamespaceSpecifiers: false, + importOrderMergeDuplicateImports: false, + importOrderCombineTypeAndValueImports: false, + importOrderSeparation: true, + importOrderSortSpecifiers: true, +}; + it('should merge duplicate imports within a given chunk', () => { const code = ` import type { A } from 'a'; @@ -38,13 +49,9 @@ it('should merge duplicate imports within a given chunk', () => { }); const nodesToOutput = getSortedNodes(allOriginalImportNodes, { - importOrder: [], - importOrderBuiltinModulesToTop: false, - importOrderCaseInsensitive: false, - importOrderGroupNamespaceSpecifiers: false, + ...defaultOptions, importOrderMergeDuplicateImports: true, - importOrderSeparation: true, - importOrderSortSpecifiers: true, + importOrderCombineTypeAndValueImports: false, }); const formatted = getCodeFromAst({ nodesToOutput, @@ -81,6 +88,234 @@ import Foo2 from "e"; import { Junk2 } from "junk-group-2"; `); }); +it('should merge type imports into regular imports', () => { + const code = ` + // Preserves 'import type' + import type { A1 } from 'a'; + import type { A2 } from 'a'; + // Preserves 'import value' + import { B1 } from 'b'; + import { B2 } from 'b'; + // Converts 'import type' to 'import value' if first + import { C2 } from 'c'; + import type { C1 } from 'c'; + // Sorts type import to end + import { D1 } from 'd'; + import type { D2 } from 'd'; + `; + const allOriginalImportNodes = getImportNodes(code, { + plugins: ['typescript'], + }); + + const nodesToOutput = getSortedNodes(allOriginalImportNodes, { + ...defaultOptions, + importOrderMergeDuplicateImports: true, + importOrderCombineTypeAndValueImports: true, + }); + const formatted = getCodeFromAst({ + nodesToOutput, + allOriginalImportNodes, + originalCode: code, + directives: [], + }); + + expect(format(formatted, { parser: 'babel' })) + .toEqual(`// Preserves 'import type' +import type { A1, A2 } from "a"; +// Preserves 'import value' +import { B1, B2 } from "b"; +// Converts 'import type' to 'import value' if first +import { C2, type C1 } from "c"; +// Sorts type import to end +import { D1, type D2 } from "d"; +`); +}); +it('should combine type import and default import', () => { + const code = ` +import type {MyType} from './source'; +import defaultValue from './source'; +`; + const allOriginalImportNodes = getImportNodes(code, { + plugins: ['typescript'], + }); + + const nodesToOutput = getSortedNodes(allOriginalImportNodes, { + ...defaultOptions, + importOrderMergeDuplicateImports: true, + importOrderCombineTypeAndValueImports: true, + }); + const formatted = getCodeFromAst({ + nodesToOutput, + allOriginalImportNodes, + originalCode: code, + directives: [], + }); + + expect(format(formatted, { parser: 'babel' })) + .toEqual(`import defaultValue, { type MyType } from "./source"; +`); +}); +it('should not combine type import and namespace import', () => { + const code = ` +import type {MyType} from './source'; +import * as Namespace from './source'; +`; + const allOriginalImportNodes = getImportNodes(code, { + plugins: ['typescript'], + }); + + const nodesToOutput = getSortedNodes(allOriginalImportNodes, { + ...defaultOptions, + importOrderMergeDuplicateImports: true, + importOrderCombineTypeAndValueImports: true, + }); + const formatted = getCodeFromAst({ + nodesToOutput, + allOriginalImportNodes, + originalCode: code, + directives: [], + }); + + expect(format(formatted, { parser: 'babel' })) + .toEqual(`import type { MyType } from "./source"; +import * as Namespace from "./source"; +`); +}); +it('should support aliased named imports', () => { + const code = ` +import type {MyType} from './source'; +import {value as alias} from './source'; +`; + const allOriginalImportNodes = getImportNodes(code, { + plugins: ['typescript'], + }); + + const nodesToOutput = getSortedNodes(allOriginalImportNodes, { + ...defaultOptions, + importOrderMergeDuplicateImports: true, + importOrderCombineTypeAndValueImports: true, + }); + const formatted = getCodeFromAst({ + nodesToOutput, + allOriginalImportNodes, + originalCode: code, + directives: [], + }); + + expect(format(formatted, { parser: 'babel' })) + .toEqual(`import { value as alias, type MyType } from "./source"; +`); +}); +it('should combine multiple imports from the same source', () => { + const code = ` +import type {MyType, SecondType} from './source'; +import {value, SecondValue} from './source'; +`; + const allOriginalImportNodes = getImportNodes(code, { + plugins: ['typescript'], + }); + + const nodesToOutput = getSortedNodes(allOriginalImportNodes, { + ...defaultOptions, + importOrderMergeDuplicateImports: true, + importOrderCombineTypeAndValueImports: true, + }); + const formatted = getCodeFromAst({ + nodesToOutput, + allOriginalImportNodes, + originalCode: code, + directives: [], + }); + + expect(format(formatted, { parser: 'babel' })) + .toEqual(`import { SecondValue, value, type MyType, type SecondType } from "./source"; +`); +}); +it('should combine multiple groups of imports', () => { + const code = ` +import type {MyType} from './source'; +import type {OtherType} from './other'; +import {value} from './source'; +import {otherValue} from './other'; +`; + const allOriginalImportNodes = getImportNodes(code, { + plugins: ['typescript'], + }); + + const nodesToOutput = getSortedNodes(allOriginalImportNodes, { + ...defaultOptions, + importOrderMergeDuplicateImports: true, + importOrderCombineTypeAndValueImports: true, + }); + const formatted = getCodeFromAst({ + nodesToOutput, + allOriginalImportNodes, + originalCode: code, + directives: [], + }); + + expect(format(formatted, { parser: 'babel' })) + .toEqual(`import { otherValue, type OtherType } from "./other"; +import { value, type MyType } from "./source"; +`); +}); +it('should combine multiple imports statements from the same source', () => { + const code = ` +import type {MyType} from './source'; +import type {SecondType} from './source'; +import {value} from './source'; +import {SecondValue} from './source'; +`; + const allOriginalImportNodes = getImportNodes(code, { + plugins: ['typescript'], + }); + + const nodesToOutput = getSortedNodes(allOriginalImportNodes, { + ...defaultOptions, + importOrderMergeDuplicateImports: true, + importOrderCombineTypeAndValueImports: true, + }); + const formatted = getCodeFromAst({ + nodesToOutput, + allOriginalImportNodes, + originalCode: code, + directives: [], + }); + + expect(format(formatted, { parser: 'babel' })) + .toEqual(`import { SecondValue, value, type MyType, type SecondType } from "./source"; +`); +}); +it('should not impact imports from different sources', () => { + const code = ` +import type {MyType} from './source'; +import type {OtherType} from './other'; +import {thirdValue} from './third' +import {value} from './source'; +`; + const allOriginalImportNodes = getImportNodes(code, { + plugins: ['typescript'], + }); + + const nodesToOutput = getSortedNodes(allOriginalImportNodes, { + ...defaultOptions, + importOrderMergeDuplicateImports: true, + importOrderCombineTypeAndValueImports: true, + }); + const formatted = getCodeFromAst({ + nodesToOutput, + allOriginalImportNodes, + originalCode: code, + directives: [], + }); + + expect(format(formatted, { parser: 'babel' })) + .toEqual(`import type { OtherType } from "./other"; +import { value, type MyType } from "./source"; +import { thirdValue } from "./third"; +`); +}); + it("doesn't merge duplicate imports if option disabled", () => { const code = ` import type { A } from 'a'; @@ -109,20 +344,15 @@ it("doesn't merge duplicate imports if option disabled", () => { import { default as Def1 } from 'd'; import Foo1 from 'e'; import Foo2 from 'e'; - `; +`; const allOriginalImportNodes = getImportNodes(code, { plugins: ['typescript'], }); - const nodesToOutput = getSortedNodes(allOriginalImportNodes, { - importOrder: [], - importOrderBuiltinModulesToTop: false, - importOrderCaseInsensitive: false, - importOrderGroupNamespaceSpecifiers: false, - importOrderMergeDuplicateImports: false, - importOrderSeparation: true, - importOrderSortSpecifiers: true, - }); + const nodesToOutput = getSortedNodes( + allOriginalImportNodes, + defaultOptions, + ); const formatted = getCodeFromAst({ nodesToOutput, allOriginalImportNodes, @@ -162,36 +392,3 @@ import Foo2 from "e"; import { Junk2 } from "junk-group-2"; `); }); - -it('merges named and default imports correctly', () => { - const code = ` - import A, { a } from "a"; - import { ahh } from "a"; - import { bee } from "b"; - import B from "b"; - `; - const allOriginalImportNodes = getImportNodes(code, { - plugins: ['typescript'], - }); - - const nodesToOutput = getSortedNodes(allOriginalImportNodes, { - importOrder: [], - importOrderBuiltinModulesToTop: false, - importOrderCaseInsensitive: false, - importOrderGroupNamespaceSpecifiers: false, - importOrderMergeDuplicateImports: true, - importOrderSeparation: true, - importOrderSortSpecifiers: true, - }); - const formatted = getCodeFromAst({ - nodesToOutput, - allOriginalImportNodes, - originalCode: code, - directives: [], - }); - - expect(format(formatted, { parser: 'babel' })) - .toEqual(`import A, { a, ahh } from "a"; -import B, { bee } from "b"; -`); -}); diff --git a/src/utils/__tests__/remove-nodes-from-original-code.spec.ts b/src/utils/__tests__/remove-nodes-from-original-code.spec.ts index cced3df..12a0ba0 100644 --- a/src/utils/__tests__/remove-nodes-from-original-code.spec.ts +++ b/src/utils/__tests__/remove-nodes-from-original-code.spec.ts @@ -27,6 +27,7 @@ test('it should remove nodes from the original code', () => { importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }); diff --git a/src/utils/get-sorted-import-specifiers.ts b/src/utils/get-sorted-import-specifiers.ts index 3897b78..d8dc3ec 100644 --- a/src/utils/get-sorted-import-specifiers.ts +++ b/src/utils/get-sorted-import-specifiers.ts @@ -4,7 +4,10 @@ import { naturalSort } from '../natural-sort'; /** * This function returns import nodes with alphabetically sorted module - * specifiers + * specifiers. + * + * type imports are sorted separately, and placed after value imports. + * * @param node Import declaration node */ export const getSortedImportSpecifiers = (node: ImportDeclaration) => { @@ -12,6 +15,13 @@ export const getSortedImportSpecifiers = (node: ImportDeclaration) => { if (a.type !== b.type) { return a.type === 'ImportDefaultSpecifier' ? -1 : 1; } + if ( + a.type === 'ImportSpecifier' && + b.type === 'ImportSpecifier' && + a.importKind !== b.importKind + ) { + return a.importKind === 'value' ? -1 : 1; + } return naturalSort(a.local.name, b.local.name); }); diff --git a/src/utils/get-sorted-nodes.ts b/src/utils/get-sorted-nodes.ts index e6c3233..6c2f8d8 100644 --- a/src/utils/get-sorted-nodes.ts +++ b/src/utils/get-sorted-nodes.ts @@ -21,7 +21,11 @@ import { mergeNodesWithMatchingImportFlavors } from './merge-nodes-with-matching * @returns A sorted array of the remaining import nodes */ export const getSortedNodes: GetSortedNodes = (nodes, options) => { - const { importOrderSeparation, importOrderMergeDuplicateImports } = options; + const { + importOrderSeparation, + importOrderMergeDuplicateImports, + importOrderCombineTypeAndValueImports, + } = options; // Split nodes at each boundary between a side-effect node and a // non-side-effect node, keeping both types of nodes together. @@ -49,7 +53,9 @@ export const getSortedNodes: GetSortedNodes = (nodes, options) => { finalNodes.push(...chunk.nodes); } else { const nodes = importOrderMergeDuplicateImports - ? mergeNodesWithMatchingImportFlavors(chunk.nodes) + ? mergeNodesWithMatchingImportFlavors(chunk.nodes, { + importOrderCombineTypeAndValueImports, + }) : chunk.nodes; // sort non-side effect nodes const sorted = getSortedNodesByImportOrder(nodes, options); diff --git a/src/utils/merge-nodes-with-matching-flavors.ts b/src/utils/merge-nodes-with-matching-flavors.ts index ecd5b39..5a760e3 100644 --- a/src/utils/merge-nodes-with-matching-flavors.ts +++ b/src/utils/merge-nodes-with-matching-flavors.ts @@ -4,6 +4,7 @@ import type { ImportNamespaceSpecifier, ImportSpecifier, } from '@babel/types'; +import assert from 'assert'; import { importFlavorType, @@ -41,7 +42,6 @@ function selectMergeableNodesByImportFlavor( }, ); } - /** * Returns the "source" (i.e. module name or path) of an import declaration * @@ -51,19 +51,37 @@ function selectNodeImportSource(node: ImportDeclaration) { return node.source.value; } -/** import * as Namespace from "someModule" */ +/** e.g. import * as Namespace from "someModule" */ function nodeIsImportNamespaceSpecifier( node: ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier, ): node is ImportNamespaceSpecifier { return node.type === 'ImportNamespaceSpecifier'; } - -/** import Default from "someModule" */ +/** e.g. import Default from "someModule" */ function nodeIsImportDefaultSpecifier( node: ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier, ): node is ImportDefaultSpecifier { return node.type === 'ImportDefaultSpecifier'; } +function nodeIsImportSpecifier( + node: ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier, +): node is ImportSpecifier { + return node.type === 'ImportSpecifier'; +} + +function convertImportSpecifierToType(node: ImportSpecifier) { + assert(node.importKind === 'value' || node.importKind === 'type'); + node.importKind = 'type'; +} + +/** Pushes an `import type` expression into `import { type …}` */ +function convertTypeImportToValueImport(node: ImportDeclaration) { + assert(node.importKind === 'type'); + node.importKind = 'value'; + node.specifiers + .filter(nodeIsImportSpecifier) + .forEach(convertImportSpecifierToType); +} /** Return false if the merge will produce an invalid result */ function mergeIsSafe( @@ -106,6 +124,18 @@ function mergeNodes( return false; } + if ( + nodeToKeep.importKind === 'type' && + nodeToForget.importKind === 'value' + ) { + convertTypeImportToValueImport(nodeToKeep); + } else if ( + nodeToKeep.importKind === 'value' && + nodeToForget.importKind === 'type' + ) { + convertTypeImportToValueImport(nodeToForget); + } + nodeToKeep.specifiers.push(...nodeToForget.specifiers); // These mutations don't update the line numbers, and that's crucial for moving things around. @@ -160,14 +190,17 @@ function mutateContextAndMerge({ * `import type {Foo}` expressions won't be converted into `import {type Foo}` or vice versa */ export const mergeNodesWithMatchingImportFlavors: MergeNodesWithMatchingImportFlavors = - (input) => { + (input, { importOrderCombineTypeAndValueImports }) => { const nodesToDelete: ImportDeclaration[] = []; - for (const group of Object.values( - selectMergeableNodesByImportFlavor(input), - )) { - // Defined in loop to reset so we don't merge across groups - const context: Record = {}; + let context: Record = {}; + const groups = selectMergeableNodesByImportFlavor(input); + for (const groupKey of mergeableImportFlavors) { + if (!importOrderCombineTypeAndValueImports) { + // Reset in loop to avoid unintended merge across variants + context = {}; + } + const group = groups[groupKey as keyof typeof groups]; for (const insertableNode of group) { mutateContextAndMerge({