https://github.com/yowainwright/tqs
quick scripts for typescript
https://github.com/yowainwright/tqs
Last synced: 2 months ago
JSON representation
quick scripts for typescript
- Host: GitHub
- URL: https://github.com/yowainwright/tqs
- Owner: yowainwright
- License: mit
- Created: 2026-03-13T04:28:03.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-04-09T07:25:56.000Z (3 months ago)
- Last Synced: 2026-04-09T09:24:07.719Z (3 months ago)
- Language: TypeScript
- Size: 616 KB
- Stars: 2
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# tqs
[](https://github.com/yowainwright/tqs/actions/workflows/ci.yml)
[](https://opensource.org/licenses/MIT)
> Compile TypeScript to very fast and very small standalone native binaries via QuickJS.
```bash
tqs my-script.ts # outputs ./my-script — a standalone native binary
```
Write TypeScript. Get a tiny self-contained binary with [QuickJS-NG](https://github.com/quickjs-ng/quickjs) embedded and `maybefetch()` for HTTP. No Node.js, no V8, no runtime dependencies.
## Why tqs?
Great for typed, tested scripts that start fast and run fast — think LLM hooks.
- **Native binaries**: `tqs my-script.ts` compiles to a standalone executable — ship it anywhere
- **Built-in HTTP**: `maybefetch()` provides fetch with retry, backoff, and timeout — zero dependencies; synchronous but fast
- **Small by default**: ~1MB binary with no build flags or tuning — smaller than a stripped Go or Rust binary with HTTP
- **Fast startup**: <1ms cold start vs ~40ms for Node.js
- **Type-safe**: Full TypeScript support with types for `qjs:std`, `qjs:os`, and `maybefetch`
## Installation
**macOS**
```bash
brew install yowainwright/tap/tqs
```
**Linux**
```bash
apt install libcurl4 cmake
git clone https://github.com/yowainwright/tqs.git
cd tqs && bun install && bun run build
```
Requires `libcurl` (`apt install libcurl4` on Linux, pre-installed on macOS).
## Quick Start
```typescript
// @tqs-script
import * as std from 'qjs:std';
import * as os from 'qjs:os';
import { maybeFetch } from 'tqs';
const cwd = os.getcwd();
std.out.puts(`Running from: ${cwd}\n`);
const data = maybeFetch('https://httpbin.org/json');
if (data) {
std.out.puts(`Response: ${data}\n`);
}
std.exit(0);
```
Compile and run:
```bash
tqs my-script.ts # creates ./my-script
./my-script
```
## How it works
```
my-script.ts
→ bun build (bundles TypeScript to self-contained JS)
→ qjsc (compiles JS + QuickJS runtime + maybefetch into a native binary)
→ ./my-script
```
The output binary embeds the QuickJS runtime and all JavaScript source inline — no external files needed at runtime.
## CLI
```bash
tqs # Compile TypeScript or JavaScript to a native binary
tqs --help # Show help
tqs --version # Show version
```
Supported inputs: `.tqs`, `.js`. For `.ts` files, add `// @tqs-script` at the top to mark them as QuickJS scripts:
```typescript
// @tqs-script
import * as std from 'qjs:std';
// ...
```
## TypeScript Types
Add this import to get types for `qjs:std`, `qjs:os`, `maybefetch`, and QuickJS globals in your scripts:
```typescript
// @tqs-script
import 'tqs/quickjs';
import * as std from 'qjs:std';
import * as os from 'qjs:os';
```
## CLI Arguments
Scripts receive arguments via `scriptArgs`. The first element is the script path:
```typescript
// @tqs-script
import 'tqs/quickjs';
const [, , url] = scriptArgs;
```
```bash
tqs my-script.ts https://example.com/api
```
## QuickJS Modules
Use `qjs:std` and `qjs:os` in your scripts. These are available at runtime inside the compiled binary.
### `qjs:std`
| Method | Signature | Description |
|--------|-----------|-------------|
| `exit` | `(code: number) => void` | Exit with status code |
| `getenv` | `(name: string) => string \| null` | Read environment variable |
| `evalScript` | `(source: string) => unknown` | Evaluate JavaScript |
| `loadScript` | `(filename: string) => unknown` | Load and evaluate file |
| `printf` | `(format: string, ...args) => void` | Formatted print to stdout |
| `sprintf` | `(format: string, ...args) => string` | Formatted string |
| `in.getline` | `() => string \| null` | Read line from stdin |
| `out.puts` | `(str: string) => void` | Write to stdout |
| `err.puts` | `(str: string) => void` | Write to stderr |
### `qjs:os`
| Method | Signature | Description |
|--------|-----------|-------------|
| `getcwd` | `() => string` | Current working directory |
| `chdir` | `(path: string) => number` | Change directory |
| `mkdir` | `(path: string, mode?) => number` | Create directory |
| `readdir` | `(path: string) => string[]` | List directory contents |
| `realpath` | `(path: string) => string` | Resolve path |
| `stat` | `(path: string) => StatResult` | File information |
| `exec` | `(args: string[], options?) => number` | Execute command |
| `unlink` | `(path: string) => number` | Delete file |
| `rename` | `(old: string, new: string) => number` | Rename file |
| `open` | `(filename: string, flags: string, mode?) => number` | Open file descriptor |
| `read` | `(fd, buffer, offset, length) => number` | Read from fd |
| `write` | `(fd, buffer, offset, length) => number` | Write to fd |
| `close` | `(fd: number) => number` | Close file descriptor |
| `sleep` | `(ms: number) => void` | Sleep milliseconds |
| `platform` | `string` | Current platform |
## maybefetch
Synchronous HTTP GET with exponential backoff retry. Available as a global in all compiled scripts — not async, blocks until complete or all retries are exhausted.
```typescript
import { maybeFetch } from 'tqs';
const body = maybeFetch('https://example.com/api');
if (body) {
std.out.puts(body);
}
```
Override specific defaults with `defaultConfig`:
```typescript
import { maybeFetch, defaultConfig } from 'tqs';
const body = maybeFetch('https://example.com/api', { ...defaultConfig, maxRetries: 5 });
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `url` | `string` | URL to fetch |
| `maxRetries` | `number` | Maximum retry attempts |
| `initialDelayMs` | `number` | Delay before first retry (ms) |
| `maxDelayMs` | `number` | Maximum delay between retries (ms) |
| `backoffFactor` | `number` | Multiplier applied to delay each retry |
| `timeoutMs` | `number` | Request timeout (ms) |
Returns `string` on success (2xx response body), `null` on failure after all retries.
## TypeScript API
`tqs` exports typed wrappers around the `maybefetch` global for use in compiled scripts:
```typescript
import { maybeFetch, defaultConfig } from 'tqs';
import type { FetchConfig } from 'tqs';
```
### `defaultConfig`
| Property | Value |
|----------|-------|
| `maxRetries` | `3` |
| `initialDelayMs` | `1000` |
| `maxDelayMs` | `30000` |
| `backoffFactor` | `2.0` |
| `timeoutMs` | `10000` |
### `maybeFetch(url, config?): string | null`
Typed wrapper around the `maybefetch` global. Only available in compiled QuickJS binaries.
## Resource Usage
| Metric | tqs | Node.js 25 |
|--------|-----|------------|
| Startup | <1ms | ~40ms |
| Memory (hello world) | ~2MB | ~15MB |
| Memory (HTTP fetch) | ~4.5MB | ~27.5MB |
| Binary size | ~1MB | ~60MB |
*Measured on Apple M4, macOS 15.*
## Development
```bash
bun install
bun run build:quickjs # Build QuickJS-NG + maybefetch + tqs binary
bun run build:ts # Build TypeScript
bun run lint # Lint
bun run typecheck # Type check
bun test # Run tests
```
## Comparison
### vs JS/TS runtimes
| Tool | Binary Size | Startup | Approach |
|---|---|---|---|
| **tqs** | ~1 MB | <1ms | QuickJS native bytecode |
| Bun compile | ~21–36 MB | ~5–10ms | JSC runtime embedded |
| Deno compile | ~60–100 MB | ~30–60ms | V8 runtime embedded |
| Node.js SEA | ~60 MB | ~40ms | V8 (Node) embedded |
### vs compiled languages
| Tool | Binary Size | Startup | Language |
|---|---|---|---|
| **tqs** | ~1 MB | <1ms | TypeScript |
| Rust (stripped, with HTTP) | ~2–3 MB | <1ms | Rust |
| Go (stripped, with HTTP) | ~5–7 MB | <1ms | Go |
tqs is competitive on binary size and startup with native compiled languages — the tradeoff is no async, no npm ecosystem, and a subset of JS APIs.
### Honest tradeoffs
**tqs is the right tool when:**
- Binary size and cold-start matter (LLM hooks, git hooks, CI steps, edge deployments)
- Your script is a synchronous pipeline: read input, call an API, write output
- You want fast compile times and a small distributable without tuning a Go or Rust build
- You want to write TypeScript, not Go or Rust
**tqs is not the right tool when:**
- You need multi-threading or concurrency
- You need the npm ecosystem or async I/O
- Your script uses Node.js built-ins (`fs`, `path`, `http`, etc.)
- You need error handling beyond synchronous retries
---
## License
MIT — See [LICENSE](LICENSE)