Skip to content

Commit 1f2670a

Browse files
authored
Add importOrderSafeSideEffects option (#240)
Closes #188 This adds a new option, `importOrderSafeSideEffects`, which is an array of regex patterns (similar to `importOrder`) specifying side-effect-only imports which are considered "safe" to reorder along with the rest of imports.
1 parent 18637ed commit 1f2670a

17 files changed

+199
-22
lines changed

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ A prettier plugin to sort import declarations by provided Regular Expression ord
44

55
This project is based on [@trivago/prettier-plugin-sort-imports](https://github.com/trivago/prettier-plugin-sort-imports), but adds additional features:
66

7-
- Does not re-order across side-effect imports
7+
- Does not re-order across side-effect imports by default
88
- Combines imports from the same source
99
- Combines type and value imports (if `importOrderTypeScriptVersion` is set to `"4.5.0"` or higher)
1010
- Groups type imports with `<TYPES>` keyword
@@ -33,6 +33,7 @@ This project is based on [@trivago/prettier-plugin-sort-imports](https://github.
3333
- [5. Group aliases with local imports](#5-group-aliases-with-local-imports)
3434
- [6. Enforce a blank line after top of file comments](#6-enforce-a-blank-line-after-top-of-file-comments)
3535
- [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)
36+
- [`importOrderSafeSideEffects`](#importordersafesideeffects)
3637
- [`importOrderTypeScriptVersion`](#importordertypescriptversion)
3738
- [`importOrderParserPlugins`](#importorderparserplugins)
3839
- [`importOrderCaseSensitive`](#importordercasesensitive)
@@ -360,6 +361,18 @@ This can also be beneficial for large projects wishing to gradually adopt a sort
360361

361362
You can also do this in reverse, where the plugin is enabled globally, but disabled for a set of files or directories in the overrides configuration. It is also useful for setting a different sort order to use in certain files or directories instead of the global sort order.
362363

364+
#### `importOrderSafeSideEffects`
365+
366+
**type**: `Array<string>`
367+
368+
**default value:** `[]`
369+
370+
In general, it is not safe to reorder imports that do not actually import anything (side-effect-only imports), because these imports are affecting the global scope, the order in which they occur can be important.
371+
372+
However, in some cases, you may know that some of your side-effect imports can be sorted along with normal imports. For example, `import "server-only"` can be used in some React applications to ensure some code only runs on the server. For these cases, this option is an escape hatch.
373+
374+
This option accepts an array of regex patterns which will be compared against side-effect-only imports to determine if they are safe to reorder along with the rest of your imports. By default, no such imports are considered safe. You can opt-in to sorting them by adding them to this option. We recommend using `^` at the start and `$` at the end of your pattern, to be sure they match exactly.
375+
363376
#### `importOrderTypeScriptVersion`
364377

365378
**type**: `string`

src/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ export const options = {
4545
description:
4646
'Should capitalization be considered when sorting imports?',
4747
},
48+
importOrderSafeSideEffects: {
49+
type: 'string',
50+
category: 'Global',
51+
array: true,
52+
default: [{ value: [] }],
53+
description:
54+
'Array of globs for side-effect-only imports that are considered safe to sort.',
55+
},
4856
} satisfies Record<
4957
keyof PluginConfig,
5058
StringArraySupportOption | BooleanSupportOption | StringSupportOption

src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@ export type NormalizableOptions = Pick<
2323
| 'importOrderParserPlugins'
2424
| 'importOrderTypeScriptVersion'
2525
| 'importOrderCaseSensitive'
26+
| 'importOrderSafeSideEffects'
2627
> &
2728
// filepath can be undefined when running prettier via the api on text input
2829
Pick<Partial<PrettierOptions>, 'filepath'>;
2930

30-
type ChunkType = typeof chunkTypeOther | typeof chunkTypeUnsortable;
31+
export type ChunkType = typeof chunkTypeOther | typeof chunkTypeUnsortable;
3132

3233
export interface ImportChunk {
3334
nodes: ImportDeclaration[];

src/utils/__tests__/get-all-comments-from-nodes.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const getSortedImportNodes = (code: string, options?: ParserOptions) => {
2222
importOrderCaseSensitive: false,
2323
hasAnyCustomGroupSeparatorsInImportOrder: false,
2424
provideGapAfterTopOfFileComments: false,
25+
importOrderSafeSideEffects: [],
2526
});
2627
};
2728

src/utils/__tests__/get-chunk-type-of-node.spec.ts

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,52 +4,68 @@ import { chunkTypeOther, chunkTypeUnsortable } from '../../constants';
44
import { getChunkTypeOfNode } from '../get-chunk-type-of-node';
55
import { getImportNodes } from '../get-import-nodes';
66

7+
const SAFE_OPTION_EMPTY: string[] = [];
8+
79
test('it classifies a default import as other', () => {
810
const importNodes = getImportNodes(`import a from "a";`);
911
expect(importNodes.length).toBe(1);
10-
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeOther);
12+
expect(getChunkTypeOfNode(importNodes[0], SAFE_OPTION_EMPTY)).toBe(
13+
chunkTypeOther,
14+
);
1115
});
1216

1317
test('it classifies a named import as other', () => {
1418
const importNodes = getImportNodes(`import {a} from "a";`);
1519
expect(importNodes.length).toBe(1);
16-
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeOther);
20+
expect(getChunkTypeOfNode(importNodes[0], SAFE_OPTION_EMPTY)).toBe(
21+
chunkTypeOther,
22+
);
1723
});
1824

1925
test('it classifies a type import as other', () => {
2026
const importNodes = getImportNodes(`import type {a, b} from "a";`, {
2127
plugins: ['typescript'],
2228
});
2329
expect(importNodes.length).toBe(1);
24-
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeOther);
30+
expect(getChunkTypeOfNode(importNodes[0], SAFE_OPTION_EMPTY)).toBe(
31+
chunkTypeOther,
32+
);
2533
});
2634

2735
test('it classifies an import with type modifiers as other', () => {
2836
const importNodes = getImportNodes(`import {type a, b} from "a";`, {
2937
plugins: ['typescript'],
3038
});
3139
expect(importNodes.length).toBe(1);
32-
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeOther);
40+
expect(getChunkTypeOfNode(importNodes[0], SAFE_OPTION_EMPTY)).toBe(
41+
chunkTypeOther,
42+
);
3343
});
3444

3545
test('it classifies a side-effect import as unsortable', () => {
3646
const importNodes = getImportNodes(`import "a";`);
3747
expect(importNodes.length).toBe(1);
38-
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeUnsortable);
48+
expect(getChunkTypeOfNode(importNodes[0], SAFE_OPTION_EMPTY)).toBe(
49+
chunkTypeUnsortable,
50+
);
3951
});
4052

4153
test('it classifies a named import with an ignore next line comment as unsortable', () => {
4254
const importNodes = getImportNodes(`// prettier-ignore
4355
import {a} from "a";`);
4456
expect(importNodes.length).toBe(1);
45-
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeUnsortable);
57+
expect(getChunkTypeOfNode(importNodes[0], SAFE_OPTION_EMPTY)).toBe(
58+
chunkTypeUnsortable,
59+
);
4660
});
4761

4862
test('it classifies a side-effect import with a ignore next line comment as unsortable', () => {
4963
const importNodes = getImportNodes(`// prettier-ignore
5064
import "a";`);
5165
expect(importNodes.length).toBe(1);
52-
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeUnsortable);
66+
expect(getChunkTypeOfNode(importNodes[0], SAFE_OPTION_EMPTY)).toBe(
67+
chunkTypeUnsortable,
68+
);
5369
});
5470

