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', () => {