diff --git a/docs/errors/OXDT0006.md b/docs/errors/OXDT0006.md
new file mode 100644
index 000000000..4743cdd66
--- /dev/null
+++ b/docs/errors/OXDT0006.md
@@ -0,0 +1,16 @@
+---
+outline: deep
+---
+# OXDT0006: Oxfmt Setup Failed
+
+## Message
+> Failed to set up Oxfmt: `{reason}`
+
+## Cause
+Vite DevTools could not install or initialize Oxfmt in the project root.
+
+## Fix
+Check the project package manager and configuration, then try again.
+
+## Source
+- [`packages/oxc/src/node/rpc/functions/oxfmt-setup.ts`](https://github.com/vitejs/devtools/blob/main/packages/oxc/src/node/rpc/functions/oxfmt-setup.ts) — runs the installation and migration commands.
diff --git a/docs/errors/index.md b/docs/errors/index.md
index c73587391..df0e2699d 100644
--- a/docs/errors/index.md
+++ b/docs/errors/index.md
@@ -79,3 +79,4 @@ Emitted by `@vitejs/devtools-oxc`.
| [OXDT0003](./OXDT0003) | error | Failed to Delete Lint Result |
| [OXDT0004](./OXDT0004) | error | Oxlint Config Inspection Failed |
| [OXDT0005](./OXDT0005) | error | Oxlint Setup Failed |
+| [OXDT0006](./OXDT0006) | error | Oxfmt Setup Failed |
diff --git a/packages/oxc/src/app/components/SetupOxfmtDialog.vue b/packages/oxc/src/app/components/SetupOxfmtDialog.vue
new file mode 100644
index 000000000..242590716
--- /dev/null
+++ b/packages/oxc/src/app/components/SetupOxfmtDialog.vue
@@ -0,0 +1,185 @@
+
+
+
+
+ Setup Oxfmt with devtools
+
+
+
+
+
+ Oxfmt will be installed as a development dependency and migrate your
+ {{ migration === 'prettier' ? 'Prettier' : 'Biome' }} configuration.
+
+
+ Oxfmt will be installed as a development dependency and create a starter configuration
+ with oxfmt --init.
+
+
+
+
+
{{ commandLine || 'Loading…' }}
+
+
+
+
+ The Git working tree is not clean. Setup may modify project files.
+
+
+
+ {{ errorMessage }}
+
+
+
+
+
+ Migrate from {{ migration === 'prettier' ? 'Prettier' : 'Biome' }}
+
+
+
Cancel
+
+ Setup Oxfmt
+
+
+
+
+
+
+
+
+
Dismiss
+
+ View in terminals
+
+
+
+
+
+
+
+ Oxfmt setup failed.
+
+
+ {{ errorMessage }}
+
+
+
+
Close
+
+ View in terminals
+
+
+
+
+
+
diff --git a/packages/oxc/src/app/components/SetupOxlintDialog.vue b/packages/oxc/src/app/components/SetupOxlintDialog.vue
index 9815f4d3a..d92c44bbe 100644
--- a/packages/oxc/src/app/components/SetupOxlintDialog.vue
+++ b/packages/oxc/src/app/components/SetupOxlintDialog.vue
@@ -20,10 +20,12 @@ const gitDirty = ref(false)
const commandLine = ref('')
const sessionId = ref()
const errorMessage = ref()
+const isLoading = ref(false)
let previewRequest = 0
async function loadPreview() {
const request = ++previewRequest
+ isLoading.value = true
try {
const preview = await rpc.value.call('devtools-oxc:setup-preview', {
migrate: migrate.value,
@@ -36,6 +38,8 @@ async function loadPreview() {
} catch (error) {
if (request !== previewRequest) return
errorMessage.value = error instanceof Error ? error.message : String(error)
+ } finally {
+ if (request === previewRequest) isLoading.value = false
}
}
@@ -83,7 +87,6 @@ async function viewInTerminal() {
params: { sessionId: sessionId.value },
})
}
- open.value = false
}
@@ -128,7 +131,7 @@ async function viewInTerminal() {
-
+
Migrate from ESLint
diff --git a/packages/oxc/src/app/pages/index.vue b/packages/oxc/src/app/pages/index.vue
index ab3ea45b2..03cc0a027 100644
--- a/packages/oxc/src/app/pages/index.vue
+++ b/packages/oxc/src/app/pages/index.vue
@@ -17,6 +17,7 @@ const {
} = useAsyncState(() => rpc.value.call('devtools-oxc:overview'), createOverview())
const setupOpen = ref(false)
+const oxfmtSetupOpen = ref(false)
interface ToolView {
title: string
@@ -155,6 +156,18 @@ const tools = computed(() => {
Install & setup Oxlint
+
+
+
+
Setup Oxfmt
+
Install & setup Oxfmt
+
+
{
+
diff --git a/packages/oxc/src/modules/rpc.ts b/packages/oxc/src/modules/rpc.ts
new file mode 100644
index 000000000..8f40d97eb
--- /dev/null
+++ b/packages/oxc/src/modules/rpc.ts
@@ -0,0 +1,24 @@
+import { addVitePlugin, defineNuxtModule } from '@nuxt/kit'
+import { DevToolsServer } from '../../../core/src/node/plugins/server'
+import { rpcFunctions } from '../node/rpc'
+
+export default defineNuxtModule({
+ meta: {
+ name: 'devtools-rpc',
+ configKey: 'devtoolsRpc',
+ },
+ setup() {
+ addVitePlugin({
+ name: 'vite:devtools:oxc',
+ devtools: {
+ setup(ctx) {
+ for (const fn of rpcFunctions) {
+ ctx.rpc.register(fn as any)
+ }
+ },
+ },
+ })
+
+ addVitePlugin(DevToolsServer())
+ },
+})
diff --git a/packages/oxc/src/node/__tests__/oxfmt-setup.test.ts b/packages/oxc/src/node/__tests__/oxfmt-setup.test.ts
new file mode 100644
index 000000000..f5c25eb28
--- /dev/null
+++ b/packages/oxc/src/node/__tests__/oxfmt-setup.test.ts
@@ -0,0 +1,110 @@
+import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { getOxfmtMigration, oxfmtSetup, oxfmtSetupPreview } from '../rpc/functions/oxfmt-setup'
+
+const fixtures: string[] = []
+
+async function createFixture() {
+ const cwd = await mkdtemp(join(tmpdir(), 'oxfmt-setup-'))
+ fixtures.push(cwd)
+ return cwd
+}
+
+afterEach(async () => {
+ await Promise.all(fixtures.splice(0).map(dir => rm(dir, { recursive: true, force: true })))
+})
+
+describe('getOxfmtMigration', () => {
+ it('prefers Prettier over Biome, then detects either Biome config', async () => {
+ const cwd = await createFixture()
+ await writeFile(join(cwd, 'biome.json'), '{}')
+ expect(getOxfmtMigration(cwd)).toBe('biome')
+
+ await mkdir(join(cwd, 'node_modules', 'prettier'), { recursive: true })
+ await writeFile(join(cwd, 'node_modules', 'prettier', 'package.json'), '{"name":"prettier"}')
+ expect(getOxfmtMigration(cwd)).toBe('prettier')
+ })
+
+ it('installs Oxfmt before running the selected migration', async () => {
+ const cwd = await createFixture()
+ await writeFile(join(cwd, 'biome.jsonc'), '{}')
+ const startChildProcess = vi
+ .fn<
+ (
+ ...args: unknown[]
+ ) => Promise<{ getResult: () => Promise<{ exitCode: number; stderr: string }> }>
+ >()
+ .mockResolvedValue({ getResult: async () => ({ exitCode: 0, stderr: '' }) })
+ const setup = oxfmtSetup.setup!({
+ cwd,
+ terminals: { startChildProcess, sessions: new Map() },
+ } as any)
+
+ await setup.handler!({ migrate: true })
+
+ expect(startChildProcess.mock.calls[0]![0].args.at(-1)).toMatch(
+ /oxfmt@latest && .*oxfmt --migrate=biome/,
+ )
+ })
+
+ it('uses the Prettier migration command', async () => {
+ const cwd = await createFixture()
+ await mkdir(join(cwd, 'node_modules', 'prettier'), { recursive: true })
+ await writeFile(join(cwd, 'node_modules', 'prettier', 'package.json'), '{"name":"prettier"}')
+ const startChildProcess = vi
+ .fn<
+ (
+ ...args: unknown[]
+ ) => Promise<{ getResult: () => Promise<{ exitCode: number; stderr: string }> }>
+ >()
+ .mockResolvedValue({ getResult: async () => ({ exitCode: 0, stderr: '' }) })
+ const setup = oxfmtSetup.setup!({
+ cwd,
+ terminals: { startChildProcess, sessions: new Map() },
+ } as any)
+
+ await setup.handler!({ migrate: true })
+
+ expect(startChildProcess.mock.calls[0]![0].args.at(-1)).toMatch(
+ /oxfmt@latest && .*oxfmt --migrate=prettier/,
+ )
+ })
+
+ it('previews initialization when migration is disabled', async () => {
+ const cwd = await createFixture()
+ await writeFile(join(cwd, 'biome.json'), '{}')
+ const setup = oxfmtSetupPreview.setup!({ cwd } as any)
+
+ const preview = await setup.handler!({ migrate: false })
+
+ expect(preview).toMatchObject({
+ canMigrate: true,
+ migration: 'biome',
+ command: expect.stringMatching(/oxfmt@latest && .*oxfmt --init/),
+ gitDirty: false,
+ })
+ })
+
+ it('initializes Oxfmt without a migration source', async () => {
+ const cwd = await createFixture()
+ const startChildProcess = vi
+ .fn<
+ (
+ ...args: unknown[]
+ ) => Promise<{ getResult: () => Promise<{ exitCode: number; stderr: string }> }>
+ >()
+ .mockResolvedValue({ getResult: async () => ({ exitCode: 0, stderr: '' }) })
+ const setup = oxfmtSetup.setup!({
+ cwd,
+ terminals: { startChildProcess, sessions: new Map() },
+ } as any)
+
+ await setup.handler!({ migrate: false })
+
+ expect(startChildProcess.mock.calls[0]![0].args.at(-1)).toMatch(
+ /oxfmt@latest && .*oxfmt --init/,
+ )
+ })
+})
diff --git a/packages/oxc/src/node/diagnostics.ts b/packages/oxc/src/node/diagnostics.ts
index ac232d7a5..daba01e2f 100644
--- a/packages/oxc/src/node/diagnostics.ts
+++ b/packages/oxc/src/node/diagnostics.ts
@@ -27,5 +27,9 @@ export const diagnostics = /* #__PURE__ */ defineDiagnostics({
why: (p: { reason: string }) => `Failed to set up Oxlint: ${p.reason}`,
fix: 'Check the project package manager and configuration, then try again.',
},
+ OXDT0006: {
+ why: (p: { reason: string }) => `Failed to set up Oxfmt: ${p.reason}`,
+ fix: 'Check the project package manager and configuration, then try again.',
+ },
},
})
diff --git a/packages/oxc/src/node/rpc/functions/oxfmt-setup.ts b/packages/oxc/src/node/rpc/functions/oxfmt-setup.ts
new file mode 100644
index 000000000..3dc1ba386
--- /dev/null
+++ b/packages/oxc/src/node/rpc/functions/oxfmt-setup.ts
@@ -0,0 +1,87 @@
+import type { DevToolsTerminalHost } from '@vitejs/devtools-kit'
+import type { DevframeNodeContext } from 'devframe/types'
+import { existsSync } from 'node:fs'
+import { isPackageExists } from 'local-pkg'
+import { addDependencyCommand, detectPackageManager, dlxCommand } from 'nypm'
+import { Diagnostic } from 'nostics'
+import { join } from 'pathe'
+import { x } from 'tinyexec'
+import { diagnostics } from '../../diagnostics'
+import { defineOxcRpc } from '../_define'
+import { startSetup } from './setup'
+
+type ContextWithTerminals = DevframeNodeContext & { terminals?: DevToolsTerminalHost }
+export type OxfmtMigration = 'prettier' | 'biome'
+
+export function getOxfmtMigration(root: string): OxfmtMigration | undefined {
+ if (isPackageExists('prettier', { paths: [root] })) return 'prettier'
+ if (existsSync(join(root, 'biome.json')) || existsSync(join(root, 'biome.jsonc'))) return 'biome'
+}
+
+async function getSetupCommands(root: string, migrate: boolean): Promise {
+ const packageManager = (await detectPackageManager(root))?.name ?? 'npm'
+ const install = addDependencyCommand(packageManager, 'oxfmt@latest', { dev: true })
+ const migration = migrate ? getOxfmtMigration(root) : undefined
+ const args = migration ? [`--migrate=${migration}`] : ['--init']
+ return [install, dlxCommand(packageManager, 'oxfmt', { args, short: true })]
+}
+
+async function isGitDirty(root: string): Promise {
+ try {
+ const result = await x('git', ['-C', root, 'status', '--porcelain'], {
+ nodeOptions: { cwd: root },
+ })
+ return result.exitCode === 0 && Boolean(result.stdout.trim())
+ } catch {
+ return false
+ }
+}
+
+async function startOxfmtSetup(
+ context: ContextWithTerminals,
+ migrate: boolean,
+): Promise<{ sessionId?: string }> {
+ try {
+ const migration = migrate ? getOxfmtMigration(context.cwd) : undefined
+ return startSetup(
+ context,
+ await getSetupCommands(context.cwd, Boolean(migration)),
+ migration
+ ? `Migrate ${migration === 'prettier' ? 'Prettier' : 'Biome'} to Oxfmt`
+ : 'Install Oxfmt',
+ diagnostics.OXDT0006,
+ )
+ } catch (error) {
+ if (error instanceof Diagnostic) throw error
+ throw diagnostics.OXDT0006({
+ reason: error instanceof Error ? error.message : String(error),
+ cause: error,
+ })
+ }
+}
+
+export const oxfmtSetup = defineOxcRpc({
+ name: 'devtools-oxc:setup-oxfmt',
+ type: 'action',
+ setup: context => ({
+ handler: ({ migrate }: { migrate: boolean }) =>
+ startOxfmtSetup(context as ContextWithTerminals, migrate),
+ }),
+})
+
+export const oxfmtSetupPreview = defineOxcRpc({
+ name: 'devtools-oxc:oxfmt-setup-preview',
+ type: 'query',
+ jsonSerializable: true,
+ setup: context => ({
+ handler: async ({ migrate }: { migrate: boolean }) => {
+ const migration = getOxfmtMigration(context.cwd)
+ return {
+ canMigrate: Boolean(migration),
+ migration,
+ command: (await getSetupCommands(context.cwd, migrate)).join(' && '),
+ gitDirty: await isGitDirty(context.cwd),
+ }
+ },
+ }),
+})
diff --git a/packages/oxc/src/node/rpc/functions/oxlint-setup.ts b/packages/oxc/src/node/rpc/functions/oxlint-setup.ts
index f92b67488..9407b84c3 100644
--- a/packages/oxc/src/node/rpc/functions/oxlint-setup.ts
+++ b/packages/oxc/src/node/rpc/functions/oxlint-setup.ts
@@ -8,6 +8,7 @@ import { x } from 'tinyexec'
import { diagnostics } from '../../diagnostics'
import { CONFIG_FILES } from '../../utils/config-files'
import { defineOxcRpc } from '../_define'
+import { startSetup, waitForSetup } from './setup'
const eslintConfigFiles = [
'eslint.config.js',
@@ -19,12 +20,6 @@ const eslintConfigFiles = [
]
type ContextWithTerminals = DevframeNodeContext & { terminals?: DevToolsTerminalHost }
-type MigrationSession = Awaited>
-
-let current: MigrationSession | undefined
-let currentSessionId: string | undefined
-let runCount = 0
-
export function needsOxlintMigration(root: string): boolean {
return (
eslintConfigFiles.some(file => existsSync(join(root, file))) &&
@@ -82,61 +77,6 @@ async function startInstall(context: ContextWithTerminals): Promise<{ sessionId?
}
}
-async function startSetup(
- context: ContextWithTerminals,
- commandLines: string[],
- title: string,
-): Promise<{ sessionId?: string }> {
- const terminals = context.terminals
- if (terminals) {
- if (currentSessionId && terminals.sessions.get(currentSessionId)?.status === 'running')
- return { sessionId: currentSessionId }
-
- const command = commandLines.join(' && ')
- currentSessionId = `devtools-oxc:setup:${++runCount}`
- current = await terminals.startChildProcess(
- process.platform === 'win32'
- ? { command: 'cmd', args: ['/d', '/s', '/c', command], cwd: context.cwd }
- : { command: 'sh', args: ['-c', command], cwd: context.cwd },
- { id: currentSessionId, title, icon: 'ph:terminal-window-duotone' },
- )
- return { sessionId: currentSessionId }
- }
-
- current = undefined
- currentSessionId = undefined
- for (const commandLine of commandLines) {
- const [command, ...args] = commandLine.split(' ')
- const result = await x(command!, args, { nodeOptions: { cwd: context.cwd } })
- if (result.exitCode !== 0) {
- throw diagnostics.OXDT0005({
- reason: result.stderr.trim() || `Command exited with code ${result.exitCode ?? 'null'}.`,
- })
- }
- }
- return {}
-}
-
-async function waitForSetup(): Promise {
- if (!current) return
- try {
- const result = await current.getResult()
- if (result.exitCode !== 0) {
- throw diagnostics.OXDT0005({
- reason:
- result.stderr.trim() ||
- `Migration command exited with code ${result.exitCode ?? 'null'}.`,
- })
- }
- } catch (error) {
- if (error instanceof Diagnostic) throw error
- throw diagnostics.OXDT0005({
- reason: error instanceof Error ? error.message : String(error),
- cause: error,
- })
- }
-}
-
export const oxlintMigrate = defineOxcRpc({
name: 'devtools-oxc:migrate-eslint',
type: 'action',
diff --git a/packages/oxc/src/node/rpc/functions/setup.ts b/packages/oxc/src/node/rpc/functions/setup.ts
new file mode 100644
index 000000000..83e1b7a2d
--- /dev/null
+++ b/packages/oxc/src/node/rpc/functions/setup.ts
@@ -0,0 +1,70 @@
+import type { DevToolsTerminalHost } from '@vitejs/devtools-kit'
+import type { DevframeNodeContext } from 'devframe/types'
+import { Diagnostic } from 'nostics'
+import { x } from 'tinyexec'
+import { diagnostics } from '../../diagnostics'
+
+export type SetupContext = DevframeNodeContext & { terminals?: DevToolsTerminalHost }
+type SetupSession = Awaited>
+
+let current: SetupSession | undefined
+let currentSessionId: string | undefined
+let runCount = 0
+let setupDiagnostic = diagnostics.OXDT0005
+
+export async function startSetup(
+ context: SetupContext,
+ commandLines: string[],
+ title: string,
+ diagnostic = diagnostics.OXDT0005,
+): Promise<{ sessionId?: string }> {
+ setupDiagnostic = diagnostic
+ const terminals = context.terminals
+ if (terminals) {
+ if (currentSessionId && terminals.sessions.get(currentSessionId)?.status === 'running')
+ return { sessionId: currentSessionId }
+
+ const command = commandLines.join(' && ')
+ currentSessionId = `devtools-oxc:setup:${++runCount}`
+ current = await terminals.startChildProcess(
+ process.platform === 'win32'
+ ? { command: 'cmd', args: ['/d', '/s', '/c', command], cwd: context.cwd }
+ : { command: 'sh', args: ['-c', command], cwd: context.cwd },
+ { id: currentSessionId, title, icon: 'ph:terminal-window-duotone' },
+ )
+ return { sessionId: currentSessionId }
+ }
+
+ current = undefined
+ currentSessionId = undefined
+ for (const commandLine of commandLines) {
+ const [command, ...args] = commandLine.split(' ')
+ const result = await x(command!, args, { nodeOptions: { cwd: context.cwd } })
+ if (result.exitCode !== 0) {
+ throw setupDiagnostic({
+ reason: result.stderr.trim() || `Command exited with code ${result.exitCode ?? 'null'}.`,
+ })
+ }
+ }
+ return {}
+}
+
+export async function waitForSetup(): Promise {
+ if (!current) return
+ try {
+ const result = await current.getResult()
+ if (result.exitCode !== 0) {
+ throw setupDiagnostic({
+ reason:
+ result.stderr.trim() ||
+ `Migration command exited with code ${result.exitCode ?? 'null'}.`,
+ })
+ }
+ } catch (error) {
+ if (error instanceof Diagnostic) throw error
+ throw setupDiagnostic({
+ reason: error instanceof Error ? error.message : String(error),
+ cause: error,
+ })
+ }
+}
diff --git a/packages/oxc/src/node/rpc/index.ts b/packages/oxc/src/node/rpc/index.ts
index 11bb6c96e..73f67aff7 100644
--- a/packages/oxc/src/node/rpc/index.ts
+++ b/packages/oxc/src/node/rpc/index.ts
@@ -15,6 +15,7 @@ import {
oxlintSetupPreview,
oxlintWaitForSetup,
} from './functions/oxlint-setup'
+import { oxfmtSetup, oxfmtSetupPreview } from './functions/oxfmt-setup'
export const rpcFunctions = [
oxlintRun,
@@ -30,6 +31,8 @@ export const rpcFunctions = [
oxlintInstall,
oxlintSetupPreview,
oxlintWaitForSetup,
+ oxfmtSetup,
+ oxfmtSetupPreview,
openInEditor,
] as const
diff --git a/packages/oxc/src/nuxt.config.ts b/packages/oxc/src/nuxt.config.ts
index 8498611ba..a664d6a56 100644
--- a/packages/oxc/src/nuxt.config.ts
+++ b/packages/oxc/src/nuxt.config.ts
@@ -1,8 +1,6 @@
import { fileURLToPath } from 'node:url'
-import { devframeViteBridge } from '@devframes/vite/single'
import { defineNuxtConfig } from 'nuxt/config'
import { alias } from '../../../alias'
-import { oxcDevframe } from './node/devframe'
const BASE = '/__devtools-oxc/'
@@ -31,7 +29,7 @@ export default defineNuxtConfig({
},
},
},
- modules: ['@unocss/nuxt', '@vueuse/nuxt'],
+ modules: ['@unocss/nuxt', '@vueuse/nuxt', './modules/rpc'],
alias,
@@ -58,7 +56,6 @@ export default defineNuxtConfig({
},
vite: {
base: BASE,
- plugins: [devframeViteBridge({ ...oxcDevframe, basePath: '/' }, { base: BASE })],
optimizeDeps: {
include: ['modern-monaco', 'floating-vue'],
},