From 73748568af6be0444d65e5617980901f33351e50 Mon Sep 17 00:00:00 2001 From: leaysgur <6259812+leaysgur@users.noreply.github.com> Date: Tue, 2 Dec 2025 07:37:06 +0000 Subject: [PATCH] feat(formatter/sort-imports): Support `options.internalPattern` (#16372) Part of #14253 The original implementation accepts a `RegExp` string, but based on the actual usage, it seems that checking as prefix is sufficient for now. https://github.com/search?q=%22internalPattern%3A+%5B%22&type=code --- crates/oxc_formatter/examples/sort_imports.rs | 1 + .../sort_imports/compute_metadata.rs | 12 +++++--- crates/oxc_formatter/src/options.rs | 4 +++ crates/oxc_formatter/src/service/oxfmtrc.rs | 3 ++ .../tests/ir_transform/sort_imports.rs | 29 +++++++++++++++++++ .../tests/snapshots/schema_json.snap | 9 ++++++ napi/playground/index.d.ts | 2 ++ napi/playground/src/lib.rs | 1 + napi/playground/src/options.rs | 2 ++ npm/oxfmt/configuration_schema.json | 9 ++++++ 10 files changed, 68 insertions(+), 4 deletions(-) diff --git a/crates/oxc_formatter/examples/sort_imports.rs b/crates/oxc_formatter/examples/sort_imports.rs index 61b0aca6230e5..9c6c98dfaf4f0 100644 --- a/crates/oxc_formatter/examples/sort_imports.rs +++ b/crates/oxc_formatter/examples/sort_imports.rs @@ -28,6 +28,7 @@ fn main() -> Result<(), String> { sort_side_effects, ignore_case, newlines_between, + internal_pattern: None, groups: None, }; diff --git a/crates/oxc_formatter/src/ir_transform/sort_imports/compute_metadata.rs b/crates/oxc_formatter/src/ir_transform/sort_imports/compute_metadata.rs index 423fd3cd72873..7ee92272024f9 100644 --- a/crates/oxc_formatter/src/ir_transform/sort_imports/compute_metadata.rs +++ b/crates/oxc_formatter/src/ir_transform/sort_imports/compute_metadata.rs @@ -30,13 +30,14 @@ pub fn compute_import_metadata<'a>( let source = extract_source_path(source); let is_style_import = is_style(source); + let path_kind = to_path_kind(source, options); // Create group matcher from import characteristics let matcher = ImportGroupMatcher { is_side_effect: *is_side_effect, is_type_import: *is_type_import, is_style_import, - path_kind: to_path_kind(source), + path_kind, is_subpath: is_subpath(source), has_default_specifier: *has_default_specifier, has_namespace_specifier: *has_namespace_specifier, @@ -346,7 +347,7 @@ enum ImportPathKind { } /// Determine the path kind for an import source. -fn to_path_kind(source: &str) -> ImportPathKind { +fn to_path_kind(source: &str, options: &options::SortImports) -> ImportPathKind { if is_builtin(source) { return ImportPathKind::Builtin; } @@ -364,8 +365,11 @@ fn to_path_kind(source: &str) -> ImportPathKind { return ImportPathKind::Sibling; } - // TODO: This can be changed via `options.internalPattern` - if source.starts_with("~/") || source.starts_with("@/") { + // Check if source matches any internal pattern + if match &options.internal_pattern { + Some(patterns) => patterns.iter().any(|p| source.starts_with(p.as_str())), + None => ["~/", "@/"].iter().any(|p| source.starts_with(*p)), + } { return ImportPathKind::Internal; } diff --git a/crates/oxc_formatter/src/options.rs b/crates/oxc_formatter/src/options.rs index 336e01acd37c4..408c4391e1014 100644 --- a/crates/oxc_formatter/src/options.rs +++ b/crates/oxc_formatter/src/options.rs @@ -1004,6 +1004,9 @@ pub struct SortImports { /// /// NOTE: Cannot be used together with `partition_by_newline: true`. pub newlines_between: bool, + /// Prefixes for internal imports. + /// If `None`, uses the default internal patterns. + pub internal_pattern: Option>, /// Groups configuration for organizing imports. /// Each inner `Vec` represents a group, and multiple group names in the same `Vec` are treated as one. /// If `None`, uses the default groups. @@ -1019,6 +1022,7 @@ impl Default for SortImports { order: SortOrder::default(), ignore_case: true, newlines_between: true, + internal_pattern: None, groups: None, } } diff --git a/crates/oxc_formatter/src/service/oxfmtrc.rs b/crates/oxc_formatter/src/service/oxfmtrc.rs index da0cc0e0caf2d..0550cfff238bc 100644 --- a/crates/oxc_formatter/src/service/oxfmtrc.rs +++ b/crates/oxc_formatter/src/service/oxfmtrc.rs @@ -150,6 +150,8 @@ pub struct SortImportsConfig { pub ignore_case: bool, #[serde(default = "default_true")] pub newlines_between: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub internal_pattern: Option>, /// Custom groups configuration for organizing imports. /// Each array element represents a group, and multiple group names in the same array are treated as one. /// Accepts both `string` and `string[]` as group elements. @@ -378,6 +380,7 @@ impl Oxfmtrc { }), ignore_case: sort_imports_config.ignore_case, newlines_between: sort_imports_config.newlines_between, + internal_pattern: sort_imports_config.internal_pattern, groups: sort_imports_config.groups, }); } diff --git a/crates/oxc_formatter/tests/ir_transform/sort_imports.rs b/crates/oxc_formatter/tests/ir_transform/sort_imports.rs index 06b66079a47f5..d19cf2b0a3998 100644 --- a/crates/oxc_formatter/tests/ir_transform/sort_imports.rs +++ b/crates/oxc_formatter/tests/ir_transform/sort_imports.rs @@ -876,6 +876,35 @@ import { z } from "z"; // --- +#[test] +fn should_support_internal_pattern_option() { + assert_format( + r##" +import type { T } from "a"; +import { a } from "a"; +import type { S } from "#b"; +import c from "#c"; +import { b1, b2 } from "#b"; +import { d } from "../d"; +"##, + r##"{ "experimentalSortImports": { "internalPattern": ["#"] } }"##, + r##" +import type { T } from "a"; + +import { a } from "a"; + +import type { S } from "#b"; + +import { b1, b2 } from "#b"; +import c from "#c"; + +import { d } from "../d"; +"##, + ); +} + +// --- + #[test] fn should_groups_and_sorts_by_type_and_source() { assert_format( diff --git a/crates/oxc_formatter/tests/snapshots/schema_json.snap b/crates/oxc_formatter/tests/snapshots/schema_json.snap index 9084f0a4db1bd..08a1e582179d4 100644 --- a/crates/oxc_formatter/tests/snapshots/schema_json.snap +++ b/crates/oxc_formatter/tests/snapshots/schema_json.snap @@ -223,6 +223,15 @@ expression: json "default": true, "type": "boolean" }, + "internalPattern": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, "newlinesBetween": { "default": true, "type": "boolean" diff --git a/napi/playground/index.d.ts b/napi/playground/index.d.ts index 3fd1b6b2e2b88..bea5fab2351dd 100644 --- a/napi/playground/index.d.ts +++ b/napi/playground/index.d.ts @@ -168,6 +168,8 @@ export interface OxcSortImportsOptions { ignoreCase?: boolean /** Add newlines between import groups (default: true) */ newlinesBetween?: boolean + /** Pattern prefixes for internal imports */ + internalPattern?: Array /** Custom groups of imports */ groups?: Array> } diff --git a/napi/playground/src/lib.rs b/napi/playground/src/lib.rs index 56b028355f085..a01c1ef179587 100644 --- a/napi/playground/src/lib.rs +++ b/napi/playground/src/lib.rs @@ -510,6 +510,7 @@ impl Oxc { order, ignore_case: sort_imports_config.ignore_case.unwrap_or(true), newlines_between: sort_imports_config.newlines_between.unwrap_or(true), + internal_pattern: sort_imports_config.internal_pattern.clone(), groups: sort_imports_config.groups.clone(), }); } diff --git a/napi/playground/src/options.rs b/napi/playground/src/options.rs index 0f76c29b75166..bc2b8cdea692e 100644 --- a/napi/playground/src/options.rs +++ b/napi/playground/src/options.rs @@ -163,6 +163,8 @@ pub struct OxcSortImportsOptions { pub ignore_case: Option, /// Add newlines between import groups (default: true) pub newlines_between: Option, + /// Pattern prefixes for internal imports + pub internal_pattern: Option>, /// Custom groups of imports pub groups: Option>>, } diff --git a/npm/oxfmt/configuration_schema.json b/npm/oxfmt/configuration_schema.json index 5fb281475c337..ca5f5668d90dd 100644 --- a/npm/oxfmt/configuration_schema.json +++ b/npm/oxfmt/configuration_schema.json @@ -219,6 +219,15 @@ "default": true, "type": "boolean" }, + "internalPattern": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, "newlinesBetween": { "default": true, "type": "boolean"