Skip to content
Closed
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
3 changes: 2 additions & 1 deletion apps/oxfmt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"node": "^20.19.0 || >=22.12.0"
},
"dependencies": {
"prettier": "3.7.4"
"prettier": "3.7.4",
"tiny-jsonc": "1.0.2"
},
"devDependencies": {
"@types/node": "catalog:",
Expand Down
19 changes: 17 additions & 2 deletions apps/oxfmt/src-js/prettier-proxy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import JSONC from "tiny-jsonc";
import { readFile } from "node:fs/promises";

// Import Prettier lazily.
// This helps to reduce initial load time if not needed.
//
Expand All @@ -11,6 +14,8 @@
// But actually, this makes `oxfmt --lsp` immediately stop with `Parse error` JSON-RPC error
let prettierCache: typeof import("prettier");

let configCache: any = {};

// ---

// Map template tag names to Prettier parsers
Expand Down Expand Up @@ -68,15 +73,25 @@ export async function formatEmbeddedCode(tagName: string, code: string): Promise
* NOTE: Called from Rust via NAPI ThreadsafeFunction with FnArgs
* @param parserName - The parser name
* @param code - The code to format
* @param [configPath] - Optional Prettier config path
* @returns Formatted code
*/
export async function formatFile(parserName: string, code: string): Promise<string> {
export async function formatFile(
parserName: string,
code: string,
configPath?: string,
): Promise<string> {
if (!prettierCache) {
prettierCache = await import("prettier");
}
if (configPath && !configCache) {
const jsonOrJsoncString = await readFile(configPath, "utf-8");
// SAFETY: Config file already validated by Rust side
configCache = JSONC.parse(jsonOrJsoncString);
}

return prettierCache.format(code, {
...configCache,
parser: parserName,
// TODO: Read config
});
}
17 changes: 11 additions & 6 deletions apps/oxfmt/src/cli/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ 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_deref()) {
Ok(config) => config,
#[cfg_attr(not(feature = "napi"), allow(unused_variables))]
let (config, config_path) = match load_config(&cwd, basic_options.config.as_deref()) {
Ok(result) => result,
Err(err) => {
print_and_flush(stderr, &format!("Failed to load configuration file.\n{err}\n"));
return CliRunResult::InvalidOptionConfig;
Expand Down Expand Up @@ -113,7 +114,8 @@ impl FormatRunner {
// Create `SourceFormatter` instance
let source_formatter = SourceFormatter::new(num_of_threads, format_options);
#[cfg(feature = "napi")]
let source_formatter = source_formatter.with_external_formatter(self.external_formatter);
let source_formatter =
source_formatter.with_external_formatter(self.external_formatter, config_path);

let output_options_clone = output_options.clone();

Expand Down Expand Up @@ -217,7 +219,10 @@ 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<&Path>) -> Result<Oxfmtrc, String> {
fn load_config(
cwd: &Path,
config_path: Option<&Path>,
) -> Result<(Oxfmtrc, Option<PathBuf>), 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() {
Expand All @@ -240,9 +245,9 @@ fn load_config(cwd: &Path, config_path: Option<&Path>) -> Result<Oxfmtrc, String
};

match config_path {
Some(ref path) => Oxfmtrc::from_file(path),
Some(path) => Oxfmtrc::from_file(&path).map(|config| (config, Some(path))),
// Default if not specified and not found
None => Ok(Oxfmtrc::default()),
None => Ok((Oxfmtrc::default(), None)),
}
}

Expand Down
35 changes: 24 additions & 11 deletions apps/oxfmt/src/core/external_formatter.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::path::Path;
use std::sync::Arc;

use napi::{
Expand All @@ -23,23 +24,24 @@ pub type JsFormatEmbeddedCb = ThreadsafeFunction<
>;

/// Type alias for the callback function signature.
/// Takes (parser_name, code) as separate arguments and returns formatted code.
/// Takes (parser_name, code, config_path) as separate arguments and returns formatted code.
pub type JsFormatFileCb = ThreadsafeFunction<
// Input arguments
FnArgs<(String, String)>, // (parser_name, code) as separate arguments
FnArgs<(String, String, Option<String>)>, // (parser_name, code, config_path)
// Return type (what JS function returns)
Promise<String>,
// Arguments (repeated)
FnArgs<(String, String)>,
FnArgs<(String, String, Option<String>)>,
// Error status
Status,
// CalleeHandled
false,
>;

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

/// External formatter that wraps a JS callback.
#[derive(Clone)]
Expand Down Expand Up @@ -73,8 +75,14 @@ impl ExternalFormatter {
}

/// Format non-js file using the JS callback.
pub fn format_file(&self, parser_name: &str, code: &str) -> Result<String, String> {
(self.format_file)(parser_name, code)
pub fn format_file(
&self,
parser_name: &str,
code: &str,
config_path: Option<&Path>,
) -> Result<String, String> {
let config_path_str = config_path.map(|p| p.to_string_lossy().into_owned());
(self.format_file)(parser_name, code, config_path_str.as_deref())
}
}

Expand All @@ -100,11 +108,16 @@ 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 |parser_name: &str, code: &str| {
fn wrap_format_file(cb: JsFormatFileCb) -> FileFormatterCallback {
Arc::new(move |parser_name: &str, code: &str, config_path: Option<&str>| {
block_on(async {
let status =
cb.call_async(FnArgs::from((parser_name.to_string(), code.to_string()))).await;
let status = cb
.call_async(FnArgs::from((
parser_name.to_string(),
code.to_string(),
config_path.map(String::from),
)))
.await;
match status {
Ok(promise) => match promise.await {
Ok(formatted_code) => Ok(formatted_code),
Expand Down
11 changes: 10 additions & 1 deletion apps/oxfmt/src/core/format.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use std::path::Path;
#[cfg(feature = "napi")]
use std::path::PathBuf;

use oxc_allocator::AllocatorPool;
use oxc_diagnostics::OxcDiagnostic;
Expand All @@ -17,6 +19,8 @@ pub struct SourceFormatter {
allocator_pool: AllocatorPool,
format_options: FormatOptions,
#[cfg(feature = "napi")]
config_path: Option<PathBuf>,
#[cfg(feature = "napi")]
external_formatter: Option<super::ExternalFormatter>,
}

Expand All @@ -26,6 +30,8 @@ impl SourceFormatter {
allocator_pool: AllocatorPool::new(num_of_threads),
format_options,
#[cfg(feature = "napi")]
config_path: None,
#[cfg(feature = "napi")]
external_formatter: None,
}
}
Expand All @@ -35,8 +41,10 @@ impl SourceFormatter {
pub fn with_external_formatter(
mut self,
external_formatter: Option<super::ExternalFormatter>,
config_path: Option<PathBuf>,
) -> Self {
self.external_formatter = external_formatter;
self.config_path = config_path;
self
}

Expand Down Expand Up @@ -126,7 +134,8 @@ impl SourceFormatter {
.as_ref()
.expect("`external_formatter` must exist when `napi` feature is enabled");

match external_formatter.format_file(parser_name, source_text) {
match external_formatter.format_file(parser_name, source_text, self.config_path.as_deref())
{
Ok(code) => FormatResult::Success { is_changed: source_text != code, code },
Err(err) => FormatResult::Error(vec![OxcDiagnostic::error(format!(
"Failed to format file with external formatter: {}\n{err}",
Expand Down
11 changes: 9 additions & 2 deletions apps/oxfmt/src/core/support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@ use oxc_formatter::get_supported_source_type;
use oxc_span::SourceType;

pub enum FormatFileSource {
OxcFormatter { path: PathBuf, source_type: SourceType },
ExternalFormatter { path: PathBuf, parser_name: &'static str },
OxcFormatter {
path: PathBuf,
source_type: SourceType,
},
#[cfg_attr(not(feature = "napi"), allow(dead_code))]
ExternalFormatter {
path: PathBuf,
parser_name: &'static str,
},
}

impl TryFrom<PathBuf> for FormatFileSource {
Expand Down
3 changes: 2 additions & 1 deletion apps/oxfmt/tsdown.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineConfig } from "tsdown";
import pkg from "./package.json" with { type: "json" };

export default defineConfig({
entry: ["src-js/index.ts", "src-js/cli.ts"],
Expand All @@ -10,5 +11,5 @@ export default defineConfig({
outDir: "dist",
shims: false,
fixedExtension: false,
noExternal: ["prettier"],
noExternal: Object.keys(pkg.dependencies),
});
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading