Skip to content

fix(lsp): typescript bunx fallback never detects a missing bunx — the try/catch around spawn is dead #135

Description

@amondnet

Problem

When no runnable typescript-language-server is found (neither the project-local one nor one on PATH), TypescriptServer.spawn falls back to bunx inside a try/catch (packages/lsp/src/server/typescript.ts:319-330):

try {
  const proc = spawn('bunx', [`typescript-language-server@${TS_LANGUAGE_SERVER_VERSION}`, '--stdio'], { … })
  attachLSPProcessHandlers(proc, 'typescript')
  return { process: proc }
}
catch (err) {
  log.error({ err }, 'Failed to spawn typescript-language-server via bunx')
  return undefined
}

spawn from node:child_process does not throw when the command is missing. It returns a child process and later emits an async error event (ENOENT). This was confirmed under both Bun and Node while working on #134. So when bunx is not on PATH, the catch never runs:

  • spawn returns a handle to a process that never started, rather than undefined.
  • LSPManager.schedule (packages/lsp/src/index.ts) marks a server broken only when spawn returns undefined or throws. It treats this dead handle as live and waits out the initialize timeout before giving up.
  • The only log line is the generic LSP process error from attachLSPProcessHandlers. The Failed to spawn … via bunx message never appears.

Expected

Check for bunx before spawning, as BiomeServer does since #134 (packages/lsp/src/server/biome.ts:77-89):

const bunx = Bun.which('bunx', { PATH: process.env.PATH ?? '' })
if (!bunx) {
  log.warn({ root }, '<install hint>')
  return undefined
}
const proc = spawn(bunx, [`typescript-language-server@${TS_LANGUAGE_SERVER_VERSION}`, '--stdio'], { … })
  • Return undefined when bunx is missing, so the server is marked broken right away instead of waiting out the initialize timeout.
  • Pass the explicit PATH to Bun.which. Bun otherwise reads the copy of PATH it captured at startup, and the tests change PATH at runtime.
  • Spawn the resolved absolute path, not the bare bunx name. That is what cleared SonarCloud S4036 on fix(lsp): run a pinned @biomejs/biome in the biome bunx fallback #134.
  • Drop the dead try/catch.

Tests

Add a case to the existing TypescriptServer classic-fallback tests in packages/lsp/test/unit/server.test.ts, modeled on the "returns undefined with the install hint when bunx is missing" test from #134. Set PATH to an empty temp dir, then assert that spawn returns undefined and that log.warn was called with the hint. The test should fail when the pre-check is removed.

Notes

  • PyrightServer (packages/lsp/src/server/pyright.ts:21-32) has the same dead try/catch around spawn('bunx', ['pyright-langserver', '--stdio']). It could get the same fix in this change or in a separate issue. Its bunx call is also unpinned, and it calls Bun.which('pyright-langserver') without the explicit PATH.

Follow-up to #133 / #134.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    lsptype:bugSomething isn't working

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions