Skip to content
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,12 @@ In the end, the plugin returns final imports with _third party imports_ on top a

The _third party imports_ position (it's top by default) can be overridden using the `<THIRD_PARTY_MODULES>` special word in the `importOrder`.

### Pattern Matching Implementation

This plugin uses [minimatch](https://github.com/isaacs/minimatch) for pattern matching of import paths. The matching is performed using the exact version specified in the plugin's dependencies to ensure consistent behavior. This is important to note because different versions of minimatch or other glob matching libraries might have subtle differences in their pattern matching behavior.

If you're experiencing unexpected matching behavior, please ensure you're using patterns compatible with minimatch's syntax, which might differ slightly from other glob implementations.

### FAQ / Troubleshooting

Having some trouble or an issue ? You can check [FAQ / Troubleshooting section](./docs/TROUBLESHOOTING.md).
Expand Down Expand Up @@ -326,10 +332,10 @@ debug some code in the plugin, check [Debugging Guidelines](./docs/DEBUG.md)

### Maintainers

| [Ayush Sharma](https://github.com/ayusharma) | [Behrang Yarahmadi](https://github.com/byara) | [Vladislav Arsenev](https://github.com/vladislavarsenev) |
| ------------------------------------------------------------------------ | --------------------------------------------------------------------- |--------------------------------------------------------------------------|
| ![ayusharma](https://avatars2.githubusercontent.com/u/6918450?s=120&v=4) | ![@byara](https://avatars2.githubusercontent.com/u/6979966?s=120&v=4) |![@vladislavarsenev](https://avatars.githubusercontent.com/u/51095682?s=120&v=4)|
| [@ayusharma](https://twitter.com/ayusharma_) | [@behrang_y](https://twitter.com/behrang_y) | |
| [Ayush Sharma](https://github.com/ayusharma) | [Behrang Yarahmadi](https://github.com/byara) |
| ------------------------------------------------------------------------ | --------------------------------------------------------------------- |
| ![ayusharma](https://avatars2.githubusercontent.com/u/6918450?s=120&v=4) | ![@byara](https://avatars2.githubusercontent.com/u/6979966?s=120&v=4) |
| [@ayusharma\_](https://twitter.com/ayusharma_) | [@behrang_y](https://twitter.com/behrang_y) |

### Disclaimer

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,16 @@
"@babel/types": "^7.28.0",
"javascript-natural-sort": "^0.7.1",
"lodash-es": "^4.17.21",
"parse-imports-exports": "^0.2.4"
"parse-imports-exports": "^0.2.4",
"minimatch": "^9.0.0"
},
"devDependencies": {
"@babel/core": "^7.26.7",
"@types/babel__core": "^7.20.5",
"@types/babel__generator": "^7.27.0",
"@types/babel__traverse": "^7.20.7",
"@types/lodash-es": "^4.17.12",
"@types/minimatch": "^5.1.2",
"@types/node": "^22.10.10",
"@vue/compiler-sfc": "^3.5.13",
"prettier": "^3.4.2",
Expand Down
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ const emberParsers = await createEmberParsers();
const svelteParsers = await createSvelteParsers();

const options: Options = {
importOrderExclude: {
type: 'path',
category: 'Global',
array: true,
default: [{ value: [] }],
description:
'Provide a list of glob patterns to exclude from import sorting.',
},
importOrder: {
type: 'path',
category: 'Global',
Expand Down
11 changes: 11 additions & 0 deletions src/preprocessors/preprocessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getCodeFromAst } from '../utils/get-code-from-ast.js';
import { getExperimentalParserPlugins } from '../utils/get-experimental-parser-plugins.js';
import { getSortedNodes } from '../utils/get-sorted-nodes.js';
import { isSortImportsIgnored } from '../utils/is-sort-imports-ignored.js';
import { shouldSkipFile } from '../utils/should-skip-file.js';

export function preprocessor(code: string, options: PrettierOptions) {
const {
Expand All @@ -20,8 +21,18 @@ export function preprocessor(code: string, options: PrettierOptions) {
importOrderSortByLength,
importOrderSideEffects,
importOrderImportAttributesKeyword,
importOrderExclude,
filepath,
} = options;

// Check if the file should be skipped
if (
filepath &&
shouldSkipFile(filepath, (importOrderExclude || []) as string[])
) {
return code;
}

const parserOptions: ParserOptions = {
sourceType: 'module',
plugins: getExperimentalParserPlugins(importOrderParserPlugins),
Expand Down
69 changes: 69 additions & 0 deletions src/utils/__tests__/should-skip-file.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { describe, expect, it } from 'vitest';

import { shouldSkipFile } from '../should-skip-file';

describe('shouldSkipFile', () => {
it('should return false when skipPatterns is empty', () => {
expect(shouldSkipFile('src/file.ts', [])).toBe(false);
});

it('should return false when no patterns match', () => {
const patterns = ['test/*.ts', 'lib/*.js'];
expect(shouldSkipFile('src/file.ts', patterns)).toBe(false);
});

it('should return true when file matches a pattern', () => {
const patterns = ['src/*.ts', 'lib/*.js'];
expect(shouldSkipFile('src/file.ts', patterns)).toBe(true);
});

it('should handle glob patterns correctly', () => {
const patterns = ['*.test.ts', 'generated/**'];
expect(shouldSkipFile('Button.test.ts', patterns)).toBe(true);
expect(shouldSkipFile('generated/types.ts', patterns)).toBe(true);
expect(shouldSkipFile('src/Button.ts', patterns)).toBe(false);
});

it('should match filename-only patterns against basename', () => {
const patterns = ['*.js', 'example.ts'];
expect(shouldSkipFile('/long/path/to/file.js', patterns)).toBe(true);
expect(shouldSkipFile('/different/path/example.ts', patterns)).toBe(
true,
);
expect(shouldSkipFile('/path/to/file.ts', patterns)).toBe(false);
});

it('should handle special characters in filenames', () => {
const patterns = ['*.spec.ts', '*test*.js'];
expect(shouldSkipFile('my-component.spec.ts', patterns)).toBe(true);
expect(shouldSkipFile('my.test.js', patterns)).toBe(true);
expect(shouldSkipFile('test.jsx', patterns)).toBe(false);
});

it('should handle multiple patterns with mixed path separators', () => {
const patterns = ['src/*.ts', 'test/*.js', '*.test.tsx'];
expect(shouldSkipFile('src/file.ts', patterns)).toBe(true);
expect(shouldSkipFile('test/file.js', patterns)).toBe(true);
expect(shouldSkipFile('component.test.tsx', patterns)).toBe(true);
expect(shouldSkipFile('src/sub/file.ts', patterns)).toBe(false);
});

it('should handle exact filename matches', () => {
const patterns = ['example.js', 'tsconfig.json'];
expect(shouldSkipFile('/any/path/example.js', patterns)).toBe(true);
expect(shouldSkipFile('/root/tsconfig.json', patterns)).toBe(true);
expect(shouldSkipFile('/path/to/example.test.js', patterns)).toBe(
false,
);
});

it('should handle directory patterns', () => {
const patterns = ['test/**/*.*', 'generated/**/*.*'];
expect(shouldSkipFile('test/file.ts', patterns)).toBe(true);
expect(shouldSkipFile('test/unit/component.js', patterns)).toBe(true);
expect(shouldSkipFile('generated/types.ts', patterns)).toBe(true);
expect(shouldSkipFile('src/components/button.ts', patterns)).toBe(
false,
);
});
});
33 changes: 33 additions & 0 deletions src/utils/should-skip-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { minimatch } from 'minimatch';
import path from 'path';

/**
* Checks if the current file path matches any of the patterns in importOrderExclude
* @param filePath The path of the current file being processed
* @param skipPatterns Array of patterns for files to skip
* @returns boolean indicating whether the file should be skipped
*/
export function shouldSkipFile(
filepath: string,
skipPatterns: string[],
): boolean {
if (skipPatterns.length === 0) {
return false;
}

const normalizedPath = filepath.split(path.sep).join('/');
const filename = path.basename(filepath);

return skipPatterns.some((pattern) => {
// Normalize pattern to use forward slashes
const normalizedPattern = pattern.split(path.sep).join('/');

// If pattern doesn't contain '/', match against filename only
if (!normalizedPattern.includes('/')) {
return minimatch(filename, normalizedPattern, { matchBase: true });
}

// Otherwise match against full path
return minimatch(normalizedPath, normalizedPattern);
});
}
12 changes: 12 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,18 @@ used to order imports within each match group.
* _Default behavior:_ When not specified, @babel/generator will try to match the style in the input code based on the AST shape.
*/
importOrderImportAttributesKeyword?: 'assert' | 'with' | 'with-legacy';

/**
* An array of glob patterns for files that should be skipped by the import sorting.
* Files matching these patterns will not have their imports sorted.
*
* ```
* "importOrderExclude": ["*.test.ts", "src/generated/**"]
* ```
*
* @default []
*/
importOrderExclude?: string[];
}

export type PrettierConfig = PluginConfig & Config;
24 changes: 24 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,11 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.20.tgz#1ca77361d7363432d29f5e55950d9ec1e1c6ea93"
integrity sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==

"@types/minimatch@^5.1.2":
version "5.1.2"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"
integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==

"@types/node@^22.10.10":
version "22.10.10"
resolved "https://registry.npmjs.org/@types/node/-/node-22.10.10.tgz"
Expand Down Expand Up @@ -802,6 +807,18 @@ axobject-query@^4.0.0:
resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz"
integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==

balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==

brace-expansion@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7"
integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==
dependencies:
balanced-match "^1.0.0"

browserslist@^4.24.0:
version "4.24.2"
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz"
Expand Down Expand Up @@ -1053,6 +1070,13 @@ mdn-data@2.0.30:
resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz"
integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==

minimatch@^9.0.0:
version "9.0.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
dependencies:
brace-expansion "^2.0.1"

ms@^2.1.3:
version "2.1.3"
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
Expand Down