From d4df2f0950f7de3af912cabc5fbedf29a54b989d Mon Sep 17 00:00:00 2001 From: Frederic Barthelemy Date: Wed, 18 May 2022 12:26:30 -0700 Subject: [PATCH 01/22] Add TypeScript enforcement to not-forgetting our Options schema! --- src/index.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 17e7e7b..953f6a3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,24 @@ +import type { RequiredOptions as PrettierRequiredOptions } from 'prettier'; import { parsers as babelParsers } from 'prettier/parser-babel'; import { parsers as flowParsers } from 'prettier/parser-flow'; import { parsers as typescriptParsers } from 'prettier/parser-typescript'; import { preprocessor } from './preprocessor'; +import type { PrettierOptions } from './types'; -const options = { +// Not sure what the type from Prettier should be, but this is a good enough start. +interface PrettierOptionSchema { + type: string; + category: 'Global'; + array?: boolean; + default: unknown; + description: string; +} + +const options: Record< + Exclude, + PrettierOptionSchema +> = { importOrder: { type: 'path', category: 'Global', From cf4e6af3093740dd1b035c3d798ae8ddb7af10ca Mon Sep 17 00:00:00 2001 From: Frederic Barthelemy Date: Wed, 18 May 2022 12:37:20 -0700 Subject: [PATCH 02/22] Change syntax of getCodeFromAst to allow for node deletion --- src/preprocessor.ts | 20 ++++--- src/types.ts | 8 +-- .../get-all-comments-from-nodes.spec.ts | 4 +- src/utils/__tests__/get-code-from-ast.spec.ts | 12 ++-- .../get-sorted-nodes-by-import-order.spec.ts | 56 +++++++++---------- src/utils/__tests__/get-sorted-nodes.spec.ts | 4 +- .../remove-nodes-from-original-code.spec.ts | 4 +- src/utils/get-code-from-ast.ts | 21 +++++-- 8 files changed, 74 insertions(+), 55 deletions(-) diff --git a/src/preprocessor.ts b/src/preprocessor.ts index 285c063..1a3f10a 100644 --- a/src/preprocessor.ts +++ b/src/preprocessor.ts @@ -11,11 +11,11 @@ export function preprocessor(code: string, options: PrettierOptions): string { const { importOrderParserPlugins, importOrder, + importOrderBuiltinModulesToTop, importOrderCaseInsensitive, - importOrderSeparation, importOrderGroupNamespaceSpecifiers, + importOrderSeparation, importOrderSortSpecifiers, - importOrderBuiltinModulesToTop, } = options; const importNodes: ImportDeclaration[] = []; @@ -40,19 +40,25 @@ export function preprocessor(code: string, options: PrettierOptions): string { }, }); - // short-circuit if there are no import declaration + // short-circuit if there are no import declarations if (importNodes.length === 0) { return code; } - const allImports = getSortedNodes(importNodes, { + const remainingImports = getSortedNodes(importNodes, { importOrder, + importOrderBuiltinModulesToTop, importOrderCaseInsensitive, - importOrderSeparation, importOrderGroupNamespaceSpecifiers, + importOrderSeparation, importOrderSortSpecifiers, - importOrderBuiltinModulesToTop, }); - return getCodeFromAst(allImports, code, directives, interpreter); + return getCodeFromAst({ + nodes: remainingImports, + importNodes, + originalCode: code, + directives, + interpreter, + }); } diff --git a/src/types.ts b/src/types.ts index 8e8567c..078afa5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,11 +5,11 @@ export interface PrettierOptions extends RequiredOptions { importOrder: string[]; importOrderCaseInsensitive: boolean; importOrderBuiltinModulesToTop: boolean; - // should be of type ParserPlugin from '@babel/parser' but prettier does not support nested arrays in options - importOrderParserPlugins: string[]; - importOrderSeparation: boolean; importOrderGroupNamespaceSpecifiers: boolean; + importOrderSeparation: boolean; importOrderSortSpecifiers: boolean; + // should be of type ParserPlugin from '@babel/parser' but prettier does not support nested arrays in options + importOrderParserPlugins: string[]; } export interface ImportChunk { @@ -27,8 +27,8 @@ export type GetSortedNodes = ( | 'importOrder' | 'importOrderBuiltinModulesToTop' | 'importOrderCaseInsensitive' - | 'importOrderSeparation' | 'importOrderGroupNamespaceSpecifiers' + | 'importOrderSeparation' | 'importOrderSortSpecifiers' >, ) => ImportOrLine[]; 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 1d543aa..cf73249 100644 --- a/src/utils/__tests__/get-all-comments-from-nodes.spec.ts +++ b/src/utils/__tests__/get-all-comments-from-nodes.spec.ts @@ -10,11 +10,11 @@ const getSortedImportNodes = (code: string, options?: ParserOptions) => { return getSortedNodes(importNodes, { importOrder: [], + importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: false, - importOrderSeparation: false, importOrderGroupNamespaceSpecifiers: false, + importOrderSeparation: false, importOrderSortSpecifiers: false, - importOrderBuiltinModulesToTop: false, }); }; diff --git a/src/utils/__tests__/get-code-from-ast.spec.ts b/src/utils/__tests__/get-code-from-ast.spec.ts index e1209fe..8b63f75 100644 --- a/src/utils/__tests__/get-code-from-ast.spec.ts +++ b/src/utils/__tests__/get-code-from-ast.spec.ts @@ -4,7 +4,7 @@ import { getCodeFromAst } from '../get-code-from-ast'; import { getImportNodes } from '../get-import-nodes'; import { getSortedNodes } from '../get-sorted-nodes'; -test('it sorts imports correctly', () => { +it('sorts imports correctly', () => { const code = `// first comment // second comment import z from 'z'; @@ -17,13 +17,17 @@ import a from 'a'; const importNodes = getImportNodes(code); const sortedNodes = getSortedNodes(importNodes, { importOrder: [], + importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: false, - importOrderSeparation: false, importOrderGroupNamespaceSpecifiers: false, + importOrderSeparation: false, importOrderSortSpecifiers: false, - importOrderBuiltinModulesToTop: false, }); - const formatted = getCodeFromAst(sortedNodes, code, [], undefined); + const formatted = getCodeFromAst({ + nodes: sortedNodes, + originalCode: code, + directives: [], + }); expect(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 b9c7e70..3e7ee95 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 @@ -28,11 +28,11 @@ test('it returns all sorted nodes', () => { const result = getImportNodes(code); const sorted = getSortedNodesByImportOrder(result, { importOrder: ['^[./]'], + importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: false, - importOrderSeparation: false, importOrderGroupNamespaceSpecifiers: false, + importOrderSeparation: false, importOrderSortSpecifiers: false, - importOrderBuiltinModulesToTop: false, }) as ImportDeclaration[]; expect(getSortedNodesNamesAndNewlines(sorted)).toEqual([ @@ -81,11 +81,11 @@ test('it returns all sorted nodes case-insensitive', () => { const result = getImportNodes(code); const sorted = getSortedNodesByImportOrder(result, { importOrder: ['^[./]'], + importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: true, - importOrderSeparation: false, importOrderGroupNamespaceSpecifiers: false, + importOrderSeparation: false, importOrderSortSpecifiers: false, - importOrderBuiltinModulesToTop: false, }) as ImportDeclaration[]; expect(getSortedNodesNamesAndNewlines(sorted)).toEqual([ @@ -134,11 +134,11 @@ test('it returns all sorted nodes with sort order', () => { const result = getImportNodes(code); const sorted = getSortedNodesByImportOrder(result, { importOrder: ['^a$', '^t$', '^k$', '^B', '^[./]'], + importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: false, - importOrderSeparation: false, importOrderGroupNamespaceSpecifiers: false, + importOrderSeparation: false, importOrderSortSpecifiers: false, - importOrderBuiltinModulesToTop: false, }) as ImportDeclaration[]; expect(getSortedNodesNamesAndNewlines(sorted)).toEqual([ @@ -187,11 +187,11 @@ test('it returns all sorted nodes with sort order case-insensitive', () => { const result = getImportNodes(code); const sorted = getSortedNodesByImportOrder(result, { importOrder: ['^a$', '^t$', '^k$', '^B', '^[./]'], + importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: true, - importOrderSeparation: false, importOrderGroupNamespaceSpecifiers: false, + importOrderSeparation: false, importOrderSortSpecifiers: false, - importOrderBuiltinModulesToTop: false, }) as ImportDeclaration[]; expect(getSortedNodesNamesAndNewlines(sorted)).toEqual([ 'c', @@ -239,11 +239,11 @@ test('it returns all sorted import nodes with sorted import specifiers', () => { const result = getImportNodes(code); const sorted = getSortedNodesByImportOrder(result, { importOrder: ['^a$', '^t$', '^k$', '^B', '^[./]'], + importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: false, - importOrderSeparation: false, importOrderGroupNamespaceSpecifiers: false, + importOrderSeparation: false, importOrderSortSpecifiers: true, - importOrderBuiltinModulesToTop: false, }) as ImportDeclaration[]; expect(getSortedNodesNamesAndNewlines(sorted)).toEqual([ 'XY', @@ -291,11 +291,11 @@ test('it returns all sorted import nodes with sorted import specifiers with case const result = getImportNodes(code); const sorted = getSortedNodesByImportOrder(result, { importOrder: ['^a$', '^t$', '^k$', '^B', '^[./]'], + importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: true, - importOrderSeparation: false, importOrderGroupNamespaceSpecifiers: false, + importOrderSeparation: false, importOrderSortSpecifiers: true, - importOrderBuiltinModulesToTop: false, }) as ImportDeclaration[]; expect(getSortedNodesNamesAndNewlines(sorted)).toEqual([ 'c', @@ -343,11 +343,11 @@ test('it returns all sorted nodes with custom third party modules', () => { const result = getImportNodes(code); const sorted = getSortedNodesByImportOrder(result, { importOrder: ['^a$', '', '^t$', '^k$', '^[./]'], - importOrderSeparation: false, + importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, + importOrderSeparation: false, importOrderSortSpecifiers: false, - importOrderBuiltinModulesToTop: false, }) as ImportDeclaration[]; expect(getSortedNodesNamesAndNewlines(sorted)).toEqual([ 'a', @@ -372,11 +372,11 @@ test('it returns all sorted nodes with namespace specifiers at the top', () => { const result = getImportNodes(code); const sorted = getSortedNodesByImportOrder(result, { importOrder: ['^[./]'], + importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: false, - importOrderSeparation: false, importOrderGroupNamespaceSpecifiers: true, + importOrderSeparation: false, importOrderSortSpecifiers: false, - importOrderBuiltinModulesToTop: false, }) as ImportDeclaration[]; expect(getSortedNodesNamesAndNewlines(sorted)).toEqual([ @@ -402,11 +402,11 @@ test('it returns all sorted nodes with builtin specifiers at the top, ', () => { const result = getImportNodes(code); const sorted = getSortedNodesByImportOrder(result, { importOrder: ['^[./]'], + importOrderBuiltinModulesToTop: true, importOrderCaseInsensitive: false, - importOrderSeparation: false, importOrderGroupNamespaceSpecifiers: false, + importOrderSeparation: false, importOrderSortSpecifiers: false, - importOrderBuiltinModulesToTop: true, }) as ImportDeclaration[]; expect(getSortedNodesNamesAndNewlines(sorted)).toEqual([ @@ -432,11 +432,11 @@ test('it returns all sorted nodes with custom third party modules and builtins a const result = getImportNodes(code); const sorted = getSortedNodesByImportOrder(result, { importOrder: ['^a$', '', '^t$', '^k$', '^[./]'], - importOrderSeparation: false, + importOrderBuiltinModulesToTop: true, importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, + importOrderSeparation: false, importOrderSortSpecifiers: false, - importOrderBuiltinModulesToTop: true, }) as ImportDeclaration[]; expect(getSortedNodesNamesAndNewlines(sorted)).toEqual([ 'node:fs/promises', @@ -461,11 +461,11 @@ test('it adds newlines when importOrderSeparation is true', () => { const result = getImportNodes(code); const sorted = getSortedNodesByImportOrder(result, { importOrder: ['^[./]'], - importOrderSeparation: true, + importOrderBuiltinModulesToTop: true, importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, + importOrderSeparation: true, importOrderSortSpecifiers: false, - importOrderBuiltinModulesToTop: true, }) as ImportDeclaration[]; expect(getSortedNodesNamesAndNewlines(sorted)).toEqual([ 'node:fs/promises', @@ -500,11 +500,11 @@ test('it returns all sorted nodes with custom separation', () => { '^k$', '^[./]', ], - importOrderSeparation: false, + importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, + importOrderSeparation: false, importOrderSortSpecifiers: false, - importOrderBuiltinModulesToTop: false, }) as ImportDeclaration[]; expect(getSortedNodesNamesAndNewlines(sorted)).toEqual([ 'a', @@ -537,11 +537,11 @@ test('it allows both importOrderSeparation and custom separation (but why?)', () '^k$', '^[./]', ], - importOrderSeparation: true, + importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, + importOrderSeparation: true, importOrderSortSpecifiers: false, - importOrderBuiltinModulesToTop: false, }) as ImportDeclaration[]; expect(getSortedNodesNamesAndNewlines(sorted)).toEqual([ 'a', @@ -580,11 +580,11 @@ test('it does not add multiple custom import separators', () => { '^k$', '^[./]', ], - importOrderSeparation: false, + importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, + importOrderSeparation: false, importOrderSortSpecifiers: false, - importOrderBuiltinModulesToTop: false, }) as ImportDeclaration[]; expect(getSortedNodesNamesAndNewlines(sorted)).toEqual([ 'a', diff --git a/src/utils/__tests__/get-sorted-nodes.spec.ts b/src/utils/__tests__/get-sorted-nodes.spec.ts index cfe19b1..ced8207 100644 --- a/src/utils/__tests__/get-sorted-nodes.spec.ts +++ b/src/utils/__tests__/get-sorted-nodes.spec.ts @@ -29,11 +29,11 @@ test('it returns all sorted nodes, preserving the order side effect nodes', () = const result = getImportNodes(code); const sorted = getSortedNodes(result, { importOrder: [], + importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: false, - importOrderSeparation: false, importOrderGroupNamespaceSpecifiers: false, + importOrderSeparation: false, importOrderSortSpecifiers: false, - importOrderBuiltinModulesToTop: false, }) as ImportDeclaration[]; expect(getSortedNodesNamesAndNewlines(sorted)).toEqual([ 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 40c81d4..4dbf67f 100644 --- a/src/utils/__tests__/remove-nodes-from-original-code.spec.ts +++ b/src/utils/__tests__/remove-nodes-from-original-code.spec.ts @@ -23,11 +23,11 @@ test('it should remove nodes from the original code', () => { const importNodes = getImportNodes(code); const sortedNodes = getSortedNodes(importNodes, { importOrder: [], + importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: false, - importOrderSeparation: false, importOrderGroupNamespaceSpecifiers: false, + importOrderSeparation: false, importOrderSortSpecifiers: false, - importOrderBuiltinModulesToTop: false, }); const allCommentsFromImports = getAllCommentsFromNodes(sortedNodes); diff --git a/src/utils/get-code-from-ast.ts b/src/utils/get-code-from-ast.ts index 753c046..2b13f34 100644 --- a/src/utils/get-code-from-ast.ts +++ b/src/utils/get-code-from-ast.ts @@ -9,23 +9,32 @@ import { removeNodesFromOriginalCode } from './remove-nodes-from-original-code'; * This function generate a code string from the passed nodes. * @param nodes All imports, in the sorted order in which they should appear in * the generated code. + * @param importNodes All nodes that were originally relevant. (This includes nodes that need to be deleted!) * @param originalCode The original input code that was passed to this plugin. * @param directives All directive prologues from the original code (e.g. * `"use strict";`). * @param interpreter Optional interpreter directives, if present (e.g. * `#!/bin/node`). */ -export const getCodeFromAst = ( - nodes: Statement[], - originalCode: string, - directives: Directive[], - interpreter?: InterpreterDirective | null, -) => { +export const getCodeFromAst = ({ + nodes, + importNodes = nodes, + originalCode, + directives, + interpreter, +}: { + nodes: Statement[]; + importNodes?: Statement[]; + originalCode: string; + directives: Directive[]; + interpreter?: InterpreterDirective | null; +}) => { const allCommentsFromImports = getAllCommentsFromNodes(nodes); const allCommentsFromDirectives = getAllCommentsFromNodes(directives); const nodesToRemoveFromCode = [ ...nodes, + ...importNodes, ...allCommentsFromImports, ...allCommentsFromDirectives, ...(interpreter ? [interpreter] : []), From f03f96b17d06e3e47ac462a6b294894d8baea78a Mon Sep 17 00:00:00 2001 From: Frederic Barthelemy Date: Wed, 18 May 2022 12:41:17 -0700 Subject: [PATCH 03/22] Github-linguist doesn't understand `ecmascript 6` Just use javascript or js --- README.md | 2 +- docs/TROUBLESHOOTING.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6f4ed53..78753a8 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ yarn add --dev @ianvs/prettier-plugin-sort-imports Add an order in prettier config file. -```ecmascript 6 +```javascript module.exports = { "printWidth": 80, "tabWidth": 4, diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index e0093c7..f0cf919 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -7,7 +7,7 @@ You can define the RegEx in the `importOrder`. For example if you want to sort the following imports: -```ecmascript 6 +```javascript import React from 'react'; import classnames from 'classnames'; import z from '@server/z'; @@ -20,7 +20,7 @@ import q from '@ui/q'; then the `importOrder` would be `["^@ui/(.*)$","^@server/(.*)$", '^[./]']`. Now, the final output would be as follows: -```ecmascript 6 +```javascript import classnames from 'classnames'; import React from 'react'; import p from '@ui/p'; @@ -35,7 +35,7 @@ import s from './'; You can define the `` special word in the `importOrder`. For example above, the `importOrder` would be like `["^@ui/(.*)$", "^@server/(.*)$", "", '^[./]']`. Now, the final output would be as follows: -```ecmascript 6 +```javascript import p from '@ui/p'; import q from '@ui/q'; import a from '@server/a'; From 6c471667582e4743b74b3b205c522bab17184468 Mon Sep 17 00:00:00 2001 From: Frederic Barthelemy Date: Wed, 18 May 2022 12:49:07 -0700 Subject: [PATCH 04/22] Feature: `importOrderMergeDuplicateImports` - Fixes #4 --- README.md | 18 +- src/constants.ts | 11 ++ src/index.ts | 6 + src/preprocessor.ts | 2 + src/types.ts | 8 + .../get-all-comments-from-nodes.spec.ts | 1 + src/utils/__tests__/get-code-from-ast.spec.ts | 41 +++++ .../get-import-flavor-of-node.spec.ts | 32 ++++ .../get-sorted-nodes-by-import-order.spec.ts | 14 ++ src/utils/__tests__/get-sorted-nodes.spec.ts | 1 + .../merge-nodes-with-matching-flavors.spec.ts | 162 +++++++++++++++++ .../remove-nodes-from-original-code.spec.ts | 1 + src/utils/get-import-flavor-of-node.ts | 29 +++ src/utils/get-sorted-nodes.ts | 15 +- .../merge-nodes-with-matching-flavors.ts | 166 ++++++++++++++++++ 15 files changed, 499 insertions(+), 8 deletions(-) create mode 100644 src/utils/__tests__/get-import-flavor-of-node.spec.ts create mode 100644 src/utils/__tests__/merge-nodes-with-matching-flavors.spec.ts create mode 100644 src/utils/get-import-flavor-of-node.ts create mode 100644 src/utils/merge-nodes-with-matching-flavors.ts diff --git a/README.md b/README.md index 78753a8..ad8bb9b 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ yarn add --dev @ianvs/prettier-plugin-sort-imports ## Usage -Add an order in prettier config file. +Add your preferred settings in your prettier config file. ```javascript module.exports = { @@ -96,11 +96,17 @@ module.exports = { "singleQuote": true, "semi": true, "importOrder": ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"], + "importOrderBuiltinModulesToTop": true, + "importOrderCaseInsensitive": true, + "importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"], + "importOrderMergeDuplicateImports": true, "importOrderSeparation": true, - "importOrderSortSpecifiers": true + "importOrderSortSpecifiers": true, } ``` +_Note: all flags are off by default, so explore your options [below](#apis)_ + ### APIs #### Prevent imports from being sorted @@ -204,6 +210,14 @@ import ExamplesList from './ExamplesList'; import ExampleView from './ExampleView'; ``` +#### `importOrderMergeDuplicateImports` + +**type**: `boolean` + +**default value:** `false` + +A boolean value to enable or disable multiple import statements referencing the same source. Not all patterns can be merged! Notably: `import type …` will not be converted to `import {type …` or vice-versa. + #### `importOrderParserPlugins` **type**: `Array` diff --git a/src/constants.ts b/src/constants.ts index 9d378d2..1996894 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -11,6 +11,17 @@ export const newLineCharacters = '\n\n'; export const chunkTypeUnsortable = 'unsortable'; export const chunkTypeOther = 'other'; +/** import Thing from ... or import {Thing} from ... */ +export const importFlavorRegular = 'regular'; +/** import type {} from ... */ +export const importFlavorType = 'type'; +export const importFlavorSideEffect = 'side-effect'; +export const importFlavorIgnore = 'prettier-ignore'; +export const mergeableImportFlavors = [ + importFlavorRegular, + importFlavorType, +] as const; + /* * Used to mark the position between RegExps, * where the not matched imports should be placed diff --git a/src/index.ts b/src/index.ts index 953f6a3..8349218 100644 --- a/src/index.ts +++ b/src/index.ts @@ -66,6 +66,12 @@ const options: Record< default: false, description: 'Should node-builtins be hoisted to the top?', }, + importOrderMergeDuplicateImports: { + type: 'boolean', + category: 'Global', + default: false, + description: 'Should duplicate imports be merged?', + }, }; module.exports = { diff --git a/src/preprocessor.ts b/src/preprocessor.ts index 1a3f10a..d751d67 100644 --- a/src/preprocessor.ts +++ b/src/preprocessor.ts @@ -14,6 +14,7 @@ export function preprocessor(code: string, options: PrettierOptions): string { importOrderBuiltinModulesToTop, importOrderCaseInsensitive, importOrderGroupNamespaceSpecifiers, + importOrderMergeDuplicateImports, importOrderSeparation, importOrderSortSpecifiers, } = options; @@ -50,6 +51,7 @@ export function preprocessor(code: string, options: PrettierOptions): string { importOrderBuiltinModulesToTop, importOrderCaseInsensitive, importOrderGroupNamespaceSpecifiers, + importOrderMergeDuplicateImports, importOrderSeparation, importOrderSortSpecifiers, }); diff --git a/src/types.ts b/src/types.ts index 078afa5..b75e34a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,6 +6,7 @@ export interface PrettierOptions extends RequiredOptions { importOrderCaseInsensitive: boolean; importOrderBuiltinModulesToTop: boolean; importOrderGroupNamespaceSpecifiers: boolean; + importOrderMergeDuplicateImports: boolean; importOrderSeparation: boolean; importOrderSortSpecifiers: boolean; // should be of type ParserPlugin from '@babel/parser' but prettier does not support nested arrays in options @@ -28,9 +29,16 @@ export type GetSortedNodes = ( | 'importOrderBuiltinModulesToTop' | 'importOrderCaseInsensitive' | 'importOrderGroupNamespaceSpecifiers' + | 'importOrderMergeDuplicateImports' | 'importOrderSeparation' | 'importOrderSortSpecifiers' >, ) => ImportOrLine[]; export type GetChunkTypeOfNode = (node: ImportDeclaration) => string; + +export type GetImportFlavorOfNode = (node: ImportDeclaration) => string; + +export type MergeNodesWithMatchingImportFlavors = ( + nodes: ImportDeclaration[], +) => 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 cf73249..afb782e 100644 --- a/src/utils/__tests__/get-all-comments-from-nodes.spec.ts +++ b/src/utils/__tests__/get-all-comments-from-nodes.spec.ts @@ -13,6 +13,7 @@ const getSortedImportNodes = (code: string, options?: ParserOptions) => { importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, + importOrderMergeDuplicateImports: 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 8b63f75..fd6207b 100644 --- a/src/utils/__tests__/get-code-from-ast.spec.ts +++ b/src/utils/__tests__/get-code-from-ast.spec.ts @@ -20,6 +20,7 @@ import a from 'a'; importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, + importOrderMergeDuplicateImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }); @@ -40,3 +41,43 @@ import z from "z"; `, ); }); + +it('merges duplicate imports correctly', () => { + const code = `// first comment +// second comment +import z from 'z'; +import c from 'c'; +import g from 'g'; +import t from 't'; +import k from 'k'; +import a from 'a'; +import {b} from 'a'; +`; + const importNodes = getImportNodes(code); + const sortedNodes = getSortedNodes(importNodes, { + importOrder: [], + importOrderBuiltinModulesToTop: false, + importOrderCaseInsensitive: false, + importOrderGroupNamespaceSpecifiers: false, + importOrderMergeDuplicateImports: true, + importOrderSeparation: false, + importOrderSortSpecifiers: false, + }); + const formatted = getCodeFromAst({ + nodes: sortedNodes, + importNodes, + originalCode: code, + directives: [], + }); + expect(format(formatted, { parser: 'babel' })).toEqual( + `// first comment +// second comment +import a, { b } from "a"; +import c from "c"; +import g from "g"; +import k from "k"; +import t from "t"; +import z from "z"; +`, + ); +}); diff --git a/src/utils/__tests__/get-import-flavor-of-node.spec.ts b/src/utils/__tests__/get-import-flavor-of-node.spec.ts new file mode 100644 index 0000000..ba67573 --- /dev/null +++ b/src/utils/__tests__/get-import-flavor-of-node.spec.ts @@ -0,0 +1,32 @@ +import { getImportFlavorOfNode } from '../get-import-flavor-of-node'; +import { getImportNodes } from '../get-import-nodes'; + +it('should correctly classify a bunch of import expressions', () => { + expect( + getImportNodes( + ` +import "./side-effects"; +import { a } from "a"; +import type { b } from "b"; +import { type C } from "c"; +import D from "d"; + +import e = require("e"); // Doesn't count as import +const f = require("f"); // Doesn't count as import + +// prettier-ignore +import { g } from "g"; +`, + { plugins: ['typescript'] }, + ).map((node) => getImportFlavorOfNode(node)), + ).toMatchInlineSnapshot(` + Array [ + "side-effect", + "regular", + "type", + "regular", + "regular", + "prettier-ignore", + ] + `); +}); 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 3e7ee95..3d7fa56 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 @@ -31,6 +31,7 @@ test('it returns all sorted nodes', () => { importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, + importOrderMergeDuplicateImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -84,6 +85,7 @@ test('it returns all sorted nodes case-insensitive', () => { importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, + importOrderMergeDuplicateImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -137,6 +139,7 @@ test('it returns all sorted nodes with sort order', () => { importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, + importOrderMergeDuplicateImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -190,6 +193,7 @@ test('it returns all sorted nodes with sort order case-insensitive', () => { importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, + importOrderMergeDuplicateImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -242,6 +246,7 @@ test('it returns all sorted import nodes with sorted import specifiers', () => { importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, + importOrderMergeDuplicateImports: false, importOrderSeparation: false, importOrderSortSpecifiers: true, }) as ImportDeclaration[]; @@ -294,6 +299,7 @@ test('it returns all sorted import nodes with sorted import specifiers with case importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, + importOrderMergeDuplicateImports: false, importOrderSeparation: false, importOrderSortSpecifiers: true, }) as ImportDeclaration[]; @@ -346,6 +352,7 @@ test('it returns all sorted nodes with custom third party modules', () => { importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, + importOrderMergeDuplicateImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -375,6 +382,7 @@ test('it returns all sorted nodes with namespace specifiers at the top', () => { importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: true, + importOrderMergeDuplicateImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -405,6 +413,7 @@ test('it returns all sorted nodes with builtin specifiers at the top, ', () => { importOrderBuiltinModulesToTop: true, importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, + importOrderMergeDuplicateImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -435,6 +444,7 @@ test('it returns all sorted nodes with custom third party modules and builtins a importOrderBuiltinModulesToTop: true, importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, + importOrderMergeDuplicateImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -464,6 +474,7 @@ test('it adds newlines when importOrderSeparation is true', () => { importOrderBuiltinModulesToTop: true, importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, + importOrderMergeDuplicateImports: false, importOrderSeparation: true, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -503,6 +514,7 @@ test('it returns all sorted nodes with custom separation', () => { importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, + importOrderMergeDuplicateImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -540,6 +552,7 @@ test('it allows both importOrderSeparation and custom separation (but why?)', () importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, + importOrderMergeDuplicateImports: false, importOrderSeparation: true, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -583,6 +596,7 @@ test('it does not add multiple custom import separators', () => { importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, + importOrderMergeDuplicateImports: 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 ced8207..586ff68 100644 --- a/src/utils/__tests__/get-sorted-nodes.spec.ts +++ b/src/utils/__tests__/get-sorted-nodes.spec.ts @@ -32,6 +32,7 @@ test('it returns all sorted nodes, preserving the order side effect nodes', () = importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, + importOrderMergeDuplicateImports: 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 new file mode 100644 index 0000000..8916eb5 --- /dev/null +++ b/src/utils/__tests__/merge-nodes-with-matching-flavors.spec.ts @@ -0,0 +1,162 @@ +import { format } from 'prettier'; + +import { getCodeFromAst } from '../get-code-from-ast'; +import { getImportNodes } from '../get-import-nodes'; +import { getSortedNodes } from '../get-sorted-nodes'; + +it('should merge duplicate imports within a given chunk', () => { + const code = ` + import type { A } from 'a'; + import { Junk } from 'junk-group-1' + import type { B } from 'a'; + import "./side-effects1"; + // C, E and D will be separated from A, B because side-effects in-between + import type { C } from 'a'; + import { D } from "a"; + import type { E } from "a"; + // prettier-ignore + import type { NoMerge1 } from "a"; + // prettier-ignore + import { NoMerge2 } from "a"; + import { H } from 'b'; + import { F } from 'a'; + // F Will be alone because prettier-ignore in-between + + import { G } from 'b'; + import * as J from 'c'; + import { Junk2 } from 'junk-group-2' + import * as K from "c"; + // * as J, * as K can't merge because both Namespaces + import {I} from "c" + import { default as Def2 } from 'd'; + import { default as Def1 } from 'd'; + import Foo1 from 'e'; + import Foo2 from 'e'; + `; + const importNodes = getImportNodes(code, { plugins: ['typescript'] }); + + const sortedNodes = getSortedNodes(importNodes, { + importOrder: [], + importOrderBuiltinModulesToTop: false, + importOrderCaseInsensitive: false, + importOrderGroupNamespaceSpecifiers: false, + importOrderMergeDuplicateImports: true, + importOrderSeparation: true, + importOrderSortSpecifiers: true, + }); + const formatted = getCodeFromAst({ + nodes: sortedNodes, + importNodes, + originalCode: code, + directives: [], + }); + + expect(format(formatted, { parser: 'babel' })).toMatchInlineSnapshot(` + "import type { A, B } from \\"a\\"; + import { Junk } from \\"junk-group-1\\"; + + import \\"./side-effects1\\"; + + // C, E and D will be separated from A, B because side-effects in-between + import type { C, E } from \\"a\\"; + import { D } from \\"a\\"; + + // prettier-ignore + import type { NoMerge1 } from \\"a\\"; + // prettier-ignore + import { NoMerge2 } from \\"a\\"; + + import { F } from \\"a\\"; + // F Will be alone because prettier-ignore in-between + import { G, H } from \\"b\\"; + import * as J from \\"c\\"; + import * as K from \\"c\\"; + // * as J, * as K can't merge because both Namespaces + import { I } from \\"c\\"; + import { default as Def1, default as Def2 } from \\"d\\"; + import Foo1 from \\"e\\"; + import Foo2 from \\"e\\"; + import { Junk2 } from \\"junk-group-2\\"; + " + `); +}); +it("doesn't merge duplicate imports if option disabled", () => { + const code = ` + import type { A } from 'a'; + import { Junk } from 'junk-group-1' + import type { B } from 'a'; + import "./side-effects1"; + // C, E and D will be separated from A, B because side-effects in-between + import type { C } from 'a'; + import { D } from "a"; + import type { E } from "a"; + // prettier-ignore + import type { NoMerge1 } from "a"; + // prettier-ignore + import { NoMerge2 } from "a"; + import { H } from 'b'; + import { F } from 'a'; + // F Will be alone because prettier-ignore in-between + + import { G } from 'b'; + import * as J from 'c'; + import { Junk2 } from 'junk-group-2' + import * as K from "c"; + // * as J, * as K can't merge because both Namespaces + import {I} from "c" + import { default as Def2 } from 'd'; + import { default as Def1 } from 'd'; + import Foo1 from 'e'; + import Foo2 from 'e'; + `; + const importNodes = getImportNodes(code, { plugins: ['typescript'] }); + + const sortedNodes = getSortedNodes(importNodes, { + importOrder: [], + importOrderBuiltinModulesToTop: false, + importOrderCaseInsensitive: false, + importOrderGroupNamespaceSpecifiers: false, + importOrderMergeDuplicateImports: false, + importOrderSeparation: true, + importOrderSortSpecifiers: true, + }); + const formatted = getCodeFromAst({ + nodes: sortedNodes, + importNodes, + originalCode: code, + directives: [], + }); + + expect(format(formatted, { parser: 'babel' })).toMatchInlineSnapshot(` + "import type { A } from \\"a\\"; + import type { B } from \\"a\\"; + import { Junk } from \\"junk-group-1\\"; + + import \\"./side-effects1\\"; + + // C, E and D will be separated from A, B because side-effects in-between + import type { C } from \\"a\\"; + import { D } from \\"a\\"; + import type { E } from \\"a\\"; + + // prettier-ignore + import type { NoMerge1 } from \\"a\\"; + // prettier-ignore + import { NoMerge2 } from \\"a\\"; + + import { F } from \\"a\\"; + import { H } from \\"b\\"; + // F Will be alone because prettier-ignore in-between + import { G } from \\"b\\"; + import * as J from \\"c\\"; + import * as K from \\"c\\"; + // * as J, * as K can't merge because both Namespaces + import { I } from \\"c\\"; + import { default as Def2 } from \\"d\\"; + import { default as Def1 } from \\"d\\"; + import Foo1 from \\"e\\"; + import Foo2 from \\"e\\"; + import { Junk2 } from \\"junk-group-2\\"; + " + `); +}); 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 4dbf67f..cced3df 100644 --- a/src/utils/__tests__/remove-nodes-from-original-code.spec.ts +++ b/src/utils/__tests__/remove-nodes-from-original-code.spec.ts @@ -26,6 +26,7 @@ test('it should remove nodes from the original code', () => { importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, + importOrderMergeDuplicateImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }); diff --git a/src/utils/get-import-flavor-of-node.ts b/src/utils/get-import-flavor-of-node.ts new file mode 100644 index 0000000..b508a2f --- /dev/null +++ b/src/utils/get-import-flavor-of-node.ts @@ -0,0 +1,29 @@ +import { + importFlavorIgnore, + importFlavorRegular, + importFlavorSideEffect, + importFlavorType, +} from '../constants'; +import type { GetImportFlavorOfNode } from '../types'; + +/** + * Classifies nodes by import-flavor, primarily informing whether the node is a candidate for merging + * + * @param node + * @returns {("prettier-ignore"|"regular"|"side-effect"|"type")} + */ +export const getImportFlavorOfNode: GetImportFlavorOfNode = (node) => { + const hasIgnoreNextNode = (node.leadingComments ?? []).some( + (comment) => comment.value.trim() === 'prettier-ignore', + ); + if (hasIgnoreNextNode) { + return importFlavorIgnore; + } + if (node.specifiers.length === 0) { + return importFlavorSideEffect; + } + if (node.importKind === 'type') { + return importFlavorType; + } + return importFlavorRegular; +}; diff --git a/src/utils/get-sorted-nodes.ts b/src/utils/get-sorted-nodes.ts index 3f0dc02..e6c3233 100644 --- a/src/utils/get-sorted-nodes.ts +++ b/src/utils/get-sorted-nodes.ts @@ -3,6 +3,7 @@ import { GetSortedNodes, ImportChunk, ImportOrLine } from '../types'; import { adjustCommentsOnSortedNodes } from './adjust-comments-on-sorted-nodes'; import { getChunkTypeOfNode } from './get-chunk-type-of-node'; import { getSortedNodesByImportOrder } from './get-sorted-nodes-by-import-order'; +import { mergeNodesWithMatchingImportFlavors } from './merge-nodes-with-matching-flavors'; /** * This function returns the given nodes, sorted in the order as indicated by @@ -16,12 +17,11 @@ import { getSortedNodesByImportOrder } from './get-sorted-nodes-by-import-order' * between the side effect nodes according to the given options. * @param nodes All import nodes that should be sorted. * @param options Options to influence the behavior of the sorting algorithm. + * + * @returns A sorted array of the remaining import nodes */ -export const getSortedNodes: GetSortedNodes = ( - nodes, - options, -): ImportOrLine[] => { - const { importOrderSeparation } = options; +export const getSortedNodes: GetSortedNodes = (nodes, options) => { + const { importOrderSeparation, importOrderMergeDuplicateImports } = options; // Split nodes at each boundary between a side-effect node and a // non-side-effect node, keeping both types of nodes together. @@ -48,8 +48,11 @@ export const getSortedNodes: GetSortedNodes = ( // do not sort side effect nodes finalNodes.push(...chunk.nodes); } else { + const nodes = importOrderMergeDuplicateImports + ? mergeNodesWithMatchingImportFlavors(chunk.nodes) + : chunk.nodes; // sort non-side effect nodes - const sorted = getSortedNodesByImportOrder(chunk.nodes, options); + const sorted = getSortedNodesByImportOrder(nodes, options); finalNodes.push(...sorted); } if (importOrderSeparation) { diff --git a/src/utils/merge-nodes-with-matching-flavors.ts b/src/utils/merge-nodes-with-matching-flavors.ts new file mode 100644 index 0000000..59fb0d2 --- /dev/null +++ b/src/utils/merge-nodes-with-matching-flavors.ts @@ -0,0 +1,166 @@ +import type { + EmptyStatement, + ImportDeclaration, + ImportDefaultSpecifier, + ImportNamespaceSpecifier, + ImportSpecifier, +} from '@babel/types'; + +import { + importFlavorRegular, + importFlavorType, + mergeableImportFlavors, +} from '../constants'; +import type { MergeNodesWithMatchingImportFlavors } from '../types'; +import { getImportFlavorOfNode } from './get-import-flavor-of-node'; + +type MergeableFlavor = typeof mergeableImportFlavors[number]; +function isMergeableFlavor(flavor: string): flavor is MergeableFlavor { + return mergeableImportFlavors.includes(flavor as MergeableFlavor); +} + +function selectMergeableNodesByImportFlavor( + nodes: ImportDeclaration[], +): Record { + return nodes.reduce( + (groups, node) => { + const flavor = getImportFlavorOfNode(node); + if (isMergeableFlavor(flavor)) { + groups[flavor].push(node); + } + return groups; + }, + { + [importFlavorRegular]: [] as ImportDeclaration[], + [importFlavorType]: [] as ImportDeclaration[], + }, + ); +} + +function selectNodeImportSource(node: ImportDeclaration) { + return node.source.value; +} + +function nodeIsImportNamespaceSpecifier( + node: ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier, +): node is ImportNamespaceSpecifier { + return node.type === 'ImportNamespaceSpecifier'; +} +function nodeIsImportDefaultSpecifier( + node: ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier, +): node is ImportDefaultSpecifier { + return node.type === 'ImportDefaultSpecifier'; +} + +/** Return false if the merge will produce an invalid result */ +function mergeIsSafe( + nodeToKeep: ImportDeclaration, + nodeToForget: ImportDeclaration, +) { + if ( + nodeToKeep.specifiers.some(nodeIsImportNamespaceSpecifier) || + nodeToForget.specifiers.some(nodeIsImportNamespaceSpecifier) + ) { + // An `import * as Foo` namespace specifier cannot be merged + // with other import expressions. + return false; + } + if ( + nodeToKeep.specifiers.some(nodeIsImportDefaultSpecifier) && + nodeToForget.specifiers.some(nodeIsImportDefaultSpecifier) + ) { + // Two `import Foo from` specifiers cannot be merged trivially. + // -- Notice: this is *not* import {default as Foo1, default as Foo2} -- that's legal! + // + // Future work could convert `import Foo1 from 'a'; import Foo2 from 'a'; + // into `import {default as Foo1, default as Foo2} from 'a';` + // But since this runs the risk of making code longer, this won't be in v1. + return false; + } + return true; +} + +/** + * @returns (true to delete node | false to keep node) + */ +function mergeNodes( + nodeToKeep: ImportDeclaration, + nodeToForget: ImportDeclaration, +) { + if (!mergeIsSafe(nodeToKeep, nodeToForget)) { + return false; + } + + nodeToKeep.specifiers.push(...nodeToForget.specifiers); + + // The line numbers will be all messed up. Is this a problem? + nodeToKeep.leadingComments = [ + ...(nodeToKeep.leadingComments || []), + ...(nodeToForget.leadingComments || []), + ]; + nodeToKeep.innerComments = [ + ...(nodeToKeep.innerComments || []), + ...(nodeToForget.innerComments || []), + ]; + nodeToKeep.trailingComments = [ + ...(nodeToKeep.trailingComments || []), + ...(nodeToForget.trailingComments || []), + ]; + + return true; +} + +/** + * Modifies context, deleteContext, + * case A: context has no node for an import source, then it's assigned. + * case B: context has a node for an import source, then it's merged, and old node is added to deleteContext. + */ +function mutateContextAndMerge({ + context, + deleteContext, + insertableNode, +}: { + context: Record; + deleteContext: ImportDeclaration[]; + insertableNode: ImportDeclaration; +}) { + const source = selectNodeImportSource(insertableNode); + if (context[source]) { + if (mergeNodes(context[source], insertableNode)) { + deleteContext.push(insertableNode); + } + } else { + context[source] = insertableNode; + } +} + +/** + * Accepts an array of nodes from a given chunk, and merges candidates that have a matching import-flavor + * + * In other words each group will be merged if they have the same source: + * - `import type` expressions from the same source + * - `import Name, {a, b}` from the same source + * + * `import type {Foo}` expressions won't be converted into `import {type Foo}` or vice versa + */ +export const mergeNodesWithMatchingImportFlavors: MergeNodesWithMatchingImportFlavors = + (input) => { + 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 = {}; + + for (const insertableNode of group) { + mutateContextAndMerge({ + context, + deleteContext: nodesToDelete, + insertableNode, + }); + } + } + + return input.filter((n) => !nodesToDelete.includes(n)); + }; From a62bcd1133998cf77b934a58621324ad0e32ea69 Mon Sep 17 00:00:00 2001 From: Frederic Barthelemy Date: Wed, 18 May 2022 13:52:44 -0700 Subject: [PATCH 05/22] Feature: `importOrderMergeTypeImportsIntoRegular` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: #4 Allows the ability for users to convert `import type` expressions into `import {type …}` expressions via a boolean flag. Should this be controlled via a string parameter? --- README.md | 25 +++++++++- src/index.ts | 7 +++ src/preprocessor.ts | 11 +++++ src/types.ts | 3 ++ .../get-all-comments-from-nodes.spec.ts | 1 + src/utils/__tests__/get-code-from-ast.spec.ts | 2 + .../get-sorted-nodes-by-import-order.spec.ts | 14 ++++++ src/utils/__tests__/get-sorted-nodes.spec.ts | 1 + .../merge-nodes-with-matching-flavors.spec.ts | 49 +++++++++++++++++++ .../remove-nodes-from-original-code.spec.ts | 1 + src/utils/get-sorted-nodes.ts | 10 +++- .../merge-nodes-with-matching-flavors.ts | 47 +++++++++++++++--- 12 files changed, 162 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ad8bb9b..f7e0f4b 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ module.exports = { "importOrderCaseInsensitive": true, "importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"], "importOrderMergeDuplicateImports": true, + "importOrderMergeTypeImportsIntoRegular": true, "importOrderSeparation": true, "importOrderSortSpecifiers": true, } @@ -216,7 +217,29 @@ import ExampleView from './ExampleView'; **default value:** `false` -A boolean value to enable or disable multiple import statements referencing the same source. Not all patterns can be merged! Notably: `import type …` will not be converted to `import {type …` or vice-versa. +A boolean value to enable or disable multiple import statements referencing the same source. Not all patterns can be merged! See also [`importOrderMergeTypeImportsIntoRegular`](#importordermergetypeimportsintoregular) + +#### `importOrderMergeTypeImportsIntoRegular` + +**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` diff --git a/src/index.ts b/src/index.ts index 8349218..eba3eeb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -72,6 +72,13 @@ const options: Record< default: false, description: 'Should duplicate imports be merged?', }, + importOrderMergeTypeImportsIntoRegular: { + 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 d751d67..17c83bb 100644 --- a/src/preprocessor.ts +++ b/src/preprocessor.ts @@ -15,10 +15,20 @@ export function preprocessor(code: string, options: PrettierOptions): string { importOrderCaseInsensitive, importOrderGroupNamespaceSpecifiers, importOrderMergeDuplicateImports, + importOrderMergeTypeImportsIntoRegular, importOrderSeparation, importOrderSortSpecifiers, } = options; + if ( + importOrderMergeTypeImportsIntoRegular && + !importOrderMergeDuplicateImports + ) { + console.warn( + "[@ianvs/prettier-plugin-sort-imports]: Option combination of both importOrderMergeTypeImportsIntoRegular: true and importOrderMergeDuplicateImports: false is not won't do anything!", + ); + } + const importNodes: ImportDeclaration[] = []; const parserOptions: ParserOptions = { sourceType: 'module', @@ -52,6 +62,7 @@ export function preprocessor(code: string, options: PrettierOptions): string { importOrderCaseInsensitive, importOrderGroupNamespaceSpecifiers, importOrderMergeDuplicateImports, + importOrderMergeTypeImportsIntoRegular, importOrderSeparation, importOrderSortSpecifiers, }); diff --git a/src/types.ts b/src/types.ts index b75e34a..3c3892e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,6 +7,7 @@ export interface PrettierOptions extends RequiredOptions { importOrderBuiltinModulesToTop: boolean; importOrderGroupNamespaceSpecifiers: boolean; importOrderMergeDuplicateImports: boolean; + importOrderMergeTypeImportsIntoRegular: boolean; importOrderSeparation: boolean; importOrderSortSpecifiers: boolean; // should be of type ParserPlugin from '@babel/parser' but prettier does not support nested arrays in options @@ -30,6 +31,7 @@ export type GetSortedNodes = ( | 'importOrderCaseInsensitive' | 'importOrderGroupNamespaceSpecifiers' | 'importOrderMergeDuplicateImports' + | 'importOrderMergeTypeImportsIntoRegular' | 'importOrderSeparation' | 'importOrderSortSpecifiers' >, @@ -41,4 +43,5 @@ export type GetImportFlavorOfNode = (node: ImportDeclaration) => string; export type MergeNodesWithMatchingImportFlavors = ( nodes: ImportDeclaration[], + options: { importOrderMergeTypeImportsIntoRegular: 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..d472e0e 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, + importOrderMergeTypeImportsIntoRegular: 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 fd6207b..1a59e32 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, + importOrderMergeTypeImportsIntoRegular: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }); @@ -60,6 +61,7 @@ import {b} from 'a'; importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: true, + importOrderMergeTypeImportsIntoRegular: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }); 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..617dddc 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, + importOrderMergeTypeImportsIntoRegular: 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, + importOrderMergeTypeImportsIntoRegular: 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, + importOrderMergeTypeImportsIntoRegular: 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, + importOrderMergeTypeImportsIntoRegular: 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, + importOrderMergeTypeImportsIntoRegular: 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, + importOrderMergeTypeImportsIntoRegular: 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, + importOrderMergeTypeImportsIntoRegular: 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, + importOrderMergeTypeImportsIntoRegular: 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, + importOrderMergeTypeImportsIntoRegular: 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, + importOrderMergeTypeImportsIntoRegular: 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, + importOrderMergeTypeImportsIntoRegular: 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, + importOrderMergeTypeImportsIntoRegular: 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, + importOrderMergeTypeImportsIntoRegular: 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, + importOrderMergeTypeImportsIntoRegular: 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..1bf2475 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, + importOrderMergeTypeImportsIntoRegular: 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 8916eb5..7799616 100644 --- a/src/utils/__tests__/merge-nodes-with-matching-flavors.spec.ts +++ b/src/utils/__tests__/merge-nodes-with-matching-flavors.spec.ts @@ -41,6 +41,7 @@ it('should merge duplicate imports within a given chunk', () => { importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: true, + importOrderMergeTypeImportsIntoRegular: false, importOrderSeparation: true, importOrderSortSpecifiers: true, }); @@ -80,6 +81,53 @@ it('should merge duplicate imports within a given chunk', () => { " `); }); +it('should type imports into regular imports if desired', () => { + 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 type { C1 } from 'c'; + import { C2 } from 'c'; + // Converts 'import type' to 'import value' if last + import { D1 } from 'd'; + import type { D2 } from 'd'; + `; + const importNodes = getImportNodes(code, { plugins: ['typescript'] }); + + const sortedNodes = getSortedNodes(importNodes, { + importOrder: [], + importOrderBuiltinModulesToTop: false, + importOrderCaseInsensitive: false, + importOrderGroupNamespaceSpecifiers: false, + importOrderMergeDuplicateImports: true, + importOrderMergeTypeImportsIntoRegular: true, + importOrderSeparation: true, + importOrderSortSpecifiers: true, + }); + const formatted = getCodeFromAst({ + nodes: sortedNodes, + importNodes, + originalCode: code, + directives: [], + }); + + expect(format(formatted, { parser: 'babel' })).toMatchInlineSnapshot(` + "// 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 { type C1, C2 } from \\"c\\"; + // Converts 'import type' to 'import value' if last + import { D1, type D2 } from \\"d\\"; + " + `); +}); + it("doesn't merge duplicate imports if option disabled", () => { const code = ` import type { A } from 'a'; @@ -117,6 +165,7 @@ it("doesn't merge duplicate imports if option disabled", () => { importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, + importOrderMergeTypeImportsIntoRegular: false, importOrderSeparation: true, importOrderSortSpecifiers: true, }); 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..c970123 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, + importOrderMergeTypeImportsIntoRegular: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }); diff --git a/src/utils/get-sorted-nodes.ts b/src/utils/get-sorted-nodes.ts index e6c3233..04a8508 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, + importOrderMergeTypeImportsIntoRegular, + } = 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, { + importOrderMergeTypeImportsIntoRegular, + }) : 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 59fb0d2..d782cbf 100644 --- a/src/utils/merge-nodes-with-matching-flavors.ts +++ b/src/utils/merge-nodes-with-matching-flavors.ts @@ -5,6 +5,7 @@ import type { ImportNamespaceSpecifier, ImportSpecifier, } from '@babel/types'; +import assert from 'assert'; import { importFlavorRegular, @@ -51,6 +52,25 @@ function nodeIsImportDefaultSpecifier( ): node is ImportDefaultSpecifier { return node.type === 'ImportDefaultSpecifier'; } +function nodeIsImportSpecifier( + node: ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier, +): node is ImportSpecifier { + return node.type === 'ImportSpecifier'; +} + +function convertImportSpecifierType(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(convertImportSpecifierType); +} /** Return false if the merge will produce an invalid result */ function mergeIsSafe( @@ -91,6 +111,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); // The line numbers will be all messed up. Is this a problem? @@ -144,14 +176,17 @@ function mutateContextAndMerge({ * `import type {Foo}` expressions won't be converted into `import {type Foo}` or vice versa */ export const mergeNodesWithMatchingImportFlavors: MergeNodesWithMatchingImportFlavors = - (input) => { + (input, { importOrderMergeTypeImportsIntoRegular }) => { 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 (!importOrderMergeTypeImportsIntoRegular) { + // Reset in loop to avoid unintended merge across variants + context = {}; + } + const group = groups[groupKey as keyof typeof groups]; for (const insertableNode of group) { mutateContextAndMerge({ From db923e7cc7cf35980cc56785a48c998e10d912e1 Mon Sep 17 00:00:00 2001 From: Frederic Barthelemy Date: Wed, 18 May 2022 14:33:07 -0700 Subject: [PATCH 06/22] merge-tests: extract defaultOptions --- .../merge-nodes-with-matching-flavors.spec.ts | 40 ++++++++----------- 1 file changed, 16 insertions(+), 24 deletions(-) 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 7799616..6507453 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, + importOrderMergeTypeImportsIntoRegular: false, + importOrderSeparation: true, + importOrderSortSpecifiers: true, +}; + it('should merge duplicate imports within a given chunk', () => { const code = ` import type { A } from 'a'; @@ -36,14 +47,9 @@ it('should merge duplicate imports within a given chunk', () => { const importNodes = getImportNodes(code, { plugins: ['typescript'] }); const sortedNodes = getSortedNodes(importNodes, { - importOrder: [], - importOrderBuiltinModulesToTop: false, - importOrderCaseInsensitive: false, - importOrderGroupNamespaceSpecifiers: false, + ...defaultOptions, importOrderMergeDuplicateImports: true, importOrderMergeTypeImportsIntoRegular: false, - importOrderSeparation: true, - importOrderSortSpecifiers: true, }); const formatted = getCodeFromAst({ nodes: sortedNodes, @@ -81,7 +87,7 @@ it('should merge duplicate imports within a given chunk', () => { " `); }); -it('should type imports into regular imports if desired', () => { +it('should merge type imports into regular imports', () => { const code = ` // Preserves 'import type' import type { A1 } from 'a'; @@ -99,14 +105,9 @@ it('should type imports into regular imports if desired', () => { const importNodes = getImportNodes(code, { plugins: ['typescript'] }); const sortedNodes = getSortedNodes(importNodes, { - importOrder: [], - importOrderBuiltinModulesToTop: false, - importOrderCaseInsensitive: false, - importOrderGroupNamespaceSpecifiers: false, + ...defaultOptions, importOrderMergeDuplicateImports: true, importOrderMergeTypeImportsIntoRegular: true, - importOrderSeparation: true, - importOrderSortSpecifiers: true, }); const formatted = getCodeFromAst({ nodes: sortedNodes, @@ -156,19 +157,10 @@ it("doesn't merge duplicate imports if option disabled", () => { import { default as Def1 } from 'd'; import Foo1 from 'e'; import Foo2 from 'e'; - `; +`; const importNodes = getImportNodes(code, { plugins: ['typescript'] }); - const sortedNodes = getSortedNodes(importNodes, { - importOrder: [], - importOrderBuiltinModulesToTop: false, - importOrderCaseInsensitive: false, - importOrderGroupNamespaceSpecifiers: false, - importOrderMergeDuplicateImports: false, - importOrderMergeTypeImportsIntoRegular: false, - importOrderSeparation: true, - importOrderSortSpecifiers: true, - }); + const sortedNodes = getSortedNodes(importNodes, defaultOptions); const formatted = getCodeFromAst({ nodes: sortedNodes, importNodes, From e2ab51d4bfbd8e7ca74fff07ea3f34df5548448c Mon Sep 17 00:00:00 2001 From: Frederic Barthelemy Date: Wed, 18 May 2022 14:33:53 -0700 Subject: [PATCH 07/22] merge-imports: Add test cases from codemod https://github.com/IanVS/type-import-codemod/blob/9f3210861a77216f07317537a04ce61467e0df7e/src/transform.test.ts --- .../merge-nodes-with-matching-flavors.spec.ts | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) 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 6507453..a7bea4e 100644 --- a/src/utils/__tests__/merge-nodes-with-matching-flavors.spec.ts +++ b/src/utils/__tests__/merge-nodes-with-matching-flavors.spec.ts @@ -128,6 +128,184 @@ it('should merge type imports into regular imports', () => { " `); }); +it('should combine type import and default import', () => { + const code = ` +import type {MyType} from './source'; +import defaultValue from './source'; +`; + const importNodes = getImportNodes(code, { plugins: ['typescript'] }); + + const sortedNodes = getSortedNodes(importNodes, { + ...defaultOptions, + importOrderMergeDuplicateImports: true, + importOrderMergeTypeImportsIntoRegular: true, + }); + const formatted = getCodeFromAst({ + nodes: sortedNodes, + importNodes, + originalCode: code, + directives: [], + }); + + expect(format(formatted, { parser: 'babel' })).toMatchInlineSnapshot(` + "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 importNodes = getImportNodes(code, { plugins: ['typescript'] }); + + const sortedNodes = getSortedNodes(importNodes, { + ...defaultOptions, + importOrderMergeDuplicateImports: true, + importOrderMergeTypeImportsIntoRegular: true, + }); + const formatted = getCodeFromAst({ + nodes: sortedNodes, + importNodes, + originalCode: code, + directives: [], + }); + + expect(format(formatted, { parser: 'babel' })).toMatchInlineSnapshot(` + "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 importNodes = getImportNodes(code, { plugins: ['typescript'] }); + + const sortedNodes = getSortedNodes(importNodes, { + ...defaultOptions, + importOrderMergeDuplicateImports: true, + importOrderMergeTypeImportsIntoRegular: true, + }); + const formatted = getCodeFromAst({ + nodes: sortedNodes, + importNodes, + originalCode: code, + directives: [], + }); + + expect(format(formatted, { parser: 'babel' })).toMatchInlineSnapshot(` + "import { type MyType, value as alias } 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 importNodes = getImportNodes(code, { plugins: ['typescript'] }); + + const sortedNodes = getSortedNodes(importNodes, { + ...defaultOptions, + importOrderMergeDuplicateImports: true, + importOrderMergeTypeImportsIntoRegular: true, + }); + const formatted = getCodeFromAst({ + nodes: sortedNodes, + importNodes, + originalCode: code, + directives: [], + }); + + expect(format(formatted, { parser: 'babel' })).toMatchInlineSnapshot(` + "import { type MyType, type SecondType, SecondValue, value } 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 importNodes = getImportNodes(code, { plugins: ['typescript'] }); + + const sortedNodes = getSortedNodes(importNodes, { + ...defaultOptions, + importOrderMergeDuplicateImports: true, + importOrderMergeTypeImportsIntoRegular: true, + }); + const formatted = getCodeFromAst({ + nodes: sortedNodes, + importNodes, + originalCode: code, + directives: [], + }); + + expect(format(formatted, { parser: 'babel' })).toMatchInlineSnapshot(` + "import { type OtherType, otherValue } from \\"./other\\"; + import { type MyType, value } 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 importNodes = getImportNodes(code, { plugins: ['typescript'] }); + + const sortedNodes = getSortedNodes(importNodes, { + ...defaultOptions, + importOrderMergeDuplicateImports: true, + importOrderMergeTypeImportsIntoRegular: true, + }); + const formatted = getCodeFromAst({ + nodes: sortedNodes, + importNodes, + originalCode: code, + directives: [], + }); + + expect(format(formatted, { parser: 'babel' })).toMatchInlineSnapshot(` + "import { type MyType, type SecondType, SecondValue, value } 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 importNodes = getImportNodes(code, { plugins: ['typescript'] }); + + const sortedNodes = getSortedNodes(importNodes, { + ...defaultOptions, + importOrderMergeDuplicateImports: true, + importOrderMergeTypeImportsIntoRegular: true, + }); + const formatted = getCodeFromAst({ + nodes: sortedNodes, + importNodes, + originalCode: code, + directives: [], + }); + + expect(format(formatted, { parser: 'babel' })).toMatchInlineSnapshot(` + "import type { OtherType } from \\"./other\\"; + import { type MyType, value } from \\"./source\\"; + import { thirdValue } from \\"./third\\"; + " + `); +}); it("doesn't merge duplicate imports if option disabled", () => { const code = ` From 9ef2abeef44b41dbd4fa3e9acc99e7390eae0dba Mon Sep 17 00:00:00 2001 From: Frederic Barthelemy Date: Thu, 19 May 2022 12:57:15 -0700 Subject: [PATCH 08/22] switch from toMatchInlineSnapshot to toEqual --- .../merge-nodes-with-matching-flavors.spec.ts | 118 +++++++++--------- 1 file changed, 58 insertions(+), 60 deletions(-) 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 8916eb5..2b3a31f 100644 --- a/src/utils/__tests__/merge-nodes-with-matching-flavors.spec.ts +++ b/src/utils/__tests__/merge-nodes-with-matching-flavors.spec.ts @@ -51,34 +51,33 @@ it('should merge duplicate imports within a given chunk', () => { directives: [], }); - expect(format(formatted, { parser: 'babel' })).toMatchInlineSnapshot(` - "import type { A, B } from \\"a\\"; - import { Junk } from \\"junk-group-1\\"; - - import \\"./side-effects1\\"; - - // C, E and D will be separated from A, B because side-effects in-between - import type { C, E } from \\"a\\"; - import { D } from \\"a\\"; - - // prettier-ignore - import type { NoMerge1 } from \\"a\\"; - // prettier-ignore - import { NoMerge2 } from \\"a\\"; - - import { F } from \\"a\\"; - // F Will be alone because prettier-ignore in-between - import { G, H } from \\"b\\"; - import * as J from \\"c\\"; - import * as K from \\"c\\"; - // * as J, * as K can't merge because both Namespaces - import { I } from \\"c\\"; - import { default as Def1, default as Def2 } from \\"d\\"; - import Foo1 from \\"e\\"; - import Foo2 from \\"e\\"; - import { Junk2 } from \\"junk-group-2\\"; - " - `); + expect(format(formatted, { parser: 'babel' })) + .toEqual(`import type { A, B } from "a"; +import { Junk } from "junk-group-1"; + +import "./side-effects1"; + +// C, E and D will be separated from A, B because side-effects in-between +import type { C, E } from "a"; +import { D } from "a"; + +// prettier-ignore +import type { NoMerge1 } from "a"; +// prettier-ignore +import { NoMerge2 } from "a"; + +import { F } from "a"; +// F Will be alone because prettier-ignore in-between +import { G, H } from "b"; +import * as J from "c"; +import * as K from "c"; +// * as J, * as K can't merge because both Namespaces +import { I } from "c"; +import { default as Def1, default as Def2 } from "d"; +import Foo1 from "e"; +import Foo2 from "e"; +import { Junk2 } from "junk-group-2"; +`); }); it("doesn't merge duplicate imports if option disabled", () => { const code = ` @@ -127,36 +126,35 @@ it("doesn't merge duplicate imports if option disabled", () => { directives: [], }); - expect(format(formatted, { parser: 'babel' })).toMatchInlineSnapshot(` - "import type { A } from \\"a\\"; - import type { B } from \\"a\\"; - import { Junk } from \\"junk-group-1\\"; - - import \\"./side-effects1\\"; - - // C, E and D will be separated from A, B because side-effects in-between - import type { C } from \\"a\\"; - import { D } from \\"a\\"; - import type { E } from \\"a\\"; - - // prettier-ignore - import type { NoMerge1 } from \\"a\\"; - // prettier-ignore - import { NoMerge2 } from \\"a\\"; - - import { F } from \\"a\\"; - import { H } from \\"b\\"; - // F Will be alone because prettier-ignore in-between - import { G } from \\"b\\"; - import * as J from \\"c\\"; - import * as K from \\"c\\"; - // * as J, * as K can't merge because both Namespaces - import { I } from \\"c\\"; - import { default as Def2 } from \\"d\\"; - import { default as Def1 } from \\"d\\"; - import Foo1 from \\"e\\"; - import Foo2 from \\"e\\"; - import { Junk2 } from \\"junk-group-2\\"; - " - `); + expect(format(formatted, { parser: 'babel' })) + .toEqual(`import type { A } from "a"; +import type { B } from "a"; +import { Junk } from "junk-group-1"; + +import "./side-effects1"; + +// C, E and D will be separated from A, B because side-effects in-between +import type { C } from "a"; +import { D } from "a"; +import type { E } from "a"; + +// prettier-ignore +import type { NoMerge1 } from "a"; +// prettier-ignore +import { NoMerge2 } from "a"; + +import { F } from "a"; +import { H } from "b"; +// F Will be alone because prettier-ignore in-between +import { G } from "b"; +import * as J from "c"; +import * as K from "c"; +// * as J, * as K can't merge because both Namespaces +import { I } from "c"; +import { default as Def2 } from "d"; +import { default as Def1 } from "d"; +import Foo1 from "e"; +import Foo2 from "e"; +import { Junk2 } from "junk-group-2"; +`); }); From 8edeb514abb6df112703ae6b55ac2ea1fe4ffd46 Mon Sep 17 00:00:00 2001 From: Frederic Barthelemy Date: Thu, 19 May 2022 13:02:21 -0700 Subject: [PATCH 09/22] switch from toMatchInlineSnapshot to toEqual --- .../merge-nodes-with-matching-flavors.spec.ts | 78 +++++++++---------- 1 file changed, 35 insertions(+), 43 deletions(-) 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 6740afe..56c9f1b 100644 --- a/src/utils/__tests__/merge-nodes-with-matching-flavors.spec.ts +++ b/src/utils/__tests__/merge-nodes-with-matching-flavors.spec.ts @@ -115,17 +115,16 @@ it('should merge type imports into regular imports', () => { directives: [], }); - expect(format(formatted, { parser: 'babel' })).toMatchInlineSnapshot(` - "// 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 { type C1, C2 } from \\"c\\"; - // Converts 'import type' to 'import value' if last - import { D1, type D2 } from \\"d\\"; - " - `); + 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 { type C1, C2 } from "c"; +// Converts 'import type' to 'import value' if last +import { D1, type D2 } from "d"; +`); }); it('should combine type import and default import', () => { const code = ` @@ -146,10 +145,9 @@ import defaultValue from './source'; directives: [], }); - expect(format(formatted, { parser: 'babel' })).toMatchInlineSnapshot(` - "import defaultValue, { type MyType } from \\"./source\\"; - " - `); + expect(format(formatted, { parser: 'babel' })) + .toEqual(`import defaultValue, { type MyType } from "./source"; +`); }); it('should not combine type import and namespace import', () => { const code = ` @@ -170,11 +168,10 @@ import * as Namespace from './source'; directives: [], }); - expect(format(formatted, { parser: 'babel' })).toMatchInlineSnapshot(` - "import type { MyType } from \\"./source\\"; - import * as Namespace from \\"./source\\"; - " - `); + expect(format(formatted, { parser: 'babel' })) + .toEqual(`import type { MyType } from "./source"; +import * as Namespace from "./source"; +`); }); it('should support aliased named imports', () => { const code = ` @@ -195,10 +192,9 @@ import {value as alias} from './source'; directives: [], }); - expect(format(formatted, { parser: 'babel' })).toMatchInlineSnapshot(` - "import { type MyType, value as alias } from \\"./source\\"; - " - `); + expect(format(formatted, { parser: 'babel' })) + .toEqual(`import { type MyType, value as alias } from "./source"; +`); }); it('should combine multiple imports from the same source', () => { const code = ` @@ -219,10 +215,9 @@ import {value, SecondValue} from './source'; directives: [], }); - expect(format(formatted, { parser: 'babel' })).toMatchInlineSnapshot(` - "import { type MyType, type SecondType, SecondValue, value } from \\"./source\\"; - " - `); + expect(format(formatted, { parser: 'babel' })) + .toEqual(`import { type MyType, type SecondType, SecondValue, value } from "./source"; +`); }); it('should combine multiple groups of imports', () => { const code = ` @@ -245,11 +240,10 @@ import {otherValue} from './other'; directives: [], }); - expect(format(formatted, { parser: 'babel' })).toMatchInlineSnapshot(` - "import { type OtherType, otherValue } from \\"./other\\"; - import { type MyType, value } from \\"./source\\"; - " - `); + expect(format(formatted, { parser: 'babel' })) + .toEqual(`import { type OtherType, otherValue } from "./other"; +import { type MyType, value } from "./source"; +`); }); it('should combine multiple imports statements from the same source', () => { const code = ` @@ -272,10 +266,9 @@ import {SecondValue} from './source'; directives: [], }); - expect(format(formatted, { parser: 'babel' })).toMatchInlineSnapshot(` - "import { type MyType, type SecondType, SecondValue, value } from \\"./source\\"; - " - `); + expect(format(formatted, { parser: 'babel' })) + .toEqual(`import { type MyType, type SecondType, SecondValue, value } from "./source"; +`); }); it('should not impact imports from different sources', () => { const code = ` @@ -298,12 +291,11 @@ import {value} from './source'; directives: [], }); - expect(format(formatted, { parser: 'babel' })).toMatchInlineSnapshot(` - "import type { OtherType } from \\"./other\\"; - import { type MyType, value } from \\"./source\\"; - import { thirdValue } from \\"./third\\"; - " - `); + expect(format(formatted, { parser: 'babel' })) + .toEqual(`import type { OtherType } from "./other"; +import { type MyType, value } from "./source"; +import { thirdValue } from "./third"; +`); }); it("doesn't merge duplicate imports if option disabled", () => { From f5f9e26e74d5d082c49ee3820f88b4e8b90626ec Mon Sep 17 00:00:00 2001 From: Frederic Barthelemy Date: Thu, 19 May 2022 20:49:22 -0700 Subject: [PATCH 10/22] PR Feedback: rename `regular` imports to `value` imports --- src/constants.ts | 6 +++--- src/utils/__tests__/get-import-flavor-of-node.spec.ts | 6 +++--- src/utils/get-import-flavor-of-node.ts | 4 ++-- src/utils/merge-nodes-with-matching-flavors.ts | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 1996894..d5ce26b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -11,14 +11,14 @@ export const newLineCharacters = '\n\n'; export const chunkTypeUnsortable = 'unsortable'; export const chunkTypeOther = 'other'; -/** import Thing from ... or import {Thing} from ... */ -export const importFlavorRegular = 'regular'; +/** Value imports (including top-level default imports) - import {Thing} from ... or import Thing from ... */ +export const importFlavorValue = 'value'; /** import type {} from ... */ export const importFlavorType = 'type'; export const importFlavorSideEffect = 'side-effect'; export const importFlavorIgnore = 'prettier-ignore'; export const mergeableImportFlavors = [ - importFlavorRegular, + importFlavorValue, importFlavorType, ] as const; diff --git a/src/utils/__tests__/get-import-flavor-of-node.spec.ts b/src/utils/__tests__/get-import-flavor-of-node.spec.ts index ba67573..c0d26c4 100644 --- a/src/utils/__tests__/get-import-flavor-of-node.spec.ts +++ b/src/utils/__tests__/get-import-flavor-of-node.spec.ts @@ -22,10 +22,10 @@ import { g } from "g"; ).toMatchInlineSnapshot(` Array [ "side-effect", - "regular", + "value", "type", - "regular", - "regular", + "value", + "value", "prettier-ignore", ] `); diff --git a/src/utils/get-import-flavor-of-node.ts b/src/utils/get-import-flavor-of-node.ts index b508a2f..2487bca 100644 --- a/src/utils/get-import-flavor-of-node.ts +++ b/src/utils/get-import-flavor-of-node.ts @@ -1,8 +1,8 @@ import { importFlavorIgnore, - importFlavorRegular, importFlavorSideEffect, importFlavorType, + importFlavorValue, } from '../constants'; import type { GetImportFlavorOfNode } from '../types'; @@ -25,5 +25,5 @@ export const getImportFlavorOfNode: GetImportFlavorOfNode = (node) => { if (node.importKind === 'type') { return importFlavorType; } - return importFlavorRegular; + return importFlavorValue; }; diff --git a/src/utils/merge-nodes-with-matching-flavors.ts b/src/utils/merge-nodes-with-matching-flavors.ts index 59fb0d2..ca9edb2 100644 --- a/src/utils/merge-nodes-with-matching-flavors.ts +++ b/src/utils/merge-nodes-with-matching-flavors.ts @@ -7,8 +7,8 @@ import type { } from '@babel/types'; import { - importFlavorRegular, importFlavorType, + importFlavorValue, mergeableImportFlavors, } from '../constants'; import type { MergeNodesWithMatchingImportFlavors } from '../types'; @@ -31,7 +31,7 @@ function selectMergeableNodesByImportFlavor( return groups; }, { - [importFlavorRegular]: [] as ImportDeclaration[], + [importFlavorValue]: [] as ImportDeclaration[], [importFlavorType]: [] as ImportDeclaration[], }, ); From 42999c9c49206407f25f192ef930c3503ca49f33 Mon Sep 17 00:00:00 2001 From: Frederic Barthelemy Date: Thu, 19 May 2022 20:49:35 -0700 Subject: [PATCH 11/22] PR Feedback: Readme wording --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ad8bb9b..974ca8b 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,7 @@ import ExampleView from './ExampleView'; **default value:** `false` -A boolean value to enable or disable multiple import statements referencing the same source. Not all patterns can be merged! Notably: `import type …` will not be converted to `import {type …` or vice-versa. +When `true`, multiple import statements from the same module will be combined into a single import. #### `importOrderParserPlugins` From 2fe3128c50ff30f2c580bac2e4e233d65b437b93 Mon Sep 17 00:00:00 2001 From: Frederic Barthelemy Date: Thu, 19 May 2022 21:04:10 -0700 Subject: [PATCH 12/22] PR Feedback: rename getCodeFromAst parameters Try to improve the naming and docs for the parameters, particularly the nodes vs importNodes distinciton --- src/preprocessor.ts | 12 +++++----- src/utils/__tests__/get-code-from-ast.spec.ts | 6 ++--- .../merge-nodes-with-matching-flavors.spec.ts | 20 +++++++++------- src/utils/get-code-from-ast.ts | 23 +++++++++---------- 4 files changed, 32 insertions(+), 29 deletions(-) diff --git a/src/preprocessor.ts b/src/preprocessor.ts index d751d67..7fb07c6 100644 --- a/src/preprocessor.ts +++ b/src/preprocessor.ts @@ -19,7 +19,7 @@ export function preprocessor(code: string, options: PrettierOptions): string { importOrderSortSpecifiers, } = options; - const importNodes: ImportDeclaration[] = []; + const allOriginalImportNodes: ImportDeclaration[] = []; const parserOptions: ParserOptions = { sourceType: 'module', plugins: getExperimentalParserPlugins(importOrderParserPlugins), @@ -36,17 +36,17 @@ export function preprocessor(code: string, options: PrettierOptions): string { isTSModuleDeclaration(p), ); if (!tsModuleParent) { - importNodes.push(path.node); + allOriginalImportNodes.push(path.node); } }, }); // short-circuit if there are no import declarations - if (importNodes.length === 0) { + if (allOriginalImportNodes.length === 0) { return code; } - const remainingImports = getSortedNodes(importNodes, { + const nodesToOutput = getSortedNodes(allOriginalImportNodes, { importOrder, importOrderBuiltinModulesToTop, importOrderCaseInsensitive, @@ -57,8 +57,8 @@ export function preprocessor(code: string, options: PrettierOptions): string { }); return getCodeFromAst({ - nodes: remainingImports, - importNodes, + nodesToOutput, + allOriginalImportNodes, originalCode: code, directives, interpreter, diff --git a/src/utils/__tests__/get-code-from-ast.spec.ts b/src/utils/__tests__/get-code-from-ast.spec.ts index fd6207b..a4983b3 100644 --- a/src/utils/__tests__/get-code-from-ast.spec.ts +++ b/src/utils/__tests__/get-code-from-ast.spec.ts @@ -25,7 +25,7 @@ import a from 'a'; importOrderSortSpecifiers: false, }); const formatted = getCodeFromAst({ - nodes: sortedNodes, + nodesToOutput: sortedNodes, originalCode: code, directives: [], }); @@ -64,8 +64,8 @@ import {b} from 'a'; importOrderSortSpecifiers: false, }); const formatted = getCodeFromAst({ - nodes: sortedNodes, - importNodes, + nodesToOutput: sortedNodes, + allOriginalImportNodes: importNodes, originalCode: code, directives: [], }); 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 2b3a31f..88a99c1 100644 --- a/src/utils/__tests__/merge-nodes-with-matching-flavors.spec.ts +++ b/src/utils/__tests__/merge-nodes-with-matching-flavors.spec.ts @@ -33,9 +33,11 @@ it('should merge duplicate imports within a given chunk', () => { import Foo1 from 'e'; import Foo2 from 'e'; `; - const importNodes = getImportNodes(code, { plugins: ['typescript'] }); + const allOriginalImportNodes = getImportNodes(code, { + plugins: ['typescript'], + }); - const sortedNodes = getSortedNodes(importNodes, { + const nodesToOutput = getSortedNodes(allOriginalImportNodes, { importOrder: [], importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: false, @@ -45,8 +47,8 @@ it('should merge duplicate imports within a given chunk', () => { importOrderSortSpecifiers: true, }); const formatted = getCodeFromAst({ - nodes: sortedNodes, - importNodes, + nodesToOutput, + allOriginalImportNodes, originalCode: code, directives: [], }); @@ -108,9 +110,11 @@ it("doesn't merge duplicate imports if option disabled", () => { import Foo1 from 'e'; import Foo2 from 'e'; `; - const importNodes = getImportNodes(code, { plugins: ['typescript'] }); + const allOriginalImportNodes = getImportNodes(code, { + plugins: ['typescript'], + }); - const sortedNodes = getSortedNodes(importNodes, { + const nodesToOutput = getSortedNodes(allOriginalImportNodes, { importOrder: [], importOrderBuiltinModulesToTop: false, importOrderCaseInsensitive: false, @@ -120,8 +124,8 @@ it("doesn't merge duplicate imports if option disabled", () => { importOrderSortSpecifiers: true, }); const formatted = getCodeFromAst({ - nodes: sortedNodes, - importNodes, + nodesToOutput, + allOriginalImportNodes, originalCode: code, directives: [], }); diff --git a/src/utils/get-code-from-ast.ts b/src/utils/get-code-from-ast.ts index 2b13f34..36c8670 100644 --- a/src/utils/get-code-from-ast.ts +++ b/src/utils/get-code-from-ast.ts @@ -6,10 +6,9 @@ import { getAllCommentsFromNodes } from './get-all-comments-from-nodes'; import { removeNodesFromOriginalCode } from './remove-nodes-from-original-code'; /** - * This function generate a code string from the passed nodes. - * @param nodes All imports, in the sorted order in which they should appear in - * the generated code. - * @param importNodes All nodes that were originally relevant. (This includes nodes that need to be deleted!) + * This function generates a code string from the passed nodes. + * @param nodesToOutput The remaining imports which should be rendered. (Node specifiers & types may be mutated) + * @param allOriginalImportNodes All import nodes that were originally relevant. (This includes nodes that need to be deleted!) * @param originalCode The original input code that was passed to this plugin. * @param directives All directive prologues from the original code (e.g. * `"use strict";`). @@ -17,24 +16,24 @@ import { removeNodesFromOriginalCode } from './remove-nodes-from-original-code'; * `#!/bin/node`). */ export const getCodeFromAst = ({ - nodes, - importNodes = nodes, + nodesToOutput, + allOriginalImportNodes = nodesToOutput, originalCode, directives, interpreter, }: { - nodes: Statement[]; - importNodes?: Statement[]; + nodesToOutput: Statement[]; + allOriginalImportNodes?: Statement[]; originalCode: string; directives: Directive[]; interpreter?: InterpreterDirective | null; }) => { - const allCommentsFromImports = getAllCommentsFromNodes(nodes); + const allCommentsFromImports = getAllCommentsFromNodes(nodesToOutput); const allCommentsFromDirectives = getAllCommentsFromNodes(directives); const nodesToRemoveFromCode = [ - ...nodes, - ...importNodes, + ...nodesToOutput, + ...allOriginalImportNodes, ...allCommentsFromImports, ...allCommentsFromDirectives, ...(interpreter ? [interpreter] : []), @@ -48,7 +47,7 @@ export const getCodeFromAst = ({ const newAST = file({ type: 'Program', - body: nodes, + body: nodesToOutput, directives: directives, sourceType: 'module', interpreter: interpreter, From e393e5e66bbdaa3fadc1119d66215174419a8f37 Mon Sep 17 00:00:00 2001 From: Frederic Barthelemy Date: Thu, 19 May 2022 21:09:02 -0700 Subject: [PATCH 13/22] PR Feedback: import-flavor @returns documentation --- src/utils/get-import-flavor-of-node.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/get-import-flavor-of-node.ts b/src/utils/get-import-flavor-of-node.ts index 2487bca..5c5f476 100644 --- a/src/utils/get-import-flavor-of-node.ts +++ b/src/utils/get-import-flavor-of-node.ts @@ -10,7 +10,7 @@ import type { GetImportFlavorOfNode } from '../types'; * Classifies nodes by import-flavor, primarily informing whether the node is a candidate for merging * * @param node - * @returns {("prettier-ignore"|"regular"|"side-effect"|"type")} + * @returns the flavor of the import node */ export const getImportFlavorOfNode: GetImportFlavorOfNode = (node) => { const hasIgnoreNextNode = (node.leadingComments ?? []).some( From e56948807b98e2d49259f305270560e75ad09eb7 Mon Sep 17 00:00:00 2001 From: Frederic Barthelemy Date: Thu, 19 May 2022 21:16:26 -0700 Subject: [PATCH 14/22] PR Feedback: hoist hasIgnoreNextNode for reuse --- src/utils/get-chunk-type-of-node.ts | 6 ++---- src/utils/get-import-flavor-of-node.ts | 6 ++---- src/utils/has-ignore-next-node.ts | 9 +++++++++ 3 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 src/utils/has-ignore-next-node.ts diff --git a/src/utils/get-chunk-type-of-node.ts b/src/utils/get-chunk-type-of-node.ts index ec3d339..4bc264d 100644 --- a/src/utils/get-chunk-type-of-node.ts +++ b/src/utils/get-chunk-type-of-node.ts @@ -1,5 +1,6 @@ import { chunkTypeOther, chunkTypeUnsortable } from '../constants'; import { GetChunkTypeOfNode } from '../types'; +import { hasIgnoreNextNode } from './has-ignore-next-node'; /** * Classifies an import declarations according to its properties, the @@ -20,11 +21,8 @@ import { GetChunkTypeOfNode } from '../types'; * @returns The type of the chunk into which the node should be put. */ export const getChunkTypeOfNode: GetChunkTypeOfNode = (node) => { - const hasIgnoreNextNode = (node.leadingComments ?? []).some( - (comment) => comment.value.trim() === 'prettier-ignore', - ); const hasNoImportedSymbols = node.specifiers.length === 0; - return hasIgnoreNextNode || hasNoImportedSymbols + return hasIgnoreNextNode(node.leadingComments) || hasNoImportedSymbols ? chunkTypeUnsortable : chunkTypeOther; }; diff --git a/src/utils/get-import-flavor-of-node.ts b/src/utils/get-import-flavor-of-node.ts index 5c5f476..9ada0ad 100644 --- a/src/utils/get-import-flavor-of-node.ts +++ b/src/utils/get-import-flavor-of-node.ts @@ -5,6 +5,7 @@ import { importFlavorValue, } from '../constants'; import type { GetImportFlavorOfNode } from '../types'; +import { hasIgnoreNextNode } from './has-ignore-next-node'; /** * Classifies nodes by import-flavor, primarily informing whether the node is a candidate for merging @@ -13,10 +14,7 @@ import type { GetImportFlavorOfNode } from '../types'; * @returns the flavor of the import node */ export const getImportFlavorOfNode: GetImportFlavorOfNode = (node) => { - const hasIgnoreNextNode = (node.leadingComments ?? []).some( - (comment) => comment.value.trim() === 'prettier-ignore', - ); - if (hasIgnoreNextNode) { + if (hasIgnoreNextNode(node.leadingComments)) { return importFlavorIgnore; } if (node.specifiers.length === 0) { diff --git a/src/utils/has-ignore-next-node.ts b/src/utils/has-ignore-next-node.ts new file mode 100644 index 0000000..258bba3 --- /dev/null +++ b/src/utils/has-ignore-next-node.ts @@ -0,0 +1,9 @@ +import type { Comment } from '@babel/types'; + +/** + * Detects if `// prettier-ignore` is present in the provided array of comments. + */ +export const hasIgnoreNextNode = (comments: readonly Comment[] | null) => + (comments ?? []).some( + (comment) => comment.value.trim() === 'prettier-ignore', + ); From 0e3a871926b94ebce84d3b5936aa8d260201a861 Mon Sep 17 00:00:00 2001 From: Frederic Barthelemy Date: Thu, 19 May 2022 21:23:25 -0700 Subject: [PATCH 15/22] PR Feedback: Add strong types for ChunkType/FlavorType --- src/types.ts | 22 ++++++++++++++++--- .../merge-nodes-with-matching-flavors.ts | 9 ++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/types.ts b/src/types.ts index b75e34a..22088c8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,15 @@ import { ExpressionStatement, ImportDeclaration } from '@babel/types'; import { RequiredOptions } from 'prettier'; +import { + chunkTypeOther, + chunkTypeUnsortable, + importFlavorIgnore, + importFlavorSideEffect, + importFlavorType, + importFlavorValue, +} from './constants'; + export interface PrettierOptions extends RequiredOptions { importOrder: string[]; importOrderCaseInsensitive: boolean; @@ -13,9 +22,16 @@ export interface PrettierOptions extends RequiredOptions { importOrderParserPlugins: string[]; } +export type ChunkType = typeof chunkTypeOther | typeof chunkTypeUnsortable; +export type FlavorType = + | typeof importFlavorIgnore + | typeof importFlavorSideEffect + | typeof importFlavorType + | typeof importFlavorValue; + export interface ImportChunk { nodes: ImportDeclaration[]; - type: string; + type: ChunkType; } export type ImportGroups = Record; @@ -35,9 +51,9 @@ export type GetSortedNodes = ( >, ) => ImportOrLine[]; -export type GetChunkTypeOfNode = (node: ImportDeclaration) => string; +export type GetChunkTypeOfNode = (node: ImportDeclaration) => ChunkType; -export type GetImportFlavorOfNode = (node: ImportDeclaration) => string; +export type GetImportFlavorOfNode = (node: ImportDeclaration) => FlavorType; export type MergeNodesWithMatchingImportFlavors = ( nodes: ImportDeclaration[], diff --git a/src/utils/merge-nodes-with-matching-flavors.ts b/src/utils/merge-nodes-with-matching-flavors.ts index ca9edb2..acb2d66 100644 --- a/src/utils/merge-nodes-with-matching-flavors.ts +++ b/src/utils/merge-nodes-with-matching-flavors.ts @@ -1,5 +1,4 @@ import type { - EmptyStatement, ImportDeclaration, ImportDefaultSpecifier, ImportNamespaceSpecifier, @@ -22,7 +21,9 @@ function isMergeableFlavor(flavor: string): flavor is MergeableFlavor { function selectMergeableNodesByImportFlavor( nodes: ImportDeclaration[], ): Record { - return nodes.reduce( + return nodes.reduce< + Record + >( (groups, node) => { const flavor = getImportFlavorOfNode(node); if (isMergeableFlavor(flavor)) { @@ -31,8 +32,8 @@ function selectMergeableNodesByImportFlavor( return groups; }, { - [importFlavorValue]: [] as ImportDeclaration[], - [importFlavorType]: [] as ImportDeclaration[], + [importFlavorValue]: [], + [importFlavorType]: [], }, ); } From d0a2692b413176c520d371987c19e7ee5debe60e Mon Sep 17 00:00:00 2001 From: Frederic Barthelemy Date: Thu, 19 May 2022 21:24:28 -0700 Subject: [PATCH 16/22] PR Feedback: deleteContext -> nodesToDelete --- src/utils/merge-nodes-with-matching-flavors.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils/merge-nodes-with-matching-flavors.ts b/src/utils/merge-nodes-with-matching-flavors.ts index acb2d66..cb160a7 100644 --- a/src/utils/merge-nodes-with-matching-flavors.ts +++ b/src/utils/merge-nodes-with-matching-flavors.ts @@ -118,17 +118,17 @@ function mergeNodes( */ function mutateContextAndMerge({ context, - deleteContext, + nodesToDelete, insertableNode, }: { context: Record; - deleteContext: ImportDeclaration[]; + nodesToDelete: ImportDeclaration[]; insertableNode: ImportDeclaration; }) { const source = selectNodeImportSource(insertableNode); if (context[source]) { if (mergeNodes(context[source], insertableNode)) { - deleteContext.push(insertableNode); + nodesToDelete.push(insertableNode); } } else { context[source] = insertableNode; @@ -157,7 +157,7 @@ export const mergeNodesWithMatchingImportFlavors: MergeNodesWithMatchingImportFl for (const insertableNode of group) { mutateContextAndMerge({ context, - deleteContext: nodesToDelete, + nodesToDelete, insertableNode, }); } From 6cb7764a36968a8648a04442b3c8d39e4ff4a13e Mon Sep 17 00:00:00 2001 From: Frederic Barthelemy Date: Thu, 19 May 2022 21:28:06 -0700 Subject: [PATCH 17/22] PR Feedback: comment-update: once you mutate nodes, lines/locations are stale! --- src/utils/merge-nodes-with-matching-flavors.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/merge-nodes-with-matching-flavors.ts b/src/utils/merge-nodes-with-matching-flavors.ts index cb160a7..ce757e8 100644 --- a/src/utils/merge-nodes-with-matching-flavors.ts +++ b/src/utils/merge-nodes-with-matching-flavors.ts @@ -94,7 +94,8 @@ function mergeNodes( nodeToKeep.specifiers.push(...nodeToForget.specifiers); - // The line numbers will be all messed up. Is this a problem? + // These mutations don't update the line numbers, and that's crucial for moving things around. + // To get updated line-numbers you would need to re-parse the code after these changes are rendered! nodeToKeep.leadingComments = [ ...(nodeToKeep.leadingComments || []), ...(nodeToForget.leadingComments || []), From 99b6716420757ac48e50d9d366f6e38abb7954d0 Mon Sep 17 00:00:00 2001 From: Frederic Barthelemy Date: Thu, 19 May 2022 21:34:51 -0700 Subject: [PATCH 18/22] PR Feedback: apply getCodeFromAst renames --- .../merge-nodes-with-matching-flavors.spec.ts | 85 ++++++++++++------- 1 file changed, 52 insertions(+), 33 deletions(-) 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 6945559..81bdf59 100644 --- a/src/utils/__tests__/merge-nodes-with-matching-flavors.spec.ts +++ b/src/utils/__tests__/merge-nodes-with-matching-flavors.spec.ts @@ -103,16 +103,18 @@ it('should merge type imports into regular imports', () => { import { D1 } from 'd'; import type { D2 } from 'd'; `; - const importNodes = getImportNodes(code, { plugins: ['typescript'] }); + const allOriginalImportNodes = getImportNodes(code, { + plugins: ['typescript'], + }); - const sortedNodes = getSortedNodes(importNodes, { + const nodesToOutput = getSortedNodes(allOriginalImportNodes, { ...defaultOptions, importOrderMergeDuplicateImports: true, importOrderMergeTypeImportsIntoRegular: true, }); const formatted = getCodeFromAst({ - nodes: sortedNodes, - importNodes, + nodesToOutput, + allOriginalImportNodes, originalCode: code, directives: [], }); @@ -133,16 +135,18 @@ it('should combine type import and default import', () => { import type {MyType} from './source'; import defaultValue from './source'; `; - const importNodes = getImportNodes(code, { plugins: ['typescript'] }); + const allOriginalImportNodes = getImportNodes(code, { + plugins: ['typescript'], + }); - const sortedNodes = getSortedNodes(importNodes, { + const nodesToOutput = getSortedNodes(allOriginalImportNodes, { ...defaultOptions, importOrderMergeDuplicateImports: true, importOrderMergeTypeImportsIntoRegular: true, }); const formatted = getCodeFromAst({ - nodes: sortedNodes, - importNodes, + nodesToOutput, + allOriginalImportNodes, originalCode: code, directives: [], }); @@ -156,16 +160,18 @@ it('should not combine type import and namespace import', () => { import type {MyType} from './source'; import * as Namespace from './source'; `; - const importNodes = getImportNodes(code, { plugins: ['typescript'] }); + const allOriginalImportNodes = getImportNodes(code, { + plugins: ['typescript'], + }); - const sortedNodes = getSortedNodes(importNodes, { + const nodesToOutput = getSortedNodes(allOriginalImportNodes, { ...defaultOptions, importOrderMergeDuplicateImports: true, importOrderMergeTypeImportsIntoRegular: true, }); const formatted = getCodeFromAst({ - nodes: sortedNodes, - importNodes, + nodesToOutput, + allOriginalImportNodes, originalCode: code, directives: [], }); @@ -180,16 +186,18 @@ it('should support aliased named imports', () => { import type {MyType} from './source'; import {value as alias} from './source'; `; - const importNodes = getImportNodes(code, { plugins: ['typescript'] }); + const allOriginalImportNodes = getImportNodes(code, { + plugins: ['typescript'], + }); - const sortedNodes = getSortedNodes(importNodes, { + const nodesToOutput = getSortedNodes(allOriginalImportNodes, { ...defaultOptions, importOrderMergeDuplicateImports: true, importOrderMergeTypeImportsIntoRegular: true, }); const formatted = getCodeFromAst({ - nodes: sortedNodes, - importNodes, + nodesToOutput, + allOriginalImportNodes, originalCode: code, directives: [], }); @@ -203,16 +211,18 @@ it('should combine multiple imports from the same source', () => { import type {MyType, SecondType} from './source'; import {value, SecondValue} from './source'; `; - const importNodes = getImportNodes(code, { plugins: ['typescript'] }); + const allOriginalImportNodes = getImportNodes(code, { + plugins: ['typescript'], + }); - const sortedNodes = getSortedNodes(importNodes, { + const nodesToOutput = getSortedNodes(allOriginalImportNodes, { ...defaultOptions, importOrderMergeDuplicateImports: true, importOrderMergeTypeImportsIntoRegular: true, }); const formatted = getCodeFromAst({ - nodes: sortedNodes, - importNodes, + nodesToOutput, + allOriginalImportNodes, originalCode: code, directives: [], }); @@ -228,16 +238,18 @@ import type {OtherType} from './other'; import {value} from './source'; import {otherValue} from './other'; `; - const importNodes = getImportNodes(code, { plugins: ['typescript'] }); + const allOriginalImportNodes = getImportNodes(code, { + plugins: ['typescript'], + }); - const sortedNodes = getSortedNodes(importNodes, { + const nodesToOutput = getSortedNodes(allOriginalImportNodes, { ...defaultOptions, importOrderMergeDuplicateImports: true, importOrderMergeTypeImportsIntoRegular: true, }); const formatted = getCodeFromAst({ - nodes: sortedNodes, - importNodes, + nodesToOutput, + allOriginalImportNodes, originalCode: code, directives: [], }); @@ -254,16 +266,18 @@ import type {SecondType} from './source'; import {value} from './source'; import {SecondValue} from './source'; `; - const importNodes = getImportNodes(code, { plugins: ['typescript'] }); + const allOriginalImportNodes = getImportNodes(code, { + plugins: ['typescript'], + }); - const sortedNodes = getSortedNodes(importNodes, { + const nodesToOutput = getSortedNodes(allOriginalImportNodes, { ...defaultOptions, importOrderMergeDuplicateImports: true, importOrderMergeTypeImportsIntoRegular: true, }); const formatted = getCodeFromAst({ - nodes: sortedNodes, - importNodes, + nodesToOutput, + allOriginalImportNodes, originalCode: code, directives: [], }); @@ -279,16 +293,18 @@ import type {OtherType} from './other'; import {thirdValue} from './third' import {value} from './source'; `; - const importNodes = getImportNodes(code, { plugins: ['typescript'] }); + const allOriginalImportNodes = getImportNodes(code, { + plugins: ['typescript'], + }); - const sortedNodes = getSortedNodes(importNodes, { + const nodesToOutput = getSortedNodes(allOriginalImportNodes, { ...defaultOptions, importOrderMergeDuplicateImports: true, importOrderMergeTypeImportsIntoRegular: true, }); const formatted = getCodeFromAst({ - nodes: sortedNodes, - importNodes, + nodesToOutput, + allOriginalImportNodes, originalCode: code, directives: [], }); @@ -333,7 +349,10 @@ it("doesn't merge duplicate imports if option disabled", () => { plugins: ['typescript'], }); - const nodesToOutput = getSortedNodes(importNodes, defaultOptions); + const nodesToOutput = getSortedNodes( + allOriginalImportNodes, + defaultOptions, + ); const formatted = getCodeFromAst({ nodesToOutput, allOriginalImportNodes, From 03b9926d6bb92aa542caa61a4808a9d8562baf9b Mon Sep 17 00:00:00 2001 From: Ian VanSchooten Date: Sun, 23 Oct 2022 22:04:58 -0400 Subject: [PATCH 19/22] Minor review comments --- src/preprocessor.ts | 2 +- src/utils/merge-nodes-with-matching-flavors.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/preprocessor.ts b/src/preprocessor.ts index 4e09850..d00d1d8 100644 --- a/src/preprocessor.ts +++ b/src/preprocessor.ts @@ -25,7 +25,7 @@ export function preprocessor(code: string, options: PrettierOptions): string { !importOrderMergeDuplicateImports ) { console.warn( - "[@ianvs/prettier-plugin-sort-imports]: Option combination of both importOrderMergeTypeImportsIntoRegular: true and importOrderMergeDuplicateImports: false is not won't do anything!", + '[@ianvs/prettier-plugin-sort-imports]: Enabling importOrderMergeTypeImportsIntoRegular will have no effect unless importOrderMergeDuplicateImports is also enabled.', ); } diff --git a/src/utils/merge-nodes-with-matching-flavors.ts b/src/utils/merge-nodes-with-matching-flavors.ts index 521f368..cb5e00f 100644 --- a/src/utils/merge-nodes-with-matching-flavors.ts +++ b/src/utils/merge-nodes-with-matching-flavors.ts @@ -51,11 +51,13 @@ function selectNodeImportSource(node: ImportDeclaration) { return node.source.value; } +/** e.g. import * as Namespace from "someModule" */ function nodeIsImportNamespaceSpecifier( node: ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier, ): node is ImportNamespaceSpecifier { return node.type === 'ImportNamespaceSpecifier'; } +/** e.g. import Default from "someModule" */ function nodeIsImportDefaultSpecifier( node: ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier, ): node is ImportDefaultSpecifier { @@ -67,7 +69,7 @@ function nodeIsImportSpecifier( return node.type === 'ImportSpecifier'; } -function convertImportSpecifierType(node: ImportSpecifier) { +function convertImportSpecifierToType(node: ImportSpecifier) { assert(node.importKind === 'value' || node.importKind === 'type'); node.importKind = 'type'; } @@ -78,7 +80,7 @@ function convertTypeImportToValueImport(node: ImportDeclaration) { node.importKind = 'value'; node.specifiers .filter(nodeIsImportSpecifier) - .forEach(convertImportSpecifierType); + .forEach(convertImportSpecifierToType); } /** Return false if the merge will produce an invalid result */ From beb9066ea57806d5e4269c2c707a3d2dbd42b550 Mon Sep 17 00:00:00 2001 From: Ian VanSchooten Date: Mon, 24 Oct 2022 08:22:42 -0400 Subject: [PATCH 20/22] Group type imports after value in importOrderSortSpecifiers --- .../get-sorted-import-specifiers.spec.ts | 20 +++++++++++++++++++ .../merge-nodes-with-matching-flavors.spec.ts | 20 +++++++++---------- src/utils/get-sorted-import-specifiers.ts | 12 ++++++++++- 3 files changed, 41 insertions(+), 11 deletions(-) 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__/merge-nodes-with-matching-flavors.spec.ts b/src/utils/__tests__/merge-nodes-with-matching-flavors.spec.ts index 81bdf59..a175c77 100644 --- a/src/utils/__tests__/merge-nodes-with-matching-flavors.spec.ts +++ b/src/utils/__tests__/merge-nodes-with-matching-flavors.spec.ts @@ -97,9 +97,9 @@ it('should merge type imports into regular imports', () => { import { B1 } from 'b'; import { B2 } from 'b'; // Converts 'import type' to 'import value' if first - import type { C1 } from 'c'; import { C2 } from 'c'; - // Converts 'import type' to 'import value' if last + import type { C1 } from 'c'; + // Sorts type import to end import { D1 } from 'd'; import type { D2 } from 'd'; `; @@ -125,8 +125,8 @@ import type { A1, A2 } from "a"; // Preserves 'import value' import { B1, B2 } from "b"; // Converts 'import type' to 'import value' if first -import { type C1, C2 } from "c"; -// Converts 'import type' to 'import value' if last +import { C2, type C1 } from "c"; +// Sorts type import to end import { D1, type D2 } from "d"; `); }); @@ -203,7 +203,7 @@ import {value as alias} from './source'; }); expect(format(formatted, { parser: 'babel' })) - .toEqual(`import { type MyType, value as alias } from "./source"; + .toEqual(`import { value as alias, type MyType } from "./source"; `); }); it('should combine multiple imports from the same source', () => { @@ -228,7 +228,7 @@ import {value, SecondValue} from './source'; }); expect(format(formatted, { parser: 'babel' })) - .toEqual(`import { type MyType, type SecondType, SecondValue, value } from "./source"; + .toEqual(`import { SecondValue, value, type MyType, type SecondType } from "./source"; `); }); it('should combine multiple groups of imports', () => { @@ -255,8 +255,8 @@ import {otherValue} from './other'; }); expect(format(formatted, { parser: 'babel' })) - .toEqual(`import { type OtherType, otherValue } from "./other"; -import { type MyType, value } from "./source"; + .toEqual(`import { otherValue, type OtherType } from "./other"; +import { value, type MyType } from "./source"; `); }); it('should combine multiple imports statements from the same source', () => { @@ -283,7 +283,7 @@ import {SecondValue} from './source'; }); expect(format(formatted, { parser: 'babel' })) - .toEqual(`import { type MyType, type SecondType, SecondValue, value } from "./source"; + .toEqual(`import { SecondValue, value, type MyType, type SecondType } from "./source"; `); }); it('should not impact imports from different sources', () => { @@ -311,7 +311,7 @@ import {value} from './source'; expect(format(formatted, { parser: 'babel' })) .toEqual(`import type { OtherType } from "./other"; -import { type MyType, value } from "./source"; +import { value, type MyType } from "./source"; import { thirdValue } from "./third"; `); }); 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); }); From 75b11fe0c3a2b640e4f4616a6f49cd24521f2bec Mon Sep 17 00:00:00 2001 From: Ian VanSchooten Date: Mon, 24 Oct 2022 08:37:24 -0400 Subject: [PATCH 21/22] Rename option for clarity --- README.md | 4 +-- src/index.ts | 2 +- src/preprocessor.ts | 8 +++--- src/types.ts | 6 ++-- .../get-all-comments-from-nodes.spec.ts | 2 +- src/utils/__tests__/get-code-from-ast.spec.ts | 4 +-- .../get-sorted-nodes-by-import-order.spec.ts | 28 +++++++++---------- src/utils/__tests__/get-sorted-nodes.spec.ts | 2 +- .../merge-nodes-with-matching-flavors.spec.ts | 20 ++++++------- .../remove-nodes-from-original-code.spec.ts | 2 +- src/utils/get-sorted-nodes.ts | 4 +-- .../merge-nodes-with-matching-flavors.ts | 4 +-- 12 files changed, 43 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 274a5d5..a3a76db 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ module.exports = { "importOrderCaseInsensitive": true, "importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"], "importOrderMergeDuplicateImports": true, - "importOrderMergeTypeImportsIntoRegular": true, + "importOrderCombineTypeAndValueImports": true, "importOrderSeparation": true, "importOrderSortSpecifiers": true, } @@ -219,7 +219,7 @@ import ExampleView from './ExampleView'; When `true`, multiple import statements from the same module will be combined into a single import. -#### `importOrderMergeTypeImportsIntoRegular` +#### `importOrderCombineTypeAndValueImports` **type**: `boolean` diff --git a/src/index.ts b/src/index.ts index eba3eeb..86a4edd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -72,7 +72,7 @@ const options: Record< default: false, description: 'Should duplicate imports be merged?', }, - importOrderMergeTypeImportsIntoRegular: { + importOrderCombineTypeAndValueImports: { type: 'boolean', category: 'Global', default: false, diff --git a/src/preprocessor.ts b/src/preprocessor.ts index d00d1d8..778239b 100644 --- a/src/preprocessor.ts +++ b/src/preprocessor.ts @@ -15,17 +15,17 @@ export function preprocessor(code: string, options: PrettierOptions): string { importOrderCaseInsensitive, importOrderGroupNamespaceSpecifiers, importOrderMergeDuplicateImports, - importOrderMergeTypeImportsIntoRegular, + importOrderCombineTypeAndValueImports, importOrderSeparation, importOrderSortSpecifiers, } = options; if ( - importOrderMergeTypeImportsIntoRegular && + importOrderCombineTypeAndValueImports && !importOrderMergeDuplicateImports ) { console.warn( - '[@ianvs/prettier-plugin-sort-imports]: Enabling importOrderMergeTypeImportsIntoRegular will have no effect unless importOrderMergeDuplicateImports is also enabled.', + '[@ianvs/prettier-plugin-sort-imports]: Enabling importOrderCombineTypeAndValueImports will have no effect unless importOrderMergeDuplicateImports is also enabled.', ); } @@ -62,7 +62,7 @@ export function preprocessor(code: string, options: PrettierOptions): string { importOrderCaseInsensitive, importOrderGroupNamespaceSpecifiers, importOrderMergeDuplicateImports, - importOrderMergeTypeImportsIntoRegular, + importOrderCombineTypeAndValueImports, importOrderSeparation, importOrderSortSpecifiers, }); diff --git a/src/types.ts b/src/types.ts index 53d06e2..7ee08f6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,7 +16,7 @@ export interface PrettierOptions extends RequiredOptions { importOrderBuiltinModulesToTop: boolean; importOrderGroupNamespaceSpecifiers: boolean; importOrderMergeDuplicateImports: boolean; - importOrderMergeTypeImportsIntoRegular: boolean; + importOrderCombineTypeAndValueImports: boolean; importOrderSeparation: boolean; importOrderSortSpecifiers: boolean; // should be of type ParserPlugin from '@babel/parser' but prettier does not support nested arrays in options @@ -47,7 +47,7 @@ export type GetSortedNodes = ( | 'importOrderCaseInsensitive' | 'importOrderGroupNamespaceSpecifiers' | 'importOrderMergeDuplicateImports' - | 'importOrderMergeTypeImportsIntoRegular' + | 'importOrderCombineTypeAndValueImports' | 'importOrderSeparation' | 'importOrderSortSpecifiers' >, @@ -59,5 +59,5 @@ export type GetImportFlavorOfNode = (node: ImportDeclaration) => FlavorType; export type MergeNodesWithMatchingImportFlavors = ( nodes: ImportDeclaration[], - options: { importOrderMergeTypeImportsIntoRegular: boolean }, + 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 d472e0e..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,7 +14,7 @@ const getSortedImportNodes = (code: string, options?: ParserOptions) => { importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, - importOrderMergeTypeImportsIntoRegular: 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 42b3ff5..caae32e 100644 --- a/src/utils/__tests__/get-code-from-ast.spec.ts +++ b/src/utils/__tests__/get-code-from-ast.spec.ts @@ -21,7 +21,7 @@ import a from 'a'; importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, - importOrderMergeTypeImportsIntoRegular: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }); @@ -63,7 +63,7 @@ import type {See} from 'c'; importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: true, - importOrderMergeTypeImportsIntoRegular: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }); 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 617dddc..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,7 +32,7 @@ test('it returns all sorted nodes', () => { importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, - importOrderMergeTypeImportsIntoRegular: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -87,7 +87,7 @@ test('it returns all sorted nodes case-insensitive', () => { importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, - importOrderMergeTypeImportsIntoRegular: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -142,7 +142,7 @@ test('it returns all sorted nodes with sort order', () => { importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, - importOrderMergeTypeImportsIntoRegular: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -197,7 +197,7 @@ test('it returns all sorted nodes with sort order case-insensitive', () => { importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, - importOrderMergeTypeImportsIntoRegular: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -251,7 +251,7 @@ test('it returns all sorted import nodes with sorted import specifiers', () => { importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, - importOrderMergeTypeImportsIntoRegular: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: true, }) as ImportDeclaration[]; @@ -305,7 +305,7 @@ test('it returns all sorted import nodes with sorted import specifiers with case importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, - importOrderMergeTypeImportsIntoRegular: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: true, }) as ImportDeclaration[]; @@ -359,7 +359,7 @@ test('it returns all sorted nodes with custom third party modules', () => { importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, - importOrderMergeTypeImportsIntoRegular: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -390,7 +390,7 @@ test('it returns all sorted nodes with namespace specifiers at the top', () => { importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: true, importOrderMergeDuplicateImports: false, - importOrderMergeTypeImportsIntoRegular: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -422,7 +422,7 @@ test('it returns all sorted nodes with builtin specifiers at the top, ', () => { importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, - importOrderMergeTypeImportsIntoRegular: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -454,7 +454,7 @@ test('it returns all sorted nodes with custom third party modules and builtins a importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, - importOrderMergeTypeImportsIntoRegular: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -485,7 +485,7 @@ test('it adds newlines when importOrderSeparation is true', () => { importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, - importOrderMergeTypeImportsIntoRegular: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: true, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -526,7 +526,7 @@ test('it returns all sorted nodes with custom separation', () => { importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, - importOrderMergeTypeImportsIntoRegular: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -565,7 +565,7 @@ test('it allows both importOrderSeparation and custom separation (but why?)', () importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, - importOrderMergeTypeImportsIntoRegular: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: true, importOrderSortSpecifiers: false, }) as ImportDeclaration[]; @@ -610,7 +610,7 @@ test('it does not add multiple custom import separators', () => { importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, - importOrderMergeTypeImportsIntoRegular: 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 1bf2475..403ca06 100644 --- a/src/utils/__tests__/get-sorted-nodes.spec.ts +++ b/src/utils/__tests__/get-sorted-nodes.spec.ts @@ -33,7 +33,7 @@ test('it returns all sorted nodes, preserving the order side effect nodes', () = importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, - importOrderMergeTypeImportsIntoRegular: 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 a175c77..922fe5d 100644 --- a/src/utils/__tests__/merge-nodes-with-matching-flavors.spec.ts +++ b/src/utils/__tests__/merge-nodes-with-matching-flavors.spec.ts @@ -10,7 +10,7 @@ const defaultOptions = { importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, - importOrderMergeTypeImportsIntoRegular: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: true, importOrderSortSpecifiers: true, }; @@ -51,7 +51,7 @@ it('should merge duplicate imports within a given chunk', () => { const nodesToOutput = getSortedNodes(allOriginalImportNodes, { ...defaultOptions, importOrderMergeDuplicateImports: true, - importOrderMergeTypeImportsIntoRegular: false, + importOrderCombineTypeAndValueImports: false, }); const formatted = getCodeFromAst({ nodesToOutput, @@ -110,7 +110,7 @@ it('should merge type imports into regular imports', () => { const nodesToOutput = getSortedNodes(allOriginalImportNodes, { ...defaultOptions, importOrderMergeDuplicateImports: true, - importOrderMergeTypeImportsIntoRegular: true, + importOrderCombineTypeAndValueImports: true, }); const formatted = getCodeFromAst({ nodesToOutput, @@ -142,7 +142,7 @@ import defaultValue from './source'; const nodesToOutput = getSortedNodes(allOriginalImportNodes, { ...defaultOptions, importOrderMergeDuplicateImports: true, - importOrderMergeTypeImportsIntoRegular: true, + importOrderCombineTypeAndValueImports: true, }); const formatted = getCodeFromAst({ nodesToOutput, @@ -167,7 +167,7 @@ import * as Namespace from './source'; const nodesToOutput = getSortedNodes(allOriginalImportNodes, { ...defaultOptions, importOrderMergeDuplicateImports: true, - importOrderMergeTypeImportsIntoRegular: true, + importOrderCombineTypeAndValueImports: true, }); const formatted = getCodeFromAst({ nodesToOutput, @@ -193,7 +193,7 @@ import {value as alias} from './source'; const nodesToOutput = getSortedNodes(allOriginalImportNodes, { ...defaultOptions, importOrderMergeDuplicateImports: true, - importOrderMergeTypeImportsIntoRegular: true, + importOrderCombineTypeAndValueImports: true, }); const formatted = getCodeFromAst({ nodesToOutput, @@ -218,7 +218,7 @@ import {value, SecondValue} from './source'; const nodesToOutput = getSortedNodes(allOriginalImportNodes, { ...defaultOptions, importOrderMergeDuplicateImports: true, - importOrderMergeTypeImportsIntoRegular: true, + importOrderCombineTypeAndValueImports: true, }); const formatted = getCodeFromAst({ nodesToOutput, @@ -245,7 +245,7 @@ import {otherValue} from './other'; const nodesToOutput = getSortedNodes(allOriginalImportNodes, { ...defaultOptions, importOrderMergeDuplicateImports: true, - importOrderMergeTypeImportsIntoRegular: true, + importOrderCombineTypeAndValueImports: true, }); const formatted = getCodeFromAst({ nodesToOutput, @@ -273,7 +273,7 @@ import {SecondValue} from './source'; const nodesToOutput = getSortedNodes(allOriginalImportNodes, { ...defaultOptions, importOrderMergeDuplicateImports: true, - importOrderMergeTypeImportsIntoRegular: true, + importOrderCombineTypeAndValueImports: true, }); const formatted = getCodeFromAst({ nodesToOutput, @@ -300,7 +300,7 @@ import {value} from './source'; const nodesToOutput = getSortedNodes(allOriginalImportNodes, { ...defaultOptions, importOrderMergeDuplicateImports: true, - importOrderMergeTypeImportsIntoRegular: true, + importOrderCombineTypeAndValueImports: true, }); const formatted = getCodeFromAst({ nodesToOutput, 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 c970123..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,7 +27,7 @@ test('it should remove nodes from the original code', () => { importOrderCaseInsensitive: false, importOrderGroupNamespaceSpecifiers: false, importOrderMergeDuplicateImports: false, - importOrderMergeTypeImportsIntoRegular: false, + importOrderCombineTypeAndValueImports: false, importOrderSeparation: false, importOrderSortSpecifiers: false, }); diff --git a/src/utils/get-sorted-nodes.ts b/src/utils/get-sorted-nodes.ts index 04a8508..6c2f8d8 100644 --- a/src/utils/get-sorted-nodes.ts +++ b/src/utils/get-sorted-nodes.ts @@ -24,7 +24,7 @@ export const getSortedNodes: GetSortedNodes = (nodes, options) => { const { importOrderSeparation, importOrderMergeDuplicateImports, - importOrderMergeTypeImportsIntoRegular, + importOrderCombineTypeAndValueImports, } = options; // Split nodes at each boundary between a side-effect node and a @@ -54,7 +54,7 @@ export const getSortedNodes: GetSortedNodes = (nodes, options) => { } else { const nodes = importOrderMergeDuplicateImports ? mergeNodesWithMatchingImportFlavors(chunk.nodes, { - importOrderMergeTypeImportsIntoRegular, + importOrderCombineTypeAndValueImports, }) : chunk.nodes; // sort non-side effect nodes diff --git a/src/utils/merge-nodes-with-matching-flavors.ts b/src/utils/merge-nodes-with-matching-flavors.ts index cb5e00f..5a760e3 100644 --- a/src/utils/merge-nodes-with-matching-flavors.ts +++ b/src/utils/merge-nodes-with-matching-flavors.ts @@ -190,13 +190,13 @@ function mutateContextAndMerge({ * `import type {Foo}` expressions won't be converted into `import {type Foo}` or vice versa */ export const mergeNodesWithMatchingImportFlavors: MergeNodesWithMatchingImportFlavors = - (input, { importOrderMergeTypeImportsIntoRegular }) => { + (input, { importOrderCombineTypeAndValueImports }) => { const nodesToDelete: ImportDeclaration[] = []; let context: Record = {}; const groups = selectMergeableNodesByImportFlavor(input); for (const groupKey of mergeableImportFlavors) { - if (!importOrderMergeTypeImportsIntoRegular) { + if (!importOrderCombineTypeAndValueImports) { // Reset in loop to avoid unintended merge across variants context = {}; } From a5e4f6b980741cd2f7e7d892e591993cecac069a Mon Sep 17 00:00:00 2001 From: Ian VanSchooten Date: Mon, 24 Oct 2022 08:40:17 -0400 Subject: [PATCH 22/22] Add TOC to readme, re-arrange a bit --- README.md | 124 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 74 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index a3a76db..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 @@ -106,27 +130,53 @@ module.exports = { } ``` -_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: + +```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`** +#### `importOrder` **type**: `Array` @@ -281,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: +### Prevent imports from being sorted -```javascript -import E from 'e'; -import F from 'f'; -import D from 'd'; -import 'c'; -import B from 'b'; -import A from 'a'; -``` - -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