diff --git a/apps/oxfmt/src-js/cli.ts b/apps/oxfmt/src-js/cli.ts index 975aa8d814f34..7efe4011880e3 100644 --- a/apps/oxfmt/src-js/cli.ts +++ b/apps/oxfmt/src-js/cli.ts @@ -40,6 +40,10 @@ void (async () => { await import("./cli/migration/migrate-prettier").then((m) => m.runMigratePrettier()); return; } + if (mode === "migrate:biome") { + await import("./cli/migration/migrate-biome").then((m) => m.runMigrateBiome()); + return; + } // Other modes are handled by Rust, just need to set `exitCode` diff --git a/apps/oxfmt/src-js/cli/migration/migrate-biome.ts b/apps/oxfmt/src-js/cli/migration/migrate-biome.ts new file mode 100644 index 0000000000000..3602c0255588b --- /dev/null +++ b/apps/oxfmt/src-js/cli/migration/migrate-biome.ts @@ -0,0 +1,426 @@ +/* oxlint-disable no-console */ + +import { join } from "node:path"; +import { readFile } from "node:fs/promises"; +import { hasOxfmtrcFile, createBlankOxfmtrcFile, saveOxfmtrcFile, exitWithError } from "./shared"; + +interface BiomeConfig { + formatter?: BiomeFormatterConfig; + javascript?: { + formatter?: BiomeJsFormatterConfig; + }; + files?: { + includes?: string[]; + }; + overrides?: BiomeOverride[]; +} + +interface BiomeFormatterConfig { + enabled?: boolean; + indentStyle?: "tab" | "space"; + indentWidth?: number; + lineWidth?: number; + lineEnding?: "lf" | "crlf" | "cr"; + attributePosition?: "auto" | "multiline"; + bracketSpacing?: boolean; +} + +interface BiomeJsFormatterConfig extends BiomeFormatterConfig { + quoteStyle?: "single" | "double"; + jsxQuoteStyle?: "single" | "double"; + quoteProperties?: "asNeeded" | "preserve"; + trailingCommas?: "all" | "es5" | "none"; + semicolons?: "always" | "asNeeded"; + arrowParentheses?: "always" | "asNeeded"; + bracketSameLine?: boolean; +} + +interface BiomeOverride { + includes?: string[]; + formatter?: BiomeFormatterConfig; + javascript?: { + formatter?: BiomeJsFormatterConfig; + }; +} + +const BIOME_DEFAULTS = { + lineWidth: 80, + indentStyle: "tab", + indentWidth: 2, + lineEnding: "lf", + attributePosition: "auto", + bracketSpacing: true, + quoteStyle: "double", + jsxQuoteStyle: "double", + quoteProperties: "asNeeded", + trailingCommas: "all", + semicolons: "always", + arrowParentheses: "always", + bracketSameLine: false, +} as const; + +/** + * Run the `--migrate biome` command to migrate Biome's config to `.oxfmtrc.json` file. + * https://biomejs.dev/reference/configuration/ + */ +export async function runMigrateBiome() { + const cwd = process.cwd(); + + if (await hasOxfmtrcFile(cwd)) { + return exitWithError("Oxfmt configuration file already exists."); + } + + const biomeConfigPath = await resolveBiomeConfigFile(cwd); + + // No Biome config found, fallback with `--init` behavior + if (!biomeConfigPath) { + console.log("No Biome configuration file found."); + + const oxfmtrc = await createBlankOxfmtrcFile(cwd); + const jsonStr = JSON.stringify(oxfmtrc, null, 2); + + // TODO: Create napi `validateConfig()` and use to ensure validity? + + try { + await saveOxfmtrcFile(cwd, jsonStr); + console.log("Created `.oxfmtrc.json` instead."); + } catch { + exitWithError("Failed to create `.oxfmtrc.json`."); + } + + return; + } + + let biomeConfig: BiomeConfig; + try { + const content = await readFile(biomeConfigPath, "utf8"); + // Biome supports JSONC (JSON with comments) + const cleanedContent = stripJsonComments(content); + biomeConfig = JSON.parse(cleanedContent); + console.log("Found Biome configuration at:", biomeConfigPath); + } catch { + return exitWithError(`Failed to parse: ${biomeConfigPath}`); + } + + // Start with blank, then fill in from `biomeConfig`. + // NOTE: Biome has two levels of formatter config: + // - `formatter.*` for global options + // - `javascript.formatter.*` for JS/TS specific options (takes precedence) + const oxfmtrc = await createBlankOxfmtrcFile(cwd); + const formatterConfig = biomeConfig.formatter ?? {}; + const jsFormatterConfig = biomeConfig.javascript?.formatter ?? {}; + + migrateIndentStyle(formatterConfig, jsFormatterConfig, oxfmtrc); + migrateIndentWidth(formatterConfig, jsFormatterConfig, oxfmtrc); + migrateLineWidth(formatterConfig, jsFormatterConfig, oxfmtrc); + migrateQuoteStyle(jsFormatterConfig, oxfmtrc); + migrateJsxQuoteStyle(jsFormatterConfig, oxfmtrc); + migrateQuoteProperties(jsFormatterConfig, oxfmtrc); + migrateTrailingCommas(jsFormatterConfig, oxfmtrc); + migrateSemicolons(jsFormatterConfig, oxfmtrc); + migrateArrowParentheses(jsFormatterConfig, oxfmtrc); + migrateBracketSameLine(formatterConfig, jsFormatterConfig, oxfmtrc); + migrateBracketSpacing(formatterConfig, jsFormatterConfig, oxfmtrc); + migrateAttributePosition(formatterConfig, jsFormatterConfig, oxfmtrc); + + // Migrate ignore patterns from `files.includes` negated patterns + const ignores = extractIgnorePatterns(biomeConfig); + if (ignores.length > 0) { + console.log("Migrated ignore patterns from Biome config"); + } + // Keep ignorePatterns at the bottom + delete oxfmtrc.ignorePatterns; + oxfmtrc.ignorePatterns = ignores; + + // TODO: Oxfmt now supports `overrides`, + // but automatic migration is complex due to different config structures. + if (biomeConfig.overrides && biomeConfig.overrides.length > 0) { + console.warn(` - "overrides" cannot be migrated automatically yet`); + } + + const jsonStr = JSON.stringify(oxfmtrc, null, 2); + + // TODO: Create napi `validateConfig()` and use to ensure validity? + + try { + await saveOxfmtrcFile(cwd, jsonStr); + console.log("Created `.oxfmtrc.json`."); + } catch { + return exitWithError("Failed to create `.oxfmtrc.json`."); + } +} + +// --- + +async function resolveBiomeConfigFile(cwd: string): Promise { + // Biome supports both `biome.json` and `biome.jsonc`. + // If both exist, `biome.json` takes priority. + const candidates = ["biome.json", "biome.jsonc"]; + + for (const filename of candidates) { + const filepath = join(cwd, filename); + try { + // oxlint-disable-next-line no-await-in-loop -- sequential check is intentional + await readFile(filepath, "utf8"); + return filepath; + } catch {} + } + + return null; +} + +function stripJsonComments(content: string): string { + let result = ""; + let i = 0; + let inString = false; + let escapeNext = false; + + while (i < content.length) { + const char = content[i]; + const nextChar = content[i + 1]; + + if (escapeNext) { + result += char; + escapeNext = false; + i++; + continue; + } + + if (char === "\\" && inString) { + result += char; + escapeNext = true; + i++; + continue; + } + + if (char === '"') { + inString = !inString; + result += char; + i++; + continue; + } + + if (!inString) { + if (char === "/" && nextChar === "/") { + while (i < content.length && content[i] !== "\n") { + i++; + } + continue; + } + + if (char === "/" && nextChar === "*") { + i += 2; + while (i < content.length - 1 && !(content[i] === "*" && content[i + 1] === "/")) { + i++; + } + i += 2; + continue; + } + } + + result += char; + i++; + } + + return result; +} + +// --- + +// `indentStyle` -> `useTabs` +function migrateIndentStyle( + formatterConfig: BiomeFormatterConfig, + jsFormatterConfig: BiomeJsFormatterConfig, + oxfmtrc: Record, +): void { + const value = jsFormatterConfig.indentStyle ?? formatterConfig.indentStyle; + if (value !== undefined) { + oxfmtrc.useTabs = value === "tab"; + } else { + // Biome default is "tab" + oxfmtrc.useTabs = BIOME_DEFAULTS.indentStyle === "tab"; + } +} + +// `indentWidth` -> `tabWidth` +function migrateIndentWidth( + formatterConfig: BiomeFormatterConfig, + jsFormatterConfig: BiomeJsFormatterConfig, + oxfmtrc: Record, +): void { + const value = jsFormatterConfig.indentWidth ?? formatterConfig.indentWidth; + if (value !== undefined) { + oxfmtrc.tabWidth = value; + } else { + oxfmtrc.tabWidth = BIOME_DEFAULTS.indentWidth; + } +} + +// `lineWidth` -> `printWidth` +function migrateLineWidth( + formatterConfig: BiomeFormatterConfig, + jsFormatterConfig: BiomeJsFormatterConfig, + oxfmtrc: Record, +): void { + const value = jsFormatterConfig.lineWidth ?? formatterConfig.lineWidth; + if (value !== undefined) { + oxfmtrc.printWidth = value; + } else { + // Biome default is 80, Oxfmt default is 100 + oxfmtrc.printWidth = BIOME_DEFAULTS.lineWidth; + } +} + +// `quoteStyle` -> `singleQuote` +function migrateQuoteStyle( + jsFormatterConfig: BiomeJsFormatterConfig, + oxfmtrc: Record, +): void { + const value = jsFormatterConfig.quoteStyle; + if (value !== undefined) { + oxfmtrc.singleQuote = value === "single"; + } else { + // Biome default is "double" + oxfmtrc.singleQuote = false; + } +} + +// `jsxQuoteStyle` -> `jsxSingleQuote` +function migrateJsxQuoteStyle( + jsFormatterConfig: BiomeJsFormatterConfig, + oxfmtrc: Record, +): void { + const value = jsFormatterConfig.jsxQuoteStyle; + if (value !== undefined) { + oxfmtrc.jsxSingleQuote = value === "single"; + } else { + // Biome default is "double" + oxfmtrc.jsxSingleQuote = false; + } +} + +// `quoteProperties` -> `quoteProps` +// Biome uses "asNeeded", Oxfmt uses "as-needed" +function migrateQuoteProperties( + jsFormatterConfig: BiomeJsFormatterConfig, + oxfmtrc: Record, +): void { + const value = jsFormatterConfig.quoteProperties; + if (value !== undefined) { + if (value === "asNeeded") { + oxfmtrc.quoteProps = "as-needed"; + } else if (value === "preserve") { + oxfmtrc.quoteProps = "preserve"; + } + } else { + oxfmtrc.quoteProps = "as-needed"; + } +} + +// `trailingCommas` -> `trailingComma` +function migrateTrailingCommas( + jsFormatterConfig: BiomeJsFormatterConfig, + oxfmtrc: Record, +): void { + const value = jsFormatterConfig.trailingCommas; + if (value !== undefined) { + oxfmtrc.trailingComma = value; + } else { + oxfmtrc.trailingComma = BIOME_DEFAULTS.trailingCommas; + } +} + +// `semicolons` -> `semi` +function migrateSemicolons( + jsFormatterConfig: BiomeJsFormatterConfig, + oxfmtrc: Record, +): void { + const value = jsFormatterConfig.semicolons; + if (value !== undefined) { + oxfmtrc.semi = value === "always"; + } else { + // Biome default is "always" + oxfmtrc.semi = BIOME_DEFAULTS.semicolons === "always"; + } +} + +// `arrowParentheses` -> `arrowParens` +// Biome uses "asNeeded", Oxfmt uses "avoid" +function migrateArrowParentheses( + jsFormatterConfig: BiomeJsFormatterConfig, + oxfmtrc: Record, +): void { + const value = jsFormatterConfig.arrowParentheses; + if (value !== undefined) { + if (value === "always") { + oxfmtrc.arrowParens = "always"; + } else if (value === "asNeeded") { + oxfmtrc.arrowParens = "avoid"; + } + } else { + // Biome default is "always" + oxfmtrc.arrowParens = BIOME_DEFAULTS.arrowParentheses === "always" ? "always" : "avoid"; + } +} + +// `bracketSameLine` +function migrateBracketSameLine( + formatterConfig: BiomeFormatterConfig, + jsFormatterConfig: BiomeJsFormatterConfig, + oxfmtrc: Record, +): void { + const value = jsFormatterConfig.bracketSameLine; + if (value !== undefined) { + oxfmtrc.bracketSameLine = value; + } else { + oxfmtrc.bracketSameLine = BIOME_DEFAULTS.bracketSameLine; + } +} + +// `bracketSpacing` +function migrateBracketSpacing( + formatterConfig: BiomeFormatterConfig, + jsFormatterConfig: BiomeJsFormatterConfig, + oxfmtrc: Record, +): void { + const value = jsFormatterConfig.bracketSpacing ?? formatterConfig.bracketSpacing; + if (value !== undefined) { + oxfmtrc.bracketSpacing = value; + } else { + oxfmtrc.bracketSpacing = BIOME_DEFAULTS.bracketSpacing; + } +} + +// `attributePosition` -> `singleAttributePerLine` +function migrateAttributePosition( + formatterConfig: BiomeFormatterConfig, + jsFormatterConfig: BiomeJsFormatterConfig, + oxfmtrc: Record, +): void { + const value = jsFormatterConfig.attributePosition ?? formatterConfig.attributePosition; + if (value !== undefined) { + if (value === "multiline") { + oxfmtrc.singleAttributePerLine = true; + } else { + oxfmtrc.singleAttributePerLine = false; + } + } +} + +// --- + +function extractIgnorePatterns(biomeConfig: BiomeConfig): string[] { + // In Biome, patterns starting with `!` (but not `!!`) are used to exclude files. + // These are converted to Oxfmt's `ignorePatterns` format. + const ignores: string[] = []; + + if (biomeConfig.files?.includes) { + for (const pattern of biomeConfig.files.includes) { + if (pattern.startsWith("!") && !pattern.startsWith("!!")) { + ignores.push(pattern.slice(1)); + } + } + } + + return ignores; +} diff --git a/apps/oxfmt/src/cli/command.rs b/apps/oxfmt/src/cli/command.rs index f384265de44da..98355e786ea9e 100644 --- a/apps/oxfmt/src/cli/command.rs +++ b/apps/oxfmt/src/cli/command.rs @@ -70,11 +70,12 @@ fn mode() -> impl bpaf::Parser { .req_flag(Mode::Init) .hide_usage(); let migrate = bpaf::long("migrate") - .help("Migrate configuration to `.oxfmtrc.json` from specified source\nAvailable sources: prettier") + .help("Migrate configuration to `.oxfmtrc.json` from specified source\nAvailable sources: prettier, biome") .argument::("SOURCE") .parse(|s| match s.cow_to_lowercase().as_ref() { "prettier" => Ok(Mode::Migrate(MigrateSource::Prettier)), - _ => Err(format!("Unknown migration source: {s}. Supported: prettier.")), + "biome" => Ok(Mode::Migrate(MigrateSource::Biome)), + _ => Err(format!("Unknown migration source: {s}. Supported: prettier, biome.")), }) .hide_usage(); let lsp = bpaf::long("lsp") @@ -131,6 +132,8 @@ fn output_mode() -> impl bpaf::Parser { pub enum MigrateSource { /// Migrate from Prettier configuration Prettier, + /// Migrate from Biome configuration + Biome, } // --- diff --git a/apps/oxfmt/src/main_napi.rs b/apps/oxfmt/src/main_napi.rs index eeaae1c2a6342..4f12983ad77c9 100644 --- a/apps/oxfmt/src/main_napi.rs +++ b/apps/oxfmt/src/main_napi.rs @@ -7,7 +7,7 @@ use oxc_napi::OxcError; use serde_json::Value; use crate::{ - cli::{FormatRunner, Mode, format_command, init_miette, init_rayon}, + cli::{FormatRunner, MigrateSource, Mode, format_command, init_miette, init_rayon}, core::{ ExternalFormatter, FormatFileStrategy, FormatResult as CoreFormatResult, JsFormatEmbeddedCb, JsFormatFileCb, JsInitExternalFormatterCb, JsSortTailwindClassesCb, @@ -69,8 +69,12 @@ pub async fn run_cli( Mode::Init => { return ("init".to_string(), None); } - Mode::Migrate(_) => { - return ("migrate:prettier".to_string(), None); + Mode::Migrate(source) => { + let mode_str = match source { + MigrateSource::Prettier => "migrate:prettier", + MigrateSource::Biome => "migrate:biome", + }; + return (mode_str.to_string(), None); } _ => {} } diff --git a/apps/oxfmt/test/cli/migrate_biome/migrate_biome.test.ts b/apps/oxfmt/test/cli/migrate_biome/migrate_biome.test.ts new file mode 100644 index 0000000000000..dbe07a2384a2e --- /dev/null +++ b/apps/oxfmt/test/cli/migrate_biome/migrate_biome.test.ts @@ -0,0 +1,324 @@ +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import fs from "node:fs/promises"; +import { describe, expect, it } from "vitest"; +import { runCli } from "../utils"; + +describe("--migrate biome", () => { + it("should create .oxfmtrc.json when no biome config exists", async () => { + const tempDir = await fs.mkdtemp(join(tmpdir(), "oxfmt-migrate-biome-test")); + + try { + const result = await runCli(tempDir, ["--migrate", "biome"]); + expect(result.exitCode).toBe(0); + + const content = await fs.readFile(join(tempDir, ".oxfmtrc.json"), "utf8"); + const oxfmtrc = JSON.parse(content); + + expect(oxfmtrc.ignorePatterns).toEqual([]); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); + + it("should abort if .oxfmtrc.json already exists", async () => { + const tempDir = await fs.mkdtemp(join(tmpdir(), "oxfmt-migrate-biome-test")); + + try { + await fs.writeFile(join(tempDir, ".oxfmtrc.json"), "{}"); + + const result = await runCli(tempDir, ["--migrate", "biome"]); + expect(result.exitCode).toBe(1); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); + + it("should migrate biome.json config to .oxfmtrc.json", async () => { + const tempDir = await fs.mkdtemp(join(tmpdir(), "oxfmt-migrate-biome-test")); + + try { + await fs.writeFile( + join(tempDir, "biome.json"), + JSON.stringify({ + formatter: { + lineWidth: 120, + indentStyle: "space", + indentWidth: 4, + }, + javascript: { + formatter: { + quoteStyle: "single", + semicolons: "asNeeded", + }, + }, + }), + ); + + const result = await runCli(tempDir, ["--migrate", "biome"]); + expect(result.exitCode).toBe(0); + + const content = await fs.readFile(join(tempDir, ".oxfmtrc.json"), "utf8"); + const oxfmtrc = JSON.parse(content); + + expect(oxfmtrc.printWidth).toBe(120); + expect(oxfmtrc.useTabs).toBe(false); + expect(oxfmtrc.tabWidth).toBe(4); + expect(oxfmtrc.singleQuote).toBe(true); + expect(oxfmtrc.semi).toBe(false); + expect(oxfmtrc.ignorePatterns).toEqual([]); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); + + it("should migrate biome.jsonc config with comments", async () => { + const tempDir = await fs.mkdtemp(join(tmpdir(), "oxfmt-migrate-biome-test")); + + try { + await fs.writeFile( + join(tempDir, "biome.jsonc"), + `{ + // This is a comment + "formatter": { + "lineWidth": 100, + /* Multi-line + comment */ + "indentStyle": "tab" + } + }`, + ); + + const result = await runCli(tempDir, ["--migrate", "biome"]); + expect(result.exitCode).toBe(0); + + const content = await fs.readFile(join(tempDir, ".oxfmtrc.json"), "utf8"); + const oxfmtrc = JSON.parse(content); + + expect(oxfmtrc.printWidth).toBe(100); + expect(oxfmtrc.useTabs).toBe(true); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); + + it("should prefer biome.json over biome.jsonc when both exist", async () => { + const tempDir = await fs.mkdtemp(join(tmpdir(), "oxfmt-migrate-biome-test")); + + try { + await fs.writeFile( + join(tempDir, "biome.json"), + JSON.stringify({ + formatter: { lineWidth: 80 }, + }), + ); + await fs.writeFile( + join(tempDir, "biome.jsonc"), + JSON.stringify({ + formatter: { lineWidth: 120 }, + }), + ); + + const result = await runCli(tempDir, ["--migrate", "biome"]); + expect(result.exitCode).toBe(0); + + const content = await fs.readFile(join(tempDir, ".oxfmtrc.json"), "utf8"); + const oxfmtrc = JSON.parse(content); + + expect(oxfmtrc.printWidth).toBe(80); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); + + it("should use Biome defaults when options are not specified", async () => { + const tempDir = await fs.mkdtemp(join(tmpdir(), "oxfmt-migrate-biome-test")); + + try { + await fs.writeFile(join(tempDir, "biome.json"), JSON.stringify({})); + + const result = await runCli(tempDir, ["--migrate", "biome"]); + expect(result.exitCode).toBe(0); + + const content = await fs.readFile(join(tempDir, ".oxfmtrc.json"), "utf8"); + const oxfmtrc = JSON.parse(content); + + expect(oxfmtrc.printWidth).toBe(80); + expect(oxfmtrc.useTabs).toBe(true); + expect(oxfmtrc.tabWidth).toBe(2); + expect(oxfmtrc.singleQuote).toBe(false); + expect(oxfmtrc.jsxSingleQuote).toBe(false); + expect(oxfmtrc.quoteProps).toBe("as-needed"); + expect(oxfmtrc.trailingComma).toBe("all"); + expect(oxfmtrc.semi).toBe(true); + expect(oxfmtrc.arrowParens).toBe("always"); + expect(oxfmtrc.bracketSameLine).toBe(false); + expect(oxfmtrc.bracketSpacing).toBe(true); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); + + it("should prefer javascript.formatter options over formatter options", async () => { + const tempDir = await fs.mkdtemp(join(tmpdir(), "oxfmt-migrate-biome-test")); + + try { + await fs.writeFile( + join(tempDir, "biome.json"), + JSON.stringify({ + formatter: { + lineWidth: 80, + indentWidth: 2, + }, + javascript: { + formatter: { + lineWidth: 120, + indentWidth: 4, + }, + }, + }), + ); + + const result = await runCli(tempDir, ["--migrate", "biome"]); + expect(result.exitCode).toBe(0); + + const content = await fs.readFile(join(tempDir, ".oxfmtrc.json"), "utf8"); + const oxfmtrc = JSON.parse(content); + + expect(oxfmtrc.printWidth).toBe(120); + expect(oxfmtrc.tabWidth).toBe(4); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); + + it("should migrate arrowParentheses correctly", async () => { + const tempDir = await fs.mkdtemp(join(tmpdir(), "oxfmt-migrate-biome-test")); + + try { + await fs.writeFile( + join(tempDir, "biome.json"), + JSON.stringify({ + javascript: { + formatter: { + arrowParentheses: "asNeeded", + }, + }, + }), + ); + + const result = await runCli(tempDir, ["--migrate", "biome"]); + expect(result.exitCode).toBe(0); + + const content = await fs.readFile(join(tempDir, ".oxfmtrc.json"), "utf8"); + const oxfmtrc = JSON.parse(content); + + expect(oxfmtrc.arrowParens).toBe("avoid"); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); + + it("should migrate attributePosition to singleAttributePerLine", async () => { + const tempDir = await fs.mkdtemp(join(tmpdir(), "oxfmt-migrate-biome-test")); + + try { + await fs.writeFile( + join(tempDir, "biome.json"), + JSON.stringify({ + formatter: { + attributePosition: "multiline", + }, + }), + ); + + const result = await runCli(tempDir, ["--migrate", "biome"]); + expect(result.exitCode).toBe(0); + + const content = await fs.readFile(join(tempDir, ".oxfmtrc.json"), "utf8"); + const oxfmtrc = JSON.parse(content); + + expect(oxfmtrc.singleAttributePerLine).toBe(true); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); + + it("should migrate ignore patterns from files.includes", async () => { + const tempDir = await fs.mkdtemp(join(tmpdir(), "oxfmt-migrate-biome-test")); + + try { + await fs.writeFile( + join(tempDir, "biome.json"), + JSON.stringify({ + files: { + includes: ["**", "!dist", "!node_modules", "!*.min.js"], + }, + }), + ); + + const result = await runCli(tempDir, ["--migrate", "biome"]); + expect(result.exitCode).toBe(0); + + const content = await fs.readFile(join(tempDir, ".oxfmtrc.json"), "utf8"); + const oxfmtrc = JSON.parse(content); + + expect(oxfmtrc.ignorePatterns).toEqual(["dist", "node_modules", "*.min.js"]); + expect(Object.keys(oxfmtrc).at(-1)).toBe("ignorePatterns"); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); + + it("should not include force-ignore patterns (starting with !!) in ignorePatterns", async () => { + const tempDir = await fs.mkdtemp(join(tmpdir(), "oxfmt-migrate-biome-test")); + + try { + await fs.writeFile( + join(tempDir, "biome.json"), + JSON.stringify({ + files: { + includes: ["**", "!dist", "!!build"], + }, + }), + ); + + const result = await runCli(tempDir, ["--migrate", "biome"]); + expect(result.exitCode).toBe(0); + + const content = await fs.readFile(join(tempDir, ".oxfmtrc.json"), "utf8"); + const oxfmtrc = JSON.parse(content); + + expect(oxfmtrc.ignorePatterns).toEqual(["dist"]); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); + + it("should warn about overrides that cannot be migrated", async () => { + const tempDir = await fs.mkdtemp(join(tmpdir(), "oxfmt-migrate-biome-test")); + + try { + await fs.writeFile( + join(tempDir, "biome.json"), + JSON.stringify({ + overrides: [ + { + includes: ["generated/**"], + formatter: { + lineWidth: 160, + }, + }, + ], + }), + ); + + const result = await runCli(tempDir, ["--migrate", "biome"]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toContain("overrides"); + expect(result.stderr).toContain("cannot be migrated automatically"); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); +}); diff --git a/tasks/website_formatter/src/snapshots/cli.snap b/tasks/website_formatter/src/snapshots/cli.snap index 6c31c8b58feff..033877ccf43b5 100644 --- a/tasks/website_formatter/src/snapshots/cli.snap +++ b/tasks/website_formatter/src/snapshots/cli.snap @@ -1,5 +1,6 @@ --- source: tasks/website_formatter/src/cli.rs +assertion_line: 8 expression: snapshot --- --- @@ -14,7 +15,7 @@ search: false - **` --init`** — Initialize `.oxfmtrc.json` with default values - **` --migrate`**=_`SOURCE`_ — - Migrate configuration to `.oxfmtrc.json` from specified source Available sources: prettier + Migrate configuration to `.oxfmtrc.json` from specified source Available sources: prettier, biome - **` --lsp`** — Start language server protocol (LSP) server - **` --stdin-filepath`**=_`PATH`_ — diff --git a/tasks/website_formatter/src/snapshots/cli_terminal.snap b/tasks/website_formatter/src/snapshots/cli_terminal.snap index abc09894f7757..b9d061c256e26 100644 --- a/tasks/website_formatter/src/snapshots/cli_terminal.snap +++ b/tasks/website_formatter/src/snapshots/cli_terminal.snap @@ -1,5 +1,6 @@ --- source: tasks/website_formatter/src/cli.rs +assertion_line: 16 expression: snapshot --- Usage: [-c=PATH] [PATH]... @@ -7,7 +8,7 @@ Usage: [-c=PATH] [PATH]... Mode Options: --init Initialize `.oxfmtrc.json` with default values --migrate=SOURCE Migrate configuration to `.oxfmtrc.json` from specified source - Available sources: prettier + Available sources: prettier, biome --lsp Start language server protocol (LSP) server --stdin-filepath=PATH Specify the file name to use to infer which parser to use