diff --git a/package.json b/package.json index 4050bcf..65e21d4 100644 --- a/package.json +++ b/package.json @@ -177,6 +177,7 @@ "@types/prompts": "^2.4.9", "@types/semver": "^7.3.9", "@types/vscode": "^1.77.0", + "@types/which": "^3.0.3", "@types/ws": "^8.5.10", "@vitest/utils": "^1.3.0", "@vitest/ws-client": "^1.3.0", @@ -209,6 +210,7 @@ "tsx": "^4.7.1", "typescript": "^5.3.3", "vitest": "^1.4.0", + "which": "^4.0.0", "ws": "^8.16.0" }, "lint-staged": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c685eb0..0f90a3a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ devDependencies: '@types/vscode': specifier: ^1.77.0 version: 1.88.0 + '@types/which': + specifier: ^3.0.3 + version: 3.0.3 '@types/ws': specifier: ^8.5.10 version: 8.5.10 @@ -140,6 +143,9 @@ devDependencies: vitest: specifier: ^1.4.0 version: 1.4.0(@types/node@18.19.17) + which: + specifier: ^4.0.0 + version: 4.0.0 ws: specifier: ^8.16.0 version: 8.16.0 @@ -955,6 +961,10 @@ packages: resolution: {integrity: sha512-rWY+Bs6j/f1lvr8jqZTyp5arRMfovdxolcqGi+//+cPDOh8SBvzXH90e7BiSXct5HJ9HGW6jATchbRTpTJpEkw==} dev: true + /@types/which@3.0.3: + resolution: {integrity: sha512-2C1+XoY0huExTbs8MQv1DuS5FS86+SEjdM9F/+GS61gg5Hqbtj8ZiDSx8MfWcyei907fIPbfPGCOrNUTnVHY1g==} + dev: true + /@types/ws@8.5.10: resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} dependencies: @@ -2935,6 +2945,11 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true + /isexe@3.1.1: + resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} + engines: {node: '>=16'} + dev: true + /istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -4547,6 +4562,14 @@ packages: isexe: 2.0.0 dev: true + /which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + dependencies: + isexe: 3.1.1 + dev: true + /why-is-node-running@2.2.2: resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} engines: {node: '>=8'} diff --git a/src/api.ts b/src/api.ts index f9f0f07..8f31662 100644 --- a/src/api.ts +++ b/src/api.ts @@ -11,7 +11,7 @@ import type { BirpcEvents, VitestEvents, VitestRPC } from './api/rpc' import { createVitestRpc } from './api/rpc' import type { WorkerRunnerOptions } from './worker/types' import type { VitestPackage } from './api/pkg' -import { pluralize, showVitestError } from './utils' +import { findNode, pluralize, showVitestError } from './utils' export class VitestReporter { constructor( @@ -222,7 +222,7 @@ interface ResolvedMeta { } } -function createChildVitestProcess(showWarning: boolean, meta: VitestPackage[]) { +async function createChildVitestProcess(showWarning: boolean, meta: VitestPackage[]) { const pnpLoaders = [ ...new Set(meta.map(meta => meta.loader).filter(Boolean) as string[]), ] @@ -240,11 +240,12 @@ function createChildVitestProcess(showWarning: boolean, meta: VitestPackage[]) { ] : undefined const env = getConfig().env || {} + const execPath = getConfig().nodeExecutable || await findNode(vscode.workspace.workspaceFile?.fsPath || vscode.workspace.workspaceFolders![0].uri.fsPath) + log.info('[API]', `Starting Vitest process with Node.js: ${execPath}`) const vitest = fork( workerPath, { - // TODO: use findNode API - execPath: getConfig().nodeExecutable, + execPath, execArgv, env: { ...process.env, diff --git a/src/utils.ts b/src/utils.ts index 03257f1..55f3d3a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,7 @@ import fs from 'node:fs' +import { spawn } from 'node:child_process' import * as vscode from 'vscode' +import which from 'which' import { relative } from 'pathe' import type { VitestPackage } from './api/pkg' import { log } from './log' @@ -84,3 +86,52 @@ 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 (pathToNodeJS) + return pathToNodeJS + + // Stage 1: Try to find Node.js via process.env.PATH + let node = await which('node').catch(() => undefined) + // 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').catch(() => undefined) + } + // 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) + throw new Error(`Unable to find 'node' executable.\nMake sure to have Node.js installed and available in your PATH.\nCurrent PATH: '${process.env.PATH}'.`) + pathToNodeJS = node + return node +} + +async function findNodeViaShell(cwd: string): Promise { + if (process.platform === 'win32') + return undefined + return new Promise((resolve) => { + const startToken = '___START_SHELL__' + const endToken = '___END_SHELL__' + const childProcess = spawn(`${vscode.env.shell} -i -c '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(undefined)) + childProcess.on('exit', (exitCode) => { + if (exitCode !== 0) + return resolve(undefined) + const start = output.indexOf(startToken) + const end = output.indexOf(endToken) + if (start === -1 || end === -1) + return resolve(undefined) + return resolve(output.substring(start + startToken.length, end).trim()) + }) + }) +}