Consuming from Node
Status: live.
@gdscript-analyzer/coreis published and this is its real, shipped surface (plans/ROADMAP.mdtracks what's still ahead on the road to1.0).
Node consumers install the napi-rs native addon:
npm i @gdscript-analyzer/core
# pnpm add @gdscript-analyzer/core
The addon is a real native .node binary built with
napi-rs v3 — no WASM overhead, full native speed. This is the
path that powers Node-based LSP servers, including guitkx's. Per-platform
prebuilt binaries (@gdscript-analyzer/core-linux-x64-gnu, -darwin-arm64,
-win32-x64-msvc, …) are pulled in automatically through
optionalDependencies, so there is no compile step for consumers.
napi vs wasm
There are two thin binding crates sharing one engine-neutral core
(gdscript-session, a URI-keyed session over gdscript-ide): gdscript-ffi
(napi-rs v3 → the Node native addon you're reading about here) and
gdscript-wasm (wasm-bindgen → the browser package). Sharing
gdscript-session means the two surfaces can't drift apart — only the thin
#[napi]/#[wasm_bindgen] delegation differs. For Node you almost always want
the native addon (this package): it is faster, has no
SharedArrayBuffer/COOP-COEP requirements, and reads files on the host side.
Reach for the wasm package only for the browser or a
sandboxed/edge runtime where a native addon can't load. See
ADR-0003 for the original binding
decision and its amendment.
The shape across the boundary
The binding keeps a stateful, URI-keyed session alive inside Rust so the
analysis cache survives edits. The JS side opens/changes/closes documents by
URI and runs queries by URI + byte offset; results come back as native JS
objects (via napi's serde_json::Value bridge — no client-side
JSON.parse). The surface is intentionally small and flat — strings and
structs cross the boundary by copy, so a query returns only its feature
result, never a whole AST.
import { AnalysisHandle } from "@gdscript-analyzer/core";
const az = new AnalysisHandle();
az.openDocument(
"inmemory://player.gd",
"extends Node\n\nfunc _ready() -> void:\n\tprint(1 + 1)\n",
"res://player.gd", // optional res:// path, or null
);
// Byte offsets in; native JS objects out. The client maps byte offsets -> UTF-16.
const diagnostics = az.diagnostics("inmemory://player.gd");
const symbols = az.documentSymbols("inmemory://player.gd");
const hover = az.hover("inmemory://player.gd", 42);
console.log(diagnostics, symbols, hover);
AnalysisHandle also exposes changeDocument/closeDocument,
setProjectConfig/setWorkspaceComplete/setWarningOverride, and one method
per IDE feature (completions, signatureHelp, codeActions,
gotoDefinition, findReferences, rename, workspaceSymbols,
semanticTokens, foldingRanges, inlayHints, format, formatRange,
syntaxTree) — see the Node package README
for the full, current list.
Position encoding (the footgun)
The core emits byte offsets. LSP uses UTF-16 code units. The binding
glue ships a byte→UTF-16 converter (backed by gdscript-base's LineIndex) —
do the conversion at the boundary, not in your application code. This is
discussed in plans/01-ARCHITECTURE.md §4.