chore: import upstream snapshot with attribution
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 79 KiB |
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env bash
|
||||
# Linux canvas smoke under Xvfb — run on a machine with NO WebKitGTK dev
|
||||
# package, which makes the build itself the native-only link test.
|
||||
#
|
||||
# Exercises the Linux gpu_surface software path without a display server:
|
||||
# builds examples/ui-inbox (a native-only app, so its GTK host compiles
|
||||
# with the WebKitGTK stub seam and never links webkitgtk-6.0) with
|
||||
# -Dplatform=linux -Dweb-engine=system -Dautomation=true, runs it under
|
||||
# Xvfb, and asserts against the automation snapshot:
|
||||
#
|
||||
# 1. the built ELF carries no WebKitGTK reference (no libwebkitgtk
|
||||
# DT_NEEDED entry, no webkit_/jsc_ dynamic symbol), audited on the
|
||||
# real binary by tools/audit_web_layer.zig
|
||||
# 2. snapshot ready=true (app booted, automation server live)
|
||||
# 3. gpu_backend=software (the software present path is active)
|
||||
# 4. gpu_nonblank=true (real pixels were presented)
|
||||
# 5. widget-click "Add task" -> '4 open' (automation input mutates state)
|
||||
# 6. automate screenshot renders a non-empty PNG
|
||||
# 7. ZERO WebKit helper processes for the whole run (a native-only app
|
||||
# has no web layer to boot WebKit with)
|
||||
#
|
||||
# Deliberately NOT `set -e` (same as windows-canvas-smoke.sh): grep exits 1
|
||||
# on zero matches, and under `set -e` an assignment like `x=$(grep ...)` or
|
||||
# a swallowed `$(cli 2>&1)` capture dies with NO output — this job failed
|
||||
# three times with nothing in the log but the exit code. Every assertion
|
||||
# goes through fail(), which dumps the snapshot and the app log.
|
||||
set -u
|
||||
|
||||
# No WebKit sandbox workaround: a native-only app's host is compiled
|
||||
# without the web layer entirely (and even in web builds the main
|
||||
# WebView is lazy), so no WebKit helper processes start and the runner's
|
||||
# user-namespace restrictions never come into play. The zero-WebKit
|
||||
# assertion below keeps it that way.
|
||||
|
||||
# GTK_A11Y=none: under Xvfb there is no session bus providing org.a11y.Bus,
|
||||
# and GTK4's a11y init blocks ~25 s on the GDBus name lookup before warning
|
||||
# and continuing — the app's first runtime event landed after the readiness
|
||||
# window had already expired (reproduced in a local container: without this
|
||||
# the wait times out at startup; with it the full smoke passes).
|
||||
# Accessibility is not what this smoke tests.
|
||||
export GTK_A11Y="${GTK_A11Y:-none}"
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
app_dir="$repo_root/examples/ui-inbox"
|
||||
snap="$app_dir/.zig-cache/native-sdk-automation/snapshot.txt"
|
||||
cli="$repo_root/zig-out/bin/native"
|
||||
app_log="${TMPDIR:-/tmp}/linux-canvas-smoke-app.log"
|
||||
|
||||
# Readiness budget. Even with GTK_A11Y=none, shared ubuntu-24.04 runners
|
||||
# show a consistent ~27 s stall between EGL init and the app's first
|
||||
# runtime event (measured in runs 28690855597 pass / 28691951139 fail —
|
||||
# the SAME stall in both; the old hard 30 s `automate wait` window flipped
|
||||
# green/red on one or two seconds of runner noise). Local containers show
|
||||
# no stall at all. Widen the budget here instead of weakening the CLI
|
||||
# default; every correctness assertion stays strict.
|
||||
ready_timeout_ms=90000
|
||||
|
||||
app_pid=""
|
||||
cleanup() {
|
||||
[ -n "$app_pid" ] && kill "$app_pid" >/dev/null 2>&1
|
||||
# xvfb-run does not forward signals to an already-detached app; reap the
|
||||
# app and its Xvfb directly so local runs exit clean (CI would otherwise
|
||||
# rely on the runner's orphan sweep).
|
||||
pkill -f "$app_dir/zig-out/bin/ui-inbox" >/dev/null 2>&1
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
diagnostics() {
|
||||
echo "---- diagnostics ----"
|
||||
echo "-- snapshot ($snap):"
|
||||
if [ -f "$snap" ]; then tr '|' '\n' < "$snap" | sed 's/^/ /'; else echo " (missing)"; fi
|
||||
echo "-- app log head ($app_log):"
|
||||
head -20 "$app_log" 2>/dev/null | sed 's/^/ /'
|
||||
echo "-- app log tail ($app_log):"
|
||||
tail -40 "$app_log" 2>/dev/null | sed 's/^/ /'
|
||||
echo "---------------------"
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $1"
|
||||
diagnostics
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Canvas apps must never spawn WebKit: the window's main WebView is
|
||||
# created lazily and nothing in this app materializes it, so any
|
||||
# WebKitWebProcess/WebKitNetworkProcess during the run means an eager
|
||||
# creation regressed (and with it launch latency, resident helper
|
||||
# processes, and the sandbox trouble this smoke used to work around).
|
||||
assert_no_webkit() {
|
||||
local helpers
|
||||
helpers=$(pgrep -af 'WebKit(Web|Network)Process' 2>/dev/null)
|
||||
if [ -n "$helpers" ]; then
|
||||
echo "-- WebKit helper processes found ($1):"
|
||||
echo "$helpers" | sed 's/^/ /'
|
||||
fail "canvas app spawned WebKit processes ($1)"
|
||||
fi
|
||||
}
|
||||
|
||||
# ---- build ----------------------------------------------------------------
|
||||
(cd "$repo_root" && zig build) || fail "root zig build (CLI) failed"
|
||||
(cd "$app_dir" && zig build -Dplatform=linux -Dweb-engine=system -Dautomation=true) \
|
||||
|| fail "ui-inbox Linux build failed (a native-only app must build without the WebKitGTK dev package)"
|
||||
|
||||
# ---- 1: the native-only ELF carries no WebKitGTK reference -----------------
|
||||
(cd "$repo_root" && zig run tools/audit_web_layer.zig -- "$app_dir/zig-out/bin/ui-inbox" absent) \
|
||||
|| fail "native-only ELF audit failed (the binary references WebKitGTK)"
|
||||
echo "== native-only ELF audit ok"
|
||||
|
||||
# ---- launch ---------------------------------------------------------------
|
||||
cd "$app_dir" || fail "missing $app_dir"
|
||||
rm -rf .zig-cache/native-sdk-automation
|
||||
xvfb-run -a "$app_dir/zig-out/bin/ui-inbox" > "$app_log" 2>&1 &
|
||||
app_pid=$!
|
||||
|
||||
# ---- 2: automation snapshot becomes ready ---------------------------------
|
||||
# `automate assert` self-reports on timeout (missing patterns + snapshot
|
||||
# tail) and prints the measured latency on success, so green logs carry
|
||||
# the readiness margin.
|
||||
"$cli" automate assert --timeout-ms "$ready_timeout_ms" 'ready=true' \
|
||||
|| fail "snapshot never became ready"
|
||||
|
||||
# ---- 3 + 4: software backend presented non-blank pixels --------------------
|
||||
"$cli" automate assert --timeout-ms 30000 'gpu_nonblank=true' \
|
||||
|| fail "gpu_nonblank never became true"
|
||||
grep -q 'gpu_backend=software' "$snap" || fail "gpu_backend is not software"
|
||||
echo "== canvas: $(grep -o 'gpu_backend=[a-z]*' "$snap" | head -1)" \
|
||||
"$(grep -o 'gpu_nonblank=[a-z]*' "$snap" | head -1)"
|
||||
assert_no_webkit "after first presented frame"
|
||||
echo "== zero WebKit processes after first presented frame"
|
||||
|
||||
# ---- 5: automation widget-click mutates the model --------------------------
|
||||
echo "== open before click: $(grep -oE '[0-9]+ open' "$snap" | head -1)"
|
||||
add_id=$(grep -o 'widget @w1/inbox-canvas#[0-9]* role=button name="Add task"' "$snap" \
|
||||
| grep -o '#[0-9]*' | tr -d '#')
|
||||
[ -n "$add_id" ] || fail "Add task button not found in snapshot"
|
||||
"$cli" automate widget-click inbox-canvas "$add_id" || fail "CLI widget-click failed"
|
||||
"$cli" automate assert --timeout-ms 30000 '4 open' \
|
||||
|| fail "widget-click did not reach '4 open'"
|
||||
echo "== open after click: $(grep -oE '[0-9]+ open' "$snap" | head -1)"
|
||||
|
||||
# ---- 6: screenshot renders a non-empty PNG ---------------------------------
|
||||
"$cli" automate screenshot inbox-canvas || fail "CLI screenshot failed"
|
||||
test -s .zig-cache/native-sdk-automation/screenshot-inbox-canvas.png \
|
||||
|| fail "screenshot PNG missing or empty"
|
||||
|
||||
# ---- 7: still zero WebKit processes at the end of the run -------------------
|
||||
assert_no_webkit "at end of run"
|
||||
echo "== zero WebKit processes at end of run"
|
||||
|
||||
echo "PASS: linux canvas smoke"
|
||||
exit 0
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env bash
|
||||
# Windows canvas smoke under Wine.
|
||||
#
|
||||
# Exercises the Windows gpu_surface software path (src/platform/windows/
|
||||
# webview2_host.cpp: child HWND, WM_TIMER frame events, SetDIBitsToDevice
|
||||
# blits) without Windows hardware: cross-compiles examples/ui-inbox for
|
||||
# x86_64-windows-gnu, runs the .exe under Xvfb + Wine, and asserts against
|
||||
# the automation snapshot:
|
||||
#
|
||||
# 1. snapshot ready=true (app booted, automation server live)
|
||||
# 2. gpu_backend=software (the SetDIBitsToDevice path is active)
|
||||
# 3. gpu_nonblank=true (real pixels were presented)
|
||||
# 4. widget-click "Add task" -> '4 open' (automation input mutates state)
|
||||
# 5. real X11 click + typing lands in the draft textbox (XTEST -> Wine ->
|
||||
# WM_LBUTTONDOWN/WM_CHAR -> runtime)
|
||||
#
|
||||
# Step 5 (xdotool) is deliberately included: it is the only coverage of the
|
||||
# Win32 pointer/keyboard input mapping in webview2_host.cpp. It is also the
|
||||
# flakiest step (window lookup, focus without a window manager), so every
|
||||
# failure path dumps the X window list, the snapshot, and the app log.
|
||||
#
|
||||
# Deliberately NOT `set -e`: grep exits 1 on zero matches inside the poll
|
||||
# loops, and we want explicit, diagnosable failures instead of silent early
|
||||
# exits. Every assertion goes through fail(), which dumps diagnostics.
|
||||
set -u
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
app_dir="$repo_root/examples/ui-inbox"
|
||||
snap="$app_dir/.zig-cache/native-sdk-automation/snapshot.txt"
|
||||
cli="$repo_root/zig-out/bin/native"
|
||||
app_log="${TMPDIR:-/tmp}/windows-canvas-smoke-app.log"
|
||||
|
||||
# Wine needs an X display; when none is present (CI), re-exec the whole
|
||||
# script under a private Xvfb server so the app and xdotool share it. The
|
||||
# explicit screen size beats xvfb-run's 640x480x8 default: the app window is
|
||||
# 720x520 and Wine wants a 24-bit visual.
|
||||
if [ -z "${DISPLAY:-}" ]; then
|
||||
exec xvfb-run -a --server-args="-screen 0 1280x800x24" "$0" "$@"
|
||||
fi
|
||||
|
||||
export WINEPREFIX="${WINEPREFIX:-$repo_root/.zig-cache/wineprefix}"
|
||||
export WINEDEBUG="${WINEDEBUG:--all}"
|
||||
|
||||
app_pid=""
|
||||
cleanup() {
|
||||
[ -n "$app_pid" ] && kill "$app_pid" >/dev/null 2>&1
|
||||
wineserver -k >/dev/null 2>&1
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
diagnostics() {
|
||||
echo "---- diagnostics ----"
|
||||
echo "-- X windows:"
|
||||
xdotool search --name "." 2>/dev/null | while read -r w; do
|
||||
echo " $w: $(xdotool getwindowname "$w" 2>/dev/null)"
|
||||
done
|
||||
echo "-- snapshot ($snap):"
|
||||
if [ -f "$snap" ]; then tr '|' '\n' < "$snap" | sed 's/^/ /'; else echo " (missing)"; fi
|
||||
echo "-- app log tail ($app_log):"
|
||||
tail -40 "$app_log" 2>/dev/null | sed 's/^/ /'
|
||||
echo "---------------------"
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $1"
|
||||
diagnostics
|
||||
exit 1
|
||||
}
|
||||
|
||||
# poll <seconds> <pattern>: wait until $snap contains <pattern>.
|
||||
poll() {
|
||||
local deadline=$((SECONDS + $1))
|
||||
while [ "$SECONDS" -lt "$deadline" ]; do
|
||||
[ -f "$snap" ] && grep -q "$2" "$snap" && return 0
|
||||
sleep 0.5
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---- build ----------------------------------------------------------------
|
||||
(cd "$repo_root" && zig build) || fail "root zig build (CLI) failed"
|
||||
(cd "$app_dir" && zig build -Dtarget=x86_64-windows-gnu -Dplatform=windows -Dweb-engine=system -Dautomation=true) \
|
||||
|| fail "ui-inbox Windows cross-compile failed"
|
||||
|
||||
# ---- wineprefix -----------------------------------------------------------
|
||||
# First run initializes the prefix (measured ~10-30s on CI-class machines);
|
||||
# subsequent runs are instant, so no cache step is needed.
|
||||
start=$SECONDS
|
||||
wineboot --init >/dev/null 2>&1
|
||||
wineserver --wait >/dev/null 2>&1
|
||||
echo "== wineprefix ready in $((SECONDS - start))s ($WINEPREFIX)"
|
||||
|
||||
# ---- launch ---------------------------------------------------------------
|
||||
cd "$app_dir" || fail "missing $app_dir"
|
||||
rm -rf .zig-cache/native-sdk-automation
|
||||
mkdir -p .zig-cache/native-sdk-automation
|
||||
wine zig-out/bin/ui-inbox.exe > "$app_log" 2>&1 &
|
||||
app_pid=$!
|
||||
|
||||
# ---- 1: automation snapshot becomes ready ---------------------------------
|
||||
poll 180 'ready=true' || fail "snapshot never became ready"
|
||||
echo "== ready: $(head -1 "$snap" | cut -d'|' -f1)"
|
||||
|
||||
# ---- 2 + 3: software backend presented non-blank pixels --------------------
|
||||
poll 60 'gpu_nonblank=true' || fail "gpu_nonblank never became true"
|
||||
grep -q 'gpu_backend=software' "$snap" || fail "gpu_backend is not software"
|
||||
echo "== canvas: $(grep -o 'gpu_backend=[a-z]*' "$snap" | head -1)" \
|
||||
"$(grep -o 'gpu_nonblank=[a-z]*' "$snap" | head -1)" \
|
||||
"$(grep -o 'gpu_sample=0x[0-9a-f]*' "$snap" | head -1)" \
|
||||
"$(grep -o 'gpu_present_mode=[a-z]*' "$snap" | head -1)"
|
||||
|
||||
# ---- 4: automation widget-click mutates the model --------------------------
|
||||
echo "== open before click: $(grep -oE '[0-9]+ open' "$snap" | head -1)"
|
||||
add_id=$(grep -o 'widget @w1/inbox-canvas#[0-9]* role=button name="Add task"' "$snap" \
|
||||
| grep -o '#[0-9]*' | tr -d '#')
|
||||
[ -n "$add_id" ] || fail "Add task button not found in snapshot"
|
||||
"$cli" automate widget-click inbox-canvas "$add_id" || fail "CLI widget-click failed"
|
||||
poll 30 '4 open' || fail "widget-click did not reach '4 open'"
|
||||
echo "== open after click: $(grep -oE '[0-9]+ open' "$snap" | head -1)"
|
||||
|
||||
# ---- 5: real X11 input through the Win32 path ------------------------------
|
||||
# Root-coordinate math. Hidden-titlebar windows keep the full overlapped
|
||||
# frame and reclaim the caption band through WM_NCCALCSIZE, so the Win32
|
||||
# client area starts at the very top of the window. Under a WM-less Wine
|
||||
# the X11 driver still places (and reports) the client X window at the
|
||||
# DEFAULT frame offset - one caption band lower - so the X origin sits a
|
||||
# band BELOW where Win32 client coordinates actually map, and the reported
|
||||
# X height is short by exactly that band (measured: X window 718x489 at
|
||||
# y=30 for a 718x519 client whose clicks land at y=0). The snapshot knows
|
||||
# the true client height, so the height shortfall IS the y correction; a
|
||||
# standard-frame window reports matching heights and corrects by zero.
|
||||
win=""
|
||||
for w in $(xdotool search --name "." 2>/dev/null); do
|
||||
case "$(xdotool getwindowname "$w" 2>/dev/null)" in
|
||||
*[Ii]nbox*) win="$w" ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$win" ] || fail "app X window not found"
|
||||
eval "$(xdotool getwindowgeometry --shell "$win")"
|
||||
client_h=$(grep -o 'window @w1 "[^"]*" bounds=([^)]*)' "$snap" | head -1 \
|
||||
| sed -n 's/.*x\([0-9]*\)[^x]*$/\1/p')
|
||||
[ -n "$client_h" ] || client_h=$HEIGHT
|
||||
y_off=$((client_h - HEIGHT))
|
||||
[ "$y_off" -ge 0 ] 2>/dev/null || y_off=0
|
||||
echo "== x window $win: pos=($X,$Y) size=${WIDTH}x${HEIGHT} client_h=$client_h y_off=$y_off"
|
||||
xdotool windowactivate "$win" >/dev/null 2>&1 || xdotool windowfocus "$win" >/dev/null 2>&1
|
||||
|
||||
draft_line=$(grep -o 'widget @w1/inbox-canvas#[0-9]* role=textbox[^|]*' "$snap" | head -1)
|
||||
[ -n "$draft_line" ] || fail "draft textbox not found in snapshot"
|
||||
bounds=$(echo "$draft_line" | grep -o 'bounds=([^)]*)')
|
||||
bx=$(echo "$bounds" | sed -n 's/bounds=(\([0-9.]*\),.*/\1/p')
|
||||
by=$(echo "$bounds" | sed -n 's/bounds=([0-9.]*,\([0-9.]*\) .*/\1/p')
|
||||
bw=$(echo "$bounds" | sed -n 's/.* \([0-9.]*\)x[0-9.]*).*/\1/p')
|
||||
bh=$(echo "$bounds" | sed -n 's/.* [0-9.]*x\([0-9.]*\)).*/\1/p')
|
||||
[ -n "$bx" ] && [ -n "$by" ] && [ -n "$bw" ] && [ -n "$bh" ] || fail "could not parse draft bounds: $draft_line"
|
||||
cx=$(awk "BEGIN{printf \"%d\", $X + $bx + $bw / 2}")
|
||||
cy=$(awk "BEGIN{printf \"%d\", $Y - $y_off + $by + $bh / 2}")
|
||||
echo "== clicking draft field $bounds at ($cx,$cy)"
|
||||
xdotool mousemove "$cx" "$cy" click 1
|
||||
# The click must move widget focus into the textbox before any keys are
|
||||
# sent: spaces in the typed string would otherwise activate whatever
|
||||
# widget held focus (a button press adds a task and the real failure -
|
||||
# input landing in the wrong widget - would read as missing text).
|
||||
poll 10 'role=textbox[^|]*focused=true' || fail "draft textbox did not take focus from the click"
|
||||
sleep 1
|
||||
xdotool type --delay 120 "hi from wine"
|
||||
poll 30 'hi from wine' || fail "typed text never appeared in the snapshot"
|
||||
echo "== draft after typing: $(grep -o 'widget @w1/inbox-canvas#[0-9]* role=textbox[^|]*' "$snap" | head -1 | cut -c1-160)"
|
||||
echo "== input latency: $(grep -o 'gpu_input_latency_ns=[0-9]*' "$snap" | head -1)"
|
||||
|
||||
echo "PASS: windows canvas smoke"
|
||||
exit 0
|
||||
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env bash
|
||||
# Windows effects smoke under Wine.
|
||||
#
|
||||
# Exercises the effect system's live Windows path (src/runtime/effects.zig
|
||||
# worker threads -> PostMessageW wake in src/platform/windows/
|
||||
# webview2_host.cpp -> loop-thread drain) without Windows hardware:
|
||||
# cross-compiles examples/effects-probe for x86_64-windows-gnu, runs the
|
||||
# .exe under Xvfb + Wine, and asserts against the automation snapshot and
|
||||
# the app's trace log:
|
||||
#
|
||||
# 1. snapshot ready=true (app booted, automation server live)
|
||||
# 2. gpu_backend=software + nonblank (the canvas presented real pixels)
|
||||
# 3. widget-click "Start stream" (fx.spawn launches cmd.exe under
|
||||
# Wine; streamed lines land in the
|
||||
# model and grow the snapshot)
|
||||
# 4. app log shows event=effects_wake (the worker's PostMessageW wake was
|
||||
# marshalled through the message
|
||||
# loop -- the wake path itself, not
|
||||
# just the frame-tick drain)
|
||||
# 5. widget-click "Cancel" (fx.cancel terminates the child;
|
||||
# status shows "cancelled")
|
||||
# 6. the line count freezes (no lines arrive after cancel,
|
||||
# sampled across ~5 more would-be
|
||||
# line intervals)
|
||||
#
|
||||
# Known caveat: the timer present mode also drains effect completions on
|
||||
# every frame tick (ui_app.zig handleFrame), so line delivery alone cannot
|
||||
# isolate the wake; that is why step 4 checks the trace log for the wake
|
||||
# events directly instead of inferring the wake from model updates.
|
||||
#
|
||||
# Deliberately NOT `set -e`: grep exits 1 on zero matches inside the poll
|
||||
# loops, and we want explicit, diagnosable failures instead of silent early
|
||||
# exits. Every assertion goes through fail(), which dumps diagnostics.
|
||||
set -u
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
app_dir="$repo_root/examples/effects-probe"
|
||||
snap="$app_dir/.zig-cache/native-sdk-automation/snapshot.txt"
|
||||
cli="$repo_root/zig-out/bin/native"
|
||||
app_log="${TMPDIR:-/tmp}/windows-effects-smoke-app.log"
|
||||
|
||||
# Wine needs an X display; when none is present (CI), re-exec the whole
|
||||
# script under a private Xvfb server. The explicit screen size beats
|
||||
# xvfb-run's 640x480x8 default: the app window is 560x480 and Wine wants a
|
||||
# 24-bit visual.
|
||||
if [ -z "${DISPLAY:-}" ]; then
|
||||
exec xvfb-run -a --server-args="-screen 0 1280x800x24" "$0" "$@"
|
||||
fi
|
||||
|
||||
export WINEPREFIX="${WINEPREFIX:-$repo_root/.zig-cache/wineprefix}"
|
||||
export WINEDEBUG="${WINEDEBUG:--all}"
|
||||
|
||||
app_pid=""
|
||||
cleanup() {
|
||||
[ -n "$app_pid" ] && kill "$app_pid" >/dev/null 2>&1
|
||||
wineserver -k >/dev/null 2>&1
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
diagnostics() {
|
||||
echo "---- diagnostics ----"
|
||||
echo "-- snapshot ($snap):"
|
||||
if [ -f "$snap" ]; then tr '|' '\n' < "$snap" | sed 's/^/ /'; else echo " (missing)"; fi
|
||||
echo "-- app log tail ($app_log):"
|
||||
tail -40 "$app_log" 2>/dev/null | sed 's/^/ /'
|
||||
echo "---------------------"
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $1"
|
||||
diagnostics
|
||||
exit 1
|
||||
}
|
||||
|
||||
# poll <seconds> <pattern>: wait until $snap contains <pattern>.
|
||||
poll() {
|
||||
local deadline=$((SECONDS + $1))
|
||||
while [ "$SECONDS" -lt "$deadline" ]; do
|
||||
[ -f "$snap" ] && grep -q "$2" "$snap" && return 0
|
||||
sleep 0.5
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# The status bar renders "{N} lines total · {M} dropped"; extract N.
|
||||
total_lines() {
|
||||
grep -o '[0-9]* lines total' "$snap" 2>/dev/null | head -1 | grep -o '^[0-9]*'
|
||||
}
|
||||
|
||||
# widget_id <name>: find a widget id in the snapshot by accessible name.
|
||||
widget_id() {
|
||||
grep -o "widget @w1/probe-canvas#[0-9]* role=button name=\"$1\"" "$snap" \
|
||||
| grep -o '#[0-9]*' | tr -d '#'
|
||||
}
|
||||
|
||||
# ---- build ----------------------------------------------------------------
|
||||
(cd "$repo_root" && zig build) || fail "root zig build (CLI) failed"
|
||||
# effects-probe is a zero-config app (app.zon + src, no build.zig): the CLI
|
||||
# synthesizes its build graph. -Doptimize=Debug keeps the smoke binary at
|
||||
# the debug shape (`native build` alone would inject ReleaseFast).
|
||||
"$cli" build "$app_dir" -Dtarget=x86_64-windows-gnu -Dplatform=windows -Dweb-engine=system -Dautomation=true -Doptimize=Debug \
|
||||
|| fail "effects-probe Windows cross-compile failed"
|
||||
|
||||
# ---- wineprefix -----------------------------------------------------------
|
||||
start=$SECONDS
|
||||
wineboot --init >/dev/null 2>&1
|
||||
wineserver --wait >/dev/null 2>&1
|
||||
echo "== wineprefix ready in $((SECONDS - start))s ($WINEPREFIX)"
|
||||
|
||||
# ---- launch ---------------------------------------------------------------
|
||||
cd "$app_dir" || fail "missing $app_dir"
|
||||
rm -rf .zig-cache/native-sdk-automation
|
||||
mkdir -p .zig-cache/native-sdk-automation
|
||||
wine zig-out/bin/effects-probe.exe > "$app_log" 2>&1 &
|
||||
app_pid=$!
|
||||
|
||||
# ---- 1: automation snapshot becomes ready ---------------------------------
|
||||
poll 180 'ready=true' || fail "snapshot never became ready"
|
||||
echo "== ready: $(head -1 "$snap" | cut -d'|' -f1)"
|
||||
|
||||
# ---- 2: software backend presented non-blank pixels ------------------------
|
||||
poll 60 'gpu_nonblank=true' || fail "gpu_nonblank never became true"
|
||||
grep -q 'gpu_backend=software' "$snap" || fail "gpu_backend is not software"
|
||||
echo "== canvas: $(grep -o 'gpu_backend=[a-z]*' "$snap" | head -1)" \
|
||||
"$(grep -o 'gpu_nonblank=[a-z]*' "$snap" | head -1)"
|
||||
grep -q 'idle' "$snap" || fail "probe did not start idle"
|
||||
|
||||
# ---- 3: Start spawns the stream and lines arrive ---------------------------
|
||||
start_id=$(widget_id "Start stream")
|
||||
[ -n "$start_id" ] || fail "Start stream button not found in snapshot"
|
||||
"$cli" automate widget-click probe-canvas "$start_id" || fail "CLI widget-click Start failed"
|
||||
poll 30 'streaming:' || fail "status never showed streaming (spawn failed under Wine?)"
|
||||
# The Windows stream paces ~1 line/s (cmd for /L + ping); wait for at
|
||||
# least 2 visible lines so cancel provably interrupts an active stream.
|
||||
poll 60 'stream line 2' || fail "stream lines never reached the model"
|
||||
echo "== streaming: $(grep -o 'streaming: [0-9]* lines' "$snap" | head -1), status-bar: $(total_lines) lines total"
|
||||
|
||||
# ---- 4: the PostMessage wake path fired ------------------------------------
|
||||
# handleFrame also drains completions on every frame tick, so lines in the
|
||||
# model alone cannot isolate the wake. The runner's default -Dtrace=events
|
||||
# sink prints every runtime event; effects_wake records prove the worker's
|
||||
# PostMessageW -> kWakeMessage -> kWake -> .effects_wake marshalling ran.
|
||||
grep -q 'event="effects_wake"' "$app_log" || fail "no effects_wake events in the app log (PostMessage wake never fired)"
|
||||
echo "== effects_wake events so far: $(grep -c 'event="effects_wake"' "$app_log")"
|
||||
|
||||
# ---- 5: Cancel terminates the child ----------------------------------------
|
||||
cancel_id=$(widget_id "Cancel")
|
||||
[ -n "$cancel_id" ] || fail "Cancel button not found in snapshot"
|
||||
"$cli" automate widget-click probe-canvas "$cancel_id" || fail "CLI widget-click Cancel failed"
|
||||
poll 30 'cancelled: code' || fail "status never showed cancelled"
|
||||
frozen=$(total_lines)
|
||||
[ -n "$frozen" ] || fail "could not read line count after cancel"
|
||||
echo "== cancelled at $frozen lines: $(grep -o 'cancelled: code [0-9-]* after [0-9]* lines' "$snap" | head -1)"
|
||||
|
||||
# ---- 6: the line count is frozen -------------------------------------------
|
||||
# ~5 more lines would have arrived at the ~1s cadence if the child were
|
||||
# still alive or queued lines were still draining.
|
||||
sleep 6
|
||||
after=$(total_lines)
|
||||
[ "$after" = "$frozen" ] || fail "line count moved after cancel ($frozen -> $after)"
|
||||
grep -q 'streaming:' "$snap" && fail "status went back to streaming after cancel"
|
||||
echo "== count frozen at $after lines across 6s"
|
||||
|
||||
echo "PASS: windows effects smoke"
|
||||
exit 0
|
||||
@@ -0,0 +1,133 @@
|
||||
name: CEF Runtime
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
cef_version:
|
||||
description: "CEF version to package"
|
||||
required: true
|
||||
default: "144.0.6+g5f7e671+chromium-144.0.7559.59"
|
||||
source:
|
||||
description: "Build from official archive or CEF source"
|
||||
required: true
|
||||
type: choice
|
||||
default: "official"
|
||||
options:
|
||||
- official
|
||||
- source
|
||||
cef_branch:
|
||||
description: "Optional CEF branch for source builds"
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build ${{ matrix.platform }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platform: macosarm64
|
||||
runner: macos-14
|
||||
- platform: macosx64
|
||||
runner: macos-15
|
||||
- platform: linux64
|
||||
runner: ubuntu-latest
|
||||
- platform: linuxarm64
|
||||
runner: ubuntu-24.04-arm
|
||||
- platform: windows64
|
||||
runner: windows-2022
|
||||
# windowsarm64 disabled: Zig 0.16.0 aarch64-windows binary is broken
|
||||
# https://codeberg.org/ziglang/zig/issues/31865
|
||||
# - platform: windowsarm64
|
||||
# runner: windows-11-arm
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: 0.16.0
|
||||
|
||||
- name: Download official CEF
|
||||
if: ${{ inputs.source == 'official' }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version="${{ inputs.cef_version }}"
|
||||
platform="${{ matrix.platform }}"
|
||||
archive="cef_binary_${version}_${platform}.tar.bz2"
|
||||
base="https://cef-builds.spotifycdn.com"
|
||||
mkdir -p zig-out/cef-download
|
||||
curl --fail --location --output "zig-out/cef-download/${archive}" "${base}/${archive}"
|
||||
curl --fail --location --output "zig-out/cef-download/${archive}.sha256" "${base}/${archive}.sha256"
|
||||
expected="$(tr -d '[:space:]' < "zig-out/cef-download/${archive}.sha256")"
|
||||
if command -v sha256sum &>/dev/null; then
|
||||
actual="$(sha256sum "zig-out/cef-download/${archive}" | awk '{print $1}')"
|
||||
elif command -v shasum &>/dev/null; then
|
||||
actual="$(shasum -a 256 "zig-out/cef-download/${archive}" | awk '{print $1}')"
|
||||
else
|
||||
echo "::error::No SHA-256 hashing utility found"; exit 1
|
||||
fi
|
||||
test "$expected" = "$actual"
|
||||
tar -xjf "zig-out/cef-download/${archive}" -C zig-out/cef-download
|
||||
echo "CEF_ROOT=zig-out/cef-download/${archive%.tar.bz2}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build CEF wrapper
|
||||
if: ${{ inputs.source == 'official' }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cmake_args=(-S "$CEF_ROOT" -B "$CEF_ROOT/build/libcef_dll_wrapper")
|
||||
case "${{ matrix.platform }}" in
|
||||
*arm64) cmake_args+=(-DPROJECT_ARCH=arm64) ;;
|
||||
macosx64) cmake_args+=(-DCMAKE_OSX_ARCHITECTURES=x86_64 -DPROJECT_ARCH=x86_64) ;;
|
||||
esac
|
||||
cmake "${cmake_args[@]}"
|
||||
cmake --build "$CEF_ROOT/build/libcef_dll_wrapper" --target libcef_dll_wrapper --config Release
|
||||
mkdir -p "$CEF_ROOT/libcef_dll_wrapper"
|
||||
case "${{ matrix.platform }}" in
|
||||
windows*) wrapper="libcef_dll_wrapper.lib" ;;
|
||||
*) wrapper="libcef_dll_wrapper.a" ;;
|
||||
esac
|
||||
find "$CEF_ROOT/build/libcef_dll_wrapper" -name "$wrapper" -print -quit | xargs -I{} cp "{}" "$CEF_ROOT/libcef_dll_wrapper/$wrapper"
|
||||
|
||||
- name: Prepare native-sdk runtime archive
|
||||
if: ${{ inputs.source == 'official' }}
|
||||
shell: bash
|
||||
run: |
|
||||
zig build
|
||||
cli="zig-out/bin/native"
|
||||
if [[ "${{ runner.os }}" == "Windows" ]]; then cli="zig-out/bin/native.exe"; fi
|
||||
"$cli" cef prepare-release --dir "$CEF_ROOT" --output zig-out/cef --version "${{ inputs.cef_version }}"
|
||||
|
||||
- name: Build CEF from source and prepare runtime
|
||||
if: ${{ inputs.source == 'source' }}
|
||||
shell: bash
|
||||
run: |
|
||||
zig build
|
||||
chmod +x tools/cef/build-from-source.sh
|
||||
args=(
|
||||
tools/cef/build-from-source.sh
|
||||
--platform "${{ matrix.platform }}"
|
||||
--version "${{ inputs.cef_version }}"
|
||||
--output zig-out/cef
|
||||
--native-sdk-bin zig-out/bin/native
|
||||
)
|
||||
if [ -n "${{ inputs.cef_branch }}" ]; then
|
||||
args+=(--cef-branch "${{ inputs.cef_branch }}")
|
||||
fi
|
||||
"${args[@]}"
|
||||
|
||||
- name: Publish release asset
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: cef-${{ inputs.cef_version }}
|
||||
name: CEF ${{ inputs.cef_version }}
|
||||
files: |
|
||||
zig-out/cef/native-sdk-cef-${{ inputs.cef_version }}-${{ matrix.platform }}.tar.gz
|
||||
zig-out/cef/native-sdk-cef-${{ inputs.cef_version }}-${{ matrix.platform }}.tar.gz.sha256
|
||||
@@ -0,0 +1,335 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
zig:
|
||||
name: Zig Core
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: 0.16.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
# The TypeScript core suites transpile at build/test time; without
|
||||
# node they skip silently, so CI must provide it.
|
||||
- run: npm ci --prefix packages/core
|
||||
- run: zig build test
|
||||
- run: zig build validate
|
||||
|
||||
macos-webview:
|
||||
name: macOS WebView
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: 0.16.0
|
||||
- run: zig build test-webview-system-link
|
||||
- run: zig build test-webview-smoke
|
||||
# Signed-package seal pin: an ad-hoc signed package must pass
|
||||
# codesign --verify --strict (macOS runners are the only tier with
|
||||
# codesign; the step skips loudly anywhere else).
|
||||
- run: zig build test-package-signing
|
||||
# Shared macos-14 runners are far noisier than a dev box (the second
|
||||
# CI run measured a 576 ms automation-ready against the 500 ms local
|
||||
# ceiling), so widen the smoke budgets here instead of weakening the
|
||||
# local defaults. NATIVE_SDK_SMOKE_BUDGET_MS raises the first-frame latency
|
||||
# budget and the automation-ready ceiling together; every correctness
|
||||
# assertion in the smokes stays strict.
|
||||
- run: zig build test-gpu-dashboard-smoke
|
||||
env:
|
||||
NATIVE_SDK_SMOKE_BUDGET_MS: "1500"
|
||||
- run: zig build test-gpu-components-smoke
|
||||
env:
|
||||
NATIVE_SDK_SMOKE_BUDGET_MS: "1500"
|
||||
|
||||
macos-gpu-perf:
|
||||
name: macOS GPU Perf
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: 0.16.0
|
||||
# Percentile perf check: 5 cold launches asserting p90
|
||||
# first-frame latency under NATIVE_SDK_PERF_BUDGET_MS, then 5 steady-state
|
||||
# widget clicks asserting p90 input latency under NATIVE_SDK_PERF_INPUT_BUDGET_MS.
|
||||
# Its own job so a shared-runner slowdown is visible in isolation and
|
||||
# never blocks the correctness smokes.
|
||||
# Shared macos-14 runners are far noisier than a dev box (first CI run
|
||||
# measured a 581 ms cold-start outlier against the 300 ms default), so
|
||||
# widen the budgets here instead of weakening the local defaults: this
|
||||
# job exists to catch step-function regressions, not runner noise.
|
||||
- run: zig build test-gpu-dashboard-perf
|
||||
env:
|
||||
NATIVE_SDK_PERF_BUDGET_MS: "1500"
|
||||
NATIVE_SDK_PERF_INPUT_BUDGET_MS: "500"
|
||||
|
||||
linux-webkitgtk:
|
||||
name: Linux WebKitGTK
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: 0.16.0
|
||||
- name: Install WebKitGTK dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y libgtk-4-dev libwebkitgtk-6.0-dev
|
||||
- run: zig build test-webview-system-link -Dplatform=linux
|
||||
# Declare-to-use, proven on real Linux executables WITH the
|
||||
# WebKitGTK dev package installed — the seam, not the environment:
|
||||
# the native-only ui-inbox binary must carry no libwebkitgtk
|
||||
# DT_NEEDED entry and no webkit_/jsc_ dynamic symbol even though
|
||||
# the headers were right there, while the webview example must
|
||||
# keep them. (The environment half — building native-only on a
|
||||
# runner with no WebKitGTK dev package at all — is the
|
||||
# linux-canvas-smoke job.)
|
||||
- run: zig build test-linux-web-layer-audit
|
||||
|
||||
windows-webview:
|
||||
name: Windows WebView
|
||||
runs-on: windows-2022
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: 0.16.0
|
||||
# Builds the WebView example with the system engine, compiling the
|
||||
# full embedded-WebView host against the vendored WebView2 SDK
|
||||
# header (third_party/webview2). The host refuses to fall back to
|
||||
# the stubbed layer, so a regression — a missing header, a broken
|
||||
# include path, or a conformance error in the embedded layer — is a
|
||||
# compile failure here, not a silent WebViewNotFound at runtime.
|
||||
- run: zig build test-webview-system-link -Dplatform=windows
|
||||
|
||||
cef-platform-tooling:
|
||||
name: CEF Platform Tooling
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: 0.16.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
# The TypeScript core suites transpile at build/test time; without
|
||||
# node they skip silently, so CI must provide it.
|
||||
- run: npm ci --prefix packages/core
|
||||
- run: zig build test-tooling
|
||||
|
||||
npm-package:
|
||||
name: npm Package
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
- run: npm --prefix packages/native-sdk run version:check
|
||||
- run: npm --prefix packages/native-sdk run scripts:check
|
||||
|
||||
native-examples:
|
||||
name: Native Examples
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: 0.16.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
# The TypeScript examples transpile at build time; the transpiler
|
||||
# needs its installed dependency.
|
||||
- run: npm ci --prefix packages/core
|
||||
- name: Install GTK dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y libgtk-4-dev libwebkitgtk-6.0-dev
|
||||
- run: zig build test-examples-native
|
||||
# Declare-to-use, proven on real Windows executables: the
|
||||
# canvas-only ui-inbox cross-compiles without the embedded WebView
|
||||
# layer (no WebView2Loader.dll reference, no loader installed) and
|
||||
# the webview example keeps it.
|
||||
- run: zig build test-windows-web-layer-audit
|
||||
|
||||
linux-canvas-smoke:
|
||||
name: Linux Canvas Smoke
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: 0.16.0
|
||||
# Deliberately NO libwebkitgtk-6.0-dev: ui-inbox declares no web
|
||||
# use, so its host compiles with the WebKitGTK stub seam and this
|
||||
# job doubles as the native-only Linux LINK test — the build can
|
||||
# only succeed if nothing in a native-only app needs the WebKitGTK
|
||||
# headers or pkg-config entry. (The seam under webkit-PRESENT
|
||||
# conditions is the linux-webkitgtk job's ELF cross-audit.)
|
||||
- name: Install GTK and Xvfb
|
||||
run: sudo apt-get update && sudo apt-get install -y libgtk-4-dev xvfb
|
||||
# Drives the gpu_surface software path under Xvfb: snapshot ready,
|
||||
# gpu_backend=software, gpu_nonblank=true, automation widget-click,
|
||||
# a rendered screenshot, an ELF audit that the built binary carries
|
||||
# no WebKitGTK reference, and ZERO WebKit helper processes (canvas
|
||||
# apps never boot WebKit). A11y env, the widened cold-start
|
||||
# readiness budget (shared runners stall ~27 s before the first
|
||||
# runtime event), and failure forensics (dump snapshot + app log)
|
||||
# all live in the script.
|
||||
- name: Build and drive ui-inbox headless
|
||||
run: .github/scripts/linux-canvas-smoke.sh
|
||||
|
||||
windows-canvas-smoke:
|
||||
name: Windows Canvas Smoke (Wine)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: 0.16.0
|
||||
- name: Install Wine, Xvfb, and xdotool
|
||||
run: sudo apt-get update && sudo apt-get install -y wine xvfb xdotool
|
||||
# Cross-compiles ui-inbox for x86_64-windows-gnu and drives the
|
||||
# gpu_surface software path (child HWND + WM_TIMER + SetDIBitsToDevice)
|
||||
# under Wine: snapshot ready, gpu_backend=software, gpu_nonblank=true,
|
||||
# automation widget-click, and real XTEST pointer/keyboard input.
|
||||
# Wineprefix init happens inline in the script (measured 21s from
|
||||
# scratch in an ubuntu-24.04 container, so no cache step).
|
||||
- name: Build and drive ui-inbox.exe under Wine
|
||||
run: .github/scripts/windows-canvas-smoke.sh
|
||||
|
||||
windows-effects-smoke:
|
||||
name: Windows Effects Smoke (Wine)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: 0.16.0
|
||||
- name: Install Wine and Xvfb
|
||||
run: sudo apt-get update && sudo apt-get install -y wine xvfb
|
||||
# Cross-compiles examples/effects-probe for x86_64-windows-gnu and
|
||||
# proves the effect system's live Windows path under Wine: fx.spawn
|
||||
# launches cmd.exe, streamed lines land in the model, the worker's
|
||||
# PostMessageW wake shows up as effects_wake events in the trace
|
||||
# log (frame ticks also drain, so the log is the wake's evidence),
|
||||
# and fx.cancel terminates the child with the line count frozen.
|
||||
- name: Build and drive effects-probe.exe under Wine
|
||||
run: .github/scripts/windows-effects-smoke.sh
|
||||
|
||||
frontend-examples:
|
||||
name: Frontend Examples
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: 0.16.0
|
||||
- run: zig build test-examples-frontends
|
||||
|
||||
mobile-examples:
|
||||
name: Mobile Examples
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: 0.16.0
|
||||
- run: zig build test-examples-mobile
|
||||
|
||||
scaffold:
|
||||
name: Generated App Scaffolds
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: 0.16.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
# The default scaffold is a TypeScript core: its build runs the
|
||||
# @native-sdk/core transpiler from this checkout's own install.
|
||||
- run: npm ci --prefix packages/core
|
||||
- run: zig build
|
||||
- name: Scaffold and test the zero-config native app
|
||||
run: |
|
||||
set -euo pipefail
|
||||
app=".zig-cache/scaffold-native-slim"
|
||||
rm -rf "$app"
|
||||
./zig-out/bin/native init "$app"
|
||||
# Slim scaffold: no build files — the CLI's generated graph drives it.
|
||||
test ! -f "$app/build.zig"
|
||||
test ! -f "$app/build.zig.zon"
|
||||
# The editor surface landed: package.json + tsconfig.json + the
|
||||
# materialized @native-sdk/core copy stock tsc resolves.
|
||||
test -f "$app/package.json"
|
||||
test -f "$app/tsconfig.json"
|
||||
test -f "$app/node_modules/@native-sdk/core/sdk/core.ts"
|
||||
# ...and none of it is build truth: every verb works without it.
|
||||
rm -rf "$app/node_modules"
|
||||
./zig-out/bin/native test "$app" -Dplatform=null
|
||||
./zig-out/bin/native check "$app"
|
||||
# check self-healed the editor copy back into place.
|
||||
test -f "$app/node_modules/@native-sdk/core/sdk/core.ts"
|
||||
./zig-out/bin/native eject "$app"
|
||||
(cd "$app" && zig build test -Dplatform=null)
|
||||
- name: Scaffold and test frontend templates
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for frontend in native next vite react svelte vue; do
|
||||
app=".zig-cache/scaffold-${frontend}"
|
||||
rm -rf "$app"
|
||||
./zig-out/bin/native init "$app" --frontend "$frontend" --full
|
||||
(cd "$app" && zig build test -Dplatform=null && ../../zig-out/bin/native validate app.zon)
|
||||
# Every scaffold ships a CI workflow; parse it as real YAML.
|
||||
test -s "$app/.github/workflows/ci.yml"
|
||||
python3 -c 'import sys, yaml; yaml.safe_load(open(sys.argv[1]))' "$app/.github/workflows/ci.yml"
|
||||
done
|
||||
|
||||
evals-typecheck:
|
||||
name: Evals Typecheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.23.0
|
||||
package_json_file: evals/package.json
|
||||
- run: pnpm install --frozen-lockfile
|
||||
working-directory: evals
|
||||
- run: pnpm typecheck
|
||||
working-directory: evals
|
||||
|
||||
docs:
|
||||
name: Docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.23.0
|
||||
package_json_file: docs/package.json
|
||||
- run: pnpm install --frozen-lockfile
|
||||
working-directory: docs
|
||||
- run: pnpm check
|
||||
working-directory: docs
|
||||
@@ -0,0 +1,255 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
check-release:
|
||||
name: Check for new version
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
should_release: ${{ steps.check.outputs.should_release }}
|
||||
needs_github_release: ${{ steps.check.outputs.needs_github_release }}
|
||||
version: ${{ steps.check.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Compare package.json version to npm and check GitHub release
|
||||
id: check
|
||||
run: |
|
||||
LOCAL_VERSION=$(node -p "require('./packages/native-sdk/package.json').version")
|
||||
echo "Local version: $LOCAL_VERSION"
|
||||
|
||||
NPM_VERSION=$(npm view @native-sdk/cli version 2>/dev/null || echo "0.0.0")
|
||||
echo "npm version: $NPM_VERSION"
|
||||
|
||||
if [ "$LOCAL_VERSION" != "$NPM_VERSION" ]; then
|
||||
echo "Version changed: $NPM_VERSION -> $LOCAL_VERSION"
|
||||
echo "should_release=true" >> "$GITHUB_OUTPUT"
|
||||
echo "needs_github_release=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Version unchanged on npm, skipping publish"
|
||||
echo "should_release=false" >> "$GITHUB_OUTPUT"
|
||||
|
||||
TAG="v$LOCAL_VERSION"
|
||||
EXISTING_ASSETS=$(gh release view "$TAG" --json assets --jq '.assets[].name' 2>/dev/null || true)
|
||||
missing=0
|
||||
for asset in \
|
||||
CHECKSUMS.txt \
|
||||
native-sdk-darwin-arm64 \
|
||||
native-sdk-darwin-x64 \
|
||||
native-sdk-linux-arm64 \
|
||||
native-sdk-linux-x64 \
|
||||
native-sdk-linux-musl-arm64 \
|
||||
native-sdk-linux-musl-x64 \
|
||||
native-sdk-win32-arm64.exe \
|
||||
native-sdk-win32-x64.exe
|
||||
do
|
||||
if ! printf '%s\n' "$EXISTING_ASSETS" | grep -Fx "$asset" >/dev/null; then
|
||||
echo "Missing release asset: $asset"
|
||||
missing=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$missing" -eq 0 ]; then
|
||||
echo "GitHub release $TAG exists with native assets"
|
||||
echo "needs_github_release=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "GitHub release $TAG is missing native assets, will create/update it"
|
||||
echo "needs_github_release=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "version=$LOCAL_VERSION" >> "$GITHUB_OUTPUT"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
github-release:
|
||||
name: Create GitHub Release
|
||||
needs: check-release
|
||||
if: needs.check-release.outputs.needs_github_release == 'true'
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Zig
|
||||
uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: 0.16.0
|
||||
|
||||
- name: Extract changelog entry
|
||||
run: |
|
||||
VERSION="${{ needs.check-release.outputs.version }}"
|
||||
awk '/<!-- release:start -->/{found=1; next} /<!-- release:end -->/{exit} found{print}' CHANGELOG.md > /tmp/release-notes.md
|
||||
|
||||
LINES=$(wc -l < /tmp/release-notes.md | tr -d ' ')
|
||||
if [ "$LINES" -lt 2 ]; then
|
||||
echo "Error: No release notes found between <!-- release:start --> and <!-- release:end --> markers in CHANGELOG.md"
|
||||
exit 1
|
||||
fi
|
||||
echo "Extracted release notes for $VERSION ($LINES lines)"
|
||||
|
||||
- name: Build native release assets (all platforms)
|
||||
run: |
|
||||
# Cross-compiles the CLI for all eight platforms and writes the
|
||||
# flat release assets + CHECKSUMS.txt into zig-out/release/.
|
||||
bash packages/native-sdk/scripts/build-binaries.sh
|
||||
|
||||
- name: Create GitHub Release
|
||||
run: |
|
||||
VERSION="${{ needs.check-release.outputs.version }}"
|
||||
TAG="v$VERSION"
|
||||
|
||||
if gh release view "$TAG" &>/dev/null; then
|
||||
echo "Release $TAG already exists, updating assets"
|
||||
else
|
||||
echo "Creating release $TAG..."
|
||||
gh release create "$TAG" \
|
||||
--title "$TAG" \
|
||||
--notes-file /tmp/release-notes.md
|
||||
fi
|
||||
|
||||
gh release upload "$TAG" \
|
||||
zig-out/release/native-sdk-* \
|
||||
zig-out/release/CHECKSUMS.txt \
|
||||
--clobber
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
publish:
|
||||
name: Publish CLI to npm
|
||||
needs: [check-release, github-release]
|
||||
if: >-
|
||||
always()
|
||||
&& needs.check-release.outputs.should_release == 'true'
|
||||
&& (needs.github-release.result == 'success' || needs.github-release.result == 'skipped')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
environment: Release
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
# Publishing uses npm trusted publishing (OIDC): the job's id-token
|
||||
# permission lets npm mint short-lived credentials, so no npm token
|
||||
# secret exists anywhere in this repo. All nine packages —
|
||||
# @native-sdk/cli plus the eight @native-sdk/cli-* platform packages
|
||||
# under packages/native-sdk/npm/ — must each be configured on
|
||||
# npmjs.com with a GitHub Actions trusted publisher pointing at
|
||||
# repository vercel-labs/native, workflow release.yml,
|
||||
# environment Release. If a package is missing that configuration,
|
||||
# npm publish fails loudly with an OIDC authentication error before
|
||||
# anything is uploaded for that package. Trusted publishing requires
|
||||
# npm >= 11.5.1, satisfied by the npm bundled with Node 24.
|
||||
# @native-sdk/core (packages/core) joins that publish set the moment
|
||||
# its "private" flag drops (see the publish step below) — its
|
||||
# npmjs.com trusted-publisher configuration must exist BEFORE that
|
||||
# flip lands.
|
||||
|
||||
- name: Check version sync
|
||||
run: npm --prefix packages/native-sdk run version:check
|
||||
|
||||
- name: Check package scripts
|
||||
run: npm --prefix packages/native-sdk run scripts:check
|
||||
|
||||
- name: Download release binaries
|
||||
run: |
|
||||
VERSION="${{ needs.check-release.outputs.version }}"
|
||||
mkdir -p /tmp/native-sdk-release
|
||||
gh release download "v$VERSION" \
|
||||
--pattern 'native-sdk-*' \
|
||||
--pattern 'CHECKSUMS.txt' \
|
||||
--dir /tmp/native-sdk-release
|
||||
(cd /tmp/native-sdk-release && sha256sum -c CHECKSUMS.txt)
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Stage binaries into the platform packages
|
||||
run: |
|
||||
# Release-asset name -> npm platform package (same table as
|
||||
# packages/native-sdk/scripts/build-binaries.sh).
|
||||
stage() {
|
||||
asset="$1"; key="$2"; ext="$3"
|
||||
mkdir -p "packages/native-sdk/npm/$key/bin"
|
||||
cp "/tmp/native-sdk-release/$asset" "packages/native-sdk/npm/$key/bin/native$ext"
|
||||
chmod 755 "packages/native-sdk/npm/$key/bin/native$ext"
|
||||
}
|
||||
stage native-sdk-darwin-arm64 darwin-arm64 ""
|
||||
stage native-sdk-darwin-x64 darwin-x64 ""
|
||||
stage native-sdk-linux-arm64 linux-arm64-gnu ""
|
||||
stage native-sdk-linux-x64 linux-x64-gnu ""
|
||||
stage native-sdk-linux-musl-arm64 linux-arm64-musl ""
|
||||
stage native-sdk-linux-musl-x64 linux-x64-musl ""
|
||||
stage native-sdk-win32-arm64.exe win32-arm64 ".exe"
|
||||
stage native-sdk-win32-x64.exe win32-x64 ".exe"
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
VERSION="${{ needs.check-release.outputs.version }}"
|
||||
|
||||
# Platform packages first, so the main package's
|
||||
# optionalDependencies pins are resolvable the moment it lands.
|
||||
# Re-runs skip anything already on the registry at this version.
|
||||
publish_dir() {
|
||||
dir="$1"
|
||||
name=$(node -p "require('./$dir/package.json').name")
|
||||
if npm view "$name@$VERSION" version >/dev/null 2>&1; then
|
||||
echo "$name@$VERSION already published, skipping"
|
||||
return 0
|
||||
fi
|
||||
(cd "$dir" && npm publish --provenance --access public)
|
||||
}
|
||||
|
||||
for dir in packages/native-sdk/npm/*/; do
|
||||
publish_dir "${dir%/}"
|
||||
done
|
||||
|
||||
# @native-sdk/core (packages/core) rides the same version and
|
||||
# release gate as the CLI (sync-version.js stamps it; the
|
||||
# version check above refuses a half-bumped tree).
|
||||
#
|
||||
# ============================ LOUD FLIP REQUIREMENT ==========
|
||||
# packages/core/package.json keeps "private": true until the
|
||||
# 0.5.0 cut BY DESIGN, and that field IS the publish switch:
|
||||
# while private, the guard below skips the package (npm would
|
||||
# refuse to publish it anyway); the 0.5.0 change that drops
|
||||
# "private" makes this same step start publishing it with no
|
||||
# workflow edit. Before dropping the flag, configure the
|
||||
# @native-sdk/core trusted publisher on npmjs.com (see the note
|
||||
# above) or the first public release fails here.
|
||||
# =============================================================
|
||||
if [ "$(node -p "require('./packages/core/package.json').private === true")" = "true" ]; then
|
||||
echo "@native-sdk/core is private (pre-0.5.0): skipping publish by design"
|
||||
else
|
||||
publish_dir packages/core
|
||||
fi
|
||||
|
||||
publish_dir packages/native-sdk
|
||||
@@ -0,0 +1,46 @@
|
||||
.DS_Store
|
||||
.zig-cache/
|
||||
zig-out/
|
||||
# CLI-generated build graph for zero-config apps (the examples here) — the
|
||||
# same entry `native init` writes into a new app's .gitignore.
|
||||
.native/
|
||||
|
||||
# Native binaries in the @native-sdk/cli npm packages (built by CI or locally)
|
||||
packages/native-sdk/bin/native-sdk-*
|
||||
packages/native-sdk/npm/*/bin/
|
||||
# SDK payload mirrored into the npm package at pack time (copy-framework.js)
|
||||
packages/native-sdk/src/
|
||||
packages/native-sdk/assets/
|
||||
packages/native-sdk/skills/
|
||||
packages/native-sdk/skill-data/
|
||||
packages/native-sdk/build/
|
||||
packages/native-sdk/build.zig
|
||||
packages/native-sdk/build.zig.zon
|
||||
packages/native-sdk/app.zon
|
||||
packages/native-sdk/third_party/
|
||||
packages/native-sdk/packages/
|
||||
packages/native-sdk/LICENSE
|
||||
# npm pack output
|
||||
packages/native-sdk/*.tgz
|
||||
packages/native-sdk/npm/*/*.tgz
|
||||
|
||||
# Downloaded/prepared CEF runtimes are large local artifacts.
|
||||
third_party/cef/macos/
|
||||
third_party/cef/windows/
|
||||
third_party/cef/linux/
|
||||
|
||||
# Compiled static libraries
|
||||
*.a
|
||||
|
||||
# TypeScript build info (generated)
|
||||
docs/tsconfig.tsbuildinfo
|
||||
.claude/
|
||||
|
||||
# Dev-tool dependency trees (installed per package, never committed)
|
||||
packages/core/node_modules/
|
||||
|
||||
# The CLI-materialized editor copy of @native-sdk/core inside the TS example
|
||||
# apps (node_modules is editor surface, never source — the same entry the
|
||||
# TS scaffold writes into a new app's .gitignore)
|
||||
examples/soundboard-ts/node_modules/
|
||||
examples/system-monitor-ts/node_modules/
|
||||
@@ -0,0 +1,33 @@
|
||||
# Agent Guide
|
||||
|
||||
Guidance for agents (and humans) working on this repository.
|
||||
|
||||
## Build, test, and gate
|
||||
|
||||
```bash
|
||||
zig build test # root engine + runtime suites
|
||||
zig build validate # sample app.zon manifest check
|
||||
zig build test-example-<name> # one example's suite (e.g. test-example-notes)
|
||||
scripts/gate.sh fast [ref] # affected-only local gate for your diff (default base: main)
|
||||
scripts/gate.sh full # everything CI-shaped that runs locally
|
||||
```
|
||||
|
||||
Run `scripts/gate.sh fast` before finishing any change; it maps your diff to the suites that cover it. The docs site checks with `pnpm --dir docs check` (the gate runs it only when `docs/` changed).
|
||||
|
||||
This repository builds with Zig 0.16.0. If a build fails with "no member named" errors on std APIs (`std.fs.cwd`, `ArrayList.init`, `std.io`), you are writing pre-0.16 idioms — `skill-data/zig/SKILL.md` maps each such compile error to the current idiom as this codebase writes it.
|
||||
|
||||
Pinned goldens (pixel signatures, schema fingerprints, command counts) are updated deliberately: review the rendered output or the counted commands first, and keep the pin's comment a self-contained description of what the value represents.
|
||||
|
||||
## Changelog
|
||||
|
||||
Do not edit `CHANGELOG.md` directly. Each user-visible change ships a fragment in `changelog.d/` — see `changelog.d/README.md` for the format and voice. Internal-only polish needs no fragment.
|
||||
|
||||
## Where things live
|
||||
|
||||
- `src/` — the engine and runtime; `src/primitives/canvas/` holds the widget, markup, and vector core.
|
||||
- `examples/` — the showcase apps, most zero-config (`app.zon` + `src/`).
|
||||
- `docs/` — the documentation site; `docs/AGENTS.md` has its MDX conventions.
|
||||
- `skills/` and `skill-data/` — the agent skills the CLI ships (`native skills list`).
|
||||
- `tools/` and `scripts/` — dev tooling and the local gate.
|
||||
|
||||
Releases are maintainer-run; see [RELEASING.md](./RELEASING.md).
|
||||
@@ -0,0 +1,395 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to the Native SDK (formerly zero-native) will be documented in this file.
|
||||
|
||||
## 0.5.0
|
||||
|
||||
<!-- release:start -->
|
||||
|
||||
### New Features
|
||||
|
||||
- **TypeScript authoring — write app cores in TypeScript**: `native init` now scaffolds a TypeScript app by default — `src/core.ts` (logic), `src/app.native` (view), `app.zon` (manifest), zero Zig to write — and the build transpiles the core to readable arena-backed Zig, so the shipped binary carries no JS engine and no GC and keeps the native dispatch path (~83ns per update). The `@native-sdk/core` package (the SDK cores import, plus the transpiler the CLI runs) publishes to npm with this release at the SDK's version. Zig cores remain first-class (`native init --template zig-core`), mixed TS-core + Zig-helper apps fall out of tree detection naturally, and a TypeScript app can eject to its emitted Zig at any time. The entries below are the pieces of this one feature (#119).
|
||||
- **examples/ai-chat-ts — an AI chat client authored entirely in TypeScript + Native markup**: a conversation UI over an OpenAI-compatible chat-completions endpoint as two subset modules and zero Zig — `Cmd.fetch` with routed `{status, body}` results, the JSON wire format as pure byte math (`src/api.ts` encodes requests and parses `choices[0].message.content` / `error.message`, refusing anything malformed), conversation history in the Model, the composer on the SDK byte-splice text engine, endpoint/model/key through the env channel (no baked endpoint, no key anywhere in the tree — a teaching state until all three arrive), a model-level in-flight guard, and honest failed states that keep the history with a Retry. The e2e battery (`tests/ts-core/ai_chat_e2e_tests.zig`, in `zig build test-ts-core-e2e`) pins the exact request bytes turn by turn and replays a recorded conversation — transport failure and retry included — byte-identically with zero network; the README states the v1 boundaries plainly (buffered responses, compile-time fetch headers) (#119).
|
||||
- **Docs: "Where Packages Go"** (`/typescript/packages`): the four first-class patterns behind "can I use npm?" — HTTP/AI APIs through `Cmd.fetch` (with a complete transpile-checked core sample), npm-heavy UIs as embedded web frontends, Node libraries as `Cmd.spawn` sidecars, and pure utilities vendored under `src/` or imported from the curated `@native-sdk/core/*` channel — with the core's no-npm boundary stated as the thing that buys replay, headless testing, and native dispatch speed (#119).
|
||||
- **Eval wave 2 — six dual-track authoring cases, one language-blind spec each**: the eval harness gains `"frontend": "app-dual"` cases that run the SAME realistic ask on both authoring tracks — `<case>@ts` scaffolds a full TypeScript app (`native init --template ts-core`), `<case>@zig` the Zig app template — graded by shared checks plus per-track behavioral harnesses asserting one spec: fetch-JSON-into-a-sortable-table, debounced notes autosave (starter provided), a pomodoro timer with a completion sound, a seeded stale-cache delete bug to root-cause, a module split with byte-exact CSV export, and a shell-command system-info panel. The ts harnesses decode the Cmd/Sub wire format (`evals/harness-lib/cmdview.zig`); the zig harnesses ride the SDK's fake effects executor inside the workspace's own `native test` graph. `--track ts|zig` selects a lane; each track gets its current skills (`ts-core`+`native-ui` vs `native-ui`+`zig`), and `pnpm metrics` now reports per-track teaching-error encounters alongside first-pass compliance and the violation taxonomy (#119).
|
||||
- **Transpiler: two real-app fixes the wave surfaced**: byte-element stores of computed values (`buf[i] = src[j]`) now emit with JS's exact ToUint8 wrap instead of stopping as invalid Zig, and records referenced from a declared text-input mirror union stay by value even when the core also stores its editor state in the Model — previously that pointer promotion silently broke `on-input` resolution (runtime view build and `native check`'s model contract alike) for any core keeping a `TextEditState` in its model (#119).
|
||||
- **examples/soundboard-ts — the soundboard authored entirely in TypeScript + Native markup**: the launch-bar port of the Zig soundboard as three files of truth and zero Zig — `src/core.ts` (the committed catalog as const tables, REAL audio through the `Cmd.audioPlay` stream with the engine's local-then-URL cascade, the play-next queue, Copy Title on the clipboard, a motion-gated `Sub.timer` playback clock, and search through the full byte-splice text engine), `src/app.native` (the whole view: grid, album detail, songs library, context menus, the now-playing transport), and `app.zon`. An end-to-end suite (`zig build test-ts-core-e2e`) drives the shipping core and markup through playback, auto-advance, the stale-event window, volume, clipboard, search, record→replay, and a dispatch-latency budget; the README carries an honest ledger of where the port diverges from the Zig original (#119).
|
||||
- **Generated TS wiring resolves the theme and the audio cache**: app.zon's `.theme` pack now reaches TypeScript apps (composed with the live system appearance), and the platform caches directory is resolved at launch so a core's URL audio playback caches under the conventional content-addressed path with no `cachePath` in the core (#119).
|
||||
- **Transpiler emit-contract fixes**: the global `undefined` VALUE now emits the optional empty (`null`) — it previously emitted Zig `undefined`, uninitialized memory — and an early-exit null guard whose narrowed value goes unread emits a plain null test instead of an unused (uncompilable) capture (#119).
|
||||
- **examples/system-monitor-ts — the system monitor authored entirely in TypeScript + Native markup**: the spawn-showcase port of the Zig system monitor as three files of truth and zero Zig — `src/core.ts` (the 2 s sampling cadence as a declarative `Sub.timer`, collect-mode `Cmd.spawn` for `ps`/`vm_stat`/`meminfo`, pure byte parsers over the collected stdout, the exact top-128-by-CPU selection, the confirmed SIGTERM action, and a runtime boot probe that discovers the host's sampler conventions where the Zig original switches at comptime — with the honest "no sampler for this OS" state when nothing answers), `src/app.native` (the whole view: chromeMsg-driven hidden-inset header, `<chart>` sparklines over the core's NaN-padded windows, the real table register with per-row context menus and controlled scroll, the modal confirmation), and `app.zon`. An end-to-end suite (`zig build test-ts-core-e2e`) drives the shipping core and markup through the probe cascade, the Zig example's committed real captures, the timer cadence with pause, search/sort, the kill round trip, and record→replay; the README carries an honest ledger of where the port diverges from the Zig original (#119).
|
||||
- **Markup chart series bind f64 iterables**: `<series values="{binding}">` now accepts `[]const f64` model fields, decls, and fns alongside f32 — transpiled TS cores carry every number array as f64, so markup charts were unreachable from a TS model — narrowed per sample into the f32 chart pipeline in both engines, the validator, and `native check`'s model contract (#119).
|
||||
- **The everyday string methods on core bytes — familiar spellings, byte-honest semantics**: TypeScript cores can now write `s.toUpperCase()`, `s.trim().toLowerCase().includes(q)`, `s.repeat(n)`, `s.padStart(w)`, `s.split(sep)`, `s.startsWith(p)`, `s.indexOf(t)`/`s.lastIndexOf(t)`, and `s.at(i)` directly on `Uint8Array` text. Every length/offset is a BYTE measure, search is byte-wise (`includes`/`indexOf`/`lastIndexOf` dispatch by argument type: bytes needle = substring search, number = the TypedArray element search), case mapping is Unicode 17 SIMPLE case mapping (locale-free, generated tables — 3.1 KiB in the binary — with invalid UTF-8 passing through unchanged), trim strips the exact JS whitespace set over UTF-8, and `split` returns a locally-owned `Uint8Array[]`. Native lowers each call onto rt kernel helpers; node runs the same methods from the same generated tables (devhost polyfill), so both runtimes produce identical bytes by construction — pinned by run-fidelity cases across Greek/Cyrillic casing, growth mappings, invalid-UTF-8 passthrough, repeat/pad edges, and split shapes, plus a machine-checked method matrix (#119).
|
||||
- **The stays-out spellings teach their reason**: `charCodeAt`/`charAt`/`codePointAt`/`normalize`/`replace`/`replaceAll` on bytes teach the new NS1060 (byte text speaks the byte-honest method set), the locale family teaches NS1005, and the regex-taking methods teach NS1040 — never a bare "property does not exist". `trimAsciiSpaces` stays for LF-preserving line parsing; `.trim()` is the canonical whitespace trim (#119).
|
||||
- **TS cores: generics, local function values, and the complete-language tail**: user-declared generic functions, interfaces, and type aliases now compile via per-call-site monomorphization from tsc's own resolved type arguments — one readable Zig fn per distinct instantiation (`pick__Task`, `pick__f64`), deduped, covering records/unions/arrays/optionals, recursion inside a generic, generics calling generics, and structural instantiation of generic types (`Box<Task>` → `Box__Task`); unresolvable call sites teach the new NS1053 (#119).
|
||||
- **Const-bound local function values hoist**: `const scale = (x: number): number => x * 3;` (arrow or function expression) becomes an ordinary module-level fn when capture-free and fully annotated, usable by direct call (recursion included) or as an array-method callback (`xs.map(scale)`); captures, reassignment, storing/returning the value, and record-field calls teach the new NS1054 — and capturing a locally-owned array ends its ownership at the capture (NS1051 machinery) (#119).
|
||||
- **The small-fry tail lands with node-byte-identical pins**: `for (const [i, x] of xs.entries())` (the destructured-pair loop form), `++`/`--`/assignments in provably order-exact value positions (`arr[i++]`, `const n = ++count`), `?.[i]` and `?.method()` optional-chain hops on supported receivers, plain `number`/`string` switch scrutinees (if/else chains with JS strict-equality and default-position semantics), and `typeof CONST` type-query aliases. Float-valued template holes stay deferred — there is no JS-exact f64 formatter in the runtime yet, and node/native divergence is never an option (#119).
|
||||
- **TS cores: exceptions and data classes — the complete language, tier 2**: `throw`/`try`/`catch`/`finally` compile as deterministic control flow — a thrown subset value (one error shape per core, the new NS1057) unwinds through a native payload slot to the nearest catch, across helper calls and out of `.map`/`.filter` callbacks; `finally` lowers to a scoped defer running on every exit (control flow inside it teaches the new NS1058, JS's own no-unsafe-finally), the catch binding narrows once (`const err = e as ParseError;`), rethrow and nested try work, and an uncaught throw is a defined panic at the exported boundary — exactly where node's process would crash. Node-parity is pinned in the run-fidelity corpus, throw-mid-mutation of an owned array included (#119).
|
||||
- **Data classes, no inheritance**: `class Task { fields; constructor; methods }` emits as a plain struct plus module-level functions; `new Task(...)` constructs a record-shaped value, `this` reaches fields and methods, and instance mutation (field writes, `this`-writing methods) follows the same local-ownership rule as arrays (NS1001/NS1051 at the boundaries). The class tail teaches by name: `extends`/`super`/`abstract` (new NS1055), accessors/statics/privates/`#`/class expressions/escaping `this` (new NS1056), generic classes (NS1053), `instanceof` (NS1041); class instances stay local values — Model storage teaches toward records (flagged follow-up) (#119).
|
||||
- **Two reconcile potholes**: arrays OF byte buffers (`const parts: Uint8Array[] = []`) now route through array ownership — `parts.push(chunk)` on your own array works and runs node-identically (bytes VALUES keep their own discipline) — and a bare reference to a module-level function or const helper is a callback (`xs.map(encodeTurn)`, `xs.toSorted(byAscending)` — the same lowering as the arrow spelled inline), plus shorthand members in union literals (`{ kind: "range", v }`) (#119).
|
||||
- **TypeScript apps are the `native init` default**: the scaffold is three files of truth and zero Zig — `src/core.ts` (logic), `src/app.native` (view), `app.zon` (manifest) — and the build detects the core from the tree (a `src/core.ts` transpiles at build time through generated wiring; `src/main.zig` stays the Zig core; both at once is a teaching error). The Zig template remains first-class via `native init --template zig-core` (#119).
|
||||
- **`native dev --core`**: the TypeScript core-logic loop under node — dispatch Msgs as JSON lines, watch the model and effect transcript, run Sub timers and Cmd.delay on a virtual clock. Logic only, honestly not a renderer; `native dev` runs the real app (#119).
|
||||
- **`native check` checks TypeScript cores**: a `src/core.ts` runs the @native-sdk/core subset checker first (NS diagnostics verbatim), then markup and app.zon as before — with a fresh model contract the markup pass type-checks bindings against the core's emitted model (#119).
|
||||
- **Markup text input reaches TypeScript cores**: a core declares its own `TextInputEvent` mirror union and `on-input` matches it structurally, translating each runtime event into the core's union at dispatch — markup text field to TS `update` to re-render, end to end (#119).
|
||||
- **Exported model helpers bind from markup**: an exported single-Model-parameter helper also emits as a Model declaration (`doneCount` binds as `{done_count}`), and `export const viewUnbound = [...]` emits the `view_unbound` lint opt-out — NS1031/NS1032 teach the collision and typo cases (#119).
|
||||
- **TS cores: runtime fetch header values and journaled env deliveries** — the two gaps the ai-chat example surfaced, closed. `Cmd.fetch` header VALUES may now be runtime bytes (`{ authorization: bearerToken(model.apiKey) }` — header NAMES stay compile-time ASCII, NS1029/NS1030 bounds still gate what is knowable at build time and the engine err-arm rejects the rest); no wire change was needed — record 0x09 always carried values as length-prefixed byte fields built at dispatch time, only the emitter's literal-only rule and the node SDK's `FetchSpec` type moved. And the `envMsgs` channel now journals each launch delivery (an additive `.env` effect record: value in `payload`, arm name in `stderr_tail`, dispatch index in `key`), so replay feeds the RECORDED values with zero env reads — a session recorded with credentials set replays byte-identically on a machine where they are unset or different; journals without env records (older recordings) re-derive from the launch configuration exactly as before. `examples/ai-chat-ts` now sends a real `Authorization: Bearer <key>` header instead of the access_token query-parameter workaround, and its replay e2e drives both the unset-env and changed-env launches. Plus NS1051: an un-annotated spread local (`const turns = [...model.turns, next]`) now teaches its array-type annotation instead of the generic emit-time stop (#119).
|
||||
- **TypeScript subset: grammar completeness**: the subset now means "TypeScript minus the ecosystem minus the purity violations" — never minus basic syntax. New in the mapping: `do...while` (body-first, `continue` re-tests, exactly node), labeled statements with labeled `break`/`continue` (loops and blocks, lowered to Zig labels), `default` arms on union-`kind` switches (the `else` prong over unnamed arms; dead-code defaults emit nothing), `**`/`**=` (JS pow corners pinned: NaN exponents, `±1 ** ±Infinity`, right-associativity), the shifts `<< >> >>>` and `~` (ToInt32 with the count masked & 31), the full compound-assignment family (`*= /= %= &= |= ^= <<= >>= >>>=` plus guarded `&&= ||= ??=`), unary `+`, const record destructuring (`const { total, done: doneCount } = stats;`), namespace imports over the core's own modules (`import * as util` — values, calls, and qualified types resolve to the flat emitted names), multi-counter for-inits and comma incrementors (`for (let lo = 0, hi = n; lo < hi; lo++, hi--)`), countdown incrementors (`i--`), hole-free template literals, `satisfies`, and the empty statement (#119).
|
||||
- **Every exclusion now teaches**: twelve new rules close every generic-error hole — NS1039 (namespace aliases are dot-syntax, SDK intrinsics import by name), NS1040 (regexes), NS1041 (runtime type/shape tests: `typeof`/`in`/`instanceof`/`Object.*`), NS1042 (generators), NS1043 (comma/`void`/assignment-as-value), NS1044 (BigInt/Symbol), NS1045 (destructuring beyond const record fields), NS1046 (nested/stored function values and `?.()`), NS1047 (default exports, `export =`, value re-exports), NS1048 (loose `==`), NS1049 (`var`), NS1050 (generic declarations) — and NS1019 broadens to the full fixed-arity story (rest params, `arguments`, call spreads). `var` and generic helpers previously emitted broken Zig silently; both now stop with teachings, and same-file type/value homonyms are caught by NS1038 instead of colliding in the emitted module (#119).
|
||||
- **The grammar matrix**: a new machine-checked suite (`packages/core/test/grammar_matrix.test.ts`) enumerates every statement, operator, expression, and declaration form of the language and pins each to its verdict (SUPPORTED emits and compiles under Zig; BANNED names its teaching rule; tsc-rejected stays tsc's), so the grammar can never grow a silent gap again. New run-fidelity corpora pin the node-vs-native behavior of every new mapping (pow corners, shift wrapping, `-0` preservation, do-while ordering, labeled-jump targets, default-arm matching, destructured aliases, `??=` on 0-vs-null) (#119).
|
||||
- **Inference fixes surfaced by the matrix**: the float-demotion fixpoint no longer terminates one pass early (a two-hop chain — field → destructured alias → local — previously stranded a phantom NS1016 conflict), and shorthand `{ x }` properties now wire the VALUE symbol into inference (a shorthand from a host-boundary parameter previously kept a false integer proof and emitted mismatched Zig) (#119).
|
||||
- **Stock-IDE support for TypeScript apps, working before the npm publish**: `native init` scaffolds `package.json` (the app's name plus an exact `@native-sdk/core` pin) and `tsconfig.json` (the checker's own compiler options, so editor errors match `native check` reality), and the CLI materializes `node_modules/@native-sdk/core` itself — a copy of exactly what the published package will contain — so VS Code et al. resolve `@native-sdk/core` and `@native-sdk/core/text` with full IntelliSense today. `native check|dev|build|test` keep the copy fresh (one info line on refresh) and `native doctor` reports version skew; once the package is on npm, a plain `npm install` writes identical content and the CLI recognizes the version and leaves it alone. None of it is build truth: builds transpile against the SDK checkout and work with node_modules deleted, tree detection still keys on `src/core.ts` alone, and the Zig template is untouched (#119).
|
||||
- **@native-sdk/core is publish-shaped**: `files: ["sdk"]`, a typed exports map for `.` and `./text`, no bin and no runtime dependencies (the transpiler dependency moved to devDependencies), with a package test pinning the manifest shape. The soundboard-ts and system-monitor-ts example ports carry the same editor surface, and a ts-core e2e suite proves the whole contract with the real tsc: fresh scaffold and both ports typecheck with zero injected paths, and the transpiler still takes the core clean after `rm -rf node_modules` (#119).
|
||||
- **TS cores: mutating array methods are legal on locally-owned arrays**: an array your function creates (a literal or a `.slice()`/`.map()`/`.filter()`/`.concat()`/`.toSorted()` copy) now takes the full mutating set — `push` (any seed, not just the empty builder), `pop`/`shift` (returning the find-miss optional), `unshift`, `splice` (JS index clamping, the removed array as its value), `reverse`, `fill`, in-place `sort` (the stable toSorted machinery applied in place), and indexed writes — with node-byte-identical semantics pinned by a new run-fidelity corpus (parser stacks, splice corners, shift/unshift order, sort stability, pop-on-empty) and a machine-checked mutation matrix beside the grammar matrix (#119).
|
||||
- **Teaching errors now fire only at the true semantic boundaries**: shared data keeps NS1001/NS1022 (both rewritten around ownership, NS1022 naming the now-legal `const copy = xs.slice(); copy.sort(cmp);` idiom), and the new NS1051 teaches mutation after an escape — returned from a callback, passed to a call, stored, or aliased — with the escape kind and line named; an escape inside a loop gates the whole loop body, and early-exit returns stay legal (#119).
|
||||
- **TypeScript cores reach the full app surface**: markup sliders and split dividers deliver their applied 0..1 fraction to a core's one-number float arm (`on-change="scrubbed"` — scrub-to-seek from markup), controlled scroll round-trips through a core-declared `ScrollState` mirror, and the generated wiring detects five host-event channels from plain exports — `frameMsg` (presented frames), `keyMsg` (the app-level key fallback), `appearanceMsg` and `chromeMsg` (system appearance and hidden-titlebar geometry as Msg arms), and `envMsgs` (launch environment variables as journaled boot Msgs) — plus `app.zon` `.assets.images` registered at launch as the `ImageId`s markup avatars bind (#119).
|
||||
- **Export lists and value re-exports compile in TypeScript cores**: `export { a, b as c }` and `export { x } from "./m.ts"` now bind real names over existing declarations in the flat emitted namespace — un-renamed entries export the declaration itself, renames emit a `pub const c = a;` alias, and re-export chains resolve end to end (node ≡ native, pinned by run-fidelity). Renamed entry-module helpers join the markup binding surface under their exported names; NS1047 narrows to the genuinely unsound tail (`export default`, `export =`, `export * from`, renamed generics/classes, wiring config, SDK re-exports), and NS1014/NS1038 keep entry points and name uniqueness honest across the new forms (#119).
|
||||
- **Heterogeneous throws with a narrowing catch**: a core may now throw several distinct kind-tagged shapes — the checker collects them into an implicit thrown union (or a declared union whose arms match), the payload slot is that union, and `catch (e)` narrows it with plain kind tests (`if (e.kind === "parse")`) with no `as` ceremony; rethrow re-raises the bound value, narrowing works across callback boundaries, and NS1057 narrows to genuinely unsound throws (untagged values in a mix, tag collisions, untyped escapes, `new Error`). All pinned node ≡ native by run-fidelity (#119).
|
||||
- **Class statics and erased privacy**: `static` methods lower to receiver-less module functions under the class's mangled names (`Task.fromRow(...)` -> `Task__fromRow`), `static readonly` fields with initializers become module consts (`Task.LIMIT`), and `private`/`protected` keywords are accepted and erased (tsc enforces them at the type level — their whole meaning). Mutable statics teach NS1010 (module state), `this` inside a static member teaches toward the class name, and `#`-fields stay taught; NS1056 narrows accordingly. Pinned node ≡ native by run-fidelity (#119).
|
||||
- **Three mutation loosenings**: `xs[xs.length] = v` on an owned array is a push (the one growth shape; compound forms stay taught), a reassigned `let` stays owned when every assignment installs a fresh owning construction (each reassignment resets the emitted builder), and passing an owned array into a `readonly T[]` parameter is a BORROW when the callee provably only reads it (coinductive analysis — recursion over borrowed slices stays legal; returns, stores, casts to mutable, and onward passes into mutable positions still end ownership). NS1001/NS1051 copy updated; mutation matrix rows moved and extended; all pinned node ≡ native (#119).
|
||||
- **Accurate teaching for the Array statics (NS1059)**: `Array.from`/`Array.of`/`Array.fromAsync` now teach their own construction rewrites (the literal, the spread copy, the push-builder loop) instead of the generic runtime-shapes copy; `Array.isArray` keeps NS1041 — it really is a runtime type test. Comma expressions in value position stay taught (NS1043): the split-statement lowering is only JS-order-exact in the pinned positions, so they did not fall out of the machinery for free (#119).
|
||||
|
||||
### Improvements
|
||||
|
||||
- **The components reference is markup-first**: every component page leads with its Native markup sample (all fences validated against the live `native markup check`), interactive samples show the core side in both authoring languages behind the TS | Zig toggle (accordion, checkbox, radio, dialog, input, scroll, select, slider, split, chart), and the builder form moves to a consistent "Programmatic construction (Zig)" section at the end of each page — real API docs, framed as the Zig tier's programmatic alternative. The section index tells the one-language story, and four samples that shipped unnamed text controls now carry accessible names (#119).
|
||||
- **The TypeScript authoring package is `@native-sdk/core`**: the transpiler package moved from `packages/ts-app-core` to `packages/core` under its real npm name — cores were already importing `@native-sdk/core`, and the dev-harness resolver now maps that one specifier straight onto the package's own SDK module (#119).
|
||||
- **TS-first docs with a TS | Zig toggle**: code samples on the flagship pages (Quick Start, App Model, Native UI, State & Data Flow) show TypeScript first with the Zig form one tab away — the reader's language choice is remembered site-wide — and the new [TypeScript Cores](https://native-sdk.dev/typescript) page covers the app-core subset, Cmd/Sub effects, text-is-bytes, the node dev loop, capacity knobs, and the eject story. Toolkit-extension pages stay Zig on purpose: that tier is the machinery itself (#119).
|
||||
- **Docs code presentation**: the TypeScript | Zig code toggle now renders as the same segmented pill control as the component previews' Default | Geist theme-pack toggle (one shared primitive, identical in dark mode), and code samples can carry a filename header — a file glyph plus the path (```` ```ts:src/core.ts ````), integrated into the toggle's header bar opposite the segments — applied across the quick-start, TypeScript, app-model, native-ui, and config pages' complete-file samples; copied markdown renders the path as a labeled line above a plain fence (#119).
|
||||
- **Markup binds your model's field names exactly as you wrote them**: TypeScript cores now emit Zig with the TS spellings intact — fields, exported single-model helpers, Msg payload records, and locals alike — so `nextId` binds as `{nextId}` and `doneCount` as `{doneCount}`, ending the dual-naming rule (camelCase in core.ts, snake_case in app.native) that every author had to hold in their head. Zig cores are untouched: their fields were already the names markup binds. The whole pipeline follows from the one change — the model contract, `native check`'s typed pass, both markup engines, hot reload, and the eject story (the emitted module now mirrors your source, and markup keeps binding the same names after you adopt it) (#119).
|
||||
- The TS-track host surfaces that matched emitted names structurally now speak the TS SDK spellings (`timestampMs`/`intervalMs`, `colorScheme`/`reduceMotion`/`highContrast`, `tabsProjected`, the audio arm's `positionMs`/`durationMs`), and the declared scroll-state mirror accepts the canvas spelling or the TS spelling — never a mix (#119).
|
||||
- NS1031 collisions are now exact-name collisions (`doneCount` the helper vs `doneCount` the field); `viewUnbound` entries were already the TS names and stay so. Scaffold templates, both ports' views, the ai-chat view, docs, and the skill teach the one rule (#119).
|
||||
- **zig-core starter parity**: `native init --template zig-core` now scaffolds the same app as the TypeScript template — counter, a ticking switch driving a repeating 1s `fx.startTimer`, a Stamp button reading the journaled clock (`fx.wallMs`), a bindable `total` helper, and the matching markup — with generated full-loop tests covering the timer and clock seams; the quick-start code toggle now shows both starters verbatim (#119).
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Packaging fails loudly when signing fails**: `native package --signing adhoc|identity` no longer exits 0 while shipping an unsigned bundle — output paths with spaces (`--output "My App.app"`) now sign correctly (the signing pipeline execs argv arrays instead of shell strings, which also unbreaks spaced paths and spaced identities in the notarization helpers), every signed bundle must pass `codesign --verify --deep --strict` before packaging succeeds, any codesign failure stops the package with codesign's own reason, and the package report proves the outcome with a `signing: adhoc (signed, verified)` line (#118).
|
||||
- **Per-thread memory no longer scales with the canvas scratch**: the render planner's fixed scratch buffers lived in static thread-local storage, so on Windows every thread the process spawned (window host, COM, accessibility, workers) privately committed a full ~6.5 MB copy — most of a small app's working set. The scratch now allocates lazily on the one thread that actually plans frames: a scaffolded counter app's private working set drops ~4x, its `.tls` section shrinks from ~6.5 MB to under 200 bytes, and the executable itself is ~6.5 MB smaller. Linux and macOS binaries shed the same per-thread TLS block (#117).
|
||||
- **Transpiler: a ternary initializer under null-guard fusion parenthesizes**: `const x = c ? f(a) : g(a); if (x === null) <exit>` lowers to Zig's `orelse` fusion, and the conditional's if/else expression now wraps in parens — bare, the `orelse` bound to the ELSE arm alone (a type error at best, the wrong value at worst). The same guard covers `??` over a ternary left side; pinned in the conformance corpus and the node/native run-fidelity corpus (#119).
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
- @SunkenInTime
|
||||
- @sepehr-safari
|
||||
<!-- release:end -->
|
||||
|
||||
## 0.4.4
|
||||
|
||||
### New Features
|
||||
|
||||
- **Native-only host builds (Windows)**: the build graph now infers web use from app.zon — a `.frontend` block, the `"webview"` capability, a `.shell` webview view, or the Chromium engine — and an app that declares none of them compiles its Windows host without the embedded WebView layer: no WebView2 header, no `WebView2Loader.dll` installed, staged, or referenced by the executable. A new `.webview_layer = "auto"|"include"|"exclude"` manifest field (and `-Dweb-layer`) overrides the inference, an exclude that contradicts a web declaration is rejected at validate, configure, and package time, and a native-only build that reaches webview creation at runtime fails fast with a teaching `WebViewLayerNotBuilt` error. `native check` and the package report print the web-layer verdict, and a CI cross-audit asserts the presence/absence of the loader reference in real cross-compiled executables (#107).
|
||||
- **Native-only host builds (Linux)**: the WebKitGTK compile seam mirrors the Windows one — an app whose app.zon declares no web use compiles its GTK host with `NATIVE_SDK_ALLOW_WEBKITGTK_STUB`, so the executable neither links `webkitgtk-6.0` nor references any `webkit_`/`jsc_` symbol, building needs no WebKitGTK development package, and users need no `libwebkitgtk` at runtime. The web-layer auditor (`tools/audit_web_layer.zig`) grew a hand-rolled ELF reader (DT_NEEDED entries + dynamic symbols) that CI runs both ways: the native-only fixture must scan clean even with the dev package installed, and the Linux canvas smoke now builds on a runner without WebKitGTK at all. `native package` refuses to package a WebKitGTK-linking binary under a native-only decision, record→replay and automation-driven sessions are pinned on native-only apps, and the macOS GPU dashboard smoke asserts a native-only app spawns zero WebKit helper processes (#110).
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Zig 0.16 guidance**: a new `zig` skill (`native skills get zig`) maps each pre-0.16 std idiom's compile error to the current one — `std.Io` file IO and writers, unmanaged `ArrayList`, `main(std.process.Init)`, spawning, clocks, `{t}`/`{f}` formatting, `build.zig` modules — with the same content for humans as the docs' Zig 0.16 Notes page; the native-ui skill carries the short table, and a failing `native build|test|dev` now points at the catalog when std members come up missing (#105).
|
||||
- **Lazy Linux WebView startup**: GTK windows now create only GTK chrome at window creation and materialize the main `WebKitWebView` on first web use, so canvas-only apps do not start WebKit processes on Linux; child-WebView bridge responses no longer require a main WebView to exist (#106).
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
- @WhiteHades
|
||||
|
||||
## 0.4.3
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Linear-light edge blending**: anti-aliased fringes on opaque rounded rectangles, path fills, and strokes now composite through a linear-light coverage path, removing the dark rims that sRGB blending produced on curved geometry while keeping opaque interiors, glyph coverage, and translucent overlays byte-identical (#89).
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Single-line fields handle overflowing values**: text, selection rects, composition underlines, and the caret now clip to the field's content rect, and a horizontal scroll offset keeps the caret visible — typing past the edge scrolls the value, Home scrolls back, and deleting never leaves trailing emptiness. Covers text fields, inputs, search fields, and comboboxes; values that fit render exactly as before (#90).
|
||||
- **Cross-drive apps on Windows**: `native dev|build|test` no longer fails with `expected path relative to build root; found absolute path` when the app and the npm-installed SDK live on different drives — the generated build graph now bridges volumes with a `.native/sdk` directory junction (no admin rights needed) and keeps the zon dependency relative; the junction is retargeted automatically when the SDK moves or upgrades. Where the bridge cannot apply (`native eject`, full-shape `native init`, or a filesystem that refuses junctions), the CLI explains the cross-volume constraint and both ways out instead of writing a build Zig would reject (#92).
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
- @fleeting-zone
|
||||
- @kvnwdev
|
||||
|
||||
## 0.4.2
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Windows rendering is DPI-aware and sharper**: Windows apps now declare Per-Monitor V2 DPI awareness, each window carries its own device scale, and canvases, native child views, hidden-titlebar sizing, and explicit WebView frames re-render/re-round correctly when moved across mixed-DPI monitors (#81).
|
||||
- **Smoother canvas geometry**: rounded-rect fills and strokes now render through continuous coverage while eligible hairline borders snap to crisp device-pixel columns, so arcs stay anti-aliased and 1px borders stay sharp under the default house and Geist packs (#81).
|
||||
- **Canonical package and documentation metadata**: npm package metadata, release automation, docs, templates, and examples now point at the renamed `vercel-labs/native` repository and `native-sdk.dev`; `version:sync` stamps repository/homepage metadata into platform packages and `version:check` rejects drift before publish (#78, #80).
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Windows embedded WebView is real from a plain checkout**: the WebView2 SDK header and loader are vendored under `third_party/webview2/` (BSD-licensed), every build graph puts the header on the include path, and the host now refuses to compile with the WebView layer silently stubbed — previously every Windows build shipped the stub and WebView loads reported `WebViewNotFound` at runtime (#86).
|
||||
- **WebView2 host conformance fixes**: a missing lambda capture in the bridge message handler, a mingw-compatible WRL event-handler factory, an `EventToken` shim, and STA COM initialization on the host thread let WebView2 environment creation and bridge messaging run on Windows (#86).
|
||||
- **WebView2Loader.dll ships with the app**: `zig build` installs the architecture's loader next to the executable, `zig build run` resolves it during dev runs, generated frontend/package commands carry `NATIVE_SDK_PATH`, and `native package --target windows` includes the loader in the artifact (the Evergreen WebView2 runtime itself is preinstalled on current Windows) (#86).
|
||||
- **Checkbox marks use the vector core**: checked boxes now draw one stroked polyline with round caps and joins instead of two aliased diagonal lines, and stroke caps ride the GPU packet path so the host and reference renderer agree (#87).
|
||||
- **Path geometry lifetimes are owned by the builder**: chart, spinner, and checkbox path commands no longer borrow threadlocal frame scratch, so separately emitted trees cannot alias each other's path elements (#87).
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
|
||||
## 0.4.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **npm package assets**: Ship the SDK's root `assets/` directory in `@native-sdk/cli` so installed packages include `assets/native-sdk.manifest`, the default macOS icon, and entitlements needed by generated apps (#72, #75).
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
- @lzitser23
|
||||
|
||||
## 0.4.0
|
||||
|
||||
### New Features
|
||||
|
||||
- **zero-native is now the Native SDK**: The toolkit, CLI, and packages are renamed end to end — the CLI binary is `native`, the Zig module and build helper are `native_sdk` (`native_sdk.addApp`, `native_sdk.addMobileLib`), the embed C ABI prefix is `native_sdk_*`, and the npm CLI package is `@native-sdk/cli`.
|
||||
- **Native-rendered apps by default**: `native init` scaffolds a native-rendered app — a declarative `.native` markup view plus Zig logic on the `UiApp` runtime (a `Model`, a `Msg` union, `update`, and a view) — with web frontends still available via `--frontend next|vite|react|svelte|vue`.
|
||||
- Native markup: HTML-inspired views with flex layout, `{bindings}` to model fields and functions, typed `on-*` message dispatch, `for`/`if`/`else` structure tags (multi-child `for` bodies, `<else>` empty states), and keyed identity; a deliberately closed grammar keeps logic in Zig.
|
||||
- Comptime compilation: views compile at build time into direct field access — release binaries carry no parser, and markup or binding mistakes are compile errors with line and column.
|
||||
- Hot reload: dev builds watch every `.native` file — imported components and fragments embedded in Zig views included — and update the running window in place, preserving model state, selection, and widget identity.
|
||||
- Expressions in bindings: arithmetic, comparisons, boolean logic, string concatenation, and a closed 17-function formatting library (`fixed`, `thousands`, `date`/`time`, `pad`, `plural`, ...), evaluated bit-identically by both markup engines; string-producing model functions bind directly through the build arena.
|
||||
- Cross-file components: `<import>` splices template files (transitively, with cycle and duplicate diagnostics), template args take literal defaults, `<slot/>` marks where use-site children land, and `native eject component` transfers a library composite's canonical source into your app exactly once.
|
||||
- `canvas.Ui`, the programmatic builder under the markup: structural widget identity, typed message handlers, flex-first layout, and per-element `opacity`/`transform` render channels for animated composition.
|
||||
- **The model–view contract, checked in both directions**: `native check` verifies every binding path, iterable, key, message tag, payload type, and expression in every `.native` file against the app's reflected `Model`/`Msg` surface in milliseconds — with did-you-mean suggestions and a dead-state lint for model fields and messages no view uses.
|
||||
- **Markup tooling**: `native markup check` (instant validation with positions), a language server (diagnostics, completion, hover), a TextMate grammar with editor setup, `native markup dump` over the canonical serialized document format, and the `native-ui` agent skill — the complete authoring reference, served through the skills CLI.
|
||||
- **Two-way tooling**: `native automate provenance` reports where a live widget was authored (file, byte span, template instantiation chain), and `native automate edit` writes minimal-diff attribute and text edits back into the markup source — validated before anything touches the file, with hot reload closing the loop.
|
||||
- **Full component catalog**: every built-in component is expressible in markup — tabs, tables, dialogs, drawers, sheets, selects, comboboxes, accordions, menus, badges, avatars, tooltips, inputs, and more — implemented in both engines with parity tests, alongside new composites in markup and Zig:
|
||||
- Charts (`<chart>` / `ui.chart`): line, area, bar, and band series drawn through the vector path pipeline with design-token colors, deterministic downsampling past 256 points, axis labels on a nice-step lattice, and pointer hover details.
|
||||
- Markdown (`<markdown>` / `native_sdk.markdown`): a GitHub-flavored subset — headings, inline styles, links, lists, task lists, fenced code, blockquotes, pipe tables, autolinks, and model-driven collapsibles — that degrades malformed input to text and never fails a build.
|
||||
- Disclosure trees with the full ARIA tree keymap, steppers and timeline items, input groups with focus-within rings, chat bubbles with reaction pills and thread-width caps, and a `ui.nav` push/pop page container with stable per-page state.
|
||||
- Resizable split panes with model-owned fractions, keyboard and assistive resize, and optional eased animation on model-driven moves.
|
||||
- Windowed virtual lists: viewport-sized widget budgets at 100,000 items, variable row extents that converge to measured truth without visible jumps, tail anchoring for chat transcripts, and `on_reach_end`/`on_reach_start` for infinite fetch and history loading.
|
||||
- Anchored floating surfaces (dropdowns, selects, popovers) that float above the tree with edge auto-flip; dismissal (Escape, click-outside, assistive dismiss) is a Msg the model owns, and focused selects get the full open/navigate/commit keymap.
|
||||
- Vector icons: an SVG stroke-icon subset parser, 50 curated built-in icons, leading or trailing icon slots on buttons, toggle chips, list and menu rows, badges, and timeline items, app-registered icons comptime-parsed from your own SVGs, model-bound icon names, and a loud missing-icon fallback.
|
||||
- **Text engine**:
|
||||
- Inline styled spans — weight (resolved to real faces), italic, monospace, color tokens, underline, strikethrough, size scale, per-span backgrounds, and hit-testable links — wrap as one paragraph in Zig and markup alike.
|
||||
- Honest single-line text: unwrapped text elides with a trailing ellipsis by default, an `overflow` policy knob keeps the deliberate hard cut available, and word wrap is an explicit opt-in — paint always agrees with measurement.
|
||||
- `heading`/`display` typography rungs on the token ladder, first-class text alignment, and fixed grid column counts.
|
||||
- **Selection and clipboard**: cmd/ctrl+C/X/V in editable fields through the platform clipboard, click-drag selection with copy on static text (surviving rebuilds, exposed to semantics and automation), and clipboard effects for app code.
|
||||
- **Interaction model**:
|
||||
- Presses fall through to the nearest pressable ancestor, so any element with a handler is a real hit target — nested pressables resolve to the deepest one, and text selection still works inside pressable rows.
|
||||
- Press-and-hold, double-click, Enter as a list row's primary action, and an app-level key fallback (`Options.on_key`) with pinned precedence — quiet list rows stay transparent to app-owned selection models.
|
||||
- Source-driven `autofocus`, observable typed scroll events (`on_scroll`), a built-in search-field clear affordance, and a quiet-hover style knob for content tiles.
|
||||
- **Effect system**: the update loop's command half — `update` gains an effects channel of bounded, key-addressed effects that deliver exactly one terminal Msg each and are fully testable against a deterministic fake executor:
|
||||
- `fx.spawn` runs subprocesses with streamed lines or whole-output collect mode (stderr tail included), raisable per-effect line bounds, and cancellation; `fx.fetch` runs HTTP(S) requests with an explicit failure taxonomy, timeouts, and a streaming response mode for line-oriented endpoints.
|
||||
- `fx.readFile`/`fx.writeFile` persistence, `fx.startTimer`/`fx.cancelTimer`, `fx.writeClipboard`/`fx.readClipboard`, `fx.registerImageBytes` for runtime images, `fx.closeWindow`/`fx.minimizeWindow`, and the `init_fx` boot hook so loading states are in the very first paint.
|
||||
- A facade time API (`nowMs`, `monotonicMs`) plus `Clock`/`TestClock` seams for deterministic time-dependent logic.
|
||||
- **Audio, end to end on five platforms**: `fx.playAudio` with full transport (pause, resume, stop, seek, volume), real decoded durations, position ticks, and honest completion and failure reports — AVFoundation on macOS, Media Foundation on Windows, GStreamer on Linux, and the experimental mobile hosts on iOS (AVFoundation) and Android (MediaPlayer).
|
||||
- Streaming with a verified track cache: URL sources resolve local file, then size-verified cache, then progressive stream (filling the cache in parallel for the next play), with honest `buffering` states and explicit failures — never a silent stall.
|
||||
- Real spectrum analysis on macOS, Windows, and Linux: 32 log-spaced bands at ~25 Hz from the app's own playback, journaled at the effect boundary so record/replay repaints identical bars; hosts that cannot analyze report the capability honestly instead of fabricating bands.
|
||||
- **Images**: a platform decode seam (CGImageSource, gdk-pixbuf, WIC) so the toolkit bundles no image decoders; runtime image registration renders through every path — GPU packets, software presentation, and screenshots — with pixels riding an out-of-band upload channel so image-bearing frames stay on the GPU path; avatars take a bound image with initials fallback.
|
||||
- **Windowing and chrome**: model-declared secondary windows (presence is visibility; a user close dispatches a Msg), enforced window minimum sizes, and present-before-show so a canvas window never appears blank.
|
||||
- Titlebar control on all three desktops: `hidden_inset`, a tall unified-toolbar variant, and fully `chromeless` styles; markup `window-drag` regions; and an `on_chrome` hook carrying the real overlay insets and control-cluster frames — with real system window controls preserved on Linux client-side decorations and Windows DWM caption buttons.
|
||||
- Native context menus, declared per widget in Zig or markup (`<context-menu>`): the real OS menu where one exists, an anchored canvas surface elsewhere, editable-text cut/copy/paste defaults, and full automation support for enumerating and invoking items.
|
||||
- A menu-bar status item with model-driven title and menu; canvas and WebView panes composed in one window; adoption of app-owned native views into the layout (`adoptViewSurface`); and native scroll drivers on macOS that give every scroll region OS momentum, rubber-band overscroll, and the system overlay scrollbar with zero app code.
|
||||
- **Experimental iOS and Android host tiers — the toolkit owns the entire mobile app**: complete UIKit and Android hosts ship in the SDK over the embed C ABI, an app project carries zero host code, and embedding a hand-written host stays first-class.
|
||||
- `native dev --target ios|android [--device name]` builds, installs, and launches on a simulator or emulator and streams the app log; `native package --target ios` emits an archive-ready Xcode project and `--target android` a complete generated host project plus a debug-signed APK — no build-system project, no plugin matrix.
|
||||
- Touch, soft keyboard, and IME forwarding; safe-area and keyboard insets on the window-chrome channel plus host-reported form factor; platform text metrics; platform audio and image decoding; and damage-rect rendering so a keystroke repaints and uploads only the changed region instead of the whole screen.
|
||||
- Declared platform chrome: apps project a tab set and primary action as a real system tab bar, and a model-owned page stack drives real push/pop transitions with the system edge-swipe back gesture — navigation state stays in the model and replays deterministically from the Msg journal.
|
||||
- The soundboard ships the proof: one codebase, a desktop composition plus a compact phone shell selected by the host-reported form factor, running on the simulator via `native dev --target ios`.
|
||||
- **Theme packs and design tokens**: named packs — the default register plus `geist`, the design register of the bundled Geist type family — compose with the live system appearance; interaction-state formulas, control metrics, and focus-ring geometry are all token-stated; new `success`/`warning`/`info` semantic color tokens; the stock theme follows the OS light/dark, high-contrast, and reduce-motion settings live; modal scrims blur the content behind them for real; app-registered TrueType fonts resolve everywhere a font id rides.
|
||||
- **Deterministic rendering core**: a bounded, std-only TTF parser inks real anti-aliased glyphs (bundled Geist and Geist Mono) on every headless path — screenshots, mobile embeds, pixel goldens — while layout measures exactly what gets inked; an allocation-free vector rasterizer with bit-identical cross-platform coverage draws paths, icons, and charts.
|
||||
- **Automation and testing**: `native automate` gains `assert` (regex polling against the accessibility snapshot), deterministic PNG screenshots, per-stage frame profiling (`profile on`), and widget verbs for hold, secondary click, context-menu invocation, drag, wheel, and tray actions.
|
||||
- Deterministic session record and replay: journal every platform event and effect result, then re-run headlessly with checkpoint verification (`native automate record` / `replay --verify`).
|
||||
- `native init` scaffolds a CI workflow: null-platform tests for every frontend plus a Linux automation smoke that drives the app's real binary under Xvfb.
|
||||
- **Accessibility as machine checks**: unnamed interactive controls, icon-only controls without labels, and misused roles are validation errors (degradations report as warnings; `--strict` promotes); a deterministic tree-level audit catches labels that resolve empty at runtime, focus-unreachable widgets, and duplicate sibling labels; and assistive actions actuate through the same activation paths keyboard users take instead of reporting success on nothing.
|
||||
- **Showcase examples**: calculator, notes (folders, trash, context menus), soundboard (a real music library with playback and search), deck (a radically re-skinned sibling proving theme packs and chrome passes), system-monitor (live effects-driven sampling), markdown-viewer (split-pane editor and preview), and feed (a 100,000-post virtual list) — each with a deterministic test suite, and a prepared real-music catalog that streams out of the box.
|
||||
- **Docs site**: a full Components section (34 pages) where every preview is rendered offscreen by the engine itself and upgrades on hover to a live engine instance running in-page via a ~306 KB (gzip) wasm build; attribute tables generate from the validator's own vocabulary so docs cannot drift; the whole site restructured native-first with new State & Data Flow, App & Runtime, Theming, and Testing in CI pages.
|
||||
- **Zero-config toolchain and distribution**: `native dev|build|test|check` work in a directory holding only `app.zon` and `src/` (`native eject` writes the build files exactly once when you want to own them); the pinned Zig toolchain downloads on consent with checksum verification; and `@native-sdk/cli` installs from npm with zero scripts — eight platform binaries plus the SDK source, so `native init && native dev` work offline right after install.
|
||||
- **One-image app icons**: drop a single square PNG or SVG in `assets/`, and `native package` generates everything — a masked, grid-correct macOS `.icns`, a multi-size Windows `.ico`, Linux hicolor PNGs, and iOS/Android catalog icons — with exact linear-light downscales, teaching errors for bad sources, and no external tools.
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Performance — frame cost scales with what changed, not view size**:
|
||||
- GPU packets ride a compact binary encoding (~10x smaller than JSON, ~40x effective capacity — text-heavy frames no longer silently fall back to software rendering), steady-state frames ship incremental patches (~20x less wire per interaction), and repaints derive per-change dirty-rect lists so pixels between two far-apart changes stay retained.
|
||||
- Per-command raster caches stop re-rasterizing unchanged content (host draw p50 dropped an order of magnitude on animated views); frame planning and widget reconciliation moved from quadratic scans to indexed lookups (end-to-end interaction p50 improved ~2.3-3.2x on large views); backdrop blur cost no longer scales with radius; a click emits one display list instead of three.
|
||||
- Launch to glass: the first canvas frame presents before the event loop starts, first paint rasterizes across cores, the main WebView is created lazily, and warm launches measured 150→120 ms on the heaviest showcase app; `NATIVE_SDK_WINDOW_TIMING=1` prints a per-phase launch breakdown.
|
||||
- Occluded windows throttle to a ~1 Hz heartbeat instead of spinning the frame clock (spectrum reports pause too); accessibility publishes only when the tree actually changed and defer off the input-to-glass path; frame pacing delivers exactly one event per display interval; input latency is measured to the responding present, honestly.
|
||||
- `zig build bench-render` runs deterministic interaction scenarios against committed per-scenario budgets, and a percentile GPU perf check gates first-frame and input-to-present latency in CI.
|
||||
- **Component fidelity**: the built-in components land a refined default look, verified pixel-for-pixel in CI under both theme packs.
|
||||
- Measured control geometry and state washes, ring-offset focus rings, flat buttons with a quiet destructive treatment, segmented button groups rendered as one bar with collapsed seams, compact badges, and hairline tables.
|
||||
- Reworked accordion, tabs, alert, and card treatments with sensible per-kind layout defaults; skeletons pulse and the caret blinks; select menus read like menus (row highlight, trailing checkmark for the committed option).
|
||||
- Native cursor conventions (the pointing hand is reserved for true links), flat list rows, axis-aware separators, and edge-pinned scrolling with opt-in rubber-band overscroll.
|
||||
- **Capacity and honesty**: per-view widget budgets quadrupled to 1024 nodes (command, glyph, and text budgets raised to match) with headroom telemetry in every snapshot; explicit `width`/`height` are definite bounds; layout overflow is diagnosed, dispatch errors degrade and record instead of exiting the app, and every effect-facing type and constant is exported from the `native_sdk` facade.
|
||||
- **Teaching validation**: handlers on elements that can never receive them, `gap` on stacking containers, `wrap` on non-text elements, and literal glyphs outside the bundled font's coverage are all positioned teaching errors, enforced identically by the validator, both engines, and the language server.
|
||||
- **Desktop parity**: the Linux and Windows hosts reach the macOS seam contract — app timers, appearance events, window options at create, interactive window moves, IME composition on Windows, and hidden-titlebar fidelity with real system controls; CI gains Windows canvas and effects smokes under Wine, a headless Linux canvas smoke, and a containerized Linux live-truth harness driving every showcase app on real GTK.
|
||||
- **Observability**: automation snapshots report the live present path and mode, patch sizes, fallback reasons with byte counts, budget headroom, audio state, tray contents, and per-stage frame percentiles while profiling; `NATIVE_SDK_GPU_DRAW_TRACE=1` attributes every present.
|
||||
- **Docs and skill accuracy**: the code-signing page documents the real ad-hoc Gatekeeper experience, form-control and picker docs match what the engine ships, the keyboard and interaction seams are documented where developers look, and stale commands and API shapes were fixed across the site.
|
||||
- **Example polish**: showcase headers carry only working controls under hidden-inset titlebars, the soundboard adopts desktop list-selection conventions, notes gains Recently Deleted and dialog autofocus, the deck refined its hardware identity across feedback passes, system-monitor lands the standard settings flow, and every showcase app ships the zero-config scaffold shape with a real neutral default app icon.
|
||||
- **Contributor workflow**: changelog fragments (`changelog.d/`) end merge conflicts on this file, and `scripts/gate.sh` runs a tiered local gate that scales with the diff.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Input and focus**: clicked and tabbed-into fields always show a caret (drawn in the field's own ink, readable in every scheme); Escape dismisses surfaces opened from non-focusable triggers; Enter inserts a newline in textareas (the primary chord submits); programmatic focus is quiet on non-editables; composite rows hover, point, and press as one surface; cross-centered overflow distributes evenly.
|
||||
- **Model-driven control state**: sliders, exclusive selections, and toggle-button chips follow the model when the source moves (a live drag is never yanked); disabled selection controls render disabled; idle disabled buttons no longer wear an accent outline.
|
||||
- **Rendering correctness**: pixel snapping no longer wraps exact-fit text or elides exact-fit badges; packet text honors engine line breaks; text bounds cover glyph ink; mono runs read as monospace on every headless path; avatar initials center; the spinner actually spins and sizes to the icon register; offscreen screenshots clear with live tokens; render animations invalidate only the affected commands; one invalid UTF-8 byte can no longer hang the renderer; budget overflows apply atomically instead of tearing the retained tree.
|
||||
- **macOS**: Debug builds no longer abort at launch on an SDK sanitizer trap; `resizable = false` is honored; frames keep pumping during live resize and menu tracking; occluded windows keep presenting and flush instantly on reveal; quitting mid-playback no longer crashes; the Chromium (CEF) host builds and runs again, verified live with child WebViews.
|
||||
- **Windows and Linux**: Windows apps launch on real Windows (common-controls manifest, dynamic task-dialog resolution) and builds link again; embed input timestamps and network error classification fixed on Windows; Linux audio no longer sticks in a buffering state; a saturated frame loop no longer freezes GTK windows; runtimes heap-allocate in every runner, fixing startup crashes under default stack limits; GTK initial allocation and overlay z-order fixed.
|
||||
- **Packaging**: signed bundles keep a valid code signature; packaged apps read their bundled assets and show their display name in the menu bar; archives are labeled with the real optimize mode; unbundled dev runs fall back to the embedded default Dock icon.
|
||||
- **Automation and CLI reliability**: commands queue with delivery acknowledgments instead of overwriting a single slot; a landing command wakes an idle app (~4 ms consumption); CLI and app handshake on a protocol version, and stale publishers or binaries are refused loudly; parseable payloads land on stdout; clicks aim at the rendered control, not its stretched box; `native dev` runs Debug so hot reload is actually compiled in; no CLI verb exits silently, and `--help` exits 0 everywhere.
|
||||
- **Hardening**: the markdown renderer survives hostile input (three quadratic blowups fixed, a fuzz corpus added); large models neither exhaust the comptime branch quota nor ride the stack (`UiApp.create` constructs in place); mobile embed libraries stage per target so cross-target builds cannot poison each other; oversized inline window sources fail loudly instead of leaving a blank window; docs live previews build, lay out with the selected pack's tokens, animate, and route keyboard shortcuts correctly.
|
||||
- **Measured-label controls no longer elide under pixel snapping**: a control sized exactly to its measured label — toggle chips (the system monitor's "PID" sort chip painted "PI…"), buttons, segmented controls and tab triggers, menu and list rows, tooltips, checkbox/radio/switch labels, hug-sized status bars — could lose a fraction of a pixel to render-time geometry snapping and swap real glyphs for an ellipsis. Every measured-label intrinsic width now rounds UP to the snap grid (the badge rule from the previous round), the switch additionally reserves its snapped track extent, and themes without geometry snapping stay bit-identical.
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### New Features
|
||||
|
||||
- **Keyboard shortcuts**: Add app-level keyboard shortcuts with manifest and runtime configuration, native delivery to Zig `Event.shortcut`, and typed JavaScript `window.zero` shortcut events (#62).
|
||||
- **Manifest-driven runner shortcuts**: Load `app.zon` shortcuts automatically in generated runners, with a `RunOptions.shortcuts` override for apps that build shortcut lists in Zig (#62).
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Shortcut documentation and validation**: Document the `app.zon` shortcut schema, portable key names, modifier behavior, backend support, and validation limits (#62).
|
||||
- **Windows WebView2 child bridges**: Enable bridge-enabled trusted child WebViews on Windows WebView2, bringing that backend closer to the macOS and Linux system WebView behavior (#62).
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Shortcut matching and delivery**: Fix shortcut modifier handling, shifted punctuation matching, backend event routing, and edge cases across AppKit, GTK, WebView2, and macOS CEF (#62).
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### New Features
|
||||
|
||||
- **Layered WebView runtime**: Model each native window as a stack of named WebViews, including the reserved startup `main` WebView and child WebViews with frame, layer, zoom, transparency, routing, resizing, reload, and close support across the native backends (#28).
|
||||
- **JavaScript WebView API**: Add typed `window.zero.webviews.*` helpers and `zero-native.webview.*` built-in bridge commands for create, list, setFrame, navigate, setZoom, setLayer, and close operations (#28).
|
||||
- **Isolated child WebViews**: Keep child WebViews bridge-isolated by default, allow trusted child chrome with `bridge: true`, enforce navigation policy on child URLs, and scope WebView commands to the calling native window (#28).
|
||||
- **Browser example**: Add a browser-style example that demonstrates layered WebViews, browser controls, isolated page content, frontend asset handling, and the root `zig build run-browser` command (#28).
|
||||
- **zero-native skills**: Ship CLI-served agent skills and reference material for building and automating zero-native apps (#38).
|
||||
|
||||
### Improvements
|
||||
|
||||
- **WebView and bridge documentation**: Document WebView APIs, built-in bridge commands, security boundaries, backend support, packaging, testing, and app model updates (#28, #38).
|
||||
- **WebView smoke coverage**: Extend automation smoke tests to exercise child WebView create, resize, navigate, and close operations for system WebView and macOS CEF builds (#28).
|
||||
- **CEF runtime builds**: Harden the CEF runtime workflows across macOS, Linux, and Windows, including Windows runtime build fixes (#25, #26).
|
||||
- **macOS compatibility**: Set the native app baseline to macOS 11 (#22).
|
||||
- **Contributor guidance**: Clarify signed commit requirements and contribution PR guidance (#10).
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Windows WebView builds**: Fix Windows WebView build failures before the layered WebView release.
|
||||
- **React example dependencies**: Include the missing React example type dependencies (#11).
|
||||
- **GitHub release notes**: Avoid duplicate contributor lists when creating GitHub releases (#24).
|
||||
- **macOS package permissions**: Preserve executable permissions for packaged macOS app binaries (#39).
|
||||
|
||||
### Contributors
|
||||
|
||||
- @Anshuman71
|
||||
- @PrathamGhaywat
|
||||
- @ctate
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### New Features
|
||||
|
||||
- **Linux and Windows desktop support**: Add platform-aware CEF tooling, Linux and Windows desktop build paths, Windows native host plumbing, and cross-platform CEF runtime packaging/release coverage.
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
|
||||
## 0.1.8
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Install completion delay** - Drain redirected GitHub responses during postinstall so npm exits immediately after the native binary is installed.
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
|
||||
## 0.1.7
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Install progress** - Show native binary download progress and checksum status during the npm postinstall step.
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
|
||||
## 0.1.6
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Init next steps** - Print the follow-up commands after scaffolding so users can immediately run their new app.
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
|
||||
## 0.1.5
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **macOS local asset loading** - Prefer current-directory asset roots during local `zig build run` so Vite-based examples render their production bundles instead of blank windows.
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
|
||||
## 0.1.4
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Scaffolded app builds** - Ship the framework source tree in the npm package and make `zero-native init` point generated apps at the installed package root so `zig build run` can resolve `src/root.zig`.
|
||||
- **Long scaffold names** - Keep generated Zig package names within Zig's 32-character manifest limit.
|
||||
- **Next scaffold builds** - Include the Node.js type package that Next expects for TypeScript projects.
|
||||
- **Frontend dependency versions** - Generate projects with current Next, React, Vite, Vue, Svelte, and plugin versions.
|
||||
- **Svelte scaffold builds** - Use the matching Svelte Vite plugin in generated Svelte projects.
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **CLI package homepage** - Point npm package metadata at `https://zero-native.dev`.
|
||||
- **Current-directory init** - Support `zero-native init --frontend <framework>` as shorthand for scaffolding into the current directory.
|
||||
- **CLI usage errors** - Exit cleanly for invalid CLI arguments instead of printing Zig stack traces for expected user input mistakes.
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **npm install fallback** - Do not fail package installation or point global shims at missing binaries when a native release asset is unavailable.
|
||||
- **Release asset ordering** - Upload the macOS arm64 native binary and `CHECKSUMS.txt` before publishing the npm package so postinstall downloads succeed immediately.
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **npm package homepage** - Add the zero-native repository homepage to the CLI package metadata.
|
||||
- **Chromium example launches** - Stage the CEF framework correctly for the `hello` and `webview` examples when running with `-Dweb-engine=chromium`.
|
||||
- **Linux WebKitGTK build** - Update navigation policy and external URI handling for current WebKitGTK and GTK4 headers.
|
||||
- **macOS WebView smoke test** - Use the emitted CLI binary and queue automation early enough for stable CI smoke tests.
|
||||
|
||||
### Release Process
|
||||
|
||||
- **GitHub releases** - Create missing GitHub releases from marked changelog entries when npm already has the version.
|
||||
- **CEF runtime release** - Publish the prepared macOS arm64 CEF runtime used by `zero-native cef install`.
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Initial Release
|
||||
|
||||
- Initial pre-release development version.
|
||||
@@ -0,0 +1,128 @@
|
||||
# Contributing
|
||||
|
||||
Thanks for helping improve the Native SDK. This guide is for maintainers and contributors working on the toolkit repository itself.
|
||||
|
||||
For app author documentation, start at [native-sdk.dev](https://native-sdk.dev).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Zig 0.16.0+](https://ziglang.org/download/)
|
||||
- Node.js with npm for the CLI package and generated frontend projects
|
||||
- pnpm for the documentation site
|
||||
- macOS for WKWebView and Chromium/CEF development
|
||||
- Linux with GTK4 and WebKitGTK 6 for Linux system WebView development
|
||||
|
||||
## Local Checks
|
||||
|
||||
Run the toolkit tests:
|
||||
|
||||
```bash
|
||||
zig build test
|
||||
```
|
||||
|
||||
Validate the sample app manifest:
|
||||
|
||||
```bash
|
||||
zig build validate
|
||||
```
|
||||
|
||||
Build the WebView example against the system engine:
|
||||
|
||||
```bash
|
||||
zig build test-webview-system-link
|
||||
```
|
||||
|
||||
Run the WebView example:
|
||||
|
||||
```bash
|
||||
zig build run-webview
|
||||
```
|
||||
|
||||
Check the npm CLI package:
|
||||
|
||||
```bash
|
||||
npm --prefix packages/native-sdk run version:check
|
||||
npm --prefix packages/native-sdk run scripts:check
|
||||
```
|
||||
|
||||
Check the documentation site:
|
||||
|
||||
```bash
|
||||
pnpm --dir docs install --frozen-lockfile
|
||||
pnpm --dir docs check
|
||||
```
|
||||
|
||||
## Web Engine Development
|
||||
|
||||
The system WebView path is the default development loop:
|
||||
|
||||
```bash
|
||||
zig build run-webview -Dweb-engine=system
|
||||
```
|
||||
|
||||
For Chromium on macOS, install CEF and run with the Chromium engine:
|
||||
|
||||
```bash
|
||||
native cef install
|
||||
zig build run-webview -Dweb-engine=chromium
|
||||
```
|
||||
|
||||
Useful Chromium smoke checks:
|
||||
|
||||
```bash
|
||||
zig build test-webview-cef-smoke -Dplatform=macos -Dweb-engine=chromium
|
||||
zig build test-package-cef-layout -Dplatform=macos
|
||||
```
|
||||
|
||||
## Packaging Development
|
||||
|
||||
Create a local package artifact:
|
||||
|
||||
```bash
|
||||
zig build package
|
||||
```
|
||||
|
||||
Package explicitly through the CLI:
|
||||
|
||||
```bash
|
||||
native package --target macos --manifest app.zon --assets assets --binary zig-out/lib/libnative-sdk.a
|
||||
```
|
||||
|
||||
For Chromium packages, configure `.web_engine = "chromium"` and `.cef` in `app.zon`, or use temporary `--web-engine` and `--cef-dir` overrides while testing.
|
||||
|
||||
Verify an ad-hoc signed package's code signature survives packaging intact (macOS; skips loudly on hosts without `codesign`):
|
||||
|
||||
```bash
|
||||
zig build test-package-signing
|
||||
```
|
||||
|
||||
## Automation Development
|
||||
|
||||
Enable automation in a build:
|
||||
|
||||
```bash
|
||||
zig build run-webview -Dautomation=true
|
||||
```
|
||||
|
||||
Interact with the running app:
|
||||
|
||||
```bash
|
||||
native automate wait
|
||||
native automate list
|
||||
native automate bridge '{"id":"ping","command":"native.ping","payload":null}'
|
||||
```
|
||||
|
||||
Automation writes artifacts under `.zig-cache/native-sdk-automation`.
|
||||
|
||||
|
||||
## Making a Pull Request
|
||||
|
||||
Branch from `main` (fork first if you don't have push access), keep the change focused, and run the tiered local gate before opening the PR:
|
||||
|
||||
```bash
|
||||
scripts/gate.sh fast # root suites + the example suites your diff touches
|
||||
```
|
||||
|
||||
If the change is user-visible, add a changelog fragment in `changelog.d/` (see [changelog.d/README.md](./changelog.d/README.md)) instead of editing `CHANGELOG.md`. Open the PR against `main` describing what changed and why; for larger changes, open an issue first so the design can be discussed.
|
||||
|
||||
Commits must be cryptographically signed (`git commit -S`, or set `commit.gpgsign = true`) so they show as **Verified** — the `Signed-off-by` trailer from `git commit -s` is a DCO attestation, not a signature.
|
||||
@@ -0,0 +1,176 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -0,0 +1,135 @@
|
||||
# Native SDK
|
||||
|
||||
**Native SDK is the complete toolkit for building native desktop applications.**
|
||||
|
||||
Native SDK exists because expressive UI and native performance should not be competing goals. Developers often choose web-based runtimes because they offer freedom, speed and control over the product experience. But that freedom often comes with a heavy runtime. Native SDK keeps the expressive authoring model and replaces the runtime with native rendering.
|
||||
|
||||
Views are declarative markup in `.native` files, logic is plain TypeScript compiled to native code at build time — or Zig, first-class by choice — and Native SDK's own engine draws every pixel into real OS windows. No browser, no WebView, no JS runtime in the binary: Zig is how everything works, TypeScript and Native markup are how apps are authored.
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset=".github/assets/soundboard-dark.webp">
|
||||
<img src=".github/assets/soundboard-light.webp" alt="The Soundboard example app rendered by the Native SDK engine: a music library with album cover art, search, and a playback bar" width="100%">
|
||||
</picture>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="70%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset=".github/assets/notes-dark.webp">
|
||||
<img src=".github/assets/notes-light.webp" alt="The Notes example app rendered by the Native SDK engine: a three-pane notes manager with folders, a note list, and an open note" width="640">
|
||||
</picture>
|
||||
</td>
|
||||
<td width="30%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset=".github/assets/calculator-dark.webp">
|
||||
<img src=".github/assets/calculator-light.webp" alt="The Calculator example app rendered by the Native SDK engine: a finished calculation above a full keypad" width="270">
|
||||
</picture>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<sub>Soundboard, Notes, and Calculator from <a href="./examples">examples/</a> — every pixel drawn by the Native SDK engine, captured through its deterministic reference renderer. The images follow your color scheme.</sub>
|
||||
|
||||
## Quick start
|
||||
|
||||
Install the CLI:
|
||||
|
||||
```bash
|
||||
npm install -g @native-sdk/cli
|
||||
```
|
||||
|
||||
Create and run an app:
|
||||
|
||||
```bash
|
||||
native init my_app
|
||||
cd my_app
|
||||
native dev
|
||||
```
|
||||
|
||||
A native window opens with a working counter. The whole app is three files of truth — view, logic, manifest — and no build config. The view is `src/app.native`, a markup file that binds values and dispatches messages (the counter row at its heart):
|
||||
|
||||
```html
|
||||
<row gap="8" main="center" cross="center" grow="1">
|
||||
<button variant="secondary" on-press="decrement">-</button>
|
||||
<text>{count}</text>
|
||||
<button variant="primary" on-press="increment">+</button>
|
||||
</row>
|
||||
```
|
||||
|
||||
All logic lives in `src/core.ts`: a `Model` interface, a `Msg` union, and one pure `update` function — the only place state changes, plain TypeScript compiled to native code at build time:
|
||||
|
||||
```ts
|
||||
export function update(model: Model, msg: Msg): Model {
|
||||
switch (msg.kind) {
|
||||
case "increment":
|
||||
return { ...model, count: model.count + 1 };
|
||||
case "decrement":
|
||||
return { ...model, count: model.count - 1 };
|
||||
case "reset":
|
||||
return { ...model, count: 0 };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Prefer Zig for the core? `native init my_app --template zig-core` scaffolds the same app with `src/main.zig` — same loop, same runtime, first-class by choice.
|
||||
|
||||
Edit `src/app.native` while `native dev` runs and the window updates in place, keeping your state. `native dev --core` runs the TypeScript core under node for instant logic checks, `native check` validates the core and every view in milliseconds without building, and `native build` produces an optimized release binary.
|
||||
|
||||
Read the full guide at [native-sdk.dev/quick-start](https://native-sdk.dev/quick-start).
|
||||
|
||||
## What you get
|
||||
|
||||
**Beautiful by default** — Great software should not start from a blank slate. The built-in component catalog — buttons, tabs, text fields, dialogs, charts, virtual lists, and more — ships with considered typography, spacing, and color, so the app `native init` scaffolds already looks intentional the first time its window opens.
|
||||
|
||||
**Customizable by design** — Your app should have its own identity, not ours. Styling is design tokens end to end: color, radius, and typography resolve by name, re-resolve live when the theme changes, and can be replaced wholesale — `examples/soundboard` and `examples/deck` are the same music player separated only by tokens and a chrome pass.
|
||||
|
||||
**Native from the start** — Every interface is rendered without a browser or WebView. The engine draws into real OS windows while scroll physics, menus, dialogs, the tray, and text input stay with the operating system, and markup compiles into the executable at build time, so a release build carries no parser or interpreter — the scaffolded counter app builds to a single binary a few megabytes small.
|
||||
|
||||
**Predictable state** — State changes should be explicit, inspectable and easy to reason about. Events produce messages, messages update state, and state renders the interface; markup can bind and dispatch but never mutate. The loop is so deterministic that `native automate record` journals a session and `replay` reproduces it headlessly, verified frame by frame against state fingerprints.
|
||||
|
||||
**Simple authoring** — Interfaces should be easy to read, easy to write and easy to generate. Views are elements, flex layout, `{bindings}`, and expressions like `selected="{f == filter}"`, and `native check` validates every view against your app's actual `Model` and `Msg` — bindings, iterables, message tags — in milliseconds, with `file:line:column` errors that teach.
|
||||
|
||||
**AI is part of the workflow** — Native SDK is designed for a world where humans and AI agents build software together. Every app embeds an automation server, so any agent can read accessibility snapshots, drive widgets, assert on live state, and take deterministic screenshots of the running window; accessibility findings are machine-checked in `native check`; and the CLI ships the agent skills that teach all of it (`native skills list`).
|
||||
|
||||
## Examples
|
||||
|
||||
The apps pictured above live in [examples/](./examples), most as zero-config projects — `app.zon` plus `src/`, no build files — run straight from their directory with `native dev`.
|
||||
|
||||
| Example | What it shows |
|
||||
| --- | --- |
|
||||
| [`calculator`](./examples/calculator) | A complete small app: markup keypad, keyboard input, chrome shortcuts, theming. |
|
||||
| [`notes`](./examples/notes) | Persistence through the effects channel: debounced writes, restore on boot, dialogs, search. |
|
||||
| [`soundboard`](./examples/soundboard) | Album grid with decoded cover art, context menus, timers, and a custom theme. |
|
||||
| [`deck`](./examples/deck) | The soundboard player rebuilt as a dense hardware chassis: two windows, same widgets, different tokens. |
|
||||
| [`feed`](./examples/feed) | A 100,000-row list, virtualized with runtime-owned scrolling. |
|
||||
|
||||
The full catalog in [examples/README.md](./examples/README.md) also covers guarded OS capabilities, GPU surfaces, WebView composition, web-frontend shells, and the iOS/Android embed hosts.
|
||||
|
||||
## Platforms
|
||||
|
||||
macOS is the primary development platform and carries the deepest support: Metal presentation, OS scroll physics, native context menus, app menus, tray, and dialogs. Linux runs the full showcase through the deterministic software renderer in real windows, with pointer, keyboard, scroll, IME composition, and HiDPI; Windows runs on a Win32 host with IME composition and is exercised in CI, including real input injection. Mobile support is experimental: iOS is simulator-proven through the embed library and Android cross-compiles with the full embed ABI, but APIs and tooling on both are still evolving — desktop is the mature surface. WebView surfaces coexist on every desktop platform. The [platform support matrix](https://native-sdk.dev/platform-support) documents exactly what each host supports today.
|
||||
|
||||
## Documentation
|
||||
|
||||
The full documentation is at [native-sdk.dev](https://native-sdk.dev).
|
||||
|
||||
- [Quick Start](https://native-sdk.dev/quick-start) — install to a running, tested app
|
||||
- [Philosophy](https://native-sdk.dev/philosophy) — the six principles behind the toolkit
|
||||
- [App Model](https://native-sdk.dev/app-model) — the model/message/update loop, wiring, and hot reload
|
||||
- [TypeScript Cores](https://native-sdk.dev/typescript) — the app-core subset, effects, subscriptions, and the node dev loop
|
||||
- [Native UI](https://native-sdk.dev/native-ui) — every element, attribute, and pattern in the markup
|
||||
- [Components](https://native-sdk.dev/components) — the component catalog
|
||||
- [State & Data Flow](https://native-sdk.dev/state) — derive-don't-store, bindings, and text editing
|
||||
- [Testing](https://native-sdk.dev/testing) — full-loop UI tests, headless on any machine
|
||||
- [Automation](https://native-sdk.dev/automation) — snapshots, widget driving, record/replay, screenshots
|
||||
- [Capabilities](https://native-sdk.dev/capabilities) — guarded OS services: notifications, clipboard, dialogs, credentials
|
||||
- [Packaging](https://native-sdk.dev/packaging) — from binary to distributable app
|
||||
- [Platform Support](https://native-sdk.dev/platform-support) — what each host supports today
|
||||
|
||||
## Contributing
|
||||
|
||||
Native SDK is pre-1.0: APIs still move, and the toolkit is evolving quickly. Bug reports and focused pull requests are welcome — for larger changes, open an issue first so the design can be discussed. See [CONTRIBUTING.md](./CONTRIBUTING.md) for the development setup and local checks.
|
||||
|
||||
## License
|
||||
|
||||
[Apache-2.0](./LICENSE)
|
||||
@@ -0,0 +1,7 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- 原始项目:`vercel-labs/zero-native`
|
||||
- 原始仓库:https://github.com/vercel-labs/zero-native
|
||||
- 导入方式:上游默认分支的最新快照
|
||||
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||
@@ -0,0 +1,18 @@
|
||||
# Releasing
|
||||
|
||||
Releases are manual, single-PR affairs. The maintainer controls the changelog voice and format.
|
||||
|
||||
To prepare a release:
|
||||
|
||||
1. Create a branch (e.g. `prepare-v1.2.0`)
|
||||
2. Bump the version in `packages/native-sdk/package.json`
|
||||
3. Run `npm --prefix packages/native-sdk run version:sync` to update all version references
|
||||
4. Run `scripts/changelog-merge.sh` to fold any pending `changelog.d/` fragments into the `## Unreleased` section
|
||||
5. Write the changelog entry in `CHANGELOG.md`, wrapped in `<!-- release:start -->` and `<!-- release:end -->` markers
|
||||
6. Populate the entry's `### Contributors` from commit authors and `Co-authored-by` trailers in the release range, using GitHub handles when available; this marked block is also the GitHub release body
|
||||
7. Remove the `<!-- release:start -->` and `<!-- release:end -->` markers from the previous release entry; only the latest release should have markers
|
||||
8. Open a PR and merge to `main`
|
||||
|
||||
CI compares the version in `packages/native-sdk/package.json` to what's on npm. If it differs, it cross-builds the CLI for every platform, creates the GitHub release with the binaries, publishes the per-platform binary packages (`packages/native-sdk/npm/*`), and publishes `@native-sdk/cli` last — so the main package only lands once every binary package it pins is live. If npm already has the version but the GitHub release is missing assets, CI recreates the GitHub release from the marked changelog entry.
|
||||
|
||||
Publishing uses npm trusted publishing (OIDC) — there is no npm token secret. One-time setup: on npmjs.com, each of the nine packages (`@native-sdk/cli` plus the eight `@native-sdk/cli-*` platform packages under `packages/native-sdk/npm/*`) must have a GitHub Actions trusted publisher configured with repository `vercel-labs/native`, workflow `release.yml`, and environment `Release`. Every publish runs with `--provenance`. If a package is missing its trusted-publisher configuration, `npm publish` fails loudly with an OIDC authentication error for that package.
|
||||
@@ -0,0 +1,30 @@
|
||||
.{
|
||||
.id = "dev.native_sdk",
|
||||
.name = "native-sdk",
|
||||
.display_name = "Native SDK",
|
||||
.version = "0.1.0",
|
||||
.icons = .{ "assets/icon.icns", "assets/icon.ico" },
|
||||
.platforms = .{ "macos" },
|
||||
.permissions = .{ "window" },
|
||||
.capabilities = .{ "webview", "js_bridge", "native_module" },
|
||||
.bridge = .{
|
||||
.commands = .{
|
||||
.{ .name = "native.ping", .origins = .{ "zero://inline", "zero://app" } },
|
||||
.{ .name = "native-sdk.window.list", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } },
|
||||
.{ .name = "native-sdk.window.create", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } },
|
||||
.{ .name = "native-sdk.window.focus", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } },
|
||||
.{ .name = "native-sdk.window.close", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } },
|
||||
},
|
||||
},
|
||||
.security = .{
|
||||
.navigation = .{
|
||||
.allowed_origins = .{ "zero://app", "zero://inline", "http://127.0.0.1:5173" },
|
||||
.external_links = .{ .action = "deny" },
|
||||
},
|
||||
},
|
||||
.web_engine = "system",
|
||||
.cef = .{ .dir = "third_party/cef/macos", .auto_install = false },
|
||||
.windows = .{
|
||||
.{ .label = "main", .title = "native-sdk", .width = 720, .height = 480, .restore_state = true },
|
||||
},
|
||||
}
|
||||
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 35 KiB |
@@ -0,0 +1,15 @@
|
||||
<!-- Generated by `zig build generate-icon` (tools/generate_app_icon.zig). Edit the tool, not this file. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||||
<defs>
|
||||
<linearGradient id="plate" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#262626"/>
|
||||
<stop offset="1" stop-color="#171717"/>
|
||||
</linearGradient>
|
||||
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0" dy="12" stdDeviation="16" flood-color="#000000" flood-opacity="0.3"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect x="100" y="100" width="824" height="824" rx="185.4" fill="url(#plate)" filter="url(#shadow)"/>
|
||||
<rect x="372" y="272" width="380" height="380" rx="84" fill="#ffffff" fill-opacity="0.52"/>
|
||||
<rect x="272" y="372" width="380" height="380" rx="84" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 853 B |
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<!-- Application manifest embedded into Windows app executables. Declaring
|
||||
the common-controls v6 side-by-side dependency activates the modern
|
||||
control styling and the v6-only exports (TaskDialogIndirect); without
|
||||
it the loader binds the system-default v5 assembly. Declaring
|
||||
per-monitor-v2 DPI awareness makes GetDpiForWindow report the real
|
||||
monitor DPI (instead of a virtualized 96) so the canvas rasterizes
|
||||
at device scale and Windows never bitmap-stretches the window.
|
||||
dpiAwareness carries an ordered fallback list: readers take the first
|
||||
value they recognize, so PerMonitorV2 degrades to PerMonitor where v2
|
||||
is unknown. The dpiAwareness element itself is ignored before Windows
|
||||
10 1607; there the legacy dpiAware element beside it picks up, and its
|
||||
true/pm value requests per-monitor awareness where supported with
|
||||
system-DPI awareness as the floor. Readers that understand dpiAwareness
|
||||
prefer it over dpiAware, so the two elements coexist safely. -->
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -0,0 +1,25 @@
|
||||
.{
|
||||
.name = .native_sdk,
|
||||
.fingerprint = 0xc309966142f33087,
|
||||
.version = "0.1.0",
|
||||
.minimum_zig_version = "0.16.0",
|
||||
.dependencies = .{},
|
||||
.paths = .{
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"LICENSE",
|
||||
"SECURITY.md",
|
||||
"app.zon",
|
||||
"assets",
|
||||
"build",
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"docs",
|
||||
"examples",
|
||||
"packages",
|
||||
"src",
|
||||
"templates",
|
||||
"tests",
|
||||
"tools",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,991 @@
|
||||
//! Framework build helper: `addApp` gives a markup/builder app a complete
|
||||
//! build (exe, run, test) from a ~5-line build.zig. The app supplies
|
||||
//! src/main.zig, app.zon, and assets; the runner and all framework modules
|
||||
//! come from the native-sdk dependency.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// The shared web-layer inference contract: this build graph is one thin
|
||||
/// adapter over it (the CLI's manifest tooling and the app runner are the
|
||||
/// others), feeding it the inputs only this boundary sees — the
|
||||
/// `-Dweb-engine` and `-Dweb-layer` flags resolved against app.zon.
|
||||
const web_layer_contract = @import("../src/primitives/app_manifest/web_layer.zig");
|
||||
|
||||
const PlatformOption = enum {
|
||||
auto,
|
||||
null,
|
||||
macos,
|
||||
linux,
|
||||
windows,
|
||||
};
|
||||
|
||||
const TraceOption = enum {
|
||||
off,
|
||||
events,
|
||||
runtime,
|
||||
all,
|
||||
};
|
||||
|
||||
const WebEngineOption = web_layer_contract.WebEngine;
|
||||
|
||||
const WebLayerOption = web_layer_contract.WebViewLayer;
|
||||
|
||||
pub const AppOptions = struct {
|
||||
name: []const u8,
|
||||
/// App entry point; defaults to src/main.zig (relative to `app_root`).
|
||||
main: []const u8 = "src/main.zig",
|
||||
/// Root of the app source tree, relative to the build root. "." for a
|
||||
/// build.zig that lives in the app directory (every ejected app). The
|
||||
/// CLI's generated build graph under `<app>/.native/build/` passes
|
||||
/// "../.." so `src/`, `app.zon`, and `assets/` keep resolving in the
|
||||
/// app directory rather than the cache directory.
|
||||
app_root: []const u8 = ".",
|
||||
};
|
||||
|
||||
/// Which core the app tree carries. No flag and no config anywhere: the
|
||||
/// tree IS the truth — `src/core.ts` is a TypeScript core (transpiled at
|
||||
/// build time, run through generated wiring), `src/main.zig` a Zig one,
|
||||
/// and both at once is a teaching error naming the two files.
|
||||
const CoreTree = enum { zig, ts, both, neither };
|
||||
|
||||
fn detectCoreTree(b: *std.Build, app_root: []const u8) CoreTree {
|
||||
const has_ts = appFileExists(b, app_root, "src/core.ts");
|
||||
const has_zig = appFileExists(b, app_root, "src/main.zig");
|
||||
if (has_ts and has_zig) return .both;
|
||||
if (has_ts) return .ts;
|
||||
if (has_zig) return .zig;
|
||||
return .neither;
|
||||
}
|
||||
|
||||
fn appFileExists(b: *std.Build, app_root: []const u8, sub_path: []const u8) bool {
|
||||
b.build_root.handle.access(b.graph.io, appPath(b, app_root, sub_path), .{}) catch return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// The staged TypeScript-core wiring: one generated directory holding the
|
||||
/// transpiled core (core.zig), its rt kernel, the app's markup, and the
|
||||
/// SDK's generated-wiring entry (ts_core_main.zig as main.zig). Built once
|
||||
/// per app build and shared by the exe and test modules.
|
||||
const TsCoreStage = struct {
|
||||
main_root: std.Build.LazyPath,
|
||||
};
|
||||
|
||||
fn tsCoreStage(b: *std.Build, dep: *std.Build.Dependency, app_root: []const u8) TsCoreStage {
|
||||
if (!appFileExists(b, app_root, "src/app.native")) {
|
||||
@panic("\nthis app has a TypeScript core (src/core.ts) but no view: TS apps render markup," ++
|
||||
" so add src/app.native (the whole view tier binds the core's emitted model)\n");
|
||||
}
|
||||
const node = b.findProgram(&.{"node"}, &.{}) catch {
|
||||
@panic("\nbuilding a TypeScript app core needs node on PATH (the @native-sdk/core transpiler runs at" ++
|
||||
" build time; the binary it emits ships no JS runtime).\nInstall Node.js 22+ — https://nodejs.org" ++
|
||||
" or `brew install node` — and re-run.\n");
|
||||
};
|
||||
dep.builder.build_root.handle.access(
|
||||
dep.builder.graph.io,
|
||||
"packages/core/node_modules/@typescript/typescript6",
|
||||
.{},
|
||||
) catch {
|
||||
// Name the SDK dependency's real location (a checkout or the
|
||||
// npm-installed @native-sdk/cli package — both carry
|
||||
// packages/core with its lockfile, so `npm ci` works in place).
|
||||
std.debug.panic("\nthe SDK's @native-sdk/core transpiler is missing its installed dependency.\nFix with:" ++
|
||||
" cd {s}/packages/core && npm ci\n", .{dep.builder.build_root.path orelse "<sdk>"});
|
||||
};
|
||||
|
||||
// The transpiler runs through build/ts_run.mjs, not as `node cli.ts`:
|
||||
// on the npm-installed layout the transpiler's .ts sources live inside
|
||||
// node_modules, where node refuses its builtin type stripping — the
|
||||
// runner strips those modules with the transpiler's own installed
|
||||
// TypeScript and is a pass-through on a repo checkout.
|
||||
const transpile = b.addSystemCommand(&.{node});
|
||||
transpile.addFileArg(dep.path("build/ts_run.mjs"));
|
||||
transpile.addFileArg(dep.path("packages/core/src/cli.ts"));
|
||||
transpile.addFileArg(b.path(appPath(b, app_root, "src/core.ts")));
|
||||
transpile.addArg("-o");
|
||||
const emitted_core = transpile.addOutputFileArg("core.zig");
|
||||
// The transpiler reads its own sources, the SDK modules, and the core's
|
||||
// WHOLE import graph at run time; declare them so a transpiler upgrade
|
||||
// or an edit to ANY module of a multi-file core re-emits it. The graph
|
||||
// is declared as every .ts under the app's src/ (a superset of the
|
||||
// reachable imports: over-approximation only re-runs the transpile,
|
||||
// never misses a stale input). Failure mode: the checker's NS
|
||||
// diagnostics stream to stderr verbatim — they are the teaching layer,
|
||||
// nothing wraps them.
|
||||
addTsDirInputs(b, dep.builder, transpile, "packages/core/sdk");
|
||||
addAppTsDirInputs(b, transpile, appPath(b, app_root, "src"));
|
||||
const transpiler_sources = [_][]const u8{
|
||||
"checker.ts", "cli.ts", "diagnostics.ts", "emitter.ts", "infer.ts", "modules.ts", "transpile.ts", "typed_ast.ts", "types.ts",
|
||||
};
|
||||
for (transpiler_sources) |source| {
|
||||
transpile.addFileInput(dep.path(b.fmt("packages/core/src/{s}", .{source})));
|
||||
}
|
||||
|
||||
// The wiring imports core.zig and app.native relatively; the emitted
|
||||
// core imports rt.zig relatively: stage all four into one directory.
|
||||
const staged = b.addWriteFiles();
|
||||
_ = staged.addCopyFile(emitted_core, "core.zig");
|
||||
_ = staged.addCopyFile(dep.path("packages/core/rt/rt.zig"), "rt.zig");
|
||||
_ = staged.addCopyFile(b.path(appPath(b, app_root, "src/app.native")), "app.native");
|
||||
const main_root = staged.addCopyFile(dep.path("src/app_runner/ts_core_main.zig"), "main.zig");
|
||||
return .{ .main_root = main_root };
|
||||
}
|
||||
|
||||
/// Declare every .ts file in an SDK-relative directory as a file input of
|
||||
/// the transpile step (the SDK library modules an app may import).
|
||||
fn addTsDirInputs(b: *std.Build, sdk_builder: *std.Build, transpile: *std.Build.Step.Run, dir_path: []const u8) void {
|
||||
var dir = sdk_builder.build_root.handle.openDir(b.graph.io, dir_path, .{ .iterate = true }) catch return;
|
||||
defer dir.close(b.graph.io);
|
||||
var it = dir.iterate();
|
||||
while (it.next(b.graph.io) catch null) |entry| {
|
||||
if (entry.kind != .file) continue;
|
||||
if (!std.mem.endsWith(u8, entry.name, ".ts")) continue;
|
||||
transpile.addFileInput(sdk_builder.path(b.fmt("{s}/{s}", .{ dir_path, entry.name })));
|
||||
}
|
||||
}
|
||||
|
||||
/// Declare every .ts file under the app's src/ (recursively — a core may
|
||||
/// split into subdirectories) as a file input of the transpile step.
|
||||
fn addAppTsDirInputs(b: *std.Build, transpile: *std.Build.Step.Run, src_path: []const u8) void {
|
||||
var dir = b.build_root.handle.openDir(b.graph.io, src_path, .{ .iterate = true }) catch return;
|
||||
defer dir.close(b.graph.io);
|
||||
var walker = dir.walk(b.allocator) catch return;
|
||||
defer walker.deinit();
|
||||
while (walker.next(b.graph.io) catch null) |entry| {
|
||||
if (entry.kind != .file) continue;
|
||||
if (!std.mem.endsWith(u8, entry.basename, ".ts")) continue;
|
||||
transpile.addFileInput(b.path(b.fmt("{s}/{s}", .{ src_path, entry.path })));
|
||||
}
|
||||
}
|
||||
|
||||
/// The `native_sdk_app_*` C ABI every embed static library exports.
|
||||
pub const mobile_export_symbol_names = [_][]const u8{
|
||||
"native_sdk_app_create",
|
||||
"native_sdk_app_destroy",
|
||||
"native_sdk_app_start",
|
||||
"native_sdk_app_activate",
|
||||
"native_sdk_app_deactivate",
|
||||
"native_sdk_app_stop",
|
||||
"native_sdk_app_resize",
|
||||
"native_sdk_app_viewport",
|
||||
"native_sdk_app_viewport_state",
|
||||
"native_sdk_app_gpu_frame_state",
|
||||
"native_sdk_app_text_input_state",
|
||||
"native_sdk_app_set_text_measure",
|
||||
"native_sdk_app_set_audio_service",
|
||||
"native_sdk_app_audio_event",
|
||||
"native_sdk_app_set_image_service",
|
||||
"native_sdk_app_set_automation_dir",
|
||||
"native_sdk_app_touch",
|
||||
"native_sdk_app_scroll",
|
||||
"native_sdk_app_key",
|
||||
"native_sdk_app_text",
|
||||
"native_sdk_app_ime",
|
||||
"native_sdk_app_command",
|
||||
"native_sdk_app_frame",
|
||||
"native_sdk_app_chrome_tab_count",
|
||||
"native_sdk_app_chrome_tab_at",
|
||||
"native_sdk_app_chrome_primary_action",
|
||||
"native_sdk_app_chrome_selected_tab",
|
||||
"native_sdk_app_chrome_navigation_depth",
|
||||
"native_sdk_app_chrome_navigation_back_command",
|
||||
"native_sdk_app_chrome_icon_pixels",
|
||||
"native_sdk_app_set_form_factor",
|
||||
"native_sdk_app_set_chrome_tabs_projected",
|
||||
"native_sdk_app_set_asset_root",
|
||||
"native_sdk_app_set_asset_entry",
|
||||
"native_sdk_app_last_command_count",
|
||||
"native_sdk_app_last_command_name",
|
||||
"native_sdk_app_last_error_name",
|
||||
"native_sdk_app_widget_semantics_count",
|
||||
"native_sdk_app_widget_semantics_at",
|
||||
"native_sdk_app_widget_semantics_by_id",
|
||||
"native_sdk_app_widget_text_geometry",
|
||||
"native_sdk_app_widget_action",
|
||||
"native_sdk_app_render_pixel_size",
|
||||
"native_sdk_app_render_pixels",
|
||||
"native_sdk_app_render_pixels_damage",
|
||||
};
|
||||
|
||||
pub const MobileSceneOption = enum {
|
||||
/// The user app's UiApp on a gpu_surface view (window 1,
|
||||
/// "mobile-surface"), pumped by the host's frame callback.
|
||||
canvas,
|
||||
/// The fixed WebView shell the ios/android/mobile-shell examples embed
|
||||
/// today; the app module is not compiled in.
|
||||
webview,
|
||||
};
|
||||
|
||||
pub const MobileLibOptions = struct {
|
||||
name: []const u8,
|
||||
/// Mobile app entry (the `"app"` module the embed host drives); must
|
||||
/// declare `Model`, `Msg`, `initModel`, and `mobileOptions` — see
|
||||
/// `src/embed/ui_host.zig`. Ignored for `.scene = .webview`.
|
||||
main: []const u8 = "src/main.zig",
|
||||
scene: MobileSceneOption = .canvas,
|
||||
};
|
||||
|
||||
/// Mobile counterpart of `addApp`: produce the embed static library
|
||||
/// (`native_sdk_app_*` C ABI) compiled with the user's UiApp. Call it from
|
||||
/// a standalone build.zig (it registers the standard `target`/`optimize`
|
||||
/// options itself).
|
||||
pub fn addMobileLib(b: *std.Build, dep: *std.Build.Dependency, options: MobileLibOptions) void {
|
||||
const target = nativeSdkTarget(b);
|
||||
const optimize_request = b.option(std.builtin.OptimizeMode, "optimize", "Prioritize performance, safety, or binary size");
|
||||
const optimize = exampleOptimizeMode(b, optimize_request, .Debug);
|
||||
addMobileLibWithTarget(b, dep, target, optimize, options);
|
||||
}
|
||||
|
||||
/// The mobile-lib wiring behind `addMobileLib`, for builds that already
|
||||
/// resolved `target`/`optimize` (`addAppArtifacts` registers the `lib`
|
||||
/// step through this for iOS/Android targets, so every standard app —
|
||||
/// generated graph or ejected `addApp` — can produce the embed library
|
||||
/// with nothing but `-Dtarget`).
|
||||
fn addMobileLibWithTarget(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, options: MobileLibOptions) void {
|
||||
const native_sdk_mod = nativeSdkModule(b, dep, target, optimize);
|
||||
// Android hosts load the embed lib inside a shared object
|
||||
// (System.loadLibrary / NativeActivity), so every object must be PIC —
|
||||
// without it Zig emits local-exec TLS relocations (R_AARCH64_TLSLE_*)
|
||||
// that the NDK linker rejects when producing the shim .so. Imported
|
||||
// modules leave `pic` null and inherit this from the root module.
|
||||
const pic: ?bool = if (target.result.abi.isAndroid()) true else null;
|
||||
const exports_mod = b.createModule(.{
|
||||
.root_source_file = dep.path(switch (options.scene) {
|
||||
.canvas => "src/embed/app_exports.zig",
|
||||
.webview => "src/embed/c_exports.zig",
|
||||
}),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.pic = pic,
|
||||
});
|
||||
exports_mod.addImport("native_sdk", native_sdk_mod);
|
||||
if (options.scene == .canvas) {
|
||||
const app_mod = localModule(b, target, optimize, options.main);
|
||||
app_mod.addImport("native_sdk", native_sdk_mod);
|
||||
exports_mod.addImport("app", app_mod);
|
||||
}
|
||||
exports_mod.export_symbol_names = &mobile_export_symbol_names;
|
||||
|
||||
const lib = b.addLibrary(.{
|
||||
.linkage = .static,
|
||||
.name = options.name,
|
||||
.root_module = exports_mod,
|
||||
// The embed C ABI (`native_sdk_app_viewport`) is exactly the
|
||||
// f32-heavy SysV signature Zig 0.16.0's self-hosted x86_64 backend
|
||||
// miscompiles (see useLlvmWorkaround in the framework build.zig):
|
||||
// clang-compiled hosts calling a self-hosted Debug lib receive
|
||||
// corrupted inset/keyboard floats on x86_64 (Android emulators,
|
||||
// Intel simulators). Force LLVM there; Release already uses it.
|
||||
.use_llvm = useLlvmWorkaround(target),
|
||||
});
|
||||
b.installArtifact(lib);
|
||||
|
||||
const lib_step = b.step("lib", "Build the mobile embed static library");
|
||||
lib_step.dependOn(&b.addInstallArtifact(lib, .{}).step);
|
||||
}
|
||||
|
||||
/// The pieces `addApp` wires, for callers that extend the standard app
|
||||
/// build (extra native sources, frameworks, post-build steps such as
|
||||
/// entitlement signing). `install` is the artifact-install step behind the
|
||||
/// default `zig build`; append dependencies to it and to `run` to order
|
||||
/// work between the emitted binary and its consumers.
|
||||
pub const AppArtifacts = struct {
|
||||
exe: *std.Build.Step.Compile,
|
||||
tests: *std.Build.Step.Compile,
|
||||
install: *std.Build.Step.InstallArtifact,
|
||||
run: *std.Build.Step.Run,
|
||||
};
|
||||
|
||||
pub fn addApp(b: *std.Build, dep: *std.Build.Dependency, app_options: AppOptions) void {
|
||||
_ = addAppArtifacts(b, dep, app_options);
|
||||
}
|
||||
|
||||
pub fn addAppArtifacts(b: *std.Build, dep: *std.Build.Dependency, app_options: AppOptions) AppArtifacts {
|
||||
const target = nativeSdkTarget(b);
|
||||
const optimize_request = b.option(std.builtin.OptimizeMode, "optimize", "Prioritize performance, safety, or binary size");
|
||||
const optimize = exampleOptimizeMode(b, optimize_request, .Debug);
|
||||
const app_optimize = exampleOptimizeMode(b, optimize_request, .ReleaseFast);
|
||||
|
||||
// The core role is detected from the tree (never a flag or config):
|
||||
// builds with a custom `main` entry declared their core explicitly and
|
||||
// skip detection.
|
||||
const core_tree: CoreTree = if (std.mem.eql(u8, app_options.main, "src/main.zig"))
|
||||
detectCoreTree(b, app_options.app_root)
|
||||
else
|
||||
.zig;
|
||||
if (core_tree == .both) {
|
||||
@panic("\nthis app declares two cores: src/core.ts (TypeScript) and src/main.zig (Zig)." ++
|
||||
"\nAn app has exactly one core - the tree is the truth. Keep src/core.ts and delete" ++
|
||||
" src/main.zig,\nor keep src/main.zig and delete src/core.ts. (Other Zig files under" ++
|
||||
" src/ are fine either way.)\n");
|
||||
}
|
||||
const ts_stage: ?TsCoreStage = if (core_tree == .ts) tsCoreStage(b, dep, app_options.app_root) else null;
|
||||
|
||||
// Mobile targets get the embed static library as a `lib` step: the
|
||||
// artifact the toolkit-owned iOS host (and any hand-written shim)
|
||||
// links, so `native dev|package --target ios` works against every
|
||||
// standard app build — generated graph or ejected — with nothing but
|
||||
// `-Dtarget`. Desktop targets keep the step absent.
|
||||
if (ts_stage != null and (target.result.os.tag == .ios or target.result.abi.isAndroid())) {
|
||||
@panic("\nTypeScript app cores build desktop apps today; the mobile embed library for TS" ++
|
||||
" cores lands with the mobile host tier.\nBuild for a desktop target, or port the core" ++
|
||||
" to a Zig `mobileOptions` app for mobile.\n");
|
||||
}
|
||||
if (target.result.os.tag == .ios or target.result.abi.isAndroid()) {
|
||||
addMobileLibWithTarget(b, dep, target, optimize, .{
|
||||
.name = app_options.name,
|
||||
.main = appPath(b, app_options.app_root, app_options.main),
|
||||
});
|
||||
}
|
||||
const platform_option = b.option(PlatformOption, "platform", "Desktop backend: auto, null, macos, linux, windows") orelse .auto;
|
||||
const trace_option = b.option(TraceOption, "trace", "Trace output: off, events, runtime, all") orelse .events;
|
||||
const debug_overlay = b.option(bool, "debug-overlay", "Enable debug overlay output") orelse false;
|
||||
const automation_enabled = b.option(bool, "automation", "Enable Native SDK automation artifacts") orelse false;
|
||||
const js_bridge_enabled = b.option(bool, "js-bridge", "Enable optional JavaScript bridge stubs") orelse false;
|
||||
const web_engine_override = b.option(WebEngineOption, "web-engine", "Override app.zon web engine: system, chromium");
|
||||
const web_layer_override = b.option(WebLayerOption, "web-layer", "Override app.zon webview_layer: auto, include, exclude");
|
||||
const cef_dir_override = b.option([]const u8, "cef-dir", "Override CEF root directory for Chromium builds");
|
||||
const cef_auto_install_override = b.option(bool, "cef-auto-install", "Override app.zon CEF auto-install setting");
|
||||
const selected_platform: PlatformOption = switch (platform_option) {
|
||||
.auto => if (target.result.os.tag == .macos) .macos else if (target.result.os.tag == .linux) .linux else if (target.result.os.tag == .windows) .windows else .null,
|
||||
else => platform_option,
|
||||
};
|
||||
if (selected_platform == .macos and target.result.os.tag != .macos) {
|
||||
@panic("-Dplatform=macos requires a macOS target");
|
||||
}
|
||||
if (selected_platform == .linux and target.result.os.tag != .linux) {
|
||||
@panic("-Dplatform=linux requires a Linux target");
|
||||
}
|
||||
if (selected_platform == .windows and target.result.os.tag != .windows) {
|
||||
@panic("-Dplatform=windows requires a Windows target");
|
||||
}
|
||||
const app_config = appManifestBuildConfig(b, app_options.app_root);
|
||||
const web_engine = web_engine_override orelse app_config.web_engine;
|
||||
const cef_dir = cef_dir_override orelse defaultCefDir(selected_platform, app_config.cef_dir);
|
||||
const cef_auto_install = cef_auto_install_override orelse app_config.cef_auto_install;
|
||||
if (web_engine == .chromium and selected_platform != .macos) {
|
||||
@panic("-Dweb-engine=chromium currently requires -Dplatform=macos");
|
||||
}
|
||||
const web_layer = resolveWebLayer(app_config, web_engine, web_layer_override);
|
||||
|
||||
const options = b.addOptions();
|
||||
options.addOption([]const u8, "platform", switch (selected_platform) {
|
||||
.auto => unreachable,
|
||||
.null => "null",
|
||||
.macos => "macos",
|
||||
.linux => "linux",
|
||||
.windows => "windows",
|
||||
});
|
||||
options.addOption([]const u8, "trace", @tagName(trace_option));
|
||||
options.addOption([]const u8, "web_engine", @tagName(web_engine));
|
||||
options.addOption(bool, "debug_overlay", debug_overlay);
|
||||
options.addOption(bool, "automation", automation_enabled);
|
||||
options.addOption(bool, "js_bridge", js_bridge_enabled);
|
||||
options.addOption(bool, "web_layer", web_layer);
|
||||
const options_mod = options.createModule();
|
||||
|
||||
const app_mod = appModule(b, dep, target, app_optimize, app_options, options_mod, ts_stage);
|
||||
const exe = b.addExecutable(.{
|
||||
.name = app_options.name,
|
||||
.root_module = app_mod,
|
||||
});
|
||||
linkPlatform(b, dep, target, app_mod, exe, selected_platform, web_engine, web_layer, cef_dir, cef_auto_install);
|
||||
const install = b.addInstallArtifact(exe, .{});
|
||||
b.getInstallStep().dependOn(&install.step);
|
||||
|
||||
const run = b.addRunArtifact(exe);
|
||||
addCefRuntimeRunFiles(b, target, run, exe, web_engine, cef_dir);
|
||||
addWebView2RuntimeRunFiles(dep, target, run, web_engine, web_layer);
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run.step);
|
||||
|
||||
const test_app_mod = if (app_optimize == optimize) app_mod else appModule(b, dep, target, optimize, app_options, options_mod, ts_stage);
|
||||
const tests = b.addTest(.{ .root_module = test_app_mod, .use_llvm = useLlvmWorkaround(target) });
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&b.addRunArtifact(tests).step);
|
||||
|
||||
// `native test` must surface the app's compile-time teaching errors,
|
||||
// not just its test failures. Test builds never analyze `main` (the
|
||||
// test runner replaces the entry point), so rules that fire inside it
|
||||
// — UiApp.create's Model-defaults rule above all — used to ambush at
|
||||
// `native build`, the LAST step in the loop. Compiling this object
|
||||
// forces full semantic analysis of the app module, entry point
|
||||
// included; nothing links or runs.
|
||||
const analysis_root = b.addWriteFiles().add("app_analysis.zig",
|
||||
\\//! Generated by the app build: force semantic analysis of the
|
||||
\\//! app's entry point at test time. Exactly `main`, transitively —
|
||||
\\//! the same surface `native build` analyzes — so test-time can
|
||||
\\//! never be stricter than the build it fronts.
|
||||
\\comptime {
|
||||
\\ const app = @import("app");
|
||||
\\ if (@hasDecl(app, "main")) _ = &app.main;
|
||||
\\}
|
||||
\\
|
||||
);
|
||||
const analysis_mod = b.createModule(.{
|
||||
.root_source_file = analysis_root,
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
analysis_mod.addImport("app", test_app_mod);
|
||||
const analysis_obj = b.addObject(.{
|
||||
.name = b.fmt("{s}-analysis", .{app_options.name}),
|
||||
.root_module = analysis_mod,
|
||||
.use_llvm = useLlvmWorkaround(target),
|
||||
});
|
||||
test_step.dependOn(&analysis_obj.step);
|
||||
|
||||
// `zig build model-contract`: reflect the app's Model/Msg into
|
||||
// zig-out/model-contract.zon so `native check` can verify markup
|
||||
// bindings against the app's real surface without compiling the app.
|
||||
// The artifact carries a hash over the app's Zig sources; the checker
|
||||
// degrades to structural checking when it goes stale. Apps without a
|
||||
// pub Model/Msg pair make this a silent no-op. The test step refreshes
|
||||
// the artifact too, so CI-checked apps always hold a fresh one.
|
||||
const contract_root = b.addWriteFiles().add("model_contract_emit.zig",
|
||||
\\//! Generated by the app build: emits the model contract artifact
|
||||
\\//! (see the toolkit's ui_markup_contract.zig).
|
||||
\\const std = @import("std");
|
||||
\\const native_sdk = @import("native_sdk");
|
||||
\\const app = @import("app");
|
||||
\\
|
||||
\\pub fn main(init: std.process.Init) !void {
|
||||
\\ try native_sdk.canvas.emitModelContractMain(app, init);
|
||||
\\}
|
||||
\\
|
||||
);
|
||||
// The emit root must share the app module's native_sdk instance so
|
||||
// the Msg payload types it classifies are the same types the app
|
||||
// declares its variants with.
|
||||
const contract_mod = b.createModule(.{
|
||||
.root_source_file = contract_root,
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
contract_mod.addImport("app", test_app_mod);
|
||||
if (test_app_mod.import_table.get("native_sdk")) |sdk_mod| {
|
||||
contract_mod.addImport("native_sdk", sdk_mod);
|
||||
}
|
||||
const contract_exe = b.addExecutable(.{
|
||||
.name = b.fmt("{s}-model-contract", .{app_options.name}),
|
||||
.root_module = contract_mod,
|
||||
.use_llvm = useLlvmWorkaround(target),
|
||||
});
|
||||
const contract_run = b.addRunArtifact(contract_exe);
|
||||
contract_run.setCwd(b.path(app_options.app_root));
|
||||
contract_run.addArgs(&.{ "--src", "src", "--out", "zig-out/model-contract.zon" });
|
||||
contract_run.has_side_effects = true;
|
||||
const contract_step = b.step("model-contract", "Emit zig-out/model-contract.zon for `native check`");
|
||||
contract_step.dependOn(&contract_run.step);
|
||||
test_step.dependOn(&contract_run.step);
|
||||
|
||||
// `zig build package`: bundle the built binary through the `native`
|
||||
// CLI (built from the native_sdk dependency), so a scaffolded app can
|
||||
// package itself without locating the CLI by hand.
|
||||
const host_os = b.graph.host.result.os.tag;
|
||||
const package_target: ?[]const u8 = switch (host_os) {
|
||||
.macos => "macos",
|
||||
.linux => "linux",
|
||||
.windows => "windows",
|
||||
else => null,
|
||||
};
|
||||
if (package_target) |package_target_name| {
|
||||
const package_run = b.addRunArtifact(dep.artifact("native"));
|
||||
// The CLI resolves SDK-owned package inputs (the vendored
|
||||
// WebView2 loader) from the framework root; the cached artifact's
|
||||
// own location cannot derive it, so hand it over explicitly.
|
||||
package_run.setEnvironmentVariable("NATIVE_SDK_PATH", dep.builder.pathFromRoot("."));
|
||||
package_run.addArgs(&.{ "package", "--target", package_target_name, "--manifest", "app.zon", "--output" });
|
||||
package_run.addArg(if (host_os == .macos)
|
||||
b.fmt("zig-out/package/{s}.app", .{app_options.name})
|
||||
else
|
||||
b.fmt("zig-out/package/{s}", .{package_target_name}));
|
||||
package_run.addArg("--binary");
|
||||
package_run.addFileArg(exe.getEmittedBin());
|
||||
// The archive and report names carry an optimize label; this
|
||||
// build graph knows the packaged binary's REAL mode, so forward
|
||||
// it instead of letting the CLI assume one.
|
||||
package_run.addArgs(&.{ "--optimize", @tagName(app_optimize) });
|
||||
// Forward the RESOLVED web-layer decision, never the raw inputs:
|
||||
// this graph already decided web vs native-only for the exe it is
|
||||
// packaging (app.zon declarations plus -Dweb-layer/-Dweb-engine),
|
||||
// and the CLI re-inferring from app.zon alone would miss a
|
||||
// flag-driven override — a WebView2-referencing exe packaged
|
||||
// without its loader. Handing over the decision itself makes
|
||||
// exe/package agreement structural.
|
||||
package_run.addArgs(&.{ "--web-layer", if (web_layer) "include" else "exclude" });
|
||||
// Same reasoning for the web engine: the CLI defaults to system, so
|
||||
// a Chromium exe packaged without these flags would ship no CEF
|
||||
// runtime (the generated build graph already forwards them).
|
||||
package_run.addArgs(&.{ "--web-engine", @tagName(web_engine), "--cef-dir", cef_dir });
|
||||
if (cef_auto_install) package_run.addArg("--cef-auto-install");
|
||||
package_run.has_side_effects = true;
|
||||
const package_step = b.step("package", "Create a distributable package via the native CLI");
|
||||
package_step.dependOn(&package_run.step);
|
||||
}
|
||||
|
||||
return .{ .exe = exe, .tests = tests, .install = install, .run = run };
|
||||
}
|
||||
|
||||
/// Zig 0.16.0's self-hosted x86_64 backend miscompiles the SysV C calling
|
||||
/// convention for f32-heavy signatures with interleaved pointer arguments
|
||||
/// (`native_sdk_app_viewport`: 11 f32s + 2 pointers): both the caller and
|
||||
/// the callee place/read the wrong registers and stack slots, so safe-area
|
||||
/// insets arrive as garbage on x86_64 Debug builds while every LLVM-backed
|
||||
/// build is correct. Minimal repro (fails under `zig test`, passes with
|
||||
/// `-fllvm` on x86_64-linux):
|
||||
///
|
||||
/// fn take(a: ?*anyopaque, w: f32, h: f32, s: f32, p: ?*anyopaque,
|
||||
/// t: f32, r: f32, bo: f32, l: f32, kt: f32, kr: f32, kb: f32,
|
||||
/// kl: f32) callconv(.c) void { ... }
|
||||
///
|
||||
/// Force the LLVM backend on x86_64 until the upstream backend is fixed;
|
||||
/// Release modes already default to LLVM, so this only changes Debug.
|
||||
pub fn useLlvmWorkaround(target: std.Build.ResolvedTarget) ?bool {
|
||||
return if (target.result.cpu.arch == .x86_64) true else null;
|
||||
}
|
||||
|
||||
fn exampleOptimizeMode(b: *std.Build, requested: ?std.builtin.OptimizeMode, default_mode: std.builtin.OptimizeMode) std.builtin.OptimizeMode {
|
||||
if (requested) |mode| return mode;
|
||||
return switch (b.release_mode) {
|
||||
.off => default_mode,
|
||||
.any, .fast => .ReleaseFast,
|
||||
.safe => .ReleaseSafe,
|
||||
.small => .ReleaseSmall,
|
||||
};
|
||||
}
|
||||
|
||||
fn appModule(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, app_options: AppOptions, options_mod: *std.Build.Module, ts_stage: ?TsCoreStage) *std.Build.Module {
|
||||
const native_sdk_mod = nativeSdkModule(b, dep, target, optimize);
|
||||
const runner_mod = b.createModule(.{
|
||||
.root_source_file = dep.path("src/app_runner/root.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
const manifest_mod = b.createModule(.{ .root_source_file = b.path(appPath(b, app_options.app_root, "app.zon")) });
|
||||
runner_mod.addImport("native_sdk", native_sdk_mod);
|
||||
runner_mod.addImport("build_options", options_mod);
|
||||
runner_mod.addImport("app_manifest_zon", manifest_mod);
|
||||
|
||||
const app_mod = if (ts_stage) |stage|
|
||||
// TypeScript core: the app module roots at the staged generated
|
||||
// wiring (ts_core_main.zig beside the transpiled core.zig, its rt
|
||||
// kernel, and the app's markup).
|
||||
b.createModule(.{
|
||||
.root_source_file = stage.main_root,
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
})
|
||||
else
|
||||
localModule(b, target, optimize, appPath(b, app_options.app_root, app_options.main));
|
||||
app_mod.addImport("native_sdk", native_sdk_mod);
|
||||
app_mod.addImport("runner", runner_mod);
|
||||
if (ts_stage != null) {
|
||||
// The wiring derives scene/identity/security from app.zon itself.
|
||||
app_mod.addImport("app_manifest_zon", manifest_mod);
|
||||
}
|
||||
return app_mod;
|
||||
}
|
||||
|
||||
fn nativeSdkTarget(b: *std.Build) std.Build.ResolvedTarget {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
if (target.result.os.tag != .macos) return target;
|
||||
|
||||
if (b.sysroot == null) {
|
||||
b.sysroot = macosSdkPath(b) orelse b.sysroot;
|
||||
}
|
||||
|
||||
var query = target.query;
|
||||
query.os_tag = .macos;
|
||||
query.os_version_min = .{ .semver = .{ .major = 11, .minor = 0, .patch = 0 } };
|
||||
return b.resolveTargetQuery(query);
|
||||
}
|
||||
|
||||
fn macosSdkPath(b: *std.Build) ?[]const u8 {
|
||||
if (b.graph.environ_map.get("SDKROOT")) |sdkroot| {
|
||||
if (sdkroot.len > 0) return sdkroot;
|
||||
}
|
||||
|
||||
const result = std.process.run(b.allocator, b.graph.io, .{
|
||||
.argv = &.{ "xcrun", "--sdk", "macosx", "--show-sdk-path" },
|
||||
.stdout_limit = .limited(4096),
|
||||
.stderr_limit = .limited(4096),
|
||||
}) catch return null;
|
||||
defer b.allocator.free(result.stderr);
|
||||
if (result.term != .exited or result.term.exited != 0) {
|
||||
b.allocator.free(result.stdout);
|
||||
return null;
|
||||
}
|
||||
return std.mem.trimEnd(u8, result.stdout, "\r\n");
|
||||
}
|
||||
|
||||
fn localModule(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, path: []const u8) *std.Build.Module {
|
||||
return b.createModule(.{
|
||||
.root_source_file = b.path(path),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
}
|
||||
|
||||
fn nativeSdkModule(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) *std.Build.Module {
|
||||
const geometry_mod = externalModule(b, dep, target, optimize, "src/primitives/geometry/root.zig");
|
||||
const assets_mod = externalModule(b, dep, target, optimize, "src/primitives/assets/root.zig");
|
||||
const app_dirs_mod = externalModule(b, dep, target, optimize, "src/primitives/app_dirs/root.zig");
|
||||
const trace_mod = externalModule(b, dep, target, optimize, "src/primitives/trace/root.zig");
|
||||
const app_manifest_mod = externalModule(b, dep, target, optimize, "src/primitives/app_manifest/root.zig");
|
||||
const diagnostics_mod = externalModule(b, dep, target, optimize, "src/primitives/diagnostics/root.zig");
|
||||
const platform_info_mod = externalModule(b, dep, target, optimize, "src/primitives/platform_info/root.zig");
|
||||
const json_mod = externalModule(b, dep, target, optimize, "src/primitives/json/root.zig");
|
||||
const canvas_mod = externalModule(b, dep, target, optimize, "src/primitives/canvas/root.zig");
|
||||
canvas_mod.addImport("geometry", geometry_mod);
|
||||
canvas_mod.addImport("json", json_mod);
|
||||
const debug_mod = externalModule(b, dep, target, optimize, "src/debug/root.zig");
|
||||
debug_mod.addImport("app_dirs", app_dirs_mod);
|
||||
debug_mod.addImport("trace", trace_mod);
|
||||
|
||||
const native_sdk_mod = externalModule(b, dep, target, optimize, "src/root.zig");
|
||||
native_sdk_mod.addImport("geometry", geometry_mod);
|
||||
native_sdk_mod.addImport("assets", assets_mod);
|
||||
native_sdk_mod.addImport("app_dirs", app_dirs_mod);
|
||||
native_sdk_mod.addImport("trace", trace_mod);
|
||||
native_sdk_mod.addImport("app_manifest", app_manifest_mod);
|
||||
native_sdk_mod.addImport("diagnostics", diagnostics_mod);
|
||||
native_sdk_mod.addImport("platform_info", platform_info_mod);
|
||||
native_sdk_mod.addImport("json", json_mod);
|
||||
native_sdk_mod.addImport("canvas", canvas_mod);
|
||||
return native_sdk_mod;
|
||||
}
|
||||
|
||||
fn externalModule(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, path: []const u8) *std.Build.Module {
|
||||
return b.createModule(.{
|
||||
.root_source_file = dep.path(path),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
}
|
||||
|
||||
// -fno-sanitize=builtin on every ObjC compile: Zig 0.16.0's Debug UBSan
|
||||
// aborts any process whose first dispatch_once runs — the macOS SDK's
|
||||
// inline `_dispatch_once` ends in `__builtin_assume(*predicate == ~0l)`
|
||||
// (dispatch/once.h), Zig's bundled clang instruments that builtin, and the
|
||||
// check fires spuriously at startup; zig's ubsan_rt then cannot even decode
|
||||
// the report ("invalid enum value" / "passing zero to clz()" panics).
|
||||
// Reproduced with a 10-line `zig cc` program against both the 14.5 and
|
||||
// 26.0 SDKs. Release builds never hit it (no UBSan), which is why only
|
||||
// Debug-built examples (standardOptimizeOption default) crashed.
|
||||
fn linkPlatform(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, app_mod: *std.Build.Module, exe: *std.Build.Step.Compile, platform: PlatformOption, web_engine: WebEngineOption, web_layer: bool, cef_dir: []const u8, cef_auto_install: bool) void {
|
||||
if (platform == .macos) {
|
||||
switch (web_engine) {
|
||||
.system => {
|
||||
const sdk_include = if (b.sysroot) |sysroot| b.fmt("-I{s}/usr/include", .{sysroot}) else "";
|
||||
const flags: []const []const u8 = if (b.sysroot) |sysroot| &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC", "-mmacosx-version-min=11.0", "-isysroot", sysroot, sdk_include } else &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC", "-mmacosx-version-min=11.0" };
|
||||
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/macos/appkit_host.m"), .flags = flags });
|
||||
app_mod.linkFramework("WebKit", .{});
|
||||
},
|
||||
.chromium => {
|
||||
const cef_check = addCefCheck(b, target, cef_dir);
|
||||
if (cef_auto_install) {
|
||||
const cef_auto = b.addSystemCommand(&.{ "native", "cef", "install", "--dir", cef_dir });
|
||||
cef_check.step.dependOn(&cef_auto.step);
|
||||
}
|
||||
exe.step.dependOn(&cef_check.step);
|
||||
const include_arg = b.fmt("-I{s}", .{cef_dir});
|
||||
const define_arg = b.fmt("-DNATIVE_SDK_CEF_DIR=\"{s}\"", .{cef_dir});
|
||||
// The SDK's usr/include must stay a system include dir (searched after zig's
|
||||
// bundled libc++/libc headers). A plain -I shadows libc++'s <string.h>/<math.h>
|
||||
// wrappers in ObjC++ and surfaces SDK nullability gaps as a diagnostic flood.
|
||||
const sdk_include = if (b.sysroot) |sysroot| b.fmt("-isystem{s}/usr/include", .{sysroot}) else "";
|
||||
const flags: []const []const u8 = if (b.sysroot) |sysroot| &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC++", "-std=c++17", "-stdlib=libc++", "-mmacosx-version-min=11.0", "-isysroot", sysroot, sdk_include, include_arg, define_arg } else &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC++", "-std=c++17", "-stdlib=libc++", "-mmacosx-version-min=11.0", include_arg, define_arg };
|
||||
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/macos/cef_host.mm"), .flags = flags });
|
||||
app_mod.addObjectFile(b.path(b.fmt("{s}/libcef_dll_wrapper/libcef_dll_wrapper.a", .{cef_dir})));
|
||||
app_mod.addFrameworkPath(b.path(b.fmt("{s}/Release", .{cef_dir})));
|
||||
app_mod.linkFramework("Chromium Embedded Framework", .{});
|
||||
app_mod.addRPath(.{ .cwd_relative = "@executable_path/Frameworks" });
|
||||
},
|
||||
}
|
||||
if (b.sysroot) |sysroot| {
|
||||
app_mod.addFrameworkPath(.{ .cwd_relative = b.pathJoin(&.{ sysroot, "System/Library/Frameworks" }) });
|
||||
}
|
||||
app_mod.linkFramework("AppKit", .{});
|
||||
// The audio playback service (the AppKit host's single AVPlayer).
|
||||
app_mod.linkFramework("AVFoundation", .{});
|
||||
// Spectrum analysis of the app's own playback: the MediaToolbox
|
||||
// audio tap hands the player's PCM to the host, and Accelerate
|
||||
// (vDSP) turns it into band magnitudes.
|
||||
app_mod.linkFramework("MediaToolbox", .{});
|
||||
app_mod.linkFramework("Accelerate", .{});
|
||||
app_mod.linkFramework("Foundation", .{});
|
||||
app_mod.linkFramework("CoreText", .{});
|
||||
app_mod.linkFramework("UniformTypeIdentifiers", .{});
|
||||
app_mod.linkFramework("Security", .{});
|
||||
app_mod.linkFramework("Metal", .{});
|
||||
app_mod.linkFramework("QuartzCore", .{});
|
||||
app_mod.linkSystemLibrary("c", .{});
|
||||
if (web_engine == .chromium) app_mod.linkSystemLibrary("c++", .{});
|
||||
} else if (platform == .linux) {
|
||||
switch (web_engine) {
|
||||
.system => if (web_layer) {
|
||||
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/linux/gtk_host.c"), .flags = &.{} });
|
||||
app_mod.linkSystemLibrary("gtk4", .{});
|
||||
app_mod.linkSystemLibrary("webkitgtk-6.0", .{});
|
||||
app_mod.linkSystemLibrary("dl", .{});
|
||||
} else {
|
||||
// Native-only app (nothing in app.zon declares web use):
|
||||
// compile the GTK host without the embedded web layer.
|
||||
// The stub define excludes the layer outright — the host
|
||||
// honors it before probing for the WebKitGTK header, so
|
||||
// the layer stays out even on machines where the
|
||||
// development package is installed — libwebkitgtk is
|
||||
// neither linked nor required at runtime, and the
|
||||
// executable carries no WebKit reference at all.
|
||||
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/linux/gtk_host.c"), .flags = &.{"-DNATIVE_SDK_ALLOW_WEBKITGTK_STUB"} });
|
||||
app_mod.linkSystemLibrary("gtk4", .{});
|
||||
app_mod.linkSystemLibrary("dl", .{});
|
||||
},
|
||||
.chromium => {
|
||||
const cef_check = addCefCheck(b, target, cef_dir);
|
||||
if (cef_auto_install) {
|
||||
const cef_auto = b.addSystemCommand(&.{ "native", "cef", "install", "--dir", cef_dir });
|
||||
cef_check.step.dependOn(&cef_auto.step);
|
||||
}
|
||||
exe.step.dependOn(&cef_check.step);
|
||||
const include_arg = b.fmt("-I{s}", .{cef_dir});
|
||||
const define_arg = b.fmt("-DNATIVE_SDK_CEF_DIR=\"{s}\"", .{cef_dir});
|
||||
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/linux/cef_host.cpp"), .flags = &.{ "-std=c++17", include_arg, define_arg } });
|
||||
app_mod.addObjectFile(b.path(b.fmt("{s}/libcef_dll_wrapper/libcef_dll_wrapper.a", .{cef_dir})));
|
||||
app_mod.addLibraryPath(b.path(b.fmt("{s}/Release", .{cef_dir})));
|
||||
app_mod.linkSystemLibrary("cef", .{});
|
||||
app_mod.addRPath(.{ .cwd_relative = "$ORIGIN" });
|
||||
},
|
||||
}
|
||||
app_mod.linkSystemLibrary("c", .{});
|
||||
if (web_engine == .chromium) app_mod.linkSystemLibrary("stdc++", .{});
|
||||
} else if (platform == .windows) {
|
||||
// Common-controls v6 side-by-side dependency: without this
|
||||
// manifest the loader binds the system-default v5 assembly, which
|
||||
// renders classic-styled controls and lacks the v6-only exports.
|
||||
// The manifest also declares per-monitor-v2 DPI awareness so the
|
||||
// canvas rasterizes at real device scale instead of Windows
|
||||
// bitmap-stretching a 96-DPI surface on scaled displays.
|
||||
exe.win32_manifest = dep.path("assets/native-sdk.manifest");
|
||||
switch (web_engine) {
|
||||
.system => if (web_layer) {
|
||||
// The vendored WebView2 SDK header (third_party/webview2)
|
||||
// turns on the host's embedded-WebView layer; the host
|
||||
// fails the compile by design if it cannot be found.
|
||||
app_mod.addIncludePath(dep.path("third_party/webview2/include"));
|
||||
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/windows/webview2_host.cpp"), .flags = &.{"-std=c++17"} });
|
||||
// WebView2Loader.dll rides next to the installed app
|
||||
// executable: the host loads it at runtime to discover
|
||||
// the machine's WebView2 runtime. Canvas apps never
|
||||
// touch it.
|
||||
const loader = b.addInstallBinFile(dep.path(webView2LoaderSubPath(target)), "WebView2Loader.dll");
|
||||
b.getInstallStep().dependOn(&loader.step);
|
||||
} else {
|
||||
// Native-only app (nothing in app.zon declares web use):
|
||||
// compile the host without the embedded-WebView layer.
|
||||
// The stub define excludes the layer outright — the host
|
||||
// honors it before probing for the WebView2 header, so
|
||||
// the layer stays out even on machines where the SDK
|
||||
// headers are reachable through the system include paths
|
||||
// — no WebView2Loader.dll is installed or path-wired,
|
||||
// and the executable carries no reference to it at all.
|
||||
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/windows/webview2_host.cpp"), .flags = &.{ "-std=c++17", "-DNATIVE_SDK_ALLOW_WEBVIEW2_STUB" } });
|
||||
},
|
||||
.chromium => {
|
||||
const cef_check = addCefCheck(b, target, cef_dir);
|
||||
if (cef_auto_install) {
|
||||
const cef_auto = b.addSystemCommand(&.{ "native", "cef", "install", "--dir", cef_dir });
|
||||
cef_check.step.dependOn(&cef_auto.step);
|
||||
}
|
||||
exe.step.dependOn(&cef_check.step);
|
||||
const include_arg = b.fmt("-I{s}", .{cef_dir});
|
||||
const define_arg = b.fmt("-DNATIVE_SDK_CEF_DIR=\"{s}\"", .{cef_dir});
|
||||
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/windows/cef_host.cpp"), .flags = &.{ "-std=c++17", include_arg, define_arg } });
|
||||
app_mod.addObjectFile(b.path(b.fmt("{s}/libcef_dll_wrapper/libcef_dll_wrapper.lib", .{cef_dir})));
|
||||
app_mod.addLibraryPath(b.path(b.fmt("{s}/Release", .{cef_dir})));
|
||||
},
|
||||
}
|
||||
app_mod.linkSystemLibrary("c", .{});
|
||||
app_mod.linkSystemLibrary("c++", .{});
|
||||
app_mod.linkSystemLibrary("user32", .{});
|
||||
app_mod.linkSystemLibrary("gdi32", .{});
|
||||
app_mod.linkSystemLibrary("imm32", .{});
|
||||
app_mod.linkSystemLibrary("comctl32", .{});
|
||||
app_mod.linkSystemLibrary("ole32", .{});
|
||||
app_mod.linkSystemLibrary("oleacc", .{});
|
||||
app_mod.linkSystemLibrary("shell32", .{});
|
||||
// The audio backend: Media Foundation (session + source resolver
|
||||
// + streaming audio renderer) and WinHTTP (the cache fill).
|
||||
app_mod.linkSystemLibrary("mf", .{});
|
||||
app_mod.linkSystemLibrary("mfplat", .{});
|
||||
app_mod.linkSystemLibrary("winhttp", .{});
|
||||
if (web_engine == .chromium) app_mod.linkSystemLibrary("libcef", .{});
|
||||
}
|
||||
}
|
||||
|
||||
/// The vendored WebView2Loader.dll for the target architecture, relative
|
||||
/// to the framework root.
|
||||
fn webView2LoaderSubPath(target: std.Build.ResolvedTarget) []const u8 {
|
||||
return if (target.result.cpu.arch == .aarch64)
|
||||
"third_party/webview2/arm64/WebView2Loader.dll"
|
||||
else
|
||||
"third_party/webview2/x64/WebView2Loader.dll";
|
||||
}
|
||||
|
||||
/// `zig build run` executes the cached artifact, which has no installed
|
||||
/// WebView2Loader.dll beside it; the vendored loader's directory goes on
|
||||
/// the run step's PATH so the host's LoadLibrary resolves it in dev runs.
|
||||
fn addWebView2RuntimeRunFiles(dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, run: *std.Build.Step.Run, web_engine: WebEngineOption, web_layer: bool) void {
|
||||
if (web_engine != .system) return;
|
||||
if (!web_layer) return;
|
||||
if (target.result.os.tag != .windows) return;
|
||||
const loader_dir = std.fs.path.dirname(webView2LoaderSubPath(target)).?;
|
||||
run.addPathDir(dep.builder.pathFromRoot(loader_dir));
|
||||
}
|
||||
|
||||
fn addCefRuntimeRunFiles(b: *std.Build, target: std.Build.ResolvedTarget, run: *std.Build.Step.Run, exe: *std.Build.Step.Compile, web_engine: WebEngineOption, cef_dir: []const u8) void {
|
||||
if (web_engine != .chromium) return;
|
||||
if (target.result.os.tag != .macos) return;
|
||||
const copy = b.addSystemCommand(&.{
|
||||
"sh", "-c",
|
||||
b.fmt(
|
||||
\\set -e
|
||||
\\exe="$0"
|
||||
\\exe_dir="$(dirname "$exe")"
|
||||
\\rm -rf "zig-out/Frameworks/Chromium Embedded Framework.framework" "zig-out/bin/Frameworks/Chromium Embedded Framework.framework" ".zig-cache/o/Frameworks/Chromium Embedded Framework.framework" &&
|
||||
\\mkdir -p "zig-out/Frameworks" "zig-out/bin/Frameworks" ".zig-cache/o/Frameworks" "$exe_dir" &&
|
||||
\\cp -R "{s}/Release/Chromium Embedded Framework.framework" "zig-out/Frameworks/" &&
|
||||
\\cp -R "{s}/Release/Chromium Embedded Framework.framework" "zig-out/bin/Frameworks/" &&
|
||||
\\cp -R "{s}/Release/Chromium Embedded Framework.framework" ".zig-cache/o/Frameworks/" &&
|
||||
\\cp "{s}/Release/Chromium Embedded Framework.framework/Libraries/libEGL.dylib" "$exe_dir/" &&
|
||||
\\cp "{s}/Release/Chromium Embedded Framework.framework/Libraries/libGLESv2.dylib" "$exe_dir/" &&
|
||||
\\cp "{s}/Release/Chromium Embedded Framework.framework/Libraries/libvk_swiftshader.dylib" "$exe_dir/" &&
|
||||
\\cp "{s}/Release/Chromium Embedded Framework.framework/Libraries/vk_swiftshader_icd.json" "$exe_dir/"
|
||||
, .{ cef_dir, cef_dir, cef_dir, cef_dir, cef_dir, cef_dir, cef_dir }),
|
||||
});
|
||||
copy.addFileArg(exe.getEmittedBin());
|
||||
run.step.dependOn(©.step);
|
||||
}
|
||||
|
||||
fn addCefCheck(b: *std.Build, target: std.Build.ResolvedTarget, cef_dir: []const u8) *std.Build.Step.Run {
|
||||
const script = switch (target.result.os.tag) {
|
||||
.macos => b.fmt(
|
||||
\\test -f "{s}/include/cef_app.h" &&
|
||||
\\test -d "{s}/Release/Chromium Embedded Framework.framework" &&
|
||||
\\test -f "{s}/libcef_dll_wrapper/libcef_dll_wrapper.a" || {{
|
||||
\\ echo "missing CEF dependency for -Dweb-engine=chromium" >&2
|
||||
\\ echo "Fix with: native cef install --dir {s}" >&2
|
||||
\\ exit 1
|
||||
\\}}
|
||||
, .{ cef_dir, cef_dir, cef_dir, cef_dir }),
|
||||
.linux => b.fmt(
|
||||
\\test -f "{s}/include/cef_app.h" &&
|
||||
\\test -f "{s}/Release/libcef.so" &&
|
||||
\\test -f "{s}/libcef_dll_wrapper/libcef_dll_wrapper.a" || {{
|
||||
\\ echo "missing CEF dependency for -Dweb-engine=chromium" >&2
|
||||
\\ echo "Fix with: native cef install --dir {s}" >&2
|
||||
\\ exit 1
|
||||
\\}}
|
||||
, .{ cef_dir, cef_dir, cef_dir, cef_dir }),
|
||||
.windows => b.fmt(
|
||||
\\test -f "{s}/include/cef_app.h" &&
|
||||
\\test -f "{s}/Release/libcef.dll" &&
|
||||
\\test -f "{s}/libcef_dll_wrapper/libcef_dll_wrapper.lib" || {{
|
||||
\\ echo "missing CEF dependency for -Dweb-engine=chromium" >&2
|
||||
\\ echo "Fix with: native cef install --dir {s}" >&2
|
||||
\\ exit 1
|
||||
\\}}
|
||||
, .{ cef_dir, cef_dir, cef_dir, cef_dir }),
|
||||
else => "echo unsupported CEF target >&2; exit 1",
|
||||
};
|
||||
return b.addSystemCommand(&.{ "sh", "-c", script });
|
||||
}
|
||||
|
||||
/// What the build graph reads out of app.zon: the web-engine/CEF knobs
|
||||
/// and the web-layer inference inputs. An unreadable or unparsable
|
||||
/// manifest falls back to the system engine WITH the web layer kept —
|
||||
/// over-inclusion is a size cost, wrong exclusion is a broken app.
|
||||
const AppManifestBuildConfig = struct {
|
||||
web_engine: WebEngineOption = .system,
|
||||
cef_dir: []const u8 = "third_party/cef/macos",
|
||||
cef_auto_install: bool = false,
|
||||
webview_layer: WebLayerOption = .auto,
|
||||
/// The first web declaration found (for teaching messages), or null
|
||||
/// when app.zon declares no web use. `web_engine = "system"` alone is
|
||||
/// NOT web intent — it is the default in many canvas manifests.
|
||||
web_declaration: ?web_layer_contract.Declaration = null,
|
||||
};
|
||||
|
||||
/// The lenient app.zon shape the build graph parses for inference: only
|
||||
/// the fields that decide the web layer and the web engine; everything
|
||||
/// else is ignored. Full schema validation stays with `native validate`
|
||||
/// and the runner's comptime import.
|
||||
const InferenceManifest = struct {
|
||||
capabilities: []const []const u8 = &.{},
|
||||
web_engine: []const u8 = "system",
|
||||
webview_layer: []const u8 = "auto",
|
||||
cef: struct {
|
||||
dir: []const u8 = "third_party/cef/macos",
|
||||
auto_install: bool = false,
|
||||
} = .{},
|
||||
frontend: ?struct {} = null,
|
||||
shell: struct {
|
||||
windows: []const struct {
|
||||
views: []const struct {
|
||||
kind: []const u8 = "",
|
||||
} = &.{},
|
||||
} = &.{},
|
||||
} = .{},
|
||||
};
|
||||
|
||||
fn defaultCefDir(platform: PlatformOption, configured: []const u8) []const u8 {
|
||||
if (!std.mem.eql(u8, configured, "third_party/cef/macos")) return configured;
|
||||
return switch (platform) {
|
||||
.linux => "third_party/cef/linux",
|
||||
.windows => "third_party/cef/windows",
|
||||
else => configured,
|
||||
};
|
||||
}
|
||||
|
||||
/// Resolve an app-relative path against `app_root` (see AppOptions). Kept
|
||||
/// lexical: `b.path` rejects absolute paths and the generated build graph
|
||||
/// hands us "../..", which openat/b.path both resolve fine.
|
||||
fn appPath(b: *std.Build, app_root: []const u8, sub_path: []const u8) []const u8 {
|
||||
if (app_root.len == 0 or std.mem.eql(u8, app_root, ".")) return sub_path;
|
||||
return b.pathJoin(&.{ app_root, sub_path });
|
||||
}
|
||||
|
||||
fn appManifestBuildConfig(b: *std.Build, app_root: []const u8) AppManifestBuildConfig {
|
||||
// The fallback for a manifest this lenient parse cannot read keeps
|
||||
// the web layer (see AppManifestBuildConfig): a shape mismatch here
|
||||
// is not proof the app declares no web use.
|
||||
const fallback: AppManifestBuildConfig = .{ .web_declaration = .unreadable_manifest };
|
||||
const source = b.build_root.handle.readFileAlloc(b.graph.io, appPath(b, app_root, "app.zon"), b.allocator, .limited(1024 * 1024)) catch return fallback;
|
||||
const source_z = b.allocator.dupeZ(u8, source) catch return fallback;
|
||||
@setEvalBranchQuota(2000);
|
||||
const raw = std.zon.parse.fromSliceAlloc(InferenceManifest, b.allocator, source_z, null, .{ .ignore_unknown_fields = true }) catch return fallback;
|
||||
return .{
|
||||
.web_engine = web_layer_contract.parseWebEngine(raw.web_engine) orelse .system,
|
||||
.cef_dir = raw.cef.dir,
|
||||
.cef_auto_install = raw.cef.auto_install,
|
||||
.webview_layer = web_layer_contract.parseWebViewLayer(raw.webview_layer) orelse @panic("app.zon .webview_layer must be \"auto\", \"include\", or \"exclude\""),
|
||||
.web_declaration = web_layer_contract.manifestDeclaration(raw),
|
||||
};
|
||||
}
|
||||
|
||||
/// The web-layer decision for this build: the shared contract fed this
|
||||
/// boundary's inputs — the manifest declarations from the lenient parse
|
||||
/// and the engine RESOLVED from `-Dweb-engine` orelse app.zon. WEB means
|
||||
/// the embedded-WebView layer compiles in; NATIVE-ONLY compiles the host
|
||||
/// without it. `.webview_layer` (and `-Dweb-layer`) override the
|
||||
/// inference — but an exclude that contradicts a web declaration is a
|
||||
/// hard configure error, never a silently broken app.
|
||||
fn resolveWebLayer(config: AppManifestBuildConfig, web_engine: WebEngineOption, override: ?WebLayerOption) bool {
|
||||
const setting = override orelse config.webview_layer;
|
||||
const declaration = web_layer_contract.foldEngine(config.web_declaration, web_engine);
|
||||
const decision = web_layer_contract.decide(setting, declaration) catch std.debug.panic(
|
||||
"the web layer is excluded ({s}) but the app declares web use ({s}); remove the exclude or drop the web declaration",
|
||||
.{ if (override != null) "-Dweb-layer=exclude" else "app.zon .webview_layer = \"exclude\"", declaration.?.text() },
|
||||
);
|
||||
return decision.enabled;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Run a TypeScript module of the @native-sdk/core transpiler tier under
|
||||
// node from ANY SDK layout:
|
||||
//
|
||||
// node ts_run.mjs <module.ts> [args...]
|
||||
//
|
||||
// From a repo checkout `node packages/core/src/cli.ts` works directly —
|
||||
// node strips types natively. From the npm-installed @native-sdk/cli the
|
||||
// SAME file sits inside node_modules, where node refuses its builtin type
|
||||
// stripping by design (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING). This
|
||||
// runner closes that gap without forking the layouts: it registers a load
|
||||
// hook that strips ONLY node_modules-resident .ts modules, using the
|
||||
// transpiler's own installed TypeScript (the `npm ci` dependency inside
|
||||
// packages/core, resolved from the target module's location), and leaves
|
||||
// every other module to node's native handling. Then it imports the
|
||||
// requested module with argv respliced so the target sees its usual shape
|
||||
// (its own path at argv[1], its arguments from argv[2]).
|
||||
//
|
||||
// On node builds without module.registerHooks (< 22.15) the hook is
|
||||
// skipped: repo checkouts keep working natively and the npm layout fails
|
||||
// with node's own teaching error naming the unsupported stripping.
|
||||
|
||||
import module, { createRequire } from 'node:module';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { pathToFileURL, fileURLToPath } from 'node:url';
|
||||
|
||||
const target = process.argv[2];
|
||||
if (!target) {
|
||||
console.error('usage: node ts_run.mjs <module.ts> [args...]');
|
||||
process.exit(2);
|
||||
}
|
||||
const targetPath = resolve(target);
|
||||
// Drop the runner from argv so the target module parses its own argv
|
||||
// exactly as when node runs it directly.
|
||||
process.argv.splice(1, 1);
|
||||
|
||||
if (typeof module.registerHooks === 'function') {
|
||||
let ts = null;
|
||||
module.registerHooks({
|
||||
load(url, context, nextLoad) {
|
||||
if (url.startsWith('file:') && url.endsWith('.ts') && url.includes('node_modules')) {
|
||||
const filePath = fileURLToPath(url);
|
||||
// The transpiler's own dependency, resolved from the transpiler's
|
||||
// location (packages/core/node_modules after the taught `npm ci`).
|
||||
ts ??= createRequire(targetPath)('@typescript/typescript6');
|
||||
const { outputText } = ts.transpileModule(readFileSync(filePath, 'utf8'), {
|
||||
fileName: filePath,
|
||||
compilerOptions: {
|
||||
target: ts.ScriptTarget.ESNext,
|
||||
module: ts.ModuleKind.ESNext,
|
||||
verbatimModuleSyntax: true,
|
||||
},
|
||||
});
|
||||
return { format: 'module', source: outputText, shortCircuit: true };
|
||||
}
|
||||
return nextLoad(url, context);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await import(pathToFileURL(targetPath).href);
|
||||
@@ -0,0 +1,29 @@
|
||||
# Changelog fragments
|
||||
|
||||
Agents and feature branches do not edit `CHANGELOG.md` directly — concurrent work would conflict on every merge. Instead, each change lands with a small fragment in this directory, and `scripts/changelog-merge.sh` folds all fragments into the `## Unreleased` section of `CHANGELOG.md` (typically during release prep, see RELEASING.md).
|
||||
|
||||
## Writing a fragment
|
||||
|
||||
Add `changelog.d/<slug>.md`, where `<slug>` names your change (e.g. `gpu-dashboard-smoke-budget.md`). The file holds a bullet or two for one changelog section:
|
||||
|
||||
- The first line starts with a section tag: `feature:`, `improvement:`, or `fix:`, followed by the first bullet's text.
|
||||
- Any further lines are additional bullets (start them with `- `; bare lines get `- ` prefixed for you).
|
||||
- One tag per fragment. A change that touches multiple sections ships multiple fragments.
|
||||
- Match the CHANGELOG voice: bold lead-in, then the story. One line per bullet — never hard-wrap.
|
||||
|
||||
Example (`changelog.d/faster-frobnication.md`):
|
||||
|
||||
```
|
||||
improvement: **Faster frobnication**: the frobnicator now memoizes per-frame, cutting rebuild time ~40% on the kanban example.
|
||||
- **Frobnication telemetry**: automation snapshots report `frob_cache_hits=`.
|
||||
```
|
||||
|
||||
Tags map to sections: `feature:` → `### New Features`, `improvement:` → `### Improvements`, `fix:` → `### Bug Fixes`.
|
||||
|
||||
## Merging
|
||||
|
||||
```sh
|
||||
scripts/changelog-merge.sh
|
||||
```
|
||||
|
||||
appends every fragment's bullets to the end of its section under `## Unreleased` (creating the section — or the whole `## Unreleased` block — when missing), then deletes the merged fragments. This `README.md` is never merged or deleted. The script refuses unknown tags loudly instead of guessing.
|
||||
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
.next/
|
||||
next-env.d.ts
|
||||
.next-gate/
|
||||
.next-agent/
|
||||
.next-check/
|
||||
@@ -0,0 +1,22 @@
|
||||
# Docs Site Conventions
|
||||
|
||||
## MDX Tables
|
||||
|
||||
Always use HTML `<table>` syntax in MDX pages, never markdown pipe tables. This ensures consistent styling and avoids MDX parsing edge cases.
|
||||
|
||||
```html
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Column</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>field</code></td>
|
||||
<td>What it does</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
```
|
||||
@@ -0,0 +1,40 @@
|
||||
import createMDX from "@next/mdx";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
// Resolve the plugin to an absolute path (still a string, so the config
|
||||
// stays serializable for Turbopack). A bare "remark-gfm" is require()d
|
||||
// from the MDX loader's own package context, which under pnpm's strict
|
||||
// module isolation cannot see this app's dependencies — production
|
||||
// builds resolved it, the Turbopack dev server did not.
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const withMDX = createMDX({
|
||||
options: {
|
||||
// GFM is what gives .mdx pages pipe tables (plus autolinks and
|
||||
// strikethrough) — without it, table markdown renders as a plain
|
||||
// paragraph of pipes.
|
||||
remarkPlugins: [[require.resolve("remark-gfm")]],
|
||||
},
|
||||
});
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
pageExtensions: ["ts", "tsx", "md", "mdx"],
|
||||
// CI-style builds set NEXT_DIST_DIR so `pnpm check` never shares .next
|
||||
// with a running dev server (a shared dist dir corrupts the dev cache).
|
||||
distDir: process.env.NEXT_DIST_DIR || ".next",
|
||||
// The gate builds into .next-gate INSIDE this dir; without an ignore,
|
||||
// the dev watcher sees every one of those build files land and
|
||||
// recompiles continuously whenever a gate runs.
|
||||
watchOptions: {
|
||||
ignored: ["**/.next-gate/**", "**/.next-check/**"],
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
// The Philosophy page became the Introduction, the opening page of the docs.
|
||||
{ source: "/philosophy", destination: "/introduction", permanent: true },
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default withMDX(nextConfig);
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "@native-sdk/docs",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.23.0",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"check": "pnpm typecheck && pnpm build && node scripts/check-code-toggle.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdx-js/loader": "^3",
|
||||
"@mdx-js/react": "^3",
|
||||
"@next/mdx": "^16.1.6",
|
||||
"clsx": "^2.1.1",
|
||||
"geist": "^1.7.0",
|
||||
"next": "^16.2.9",
|
||||
"next-themes": "^0.4.6",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19",
|
||||
"react-dom": "^19",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"shiki": "^4.0.2",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/mdx": "^2",
|
||||
"@types/node": "^22",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"postcss@<8.5.10": ">=8.5.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 614 B |
|
After Width: | Height: | Size: 634 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 854 B |
|
After Width: | Height: | Size: 864 B |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 822 B |
|
After Width: | Height: | Size: 832 B |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 6.7 KiB |