Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ This project is based on [@trivago/prettier-plugin-sort-imports](https://github.
- [5. Group aliases with local imports](#5-group-aliases-with-local-imports)
- [6. Enforce a blank line after top of file comments](#6-enforce-a-blank-line-after-top-of-file-comments)
- [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)
- [`importOrderSafeSideEffects`](#importordersafesideeffects)
- [`importOrderTypeScriptVersion`](#importordertypescriptversion)
- [`importOrderParserPlugins`](#importorderparserplugins)
- [`importOrderCaseSensitive`](#importordercasesensitive)
Expand Down Expand Up @@ -360,6 +361,18 @@ This can also be beneficial for large projects wishing to gradually adopt a sort

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.

#### `importOrderSafeSideEffects`

**type**: `Array<string>`

**default value:** `[]`

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.

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.

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.

#### `importOrderTypeScriptVersion`

**type**: `string`
Expand Down
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ export const options = {
description:
'Should capitalization be considered when sorting imports?',
},
importOrderSafeSideEffects: {
type: 'string',
category: 'Global',
array: true,
default: [{ value: [] }],
description:
'Array of globs for side-effect-only imports that are considered safe to sort.',
},
} satisfies Record<
keyof PluginConfig,
StringArraySupportOption | BooleanSupportOption | StringSupportOption
Expand Down
3 changes: 2 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ export type NormalizableOptions = Pick<
| 'importOrderParserPlugins'
| 'importOrderTypeScriptVersion'
| 'importOrderCaseSensitive'
| 'importOrderSafeSideEffects'
> &
// filepath can be undefined when running prettier via the api on text input
Pick<Partial<PrettierOptions>, 'filepath'>;

type ChunkType = typeof chunkTypeOther | typeof chunkTypeUnsortable;
export type ChunkType = typeof chunkTypeOther | typeof chunkTypeUnsortable;

export interface ImportChunk {
nodes: ImportDeclaration[];
Expand Down
1 change: 1 addition & 0 deletions src/utils/__tests__/get-all-comments-from-nodes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const getSortedImportNodes = (code: string, options?: ParserOptions) => {
importOrderCaseSensitive: false,
hasAnyCustomGroupSeparatorsInImportOrder: false,
provideGapAfterTopOfFileComments: false,
importOrderSafeSideEffects: [],
});
};

Expand Down
64 changes: 53 additions & 11 deletions src/utils/__tests__/get-chunk-type-of-node.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,68 @@ import { chunkTypeOther, chunkTypeUnsortable } from '../../constants';
import { getChunkTypeOfNode } from '../get-chunk-type-of-node';
import { getImportNodes } from '../get-import-nodes';

const SAFE_OPTION_EMPTY: string[] = [];

test('it classifies a default import as other', () => {
const importNodes = getImportNodes(`import a from "a";`);
expect(importNodes.length).toBe(1);
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeOther);
expect(getChunkTypeOfNode(importNodes[0], SAFE_OPTION_EMPTY)).toBe(
chunkTypeOther,
);
});

test('it classifies a named import as other', () => {
const importNodes = getImportNodes(`import {a} from "a";`);
expect(importNodes.length).toBe(1);
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeOther);
expect(getChunkTypeOfNode(importNodes[0], SAFE_OPTION_EMPTY)).toBe(
chunkTypeOther,
);
});

test('it classifies a type import as other', () => {
const importNodes = getImportNodes(`import type {a, b} from "a";`, {
plugins: ['typescript'],
});
expect(importNodes.length).toBe(1);
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeOther);
expect(getChunkTypeOfNode(importNodes[0], SAFE_OPTION_EMPTY)).toBe(
chunkTypeOther,
);
});

test('it classifies an import with type modifiers as other', () => {
const importNodes = getImportNodes(`import {type a, b} from "a";`, {
plugins: ['typescript'],
});
expect(importNodes.length).toBe(1);
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeOther);
expect(getChunkTypeOfNode(importNodes[0], SAFE_OPTION_EMPTY)).toBe(
chunkTypeOther,
);
});

test('it classifies a side-effect import as unsortable', () => {
const importNodes = getImportNodes(`import "a";`);
expect(importNodes.length).toBe(1);
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeUnsortable);
expect(getChunkTypeOfNode(importNodes[0], SAFE_OPTION_EMPTY)).toBe(
chunkTypeUnsortable,
);
});

test('it classifies a named import with an ignore next line comment as unsortable', () => {
const importNodes = getImportNodes(`// prettier-ignore
import {a} from "a";`);
expect(importNodes.length).toBe(1);
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeUnsortable);
expect(getChunkTypeOfNode(importNodes[0], SAFE_OPTION_EMPTY)).toBe(
chunkTypeUnsortable,
);
});

