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
79 changes: 79 additions & 0 deletions apps/oxfmt/cmp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// oxlint-disable

import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
import * as prettier from "prettier";
import { format as oxfmtFormat } from "./dist/index.js";

const FIXTURES_DIR = join(
import.meta.dirname,
"../../tasks/prettier_conformance/prettier/tests/format/js/multiparser-graphql",
);

const EXCLUDE = new Set([
"format.test.js",
"comment-tag.js", // /* GraphQL */ comment tag (not yet supported)
"expressions.js", // graphql() function call pattern (not yet supported)
"graphql.js", // graphql() function call pattern (not yet supported)
]);

const files = (await readdir(FIXTURES_DIR))
.filter((f) => f.endsWith(".js") && !EXCLUDE.has(f))
.sort();

let matchCount = 0;
let mismatchCount = 0;
let errorCount = 0;

for (const file of files) {
const filePath = join(FIXTURES_DIR, file);
const source = await readFile(filePath, "utf8");

const prettierOutput = await prettier.format(source, {
parser: "babel",
printWidth: 80,
});

let oxfmtOutput;
try {
const oxfmtResult = await oxfmtFormat(file, source, { printWidth: 80 });
oxfmtOutput = oxfmtResult.code;
} catch (e) {
console.log(`✗ ${file} (ERROR: ${e.message})`);
errorCount++;
continue;
}

if (prettierOutput === oxfmtOutput) {
console.log(`✓ ${file}`);
matchCount++;
} else {
console.log(`✗ ${file}`);
mismatchCount++;
printUnifiedDiff(prettierOutput, oxfmtOutput);
}
}

console.log(`\n--- Summary ---`);
console.log(
`Match: ${matchCount}, Mismatch: ${mismatchCount}, Error: ${errorCount}, Total: ${files.length}`,
);

