Skip to content

Commit 4df9b9f

Browse files
authored
Merge branch 'main' into daniel-fix/linter-no-empty-interface-help
2 parents 7f107da + 93bb861 commit 4df9b9f

File tree

59 files changed

+2813
-440
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+2813
-440
lines changed

apps/oxfmt/src-js/bindings.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export declare const enum Severity {
3434
* # Panics
3535
* Panics if the current working directory cannot be determined.
3636
*/
37-
export declare function format(filename: string, sourceText: string, options: any | undefined | null, initExternalFormatterCb: (numThreads: number) => Promise<string[]>, formatEmbeddedCb: (options: Record<string, any>, code: string) => Promise<string>, formatFileCb: (options: Record<string, any>, code: string) => Promise<string>, sortTailwindClassesCb: (options: Record<string, any>, classes: string[]) => Promise<string[]>): Promise<FormatResult>
37+
export declare function format(filename: string, sourceText: string, options: any | undefined | null, initExternalFormatterCb: (numThreads: number) => Promise<string[]>, formatEmbeddedCb: (options: Record<string, any>, code: string) => Promise<string>, formatEmbeddedDocCb: (options: Record<string, any>, texts: string[]) => Promise<string[]>, formatFileCb: (options: Record<string, any>, code: string) => Promise<string>, sortTailwindClassesCb: (options: Record<string, any>, classes: string[]) => Promise<string[]>): Promise<FormatResult>
3838

3939
export interface FormatResult {
4040
/** The formatted code. */
@@ -49,7 +49,7 @@ export interface FormatResult {
4949
* This API is specialized for JS/TS snippets embedded in non-JS files.
5050
* Unlike `format()`, it is called only for js-in-xxx `textToDoc()` flow.
5151
*/
52-
export declare function jsTextToDoc(sourceExt: string, sourceText: string, oxfmtPluginOptionsJson: string, parentContext: string, initExternalFormatterCb: (numThreads: number) => Promise<string[]>, formatEmbeddedCb: (options: Record<string, any>, code: string) => Promise<string>, formatFileCb: (options: Record<string, any>, code: string) => Promise<string>, sortTailwindClassesCb: (options: Record<string, any>, classes: string[]) => Promise<string[]>): Promise<string | null>
52+
export declare function jsTextToDoc(sourceExt: string, sourceText: string, oxfmtPluginOptionsJson: string, parentContext: string, initExternalFormatterCb: (numThreads: number) => Promise<string[]>, formatEmbeddedCb: (options: Record<string, any>, code: string) => Promise<string>, formatEmbeddedDocCb: (options: Record<string, any>, texts: string[]) => Promise<string[]>, formatFileCb: (options: Record<string, any>, code: string) => Promise<string>, sortTailwindClassesCb: (options: Record<string, any>, classes: string[]) => Promise<string[]>): Promise<string | null>
5353

5454
/**
5555
* NAPI based JS CLI entry point.
@@ -66,4 +66,4 @@ export declare function jsTextToDoc(sourceExt: string, sourceText: string, oxfmt
6666
* - `mode`: If main logic will run in JS side, use this to indicate which mode
6767
* - `exitCode`: If main logic already ran in Rust side, return the exit code
6868
*/
69-
export declare function runCli(args: Array<string>, initExternalFormatterCb: (numThreads: number) => Promise<string[]>, formatEmbeddedCb: (options: Record<string, any>, code: string) => Promise<string>, formatFileCb: (options: Record<string, any>, code: string) => Promise<string>, sortTailwindcssClassesCb: (options: Record<string, any>, classes: string[]) => Promise<string[]>): Promise<[string, number | undefined | null]>
69+
export declare function runCli(args: Array<string>, initExternalFormatterCb: (numThreads: number) => Promise<string[]>, formatEmbeddedCb: (options: Record<string, any>, code: string) => Promise<string>, formatEmbeddedDocCb: (options: Record<string, any>, texts: string[]) => Promise<string[]>, formatFileCb: (options: Record<string, any>, code: string) => Promise<string>, sortTailwindcssClassesCb: (options: Record<string, any>, classes: string[]) => Promise<string[]>): Promise<[string, number | undefined | null]>

apps/oxfmt/src-js/cli-worker.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
// `oxfmt` CLI - Worker Thread Entry Point
22

33
// Re-exports core functions for use in `worker_threads`
4-
export { formatEmbeddedCode, formatFile, sortTailwindClasses } from "./libs/apis";
4+
export {
5+
formatEmbeddedCode,
6+
formatEmbeddedDoc,
7+
formatFile,
8+
sortTailwindClasses,
9+
} from "./libs/apis";

apps/oxfmt/src-js/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { runCli } from "./bindings";
22
import {
33
initExternalFormatter,
44
formatEmbeddedCode,
5+
formatEmbeddedDoc,
56
formatFile,
67
sortTailwindClasses,
78
disposeExternalFormatter,
@@ -28,6 +29,7 @@ void (async () => {
2829
args,
2930
initExternalFormatter,
3031
formatEmbeddedCode,
32+
formatEmbeddedDoc,
3133
formatFile,
3234
sortTailwindClasses,
3335
);

apps/oxfmt/src-js/cli/worker-proxy.ts

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Tinypool from "tinypool";
22
import { resolvePlugins } from "../libs/apis";
33
import type {
44
FormatEmbeddedCodeParam,
5+
FormatEmbeddedDocParam,
56
FormatFileParam,
67
SortTailwindClassesArgs,
78
} from "../libs/apis";
@@ -35,41 +36,54 @@ export async function formatEmbeddedCode(
3536
options: FormatEmbeddedCodeParam["options"],
3637
code: string,
3738
): Promise<string> {
38-
return pool!.run({ options, code } satisfies FormatEmbeddedCodeParam, {
39-
name: "formatEmbeddedCode",
40-
});
39+
return pool!
40+
.run({ options, code } satisfies FormatEmbeddedCodeParam, { name: "formatEmbeddedCode" })
41+
.catch(rethrowAsError);
42+
}
43+
44+
export async function formatEmbeddedDoc(
45+
options: FormatEmbeddedDocParam["options"],
46+
texts: string[],
47+
): Promise<string[]> {
48+
return pool!
49+
.run({ options, texts } satisfies FormatEmbeddedDocParam, {
50+
name: "formatEmbeddedDoc",
51+
})
52+
.catch(rethrowAsError);
4153
}
4254

4355
export async function formatFile(
4456
options: FormatFileParam["options"],
4557
code: string,
4658
): Promise<string> {
47-
return (
48-
pool!
49-
.run({ options, code } satisfies FormatFileParam, { name: "formatFile" })
50-
// `tinypool` with `runtime: "child_process"` serializes Error as plain objects via IPC.
51-
// (e.g. `{ name, message, stack, ... }`)
52-
// And napi-rs converts unknown JS values to Rust Error by calling `String()` on them,
53-
// which yields `"[object Object]"` for plain objects...
54-
// So, this function reconstructs a proper `Error` instance so napi-rs can extract the message.
55-
.catch((err) => {
56-
if (err instanceof Error) throw err;
57-
if (err !== null && typeof err === "object") {
58-
const obj = err as { name: string; message: string };
59-
const newErr = new Error(obj.message);
60-
newErr.name = obj.name;
61-
throw newErr;
62-
}
63-
throw new Error(String(err));
64-
})
65-
);
59+
return pool!
60+
.run({ options, code } satisfies FormatFileParam, { name: "formatFile" })
61+
.catch(rethrowAsError);
6662
}
6763

6864
export async function sortTailwindClasses(
6965
options: SortTailwindClassesArgs["options"],
7066
classes: string[],
7167
): Promise<string[]> {
72-
return pool!.run({ classes, options } satisfies SortTailwindClassesArgs, {
73-
name: "sortTailwindClasses",
74-
});
68+
return pool!
69+
.run({ classes, options } satisfies SortTailwindClassesArgs, { name: "sortTailwindClasses" })
70+
.catch(rethrowAsError);
71+
}
72+
73+
// ---
74+
75+
// `tinypool` with `runtime: "child_process"` serializes Error as plain objects via IPC.
76+
// (e.g. `{ name, message, stack, ... }`)
77+
// And napi-rs converts unknown JS values to Rust Error by calling `String()` on them,
78+
// which yields `"[object Object]"` for plain objects...
79+
// So, this function reconstructs a proper `Error` instance so napi-rs can extract the message.
80+
function rethrowAsError(err: unknown): never {
81+
if (err instanceof Error) throw err;
82+
if (err !== null && typeof err === "object") {
83+
const obj = err as { name: string; message: string };
84+
const newErr = new Error(obj.message);
85+
newErr.name = obj.name;
86+
throw newErr;
87+
}
88+
throw new Error(String(err));
7589
}

apps/oxfmt/src-js/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { format as napiFormat, jsTextToDoc as napiJsTextToDoc } from "./bindings";
2-
import { resolvePlugins, formatEmbeddedCode, formatFile, sortTailwindClasses } from "./libs/apis";
2+
import {
3+
resolvePlugins,
4+
formatEmbeddedCode,
5+
formatEmbeddedDoc,
6+
formatFile,
7+
sortTailwindClasses,
8+
} from "./libs/apis";
39
import type { Options } from "prettier";
410

511
// napi-JS `oxfmt` API entry point
@@ -18,6 +24,7 @@ export async function format(fileName: string, sourceText: string, options?: For
1824
options ?? {},
1925
resolvePlugins,
2026
(options, code) => formatEmbeddedCode({ options, code }),
27+
(options, texts) => formatEmbeddedDoc({ options, texts }),
2128
(options, code) => formatFile({ options, code }),
2229
(options, classes) => sortTailwindClasses({ options, classes }),
2330
);
@@ -39,6 +46,7 @@ export async function jsTextToDoc(
3946
parentContext,
4047
resolvePlugins,
4148
(options, code) => formatEmbeddedCode({ options, code }),
49+
(options, texts) => formatEmbeddedDoc({ options, texts }),
4250
(_options, _code) => Promise.reject(/* Unreachable */),
4351
(options, classes) => sortTailwindClasses({ options, classes }),
4452
);

apps/oxfmt/src-js/libs/apis.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,12 @@ export type FormatEmbeddedCodeParam = {
5050
};
5151

5252
/**
53-
* Format xxx-in-js code snippets
53+
* Format xxx-in-js code snippets into formatted string.
54+
*
55+
* This will be gradually replaced by `formatEmbeddedDoc` which returns `Doc`.
56+
* For now, html|css|md-in-js are using this.
5457
*
5558
* @returns Formatted code snippet
56-
* TODO: In the future, this should return `Doc` instead of string,
57-
* otherwise, we cannot calculate `printWidth` correctly.
5859
*/
5960
export async function formatEmbeddedCode({
6061
code,
@@ -74,6 +75,60 @@ export async function formatEmbeddedCode({
7475

7576
// ---
7677

78+
export type FormatEmbeddedDocParam = {
79+
texts: string[];
80+
options: Options;
81+
};
82+
83+
/**
84+
* Format xxx-in-js code snippets into Prettier `Doc` JSON strings.
85+
*
86+
* This makes `oxc_formatter` correctly handle `printWidth` even for embedded code.
87+
* - For gql-in-js, `texts` contains multiple parts split by `${}` in a template literal
88+
* - For others, `texts` always contains a single string with `${}` parts replaced by placeholders
89+
* However, this function does not need to be aware of that,
90+
* as it simply formats each text part independently and returns an array of formatted parts.
91+
*
92+
* @returns Doc JSON strings (one per input text)
93+
*/
94+
export async function formatEmbeddedDoc({
95+
texts,
96+
options,
97+
}: FormatEmbeddedDocParam): Promise<string[]> {
98+
const prettier = await loadPrettier();
99+
100+
// Enable Tailwind CSS plugin for embedded code (e.g., html`...` in JS) if needed
101+
await setupTailwindPlugin(options);
102+
103+
// NOTE: This will throw if:
104+
// - Specified parser is not available
105+
// - Or, code has syntax errors
106+
// In such cases, Rust side will fallback to original code
107+
return Promise.all(
108+
texts.map(async (text) => {
109+
// @ts-expect-error: Use internal API, but it's necessary and only way to get `Doc`
110+
const doc = await prettier.__debug.printToDoc(text, options);
111+
112+
// Serialize Doc to JSON, handling special values in a single pass:
113+
// - Symbol group IDs (used by `group`, `if-break`, `indent-if-break`) → numeric counters
114+
// - -Infinity (used by `dedentToRoot` via `align`) → marker string
115+
const symbolToNumber = new Map<symbol, number>();
116+
let nextId = 1;
117+
118+
return JSON.stringify(doc, (_key, value) => {
119+
if (typeof value === "symbol") {
120+
if (!symbolToNumber.has(value)) symbolToNumber.set(value, nextId++);
121+
return symbolToNumber.get(value);
122+
}
123+
if (value === -Infinity) return "__NEGATIVE_INFINITY__";
124+
return value;
125+
});
126+
}),
127+
);
128+
}
129+
130+
// ---
131+
77132
export type FormatFileParam = {
78133
code: string;
79134
options: Options;

apps/oxfmt/src/api/format_api.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ use serde_json::Value;
55
use oxc_napi::OxcError;
66

77
use crate::core::{
8-
ExternalFormatter, FormatFileStrategy, FormatResult, JsFormatEmbeddedCb, JsFormatFileCb,
9-
JsInitExternalFormatterCb, JsSortTailwindClassesCb, SourceFormatter,
8+
ExternalFormatter, FormatFileStrategy, FormatResult, JsFormatEmbeddedCb, JsFormatEmbeddedDocCb,
9+
JsFormatFileCb, JsInitExternalFormatterCb, JsSortTailwindClassesCb, SourceFormatter,
1010
resolve_options_from_value,
1111
};
1212

@@ -25,6 +25,7 @@ pub fn run(
2525
options: Option<Value>,
2626
init_external_formatter_cb: JsInitExternalFormatterCb,
2727
format_embedded_cb: JsFormatEmbeddedCb,
28+
format_embedded_doc_cb: JsFormatEmbeddedDocCb,
2829
format_file_cb: JsFormatFileCb,
2930
sort_tailwind_classes_cb: JsSortTailwindClassesCb,
3031
) -> ApiFormatResult {
@@ -37,6 +38,7 @@ pub fn run(
3738
let external_formatter = ExternalFormatter::new(
3839
init_external_formatter_cb,
3940
format_embedded_cb,
41+
format_embedded_doc_cb,
4042
format_file_cb,
4143
sort_tailwind_classes_cb,
4244
);

apps/oxfmt/src/api/text_to_doc_api.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ use oxc_span::SourceType;
1414

1515
use crate::{
1616
core::{
17-
ExternalFormatter, FormatFileStrategy, FormatResult, JsFormatEmbeddedCb, JsFormatFileCb,
18-
JsInitExternalFormatterCb, JsSortTailwindClassesCb, ResolvedOptions, SourceFormatter,
19-
resolve_options_from_value,
17+
ExternalFormatter, FormatFileStrategy, FormatResult, JsFormatEmbeddedCb,
18+
JsFormatEmbeddedDocCb, JsFormatFileCb, JsInitExternalFormatterCb, JsSortTailwindClassesCb,
19+
ResolvedOptions, SourceFormatter, resolve_options_from_value,
2020
},
2121
prettier_compat::to_prettier_doc,
2222
};
@@ -52,6 +52,7 @@ pub fn run(
5252
parent_context: &str,
5353
init_external_formatter_cb: JsInitExternalFormatterCb,
5454
format_embedded_cb: JsFormatEmbeddedCb,
55+
format_embedded_doc_cb: JsFormatEmbeddedDocCb,
5556
format_file_cb: JsFormatFileCb,
5657
sort_tailwind_classes_cb: JsSortTailwindClassesCb,
5758
) -> Option<String> {
@@ -72,6 +73,7 @@ pub fn run(
7273
oxfmt_plugin_options_json,
7374
init_external_formatter_cb,
7475
format_embedded_cb,
76+
format_embedded_doc_cb,
7577
format_file_cb,
7678
sort_tailwind_classes_cb,
7779
)?
@@ -92,6 +94,7 @@ fn run_full(
9294
oxfmt_plugin_options_json: &str,
9395
init_external_formatter_cb: JsInitExternalFormatterCb,
9496
format_embedded_cb: JsFormatEmbeddedCb,
97+
format_embedded_doc_cb: JsFormatEmbeddedDocCb,
9598
format_file_cb: JsFormatFileCb,
9699
sort_tailwind_classes_cb: JsSortTailwindClassesCb,
97100
) -> Option<Value> {
@@ -105,6 +108,7 @@ fn run_full(
105108
let external_formatter = ExternalFormatter::new(
106109
init_external_formatter_cb,
107110
format_embedded_cb,
111+
format_embedded_doc_cb,
108112
format_file_cb,
109113
sort_tailwind_classes_cb,
110114
);

0 commit comments

Comments
 (0)