5571
test('it classifies a type import with an ignore next line comment as unsortable', () => {
@@ -59,7 +75,9 @@ test('it classifies a type import with an ignore next line comment as unsortable
5975
{ plugins: ['typescript'] },
6076
);
6177
expect(importNodes.length).toBe(1);
62-
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeUnsortable);
78+
expect(getChunkTypeOfNode(importNodes[0], SAFE_OPTION_EMPTY)).toBe(
79+
chunkTypeUnsortable,
80+
);
6381
});
6482

6583
test('it classifies an import with a type modifier and an ignore next line comment as unsortable', () => {
@@ -69,14 +87,38 @@ test('it classifies an import with a type modifier and an ignore next line comme
6987
{ plugins: ['typescript'] },
7088
);
7189
expect(importNodes.length).toBe(1);
72-
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeUnsortable);
90+
expect(getChunkTypeOfNode(importNodes[0], SAFE_OPTION_EMPTY)).toBe(
91+
chunkTypeUnsortable,
92+
);
7393
});
7494

7595
test('it only applies the ignore next line comments to the next line', () => {
7696
const importNodes = getImportNodes(`// prettier-ignore
7797
import {b} from "b";
7898
import {a} from "a";`);
7999
expect(importNodes.length).toBe(2);
80-
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeUnsortable);
81-
expect(getChunkTypeOfNode(importNodes[1])).toBe(chunkTypeOther);
100+
expect(getChunkTypeOfNode(importNodes[0], SAFE_OPTION_EMPTY)).toBe(
101+
chunkTypeUnsortable,
102+
);
103+
expect(getChunkTypeOfNode(importNodes[1], SAFE_OPTION_EMPTY)).toBe(
104+
chunkTypeOther,
105+
);
106+
});
107+
108+
test('it treats side-effect imports as safe if found in importOrderSafeSideEffects', () => {
109+
const importOrderSafeSideEffects = ['^\./.*\.css?', '^a$'];
110+
const importNodes = getImportNodes(`
111+
import "a";
112+
import "./styles.css";
113+
import "ab";`);
114+
expect(importNodes.length).toBe(3);
115+
expect(getChunkTypeOfNode(importNodes[0], importOrderSafeSideEffects)).toBe(
116+
chunkTypeOther,
117+
);
118+
expect(getChunkTypeOfNode(importNodes[1], importOrderSafeSideEffects)).toBe(
119+
chunkTypeOther,
120+
);
121+
expect(getChunkTypeOfNode(importNodes[2], importOrderSafeSideEffects)).toBe(
122+
chunkTypeUnsortable,
123+
);
82124
});

