From 856a01ff4019d9bce34fefd0ec9872fcba69cc28 Mon Sep 17 00:00:00 2001 From: leaysgur <6259812+leaysgur@users.noreply.github.com> Date: Tue, 10 Feb 2026 02:14:09 +0000 Subject: [PATCH] feat(formatter/sort_imports)!: Replace prefix match with glob pattern in `customGroups.elementNamePattern` (#19066) Fixes #18726 Support glob matching instead of prefix matching. The original used regex, but there was no necessity for it, and this approach offers better performance. In any case, it will be a braking change. ```js // before: Matches react, reactify, react-router, etc "elementNamePattern": ["react"] // after: Matches react only // before: Matches loot-core, loot-core/foo, etc "elementNamePattern": ["loot-core"] // after: Matches only loot-core, should update to "elementNamePattern": ["loot-core/*"] ``` --- Cargo.lock | 1 + apps/oxfmt/src/core/oxfmtrc.rs | 2 +- apps/oxfmt/test/api/sort_imports.test.ts | 4 +- .../fixtures/custom_groups/.oxfmtrc.json | 4 +- crates/oxc_formatter/Cargo.toml | 1 + .../sort_imports/group_matcher.rs | 2 +- .../src/ir_transform/sort_imports/options.rs | 2 + .../sort_imports/custom_groups.rs | 139 +++++++++++++++--- npm/oxfmt/configuration_schema.json | 4 +- .../src/snapshots/schema_json.snap | 4 +- .../src/snapshots/schema_markdown.snap | 4 +- 11 files changed, 133 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6d2027a86b918..0084aacf0ea9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1931,6 +1931,7 @@ name = "oxc_formatter" version = "0.28.0" dependencies = [ "cow-utils", + "fast-glob", "insta", "natord", "nodejs-built-in-modules", diff --git a/apps/oxfmt/src/core/oxfmtrc.rs b/apps/oxfmt/src/core/oxfmtrc.rs index 833a99d57f85a..c2844a10d7d5d 100644 --- a/apps/oxfmt/src/core/oxfmtrc.rs +++ b/apps/oxfmt/src/core/oxfmtrc.rs @@ -688,7 +688,7 @@ impl SortGroupItemConfig { pub struct CustomGroupItemConfig { /// Name of the custom group, used in the `groups` option. pub group_name: String, - /// List of import name prefixes to match for this group. + /// List of glob patterns to match import sources for this group. pub element_name_pattern: Vec, } diff --git a/apps/oxfmt/test/api/sort_imports.test.ts b/apps/oxfmt/test/api/sort_imports.test.ts index 91ac29a2c3612..a43ed2c975597 100644 --- a/apps/oxfmt/test/api/sort_imports.test.ts +++ b/apps/oxfmt/test/api/sort_imports.test.ts @@ -11,8 +11,8 @@ import { store } from "~/stores/store"; experimentalSortImports: { newlinesBetween: false, customGroups: [ - { elementNamePattern: ["~/stores/"], groupName: "stores" }, - { elementNamePattern: ["~/utils/"], groupName: "utils" }, + { elementNamePattern: ["~/stores/*"], groupName: "stores" }, + { elementNamePattern: ["~/utils/*"], groupName: "utils" }, ], groups: ["stores", "utils", "sibling"], }, diff --git a/apps/oxfmt/test/cli/sort_imports/fixtures/custom_groups/.oxfmtrc.json b/apps/oxfmt/test/cli/sort_imports/fixtures/custom_groups/.oxfmtrc.json index 5b34e60d4538b..6d50cda806689 100644 --- a/apps/oxfmt/test/cli/sort_imports/fixtures/custom_groups/.oxfmtrc.json +++ b/apps/oxfmt/test/cli/sort_imports/fixtures/custom_groups/.oxfmtrc.json @@ -1,8 +1,8 @@ { "experimentalSortImports": { "customGroups": [ - { "elementNamePattern": ["~/stores/"], "groupName": "stores" }, - { "elementNamePattern": ["~/utils/"], "groupName": "utils" } + { "elementNamePattern": ["~/stores/*"], "groupName": "stores" }, + { "elementNamePattern": ["~/utils/*"], "groupName": "utils" } ], "groups": ["stores", "utils", "sibling"] } diff --git a/crates/oxc_formatter/Cargo.toml b/crates/oxc_formatter/Cargo.toml index 283a99d56146e..9d6353ce0012a 100644 --- a/crates/oxc_formatter/Cargo.toml +++ b/crates/oxc_formatter/Cargo.toml @@ -29,6 +29,7 @@ oxc_span = { workspace = true } oxc_syntax = { workspace = true } cow-utils = { workspace = true } +fast-glob = { workspace = true } natord = { workspace = true } nodejs-built-in-modules = { workspace = true } phf = { workspace = true, features = ["macros"] } diff --git a/crates/oxc_formatter/src/ir_transform/sort_imports/group_matcher.rs b/crates/oxc_formatter/src/ir_transform/sort_imports/group_matcher.rs index 1670de4bf6204..d00377258372c 100644 --- a/crates/oxc_formatter/src/ir_transform/sort_imports/group_matcher.rs +++ b/crates/oxc_formatter/src/ir_transform/sort_imports/group_matcher.rs @@ -65,7 +65,7 @@ impl GroupMatcher { let is_match = custom_group .element_name_pattern .iter() - .any(|pattern| import_metadata.source.starts_with(pattern)); + .any(|pattern| fast_glob::glob_match(pattern, import_metadata.source)); if is_match { return *index; } diff --git a/crates/oxc_formatter/src/ir_transform/sort_imports/options.rs b/crates/oxc_formatter/src/ir_transform/sort_imports/options.rs index 9febd6973c007..c6f5ce2b85369 100644 --- a/crates/oxc_formatter/src/ir_transform/sort_imports/options.rs +++ b/crates/oxc_formatter/src/ir_transform/sort_imports/options.rs @@ -97,7 +97,9 @@ impl fmt::Display for SortOrder { #[derive(Debug, Default, Clone, Eq, PartialEq)] pub struct CustomGroupDefinition { + /// The identifier used in `groups` representing this group. pub group_name: String, + /// List of glob patterns to match import sources for this group. pub element_name_pattern: Vec, } diff --git a/crates/oxc_formatter/tests/ir_transform/sort_imports/custom_groups.rs b/crates/oxc_formatter/tests/ir_transform/sort_imports/custom_groups.rs index 5413c67b42fc5..393ba2448bdc3 100644 --- a/crates/oxc_formatter/tests/ir_transform/sort_imports/custom_groups.rs +++ b/crates/oxc_formatter/tests/ir_transform/sort_imports/custom_groups.rs @@ -81,47 +81,47 @@ import CartComponentB from "./cart/CartComponentB.vue"; "experimentalSortImports": { "customGroups": [ { - "elementNamePattern": ["~/validators/"], + "elementNamePattern": ["~/validators/*"], "groupName": "validators" }, { - "elementNamePattern": ["~/composable/"], + "elementNamePattern": ["~/composable/*"], "groupName": "composable" }, { - "elementNamePattern": ["~/components/"], + "elementNamePattern": ["~/components/*"], "groupName": "components" }, { - "elementNamePattern": ["~/services/"], + "elementNamePattern": ["~/services/*"], "groupName": "services" }, { - "elementNamePattern": ["~/widgets/"], + "elementNamePattern": ["~/widgets/*"], "groupName": "widgets" }, { - "elementNamePattern": ["~/stores/"], + "elementNamePattern": ["~/stores/*"], "groupName": "stores" }, { - "elementNamePattern": ["~/logics/"], + "elementNamePattern": ["~/logics/*"], "groupName": "logics" }, { - "elementNamePattern": ["~/assets/"], + "elementNamePattern": ["~/assets/*"], "groupName": "assets" }, { - "elementNamePattern": ["~/utils/"], + "elementNamePattern": ["~/utils/*"], "groupName": "utils" }, { - "elementNamePattern": ["~/pages/"], + "elementNamePattern": ["~/pages/*"], "groupName": "pages" }, { - "elementNamePattern": ["~/ui/"], + "elementNamePattern": ["~/ui/*"], "groupName": "ui" } ], @@ -196,47 +196,47 @@ import ComponentC from "~/components/ComponentC.vue"; "experimentalSortImports": { "customGroups": [ { - "elementNamePattern": ["~/validators/"], + "elementNamePattern": ["~/validators/*"], "groupName": "validators" }, { - "elementNamePattern": ["~/composable/"], + "elementNamePattern": ["~/composable/*"], "groupName": "composable" }, { - "elementNamePattern": ["~/components/"], + "elementNamePattern": ["~/components/*"], "groupName": "components" }, { - "elementNamePattern": ["~/services/"], + "elementNamePattern": ["~/services/*"], "groupName": "services" }, { - "elementNamePattern": ["~/widgets/"], + "elementNamePattern": ["~/widgets/*"], "groupName": "widgets" }, { - "elementNamePattern": ["~/stores/"], + "elementNamePattern": ["~/stores/*"], "groupName": "stores" }, { - "elementNamePattern": ["~/logics/"], + "elementNamePattern": ["~/logics/*"], "groupName": "logics" }, { - "elementNamePattern": ["~/assets/"], + "elementNamePattern": ["~/assets/*"], "groupName": "assets" }, { - "elementNamePattern": ["~/utils/"], + "elementNamePattern": ["~/utils/*"], "groupName": "utils" }, { - "elementNamePattern": ["~/pages/"], + "elementNamePattern": ["~/pages/*"], "groupName": "pages" }, { - "elementNamePattern": ["~/ui/"], + "elementNamePattern": ["~/ui/*"], "groupName": "ui" } ], @@ -286,3 +286,98 @@ import CartComponentB from "./cart/CartComponentB.vue"; "#, ); } + +#[test] +fn glob_pattern_suffix_matching() { + assert_format( + r#" +import { setup } from "./setup.mock.ts"; +import { a } from "./a.ts"; +"#, + r#" +{ + "experimentalSortImports": { + "customGroups": [ + { + "groupName": "mocks", + "elementNamePattern": ["**/*.mock.ts"] + } + ], + "groups": [ + "mocks", + "unknown" + ] + } +} +"#, + r#" +import { setup } from "./setup.mock.ts"; + +import { a } from "./a.ts"; +"#, + ); +} + +#[test] +fn glob_pattern_brace_expansion() { + assert_format( + r#" +import { createApp } from "vue"; +import React from "react"; +import Vuetify from "vuetify"; +"#, + r#" +{ + "experimentalSortImports": { + "customGroups": [ + { + "groupName": "frameworks", + "elementNamePattern": ["{react,vue}"] + } + ], + "groups": [ + "frameworks", + "unknown" + ] + } +} +"#, + r#" +import React from "react"; +import { createApp } from "vue"; + +import Vuetify from "vuetify"; +"#, + ); +} + +#[test] +fn glob_pattern_exact_match() { + assert_format( + r#" +import { createApp } from "vue"; +import Vuetify from "vuetify"; +"#, + r#" +{ + "experimentalSortImports": { + "customGroups": [ + { + "groupName": "vue-core", + "elementNamePattern": ["vue"] + } + ], + "groups": [ + "vue-core", + "unknown" + ] + } +} +"#, + r#" +import { createApp } from "vue"; + +import Vuetify from "vuetify"; +"#, + ); +} diff --git a/npm/oxfmt/configuration_schema.json b/npm/oxfmt/configuration_schema.json index 8d3460cbe8a7a..1895b4605ff9c 100644 --- a/npm/oxfmt/configuration_schema.json +++ b/npm/oxfmt/configuration_schema.json @@ -265,13 +265,13 @@ "type": "object", "properties": { "elementNamePattern": { - "description": "List of import name prefixes to match for this group.", + "description": "List of glob patterns to match import sources for this group.", "default": [], "type": "array", "items": { "type": "string" }, - "markdownDescription": "List of import name prefixes to match for this group." + "markdownDescription": "List of glob patterns to match import sources for this group." }, "groupName": { "description": "Name of the custom group, used in the `groups` option.", diff --git a/tasks/website_formatter/src/snapshots/schema_json.snap b/tasks/website_formatter/src/snapshots/schema_json.snap index 0a282890d65eb..d1e697d38cfab 100644 --- a/tasks/website_formatter/src/snapshots/schema_json.snap +++ b/tasks/website_formatter/src/snapshots/schema_json.snap @@ -269,13 +269,13 @@ expression: json "type": "object", "properties": { "elementNamePattern": { - "description": "List of import name prefixes to match for this group.", + "description": "List of glob patterns to match import sources for this group.", "default": [], "type": "array", "items": { "type": "string" }, - "markdownDescription": "List of import name prefixes to match for this group." + "markdownDescription": "List of glob patterns to match import sources for this group." }, "groupName": { "description": "Name of the custom group, used in the `groups` option.", diff --git a/tasks/website_formatter/src/snapshots/schema_markdown.snap b/tasks/website_formatter/src/snapshots/schema_markdown.snap index 5e0f6729d175f..93d81efcc3737 100644 --- a/tasks/website_formatter/src/snapshots/schema_markdown.snap +++ b/tasks/website_formatter/src/snapshots/schema_markdown.snap @@ -111,7 +111,7 @@ type: `string[]` default: `[]` -List of import name prefixes to match for this group. +List of glob patterns to match import sources for this group. ##### experimentalSortImports.customGroups[n].groupName @@ -599,7 +599,7 @@ type: `string[]` default: `[]` -List of import name prefixes to match for this group. +List of glob patterns to match import sources for this group. ######## overrides[n].options.experimentalSortImports.customGroups[n].groupName