| title | Node.js binding |
|---|---|
| description | Use @corsa-bind/napi from Node.js, Deno, and Bun — spawn options, sync vs. async, virtual documents, checker queries, and the raw call escape hatches. |
@corsa-bind/napi exposes the corsa Rust workspace to JavaScript runtimes
through napi-rs. The performance-critical work stays in Rust; the package
gives you an ergonomic, Promise-friendly TypeScript surface on top.
This guide covers the public API. For the authoring model of type-aware lint rules built on the same Rust core, see the Oxlint guide.
npm i @corsa-bind/napiThe published root package is JS-only and pulls in the matching native binary through platform-specific optional dependencies. Supported runtimes:
| Runtime | Minimum version |
|---|---|
| Node.js | 22+ |
| Deno | 2.0+ |
| Bun | 1.2+ |
Deno needs npm resolution with a local node_modules directory and native
addon permissions:
deno run --node-modules-dir=auto --allow-ffi --allow-read --allow-env --allow-run ./main.tsBun installs and imports the package directly:
bun add @corsa-bind/napi
bun run ./main.tsImportant
The package never bundles a Corsa executable. You must provide a compatible
binary yourself and pass its path through CorsaApiClient.spawn({ executable }).
During local development the repo-local mock binary
(vp run -w build_mock) is enough for most flows.
Every client method ships in two forms:
*Async(Promise-based) — the right choice for production Node services and editor integrations. Work runs off the JavaScript call path.- synchronous — convenient for short scripts and latency-bounded tooling, but it runs on the JS call path and will block the event loop.
The examples below use the synchronous form for brevity; prepend Async and
await for the non-blocking variant.
CorsaApiClient.spawn(options) starts a Corsa process and returns a client.
import { CorsaApiClient } from "@corsa-bind/napi";
const client = CorsaApiClient.spawn({
executable: "/path/to/corsa",
cwd: process.cwd(),
mode: "msgpack",
});| Field | Type | Default | Notes |
|---|---|---|---|
executable |
string |
(required) | Path to the Corsa binary you provide. |
cwd |
string |
process cwd | Working directory the checker resolves projects against. |
mode |
"jsonrpc" | "msgpack" |
"msgpack" |
Transport. msgpack is fastest on the pinned upstream commit. |
requestTimeoutMs |
number |
30000 |
Per-request timeout. |
shutdownTimeoutMs |
number |
2000 |
Graceful shutdown budget on close(). |
outboundCapacity |
number |
256 |
Outbound request queue capacity. |
allowUnstableUpstreamCalls |
boolean |
false |
Opt in to unstable upstream endpoints such as printNode. |
runExternalCode |
boolean |
false |
Opt in to trusted TypeScript content mapper processes. |
Always pair a spawn() with a close() (use try/finally):
try {
client.initialize();
// ... work ...
} finally {
client.close();
}import { CorsaApiClient } from "@corsa-bind/napi";
const client = CorsaApiClient.spawn({ executable: process.env.CORSA_BIN!, mode: "jsonrpc" });
let snapshotHandle: string | undefined;
try {
const init = client.initialize();
const snapshot = client.updateSnapshot({ openProject: "./tsconfig.json" });
snapshotHandle = snapshot.snapshot;
const project = snapshot.projects[0];
if (!project) throw new Error("no project resolved");
// Read a source file as it sees it.
const sourceFile = client.getSourceFile(snapshot.snapshot, project.id, "./src/index.ts");
console.log(Buffer.from(sourceFile ?? []).toString("utf8"));
// Query a type and stringify it.
const stringType = client.getStringType(snapshot.snapshot, project.id);
console.log(client.typeToString(snapshot.snapshot, project.id, stringType.id));
console.log("checker cwd:", init.currentDirectory);
} finally {
if (snapshotHandle) client.releaseHandle(snapshotHandle);
client.close();
}For TypeScript content mappers, keep runExternalCode unset unless the
workspace is trusted. Setting it to true forwards TypeScript's
--runExternalCode gate to the spawned checker so configured contentMappers
may launch their mapper processes.
The same checker path understands TypeScript explicit resource management
syntax, including using declarations and await using inside async scopes.
updateSnapshot() returns a snapshot handle plus the resolved projects.
Most query methods take (snapshot, projectId, …). Handles are owned by the
Rust side — release them with releaseHandle(handle) when you are done, and
always close() the client.
updateSnapshot() accepts:
openProject— atsconfig.jsonto openfileChanges— on-disk file change notificationsoverlayChanges— in-memory overlay edits
All of these have *Async counterparts.
| Method | Returns |
|---|---|
initialize() |
InitializeResponse (e.g. currentDirectory) |
parseConfigFile(file) |
ConfigResponse |
updateSnapshot(params?) |
{ snapshot, projects } |
getSourceFile(snapshot, project, file) |
Buffer | null |
getStringType(snapshot, project) |
TypeResponse |
getTypeAtPosition(snapshot, project, file, position) |
TypeResponse | null |
getSymbolAtPosition(snapshot, project, file, position) |
symbol response |
getTypeArguments(snapshot, project, type) |
type list |
getTypeOfSymbol(snapshot, project, symbol) |
TypeResponse | null |
getDeclaredTypeOfSymbol(snapshot, project, symbol) |
TypeResponse | null |
typeToString(snapshot, project, type) |
string |
For endpoints without a typed wrapper, drop down to the raw calls below.
When you need an endpoint that does not have a dedicated method, call it
directly. callJson is the readable path; callBinary returns the raw msgpack
payload for hot loops.
const result = client.callBinary("getSomethingCustom", { snapshot, project: project.id });callJson/callBinary are how the advanced checker_queries and raw_calls
examples reach symbol, signature, and relation endpoints.
Note
printNode and other unstable upstream endpoints are disabled unless you
pass allowUnstableUpstreamCalls: true. The pinned Corsa commit can panic in
internal/printer on real project data — opt in only when you accept that.
CorsaVirtualDocument edits in-memory documents without any Corsa process.
This is the fastest way to prototype rules and editor flows.
import { CorsaVirtualDocument } from "@corsa-bind/napi";
const document = CorsaVirtualDocument.untitled(
"/virtual/example.ts",
"typescript",
"const answer = 41;\n",
);
document.applyChanges([
{
range: { start: { line: 0, character: 15 }, end: { line: 0, character: 17 } },
text: "42",
},
]);
console.log(document.state()); // { uri, languageId, version, text }CorsaVirtualDocument.untitled(path, languageId, text)— untitled documentCorsaVirtualDocument.inMemory(authority, path, languageId, text)— authority-scoped documentdocument.replace(text)— full-text replacedocument.applyChanges(changes)— incremental LSP-style editsdocument.state()— current{ uri, languageId, version, text }
A VirtualChange with a range splices that range; omit the range to replace
the whole document.
The binding re-exports the Rust-backed type-text helpers used by the lint layer.
They work on type-text strings (e.g. "Promise<any>") with no checker process:
import { isUnsafeAssignment, isUnsafeReturn, classifyTypeText, Utils } from "@corsa-bind/napi";
isUnsafeAssignment({ sourceTypeTexts: ["Set<any>"], targetTypeTexts: ["Set<string>"] }); // true
isUnsafeReturn({ sourceTypeTexts: ["Promise<any>"], targetTypeTexts: ["Promise<string>"] });
classifyTypeText("string[]");Utils groups the predicate family: isStringLikeTypeTexts,
isNumberLikeTypeTexts, isPromiseLikeTypeTexts, splitTopLevelTypeText,
splitTypeText, and friends. The native lint entry points
(runNativeLintRule, nativeLintRuleMetas) are also exported here and are
what corsa-oxlint builds on.
- The Rust crate
corsa_lspprovides full LSP client support with virtual document overlays; the Node binding surfaces the document primitives shown above. See the Rustlsp_overlayexample for thedidOpen/didChange/didCloseflow. CorsaDistributedOrchestratorexposes the experimental replicated-state API (campaign,leaderId,openVirtualDocument,changeVirtualDocument). This is behind the experimental distributed work — see Support policy for its scope.
Build and test the binding from the workspace:
vp install
vp run -w build_wrapper
vp test run --config ./vite.config.ts src/bindings/nodejs/corsa_node/ts/**/*.test.tsBenchmarks for the Node binding:
vp run -w bench_ts- Getting started — first program in Node and Rust
- Oxlint guide — type-aware rules on the same Rust core
- Performance — transport measurements and benchmark entry points
- Examples — runnable Node samples