Compiles Node.js HTTP apps — using the node:http API and the hono npm
library — to native macOS/Linux binaries with
geatsc, and checks the result against
Node.
The same TypeScript file runs under Node and compiles to a native binary. A raw-socket test battery compares the two byte-for-byte.
-
Full-stack Gea + Hono + MongoDB: the todo application compiles the Gea frontend, Hono, the MongoDB package's typed Gea entry, BSON, and the node-compat runtime into one native executable. Its deterministic HTTP test passes create, persisted read, update, persisted read, delete, and final absence. The native driver reuses a client-owned TCP pool and measured 1.07× the official Node.js driver on the tracked sequential CRUD benchmark. A 2.94-million-request HTTP stress run kept the native server at 22.4 MiB peak RSS versus Node's 296.8 MiB maximum observed sample. See the application documentation.
-
Correctness: 38/38 scenarios byte-identical to Node v24 (apps/http-parity) — request header parsing with Node's exact joining rules, content-length + chunked bodies (extensions, trailers, 1 MB payloads),
Expect: 100-continue, keep-alive + pipelining ordering, HTTP/1.0 close-delimited semantics, HEAD/1xx/204/304 framing, implicit Content-Length vswriteHead→chunked, streaming writes, async handlers, timers, statusMessage, set-cookie arrays, the header API surface, events, and Node-identical 400/431 error responses to malformed input. -
Throughput (8-core Xeon E3-1231 v3,
wrk, 2 rounds x 8 s, against Node v24.18.0, measured 2026-09-19):server GET /req/s/jsonreq/sp50 / p99 peak RSS gea, 1 worker 110,624 110,312 594 us / 1.11 ms 6 MB axum, 1 thread 119,798 126,280 549 us / 605 us 5 MB node, 1 worker 37,342 37,954 1.70 ms / 2.07 ms 91 MB gea, 8 workers 265,183 265,624 123 us / 4.30 ms 35 MB axum, 8 threads 219,927 223,387 214 us / 1.45 ms 6 MB node, 8 workers 124,762 124,638 370 us / 3.34 ms 685 MB Single-threaded, that is 92% of axum (a Rust framework doing the same full protocol flow) and 3.0x Node, using 6 MB against Node's 91 MB. Across eight workers it is 2.1x Node and ahead of axum. The eight-worker servers share CPUs with the load generator, so those rows are host-limited rather than a ceiling. Full measured history in BENCHMARKS.md.
The compiler depends on @geastack/node-compat and resolves its exported
build driver automatically when you run geatsc in a Node project, so an
application never needs a sibling source checkout.
The package ships plugin/, runtime/ and scripts/build.mjs; the example
applications and the benchmarks are not included. Its optional compiler peer
supplies the compiler API when the target is used, rather than installing a
second compiler as a runtime dependency.
In your own project, add the package and point the build driver at a server file. Node 22 or newer is required for TypeScript type stripping.
npm install @geastack/node-compat
npx geatsc-node build server.ts
./dist/server # → listening on http://127.0.0.1:3000To work on this repository instead, build the compiler first
(npm run build in a geastack/compiler checkout), then drive the bundled
applications directly:
npm --prefix apps/hono-hello install # hono, for the hono apps only
node scripts/build.mjs apps/raw-http-hello/server.ts
./apps/raw-http-hello/dist/server
# the Node-parity battery
node scripts/build.mjs apps/http-parity/server.ts
node apps/http-parity/driver.mjs apps/http-parity/dist/server apps/http-parity/server.ts
# → 36/36 parity, 0 diffsA library is compiled from its typed .ts source automatically, with no flag.
The compiler acquires the source for the exact installed version and caches it
under node_modules/.cache/geatsc/sources, restoring the generics tsc
erased so the library's hot path monomorphizes to fully typed C++ (see
docs/ARCHITECTURE.md).
--debugbuilds at-O0 -gfor lldb. Optimized builds are stripped and link-time optimized, at-Osunderclang++(theCXXdefault) and-O2under g++, because the right level depends on the compiler. Measured on the bench host (clang 18, four pinned workers, compiler 1.0.17):-Os -fltois level with-O2 -fltoon the raw HTTP server (307-320k vs 304-308k req/s, inside the round-to-round spread) and 5-7% faster on the compiled Hono app (150-152k vs 141-144k), at binaries about a third smaller (1.05 MB / 5.04 MB against 1.64 MB / 7.27 MB) and less memory — Hono runs more instructions at-Osbut fewer cycles, so its hot path is instruction-cache bound.-O3gives no gain on either compiler. g++ 13 is the other way round (-Oscosts 25%) and is 4-10% behind clang on the same emitted source, so the published numbers are clang's.GEA_OPT_LEVELoverrides the level,GEA_LTO=0drops the LTO.- Binaries link with hidden visibility and dead-stripping
(
-fvisibility=hidden,-Wl,-dead_strip/--gc-sections). OpenSSL is linked only when the program reachesnode:crypto; a server that never hashes anything maps nolibcrypto(measured: 0.45 MB of PSS per process).
Every figure above and its history is in BENCHMARKS.md.
- runtime/gea_node.cpp — the native layer: a single-threaded reactor (epoll on Linux, poll fallback), a full HTTP/1.x request parser, per-connection response streaming, reactor-integrated timers, and a size-class pool allocator (Linux).
- runtime/node/ —
node:httpandnode:eventswritten in TypeScript and compiled by geatsc itself, sitting on the reactor through a small intrinsic surface. Response serialization matches Node byte-for-byte; hot-path listener storage is fully typed (nativestd::functionslots — no dynamic-value boxing). - apps/http-parity/ — the parity test: one app file, two runtimes, byte-diffed responses.
- apps/cluster-hello/ — the
node:clusterparity probe: the same file forks workers under Node and natively; workers that die are replaced, workers told to leave are not, SIGTERM to the primary leaves no orphans. Output matches Node's run line for line. - apps/raw-http-hello/ — the benchmark app, plus
the Rust comparison servers (raw hyper and full-flow axum, single- and
multi-threaded) under
rust-server/. - apps/hono-hello/ — Hono's full default router stack
served natively over the
node:httpbridge; the build compiles Hono from its typed source, so its hot path is monomorphized with no boxed values. Its parity test covers JSON and multipart POST bodies as well as GET routing. - apps/hono-mongodb-todo/ — the full-stack Gea frontend, Hono server, and native MongoDB application, including driver architecture, validation, pool behavior, and Node/Rust/C++ benchmark docs.
- docs/ARCHITECTURE.md — how the pieces fit, the dispatch contract, and the performance engineering notes.
- BENCHMARKS.md — the full measured history, oldest to newest, including negative results.
These are intentional. The header of runtime/node/http.ts documents each one.
- A throwing request handler is isolated to a 500 + connection close. Node
kills the process on the same error (
uncaughtException). - Request/response listeners compile to native closures with no JS function
identity, so
removeListener(name, fn)on req/res cannot match a specific listener — useremoveAllListeners(name). - Request
set-cookieduplicates join with", "instead of becoming an array (req.headersstays a string→string map);getHeaderreturns multi-value headers joined with", ". writeHead(status, statusMessage, headers)3-arg form is unsupported — setres.statusMessageinstead.- Bodies are byte-preserving strings, not Buffers; chunk trailers are parsed
and discarded;
res.reqis not provided. node:cluster(runtime/node/cluster.ts) forks worker processes (a re-exec of the binary;isPrimary/isWorker,fork(env),worker.id/process.pid,kill/disconnect,'online','exit'with Node's(code, signal)null encoding,exitedAfterDisconnect,cluster.disconnect()) and connections are distributed by the kernel through SO_REUSEPORT listeners — Node'sSCHED_NONE;schedulingPolicyis accepted and ignored. There is no IPC channel:worker.send/process.sendthrow,'message'never fires,'listening'is not emitted, and'online'is emitted by the primary right after the fork. A worker exits when the primary dies (a liveness pipe closes), like Node's'disconnect'.process.pid,process.ppid,process.exit(code),os.availableParallelism()andos.cpus()(count-only entries) are provided for the cluster shape.
node:httpserver surface: complete; the parity battery passes (see above).node:cluster: forking, replacement, signals and orderly shutdown proven natively against Node's output withapps/cluster-hello(0 refusals; the 66 boxed carriers are EventEmitter's deliberateany[]argument boundary).- hono: the full default router stack compiles from typed source and serves
GET, JSON POST, URL-encoded forms and multipart forms with binary
Fileparts. Routes, the 404/compose middleware path, status codes, and response headers (application/json, hono's owncharset=UTF-8) are correct. The build's router is monomorphized (SmartRouter,RegExpRouterandTrieRouter, withstd::tupleroute storage and no boxed handlers at rest). - MongoDB todo: compiles and runs against a local MongoDB server through BSON OP_MSG and a reusable native TCP pool. The supported Gea entry covers the app's direct-host CRUD surface; authentication, TLS, cluster topology, retry layers, and multi-batch cursors remain outside that surface.
- Compiler-feature regression apps: apps/tuple-test (tuple monomorphization + the hono router/Result/entries shapes, byte- diffed vs node) and apps/mapkeys-test (Object.keys/values/entries over dictionary storages).
- fastify: blocked on about 20 emitter codegen bugs.
new Functionis supported: a bounded runtime evaluator covers find-my-way's generated code (seeapps/newfn-test).
Apache-2.0 (see LICENSE). You can ship closed-source products
built on it. The only GeaStack code under a different license is
the embedded board support (targets and @geastack/chips, GPL-3.0-only):
shipping closed-source firmware through those needs a commercial license.
Contact contact@geastack.com for commercial terms, support and hosted builds.