Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 28e0682

Browse files
committed
feat(oxfmt): Enable experimental package.json sorting by default (#16593)
Fixes #16525, closes #16541 - Use `sort-package-json: 0.0.2` - Enable by default, can be disabled by `experimentalSortPackageJson: false`
1 parent e39f487 commit 28e0682

File tree

10 files changed

+116
-32
lines changed

10 files changed

+116
-32
lines changed

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/oxfmt/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ phf = { workspace = true, features = ["macros"] }
4343
rayon = { workspace = true }
4444
serde_json = { workspace = true }
4545
simdutf8 = { workspace = true }
46+
sort-package-json = "0.0.2"
4647
tokio = { workspace = true, features = ["rt-multi-thread", "io-std", "macros"] }
4748
tracing = { workspace = true }
4849
tracing-subscriber = { workspace = true, features = [] } # Omit the `regex` feature

apps/oxfmt/src/cli/format.rs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ impl FormatRunner {
6969
let config_path = load_config_path(&cwd, basic_options.config.as_deref());
7070
// Load and parse config file
7171
// - `format_options`: Parsed formatting options used by `oxc_formatter`
72-
// - external_config`: JSON value used by `external_formatter`, populated with `format_options`
73-
let (format_options, ignore_patterns, external_config) =
72+
// - `external_config`: JSON value used by `external_formatter`, populated with `format_options`
73+
let (format_options, oxfmt_options, external_config) =
7474
match load_config(config_path.as_deref()) {
7575
Ok(c) => c,
7676
Err(err) => {
@@ -99,15 +99,16 @@ impl FormatRunner {
9999
);
100100
return CliRunResult::InvalidOptionConfig;
101101
}
102+
102103
#[cfg(not(feature = "napi"))]
103-
let _ = external_config;
104+
let _ = (external_config, oxfmt_options.is_sort_package_json);
104105

105106
let walker = match Walk::build(
106107
&cwd,
107108
&paths,
108109
&ignore_options.ignore_path,
109110
ignore_options.with_node_modules,
110-
&ignore_patterns,
111+
&oxfmt_options.ignore_patterns,
111112
) {
112113
Ok(Some(walker)) => walker,
113114
// All target paths are ignored
@@ -144,7 +145,8 @@ impl FormatRunner {
144145
// Create `SourceFormatter` instance
145146
let source_formatter = SourceFormatter::new(num_of_threads, format_options);
146147
#[cfg(feature = "napi")]
147-
let source_formatter = source_formatter.with_external_formatter(self.external_formatter);
148+
let source_formatter = source_formatter
149+
.with_external_formatter(self.external_formatter, oxfmt_options.is_sort_package_json);
148150

149151
let output_options_clone = output_options.clone();
150152

@@ -269,11 +271,17 @@ fn load_config_path(cwd: &Path, config_path: Option<&Path>) -> Option<PathBuf> {
269271
})
270272
}
271273

274+
#[derive(Debug)]
275+
struct OxfmtOptions {
276+
ignore_patterns: Vec<String>,
277+
is_sort_package_json: bool,
278+
}
279+
272280
/// # Errors
273281
/// Returns error if:
274282
/// - Config file is specified but not found or invalid
275283
/// - Config file parsing fails
276-
fn load_config(config_path: Option<&Path>) -> Result<(FormatOptions, Vec<String>, Value), String> {
284+
fn load_config(config_path: Option<&Path>) -> Result<(FormatOptions, OxfmtOptions, Value), String> {
277285
// Read and parse config file, or use empty JSON if not found
278286
let json_string = match config_path {
279287
Some(path) => {
@@ -297,7 +305,10 @@ fn load_config(config_path: Option<&Path>) -> Result<(FormatOptions, Vec<String>
297305
let oxfmtrc: Oxfmtrc = serde_json::from_str(&json_string)
298306
.map_err(|err| format!("Failed to deserialize config: {err}"))?;
299307

300-
let ignore_patterns = oxfmtrc.ignore_patterns.clone().unwrap_or_default();
308+
let oxfmt_options = OxfmtOptions {
309+
ignore_patterns: oxfmtrc.ignore_patterns.clone().unwrap_or_default(),
310+
is_sort_package_json: oxfmtrc.experimental_sort_package_json,
311+
};
301312

302313
// NOTE: Other validation based on it's field values are done here
303314
let format_options = oxfmtrc
@@ -307,7 +318,7 @@ fn load_config(config_path: Option<&Path>) -> Result<(FormatOptions, Vec<String>
307318
// Populate `raw_config` with resolved options to apply our defaults
308319
Oxfmtrc::populate_prettier_config(&format_options, &mut raw_config);
309320

310-
Ok((format_options, ignore_patterns, raw_config))
321+
Ok((format_options, oxfmt_options, raw_config))
311322
}
312323

313324
fn print_and_flush(writer: &mut dyn Write, message: &str) {

apps/oxfmt/src/core/format.rs

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#[cfg(feature = "napi")]
2+
use std::borrow::Cow;
13
use std::path::Path;
24

35
use oxc_allocator::AllocatorPool;
@@ -17,6 +19,8 @@ pub struct SourceFormatter {
1719
allocator_pool: AllocatorPool,
1820
format_options: FormatOptions,
1921
#[cfg(feature = "napi")]
22+
pub is_sort_package_json: bool,
23+
#[cfg(feature = "napi")]
2024
external_formatter: Option<super::ExternalFormatter>,
2125
}
2226

@@ -26,6 +30,8 @@ impl SourceFormatter {
2630
allocator_pool: AllocatorPool::new(num_of_threads),
2731
format_options,
2832
#[cfg(feature = "napi")]
33+
is_sort_package_json: false,
34+
#[cfg(feature = "napi")]
2935
external_formatter: None,
3036
}
3137
}
@@ -35,25 +41,47 @@ impl SourceFormatter {
3541
pub fn with_external_formatter(
3642
mut self,
3743
external_formatter: Option<super::ExternalFormatter>,
44+
sort_package_json: bool,
3845
) -> Self {
3946
self.external_formatter = external_formatter;
47+
self.is_sort_package_json = sort_package_json;
4048
self
4149
}
4250

4351
/// Format a file based on its source type.
4452
pub fn format(&self, entry: &FormatFileSource, source_text: &str) -> FormatResult {
45-
match entry {
53+
let result = match entry {
4654
FormatFileSource::OxcFormatter { path, source_type } => {
4755
self.format_by_oxc_formatter(source_text, path, *source_type)
4856
}
4957
#[cfg(feature = "napi")]
5058
FormatFileSource::ExternalFormatter { path, parser_name } => {
51-
self.format_by_external_formatter(source_text, path, parser_name)
59+
let text_to_format: Cow<'_, str> =
60+
if self.is_sort_package_json && entry.is_package_json() {
61+
match sort_package_json::sort_package_json(source_text) {
62+
Ok(sorted) => Cow::Owned(sorted),
63+
Err(err) => {
64+
return FormatResult::Error(vec![OxcDiagnostic::error(format!(
65+
"Failed to sort package.json: {}\n{err}",
66+
path.display()
67+
))]);
68+
}
69+
}
70+
} else {
71+
Cow::Borrowed(source_text)
72+
};
73+
74+
self.format_by_external_formatter(&text_to_format, path, parser_name)
5275
}
5376
#[cfg(not(feature = "napi"))]
5477
FormatFileSource::ExternalFormatter { .. } => {
5578
unreachable!("If `napi` feature is disabled, this should not be passed here")
5679
}
80+
};
81+
82+
match result {
83+
Ok(code) => FormatResult::Success { is_changed: source_text != code, code },
84+
Err(err) => FormatResult::Error(vec![err]),
5785
}
5886
}
5987

@@ -63,15 +91,16 @@ impl SourceFormatter {
6391
source_text: &str,
6492
path: &Path,
6593
source_type: SourceType,
66-
) -> FormatResult {
94+
) -> Result<String, OxcDiagnostic> {
6795
let source_type = enable_jsx_source_type(source_type);
6896
let allocator = self.allocator_pool.get();
6997

7098
let ret = Parser::new(&allocator, source_text, source_type)
7199
.with_options(get_parse_options())
72100
.parse();
73101
if !ret.errors.is_empty() {
74-
return FormatResult::Error(ret.errors);
102+
// Return the first error for simplicity
103+
return Err(ret.errors.into_iter().next().unwrap());
75104
}
76105

77106
let base_formatter = Formatter::new(&allocator, self.format_options.clone());
@@ -92,25 +121,23 @@ impl SourceFormatter {
92121
#[cfg(not(feature = "napi"))]
93122
let formatted = base_formatter.format(&ret.program);
94123

95-
let code = match formatted.print() {
96-
Ok(printed) => printed.into_code(),
97-
Err(err) => {
98-
return FormatResult::Error(vec![OxcDiagnostic::error(format!(
99-
"Failed to print formatted code: {}\n{err}",
100-
path.display()
101-
))]);
102-
}
103-
};
124+
let code = formatted.print().map_err(|err| {
125+
OxcDiagnostic::error(format!(
126+
"Failed to print formatted code: {}\n{err}",
127+
path.display()
128+
))
129+
})?;
104130

105131
#[cfg(feature = "detect_code_removal")]
106132
{
107-
if let Some(diff) = oxc_formatter::detect_code_removal(source_text, &code, source_type)
133+
if let Some(diff) =
134+
oxc_formatter::detect_code_removal(source_text, code.as_code(), source_type)
108135
{
109136
unreachable!("Code removal detected in `{}`:\n{diff}", path.to_string_lossy());
110137
}
111138
}
112139

113-
FormatResult::Success { is_changed: source_text != code, code }
140+
Ok(code.into_code())
114141
}
115142

116143
/// Format non-JS/TS file using external formatter (Prettier).
@@ -120,7 +147,7 @@ impl SourceFormatter {
120147
source_text: &str,
121148
path: &Path,
122149
parser_name: &str,
123-
) -> FormatResult {
150+
) -> Result<String, OxcDiagnostic> {
124151
let external_formatter = self
125152
.external_formatter
126153
.as_ref()
@@ -136,12 +163,11 @@ impl SourceFormatter {
136163
// (without supporting `overrides` in config file)
137164
let file_name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
138165

139-
match external_formatter.format_file(parser_name, file_name, source_text) {
140-
Ok(code) => FormatResult::Success { is_changed: source_text != code, code },
141-
Err(err) => FormatResult::Error(vec![OxcDiagnostic::error(format!(
166+
external_formatter.format_file(parser_name, file_name, source_text).map_err(|err| {
167+
OxcDiagnostic::error(format!(
142168
"Failed to format file with external formatter: {}\n{err}",
143169
path.display()
144-
))]),
145-
}
170+
))
171+
})
146172
}
147173
}

apps/oxfmt/src/core/support.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ impl FormatFileSource {
4040
Self::OxcFormatter { path, .. } | Self::ExternalFormatter { path, .. } => path,
4141
}
4242
}
43+
44+
#[cfg(feature = "napi")]
45+
pub fn is_package_json(&self) -> bool {
46+
match self {
47+
Self::OxcFormatter { .. } => false,
48+
Self::ExternalFormatter { path, parser_name } => {
49+
parser_name == &"json-stringify"
50+
&& path.file_name().and_then(|f| f.to_str()) == Some("package.json")
51+
}
52+
}
53+
}
4354
}
4455

4556
// ---

apps/oxfmt/test/__snapshots__/external_formatter.test.ts.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,15 @@ package.json
4646
"build-js": "node scripts/build.js",
4747
"test": "tsc && vitest --dir test run"
4848
},
49-
"engines": {
50-
"node": "^20.19.0 || >=22.12.0"
51-
},
5249
"dependencies": {
5350
"prettier": "3.7.4"
5451
},
5552
"devDependencies": {
5653
"@types/node": "catalog:",
5754
"execa": "^9.6.0"
55+
},
56+
"engines": {
57+
"node": "^20.19.0 || >=22.12.0"
5858
}
5959
}
6060

crates/oxc_formatter/src/service/oxfmtrc.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ pub struct Oxfmtrc {
7878
#[serde(skip_serializing_if = "Option::is_none")]
7979
pub experimental_sort_imports: Option<SortImportsConfig>,
8080

81+
/// Experimental: Sort `package.json` keys. (Default: true)
82+
#[serde(default = "default_true")]
83+
pub experimental_sort_package_json: bool,
84+
8185
/// Ignore files matching these glob patterns. Current working directory is used as the root.
8286
#[serde(skip_serializing_if = "Option::is_none")]
8387
pub ignore_patterns: Option<Vec<String>>,
@@ -521,6 +525,7 @@ impl Oxfmtrc {
521525
// Below are our own extensions, just remove them
522526
obj.remove("ignorePatterns");
523527
obj.remove("experimentalSortImports");
528+
obj.remove("experimentalSortPackageJson");
524529

525530
// Any other unknown fields are preserved as-is.
526531
// e.g. `plugins`, `htmlWhitespaceSensitivity`, `vueIndentScriptAndStyle`, etc.

crates/oxc_formatter/tests/snapshots/schema_json.snap

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ expression: json
6666
}
6767
]
6868
},
69+
"experimentalSortPackageJson": {
70+
"description": "Experimental: Sort `package.json` keys. (Default: true)",
71+
"default": true,
72+
"type": "boolean"
73+
},
6974
"ignorePatterns": {
7075
"description": "Ignore files matching these glob patterns. Current working directory is used as the root.",
7176
"type": [

npm/oxfmt/configuration_schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@
6262
}
6363
]
6464
},
65+
"experimentalSortPackageJson": {
66+
"description": "Experimental: Sort `package.json` keys. (Default: true)",
67+
"default": true,
68+
"type": "boolean"
69+
},
6570
"ignorePatterns": {
6671
"description": "Ignore files matching these glob patterns. Current working directory is used as the root.",
6772
"type": [

tasks/website_formatter/src/snapshots/schema_markdown.snap

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,15 @@ default: `false`
138138

139139

140140

141+
## experimentalSortPackageJson
142+
143+
type: `boolean`
144+
145+
default: `true`
146+
147+
Experimental: Sort `package.json` keys. (Default: true)
148+
149+
141150
## ignorePatterns
142151

143152
type: `string[]`

0 commit comments

Comments
 (0)