diff --git a/src/api/child_process.ts b/src/api/child_process.ts index aeeb33b..fd95e52 100644 --- a/src/api/child_process.ts +++ b/src/api/child_process.ts @@ -6,7 +6,7 @@ import { createServer } from 'node:http' import getPort from 'get-port' import type WebSocket from 'ws' import { WebSocketServer } from 'ws' -import { formatPkg, showVitestError } from '../utils' +import { findNode, formatPkg, showVitestError } from '../utils' import { createErrorLogger, log } from '../log' import { getConfig } from '../config' import { workerPath } from '../constants' @@ -32,14 +32,15 @@ export async function createVitestProcess(pkg: VitestPackage) { ] : runtimeArgs const arvString = execArgv.join(' ') - const script = `node ${arvString ? `${arvString} ` : ''}${workerPath}`.trim() + const executable = await findNode(pkg.cwd) + const script = `${executable} ${arvString ? `${arvString} ` : ''}${workerPath}`.trim() log.info('[API]', `Running ${formatPkg(pkg)} with "${script}"`) const logLevel = getConfig(pkg.folder).logLevel const port = await getPort() const server = createServer().listen(port).unref() const wss = new WebSocketServer({ server }) const wsAddress = `ws://localhost:${port}` - const vitest = spawn(getConfig(pkg.folder).nodeExecutable || 'node', [...execArgv, workerPath], { + const vitest = spawn(executable, [...execArgv, workerPath], { env: { ...process.env, ...env, diff --git a/src/debug.ts b/src/debug.ts index 67041c5..dcba5e1 100644 --- a/src/debug.ts +++ b/src/debug.ts @@ -14,6 +14,7 @@ import { workerPath } from './constants' import type { WsConnectionMetadata } from './api/ws' import { waitForWsConnection } from './api/ws' import type { ExtensionWorkerProcess } from './api/types' +import { findNode } from './utils' export async function debugTests( controller: vscode.TestController, @@ -172,8 +173,9 @@ async function getRuntimeOptions(pkg: VitestPackage) { ] : runtimeArgs if (config.shellType === 'child_process') { + const executable = await findNode(pkg.cwd) return { - runtimeExecutable: config.nodeExecutable || 'node', + runtimeExecutable: executable, runtimeArgs: execArgv, } } @@ -197,17 +199,18 @@ class ExtensionDebugProcess implements ExtensionWorkerProcess { this._stopped = new Promise((resolve) => { const { dispose } = vscode.debug.onDidTerminateDebugSession((terminatedSession) => { if (session === terminatedSession) { - dispose() - resolve() this._onDidExit.fire() this._onDidExit.dispose() this.closed = true + resolve() + dispose() } }) }) // if websocket connection stopped working, close the debug session - // otherwise it might hand indefinitely + // otherwise it might hang indefinitely ws.on('close', () => { + this.closed = true this.close() }) } diff --git a/src/utils.ts b/src/utils.ts index 99b65d4..2548c59 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,8 +1,11 @@ import fs from 'node:fs' +import { spawn } from 'node:child_process' import * as vscode from 'vscode' import { dirname, relative } from 'pathe' +import which from 'which' import type { VitestPackage } from './api/pkg' import { log } from './log' +import { getConfig } from './config' export function noop() {} @@ -91,3 +94,66 @@ export function waitUntilExists(file: string, timeoutMs = 5000) { }, 50) }) } + +let pathToNodeJS: string | undefined + +// based on https://github.com/microsoft/playwright-vscode/blob/main/src/utils.ts#L144 +export async function findNode(cwd: string): Promise { + if (getConfig().nodeExecutable) + // if empty string, keep as undefined + pathToNodeJS = getConfig().nodeExecutable || undefined + + if (pathToNodeJS) + return pathToNodeJS + + // Stage 1: Try to find Node.js via process.env.PATH + let node: string | null = await which('node', { nothrow: true }) + // Stage 2: When extension host boots, it does not have the right env set, so we might need to wait. + for (let i = 0; i < 5 && !node; ++i) { + await new Promise(f => setTimeout(f, 200)) + node = await which('node', { nothrow: true }) + } + // Stage 3: If we still haven't found Node.js, try to find it via a subprocess. + // This evaluates shell rc/profile files and makes nvm work. + node ??= await findNodeViaShell(cwd) + + if (!node) { + const msg = `Unable to find 'node' executable.\nMake sure to have Node.js installed and available in your PATH.\nCurrent PATH: '${process.env.PATH}'.` + log.error(msg) + throw new Error(msg) + } + pathToNodeJS = node + return node +} + +async function findNodeViaShell(cwd: string): Promise { + if (process.platform === 'win32') + return null + return new Promise((resolve) => { + const startToken = '___START_SHELL__' + const endToken = '___END_SHELL__' + try { + const childProcess = spawn(`${vscode.env.shell} -i -c 'if [[ $(type node 2>/dev/null) == *function* ]]; then node --version; fi; echo ${startToken} && which node && echo ${endToken}'`, { + stdio: 'pipe', + shell: true, + cwd, + }) + let output = '' + childProcess.stdout.on('data', data => output += data.toString()) + childProcess.on('error', () => resolve(null)) + childProcess.on('exit', (exitCode) => { + if (exitCode !== 0) + return resolve(null) + const start = output.indexOf(startToken) + const end = output.indexOf(endToken) + if (start === -1 || end === -1) + return resolve(null) + return resolve(output.substring(start + startToken.length, end).trim()) + }) + } + catch (e) { + log.error('[SPAWN]', vscode.env.shell, e) + resolve(null) + } + }) +}