Getting Started
This guide takes you from a clean checkout to a working corsa-bind program.
It covers three entry points:
- the Rust facade crate (
corsa) - the Node.js binding (
@corsa-bind/napi) - the type-aware Oxlint framework (
corsa-oxlint)
You can complete the first two sections without building the real Corsa upstream binary. The Real pinned Corsa section shows how to wire in the actual checker when you want end-to-end behavior.
For the bigger picture and design rationale, read the Architecture guide. For everything that ships as runnable code, see the examples workspace.
1. Set up the toolchain
corsa-bind is a multi-language workspace. The Nix dev shell provides every
toolchain used by the Rust crates, the Node binding, and the
language bindings:
nix develop
vp install
vp is Vite+; it manages Node 24, pnpm, oxfmt, and
oxlint for the JS side. The Nix shell is authored in flake.tnix and compiled
to flake.nix with tnix.
Build the whole workspace once so the native binding and mock server exist:
vp run -w build
This produces, among other things:
- the Rust workspace crates
- the
@corsa-bind/napinative addon and its TypeScript wrapper - the repo-local mock Corsa binary used by examples and tests
TIP
The mock binary lets you exercise realistic API and LSP flows without
building the real upstream checker. Build only the mock with
vp run -w build_mock.
2. First program in Rust
The corsa facade crate re-exports the API client, LSP types, virtual
documents, and the lightweight runtime. The smallest useful program edits an
in-memory document — no Corsa binary required:
use corsa::{
lsp::{VirtualChange, VirtualDocument},
runtime::block_on,
};
use lsp_types::{Position, Range};
fn main() -> Result<(), corsa::CorsaError> {
block_on(async {
let mut document =
VirtualDocument::untitled("/virtual/minimal.ts", "typescript", "const answer = 41;\n")?;
let events = document.apply_changes(&[VirtualChange::splice(
Range::new(Position::new(0, 15), Position::new(0, 17)),
"42",
)])?;
println!("emitted {} change event(s)", events.len());
Ok(())
})
}
Run the bundled equivalent directly from the workspace:
cargo run -p corsa --example minimal_start
When you want to talk to a checker, spawn an ApiClient. Point
ApiSpawnConfig at a cache directory and a working directory that contains a
Corsa-resolvable project:
use corsa::{
api::{ApiClient, ApiSpawnConfig},
runtime::block_on,
};
fn main() -> Result<(), corsa::CorsaError> {
block_on(async {
let client = ApiClient::spawn(
ApiSpawnConfig::new(".cache/corsa")
.with_cwd("ref/corsa-upstream/_packages/native-preview"),
)
.await?;
let init = client.initialize().await?;
println!("current directory: {}", init.current_directory);
client.close().await?;
Ok(())
})
}
ApiSpawnConfig::new() defaults to the SyncMsgpackStdio transport, which is
the fastest transport on the pinned upstream commit. See the
Architecture guide for the crate layout behind these
types.
3. First program in Node.js
Install the published binding into any Node 22+, Deno 2.0+, or Bun 1.2+
project. The root package is JS-only and pulls in the matching native binary
through platform-specific optional dependencies:
npm i @corsa-bind/napi
The no-binary minimal start mirrors the Rust example — edit a virtual document and call the Rust-backed type helpers:
import { CorsaVirtualDocument, isUnsafeAssignment } from "@corsa-bind/napi";
const document = CorsaVirtualDocument.untitled(
"/virtual/minimal.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());
console.log(
isUnsafeAssignment({ sourceTypeTexts: ["Set<any>"], targetTypeTexts: ["Set<string>"] }),
);
To drive a checker, spawn CorsaApiClient with the path to a Corsa
executable. The promise-based methods (spawnAsync, initializeAsync,
updateSnapshotAsync, callJsonAsync, …) are the right choice for services and
editor integrations; the synchronous variants stay available for short scripts:
import { CorsaApiClient } from "@corsa-bind/napi";
const client = CorsaApiClient.spawn({
executable: process.env.CORSA_BIN!, // you provide the Corsa binary
cwd: process.cwd(),
mode: "msgpack",
});
try {
const init = client.initialize();
const snapshot = client.updateSnapshot({ openProject: "./tsconfig.json" });
const project = snapshot.projects[0];
const stringType = client.getStringType(snapshot.snapshot, project.id);
console.log(client.typeToString(snapshot.snapshot, project.id, stringType.id));
} finally {
client.close();
}
IMPORTANT
@corsa-bind/napi never bundles a Corsa executable. You must provide a
compatible binary and pass its path through spawn({ executable }). During
development the repo-local mock binary (vp run -w build_mock) works for most
flows.
The full Node surface — spawn options, sync vs. async, virtual documents,
callJson/callBinary, and LSP overlays — is documented in the
Node binding guide.
4. Type-aware Oxlint
corsa-oxlint lets you author Oxlint JS plugins with real type information
sourced from Corsa, keeping the typescript-eslint-style authoring model:
import { OxlintUtils } from "corsa-oxlint";
const createRule = OxlintUtils.RuleCreator((name) => `https://example.com/rules/${name}`);
export const noStringPlusNumber = createRule({
name: "no-string-plus-number",
meta: {
type: "problem",
docs: { description: "forbid string + number", requiresTypeChecking: true },
messages: { unexpected: "string plus number is forbidden" },
schema: [],
},
defaultOptions: [],
create(context) {
const services = OxlintUtils.getParserServices(context);
const checker = services.program.getTypeChecker();
return {
BinaryExpression(node) {
if (node.operator !== "+") return;
const left = checker.getTypeAtLocation(node.left);
const right = checker.getTypeAtLocation(node.right);
if (
checker.typeToString(checker.getBaseTypeOfLiteralType(left) ?? left) === "string" &&
checker.typeToString(checker.getBaseTypeOfLiteralType(right) ?? right) === "number"
) {
context.report({ node, messageId: "unexpected" });
}
},
};
},
});
Type-aware settings live under settings.corsaOxlint. The full authoring model,
native rule set, and stylistic rules are covered in the
Oxlint guide.
5. Explore the examples
The repository ships executable examples for Rust, @corsa-bind/napi, and
corsa-oxlint. The smoke-tested set runs without the real upstream binary:
vp run -w examples_smoke # all smoke examples
vp run -w examples_node_smoke # Node / TypeScript only
vp run -w examples_rust_smoke # Rust only
Run a single example directly:
pnpm --dir examples run minimal-start
cargo run -p corsa --example minimal_start
The examples README has a goal-oriented map from minimal edits up through checker queries, orchestration, and observability.
Run against the real pinned Corsa
When you want end-to-end behavior against the exact upstream commit, sync and verify the pinned checkout, build the real binary, then run the real examples:
vp run -w sync_ref # materialize ref/corsa-upstream at the pinned commit
vp run -w verify_ref # confirm the checkout matches corsa_ref.lock.toml
vp run -w build_corsa # build the real Corsa binary into .cache/corsa
vp run -w examples_real # run the real-snapshot examples
The pinned ref is the source of truth for reproducibility; how it is synced and verified is documented in Upstream dependency management.
Where to go next
- Architecture — workspace shape, upstream policy, and extension points
- Node binding guide — the full
@corsa-bind/napisurface - Language bindings — C, C++, Go, Zig, C#, Swift, and MoonBit
- Oxlint guide — type-aware and native rule authoring
- CI and local checks — reproduce the GitHub checks locally
- Production readiness — runtime controls and release gates