test('it classifies a side-effect import with a ignore next line comment as unsortable', () => {
const importNodes = getImportNodes(`// prettier-ignore
import "a";`);
expect(importNodes.length).toBe(1);
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeUnsortable);
expect(getChunkTypeOfNode(importNodes[0], SAFE_OPTION_EMPTY)).toBe(
chunkTypeUnsortable,
);
});

test('it classifies a type import with an ignore next line comment as unsortable', () => {
Expand All @@ -59,7 +75,9 @@ test('it classifies a type import with an ignore next line comment as unsortable
{ plugins: ['typescript'] },
);
expect(importNodes.length).toBe(1);
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeUnsortable);
expect(getChunkTypeOfNode(importNodes[0], SAFE_OPTION_EMPTY)).toBe(
chunkTypeUnsortable,
);
});

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

test('it only applies the ignore next line comments to the next line', () => {
const importNodes = getImportNodes(`// prettier-ignore
import {b} from "b";
import {a} from "a";`);
expect(importNodes.length).toBe(2);
expect(getChunkTypeOfNode(importNodes[0])).toBe(chunkTypeUnsortable);
expect(getChunkTypeOfNode(importNodes[1])).toBe(chunkTypeOther);
expect(getChunkTypeOfNode(importNodes[0], SAFE_OPTION_EMPTY)).toBe(
chunkTypeUnsortable,
);
expect(getChunkTypeOfNode(importNodes[1], SAFE_OPTION_EMPTY)).toBe(
chunkTypeOther,
);
});

