Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: importOrderMergeDuplicateImports #19

Merged
merged 15 commits into from Jun 9, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 17 additions & 3 deletions README.md
Expand Up @@ -86,21 +86,27 @@ 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.

```ecmascript 6
```javascript
module.exports = {
"printWidth": 80,
"tabWidth": 4,
"trailingComma": "all",
"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
Expand Down Expand Up @@ -204,6 +210,14 @@ import ExamplesList from './ExamplesList';
import ExampleView from './ExampleView';
```

#### `importOrderMergeDuplicateImports`
IanVS marked this conversation as resolved.
Show resolved Hide resolved

**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.
fbartho marked this conversation as resolved.
Show resolved Hide resolved

#### `importOrderParserPlugins`

**type**: `Array<string>`
Expand Down
6 changes: 3 additions & 3 deletions docs/TROUBLESHOOTING.md
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -35,7 +35,7 @@ import s from './';
You can define the `<THIRD_PARTY_MODULES>` special word in the `importOrder`. For example above, the `importOrder` would be like `["^@ui/(.*)$", "^@server/(.*)$", "<THIRD_PARTY_MODULES>", '^[./]']`.
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';
Expand Down
11 changes: 11 additions & 0 deletions src/constants.ts
Expand Up @@ -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';
fbartho marked this conversation as resolved.
Show resolved Hide resolved
/** 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
Expand Down
22 changes: 21 additions & 1 deletion 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<keyof PrettierOptions, keyof PrettierRequiredOptions>,
PrettierOptionSchema
> = {
importOrder: {
type: 'path',
category: 'Global',
Expand Down Expand Up @@ -52,6 +66,12 @@ const options = {
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 = {
Expand Down
22 changes: 15 additions & 7 deletions src/preprocessor.ts
Expand Up @@ -11,11 +11,12 @@ export function preprocessor(code: string, options: PrettierOptions): string {
const {
importOrderParserPlugins,
importOrder,
importOrderBuiltinModulesToTop,
importOrderCaseInsensitive,
importOrderSeparation,
importOrderGroupNamespaceSpecifiers,
importOrderMergeDuplicateImports,
importOrderSeparation,
importOrderSortSpecifiers,
importOrderBuiltinModulesToTop,
} = options;

const importNodes: ImportDeclaration[] = [];
Expand All @@ -40,19 +41,26 @@ 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, {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question, why the change to the name here? Remaining after what? Is this because getSortedNodes will now also combine/remove some nodes?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the return value of getSortedNodes is no longer "all imports" it's just the "imports that weren't deleted"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll rename this inline with renames I'm doing to match your feedback on naming of parameters to getCodeFromAst

importOrder,
importOrderBuiltinModulesToTop,
importOrderCaseInsensitive,
importOrderSeparation,
importOrderGroupNamespaceSpecifiers,
importOrderMergeDuplicateImports,
importOrderSeparation,
importOrderSortSpecifiers,
importOrderBuiltinModulesToTop,
});

return getCodeFromAst(allImports, code, directives, interpreter);
return getCodeFromAst({
nodes: remainingImports,
importNodes,
originalCode: code,
directives,
interpreter,
});
}
16 changes: 12 additions & 4 deletions src/types.ts
Expand Up @@ -5,11 +5,12 @@ 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;
importOrderMergeDuplicateImports: 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 {
Expand All @@ -27,10 +28,17 @@ export type GetSortedNodes = (
| 'importOrder'
| 'importOrderBuiltinModulesToTop'
| 'importOrderCaseInsensitive'
| 'importOrderSeparation'
| 'importOrderGroupNamespaceSpecifiers'
| 'importOrderMergeDuplicateImports'
| 'importOrderSeparation'
| 'importOrderSortSpecifiers'
>,
) => ImportOrLine[];

export type GetChunkTypeOfNode = (node: ImportDeclaration) => string;

export type GetImportFlavorOfNode = (node: ImportDeclaration) => string;
IanVS marked this conversation as resolved.
Show resolved Hide resolved

export type MergeNodesWithMatchingImportFlavors = (
nodes: ImportDeclaration[],
) => ImportDeclaration[];
5 changes: 3 additions & 2 deletions src/utils/__tests__/get-all-comments-from-nodes.spec.ts
Expand Up @@ -10,11 +10,12 @@ const getSortedImportNodes = (code: string, options?: ParserOptions) => {

return getSortedNodes(importNodes, {
importOrder: [],
importOrderBuiltinModulesToTop: false,
importOrderCaseInsensitive: false,
importOrderSeparation: false,
importOrderGroupNamespaceSpecifiers: false,
importOrderMergeDuplicateImports: false,
importOrderSeparation: false,
importOrderSortSpecifiers: false,
importOrderBuiltinModulesToTop: false,
});
};

Expand Down
53 changes: 49 additions & 4 deletions src/utils/__tests__/get-code-from-ast.spec.ts
Expand Up @@ -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';
Expand All @@ -17,13 +17,18 @@ import a from 'a';
const importNodes = getImportNodes(code);
const sortedNodes = getSortedNodes(importNodes, {
importOrder: [],
importOrderBuiltinModulesToTop: false,
importOrderCaseInsensitive: false,
importOrderSeparation: false,
importOrderGroupNamespaceSpecifiers: false,
importOrderMergeDuplicateImports: 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
Expand All @@ -36,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";
fbartho marked this conversation as resolved.
Show resolved Hide resolved
import c from "c";
import g from "g";
import k from "k";
import t from "t";
import z from "z";
`,
);
});
32 changes: 32 additions & 0 deletions 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', () => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a bit of a preference for breaking this into individual tests, with explicit assertions, rather than an inline snapshot. I find it easier to read, understand, and maintain.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bunch of the individual cases for this are actually added in #20 -- would you mind reviewing the test file in there instead?

This is just a bulk test that I had originally, lazily created. If after reviewing the tests there you still want me to delete or recreate this test, please let me know!

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",
fbartho marked this conversation as resolved.
Show resolved Hide resolved
"regular",
"prettier-ignore",
]
`);
});