function printUnifiedDiff(expected, actual) {
const expectedLines = expected.split("\n");
const actualLines = actual.split("\n");
console.log(" --- prettier");
console.log(" +++ oxfmt");
const maxLen = Math.max(expectedLines.length, actualLines.length);
for (let i = 0; i < maxLen; i++) {
const e = expectedLines[i];
const a = actualLines[i];
if (e === a) {
console.log(` ${e ?? ""}`);
} else {
if (e !== undefined) console.log(` -${e}`);
if (a !== undefined) console.log(` +${a}`);
}
}
console.log();
}
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 @@ -34,7 +34,7 @@ export declare const enum Severity {
* # Panics
* Panics if the current working directory cannot be determined.
*/
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>
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>

export interface FormatResult {
/** The formatted code. */
Expand All @@ -49,7 +49,7 @@ export interface FormatResult {
* This API is specialized for JS/TS snippets embedded in non-JS files.
* Unlike `format()`, it is called only for js-in-xxx `textToDoc()` flow.
*/
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>
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>

/**
* NAPI based JS CLI entry point.
Expand All @@ -66,4 +66,4 @@ export declare function jsTextToDoc(sourceExt: string, sourceText: string, oxfmt
* - `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>, 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]>
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]>
7 changes: 6 additions & 1 deletion apps/oxfmt/src-js/cli-worker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
// `oxfmt` CLI - Worker Thread Entry Point

// Re-exports core functions for use in `worker_threads`
export { formatEmbeddedCode, formatFile, sortTailwindClasses } from "./libs/apis";
export {
formatEmbeddedCode,
formatEmbeddedDoc,
formatFile,
sortTailwindClasses,
} from "./libs/apis";
2 changes: 2 additions & 0 deletions apps/oxfmt/src-js/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { runCli } from "./bindings";
import {
initExternalFormatter,
formatEmbeddedCode,
formatEmbeddedDoc,
formatFile,
sortTailwindClasses,
disposeExternalFormatter,
Expand All @@ -28,6 +29,7 @@ void (async () => {
args,
initExternalFormatter,
formatEmbeddedCode,
formatEmbeddedDoc,
formatFile,
sortTailwindClasses,
);
Expand Down
12 changes: 12 additions & 0 deletions apps/oxfmt/src-js/cli/worker-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Tinypool from "tinypool";
import { resolvePlugins } from "../libs/apis";
import type {
FormatEmbeddedCodeParam,
FormatEmbeddedDocParam,
FormatFileParam,
SortTailwindClassesArgs,
} from "../libs/apis";
Expand Down Expand Up @@ -40,6 +41,17 @@ export async function formatEmbeddedCode(
.catch(rethrowAsError);
}

export async function formatEmbeddedDoc(
options: FormatEmbeddedDocParam["options"],
texts: string[],
): Promise<string[]> {
return pool!
.run({ options, texts } satisfies FormatEmbeddedDocParam, {
name: "formatEmbeddedDoc",
})
.catch(rethrowAsError);
}

export async function formatFile(
options: FormatFileParam["options"],
code: string,
Expand Down
10 changes: 9 additions & 1 deletion apps/oxfmt/src-js/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { format as napiFormat, jsTextToDoc as napiJsTextToDoc } from "./bindings";
import { resolvePlugins, formatEmbeddedCode, formatFile, sortTailwindClasses } from "./libs/apis";
import {
resolvePlugins,
formatEmbeddedCode,
formatEmbeddedDoc,
formatFile,
sortTailwindClasses,
} from "./libs/apis";
import type { Options } from "prettier";

// napi-JS `oxfmt` API entry point
Expand All @@ -18,6 +24,7 @@ export async function format(fileName: string, sourceText: string, options?: For
options ?? {},
resolvePlugins,
(options, code) => formatEmbeddedCode({ options, code }),
(options, texts) => formatEmbeddedDoc({ options, texts }),
(options, code) => formatFile({ options, code }),
(options, classes) => sortTailwindClasses({ options, classes }),
);
Expand All @@ -39,6 +46,7 @@ export async function jsTextToDoc(
parentContext,
resolvePlugins,
(options, code) => formatEmbeddedCode({ options, code }),
(options, texts) => formatEmbeddedDoc({ options, texts }),
(_options, _code) => Promise.reject(/* Unreachable */),
(options, classes) => sortTailwindClasses({ options, classes }),
);
Expand Down
61 changes: 58 additions & 3 deletions apps/oxfmt/src-js/libs/apis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,12 @@ export type FormatEmbeddedCodeParam = {
};

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

// ---

export type FormatEmbeddedDocParam = {
texts: string[];
options: Options;
};

/**
* Format xxx-in-js code snippets into Prettier `Doc` JSON strings.
*
* This makes `oxc_formatter` correctly handle `printWidth` even for embedded code.
* - For gql-in-js, `texts` contains multiple parts split by `${}` in a template literal
* - For others, `texts` always contains a single string with `${}` parts replaced by placeholders
* However, this function does not need to be aware of that,
* as it simply formats each text part independently and returns an array of formatted parts.
*
* @returns Doc JSON strings (one per input text)
*/
export async function formatEmbeddedDoc({
texts,
options,
}: FormatEmbeddedDocParam): Promise<string[]> {
const prettier = await loadPrettier();

// Enable Tailwind CSS plugin for embedded code (e.g., html`...` in JS) if needed
await setupTailwindPlugin(options);

// NOTE: This will throw if:
// - Specified parser is not available
// - Or, code has syntax errors
// In such cases, Rust side will fallback to original code
return Promise.all(
texts.map(async (text) => {
// @ts-expect-error: Use internal API, but it's necessary and only way to get `Doc`
const doc = await prettier.__debug.printToDoc(text, options);

// Serialize Doc to JSON, handling special values in a single pass:
// - Symbol group IDs (used by `group`, `if-break`, `indent-if-break`) → numeric counters
// - -Infinity (used by `dedentToRoot` via `align`) → marker string
const symbolToNumber = new Map<symbol, number>();
let nextId = 1;

return JSON.stringify(doc, (_key, value) => {
if (typeof value === "symbol") {
if (!symbolToNumber.has(value)) symbolToNumber.set(value, nextId++);
return symbolToNumber.get(value);
}
if (value === -Infinity) return "__NEGATIVE_INFINITY__";
return value;
});
}),
);
}

// ---

export type FormatFileParam = {
code: string;
options: Options;
Expand Down
6 changes: 4 additions & 2 deletions apps/oxfmt/src/api/format_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use serde_json::Value;
use oxc_napi::OxcError;

use crate::core::{
ExternalFormatter, FormatFileStrategy, FormatResult, JsFormatEmbeddedCb, JsFormatFileCb,
JsInitExternalFormatterCb, JsSortTailwindClassesCb, SourceFormatter,
ExternalFormatter, FormatFileStrategy, FormatResult, JsFormatEmbeddedCb, JsFormatEmbeddedDocCb,
JsFormatFileCb, JsInitExternalFormatterCb, JsSortTailwindClassesCb, SourceFormatter,
resolve_options_from_value,
};

Expand All @@ -25,6 +25,7 @@ pub fn run(
options: Option<Value>,
init_external_formatter_cb: JsInitExternalFormatterCb,
format_embedded_cb: JsFormatEmbeddedCb,
format_embedded_doc_cb: JsFormatEmbeddedDocCb,
format_file_cb: JsFormatFileCb,
sort_tailwind_classes_cb: JsSortTailwindClassesCb,
) -> ApiFormatResult {
Expand All @@ -37,6 +38,7 @@ pub fn run(
let external_formatter = ExternalFormatter::new(
init_external_formatter_cb,
format_embedded_cb,
format_embedded_doc_cb,
format_file_cb,
sort_tailwind_classes_cb,
);
Expand Down
10 changes: 7 additions & 3 deletions apps/oxfmt/src/api/text_to_doc_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ use oxc_span::SourceType;

use crate::{
core::{
ExternalFormatter, FormatFileStrategy, FormatResult, JsFormatEmbeddedCb, JsFormatFileCb,
JsInitExternalFormatterCb, JsSortTailwindClassesCb, ResolvedOptions, SourceFormatter,
resolve_options_from_value,
ExternalFormatter, FormatFileStrategy, FormatResult, JsFormatEmbeddedCb,
JsFormatEmbeddedDocCb, JsFormatFileCb, JsInitExternalFormatterCb, JsSortTailwindClassesCb,
ResolvedOptions, SourceFormatter, resolve_options_from_value,
},
prettier_compat::to_prettier_doc,
};
Expand Down Expand Up @@ -52,6 +52,7 @@ pub fn run(
parent_context: &str,
init_external_formatter_cb: JsInitExternalFormatterCb,
format_embedded_cb: JsFormatEmbeddedCb,
format_embedded_doc_cb: JsFormatEmbeddedDocCb,
format_file_cb: JsFormatFileCb,
sort_tailwind_classes_cb: JsSortTailwindClassesCb,
) -> Option<String> {
Expand All @@ -72,6 +73,7 @@ pub fn run(
oxfmt_plugin_options_json,
init_external_formatter_cb,
format_embedded_cb,
format_embedded_doc_cb,
format_file_cb,
sort_tailwind_classes_cb,
)?
Expand All @@ -92,6 +94,7 @@ fn run_full(
oxfmt_plugin_options_json: &str,
init_external_formatter_cb: JsInitExternalFormatterCb,
format_embedded_cb: JsFormatEmbeddedCb,
format_embedded_doc_cb: JsFormatEmbeddedDocCb,
format_file_cb: JsFormatFileCb,
sort_tailwind_classes_cb: JsSortTailwindClassesCb,
) -> Option<Value> {
Expand All @@ -105,6 +108,7 @@ fn run_full(
let external_formatter = ExternalFormatter::new(
init_external_formatter_cb,
format_embedded_cb,
format_embedded_doc_cb,
format_file_cb,
sort_tailwind_classes_cb,
);
Expand Down
Loading
Loading