From 6dfba13016f2f21d82d5c8127454f79353ca67c8 Mon Sep 17 00:00:00 2001 From: yuyinws Date: Mon, 24 Aug 2026 17:35:54 +0800 Subject: [PATCH 1/4] fix(oxc): register standalone RPC through DevTools --- packages/oxc/src/modules/rpc.ts | 24 ++++++++++++++++++++++++ packages/oxc/src/nuxt.config.ts | 5 +---- 2 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 packages/oxc/src/modules/rpc.ts 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/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'], }, From 856f1894c53bb7c5fd59baf870983b1d20d44b25 Mon Sep 17 00:00:00 2001 From: yuyinws Date: Mon, 24 Aug 2026 20:44:18 +0800 Subject: [PATCH 2/4] feat: oxfmt setup flow --- docs/errors/OXDT0006.md | 16 ++ docs/errors/index.md | 1 + .../src/app/components/SetupOxfmtDialog.vue | 182 ++++++++++++++++++ packages/oxc/src/app/pages/index.vue | 14 ++ .../src/node/__tests__/oxfmt-setup.test.ts | 110 +++++++++++ packages/oxc/src/node/diagnostics.ts | 4 + .../oxc/src/node/rpc/functions/oxfmt-setup.ts | 87 +++++++++ .../src/node/rpc/functions/oxlint-setup.ts | 62 +----- packages/oxc/src/node/rpc/functions/setup.ts | 70 +++++++ packages/oxc/src/node/rpc/index.ts | 3 + 10 files changed, 488 insertions(+), 61 deletions(-) create mode 100644 docs/errors/OXDT0006.md create mode 100644 packages/oxc/src/app/components/SetupOxfmtDialog.vue create mode 100644 packages/oxc/src/node/__tests__/oxfmt-setup.test.ts create mode 100644 packages/oxc/src/node/rpc/functions/oxfmt-setup.ts create mode 100644 packages/oxc/src/node/rpc/functions/setup.ts 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..16aec602b --- /dev/null +++ b/packages/oxc/src/app/components/SetupOxfmtDialog.vue @@ -0,0 +1,182 @@ + + + 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
+ { + 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 From 9d5b84eb1c3cc1dc4383bf0320539ab803a1388e Mon Sep 17 00:00:00 2001 From: yuyinws Date: Mon, 24 Aug 2026 20:50:43 +0800 Subject: [PATCH 3/4] fix(oxc): keep setup dialog open in terminal --- packages/oxc/src/app/components/SetupOxfmtDialog.vue | 1 - packages/oxc/src/app/components/SetupOxlintDialog.vue | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/oxc/src/app/components/SetupOxfmtDialog.vue b/packages/oxc/src/app/components/SetupOxfmtDialog.vue index 16aec602b..850684124 100644 --- a/packages/oxc/src/app/components/SetupOxfmtDialog.vue +++ b/packages/oxc/src/app/components/SetupOxfmtDialog.vue @@ -85,7 +85,6 @@ async function viewInTerminal() { params: { sessionId: sessionId.value }, }) } - open.value = false } diff --git a/packages/oxc/src/app/components/SetupOxlintDialog.vue b/packages/oxc/src/app/components/SetupOxlintDialog.vue index 9815f4d3a..755bac73b 100644 --- a/packages/oxc/src/app/components/SetupOxlintDialog.vue +++ b/packages/oxc/src/app/components/SetupOxlintDialog.vue @@ -83,7 +83,6 @@ async function viewInTerminal() { params: { sessionId: sessionId.value }, }) } - open.value = false } From 0f1f9e1d1bd52d8f7a54cc284b45d07dc326400e Mon Sep 17 00:00:00 2001 From: yuyinws Date: Mon, 24 Aug 2026 20:56:31 +0800 Subject: [PATCH 4/4] fix(oxc): hide setup actions while loading --- packages/oxc/src/app/components/SetupOxfmtDialog.vue | 6 +++++- packages/oxc/src/app/components/SetupOxlintDialog.vue | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/oxc/src/app/components/SetupOxfmtDialog.vue b/packages/oxc/src/app/components/SetupOxfmtDialog.vue index 850684124..242590716 100644 --- a/packages/oxc/src/app/components/SetupOxfmtDialog.vue +++ b/packages/oxc/src/app/components/SetupOxfmtDialog.vue @@ -22,10 +22,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:oxfmt-setup-preview', { migrate: migrate.value, @@ -39,6 +41,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 } } @@ -121,7 +125,7 @@ async function viewInTerminal() {

-
+
Migrate from {{ migration === 'prettier' ? 'Prettier' : 'Biome' }}() 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 } } @@ -127,7 +131,7 @@ async function viewInTerminal() {

-
+
Migrate from ESLint