diff --git a/Cargo.lock b/Cargo.lock index 940ba6f62b404..d5dfdd32943ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2527,6 +2527,7 @@ dependencies = [ "oxc-schemars", "oxc-toml", "oxc_allocator", + "oxc_ast", "oxc_data_structures", "oxc_diagnostics", "oxc_formatter", diff --git a/apps/oxfmt/Cargo.toml b/apps/oxfmt/Cargo.toml index 536138d2e22ee..42ee3ddd17ec8 100644 --- a/apps/oxfmt/Cargo.toml +++ b/apps/oxfmt/Cargo.toml @@ -28,6 +28,7 @@ doctest = false [dependencies] oxc_allocator = { workspace = true, features = ["pool"] } +oxc_ast = { workspace = true } oxc_data_structures = { workspace = true, features = ["rope"] } oxc_diagnostics = { workspace = true } oxc_formatter = { workspace = true } diff --git a/apps/oxfmt/src-js/bindings.d.ts b/apps/oxfmt/src-js/bindings.d.ts index 7f1262eea4a45..6b35cca5eb9ff 100644 --- a/apps/oxfmt/src-js/bindings.d.ts +++ b/apps/oxfmt/src-js/bindings.d.ts @@ -47,12 +47,9 @@ export interface FormatResult { * NAPI based `textToDoc` API entry point for `prettier-plugin-oxfmt`. * * This API is specialized for JS/TS snippets embedded in non-JS files. - * Unlike `format()`, it is called only for JS/TS-in-xxx `textToDoc` flow. - * - * # Panics - * Panics if the current working directory cannot be determined. + * Unlike `format()`, it is called only for js-in-xxx `textToDoc()` flow. */ -export declare function jsTextToDoc(filename: string, sourceText: string, oxfmtPluginOptionsJson: string, parentContext: string, initExternalFormatterCb: (numThreads: number) => Promise, formatEmbeddedCb: (options: Record, code: string) => Promise, formatFileCb: (options: Record, code: string) => Promise, sortTailwindClassesCb: (options: Record, classes: string[]) => Promise): Promise +export declare function jsTextToDoc(sourceExt: string, sourceText: string, oxfmtPluginOptionsJson: string, parentContext: string, initExternalFormatterCb: (numThreads: number) => Promise, formatEmbeddedCb: (options: Record, code: string) => Promise, formatFileCb: (options: Record, code: string) => Promise, sortTailwindClassesCb: (options: Record, classes: string[]) => Promise): Promise /** * NAPI based JS CLI entry point. @@ -70,10 +67,3 @@ export declare function jsTextToDoc(filename: string, sourceText: string, oxfmtP * - `exitCode`: If main logic already ran in Rust side, return the exit code */ export declare function runCli(args: Array, initExternalFormatterCb: (numThreads: number) => Promise, formatEmbeddedCb: (options: Record, code: string) => Promise, formatFileCb: (options: Record, code: string) => Promise, sortTailwindcssClassesCb: (options: Record, classes: string[]) => Promise): Promise<[string, number | undefined | null]> - -export interface TextToDocResult { - /** The formatted code. */ - doc: string - /** Parse and format errors. */ - errors: Array -} diff --git a/apps/oxfmt/src-js/index.ts b/apps/oxfmt/src-js/index.ts index 746b2f4b4796e..98ecd6fec9d09 100644 --- a/apps/oxfmt/src-js/index.ts +++ b/apps/oxfmt/src-js/index.ts @@ -27,13 +27,13 @@ export async function format(fileName: string, sourceText: string, options?: For * Format a JS/TS snippet for Prettier `textToDoc()` plugin flow. */ export async function jsTextToDoc( - fileName: string, + sourceExt: string, sourceText: string, oxfmtPluginOptionsJson: string, parentContext: string, ) { return napiJsTextToDoc( - fileName, + sourceExt, sourceText, oxfmtPluginOptionsJson, parentContext, diff --git a/apps/oxfmt/src-js/libs/prettier-plugin-oxfmt/index.ts b/apps/oxfmt/src-js/libs/prettier-plugin-oxfmt/index.ts index 287f63f2f2698..8b14da4bafa42 100644 --- a/apps/oxfmt/src-js/libs/prettier-plugin-oxfmt/index.ts +++ b/apps/oxfmt/src-js/libs/prettier-plugin-oxfmt/index.ts @@ -32,6 +32,7 @@ const oxfmtParser: Parser = { export const parsers: Record = { // Override default JS/TS parsers babel: oxfmtParser, + "babel-ts": oxfmtParser, typescript: oxfmtParser, }; diff --git a/apps/oxfmt/src-js/libs/prettier-plugin-oxfmt/text-to-doc.ts b/apps/oxfmt/src-js/libs/prettier-plugin-oxfmt/text-to-doc.ts index 087450ac61480..43640f24f77fe 100644 --- a/apps/oxfmt/src-js/libs/prettier-plugin-oxfmt/text-to-doc.ts +++ b/apps/oxfmt/src-js/libs/prettier-plugin-oxfmt/text-to-doc.ts @@ -1,34 +1,54 @@ -import { doc } from "prettier"; import { jsTextToDoc } from "../../index"; import type { Parser, Doc } from "prettier"; -const { hardline, join } = doc.builders; -const LINE_BREAK_RE = /\r?\n/; - export const textToDoc: Parser["parse"] = async (embeddedSourceText, textToDocOptions) => { - // NOTE: For (j|t)s-in-xxx, default `parser` is either `babel` or `typescript` - // In case of ts-in-md, `filepath` is overridden to distinguish TSX or TS - // We need to infer `SourceType::from_path(fileName)` for `oxc_formatter`. + // `_oxfmtPluginOptionsJson` is a JSON string bundled by Rust (`oxfmtrc::finalize_external_options`), + // containing format options + parent filepath for the Rust-side `oxc_formatter`. const { parser, parentParser, filepath, _oxfmtPluginOptionsJson } = textToDocOptions; - const fileName = - parser === "typescript" - ? filepath.endsWith(".tsx") - ? "dummy.tsx" // tsx-in-md - : "dummy.ts" // ts-in-md / ts-in-xxx - : "dummy.jsx"; // Otherwise, always enable JSX for js-in-xxx, it's safe - - const { doc: formattedText, errors } = await jsTextToDoc( - fileName, + + // For (j|t)s-in-xxx, default `parser` is either `babel`, `babel-ts` or `typescript` + // We need to infer `SourceType::from_extension(ext)` for `oxc_formatter`. + // - JS: always enable JSX for js-in-xxx, it's safe + // - TS: `typescript` (ts-in-vue|markdown|mdx) or `babel-ts` (ts-in-vue(script generic="...")) + // - In case of ts-in-md, `filepath` is overridden as `dummy.tx(x)` to distinguish TSX or TS + // - NOTE: tsx-in-vue is not supported since there is no signal from Prettier to detect it + // - Prettier is using `maybeJSXRe.test(sourceText)` to detect, but it's slow! + const isTS = parser === "typescript" || parser === "babel-ts"; + const embeddedSourceExt = isTS ? (filepath?.endsWith(".tsx") ? "tsx" : "ts") : "jsx"; + + // Detect context from Prettier's internal flags + const parentContext = detectParentContext(parentParser!, textToDocOptions); + + const doc = await jsTextToDoc( + embeddedSourceExt, embeddedSourceText, _oxfmtPluginOptionsJson as string, - [parentParser].join(":"), + parentContext, ); - if (0 < errors.length) throw new Error(errors[0].message); + if (doc === null) { + throw new Error("`oxfmt::textToDoc()` failed. Use `OXC_LOG` env var to see Rust-side logs."); + } - // NOTE: This is required for the parent ((j|t)s-in-xxx) printer - // to handle line breaks correctly, - // not only for `options.vueIndentScriptAndStyle` but also for basic printing. - // TODO: Will be handled in Rust, convert our IR to Prettier's Doc directly. - return join(hardline, formattedText.split(LINE_BREAK_RE)); + // SAFETY: Rust side returns Prettier's `Doc` JSON + return JSON.parse(doc) as Doc; }; + +/** + * Detects Vue fragment mode from Prettier's internal flags. + * + * When Prettier formats Vue SFC templates, it calls textToDoc with special flags: + * - `__isVueForBindingLeft`: v-for left-hand side (e.g., `(item, index)` in `v-for="(item, index) in items"`) + * - `__isVueBindings`: v-slot bindings (e.g., `{ item }` in `#default="{ item }"`) + * - `__isEmbeddedTypescriptGenericParameters`: `