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
6 changes: 3 additions & 3 deletions apps/oxfmt/src-js/bindings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export declare const enum Severity {
*
* Since it internally uses `await prettier.format()` in JS side, `formatSync()` cannot be provided.
*/
export declare function format(filename: string, sourceText: string, options: any | undefined | null, setupConfigCb: (configJSON: string, numThreads: number) => Promise<string[]>, formatEmbeddedCb: (tagName: string, code: string) => Promise<string>, formatFileCb: (parserName: string, fileName: string, code: string) => Promise<string>): Promise<FormatResult>
export declare function format(filename: string, sourceText: string, options: any | undefined | null, initExternalFormatterCb: (numThreads: number) => Promise<string[]>, formatEmbeddedCb: (options: Record<string, any>, tagName: string, code: string) => Promise<string>, formatFileCb: (options: Record<string, any>, parserName: string, fileName: string, code: string) => Promise<string>): Promise<FormatResult>

export interface FormatResult {
/** The formatted code. */
Expand All @@ -46,12 +46,12 @@ export interface FormatResult {
*
* JS side passes in:
* 1. `args`: Command line arguments (process.argv.slice(2))
* 2. `setup_config_cb`: Callback to setup Prettier config
* 2. `init_external_formatter_cb`: Callback to initialize external formatter
* 3. `format_embedded_cb`: Callback to format embedded code in templates
* 4. `format_file_cb`: Callback to format files
*
* Returns a tuple of `[mode, exitCode]`:
* - `mode`: If main logic will run in JS side, use this to indicate which mode
* - `exitCode`: If main logic already ran in Rust side, return the exit code
*/
export declare function runCli(args: Array<string>, setupConfigCb: (configJSON: string, numThreads: number) => Promise<string[]>, formatEmbeddedCb: (tagName: string, code: string) => Promise<string>, formatFileCb: (parserName: string, fileName: string, code: string) => Promise<string>): Promise<[string, number | undefined | null]>
export declare function runCli(args: Array<string>, initExternalFormatterCb: (numThreads: number) => Promise<string[]>, formatEmbeddedCb: (options: Record<string, any>, tagName: string, code: string) => Promise<string>, formatFileCb: (options: Record<string, any>, parserName: string, fileName: string, code: string) => Promise<string>): Promise<[string, number | undefined | null]>
9 changes: 7 additions & 2 deletions apps/oxfmt/src-js/cli.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { runCli } from "./bindings.js";
import { setupConfig, formatEmbeddedCode, formatFile } from "./prettier-proxy.js";
import { initExternalFormatter, formatEmbeddedCode, formatFile } from "./prettier-proxy.js";
import { runInit, runMigratePrettier } from "./migration/index.js";

void (async () => {
const args = process.argv.slice(2);

// Call the Rust CLI to parse args and determine mode
// NOTE: If the mode is formatter CLI, it will also perform formatting and return an exit code
const [mode, exitCode] = await runCli(args, setupConfig, formatEmbeddedCode, formatFile);
const [mode, exitCode] = await runCli(
args,
initExternalFormatter,
formatEmbeddedCode,
formatFile,
);

switch (mode) {
// Handle `--init` and `--migrate` command in JS
Expand Down
4 changes: 2 additions & 2 deletions apps/oxfmt/src-js/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { format as napiFormat } from "./bindings.js";
import { setupConfig, formatEmbeddedCode, formatFile } from "./prettier-proxy.js";
import { initExternalFormatter, formatEmbeddedCode, formatFile } from "./prettier-proxy.js";

export async function format(fileName: string, sourceText: string, options?: FormatOptions) {
if (typeof fileName !== "string") throw new TypeError("`fileName` must be a string");
Expand All @@ -9,7 +9,7 @@ export async function format(fileName: string, sourceText: string, options?: For
fileName,
sourceText,
options ?? {},
setupConfig,
initExternalFormatter,
formatEmbeddedCode,
formatFile,
);
Expand Down
45 changes: 25 additions & 20 deletions apps/oxfmt/src-js/prettier-proxy.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,42 @@
import Tinypool from "tinypool";
import type { WorkerData, FormatEmbeddedCodeArgs, FormatFileArgs } from "./prettier-worker.ts";
import type { Options } from "prettier";
import type { FormatEmbeddedCodeArgs, FormatFileArgs } from "./prettier-worker.ts";

// Worker pool for parallel Prettier formatting
// Used by each exported function
let pool: Tinypool | null = null;

type SetupResult = string[];
let setupCache: SetupResult | null = null;
type InitResult = string[];
let initResultCache: InitResult | null = null;

// ---

/**
* Setup Prettier configuration.
* Setup worker pool for Prettier formatting.
* NOTE: Called from Rust via NAPI ThreadsafeFunction with FnArgs
* @param configJSON - Prettier configuration as JSON string
* @param numThreads - Number of worker threads to use (same as Rayon thread count)
* @returns Array of loaded plugin's `languages` info
* */
export async function setupConfig(configJSON: string, numThreads: number): Promise<SetupResult> {
*/
export async function initExternalFormatter(numThreads: number): Promise<InitResult> {
// NOTE: When called from CLI, it's only called once at the beginning.
// However, when called via API, like `format(fileName, code)`, it may be called multiple times.
// Therefore, allow it by returning cached result.
if (setupCache !== null) return setupCache;

const workerData: WorkerData = {
// SAFETY: Always valid JSON constructed by Rust side
prettierConfig: JSON.parse(configJSON),
};
if (initResultCache !== null) return initResultCache;

// Initialize worker pool for parallel Prettier formatting
// Pass config via workerData so all workers get it on initialization
pool = new Tinypool({
filename: new URL("./prettier-worker.js", import.meta.url).href,
minThreads: numThreads,
maxThreads: numThreads,
workerData,
});

// TODO: Plugins support
// - Read `plugins` field
// - Load plugins dynamically and parse `languages` field
// - Map file extensions and filenames to Prettier parsers
setupCache = [];
initResultCache = [];

return setupCache;
return initResultCache;
}

// ---
Expand All @@ -66,19 +59,26 @@ const TAG_TO_PARSER: Record<string, string> = {
/**
* Format embedded code using Prettier.
* NOTE: Called from Rust via NAPI ThreadsafeFunction with FnArgs
* @param options - Prettier configuration as object
* @param tagName - The template tag name (e.g., "css", "gql", "html")
* @param code - The code to format
* @returns Formatted code
*/
export async function formatEmbeddedCode(tagName: string, code: string): Promise<string> {
export async function formatEmbeddedCode(
options: Options,
tagName: string,
code: string,
): Promise<string> {
const parser = TAG_TO_PARSER[tagName];

// Unknown tag, return original code
if (!parser) {
return code;
}

return pool!.run({ parser, code } satisfies FormatEmbeddedCodeArgs, {
options.parser = parser;

return pool!.run({ options, code } satisfies FormatEmbeddedCodeArgs, {
name: "formatEmbeddedCode",
});
}
Expand All @@ -88,17 +88,22 @@ 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 options - Prettier configuration as object
* @param parserName - The parser name
* @param fileName - The file name (e.g., "package.json")
* @param code - The code to format
* @returns Formatted code
*/
export async function formatFile(
options: Options,
parserName: string,
fileName: string,
code: string,
): Promise<string> {
return pool!.run({ parserName, fileName, code } satisfies FormatFileArgs, {
options.parser = parserName;
options.filepath = fileName;

return pool!.run({ options, code } satisfies FormatFileArgs, {
name: "formatFile",
});
}
29 changes: 6 additions & 23 deletions apps/oxfmt/src-js/prettier-worker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { workerData } from "node:worker_threads";
import type { Options } from "prettier";

// Lazy load Prettier in each worker thread
Expand All @@ -8,54 +7,38 @@ import type { Options } from "prettier";
// Nevertheless, we will keep it as lazy loading just in case.
let prettierCache: typeof import("prettier");

export type WorkerData = {
prettierConfig: Options;
};

// Initialize config from `workerData` (passed during pool creation)
// NOTE: The 1st element is thread id, passed by `tinypool`
const [, { prettierConfig }] = workerData satisfies [unknown, WorkerData];

// ---

export type FormatEmbeddedCodeArgs = {
parser: string;
code: string;
options: Options;
};

export async function formatEmbeddedCode({
parser,
options,
code,
}: FormatEmbeddedCodeArgs): Promise<string> {
if (!prettierCache) {
prettierCache = await import("prettier");
}

return prettierCache
.format(code, {
...prettierConfig,
parser,
})
.format(code, options)
.then((formatted) => formatted.trimEnd())
.catch(() => code);
}

// ---

export type FormatFileArgs = {
parserName: string;
fileName: string;
code: string;
options: Options;
};

export async function formatFile({ parserName, fileName, code }: FormatFileArgs): Promise<string> {
export async function formatFile({ options, code }: FormatFileArgs): Promise<string> {
if (!prettierCache) {
prettierCache = await import("prettier");
}

return prettierCache.format(code, {
...prettierConfig,
parser: parserName,
filepath: fileName,
});
return prettierCache.format(code, options);
}
73 changes: 38 additions & 35 deletions apps/oxfmt/src/cli/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use super::{
service::{FormatService, SuccessResult},
walk::Walk,
};
use crate::core::{SourceFormatter, load_config, resolve_config_path, utils};
use crate::core::{ConfigResolver, SourceFormatter, resolve_config_path, utils};

#[derive(Debug)]
pub struct FormatRunner {
Expand Down Expand Up @@ -63,54 +63,57 @@ impl FormatRunner {
};
let num_of_threads = rayon::current_num_threads();

// Find config file
// Find and load config file
// 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_path = resolve_config_path(&cwd, config_options.config.as_deref());
// Load and parse config file
// - `format_options`: Parsed formatting options used by `oxc_formatter`
// - `external_config`: JSON value used by `external_formatter`, populated with `format_options`
let (format_options, oxfmt_options, external_config) =
match load_config(config_path.as_deref()) {
Ok(c) => c,
Err(err) => {
utils::print_and_flush(
stderr,
&format!("Failed to load configuration file.\n{err}\n"),
);
return CliRunResult::InvalidOptionConfig;
}
};
let mut config_resolver = match ConfigResolver::from_config_path(config_path.as_deref()) {
Ok(r) => r,
Err(err) => {
utils::print_and_flush(
stderr,
&format!("Failed to load configuration file.\n{err}\n"),
);
return CliRunResult::InvalidOptionConfig;
}
};
let ignore_patterns = match config_resolver.build_and_validate() {
Ok(patterns) => patterns,
Err(err) => {
utils::print_and_flush(stderr, &format!("Failed to parse configuration.\n{err}\n"));
return CliRunResult::InvalidOptionConfig;
}
};

// TODO: Plugins support
// - Parse returned `languages`
// - Allow its `extensions` and `filenames` in `walk.rs`
// - Pass `parser` to `SourceFormatter`
// Use `block_in_place()` to avoid nested async runtime access
#[cfg(feature = "napi")]
if let Err(err) = tokio::task::block_in_place(|| {
match tokio::task::block_in_place(|| {
self.external_formatter
.as_ref()
.expect("External formatter must be set when `napi` feature is enabled")
.setup_config(&external_config.to_string(), num_of_threads)
.init(num_of_threads)
}) {
utils::print_and_flush(
stderr,
&format!("Failed to setup external formatter config.\n{err}\n"),
);
return CliRunResult::InvalidOptionConfig;
// TODO: Plugins support
// - Parse returned `languages`
// - Allow its `extensions` and `filenames` in `walk.rs`
// - Pass `parser` to `SourceFormatter`
Ok(_) => {}
Err(err) => {
utils::print_and_flush(
stderr,
&format!("Failed to setup external formatter.\n{err}\n"),
);
return CliRunResult::InvalidOptionConfig;
}
}

#[cfg(not(feature = "napi"))]
let _ = (external_config, oxfmt_options.sort_package_json);

let walker = match Walk::build(
&cwd,
&paths,
&ignore_options.ignore_path,
ignore_options.with_node_modules,
&oxfmt_options.ignore_patterns,
&ignore_patterns,
) {
Ok(Some(walker)) => walker,
// All target paths are ignored
Expand Down Expand Up @@ -145,16 +148,16 @@ impl FormatRunner {
}

// Create `SourceFormatter` instance
let source_formatter = SourceFormatter::new(num_of_threads, format_options);
let source_formatter = SourceFormatter::new(num_of_threads);
#[cfg(feature = "napi")]
let source_formatter = source_formatter
.with_external_formatter(self.external_formatter, oxfmt_options.sort_package_json);
let source_formatter = source_formatter.with_external_formatter(self.external_formatter);

let format_mode_clone = format_mode.clone();

// Spawn a thread to run formatting service with streaming entries
rayon::spawn(move || {
let format_service = FormatService::new(cwd, format_mode_clone, source_formatter);
let format_service =
FormatService::new(cwd, format_mode_clone, source_formatter, config_resolver);
format_service.run_streaming(rx_entry, &tx_error, &tx_success);
});

Expand Down
Loading
Loading