Skip to content

Commit

Permalink
feat: add multi-root workspace run/debug support
Browse files Browse the repository at this point in the history
  • Loading branch information
evanandrewrose authored and zxch3n committed Jun 18, 2022
1 parent cd00947 commit 27951ad
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 103 deletions.
11 changes: 11 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@
"outFiles": ["${workspaceFolder}/out/**/*.js"]
// "preLaunchTask": "${defaultBuildTask}"
},
{
"name": "Run Extension Multi-Root Workspace Sample",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"${workspaceFolder}/samples/multi-root-workspace/sample.code-workspace"
],
"outFiles": ["${workspaceFolder}/out/**/*.js"],
"preLaunchTask": "${defaultBuildTask}"
},
{
"name": "Run Extension Monorepo Sample",
"type": "extensionHost",
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,12 @@
"null"
],
"default": null,
"scope": "machine"
"scope": "resource"
},
"vitest.showFailMessages": {
"description": "Get instant feedback when using Watch Mode. Pop-ups an error when a test fails.",
"type": "boolean",
"scope": "resource",
"scope": "window",
"default": false
}
}
Expand Down
15 changes: 15 additions & 0 deletions samples/multi-root-workspace/sample.code-workspace
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"folders": [
{
"name": "basic",
"path": "../basic"
},
{
"name": "react",
"path": "../monorepo/packages/react"
}
],
"settings": {
"testing.automaticallyOpenPeekView": "never"
}
}
41 changes: 33 additions & 8 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
import { workspace } from 'vscode'
import type { WorkspaceConfiguration, WorkspaceFolder } from 'vscode'
export const extensionId = 'zxch3n.vitest-explorer'

