diff --git a/editors/vscode/client/linter.ts b/editors/vscode/client/linter.ts index 151bf0a226212..43022e39903c9 100644 --- a/editors/vscode/client/linter.ts +++ b/editors/vscode/client/linter.ts @@ -26,6 +26,7 @@ import { join } from 'node:path'; import { ConfigService } from './ConfigService'; import { VSCodeConfig } from './VSCodeConfig'; import { OxcCommands } from './commands'; +import { runExecutable } from './lsp_helper'; const languageClientName = 'oxc'; @@ -90,40 +91,9 @@ export async function activate( return process.env.SERVER_PATH_DEV ?? join(context.extensionPath, `./target/release/oxc_language_server${ext}`); } - const nodePath = configService.vsCodeConfig.nodePath; - const serverEnv: Record = { - ...process.env, - RUST_LOG: process.env.RUST_LOG || 'info', - }; - if (nodePath) { - serverEnv.PATH = `${nodePath}${process.platform === 'win32' ? ';' : ':'}${process.env.PATH ?? ''}`; - } - const path = await findBinary(); - const isNode = path.endsWith('.js') || path.endsWith('.cjs') || path.endsWith('.mjs'); - - const run: Executable = isNode - ? { - command: 'node', - args: [path!, '--lsp'], - options: { - env: serverEnv, - }, - } - : { - command: path!, - args: ['--lsp'], - options: { - // On Windows we need to run the binary in a shell to be able to execute the shell npm bin script. - // Searching for the right `.exe` file inside `node_modules/` is not reliable as it depends on - // the package manager used (npm, yarn, pnpm, etc) and the package version. - // The npm bin script is a shell script that points to the actual binary. - // Security: We validated the userDefinedBinary in `configService.getUserServerBinPath()`. - shell: process.platform === 'win32', - env: serverEnv, - }, - }; + const run: Executable = runExecutable(path, configService.vsCodeConfig.nodePath); const serverOptions: ServerOptions = { run, debug: run, diff --git a/editors/vscode/client/lsp_helper.ts b/editors/vscode/client/lsp_helper.ts new file mode 100644 index 0000000000000..9c61ebc06de18 --- /dev/null +++ b/editors/vscode/client/lsp_helper.ts @@ -0,0 +1,34 @@ +import { Executable } from 'vscode-languageclient/node'; + +export function runExecutable(path: string, nodePath?: string): Executable { + const serverEnv: Record = { + ...process.env, + RUST_LOG: process.env.RUST_LOG || 'info', + }; + if (nodePath) { + serverEnv.PATH = `${nodePath}${process.platform === 'win32' ? ';' : ':'}${process.env.PATH ?? ''}`; + } + const isNode = path.endsWith('.js') || path.endsWith('.cjs') || path.endsWith('.mjs'); + + return isNode + ? { + command: 'node', + args: [path, '--lsp'], + options: { + env: serverEnv, + }, + } + : { + command: path, + args: ['--lsp'], + options: { + // On Windows we need to run the binary in a shell to be able to execute the shell npm bin script. + // Searching for the right `.exe` file inside `node_modules/` is not reliable as it depends on + // the package manager used (npm, yarn, pnpm, etc) and the package version. + // The npm bin script is a shell script that points to the actual binary. + // Security: We validated the userDefinedBinary in `configService.getUserServerBinPath()`. + shell: process.platform === 'win32', + env: serverEnv, + }, + }; +} diff --git a/editors/vscode/tests/lsp_helper.spec.ts b/editors/vscode/tests/lsp_helper.spec.ts new file mode 100644 index 0000000000000..d01203491e2b2 --- /dev/null +++ b/editors/vscode/tests/lsp_helper.spec.ts @@ -0,0 +1,61 @@ +import { strictEqual } from 'assert'; +import { runExecutable } from '../client/lsp_helper'; + +suite('runExecutable', () => { + const originalPlatform = process.platform; + const originalEnv = process.env; + + teardown(() => { + Object.defineProperty(process, 'platform', { value: originalPlatform }); + process.env = originalEnv; + }); + + test('should create Node.js executable for .js files', () => { + const result = runExecutable('/path/to/server.js'); + + strictEqual(result.command, 'node'); + strictEqual(result.args?.[0], '/path/to/server.js'); + strictEqual(result.args?.[1], '--lsp'); + }); + + test('should create Node.js executable for .cjs files', () => { + const result = runExecutable('/path/to/server.cjs'); + + strictEqual(result.command, 'node'); + strictEqual(result.args?.[0], '/path/to/server.cjs'); + strictEqual(result.args?.[1], '--lsp'); + }); + + test('should create Node.js executable for .mjs files', () => { + const result = runExecutable('/path/to/server.mjs'); + + strictEqual(result.command, 'node'); + strictEqual(result.args?.[0], '/path/to/server.mjs'); + strictEqual(result.args?.[1], '--lsp'); + }); + + test('should create binary executable for non-Node files', () => { + const result = runExecutable('/path/to/oxc-language-server'); + + strictEqual(result.command, '/path/to/oxc-language-server'); + strictEqual(result.args?.[0], '--lsp'); + strictEqual(result.options?.shell, false); + }); + + test('should use shell on Windows for binary executables', () => { + Object.defineProperty(process, 'platform', { value: 'win32' }); + + const result = runExecutable('/path/to/oxc-language-server'); + + strictEqual(result.options?.shell, true); + }); + + test('should prepend nodePath to PATH', () => { + Object.defineProperty(process, 'platform', { value: 'linux' }); + process.env.PATH = '/usr/bin:/bin'; + + const result = runExecutable('/path/to/server', '/custom/node/path'); + + strictEqual(result.options?.env?.PATH, '/custom/node/path:/usr/bin:/bin'); + }); +});