Skip to content
Open
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,8 @@
"enum": [
"auto",
"node",
"deno"
"deno",
"bun"
]
},
"vitest.watchOnStartup": {
Expand Down
2 changes: 1 addition & 1 deletion packages/extension/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export function getConfig(workspaceFolder?: WorkspaceFolder) {
const ignoreWorkspace = get<boolean>('ignoreWorkspace', false) ?? false
const showInlineConsoleLog = get<boolean>('showInlineConsoleLog', true) ?? true
const forceCancelTimeout = get<number>('forceCancelTimeout', 1000) ?? 1000
const runtime = get<'node' | 'deno' | 'auto'>('runtime', 'auto') ?? 'auto'
const runtime = get<'node' | 'deno' | 'bun' | 'auto'>('runtime', 'auto') ?? 'auto'
const watchOnStartup = get<boolean>('watchOnStartup', false) ?? false

return {
Expand Down
2 changes: 2 additions & 0 deletions packages/extension/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { resolve } from 'pathe'
export const minimumVersion = '1.4.0'
// follows minimum Vitest
export const minimumNodeVersion = '18.0.0'
// Bun 1.4 added native Vitest support
export const minimumBunVersion = '1.4.0'

export const distDir = __dirname
export const workerPath = resolve(__dirname, 'worker.js')
Expand Down
3 changes: 3 additions & 0 deletions packages/extension/src/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ export async function debugTests(
if (pkg.runtime === 'deno') {
runtimeArgs.push('-A')
}
if (pkg.runtime === 'bun') {
runtimeArgs.push('--bun')
}

log.info('[DEBUG]', 'Starting debugging session', runtimeExecutable, ...(runtimeArgs || []))

Expand Down
3 changes: 3 additions & 0 deletions packages/extension/src/spawn/child_process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ export async function createVitestProcess(pkg: VitestPackage, options?: ProcessS
execArgv.push('-A')
executablePath = pathToFileURL(workerPath).toString()
}
if (folderConfig.runtime === 'bun') {
execArgv.push('--bun')
}
const arvString = execArgv.join(' ')
const script = `${executable} ${arvString ? `${arvString} ` : ''}${executablePath}`.trim()
log.info('[API]', `Running ${formatPkg(pkg)} with "${script}"`)
Expand Down
78 changes: 56 additions & 22 deletions packages/extension/src/spawn/pkg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { gte } from 'semver'
import { getSuggestedInstallCommand } from 'vitest-vscode-shared'
import * as vscode from 'vscode'
import { getConfig } from '../config'
import { configGlob, minimumVersion, workspaceGlob } from '../constants'
import { configGlob, minimumBunVersion, minimumVersion, workspaceGlob } from '../constants'
import { log } from '../log'
import { getBunVersion, validateBunVersion } from '../utils'
import { resolveVitestPackage } from './resolve'

function nonNullable<T>(value: T | null | undefined): value is T {
Expand All @@ -27,7 +28,7 @@ export interface VitestPackage {
workspaceFile?: string
loader?: string
pnp?: string
runtime: 'deno' | 'node'
runtime: 'deno' | 'node' | 'bun'
}

// Before 5.0.0-rc.1 `testNamePattern` was matched the same way Jest does it:
Expand All @@ -48,10 +49,10 @@ function isVitestInPackageJson(root: string) {
return false
}

function resolveVitestConfig(
async function resolveVitestConfig(
showWarning: boolean,
configOrWorkspaceFile: vscode.Uri,
): VitestPackage | null {
): Promise<VitestPackage | null> {
const folder = vscode.workspace.getWorkspaceFolder(configOrWorkspaceFile)!
if (!folder)
throw new Error(`Workspace folder not found for ${configOrWorkspaceFile}. Does the file exist?`)
Expand Down Expand Up @@ -88,6 +89,14 @@ function resolveVitestConfig(
const prefix = `${basename(dirname(id))}:${basename(id)}`
const runtime = guessRuntime(cwd, folder)

if (runtime === 'bun') {
const bunVersion = await getBunVersion(cwd)
if (bunVersion && !(await validateBunVersion(bunVersion, cwd, showWarning))) {
log.error('[API]', `Skipping ${configOrWorkspaceFile.fsPath} due to unsupported Bun version.`)
return null
}
}

if (vitest.pnp) {
return {
folder,
Expand Down Expand Up @@ -155,8 +164,10 @@ export async function resolveVitestPackages(
])
if (!workspaceConfigs.meta.length && !configs.meta.length) {
const pkg = await resolveVitestPackagesViaPackageJson(showWarning)
if (!pkg.meta.length && !pkg.warned)
return { configs: resolveVitestWorkspacePackages(showWarning).meta, workspaces: [] }
if (!pkg.meta.length && !pkg.warned) {
const workspacePackages = await resolveVitestWorkspacePackages(showWarning)
return { configs: workspacePackages.meta, workspaces: [] }
}
return { configs: pkg.meta, workspaces: [] }
}
return {
Expand All @@ -165,10 +176,11 @@ export async function resolveVitestPackages(
}
}

function resolveVitestWorkspacePackages(showWarning: boolean) {
async function resolveVitestWorkspacePackages(showWarning: boolean) {
let warned = false
const meta: VitestPackage[] = []
vscode.workspace.workspaceFolders?.forEach((folder) => {

const processFolder = async (folder: vscode.WorkspaceFolder) => {
const cwd = normalize(folder.uri.fsPath)
const vitest = resolveVitestPackage(cwd, folder)
if (!vitest) return
Expand All @@ -181,6 +193,16 @@ function resolveVitestWorkspacePackages(showWarning: boolean) {
const id = normalize(folder.uri.fsPath)
const prefix = `${basename(cwd)}:${basename(id)}`
const runtime = guessRuntime(cwd, folder)

if (runtime === 'bun') {
const bunVersion = await getBunVersion(cwd)
if (bunVersion && !(await validateBunVersion(bunVersion, cwd, showWarning))) {
log.error('[API]', `Skipping ${cwd} due to unsupported Bun version.`)
warned = true
return
}
}

meta.push({
folder,
id,
Expand All @@ -192,7 +214,10 @@ function resolveVitestWorkspacePackages(showWarning: boolean) {
runtime,
name: vitest.packageName,
})
})
}

await Promise.all(vscode.workspace.workspaceFolders?.map(processFolder) ?? [])

return {
meta,
warned,
Expand Down Expand Up @@ -280,16 +305,18 @@ async function resolveVitestWorkspaceConfigs() {

if (vitestWorkspaces.length) {
// if there is a workspace config, use it as root
const resolved = await Promise.all(
vitestWorkspaces.map((config) =>
resolveVitestConfig(
/* don't show warnings for workspaces because they have limited support */ false,
config,
),
),
)
const meta = resolvePackagUniquePrefixes(
vitestWorkspaces
.map((config) => {
const vitest = resolveVitestConfig(
/* don't show warnings for workspaces because they have limited support */ false,
config,
)
if (!vitest) {
return null
}
resolved
.filter(nonNullable)
.map((vitest) => {
// Version 4 doesn't support workspace files
if (gte(vitest.version, '4.0.0')) {
return null
Expand Down Expand Up @@ -348,8 +375,8 @@ async function resolveVitestConfigs(showWarning: boolean) {
const filteredConfigFiles = hasViteAndVitestConfig
? configFiles.filter((file) => !basename(file.fsPath).includes('vite.'))
: configFiles
filteredConfigFiles.forEach((config) => {
const vitest = resolveVitestConfig(showWarning, config)
for (const config of filteredConfigFiles) {
const vitest = await resolveVitestConfig(showWarning, config)
if (vitest) {
resolvedMeta.push({
...vitest,
Expand All @@ -358,7 +385,7 @@ async function resolveVitestConfigs(showWarning: boolean) {
} else {
warned = true
}
})
}
}

return {
Expand All @@ -367,7 +394,7 @@ async function resolveVitestConfigs(showWarning: boolean) {
}
}

function guessRuntime(cwd: string, folder: vscode.WorkspaceFolder): 'deno' | 'node' {
function guessRuntime(cwd: string, folder: vscode.WorkspaceFolder): 'deno' | 'node' | 'bun' {
const vitestConfig = getConfig(folder)
if (vitestConfig.runtime !== 'auto') {
return vitestConfig.runtime
Expand All @@ -379,6 +406,13 @@ function guessRuntime(cwd: string, folder: vscode.WorkspaceFolder): 'deno' | 'no
if (existsSync(resolve(cwd, 'deno.json'))) {
return 'deno'
}
if (
existsSync(resolve(cwd, 'bun.lock')) ||
existsSync(resolve(cwd, 'bun.lockb')) ||
existsSync(resolve(cwd, 'bunfig.toml'))
) {
return 'bun'
}
return 'node'
}

Expand Down
3 changes: 3 additions & 0 deletions packages/extension/src/spawn/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ export async function createVitestTerminalProcess(
if (pkg.runtime === 'deno') {
command += ' -A'
}
if (pkg.runtime === 'bun') {
command += ' --bun'
}
command += ` ${workerPath};`

log.info('[TERMINAL]', `Initiated ws connection via ${wsAddress}`)
Expand Down
47 changes: 45 additions & 2 deletions packages/extension/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { spawn } from 'node:child_process'
import fs from 'node:fs'
import { inspect, stripVTControlCharacters } from 'node:util'
import { dirname, relative } from 'pathe'
import { gte } from 'semver'
import * as vscode from 'vscode'
import which from 'which'
import { getConfig } from './config'
Expand Down Expand Up @@ -32,6 +33,47 @@ export function pluralize(count: number, singular: string) {
return `${count} ${singular}${count === 1 ? '' : 's'}`
}

export async function getBunVersion(cwd: string): Promise<string | null> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to check bun version. We don't check node or deno

const executable = await findRuntimeExecutable('bun', cwd).catch(() => null)
if (!executable) return null

return new Promise<string | null>((resolve) => {
const child = spawn(executable, ['--version'], {
cwd,
stdio: 'pipe',
})

let output = ''
child.stdout.on('data', (data) => (output += data.toString()))
child.on('error', () => resolve(null))
child.on('exit', (exitCode) => {
if (exitCode !== 0) return resolve(null)
const version = output.trim()
if (!version) return resolve(null)
resolve(version)
})
})
}

export async function validateBunVersion(
bunVersion: string,
cwd: string,
showWarning: boolean,
): Promise<boolean> {
const { minimumBunVersion } = await import('./constants')
if (gte(bunVersion, minimumBunVersion)) {
return true
}

const message = `Bun v${bunVersion} is not supported. Bun v${minimumBunVersion} or newer is required for Vitest support.`
if (showWarning) {
vscode.window.showWarningMessage(message)
} else {
log.error('[API]', message)
}
return false
}

export function debounce<T extends (...args: any[]) => void>(cb: T, wait = 20) {
let h: NodeJS.Timeout | undefined
const callable = (...args: any) => {
Expand All @@ -57,13 +99,14 @@ export function waitUntilExists(file: string, timeoutMs = 5000) {
}

const pathToRuntime: {
bun?: string
deno?: string
node?: string
} = {}

// based on https://github.com/microsoft/playwright-vscode/blob/main/src/utils.ts#L144
export async function findRuntimeExecutable(
runtime: 'node' | 'deno',
runtime: 'node' | 'deno' | 'bun',
cwd: string,
): Promise<string> {
if (getConfig().nodeExecutable)
Expand Down Expand Up @@ -94,7 +137,7 @@ export async function findRuntimeExecutable(
return node
}

async function findRuntimeViaShell(runtime: 'node' | 'deno', cwd: string): Promise<string | null> {
async function findRuntimeViaShell(runtime: 'node' | 'deno' | 'bun', cwd: string): Promise<string | null> {
if (process.platform === 'win32') return null
return new Promise<string | null>((resolve) => {
const startToken = '___START_SHELL__'
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export interface WorkerInitMetadata {
id: string
cwd: string
arguments?: string
runtime: 'node' | 'deno'
runtime: 'node' | 'deno' | 'bun'
configFile?: string
workspaceFile?: string
env: Record<string, any> | undefined
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions samples/bun/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "bun-sample",
"version": "1.0.0",
"private": true,
"description": "Sample project using Bun runtime with Vitest",
"license": "ISC",
"scripts": {
"test": "bun --bun vitest run",
"test:watch": "bun --bun vitest"
},
"devDependencies": {
"vitest": "catalog:v3"
}
}
7 changes: 7 additions & 0 deletions samples/bun/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function greet(name: string): string {
return `Hello, ${name}!`
}

export function add(a: number, b: number): number {
return a + b
}
22 changes: 22 additions & 0 deletions samples/bun/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, it, expect } from 'vitest'
import { greet, add } from '../src/index'

describe('greet', () => {
it('should greet by name', () => {
expect(greet('World')).toBe('Hello, World!')
})

it('should handle empty string', () => {
expect(greet('')).toBe('Hello, !')
})
})

describe('add', () => {
it('should add two numbers', () => {
expect(add(1, 2)).toBe(3)
})

it('should handle negative numbers', () => {
expect(add(-1, -2)).toBe(-3)
})
})
7 changes: 7 additions & 0 deletions samples/bun/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
pool: 'forks',
},
})