Skip to content

TypeScript LSP doesn't spawn when project is in subdirectory (Module.resolve uses ctx.directory instead of resolved root) #35396

Closed as not planned

Description

@iguinea

Bug summary

The TypeScript LSP server (packages/opencode/src/lsp/server.ts) fails to spawn when the TypeScript project lives in a subdirectory of the worktree (e.g. frontend-spa/ instead of the worktree root). The root is correctly resolved to the subdirectory via NearestRoot, but Module.resolve("typescript/lib/tsserver.js", ctx.directory) uses ctx.directory (the worktree root) instead of the already-resolved root, so it never finds tsserver.js.

Environment

  • opencode 1.17.10 (Homebrew, macOS arm64)
  • Config: "lsp": true (all built-in LSPs enabled)
  • Project layout: worktree root has no package.json; frontend-spa/ has package.json with typescript ^5.0.0 and node_modules/typescript/lib/tsserver.js installed.

Steps to reproduce

  1. Create a project where the TypeScript app lives in a subdirectory:
    my-worktree/
    ├── app/                  # Python backend (Pyright works)
    └── frontend-spa/
        ├── package.json      # has "typescript": "^5.0.0" in devDependencies
        ├── package-lock.json
        ├── tsconfig.json
        ├── node_modules/
        │   └── typescript/lib/tsserver.js   # EXISTS
        └── src/index.ts
    
  2. Enable LSP: "lsp": true in opencode.json.
  3. Run opencode debug lsp diagnostics <any .ts file in frontend-spa/src/>.

Expected behavior

The TypeScript LSP spawns and returns diagnostics (type errors, etc.).

Actual behavior

opencode debug lsp diagnostics returns {} (empty object, no diagnostics). No tsserver or typescript-language-server process is spawned. ps aux | grep tsserver shows nothing.

Meanwhile, Pyright (Python) works correctly in the same worktree because it uses which("pyright-langserver") (PATH lookup) instead of Module.resolve(..., ctx.directory).

Root cause

In packages/opencode/src/lsp/server.ts, the Typescript server definition:

export const Typescript: Info = {
  id: "typescript",
  root: NearestRoot(
    ["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"],
    ["deno.json", "deno.jsonc"],
  ),
  extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
  async spawn(root, ctx) {
    const tsserver = Module.resolve("typescript/lib/tsserver.js", ctx.directory)  // ← BUG: uses ctx.directory
    if (!tsserver) return
    const bin = await Npm.which("typescript-language-server")
    if (!bin) return
    // ...
  },
}

The root parameter is correctly resolved by NearestRoot to frontend-spa/ (because that is where package-lock.json lives). However, Module.resolve is called with ctx.directory (the worktree root) instead of root. Since there is no node_modules/typescript/lib/tsserver.js at the worktree root, Module.resolve returns undefined and the LSP silently bails out.

Suggested fix

Use root (the already-resolved project root) instead of ctx.directory in the Module.resolve call:

async spawn(root, ctx) {
  const tsserver = Module.resolve("typescript/lib/tsserver.js", root)  // ← use root, not ctx.directory
  if (!tsserver) return
  // ...
}

The same pattern appears in the Astro server definition (which also calls Module.resolve("typescript/lib/tsserver.js", ctx.directory)), so it likely has the same bug.

Workaround

Create a symlink at the worktree root pointing to the subdirectory's node_modules:

ln -s frontend-spa/node_modules ./node_modules

After this, Module.resolve("typescript/lib/tsserver.js", ctx.directory) finds tsserver.js via the symlink and the TypeScript LSP spawns correctly. Verified with opencode debug lsp diagnostics:

=== TYPESCRIPT ===
{
  "/.../frontend-spa/src/__lsp_probe.ts": [
    {
      "message": "Type 'string' is not assignable to type 'number'.",
      "severity": 1,
      "code": 2322,
      "source": "typescript"
    },
    ...
  ]
}

Logs

opencode debug config shows "lsp": true and typescript is listed in enabled LSP servers. But no tsserver process is ever spawned:

$ ps aux | grep tsserver
(no output)

$ ps aux | grep pyright-langserver
iguinea  10947  ... node /opt/homebrew/bin/pyright-langserver --stdio

Impact

Any monorepo or subdirectory-layout project where the TypeScript app is not at the worktree root will have a silently broken TypeScript LSP. This is a common layout (backend + frontend split, Python + React, etc.).

Activity

  1. github-actions commented on Jul 5, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of an existing issue. Please check:

  2. github-actions commented on Sep 4, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions