🦀 tsrs for Ruby
A drop-in replacement for the TypeScript 7 compiler and language server, written in Rust.
What is tsrs for Ruby?
Ruby bindings for tsrs, a drop-in replacement for the TypeScript 7 compiler and language server, written in Rust. Type check, compile and transpile TypeScript from Ruby, without the need for a JavaScript runtime.
Everything here is tsrs doing the work. For what the options mean and what it can do, the TypeScript documentation is the reference.
Installation
bundle add tsrsAnywhere a precompiled gem is not published, the gem builds from source and needs the Rust toolchain. tsrs needs a recent Rust, 1.99.1 at the moment. rust/rust-toolchain.toml pins it, so rustup installs it for the build.
tsrs only runs on 64-bit platforms, so the gem doesn't support 32-bit ARM or x86.
Versioning
The gem carries the version of tsrs it packages. Gem v0.18.0 bundles tsrs v0.18.0. Tsrs.tsrs_version reports what a given build was compiled against, and Tsrs.typescript_version reports the TypeScript version that tsrs release is a port of.
So the version tracks tsrs, not the Ruby API here. If the gem needs releasing again for the same tsrs version, that release adds a fourth digit, as in v0.18.0.1.
Usage
Type checking
require "tsrs"
result = Tsrs.check(cwd: "app/javascript")
# => #<Tsrs::CheckResult status=2 errors=1 diagnostics=1>
result.success?
# => false
puts result
# app/javascript/src/index.ts:1:7 - error TS2322: Type 'string' is not assignable to type 'number'.
result.errors.first.code
# => 2322Without arguments, tsc finds the tsconfig.json in cwd. You can also point it at a project, or at files:
Tsrs.check(project: "app/javascript/tsconfig.json")
Tsrs.check("app/javascript/index.ts", strict: true)Tsrs.check never writes anything, as with tsc --noEmit. validate! raises Tsrs::TypeError if there were errors, so a rake task can fail on them:
task :typecheck do
Tsrs.check(cwd: "app/javascript").validate!
endChecking again and again
Tsrs.check runs tsc's command line, which is built to check once and exit, so a check's memory stays with the process. For anything long-lived, like a web server, a file watcher or a test suite that checks often, use a session instead:
session = Tsrs::Session.new(cwd: "app/javascript")
session.check.validate! # tsconfig.json in cwd
session.check("admin/tsconfig.json").errors # or any other project
session.closeTsrs::Session.open(cwd: ...) { |session| ... } closes the session when the block returns. A session is tsrs's API server, the same one behind tsrs's Node API. Each check builds the program in a snapshot and releases it afterwards, so memory levels off instead of growing by about 38 MB a check. Checks report the same diagnostics as Tsrs.check. They pick up what changed on disk since the last check, including the tsconfig.json, and can run from several threads at once.
The project's tsconfig.json decides the compiler options: a session check takes none of its own. It never writes anything, and raises Tsrs::CheckError when there is no project to load.
Compiling
Tsrs.compile runs tsc and writes its outputs, the way tsc does:
Tsrs.compile(project: "tsconfig.json", declaration: true)Tsrs.tsc takes command-line arguments exactly as tsc would:
Tsrs.tsc("--listFilesOnly", cwd: "app/javascript").outputTranspiling one file
Tsrs.transpile turns one TypeScript source into JavaScript without type checking it, like TypeScript's transpileModule. It doesn't look at the file system:
Tsrs.transpile("export const answer: number = 42;", target: "es2020").code
# => "export const answer = 42;\n"
result = Tsrs.transpile(source, file_name: "app.tsx", jsx: "react-jsx", source_map: true)
result.code
result.mapTsrs.transpile_declaration emits the .d.ts for a source written for isolatedDeclarations:
Tsrs.transpile_declaration("export function greet(name: string): string { return name; }").code
# => "export declare function greet(name: string): string;\n"A source that doesn't parse raises Tsrs::SyntaxError, which carries the diagnostics.
Compiler options
Compiler options are tsc's, written in snake case or as tsc spells them. They are passed to tsc as command-line flags, so tsc reads them the way it always does:
| Ruby | tsc |
|---|---|
strict: true |
--strict |
no_emit: false |
--noEmit false |
target: "es2022" |
--target es2022 |
lib: ["es2022", "dom"] |
--lib es2022,dom |
"esModuleInterop" => true |
--esModuleInterop |
Options that only make sense in a terminal, like watch, build, init and help, raise Tsrs::OptionError. So does a hash, which tsc's command line can't express. Put options like paths in a tsconfig.json.
Diagnostics
Each Tsrs::Diagnostic has a code, a category (:error, :warning, :suggestion or :message), a message, a file, and a 1-based line and column. start and finish are offsets in UTF-16 code units, the unit TypeScript counts in. message_chain and related_information hold the elaborations tsc attaches, and to_s prints a diagnostic the way tsc does.
Threads and memory
Every call releases the GVL, so other Ruby threads keep running and several checks can run at once. The work runs on a few long-lived native threads with large stacks, since type checking recurses deeply.
Transpiling gives back all the memory it used, and so does checking with a Tsrs::Session. Tsrs.check, Tsrs.compile and Tsrs.tsc don't: tsrs runs them like tsc, a process that checks once and exits, so much of what they allocate stays with the process. That's fine for a rake task or CI. In a long-lived process, check with a session instead.
Development
The gem is a C extension over a Rust crate. rust/ builds a static library and generates the C header with cbindgen, ext/tsrs/ wraps it, and lib/ is the Ruby API over that.
tsrs doesn't publish its crates, so rust/Cargo.toml takes them from the release tag on GitHub. rust/.cargo/config.toml stamps the TypeScript version the way tsrs's own release builds do. The upstream workflow keeps the tag, that version, and the Rust toolchain in step with each tsrs release.
bin/setup
bundle exec rakesig/ is generated from the #: annotations next to the code. Regenerate it with rake rbs after changing a signature, and CI checks that it matches.
Acknowledgements
tsrs is maintained at oxc-project/tsrs, and is a port of TypeScript. This gem only calls into it. Thank you for building these tools!
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/marcoroth/tsrs-ruby. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
Issues with type checking or emit belong upstream, with a minimal reproduction, since this gem does none of that itself. Issues with the Ruby API, the build, or the bindings belong here.
License
The Ruby, C, and Rust code in this gem is available under the terms of the MIT License.
It builds against tsrs, which is Apache-2.0 licensed and a derivative of TypeScript. Its native extension embeds TypeScript's lib.*.d.ts files. tsrs's license and NOTICE travel with the gem in licenses/.