diff --git a/README.md b/README.md index 78b71c4..6762e95 100644 --- a/README.md +++ b/README.md @@ -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 `` 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). @@ -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 diff --git a/package.json b/package.json index 71a3e06..06f1619 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "@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", @@ -49,6 +50,7 @@ "@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", diff --git a/src/index.ts b/src/index.ts index b481c16..b449bfd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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', diff --git a/src/preprocessors/preprocessor.ts b/src/preprocessors/preprocessor.ts index b0062b4..21a71e8 100644 --- a/src/preprocessors/preprocessor.ts +++ b/src/preprocessors/preprocessor.ts @@ -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 { @@ -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), diff --git a/src/utils/__tests__/should-skip-file.spec.ts b/src/utils/__tests__/should-skip-file.spec.ts new file mode 100644 index 0000000..21e28a1 --- /dev/null +++ b/src/utils/__tests__/should-skip-file.spec.ts @@ -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, + ); + }); +}); diff --git a/src/utils/should-skip-file.ts b/src/utils/should-skip-file.ts new file mode 100644 index 0000000..73c377d --- /dev/null +++ b/src/utils/should-skip-file.ts @@ -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); + }); +} diff --git a/types/index.d.ts b/types/index.d.ts index c279e35..8019b88 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -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; diff --git a/yarn.lock b/yarn.lock index a7d1105..330917e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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" @@ -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"