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
1 change: 1 addition & 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 @@ -38,6 +38,7 @@ bpaf = { workspace = true, features = ["autocomplete", "bright-color", "derive"]
cow-utils = { workspace = true }
ignore = { workspace = true, features = ["simd-accel"] }
miette = { workspace = true }
phf = { workspace = true, features = ["macros"] }
rayon = { workspace = true }
simdutf8 = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "io-std", "macros"] }
Expand Down
4 changes: 2 additions & 2 deletions apps/oxfmt/src/cli/walk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,13 +222,13 @@ impl ignore::ParallelVisitor for WalkVisitor {
// Use `is_file()` to detect symlinks to the directory named `.js`
#[expect(clippy::filetype_is_file)]
if file_type.is_file() {
let path = entry.path();
// Determine this file should be handled or NOT
// Tier 1 = `.js`, `.tsx`, etc: JS/TS files supported by `oxc_formatter`
// Tier 2 = `.html`, `.json`, etc: Other files supported by Prettier
// (Tier 3 = `.astro`, `.svelte`, etc: Other files supported by Prettier plugins)
// Tier 4 = everything else: Not handled
let Ok(format_file_source) = FormatFileSource::try_from(path) else {
let Ok(format_file_source) = FormatFileSource::try_from(entry.into_path())
else {
return ignore::WalkState::Continue;
};

Expand Down
228 changes: 214 additions & 14 deletions apps/oxfmt/src/core/support.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::path::{Path, PathBuf};

use phf::phf_set;

use oxc_formatter::get_supported_source_type;
use oxc_span::SourceType;

Expand All @@ -8,30 +10,25 @@ pub enum FormatFileSource {
path: PathBuf,
source_type: SourceType,
},
#[expect(dead_code)]
ExternalFormatter {
path: PathBuf,
parser_name: String,
#[cfg_attr(not(feature = "napi"), expect(dead_code))]
parser_name: &'static str,
},
}

impl TryFrom<&Path> for FormatFileSource {
impl TryFrom<PathBuf> for FormatFileSource {
type Error = ();

fn try_from(path: &Path) -> Result<Self, Self::Error> {
fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
// TODO: This logic should(can) move to this file, after LSP support is also moved here.
if let Some(source_type) = get_supported_source_type(path) {
return Ok(Self::OxcFormatter { path: path.to_path_buf(), source_type });
if let Some(source_type) = get_supported_source_type(&path) {
return Ok(Self::OxcFormatter { path, source_type });
}

// TODO: Support more files with `ExternalFormatter`
// - JSON
// - HTML(include .vue)
// - CSS
// - GraphQL
// - Markdown
// - YAML
// - Handlebars
if let Some(parser_name) = get_external_parser_name(&path) {
return Ok(Self::ExternalFormatter { path, parser_name });
}

Err(())
}
Expand All @@ -44,3 +41,206 @@ impl FormatFileSource {
}
}
}

// ---

/// Returns the Prettier parser name for file at `path`, if supported.
/// See also `prettier --support-info | jq '.languages[]'`
/// NOTE: The order matters: more specific matches (like `package.json`) must come before generic ones.
fn get_external_parser_name(path: &Path) -> Option<&'static str> {
let file_name = path.file_name()?.to_str()?;
let extension = path.extension().and_then(|ext| ext.to_str());

// O(1) file name check goes first
if JSON_STRINGIFY_FILENAMES.contains(file_name) || extension == Some("importmap") {
return Some("json-stringify");
}
if JSON_FILENAMES.contains(file_name) {
return Some("json");
}

// Then, check by extension
if (file_name.starts_with("tsconfig.") || file_name.starts_with("jsconfig."))
&& extension == Some("json")
{
return Some("jsonc");
}
if let Some(ext) = extension
&& JSON_EXTENSIONS.contains(ext)
{
return Some("json");
}
if let Some(ext) = extension
&& JSONC_EXTENSIONS.contains(ext)
{
return Some("jsonc");
}
if extension == Some("json5") {
return Some("json5");
}

// TODO: Support more default supported file types
// {
// "extensions": [".html", ".hta", ".htm", ".html.hl", ".inc", ".xht", ".xhtml"],
// "name": "HTML",
// "parsers": ["html"]
// },
// {
// "extensions": [".mjml"],
// "name": "MJML",
// "parsers": ["mjml"]
// },
// {
// "extensions": [".component.html"],
// "name": "Angular",
// "parsers": ["angular"]
// },
// {
// "extensions": [".vue"],
// "name": "Vue",
// "parsers": ["vue"]
// },
//
// {
// "extensions": [".css", ".wxss"],
// "name": "CSS",
// "parsers": ["css"]
// },
// {
// "extensions": [".less"],
// "name": "Less",
// "parsers": ["less"]
// },
// {
// "extensions": [".pcss", ".postcss"],
// "group": "CSS",
// "name": "PostCSS",
// "parsers": ["css"]
// },
// {
// "extensions": [".scss"],
// "name": "SCSS",
// "parsers": ["scss"]
// },
//
// {
// "extensions": [".graphql", ".gql", ".graphqls"],
// "name": "GraphQL",
// "parsers": ["graphql"]
// },
//
// {
// "extensions": [".handlebars", ".hbs"],
// "name": "Handlebars",
// "parsers": ["glimmer"]
// },
//
// {
// "extensions": [".md", ".livemd", ".markdown", ".mdown", ".mdwn", ".mkd", ".mkdn", ".mkdown", ".ronn", ".scd", ".workbook"],
// "filenames": ["contents.lr", "README"],
// "name": "Markdown",
// "parsers": ["markdown"]
// },
// {
// "extensions": [".mdx"],
// "name": "MDX",
// "parsers": ["mdx"]
// },

None
}

static JSON_STRINGIFY_FILENAMES: phf::Set<&'static str> = phf_set! {
"package.json",
"package-lock.json",
"composer.json",
};

static JSON_EXTENSIONS: phf::Set<&'static str> = phf_set! {
"json",
"4DForm",
"4DProject",
"avsc",
"geojson",
"gltf",
"har",
"ice",
"JSON-tmLanguage",
"json.example",
"mcmeta",
"sarif",
"tact",
"tfstate",
"tfstate.backup",
"topojson",
"webapp",
"webmanifest",
"yy",
"yyp",
};

