Skip to content

feat(lsp): prefer tsrs for the TypeScript LSP when installed - #124

Merged
amondnet merged 8 commits into
mainfrom
amondnet/tsrs
Oct 21, 2026
Merged

amondnet merged 8 commits into
mainfrom
amondnet/tsrs

Conversation

@amondnet

@amondnet amondnet commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The typescript LSP server now prefers a project-local tsrs, the Rust port of TypeScript 7 (announcement). Per tsrs's docs/LSP.md, tsrs --lsp --stdio speaks the same protocol as tsgo --lsp --stdio, including pull diagnostics, which the client already supports.

Resolution order: project-local tsrs, then native tsc (TS ≥ 7), then tsgo (@typescript/native-preview), then a tsrs on PATH, then classic typescript-language-server. There is still a single server id and no config toggle, so diagnostics are not duplicated.

What changed

  • tsrs is chosen only when its native binary is available: TSRS_BINARY pointing at an existing file, or an installed @tsrs/* optional dependency whose os/cpu match the host and which contains the tsrs executable. This mirrors the package's lib/getExePath.js.
  • Without that guard, unsupported platforms (for example darwin-x64) would launch a wrapper that exits 1, the client would wait out its 45s initialize timeout, and TypeScript would end up with no LSP.
  • Docs updated: configuration overview (new tsrs subsection), supported-languages, packages/lsp/CLAUDE.md, root CLAUDE.md.

Scope

A tsrs on PATH is used as a fallback when the project has no local tsrs, native tsc, or tsgo, so a compiler the project pins still wins. When the PATH entry is a global npm launcher (a symlink into node_modules/tsrs, or a Windows tsrs.cmd next to node_modules/tsrs), it gets the same platform-binary check. Any other PATH entry, such as a cargo-built binary, is used as is.

Known gap: a pnpm global install puts a wrapper script on PATH that can't be traced back to the package, so on an unsupported platform that case is not guarded.

Verification

  • 6 new unit tests in packages/lsp/test/unit/typescript-native.test.ts: tsrs preferred over tsc/tsgo, tsrs hoisted in a parent node_modules, tsrs without a platform binary falling back to tsc, a tsrs binary on PATH used as a fallback, a global npm launcher without a platform binary skipped, and a local native tsc preferred over a global tsrs. The suite runs with an isolated empty PATH. The native/client unit tests pass (34); typecheck and eslint are clean.
  • linux/amd64 Docker container (oven/bun:1.3.5) with the real tsrs@0.18.0-ts7.1.0-dev.20260929: LSPManager logged "Using tsrs language server" and returned diagnostics ("Type 'string' is not assignable to type 'number'.", "'x' is declared but its value is never read."); hover returned const x: number.
  • darwin-x64 with the real package installed: the resolver correctly returned no native server and fell back. The same holds for a real npm install -g tsrs on PATH.
  • linux/amd64 container with a real global bun add -g tsrs: LSPManager logged "Using global tsrs language server" and returned the expected diagnostics.

Related issue

None.

Checklist

  • PR title follows Conventional Commits
  • Tests added or updated, and the suite passes (bun run test)
  • Lint/format pass (bun run lint)
  • Documentation updated if behavior changed
  • No breaking change, or a BREAKING CHANGE: note is included

Summary by cubic

The TypeScript LSP now prefers a project-local tsrs (the Rust port of TypeScript 7) when its native binary is available, falling back to native tsc, tsgo, a global tsrs on PATH, then the classic typescript-language-server. Previously only native tsc/tsgo were auto-selected, and on unsupported platforms like darwin-x64 the tsrs wrapper exited 1 and left TypeScript without an LSP after a 45s timeout.

Behavior details

  • Resolution order is tsrs, native tsc (TS ≥ 7), tsgo, global tsrs on PATH, then the classic server, all under one server id so diagnostics aren't duplicated.
  • tsrs is selected only when TSRS_BINARY resolves to an existing binary (relative paths against the project root) or an installed @tsrs/* platform package matches the host os/cpu — including the launcher's -musl/-gnu selection on Linux — and contains the executable.
  • A global tsrs is used only when it's the real binary, an npm launcher with its platform binary installed, or a wrapper script whose package has the binary, including wrapper scripts in PATH directories containing spaces; otherwise it falls through to the classic server.
  • A PATH entry that disappears between lookup and realpath now falls back instead of throwing.
  • On Windows the chosen command is quoted when spawned so install paths with spaces work; tests mirror the real global <prefix>\tsrs.cmd layout.
  • Helper functions for libc detection, platform-package naming, and Windows spawn args are injectable and unit-tested.
  • Docs updated across the configuration overview, supported-languages reference (including root detection markers), and packages/lsp/CLAUDE.md; unit tests cover the resolution order, unsupported-platform and missing-binary fallbacks, hoisted monorepo installs, the global PATH fallback, and relative TSRS_BINARY, and are isolated from an ambient TSRS_BINARY.

Written for commit fada9d5. Summary will update on new commits.

Turn on auto-fix

@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@codecov

codecov Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.34247% with 36 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
packages/lsp/src/server/typescript.ts 75.34% 36 Missing ⚠️

📢 Thoughts on this report? Let us know!

…bient TSRS_BINARY

A PATH entry that disappears between Bun.which and realpathSync now falls
back instead of throwing out of spawn. The typescript-native tests clear
TSRS_BINARY so a developer's exported override cannot change their results,
and packages/lsp/CLAUDE.md now describes the single launcher-resolved
platform package.
@amondnet
amondnet marked this pull request as ready for review October 21, 2026 07:14
@greptile-apps

greptile-apps Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; the previously reported Windows fixture issue is fixed.

Summary

The PR prefers usable project-local tsrs before native tsc and tsgo, with global tsrs before the classic server.

  • TypeScript projects can use a supported local tsrs first.
  • A usable tsrs on PATH can serve projects without local native tools.
  • Review-tool decisions are kept in a tracked ledger.

Reviews (3) · Last reviewed commit: "test(lsp): mirror the Windows global npm..." · Reviewed by Greptile

Comment thread packages/lsp/test/unit/typescript-native.test.ts
Comment thread packages/lsp/src/server/typescript.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 7 files

Architecture diagram
sequenceDiagram
    participant LSP as LSPManager (spawn)
    participant Resolver as resolveNativeTypeScriptServer
    participant FS as File System / node_modules
    participant PATH as PATH / Global Bins
    participant Proc as Child Process (LSP)

    Note over LSP,Proc: TypeScript LSP selection flow (project root)

    LSP->>Resolver: resolveNativeTypeScriptServer(root)
    
    alt Project-local tsrs installed
        Resolver->>FS: findPackageBin(root, "tsrs")
        FS-->>Resolver: tsrs bin path (or undefined)
        alt tsrs bin found
            Resolver->>FS: hasTsrsBinary(packageRoot, root)
            alt TSRS_BINARY set
                FS-->>Resolver: exists(path.resolve(root, TSRS_BINARY))
            else TSRS_BINARY unset
                Resolver->>FS: resolve @tsrs/<platform> package.json
                FS-->>Resolver: platform binary exists?
            end
            alt Binary available
                Resolver-->>LSP: { command: bin, label: "tsrs" }
            else No binary (e.g. darwin-x64)
                Resolver->>Resolver: fall through to tsc
            end
        end
    end

    alt Project-local native tsc (TS >= 7)
        Resolver->>FS: findTypeScriptPackageRoot(root)
        FS-->>Resolver: packageRoot
        alt isNativeTypeScriptPackage(packageRoot)
            Resolver-->>LSP: { command: .bin/tsc, label: "native tsc" }
        end
    end

    alt Project-local tsgo (@typescript/native-preview)
        Resolver->>FS: findPackageBin(root, "@typescript/native-preview", "tsgo")
        FS-->>Resolver: tsgo bin path
        alt tsgo found
            Resolver-->>LSP: { command: tsgo, label: "native tsgo" }
        end
    end

    alt Global tsrs on PATH
        Resolver->>PATH: Bun.which("tsrs")
        PATH-->>Resolver: global bin path (or undefined)
        alt Global tsrs found
            Resolver->>FS: realpathSync(bin)
            alt realpath fails (disappeared)
                Resolver->>Resolver: return undefined (fall back)
            else realpath succeeds
                alt npm launcher (symlink/global package)
                    Resolver->>FS: check package.json name === "tsrs"
                    alt package found
                        Resolver->>FS: hasTsrsBinary(packageRoot, root)
                        alt platform binary installed
                            Resolver-->>LSP: { command: bin, label: "global tsrs" }
                        else no platform binary
                            Resolver->>Resolver: skip (fall through)
                        end
                    end
                else native executable (ELF/Mach-O/PE)
                    Resolver->>FS: isNativeExecutable(realBin)
                    alt is native binary
                        Resolver-->>LSP: { command: bin, label: "global tsrs" }
                    else wrapper script (pnpm/shim)
                        Resolver->>FS: findWrapperPackageRoot(script, bin)
                        alt package root found
                            Resolver->>FS: hasTsrsBinary(packageRoot, root)
                            alt platform binary installed
                                Resolver-->>LSP: { command: bin, label: "global tsrs" }
                            else no binary
                                Resolver->>Resolver: skip (fall through)
                            end
                        else no package root
                            Resolver->>Resolver: skip (fall through)
                        end
                    end
                end
            end
        end
    end

    alt Native server selected
        LSP->>Proc: spawn(command, ["--lsp", "--stdio"])
        Note over Proc: CHANGED: Windows command quoted, shell enabled
        Proc-->>LSP: Process running
        LSP-->>LSP: log "Using <label> language server"
    else No native server found
        LSP->>LSP: fall back to classic typescript-language-server --stdio
    end
Loading

Reply with feedback, questions, or to request a fix.

Turn on auto-fix | Re-trigger cubic

Comment thread apps/docs/content/docs/3.configuration/1.overview.md Outdated
Comment thread packages/lsp/test/unit/typescript-native.test.ts Outdated
Comment thread packages/lsp/test/unit/typescript-native.test.ts
Comment thread packages/lsp/src/server/typescript.ts Outdated
Comment thread apps/docs/content/docs/4.reference/2.supported-languages.md Outdated
Make the libc and platform-package helpers injectable and test them with
independent expected names, extract the Windows-quoted spawn arguments,
exercise the global npm launcher with an executable target, and align the
tsrs docs with the resolver's fallback order and root markers.
Comment thread packages/lsp/test/unit/typescript-native.test.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 4 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Turn on auto-fix | Re-trigger cubic

Comment thread packages/lsp/test/unit/typescript-native.test.ts
… tests

npm on Windows writes a global <prefix>\tsrs.cmd beside node_modules, but
the fixture put the shim in node_modules\.bin, which resolveGlobalTsrs does
not recognize, so the positive global-launcher test failed there.
@sonarqubecloud

Copy link
Copy Markdown

@amondnet
amondnet merged commit 3d230fe into main Oct 21, 2026
10 checks passed
@amondnet
amondnet deleted the amondnet/tsrs branch October 21, 2026 14:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant