diff --git a/package-lock.json b/package-lock.json index 3a5c44a..841fef0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "pretty-format": "^29.7.0", "split2": "^4.2.0", "stacktrace-parser": "^0.1.10", + "which": "^4.0.0", "ws": "^8.16.0" }, "devDependencies": { @@ -37,6 +38,7 @@ "@types/sinon": "^17.0.3", "@types/split2": "^4.2.3", "@types/vscode": "^1.88.0", + "@types/which": "^3.0.3", "@types/ws": "^8.5.10", "@vscode/test-electron": "^2.3.9", "acorn": "^8.11.3", @@ -1059,6 +1061,12 @@ "integrity": "sha512-rWY+Bs6j/f1lvr8jqZTyp5arRMfovdxolcqGi+//+cPDOh8SBvzXH90e7BiSXct5HJ9HGW6jATchbRTpTJpEkw==", "dev": true }, + "node_modules/@types/which": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/which/-/which-3.0.3.tgz", + "integrity": "sha512-2C1+XoY0huExTbs8MQv1DuS5FS86+SEjdM9F/+GS61gg5Hqbtj8ZiDSx8MfWcyei907fIPbfPGCOrNUTnVHY1g==", + "dev": true + }, "node_modules/@types/ws": { "version": "8.5.10", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", @@ -1557,6 +1565,25 @@ "node": ">= 8" } }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", @@ -2055,9 +2082,12 @@ "dev": true }, "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "engines": { + "node": ">=16" + } }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", @@ -4291,17 +4321,17 @@ } }, "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", "dependencies": { - "isexe": "^2.0.0" + "isexe": "^3.1.1" }, "bin": { - "node-which": "bin/node-which" + "node-which": "bin/which.js" }, "engines": { - "node": ">= 8" + "node": "^16.13.0 || >=18.0.0" } }, "node_modules/why-is-node-running": { @@ -5126,6 +5156,12 @@ "integrity": "sha512-rWY+Bs6j/f1lvr8jqZTyp5arRMfovdxolcqGi+//+cPDOh8SBvzXH90e7BiSXct5HJ9HGW6jATchbRTpTJpEkw==", "dev": true }, + "@types/which": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/which/-/which-3.0.3.tgz", + "integrity": "sha512-2C1+XoY0huExTbs8MQv1DuS5FS86+SEjdM9F/+GS61gg5Hqbtj8ZiDSx8MfWcyei907fIPbfPGCOrNUTnVHY1g==", + "dev": true + }, "@types/ws": { "version": "8.5.10", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", @@ -5496,6 +5532,21 @@ "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" + }, + "dependencies": { + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + } } }, "data-uri-to-buffer": { @@ -5842,9 +5893,9 @@ "dev": true }, "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==" }, "istanbul-lib-coverage": { "version": "3.2.2", @@ -7222,11 +7273,11 @@ } }, "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", "requires": { - "isexe": "^2.0.0" + "isexe": "^3.1.1" } }, "why-is-node-running": { diff --git a/package.json b/package.json index b8e8caa..0d5c680 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,8 @@ "properties": { "nodejs-testing.nodejsPath": { "type": "string", - "default": "node", - "description": "Path to the Node.js binary used to run tests" + "default": null, + "description": "Path to the Node.js binary used to run tests (default is the one on the PATH in the workspace directory)." }, "nodejs-testing.nodejsParameters": { "type": "array", @@ -195,6 +195,7 @@ "@types/sinon": "^17.0.3", "@types/split2": "^4.2.3", "@types/vscode": "^1.88.0", + "@types/which": "^3.0.3", "@types/ws": "^8.5.10", "@vscode/test-electron": "^2.3.9", "acorn": "^8.11.3", @@ -228,6 +229,7 @@ "pretty-format": "^29.7.0", "split2": "^4.2.0", "stacktrace-parser": "^0.1.10", + "which": "^4.0.0", "ws": "^8.16.0" } } diff --git a/src/extension.ts b/src/extension.ts index a2a7f28..9caf71f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,6 +5,7 @@ import { Pretest } from "./pretest"; import { TestRunner } from "./runner"; import { SourceMapStore } from "./source-map-store"; import { Style } from "./styles"; +import { findNode } from "./find-node"; export async function activate(context: vscode.ExtensionContext) { const smStore = new SourceMapStore(); @@ -18,15 +19,15 @@ export async function activate(context: vscode.ExtensionContext) { ]); const ctrls = new Map(); - const refreshFolders = () => { + const refreshFolders = async () => { for (const ctrl of ctrls.values()) { ctrl.dispose(); } ctrls.clear(); - syncWorkspaceFolders(); + await syncWorkspaceFolders(); }; - const syncWorkspaceFolders = () => { + const syncWorkspaceFolders = async () => { if (!extensions.value?.length) { const msg = "nodejs-testing.extensions array is empty. Please remove the setting 'nodejs-testing.extensions' or define at least one element."; @@ -36,10 +37,16 @@ export async function activate(context: vscode.ExtensionContext) { const folders = vscode.workspace.workspaceFolders ?? []; for (const folder of folders) { if (!ctrls.has(folder)) { + + const nodeJsPath = await findNode(folder.uri.fsPath).catch((e) => { + vscode.window.showErrorMessage("nodejs-testing failed to find node path: " + e.message); + return 'node'; + }); + const runner = new TestRunner( smStore, new ConfigValue("concurrency", 0, folder), - new ConfigValue("nodejsPath", "node", folder), + new ConfigValue("nodejsPath", nodeJsPath || "node", folder), new ConfigValue("verbose", false, folder), new ConfigValue("style", Style.Spec, folder), context.extensionUri.fsPath, @@ -110,7 +117,7 @@ export async function activate(context: vscode.ExtensionContext) { new vscode.Disposable(() => ctrls.forEach((c) => c.dispose())), ); - syncWorkspaceFolders(); + await syncWorkspaceFolders(); for (const editor of vscode.window.visibleTextEditors) { syncTextDocument(editor.document); } diff --git a/src/find-node.ts b/src/find-node.ts new file mode 100644 index 0000000..c24a527 --- /dev/null +++ b/src/find-node.ts @@ -0,0 +1,54 @@ +import { spawn } from "node:child_process"; +import * as vscode from "vscode"; +import which from "which"; + +const cwdToNodeJs = new Map(); + +// based on https://github.com/microsoft/playwright-vscode/blob/main/src/utils.ts#L144 +export async function findNode(cwd: string): Promise { + if (cwdToNodeJs.has(cwd)) + return cwdToNodeJs.get(cwd)!; + + // 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}'.`); + + cwdToNodeJs.set(cwd, 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()); + }); + }); +}