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
176 changes: 140 additions & 36 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,13 @@ tracing-subscriber = "0.3.22" # Tracing implementation
ureq = { version = "3.1.4", default-features = false } # HTTP client
url = { version = "2.5.7" } # URL parsing
walkdir = "2.5.0" # Directory traversal
editorconfig-parser = "0.0.3"
natord = "1.0.9"
oxfmt = { path = "apps/oxfmt" }
sort-package-json = "0.0.5"
oxc-toml = "0.14.1"
unicode-width = "0.2"
website_common = { path = "tasks/website_common" }

[workspace.metadata.cargo-shear]
ignored = ["oxc_transform_napi", "oxc_parser_napi", "oxc_minify_napi"]
Expand Down
5 changes: 3 additions & 2 deletions apps/oxfmt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,16 @@ oxc_span = { workspace = true }

bpaf = { workspace = true, features = ["autocomplete", "bright-color", "derive"] }
cow-utils = { workspace = true }
editorconfig-parser = "0.0.3"
editorconfig-parser = { workspace = true }
ignore = { workspace = true, features = ["simd-accel"] }
json-strip-comments = { workspace = true }
miette = { workspace = true }
phf = { workspace = true, features = ["macros"] }
rayon = { workspace = true }
serde_json = { workspace = true }
simdutf8 = { workspace = true }
sort-package-json = "0.0.5"
sort-package-json = { workspace = true }
oxc-toml = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "io-std", "macros"] }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, features = [] } # Omit the `regex` feature
Expand Down
26 changes: 26 additions & 0 deletions apps/oxfmt/src/core/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use editorconfig_parser::{
EditorConfig, EditorConfigProperties, EditorConfigProperty, EndOfLine, IndentStyle,
MaxLineLength,
};
use oxc_toml::formatter::Options as TomlFormatterOptions;
use serde_json::Value;

