diff --git a/Cargo.lock b/Cargo.lock index 50126c7269c44..bf74ad9294bef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -691,7 +691,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2541,6 +2541,7 @@ dependencies = [ "phf", "rayon", "simdutf8", + "sort-package-json", "tokio", "tracing-subscriber", ] @@ -2958,7 +2959,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3053,7 +3054,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3257,6 +3258,16 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +[[package]] +name = "sort-package-json" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e4f9c20eae7d7e2b63538999157c55fe7c0d8baf79ed7e9462174b0faa58fd" +dependencies = [ + "ignore", + "serde_json", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -3344,7 +3355,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3899,7 +3910,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/apps/oxfmt/Cargo.toml b/apps/oxfmt/Cargo.toml index 62c987529d865..01dca4d74ea26 100644 --- a/apps/oxfmt/Cargo.toml +++ b/apps/oxfmt/Cargo.toml @@ -33,6 +33,7 @@ oxc_formatter = { workspace = true } oxc_language_server = { workspace = true, default-features = false, features = ["formatter"] } oxc_parser = { workspace = true } oxc_span = { workspace = true } +sort-package-json = "0.0.1" bpaf = { workspace = true, features = ["autocomplete", "bright-color", "derive"] } cow-utils = { workspace = true } diff --git a/apps/oxfmt/src/core/format.rs b/apps/oxfmt/src/core/format.rs index aceab04a20554..4a513d2b0cec8 100644 --- a/apps/oxfmt/src/core/format.rs +++ b/apps/oxfmt/src/core/format.rs @@ -7,6 +7,8 @@ use oxc_parser::Parser; use oxc_span::SourceType; use super::FormatFileSource; +#[cfg(feature = "napi")] +use super::package_json_sorter::sort_package_json_content; pub enum FormatResult { Success { is_changed: bool, code: String }, @@ -114,6 +116,7 @@ impl SourceFormatter { } /// Format non-JS/TS file using external formatter (Prettier). + /// For package.json files, sorts them first before formatting. #[cfg(feature = "napi")] fn format_by_external_formatter( &self, @@ -126,7 +129,25 @@ impl SourceFormatter { .as_ref() .expect("`external_formatter` must exist when `napi` feature is enabled"); - match external_formatter.format_file(parser_name, source_text) { + // Special handling for package.json: sort before formatting + let code_to_format = if parser_name == "json-stringify" + && path.file_name().and_then(|f| f.to_str()) == Some("package.json") + { + // Sort package.json content first + match sort_package_json_content(source_text) { + Ok(sorted) => sorted, + Err(err) => { + // If sorting fails, return error - don't fall back to unsorted + return FormatResult::Error(vec![err]); + } + } + } else { + // For all other files, use original source + source_text.to_string() + }; + + // Format with Prettier + match external_formatter.format_file(parser_name, &code_to_format) { Ok(code) => FormatResult::Success { is_changed: source_text != code, code }, Err(err) => FormatResult::Error(vec![OxcDiagnostic::error(format!( "Failed to format file with external formatter: {}\n{err}", diff --git a/apps/oxfmt/src/core/mod.rs b/apps/oxfmt/src/core/mod.rs index 7f8116360e0de..355221e1e618c 100644 --- a/apps/oxfmt/src/core/mod.rs +++ b/apps/oxfmt/src/core/mod.rs @@ -3,6 +3,8 @@ mod support; #[cfg(feature = "napi")] mod external_formatter; +#[cfg(feature = "napi")] +mod package_json_sorter; pub use format::{FormatResult, SourceFormatter}; pub use support::FormatFileSource; diff --git a/apps/oxfmt/src/core/package_json_sorter.rs b/apps/oxfmt/src/core/package_json_sorter.rs new file mode 100644 index 0000000000000..b14ef1eedc1a4 --- /dev/null +++ b/apps/oxfmt/src/core/package_json_sorter.rs @@ -0,0 +1,58 @@ +//! Package.json sorting functionality using the sort-package-json crate. + +use oxc_diagnostics::OxcDiagnostic; + +/// Sort a package.json file's content. +/// +/// Uses the sort-package-json crate to reorder top-level fields +/// according to well-established npm conventions. +/// +/// # Arguments +/// * `source_text` - The raw package.json content as a string +/// +/// # Returns +/// * `Ok(String)` - The sorted JSON content +/// * `Err(OxcDiagnostic)` - If parsing or sorting fails +pub fn sort_package_json_content(source_text: &str) -> Result { + sort_package_json::sort_package_json(source_text).map_err(|err| { + OxcDiagnostic::error(format!("Failed to parse package.json for sorting: {err}")) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sort_package_json_basic() { + let input = r#"{"version":"1.0.0","name":"test"}"#; + let result = sort_package_json_content(input).unwrap(); + // Name should come before version + assert!(result.find("\"name\"").unwrap() < result.find("\"version\"").unwrap()); + } + + #[test] + fn test_sort_package_json_preserves_data() { + let input = r#"{"scripts":{"test":"echo"},"name":"test","version":"1.0.0"}"#; + let result = sort_package_json_content(input).unwrap(); + assert!(result.contains("\"name\"")); + assert!(result.contains("\"version\"")); + assert!(result.contains("\"scripts\"")); + assert!(result.contains("\"test\"")); + assert!(result.contains("\"echo\"")); + } + + #[test] + fn test_sort_package_json_invalid_json() { + let input = r#"{"name": invalid}"#; + let result = sort_package_json_content(input); + assert!(result.is_err()); + } + + #[test] + fn test_sort_package_json_empty_object() { + let input = r#"{}"#; + let result = sort_package_json_content(input).unwrap(); + assert_eq!(result.trim(), "{}"); + } +}