test('it treats side-effect imports as safe if found in importOrderSafeSideEffects', () => {
const importOrderSafeSideEffects = ['^\./.*\.css?', '^a$'];
const importNodes = getImportNodes(`
import "a";
import "./styles.css";
import "ab";`);
expect(importNodes.length).toBe(3);
expect(getChunkTypeOfNode(importNodes[0], importOrderSafeSideEffects)).toBe(
chunkTypeOther,
);
expect(getChunkTypeOfNode(importNodes[1], importOrderSafeSideEffects)).toBe(
chunkTypeOther,
);
expect(getChunkTypeOfNode(importNodes[2], importOrderSafeSideEffects)).toBe(
chunkTypeUnsortable,
);
});
3 changes: 3 additions & 0 deletions src/utils/__tests__/get-code-from-ast.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import a from 'a';
importOrder: defaultImportOrder,
importOrderCombineTypeAndValueImports: true,
importOrderCaseSensitive: false,
importOrderSafeSideEffects: [],
hasAnyCustomGroupSeparatorsInImportOrder: false,
provideGapAfterTopOfFileComments: false,
});
Expand Down Expand Up @@ -58,6 +59,7 @@ import type {See} from 'c';
importOrder: defaultImportOrder,
importOrderCombineTypeAndValueImports: true,
importOrderCaseSensitive: false,
importOrderSafeSideEffects: [],
hasAnyCustomGroupSeparatorsInImportOrder: false,
provideGapAfterTopOfFileComments: false,
});
Expand Down Expand Up @@ -90,6 +92,7 @@ import c from 'c' assert { type: 'json' };
importOrder: defaultImportOrder,
importOrderCombineTypeAndValueImports: true,
importOrderCaseSensitive: false,
importOrderSafeSideEffects: [],
hasAnyCustomGroupSeparatorsInImportOrder: false,
provideGapAfterTopOfFileComments: false,
});
Expand Down
1 change: 1 addition & 0 deletions src/utils/__tests__/get-sorted-nodes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ test('it returns all sorted nodes, preserving the order side effect nodes', () =
testingOnly.normalizeImportOrderOption(DEFAULT_IMPORT_ORDER),
importOrderCombineTypeAndValueImports: true,
importOrderCaseSensitive: false,
importOrderSafeSideEffects: [],
hasAnyCustomGroupSeparatorsInImportOrder: false,
provideGapAfterTopOfFileComments: false,
}) as ImportDeclaration[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const defaultOptions = examineAndNormalizePluginOptions({
importOrderTypeScriptVersion: '5.0.0',
importOrderCaseSensitive: false,
importOrderParserPlugins: [],
importOrderSafeSideEffects: [],
filepath: __filename,
});

Expand Down
14 changes: 14 additions & 0 deletions src/utils/__tests__/normalize-plugin-options.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ describe('examineAndNormalizePluginOptions', () => {
importOrderParserPlugins: [],
importOrderCaseSensitive: false,
importOrderTypeScriptVersion: '1.0.0',
importOrderSafeSideEffects: [],
filepath: __filename,
}),
).toEqual({
Expand All @@ -110,6 +111,7 @@ describe('examineAndNormalizePluginOptions', () => {
importOrderCaseSensitive: false,
plugins: [],
provideGapAfterTopOfFileComments: false,
importOrderSafeSideEffects: [],
});
});
test('it should detect group separators anywhere (relevant for side-effects)', () => {
Expand All @@ -124,6 +126,7 @@ describe('examineAndNormalizePluginOptions', () => {
importOrderParserPlugins: [],
importOrderCaseSensitive: false,
importOrderTypeScriptVersion: '1.0.0',
importOrderSafeSideEffects: [],
filepath: __filename,
}),
).toEqual({
Expand All @@ -136,6 +139,7 @@ describe('examineAndNormalizePluginOptions', () => {
],
importOrderCombineTypeAndValueImports: true,
importOrderCaseSensitive: false,
importOrderSafeSideEffects: [],
plugins: [],
provideGapAfterTopOfFileComments: false,
});
Expand All @@ -147,6 +151,7 @@ describe('examineAndNormalizePluginOptions', () => {
importOrderParserPlugins: [],
importOrderCaseSensitive: false,
importOrderTypeScriptVersion: '1.0.0',
importOrderSafeSideEffects: [],
filepath: __filename,
}),
).toEqual({
Expand All @@ -158,6 +163,7 @@ describe('examineAndNormalizePluginOptions', () => {
],
importOrderCombineTypeAndValueImports: true,
importOrderCaseSensitive: false,
importOrderSafeSideEffects: [],
plugins: [],
provideGapAfterTopOfFileComments: true,
});
Expand All @@ -169,6 +175,7 @@ describe('examineAndNormalizePluginOptions', () => {
importOrderParserPlugins: ['typescript'],
importOrderTypeScriptVersion: '5.0.0',
importOrderCaseSensitive: false,
importOrderSafeSideEffects: [],
filepath: __filename,
}),
).toEqual({
Expand All @@ -180,6 +187,7 @@ describe('examineAndNormalizePluginOptions', () => {
],
importOrderCombineTypeAndValueImports: true,
importOrderCaseSensitive: false,
importOrderSafeSideEffects: [],
plugins: ['typescript'],
provideGapAfterTopOfFileComments: false,
});
Expand All @@ -192,6 +200,7 @@ describe('examineAndNormalizePluginOptions', () => {
importOrderParserPlugins: ['typescript', 'jsx'],
importOrderTypeScriptVersion: '5.0.0',
importOrderCaseSensitive: false,
importOrderSafeSideEffects: [],
filepath: __filename,
}),
).toEqual({
Expand All @@ -203,6 +212,7 @@ describe('examineAndNormalizePluginOptions', () => {
],
importOrderCombineTypeAndValueImports: true,
importOrderCaseSensitive: false,
importOrderSafeSideEffects: [],
plugins: ['typescript'],
provideGapAfterTopOfFileComments: false,
});
Expand All @@ -214,6 +224,7 @@ describe('examineAndNormalizePluginOptions', () => {
importOrderParserPlugins: [],
importOrderCaseSensitive: false,
importOrderTypeScriptVersion: '1.0.0',
importOrderSafeSideEffects: [],
filepath: undefined,
}),
).toEqual({
Expand All @@ -225,6 +236,7 @@ describe('examineAndNormalizePluginOptions', () => {
],
importOrderCombineTypeAndValueImports: true,
importOrderCaseSensitive: false,
importOrderSafeSideEffects: [],
plugins: [],
provideGapAfterTopOfFileComments: false,
});
Expand All @@ -237,13 +249,15 @@ describe('examineAndNormalizePluginOptions', () => {
importOrderParserPlugins: [],
importOrderCaseSensitive: false,
importOrderTypeScriptVersion: '1.0.0',
importOrderSafeSideEffects: [],
filepath: __filename,
}),
).toEqual({
hasAnyCustomGroupSeparatorsInImportOrder: false,
importOrder: [],
importOrderCombineTypeAndValueImports: true,
importOrderCaseSensitive: false,
importOrderSafeSideEffects: [],
plugins: [],
provideGapAfterTopOfFileComments: false,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ test('it should remove nodes from the original code', async () => {
testingOnly.normalizeImportOrderOption(DEFAULT_IMPORT_ORDER),
importOrderCombineTypeAndValueImports: true,
importOrderCaseSensitive: false,
importOrderSafeSideEffects: [],
hasAnyCustomGroupSeparatorsInImportOrder: false,
provideGapAfterTopOfFileComments: false,
});
Expand Down
Loading
Loading