Skip to main content

TypeScript and JavaScript Beta

Rune ships Tier 1 TypeScript and JavaScript support. Code intelligence comes from the native TypeScript 7 compiler, typescript-go, which also serves the language server protocol. On top of that you get tsgo's code actions on the command prompt and a Rune console command for type-checking, installing dependencies, and running package.json scripts.

One language server serves every dialect: .ts, .mts, .cts, .tsx, .js, .mjs, .cjs, and .jsx. Code intelligence works across them, so going to a definition from a .tsx component lands in the .ts module that declares it.

Setup​

There is no setup. The typescript package bundles the TypeScript 7 compiler. It needs no Node.js runtime, and installing it also installs the javascript and tsx grammars. Rune picks the compiler for each project in this order:

  1. The lsp_path setting, when you set one.
  2. The project's own TypeScript 7 or later, installed by its package manager under node_modules, including the isolated layouts of pnpm and bun. Rune looks in the project and in every folder above it up to the workspace root, so a monorepo's hoisted install is found too. This way the server checks your code with the same compiler your build uses.
  3. The compiler from the typescript package.
  4. A tsgo or tsc on the workspace host's PATH, as long as it is TypeScript 7 or later. Older tsc releases do not serve LSP.

To use a compiler of your own, point Rune at it in config.yaml:

extensions:
typescript:
config:
lsp_path: "/path/to/tsgo"

A project pinned to TypeScript 6 or older is still analyzed by the TypeScript 7 server, with TypeScript 7 semantics. TypeScript 7 removed tsconfig.json options that older releases accept, such as baseUrl, target ES5 and moduleResolution node. When the project's node_modules holds an older typescript, Rune warns once, and ts check names each removed option. The removed options also show as diagnostics on tsconfig.json while it is open. ts version shows both versions when they differ.

Dependencies​

The language server works without node_modules. Imports of packages that are not installed show up as unresolved-import diagnostics. Rune never installs dependencies by itself, because package install scripts run arbitrary code. When a project declares dependencies that are not installed, Rune shows a one-time hint. For a project at the workspace root, the hint points at ts install. For a nested project, it names the package manager command to run in that project's folder.

How Rune finds your project​

A TypeScript or JavaScript project starts at the folder that holds its tsconfig.json, jsconfig.json, or package.json. TypeScript finds the nearest tsconfig.json for each file on its own, so one language server covers every package below its root. Rune roots it at the outermost of those folders, so the packages of a monorepo share one server:

  • Workspace root. The language server starts right away when the workspace you open has one of those files, or has TypeScript or JavaScript files at its top level.
  • Nested projects. When the workspace root has no marker, opening a file starts a server at the outermost folder between the file and the workspace root that has one. A monorepo in a subfolder of the workspace gets one server for all of its packages.
  • Loose scripts. A file with no marker in any folder between it and the workspace root is served from the workspace root.

Because the packages share a server, find references and rename reach the other packages that import a symbol. The server only searches the projects it has loaded, which happens when you open one of their files.

A TypeScript file that no tsconfig.json includes is checked as an inferred project, with default compiler options: no paths aliases, and no packages' types, such as @types/node. Unlike Go, Rust, and Zig, such a file still gets code intelligence. Rune tells you once per server when you open one, so its unresolved imports do not come as a surprise. An inferred project holds only the files you have open and the files they import, so find references and rename miss callers in files that are closed: references to a helper list only its declaration until you open a file that calls it. Add a tsconfig.json that includes the files to fix both.

Working across several projects in one repository? See the Monorepos guide.

Code intelligence: the lsp command​

The cross-language lsp command works for TypeScript and JavaScript the same way it works everywhere: hover, definition, type definition, implementation, references, completion, signature help, rename, formatting, and diagnostics. See the lsp command for the full table. TypeScript 7.0 does not implement go to declaration, so lsp declaration reports no results.

tsgo commands: the ts command​

The command prompt has its own ts command for tsgo's code actions and its extensions to the protocol. Open the command prompt and enter ts <subcommand>. This command is separate from the ts console command.

