Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 51 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,61 @@
# tslua

Go reimplementation of [TypeScriptToLua](https://github.com/TypeScriptToLua/TypeScriptToLua) using [typescript-go](https://github.com/microsoft/typescript-go) internals.
TypeScript-to-Lua transpiler built on [typescript-go](https://github.com/microsoft/typescript-go). Single binary, no Node runtime required.

Work in progress. Passes a large portion of TSTL's test suite but not yet a drop-in replacement.
Built on the architecture and test suite of [TypeScriptToLua](https://github.com/TypeScriptToLua/TypeScriptToLua). For general TypeScript-to-Lua usage and caveats, see the [TSTL docs](https://typescripttolua.github.io/). Targets LuaJIT and Lua 5.0-5.5.

Targets: LuaJIT, Lua 5.0–5.5, universal.
[Docs](https://realcoldfry.github.io/tslua/) · [Playground](https://realcoldfry.github.io/tslua/playground/) · [CLI reference](https://realcoldfry.github.io/tslua/cli/overview/)

tslua ports from TSTL's architecture, transforms, and test suite.
## Install

## What's missing
```bash
npm i @tslua/cli
```

tslua doesn't yet cover everything TSTL does. Notable gaps:
## Try it

- **Plugins** - TSTL's `luaPlugins` hook system isn't ported. A Go binary can't load JS transformer plugins, so this needs a different approach. In practice, many TSTL plugins handle things like enum remapping, array proxy workarounds, and lualib patching. A native Go pipeline can address these differently since it transpiles lualib through the same path as user code. We're still figuring out what a plugin story looks like here.
- **Build modes** - `--buildMode library` not implemented.
- **Diagnostics** - not all TSTL diagnostics are implemented, and some differ due to using a different type checker.
```ts
// tslua eval -e
const items = [10, 20, 30];
for (const x of items) { print(x * 2) }

## Background reading
// output:
// items = {10, 20, 30}
// for ____, x in ipairs(items) do
// print(x * 2)
// end
```

## Why tslua

- **Native Go binary.** Uses typescript-go's type checker and AST directly via `go:linkname` shims, no IPC or JS runtime in the loop.
- **TSTL-compatible.** Ports TSTL's transforms and lualib faithfully. Reads the same `tsconfig.json` options and produces compatible output.
- **TS 7 ready.** Built on the compiler that TypeScript is migrating to.
- **Fast.** ~6-18ms incremental rebuilds in watch mode. [Benchmarks](https://realcoldfry.github.io/tslua/performance/)
- **Alternative class styles.** `tstl` (default, TSTL-compatible), `inline`, `luabind`, `middleclass`.

## Compatibility

Two verification approaches, both running TSTL's own tests:

- **[Jest harness](https://realcoldfry.github.io/tslua/testing/overview/#jest-harness).** TSTL's Jest suite runs unmodified, but with tslua's transpiler swapped in via a Unix socket server. **6071 / 6179 tests pass (98.3%).**
- **[Migrated Go tests](https://realcoldfry.github.io/tslua/testing/overview/#migrated-go-tests).** A migration system extracts TSTL's Jest specs and code-generates them into native Go tests. **5656 / 5903 cases migrated (95.8%)** across 70 of 71 spec files, with **100% behavioral pass rate** on migrated cases. The 247 unmigrated cases use TSTL assertion methods (`getMainLuaCodeChunk`, `getLuaExecutionResult`, etc.) not yet supported by the migration script.

## What's not done yet

- **Plugins.** TSTL's `luaPlugins` hook system. A Go binary can't load JS transformer plugins; the plugin story needs a different shape.
- **Build modes.** `--buildMode library` not implemented.
- **Diagnostics.** Not all TSTL diagnostics are ported, and some differ due to typescript-go's type checker.

## Building from source

Requires Go 1.24+, Node 20+, and [just](https://github.com/casey/just).

```bash
git clone https://github.com/RealColdFry/tslua
cd tslua
just setup
just build
./tslua eval -e 'print("hello")'
```

- [Progress on TypeScript 7 (December 2025)](https://devblogs.microsoft.com/typescript/progress-on-typescript-7-december-2025/) — TS7 won't support the existing JS API; third-party tools (including TSTL plugins) face a migration
- [typescript-go: API design direction](https://github.com/microsoft/typescript-go/discussions/455) — IPC-based programmatic API over msgpack, no stable Go API planned
- [typescript-go: Public Go API for embedding?](https://github.com/microsoft/typescript-go/discussions/481) — whether typescript-go will expose stable Go packages for in-process use
- [typescript-go: Transformer plugin / compiler API](https://github.com/microsoft/typescript-go/issues/516) — ecosystem asking what happens to compiler plugins (TSTL, typia, Angular) under TS7
- [typescript-go: API patterns for editor extensions](https://github.com/microsoft/typescript-go/issues/2824) — concrete IPC API design replacing TS Server plugins (Vue case study)
24 changes: 7 additions & 17 deletions npm/tslua/README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,18 @@
# tslua

TypeScript-to-Lua transpiler built on [typescript-go](https://github.com/microsoft/typescript-go).
TypeScript-to-Lua transpiler built on [typescript-go](https://github.com/microsoft/typescript-go). Single binary, no Node runtime required.

**Supported targets:** LuaJIT and Lua 5.0-5.5.

> [!WARNING]
> This is an early development build. Use at your own risk.
Built on the architecture and test suite of [TypeScriptToLua](https://github.com/TypeScriptToLua/TypeScriptToLua). For general TypeScript-to-Lua usage and caveats, see the [TSTL docs](https://typescripttolua.github.io/). Targets LuaJIT and Lua 5.0-5.5.

## Install

```bash
npm install @tslua/cli
```

## Usage

```bash
# Transpile a project
npx tslua -p tsconfig.json

# Transpile inline code
npx tslua eval -e 'const x: number = 1; print(x)'
```

## Binary downloads
## Links

Pre-built binaries are also available on [GitHub Releases](https://github.com/RealColdFry/tslua/releases).
- [Docs](https://realcoldfry.github.io/tslua/)
- [Playground](https://realcoldfry.github.io/tslua/playground/)
- [CLI reference](https://realcoldfry.github.io/tslua/cli/overview/)
- [GitHub](https://github.com/RealColdFry/tslua)
34 changes: 34 additions & 0 deletions scripts/migrate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# TSTL Test Migration

Extracts test cases from TSTL's Jest spec files and code-generates them into native Go tests under `internal/tstltest/`.

See the [testing docs](https://realcoldfry.github.io/tslua/testing/overview/#migrated-go-tests) for how this fits into tslua's overall test strategy.

## How it works

Each TSTL spec file is run in a sandboxed VM with mock `util.testExpression` / `util.testFunction` / `util.testModule` builders. Instead of executing the tests, these builders capture test structure (TypeScript source, options, expected values) and hand it off to the Go code generator.

## Pipeline

| File | Role |
| -------------- | ----------------------------------------------------------------------- |
| `cli.ts` | Entry point, spec discovery, cache pre-warming, `-c` check mode |
| `extract.ts` | Sandboxed spec execution, test case capture |
| `builder.ts` | Mock builder that records `.setOptions()`, `.ignoreDiagnostics()`, etc. |
| `evaluate.ts` | JS baking - transpiles TS to JS and runs it to get expected values |
| `tstl-ref.ts` | Runs code through TSTL to capture reference Lua for codegen comparison |
| `codegen.ts` | Emits Go test files with batch test cases |
| `constants.ts` | Overrides, skips, and target mappings |
| `types.ts` | `TestCase` type definition |
| `serialize.ts` | Go literal serialization |
| `migrate.ts` | Orchestrates extract + evaluate + codegen for a single spec |

## Usage

```bash
just migrate expressions # migrate a specific TSTL spec
just migrate-all # regenerate all migrated test files

# check migration coverage without generating files
node --require tsx/cjs scripts/migrate/cli.ts -c -a
```
8 changes: 5 additions & 3 deletions scripts/migrate/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,12 @@ function runCheck(specPaths: string[]): void {
cases = [];
extractionErrors = [{ name: "(crash)", error: e.message ?? String(e) }];
}
// Count "other" assertion cases as extraction failures they can't be migrated
// Count "other" assertion cases as extraction failures, they can't be migrated
for (const c of cases) {
if (c.assertion === "other") {
extractionErrors.push({
name: c.name,
error: `uses .${c.otherReason ?? "unknown"}() not migratable`,
error: `uses .${c.otherReason ?? "unknown"}(), not migratable`,
});
}
}
Expand Down Expand Up @@ -118,8 +118,10 @@ function runCheck(specPaths: string[]): void {
// Sort by count descending
const sorted = [...byCapability.entries()].toSorted((a, b) => b[1].count - a[1].count);

const totalFound = totalCases + totalErrors;
const pct = totalFound > 0 ? ((totalCases / totalFound) * 100).toFixed(1) : "0";
console.error(
`\n${totalSpecs} specs scanned, ${totalCases} cases extracted, ${totalErrors} extraction failures in ${specsWithErrors} specs\n`,
`\n${totalSpecs} specs scanned, ${totalCases} / ${totalFound} cases migrated (${pct}%), ${totalErrors} extraction failures in ${specsWithErrors} specs\n`,
);

if (sorted.length === 0) {
Expand Down
15 changes: 14 additions & 1 deletion website/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,22 @@ export default defineConfig({
},
{
label: "Configuration",
items: [{ label: "tsconfig.json", slug: "config/tsconfig" }],
},
{
label: "Customization",
items: [
{ label: "tsconfig.json", slug: "config/tsconfig" },
{ label: "Class Styles", slug: "config/class-style" },
{ label: "Emit Mode", slug: "config/emit-mode" },
{ label: "Export as Global", slug: "config/export-as-global" },
],
},
{
label: "Development",
items: [
{ label: "Testing", slug: "testing/overview" },
{ label: "Performance", slug: "performance" },
{ label: "Background", slug: "background" },
],
},
],
Expand Down
28 changes: 28 additions & 0 deletions website/src/content/docs/background.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
title: Background
description: Context on TypeScript 7, typescript-go, and the future of TypeScript-to-Lua tooling.
---

tslua exists because TypeScript is being rewritten in Go. This page collects the key discussions and decisions shaping that transition, particularly for tools that depend on TypeScript's compiler API.

## Approaches to TS7 integration

Tools that depend on TypeScript's compiler API have a few options for the TS7 transition:

**IPC API consumer.** Wait for typescript-go's planned msgpack-based IPC API, then call it from any language (JS, Go, Rust, etc.) as a subprocess. This is the officially supported path. The API surface is still being designed ([discussion #455](https://github.com/microsoft/typescript-go/discussions/455)), and it's unclear how much of the type checker and AST will be exposed; editor tooling is the priority, not compiler plugins. A transpiler like TSTL needs deep access to types, symbols, and the full AST, which may or may not be available over IPC.

**Fork typescript-go.** Fork the Go codebase and add transpilation directly. Full control, but a large maintenance surface to keep in sync with upstream.

**Direct linking via shims.** Link against typescript-go's internals without forking, using `go:linkname` to access unexported APIs. This is what tslua does.

### How tslua uses typescript-go

tslua takes the third approach. A [`gen_shims`](https://github.com/RealColdFry/tslua/tree/master/tools/gen_shims) tool (originally from [tsgolint](https://github.com/oxc-project/tsgolint), now maintained in-tree) generates `go:linkname` shims that expose typescript-go's type checker, AST, and program APIs as importable Go packages. tslua gets full access to the same data structures that typescript-go uses internally, at the cost of tracking upstream changes as typescript-go evolves. There is no IPC overhead or subprocess; tslua is a single binary with the type checker compiled in.

## Reading list

- [Progress on TypeScript 7 (December 2025)](https://devblogs.microsoft.com/typescript/progress-on-typescript-7-december-2025/). TS7 won't support the existing JS API; third-party tools (including TSTL plugins) face a migration.
- [typescript-go: API design direction](https://github.com/microsoft/typescript-go/discussions/455). IPC-based programmatic API over msgpack, no stable Go API planned.
- [typescript-go: Public Go API for embedding?](https://github.com/microsoft/typescript-go/discussions/481). Whether typescript-go will expose stable Go packages for in-process use.
- [typescript-go: Transformer plugin / compiler API](https://github.com/microsoft/typescript-go/issues/516). Ecosystem asking what happens to compiler plugins (TSTL, typia, Angular) under TS7.
- [typescript-go: API patterns for editor extensions](https://github.com/microsoft/typescript-go/issues/2824). Concrete IPC API design replacing TS Server plugins (Vue case study).
2 changes: 2 additions & 0 deletions website/src/content/docs/config/class-style.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ description: Configure how TypeScript classes are emitted to Lua.

TypeScript classes can be emitted using different Lua object system conventions. By default, tslua emits TSTL-compatible prototype chains. The `classStyle` option lets you target alternative object systems.

Available styles: [`tstl`](#tstl-default) (default), [`luabind`](#luabind), [`middleclass`](#middleclass), [`inline`](#inline).

## Quick start

Set `classStyle` in your tsconfig.json under the `tstl` key:
Expand Down
32 changes: 32 additions & 0 deletions website/src/content/docs/config/emit-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
title: Emit Mode
description: TSTL-compatible vs optimized Lua output.
---

tslua has two emit modes, controlled by the `emitMode` option in tsconfig.json or the `--emitMode` CLI flag.

```json
{
"tstl": {
"emitMode": "optimized"
}
}
```

## `"tstl"` (default)

Matches TSTL's Lua output as closely as possible. Use this when you need byte-for-byte compatibility with TSTL, or when comparing output between the two transpilers.

## `"optimized"`

Emits cleaner Lua where tslua can prove the result is semantically equivalent. Every optimization preserves identical runtime behavior; the Lua evaluates to the same result as the default mode.

This mode is a work in progress. Current optimizations:

| Area | Default (`tstl`) | Optimized | Notes |
| ---------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------- |
| `tostring()` in concat | Wraps all non-string operands including numeric vars | Skips wrapping for numeric types | Lua `..` handles numbers natively |
| C-style for loops | `while` loop with manual init/increment | `for i = start, limit` when pattern matches | Simpler, faster Lua |
| Map/Set for-of | Allocates intermediate tables | Zero-garbage stateless iteration via [custom lualib helpers](https://github.com/RealColdFry/tslua/blob/master/internal/lualib/patches.lua) | Less GC pressure |

More optimizations will be added over time. If you find a case where optimized mode changes runtime behavior, please [open an issue](https://github.com/RealColdFry/tslua/issues).
70 changes: 70 additions & 0 deletions website/src/content/docs/config/export-as-global.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
title: Export as Global
description: Strip module wrappers and emit exports as bare Lua globals.
---

By default, tslua wraps each module's exports in a `____exports` table and returns it, matching standard Lua module conventions. The `exportAsGlobal` option strips this wrapper and emits exported declarations as bare globals instead.

This is useful for embedded Lua environments where scripts run in a global scope rather than as `require()`-able modules.

## Usage

Set `exportAsGlobal` in tsconfig.json:

```json
{
"tstl": {
"exportAsGlobal": true
}
}
```

Or use the CLI flag:

```bash
tslua -p tsconfig.json --exportAsGlobal
```

## Example

Given this TypeScript:

```typescript
export const SPEED = 200;
export const GRAVITY = 9.8;
export const PLAYER_NAME = "hero";
```

**Default output** (module wrapper):

```lua
local ____exports = {}
____exports.SPEED = 200
____exports.GRAVITY = 9.8
____exports.PLAYER_NAME = "hero"
return ____exports
```

**With `exportAsGlobal: true`**:

```lua
SPEED = 200
GRAVITY = 9.8
PLAYER_NAME = "hero"
```

Exports become top-level declarations, accessible to the host environment without a module wrapper.

## Selective matching

Instead of a boolean, `exportAsGlobal` accepts a regex string to selectively apply to specific files:

```json
{
"tstl": {
"exportAsGlobal": "\\.script\\.ts$"
}
}
```

This applies export-as-global only to files matching the pattern (e.g. `game.script.ts`), while other files (e.g. `util.ts`) keep their module wrappers. Useful when some files are entry-point scripts and others are shared modules.
Loading