src/utils/__tests__/get-code-from-ast.spec.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import a from 'a';
2323
importOrder: defaultImportOrder,
2424
importOrderCombineTypeAndValueImports: true,
2525
importOrderCaseSensitive: false,
26+
importOrderSafeSideEffects: [],
2627
hasAnyCustomGroupSeparatorsInImportOrder: false,
2728
provideGapAfterTopOfFileComments: false,
2829
});
@@ -58,6 +59,7 @@ import type {See} from 'c';
5859
importOrder: defaultImportOrder,
5960
importOrderCombineTypeAndValueImports: true,
6061
importOrderCaseSensitive: false,
62+
importOrderSafeSideEffects: [],
6163
hasAnyCustomGroupSeparatorsInImportOrder: false,
6264
provideGapAfterTopOfFileComments: false,
6365
});
@@ -90,6 +92,7 @@ import c from 'c' assert { type: 'json' };
9092
importOrder: defaultImportOrder,
9193
importOrderCombineTypeAndValueImports: true,
9294
importOrderCaseSensitive: false,
95+
importOrderSafeSideEffects: [],
9396
hasAnyCustomGroupSeparatorsInImportOrder: false,
9497
provideGapAfterTopOfFileComments: false,
9598
});

src/utils/__tests__/get-sorted-nodes.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ test('it returns all sorted nodes, preserving the order side effect nodes', () =
3434
testingOnly.normalizeImportOrderOption(DEFAULT_IMPORT_ORDER),
3535
importOrderCombineTypeAndValueImports: true,
3636
importOrderCaseSensitive: false,
37+
importOrderSafeSideEffects: [],
3738
hasAnyCustomGroupSeparatorsInImportOrder: false,
3839
provideGapAfterTopOfFileComments: false,
3940
}) as ImportDeclaration[];

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const defaultOptions = examineAndNormalizePluginOptions({
1515
importOrderTypeScriptVersion: '5.0.0',
1616
importOrderCaseSensitive: false,
1717
importOrderParserPlugins: [],
18+
importOrderSafeSideEffects: [],
1819
filepath: __filename,
1920
});
2021

