A Rust-based TypeScript runtime with compile-time macros, HTTP imports, and declaration-level bundling.
funee is a TypeScript runtime designed for functional programming. It bundles and executes your code's default export, providing:
- Compile-time macros — Transform code at bundle time, not runtime
- HTTP imports — Import directly from URLs (like Deno)
- Host imports (
host://) — Explicit host-provided capabilities - Declaration-level tree-shaking — Only include what's actually used
- Full runtime — HTTP server, fetch, filesystem, subprocess, timers
usingkeyword support — TypeScript 5.2+ explicit resource management- Watch mode — Re-run on file changes with closure-level tracking
# Build from source
cargo build --release
# Binary at target/release/funee# Install from GitHub Releases (recommended for stable runner binaries)
# 1) Download funee-vX.Y.Z-<target>.tar.gz from the Releases page
# 2) Extract and move bin/funee into your PATH
# 3) Keep funee-lib/ next to the binary's parent directory// hello.ts
import { log } from "host://console";
export default () => {
log("Hello, funee!");
};$ funee hello.ts
Hello, funee!# Run a TypeScript file (executes default export)
funee main.ts
# Emit bundled JavaScript without executing
funee --emit main.ts
# Bypass HTTP cache and fetch fresh
funee --reload main.ts
# Print runtime version
funee --version| Flag | Description |
|---|---|
--emit |
Print bundled JavaScript instead of executing |
--reload |
Bypass HTTP cache, fetch fresh from network |
--version |
Print funee version and exit |
Create web servers with automatic resource cleanup:
import { serve, createResponse, createJsonResponse } from "host://http/server";
export default async () => {
await using server = serve({
port: 3000,
handler: async (req) => {
if (req.url === "/api/hello") {
return createJsonResponse({ message: "Hello!" });
}
return createResponse("Not found", { status: 404 });
},
});
console.log(`Server running on port ${server.port}`);
// Server auto-shuts down when scope exits
};Web-standard fetch with full Response/Headers support:
import { fetch } from "host://http";
export default async () => {
const res = await fetch("https://api.github.com/users/octocat");
const data = await res.json();
return data.name;
};Transform code at bundle time with full AST access:
import { closure, Closure } from "funee";
import { log } from "host://console";
// closure macro captures the expression as AST
const add = closure((a: number, b: number) => a + b);
export default () => {
log(`Expression type: ${add.expression.type}`);
// "ArrowFunctionExpression"
log(`References: ${add.references.size}`);
// Captured scope references
};Built-in macros:
closure(expr)— Capture expression AST and scope referencescanonicalName(ref)— Get{ uri, name }for any referencedefinition(ref)— Get declaration AST and its references
Import modules directly from URLs with caching:
import { add } from "https://esm.sh/lodash-es@4.17.21/add";
export default () => add(1, 2);- Cached at
~/.funee/cache/ - Stale cache fallback on network failures
- Redirect handling
- Relative imports from HTTP modules work correctly
import { readFile, writeFile, isFile, readdir, tempDir } from "host://fs";
export default async () => {
// Disposable temp directory (auto-deletes on scope exit)
await using tmp = tempDir();
await writeFile(`${tmp.path}/data.txt`, "Hello!");
const content = await readFile(`${tmp.path}/data.txt`);
const files = await readdir(tmp.path);
const exists = await isFile(`${tmp.path}/data.txt`);
};import { spawn } from "host://process";
export default async () => {
// Simple form — run and capture output
const result = await spawn("echo", ["Hello, world!"]);
console.log(result.stdoutText()); // "Hello, world!\n"
console.log(result.status.code); // 0
// Advanced form — streaming with options
const proc = spawn({
cmd: ["cat"],
stdin: "piped",
stdout: "piped",
cwd: "/tmp",
env: { MY_VAR: "value" },
});
await proc.writeInput("Hello");
const output = await proc.output();
};import { setTimeout, clearTimeout, setInterval, clearInterval } from "host://time";
export default async () => {
// setTimeout with cancellation
const id = setTimeout(() => console.log("fired"), 1000);
clearTimeout(id);
// setInterval
let count = 0;
const intervalId = setInterval(() => {
console.log(++count);
if (count >= 3) clearInterval(intervalId);
}, 100);
};Re-run scenarios when referenced files change:
import { scenario, runScenariosWatch, closure, assertThat, is } from "funee";
import { log } from "host://console";
import { add } from "./math.ts";
const scenarios = [
scenario({
description: "add works correctly",
verify: closure(async () => {
await assertThat(add(2, 3), is(5));
}),
}),
];
export default async () => {
// Watches files referenced by closure macro
await runScenariosWatch(scenarios, { logger: log });
};import {
assertThat, is, not, both, contains, matches,
greaterThan, lessThan, scenario, runScenarios, closure
} from "funee";
import { log } from "host://console";
const scenarios = [
scenario({
description: "string assertions",
verify: closure(async () => {
await assertThat("hello world", contains("world"));
await assertThat("test@example.com", matches(/^[\w]+@[\w]+\.\w+$/));
}),
}),
scenario({
description: "numeric comparisons",
verify: closure(async () => {
await assertThat(10, greaterThan(5));
await assertThat(3, lessThan(10));
await assertThat(42, both(greaterThan(0), lessThan(100)));
}),
}),
];
export default async () => {
await runScenarios(scenarios, { logger: log });
};import { fromArray, toArray, map, filter, pipe } from "funee";
export default async () => {
const numbers = fromArray([1, 2, 3, 4, 5]);
const result = await pipe(
numbers,
filter((n) => n % 2 === 0),
map((n) => n * 10),
toArray
);
return result; // [20, 40]
};funee only includes declarations that are actually referenced:
// utils.ts
export const used = () => "included";
export const unused = () => "removed";
// main.ts
import { used } from "./utils.ts";
export default () => used();
// Output: only `used` appearsfunee uses a two-layer import system:
Host-provided runtime capabilities — things that require the native runtime:
// File system
import { readFile, writeFile, isFile, lstat, readdir, mkdir, tmpdir, tempDir } from "host://fs";
// HTTP client
import { fetch, httpGetJSON, httpPostJSON } from "host://http";
// HTTP server
import { serve, createResponse, createJsonResponse } from "host://http/server";
// Subprocess
import { spawn } from "host://process";
// Timers
import { setTimeout, clearTimeout, setInterval, clearInterval } from "host://time";
// File watching
import { watchFile, watchDirectory } from "host://watch";
// Crypto
import { randomBytes } from "host://crypto";
// Console
import { log, debug } from "host://console";Pure JavaScript/TypeScript library code — works anywhere:
import {
// Macros (compile-time)
Closure, CanonicalName, Definition, createMacro,
closure, canonicalName, definition,
// Assertions (pure JS)
assertThat, is, not, both, contains, matches,
greaterThan, lessThan, greaterThanOrEqual, lessThanOrEqual,
// Testing framework
scenario, runScenarios, runScenariosWatch,
// Streams (async iterables)
fromArray, toArray, map, filter, reduce, pipe,
fromString, toString, fromBuffer, toBuffer,
// Utilities
cryptoRandomString, someString, someDirectory,
join, // path joining is pure string manipulation
} from "funee";The host:// scheme makes host dependencies explicit:
- Clear contract — See exactly what runtime capabilities a module needs
- Portability — Pure
"funee"imports work in any JavaScript environment - Alternative runtimes — Browser, edge workers, etc. can provide different
host://implementations - Testing — Easy to mock
host://imports - Tree-shaking — Unused host modules aren't loaded
Moving from old-style imports to host://:
// ❌ Old way (deprecated)
import { readFile, writeFile } from "funee";
import { fetch } from "funee";
import { serve } from "funee";
import { spawn } from "funee";
import { log, debug } from "funee";
// ✅ New way
import { readFile, writeFile } from "host://fs";
import { fetch } from "host://http";
import { serve } from "host://http/server";
import { spawn } from "host://process";
import { log, debug } from "host://console";Quick reference:
| Old import | New import |
|---|---|
readFile, writeFile, isFile, readdir, mkdir, tmpdir, tempDir |
host://fs |
fetch, httpGetJSON, httpPostJSON |
host://http |
serve, createResponse, createJsonResponse |
host://http/server |
spawn |
host://process |
setTimeout, clearTimeout, setInterval, clearInterval |
host://time |
watchFile, watchDirectory |
host://watch |
randomBytes |
host://crypto |
log, debug |
host://console |
Built in Rust using:
- SWC — TypeScript parsing and code generation
- deno_core — JavaScript runtime (V8)
- hyper — HTTP server
- reqwest — HTTP client
- notify — File system watching
- petgraph — Dependency graph analysis
The functional-only design (no classes) enables aggressive optimizations and clean macro semantics.
# Run vitest tests
cd tests && npm test
# Run self-hosted tests (funee testing funee)
./scripts/prepare-sut.sh
./scripts/run-self-hosted.sh
# Optional: choose runner and SUT explicitly
FUNEE_RUNNER_BIN=/usr/local/bin/funee \
FUNEE_SUT_BIN=$PWD/target/sut/funee \
./scripts/run-self-hosted.sh
# Run Rust unit tests
cargo test
# Build release
cargo build --releaseRelease workflow details: docs/RELEASE_PROCESS.md.
- ✅ 163 vitest tests passing
- ✅ 140 self-hosted tests passing (funee testing funee)
- ✅ Macro system complete
- ✅ HTTP imports with caching
- ✅ HTTP server with async dispose
- ✅ Fetch API (web-standard)
- ✅ File system operations
- ✅ Subprocess spawning
- ✅ Watch mode with closure tracking
- ✅ TypeScript
usingkeyword support
MIT