From d940e06cf4223f0e21e4d2a2fae80d2308474066 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Wed, 25 Mar 2026 20:44:06 +0000 Subject: [PATCH 1/8] implement import deduping --- src/index.ts | 7 +++++ src/preprocessors/preprocessor.ts | 6 +++-- src/types.ts | 4 +-- src/utils/get-merged-specifiers.ts | 27 +++++++++++++++++++ src/utils/get-sorted-nodes-by-import-order.ts | 9 ++++++- types/index.d.ts | 23 ++++++++++++++++ 6 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 src/utils/get-merged-specifiers.ts diff --git a/src/index.ts b/src/index.ts index b449bfd2..774be35e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -93,6 +93,13 @@ const options: Options = { default: 'with', description: 'Provide a keyword for import attributes', }, + importOrderCombineImportSpecifiers: { + type: 'boolean', + category: 'Global', + default: false, + description: + 'Should duplicate import sources be combined into a single import declaration?', + }, }; export default { diff --git a/src/preprocessors/preprocessor.ts b/src/preprocessors/preprocessor.ts index 27563a94..4ef2b126 100644 --- a/src/preprocessors/preprocessor.ts +++ b/src/preprocessors/preprocessor.ts @@ -22,6 +22,7 @@ export function preprocessor(code: string, options: PrettierOptions) { importOrderSideEffects, importOrderImportAttributesKeyword, importOrderExclude, + importOrderCombineImportSpecifiers, filepath, } = options; @@ -54,7 +55,7 @@ export function preprocessor(code: string, options: PrettierOptions) { if (importNodes.length === 0) return code; if (isSortImportsIgnored(getAllCommentsFromNodes(importNodes))) return code; - const allImports = getSortedNodes(importNodes, { + const sortedImports = getSortedNodes(importNodes, { importOrder, importOrderCaseInsensitive, importOrderSeparation, @@ -62,9 +63,10 @@ export function preprocessor(code: string, options: PrettierOptions) { importOrderSortSpecifiers, importOrderSortByLength, importOrderSideEffects, + importOrderCombineImportSpecifiers, }); - return getCodeFromAst(allImports, code, injectIdx, { + return getCodeFromAst(importNodes, code, sortedImports, injectIdx, { importOrderImportAttributesKeyword, }); } diff --git a/src/types.ts b/src/types.ts index 799e4e00..9348ae70 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,8 +4,7 @@ import { RequiredOptions } from 'prettier'; import { PluginConfig } from '../types'; export interface PrettierOptions - extends Required, - RequiredOptions {} + extends Required, RequiredOptions {} export type ImportGroups = Record; export type ImportOrLine = ImportDeclaration | ExpressionStatement; @@ -21,6 +20,7 @@ export type GetSortedNodes = ( | 'importOrderSortSpecifiers' | 'importOrderSortByLength' | 'importOrderSideEffects' + | 'importOrderCombineImportSpecifiers' >, ) => ImportOrLine[]; diff --git a/src/utils/get-merged-specifiers.ts b/src/utils/get-merged-specifiers.ts new file mode 100644 index 00000000..88542c7f --- /dev/null +++ b/src/utils/get-merged-specifiers.ts @@ -0,0 +1,27 @@ +import type { ImportDeclaration } from '@babel/types'; + +export function getMergedSpecifiers(nodes: ImportDeclaration[]) { + // Combine import specifiers + const merged = nodes.reduce((acc, node) => { + if (node.specifiers.length === 0) { + acc.push(node); + return acc; + } + + const exists = acc.find((n) => { + return ( + n.source.value === node.source.value && + // ignore import if it's a namespace specifier + n.specifiers[0]?.type !== 'ImportNamespaceSpecifier' + ); + }); + if (exists) { + exists.specifiers.push(...node.specifiers); + } else { + acc.push(node); + } + return acc; + }, []); + + return merged; +} diff --git a/src/utils/get-sorted-nodes-by-import-order.ts b/src/utils/get-sorted-nodes-by-import-order.ts index e0f663a5..0c57048f 100644 --- a/src/utils/get-sorted-nodes-by-import-order.ts +++ b/src/utils/get-sorted-nodes-by-import-order.ts @@ -9,6 +9,7 @@ import { import { naturalSort } from '../natural-sort/index.js'; import { GetSortedNodes, ImportGroups, ImportOrLine } from '../types'; import { getImportNodesMatchedGroup } from './get-import-nodes-matched-group.js'; +import { getMergedSpecifiers } from './get-merged-specifiers.js'; import { getSortedImportSpecifiers } from './get-sorted-import-specifiers.js'; import { getSortedNodesGroup } from './get-sorted-nodes-group.js'; @@ -27,6 +28,7 @@ export const getSortedNodesByImportOrder: GetSortedNodes = (nodes, options) => { importOrderSeparation, importOrderSortSpecifiers, importOrderGroupNamespaceSpecifiers, + importOrderCombineImportSpecifiers, } = options; const originalNodes = nodes.map(clone); @@ -71,11 +73,16 @@ export const getSortedNodesByImportOrder: GetSortedNodes = (nodes, options) => { } if (groupNodes.length === 0) continue; - const sortedInsideGroup = getSortedNodesGroup(groupNodes, { + let sortedInsideGroup = getSortedNodesGroup(groupNodes, { importOrderGroupNamespaceSpecifiers, importOrderSortByLength, }); + // Combine import declarations with the same source into a single import declaration + if (importOrderCombineImportSpecifiers) { + sortedInsideGroup = getMergedSpecifiers(sortedInsideGroup); + } + // Sort the import specifiers if (importOrderSortSpecifiers) { sortedInsideGroup.forEach((node) => diff --git a/types/index.d.ts b/types/index.d.ts index 8019b888..657fe63a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -133,6 +133,29 @@ used to order imports within each match group. * @default [] */ importOrderExclude?: string[]; + + /** + * Combines multiple import declarations into a single import declaration, combining all their specifiers. + * + * For example, provided the following input: + * ```js + * import * as NamespacedImport from './example'; + * import DefaultImport from './example'; + * import { NamedImport1 } from './example'; + * import { NamedImport2 } from './example'; + * import { NamedImport3 } from './example'; + * ``` + * + * will be transformed into: + * + * ```js + * import * as NamespacedImport from './example'; + * import DefaultImport, { NamedImport1, NamedImport2, NamedImport3 } from './example'; + * ``` + * + * @default false + */ + importOrderCombineImportSpecifiers?: boolean; } export type PrettierConfig = PluginConfig & Config; From d9c74d48f6facc0afa09d9fddc9cecdb315b504e Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Wed, 25 Mar 2026 20:44:17 +0000 Subject: [PATCH 2/8] add test --- .../__tests__/get-merged-specifiers.spec.ts | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/utils/__tests__/get-merged-specifiers.spec.ts diff --git a/src/utils/__tests__/get-merged-specifiers.spec.ts b/src/utils/__tests__/get-merged-specifiers.spec.ts new file mode 100644 index 00000000..ec01cb7d --- /dev/null +++ b/src/utils/__tests__/get-merged-specifiers.spec.ts @@ -0,0 +1,60 @@ +import { expect, test } from 'vitest'; + +import { getImportNodes } from '../get-import-nodes'; +import { getMergedSpecifiers } from '../get-merged-specifiers.js'; +import { getSortedNodesModulesNames } from '../get-sorted-nodes-modules-names'; + +test('should merge import specifiers', () => { + const code = ` + import { eventHandler } from '@server/z'; + import { filter } from '@server/z'; + import { reduce } from '@server/z'; + `; + const importNodes = getImportNodes(code); + const [importDeclaration] = getMergedSpecifiers(importNodes); + const specifiersList = getSortedNodesModulesNames( + importDeclaration.specifiers, + ); + + expect(specifiersList).toEqual(['eventHandler', 'filter', 'reduce']); +}); + +test('should merge import specifiers with default import', () => { + const code = ` + import Component from '@server/z'; + import { eventHandler } from '@server/z'; + import { filter } from '@server/z'; + import { reduce } from '@server/z'; + `; + const importNodes = getImportNodes(code); + const [importDeclaration] = getMergedSpecifiers(importNodes); + const specifiersList = getSortedNodesModulesNames( + importDeclaration.specifiers, + ); + + expect(specifiersList).toEqual([ + 'Component', + 'eventHandler', + 'filter', + 'reduce', + ]); +}); + +test('should ignore namespace specifiers', () => { + const code = ` + import * as Component from '@server/z'; + import { eventHandler } from '@server/z'; + import { filter } from '@server/z'; + import { reduce } from '@server/z'; + `; + const importNodes = getImportNodes(code); + const importDeclarations = getMergedSpecifiers(importNodes); + const specifiersLists = importDeclarations.map((declaration) => + getSortedNodesModulesNames(declaration.specifiers), + ); + + expect(specifiersLists).toEqual([ + ['Component'], + ['eventHandler', 'filter', 'reduce'], + ]); +}); From b4ac37218f001d83c089a4d8d55e2c7c2a59b362 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Wed, 25 Mar 2026 20:44:24 +0000 Subject: [PATCH 3/8] fix types of other tests --- src/utils/__tests__/assemble-updated-code.spec.ts | 2 ++ .../__tests__/get-all-comments-from-nodes.spec.ts | 1 + src/utils/__tests__/get-code-from-ast.spec.ts | 3 ++- .../get-sorted-nodes-by-import-order.spec.ts | 13 +++++++++++++ src/utils/__tests__/get-sorted-nodes.spec.ts | 11 +++++++++++ 5 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/utils/__tests__/assemble-updated-code.spec.ts b/src/utils/__tests__/assemble-updated-code.spec.ts index 76204867..3b1c321c 100644 --- a/src/utils/__tests__/assemble-updated-code.spec.ts +++ b/src/utils/__tests__/assemble-updated-code.spec.ts @@ -29,6 +29,7 @@ test('it should remove nodes from the original code', async () => { importOrderSortSpecifiers: false, importOrderSideEffects: true, importOrderSortByLength: null, + importOrderCombineImportSpecifiers: false, }); const allCommentsFromImports = getAllCommentsFromNodes(sortedNodes); @@ -57,6 +58,7 @@ test('it should inject the generated code at the correct location', async () => importOrderSortSpecifiers: false, importOrderSideEffects: true, importOrderSortByLength: null, + importOrderCombineImportSpecifiers: false, }); const allCommentsFromImports = getAllCommentsFromNodes(sortedNodes); 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 4455d55c..9b55a1a9 100644 --- a/src/utils/__tests__/get-all-comments-from-nodes.spec.ts +++ b/src/utils/__tests__/get-all-comments-from-nodes.spec.ts @@ -17,6 +17,7 @@ const getSortedImportNodes = (code: string, options?: ParserOptions) => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, + importOrderCombineImportSpecifiers: false, }); }; diff --git a/src/utils/__tests__/get-code-from-ast.spec.ts b/src/utils/__tests__/get-code-from-ast.spec.ts index 7c238b53..6a3788cf 100644 --- a/src/utils/__tests__/get-code-from-ast.spec.ts +++ b/src/utils/__tests__/get-code-from-ast.spec.ts @@ -24,8 +24,9 @@ import a from 'a'; importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, + importOrderCombineImportSpecifiers: false, }); - const formatted = getCodeFromAst(sortedNodes, code); + const formatted = getCodeFromAst(importNodes, code, sortedNodes); expect(await format(formatted, { parser: 'babel' })).toEqual( `// first comment // second comment 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 ff9e8dd1..67745a1a 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 @@ -33,6 +33,7 @@ test('it returns all sorted nodes', () => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, + importOrderCombineImportSpecifiers: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ @@ -79,6 +80,7 @@ test('it returns all sorted nodes case-insensitive', () => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, + importOrderCombineImportSpecifiers: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ @@ -125,6 +127,7 @@ test('it returns all sorted nodes with sort order', () => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, + importOrderCombineImportSpecifiers: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ @@ -171,6 +174,7 @@ test('it returns all sorted nodes with sort order case-insensitive', () => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, + importOrderCombineImportSpecifiers: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ 'c', @@ -216,6 +220,7 @@ test('it returns all sorted import nodes with sorted import specifiers', () => { importOrderSortSpecifiers: true, importOrderSortByLength: null, importOrderSideEffects: true, + importOrderCombineImportSpecifiers: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ 'XY', @@ -261,6 +266,7 @@ test('it returns all sorted import nodes with sorted import specifiers with case importOrderSortSpecifiers: true, importOrderSortByLength: null, importOrderSideEffects: true, + importOrderCombineImportSpecifiers: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ 'c', @@ -306,6 +312,7 @@ test('it returns all sorted nodes with custom third party modules', () => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, + importOrderCombineImportSpecifiers: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ 'a', @@ -332,6 +339,7 @@ test('it returns all sorted nodes with namespace specifiers at the top', () => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, + importOrderCombineImportSpecifiers: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ @@ -367,6 +375,7 @@ test('it returns the default separations if `importOrderSeparation` is false', ( importOrderSortSpecifiers: false, importOrderSideEffects: true, importOrderSortByLength: null, + importOrderCombineImportSpecifiers: false, }); expect(getSeparationData(sorted)).toEqual([ { type: 'ImportDeclaration', value: 'XY' }, @@ -394,6 +403,7 @@ test('it returns default import module separations', () => { importOrderSortSpecifiers: false, importOrderSideEffects: true, importOrderSortByLength: null, + importOrderCombineImportSpecifiers: false, }); expect(getSeparationData(sorted)).toEqual([ { type: 'ImportDeclaration', value: 'XY' }, @@ -426,6 +436,7 @@ test('it returns targeted import module separations', () => { importOrderSortSpecifiers: false, importOrderSideEffects: true, importOrderSortByLength: null, + importOrderCombineImportSpecifiers: false, }); expect(getSeparationData(sorted)).toEqual([ { type: 'ImportDeclaration', value: 'XY' }, @@ -459,6 +470,7 @@ test('it never returns a separation at the top of the list (leading separator)', importOrderSortSpecifiers: false, importOrderSideEffects: true, importOrderSortByLength: null, + importOrderCombineImportSpecifiers: false, }); expect(getSeparationData(sorted)).toEqual([ { type: 'ImportDeclaration', value: './test' }, @@ -480,6 +492,7 @@ test('it never returns a separation at the top of the list (zero preceding impor importOrderSortSpecifiers: false, importOrderSideEffects: true, importOrderSortByLength: null, + importOrderCombineImportSpecifiers: false, }); expect(getSeparationData(sorted)).toEqual([ { type: 'ImportDeclaration', value: './test' }, diff --git a/src/utils/__tests__/get-sorted-nodes.spec.ts b/src/utils/__tests__/get-sorted-nodes.spec.ts index fb9ac6af..0bb32167 100644 --- a/src/utils/__tests__/get-sorted-nodes.spec.ts +++ b/src/utils/__tests__/get-sorted-nodes.spec.ts @@ -49,6 +49,7 @@ test('it returns all sorted nodes', () => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, + importOrderCombineImportSpecifiers: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ @@ -95,6 +96,7 @@ test('it returns all sorted nodes case-insensitive', () => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, + importOrderCombineImportSpecifiers: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ @@ -141,6 +143,7 @@ test('it returns all sorted nodes with sort order', () => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, + importOrderCombineImportSpecifiers: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ @@ -187,6 +190,7 @@ test('it returns all sorted nodes with sort order case-insensitive', () => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, + importOrderCombineImportSpecifiers: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ 'c', @@ -232,6 +236,7 @@ test('it returns all sorted import nodes with sorted import specifiers', () => { importOrderSortSpecifiers: true, importOrderSortByLength: null, importOrderSideEffects: true, + importOrderCombineImportSpecifiers: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ 'XY', @@ -277,6 +282,7 @@ test('it returns all sorted import nodes with sorted import specifiers with case importOrderSortSpecifiers: true, importOrderSortByLength: null, importOrderSideEffects: true, + importOrderCombineImportSpecifiers: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ 'c', @@ -322,6 +328,7 @@ test('it returns all sorted nodes with custom third party modules', () => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, + importOrderCombineImportSpecifiers: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ 'a', @@ -348,6 +355,7 @@ test('it returns all sorted nodes with namespace specifiers at the top', () => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, + importOrderCombineImportSpecifiers: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ @@ -375,6 +383,7 @@ test('it returns all sorted nodes, sorted shortest to longest', () => { importOrderSortSpecifiers: false, importOrderSideEffects: true, importOrderSortByLength: 'asc', + importOrderCombineImportSpecifiers: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ 'g', @@ -401,6 +410,7 @@ test('it returns all sorted nodes, sorted longest to shortest', () => { importOrderSortSpecifiers: false, importOrderSideEffects: false, importOrderSortByLength: 'desc', + importOrderCombineImportSpecifiers: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ 't', @@ -429,6 +439,7 @@ test('it returns all sorted nodes with types', () => { importOrderSortSpecifiers: false, importOrderSideEffects: true, importOrderSortByLength: null, + importOrderCombineImportSpecifiers: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ From 91458ea9a54a5e2a1bac74f89bdef1681c93293c Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Wed, 25 Mar 2026 20:45:18 +0000 Subject: [PATCH 4/8] fix issue where the updated code replaces the wrong nodes --- src/utils/get-code-from-ast.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/utils/get-code-from-ast.ts b/src/utils/get-code-from-ast.ts index 1fd7ce1b..17f14a91 100644 --- a/src/utils/get-code-from-ast.ts +++ b/src/utils/get-code-from-ast.ts @@ -10,22 +10,23 @@ const generate = (generateModule as any).default || generateModule; /** * This function generate a code string from the passed nodes. - * @param nodes all imports + * @param originalNodes all imports * @param originalCode */ export const getCodeFromAst = ( - nodes: Statement[], + originalNodes: Statement[], originalCode: string, + nodesToInject: Statement[], injectIdx: number = 0, options?: Pick, ) => { - const allCommentsFromImports = getAllCommentsFromNodes(nodes); + const allCommentsFromImports = getAllCommentsFromNodes(originalNodes); - const nodesToRemoveFromCode = [...nodes, ...allCommentsFromImports]; + const nodesToRemoveFromCode = [...originalNodes, ...allCommentsFromImports]; const newAST = file({ type: 'Program', - body: nodes, + body: nodesToInject, directives: [], sourceType: 'module', leadingComments: [], From f539422e42027f2c8c11a9528b8868fc5c588dea Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Wed, 25 Mar 2026 20:57:32 +0000 Subject: [PATCH 5/8] change name to `importOrderMergeImports` --- src/preprocessors/preprocessor.ts | 4 +-- src/types.ts | 2 +- .../__tests__/assemble-updated-code.spec.ts | 4 +-- .../get-all-comments-from-nodes.spec.ts | 2 +- src/utils/__tests__/get-code-from-ast.spec.ts | 2 +- .../get-sorted-nodes-by-import-order.spec.ts | 26 +++++++++---------- src/utils/__tests__/get-sorted-nodes.spec.ts | 22 ++++++++-------- src/utils/get-sorted-nodes-by-import-order.ts | 4 +-- types/index.d.ts | 4 +-- 9 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/preprocessors/preprocessor.ts b/src/preprocessors/preprocessor.ts index 4ef2b126..15f0e8d8 100644 --- a/src/preprocessors/preprocessor.ts +++ b/src/preprocessors/preprocessor.ts @@ -22,7 +22,7 @@ export function preprocessor(code: string, options: PrettierOptions) { importOrderSideEffects, importOrderImportAttributesKeyword, importOrderExclude, - importOrderCombineImportSpecifiers, + importOrderMergeImports, filepath, } = options; @@ -63,7 +63,7 @@ export function preprocessor(code: string, options: PrettierOptions) { importOrderSortSpecifiers, importOrderSortByLength, importOrderSideEffects, - importOrderCombineImportSpecifiers, + importOrderMergeImports, }); return getCodeFromAst(importNodes, code, sortedImports, injectIdx, { diff --git a/src/types.ts b/src/types.ts index 9348ae70..7c076c97 100644 --- a/src/types.ts +++ b/src/types.ts @@ -20,7 +20,7 @@ export type GetSortedNodes = ( | 'importOrderSortSpecifiers' | 'importOrderSortByLength' | 'importOrderSideEffects' - | 'importOrderCombineImportSpecifiers' + | 'importOrderMergeImports' >, ) => ImportOrLine[]; diff --git a/src/utils/__tests__/assemble-updated-code.spec.ts b/src/utils/__tests__/assemble-updated-code.spec.ts index 3b1c321c..ae0dc6b8 100644 --- a/src/utils/__tests__/assemble-updated-code.spec.ts +++ b/src/utils/__tests__/assemble-updated-code.spec.ts @@ -29,7 +29,7 @@ test('it should remove nodes from the original code', async () => { importOrderSortSpecifiers: false, importOrderSideEffects: true, importOrderSortByLength: null, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }); const allCommentsFromImports = getAllCommentsFromNodes(sortedNodes); @@ -58,7 +58,7 @@ test('it should inject the generated code at the correct location', async () => importOrderSortSpecifiers: false, importOrderSideEffects: true, importOrderSortByLength: null, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }); const allCommentsFromImports = getAllCommentsFromNodes(sortedNodes); 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 9b55a1a9..001aefc3 100644 --- a/src/utils/__tests__/get-all-comments-from-nodes.spec.ts +++ b/src/utils/__tests__/get-all-comments-from-nodes.spec.ts @@ -17,7 +17,7 @@ const getSortedImportNodes = (code: string, options?: ParserOptions) => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }); }; diff --git a/src/utils/__tests__/get-code-from-ast.spec.ts b/src/utils/__tests__/get-code-from-ast.spec.ts index 6a3788cf..3a0365d1 100644 --- a/src/utils/__tests__/get-code-from-ast.spec.ts +++ b/src/utils/__tests__/get-code-from-ast.spec.ts @@ -24,7 +24,7 @@ import a from 'a'; importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }); const formatted = getCodeFromAst(importNodes, code, sortedNodes); expect(await format(formatted, { parser: 'babel' })).toEqual( 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 67745a1a..763efb10 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 @@ -33,7 +33,7 @@ test('it returns all sorted nodes', () => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ @@ -80,7 +80,7 @@ test('it returns all sorted nodes case-insensitive', () => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ @@ -127,7 +127,7 @@ test('it returns all sorted nodes with sort order', () => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ @@ -174,7 +174,7 @@ test('it returns all sorted nodes with sort order case-insensitive', () => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ 'c', @@ -220,7 +220,7 @@ test('it returns all sorted import nodes with sorted import specifiers', () => { importOrderSortSpecifiers: true, importOrderSortByLength: null, importOrderSideEffects: true, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ 'XY', @@ -266,7 +266,7 @@ test('it returns all sorted import nodes with sorted import specifiers with case importOrderSortSpecifiers: true, importOrderSortByLength: null, importOrderSideEffects: true, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ 'c', @@ -312,7 +312,7 @@ test('it returns all sorted nodes with custom third party modules', () => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ 'a', @@ -339,7 +339,7 @@ test('it returns all sorted nodes with namespace specifiers at the top', () => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ @@ -375,7 +375,7 @@ test('it returns the default separations if `importOrderSeparation` is false', ( importOrderSortSpecifiers: false, importOrderSideEffects: true, importOrderSortByLength: null, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }); expect(getSeparationData(sorted)).toEqual([ { type: 'ImportDeclaration', value: 'XY' }, @@ -403,7 +403,7 @@ test('it returns default import module separations', () => { importOrderSortSpecifiers: false, importOrderSideEffects: true, importOrderSortByLength: null, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }); expect(getSeparationData(sorted)).toEqual([ { type: 'ImportDeclaration', value: 'XY' }, @@ -436,7 +436,7 @@ test('it returns targeted import module separations', () => { importOrderSortSpecifiers: false, importOrderSideEffects: true, importOrderSortByLength: null, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }); expect(getSeparationData(sorted)).toEqual([ { type: 'ImportDeclaration', value: 'XY' }, @@ -470,7 +470,7 @@ test('it never returns a separation at the top of the list (leading separator)', importOrderSortSpecifiers: false, importOrderSideEffects: true, importOrderSortByLength: null, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }); expect(getSeparationData(sorted)).toEqual([ { type: 'ImportDeclaration', value: './test' }, @@ -492,7 +492,7 @@ test('it never returns a separation at the top of the list (zero preceding impor importOrderSortSpecifiers: false, importOrderSideEffects: true, importOrderSortByLength: null, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }); expect(getSeparationData(sorted)).toEqual([ { type: 'ImportDeclaration', value: './test' }, diff --git a/src/utils/__tests__/get-sorted-nodes.spec.ts b/src/utils/__tests__/get-sorted-nodes.spec.ts index 0bb32167..aeac6ecf 100644 --- a/src/utils/__tests__/get-sorted-nodes.spec.ts +++ b/src/utils/__tests__/get-sorted-nodes.spec.ts @@ -49,7 +49,7 @@ test('it returns all sorted nodes', () => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ @@ -96,7 +96,7 @@ test('it returns all sorted nodes case-insensitive', () => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ @@ -143,7 +143,7 @@ test('it returns all sorted nodes with sort order', () => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ @@ -190,7 +190,7 @@ test('it returns all sorted nodes with sort order case-insensitive', () => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ 'c', @@ -236,7 +236,7 @@ test('it returns all sorted import nodes with sorted import specifiers', () => { importOrderSortSpecifiers: true, importOrderSortByLength: null, importOrderSideEffects: true, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ 'XY', @@ -282,7 +282,7 @@ test('it returns all sorted import nodes with sorted import specifiers with case importOrderSortSpecifiers: true, importOrderSortByLength: null, importOrderSideEffects: true, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ 'c', @@ -328,7 +328,7 @@ test('it returns all sorted nodes with custom third party modules', () => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ 'a', @@ -355,7 +355,7 @@ test('it returns all sorted nodes with namespace specifiers at the top', () => { importOrderSortSpecifiers: false, importOrderSortByLength: null, importOrderSideEffects: true, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ @@ -383,7 +383,7 @@ test('it returns all sorted nodes, sorted shortest to longest', () => { importOrderSortSpecifiers: false, importOrderSideEffects: true, importOrderSortByLength: 'asc', - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ 'g', @@ -410,7 +410,7 @@ test('it returns all sorted nodes, sorted longest to shortest', () => { importOrderSortSpecifiers: false, importOrderSideEffects: false, importOrderSortByLength: 'desc', - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ 't', @@ -439,7 +439,7 @@ test('it returns all sorted nodes with types', () => { importOrderSortSpecifiers: false, importOrderSideEffects: true, importOrderSortByLength: null, - importOrderCombineImportSpecifiers: false, + importOrderMergeImports: false, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ diff --git a/src/utils/get-sorted-nodes-by-import-order.ts b/src/utils/get-sorted-nodes-by-import-order.ts index 0c57048f..72bb5d37 100644 --- a/src/utils/get-sorted-nodes-by-import-order.ts +++ b/src/utils/get-sorted-nodes-by-import-order.ts @@ -28,7 +28,7 @@ export const getSortedNodesByImportOrder: GetSortedNodes = (nodes, options) => { importOrderSeparation, importOrderSortSpecifiers, importOrderGroupNamespaceSpecifiers, - importOrderCombineImportSpecifiers, + importOrderMergeImports, } = options; const originalNodes = nodes.map(clone); @@ -79,7 +79,7 @@ export const getSortedNodesByImportOrder: GetSortedNodes = (nodes, options) => { }); // Combine import declarations with the same source into a single import declaration - if (importOrderCombineImportSpecifiers) { + if (importOrderMergeImports) { sortedInsideGroup = getMergedSpecifiers(sortedInsideGroup); } diff --git a/types/index.d.ts b/types/index.d.ts index 657fe63a..87a1e786 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -135,7 +135,7 @@ used to order imports within each match group. importOrderExclude?: string[]; /** - * Combines multiple import declarations into a single import declaration, combining all their specifiers. + * Merges multiple import declarations into a single import declaration, combining all their specifiers. * * For example, provided the following input: * ```js @@ -155,7 +155,7 @@ used to order imports within each match group. * * @default false */ - importOrderCombineImportSpecifiers?: boolean; + importOrderMergeImports?: boolean; } export type PrettierConfig = PluginConfig & Config; From 8c3f9048ca174217fb239b3193e42dcb2afeedc0 Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Wed, 25 Mar 2026 21:00:04 +0000 Subject: [PATCH 6/8] update README --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 6762e95e..9d0e153d 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,30 @@ A boolean value to enable or disable sorting of the specifiers in an import decl A boolean value to enable or disable sorting the namespace specifiers to the top of the import group. +#### `importOrderMergeImports` + +**type**: `boolean` + +**default value:** `false` + +Merges multiple import declarations into a single import declaration, combining all their specifiers. + +Initial file: +```js +import * as NamespacedImport from './example'; +import DefaultImport from './example'; +import { NamedImport1 } from './example'; +import { NamedImport2 } from './example'; +import { NamedImport3 } from './example'; +``` + +When sorted +```js +// namespaced imports are ignored as they cannot contain other specifier types +import * as NamespacedImport from './example'; +import DefaultImport, { NamedImport1, NamedImport2, NamedImport3 } from './example'; +``` + #### `importOrderCaseInsensitive` **type**: `boolean` From d266ca46c2c7c3467611d4d7bbd5eae8b42826bc Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Wed, 25 Mar 2026 21:02:16 +0000 Subject: [PATCH 7/8] update name in config too --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 774be35e..107d7418 100644 --- a/src/index.ts +++ b/src/index.ts @@ -93,7 +93,7 @@ const options: Options = { default: 'with', description: 'Provide a keyword for import attributes', }, - importOrderCombineImportSpecifiers: { + importOrderMergeImports: { type: 'boolean', category: 'Global', default: false, From 47f69555627b371ecb3a6e88dd2d4adb26715f1d Mon Sep 17 00:00:00 2001 From: AdrianGonz97 <31664583+AdrianGonz97@users.noreply.github.com> Date: Fri, 27 Mar 2026 00:07:46 +0000 Subject: [PATCH 8/8] add comments and tweak naming --- src/utils/get-merged-specifiers.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/utils/get-merged-specifiers.ts b/src/utils/get-merged-specifiers.ts index 88542c7f..b29b29ed 100644 --- a/src/utils/get-merged-specifiers.ts +++ b/src/utils/get-merged-specifiers.ts @@ -1,25 +1,30 @@ import type { ImportDeclaration } from '@babel/types'; +/** + * This function merges the specifiers of import nodes that have the same source + * into a single import declaration. + * @param node All imports nodes that should be merged. + */ export function getMergedSpecifiers(nodes: ImportDeclaration[]) { - // Combine import specifiers const merged = nodes.reduce((acc, node) => { if (node.specifiers.length === 0) { acc.push(node); return acc; } - const exists = acc.find((n) => { - return ( + const nodeToMerge = acc.find( + (n) => n.source.value === node.source.value && // ignore import if it's a namespace specifier - n.specifiers[0]?.type !== 'ImportNamespaceSpecifier' - ); - }); - if (exists) { - exists.specifiers.push(...node.specifiers); + n.specifiers[0]?.type !== 'ImportNamespaceSpecifier', + ); + + if (nodeToMerge) { + nodeToMerge.specifiers.push(...node.specifiers); } else { acc.push(node); } + return acc; }, []);