Quick Start Guide for Xantham.Fable
This guide will help you quickly understand and get started with working on the Xantham.Fable project.
Project Overview
Xantham.Fable is the TypeScript extraction component of the Xantham schema-driven TypeScript-to-F# bindings generator. It uses Fable to compile F# code to JavaScript and leverages the TypeScript Compiler API to parse .d.ts files and emit JSON conforming to a common schema.
Getting Started
Prerequisites
-
F# Development Environment:
- Visual Studio 2022 or later with F# tools
- VS Code with Ionide F# extension
- .NET SDK (version 6.0 or later)
-
Building Tools:
- Node.js and npm (for package management)
- Git for source control
Setup Instructions
-
Clone the Repository:
git clone https://github.com/your-organization/xantham.git cd xantham -
Restore Dependencies:
dotnet restore -
Run Tests:
npm run watch-test
Understanding the Codebase Structure
Core Directory Structure
|
Key Concepts to Understand
1. The XanthamTag System
The fundamental unit of data association in Xantham.Fable:
// XanthamTag is a Tracer<XanTagKind> with an identity Guard, attached
// directly to the underlying TS object via well-known JS symbol keys.
type XanthamTag =
inherit GuardedTracer<XanTagKind, GuardTracer>
// Created via three SRTP overloads dispatching on Ts.Node / Ts.Type / Ts.Symbol:
let tagState, guardState = XanthamTag.Create(node, checker)
let tag : XanthamTag = TagState.value tagState
let identityKey : IdentityKey = tag.IdentityKey // tag.Guard.Value
let xanTagKind : XanTagKind = tag.Value // discriminated kind
tag.Value is the XanTagKind discriminator (TypeDeclaration, MemberDeclaration,
TypeNode, JSDocTag, ...). tag.IdentityKey reads through to the guard's
identity (Symbol, AliasSymbol, DeclarationPosition, or Id TypeKey).
The tag also exposes a typed property bag and a separate keyed bag stored on
its guard — see Guarded Properties.
2. Signal-based Architecture
Xantham.Fable uses a sophisticated signal system for reactive data flow:
// Source signal (mutable value)
let sourceSignal = Signal.source 42
// Computed signal (depends on others)
let computedSignal = Signal.auto (fun () -> sourceSignal.Value * 2)
// Pending signal (filled later)
let pendingSignal = Signal.pending<int>()
// Later...
Signal.fill 100 pendingSignal
3. Stack-based Traversal
To prevent JavaScript stack overflows from deep AST structures:
// Processing uses explicit stack instead of recursion
let runReader (reader: TypeScriptReader) =
let mutable stackEntry = Unchecked.defaultof<XanthamTag>
while reader.stack.TryPop(&stackEntry) do
Dispatcher.dispatch reader stackEntry
reader
Key Files to Explore
1. Main Entry Point
Program.fs - Contains the CLI interface and main execution flow:
// Entry point that handles command line arguments
let main argv =
match argv with
| [| inputFile |] ->
let reader = TypeScriptReader.create inputFile
// Process and output results
| _ ->
// Handle error / show help
2. Core Reading Logic
Read.fs - Main processing logic for transforming AST to JSON:
// Core reading function
let read (reader: TypeScriptReader) : EncodedResult =
// Setup processing
// Traverse AST using stack
// Process type declarations
// Resolve duplicates
// Encode result
3. Signal System
Types/Signal.fs - The reactive system at the heart of data flow:
// Unified reactive signal — covers source and computed signals behind one surface.
type Signal<'a> =
member Value : 'a // re-computes if dirty; registers as dep in auto-scope
member Invalidated : IEvent<unit> // fires once per dirty transition
member Set : 'a -> unit // source signals only; equality-checked
member FulfillWith : (unit -> 'a) -> unit // retrofit a thunk onto a pending source
See Signal Architecture for the full surface
(Signal.source, Signal.auto, Signal.computed, Signal.pending,
Signal.map, Signal.effect, ...).
4. Tag Dispatching
Reading/Dispatcher.fs - Routes processing to appropriate handlers:
let dispatch (ctx: TypeScriptReader) (tag: XanthamTag) =
match tag.Value with
| XanTagKind.TypeDeclaration typeDecl ->
TypeDeclaration.dispatch ctx tag typeDecl
// ... other handlers
Development Workflow
1. Adding New Node Type Processing
To add processing for a new TypeScript construct:
-
Define tag kind in
XanTagKind.fs:type XanTagKind = // ... existing kinds | CustomNode of CustomNodeData -
Create processing module in
Reading/CustomNode.fs:module CustomNode = let dispatch (ctx: TypeScriptReader) (tag: XanthamTag) (nodeData: CustomNodeData) = // Process the custom node // Return results through signals/property system -
Update dispatcher in
Reading/Dispatcher.fs:| XanTagKind.CustomNode nodeData -> CustomNode.dispatch ctx tag nodeData
2. Working with Signals and Properties
// Getting or setting guarded properties
let processNode (ctx: TypeScriptReader) (tag: XanthamTag) =
// Get or create a builder
let builder = AstNodeBuilder.getOrSetWith (fun () -> SType()) tag
// Set properties on the builder
builder.NodeType <- "CustomType"
// Store back on tag
AstNodeBuilder.set builder tag
3. Debugging Tips
Xantham.Fable has a per-tag opt-in tracing system — see Debugging for the full surface. The short version:
-
Seed a single tag with
XanthamTag.setDebugForReason "why"near the handler you suspect. - Propagate to children with
XanthamTag.chainDebug parent child. -
Sprinkle
XanthamTag.debugLocationAndForget "Module.function" taginside handlers; in release builds these elide to no-ops. -
Use
XanthamTag.withDebugOneShot key fn tagfor hot paths (cache lookups, the stack-pop loop) so a re-entered tag does not flood the log. -
Stack overflows usually mean a
Signal.fulfillWiththunk is reading the same signal it is filling — see the self-reference hazard notes in Signal Architecture.
Testing
The project implements unit tests using Fable.Mocha, these can be run using the following commands:
|
Common Patterns for Extending
1. Adding New Type Processing
// In Reading/MyNewType.fs
module MyNewType =
let dispatch (ctx: TypeScriptReader) (tag: XanthamTag) (typeNode: MyNewTypeNode) =
// Process with computed signals
let computedResult = Signal.auto (fun () ->
// Complex logic here
processNewType typeNode
)
// Store result using guarded properties
TypeSignal.fill computedResult typeSignal
2. Handling Complex Types
// For recursive or complex types
let handleComplexType (ctx: TypeScriptReader) (tag: XanthamTag) =
if not (isAlreadyProcessed ctx tag.IdentityKey) then
// Process complex type
// Push children to stack
typeChildren
|> Array.iter (fun child ->
pushToStack ctx (createTagFor child)
)
else
// Skip already processed
()
This quick start guide should help you understand the main architecture, key components, and development patterns used in Xantham.Fable. The system is designed to be extensible and follows F# best practices for reactive programming and functional design.
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
module Set from Microsoft.FSharp.Collections
--------------------
type Set<'T (requires comparison)> = interface IReadOnlyCollection<'T> interface IStructuralEquatable interface IComparable interface IEnumerable interface IEnumerable<'T> interface ICollection<'T> new: elements: 'T seq -> Set<'T> member Add: value: 'T -> Set<'T> member Contains: value: 'T -> bool member IsProperSubsetOf: otherSet: Set<'T> -> bool ...
--------------------
new: elements: 'T seq -> Set<'T>