Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions apps/oxfmt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ phf = { workspace = true, features = ["macros"] }
rayon = { workspace = true }
serde_json = { workspace = true }
simdutf8 = { workspace = true }
sort-package-json = "0.0.2"
tokio = { workspace = true, features = ["rt-multi-thread", "io-std", "macros"] }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, features = [] } # Omit the `regex` feature
Expand Down
27 changes: 19 additions & 8 deletions apps/oxfmt/src/cli/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ impl FormatRunner {
let config_path = load_config_path(&cwd, basic_options.config.as_deref());
// Load and parse config file
// - `format_options`: Parsed formatting options used by `oxc_formatter`
// - external_config`: JSON value used by `external_formatter`, populated with `format_options`
let (format_options, ignore_patterns, external_config) =
// - `external_config`: JSON value used by `external_formatter`, populated with `format_options`
let (format_options, oxfmt_options, external_config) =
match load_config(config_path.as_deref()) {
Ok(c) => c,
Err(err) => {
Expand Down Expand Up @@ -99,15 +99,16 @@ impl FormatRunner {
);
return CliRunResult::InvalidOptionConfig;
}

#[cfg(not(feature = "napi"))]
let _ = external_config;
let _ = (external_config, oxfmt_options.is_sort_package_json);

let walker = match Walk::build(
&cwd,
&paths,
&ignore_options.ignore_path,
ignore_options.with_node_modules,
&ignore_patterns,
&oxfmt_options.ignore_patterns,
) {
Ok(Some(walker)) => walker,
// All target paths are ignored
Expand Down Expand Up @@ -144,7 +145,8 @@ impl FormatRunner {
// Create `SourceFormatter` instance
let source_formatter = SourceFormatter::new(num_of_threads, format_options);
#[cfg(feature = "napi")]
let source_formatter = source_formatter.with_external_formatter(self.external_formatter);
let source_formatter = source_formatter
.with_external_formatter(self.external_formatter, oxfmt_options.is_sort_package_json);

let output_options_clone = output_options.clone();

Expand Down Expand Up @@ -269,11 +271,17 @@ fn load_config_path(cwd: &Path, config_path: Option<&Path>) -> Option<PathBuf> {
})
}

#[derive(Debug)]
struct OxfmtOptions {
ignore_patterns: Vec<String>,
is_sort_package_json: bool,
}

/// # Errors
/// Returns error if:
/// - Config file is specified but not found or invalid
/// - Config file parsing fails
fn load_config(config_path: Option<&Path>) -> Result<(FormatOptions, Vec<String>, Value), String> {
fn load_config(config_path: Option<&Path>) -> Result<(FormatOptions, OxfmtOptions, Value), String> {
// Read and parse config file, or use empty JSON if not found
let json_string = match config_path {
Some(path) => {
Expand All @@ -297,7 +305,10 @@ fn load_config(config_path: Option<&Path>) -> Result<(FormatOptions, Vec<String>
let oxfmtrc: Oxfmtrc = serde_json::from_str(&json_string)
.map_err(|err| format!("Failed to deserialize config: {err}"))?;

let ignore_patterns = oxfmtrc.ignore_patterns.clone().unwrap_or_default();
let oxfmt_options = OxfmtOptions {
ignore_patterns: oxfmtrc.ignore_patterns.clone().unwrap_or_default(),
is_sort_package_json: oxfmtrc.experimental_sort_package_json,
};

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

Ok((format_options, ignore_patterns, raw_config))
Ok((format_options, oxfmt_options, raw_config))
}

fn print_and_flush(writer: &mut dyn Write, message: &str) {
Expand Down
68 changes: 47 additions & 21 deletions apps/oxfmt/src/core/format.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#[cfg(feature = "napi")]
use std::borrow::Cow;
use std::path::Path;

use oxc_allocator::AllocatorPool;
Expand All @@ -17,6 +19,8 @@ pub struct SourceFormatter {
allocator_pool: AllocatorPool,
format_options: FormatOptions,
#[cfg(feature = "napi")]
pub is_sort_package_json: bool,
#[cfg(feature = "napi")]
external_formatter: Option<super::ExternalFormatter>,
}

Expand All @@ -26,6 +30,8 @@ impl SourceFormatter {
allocator_pool: AllocatorPool::new(num_of_threads),
format_options,
#[cfg(feature = "napi")]
is_sort_package_json: false,
#[cfg(feature = "napi")]
external_formatter: None,
}
}
Expand All @@ -35,25 +41,47 @@ impl SourceFormatter {
pub fn with_external_formatter(
mut self,
external_formatter: Option<super::ExternalFormatter>,
sort_package_json: bool,
) -> Self {
self.external_formatter = external_formatter;
self.is_sort_package_json = sort_package_json;
self
}

/// Format a file based on its source type.
pub fn format(&self, entry: &FormatFileSource, source_text: &str) -> FormatResult {
match entry {
let result = match entry {
FormatFileSource::OxcFormatter { path, source_type } => {
self.format_by_oxc_formatter(source_text, path, *source_type)
}
#[cfg(feature = "napi")]
FormatFileSource::ExternalFormatter { path, parser_name } => {
self.format_by_external_formatter(source_text, path, parser_name)
let text_to_format: Cow<'_, str> =
if self.is_sort_package_json && entry.is_package_json() {
match sort_package_json::sort_package_json(source_text) {
Ok(sorted) => Cow::Owned(sorted),
Err(err) => {
return FormatResult::Error(vec![OxcDiagnostic::error(format!(
"Failed to sort package.json: {}\n{err}",
path.display()
))]);
}
}
} else {
Cow::Borrowed(source_text)
};

self.format_by_external_formatter(&text_to_format, path, parser_name)
}
#[cfg(not(feature = "napi"))]
FormatFileSource::ExternalFormatter { .. } => {
unreachable!("If `napi` feature is disabled, this should not be passed here")
}
};

match result {
Ok(code) => FormatResult::Success { is_changed: source_text != code, code },
Err(err) => FormatResult::Error(vec![err]),
}
}

Expand All @@ -63,15 +91,16 @@ impl SourceFormatter {
source_text: &str,
path: &Path,
source_type: SourceType,
) -> FormatResult {
) -> Result<String, OxcDiagnostic> {
let source_type = enable_jsx_source_type(source_type);
let allocator = self.allocator_pool.get();

let ret = Parser::new(&allocator, source_text, source_type)
.with_options(get_parse_options())
.parse();
if !ret.errors.is_empty() {
return FormatResult::Error(ret.errors);
// Return the first error for simplicity
return Err(ret.errors.into_iter().next().unwrap());
}

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

let code = match formatted.print() {
Ok(printed) => printed.into_code(),
Err(err) => {
return FormatResult::Error(vec![OxcDiagnostic::error(format!(
"Failed to print formatted code: {}\n{err}",
path.display()
))]);
}
};
let code = formatted.print().map_err(|err| {
OxcDiagnostic::error(format!(
"Failed to print formatted code: {}\n{err}",
path.display()
))
})?;

#[cfg(feature = "detect_code_removal")]
{
if let Some(diff) = oxc_formatter::detect_code_removal(source_text, &code, source_type)
if let Some(diff) =
oxc_formatter::detect_code_removal(source_text, code.as_code(), source_type)
{
unreachable!("Code removal detected in `{}`:\n{diff}", path.to_string_lossy());
}
}

FormatResult::Success { is_changed: source_text != code, code }
Ok(code.into_code())
}

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

match external_formatter.format_file(parser_name, file_name, source_text) {
Ok(code) => FormatResult::Success { is_changed: source_text != code, code },
Err(err) => FormatResult::Error(vec![OxcDiagnostic::error(format!(
external_formatter.format_file(parser_name, file_name, source_text).map_err(|err| {
OxcDiagnostic::error(format!(
"Failed to format file with external formatter: {}\n{err}",
path.display()
))]),
}
))
})
}
}
11 changes: 11 additions & 0 deletions apps/oxfmt/src/core/support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ impl FormatFileSource {
Self::OxcFormatter { path, .. } | Self::ExternalFormatter { path, .. } => path,
}
}

#[cfg(feature = "napi")]
pub fn is_package_json(&self) -> bool {
match self {
Self::OxcFormatter { .. } => false,
Self::ExternalFormatter { path, parser_name } => {
parser_name == &"json-stringify"
&& path.file_name().and_then(|f| f.to_str()) == Some("package.json")
}
}
}
}

// ---
Expand Down
6 changes: 3 additions & 3 deletions apps/oxfmt/test/__snapshots__/external_formatter.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ package.json
"build-js": "node scripts/build.js",
"test": "tsc && vitest --dir test run"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"dependencies": {
"prettier": "3.7.4"
},
"devDependencies": {
"@types/node": "catalog:",
"execa": "^9.6.0"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
}

Expand Down
5 changes: 5 additions & 0 deletions crates/oxc_formatter/src/service/oxfmtrc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ pub struct Oxfmtrc {
#[serde(skip_serializing_if = "Option::is_none")]
pub experimental_sort_imports: Option<SortImportsConfig>,

/// Experimental: Sort `package.json` keys. (Default: true)
#[serde(default = "default_true")]
pub experimental_sort_package_json: bool,

/// Ignore files matching these glob patterns. Current working directory is used as the root.
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore_patterns: Option<Vec<String>>,
Expand Down Expand Up @@ -521,6 +525,7 @@ impl Oxfmtrc {
// Below are our own extensions, just remove them
obj.remove("ignorePatterns");
obj.remove("experimentalSortImports");
obj.remove("experimentalSortPackageJson");

// Any other unknown fields are preserved as-is.
// e.g. `plugins`, `htmlWhitespaceSensitivity`, `vueIndentScriptAndStyle`, etc.
Expand Down
5 changes: 5 additions & 0 deletions crates/oxc_formatter/tests/snapshots/schema_json.snap
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ expression: json
}
]
},
"experimentalSortPackageJson": {
"description": "Experimental: Sort `package.json` keys. (Default: true)",
"default": true,
"type": "boolean"
},
"ignorePatterns": {
"description": "Ignore files matching these glob patterns. Current working directory is used as the root.",
"type": [
Expand Down
5 changes: 5 additions & 0 deletions npm/oxfmt/configuration_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@
}
]
},
"experimentalSortPackageJson": {
"description": "Experimental: Sort `package.json` keys. (Default: true)",
"default": true,
"type": "boolean"
},
"ignorePatterns": {
"description": "Ignore files matching these glob patterns. Current working directory is used as the root.",
"type": [
Expand Down
9 changes: 9 additions & 0 deletions tasks/website_formatter/src/snapshots/schema_markdown.snap
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,15 @@ default: `false`



## experimentalSortPackageJson

type: `boolean`

default: `true`

Experimental: Sort `package.json` keys. (Default: true)


## ignorePatterns

type: `string[]`
Expand Down
Loading