Project Structure

A RustScript project compiles in-place: generated .rs files live alongside your .rts source files.

Layout

my-project/
  rustscript.json     # Project manifest
  Cargo.toml          # Generated by compiler, commit this
  Cargo.lock          # Generated by Cargo, commit this
  .gitignore          # Covers /src/*.rs and /target
  src/
    main.rts          # Entry point

After building, .rs files appear next to your .rts files:

my-project/
  rustscript.json
  Cargo.toml
  Cargo.lock
  .gitignore
  src/
    main.rts
    main.rs           # Generated -- gitignored, don't edit
  target/             # Cargo build artifacts (gitignored)

rustscript.json

The project manifest. Created by rustscript init, lives in the project root:

{
  "name": "my-project",
  "edition": "2024"
}

You can also declare explicit dependencies with pinned versions:

{
  "name": "my-project",
  "edition": "2024",
  "dependencies": {
    "clap": { "version": "4", "features": ["derive"] }
  }
}

Fields:

| Field | Required | Description | |-------|----------|-------------| | name | Yes | Package name (used in Cargo.toml) | | version | No | Project version, defaults to "0.1.0" | | edition | No | Rust edition, defaults to "2024" | | dependencies | No | Explicit dependency versions and features | | devDependencies | No | Dev-only dependencies |

Cargo.toml

The compiler generates Cargo.toml in the project root and keeps it up to date. It is a standard Cargo manifest — commit it to git.

The compiler merges rather than overwrites: if you edit Cargo.toml directly (to pin a version, add a feature flag, or add a section), the compiler preserves your edits on the next build.

Example generated Cargo.toml:

[package]
name = "my-project"
edition = "2024"

[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

The [dependencies] section is rebuilt from your imports every time you compile. Other sections you add are preserved.

src/main.rts

Your entry point. Must contain a function main() (or async function main() for async programs).

function main() {
  console.log("Hello!");
}

You can organize code into multiple .rts files, but main.rts is where execution starts.

.gitignore

rustscript init generates a .gitignore that covers the right files:

/src/*.rs
/target

Generated .rs files are intentionally gitignored — they're build artifacts derived from your .rts source. The generated Cargo.toml and Cargo.lock are committed.

Templates

rustscript init supports templates for common project types:

rustscript init my-api --template web-server    # axum web server scaffold
rustscript init my-cli --template cli           # CLI tool scaffold
rustscript init my-wasm --template wasm         # WASM project scaffold

The compilation pipeline

.rts  -->  Parse  -->  AST  -->  Type Check  -->  Lower  -->  Rust IR  -->  Emit  -->  .rs  -->  cargo build  -->  binary

Each stage:

  1. Parse -- Lexer + recursive descent parser turns .rts into a RustScript AST
  2. Type Check -- Resolves types, checks assignments, infers where possible
  3. Lower -- Transforms the RustScript AST to Rust IR. This is where TypeScript patterns become Rust patterns: Array<T> becomes Vec<T>, console.log becomes println!, ownership is inferred
  4. Emit -- Rust IR becomes .rs text, written in-place next to source files
  5. Cargo -- Standard cargo build compiles the generated Rust to a native binary

Ejecting to pure Rust

Because the generated Rust is idiomatic and human-readable, you can eject at any time:

rustscript eject

This converts the project to a standard Rust project: removes rustscript.json, unstages the generated .rs files from .gitignore, and leaves you with a pure Rust codebase. No lock-in.