Skip to main content

Crate tsrun

Crate tsrun 

Source
Expand description

A minimal TypeScript runtime for embedding in applications.

This crate provides a TypeScript interpreter written in Rust, designed for configuration files where users benefit from IDE autocompletion, type checking, and error highlighting. TypeScript features like enums, interfaces, and generics are fully parsed; types are stripped at runtime (not type-checked).

§TypeScript Features

The interpreter supports TypeScript-specific syntax for better editor experience:

  • Enums - Numeric and string enums with reverse mappings
  • Interfaces & Types - Parsed for IDE support, stripped at runtime
  • Decorators - Class, method, property, and parameter decorators
  • Namespaces - TypeScript namespace declarations
  • Generics - Generic functions and classes
  • Parameter Properties - constructor(public x: number) syntax

§Quick Start

use tsrun::{Interpreter, StepResult};

let mut interp = Interpreter::new();
interp.prepare(r#"
    enum Status { Active = 1, Inactive = 0 }
    interface Config { status: Status; }
    const cfg: Config = { status: Status.Active };
    cfg.status
"#, None).unwrap();

loop {
    match interp.step().unwrap() {
        StepResult::Continue => continue,
        StepResult::Complete(value) => {
            assert_eq!(value.as_number(), Some(1.0));
            break;
        }
        _ => panic!("Unexpected result"),
    }
}

§Execution Model

The interpreter uses step-based execution, giving hosts full control:

§Working with Values

Use the api module for creating and manipulating JavaScript values:

use tsrun::{Interpreter, api};

let mut interp = Interpreter::new();
let guard = api::create_guard(&interp);

// Create objects from JSON
let user = api::create_from_json(&mut interp, &guard, &serde_json::json!({
    "name": "Alice",
    "scores": [95, 87, 92]
})).unwrap();

// Read properties
let name = api::get_property(&user, "name").unwrap();
assert_eq!(name.as_str(), Some("Alice"));

// Call methods on arrays
let scores = api::get_property(&user, "scores").unwrap();
let joined = api::call_method(&mut interp, &guard, &scores, "join", &["-".into()]).unwrap();
assert_eq!(joined.as_str(), Some("95-87-92"));

§Module Loading

ES modules are loaded on-demand. When execution needs an import:

use tsrun::{Interpreter, StepResult, ModulePath};

let mut interp = Interpreter::new();
interp.prepare(r#"import { x } from "./config.ts"; x"#, Some("/main.ts".into())).unwrap();

loop {
    match interp.step().unwrap() {
        StepResult::Continue => continue,
        StepResult::NeedImports(imports) => {
            for import in imports {
                // Host provides module source code
                let source = "export const x = 42;";
                interp.provide_module(import.resolved_path, source).unwrap();
            }
        }
        StepResult::Complete(value) => {
            assert_eq!(value.as_number(), Some(42.0));
            break;
        }
        _ => break,
    }
}

§Internal Modules

Register Rust functions as importable modules:

use tsrun::{Interpreter, InterpreterConfig, InternalModule, JsValue, Guarded, JsError};

fn get_version(
    _interp: &mut Interpreter,
    _this: JsValue,
    _args: &[JsValue]
) -> Result<Guarded, JsError> {
    Ok(Guarded::unguarded(JsValue::from("1.0.0")))
}

let config = InterpreterConfig {
    internal_modules: vec![
        InternalModule::native("app:version")
            .with_function("getVersion", get_version, 0)
            .build(),
    ],
    ..Default::default()
};
let interp = Interpreter::with_config(config);
// Now code can: import { getVersion } from "app:version";

§GC Safety

Objects are garbage-collected. Use Guard to keep them alive:

use tsrun::{Interpreter, api};

let mut interp = Interpreter::new();
let guard = api::create_guard(&interp);

// Objects allocated with guard stay alive until guard is dropped
let obj = api::create_object(&mut interp, &guard).unwrap();
api::set_property(&obj, "x", 42.into()).unwrap();

// guard dropped here - obj may be collected

§Use Case Examples

§Kubernetes Deployment Configuration

Generate type-safe Kubernetes manifests with IDE autocompletion:

use tsrun::{Interpreter, StepResult, js_value_to_json};

let mut interp = Interpreter::new();
interp.prepare(r#"
    interface DeploymentConfig {
        name: string;
        image: string;
        replicas: number;
        port: number;
    }

    function deployment(config: DeploymentConfig) {
        return {
            apiVersion: "apps/v1",
            kind: "Deployment",
            metadata: { name: config.name },
            spec: {
                replicas: config.replicas,
                selector: { matchLabels: { app: config.name } },
                template: {
                    metadata: { labels: { app: config.name } },
                    spec: {
                        containers: [{
                            name: config.name,
                            image: config.image,
                            ports: [{ containerPort: config.port }]
                        }]
                    }
                }
            }
        };
    }

    deployment({ name: "api", image: "myapp:v1.2.0", replicas: 3, port: 8080 })
"#, None).unwrap();

let result = loop {
    match interp.step().unwrap() {
        StepResult::Continue => continue,
        StepResult::Complete(value) => {
            break js_value_to_json(value.value()).unwrap();
        }
        _ => panic!("Unexpected result"),
    }
};

assert_eq!(result["apiVersion"], "apps/v1");
assert_eq!(result["kind"], "Deployment");
assert_eq!(result["metadata"]["name"], "api");
assert_eq!(result["spec"]["replicas"], 3);

§Game Item Configuration

Define game items with enums and computed loot tables:

use tsrun::{Interpreter, StepResult, js_value_to_json};

let mut interp = Interpreter::new();
interp.prepare(r#"
    enum Rarity { Common, Rare, Epic, Legendary }

    interface Item {
        name: string;
        rarity: Rarity;
        basePrice: number;
        effects?: string[];
    }

    function createLootTable(items: Item[]) {
        return items.map(item => ({
            ...item,
            dropWeight: item.rarity === Rarity.Legendary ? 1 :
                        item.rarity === Rarity.Epic ? 5 :
                        item.rarity === Rarity.Rare ? 15 : 50,
            sellPrice: Math.floor(item.basePrice * (1 + item.rarity * 0.5))
        }));
    }

    createLootTable([
        { name: "Iron Sword", rarity: Rarity.Common, basePrice: 100 },
        { name: "Dragon Scale", rarity: Rarity.Legendary, basePrice: 5000,
          effects: ["Fire Resistance", "+50 Defense"] }
    ])
"#, None).unwrap();

let result = loop {
    match interp.step().unwrap() {
        StepResult::Continue => continue,
        StepResult::Complete(value) => {
            break js_value_to_json(value.value()).unwrap();
        }
        _ => panic!("Unexpected result"),
    }
};

// Common item: dropWeight=50, sellPrice=100*(1+0*0.5)=100
assert_eq!(result[0]["name"], "Iron Sword");
assert_eq!(result[0]["dropWeight"], 50);
assert_eq!(result[0]["sellPrice"], 100);

// Legendary item: dropWeight=1, sellPrice=5000*(1+3*0.5)=12500
assert_eq!(result[1]["name"], "Dragon Scale");
assert_eq!(result[1]["dropWeight"], 1);
assert_eq!(result[1]["sellPrice"], 12500);
assert_eq!(result[1]["effects"][0], "Fire Resistance");

§API Router Configuration

Configure REST endpoints with typed middleware and rate limits:

use tsrun::{Interpreter, StepResult, js_value_to_json};

let mut interp = Interpreter::new();
interp.prepare(r#"
    interface Route {
        method: "GET" | "POST" | "PUT" | "DELETE";
        path: string;
        handler: string;
        middleware?: string[];
        rateLimit?: { requests: number; window: string };
    }

    const routes: Route[] = [
        {
            method: "GET",
            path: "/users/:id",
            handler: "users::get",
            middleware: ["auth", "cache"]
        },
        {
            method: "POST",
            path: "/users",
            handler: "users::create",
            middleware: ["auth", "validate"],
            rateLimit: { requests: 10, window: "1m" }
        },
        {
            method: "DELETE",
            path: "/users/:id",
            handler: "users::delete",
            middleware: ["auth", "admin"]
        }
    ];

    routes
"#, None).unwrap();

let result = loop {
    match interp.step().unwrap() {
        StepResult::Continue => continue,
        StepResult::Complete(value) => {
            break js_value_to_json(value.value()).unwrap();
        }
        _ => panic!("Unexpected result"),
    }
};

assert_eq!(result.as_array().unwrap().len(), 3);
assert_eq!(result[0]["method"], "GET");
assert_eq!(result[0]["path"], "/users/:id");
assert_eq!(result[1]["rateLimit"]["requests"], 10);
assert_eq!(result[2]["middleware"][1], "admin");

§Build Tool Configuration

Create plugin-based build configurations like webpack or vite:

use tsrun::{Interpreter, StepResult, js_value_to_json};

let mut interp = Interpreter::new();
interp.prepare(r#"
    interface Plugin {
        name: string;
        options?: Record<string, any>;
    }

    interface BuildConfig {
        entry: string;
        output: { path: string; filename: string };
        plugins: Plugin[];
        minify: boolean;
    }

    const config: BuildConfig = {
        entry: "./src/index.ts",
        output: {
            path: "./dist",
            filename: "[name].[hash].js"
        },
        plugins: [
            { name: "typescript", options: { target: "ES2022" } },
            { name: "minify", options: { dropConsole: true } },
            { name: "bundle-analyzer" }
        ],
        minify: true
    };

    config
"#, None).unwrap();

let result = loop {
    match interp.step().unwrap() {
        StepResult::Continue => continue,
        StepResult::Complete(value) => {
            break js_value_to_json(value.value()).unwrap();
        }
        _ => panic!("Unexpected result"),
    }
};

assert_eq!(result["entry"], "./src/index.ts");
assert_eq!(result["output"]["path"], "./dist");
assert_eq!(result["plugins"].as_array().unwrap().len(), 3);
assert_eq!(result["plugins"][0]["name"], "typescript");
assert_eq!(result["plugins"][0]["options"]["target"], "ES2022");
assert_eq!(result["minify"], true);

§Validation Schema

Define form validation schemas with discriminated unions:

use tsrun::{Interpreter, StepResult, js_value_to_json};

let mut interp = Interpreter::new();
interp.prepare(r#"
    type Rule =
        | { type: "required" }
        | { type: "minLength"; value: number }
        | { type: "maxLength"; value: number }
        | { type: "pattern"; regex: string; message: string }
        | { type: "email" };

    interface FieldSchema {
        name: string;
        label: string;
        rules: Rule[];
    }

    const userSchema: FieldSchema[] = [
        {
            name: "email",
            label: "Email Address",
            rules: [
                { type: "required" },
                { type: "email" }
            ]
        },
        {
            name: "password",
            label: "Password",
            rules: [
                { type: "required" },
                { type: "minLength", value: 8 },
                { type: "pattern", regex: "[A-Z]", message: "Must contain uppercase" }
            ]
        }
    ];

    userSchema
"#, None).unwrap();

let result = loop {
    match interp.step().unwrap() {
        StepResult::Continue => continue,
        StepResult::Complete(value) => {
            break js_value_to_json(value.value()).unwrap();
        }
        _ => panic!("Unexpected result"),
    }
};

assert_eq!(result.as_array().unwrap().len(), 2);
assert_eq!(result[0]["name"], "email");
assert_eq!(result[0]["label"], "Email Address");
assert_eq!(result[0]["rules"][0]["type"], "required");
assert_eq!(result[1]["rules"][1]["type"], "minLength");
assert_eq!(result[1]["rules"][1]["value"], 8);
assert_eq!(result[1]["rules"][2]["message"], "Must contain uppercase");

Re-exports§

pub use error::JsError;
pub use gc::Gc;
pub use gc::GcStats;
pub use gc::Guard;
pub use gc::Heap;
pub use gc::Reset;
pub use string_dict::StringDict;
pub use value::CheapClone;
pub use value::EnvRef;
pub use value::Guarded;
pub use value::JsObject;
pub use value::JsString;
pub use value::JsValue;

Modules§

api
Public API for interacting with JavaScript values from Rust.
ast
Abstract Syntax Tree types for TypeScript
compiler
Bytecode compiler for TypeScript/JavaScript
error
Error types for the TypeScript interpreter.
gc
Mark-and-sweep garbage collection system.
parser
Generated parser for TypeScript.
platform
Platform abstraction traits for no_std compatibility.
string_dict
String dictionary for deduplicating JsString instances.
value
JavaScript value representation.

Structs§

ImportRequest
A pending import request with context about where it was requested from.
InternalModule
Definition of an internal module that can be imported from JavaScript.
Interpreter
The interpreter state
InterpreterConfig
Configuration for creating an Interpreter
ModulePath
A normalized, absolute module path.
NativeModuleBuilder
Builder for creating native internal modules
Order
An order is a request for an external effect. The payload is a RuntimeValue that the host interprets to perform side effects. The RuntimeValue keeps the payload alive until the order is fulfilled or dropped.
OrderId
Unique identifier for an order
OrderResponse
Response to fulfill an order from the host
RuntimeValue
A JS value with an attached guard that keeps it alive until dropped.

Enums§

InternalExport
Definition of an export from an internal module
InternalModuleKind
How an internal module is defined
StepResult
Result of executing a single step.

Functions§

create_eval_internal_module
Create the tsrun:host module
js_value_to_json
Convert a JsValue to JSON, with public API for external callers (without circular detection)
json_to_js_value_with_guard
Convert a serde_json value to a JsValue using a provided guard. The guard keeps any created objects alive until it is dropped. This is the preferred method when you need to control the lifetime of the result.
json_to_js_value_with_interp
Convert a serde_json value to a JsValue using the interpreter’s GC space

Type Aliases§

InternalFn
A native function that can be exported from an internal module