From ab168177bbdf4c94af77a15d555d0e390b262b4e Mon Sep 17 00:00:00 2001 From: Dunqing <29533304+Dunqing@users.noreply.github.com> Date: Thu, 23 Oct 2025 00:42:30 +0000 Subject: [PATCH] feat(playground): support load formatter options and output formatter IR (#14856) --- napi/playground/index.d.ts | 50 +++++++++- napi/playground/src/lib.rs | 168 ++++++++++++++++++++++++++++----- napi/playground/src/options.rs | 54 ++++++++++- 3 files changed, 245 insertions(+), 27 deletions(-) diff --git a/napi/playground/index.d.ts b/napi/playground/index.d.ts index 28786fe5b0b50..c34cccef969e5 100644 --- a/napi/playground/index.d.ts +++ b/napi/playground/index.d.ts @@ -68,6 +68,41 @@ export interface OxcDefineOptions { define: Record } +export interface OxcFormatterOptions { + /** Use tabs instead of spaces (default: false) */ + useTabs?: boolean + /** Number of spaces per indentation level (default: 2) */ + tabWidth?: number + /** Line ending type: "lf" | "crlf" | "cr" (default: "lf") */ + endOfLine?: string + /** Maximum line width (default: 80) */ + printWidth?: number + /** Use single quotes instead of double quotes (default: false) */ + singleQuote?: boolean + /** Use single quotes in JSX (default: false) */ + jsxSingleQuote?: boolean + /** When to add quotes around object properties: "as-needed" | "preserve" (default: "as-needed") */ + quoteProps?: string + /** Print trailing commas: "all" | "es5" | "none" (default: "all") */ + trailingComma?: string + /** Print semicolons (default: true) */ + semi?: boolean + /** Include parentheses around arrow function parameters: "always" | "avoid" (default: "always") */ + arrowParens?: string + /** Print spaces between brackets in object literals (default: true) */ + bracketSpacing?: boolean + /** Put > of multi-line elements at the end of the last line (default: false) */ + bracketSameLine?: boolean + /** Object wrapping style: "preserve" | "collapse" | "always" (default: "preserve") */ + objectWrap?: string + /** Put each attribute on its own line (default: false) */ + singleAttributePerLine?: boolean + /** Operator position: "start" | "end" (default: "end") */ + experimentalOperatorPosition?: string + /** Sort imports configuration */ + experimentalSortImports?: OxcSortImportsOptions +} + export interface OxcInjectOptions { /** Map of variable name to module source or [source, specifier] */ inject: Record @@ -90,6 +125,7 @@ export interface OxcOptions { run: OxcRunOptions parser: OxcParserOptions linter?: OxcLinterOptions + formatter?: OxcFormatterOptions transformer?: OxcTransformerOptions isolatedDeclarations?: OxcIsolatedDeclarationsOptions codegen?: OxcCodegenOptions @@ -111,7 +147,6 @@ export interface OxcParserOptions { export interface OxcRunOptions { lint: boolean formatter: boolean - formatterIr: boolean transform: boolean isolatedDeclarations: boolean whitespace: boolean @@ -122,6 +157,19 @@ export interface OxcRunOptions { cfg: boolean } +export interface OxcSortImportsOptions { + /** Partition imports by newlines (default: false) */ + partitionByNewline?: boolean + /** Partition imports by comments (default: false) */ + partitionByComment?: boolean + /** Sort side effects imports (default: false) */ + sortSideEffects?: boolean + /** Sort order: "asc" | "desc" (default: "asc") */ + order?: string + /** Ignore case when sorting (default: true) */ + ignoreCase?: boolean +} + export interface OxcTransformerOptions { target?: string useDefineForClassFields: boolean diff --git a/napi/playground/src/lib.rs b/napi/playground/src/lib.rs index 2dfaa20e93130..7455c3255d5de 100644 --- a/napi/playground/src/lib.rs +++ b/napi/playground/src/lib.rs @@ -29,7 +29,11 @@ use oxc::{ syntax::reference::ReferenceFlags, transformer::{TransformOptions, Transformer}, }; -use oxc_formatter::{FormatOptions, Formatter}; +use oxc_formatter::{ + ArrowParentheses, AttributePosition, BracketSameLine, BracketSpacing, Expand, FormatOptions, + Formatter, IndentStyle, IndentWidth, LineEnding, LineWidth, OperatorPosition, QuoteProperties, + QuoteStyle, Semicolons, SortImports, SortOrder, TrailingCommas, +}; use oxc_linter::{ ConfigStore, ConfigStoreBuilder, ContextSubHost, ExternalPluginStore, LintOptions, Linter, ModuleRecord, Oxlintrc, @@ -41,8 +45,9 @@ use oxc_transformer_plugins::{ }; use crate::options::{ - OxcControlFlowOptions, OxcDefineOptions, OxcInjectOptions, OxcIsolatedDeclarationsOptions, - OxcLinterOptions, OxcOptions, OxcParserOptions, OxcRunOptions, OxcTransformerOptions, + OxcControlFlowOptions, OxcDefineOptions, OxcFormatterOptions, OxcInjectOptions, + OxcIsolatedDeclarationsOptions, OxcLinterOptions, OxcOptions, OxcParserOptions, OxcRunOptions, + OxcTransformerOptions, }; mod options; @@ -100,6 +105,7 @@ impl Oxc { run: ref run_options, parser: ref parser_options, linter: ref linter_options, + formatter: ref formatter_options, transformer: ref transform_options, control_flow: ref control_flow_options, isolated_declarations: ref isolated_declarations_options, @@ -109,6 +115,7 @@ impl Oxc { .. } = options; let linter_options = linter_options.clone().unwrap_or_default(); + let formatter_options = formatter_options.clone().unwrap_or_default(); let transform_options = transform_options.clone().unwrap_or_default(); let control_flow_options = control_flow_options.clone().unwrap_or_default(); let codegen_options = codegen_options.clone().unwrap_or_default(); @@ -142,14 +149,7 @@ impl Oxc { &allocator, ); - // Phase 4: Run formatter - let parse_options = ParseOptions { - parse_regular_expression: true, - allow_return_outside_function: parser_options.allow_return_outside_function, - preserve_parens: parser_options.preserve_parens, - allow_v8_intrinsics: parser_options.allow_v8_intrinsics, - }; - self.run_formatter(run_options, parse_options, &source_text, source_type); + self.run_formatter(run_options, &source_text, source_type, &formatter_options); let mut scoping = semantic.into_scoping(); @@ -414,30 +414,148 @@ impl Oxc { } } + fn convert_formatter_options(options: &OxcFormatterOptions) -> FormatOptions { + let mut format_options = FormatOptions::default(); + + if let Some(use_tabs) = options.use_tabs { + format_options.indent_style = + if use_tabs { IndentStyle::Tab } else { IndentStyle::Space }; + } + + if let Some(tab_width) = options.tab_width + && let Ok(width) = IndentWidth::try_from(tab_width) + { + format_options.indent_width = width; + } + + if let Some(ref end_of_line) = options.end_of_line + && let Ok(ending) = end_of_line.parse::() + { + format_options.line_ending = ending; + } + + if let Some(print_width) = options.print_width + && let Ok(width) = LineWidth::try_from(print_width) + { + format_options.line_width = width; + } + + if let Some(single_quote) = options.single_quote { + format_options.quote_style = + if single_quote { QuoteStyle::Single } else { QuoteStyle::Double }; + } + + if let Some(jsx_single_quote) = options.jsx_single_quote { + format_options.jsx_quote_style = + if jsx_single_quote { QuoteStyle::Single } else { QuoteStyle::Double }; + } + + if let Some(ref quote_props) = options.quote_props + && let Ok(props) = quote_props.parse::() + { + format_options.quote_properties = props; + } + + if let Some(ref trailing_comma) = options.trailing_comma + && let Ok(commas) = trailing_comma.parse::() + { + format_options.trailing_commas = commas; + } + + if let Some(semi) = options.semi { + format_options.semicolons = + if semi { Semicolons::Always } else { Semicolons::AsNeeded }; + } + + if let Some(ref arrow_parens) = options.arrow_parens { + let normalized = + if arrow_parens == "avoid" { "as-needed" } else { arrow_parens.as_str() }; + if let Ok(parens) = normalized.parse::() { + format_options.arrow_parentheses = parens; + } + } + + if let Some(bracket_spacing) = options.bracket_spacing { + format_options.bracket_spacing = BracketSpacing::from(bracket_spacing); + } + + if let Some(bracket_same_line) = options.bracket_same_line { + format_options.bracket_same_line = BracketSameLine::from(bracket_same_line); + } + + if let Some(single_attribute_per_line) = options.single_attribute_per_line { + format_options.attribute_position = if single_attribute_per_line { + AttributePosition::Multiline + } else { + AttributePosition::Auto + }; + } + + if let Some(ref object_wrap) = options.object_wrap { + let normalized = match object_wrap.as_str() { + "preserve" => "auto", + "collapse" => "never", + _ => object_wrap.as_str(), + }; + if let Ok(expand) = normalized.parse::() { + format_options.expand = expand; + } + } + + if let Some(ref position) = options.experimental_operator_position + && let Ok(op_position) = position.parse::() + { + format_options.experimental_operator_position = op_position; + } + + if let Some(ref sort_imports_config) = options.experimental_sort_imports { + let order = sort_imports_config + .order + .as_ref() + .and_then(|o| o.parse::().ok()) + .unwrap_or_default(); + + format_options.experimental_sort_imports = Some(SortImports { + partition_by_newline: sort_imports_config.partition_by_newline.unwrap_or(false), + partition_by_comment: sort_imports_config.partition_by_comment.unwrap_or(false), + sort_side_effects: sort_imports_config.sort_side_effects.unwrap_or(false), + order, + ignore_case: sort_imports_config.ignore_case.unwrap_or(true), + }); + } + + format_options + } + fn run_formatter( &mut self, run_options: &OxcRunOptions, - parser_options: ParseOptions, source_text: &str, source_type: SourceType, + formatter_options: &OxcFormatterOptions, ) { let allocator = Allocator::default(); - if run_options.formatter || run_options.formatter_ir { + if run_options.formatter { let ret = Parser::new(&allocator, source_text, source_type) - .with_options(ParseOptions { preserve_parens: false, ..parser_options }) + .with_options(ParseOptions { + preserve_parens: false, + allow_return_outside_function: true, + allow_v8_intrinsics: true, + parse_regular_expression: false, + }) .parse(); - let formatter = Formatter::new(&allocator, FormatOptions::default()); - self.formatter_formatted_text = formatter.build(&ret.program); - - // if run_options.formatter_ir.unwrap_or_default() { - // let formatter_doc = formatter.doc(&ret.program).to_string(); - // self.formatter_ir_text = { - // let ret = - // Parser::new(&allocator, &formatter_doc, SourceType::default()).parse(); - // Formatter::new(&allocator, FormatOptions::default()).build(&ret.program) - // }; - // } + let format_options = Self::convert_formatter_options(formatter_options); + let formatter = Formatter::new(&allocator, format_options); + let formatted = formatter.format(&ret.program); + if run_options.formatter { + self.formatter_formatted_text = match formatted.print() { + Ok(printer) => printer.into_code(), + Err(err) => err.to_string(), + }; + + self.formatter_ir_text = formatted.into_document().to_string(); + } } } diff --git a/napi/playground/src/options.rs b/napi/playground/src/options.rs index 19673870f8719..40ef1754975ea 100644 --- a/napi/playground/src/options.rs +++ b/napi/playground/src/options.rs @@ -8,6 +8,7 @@ pub struct OxcOptions { pub run: OxcRunOptions, pub parser: OxcParserOptions, pub linter: Option, + pub formatter: Option, pub transformer: Option, pub isolated_declarations: Option, pub codegen: Option, @@ -23,7 +24,6 @@ pub struct OxcOptions { pub struct OxcRunOptions { pub lint: bool, pub formatter: bool, - pub formatter_ir: bool, pub transform: bool, pub isolated_declarations: bool, pub whitespace: bool, @@ -112,3 +112,55 @@ pub struct OxcMangleOptions { #[napi(object)] #[derive(Clone, Copy, Default)] pub struct OxcCompressOptions; + +#[napi(object)] +#[derive(Default, Clone)] +pub struct OxcFormatterOptions { + /// Use tabs instead of spaces (default: false) + pub use_tabs: Option, + /// Number of spaces per indentation level (default: 2) + pub tab_width: Option, + /// Line ending type: "lf" | "crlf" | "cr" (default: "lf") + pub end_of_line: Option, + /// Maximum line width (default: 80) + pub print_width: Option, + /// Use single quotes instead of double quotes (default: false) + pub single_quote: Option, + /// Use single quotes in JSX (default: false) + pub jsx_single_quote: Option, + /// When to add quotes around object properties: "as-needed" | "preserve" (default: "as-needed") + pub quote_props: Option, + /// Print trailing commas: "all" | "es5" | "none" (default: "all") + pub trailing_comma: Option, + /// Print semicolons (default: true) + pub semi: Option, + /// Include parentheses around arrow function parameters: "always" | "avoid" (default: "always") + pub arrow_parens: Option, + /// Print spaces between brackets in object literals (default: true) + pub bracket_spacing: Option, + /// Put > of multi-line elements at the end of the last line (default: false) + pub bracket_same_line: Option, + /// Object wrapping style: "preserve" | "collapse" | "always" (default: "preserve") + pub object_wrap: Option, + /// Put each attribute on its own line (default: false) + pub single_attribute_per_line: Option, + /// Operator position: "start" | "end" (default: "end") + pub experimental_operator_position: Option, + /// Sort imports configuration + pub experimental_sort_imports: Option, +} + +#[napi(object)] +#[derive(Default, Clone)] +pub struct OxcSortImportsOptions { + /// Partition imports by newlines (default: false) + pub partition_by_newline: Option, + /// Partition imports by comments (default: false) + pub partition_by_comment: Option, + /// Sort side effects imports (default: false) + pub sort_side_effects: Option, + /// Sort order: "asc" | "desc" (default: "asc") + pub order: Option, + /// Ignore case when sorting (default: true) + pub ignore_case: Option, +}