mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(formatter,oxfmt): apply effective print width for JSDoc fence (#25413)
As JSDoc is always contained within the ` * ` fenced comments, the `printWidth` option should not have been used as is. The dedicated `jsdocPrintWidth` option has not yet been implemented. #24633
This commit is contained in:
1 parent
ce29325cfe
commit
1a2c64a356
14 files changed
+202
-57
No files matched your search
@@ -62,9 +62,10 @@ Per-language options are NOT built up front: `ResolvedDispatchConfig` maps them
|
||||
A separate string-out channel (the session's `string_embedder` service, NOT the dispatcher) carries the string-in/string-out consumers:
|
||||
|
||||
- JSDoc fenced code blocks: routing follows ONE rule, the same `dispatcher::route` table
|
||||
- a `Native` fence language formats through the dispatcher via a thin string adapter (`embed/fence.rs::format_native_fence`, EVERY build, the pure Rust build wires it via `services::for_root`)
|
||||
- a `Native` fence language formats through `FormatSession::dispatch_to_string` via a thin string adapter (`embed/jsdoc_fence.rs::format_native_fence`, EVERY build, the pure Rust build wires it via `services::for_root`)
|
||||
- md/html/angular fences stay on the Prettier string path (`embed/prettier_string.rs`, napi only; their Doc→IR conversion has unrepresentable cases);
|
||||
- everything else stays verbatim
|
||||
- the embedder carries the caller's effective print width; both branches honor it (native via `PrintWidth` override, Prettier via `printWidth` in the options JSON), so a fence prints at the same width a JS/TS snippet in the same position would (see `upstream-jsdoc-bugs.md` #11 for the deliberate divergence from upstream's flat `printWidth - 4`)
|
||||
- temporary html-in-js string recovery (`format_js_in_html_as_fallback` in `oxc_formatter/src/print/template/embed/html.rs`): rescues the template as text when the Doc→IR conversion cannot represent the returned Doc
|
||||
|
||||
NOTE: These string-out channel is temporary workaround, should be replaced by native implementations and Prettier usage should be eliminated in the future.
|
||||
|
||||
@@ -228,7 +228,8 @@ impl ResolvedDispatchConfig {
|
||||
}
|
||||
|
||||
/// Printer options from the shared resolved core bundle;
|
||||
/// the fence adapter ([`super::fence`]) prints dispatched child IR standalone with these.
|
||||
/// the fence adapter ([`super::jsdoc_fence`]) derives its per-fence options from these
|
||||
/// (width overridden to the fence's effective width).
|
||||
pub fn print_options(&self) -> PrinterOptions {
|
||||
self.core.as_print_options()
|
||||
}
|
||||
|
||||
@@ -13,27 +13,27 @@ use tracing::debug_span;
|
||||
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_formatter_core::{
|
||||
DispatchOutcome, DispatchRequest, DispatchResult, Document, FormatDispatcher, FormatSession,
|
||||
InputKind, SessionServices, TailwindSorter,
|
||||
DispatchRequest, FormatDispatcher, FormatSession, InputKind, PrintWidth, SessionServices,
|
||||
TailwindSorter,
|
||||
};
|
||||
|
||||
use super::dispatcher::ResolvedDispatchConfig;
|
||||
|
||||
/// Format a JSDoc fenced code block through the native dispatch registry:
|
||||
/// a string-in/string-out adapter over the IR contract.
|
||||
/// a string-in/string-out adapter over `FormatSession::dispatch_to_string`.
|
||||
///
|
||||
/// Load-bearing notes:
|
||||
/// - The fence has no parent index space, so its Tailwind classes are sorted here
|
||||
/// (element-wise: the sorter reorders classes WITHIN each collected string, never the vector,
|
||||
/// keeping `TailwindClass(index)` references valid). The pure build passes no sorter.
|
||||
/// - `Err` keeps the fence verbatim, covering both `PreserveOriginal`
|
||||
/// (parse failure; a failed native language never re-routes to Prettier) and operational errors.
|
||||
/// - `print_width` is the fence's effective width at its comment position;
|
||||
/// it overrides the configured width, the other print knobs come from the resolved config
|
||||
/// - `Err` keeps the fence verbatim, covering both the deliberate keep
|
||||
/// (`Ok(None)`; a failed native language never re-routes to Prettier) and operational errors
|
||||
/// - The session-less `StringEmbedder` contract forces a fresh root session per fence,
|
||||
/// so `dispatch_depth` resets at this string boundary (inert today: no native fence language re-dispatches).
|
||||
/// Threading the parent session through the callback is the eventual fix.
|
||||
pub fn format_native_fence(
|
||||
language: &str,
|
||||
code: &str,
|
||||
print_width: usize,
|
||||
fence_dispatcher: &FormatDispatcher,
|
||||
dispatch_config: &ResolvedDispatchConfig,
|
||||
sort_tailwind: Option<&TailwindSorter>,
|
||||
@@ -49,29 +49,25 @@ pub fn format_native_fence(
|
||||
..SessionServices::default()
|
||||
},
|
||||
);
|
||||
let outcome = session.dispatch(DispatchRequest {
|
||||
language,
|
||||
text: code,
|
||||
input_kind: InputKind::Fragment,
|
||||
parent_context: None,
|
||||
})?;
|
||||
|
||||
let DispatchOutcome::Formatted(result) = outcome else {
|
||||
return Err(format!("Native formatter for '{language}' kept the input as-is"));
|
||||
};
|
||||
// The parent-less consumer: no index space to remap into,
|
||||
// so the classes sort locally instead of going through `into_doc`.
|
||||
let DispatchResult { doc: ir, tailwind_classes, .. } = result;
|
||||
|
||||
let tailwind_classes = session.sort_tailwind_classes(tailwind_classes);
|
||||
|
||||
let mut code = Document::new(ir, tailwind_classes)
|
||||
.print(code.len(), dispatch_config.print_options())
|
||||
.map_err(|err| err.to_string())?
|
||||
.into_code();
|
||||
// The block is re-embedded line-by-line into the comment; no trailing newline
|
||||
code.truncate(code.trim_end().len());
|
||||
|
||||
Ok(code)
|
||||
let printer_options = dispatch_config
|
||||
.print_options()
|
||||
.with_print_width(PrintWidth::new(u32::try_from(print_width).unwrap_or(u32::MAX)));
|
||||
session
|
||||
.dispatch_to_string(
|
||||
DispatchRequest {
|
||||
language,
|
||||
text: code,
|
||||
input_kind: InputKind::Fragment,
|
||||
parent_context: None,
|
||||
},
|
||||
printer_options,
|
||||
)?
|
||||
.map(|mut code| {
|
||||
// The block is re-embedded line-by-line into the comment; no trailing newline
|
||||
code.truncate(code.trim_end().len());
|
||||
code
|
||||
})
|
||||
.ok_or_else(|| format!("Native formatter for '{language}' kept the input as-is"))
|
||||
})
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
//! `ResolvedDispatchConfig` (lazy per-language options) + `build_dispatcher` with a Rust branch per `NativeLanguage`;
|
||||
//! IR integrates into the parent's arena / `GroupId` space
|
||||
//! - [`services`] (every build): the root `SessionServices` assembly (`for_root`, one definition per build)
|
||||
//! - [`fence`] (every build): the JSDoc native-fence string adapter over the registry
|
||||
//! - [`jsdoc_fence`] (every build): the JSDoc native-fence string adapter over the registry
|
||||
//! - [`prettier_doc`] (napi only): Prettier Doc→IR path for the `Route::Prettier` set
|
||||
//! - [`prettier_string`] (napi only): the Prettier string paths of the string-out channel
|
||||
//! (md/html/angular JSDoc fences + the html-in-js string recovery; results re-embed line-by-line)
|
||||
@@ -21,7 +21,7 @@ use std::sync::Arc;
|
||||
use serde_json::Value;
|
||||
|
||||
pub mod dispatcher;
|
||||
pub mod fence;
|
||||
pub mod jsdoc_fence;
|
||||
#[cfg(feature = "napi")]
|
||||
pub mod prettier_doc;
|
||||
#[cfg(feature = "napi")]
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//! so the parent re-requests the same HTML via this string channel and substitutes placeholders back to `${expr}`.
|
||||
//!
|
||||
//! Fence routing follows ONE rule, the shared routing table ([`dispatcher::route`]):
|
||||
//! a `Route::Native` language formats through the dispatcher via [`super::fence::format_native_fence`] (the build-independent adapter);
|
||||
//! a `Route::Native` language formats through the dispatcher via [`super::jsdoc_fence::format_native_fence`] (the build-independent adapter);
|
||||
//! the `Route::Prettier` set (md/html/angular) stays on the Prettier string path
|
||||
//! (their Doc→IR conversion has unrepresentable cases,
|
||||
//! so forcing them through the dispatcher would regress to verbatim; the wall falls with the HTML Rust port).
|
||||
@@ -26,9 +26,9 @@ use crate::core::{
|
||||
embed::{
|
||||
FormatEmbeddedWithConfigCallback,
|
||||
dispatcher::{self, Route},
|
||||
fence,
|
||||
jsdoc_fence,
|
||||
},
|
||||
options::inject_parser,
|
||||
options::{inject_parser, inject_print_width},
|
||||
};
|
||||
|
||||
/// Build the napi build's string embedder installed on the session.
|
||||
@@ -49,13 +49,14 @@ pub fn build_string_embedder(
|
||||
// Fence dispatchers never take the Prettier fallback (native fences never fall back),
|
||||
// so one is invariant across the callback's lifetime: build it once, not per fence.
|
||||
let fence_dispatcher = dispatcher::build_dispatcher(Arc::clone(&dispatch_config), None);
|
||||
Arc::new(move |language: &str, code: &str| {
|
||||
Arc::new(move |language: &str, code: &str, print_width: usize| {
|
||||
let parser_name = match dispatcher::route(language) {
|
||||
// Native branch (JSDoc fenced code blocks): through the dispatcher, never Prettier.
|
||||
Route::Native(_) => {
|
||||
return fence::format_native_fence(
|
||||
return jsdoc_fence::format_native_fence(
|
||||
language,
|
||||
code,
|
||||
print_width,
|
||||
&fence_dispatcher,
|
||||
&dispatch_config,
|
||||
sort_tailwind.as_ref(),
|
||||
@@ -71,6 +72,8 @@ pub fn build_string_embedder(
|
||||
// because there may be multiple embedded sections in one JS/TS file.
|
||||
let mut options = dispatch_config.external_options().clone();
|
||||
inject_parser(&mut options, parser_name);
|
||||
// The same effective width the native branch prints at
|
||||
inject_print_width(&mut options, print_width);
|
||||
(format_embedded)(options, code)
|
||||
.map(|mut code| {
|
||||
// Remove trailing newline added by Prettier without allocation.
|
||||
|
||||
@@ -34,10 +34,11 @@ pub fn for_root(dispatch_config: &Arc<ResolvedDispatchConfig>) -> SessionService
|
||||
let string_embedder = dispatcher.as_ref().map(|dispatcher| {
|
||||
let fence_dispatcher = Arc::clone(dispatcher);
|
||||
let dispatch_config = Arc::clone(dispatch_config);
|
||||
Arc::new(move |language: &str, code: &str| {
|
||||
super::fence::format_native_fence(
|
||||
Arc::new(move |language: &str, code: &str, print_width: usize| {
|
||||
super::jsdoc_fence::format_native_fence(
|
||||
language,
|
||||
code,
|
||||
print_width,
|
||||
&fence_dispatcher,
|
||||
&dispatch_config,
|
||||
None,
|
||||
|
||||
@@ -34,6 +34,6 @@ pub use to_oxc_toml::to_oxc_toml;
|
||||
#[cfg(feature = "napi")]
|
||||
pub use to_prettier::{
|
||||
build_external_options, inject_filepath, inject_oxfmt_plugin_payload, inject_parser,
|
||||
inject_svelte_plugin_payload, inject_tailwind_plugin_payload, to_prettier,
|
||||
inject_print_width, inject_svelte_plugin_payload, inject_tailwind_plugin_payload, to_prettier,
|
||||
};
|
||||
pub use validate::{ValidatedOptions, validate};
|
||||
@@ -159,6 +159,11 @@ pub fn inject_parser(opts: &mut Value, parser_name: &str) {
|
||||
as_object_mut(opts).insert("parser".to_string(), Value::String(parser_name.to_string()));
|
||||
}
|
||||
|
||||
/// Inject `printWidth` key, overriding the configured one.
|
||||
pub fn inject_print_width(opts: &mut Value, print_width: usize) {
|
||||
as_object_mut(opts).insert("printWidth".to_string(), Value::from(print_width));
|
||||
}
|
||||
|
||||
/// Inject `filepath` key.
|
||||
///
|
||||
/// Some plugins (Tailwind sorter, etc.) depend on it.
|
||||
|
||||
@@ -145,6 +145,86 @@ describe("JSDoc", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should print native fences at the fence's effective width, not the full printWidth", async () => {
|
||||
// Effective width at top level = printWidth(100) - " * "(3) - 4 = 93,
|
||||
// the same width JS/TS snippets in the same position already use.
|
||||
// The 94-char field line must break; the 92-char one must not.
|
||||
// (Upstream `prettier-plugin-jsdoc` uses a flat printWidth - 4, which overflows `printWidth` for indented comments;
|
||||
// see tasks/prettier_conformance/jsdoc/upstream-jsdoc-bugs.md)
|
||||
const source = `
|
||||
/**
|
||||
* \`\`\`graphql
|
||||
* query { fieldName(argOne: "value1", argTwo: "value2", argThree: "value3", four: 4444, five: 5555577) }
|
||||
* \`\`\`
|
||||
*
|
||||
* \`\`\`graphql
|
||||
* query { fieldName(argOne: "value1", argTwo: "value2", argThree: "value3", four: 4444, five: 55555) }
|
||||
* \`\`\`
|
||||
*/
|
||||
`.trim();
|
||||
|
||||
const result = await format("a.ts", source, { jsdoc: {} });
|
||||
expect(result.errors).toStrictEqual([]);
|
||||
expect(result.code).toBe(
|
||||
`
|
||||
/**
|
||||
* \`\`\`graphql
|
||||
* query {
|
||||
* fieldName(
|
||||
* argOne: "value1"
|
||||
* argTwo: "value2"
|
||||
* argThree: "value3"
|
||||
* four: 4444
|
||||
* five: 5555577
|
||||
* )
|
||||
* }
|
||||
* \`\`\`
|
||||
*
|
||||
* \`\`\`graphql
|
||||
* query {
|
||||
* fieldName(argOne: "value1", argTwo: "value2", argThree: "value3", four: 4444, five: 55555)
|
||||
* }
|
||||
* \`\`\`
|
||||
*/
|
||||
`.trimStart(),
|
||||
);
|
||||
});
|
||||
|
||||
it("should pass the fence's effective width to the Prettier string path too", async () => {
|
||||
// Same 93-char boundary as the native case, exercised through the Prettier
|
||||
// branch (html fences go to Prettier with `printWidth` injected):
|
||||
// the 96-char element must break, the 93-char one must not.
|
||||
const source = `
|
||||
/**
|
||||
* \`\`\`html
|
||||
* <section class="wrapper-xl" data-role="banner" id="hero"><span>hello world text</span></section>
|
||||
* \`\`\`
|
||||
*
|
||||
* \`\`\`html
|
||||
* <section class="wrapper" data-role="banner" id="hero"><span>hello world text</span></section>
|
||||
* \`\`\`
|
||||
*/
|
||||
`.trim();
|
||||
|
||||
const result = await format("a.ts", source, { jsdoc: {} });
|
||||
expect(result.errors).toStrictEqual([]);
|
||||
expect(result.code).toBe(
|
||||
`
|
||||
/**
|
||||
* \`\`\`html
|
||||
* <section class="wrapper-xl" data-role="banner" id="hero">
|
||||
* <span>hello world text</span>
|
||||
* </section>
|
||||
* \`\`\`
|
||||
*
|
||||
* \`\`\`html
|
||||
* <section class="wrapper" data-role="banner" id="hero"><span>hello world text</span></section>
|
||||
* \`\`\`
|
||||
*/
|
||||
`.trimStart(),
|
||||
);
|
||||
});
|
||||
|
||||
it("should keep fenced code verbatim for languages outside the registry", async () => {
|
||||
const source = `
|
||||
/**
|
||||
|
||||
@@ -765,14 +765,9 @@ fn format_code_value<'a>(
|
||||
&& !is_js_ts_lang(l)
|
||||
{
|
||||
if let Some(embed) = opts.string_embedder
|
||||
&& let Ok(formatted) = embed(l, code)
|
||||
&& let Ok(formatted) = embed(l, code, width)
|
||||
{
|
||||
let mut result = formatted;
|
||||
// Trim trailing newline that Prettier adds
|
||||
if result.ends_with('\n') {
|
||||
result.pop();
|
||||
}
|
||||
return Cow::Owned(result);
|
||||
return Cow::Owned(formatted);
|
||||
}
|
||||
return Cow::Borrowed(code);
|
||||
}
|
||||
|
||||
@@ -286,7 +286,10 @@ fn format_js_in_html_as_fallback<'a>(
|
||||
expressions: &[&AstNode<'a, Expression<'a>>],
|
||||
f: &mut JsFormatter<'_, 'a>,
|
||||
) -> bool {
|
||||
let Some(Ok(formatted)) = f.session().string_embedder().map(|embed| embed("html", joined))
|
||||
// Configured width; the template's indent is not subtracted (recovery path, matches prior behavior)
|
||||
let print_width = usize::from(f.options().line_width.value());
|
||||
let Some(Ok(formatted)) =
|
||||
f.session().string_embedder().map(|embed| embed("html", joined, print_width))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -87,7 +87,8 @@ The core is parameterized over a consumer-supplied context so it stays language-
|
||||
- The printer's `debug_assert` backstops any hand-rolled consumption that skips the merge
|
||||
- `FormatSession` (`session.rs`) is the execution unit:
|
||||
- One arena, one shared `GroupId` space (`Arc<UniqueGroupIdBuilder>`), the host's `SessionServices`, and the input's envelope semantics (`InputKind`), usable by standalone roots and dispatched children alike
|
||||
- `SessionServices` names the three per-run duties, one field each: `dispatcher` (IR channel), `string_embedder` (string-out channel; temporary while some languages only format via a string API), `tailwind_sorter` (print-time batch sort). Core only transports them
|
||||
- `SessionServices` names the three per-run duties, one field each: `dispatcher` (IR channel), `string_embedder` (string-out channel, `(language, code, print_width)`; temporary while some languages only format via a string API), `tailwind_sorter` (print-time batch sort). Core only transports them
|
||||
- `FormatSession::dispatch_to_string(request, printer_options)` is the string-out counterpart of `dispatch` (caller supplies the printer options; `Ok(None)` = deliberate keep; see its rustdoc)
|
||||
- `FormatState` holds one (`new_with_session`; plain `new` wraps a service-less `PhysicalFile` session), and `Formatter::session()` exposes it during a write
|
||||
- A dispatch states its request as `DispatchRequest` (language, text, `InputKind`, pair-specific context) and yields `Result<DispatchOutcome, String>`:
|
||||
- `DispatchOutcome::PreserveOriginal` is the DELIBERATE "keep the source as-is" answer (unsupported language, child parse failure, no dispatcher installed); `Err` is reserved for operational failures (transport / internal, recursion limit)
|
||||
|
||||
@@ -4,20 +4,25 @@ use std::sync::Arc;
|
||||
|
||||
use oxc_allocator::Allocator;
|
||||
|
||||
use crate::{DispatchOutcome, DispatchRequest, FormatDispatcher, UniqueGroupIdBuilder};
|
||||
use crate::{
|
||||
DispatchOutcome, DispatchRequest, DispatchResult, Document, FormatDispatcher, PrinterOptions,
|
||||
UniqueGroupIdBuilder,
|
||||
};
|
||||
|
||||
/// Upper bound on nested embedded dispatches;
|
||||
/// exceeding it is an operational error, catching accidental A-in-B-in-A infinite recursion.
|
||||
/// Real nesting is shallow (css-in-md-in-js would be 3); 8 is a generous ceiling.
|
||||
const MAX_DISPATCH_DEPTH: u8 = 8;
|
||||
|
||||
/// String-in/string-out embedded formatter: `(language, code)` to formatted code.
|
||||
/// String-in/string-out embedded formatter: `(language, code, print_width)` to formatted code.
|
||||
///
|
||||
/// The string-out channel's session-carried contract,
|
||||
/// for consumers whose result must come back as TEXT
|
||||
/// The string-out channel's session-carried contract, for consumers whose result must come back as TEXT
|
||||
/// (a JSDoc fence re-embedded line-by-line into a comment, the html-in-js temporary recovery path).
|
||||
/// `print_width` is the caller's effective width at the embedding position
|
||||
/// (e.g. a JSDoc fence's comment-content width minus the fence indent).
|
||||
/// The returned text carries no trailing whitespace (implementations trim before returning).
|
||||
/// `Err` means "keep the input verbatim".
|
||||
pub type StringEmbedder = Arc<dyn Fn(&str, &str) -> Result<String, String> + Send + Sync>;
|
||||
pub type StringEmbedder = Arc<dyn Fn(&str, &str, usize) -> Result<String, String> + Send + Sync>;
|
||||
|
||||
/// Print-time batch sorter for the classes referenced by `FormatElement::TailwindClass(index)`;
|
||||
/// runs once when a root formatter finalizes its `Document`.
|
||||
@@ -141,6 +146,41 @@ impl<'a> FormatSession<'a> {
|
||||
dispatcher(&child, request)
|
||||
}
|
||||
|
||||
/// Formats one embedded request and prints the result to text:
|
||||
/// dispatch → local Tailwind sort → [`Document`] print.
|
||||
///
|
||||
/// The string-out counterpart of [`Self::dispatch`], for consumers whose result
|
||||
/// must come back as TEXT. The caller supplies `printer_options`, typically its own
|
||||
/// print options with the embedding position's effective width, and owns any
|
||||
/// re-embedding conventions (e.g. trailing-newline handling) on the returned text.
|
||||
/// A text consumer has no parent index space, so Tailwind classes sort locally
|
||||
/// through this session's sorter instead of merging upward.
|
||||
///
|
||||
/// Returns `Ok(None)` when the dispatch deliberately preserved the input
|
||||
/// (see [`DispatchOutcome::PreserveOriginal`]); the caller keeps its original text.
|
||||
///
|
||||
/// # Errors
|
||||
/// Operational failures only (dispatch transport, recursion limit, print).
|
||||
pub fn dispatch_to_string(
|
||||
&self,
|
||||
request: DispatchRequest<'_>,
|
||||
printer_options: PrinterOptions,
|
||||
) -> Result<Option<String>, String> {
|
||||
let text_len = request.text.len();
|
||||
let DispatchOutcome::Formatted(DispatchResult { doc, tailwind_classes, .. }) =
|
||||
self.dispatch(request)?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let tailwind_classes = self.sort_tailwind_classes(tailwind_classes);
|
||||
|
||||
let code = Document::new(doc, tailwind_classes)
|
||||
.print(text_len, printer_options)
|
||||
.map_err(|err| err.to_string())?
|
||||
.into_code();
|
||||
Ok(Some(code))
|
||||
}
|
||||
|
||||
/// The arena shared between the root and every embedded child.
|
||||
#[inline]
|
||||
pub fn allocator(&self) -> &'a Allocator {
|
||||
|
||||
@@ -303,3 +303,22 @@ The plugin has two bugs with `@typedef{type}` (no space before brace):
|
||||
```
|
||||
|
||||
**Found in**: conformance tests (main/011-bad-defined-name)
|
||||
|
||||
## 11. Fenced-code width base ignores the comment prefix, exceeding printWidth in indented comments
|
||||
|
||||
**Severity**: Low — fence content in indented comments can exceed printWidth
|
||||
|
||||
The plugin formats fenced code (and `@example` bodies) at a flat `printWidth - 4`
|
||||
(`utils.ts` `formatCode`: `examplePrintWith = printWidth - 4`), without subtracting the
|
||||
comment's indent or the `*` line prefix. At top level the re-embedded line lands at
|
||||
`content + 3 <= printWidth - 1`, but for a comment indented by N columns the final line is
|
||||
`N + 3 + (printWidth - 4)`, exceeding printWidth for any N > 1.
|
||||
|
||||
**Root cause**: `formatCode` receives only `printWidth`; the re-embedding indent
|
||||
(`beginningSpace`) is applied after formatting and never enters the width budget.
|
||||
|
||||
**oxfmt behavior**: fences format at `effective width - 4`, where the effective width already
|
||||
subtracts the comment indent and the `*` prefix — the same base every JS/TS snippet in the
|
||||
same position uses. Final lines never exceed `printWidth - 4 + prefix`. At top level this is
|
||||
3 chars narrower than upstream (content 93 vs 96 at printWidth=100); pinned by
|
||||
`apps/oxfmt/test/api/jsdoc.test.ts` ("effective width" case).
|
||||
Reference in new issue
Block a user