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
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,82 @@
import * as os from 'os';
import * as path from 'path';
import * as vscode from 'vscode';
import * as child_process from 'child_process';
import * as fs from 'fs';
import { BaseServiceConfigurationProvider } from './configuration';
import { RelativeWorkspacePathResolver } from '../utils/relativePathResolver';

type IsExecutableFile = (candidate: string) => boolean;

function defaultIsExecutableFile(candidate: string): boolean {
try {
const stat = fs.lstatSync(candidate);
if (!stat.isFile()) {
return false;
}
if (process.platform !== 'win32') {
fs.accessSync(candidate, fs.constants.X_OK);
}
return true;
} catch {
return false;
}
}

function getCaseInsensitiveEnvValue(env: NodeJS.ProcessEnv, key: string): string | undefined {
const foundKey = Object.keys(env).find(k => k.toUpperCase() === key.toUpperCase());
return foundKey ? env[foundKey] : undefined;
}

export function resolveNodeExecutableFromPath(
env: NodeJS.ProcessEnv,
cwd: string,
isExecutableFile: IsExecutableFile = defaultIsExecutableFile,
platform: NodeJS.Platform = process.platform,
): string | null {
const pathLib = platform === 'win32' ? path.win32 : path.posix;
const windowsExecutableSuffixes = platform === 'win32'
? (getCaseInsensitiveEnvValue(env, 'PATHEXT') || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)
: [];

const pathValue = getCaseInsensitiveEnvValue(env, 'PATH');
const searchPaths = pathValue ? pathValue.split(pathLib.delimiter).filter(Boolean) : [];

for (const pathEntry of searchPaths) {
const baseDir = pathLib.isAbsolute(pathEntry) ? pathEntry : pathLib.join(cwd, pathEntry);

if (platform === 'win32') {
for (const ext of windowsExecutableSuffixes) {
const candidate = pathLib.join(baseDir, `node${ext}`);
if (isExecutableFile(candidate)) {
return candidate;
}
}
}

const candidate = pathLib.join(baseDir, 'node');
if (isExecutableFile(candidate)) {
return candidate;
}
}

// Fallback: check cwd directly when node is not found via PATH
if (platform === 'win32') {
for (const ext of windowsExecutableSuffixes) {
const candidate = pathLib.join(cwd, `node${ext}`);
if (isExecutableFile(candidate)) {
return candidate;
}
}
}

const cwdCandidate = pathLib.join(cwd, 'node');
if (isExecutableFile(cwdCandidate)) {
return cwdCandidate;
}

return null;
}

export class ElectronServiceConfigurationProvider extends BaseServiceConfigurationProvider {

private fixPathPrefixes(inspectValue: string): string {
Expand Down Expand Up @@ -92,18 +163,12 @@ export class ElectronServiceConfigurationProvider extends BaseServiceConfigurati
}

private findNodePath(): string | null {
try {
const out = child_process.execFileSync('node', ['-e', 'console.log(process.execPath)'], {
windowsHide: true,
timeout: 2000,
cwd: vscode.workspace.workspaceFolders?.[0].uri.fsPath,
encoding: 'utf-8',
});
return out.trim();
} catch (error) {
const cwd = vscode.workspace.workspaceFolders?.[0].uri.fsPath ?? process.cwd();
const resolvedNodePath = resolveNodeExecutableFromPath(process.env, cwd);
if (!resolvedNodePath) {
vscode.window.showWarningMessage(vscode.l10n.t("Could not detect a Node installation to run TS Server."));
return null;
}
return resolvedNodePath;
}

private validatePath(nodePath: string | null): string | null {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as assert from 'assert';
import 'mocha';
import { resolveNodeExecutableFromPath } from '../../configuration/configuration.electron';

suite('typescript.configuration.electron', () => {
test('resolves node from PATH on win32', () => {
const found = resolveNodeExecutableFromPath(
{ PATH: 'C:\\Windows;C:\\Tools', PATHEXT: '.COM;.EXE;.BAT;.CMD' },
'C:\\workspace',
candidate => candidate.toLowerCase() === 'c:\\tools\\node.exe',
'win32',
);

assert.strictEqual(found, 'C:\\Tools\\node.EXE');
});

test('resolves node from PATH on non-win32', () => {
const found = resolveNodeExecutableFromPath(
{ PATH: '/bin:/usr/local/bin' },
'/workspace',
candidate => candidate === '/usr/local/bin/node',
'linux',
);

assert.strictEqual(found, '/usr/local/bin/node');
});

test('returns null when node is not found', () => {
const found = resolveNodeExecutableFromPath(
{ PATH: '/bin:/usr/local/bin' },
'/workspace',
() => false,
'linux',
);

assert.strictEqual(found, null);
});

test('falls back to cwd when PATH is missing on non-win32', () => {
const found = resolveNodeExecutableFromPath(
{},
'/workspace',
candidate => candidate === '/workspace/node',
'linux',
);

assert.strictEqual(found, '/workspace/node');
});

test('falls back to cwd with PATHEXT when PATH is missing on win32', () => {
const found = resolveNodeExecutableFromPath(
{ PATHEXT: '.COM;.EXE;.BAT;.CMD' },
'C:\\workspace',
candidate => candidate.toLowerCase() === 'c:\\workspace\\node.exe',
'win32',
);

assert.strictEqual(found, 'C:\\workspace\\node.EXE');
});

test('returns null when node is not found in PATH or cwd', () => {
const found = resolveNodeExecutableFromPath(
{},
'/workspace',
() => false,
'linux',
);

assert.strictEqual(found, null);
});
});