static JSON_FILENAMES: phf::Set<&'static str> = phf_set! {
".all-contributorsrc",
".arcconfig",
".auto-changelog",
".c8rc",
".htmlhintrc",
".imgbotconfig",
".nycrc",
".tern-config",
".tern-project",
".watchmanconfig",
".babelrc",
".jscsrc",
".jshintrc",
".jslintrc",
".swcrc",
};

static JSONC_EXTENSIONS: phf::Set<&'static str> = phf_set! {
"jsonc",
"code-snippets",
"code-workspace",
"sublime-build",
"sublime-color-scheme",
"sublime-commands",
"sublime-completions",
"sublime-keymap",
"sublime-macro",
"sublime-menu",
"sublime-mousemap",
"sublime-project",
"sublime-settings",
"sublime-theme",
"sublime-workspace",
"sublime_metrics",
"sublime_session",
};

#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;

#[test]
fn test_get_external_parser_name() {
let test_cases = vec![
("package.json", Some("json-stringify")),
("package-lock.json", Some("json-stringify")),
("config.importmap", Some("json-stringify")),
("tsconfig.json", Some("jsonc")),
("jsconfig.dev.json", Some("jsonc")),
("data.json", Some("json")),
("schema.avsc", Some("json")),
("config.code-workspace", Some("jsonc")),
("unknown.txt", None),
("prof.png", None),
("foo", None),
];

for (file_name, expected) in test_cases {
let result = get_external_parser_name(Path::new(file_name));
assert_eq!(result, expected, "`{file_name}` should be parsed as {expected:?}");
}
}
}
10 changes: 5 additions & 5 deletions apps/oxfmt/test/__snapshots__/config_file.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

exports[`config_file > auto discovery > nested 1`] = `
"--------------------
arguments: --check
arguments: --check !*.{json,jsonc}
working directory: fixtures/config_file/nested
exit code: 1
--- STDOUT ---------
Expand All @@ -19,7 +19,7 @@ Finished in <variable>ms on 1 files using 1 threads.

exports[`config_file > auto discovery > nested_deep 1`] = `
"--------------------
arguments: --check
arguments: --check !*.{json,jsonc}
working directory: fixtures/config_file/nested/deep
exit code: 1
--- STDOUT ---------
Expand All @@ -36,7 +36,7 @@ Finished in <variable>ms on 1 files using 1 threads.

exports[`config_file > auto discovery > root 1`] = `
"--------------------
arguments: --check
arguments: --check !*.{json,jsonc}
working directory: fixtures/config_file
exit code: 1
--- STDOUT ---------
Expand All @@ -53,7 +53,7 @@ Finished in <variable>ms on 1 files using 1 threads.

exports[`config_file > explicit config 1`] = `
"--------------------
arguments: --check --config ./fmt.json
arguments: --check !*.{json,jsonc} --config ./fmt.json
working directory: fixtures/config_file
exit code: 1
--- STDOUT ---------
Expand All @@ -67,7 +67,7 @@ Finished in <variable>ms on 1 files using 1 threads.

--------------------
--------------------
arguments: --check --config ./fmt.jsonc
arguments: --check !*.{json,jsonc} --config ./fmt.jsonc
working directory: fixtures/config_file
exit code: 1
--- STDOUT ---------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,25 @@ exports[`config_ignore_patterns > should respect ignorePatterns in config 1`] =
"--------------------
arguments: --check
working directory: fixtures/config_ignore_patterns
exit code: 1
exit code: 0
--- STDOUT ---------
Checking formatting...

All matched files use the correct format.
Finished in <variable>ms on 2 files using 1 threads.
--- STDERR ---------
Expected at least one target file

--------------------
--------------------
arguments: --check --config fmtrc.jsonc
working directory: fixtures/config_ignore_patterns
exit code: 1
exit code: 0
--- STDOUT ---------
Checking formatting...

All matched files use the correct format.
Finished in <variable>ms on 2 files using 1 threads.
--- STDERR ---------
Expected at least one target file

--------------------"
`;
Loading
Loading