Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 92 additions & 3 deletions packages/oxc/src/node/__tests__/oxfmt-run.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
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 { x } from 'tinyexec'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
getOxfmtFormatCommand,
getOxfmtRunError,
listOxfmtFormatResults,
oxfmtRun,
parseOxfmtFormatOutput,
saveOxfmtFormatResult,
} from '../rpc/functions/oxfmt-run'

vi.mock('tinyexec', () => ({ x: vi.fn<typeof x>() }))

const fixtures: string[] = []

async function createFixture() {
Expand All @@ -35,8 +39,8 @@ describe('getOxfmtFormatCommand', () => {
})

it('returns Oxfmt diagnostics without whitespace', () => {
expect(getOxfmtRunError(' Invalid config.\n')).toBe('Invalid config.')
expect(getOxfmtRunError(' \n')).toBeUndefined()
expect(getOxfmtRunError(' Invalid config.\n', 2, 'error')).toBe('Invalid config.')
expect(getOxfmtRunError(' \n', 0, 'clean')).toBeUndefined()
})

it('persists each parsed check under its timestamp directory', async () => {
Expand Down Expand Up @@ -134,3 +138,88 @@ Finished in 113ms on 11 files using 8 threads.`,
})
})
})

describe('oxfmtRun', () => {
const notice =
'No config found, using defaults. Please add a config file or try `oxfmt --init` if needed.'
const clean =
'All matched files use the correct format.\nFinished in 3ms on 1 files using 8 threads.'
const issues =
'sample.js (1ms)\nFormat issues found in above 1 files. Run without `--check` to fix.\nFinished in 3ms on 1 files using 8 threads.'

it.each([false, true])(
'saves a successful run despite stderr notices (write=%s)',
async write => {
const cwd = await createFixture()
vi.mocked(x).mockResolvedValue({ exitCode: 0, stdout: clean, stderr: notice })
const { handler } = oxfmtRun.setup!({ cwd } as any)

await expect(handler({ write })).resolves.toEqual({ exitCode: 0 })
await expect(listOxfmtFormatResults(cwd)).resolves.toMatchObject([
{ mode: write ? 'write' : 'check', status: 'clean', summary: { fileCount: 1 } },
])
},
)

it('saves check findings with exit code 1 despite stderr notices', async () => {
const cwd = await createFixture()
vi.mocked(x).mockResolvedValue({ exitCode: 1, stdout: issues, stderr: notice })
const { handler } = oxfmtRun.setup!({ cwd } as any)

await expect(handler({ write: false })).resolves.toEqual({ exitCode: 1 })
await expect(listOxfmtFormatResults(cwd)).resolves.toMatchObject([
{ mode: 'check', status: 'issues', files: [{ path: 'sample.js' }] },
])
})

it.each([
{
write: true,
exitCode: undefined,
stdout: '',
stderr: '',
reason: 'Command exited with code unknown.',
},
{ write: false, exitCode: 2, stdout: '', stderr: 'Invalid config.', reason: 'Invalid config.' },
{
write: true,
exitCode: 1,
stdout: issues,
stderr: 'Cannot write file.',
reason: 'Cannot write file.',
},
{ write: false, exitCode: 1, stdout: '', stderr: '', reason: 'Command exited with code 1.' },
{
write: false,
exitCode: 2,
stdout: issues,
stderr: '',
reason: 'Command exited with code 2.',
},
])(
'rejects execution failures: $write/$exitCode/$reason',
async ({ write, reason, ...result }) => {
const cwd = await createFixture()
vi.mocked(x).mockResolvedValue(result)
const { handler } = oxfmtRun.setup!({ cwd } as any)

await expect(handler({ write })).rejects.toMatchObject({
name: 'OXDT0007',
message: expect.stringContaining(reason),
})
await expect(listOxfmtFormatResults(cwd)).resolves.toEqual([])
},
)

it('preserves process startup failures', async () => {
const cwd = await createFixture()
vi.mocked(x).mockRejectedValue(new Error('spawn oxfmt ENOENT'))
const { handler } = oxfmtRun.setup!({ cwd } as any)

await expect(handler({ write: false })).rejects.toMatchObject({
name: 'OXDT0007',
message: expect.stringContaining('spawn oxfmt ENOENT'),
})
await expect(listOxfmtFormatResults(cwd)).resolves.toEqual([])
})
})
22 changes: 15 additions & 7 deletions packages/oxc/src/node/rpc/functions/oxfmt-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,14 @@ export function getOxfmtFormatCommand(write: boolean, vitePlus: boolean): OxfmtC
return { command: 'oxfmt', args: [option] }
}

export function getOxfmtRunError(stderr: string) {
return stderr.trim() || undefined
export function getOxfmtRunError(
stderr: string,
exitCode: number | undefined,
status: OxfmtFormatLog['status'],
) {
// Check mode uses exit code 1 for formatting findings, which are saved results.
if (exitCode === 0 || (exitCode === 1 && status === 'issues')) return undefined
return stderr.trim() || `Command exited with code ${exitCode ?? 'unknown'}.`
}

async function getPreview(root: string, write: boolean) {
Expand Down Expand Up @@ -123,16 +129,18 @@ 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)
const log = parseOxfmtFormatOutput(
result.stdout,
write ? 'write' : 'check',
result.exitCode,
)
const reason = getOxfmtRunError(result.stderr, result.exitCode, log.status)
if (reason) {
throw diagnostics.OXDT0007({
reason,
})
}
await saveOxfmtFormatResult(
context.cwd,
parseOxfmtFormatOutput(result.stdout, write ? 'write' : 'check', result.exitCode),
)
await saveOxfmtFormatResult(context.cwd, log)
return { exitCode: result.exitCode }
} catch (error) {
if (error instanceof Diagnostic) throw error
Expand Down
Loading