CommandWhat it does
ts organize-importsRemove unused imports, then sort and merge the rest.
ts remove-unused-importsRemove unused imports and keep the order of the rest.
ts sort-importsSort and merge imports without removing any.
ts fix-allApply every fix tsgo can make on its own across the file, such as adding missing imports.
ts quickfixApply a fix for the diagnostic at the cursor. When several fixes apply, Rune opens a picker.
ts listList every code action at the cursor or selection and apply the one you choose.
ts source-definitionGo to the JavaScript implementation of the symbol at the cursor, where lsp definition stops at its .d.ts declaration.
ts open-tsconfigOpen the tsconfig.json or jsconfig.json that owns the current file, or report that the file is in an inferred project.

TypeScript 7.0 ships no refactorings yet. Its code actions are the organize-imports family, fix-all, and quick fixes that add a missing import, implement the members of an interface a class declares, or add the type annotations isolatedDeclarations requires. ts list shows whatever the server offers, so new actions appear there as TypeScript releases them.

Profiling the language server​

If you work on tsgo itself, turn on its profiling commands:

extensions:
typescript:
config:
developer: true
CommandWhat it does
ts cpu-profile start [<dir>]Start profiling the CPU of the TypeScript language servers.
ts cpu-profile stopStop profiling and report where each server wrote its profile.
ts heap-profile [<dir>]Save a heap profile of a TypeScript language server.
ts alloc-profile [<dir>]Save an allocation profile of a TypeScript language server.
ts gcRun the garbage collector of the TypeScript language servers.

Profiles are written on the workspace host. They go to /tmp/rune-tsgo-profiles unless you pass a directory; a relative directory is resolved against the workspace root.

The console ts command​

The ts Rune console command type-checks, installs, and runs scripts. Open the console and run it as ts <subcommand>, or submit a one-off from the command prompt with console ts <subcommand>.

CommandWhat it does
ts check [<project>]Type-check a project with the TypeScript 7 compiler. <project> is a folder or a config file. A folder without a tsconfig.json checks every project below it.
ts install [<package>/] [<args>]Install dependencies with the package's package manager.
ts run [<package>/] [<script> [<args>]]Run a package.json script. Without a script, list them. Press <tab> to complete packages and script names.
ts test [<package>/] [<args>]Run the package.json test script. A bun package without one runs bun test, as bun init sets it up.
ts restartRestart every TypeScript language server, for example after installing typescript@7 into the project.
ts version [<project>]Show the language server's compiler and version, and the project's own typescript version when it differs.
ts help [<subcommand>]Show the command's usage, or one subcommand's details.

Without a project or package, a subcommand works on the file you last focused: ts check checks the nearest tsconfig.json above it, and the others use the nearest package.json. With no file focused, they work at the workspace root. A package argument must be . or contain a /, such as packages/api/, so that it is not taken for a script name or a test runner's argument. Projects and packages are relative to the workspace root, and <tab> completes them.

A tsconfig.json with references, such as the one Vite creates, lists no files of its own. ts check builds it with tsc -b so the referenced projects are checked, and writes the build info and any declarations they are configured to emit, as tsc -b in the project would. Other projects are checked with tsc --noEmit -p.

ts install, ts run, and ts test pick the package manager from the package's folder, or from the nearest folder above it that names one, as a workspace root does for its packages. A folder names it with the packageManager field of its package.json, or else with its lockfile: bun.lock or bun.lockb for bun, pnpm-lock.yaml for pnpm, yarn.lock for yarn, and package-lock.json for npm. Without either anywhere, it is npm. Arguments after the script name reach the script. With npm, Rune adds the -- that npm needs for that.

Their output appears when the command exits, so run watch modes and development servers from a terminal instead.

Symbol index​

The symbol index resolves qualified names in TypeScript and JavaScript without a running language server. A module is named after its file, or after its folder for an index.* file. For example, geometry.Shape names the Shape class in geometry.ts, and geometry.Shape.area names its area method.

The index resolves:

  • references through namespace imports (import * as geometry), import x = require() in TypeScript, and require() in JavaScript;
  • classes, interfaces, type aliases, enums, and functions, including const f = () => …;
  • methods of classes and abstract classes;
  • names re-exported from index.* files, including renamed ones.

Some constructs are left to the language server, which resolves them precisely. These are default imports bound to arbitrary local names, tsconfig paths aliases, bare package names resolved from node_modules, dynamic import(), export * chains through more than one index.* file, declaration merging, and methods assigned on prototypes. Leaving them unresolved avoids jumping to the wrong place.

Debugging​

Rune does not ship a JavaScript debug adapter yet. Support for debugging Node.js programs is planned.

Ask Rune Agent