Xantham.TypeScript.Wire
0.2.0
dotnet add package Xantham.TypeScript.Wire --version 0.2.0
NuGet\Install-Package Xantham.TypeScript.Wire -Version 0.2.0
<PackageReference Include="Xantham.TypeScript.Wire" Version="0.2.0" />
<PackageVersion Include="Xantham.TypeScript.Wire" Version="0.2.0" />
<PackageReference Include="Xantham.TypeScript.Wire" />
paket add Xantham.TypeScript.Wire --version 0.2.0
#r "nuget: Xantham.TypeScript.Wire, 0.2.0"
#:package Xantham.TypeScript.Wire@0.2.0
#addin nuget:?package=Xantham.TypeScript.Wire&version=0.2.0
#tool nuget:?package=Xantham.TypeScript.Wire&version=0.2.0
Xantham.TypeScript.Wire
An F# client for the TypeScript 7 compiler's API server — the Go tsc binary run as
tsc --api. It speaks the compiler's own msgpack protocol over stdio, and reads the binary AST
it returns without going through JSON.
- The full API surface, generated from the compiler's shipped schema: 142 synchronous calls,
the same set again as
Async, and typed records for every parameter and response. - The binary AST, read in place. A node is a struct over the blob and an index, not an object
graph, and the typed layer gives each one a tag —
Node<FunctionDeclaration>— so narrowing is a compile-time question. Generated fromast.json, so the 351 kinds and their child slots come from the compiler rather than from hand-written constants. Session<'T>, which binds the snapshot and project that 126 of those 142 calls repeat, so a call site carries only what varies.- A batching mailbox that collects overlapping calls into one
batchRequestsround trip. Under load it runs 2.1–2.3× the serial path; a lone caller pays nothing for it. - A virtual filesystem, so the compiler can be pointed at sources that exist only in memory.
Targets net10.0, net8.0 and netstandard2.1.
Getting started
The compiler is not bundled — install the typescript npm package whose version matches this
one, since the protocol is unversioned and both sides must come from the same tree.
open Xantham.TypeScript.Wire
open Xantham.TypeScript.Wire.Proto
// Finds the platform's tsc executable under a directory containing node_modules.
let exe = (Tsc.locate "./my-project").Value
use channel = new TscChannel(exe, "./my-project")
Api.initialize channel |> ignore
// Records whose fields are all optional carry a `Default` - copy-update the ones you mean.
let program =
Api.createProgram channel
{ CreateProgramParams.Default with
RootFiles = ValueSome [| DocumentIdentifier.FileName "./my-project/index.d.ts" |] }
let project = program.Project.Value.Id
match Api.getSourceFile channel { Snapshot = program.Snapshot; Project = project; File = DocumentIdentifier.FileName "./my-project/index.d.ts" } with
| ValueSome ast -> printfn $"%d{ast.NodeCount} nodes"
| ValueNone -> ()
Every method is also an extension member on the channel, with a second overload taking the parameter record's fields directly, so the channel need not be threaded through by hand:
let ast =
channel.getSourceFile(
snapshot = program.Snapshot,
project = project,
file = DocumentIdentifier.FileName "./my-project/index.d.ts")
Sessions
Most of the API resolves everything against a snapshot and a project, and restating that pair at
every call is most of what a call site says. Session<'T> binds it once and re-exposes the methods
without it:
let session = channel.Session program // the createProgram response already is the pair
let ast = session.getSourceFile(DocumentIdentifier.FileName "./my-project/index.d.ts")
let symbol = session.getSymbolAtPosition(DocumentIdentifier.FileName "./my-project/index.d.ts", 42)
The pair is data rather than identity, so a session moves: session.WithSnapshot updated.Snapshot
after a file changed, session.WithProject "tsconfig.build", and session.ForSymbol symbol for
the project a symbol was first observed in. The calls that precede a snapshot and so cannot take
one — updateSnapshot, createProgram, the transpile* family — live on session.Sessionless.
Session<TscMailbox> is the same members returning Async, so the transport is chosen where the
session is built and nowhere else.
TscMailbox is the same surface asynchronously, and batches whatever overlaps:
use mailbox = new TscMailbox(exe, "./my-project")
Async.RunSynchronously(AsyncApi.initialize mailbox) |> ignore
let! names = mailbox.getSourceFileNames(snapshot, project)
Reading the AST
Navigate through the typed layer rather than raw indexes — see docs/wire-navigation.md:
open Xantham.TypeScript.Wire.Patterns
for statement in SourceFile.statements (Node.root ast) do
match statement with
| FunctionDeclaration declaration ->
// `name` is a Node<Identifier> voption - a function declaration need not have one.
match FunctionDeclaration.name declaration with
| ValueSome name -> printfn $"function %A{Identifier.text name}"
| ValueNone -> ()
| _ -> ()
A Node<'Tag> is a struct over the blob and an index. The tags inherit each other exactly when
one's kinds are a subset of the other's, so 'Tag :> Expression is the compile-time form of
isExpression; narrowing is a [<return: Struct>] active pattern that allocates nothing.
Serving files from memory
let fs =
{ VirtualFileSystem.Default with
ReadFile = ValueSome(fun path -> if path = "/virtual.ts" then Content "export const a = 1" else FallBack)
FileExists = ValueSome(fun path -> if path = "/virtual.ts" then ValueSome true else ValueNone) }
use channel = new TscChannel(exe, cwd, VirtualFileSystem.callbacks fs)
FallBack and ValueNone mean "not answered, read the real filesystem"; Missing means the file
does not exist and resolution stops there. The distinction changes module resolution, so it is in
the types rather than in a comment.
Documentation
docs/wire-navigation.md— the typed layer, tags, views and the escape hatches.docs/wire-hand-written.md— every fact in the pipeline transcribed from upstream rather than derived from its schema, with how to update each one.docs/plans/tsgo-protocol.md— the wire protocol itself, verified against live byte traces.
Licensed under Apache-2.0.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 is compatible. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.1
- FSharp.Core (>= 10.1.400)
- System.Text.Json (>= 10.0.11)
-
net10.0
- FSharp.Core (>= 10.1.400)
-
net8.0
- FSharp.Core (>= 10.1.400)
- System.Text.Json (>= 10.0.11)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.2.0 | 39 | 9/1/2026 |