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
14 changes: 7 additions & 7 deletions apps/oxfmt/src/cli/walk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,22 +228,22 @@ impl ignore::ParallelVisitor for WalkVisitor {
if file_type.is_file() {
// 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) = FormatFileStrategy::try_from(entry.into_path())
else {
// Tier 2 = `.toml`, etc: Some files supported by `oxfmt` directly
// Tier 3 = `.html`, `.json`, etc: Other files supported by Prettier
// (Tier 4 = `.astro`, `.svelte`, etc: Other files supported by Prettier plugins)
// Everything else: Ignored
let Ok(strategy) = FormatFileStrategy::try_from(entry.into_path()) else {
return ignore::WalkState::Continue;
};

#[cfg(not(feature = "napi"))]
if !matches!(format_file_source, FormatFileStrategy::OxcFormatter { .. }) {
if !strategy.can_format_without_external() {
return ignore::WalkState::Continue;
}

// Send each entry immediately through the channel
// If send fails, the receiver has been dropped, so stop walking
if self.sender.send(format_file_source).is_err() {
if self.sender.send(strategy).is_err() {
return ignore::WalkState::Quit;
}
}
Expand Down
138 changes: 69 additions & 69 deletions apps/oxfmt/src/core/support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,43 @@ impl TryFrom<PathBuf> for FormatFileStrategy {
type Error = ();

fn try_from(path: PathBuf) -> Result<Self, Self::Error> {
// Check JS/TS files first
// 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, source_type });
}

if let Some(source) = get_external_format_source(path) {
return Ok(source);
// Extract file_name and extension once for all subsequent checks
let Some(file_name) = path.file_name().and_then(|f| f.to_str()) else {
return Err(());
};

// Excluded files like lock files
if EXCLUDE_FILENAMES.contains(file_name) {
return Err(());
}

// Then external formatter files
// `package.json` is special: sorted then formatted
if file_name == "package.json" {
return Ok(Self::ExternalFormatterPackageJson { path, parser_name: "json-stringify" });
}

let extension = path.extension().and_then(|ext| ext.to_str());
if let Some(parser_name) = get_external_parser_name(file_name, extension) {
return Ok(Self::ExternalFormatter { path, parser_name });
}

Err(())
}
}

impl FormatFileStrategy {
#[cfg(not(feature = "napi"))]
pub fn can_format_without_external(&self) -> bool {
matches!(self, Self::OxcFormatter { .. })
}

pub fn path(&self) -> &Path {
match self {
Self::OxcFormatter { path, .. }
Expand All @@ -50,132 +73,118 @@ impl FormatFileStrategy {
}
}

static EXCLUDE_FILENAMES: phf::Set<&'static str> = phf_set! {
// JSON, YAML lock files
"package-lock.json",
"pnpm-lock.yaml",
"yarn.lock",
"MODULE.bazel.lock",
"bun.lock",
"deno.lock",
"composer.lock",
"Package.resolved",
"Pipfile.lock",
"flake.lock",
"mcmod.info",
};

// ---

/// Returns `FormatFileSource` for external formatter, if supported.
/// Returns parser name for external formatter, if supported.
/// NOTE: `package.json` is handled separately in `TryFrom`.
/// 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_format_source(path: PathBuf) -> Option<FormatFileStrategy> {
let file_name = path.file_name()?.to_str()?;
let extension = path.extension().and_then(|ext| ext.to_str());

// Excluded files like lock files
if EXCLUDE_FILENAMES.contains(file_name) {
return None;
}

fn get_external_parser_name(file_name: &str, extension: Option<&str>) -> Option<&'static str> {
// JSON and variants
// `package.json` is special case
if file_name == "package.json" {
return Some(FormatFileStrategy::ExternalFormatterPackageJson {
path,
parser_name: "json-stringify",
});
}
if JSON_STRINGIFY_FILENAMES.contains(file_name) || extension == Some("importmap") {
return Some(FormatFileStrategy::ExternalFormatter { path, parser_name: "json-stringify" });
return Some("json-stringify");
}
if JSON_FILENAMES.contains(file_name) {
return Some(FormatFileStrategy::ExternalFormatter { path, parser_name: "json" });
return Some("json");
}
if let Some(ext) = extension
&& JSON_EXTENSIONS.contains(ext)
{
return Some(FormatFileStrategy::ExternalFormatter { path, parser_name: "json" });
return Some("json");
}
if let Some(ext) = extension
&& JSONC_EXTENSIONS.contains(ext)
{
return Some(FormatFileStrategy::ExternalFormatter { path, parser_name: "jsonc" });
return Some("jsonc");
}
if extension == Some("json5") {
return Some(FormatFileStrategy::ExternalFormatter { path, parser_name: "json5" });
return Some("json5");
}

// YAML
if YAML_FILENAMES.contains(file_name) {
return Some(FormatFileStrategy::ExternalFormatter { path, parser_name: "yaml" });
return Some("yaml");
}
if let Some(ext) = extension
&& YAML_EXTENSIONS.contains(ext)
{
return Some(FormatFileStrategy::ExternalFormatter { path, parser_name: "yaml" });
return Some("yaml");
}

// Markdown and variants
if MARKDOWN_FILENAMES.contains(file_name) {
return Some(FormatFileStrategy::ExternalFormatter { path, parser_name: "markdown" });
return Some("markdown");
}
if let Some(ext) = extension
&& MARKDOWN_EXTENSIONS.contains(ext)
{
return Some(FormatFileStrategy::ExternalFormatter { path, parser_name: "markdown" });
return Some("markdown");
}
if extension == Some("mdx") {
return Some(FormatFileStrategy::ExternalFormatter { path, parser_name: "mdx" });
return Some("mdx");
}

// HTML and variants
// Must be checked before generic HTML
if file_name.ends_with(".component.html") {
return Some(FormatFileStrategy::ExternalFormatter { path, parser_name: "angular" });
return Some("angular");
}
if let Some(ext) = extension
&& HTML_EXTENSIONS.contains(ext)
{
return Some(FormatFileStrategy::ExternalFormatter { path, parser_name: "html" });
return Some("html");
}
if extension == Some("vue") {
return Some(FormatFileStrategy::ExternalFormatter { path, parser_name: "vue" });
return Some("vue");
}
if extension == Some("mjml") {
return Some(FormatFileStrategy::ExternalFormatter { path, parser_name: "mjml" });
return Some("mjml");
}

// CSS and variants
if let Some(ext) = extension
&& CSS_EXTENSIONS.contains(ext)
{
return Some(FormatFileStrategy::ExternalFormatter { path, parser_name: "css" });
return Some("css");
}
if extension == Some("less") {
return Some(FormatFileStrategy::ExternalFormatter { path, parser_name: "less" });
return Some("less");
}
if extension == Some("scss") {
return Some(FormatFileStrategy::ExternalFormatter { path, parser_name: "scss" });
return Some("scss");
}

// GraphQL
if let Some(ext) = extension
&& GRAPHQL_EXTENSIONS.contains(ext)
{
return Some(FormatFileStrategy::ExternalFormatter { path, parser_name: "graphql" });
return Some("graphql");
}

// Handlebars
if let Some(ext) = extension
&& HANDLEBARS_EXTENSIONS.contains(ext)
{
return Some(FormatFileStrategy::ExternalFormatter { path, parser_name: "glimmer" });
return Some("glimmer");
}

None
}

static EXCLUDE_FILENAMES: phf::Set<&'static str> = phf_set! {
"package-lock.json",
"pnpm-lock.yaml",
"yarn.lock",
"MODULE.bazel.lock",
"bun.lock",
"deno.lock",
"composer.lock",
"Package.resolved",
"Pipfile.lock",
"flake.lock",
"mcmod.info",
};

static JSON_STRINGIFY_FILENAMES: phf::Set<&'static str> = phf_set! {
// NOTE: `package.json` is handled separately as `ExternalFormatterPackageJson`
"composer.json",
Expand Down Expand Up @@ -319,20 +328,15 @@ mod tests {
use super::*;

fn get_parser_name(file_name: &str) -> Option<&'static str> {
match get_external_format_source(PathBuf::from(file_name)) {
Some(
FormatFileStrategy::ExternalFormatter { parser_name, .. }
| FormatFileStrategy::ExternalFormatterPackageJson { parser_name, .. },
) => Some(parser_name),
_ => None,
}
let path = Path::new(file_name);
let extension = path.extension().and_then(|ext| ext.to_str());
get_external_parser_name(file_name, extension)
}

#[test]
fn test_get_external_format_source() {
fn test_get_external_parser_name() {
let test_cases = vec![
// JSON
("package.json", Some("json-stringify")),
// JSON (NOTE: `package.json` is handled in TryFrom, not here)
("config.importmap", Some("json-stringify")),
("data.json", Some("json")),
("schema.avsc", Some("json")),
Expand Down Expand Up @@ -375,10 +379,6 @@ mod tests {
("config.yml", Some("yaml")),
("settings.yaml", Some("yaml")),
("grammar.sublime-syntax", Some("yaml")),
// Excluded lock files
("package-lock.json", None),
("pnpm-lock.yaml", None),
("yarn.lock", None),
// Unknown
("unknown.txt", None),
("prof.png", None),
Expand All @@ -393,10 +393,10 @@ mod tests {

#[test]
fn test_package_json_is_special() {
let source = get_external_format_source(PathBuf::from("package.json")).unwrap();
let source = FormatFileStrategy::try_from(PathBuf::from("package.json")).unwrap();
assert!(matches!(source, FormatFileStrategy::ExternalFormatterPackageJson { .. }));

let source = get_external_format_source(PathBuf::from("composer.json")).unwrap();
let source = FormatFileStrategy::try_from(PathBuf::from("composer.json")).unwrap();
assert!(matches!(source, FormatFileStrategy::ExternalFormatter { .. }));
}
}
Loading