From 009ccc680874a4ef37c0693d609419af16be4f37 Mon Sep 17 00:00:00 2001 From: yuyinws Date: Thu, 3 Sep 2026 16:58:31 +0800 Subject: [PATCH 1/8] feat: init oxfmt format --- docs/errors/OXDT0007.md | 16 ++ docs/errors/index.md | 1 + .../oxc/src/app/components/RunOxfmtDialog.vue | 137 ++++++++++++++++++ packages/oxc/src/app/pages/index.vue | 6 + packages/oxc/src/app/pages/oxfmt.vue | 5 + packages/oxc/src/app/pages/oxfmt/format.vue | 36 +++++ .../oxc/src/node/__tests__/oxfmt-run.test.ts | 15 ++ packages/oxc/src/node/diagnostics.ts | 4 + .../oxc/src/node/rpc/functions/oxfmt-run.ts | 80 ++++++++++ .../oxc/src/node/rpc/functions/oxfmt-setup.ts | 2 +- packages/oxc/src/node/rpc/index.ts | 3 + 11 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 docs/errors/OXDT0007.md create mode 100644 packages/oxc/src/app/components/RunOxfmtDialog.vue create mode 100644 packages/oxc/src/app/pages/oxfmt/format.vue create mode 100644 packages/oxc/src/node/__tests__/oxfmt-run.test.ts create mode 100644 packages/oxc/src/node/rpc/functions/oxfmt-run.ts diff --git a/docs/errors/OXDT0007.md b/docs/errors/OXDT0007.md new file mode 100644 index 000000000..638422ce1 --- /dev/null +++ b/docs/errors/OXDT0007.md @@ -0,0 +1,16 @@ +--- +outline: deep +--- +# OXDT0007: Failed to Run Oxfmt + +## Message +> Failed to run Oxfmt: `{reason}` + +## Cause +Oxfmt could not be started for the current workspace. + +## Fix +Install Oxfmt, check its configuration, and run format again. + +## Source +- [`packages/oxc/src/node/rpc/functions/oxfmt-run.ts`](https://github.com/vitejs/devtools/blob/main/packages/oxc/src/node/rpc/functions/oxfmt-run.ts) — reports execution failures. diff --git a/docs/errors/index.md b/docs/errors/index.md index df0e2699d..99083dac2 100644 --- a/docs/errors/index.md +++ b/docs/errors/index.md @@ -80,3 +80,4 @@ Emitted by `@vitejs/devtools-oxc`. | [OXDT0004](./OXDT0004) | error | Oxlint Config Inspection Failed | | [OXDT0005](./OXDT0005) | error | Oxlint Setup Failed | | [OXDT0006](./OXDT0006) | error | Oxfmt Setup Failed | +| [OXDT0007](./OXDT0007) | error | Failed to Run Oxfmt | diff --git a/packages/oxc/src/app/components/RunOxfmtDialog.vue b/packages/oxc/src/app/components/RunOxfmtDialog.vue new file mode 100644 index 000000000..88128b99e --- /dev/null +++ b/packages/oxc/src/app/components/RunOxfmtDialog.vue @@ -0,0 +1,137 @@ + + + diff --git a/packages/oxc/src/app/pages/index.vue b/packages/oxc/src/app/pages/index.vue index 03cc0a027..a87024c05 100644 --- a/packages/oxc/src/app/pages/index.vue +++ b/packages/oxc/src/app/pages/index.vue @@ -53,6 +53,12 @@ const tools = computed(() => { }, ] const oxfmtViews: ToolView[] = [ + { + title: 'Format Inspector', + description: 'Run and inspect formatting', + icon: 'i-ph-paint-brush-duotone', + to: '/oxfmt/format', + }, { title: 'Documents', description: 'Guides and references', diff --git a/packages/oxc/src/app/pages/oxfmt.vue b/packages/oxc/src/app/pages/oxfmt.vue index 982408ce4..e3f9f7022 100644 --- a/packages/oxc/src/app/pages/oxfmt.vue +++ b/packages/oxc/src/app/pages/oxfmt.vue @@ -13,6 +13,11 @@ useSideNav(() => [ // icon: 'i-ph-sliders-duotone', // to: '/oxfmt/config', // }, + { + title: 'Format Inspector', + icon: 'i-ph-paint-brush-duotone', + to: '/oxfmt/format', + }, { title: 'Documents', icon: 'i-ph-book-open-duotone', diff --git a/packages/oxc/src/app/pages/oxfmt/format.vue b/packages/oxc/src/app/pages/oxfmt/format.vue new file mode 100644 index 000000000..ee778c33d --- /dev/null +++ b/packages/oxc/src/app/pages/oxfmt/format.vue @@ -0,0 +1,36 @@ + + + diff --git a/packages/oxc/src/node/__tests__/oxfmt-run.test.ts b/packages/oxc/src/node/__tests__/oxfmt-run.test.ts new file mode 100644 index 000000000..bc987c8df --- /dev/null +++ b/packages/oxc/src/node/__tests__/oxfmt-run.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest' +import { getOxfmtFormatCommand } from '../rpc/functions/oxfmt-run' + +describe('getOxfmtFormatCommand', () => { + it('uses Vite+ first, then the detected package manager', () => { + expect(getOxfmtFormatCommand('pnpm', false, true)).toEqual({ + command: 'vp', + args: ['fmt', '--check'], + }) + expect(getOxfmtFormatCommand('pnpm', true, false)).toEqual({ + command: 'pnpm', + args: ['exec', 'oxfmt', '--write'], + }) + }) +}) diff --git a/packages/oxc/src/node/diagnostics.ts b/packages/oxc/src/node/diagnostics.ts index daba01e2f..90242005a 100644 --- a/packages/oxc/src/node/diagnostics.ts +++ b/packages/oxc/src/node/diagnostics.ts @@ -31,5 +31,9 @@ export const diagnostics = /* #__PURE__ */ defineDiagnostics({ why: (p: { reason: string }) => `Failed to set up Oxfmt: ${p.reason}`, fix: 'Check the project package manager and configuration, then try again.', }, + OXDT0007: { + why: (p: { reason: string }) => `Failed to run Oxfmt: ${p.reason}`, + fix: 'Check that Oxfmt is installed and its configuration is valid, then try again.', + }, }, }) diff --git a/packages/oxc/src/node/rpc/functions/oxfmt-run.ts b/packages/oxc/src/node/rpc/functions/oxfmt-run.ts new file mode 100644 index 000000000..aff9d781d --- /dev/null +++ b/packages/oxc/src/node/rpc/functions/oxfmt-run.ts @@ -0,0 +1,80 @@ +import type { PackageManagerName } from 'nypm' +import { detectPackageManager } from 'nypm' +import { Diagnostic } from 'nostics' +import { x } from 'tinyexec' +import { diagnostics } from '../../diagnostics' +import { isVitePlusInstalled } from '../../utils/vite-plus' +import { defineOxcRpc } from '../_define' +import { isGitDirty } from './oxfmt-setup' + +type OxfmtCommand = { command: string; args: string[] } + +export function getOxfmtFormatCommand( + packageManager: PackageManagerName | undefined, + write: boolean, + vitePlus: boolean, +): OxfmtCommand { + const option = write ? '--write' : '--check' + if (vitePlus) return { command: 'vp', args: ['fmt', option] } + + switch (packageManager) { + case 'npm': + return { command: 'npx', args: ['--no-install', 'oxfmt', option] } + case 'pnpm': + return { command: 'pnpm', args: ['exec', 'oxfmt', option] } + case 'yarn': + return { command: 'yarn', args: ['oxfmt', option] } + case 'bun': + return { command: 'bun', args: ['x', '--no-install', 'oxfmt', option] } + default: + return { command: 'oxfmt', args: [option] } + } +} + +async function getPreview(root: string, write: boolean) { + const command = getOxfmtFormatCommand( + (await detectPackageManager(root))?.name, + write, + isVitePlusInstalled(root), + ) + return { + command: [command.command, ...command.args].join(' '), + gitDirty: write && (await isGitDirty(root)), + } +} + +export const oxfmtFormatPreview = defineOxcRpc({ + name: 'devtools-oxc:oxfmt-format-preview', + type: 'query', + jsonSerializable: true, + setup: context => ({ + handler: ({ write }: { write: boolean }) => getPreview(context.cwd, write), + }), +}) + +export const oxfmtRun = defineOxcRpc({ + name: 'devtools-oxc:run-format', + type: 'action', + jsonSerializable: true, + setup: context => ({ + handler: async ({ write }: { write: boolean }) => { + try { + const command = getOxfmtFormatCommand( + (await detectPackageManager(context.cwd))?.name, + write, + isVitePlusInstalled(context.cwd), + ) + const result = await x(command.command, command.args, { + nodeOptions: { cwd: context.cwd, env: { FORCE_COLOR: '0', NO_COLOR: '1' } }, + }) + return { exitCode: result.exitCode } + } catch (error) { + if (error instanceof Diagnostic) throw error + throw diagnostics.OXDT0007({ + reason: error instanceof Error ? error.message : String(error), + cause: error, + }) + } + }, + }), +}) diff --git a/packages/oxc/src/node/rpc/functions/oxfmt-setup.ts b/packages/oxc/src/node/rpc/functions/oxfmt-setup.ts index 3dc1ba386..2553bce0c 100644 --- a/packages/oxc/src/node/rpc/functions/oxfmt-setup.ts +++ b/packages/oxc/src/node/rpc/functions/oxfmt-setup.ts @@ -26,7 +26,7 @@ async function getSetupCommands(root: string, migrate: boolean): Promise { +export async function isGitDirty(root: string): Promise { try { const result = await x('git', ['-C', root, 'status', '--porcelain'], { nodeOptions: { cwd: root }, diff --git a/packages/oxc/src/node/rpc/index.ts b/packages/oxc/src/node/rpc/index.ts index 73f67aff7..478bd069d 100644 --- a/packages/oxc/src/node/rpc/index.ts +++ b/packages/oxc/src/node/rpc/index.ts @@ -16,6 +16,7 @@ import { oxlintWaitForSetup, } from './functions/oxlint-setup' import { oxfmtSetup, oxfmtSetupPreview } from './functions/oxfmt-setup' +import { oxfmtFormatPreview, oxfmtRun } from './functions/oxfmt-run' export const rpcFunctions = [ oxlintRun, @@ -33,6 +34,8 @@ export const rpcFunctions = [ oxlintWaitForSetup, oxfmtSetup, oxfmtSetupPreview, + oxfmtFormatPreview, + oxfmtRun, openInEditor, ] as const From e7ce69899614f808c7da0a4bf7e07050327f4b8c Mon Sep 17 00:00:00 2001 From: yuyinws Date: Sun, 6 Sep 2026 13:22:37 +0800 Subject: [PATCH 2/8] feat(oxc): add grouped oxfmt check history --- .../oxc/src/app/components/RunOxfmtDialog.vue | 30 ++-- packages/oxc/src/app/pages/oxfmt/format.vue | 128 +++++++++++++++++- .../oxc/src/node/__tests__/oxfmt-run.test.ts | 84 +++++++++++- .../node/rpc/functions/oxfmt-list-results.ts | 11 ++ .../oxc/src/node/rpc/functions/oxfmt-run.ts | 100 ++++++++++---- packages/oxc/src/node/rpc/index.ts | 2 + 6 files changed, 296 insertions(+), 59 deletions(-) create mode 100644 packages/oxc/src/node/rpc/functions/oxfmt-list-results.ts diff --git a/packages/oxc/src/app/components/RunOxfmtDialog.vue b/packages/oxc/src/app/components/RunOxfmtDialog.vue index 88128b99e..b3c75ffa6 100644 --- a/packages/oxc/src/app/components/RunOxfmtDialog.vue +++ b/packages/oxc/src/app/components/RunOxfmtDialog.vue @@ -7,9 +7,10 @@ import { ref, watch } from 'vue' import { useRpc } from '#imports' const open = defineModel('open', { default: false }) +const emit = defineEmits<{ refresh: [] }>() const rpc = useRpc() -type Stage = 'confirm' | 'running' | 'success' | 'checked' | 'error' +type Stage = 'confirm' | 'running' | 'success' | 'error' const stage = ref('confirm') const write = ref(false) const commandLine = ref('') @@ -52,7 +53,12 @@ async function confirmRun() { try { const { exitCode } = await rpc.value.call('devtools-oxc:run-format', { write: write.value }) if (!open.value) return - stage.value = exitCode === 0 ? 'success' : write.value ? 'error' : 'checked' + if (!write.value) { + emit('refresh') + open.value = false + return + } + stage.value = exitCode === 0 ? 'success' : 'error' if (exitCode !== 0) errorMessage.value = `Oxfmt exited with code ${exitCode}.` } catch (error) { if (!open.value) return @@ -104,27 +110,13 @@ async function confirmRun() { diff --git a/packages/oxc/src/node/__tests__/oxfmt-delete-result.test.ts b/packages/oxc/src/node/__tests__/oxfmt-delete-result.test.ts new file mode 100644 index 000000000..33bc5481f --- /dev/null +++ b/packages/oxc/src/node/__tests__/oxfmt-delete-result.test.ts @@ -0,0 +1,40 @@ +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { expect, it } from 'vitest' +import { oxfmtDeleteResult } from '../rpc/functions/oxfmt-delete-result' + +it('deletes only the selected log and rejects invalid or missing IDs', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'oxfmt-delete-')) + try { + for (const [id, mode] of [ + ['1', 'check'], + ['2', 'write'], + ]) { + const dir = join(cwd, '.devtools-oxc', 'fmt', id) + await mkdir(dir, { recursive: true }) + await writeFile(join(dir, 'log.json'), JSON.stringify({ mode })) + } + const source = join(cwd, 'index.ts') + await writeFile(source, 'export const value = 1\n') + const { handler } = oxfmtDeleteResult.setup!({ cwd } as any) + for (const resultId of ['../..', '', '/tmp', '1/../../..']) { + await expect(handler({ resultId })).rejects.toMatchObject({ code: 'OXDT0008' }) + } + await handler({ resultId: '2' }) + await expect(readFile(join(cwd, '.devtools-oxc/fmt/2/log.json'))).rejects.toMatchObject({ + code: 'ENOENT', + }) + await expect(readFile(join(cwd, '.devtools-oxc/fmt/1/log.json'), 'utf8')).resolves.toContain( + 'check', + ) + await expect(readFile(source, 'utf8')).resolves.toBe('export const value = 1\n') + await expect(handler({ resultId: '2' })).rejects.toMatchObject({ code: 'OXDT0008' }) + await handler({ resultId: '1' }) + await expect(readFile(join(cwd, '.devtools-oxc/fmt/1/log.json'))).rejects.toMatchObject({ + code: 'ENOENT', + }) + } finally { + await rm(cwd, { recursive: true, force: true }) + } +}) diff --git a/packages/oxc/src/node/__tests__/oxfmt-run.test.ts b/packages/oxc/src/node/__tests__/oxfmt-run.test.ts index 7f4a7869e..0728b0675 100644 --- a/packages/oxc/src/node/__tests__/oxfmt-run.test.ts +++ b/packages/oxc/src/node/__tests__/oxfmt-run.test.ts @@ -1,12 +1,12 @@ -import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { getOxfmtFormatCommand, - listOxfmtCheckResults, - parseOxfmtCheckOutput, - saveOxfmtCheckResult, + listOxfmtFormatResults, + parseOxfmtFormatOutput, + saveOxfmtFormatResult, } from '../rpc/functions/oxfmt-run' const fixtures: string[] = [] @@ -36,27 +36,52 @@ describe('getOxfmtFormatCommand', () => { it('persists each parsed check under its timestamp directory', async () => { const cwd = await createFixture() await writeFile(join(cwd, '.gitignore'), '') - const log = parseOxfmtCheckOutput( + const log = parseOxfmtFormatOutput( 'Checking formatting...\n\nAll matched files use the correct format.\nFinished in 113ms on 11 files using 8 threads.', )! - await saveOxfmtCheckResult(cwd, log) + await saveOxfmtFormatResult(cwd, log) - await expect(listOxfmtCheckResults(cwd)).resolves.toMatchObject([ - { status: 'clean', summary: { durationMs: 113 } }, + await expect(listOxfmtFormatResults(cwd)).resolves.toMatchObject([ + { mode: 'check', status: 'clean', summary: { durationMs: 113 } }, ]) }) }) -describe('parseOxfmtCheckOutput', () => { +describe('parseOxfmtFormatOutput', () => { + it('persists write results and uses the exit code for their status', async () => { + const cwd = await createFixture() + await writeFile(join(cwd, '.gitignore'), '') + const stdout = 'src/main.ts (0.5ms)\nFinished in 2ms on 1 files using 8 threads.' + const log = parseOxfmtFormatOutput(stdout, 'write', 0) + expect(log).toMatchObject({ + mode: 'write', + status: 'clean', + files: [{ path: 'src/main.ts', durationMs: 0.5 }], + }) + expect(parseOxfmtFormatOutput(stdout, 'write', 1).status).toBe('error') + await saveOxfmtFormatResult(cwd, log) + await expect(listOxfmtFormatResults(cwd)).resolves.toMatchObject([log]) + }) + + it('treats legacy logs without a mode as checks', async () => { + const cwd = await createFixture() + const dir = join(cwd, '.devtools-oxc', 'fmt', '1') + await mkdir(dir, { recursive: true }) + const { mode: _mode, ...legacy } = parseOxfmtFormatOutput('') + await writeFile(join(dir, 'log.json'), JSON.stringify({ timestamp: 1, ...legacy })) + await expect(listOxfmtFormatResults(cwd)).resolves.toMatchObject([{ mode: 'check' }]) + }) + it('parses files and summary when formatting is needed', () => { expect( - parseOxfmtCheckOutput(`Checking formatting... + parseOxfmtFormatOutput(`Checking formatting... index.html (103ms) src/main.js (0ms) Format issues found in above 2 files. Run without \`--check\` to fix. Finished in 113ms on 11 files using 8 threads.`), ).toEqual({ + mode: 'check', status: 'issues', files: [ { path: 'index.html', durationMs: 103 }, @@ -73,11 +98,11 @@ Finished in 113ms on 11 files using 8 threads.`, it('records failed checks even when Oxfmt does not print a summary', () => { expect( - parseOxfmtCheckOutput( + parseOxfmtFormatOutput( 'Checking formatting...\n\nAll matched files use the correct format.\nFinished in 113ms on 11 files using 8 threads.', ), ).toMatchObject({ status: 'clean', files: [] }) - expect(parseOxfmtCheckOutput('Oxfmt failed to load config.')).toMatchObject({ + expect(parseOxfmtFormatOutput('Oxfmt failed to load config.')).toMatchObject({ status: 'error', summary: { durationMs: 0, fileCount: 0, threadCount: 0 }, }) diff --git a/packages/oxc/src/node/diagnostics.ts b/packages/oxc/src/node/diagnostics.ts index 90242005a..32d98478f 100644 --- a/packages/oxc/src/node/diagnostics.ts +++ b/packages/oxc/src/node/diagnostics.ts @@ -5,6 +5,11 @@ export const diagnostics = /* #__PURE__ */ defineDiagnostics({ docsBase: 'https://devtools.vite.dev/errors', reporters: [/* #__PURE__ */ createConsoleReporter()], codes: { + OXDT0008: { + why: (p: { resultId: string; reason: string }) => + `Failed to delete format result "${p.resultId}": ${p.reason}`, + fix: 'Use a numeric ID from the format result list and check that the result exists and the project directory is writable.', + }, OXDT0001: { why: (p: { reason: string }) => `Failed to create a lint result: ${p.reason}`, fix: 'Check that oxlint is installed, its configuration is valid, and the project directory is writable.', diff --git a/packages/oxc/src/node/rpc/functions/oxfmt-delete-result.ts b/packages/oxc/src/node/rpc/functions/oxfmt-delete-result.ts new file mode 100644 index 000000000..fe2a3ebfd --- /dev/null +++ b/packages/oxc/src/node/rpc/functions/oxfmt-delete-result.ts @@ -0,0 +1,27 @@ +import { rm } from 'node:fs/promises' +import { Diagnostic } from 'nostics' +import { resolve } from 'pathe' +import { diagnostics } from '../../diagnostics' +import { defineOxcRpc } from '../_define' + +export const oxfmtDeleteResult = defineOxcRpc({ + name: 'devtools-oxc:delete-format-result', + type: 'action', + setup: context => ({ + handler: async ({ resultId }: { resultId: string }) => { + try { + if (typeof resultId !== 'string' || !/^\d+$/.test(resultId)) { + throw diagnostics.OXDT0008({ resultId, reason: 'Invalid format result ID.' }) + } + await rm(resolve(context.cwd, '.devtools-oxc', 'fmt', resultId), { recursive: true }) + } catch (error) { + if (error instanceof Diagnostic) throw error + throw diagnostics.OXDT0008({ + resultId, + reason: error instanceof Error ? error.message : String(error), + cause: error, + }) + } + }, + }), +}) diff --git a/packages/oxc/src/node/rpc/functions/oxfmt-list-results.ts b/packages/oxc/src/node/rpc/functions/oxfmt-list-results.ts index f5063c330..5efdb7b72 100644 --- a/packages/oxc/src/node/rpc/functions/oxfmt-list-results.ts +++ b/packages/oxc/src/node/rpc/functions/oxfmt-list-results.ts @@ -1,11 +1,11 @@ import { defineOxcRpc } from '../_define' -import { listOxfmtCheckResults } from './oxfmt-run' +import { listOxfmtFormatResults } from './oxfmt-run' export const oxfmtListResults = defineOxcRpc({ name: 'devtools-oxc:list-format-results', type: 'query', jsonSerializable: true, setup: context => ({ - handler: () => listOxfmtCheckResults(context.cwd), + handler: () => listOxfmtFormatResults(context.cwd), }), }) diff --git a/packages/oxc/src/node/rpc/functions/oxfmt-run.ts b/packages/oxc/src/node/rpc/functions/oxfmt-run.ts index 6571afbdc..417cef894 100644 --- a/packages/oxc/src/node/rpc/functions/oxfmt-run.ts +++ b/packages/oxc/src/node/rpc/functions/oxfmt-run.ts @@ -11,16 +11,17 @@ import { isGitDirty } from './oxfmt-setup' type OxfmtCommand = { command: string; args: string[] } -export type OxfmtCheckLog = { +export type OxfmtFormatLog = { + mode: 'check' | 'write' status: 'clean' | 'issues' | 'error' files: { path: string; durationMs: number }[] summary: { durationMs: number; fileCount: number; threadCount: number } stdout: string } -export type OxfmtCheckResult = OxfmtCheckLog & { timestamp: number } +export type OxfmtFormatResult = OxfmtFormatLog & { timestamp: number } -export async function saveOxfmtCheckResult(root: string, log: OxfmtCheckLog) { +export async function saveOxfmtFormatResult(root: string, log: OxfmtFormatLog) { const timestamp = Date.now() const dir = resolve(root, '.devtools-oxc', 'fmt', String(timestamp)) await ensureOxcGitignored(root) @@ -28,7 +29,7 @@ export async function saveOxfmtCheckResult(root: string, log: OxfmtCheckLog) { await writeFile(resolve(dir, 'log.json'), JSON.stringify({ timestamp, ...log }, null, 2), 'utf-8') } -export async function listOxfmtCheckResults(root: string): Promise { +export async function listOxfmtFormatResults(root: string): Promise { const dir = resolve(root, '.devtools-oxc', 'fmt') if (!existsSync(dir)) return [] @@ -38,9 +39,11 @@ export async function listOxfmtCheckResults(root: string): Promise Number(b.name) - Number(a.name)) .map(async entry => { try { - return JSON.parse( + const result = JSON.parse( await readFile(resolve(dir, entry.name, 'log.json'), 'utf-8'), - ) as OxfmtCheckResult + ) as OxfmtFormatResult + result.mode ??= 'check' + return result } catch { return null } @@ -49,18 +52,28 @@ export async function listOxfmtCheckResults(root: string): Promise result !== null) } -export function parseOxfmtCheckOutput(stdout: string): OxfmtCheckLog { - const status = stdout.includes('All matched files use the correct format.') - ? 'clean' - : /Format issues found in above \d+ files\./.test(stdout) - ? 'issues' - : 'error' +export function parseOxfmtFormatOutput( + stdout: string, + mode: OxfmtFormatLog['mode'] = 'check', + exitCode = 0, +): OxfmtFormatLog { + const status = + mode === 'write' + ? exitCode === 0 + ? 'clean' + : 'error' + : stdout.includes('All matched files use the correct format.') + ? 'clean' + : /Format issues found in above \d+ files\./.test(stdout) + ? 'issues' + : 'error' const summary = stdout.match(/Finished in (\d+(?:\.\d+)?)ms on (\d+) files using (\d+) threads\./) return { + mode, status, files: [...stdout.matchAll(/^(.+) \((\d+(?:\.\d+)?)ms\)$/gm)].map(([, path, durationMs]) => ({ - path, + path: path!, durationMs: Number(durationMs), })), summary: { @@ -106,7 +119,10 @@ export const oxfmtRun = defineOxcRpc({ const result = await x(command.command, command.args, { nodeOptions: { cwd: context.cwd, env: { FORCE_COLOR: '0', NO_COLOR: '1' } }, }) - if (!write) await saveOxfmtCheckResult(context.cwd, parseOxfmtCheckOutput(result.stdout)) + await saveOxfmtFormatResult( + context.cwd, + parseOxfmtFormatOutput(result.stdout, write ? 'write' : 'check', result.exitCode), + ) return { exitCode: result.exitCode } } catch (error) { if (error instanceof Diagnostic) throw error diff --git a/packages/oxc/src/node/rpc/index.ts b/packages/oxc/src/node/rpc/index.ts index aadea5873..ec779e01f 100644 --- a/packages/oxc/src/node/rpc/index.ts +++ b/packages/oxc/src/node/rpc/index.ts @@ -18,6 +18,7 @@ import { import { oxfmtSetup, oxfmtSetupPreview } from './functions/oxfmt-setup' import { oxfmtFormatPreview, oxfmtRun } from './functions/oxfmt-run' import { oxfmtListResults } from './functions/oxfmt-list-results' +import { oxfmtDeleteResult } from './functions/oxfmt-delete-result' export const rpcFunctions = [ oxlintRun, @@ -38,6 +39,7 @@ export const rpcFunctions = [ oxfmtFormatPreview, oxfmtRun, oxfmtListResults, + oxfmtDeleteResult, openInEditor, ] as const From fd35bb4eb46b39c3ff084185b55c9545370d06ce Mon Sep 17 00:00:00 2001 From: yuyinws Date: Mon, 7 Sep 2026 11:17:36 +0800 Subject: [PATCH 4/8] feat: open in editor --- packages/oxc/src/app/pages/oxfmt/format.vue | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/oxc/src/app/pages/oxfmt/format.vue b/packages/oxc/src/app/pages/oxfmt/format.vue index e735d25c0..6239ee53c 100644 --- a/packages/oxc/src/app/pages/oxfmt/format.vue +++ b/packages/oxc/src/app/pages/oxfmt/format.vue @@ -43,6 +43,9 @@ const formatResultGroups = computed(() => groupByDate(formatResults.value, result => result.timestamp), ) const openOverrides = reactive>({}) +function openInEditor(path: string) { + rpc.value.call('devtools-oxc:open-in-editor', path) +} function isGroupOpen(group: { key: string; defaultOpen: boolean }) { return openOverrides[group.key] ?? group.defaultOpen } @@ -154,7 +157,13 @@ function toggleGroup(group: { key: string }, open: boolean) { >
  • - {{ file.path }} + {{ file.durationMs }}ms
  • From cbf4b6624f90ee103dbf61a044a3ed7704d95d3c Mon Sep 17 00:00:00 2001 From: yuyinws Date: Mon, 7 Sep 2026 11:17:49 +0800 Subject: [PATCH 5/8] feat: file list sort --- .../oxc/src/node/__tests__/oxfmt-run.test.ts | 12 +++++++++--- .../oxc/src/node/rpc/functions/oxfmt-run.ts | 17 +++++++++++++---- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/oxc/src/node/__tests__/oxfmt-run.test.ts b/packages/oxc/src/node/__tests__/oxfmt-run.test.ts index 0728b0675..1476287e2 100644 --- a/packages/oxc/src/node/__tests__/oxfmt-run.test.ts +++ b/packages/oxc/src/node/__tests__/oxfmt-run.test.ts @@ -4,6 +4,7 @@ import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { getOxfmtFormatCommand, + getOxfmtRunError, listOxfmtFormatResults, parseOxfmtFormatOutput, saveOxfmtFormatResult, @@ -33,6 +34,11 @@ describe('getOxfmtFormatCommand', () => { }) }) + it('returns Oxfmt diagnostics without whitespace', () => { + expect(getOxfmtRunError(' Invalid config.\n')).toBe('Invalid config.') + expect(getOxfmtRunError(' \n')).toBeUndefined() + }) + it('persists each parsed check under its timestamp directory', async () => { const cwd = await createFixture() await writeFile(join(cwd, '.gitignore'), '') @@ -77,20 +83,20 @@ describe('parseOxfmtFormatOutput', () => { expect( parseOxfmtFormatOutput(`Checking formatting... index.html (103ms) -src/main.js (0ms) +src/main.js (200ms) Format issues found in above 2 files. Run without \`--check\` to fix. Finished in 113ms on 11 files using 8 threads.`), ).toEqual({ mode: 'check', status: 'issues', files: [ + { path: 'src/main.js', durationMs: 200 }, { path: 'index.html', durationMs: 103 }, - { path: 'src/main.js', durationMs: 0 }, ], summary: { durationMs: 113, fileCount: 11, threadCount: 8 }, stdout: `Checking formatting... index.html (103ms) -src/main.js (0ms) +src/main.js (200ms) Format issues found in above 2 files. Run without \`--check\` to fix. Finished in 113ms on 11 files using 8 threads.`, }) diff --git a/packages/oxc/src/node/rpc/functions/oxfmt-run.ts b/packages/oxc/src/node/rpc/functions/oxfmt-run.ts index 417cef894..0e16b8bee 100644 --- a/packages/oxc/src/node/rpc/functions/oxfmt-run.ts +++ b/packages/oxc/src/node/rpc/functions/oxfmt-run.ts @@ -72,10 +72,9 @@ export function parseOxfmtFormatOutput( return { mode, status, - files: [...stdout.matchAll(/^(.+) \((\d+(?:\.\d+)?)ms\)$/gm)].map(([, path, durationMs]) => ({ - path: path!, - durationMs: Number(durationMs), - })), + files: [...stdout.matchAll(/^(.+) \((\d+(?:\.\d+)?)ms\)$/gm)] + .map(([, path, durationMs]) => ({ path: path!, durationMs: Number(durationMs) })) + .sort((a, b) => b.durationMs - a.durationMs), summary: { durationMs: Number(summary?.[1] ?? 0), fileCount: Number(summary?.[2] ?? 0), @@ -91,6 +90,10 @@ export function getOxfmtFormatCommand(write: boolean, vitePlus: boolean): OxfmtC return { command: 'oxfmt', args: [option] } } +export function getOxfmtRunError(stderr: string) { + return stderr.trim() || undefined +} + async function getPreview(root: string, write: boolean) { const command = getOxfmtFormatCommand(write, isVitePlusInstalled(root)) return { @@ -119,6 +122,12 @@ export const oxfmtRun = defineOxcRpc({ const result = await x(command.command, command.args, { nodeOptions: { cwd: context.cwd, env: { FORCE_COLOR: '0', NO_COLOR: '1' } }, }) + const reason = getOxfmtRunError(result.stderr) + if (reason) { + throw diagnostics.OXDT0007({ + reason, + }) + } await saveOxfmtFormatResult( context.cwd, parseOxfmtFormatOutput(result.stdout, write ? 'write' : 'check', result.exitCode), From cc53430a39aad45a009ebacef695b904c313940c Mon Sep 17 00:00:00 2001 From: yuyinws Date: Mon, 7 Sep 2026 11:44:03 +0800 Subject: [PATCH 6/8] feat(oxc): improve format result file lists --- .../oxc/src/app/components/RunOxfmtDialog.vue | 4 +- packages/oxc/src/app/pages/oxfmt/format.vue | 46 ++++++++++++++++--- .../oxc/src/node/__tests__/oxfmt-run.test.ts | 26 +++++++++-- .../oxc/src/node/rpc/functions/oxfmt-run.ts | 1 + 4 files changed, 65 insertions(+), 12 deletions(-) diff --git a/packages/oxc/src/app/components/RunOxfmtDialog.vue b/packages/oxc/src/app/components/RunOxfmtDialog.vue index 229f4ee0a..e02ba4018 100644 --- a/packages/oxc/src/app/components/RunOxfmtDialog.vue +++ b/packages/oxc/src/app/components/RunOxfmtDialog.vue @@ -7,7 +7,7 @@ import { ref, watch } from 'vue' import { useRpc } from '#imports' const open = defineModel('open', { default: false }) -const emit = defineEmits<{ refresh: [] }>() +const emit = defineEmits<{ complete: [write: boolean] }>() const rpc = useRpc() type Stage = 'confirm' | 'running' | 'success' | 'error' @@ -52,7 +52,7 @@ async function confirmRun() { errorMessage.value = '' try { const { exitCode } = await rpc.value.call('devtools-oxc:run-format', { write: write.value }) - emit('refresh') + emit('complete', write.value) if (!open.value) return if (!write.value) { open.value = false diff --git a/packages/oxc/src/app/pages/oxfmt/format.vue b/packages/oxc/src/app/pages/oxfmt/format.vue index 6239ee53c..f6228efa5 100644 --- a/packages/oxc/src/app/pages/oxfmt/format.vue +++ b/packages/oxc/src/app/pages/oxfmt/format.vue @@ -17,6 +17,12 @@ const deleteOpen = ref(false) const selectedResultId = ref('') const deleteError = ref('') const isDeleting = ref(false) +async function handleFormatComplete(write: boolean) { + await reloadResults() + if (write) return + const [latestResult] = formatResults.value + if (latestResult?.files.length) expandedResults.value = { [latestResult.timestamp]: true } +} function requestDelete(timestamp: number) { selectedResultId.value = String(timestamp) deleteError.value = '' @@ -40,7 +46,22 @@ const { state: formatResults, execute: reloadResults } = useAsyncState( [], ) const formatResultGroups = computed(() => - groupByDate(formatResults.value, result => result.timestamp), + groupByDate(formatResults.value, result => result.timestamp).map(group => + Object.assign({}, group, { + items: group.items.map(result => { + const maxDurationMs = Math.max(0, ...result.files.map(file => file.durationMs)) + const totalDurationMs = result.files.reduce((total, file) => total + file.durationMs, 0) + return Object.assign({}, result, { + files: result.files.map(file => + Object.assign({}, file, { + barWidth: maxDurationMs > 0 ? `${(file.durationMs / maxDurationMs) * 100}%` : '0%', + percentage: totalDurationMs > 0 ? (file.durationMs / totalDurationMs) * 100 : 0, + }), + ), + }) + }), + }), + ), ) const openOverrides = reactive>({}) function openInEditor(path: string) { @@ -151,20 +172,31 @@ function toggleGroup(group: { key: string }, open: boolean) { v-if="result.files.length" v-show="expandedResults[result.timestamp]" :id="`format-files-${result.timestamp}`" - class="my3 max-h-64 overflow-y-auto text-sm font-mono" + class="my3 max-h-64 space-y-1 overflow-y-auto text-sm font-mono" aria-label="Format file results" tabindex="0" > -
  • - +
  • +
    + - {{ file.durationMs }}ms + {{ file.percentage.toFixed(1) }}% + {{ file.durationMs }}ms
  • @@ -200,7 +232,7 @@ function toggleGroup(group: { key: string }, open: boolean) { - +
    diff --git a/packages/oxc/src/node/__tests__/oxfmt-run.test.ts b/packages/oxc/src/node/__tests__/oxfmt-run.test.ts index 1476287e2..7c498c0e2 100644 --- a/packages/oxc/src/node/__tests__/oxfmt-run.test.ts +++ b/packages/oxc/src/node/__tests__/oxfmt-run.test.ts @@ -70,13 +70,33 @@ describe('parseOxfmtFormatOutput', () => { await expect(listOxfmtFormatResults(cwd)).resolves.toMatchObject([log]) }) - it('treats legacy logs without a mode as checks', async () => { + it('treats legacy logs as checks and sorts their files by duration', async () => { const cwd = await createFixture() const dir = join(cwd, '.devtools-oxc', 'fmt', '1') await mkdir(dir, { recursive: true }) const { mode: _mode, ...legacy } = parseOxfmtFormatOutput('') - await writeFile(join(dir, 'log.json'), JSON.stringify({ timestamp: 1, ...legacy })) - await expect(listOxfmtFormatResults(cwd)).resolves.toMatchObject([{ mode: 'check' }]) + await writeFile( + join(dir, 'log.json'), + JSON.stringify({ + timestamp: 1, + ...legacy, + files: [ + { path: 'eslint.config.js', durationMs: 1 }, + { path: 'index.html', durationMs: 115 }, + { path: 'vite.config.js', durationMs: 2 }, + ], + }), + ) + await expect(listOxfmtFormatResults(cwd)).resolves.toMatchObject([ + { + mode: 'check', + files: [ + { path: 'index.html', durationMs: 115 }, + { path: 'vite.config.js', durationMs: 2 }, + { path: 'eslint.config.js', durationMs: 1 }, + ], + }, + ]) }) it('parses files and summary when formatting is needed', () => { diff --git a/packages/oxc/src/node/rpc/functions/oxfmt-run.ts b/packages/oxc/src/node/rpc/functions/oxfmt-run.ts index 0e16b8bee..c79dcabee 100644 --- a/packages/oxc/src/node/rpc/functions/oxfmt-run.ts +++ b/packages/oxc/src/node/rpc/functions/oxfmt-run.ts @@ -43,6 +43,7 @@ export async function listOxfmtFormatResults(root: string): Promise b.durationMs - a.durationMs) return result } catch { return null From ff3cd63dbdffa26c9c8077812b06974d1ce44754 Mon Sep 17 00:00:00 2001 From: yuyinws Date: Mon, 7 Sep 2026 13:50:03 +0800 Subject: [PATCH 7/8] feat: icon --- packages/oxc/src/app/pages/index.vue | 2 +- packages/oxc/src/app/pages/oxfmt.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/oxc/src/app/pages/index.vue b/packages/oxc/src/app/pages/index.vue index a87024c05..50467a905 100644 --- a/packages/oxc/src/app/pages/index.vue +++ b/packages/oxc/src/app/pages/index.vue @@ -56,7 +56,7 @@ const tools = computed(() => { { title: 'Format Inspector', description: 'Run and inspect formatting', - icon: 'i-ph-paint-brush-duotone', + icon: 'i-ph-magnifying-glass-duotone', to: '/oxfmt/format', }, { diff --git a/packages/oxc/src/app/pages/oxfmt.vue b/packages/oxc/src/app/pages/oxfmt.vue index e3f9f7022..2cd802ec5 100644 --- a/packages/oxc/src/app/pages/oxfmt.vue +++ b/packages/oxc/src/app/pages/oxfmt.vue @@ -15,7 +15,7 @@ useSideNav(() => [ // }, { title: 'Format Inspector', - icon: 'i-ph-paint-brush-duotone', + icon: 'i-ph-magnifying-glass-duotone', to: '/oxfmt/format', }, { From b8b6a178d753991d6c7358162f3f9f75335bca25 Mon Sep 17 00:00:00 2001 From: yuyinws Date: Mon, 7 Sep 2026 17:25:31 +0800 Subject: [PATCH 8/8] fix(oxc): hide format inspector until installed --- packages/oxc/src/app/pages/index.vue | 16 ++++++++++------ packages/oxc/src/app/pages/oxfmt.vue | 23 ++++++++++++++++++----- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/packages/oxc/src/app/pages/index.vue b/packages/oxc/src/app/pages/index.vue index 50467a905..5b2d8f105 100644 --- a/packages/oxc/src/app/pages/index.vue +++ b/packages/oxc/src/app/pages/index.vue @@ -53,12 +53,16 @@ const tools = computed(() => { }, ] const oxfmtViews: ToolView[] = [ - { - title: 'Format Inspector', - description: 'Run and inspect formatting', - icon: 'i-ph-magnifying-glass-duotone', - to: '/oxfmt/format', - }, + ...(overview.value.oxfmt.installed + ? [ + { + title: 'Format Inspector', + description: 'Run and inspect formatting', + icon: 'i-ph-magnifying-glass-duotone', + to: '/oxfmt/format', + }, + ] + : []), { title: 'Documents', description: 'Guides and references', diff --git a/packages/oxc/src/app/pages/oxfmt.vue b/packages/oxc/src/app/pages/oxfmt.vue index 2cd802ec5..ab81a80c4 100644 --- a/packages/oxc/src/app/pages/oxfmt.vue +++ b/packages/oxc/src/app/pages/oxfmt.vue @@ -1,6 +1,15 @@