Skip to content

Commit 39b539d

Browse files
authored
Merge pull request #68 from justrach/feat/turboapi-core-integration
feat: integrate turboapi-core shared Zig library (v0.2.1)
2 parents 3c3ba7a + 65ad851 commit 39b539d

10 files changed

Lines changed: 1565 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ All notable changes to merjs will be documented in this file.
44

55
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [0.2.1] — 2026-03-28
8+
9+
### Added
10+
- **turboapi-core dependency** — merjs now imports [turboapi-core](https://github.com/justrach/turboAPI/tree/main/turboapi-core), a shared Zig library providing a radix trie router, HTTP utilities (`percentDecode`, `queryStringGet`, `statusText`, `formatHttpDate`), and a bounded response cache. This is the first step toward sharing routing primitives between merjs and turboAPI. See [#66](https://github.com/justrach/merjs/issues/66) for the full integration roadmap.
11+
12+
### Next (tracked in #66)
13+
- Method-based API routing via turboapi-core's radix trie (GET vs POST on same path)
14+
- Replace `queryParamFromStr` with turboapi-core's `queryStringGet`
15+
- Optional: radix trie for dynamic page routes (perf upgrade for large route counts)
16+
17+
---
18+
719
## [Unreleased]
820

921
### Added

MerApp.app/Contents/MacOS/merapp

4.98 MB
Binary file not shown.

build.zig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ pub fn build(b: *std.Build) void {
2626
mer_mod.addImport("dhi_model", dhi_model_mod);
2727
mer_mod.addImport("dhi_validator", dhi_validator_mod);
2828

29+
// ── turboapi-core (shared router + HTTP utilities) ──
30+
const core_dep = b.dependency("turboapi_core", .{});
31+
const core_mod = core_dep.module("turboapi-core");
32+
mer_mod.addImport("turboapi-core", core_mod);
33+
2934
// ── Demo site (examples/site) ───────────────────────────────────────────
3035
const counter_config_mod = b.addModule("counter_config", .{
3136
.root_source_file = b.path("examples/site/wasm/counter_config.zig"),

build.zig.zon

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
.{
22
.name = .merjs,
33
.fingerprint = 0xaa135c45924bbfa8,
4-
.version = "0.2.0",
4+
.version = "0.2.1",
55
.minimum_zig_version = "0.15.1",
66
.dependencies = .{
77
.dhi = .{
@@ -12,6 +12,10 @@
1212
.url = "git+https://github.com/justrach/kuri.git#81baa2ffb060f87d2e8f6de2aa69eb509c700b91",
1313
.hash = "agentic_browdie-0.1.0-uVYLKYovCADlIJw6TDdL9NnVr-LAtCLt5Sz4o1e8l6Y2",
1414
},
15+
.turboapi_core = .{
16+
.url = "git+https://github.com/justrach/turboapi-core.git#6c4217b8d0cdc297f5b827527cdc9237213b14f1",
17+
.hash = "turboapi_core-0.1.0-DjdHgu19AABfWBxcHyuAg51BxbZ7jmtlJ5VRt9GTURAP",
18+
},
1519
},
1620
.paths = .{
1721
"build.zig",

examples/site/app/desktop.zig

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
const mer = @import("mer");
2+
3+
pub const meta: mer.Meta = .{
4+
.title = "Desktop — merjs as a native macOS app",
5+
.description = "No Electron. No Tauri. One 5.3MB Zig binary. Native AppKit + WKWebView.",
6+
.og_title = "merjs Desktop — Native macOS App in Zig",
7+
};
8+
9+
pub fn render(req: mer.Request) mer.Response {
10+
_ = req;
11+
return mer.html(html);
12+
}
13+
14+
const html =
15+
\\<!DOCTYPE html>
16+
\\<html lang="en">
17+
\\<head>
18+
\\ <meta charset="UTF-8">
19+
\\ <meta name="viewport" content="width=device-width, initial-scale=1.0">
20+
\\ <title>Desktop — merjs</title>
21+
\\ <link rel="preconnect" href="https://fonts.googleapis.com">
22+
\\ <link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Sans:wght@400;500;600&display=swap" rel="stylesheet">
23+
\\ <style>
24+
\\ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
25+
\\ :root { --bg:#f0ebe3; --bg2:#e8e2d9; --bg3:#ddd5cc; --text:#252530; --muted:#8a7f78; --border:#d5cdc4; --red:#e8251f; }
26+
\\ body { background:var(--bg); color:var(--text); font-family:'DM Sans',system-ui,sans-serif; min-height:100vh; }
27+
\\ a { color:inherit; text-decoration:none; }
28+
\\ .page { max-width:680px; margin:0 auto; padding:48px 32px 96px; }
29+
\\ .header { display:flex; align-items:center; justify-content:space-between; margin-bottom:56px; }
30+
\\ .wordmark { font-family:'DM Serif Display',Georgia,serif; font-size:18px; letter-spacing:-0.02em; }
31+
\\ .wordmark span { color:var(--red); }
32+
\\ .back { font-size:13px; color:var(--muted); transition:color 0.15s; }
33+
\\ .back:hover { color:var(--text); }
34+
\\ h1 { font-family:'DM Serif Display',Georgia,serif; font-size:38px; letter-spacing:-0.02em; line-height:1.1; margin-bottom:16px; }
35+
\\ .subtitle { font-size:15px; color:var(--muted); line-height:1.6; margin-bottom:40px; }
36+
\\ h2 { font-family:'DM Serif Display',Georgia,serif; font-size:22px; letter-spacing:-0.01em; color:var(--text); margin:40px 0 14px; }
37+
\\ p { font-size:15px; color:var(--muted); line-height:1.75; margin-bottom:16px; }
38+
\\ p strong { color:var(--text); font-weight:500; }
39+
\\ code { font-family:'SF Mono','Fira Code',monospace; font-size:13px; background:var(--bg3); border-radius:4px; padding:1px 6px; color:var(--text); }
40+
\\ pre { background:var(--bg2); border:1px solid var(--border); border-radius:8px; padding:16px; overflow-x:auto; font-family:'SF Mono','Fira Code',monospace; font-size:13px; color:var(--text); margin:16px 0; line-height:1.6; }
41+
\\ .rule { border:none; border-top:1px solid var(--border); margin:40px 0; }
42+
\\ .stats { display:grid; grid-template-columns:repeat(3,1fr); gap:12px; margin:24px 0 40px; }
43+
\\ .stat { text-align:center; background:var(--bg2); border:1px solid var(--border); border-radius:8px; padding:20px 12px; }
44+
\\ .stat-num { font-family:'DM Serif Display',Georgia,serif; font-size:28px; color:var(--red); }
45+
\\ .stat-label { font-size:12px; color:var(--muted); margin-top:4px; }
46+
\\ .targets { display:flex; flex-direction:column; gap:8px; margin:16px 0; }
47+
\\ .target { display:flex; align-items:center; gap:16px; background:var(--bg2); border:1px solid var(--border); border-radius:8px; padding:12px 16px; }
48+
\\ .target-cmd { font-family:'SF Mono','Fira Code',monospace; font-size:13px; color:var(--red); min-width:160px; }
49+
\\ .target-desc { font-size:14px; color:var(--muted); }
50+
\\ .links { display:flex; gap:12px; margin-top:40px; flex-wrap:wrap; }
51+
\\ .btn { display:inline-flex; align-items:center; font-size:14px; font-weight:500; padding:11px 22px; border-radius:6px; transition:opacity 0.15s; }
52+
\\ .btn-red { background:var(--red); color:var(--bg); }
53+
\\ .btn-red:hover { opacity:0.88; }
54+
\\ .btn-outline { border:1px solid var(--border); color:var(--muted); }
55+
\\ .btn-outline:hover { color:var(--text); border-color:var(--text); }
56+
\\ .compare { display:flex; flex-direction:column; gap:8px; margin:16px 0; }
57+
\\ .compare-row { display:grid; grid-template-columns:140px 100px 1fr; gap:12px; align-items:center; padding:10px 0; border-bottom:1px solid var(--border); font-size:14px; }
58+
\\ .compare-row:last-child { border-bottom:none; }
59+
\\ .compare-name { font-weight:500; color:var(--text); }
60+
\\ .compare-size { font-family:'SF Mono',monospace; font-size:13px; }
61+
\\ .compare-note { color:var(--muted); font-size:13px; }
62+
\\ .highlight { color:var(--red); font-weight:600; }
63+
\\ </style>
64+
\\</head>
65+
\\<body>
66+
\\<div class="page">
67+
\\ <header class="header">
68+
\\ <a href="/" class="wordmark">mer<span>js</span></a>
69+
\\ <a href="/" class="back">&larr; home</a>
70+
\\ </header>
71+
\\
72+
\\ <h1>desktop.zig</h1>
73+
\\ <p class="subtitle">
74+
\\ Native macOS app. No Electron. No Tauri. No Node.js.<br>
75+
\\ One Zig binary. AppKit + WKWebView. Instant launch.
76+
\\ </p>
77+
\\
78+
\\ <div class="stats">
79+
\\ <div class="stat"><div class="stat-num">5.3MB</div><div class="stat-label">binary size</div></div>
80+
\\ <div class="stat"><div class="stat-num">0ms</div><div class="stat-label">startup overhead</div></div>
81+
\\ <div class="stat"><div class="stat-num">171</div><div class="stat-label">lines of Zig</div></div>
82+
\\ </div>
83+
\\
84+
\\ <hr class="rule">
85+
\\ <h2>How it works</h2>
86+
\\ <p>
87+
\\ The app spawns the merjs HTTP server on a <strong>background thread</strong>,
88+
\\ waits for it to bind a random port, then opens a native
89+
\\ <strong>NSWindow</strong> with <strong>WKWebView</strong> pointed at localhost.
90+
\\ Same server, same routes, same SSR &mdash; just wrapped in a native window.
91+
\\ </p>
92+
\\ <pre>main thread: NSApplication + NSWindow + WKWebView
93+
\\bg thread: merjs HTTP server (:random-port)
94+
\\protocol: plain HTTP (no IPC, no bridge)</pre>
95+
\\
96+
\\ <hr class="rule">
97+
\\ <h2>The ObjC bridge</h2>
98+
\\ <p>
99+
\\ No <code>@cImport</code>. No headers. No binding generators.
100+
\\ Three <code>extern fn</code> declarations give Zig full access to AppKit and WebKit:
101+
\\ </p>
102+
\\ <pre>extern fn objc_getClass([*:0]const u8) ?*anyopaque;
103+
\\extern fn sel_registerName([*:0]const u8) *anyopaque;
104+
\\extern fn objc_msgSend() void;</pre>
105+
\\ <p>
106+
\\ Cast <code>objc_msgSend</code> per call site.
107+
\\ Zig's comptime handles the rest. No Swift. No Objective-C files.
108+
\\ </p>
109+
\\
110+
\\ <hr class="rule">
111+
\\ <h2>vs Electron</h2>
112+
\\ <div class="compare">
113+
\\ <div class="compare-row">
114+
\\ <div class="compare-name">Electron</div>
115+
\\ <div class="compare-size">~200MB</div>
116+
\\ <div class="compare-note">Chromium + Node.js, ~300ms launch</div>
117+
\\ </div>
118+
\\ <div class="compare-row">
119+
\\ <div class="compare-name">Tauri</div>
120+
\\ <div class="compare-size">~10MB</div>
121+
\\ <div class="compare-note">System webview, still needs JS runtime</div>
122+
\\ </div>
123+
\\ <div class="compare-row">
124+
\\ <div class="compare-name highlight">merjs</div>
125+
\\ <div class="compare-size highlight">5.3MB</div>
126+
\\ <div class="compare-note">Native binary, zero runtime, instant</div>
127+
\\ </div>
128+
\\ </div>
129+
\\
130+
\\ <hr class="rule">
131+
\\ <h2>One codebase, five targets</h2>
132+
\\ <div class="targets">
133+
\\ <div class="target"><div class="target-cmd">zig build serve</div><div class="target-desc">Native HTTP server</div></div>
134+
\\ <div class="target"><div class="target-cmd">zig build worker</div><div class="target-desc">Cloudflare Workers (233KB WASM)</div></div>
135+
\\ <div class="target"><div class="target-cmd">zig build worker</div><div class="target-desc">Vercel Edge (same WASM)</div></div>
136+
\\ <div class="target"><div class="target-cmd">docker build</div><div class="target-desc">Container (160MB image)</div></div>
137+
\\ <div class="target"><div class="target-cmd">zig build desktop</div><div class="target-desc">macOS app (5.3MB binary)</div></div>
138+
\\ </div>
139+
\\
140+
\\ <hr class="rule">
141+
\\ <h2>Try it</h2>
142+
\\ <pre>zig build desktop
143+
\\open zig-out/MerApp.app</pre>
144+
\\ <p>Two commands. Native app on your dock.</p>
145+
\\
146+
\\ <div class="links">
147+
\\ <a href="https://github.com/justrach/merjs" class="btn btn-red">GitHub</a>
148+
\\ <a href="/" class="btn btn-outline">Home</a>
149+
\\ <a href="/about" class="btn btn-outline">Philosophy</a>
150+
\\ </div>
151+
\\</div>
152+
\\</body>
153+
\\</html>
154+
;

examples/site/worker/grep.wasm

3.74 KB
Binary file not shown.

0 commit comments

Comments
 (0)