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
4 changes: 4 additions & 0 deletions Cargo.lock

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

3 changes: 2 additions & 1 deletion apps/oxfmt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ oxc_allocator = { workspace = true, features = ["pool"] }
oxc_diagnostics = { workspace = true }
oxc_formatter = { workspace = true }
oxc_language_server = { workspace = true, default-features = false, features = ["formatter"] }
oxc_napi = { workspace = true }
oxc_parser = { workspace = true }
oxc_span = { workspace = true }

Expand Down Expand Up @@ -66,7 +67,7 @@ mimalloc-safe = { workspace = true, optional = true, features = ["skip_collect_o

[features]
default = ["napi"]
napi = ["dep:napi", "dep:napi-derive"]
napi = ["dep:napi", "dep:napi-derive", "oxc_formatter/napi"]
allocator = ["dep:mimalloc-safe"]
detect_code_removal = ["oxc_formatter/detect_code_removal"]

Expand Down
148 changes: 147 additions & 1 deletion apps/oxfmt/src-js/bindings.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,153 @@
/* auto-generated by NAPI-RS */
/* eslint-disable */
export declare const enum ArrowParensConfig {
Always = 'Always',
Avoid = 'Avoid'
}

export declare const enum EmbeddedLanguageFormattingConfig {
Auto = 'Auto',
Off = 'Off'
}

export declare const enum EndOfLineConfig {
Lf = 'Lf',
Crlf = 'Crlf',
Cr = 'Cr'
}

export declare const enum ObjectWrapConfig {
Preserve = 'Preserve',
Collapse = 'Collapse',
Always = 'Always'
}

/**
* NAPI entry point.
* Configuration options for the Oxfmt.
*
* Most options are the same as Prettier's options.
* See also <https://prettier.io/docs/options>
*
* In addition, some options are our own extensions.
*/
export interface Oxfmtrc {
/** Use tabs for indentation or spaces. (Default: `false`) */
useTabs?: boolean
/** Number of spaces per indentation level. (Default: `2`) */
tabWidth?: number
/** Which end of line characters to apply. (Default: `"lf"`) */
endOfLine?: EndOfLineConfig
/** The line length that the printer will wrap on. (Default: `100`) */
printWidth?: number
/** Use single quotes instead of double quotes. (Default: `false`) */
singleQuote?: boolean
/** Use single quotes instead of double quotes in JSX. (Default: `false`) */
jsxSingleQuote?: boolean
/** Change when properties in objects are quoted. (Default: `"as-needed"`) */
quoteProps?: QuotePropsConfig
/** Print trailing commas wherever possible. (Default: `"all"`) */
trailingComma?: TrailingCommaConfig
/** Print semicolons at the ends of statements. (Default: `true`) */
semi?: boolean
/** Include parentheses around a sole arrow function parameter. (Default: `"always"`) */
arrowParens?: ArrowParensConfig
/** Print spaces between brackets in object literals. (Default: `true`) */
bracketSpacing?: boolean
/**
* Put the `>` of a multi-line JSX element at the end of the last line
* instead of being alone on the next line. (Default: `false`)
*/
bracketSameLine?: boolean
/**
* How to wrap object literals when they could fit on one line or span multiple lines. (Default: `"preserve"`)
* NOTE: In addition to Prettier's `"preserve"` and `"collapse"`, we also support `"always"`.
*/
objectWrap?: ObjectWrapConfig
/** Put each attribute on a new line in JSX. (Default: `false`) */
singleAttributePerLine?: boolean
experimentalOperatorPosition?: boolean
experimentalTernaries?: boolean
/** Control whether formats quoted code embedded in the file. (Default: `"auto"`) */
embeddedLanguageFormatting?: EmbeddedLanguageFormattingConfig
/** Experimental: Sort import statements. Disabled by default. */
experimentalSortImports?: SortImportsConfig
/** Experimental: Sort `package.json` keys. (Default: `true`) */
experimentalSortPackageJson?: boolean
/** Ignore files matching these glob patterns. Current working directory is used as the root. */
ignorePatterns?: Array<string>
}

export declare const enum QuotePropsConfig {
AsNeeded = 'AsNeeded',
Consistent = 'Consistent',
Preserve = 'Preserve'
}

export interface SortImportsConfig {
partitionByNewline: boolean
partitionByComment: boolean
sortSideEffects: boolean
order?: SortOrderConfig
ignoreCase: boolean
newlinesBetween: boolean
internalPattern?: Array<string>
/**
* Custom groups configuration for organizing imports.
* Each array element represents a group, and multiple group names in the same array are treated as one.
* Accepts both `string` and `string[]` as group elements.
*/
groups?: Array<Array<string>>
}

export declare const enum SortOrderConfig {
Asc = 'Asc',
Desc = 'Desc'
}

export declare const enum TrailingCommaConfig {
All = 'All',
Es5 = 'Es5',
None = 'None'
}
export interface Comment {
type: 'Line' | 'Block'
value: string
start: number
end: number
}

export interface ErrorLabel {
message: string | null
start: number
end: number
}

export interface OxcError {
severity: Severity
message: string
labels: Array<ErrorLabel>
helpMessage: string | null
codeframe: string | null
}

export declare const enum Severity {
Error = 'Error',
Warning = 'Warning',
Advice = 'Advice'
}
/** Format source code asynchronously. */
export declare function format(filename: string, sourceText: string, options: Oxfmtrc | 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 interface FormatResult {
/** The formatted code. */
code: string
/** Parse and format errors. */
errors: Array<OxcError>
}

/**
* NAPI based JS CLI entry point.
* For pure Rust CLI entry point, see `main.rs`.
*
* JS side passes in:
* 1. `args`: Command line arguments (process.argv.slice(2))
Expand Down
11 changes: 10 additions & 1 deletion apps/oxfmt/src-js/bindings.js
Original file line number Diff line number Diff line change
Expand Up @@ -575,5 +575,14 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}

const { runCli } = nativeBinding
const { ArrowParensConfig, EmbeddedLanguageFormattingConfig, EndOfLineConfig, ObjectWrapConfig, QuotePropsConfig, SortOrderConfig, TrailingCommaConfig, Severity, format, runCli } = nativeBinding
export { ArrowParensConfig }
export { EmbeddedLanguageFormattingConfig }
export { EndOfLineConfig }
export { ObjectWrapConfig }
export { QuotePropsConfig }
export { SortOrderConfig }
export { TrailingCommaConfig }
export { Severity }
export { format }
export { runCli }
31 changes: 29 additions & 2 deletions apps/oxfmt/src-js/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,29 @@
export * from "./bindings.js";
export { setupConfig, formatEmbeddedCode, formatFile } from "./prettier-proxy.js";
import * as napi from "./bindings.js";
import { setupConfig, formatEmbeddedCode, formatFile } from "./prettier-proxy.js";
import type { Oxfmtrc } from "./bindings.js";

// NOTE: We need to re-export related types and enum fields manually
export type { Oxfmtrc } from "./bindings.js";
export {
EndOfLineConfig,
QuotePropsConfig,
ArrowParensConfig,
EmbeddedLanguageFormattingConfig,
ObjectWrapConfig,
TrailingCommaConfig,
SortOrderConfig,
} from "./bindings.js";

export async function format(fileName: string, sourceText: string, options?: Oxfmtrc) {
if (typeof fileName !== "string") throw new TypeError("`fileName` must be a string");
if (typeof sourceText !== "string") throw new TypeError("`sourceText` must be a string");

return napi.format(
fileName,
sourceText,
options ?? {},
setupConfig,
formatEmbeddedCode,
formatFile,
);
}
3 changes: 2 additions & 1 deletion apps/oxfmt/src-js/prettier-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ export async function setupConfig(configJSON: string, numThreads: number): Promi
prettierConfig: JSON.parse(configJSON),
};

if (pool) throw new Error("`setupConfig()` has already been called");
// Just ignore if already initialized
if (pool) return [];

// Initialize worker pool for parallel Prettier formatting
// Pass config via workerData so all workers get it on initialization
Expand Down
96 changes: 91 additions & 5 deletions apps/oxfmt/src/main_napi.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
use std::ffi::OsString;
use std::path::PathBuf;

use napi_derive::napi;

use oxc_formatter::Oxfmtrc;
use oxc_napi::OxcError;
use serde_json::Value;

use crate::{
cli::{FormatRunner, Mode, format_command, init_miette, init_rayon, init_tracing},
core::{ExternalFormatter, JsFormatEmbeddedCb, JsFormatFileCb, JsSetupConfigCb},
core::{
ExternalFormatter, FormatFileStrategy, FormatResult as CoreFormatResult,
JsFormatEmbeddedCb, JsFormatFileCb, JsSetupConfigCb, SourceFormatter,
},
lsp::run_lsp,
stdin::StdinRunner,
};

// NAPI based JS CLI entry point.
// For pure Rust CLI entry point, see `main.rs`.

/// NAPI entry point.
/// NAPI based JS CLI entry point.
/// For pure Rust CLI entry point, see `main.rs`.
///
/// JS side passes in:
/// 1. `args`: Command line arguments (process.argv.slice(2))
Expand Down Expand Up @@ -91,3 +97,83 @@ pub async fn run_cli(
}
}
}

// ---

#[napi(object)]
pub struct FormatResult {
/// The formatted code.
pub code: String,
/// Parse and format errors.
pub errors: Vec<OxcError>,
}

/// Format source code asynchronously.
#[expect(clippy::allow_attributes)]
#[allow(clippy::trailing_empty_array, clippy::unused_async)] // https://github.com/napi-rs/napi-rs/issues/2758
#[napi]
pub async fn format(
filename: String,
source_text: String,
options: Option<Oxfmtrc>,
#[napi(ts_arg_type = "(configJSON: string, numThreads: number) => Promise<string[]>")]
setup_config_cb: JsSetupConfigCb,
#[napi(ts_arg_type = "(tagName: string, code: string) => Promise<string>")]
format_embedded_cb: JsFormatEmbeddedCb,
#[napi(
ts_arg_type = "(parserName: string, fileName: string, code: string) => Promise<string>"
)]
format_file_cb: JsFormatFileCb,
) -> FormatResult {
let external_formatter =
ExternalFormatter::new(setup_config_cb, format_embedded_cb, format_file_cb);

// Determine format strategy from file path
let Ok(entry) = FormatFileStrategy::try_from(PathBuf::from(&filename)) else {
return FormatResult {
code: source_text,
errors: vec![OxcError::new(format!("Unsupported file type: {filename}"))],
};
};

// Convert Oxfmtrc to FormatOptions and OxfmtOptions
let oxfmtrc = options.unwrap_or_default();
let (format_options, oxfmt_options) = match oxfmtrc.into_options() {
Ok(opts) => opts,
Err(err) => {
return FormatResult {
code: source_text,
errors: vec![OxcError::new(format!("Invalid options: {err}"))],
};
}
};

// Build external config JSON for Prettier (with defaults applied)
let mut external_config = Value::Object(serde_json::Map::new());
Oxfmtrc::populate_prettier_config(&format_options, &mut external_config);

let num_of_threads = 1;

// Setup external formatter config (calls JS callback to initialize Prettier)
if let Err(err) = external_formatter.setup_config(&external_config.to_string(), num_of_threads)
{
return FormatResult {
code: source_text,
errors: vec![OxcError::new(format!("Failed to setup external formatter: {err}"))],
};
}

// Create source formatter with external formatter
let formatter = SourceFormatter::new(num_of_threads, format_options)
.with_external_formatter(Some(external_formatter), oxfmt_options.sort_package_json);

// Run formatting in a blocking task within tokio runtime
// This is needed because external formatter uses `tokio::runtime::Handle::current()`
match tokio::task::block_in_place(|| formatter.format(&entry, &source_text)) {
CoreFormatResult::Success { code, .. } => FormatResult { code, errors: vec![] },
CoreFormatResult::Error(diagnostics) => {
let errors = OxcError::from_diagnostics(&filename, &source_text, diagnostics);
FormatResult { code: source_text, errors }
}
}
}
35 changes: 35 additions & 0 deletions apps/oxfmt/test/api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { format, QuotePropsConfig } from "../dist/index.js";
import { describe, expect, test } from "vitest";
import type { Oxfmtrc } from "../dist/index.js";

describe("API Tests", () => {
test("`format()` function exists", () => {
expect(typeof format).toBe("function");
});

test("should `format()` multiple times", async () => {
const result1 = await format("a.ts", "const x:number=42");
expect(result1.code).toBe("const x: number = 42;\n");
expect(result1.errors).toStrictEqual([]);

const result2 = await format("a.json", '{"key": "value"}');
expect(result2.code).toBe('{ "key": "value" }\n');
expect(result2.errors).toStrictEqual([]);
});

test("should TS types work correctly", async () => {
const options: Oxfmtrc = {
quoteProps: QuotePropsConfig.AsNeeded,
printWidth: 120,
semi: false,
experimentalSortPackageJson: false,
experimentalSortImports: {
partitionByComment: false,
},
};

const result = await format("a.ts", "const x={'y':1}", options);
expect(result.code).toBe("const x = { y: 1 }\n");
expect(result.errors).toStrictEqual([]);
});
});
Loading
Loading