From 03d53c98a92af2b2a27f1d7262dbaf826723341b Mon Sep 17 00:00:00 2001 From: Vladislav Arsenev Date: Wed, 27 Nov 2024 21:51:30 +0400 Subject: [PATCH] feature: order respects side effects --- README.md | 31 ++ examples/example.jsx | 1 - src/constants.ts | 3 + src/index.ts | 9 +- src/preprocessors/preprocessor.ts | 2 + src/types.ts | 6 + .../adjust-comments-on-sorted-nodes.spec.ts | 107 ++++++ .../get-all-comments-from-nodes.spec.ts | 1 + src/utils/__tests__/get-code-from-ast.spec.ts | 1 + .../get-sorted-nodes-by-import-order.spec.ts | 339 ++++++++++++++++++ src/utils/__tests__/get-sorted-nodes.spec.ts | 11 +- .../remove-nodes-from-original-code.spec.ts | 5 +- src/utils/adjust-comments-on-sorted-nodes.ts | 42 +++ src/utils/get-all-comments-from-nodes.ts | 21 +- src/utils/get-sorted-nodes-by-import-order.ts | 78 ++++ src/utils/get-sorted-nodes.ts | 144 +++----- src/utils/is-sort-imports-ignored.ts | 3 +- test-setup/run_spec.js | 2 +- .../__snapshots__/ppsi.spec.js.snap | 102 ++++++ .../imports-with-side-effect-imports.js | 45 +++ .../ppsi.spec.js | 6 + 21 files changed, 854 insertions(+), 105 deletions(-) create mode 100644 src/utils/__tests__/adjust-comments-on-sorted-nodes.spec.ts create mode 100644 src/utils/__tests__/get-sorted-nodes-by-import-order.spec.ts create mode 100644 src/utils/adjust-comments-on-sorted-nodes.ts create mode 100644 src/utils/get-sorted-nodes-by-import-order.ts create mode 100644 tests/ImportPreventSortingSideEffects/__snapshots__/ppsi.spec.js.snap create mode 100644 tests/ImportPreventSortingSideEffects/imports-with-side-effect-imports.js create mode 100644 tests/ImportPreventSortingSideEffects/ppsi.spec.js diff --git a/README.md b/README.md index 82737e85..b6c4129d 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,37 @@ with options as a JSON string of the plugin array: importOrderParserPlugins: [] ``` +### `importOrderSideEffects` +**type**: `boolean` +**default value**: `true` + +By default, the plugin sorts [side effect imports](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#import_a_module_for_its_side_effects_only) like any other imports in the file. If you need to keep side effect imports in the same place but sort all other imports around them, set this option to false. + +Example: + +Initial file: + +```js +import z from 'z' +import a from 'a' + +import 'side-effect-lib' + +import c from 'c' +import b from 'b' +``` +When sorted: + +```js +import a from 'a' +import z from 'z' + +import 'side-effect-lib' + +import b from 'b' +import c from 'c' +``` + ### How does import sort work ? The plugin extracts the imports which are defined in `importOrder`. These imports are considered as _local imports_. diff --git a/examples/example.jsx b/examples/example.jsx index b68b9f1b..add1ae7f 100644 --- a/examples/example.jsx +++ b/examples/example.jsx @@ -8,7 +8,6 @@ import otherthing from '@core/otherthing'; import twoLevelRelativePath from '../../twoLevelRelativePath'; import component from '@ui/hello'; - const HelloWorld = ({ name }) => { return
Hello, {name}
; }; diff --git a/src/constants.ts b/src/constants.ts index a2e6e4ef..28c009e7 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -9,6 +9,9 @@ export const newLineCharacters = '\n\n'; export const sortImportsIgnoredComment = 'sort-imports-ignore'; +export const chunkSideEffectNode = 'side-effect-node'; +export const chunkSideOtherNode = 'other-node'; + /* * 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 9368420c..f36a98b6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,9 +2,10 @@ import { parsers as babelParsers } from 'prettier/plugins/babel'; import { parsers as flowParsers } from 'prettier/plugins/flow'; import { parsers as htmlParsers } from 'prettier/plugins/html'; import { parsers as typescriptParsers } from 'prettier/plugins/typescript'; + import { defaultPreprocessor } from './preprocessors/default-processor'; -import { vuePreprocessor } from './preprocessors/vue-preprocessor'; import { sveltePreprocessor } from './preprocessors/svelte-preprocessor'; +import { vuePreprocessor } from './preprocessors/vue-preprocessor'; const { parsers: svelteParsers } = require('prettier-plugin-svelte'); @@ -50,6 +51,12 @@ const options = { default: false, description: 'Should specifiers be sorted?', }, + importOrderSideEffects: { + type: 'boolean', + category: 'Global', + default: true, + description: 'Should side effects be sorted?', + }, }; module.exports = { diff --git a/src/preprocessors/preprocessor.ts b/src/preprocessors/preprocessor.ts index bf5da668..bd0b4099 100644 --- a/src/preprocessors/preprocessor.ts +++ b/src/preprocessors/preprocessor.ts @@ -16,6 +16,7 @@ export function preprocessor(code: string, options: PrettierOptions) { importOrderSeparation, importOrderGroupNamespaceSpecifiers, importOrderSortSpecifiers, + importOrderSideEffects, } = options; const parserOptions: ParserOptions = { @@ -42,6 +43,7 @@ export function preprocessor(code: string, options: PrettierOptions) { importOrderSeparation, importOrderGroupNamespaceSpecifiers, importOrderSortSpecifiers, + importOrderSideEffects, }); return getCodeFromAst(allImports, directives, code, interpreter); diff --git a/src/types.ts b/src/types.ts index 89eda919..085c0b15 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,5 +19,11 @@ export type GetSortedNodes = ( | 'importOrderSeparation' | 'importOrderGroupNamespaceSpecifiers' | 'importOrderSortSpecifiers' + | 'importOrderSideEffects' >, ) => ImportOrLine[]; + +export interface ImportChunk { + nodes: ImportDeclaration[]; + type: string; +} diff --git a/src/utils/__tests__/adjust-comments-on-sorted-nodes.spec.ts b/src/utils/__tests__/adjust-comments-on-sorted-nodes.spec.ts new file mode 100644 index 00000000..acf76c09 --- /dev/null +++ b/src/utils/__tests__/adjust-comments-on-sorted-nodes.spec.ts @@ -0,0 +1,107 @@ +import { ImportDeclaration } from '@babel/types'; + +import { adjustCommentsOnSortedNodes } from '../adjust-comments-on-sorted-nodes'; +import { getImportNodes } from '../get-import-nodes'; + +function leadingComments(node: ImportDeclaration): string[] { + return node.leadingComments?.map((c) => c.value) ?? []; +} + +function trailingComments(node: ImportDeclaration): string[] { + return node.trailingComments?.map((c) => c.value) ?? []; +} + +test('it preserves the single leading comment for each import declaration', () => { + const importNodes = getImportNodes(` + import {x} from "c"; + // comment b + import {y} from "b"; + // comment a + import {z} from "a"; + `); + expect(importNodes).toHaveLength(3); + const finalNodes = [importNodes[2], importNodes[1], importNodes[0]]; + adjustCommentsOnSortedNodes(importNodes, finalNodes); + expect(finalNodes).toHaveLength(3); + expect(leadingComments(finalNodes[0])).toEqual([' comment a']); + expect(trailingComments(finalNodes[0])).toEqual([]); + expect(leadingComments(finalNodes[1])).toEqual([' comment b']); + expect(trailingComments(finalNodes[1])).toEqual([]); + expect(leadingComments(finalNodes[2])).toEqual([]); + expect(trailingComments(finalNodes[2])).toEqual([]); +}); + +test('it preserves multiple leading comments for each import declaration', () => { + const importNodes = getImportNodes(` + import {x} from "c"; + // comment b1 + // comment b2 + // comment b3 + import {y} from "b"; + // comment a1 + // comment a2 + // comment a3 + import {z} from "a"; + `); + expect(importNodes).toHaveLength(3); + const finalNodes = [importNodes[2], importNodes[1], importNodes[0]]; + adjustCommentsOnSortedNodes(importNodes, finalNodes); + expect(finalNodes).toHaveLength(3); + expect(leadingComments(finalNodes[0])).toEqual([ + ' comment a1', + ' comment a2', + ' comment a3', + ]); + expect(trailingComments(finalNodes[0])).toEqual([]); + expect(leadingComments(finalNodes[1])).toEqual([ + ' comment b1', + ' comment b2', + ' comment b3', + ]); + expect(trailingComments(finalNodes[1])).toEqual([]); + expect(leadingComments(finalNodes[2])).toEqual([]); + expect(trailingComments(finalNodes[2])).toEqual([]); +}); + +test('it does not move comments at before all import declarations', () => { + const importNodes = getImportNodes(` + // comment c1 + // comment c2 + import {x} from "c"; + import {y} from "b"; + import {z} from "a"; + `); + expect(importNodes).toHaveLength(3); + const finalNodes = [importNodes[2], importNodes[1], importNodes[0]]; + adjustCommentsOnSortedNodes(importNodes, finalNodes); + expect(finalNodes).toHaveLength(3); + expect(leadingComments(finalNodes[0])).toEqual([ + ' comment c1', + ' comment c2', + ]); + expect(trailingComments(finalNodes[0])).toEqual([]); + expect(leadingComments(finalNodes[1])).toEqual([]); + expect(trailingComments(finalNodes[1])).toEqual([]); + expect(leadingComments(finalNodes[2])).toEqual([]); + expect(trailingComments(finalNodes[2])).toEqual([]); +}); + +test('it does not affect comments after all import declarations', () => { + const importNodes = getImportNodes(` + import {x} from "c"; + import {y} from "b"; + import {z} from "a"; + // comment final 1 + // comment final 2 + `); + expect(importNodes).toHaveLength(3); + const finalNodes = [importNodes[2], importNodes[1], importNodes[0]]; + adjustCommentsOnSortedNodes(importNodes, finalNodes); + expect(finalNodes).toHaveLength(3); + expect(leadingComments(finalNodes[0])).toEqual([]); + expect(trailingComments(finalNodes[0])).toEqual([]); + expect(leadingComments(finalNodes[1])).toEqual([]); + expect(trailingComments(finalNodes[1])).toEqual([]); + expect(leadingComments(finalNodes[2])).toEqual([]); + expect(trailingComments(finalNodes[2])).toEqual([]); +}); 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 ff8f156b..c076e927 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) => { importOrderSeparation: false, importOrderGroupNamespaceSpecifiers: false, importOrderSortSpecifiers: false, + importOrderSideEffects: true, }); }; diff --git a/src/utils/__tests__/get-code-from-ast.spec.ts b/src/utils/__tests__/get-code-from-ast.spec.ts index 79965048..298e9640 100644 --- a/src/utils/__tests__/get-code-from-ast.spec.ts +++ b/src/utils/__tests__/get-code-from-ast.spec.ts @@ -25,6 +25,7 @@ import a from 'a'; importOrderSeparation: false, importOrderGroupNamespaceSpecifiers: false, importOrderSortSpecifiers: false, + importOrderSideEffects: true, }); const formatted = getCodeFromAst(sortedNodes, [], code, null); expect(await format(formatted, { parser: 'babel' })).toEqual( diff --git a/src/utils/__tests__/get-sorted-nodes-by-import-order.spec.ts b/src/utils/__tests__/get-sorted-nodes-by-import-order.spec.ts new file mode 100644 index 00000000..cc861b61 --- /dev/null +++ b/src/utils/__tests__/get-sorted-nodes-by-import-order.spec.ts @@ -0,0 +1,339 @@ +import { ImportDeclaration } from '@babel/types'; + +import { getImportNodes } from '../get-import-nodes'; +import { getSortedNodes } from '../get-sorted-nodes'; +import { getSortedNodesModulesNames } from '../get-sorted-nodes-modules-names'; +import { getSortedNodesNames } from '../get-sorted-nodes-names'; + +const code = `// first comment +// second comment +import z from 'z'; +import c, { cD } from 'c'; +import g from 'g'; +import { tC, tA, tB } from 't'; +import k, { kE, kB } from 'k'; +import * as a from 'a'; +import * as x from 'x'; +import BY from 'BY'; +import Ba from 'Ba'; +import XY from 'XY'; +import Xa from 'Xa'; +`; + +test('it returns all sorted nodes', () => { + const result = getImportNodes(code); + const sorted = getSortedNodes(result, { + importOrder: [], + importOrderCaseInsensitive: false, + importOrderSeparation: false, + importOrderGroupNamespaceSpecifiers: false, + importOrderSortSpecifiers: false, + importOrderSideEffects: true, + }) as ImportDeclaration[]; + + expect(getSortedNodesNames(sorted)).toEqual([ + 'BY', + 'Ba', + 'XY', + 'Xa', + 'a', + 'c', + 'g', + 'k', + 't', + 'x', + 'z', + ]); + expect( + sorted + .filter((node) => node.type === 'ImportDeclaration') + .map((importDeclaration) => + getSortedNodesModulesNames(importDeclaration.specifiers), + ), + ).toEqual([ + ['BY'], + ['Ba'], + ['XY'], + ['Xa'], + ['a'], + ['c', 'cD'], + ['g'], + ['k', 'kE', 'kB'], + ['tC', 'tA', 'tB'], + ['x'], + ['z'], + ]); +}); + +test('it returns all sorted nodes case-insensitive', () => { + const result = getImportNodes(code); + const sorted = getSortedNodes(result, { + importOrder: [], + importOrderCaseInsensitive: true, + importOrderSeparation: false, + importOrderGroupNamespaceSpecifiers: false, + importOrderSortSpecifiers: false, + importOrderSideEffects: true, + }) as ImportDeclaration[]; + + expect(getSortedNodesNames(sorted)).toEqual([ + 'a', + 'Ba', + 'BY', + 'c', + 'g', + 'k', + 't', + 'x', + 'Xa', + 'XY', + 'z', + ]); + expect( + sorted + .filter((node) => node.type === 'ImportDeclaration') + .map((importDeclaration) => + getSortedNodesModulesNames(importDeclaration.specifiers), + ), + ).toEqual([ + ['a'], + ['Ba'], + ['BY'], + ['c', 'cD'], + ['g'], + ['k', 'kE', 'kB'], + ['tC', 'tA', 'tB'], + ['x'], + ['Xa'], + ['XY'], + ['z'], + ]); +}); + +test('it returns all sorted nodes with sort order', () => { + const result = getImportNodes(code); + const sorted = getSortedNodes(result, { + importOrder: ['^a$', '^t$', '^k$', '^B'], + importOrderCaseInsensitive: false, + importOrderSeparation: false, + importOrderGroupNamespaceSpecifiers: false, + importOrderSortSpecifiers: false, + importOrderSideEffects: true, + }) as ImportDeclaration[]; + + expect(getSortedNodesNames(sorted)).toEqual([ + 'XY', + 'Xa', + 'c', + 'g', + 'x', + 'z', + 'a', + 't', + 'k', + 'BY', + 'Ba', + ]); + expect( + sorted + .filter((node) => node.type === 'ImportDeclaration') + .map((importDeclaration) => + getSortedNodesModulesNames(importDeclaration.specifiers), + ), + ).toEqual([ + ['XY'], + ['Xa'], + ['c', 'cD'], + ['g'], + ['x'], + ['z'], + ['a'], + ['tC', 'tA', 'tB'], + ['k', 'kE', 'kB'], + ['BY'], + ['Ba'], + ]); +}); + +test('it returns all sorted nodes with sort order case-insensitive', () => { + const result = getImportNodes(code); + const sorted = getSortedNodes(result, { + importOrder: ['^a$', '^t$', '^k$', '^B'], + importOrderCaseInsensitive: true, + importOrderSeparation: false, + importOrderGroupNamespaceSpecifiers: false, + importOrderSortSpecifiers: false, + importOrderSideEffects: true, + }) as ImportDeclaration[]; + expect(getSortedNodesNames(sorted)).toEqual([ + 'c', + 'g', + 'x', + 'Xa', + 'XY', + 'z', + 'a', + 't', + 'k', + 'Ba', + 'BY', + ]); + expect( + sorted + .filter((node) => node.type === 'ImportDeclaration') + .map((importDeclaration) => + getSortedNodesModulesNames(importDeclaration.specifiers), + ), + ).toEqual([ + ['c', 'cD'], + ['g'], + ['x'], + ['Xa'], + ['XY'], + ['z'], + ['a'], + ['tC', 'tA', 'tB'], + ['k', 'kE', 'kB'], + ['Ba'], + ['BY'], + ]); +}); + +test('it returns all sorted import nodes with sorted import specifiers', () => { + const result = getImportNodes(code); + const sorted = getSortedNodes(result, { + importOrder: ['^a$', '^t$', '^k$', '^B'], + importOrderCaseInsensitive: false, + importOrderSeparation: false, + importOrderGroupNamespaceSpecifiers: false, + importOrderSortSpecifiers: true, + importOrderSideEffects: true, + }) as ImportDeclaration[]; + expect(getSortedNodesNames(sorted)).toEqual([ + 'XY', + 'Xa', + 'c', + 'g', + 'x', + 'z', + 'a', + 't', + 'k', + 'BY', + 'Ba', + ]); + expect( + sorted + .filter((node) => node.type === 'ImportDeclaration') + .map((importDeclaration) => + getSortedNodesModulesNames(importDeclaration.specifiers), + ), + ).toEqual([ + ['XY'], + ['Xa'], + ['c', 'cD'], + ['g'], + ['x'], + ['z'], + ['a'], + ['tA', 'tB', 'tC'], + ['k', 'kB', 'kE'], + ['BY'], + ['Ba'], + ]); +}); + +test('it returns all sorted import nodes with sorted import specifiers with case-insensitive ', () => { + const result = getImportNodes(code); + const sorted = getSortedNodes(result, { + importOrder: ['^a$', '^t$', '^k$', '^B'], + importOrderCaseInsensitive: true, + importOrderSeparation: false, + importOrderGroupNamespaceSpecifiers: false, + importOrderSortSpecifiers: true, + importOrderSideEffects: true, + }) as ImportDeclaration[]; + expect(getSortedNodesNames(sorted)).toEqual([ + 'c', + 'g', + 'x', + 'Xa', + 'XY', + 'z', + 'a', + 't', + 'k', + 'Ba', + 'BY', + ]); + expect( + sorted + .filter((node) => node.type === 'ImportDeclaration') + .map((importDeclaration) => + getSortedNodesModulesNames(importDeclaration.specifiers), + ), + ).toEqual([ + ['c', 'cD'], + ['g'], + ['x'], + ['Xa'], + ['XY'], + ['z'], + ['a'], + ['tA', 'tB', 'tC'], + ['k', 'kB', 'kE'], + ['Ba'], + ['BY'], + ]); +}); + +test('it returns all sorted nodes with custom third party modules', () => { + const result = getImportNodes(code); + const sorted = getSortedNodes(result, { + importOrder: ['^a$', '', '^t$', '^k$'], + importOrderSeparation: false, + importOrderCaseInsensitive: true, + importOrderGroupNamespaceSpecifiers: false, + importOrderSortSpecifiers: false, + importOrderSideEffects: true, + }) as ImportDeclaration[]; + expect(getSortedNodesNames(sorted)).toEqual([ + 'a', + 'Ba', + 'BY', + 'c', + 'g', + 'x', + 'Xa', + 'XY', + 'z', + 't', + 'k', + ]); +}); + +test('it returns all sorted nodes with namespace specifiers at the top', () => { + const result = getImportNodes(code); + const sorted = getSortedNodes(result, { + importOrder: [], + importOrderCaseInsensitive: false, + importOrderSeparation: false, + importOrderGroupNamespaceSpecifiers: true, + importOrderSortSpecifiers: false, + importOrderSideEffects: true, + }) as ImportDeclaration[]; + + expect(getSortedNodesNames(sorted)).toEqual([ + 'a', + 'x', + 'BY', + 'Ba', + 'XY', + 'Xa', + 'c', + 'g', + 'k', + 't', + 'z', + ]); +}); diff --git a/src/utils/__tests__/get-sorted-nodes.spec.ts b/src/utils/__tests__/get-sorted-nodes.spec.ts index 6f7e71ea..17dca351 100644 --- a/src/utils/__tests__/get-sorted-nodes.spec.ts +++ b/src/utils/__tests__/get-sorted-nodes.spec.ts @@ -46,6 +46,7 @@ test('it returns all sorted nodes', () => { importOrderSeparation: false, importOrderGroupNamespaceSpecifiers: false, importOrderSortSpecifiers: false, + importOrderSideEffects: true, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ @@ -90,6 +91,7 @@ test('it returns all sorted nodes case-insensitive', () => { importOrderSeparation: false, importOrderGroupNamespaceSpecifiers: false, importOrderSortSpecifiers: false, + importOrderSideEffects: true, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ @@ -134,6 +136,7 @@ test('it returns all sorted nodes with sort order', () => { importOrderSeparation: false, importOrderGroupNamespaceSpecifiers: false, importOrderSortSpecifiers: false, + importOrderSideEffects: true, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ @@ -178,6 +181,7 @@ test('it returns all sorted nodes with sort order case-insensitive', () => { importOrderSeparation: false, importOrderGroupNamespaceSpecifiers: false, importOrderSortSpecifiers: false, + importOrderSideEffects: true, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ 'c', @@ -221,6 +225,7 @@ test('it returns all sorted import nodes with sorted import specifiers', () => { importOrderSeparation: false, importOrderGroupNamespaceSpecifiers: false, importOrderSortSpecifiers: true, + importOrderSideEffects: true, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ 'XY', @@ -264,6 +269,7 @@ test('it returns all sorted import nodes with sorted import specifiers with case importOrderSeparation: false, importOrderGroupNamespaceSpecifiers: false, importOrderSortSpecifiers: true, + importOrderSideEffects: true, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ 'c', @@ -307,6 +313,7 @@ test('it returns all sorted nodes with custom third party modules', () => { importOrderCaseInsensitive: true, importOrderGroupNamespaceSpecifiers: false, importOrderSortSpecifiers: false, + importOrderSideEffects: true, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ 'a', @@ -331,6 +338,7 @@ test('it returns all sorted nodes with namespace specifiers at the top', () => { importOrderSeparation: false, importOrderGroupNamespaceSpecifiers: true, importOrderSortSpecifiers: false, + importOrderSideEffects: true, }) as ImportDeclaration[]; expect(getSortedNodesNames(sorted)).toEqual([ @@ -353,11 +361,12 @@ test('it returns all sorted nodes with types', () => { plugins: ['typescript'], }); const sorted = getSortedNodes(result, { - importOrder: ["", "^[./]", "^[./]"], + importOrder: ['', '^[./]', '^[./]'], importOrderCaseInsensitive: false, importOrderSeparation: false, importOrderGroupNamespaceSpecifiers: false, importOrderSortSpecifiers: false, + importOrderSideEffects: true, }) as ImportDeclaration[]; expect(getSortedNodesNames(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 b7f2ebd9..21dd7858 100644 --- a/src/utils/__tests__/remove-nodes-from-original-code.spec.ts +++ b/src/utils/__tests__/remove-nodes-from-original-code.spec.ts @@ -25,6 +25,7 @@ test('it should remove nodes from the original code', async () => { importOrderSeparation: false, importOrderGroupNamespaceSpecifiers: false, importOrderSortSpecifiers: false, + importOrderSideEffects: true, }); const allCommentsFromImports = getAllCommentsFromNodes(sortedNodes); @@ -36,6 +37,8 @@ test('it should remove nodes from the original code', async () => { code, commentAndImportsToRemoveFromCode, ); - const result = await format(codeWithoutImportDeclarations, { parser: 'babel' }); + const result = await format(codeWithoutImportDeclarations, { + parser: 'babel', + }); expect(result).toEqual(''); }); diff --git a/src/utils/adjust-comments-on-sorted-nodes.ts b/src/utils/adjust-comments-on-sorted-nodes.ts new file mode 100644 index 00000000..91396e9e --- /dev/null +++ b/src/utils/adjust-comments-on-sorted-nodes.ts @@ -0,0 +1,42 @@ +import { ImportDeclaration, addComments, removeComments } from '@babel/types'; +import { clone, isEqual } from 'lodash'; + +import { ImportOrLine } from '../types'; + +/** + * Takes the original nodes before sorting and the final nodes after sorting. + * Adjusts the comments on the final nodes so that they match the comments as + * they were in the original nodes. + * @param nodes A list of nodes in the order as they were originally. + * @param finalNodes The same set of nodes, but in the final sorting order. + */ +export const adjustCommentsOnSortedNodes = ( + nodes: ImportDeclaration[], + finalNodes: ImportOrLine[], +) => { + // maintain a copy of the nodes to extract comments from + const finalNodesClone = finalNodes.map(clone); + + const firstNodesComments = nodes[0].leadingComments; + + // Remove all comments from sorted nodes + finalNodes.forEach(removeComments); + + // insert comments other than the first comments + finalNodes.forEach((node, index) => { + if (isEqual(nodes[0].loc, node.loc)) return; + // remove comments location to not confuse print AST + firstNodesComments?.forEach((comment) => { + delete comment.loc; + }); + addComments( + node, + 'leading', + finalNodesClone[index].leadingComments || [], + ); + }); + + if (firstNodesComments) { + addComments(finalNodes[0], 'leading', firstNodesComments); + } +}; diff --git a/src/utils/get-all-comments-from-nodes.ts b/src/utils/get-all-comments-from-nodes.ts index 25b4e0f4..9c0896c7 100644 --- a/src/utils/get-all-comments-from-nodes.ts +++ b/src/utils/get-all-comments-from-nodes.ts @@ -1,12 +1,15 @@ import { CommentBlock, CommentLine, Statement } from '@babel/types'; export const getAllCommentsFromNodes = (nodes: Statement[]) => - nodes.reduce((acc, node) => { - if ( - Array.isArray(node.leadingComments) && - node.leadingComments.length > 0 - ) { - acc = [...acc, ...node.leadingComments]; - } - return acc; - }, [] as (CommentBlock | CommentLine)[]); + nodes.reduce( + (acc, node) => { + if ( + Array.isArray(node.leadingComments) && + node.leadingComments.length > 0 + ) { + acc = [...acc, ...node.leadingComments]; + } + return acc; + }, + [] as (CommentBlock | CommentLine)[], + ); diff --git a/src/utils/get-sorted-nodes-by-import-order.ts b/src/utils/get-sorted-nodes-by-import-order.ts new file mode 100644 index 00000000..d8ddd623 --- /dev/null +++ b/src/utils/get-sorted-nodes-by-import-order.ts @@ -0,0 +1,78 @@ +import { clone } from 'lodash'; + +import { THIRD_PARTY_MODULES_SPECIAL_WORD, newLineNode } from '../constants'; +import { naturalSort } from '../natural-sort'; +import { GetSortedNodes, ImportGroups, ImportOrLine } from '../types'; +import { getImportNodesMatchedGroup } from './get-import-nodes-matched-group'; +import { getSortedImportSpecifiers } from './get-sorted-import-specifiers'; +import { getSortedNodesGroup } from './get-sorted-nodes-group'; + +/** + * This function returns the given nodes, sorted in the order as indicated by + * the importOrder array from the given options. + * The plugin considers these import nodes as local import declarations. + * @param nodes A subset of all import nodes that should be sorted. + * @param options Options to influence the behavior of the sorting algorithm. + */ +export const getSortedNodesByImportOrder: GetSortedNodes = (nodes, options) => { + naturalSort.insensitive = options.importOrderCaseInsensitive; + + let { importOrder } = options; + const { + importOrderSeparation, + importOrderSortSpecifiers, + importOrderGroupNamespaceSpecifiers, + } = options; + + const originalNodes = nodes.map(clone); + const finalNodes: ImportOrLine[] = []; + + if (!importOrder.includes(THIRD_PARTY_MODULES_SPECIAL_WORD)) { + importOrder = [THIRD_PARTY_MODULES_SPECIAL_WORD, ...importOrder]; + } + + const importOrderGroups = importOrder.reduce( + (groups, regexp) => ({ + ...groups, + [regexp]: [], + }), + {}, + ); + + const importOrderWithOutThirdPartyPlaceholder = importOrder.filter( + (group) => group !== THIRD_PARTY_MODULES_SPECIAL_WORD, + ); + + for (const node of originalNodes) { + const matchedGroup = getImportNodesMatchedGroup( + node, + importOrderWithOutThirdPartyPlaceholder, + ); + importOrderGroups[matchedGroup].push(node); + } + + for (const group of importOrder) { + const groupNodes = importOrderGroups[group]; + + if (groupNodes.length === 0) continue; + + const sortedInsideGroup = getSortedNodesGroup(groupNodes, { + importOrderGroupNamespaceSpecifiers, + }); + + // Sort the import specifiers + if (importOrderSortSpecifiers) { + sortedInsideGroup.forEach((node) => + getSortedImportSpecifiers(node), + ); + } + + finalNodes.push(...sortedInsideGroup); + + if (importOrderSeparation) { + finalNodes.push(newLineNode); + } + } + + return finalNodes; +}; diff --git a/src/utils/get-sorted-nodes.ts b/src/utils/get-sorted-nodes.ts index 04b46760..4f5440b5 100644 --- a/src/utils/get-sorted-nodes.ts +++ b/src/utils/get-sorted-nodes.ts @@ -1,110 +1,74 @@ -import { addComments, removeComments } from '@babel/types'; -import { clone, isEqual } from 'lodash'; - -import { THIRD_PARTY_MODULES_SPECIAL_WORD, newLineNode } from '../constants'; -import { naturalSort } from '../natural-sort'; -import { GetSortedNodes, ImportGroups, ImportOrLine } from '../types'; -import { getImportNodesMatchedGroup } from './get-import-nodes-matched-group'; -import { getSortedImportSpecifiers } from './get-sorted-import-specifiers'; -import { getSortedNodesGroup } from './get-sorted-nodes-group'; +import { + chunkSideEffectNode, + chunkSideOtherNode, + newLineNode, +} from '../constants'; +import { GetSortedNodes, ImportChunk, ImportOrLine } from '../types'; +import { adjustCommentsOnSortedNodes } from './adjust-comments-on-sorted-nodes'; +import { getSortedNodesByImportOrder } from './get-sorted-nodes-by-import-order'; /** - * This function returns all the nodes which are in the importOrder array. - * The plugin considered these import nodes as local import declarations. - * @param nodes all import nodes - * @param options + * This function returns the given nodes, sorted in the order as indicated by + * the importOrder array. The plugin considers these import nodes as local + * import declarations + * + * In addition, this method preserves the relative order of side effect imports + * and non side effect imports. A side effect import is an ImportDeclaration + * without any import specifiers. It does this by splitting the import nodes at + * each side effect node, then sorting only the non side effect import nodes + * 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. */ export const getSortedNodes: GetSortedNodes = (nodes, options) => { - naturalSort.insensitive = options.importOrderCaseInsensitive; - - let { importOrder } = options; - const { - importOrderSeparation, - importOrderSortSpecifiers, - importOrderGroupNamespaceSpecifiers, - } = options; - - const originalNodes = nodes.map(clone); - const finalNodes: ImportOrLine[] = []; - - if (!importOrder.includes(THIRD_PARTY_MODULES_SPECIAL_WORD)) { - importOrder = [THIRD_PARTY_MODULES_SPECIAL_WORD, ...importOrder]; - } - - const importOrderGroups = importOrder.reduce( - (groups, regexp) => ({ - ...groups, - [regexp]: [], - }), - {}, - ); - - const importOrderWithOutThirdPartyPlaceholder = importOrder.filter( - (group) => group !== THIRD_PARTY_MODULES_SPECIAL_WORD, + const { importOrderSeparation, importOrderSideEffects } = + options; + + // Split nodes at each boundary between a side-effect node and a + // non-side-effect node, keeping both types of nodes together. + const splitBySideEffectNodes = nodes.reduce( + (chunks, node) => { + const isChunkEffectNode = + node.specifiers.length === 0 && + importOrderSideEffects === false; + const type = isChunkEffectNode + ? chunkSideEffectNode + : chunkSideOtherNode; + const last = chunks[chunks.length - 1]; + if (last === undefined || last.type !== type) { + chunks.push({ type, nodes: [node] }); + } else { + last.nodes.push(node); + } + return chunks; + }, + [], ); - for (const node of originalNodes) { - const matchedGroup = getImportNodesMatchedGroup( - node, - importOrderWithOutThirdPartyPlaceholder, - ); - importOrderGroups[matchedGroup].push(node); - } - - for (const group of importOrder) { - const groupNodes = importOrderGroups[group]; - - if (groupNodes.length === 0) continue; - - const sortedInsideGroup = getSortedNodesGroup(groupNodes, { - importOrderGroupNamespaceSpecifiers, - }); + const finalNodes: ImportOrLine[] = []; - // Sort the import specifiers - if (importOrderSortSpecifiers) { - sortedInsideGroup.forEach((node) => - getSortedImportSpecifiers(node), - ); + // Sort each chunk of side-effect and non-side-effect nodes, and insert new + // lines according the importOrderSeparation option. + for (const chunk of splitBySideEffectNodes) { + if (chunk.type === chunkSideEffectNode) { + // do not sort side effect nodes + finalNodes.push(...chunk.nodes); + } else { + // sort non-side effect nodes + const sorted = getSortedNodesByImportOrder(chunk.nodes, options); + finalNodes.push(...sorted); } - - finalNodes.push(...sortedInsideGroup); - if (importOrderSeparation) { finalNodes.push(newLineNode); } } if (finalNodes.length > 0 && !importOrderSeparation) { - // a newline after all imports finalNodes.push(newLineNode); } - // maintain a copy of the nodes to extract comments from - const finalNodesClone = finalNodes.map(clone); - - const firstNodesComments = nodes[0].leadingComments; - - // Remove all comments from sorted nodes - finalNodes.forEach(removeComments); - - // insert comments other than the first comments - finalNodes.forEach((node, index) => { - if (isEqual(nodes[0].loc, node.loc)) return; - - addComments( - node, - 'leading', - finalNodesClone[index].leadingComments || [], - ); - }); - - if (firstNodesComments) { - // remove comments location to not confuse printe - firstNodesComments.forEach((comment) => { - delete comment.loc; - }); - addComments(finalNodes[0], 'leading', firstNodesComments); - } + // Adjust the comments on the sorted nodes to match the original comments + adjustCommentsOnSortedNodes(nodes, finalNodes); return finalNodes; }; diff --git a/src/utils/is-sort-imports-ignored.ts b/src/utils/is-sort-imports-ignored.ts index 357e02a7..6a1845fb 100644 --- a/src/utils/is-sort-imports-ignored.ts +++ b/src/utils/is-sort-imports-ignored.ts @@ -6,6 +6,7 @@ import { getAllCommentsFromNodes } from './get-all-comments-from-nodes'; export const isSortImportsIgnored = (nodes: Statement[]) => getAllCommentsFromNodes(nodes).some( (comment) => - comment.loc && comment.loc.start.line === 1 && + comment.loc && + comment.loc.start.line === 1 && comment.value.includes(sortImportsIgnoredComment), ); diff --git a/test-setup/run_spec.js b/test-setup/run_spec.js index 73ad1200..f9a3c191 100644 --- a/test-setup/run_spec.js +++ b/test-setup/run_spec.js @@ -35,7 +35,7 @@ function run_spec(dirname, parsers, options) { test(`${filename} - ${mergedOptions.parser}-verify`, async () => { try { expect( - raw(source + '~'.repeat(80) + '\n' + await output), + raw(source + '~'.repeat(80) + '\n' + (await output)), ).toMatchSnapshot(filename); } catch (e) { console.error(e, path); diff --git a/tests/ImportPreventSortingSideEffects/__snapshots__/ppsi.spec.js.snap b/tests/ImportPreventSortingSideEffects/__snapshots__/ppsi.spec.js.snap new file mode 100644 index 00000000..fba106ea --- /dev/null +++ b/tests/ImportPreventSortingSideEffects/__snapshots__/ppsi.spec.js.snap @@ -0,0 +1,102 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`imports-with-side-effect-imports.js - typescript-verify: imports-with-side-effect-imports.js 1`] = ` +// I am top level comment in this file. +import thirdParty0 from "third-party0"; +import something3 from "@core/something3"; +import thirdDisco0 from "third-disco0"; +import otherthing3 from "@core/otherthing3"; + +import "side-effect-z"; + +import anotherSameLevelRelativePath3 from "./anotherSameLevelRelativePath3"; +import something0 from "@core/something0"; +import thirdDisco1 from "third-disco1"; +import otherthing0 from "@core/otherthing0"; +import sameLevelRelativePath3 from "./sameLevelRelativePath3"; +import thirdParty1 from "third-party1"; +import oneLevelRelativePath1 from "../oneLevelRelativePath1"; +import anotherOneLevelRelativePath1 from "../anotherOneLevelRelativePath1"; + +import "side-effect-y3"; +import "side-effect-y1"; +import "side-effect-y2"; + +import oneLevelRelativePath2 from "../oneLevelRelativePath2"; +import anotherOneLevelRelativePath2 from "../anotherOneLevelRelativePath2"; +import something2 from "@core/something2"; +import thirdParty3 from "third-party3"; +import anotherSameLevelRelativePath1 from "./anotherSameLevelRelativePath1"; +import sameLevelRelativePath1 from "./sameLevelRelativePath1"; +import otherthing2 from "@core/otherthing2"; +import thirdDisco3 from "third-disco3"; + +import "side-effect-x"; +import anotherSameLevelRelativePath2 from "./anotherSameLevelRelativePath2"; +import sameLevelRelativePath2 from "./sameLevelRelativePath2"; +import something1 from "@core/something1"; +import oneLevelRelativePath3 from "../oneLevelRelativePath3"; +import anotherOneLevelRelativePath3 from "../anotherOneLevelRelativePath3"; +import otherthing1 from "@core/otherthing1"; +import thirdDisco2 from "third-disco2"; +import thirdParty2 from "third-party2"; + +import { Component } from "@angular/core"; + +function add(a,b) { + return a + b; +}~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// I am top level comment in this file. +import thirdDisco0 from "third-disco0"; +import thirdParty0 from "third-party0"; + +import otherthing3 from "@core/otherthing3"; +import something3 from "@core/something3"; + +import "side-effect-z"; + +import thirdDisco1 from "third-disco1"; +import thirdParty1 from "third-party1"; + +import otherthing0 from "@core/otherthing0"; +import something0 from "@core/something0"; + +import anotherOneLevelRelativePath1 from "../anotherOneLevelRelativePath1"; +import oneLevelRelativePath1 from "../oneLevelRelativePath1"; +import anotherSameLevelRelativePath3 from "./anotherSameLevelRelativePath3"; +import sameLevelRelativePath3 from "./sameLevelRelativePath3"; + +import "side-effect-y3"; +import "side-effect-y1"; +import "side-effect-y2"; + +import thirdDisco3 from "third-disco3"; +import thirdParty3 from "third-party3"; + +import otherthing2 from "@core/otherthing2"; +import something2 from "@core/something2"; + +import anotherOneLevelRelativePath2 from "../anotherOneLevelRelativePath2"; +import oneLevelRelativePath2 from "../oneLevelRelativePath2"; +import anotherSameLevelRelativePath1 from "./anotherSameLevelRelativePath1"; +import sameLevelRelativePath1 from "./sameLevelRelativePath1"; + +import "side-effect-x"; + +import { Component } from "@angular/core"; +import thirdDisco2 from "third-disco2"; +import thirdParty2 from "third-party2"; + +import otherthing1 from "@core/otherthing1"; +import something1 from "@core/something1"; + +import anotherOneLevelRelativePath3 from "../anotherOneLevelRelativePath3"; +import oneLevelRelativePath3 from "../oneLevelRelativePath3"; +import anotherSameLevelRelativePath2 from "./anotherSameLevelRelativePath2"; +import sameLevelRelativePath2 from "./sameLevelRelativePath2"; + +function add(a, b) { + return a + b; +} + +`; diff --git a/tests/ImportPreventSortingSideEffects/imports-with-side-effect-imports.js b/tests/ImportPreventSortingSideEffects/imports-with-side-effect-imports.js new file mode 100644 index 00000000..40dfe78d --- /dev/null +++ b/tests/ImportPreventSortingSideEffects/imports-with-side-effect-imports.js @@ -0,0 +1,45 @@ +// I am top level comment in this file. +import thirdParty0 from "third-party0"; +import something3 from "@core/something3"; +import thirdDisco0 from "third-disco0"; +import otherthing3 from "@core/otherthing3"; + +import "side-effect-z"; + +import anotherSameLevelRelativePath3 from "./anotherSameLevelRelativePath3"; +import something0 from "@core/something0"; +import thirdDisco1 from "third-disco1"; +import otherthing0 from "@core/otherthing0"; +import sameLevelRelativePath3 from "./sameLevelRelativePath3"; +import thirdParty1 from "third-party1"; +import oneLevelRelativePath1 from "../oneLevelRelativePath1"; +import anotherOneLevelRelativePath1 from "../anotherOneLevelRelativePath1"; + +import "side-effect-y3"; +import "side-effect-y1"; +import "side-effect-y2"; + +import oneLevelRelativePath2 from "../oneLevelRelativePath2"; +import anotherOneLevelRelativePath2 from "../anotherOneLevelRelativePath2"; +import something2 from "@core/something2"; +import thirdParty3 from "third-party3"; +import anotherSameLevelRelativePath1 from "./anotherSameLevelRelativePath1"; +import sameLevelRelativePath1 from "./sameLevelRelativePath1"; +import otherthing2 from "@core/otherthing2"; +import thirdDisco3 from "third-disco3"; + +import "side-effect-x"; +import anotherSameLevelRelativePath2 from "./anotherSameLevelRelativePath2"; +import sameLevelRelativePath2 from "./sameLevelRelativePath2"; +import something1 from "@core/something1"; +import oneLevelRelativePath3 from "../oneLevelRelativePath3"; +import anotherOneLevelRelativePath3 from "../anotherOneLevelRelativePath3"; +import otherthing1 from "@core/otherthing1"; +import thirdDisco2 from "third-disco2"; +import thirdParty2 from "third-party2"; + +import { Component } from "@angular/core"; + +function add(a,b) { + return a + b; +} \ No newline at end of file diff --git a/tests/ImportPreventSortingSideEffects/ppsi.spec.js b/tests/ImportPreventSortingSideEffects/ppsi.spec.js new file mode 100644 index 00000000..41bf3319 --- /dev/null +++ b/tests/ImportPreventSortingSideEffects/ppsi.spec.js @@ -0,0 +1,6 @@ +run_spec(__dirname, ["typescript"], { + importOrder: ['^@core/(.*)$', '^@server/(.*)', '^@ui/(.*)$', '^[./]'], + importOrderSeparation: true, + importOrderSideEffects: false, + importOrderParserPlugins: ['typescript'] +});