src/utils/__tests__/normalize-plugin-options.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ describe('examineAndNormalizePluginOptions', () => {
9797
importOrderParserPlugins: [],
9898
importOrderCaseSensitive: false,
9999
importOrderTypeScriptVersion: '1.0.0',
100+
importOrderSafeSideEffects: [],
100101
filepath: __filename,
101102
}),
102103
).toEqual({
@@ -110,6 +111,7 @@ describe('examineAndNormalizePluginOptions', () => {
110111
importOrderCaseSensitive: false,
111112
plugins: [],
112113
provideGapAfterTopOfFileComments: false,
114+
importOrderSafeSideEffects: [],
113115
});
114116
});
115117
test('it should detect group separators anywhere (relevant for side-effects)', () => {
@@ -124,6 +126,7 @@ describe('examineAndNormalizePluginOptions', () => {
124126
importOrderParserPlugins: [],
125127
importOrderCaseSensitive: false,
126128
importOrderTypeScriptVersion: '1.0.0',
129+
importOrderSafeSideEffects: [],
127130
filepath: __filename,
128131
}),
129132
).toEqual({
@@ -136,6 +139,7 @@ describe('examineAndNormalizePluginOptions', () => {
136139
],
137140
importOrderCombineTypeAndValueImports: true,
138141
importOrderCaseSensitive: false,
142+
importOrderSafeSideEffects: [],
139143
plugins: [],
140144
provideGapAfterTopOfFileComments: false,
141145
});
@@ -147,6 +151,7 @@ describe('examineAndNormalizePluginOptions', () => {
147151
importOrderParserPlugins: [],
148152
importOrderCaseSensitive: false,
149153
importOrderTypeScriptVersion: '1.0.0',
154+
importOrderSafeSideEffects: [],
150155
filepath: __filename,
151156
}),
152157
).toEqual({
@@ -158,6 +163,7 @@ describe('examineAndNormalizePluginOptions', () => {
158163
],
159164
importOrderCombineTypeAndValueImports: true,
160165
importOrderCaseSensitive: false,
166+
importOrderSafeSideEffects: [],
161167
plugins: [],
162168
provideGapAfterTopOfFileComments: true,
163169
});
@@ -169,6 +175,7 @@ describe('examineAndNormalizePluginOptions', () => {
169175
importOrderParserPlugins: ['typescript'],
170176
importOrderTypeScriptVersion: '5.0.0',
171177
importOrderCaseSensitive: false,
178+
importOrderSafeSideEffects: [],
172179
filepath: __filename,
173180
}),
174181
).toEqual({
@@ -180,6 +187,7 @@ describe('examineAndNormalizePluginOptions', () => {
180187
],
181188
importOrderCombineTypeAndValueImports: true,
182189
importOrderCaseSensitive: false,
190+
importOrderSafeSideEffects: [],
183191
plugins: ['typescript'],
184192
provideGapAfterTopOfFileComments: false,
185193
});
@@ -192,6 +200,7 @@ describe('examineAndNormalizePluginOptions', () => {
192200
importOrderParserPlugins: ['typescript', 'jsx'],
193201
importOrderTypeScriptVersion: '5.0.0',
194202
importOrderCaseSensitive: false,
203+
importOrderSafeSideEffects: [],
195204
filepath: __filename,
196205
}),
197206
).toEqual({
@@ -203,6 +212,7 @@ describe('examineAndNormalizePluginOptions', () => {
203212
],
204213
importOrderCombineTypeAndValueImports: true,
205214
importOrderCaseSensitive: false,
215+
importOrderSafeSideEffects: [],
206216
plugins: ['typescript'],
207217
provideGapAfterTopOfFileComments: false,
208218
});
@@ -214,6 +224,7 @@ describe('examineAndNormalizePluginOptions', () => {
214224
importOrderParserPlugins: [],
215225
importOrderCaseSensitive: false,
216226
importOrderTypeScriptVersion: '1.0.0',
227+
importOrderSafeSideEffects: [],
217228
filepath: undefined,
218229
}),
219230
).toEqual({
@@ -225,6 +236,7 @@ describe('examineAndNormalizePluginOptions', () => {
225236
],
226237
importOrderCombineTypeAndValueImports: true,
227238
importOrderCaseSensitive: false,
239+
importOrderSafeSideEffects: [],
228240
plugins: [],
229241
provideGapAfterTopOfFileComments: false,
230242
});
@@ -237,13 +249,15 @@ describe('examineAndNormalizePluginOptions', () => {
237249
importOrderParserPlugins: [],
238250
importOrderCaseSensitive: false,
239251
importOrderTypeScriptVersion: '1.0.0',
252+
importOrderSafeSideEffects: [],
240253
filepath: __filename,
241254
}),
242255
).toEqual({
243256
hasAnyCustomGroupSeparatorsInImportOrder: false,
244257
importOrder: [],
245258
importOrderCombineTypeAndValueImports: true,
246259
importOrderCaseSensitive: false,
260+
importOrderSafeSideEffects: [],
247261
plugins: [],
248262
provideGapAfterTopOfFileComments: false,
249263
});

src/utils/__tests__/remove-nodes-from-original-code.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ test('it should remove nodes from the original code', async () => {
2929
testingOnly.normalizeImportOrderOption(DEFAULT_IMPORT_ORDER),
3030
importOrderCombineTypeAndValueImports: true,
3131
importOrderCaseSensitive: false,
32+
importOrderSafeSideEffects: [],
3233
hasAnyCustomGroupSeparatorsInImportOrder: false,
3334
provideGapAfterTopOfFileComments: false,
3435
});

0 commit comments

Comments
 (0)