Skip to content

Commit 51ddfa8

Browse files
committed
feat(oxfmt): Support .oxfmtrc.json(c) config file (#14398)
Fixes #13611 - Support config file with JSON(C) format - Automatically find the nearest one from `cwd` - Do not merge, just use the first one - Search only `.oxfmtrc.json`(not `.jsonc`) - Use `-c` or `--config` to specify path Example `.oxfmtrc.json`: ```jsonc { "indentStyle": "space", "indentWidth": 2, "lineWidth": 80, "lineEnding": "lf", "quoteStyle": "single", "jsxQuoteStyle": "double", "quoteProperties": "as-needed", "trailingCommas": "all", "semicolons": "always", "arrowParentheses": "always", "bracketSpacing": true, "bracketSameLine": false, "attributePosition": "auto", "expand": "auto", "experimentalOperatorPosition": "before", "experimentalSortImports": { "partitionByNewline": false, "partitionByComment": false, "sortSideEffects": false, "order": "asc", "ignoreCase": true } } ``` We may provide tools like `oxfmt-migrate` to convert `.prettierrc`.
1 parent 78261d6 commit 51ddfa8

18 files changed

+467
-9
lines changed

Cargo.lock

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

apps/oxfmt/src/command.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ pub struct FormatCommand {
2424
#[bpaf(external, fallback(OutputOptions::DefaultWrite))]
2525
pub output_options: OutputOptions,
2626

27+
#[bpaf(external)]
28+
pub basic_options: BasicOptions,
29+
2730
#[bpaf(external)]
2831
pub misc_options: MiscOptions,
2932

@@ -49,6 +52,14 @@ pub enum OutputOptions {
4952
ListDifferent,
5053
}
5154

55+
/// Basic Options
56+
#[derive(Debug, Clone, Bpaf)]
57+
pub struct BasicOptions {
58+
/// Path to the configuration file
59+
#[bpaf(short, long, argument("PATH"))]
60+
pub config: Option<PathBuf>,
61+
}
62+
5263
/// Miscellaneous
5364
#[derive(Debug, Clone, Bpaf)]
5465
pub struct MiscOptions {

apps/oxfmt/src/format.rs

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1-
use std::{env, io::Write, path::PathBuf, sync::mpsc, time::Instant};
1+
use std::{
2+
env,
3+
io::Write,
4+
path::{Path, PathBuf},
5+
sync::mpsc,
6+
time::Instant,
7+
};
28

39
use ignore::overrides::OverrideBuilder;
410

511
use oxc_diagnostics::DiagnosticService;
12+
use oxc_formatter::{FormatOptions, Oxfmtrc};
613

714
use crate::{
815
cli::{CliRunResult, FormatCommand},
@@ -37,7 +44,19 @@ impl FormatRunner {
3744
let start_time = Instant::now();
3845

3946
let cwd = self.cwd;
40-
let FormatCommand { paths, output_options, misc_options } = self.options;
47+
let FormatCommand { paths, output_options, basic_options, misc_options } = self.options;
48+
49+
// Find and load config
50+
let format_options = match load_config(&cwd, basic_options.config.as_ref()) {
51+
Ok(options) => options,
52+
Err(err) => {
53+
print_and_flush_stdout(
54+
stdout,
55+
&format!("Failed to load configuration file.\n{err}\n"),
56+
);
57+
return CliRunResult::InvalidOptionConfig;
58+
}
59+
};
4160

4261
// Default to current working directory if no paths are provided
4362
let paths = if paths.is_empty() { vec![cwd.clone()] } else { paths };
@@ -82,7 +101,7 @@ impl FormatRunner {
82101
let output_options_clone = output_options.clone();
83102
// Spawn a thread to run formatting service with streaming entries
84103
rayon::spawn(move || {
85-
let format_service = FormatService::new(cwd, output_options_clone);
104+
let format_service = FormatService::new(cwd, output_options_clone, format_options);
86105
format_service.run_streaming(rx_entry, &tx_error, tx_count);
87106
});
88107

@@ -156,6 +175,40 @@ impl FormatRunner {
156175
}
157176
}
158177

178+
const DEFAULT_OXFMTRC: &str = ".oxfmtrc.json";
179+
180+
/// # Errors
181+
///
182+
/// Returns error if:
183+
/// - Config file is specified but not found or invalid
184+
/// - Config file parsing fails
185+
fn load_config(cwd: &Path, config: Option<&PathBuf>) -> Result<FormatOptions, String> {
186+
// If `--config` is explicitly specified, use that path directly
187+
if let Some(config_path) = config {
188+
let full_path = if config_path.is_absolute() {
189+
PathBuf::from(config_path)
190+
} else {
191+
cwd.join(config_path)
192+
};
193+
194+
// This will error if the file does not exist or is invalid
195+
let oxfmtrc = Oxfmtrc::from_file(&full_path)?;
196+
return oxfmtrc.into_format_options();
197+
}
198+
199+
// If `--config` is not specified, search the nearest config file from cwd upwards
200+
for dir in cwd.ancestors() {
201+
let config_path = dir.join(DEFAULT_OXFMTRC);
202+
if config_path.exists() {
203+
let oxfmtrc = Oxfmtrc::from_file(&config_path)?;
204+
return oxfmtrc.into_format_options();
205+
}
206+
}
207+
208+
// No config file found, use defaults
209+
Ok(FormatOptions::default())
210+
}
211+
159212
fn print_and_flush_stdout(stdout: &mut dyn Write, message: &str) {
160213
use std::io::{Error, ErrorKind};
161214
fn check_for_writer_error(error: Error) -> Result<(), Error> {

apps/oxfmt/src/service.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@ use crate::{command::OutputOptions, walk::WalkEntry};
1313
pub struct FormatService {
1414
cwd: Box<Path>,
1515
output_options: OutputOptions,
16+
format_options: FormatOptions,
1617
}
1718

1819
impl FormatService {
19-
pub fn new<T>(cwd: T, output_options: OutputOptions) -> Self
20+
pub fn new<T>(cwd: T, output_options: OutputOptions, format_options: FormatOptions) -> Self
2021
where
2122
T: Into<Box<Path>>,
2223
{
23-
Self { cwd: cwd.into(), output_options }
24+
Self { cwd: cwd.into(), output_options, format_options }
2425
}
2526

2627
/// Process entries as they are received from the channel
@@ -72,9 +73,7 @@ impl FormatService {
7273
return;
7374
}
7475

75-
// TODO: Read and apply config
76-
let options = FormatOptions::default();
77-
let code = Formatter::new(&allocator, options).build(&ret.program);
76+
let code = Formatter::new(&allocator, self.format_options.clone()).build(&ret.program);
7877

7978
let elapsed = start_time.elapsed();
8079
let is_changed = source_text != code;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"semicolons": "always"
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"semicolons": "always"
3+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
// Supports JSONC!
3+
"semicolons": "always"
4+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"semicolons": "always"
3+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
const a = 1

apps/oxfmt/tests/mod.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,29 @@ fn write_mode() {
4949
let after = "class Foo {}\n";
5050
Tester::test_write("tests/fixtures/temp.js", before, after);
5151
}
52+
53+
#[test]
54+
fn config_file_auto_discovery() {
55+
Tester::new()
56+
.with_cwd(PathBuf::from("tests/fixtures/config_file"))
57+
.test_and_snapshot_multiple(&[&["--check"]]);
58+
59+
Tester::new()
60+
.with_cwd(PathBuf::from("tests/fixtures/config_file/nested"))
61+
.test_and_snapshot_multiple(&[&["--check"]]);
62+
63+
Tester::new()
64+
.with_cwd(PathBuf::from("tests/fixtures/config_file/nested/deep"))
65+
.test_and_snapshot_multiple(&[&["--check"]]);
66+
}
67+
68+
#[test]
69+
fn config_file_explicit() {
70+
Tester::new().with_cwd(PathBuf::from("tests/fixtures/config_file")).test_and_snapshot_multiple(
71+
&[
72+
&["--check", "--config", "./fmt.json"],
73+
&["--check", "--config", "./fmt.jsonc"],
74+
&["--check", "--config", "NOT_EXISTS.json"],
75+
],
76+
);
77+
}

0 commit comments

Comments
 (0)