Skip to content

Commit e28fa0e

Browse files
connebsIanVS
andauthored
Re-add importOrderCaseSensitive option (#184)
Implements a custom natural sort algorithm which allows for numeric natural sorting while also sorting all uppercase letters before all lowercase letters, which is the desired behavior here. For example, when `importOrderCaseSensitive` is false (the default): ```js import ExampleComponent from './ExampleComponent'; import ExamplesList from './ExamplesList'; import ExampleWidget from './ExampleWidget'; ``` Compared with `"importOrderCaseSensitive": true`: ```js import ExampleComponent from './ExampleComponent'; import ExampleWidget from './ExampleWidget'; import ExamplesList from './ExamplesList'; ``` Closes #151 --------- Co-authored-by: Ian VanSchooten <ian.vanschooten@gmail.com>
1 parent fef70a6 commit e28fa0e

17 files changed

Lines changed: 334 additions & 34 deletions

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ This project is based on [@trivago/prettier-plugin-sort-imports](https://github.
3434
- [7. Enable/disable plugin or use different order in certain folders or files](#7-enabledisable-plugin-or-use-different-order-in-certain-folders-or-files)
3535
- [`importOrderTypeScriptVersion`](#importordertypescriptversion)
3636
- [`importOrderParserPlugins`](#importorderparserplugins)
37+
- [`importOrderCaseSensitive`](#importordercasesensitive)
3738
- [Prevent imports from being sorted](#prevent-imports-from-being-sorted)
3839
- [Comments](#comments)
3940
- [FAQ / Troubleshooting](#faq--troubleshooting)
@@ -139,6 +140,7 @@ module.exports = {
139140
importOrder: ['^@core/(.*)$', '', '^@server/(.*)$', '', '^@ui/(.*)$', '', '^[./]'],
140141
importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'],
141142
importOrderTypeScriptVersion: '5.0.0',
143+
importOrderCaseSensitive: false,
142144
};
143145
```
144146

@@ -393,6 +395,33 @@ with options as a JSON string of the plugin array:
393395
"importOrderParserPlugins": []
394396
```
395397

398+
#### `importOrderCaseSensitive`
399+
400+
**type**: `boolean`
401+
402+
**default value**: `false`
403+
404+
A boolean value to enable case-sensitivity in the sorting algorithm
405+
used to order imports within each match group.
406+
407+
For example, when false (or not specified):
408+
409+
```javascript
410+
import {CatComponent, catFilter, DogComponent, dogFilter} from './animals';
411+
import ExampleComponent from './ExampleComponent';
412+
import ExamplesList from './ExamplesList';
413+
import ExampleWidget from './ExampleWidget';
414+
```
415+
416+
compared with `"importOrderCaseSensitive": true`:
417+
418+
```javascript
419+
import ExampleComponent from './ExampleComponent';
420+
import ExampleWidget from './ExampleWidget';
421+
import ExamplesList from './ExamplesList';
422+
import {CatComponent, DogComponent, catFilter, dogFilter} from './animals';
423+
```
424+
396425
### Prevent imports from being sorted
397426

398427
This plugin supports standard prettier ignore comments. By default, side-effect imports (like

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ export const options: Record<
5050
description:
5151
'Version of TypeScript in use in the project. Determines some output syntax when using TypeScript.',
5252
},
53+
importOrderCaseSensitive: {
54+
type: 'boolean',
55+
category: 'Global',
56+
default: false,
57+
description: 'Provide a case sensitivity boolean flag',
58+
},
5359
};
5460

5561
export const parsers = {
Lines changed: 109 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,115 @@
1-
import { expect, test } from 'vitest';
1+
import { describe, expect, test } from 'vitest';
22

3-
import { naturalSort } from '..';
3+
import { naturalSort, naturalSortCaseSensitive } from '..';
44

5-
test('should sort normal things alphabetically', () => {
6-
expect(
7-
['a', 'h', 'b', 'i', 'c', 'd', 'j', 'e', 'k', 'f', 'g'].sort((a, b) =>
8-
naturalSort(a, b),
9-
),
10-
).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k']);
11-
});
5+
describe('naturalSort', () => {
6+
test('should sort normal things alphabetically', () => {
7+
expect(
8+
['a', 'h', 'b', 'i', 'c', 'd', 'j', 'e', 'k', 'f', 'g'].sort(
9+
(a, b) => naturalSort(a, b),
10+
),
11+
).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k']);
12+
});
13+
14+
test('should ignore capitalization differences', () => {
15+
expect(
16+
['./ExampleComponent', './ExamplesList', './ExampleWidget'].sort(
17+
(a, b) => naturalSort(a, b),
18+
),
19+
).toEqual(['./ExampleComponent', './ExamplesList', './ExampleWidget']);
20+
});
1221

13-
test('should ignore capitalization differences', () => {
14-
// We have no option to cause case-sensitive sorting, so this is the "default" case!
15-
expect(
16-
['./ExampleView', './ExamplesList'].sort((a, b) => naturalSort(a, b)),
17-
).toEqual(['./ExamplesList', './ExampleView']);
22+
test('should sort things numerically', () => {
23+
expect(
24+
[
25+
'a2',
26+
'a3',
27+
'a10',
28+
'a1',
29+
'a11',
30+
'a9',
31+
'a1b',
32+
'file000b',
33+
'file000a',
34+
'file00a',
35+
'file00z',
36+
].sort(naturalSort),
37+
).toEqual([
38+
'a1',
39+
'a1b',
40+
'a2',
41+
'a3',
42+
'a9',
43+
'a10',
44+
'a11',
45+
'file000a',
46+
'file00a',
47+
'file000b',
48+
'file00z',
49+
]);
50+
});
1851
});
1952

20-
test('should sort things numerically', () => {
21-
expect(
22-
['a2', 'a3', 'a10', 'a1', 'a11', 'a9'].sort((a, b) =>
23-
naturalSort(a, b),
24-
),
25-
).toEqual(['a1', 'a2', 'a3', 'a9', 'a10', 'a11']);
53+
describe('naturalSortCaseSensitive', () => {
54+
test('should not ignore capitalization differences', () => {
55+
expect(
56+
['./ExampleComponent', './ExamplesList', './ExampleWidget'].sort(
57+
(a, b) => naturalSortCaseSensitive(a, b),
58+
),
59+
).toEqual(['./ExampleComponent', './ExampleWidget', './ExamplesList']);
60+
});
61+
62+
test('should sort numerically and case-sensitively', () => {
63+
expect(
64+
[
65+
'file1',
66+
'File10',
67+
'AbA',
68+
'file10',
69+
'files10',
70+
'file1z',
71+
'file10ab',
72+
'file2s',
73+
'a',
74+
'Ab',
75+
'file20',
76+
'file22',
77+
'file11',
78+
'file2',
79+
'File20',
80+
'file000b',
81+
'file000a',
82+
'file00a',
83+
'file00z',
84+
'aaa',
85+
'AAA',
86+
'bBb',
87+
'BBB',
88+
].sort(naturalSortCaseSensitive),
89+
).toEqual([
90+
'AAA',
91+
'Ab',
92+
'AbA',
93+
'BBB',
94+
'File10',
95+
'File20',
96+
'a',
97+
'aaa',
98+
'bBb',
99+
'file000a',
100+
'file00a',
101+
'file000b',
102+
'file00z',
103+
'file1',
104+
'file1z',
105+
'file2',
106+
'file2s',
107+
'file10',
108+
'file10ab',
109+
'file11',
110+
'file20',
111+
'file22',
112+
'files10',
113+
]);
114+
});
26115
});

src/natural-sort/index.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,47 @@
11
export function naturalSort(a: string, b: string): number {
22
const left = typeof a === 'string' ? a : String(a);
3-
43
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#syntax
54
const sortOptions: Intl.CollatorOptions = {
65
sensitivity: 'base',
76
numeric: true,
87
caseFirst: 'lower',
98
};
10-
119
return left.localeCompare(b, 'en', sortOptions);
1210
}
11+
12+
/**
13+
* Using a custom comparison function here, as `String.localeCompare` does not
14+
* support sorting characters with all uppercase letters before lowercase
15+
* letters, which is the desired behavior for a case-sensitive import sort. When
16+
* `sensitivity` is set to `base`, `String.localeCompare` sorts alphabetically
17+
* and then by case, but we want to sort by case first (then alphabetical).
18+
*/
19+
const numericRegex = /^\d+/;
20+
export function naturalSortCaseSensitive(a: string, b: string) {
21+
let aIndex = 0;
22+
let bIndex = 0;
23+
while (aIndex < Math.max(a.length, b.length)) {
24+
// check if we've encountered a number and compare appropriately if so
25+
const aNumericMatch = a.slice(aIndex).match(numericRegex);
26+
const bNumericMatch = b.slice(bIndex).match(numericRegex);
27+
if (aNumericMatch && !bNumericMatch) return -1;
28+
if (!aNumericMatch && bNumericMatch) return 1;
29+
if (aNumericMatch && bNumericMatch) {
30+
const aNumber = parseInt(aNumericMatch[0]);
31+
const bNumber = parseInt(bNumericMatch[0]);
32+
if (aNumber > bNumber) return 1;
33+
if (aNumber < bNumber) return -1;
34+
aIndex += aNumericMatch[0].length;
35+
bIndex += bNumericMatch[0].length;
36+
}
37+
// otherwise just compare characters directly
38+
const aChar = a[aIndex];
39+
const bChar = b[bIndex];
40+
if (aChar && !bChar) return 1;
41+
if (!aChar && bChar) return -1;
42+
if (aChar !== bChar) return aChar.charCodeAt(0) - bChar.charCodeAt(0);
43+
aIndex++;
44+
bIndex++;
45+
}
46+
return 0;
47+
}

src/types.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ export interface PrettierOptions
2626
/** Subset of options that need to be normalized, or affect normalization */
2727
export type NormalizableOptions = Pick<
2828
PrettierOptions,
29-
'importOrder' | 'importOrderParserPlugins' | 'importOrderTypeScriptVersion'
29+
| 'importOrder'
30+
| 'importOrderParserPlugins'
31+
| 'importOrderTypeScriptVersion'
32+
| 'importOrderCaseSensitive'
3033
> &
3134
// filepath can be undefined when running prettier via the api on text input
3235
Pick<Partial<PrettierOptions>, 'filepath'>;
@@ -63,6 +66,7 @@ export type ImportRelated = ImportOrLine | SomeSpecifier;
6366
export interface ExtendedOptions {
6467
importOrder: PrettierOptions['importOrder'];
6568
importOrderCombineTypeAndValueImports: boolean;
69+
importOrderCaseSensitive: boolean;
6670
hasAnyCustomGroupSeparatorsInImportOrder: boolean;
6771
provideGapAfterTopOfFileComments: boolean;
6872
plugins: ParserPlugin[];
@@ -79,7 +83,9 @@ export type GetSortedNodes = (
7983

8084
export type GetSortedNodesByImportOrder = (
8185
nodes: ImportDeclaration[],
82-
options: Pick<ExtendedOptions, 'importOrder'>,
86+
options: Pick<ExtendedOptions, 'importOrder'> & {
87+
importOrderCaseSensitive?: boolean;
88+
},
8389
) => ImportOrLine[];
8490

8591
export type GetChunkTypeOfNode = (node: ImportDeclaration) => ChunkType;

src/utils/__tests__/get-sorted-import-specifiers.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,20 @@ test('should group type imports after value imports - flow', () => {
7070
'TypeB',
7171
]);
7272
});
73+
74+
test('should sort case-sensitively', () => {
75+
const code = `import { ExampleComponent, ExamplesList, ExampleWidget } from '@components/e';`;
76+
const [importNode] = getImportNodes(code);
77+
const sortedImportSpecifiers = getSortedImportSpecifiers(importNode, {
78+
importOrderCaseSensitive: true,
79+
});
80+
const specifiersList = getSortedNodesModulesNames(
81+
sortedImportSpecifiers.specifiers,
82+
);
83+
84+
expect(specifiersList).toEqual([
85+
'ExampleComponent',
86+
'ExampleWidget',
87+
'ExamplesList',
88+
]);
89+
});

src/utils/__tests__/get-sorted-nodes-by-import-order.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,28 @@ test('it does not add multiple custom import separators', () => {
274274
'./local',
275275
]);
276276
});
277+
278+
test('it should sort nodes case-sensitively', () => {
279+
const result = getImportNodes(code);
280+
const sorted = getSortedNodesByImportOrder(result, {
281+
importOrder: testingOnly.normalizeImportOrderOption(['^[./]']),
282+
importOrderCaseSensitive: true,
283+
}) as ImportDeclaration[];
284+
expect(getSortedNodesNamesAndNewlines(sorted)).toEqual([
285+
'node:fs/promises',
286+
'node:url',
287+
'path',
288+
'BY',
289+
'Ba',
290+
'XY',
291+
'Xa',
292+
'a',
293+
'c',
294+
'g',
295+
'k',
296+
't',
297+
'x',
298+
'z',
299+
'./local',
300+
]);
301+
});

src/utils/__tests__/merge-nodes-with-matching-flavors.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const defaultOptions = examineAndNormalizePluginOptions({
1313
// First separator for top-of-file comments, second to separate side-effect and ignored chunks, for easier test readability
1414
importOrder: testingOnly.normalizeImportOrderOption(['', '']),
1515
importOrderTypeScriptVersion: '5.0.0',
16+
importOrderCaseSensitive: false,
1617
importOrderParserPlugins: [],
1718
filepath: __filename,
1819
});

0 commit comments

Comments
 (0)