export function getConfig() {
const config = workspace.getConfiguration('vitest')
export function getConfigValue<T>(
rootConfig: WorkspaceConfiguration,
folderConfig: WorkspaceConfiguration,
key: string,
defaultValue: T,
): T {
return folderConfig.has(key)
? folderConfig.get<T>(key)!
: rootConfig.has(key)
? rootConfig.get<T>(key)!
: defaultValue
}

export function getConfig(workspaceFolder: WorkspaceFolder) {
const folderConfig = workspace.getConfiguration('vitest', workspaceFolder)
const rootConfig = workspace.getConfiguration('vitest')

const get = <T>(key: string, defaultValue: T) => getConfigValue<T>(rootConfig, folderConfig, key, defaultValue)

return {
env: get<null | Record<string, string>>('nodeEnv', null),
commandLine: get<string | undefined>('commandLine', undefined),
include: get<string[]>('include', []),
exclude: get<string[]>('exclude', []),
enable: get<boolean>('enable', false),
}
}

export function getRootConfig() {
const rootConfig = workspace.getConfiguration('vitest')

return {
env: config.get('nodeEnv') as null | Record<string, string>,
commandLine: (config.get('commandLine') || undefined) as string | undefined,
include: config.get('include') as string[],
exclude: config.get('exclude') as string[],
enable: config.get('enable') as boolean,
showFailMessages: config.get('showFailMessages') as boolean,
showFailMessages: rootConfig.get('showFailMessages', false),
}
}
8 changes: 4 additions & 4 deletions src/discover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ export class TestFileDiscoverer extends vscode.Disposable {
const watchers = [] as vscode.FileSystemWatcher[]
await Promise.all(
vscode.workspace.workspaceFolders.map(async (workspaceFolder) => {
const exclude = getConfig().exclude
for (const include of getConfig().include) {
const exclude = getConfig(workspaceFolder).exclude
for (const include of getConfig(workspaceFolder).include) {
const pattern = new vscode.RelativePattern(
workspaceFolder.uri,
include,
Expand Down Expand Up @@ -101,8 +101,8 @@ export class TestFileDiscoverer extends vscode.Disposable {

await Promise.all(
vscode.workspace.workspaceFolders.map(async (workspaceFolder) => {
const exclude = getConfig().exclude
for (const include of getConfig().include) {
const exclude = getConfig(workspaceFolder).exclude
for (const include of getConfig(workspaceFolder).include) {
const pattern = new vscode.RelativePattern(
workspaceFolder.uri,
include,
Expand Down
158 changes: 99 additions & 59 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { effect } from '@vue/reactivity'
import { extensionId, getConfig } from './config'
import { TestFileDiscoverer } from './discover'
import { isVitestEnv } from './pure/isVitestEnv'
import { getVitestCommand, getVitestVersion, isNodeAvailable, stringToCmd } from './pure/utils'
import { debugHandler, runHandler, updateSnapshot } from './runHandler'
import { getVitestCommand, getVitestVersion, isNodeAvailable, negate } from './pure/utils'
import { debugHandler, gatherTestItemsFromWorkspace, runHandler, updateSnapshot } from './runHandler'
import { TestFile, WEAKMAP_TEST_DATA } from './TestData'
import { TestWatcher } from './watch'
import { Command } from './command'
Expand All @@ -19,10 +19,10 @@ export async function activate(context: vscode.ExtensionContext) {
)
return

if (
!getConfig().enable
&& !(await isVitestEnv(vscode.workspace.workspaceFolders[0].uri.fsPath))
)
const vitestEnvironmentFolders = vscode.workspace.workspaceFolders.filter(async folder =>
await isVitestEnv(folder) || getConfig(folder).enable)

if (vitestEnvironmentFolders.length === 0)
return

const ctrl = vscode.tests.createTestController(`${extensionId}`, 'Vitest')
Expand All @@ -48,46 +48,56 @@ export async function activate(context: vscode.ExtensionContext) {
}
}

const vitestCmd = getVitestCommand(
vscode.workspace.workspaceFolders[0].uri.fsPath,
) ?? {
cmd: 'npx',
args: ['vitest'],
}
const vitestRunConfigs: {
workspace: vscode.WorkspaceFolder
cmd: string
args: string[]
version: string | null
}[] = await Promise.all(vitestEnvironmentFolders.map(async (folder) => {
const cmd = getVitestCommand(folder.uri.fsPath)

const vitestVersion = await getVitestVersion(vitestCmd, getConfig().env || undefined).catch(async (e) => {
log.appendLine(e.toString())
log.appendLine(`process.env.PATH = ${process.env.PATH}`)
log.appendLine(`vitest.nodeEnv = ${JSON.stringify(getConfig().env)}`)
let errorMsg = e.toString()
if (!isNodeAvailable(getConfig().env || undefined)) {
log.appendLine('Cannot spawn node process')
errorMsg += 'Cannot spawn node process. Please try setting vitest.nodeEnv as {"PATH": "/path/to/node"} in your settings.'
}
const version = await getVitestVersion(cmd, getConfig(folder).env || undefined).catch(async (e) => {
log.appendLine(e.toString())
log.appendLine(`process.env.PATH = ${process.env.PATH}`)
log.appendLine(`vitest.nodeEnv = ${JSON.stringify(getConfig(folder).env)}`)
let errorMsg = e.toString()
if (!isNodeAvailable(getConfig(folder).env || undefined)) {
log.appendLine('Cannot spawn node process')
errorMsg += 'Cannot spawn node process. Please try setting vitest.nodeEnv as {"PATH": "/path/to/node"} in your settings.'
}

vscode.window.showErrorMessage(errorMsg)
})

return cmd
? {
cmd: cmd.cmd,
args: cmd.args,
workspace: folder,
version: version ?? null,
}
: {
cmd: 'npx',
args: ['vitest'],
workspace: folder,
version: version ?? null,
}
}))

vscode.window.showErrorMessage(errorMsg)
vitestRunConfigs.forEach((vitest) => {
console.log(`Vitest Workspace: [${vitest.workspace.name}] Version: ${vitest.version}`)
})

console.dir({ vitestVersion })
const isCompatibleVitestConfig = (config: typeof vitestRunConfigs[number]) =>
(config.version && semver.gte(config.version, '0.8.0')) || getConfig(config.workspace).commandLine

const customTestCmd = getConfig().commandLine
if ((vitestVersion && semver.gte(vitestVersion, '0.8.0')) || customTestCmd) {
// enable run/debug/watch tests only if vitest version >= 0.8.0
const testWatcher: undefined | TestWatcher = registerWatchHandler(
vitestCmd ?? stringToCmd(customTestCmd!),
ctrl,
fileDiscoverer,
context,
)
registerRunHandler(ctrl, testWatcher)
context.subscriptions.push(
vscode.commands.registerCommand(Command.UpdateSnapshot, (test) => {
updateSnapshot(ctrl, test)
}),
)
}
else {
const msg = 'Because Vitest version < 0.8.0, run/debug/watch tests from Vitest extension disabled.\n'
vitestRunConfigs.filter(negate(isCompatibleVitestConfig)).forEach((config) => {
vscode.window.showWarningMessage(`Because Vitest version < 0.8.0 for ${config.workspace.name} `
+ ', run/debug/watch tests from Vitest extension disabled for that workspace.\n')
})

if (vitestRunConfigs.every(negate(isCompatibleVitestConfig))) {
const msg = 'Because Vitest version < 0.8.0 for every workspace folder, run/debug/watch tests from Vitest extension disabled.\n'
context.subscriptions.push(
vscode.commands.registerCommand(Command.ToggleWatching, () => {
vscode.window.showWarningMessage(msg)
Expand All @@ -102,6 +112,20 @@ export async function activate(context: vscode.ExtensionContext) {
vscode.window.showWarningMessage(msg)
}

// enable run/debug/watch tests only if vitest version >= 0.8.0
const testWatchers = registerWatchHandlers(
vitestRunConfigs.filter(isCompatibleVitestConfig),
ctrl,
fileDiscoverer,
context,
) ?? []
registerRunHandler(ctrl, testWatchers)
context.subscriptions.push(
vscode.commands.registerCommand(Command.UpdateSnapshot, (test) => {
updateSnapshot(ctrl, test)
}),
)

vscode.window.visibleTextEditors.forEach(x =>
fileDiscoverer.discoverTestFromDoc(ctrl, x.document),
)
Expand All @@ -122,40 +146,53 @@ export async function activate(context: vscode.ExtensionContext) {
)
}

function aggregateTestWatcherStatuses(testWatchers: TestWatcher[]) {
return testWatchers.reduce((aggregate, watcher) => {
return {
passed: aggregate.passed + watcher.testStatus.value.passed,
failed: aggregate.failed + watcher.testStatus.value.failed,
skipped: aggregate.skipped + watcher.testStatus.value.skipped,
}
}, {
passed: 0,
failed: 0,
skipped: 0,
})
}

let statusBarItem: StatusBarItem
function registerWatchHandler(
vitestCmd: { cmd: string; args: string[] } | undefined,
function registerWatchHandlers(
vitestConfigs: { cmd: string; args: string[]; workspace: vscode.WorkspaceFolder }[],
ctrl: vscode.TestController,
fileDiscoverer: TestFileDiscoverer,
context: vscode.ExtensionContext,
) {
if (!vitestCmd)
return
const testWatchers = vitestConfigs.map((vitestConfig, index) =>
TestWatcher.create(ctrl, fileDiscoverer, vitestConfig, vitestConfig.workspace, index),
) ?? []

const testWatcher = TestWatcher.create(ctrl, fileDiscoverer, vitestCmd)
statusBarItem = new StatusBarItem()
effect(() => {
if (testWatcher.isRunning.value) {
if (testWatchers.some(watcher => watcher.isRunning.value)) {
statusBarItem.toRunningMode()
return
}

if (testWatcher.isWatching.value) {
statusBarItem.toWatchMode(testWatcher.testStatus.value)
else if (testWatchers.some(watcher => watcher.isWatching.value)) {
statusBarItem.toWatchMode(aggregateTestWatcherStatuses(testWatchers))
return
}

statusBarItem.toDefaultMode()
})

const stopWatching = () => {
testWatcher!.dispose()
testWatchers.forEach(watcher => watcher.dispose())
vscode.workspace
.getConfiguration('testing')
.update('automaticallyOpenPeekView', undefined)
}
const startWatching = () => {
testWatcher!.watch()
testWatchers.forEach(watcher => watcher.watch())
vscode.workspace
.getConfiguration('testing')
.update('automaticallyOpenPeekView', 'never')
Expand All @@ -165,12 +202,13 @@ function registerWatchHandler(
{
dispose: stopWatching,
},
testWatcher,
...testWatchers,
statusBarItem,
vscode.commands.registerCommand(Command.StartWatching, startWatching),
vscode.commands.registerCommand(Command.StopWatching, stopWatching),
vscode.commands.registerCommand(Command.ToggleWatching, () => {
if (testWatcher.isWatching.value)
const anyWatching = testWatchers.some(watcher => watcher.isWatching.value)
if (anyWatching)
stopWatching()
else
startWatching()
Expand All @@ -194,21 +232,23 @@ function registerWatchHandler(
)
return

await testWatcher.watch()
testWatcher.runTests(request.include)
await Promise.all(testWatchers.map(watcher => watcher.watch()))
testWatchers.forEach((watcher) => {
watcher.runTests(gatherTestItemsFromWorkspace(request.include ?? [], watcher.workspace.uri.fsPath))
})
}

return testWatcher
return testWatchers
}

function registerRunHandler(
ctrl: vscode.TestController,
testWatcher?: TestWatcher,
testWatchers: TestWatcher[],
) {
ctrl.createRunProfile(
'Run Tests',
vscode.TestRunProfileKind.Run,
runHandler.bind(null, ctrl, testWatcher),
runHandler.bind(null, ctrl, testWatchers),
true,
)

Expand Down
6 changes: 5 additions & 1 deletion src/pure/isVitestEnv.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { existsSync } from 'fs'
import path = require('path')
import { readFile, readdir } from 'fs-extra'
import type { WorkspaceFolder } from 'vscode'
import { getVitestPath } from './utils'

export async function isVitestEnv(projectRoot: string): Promise<boolean> {
export async function isVitestEnv(projectRoot: string | WorkspaceFolder): Promise<boolean> {
if (typeof projectRoot !== 'string')
return isVitestEnv(projectRoot.uri.fsPath)

if (getVitestPath(projectRoot))
return true

Expand Down
4 changes: 4 additions & 0 deletions src/pure/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,7 @@ export function stringToCmd(cmdStr: string): Cmd {
args: list.slice(1),
}
}

export function negate(func: Function): (...args: typeof func.arguments) => Boolean {
return (...args: typeof func.arguments) => !func(...args)
}
Loading

0 comments on commit 27951ad

Please sign in to comment.