use oxc_formatter::{
Expand Down Expand Up @@ -53,6 +54,8 @@ pub enum ResolvedOptions {
/// For embedded language formatting (e.g., CSS in template literals)
external_options: Value,
},
/// For TOML files.
OxfmtToml { toml_options: TomlFormatterOptions },
/// For non-JS files formatted by external formatter (Prettier).
#[cfg(feature = "napi")]
ExternalFormatter { external_options: Value },
Expand Down Expand Up @@ -189,6 +192,9 @@ impl ConfigResolver {
FormatFileStrategy::OxcFormatter { .. } => {
ResolvedOptions::OxcFormatter { format_options, external_options }
}
FormatFileStrategy::OxfmtToml { .. } => {
ResolvedOptions::OxfmtToml { toml_options: build_toml_options(&format_options) }
}
#[cfg(feature = "napi")]
FormatFileStrategy::ExternalFormatter { .. } => {
ResolvedOptions::ExternalFormatter { external_options }
Expand Down Expand Up @@ -319,3 +325,23 @@ fn apply_editorconfig(oxfmtrc: &mut Oxfmtrc, props: &EditorConfigProperties) {
oxfmtrc.tab_width = Some(size as u8);
}
}

// ---

/// Build `toml` formatter options.
/// The same as `prettier-plugin-toml`.
/// <https://github.com/un-ts/prettier/blob/7a4346d5dbf6b63987c0f81228fc46bb12f8692f/packages/toml/src/index.ts#L27-L31>
fn build_toml_options(format_options: &FormatOptions) -> TomlFormatterOptions {
TomlFormatterOptions {
column_width: format_options.line_width.value() as usize,
indent_string: if format_options.indent_style.is_tab() {
"\t".to_string()
} else {
" ".repeat(format_options.indent_width.value() as usize)
},
trailing_newline: true,
array_trailing_comma: !format_options.trailing_commas.is_none(),
crlf: format_options.line_ending.is_carriage_return_line_feed(),
..Default::default()
}
}
8 changes: 8 additions & 0 deletions apps/oxfmt/src/core/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ impl SourceFormatter {
format_options,
external_options,
),
(FormatFileStrategy::OxfmtToml { .. }, ResolvedOptions::OxfmtToml { toml_options }) => {
Ok(Self::format_by_toml(source_text, toml_options))
}
#[cfg(feature = "napi")]
(
FormatFileStrategy::ExternalFormatter { path, parser_name },
Expand Down Expand Up @@ -149,6 +152,11 @@ impl SourceFormatter {
Ok(code.into_code())
}

/// Format TOML file using `toml`.
fn format_by_toml(source_text: &str, options: &oxc_toml::formatter::Options) -> String {
oxc_toml::formatter::format(source_text, options.clone())
}

/// Format non-JS/TS file using external formatter (Prettier).
#[cfg(feature = "napi")]
fn format_by_external_formatter(
Expand Down
77 changes: 69 additions & 8 deletions apps/oxfmt/src/core/support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ pub enum FormatFileStrategy {
path: PathBuf,
source_type: SourceType,
},
/// TOML files formatted by taplo (Pure Rust).
OxfmtToml {
path: PathBuf,
},
ExternalFormatter {
path: PathBuf,
#[cfg_attr(not(feature = "napi"), expect(dead_code))]
Expand Down Expand Up @@ -43,6 +47,11 @@ impl TryFrom<PathBuf> for FormatFileStrategy {
return Err(());
}

// Then TOML files
if is_toml_file(file_name) {
return Ok(Self::OxfmtToml { path });
}

// Then external formatter files
// `package.json` is special: sorted then formatted
if file_name == "package.json" {
Expand All @@ -61,12 +70,13 @@ impl TryFrom<PathBuf> for FormatFileStrategy {
impl FormatFileStrategy {
#[cfg(not(feature = "napi"))]
pub fn can_format_without_external(&self) -> bool {
matches!(self, Self::OxcFormatter { .. })
matches!(self, Self::OxcFormatter { .. } | Self::OxfmtToml { .. })
}

pub fn path(&self) -> &Path {
match self {
Self::OxcFormatter { path, .. }
| Self::OxfmtToml { path }
| Self::ExternalFormatter { path, .. }
| Self::ExternalFormatterPackageJson { path, .. } => path,
}
Expand All @@ -86,16 +96,43 @@ static EXCLUDE_FILENAMES: phf::Set<&'static str> = phf_set! {
"Pipfile.lock",
"flake.lock",
"mcmod.info",
// TOML lock files
"Cargo.lock",
"Gopkg.lock",
"pdm.lock",
"poetry.lock",
"uv.lock",
};

// ---

/// Returns `true` if this is a TOML file.
fn is_toml_file(file_name: &str) -> bool {
if TOML_FILENAMES.contains(file_name) {
return true;
}

#[expect(clippy::case_sensitive_file_extension_comparisons)]
if file_name.ends_with(".toml.example") || file_name.ends_with(".toml") {
return true;
}

false
}

static TOML_FILENAMES: phf::Set<&'static str> = phf_set! {
"Pipfile",
"Cargo.toml.orig",
};

// ---

/// Returns parser name for external formatter, if supported.
/// NOTE: `package.json` is handled separately in `TryFrom`.
/// See also `prettier --support-info | jq '.languages[]'`
fn get_external_parser_name(file_name: &str, extension: Option<&str>) -> Option<&'static str> {
// JSON and variants
if JSON_STRINGIFY_FILENAMES.contains(file_name) || extension == Some("importmap") {
// NOTE: `package.json` is handled separately in `FormatFileStrategy::try_from()`
if file_name == "composer.json" || extension == Some("importmap") {
return Some("json-stringify");
}
if JSON_FILENAMES.contains(file_name) {
Expand Down Expand Up @@ -185,11 +222,6 @@ fn get_external_parser_name(file_name: &str, extension: Option<&str>) -> Option<
None
}

static JSON_STRINGIFY_FILENAMES: phf::Set<&'static str> = phf_set! {
// NOTE: `package.json` is handled separately as `ExternalFormatterPackageJson`
"composer.json",
};

static JSON_EXTENSIONS: phf::Set<&'static str> = phf_set! {
"json",
"4DForm",
Expand Down Expand Up @@ -399,4 +431,33 @@ mod tests {
let source = FormatFileStrategy::try_from(PathBuf::from("composer.json")).unwrap();
assert!(matches!(source, FormatFileStrategy::ExternalFormatter { .. }));
}

#[test]
fn test_toml_files() {
// Files that should be detected as TOML
let toml_files = vec![
"Cargo.toml",
"pyproject.toml",
"config.toml",
"config.toml.example",
"Pipfile",
"Cargo.toml.orig",
];

for file_name in toml_files {
let result = FormatFileStrategy::try_from(PathBuf::from(file_name));
assert!(
matches!(result, Ok(FormatFileStrategy::OxfmtToml { .. })),
"`{file_name}` should be detected as TOML"
);
}

// Lock files that should be excluded
let excluded_files = vec!["Cargo.lock", "poetry.lock", "pdm.lock", "uv.lock", "Gopkg.lock"];

for file_name in excluded_files {
let result = FormatFileStrategy::try_from(PathBuf::from(file_name));
assert!(result.is_err(), "`{file_name}` should be excluded (lock file)");
}
}
}
4 changes: 2 additions & 2 deletions crates/oxc_formatter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ oxc_span = { workspace = true }
oxc_syntax = { workspace = true }

cow-utils = { workspace = true }
natord = "1.0.9"
natord = { workspace = true }
phf = { workspace = true, features = ["macros"] }
rustc-hash = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
unicode-width = "0.2"
unicode-width = { workspace = true }

[dev-dependencies]
insta = { workspace = true }
Expand Down
6 changes: 3 additions & 3 deletions tasks/website_formatter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ doctest = false

[dependencies]
bpaf = { workspace = true, features = ["docgen"] }
oxc_formatter = { path = "../../crates/oxc_formatter" }
oxfmt = { path = "../../apps/oxfmt" }
oxc_formatter = { workspace = true }
oxfmt = { workspace = true }
pico-args = { workspace = true }
schemars = { workspace = true }
serde_json = { workspace = true }
website_common = { path = "../website_common" }
website_common = { workspace = true }

[dev-dependencies]
insta = { workspace = true }
Expand Down
4 changes: 2 additions & 2 deletions tasks/website_linter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ doctest = false
bpaf = { workspace = true, features = ["docgen"] }
itertools = { workspace = true }
oxc_linter = { workspace = true, features = ["ruledocs"] }
oxlint = { path = "../../apps/oxlint", default-features = false }
oxlint = { workspace = true }
pico-args = { workspace = true }
project-root = { workspace = true }
schemars = { workspace = true }
website_common = { path = "../website_common" }
website_common = { workspace = true }

[dev-dependencies]
insta = { workspace = true }
Expand Down
Loading