From 55bd09adbdd99d8ce0e601aa9d85d53954af6b56 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Fri, 23 Oct 2026 03:18:37 +0900 Subject: [PATCH] fix(lsp): check for bunx before the typescript bunx fallback spawn reports a missing command as an async 'error' event, so the old try/catch never ran and LSPManager waited out the initialize timeout. Resolve bunx with Bun.which first. When it is missing, log the install hint and return undefined. Otherwise spawn the resolved path and drop the dead try/catch. Closes #135 --- packages/lsp/src/server/typescript.ts | 20 ++++++++++---------- packages/lsp/test/unit/server.test.ts | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/packages/lsp/src/server/typescript.ts b/packages/lsp/src/server/typescript.ts index 4cc2080..7d8b25e 100644 --- a/packages/lsp/src/server/typescript.ts +++ b/packages/lsp/src/server/typescript.ts @@ -316,18 +316,18 @@ export const TypescriptServer: LSPServerInfo = { if (!tsserver) { // Try via bunx. The version tag makes bunx install from the registry instead of // reusing a typescript-language-server on PATH, which may be the shim just rejected. - try { - const proc = spawn('bunx', [`typescript-language-server@${TS_LANGUAGE_SERVER_VERSION}`, '--stdio'], { - cwd: root, - env: { ...process.env, BUN_BE_BUN: '1' }, - }) - attachLSPProcessHandlers(proc, 'typescript') - return { process: proc } - } - catch (err) { - log.error({ err }, 'Failed to spawn typescript-language-server via bunx') + // spawn reports a missing bunx only as an async 'error' event, so check for it first. + const bunx = Bun.which('bunx', { PATH: process.env.PATH ?? '' }) + if (!bunx) { + log.warn({ root }, 'typescript-language-server not found. Install with: npm install typescript-language-server typescript') return undefined } + const proc = spawn(bunx, [`typescript-language-server@${TS_LANGUAGE_SERVER_VERSION}`, '--stdio'], { + cwd: root, + env: { ...process.env, BUN_BE_BUN: '1' }, + }) + attachLSPProcessHandlers(proc, 'typescript') + return { process: proc } } // On Windows the bin is a .cmd shim, which only runs through a shell. diff --git a/packages/lsp/test/unit/server.test.ts b/packages/lsp/test/unit/server.test.ts index 41eca9c..e4c95fc 100644 --- a/packages/lsp/test/unit/server.test.ts +++ b/packages/lsp/test/unit/server.test.ts @@ -533,6 +533,27 @@ describe.skipIf(process.platform === 'win32')('TypescriptServer classic fallback const invoked = await spawnTypescriptWith({ bunx: recorder('bunx') }, rootDir => installLocalServer(rootDir, '#!/bin/sh\nexit 1\n')) expect(invoked).toBe('bunx typescript-language-server@4.3.3 --stdio') }) + + test('returns undefined with the install hint when bunx is missing', async () => { + const emptyDir = await fs.mkdtemp(path.join(os.tmpdir(), 'lsp-fallback-empty-')) + const savedPath = process.env.PATH + const warn = spyOn(log, 'warn') + try { + process.env.PATH = emptyDir + const handle = await TypescriptServer.spawn(emptyDir) + process.env.PATH = savedPath + expect(handle).toBeUndefined() + expect(warn).toHaveBeenCalledWith( + expect.anything(), + 'typescript-language-server not found. Install with: npm install typescript-language-server typescript', + ) + } + finally { + warn.mockRestore() + process.env.PATH = savedPath + await fs.rm(emptyDir, { recursive: true, force: true }) + } + }) }) describe.skipIf(process.platform === 'win32')('BiomeServer fallback', () => {