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
2 changes: 1 addition & 1 deletion apps/oxfmt/src-js/bindings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@
*
* Returns `true` if formatting succeeded without errors, `false` otherwise.
*/
export declare function format(args: Array<string>, formatEmbeddedCb: (tagName: string, code: string) => Promise<string>, formatFileCb: (fileName: string, code: string) => Promise<string>): Promise<boolean>
export declare function format(args: Array<string>, formatEmbeddedCb: (tagName: string, code: string) => Promise<string>, formatFileCb: (parserName: string, code: string) => Promise<string>): Promise<boolean>
9 changes: 3 additions & 6 deletions apps/oxfmt/src-js/prettier-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,20 +66,17 @@ export async function formatEmbeddedCode(tagName: string, code: string): Promise
/**
* Format whole file content using Prettier.
* NOTE: Called from Rust via NAPI ThreadsafeFunction with FnArgs
* @param fileName - The file name (used to infer parser)
* @param parserName - The parser name
* @param code - The code to format
* @returns Formatted code
*/
export async function formatFile(fileName: string, code: string): Promise<string> {
export async function formatFile(parserName: string, code: string): Promise<string> {
if (!prettierCache) {
prettierCache = await import("prettier");
}

// TODO: Tweak parser for `tsconfig.json` with `jsonc` parser?

return prettierCache.format(code, {
// Let Prettier infer the parser
filepath: fileName,
parser: parserName,
// TODO: Read config
});
}
6 changes: 3 additions & 3 deletions apps/oxfmt/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ impl FormatRunner {
// NOTE: Currently, we only load single config file.
// - from `--config` if specified
// - else, search nearest for the nearest `.oxfmtrc.json` from cwd upwards
let config = match load_config(&cwd, basic_options.config.as_ref()) {
let config = match load_config(&cwd, basic_options.config.as_deref()) {
Ok(config) => config,
Err(err) => {
print_and_flush(stderr, &format!("Failed to load configuration file.\n{err}\n"));
Expand Down Expand Up @@ -220,11 +220,11 @@ impl FormatRunner {
/// Returns error if:
/// - Config file is specified but not found or invalid
/// - Config file parsing fails
fn load_config(cwd: &Path, config_path: Option<&PathBuf>) -> Result<Oxfmtrc, String> {
fn load_config(cwd: &Path, config_path: Option<&Path>) -> Result<Oxfmtrc, String> {
let config_path = if let Some(config_path) = config_path {
// If `--config` is explicitly specified, use that path
Some(if config_path.is_absolute() {
PathBuf::from(config_path)
config_path.to_path_buf()
} else {
cwd.join(config_path)
})
Expand Down
1 change: 1 addition & 0 deletions apps/oxfmt/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod lsp;
mod reporter;
mod result;
mod service;
mod support;
mod walk;

// Public re-exports for use in main.rs and lib consumers
Expand Down
2 changes: 1 addition & 1 deletion apps/oxfmt/src/main_napi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub async fn format(
args: Vec<String>,
#[napi(ts_arg_type = "(tagName: string, code: string) => Promise<string>")]
format_embedded_cb: JsFormatEmbeddedCb,
#[napi(ts_arg_type = "(fileName: string, code: string) => Promise<string>")]
#[napi(ts_arg_type = "(parserName: string, code: string) => Promise<string>")]
format_file_cb: JsFormatFileCb,
) -> bool {
format_impl(args, format_embedded_cb, format_file_cb).await.report() == ExitCode::SUCCESS
Expand Down
22 changes: 11 additions & 11 deletions apps/oxfmt/src/prettier_plugins/external_formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ pub type JsFormatEmbeddedCb = ThreadsafeFunction<
>;

/// Type alias for the callback function signature.
/// Takes (tag_name, code) as separate arguments and returns formatted code.
/// Takes (parser_name, code) as separate arguments and returns formatted code.
pub type JsFormatFileCb = ThreadsafeFunction<
// Input arguments
FnArgs<(String, String)>, // (file_name, code) as separate arguments
FnArgs<(String, String)>, // (parser_name, code) as separate arguments
// Return type (what JS function returns)
Promise<String>,
// Arguments (repeated)
Expand All @@ -38,7 +38,7 @@ pub type JsFormatFileCb = ThreadsafeFunction<
>;

/// Callback function type for formatting files.
/// Takes (file_name, code) and returns formatted code or an error.
/// Takes (parser_name, code) and returns formatted code or an error.
type FileFormatterCallback = Arc<dyn Fn(&str, &str) -> Result<String, String> + Send + Sync>;

/// External formatter that wraps a JS callback.
Expand Down Expand Up @@ -73,8 +73,8 @@ impl ExternalFormatter {
}

/// Format non-js file using the JS callback.
pub fn format_file(&self, file_name: &str, code: &str) -> Result<String, String> {
(self.format_file)(file_name, code)
pub fn format_file(&self, parser_name: &str, code: &str) -> Result<String, String> {
(self.format_file)(parser_name, code)
}
}

Expand All @@ -101,19 +101,19 @@ fn wrap_format_embedded(cb: JsFormatEmbeddedCb) -> EmbeddedFormatterCallback {

/// Wrap JS `formatFile` callback as a normal Rust function.
fn wrap_format_file(cb: JsFormatFileCb) -> EmbeddedFormatterCallback {
Arc::new(move |file_name: &str, code: &str| {
Arc::new(move |parser_name: &str, code: &str| {
block_on(async {
let status =
cb.call_async(FnArgs::from((file_name.to_string(), code.to_string()))).await;
cb.call_async(FnArgs::from((parser_name.to_string(), code.to_string()))).await;
match status {
Ok(promise) => match promise.await {
Ok(formatted_code) => Ok(formatted_code),
Err(err) => {
Err(format!("JS formatter promise rejected for file '{file_name}': {err}"))
}
Err(err) => Err(format!(
"JS formatter promise rejected for file '{parser_name}': {err}"
)),
},
Err(err) => Err(format!(
"Failed to call JS formatting callback for file '{file_name}': {err}"
"Failed to call JS formatting callback for file '{parser_name}': {err}"
)),
}
})
Expand Down
80 changes: 41 additions & 39 deletions apps/oxfmt/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ use rayon::prelude::*;

use oxc_allocator::AllocatorPool;
use oxc_diagnostics::{DiagnosticSender, DiagnosticService, Error, OxcDiagnostic};
use oxc_formatter::{
FormatOptions, Formatter, enable_jsx_source_type, get_parse_options, get_supported_source_type,
};
use oxc_formatter::{FormatOptions, Formatter, enable_jsx_source_type, get_parse_options};
use oxc_parser::Parser;
use oxc_span::SourceType;

use crate::{command::OutputOptions, walk::WalkEntry};
use crate::{command::OutputOptions, support::FormatFileSource};

enum FormatFileResult {
Success { is_changed: bool, code: String },
Error(Vec<Error>),
}

pub enum SuccessResult {
Changed(String),
Expand Down Expand Up @@ -60,23 +63,23 @@ impl FormatService {
/// Process entries as they are received from the channel
pub fn run_streaming(
&self,
rx_entry: mpsc::Receiver<WalkEntry>,
rx_entry: mpsc::Receiver<FormatFileSource>,
tx_error: &DiagnosticSender,
tx_success: &mpsc::Sender<SuccessResult>,
) {
rx_entry.into_iter().par_bridge().for_each(|entry| {
let start_time = Instant::now();

let path = &entry.path;
let (code, is_changed) = match self.format_file(&entry) {
Ok(res) => res,
Err(diagnostics) => {
FormatFileResult::Success { is_changed, code } => (code, is_changed),
FormatFileResult::Error(diagnostics) => {
tx_error.send(diagnostics).unwrap();
return;
}
};

let elapsed = start_time.elapsed();
let path = &entry.path();

// Write back if needed
if matches!(self.output_options, OutputOptions::Write) && is_changed {
Expand Down Expand Up @@ -110,11 +113,13 @@ impl FormatService {
});
}

fn format_file(&self, entry: &WalkEntry) -> Result<(String, bool), Vec<Error>> {
let path = &entry.path;
// NOTE: This may be `pub` for LSP usage in the future
fn format_file(&self, entry: &FormatFileSource) -> FormatFileResult {
let path = entry.path();

let Ok(source_text) = read_to_string(path) else {
// This happens if `.ts` for MPEG-TS binary is attempted to be formatted
// This happens if binary file is attempted to be formatted
// e.g. `.ts` for MPEG-TS video file
let diagnostics = DiagnosticService::wrap_diagnostics(
self.cwd.clone(),
path,
Expand All @@ -124,32 +129,30 @@ impl FormatService {
.with_help("This may be due to the file being a binary or inaccessible."),
],
);
return Err(diagnostics);
return FormatFileResult::Error(diagnostics);
};

let code = match get_supported_source_type(path.as_path()) {
Some(source_type) => self.format_by_oxc_formatter(entry, &source_text, source_type)?,
match entry {
FormatFileSource::OxcFormatter { path, source_type } => {
self.format_by_oxc_formatter(&source_text, path, *source_type)
}
#[cfg(feature = "napi")]
None => self.format_by_external_formatter(entry, &source_text)?,
FormatFileSource::ExternalFormatter { path, parser_name } => {
self.format_by_external_formatter(&source_text, path, parser_name)
}
#[cfg(not(feature = "napi"))]
None => unreachable!(
"If `napi` feature is disabled, non-supported entry should not be passed: {}",
path.display()
),
};

let is_changed = source_text != code;

Ok((code, is_changed))
FormatFileSource::ExternalFormatter { .. } => {
unreachable!("If `napi` feature is disabled, this should not be passed here",)
}
}
}

fn format_by_oxc_formatter(
&self,
entry: &WalkEntry,
source_text: &str,
path: &Path,
source_type: SourceType,
) -> Result<String, Vec<Error>> {
let path = &entry.path;
) -> FormatFileResult {
let source_type = enable_jsx_source_type(source_type);

let allocator = self.allocator_pool.get();
Expand All @@ -163,7 +166,7 @@ impl FormatService {
source_text,
ret.errors,
);
return Err(diagnostics);
return FormatFileResult::Error(diagnostics);
}

let base_formatter = Formatter::new(&allocator, self.format_options.clone());
Expand Down Expand Up @@ -196,7 +199,7 @@ impl FormatService {
path.display()
))],
);
return Err(diagnostics);
return FormatFileResult::Error(diagnostics);
}
};

Expand All @@ -208,27 +211,24 @@ impl FormatService {
}
}

Ok(code)
FormatFileResult::Success { is_changed: source_text != code, code }
}

#[cfg(feature = "napi")]
fn format_by_external_formatter(
&self,
entry: &WalkEntry,
source_text: &str,
) -> Result<String, Vec<Error>> {
let path = &entry.path;
let file_name = path.file_name().expect("Path must have a file name").to_string_lossy();

path: &Path,
parser_name: &str,
) -> FormatFileResult {
let code = match self
.external_formatter
.as_ref()
.expect("`external_formatter` must exist when `napi` feature is enabled")
.format_file(&file_name, source_text)
.format_file(parser_name, source_text)
{
Ok(code) => code,
Err(err) => {
// TODO: Need to handle `UndefinedParserError` or not
let diagnostics = DiagnosticService::wrap_diagnostics(
self.cwd.clone(),
path,
Expand All @@ -238,14 +238,16 @@ impl FormatService {
path.display()
))],
);
return Err(diagnostics);
return FormatFileResult::Error(diagnostics);
}
};

Ok(code)
FormatFileResult::Success { is_changed: source_text != code, code }
}
}

// ---

fn read_to_string(path: &Path) -> io::Result<String> {
// `simdutf8` is faster than `std::str::from_utf8` which `fs::read_to_string` uses internally
let bytes = fs::read(path)?;
Expand Down
46 changes: 46 additions & 0 deletions apps/oxfmt/src/support.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use std::path::{Path, PathBuf};

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

pub enum FormatFileSource {
OxcFormatter {
path: PathBuf,
source_type: SourceType,
},
#[expect(dead_code)]
ExternalFormatter {
path: PathBuf,
parser_name: String,
},
}

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

fn try_from(path: &Path) -> 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 });
}

// TODO: Support more files with `ExternalFormatter`
// - JSON
// - HTML(include .vue)
// - CSS
// - GraphQL
// - Markdown
// - YAML
// - Handlebars

Err(())
}
}

impl FormatFileSource {
pub fn path(&self) -> &Path {
match self {
Self::OxcFormatter { path, .. } | Self::ExternalFormatter { path, .. } => path,
}
}
}
Loading
Loading