This commit is contained in:
Chris Tate
2026-05-08 17:54:42 -05:00
commit aba572866d
244 changed files with 34132 additions and 0 deletions
@@ -0,0 +1,39 @@
---
name: automate-zero-native
description: Automate and inspect running zero-native WebView shell apps via the built-in automation server. Use when the user asks to test the app, list windows, take a screenshot, inspect a snapshot, reload the WebView, or verify a running zero-native example.
---
# Automate zero-native apps
zero-native has a built-in automation system for inspecting running WebView shell apps. It works through file-based IPC in `.zig-cache/zero-native-automation/`.
## Prerequisites
Run an app with automation enabled:
```bash
zig build run-webview -Dplatform=macos -Dautomation=true
```
## Commands
```bash
zig build
zig-out/bin/zero-native automate list
zig-out/bin/zero-native automate snapshot
zig-out/bin/zero-native automate screenshot [path]
zig-out/bin/zero-native automate reload
```
## Workflow
1. Start the app with automation enabled.
2. Run `zig-out/bin/zero-native automate snapshot` to confirm the window and WebView source.
3. Use `zig-out/bin/zero-native automate reload` to request a reload.
4. Use `zig-out/bin/zero-native automate screenshot [path]` when a placeholder screenshot artifact is enough.
## Notes
- Automation is compile-time gated: apps built without `-Dautomation=true` ignore automation files.
- The current screenshot artifact is a placeholder PPM.
- WebView DOM interaction is intentionally out of scope for this file-based automation layer.
+105
View File
@@ -0,0 +1,105 @@
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-13
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")"
actual="$(shasum -a 256 "zig-out/cef-download/${archive}" | awk '{print $1}')"
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 -S "$CEF_ROOT" -B "$CEF_ROOT/build/libcef_dll_wrapper"
cmake --build "$CEF_ROOT/build/libcef_dll_wrapper" --target libcef_dll_wrapper --config Release
mkdir -p "$CEF_ROOT/libcef_dll_wrapper"
find "$CEF_ROOT/build/libcef_dll_wrapper" -name libcef_dll_wrapper.a -print -quit | xargs -I{} cp "{}" "$CEF_ROOT/libcef_dll_wrapper/libcef_dll_wrapper.a"
- name: Prepare zero-native runtime archive
if: ${{ inputs.source == 'official' }}
run: |
zig build
zig-out/bin/zero-native 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
--zero-native-bin zig-out/bin/zero-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/zero-native-cef-${{ inputs.cef_version }}-${{ matrix.platform }}.tar.gz
zig-out/cef/zero-native-cef-${{ inputs.cef_version }}-${{ matrix.platform }}.tar.gz.sha256
+112
View File
@@ -0,0 +1,112 @@
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
- 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
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
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/zero-native run version:check
- run: npm --prefix packages/zero-native run scripts:check
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
- run: zig build
- name: Scaffold and test frontend templates
run: |
set -euo pipefail
for frontend in next vite react svelte vue; do
app=".zig-cache/scaffold-${frontend}"
rm -rf "$app"
./zig-out/bin/zero-native init "$app" --frontend "$frontend"
(cd "$app" && zig build test -Dplatform=null && ../../zig-out/bin/zero-native validate app.zon)
done
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
+78
View File
@@ -0,0 +1,78 @@
name: Release
on:
push:
branches:
- main
workflow_dispatch:
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
check-release:
name: Check for new CLI version
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
should_release: ${{ steps.check.outputs.should_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
id: check
run: |
LOCAL_VERSION=$(node -p "require('./packages/zero-native/package.json').version")
echo "Local version: $LOCAL_VERSION"
NPM_VERSION=$(npm view zero-native 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"
else
echo "Version unchanged on npm, skipping publish"
echo "should_release=false" >> "$GITHUB_OUTPUT"
fi
echo "version=$LOCAL_VERSION" >> "$GITHUB_OUTPUT"
publish:
name: Publish CLI to npm
needs: check-release
if: needs.check-release.outputs.should_release == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
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"
- name: Check version sync
run: npm --prefix packages/zero-native run version:check
- name: Check package scripts
run: npm --prefix packages/zero-native run scripts:check
- name: Publish to npm
run: npm publish --provenance --access public
working-directory: packages/zero-native
+17
View File
@@ -0,0 +1,17 @@
.DS_Store
.zig-cache/
zig-out/
# Native binaries in zero-native npm package (built by CI or locally)
packages/zero-native/bin/zero-native-*
# 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
+7
View File
@@ -0,0 +1,7 @@
# Changelog
All notable changes to zero-native will be documented in this file.
## 0.1.0
- Initial pre-release development version.
+109
View File
@@ -0,0 +1,109 @@
# Contributing
Thanks for helping improve zero-native. This guide is for maintainers and contributors working on the framework repository itself.
For app author documentation, start at [zero-native.dev](https://zero-native.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 framework 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/zero-native run version:check
npm --prefix packages/zero-native 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
zero-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
zero-native package --target macos --manifest app.zon --assets assets --binary zig-out/lib/libzero-native.a
```
For Chromium packages, configure `.web_engine = "chromium"` and `.cef` in `app.zon`, or use temporary `--web-engine` and `--cef-dir` overrides while testing.
## Automation Development
Enable automation in a build:
```bash
zig build run-webview -Dautomation=true
```
Interact with the running app:
```bash
zero-native automate wait
zero-native automate list
zero-native automate bridge '{"id":"ping","command":"native.ping","payload":null}'
```
Automation writes artifacts under `.zig-cache/zero-native-automation`.
+176
View File
@@ -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
+120
View File
@@ -0,0 +1,120 @@
# zero-native
Build native desktop apps with web UI. Tiny binaries. Minimal memory. Instant rebuilds.
zero-native is a Zig desktop app shell for modern web frontends. Use the platform WebView when you want the smallest possible app, or bundle Chromium through CEF when rendering consistency matters.
## Why zero-native
### Tiny and fast
System WebView apps do not bundle a browser runtime, so the native shell stays small and starts quickly. Your app uses WKWebView on macOS and WebKitGTK on Linux.
### Choose your web engine
Pick the engine that fits the product. System WebView gives you a lightweight native footprint. Chromium through CEF gives you predictable rendering and a pinned web platform on supported targets.
### Fast native rebuilds
The native layer is Zig, so app logic, bridge commands, and platform integrations rebuild quickly. Your frontend can still use the web tooling you already know.
### Native power without heavy glue
Zig calls C directly, which keeps platform SDKs, native libraries, codecs, and local system integrations within reach when the WebView layer needs to do real native work.
### Explicit security model
The WebView is treated as untrusted by default. Native commands, permissions, navigation, external links, and window APIs are opt-in and policy controlled.
## Status
zero-native is pre-release. The current beta focus is macOS desktop apps, with Linux system WebView support available and Windows work in progress. Chromium/CEF support is currently macOS-focused.
## Quick Start
Install the CLI:
```bash
npm install -g zero-native
```
Create and run an app:
```bash
zero-native init my_app --frontend next
cd my_app
zig build run
```
The first run installs frontend dependencies, builds the generated native shell, and opens a desktop window rendering your web UI.
Read the full guide at [zero-native.dev/quick-start](https://zero-native.dev/quick-start).
## Core Concepts
`App` is the small Zig object that describes your application: name, WebView source, lifecycle hooks, and optional native services.
`Runtime` owns the event loop, windows, bridge dispatch, automation hooks, tracing, and platform services.
`WebViewSource` tells the runtime what to load: inline HTML, a URL, or packaged frontend assets served from a local app origin.
`app.zon` is the app manifest. It declares app metadata, icons, windows, frontend assets, web engine selection, security policy, bridge permissions, and packaging inputs.
`window.zero.invoke()` is the JavaScript-to-Zig bridge. Calls are size-limited, origin checked, permission checked, and routed only to registered handlers.
## Configuration
Most project-level behavior lives in `app.zon`:
```zig
.{
.id = "com.example.my-app",
.name = "my-app",
.display_name = "My App",
.version = "0.1.0",
.web_engine = "system",
.permissions = .{ "window" },
.capabilities = .{ "webview", "js_bridge" },
.security = .{
.navigation = .{
.allowed_origins = .{ "zero://app", "http://127.0.0.1:5173" },
},
},
.windows = .{
.{ .label = "main", .title = "My App", .width = 960, .height = 640 },
},
}
```
Use `.web_engine = "system"` for the platform WebView. On supported macOS builds, use `.web_engine = "chromium"` with a `.cef` config when you want to bundle Chromium.
## Documentation
The full documentation is at [zero-native.dev](https://zero-native.dev).
- [Quick Start](https://zero-native.dev/quick-start)
- [Web Engines](https://zero-native.dev/web-engines)
- [App Model](https://zero-native.dev/app-model)
- [Bridge](https://zero-native.dev/bridge)
- [Security](https://zero-native.dev/security)
- [Packaging](https://zero-native.dev/packaging)
## Examples
Framework-specific starter examples live in `examples/`:
- `examples/next`
- `examples/react`
- `examples/svelte`
- `examples/vue`
Each example is a complete zero-native app with `app.zon`, a Zig shell, and a minimal frontend project. Run one with `zig build run` from its directory.
Mobile embedding examples are available too:
- `examples/ios`
- `examples/android`
These show how an iOS or Android host app links the zero-native C ABI from `libzero-native.a`.
For local framework development, see [CONTRIBUTING.md](./CONTRIBUTING.md).
+30
View File
@@ -0,0 +1,30 @@
.{
.id = "dev.zero_native",
.name = "zero-native",
.display_name = "zero-native",
.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 = "zero-native.window.list", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } },
.{ .name = "zero-native.window.create", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } },
.{ .name = "zero-native.window.focus", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } },
.{ .name = "zero-native.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 = "zero-native", .width = 720, .height = 480, .restore_state = true },
},
}
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+10
View File
@@ -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>
+519
View File
@@ -0,0 +1,519 @@
const std = @import("std");
const web_engine_tool = @import("src/tooling/web_engine.zig");
const PlatformOption = enum {
auto,
null,
macos,
linux,
};
const TraceOption = enum {
off,
events,
runtime,
all,
};
const WebEngineOption = enum {
system,
chromium,
};
const PackageTarget = enum {
macos,
windows,
linux,
ios,
android,
};
const SigningMode = enum {
none,
adhoc,
identity,
};
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const platform_option = b.option(PlatformOption, "platform", "Desktop backend: auto, null, macos, linux") orelse .auto;
const trace_option = b.option(TraceOption, "trace", "Trace output: off, events, runtime, all") orelse .events;
_ = b.option(bool, "debug-overlay", "Enable debug overlay output") orelse false;
_ = b.option(bool, "automation", "Enable zero-native automation artifacts") orelse false;
_ = b.option(bool, "webview", "Deprecated: WebView is the only runtime surface") orelse true;
const web_engine_override = b.option(WebEngineOption, "web-engine", "Override app.zon web engine: system, chromium");
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");
_ = b.option(bool, "js-bridge", "Enable optional JavaScript bridge stubs") orelse false;
const package_target = b.option(PackageTarget, "package-target", "Package target: macos, windows, linux, ios, android") orelse .macos;
const signing_mode = b.option(SigningMode, "signing", "Signing mode: none, adhoc, identity") orelse .none;
const package_version = packageVersion(b);
const optimize_name = @tagName(optimize);
const app_web_engine = web_engine_tool.readManifestConfig(b.allocator, b.graph.io, "app.zon") catch |err| {
std.debug.panic("failed to read app.zon web engine config: {s}", .{@errorName(err)});
};
const resolved_web_engine = web_engine_tool.resolve(app_web_engine, .{
.web_engine = if (web_engine_override) |value| webEngineFromBuildOption(value) else null,
.cef_dir = cef_dir_override,
.cef_auto_install = cef_auto_install_override,
}) catch |err| {
std.debug.panic("invalid app.zon web engine config: {s}", .{@errorName(err)});
};
const web_engine = buildWebEngineFromResolved(resolved_web_engine.engine);
const cef_dir = resolved_web_engine.cef_dir;
const cef_auto_install = resolved_web_engine.cef_auto_install;
const selected_platform: PlatformOption = switch (platform_option) {
.auto => if (target.result.os.tag == .macos) .macos else if (target.result.os.tag == .linux) .linux 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 (web_engine == .chromium and selected_platform != .macos) {
@panic("-Dweb-engine=chromium is currently supported only with -Dplatform=macos; Linux uses WebKitGTK through -Dweb-engine=system");
}
const geometry_mod = module(b, target, optimize, "src/primitives/geometry/root.zig");
const assets_mod = module(b, target, optimize, "src/primitives/assets/root.zig");
const app_dirs_mod = module(b, target, optimize, "src/primitives/app_dirs/root.zig");
const trace_mod = module(b, target, optimize, "src/primitives/trace/root.zig");
const app_manifest_mod = module(b, target, optimize, "src/primitives/app_manifest/root.zig");
const diagnostics_mod = module(b, target, optimize, "src/primitives/diagnostics/root.zig");
const platform_info_mod = module(b, target, optimize, "src/primitives/platform_info/root.zig");
const json_mod = module(b, target, optimize, "src/primitives/json/root.zig");
const debug_mod = module(b, target, optimize, "src/debug/root.zig");
debug_mod.addImport("app_dirs", app_dirs_mod);
debug_mod.addImport("trace", trace_mod);
const geometry_tests = testArtifact(b, geometry_mod);
const assets_tests = testArtifact(b, assets_mod);
const app_dirs_tests = testArtifact(b, app_dirs_mod);
const trace_tests = testArtifact(b, trace_mod);
const app_manifest_tests = testArtifact(b, app_manifest_mod);
const diagnostics_tests = testArtifact(b, diagnostics_mod);
const platform_info_tests = testArtifact(b, platform_info_mod);
const json_tests = testArtifact(b, json_mod);
const desktop_mod = module(b, target, optimize, "src/root.zig");
desktop_mod.addImport("geometry", geometry_mod);
desktop_mod.addImport("app_dirs", app_dirs_mod);
desktop_mod.addImport("assets", assets_mod);
desktop_mod.addImport("trace", trace_mod);
desktop_mod.addImport("app_manifest", app_manifest_mod);
desktop_mod.addImport("diagnostics", diagnostics_mod);
desktop_mod.addImport("platform_info", platform_info_mod);
desktop_mod.addImport("json", json_mod);
desktop_mod.export_symbol_names = &.{
"zero_native_app_create",
"zero_native_app_destroy",
"zero_native_app_start",
"zero_native_app_stop",
"zero_native_app_resize",
"zero_native_app_touch",
"zero_native_app_frame",
"zero_native_app_set_asset_root",
"zero_native_app_last_command_count",
"zero_native_app_last_error_name",
};
const desktop_tests = testArtifact(b, desktop_mod);
const embed_lib = b.addLibrary(.{
.linkage = .static,
.name = "zero-native",
.root_module = desktop_mod,
});
b.installArtifact(embed_lib);
const automation_protocol_mod = module(b, target, optimize, "src/automation/protocol.zig");
const tooling_mod = module(b, target, optimize, "src/tooling/root.zig");
tooling_mod.addImport("assets", assets_mod);
tooling_mod.addImport("app_dirs", app_dirs_mod);
tooling_mod.addImport("app_manifest", app_manifest_mod);
tooling_mod.addImport("diagnostics", diagnostics_mod);
tooling_mod.addImport("debug", debug_mod);
tooling_mod.addImport("platform_info", platform_info_mod);
tooling_mod.addImport("trace", trace_mod);
const tooling_tests = testArtifact(b, tooling_mod);
const cli_mod = module(b, target, optimize, "tools/zero-native/main.zig");
cli_mod.addImport("tooling", tooling_mod);
cli_mod.addImport("automation_protocol", automation_protocol_mod);
const cli_exe = b.addExecutable(.{
.name = "zero-native",
.root_module = cli_mod,
});
b.installArtifact(cli_exe);
const platform_arg = switch (selected_platform) {
.auto => unreachable,
.null => "null",
.macos => "macos",
.linux => "linux",
};
const test_step = b.step("test", "Run package and framework tests");
test_step.dependOn(&b.addRunArtifact(geometry_tests).step);
test_step.dependOn(&b.addRunArtifact(assets_tests).step);
test_step.dependOn(&b.addRunArtifact(app_dirs_tests).step);
test_step.dependOn(&b.addRunArtifact(trace_tests).step);
test_step.dependOn(&b.addRunArtifact(app_manifest_tests).step);
test_step.dependOn(&b.addRunArtifact(diagnostics_tests).step);
test_step.dependOn(&b.addRunArtifact(platform_info_tests).step);
test_step.dependOn(&b.addRunArtifact(json_tests).step);
test_step.dependOn(&b.addRunArtifact(desktop_tests).step);
test_step.dependOn(&b.addRunArtifact(tooling_tests).step);
addTestStep(b, "test-geometry", "Run geometry module tests", geometry_tests);
addTestStep(b, "test-assets", "Run assets module tests", assets_tests);
addTestStep(b, "test-app-dirs", "Run app directory module tests", app_dirs_tests);
addTestStep(b, "test-trace", "Run trace module tests", trace_tests);
addTestStep(b, "test-app-manifest", "Run app manifest module tests", app_manifest_tests);
addTestStep(b, "test-diagnostics", "Run diagnostics module tests", diagnostics_tests);
addTestStep(b, "test-platform-info", "Run platform info module tests", platform_info_tests);
addTestStep(b, "test-json", "Run JSON primitive tests", json_tests);
addTestStep(b, "test-desktop", "Run zero-native framework tests", desktop_tests);
addTestStep(b, "test-tooling", "Run zero-native tooling tests", tooling_tests);
const run_hello = b.addSystemCommand(&.{ "zig", "build", "run", b.fmt("-Dplatform={s}", .{platform_arg}), b.fmt("-Dtrace={s}", .{@tagName(trace_option)}) });
run_hello.setCwd(b.path("examples/hello"));
const run_hello_step = b.step("run-hello", "Run the zero-native hello WebView example");
run_hello_step.dependOn(&run_hello.step);
const run_webview = b.addSystemCommand(&.{ "zig", "build", "run", b.fmt("-Dplatform={s}", .{platform_arg}), b.fmt("-Dtrace={s}", .{@tagName(trace_option)}), b.fmt("-Dweb-engine={s}", .{@tagName(web_engine)}), b.fmt("-Dcef-dir={s}", .{cef_dir}) });
run_webview.setCwd(b.path("examples/webview"));
const run_webview_step = b.step("run-webview", "Run the zero-native WebView example");
run_webview_step.dependOn(&run_webview.step);
const build_webview_system = b.addSystemCommand(&.{ "zig", "build", b.fmt("-Dplatform={s}", .{platform_arg}), "-Dweb-engine=system" });
build_webview_system.setCwd(b.path("examples/webview"));
const webview_system_link_step = b.step("test-webview-system-link", "Build the WebView example with the system engine");
webview_system_link_step.dependOn(&build_webview_system.step);
const frontend_examples_step = b.step("test-examples-frontends", "Run frontend example tests");
addExampleTestStep(b, frontend_examples_step, "test-example-next", "Run Next example tests", "examples/next");
addExampleTestStep(b, frontend_examples_step, "test-example-react", "Run React example tests", "examples/react");
addExampleTestStep(b, frontend_examples_step, "test-example-svelte", "Run Svelte example tests", "examples/svelte");
addExampleTestStep(b, frontend_examples_step, "test-example-vue", "Run Vue example tests", "examples/vue");
const mobile_examples_step = b.step("test-examples-mobile", "Verify mobile example project layouts");
addLayoutCheckStep(b, mobile_examples_step, "test-example-ios-layout", "Verify iOS example layout", &.{
"examples/ios/README.md",
"examples/ios/app.zon",
"examples/ios/ZeroNativeIOSExample.xcodeproj/project.pbxproj",
"examples/ios/ZeroNativeIOSExample/AppDelegate.swift",
"examples/ios/ZeroNativeIOSExample/SceneDelegate.swift",
"examples/ios/ZeroNativeIOSExample/ZeroNativeHostViewController.swift",
"examples/ios/ZeroNativeIOSExample/zero_native.h",
});
addLayoutCheckStep(b, mobile_examples_step, "test-example-android-layout", "Verify Android example layout", &.{
"examples/android/README.md",
"examples/android/app.zon",
"examples/android/settings.gradle",
"examples/android/build.gradle",
"examples/android/app/build.gradle",
"examples/android/app/src/main/AndroidManifest.xml",
"examples/android/app/src/main/java/dev/zero_native/examples/android/MainActivity.kt",
"examples/android/app/src/main/cpp/CMakeLists.txt",
"examples/android/app/src/main/cpp/zero_native_jni.c",
"examples/android/app/src/main/cpp/zero_native.h",
});
const build_webview_cef = b.addSystemCommand(&.{ "zig", "build", "-Dplatform=macos", "-Dweb-engine=chromium", b.fmt("-Dcef-dir={s}", .{cef_dir}) });
build_webview_cef.setCwd(b.path("examples/webview"));
const webview_cef_link_step = b.step("test-webview-cef-link", "Build the WebView example with Chromium/CEF");
webview_cef_link_step.dependOn(&build_webview_cef.step);
const webview_smoke_step = b.step("test-webview-smoke", "Run macOS WebView automation smoke test");
const webview_smoke_build = b.addSystemCommand(&.{ "zig", "build", "-Dplatform=macos", "-Dweb-engine=system", "-Dautomation=true", "-Djs-bridge=true" });
webview_smoke_build.setCwd(b.path("examples/webview"));
const webview_smoke_run = b.addSystemCommand(&.{
"sh", "-c",
b.fmt(
\\set -eu
\\cd examples/webview
\\app="zig-out/bin/webview"
\\cli="{s}"
\\mkdir -p .zig-cache/zero-native-automation
\\rm -f .zig-cache/zero-native-automation/snapshot.txt .zig-cache/zero-native-automation/windows.txt .zig-cache/zero-native-automation/bridge-response.txt
\\"$app" > .zig-cache/zero-native-webview-smoke.log 2>&1 &
\\pid=$!
\\trap 'kill "$pid" >/dev/null 2>&1 || true; wait "$pid" >/dev/null 2>&1 || true' EXIT
\\snapshot="$("$cli" automate wait 2>&1)"
\\case "$snapshot" in *"ready=true"*) ;; *) echo "automation snapshot was not ready" >&2; exit 1 ;; esac
\\response="$("$cli" automate bridge '{{"id":"smoke","command":"native.ping","payload":{{"source":"smoke"}}}}' 2>&1)"
\\case "$response" in *'"ok":true'*) ;; *) echo "native.ping did not succeed: $response" >&2; exit 1 ;; esac
\\case "$response" in *'pong from Zig'*) ;; *) echo "native.ping response was unexpected: $response" >&2; exit 1 ;; esac
\\echo "webview smoke ok"
, .{"zig-out/bin/zero-native"}),
});
webview_smoke_run.step.dependOn(&webview_smoke_build.step);
webview_smoke_run.step.dependOn(&cli_exe.step);
webview_smoke_step.dependOn(&webview_smoke_run.step);
const webview_cef_smoke_step = b.step("test-webview-cef-smoke", "Run macOS Chromium WebView automation smoke test");
const webview_cef_smoke_build = b.addSystemCommand(&.{ "zig", "build", "-Dplatform=macos", "-Dweb-engine=chromium", b.fmt("-Dcef-dir={s}", .{cef_dir}), "-Dautomation=true", "-Djs-bridge=true" });
webview_cef_smoke_build.setCwd(b.path("examples/webview"));
const webview_cef_smoke_run = b.addSystemCommand(&.{
"sh", "-c",
b.fmt(
\\set -eu
\\cd examples/webview
\\app="zig-out/bin/webview"
\\cli="{s}"
\\mkdir -p .zig-cache/zero-native-automation
\\rm -f .zig-cache/zero-native-automation/snapshot.txt .zig-cache/zero-native-automation/windows.txt .zig-cache/zero-native-automation/bridge-response.txt
\\"$app" > .zig-cache/zero-native-webview-cef-smoke.log 2>&1 &
\\pid=$!
\\trap 'kill "$pid" >/dev/null 2>&1 || true; wait "$pid" >/dev/null 2>&1 || true' EXIT
\\snapshot="$("$cli" automate wait 2>&1)"
\\case "$snapshot" in *"ready=true"*) ;; *) echo "automation snapshot was not ready" >&2; exit 1 ;; esac
\\response="$("$cli" automate bridge '{{"id":"ping","command":"native.ping","payload":{{"source":"cef-smoke"}}}}' 2>&1)"
\\case "$response" in *'"ok":true'*'pong from Zig'*) ;; *) echo "native.ping response was unexpected: $response" >&2; exit 1 ;; esac
\\echo "cef webview smoke ok"
, .{"zig-out/bin/zero-native"}),
});
webview_cef_smoke_run.step.dependOn(&webview_cef_smoke_build.step);
webview_cef_smoke_run.step.dependOn(&cli_exe.step);
webview_cef_smoke_step.dependOn(&webview_cef_smoke_run.step);
const dev_run = b.addSystemCommand(&.{ "zig", "build", "run", b.fmt("-Dplatform={s}", .{platform_arg}) });
dev_run.setCwd(b.path("examples/webview"));
const dev_step = b.step("dev", "Run managed frontend dev server and native shell");
dev_step.dependOn(&dev_run.step);
const lib_step = b.step("lib", "Build zero-native embeddable static library");
lib_step.dependOn(&b.addInstallArtifact(embed_lib, .{}).step);
const doctor_run = b.addRunArtifact(cli_exe);
doctor_run.addArg("doctor");
const doctor_step = b.step("doctor", "Print zero-native platform diagnostics");
doctor_step.dependOn(&doctor_run.step);
const validate_run = b.addRunArtifact(cli_exe);
validate_run.addArgs(&.{ "validate", "app.zon" });
const validate_step = b.step("validate", "Validate app.zon");
validate_step.dependOn(&validate_run.step);
const bundle_run = b.addRunArtifact(cli_exe);
bundle_run.addArgs(&.{ "bundle-assets", "app.zon", "assets", "zig-out/assets" });
const bundle_step = b.step("bundle-assets", "Bundle app assets");
bundle_step.dependOn(&bundle_run.step);
const package_run = b.addRunArtifact(cli_exe);
package_run.addArgs(&.{
"package",
"--target",
@tagName(package_target),
"--output",
b.fmt("zig-out/package/zero-native-{s}-{s}-{s}{s}", .{ package_version, @tagName(package_target), optimize_name, packageSuffix(package_target) }),
"--binary",
});
package_run.addFileArg(embed_lib.getEmittedBin());
package_run.addArgs(&.{ "--manifest", "app.zon", "--assets", "assets", "--optimize", optimize_name, "--signing", @tagName(signing_mode), "--web-engine", @tagName(web_engine), "--cef-dir", cef_dir });
if (cef_auto_install) package_run.addArg("--cef-auto-install");
package_run.step.dependOn(&embed_lib.step);
package_run.step.dependOn(&bundle_run.step);
const package_step = b.step("package", "Create local package artifact");
package_step.dependOn(&package_run.step);
const package_cef_run = b.addRunArtifact(cli_exe);
package_cef_run.addArgs(&.{
"package",
"--target",
"macos",
"--output",
b.fmt("zig-out/package/zero-native-cef-smoke-{s}.app", .{optimize_name}),
"--binary",
});
package_cef_run.addFileArg(embed_lib.getEmittedBin());
package_cef_run.addArgs(&.{ "--manifest", "app.zon", "--assets", "assets", "--optimize", optimize_name, "--web-engine", "chromium", "--cef-dir", cef_dir });
if (cef_auto_install) package_cef_run.addArg("--cef-auto-install");
package_cef_run.step.dependOn(&embed_lib.step);
package_cef_run.step.dependOn(&bundle_run.step);
const package_cef_check = b.addSystemCommand(&.{
"sh", "-c",
b.fmt(
\\set -e
\\app="zig-out/package/zero-native-cef-smoke-{s}.app"
\\test -d "$app/Contents/Frameworks/Chromium Embedded Framework.framework"
\\test -f "$app/Contents/Frameworks/Chromium Embedded Framework.framework/Resources/icudtl.dat"
\\test -f "$app/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libGLESv2.dylib"
\\test -f "$app/Contents/Resources/package-manifest.zon"
\\echo "cef package layout ok"
, .{optimize_name}),
});
package_cef_check.step.dependOn(&package_cef_run.step);
const package_cef_smoke_step = b.step("test-package-cef-layout", "Verify macOS Chromium package layout");
package_cef_smoke_step.dependOn(&package_cef_check.step);
const package_windows_run = b.addRunArtifact(cli_exe);
package_windows_run.addArgs(&.{ "package-windows", "--output", b.fmt("zig-out/package/zero-native-{s}-windows-Debug", .{package_version}), "--manifest", "app.zon", "--assets", "assets" });
const package_windows_step = b.step("package-windows", "Create local Windows artifact directory");
package_windows_step.dependOn(&package_windows_run.step);
const package_linux_run = b.addRunArtifact(cli_exe);
package_linux_run.addArgs(&.{ "package-linux", "--output", b.fmt("zig-out/package/zero-native-{s}-linux-Debug", .{package_version}), "--manifest", "app.zon", "--assets", "assets" });
const package_linux_step = b.step("package-linux", "Create local Linux artifact directory");
package_linux_step.dependOn(&package_linux_run.step);
const package_ios_run = b.addRunArtifact(cli_exe);
package_ios_run.addArgs(&.{ "package-ios", "--output", b.fmt("zig-out/mobile/zero-native-{s}-ios-Debug", .{package_version}), "--manifest", "app.zon", "--assets", "assets", "--binary" });
package_ios_run.addFileArg(embed_lib.getEmittedBin());
package_ios_run.step.dependOn(&embed_lib.step);
const package_ios_step = b.step("package-ios", "Create local iOS host skeleton");
package_ios_step.dependOn(&package_ios_run.step);
const package_android_run = b.addRunArtifact(cli_exe);
package_android_run.addArgs(&.{ "package-android", "--output", b.fmt("zig-out/mobile/zero-native-{s}-android-Debug", .{package_version}), "--manifest", "app.zon", "--assets", "assets", "--binary" });
package_android_run.addFileArg(embed_lib.getEmittedBin());
package_android_run.step.dependOn(&embed_lib.step);
const package_android_step = b.step("package-android", "Create local Android host skeleton");
package_android_step.dependOn(&package_android_run.step);
const generate_icon_step = b.step("generate-icon", "Generate .icns and .ico from assets/icon.png");
const iconset_script = b.addSystemCommand(&.{
"sh", "-c",
\\set -e
\\command -v python3 >/dev/null || { echo "python3 required for icon generation" >&2; exit 1; }
\\python3 -c "
\\from PIL import Image; import os
\\img = Image.open('assets/icon.png')
\\iconset = 'zig-out/icon.iconset'
\\os.makedirs(iconset, exist_ok=True)
\\for name, sz in {'icon_16x16.png':16,'icon_16x16@2x.png':32,'icon_32x32.png':32,'icon_32x32@2x.png':64,'icon_128x128.png':128,'icon_128x128@2x.png':256,'icon_256x256.png':256,'icon_256x256@2x.png':512,'icon_512x512.png':512,'icon_512x512@2x.png':1024}.items():
\\ img.resize((sz,sz),Image.LANCZOS).save(os.path.join(iconset,name),'PNG')
\\img.save('assets/icon.ico',format='ICO',sizes=[(16,16),(32,32),(48,48),(64,64),(128,128),(256,256)])
\\"
\\iconutil -c icns zig-out/icon.iconset -o assets/icon.icns
\\echo "generated assets/icon.icns and assets/icon.ico"
});
generate_icon_step.dependOn(&iconset_script.step);
const notarize_run = b.addRunArtifact(cli_exe);
notarize_run.addArgs(&.{
"package",
"--target",
"macos",
"--output",
b.fmt("zig-out/package/zero-native-{s}-macos-{s}.app", .{ package_version, optimize_name }),
"--binary",
});
notarize_run.addFileArg(embed_lib.getEmittedBin());
notarize_run.addArgs(&.{ "--manifest", "app.zon", "--assets", "assets", "--optimize", optimize_name, "--signing", "identity", "--web-engine", @tagName(web_engine), "--cef-dir", cef_dir });
if (cef_auto_install) notarize_run.addArg("--cef-auto-install");
notarize_run.step.dependOn(&embed_lib.step);
notarize_run.step.dependOn(&bundle_run.step);
const notarize_step = b.step("notarize", "Package, sign with identity, and notarize for macOS distribution");
notarize_step.dependOn(&notarize_run.step);
const dmg_script = b.addSystemCommand(&.{
"sh", "-c",
b.fmt(
\\APP="zig-out/package/zero-native-{s}-macos-{s}.app"
\\DMG="zig-out/package/zero-native-{s}-macos-{s}.dmg"
\\test -d "$APP" || {{ echo "run 'zig build package' first" >&2; exit 1; }}
\\hdiutil create -volname "zero-native" -srcfolder "$APP" -ov -format UDZO "$DMG"
\\echo "created $DMG"
, .{ package_version, optimize_name, package_version, optimize_name }),
});
dmg_script.step.dependOn(&package_run.step);
const dmg_step = b.step("dmg", "Create macOS .dmg disk image from the packaged .app");
dmg_step.dependOn(&dmg_script.step);
const cef_bundle_script = b.addSystemCommand(&.{
"sh", "-c",
b.fmt(
\\set -e
\\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"
\\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/"
\\if [ -d "{s}/Resources" ]; then
\\ mkdir -p "zig-out/bin/Resources/cef"
\\ cp -R "{s}/Resources/"* "zig-out/bin/Resources/cef/"
\\fi
\\echo "CEF framework copied for local dev runs"
, .{ cef_dir, cef_dir, cef_dir, cef_dir, cef_dir }),
});
const cef_bundle_step = b.step("cef-bundle", "Copy CEF framework and resources into zig-out/bin/ for local dev runs");
if (cef_auto_install) {
const cef_bundle_auto = b.addRunArtifact(cli_exe);
cef_bundle_auto.addArgs(&.{ "cef", "install", "--dir", cef_dir });
cef_bundle_script.step.dependOn(&cef_bundle_auto.step);
}
cef_bundle_step.dependOn(&cef_bundle_script.step);
}
fn module(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 testArtifact(b: *std.Build, mod: *std.Build.Module) *std.Build.Step.Compile {
return b.addTest(.{ .root_module = mod });
}
fn addTestStep(b: *std.Build, name: []const u8, description: []const u8, artifact: *std.Build.Step.Compile) void {
const step = b.step(name, description);
step.dependOn(&b.addRunArtifact(artifact).step);
}
fn addExampleTestStep(b: *std.Build, group: *std.Build.Step, name: []const u8, description: []const u8, example_path: []const u8) void {
const run = b.addSystemCommand(&.{ "zig", "build", "test", "-Dplatform=null" });
run.setCwd(b.path(example_path));
const step = b.step(name, description);
step.dependOn(&run.step);
group.dependOn(&run.step);
}
fn addLayoutCheckStep(b: *std.Build, group: *std.Build.Step, name: []const u8, description: []const u8, paths: []const []const u8) void {
const step = b.step(name, description);
for (paths) |path| {
const check = b.addSystemCommand(&.{ "test", "-f", path });
step.dependOn(&check.step);
group.dependOn(&check.step);
}
}
fn packageSuffix(target: PackageTarget) []const u8 {
return switch (target) {
.macos => ".app",
.windows, .linux, .ios, .android => "",
};
}
fn packageVersion(b: *std.Build) []const u8 {
var file = std.Io.Dir.cwd().openFile(b.graph.io, "build.zig.zon", .{}) catch return "0.1.0";
defer file.close(b.graph.io);
var buffer: [4096]u8 = undefined;
const len = file.readPositionalAll(b.graph.io, &buffer, 0) catch return "0.1.0";
const bytes = buffer[0..len];
const marker = ".version = \"";
const start = std.mem.indexOf(u8, bytes, marker) orelse return "0.1.0";
const value_start = start + marker.len;
const value_end = std.mem.indexOfScalarPos(u8, bytes, value_start, '"') orelse return "0.1.0";
return b.allocator.dupe(u8, bytes[value_start..value_end]) catch return "0.1.0";
}
fn webEngineFromBuildOption(option: WebEngineOption) web_engine_tool.Engine {
return switch (option) {
.system => .system,
.chromium => .chromium,
};
}
fn buildWebEngineFromResolved(engine: web_engine_tool.Engine) WebEngineOption {
return switch (engine) {
.system => .system,
.chromium => .chromium,
};
}
+23
View File
@@ -0,0 +1,23 @@
.{
.name = .zero_native,
.fingerprint = 0x338d08a1e3dd81aa,
.version = "0.1.0",
.minimum_zig_version = "0.16.0",
.dependencies = .{},
.paths = .{
"README.md",
"CHANGELOG.md",
"LICENSE",
"SECURITY.md",
"app.zon",
"assets",
"build.zig",
"build.zig.zon",
"docs",
"examples",
"src",
"templates",
"tests",
"tools",
},
}
+3
View File
@@ -0,0 +1,3 @@
node_modules/
.next/
next-env.d.ts
+22
View File
@@ -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>
```
+10
View File
@@ -0,0 +1,10 @@
import createMDX from "@next/mdx";
const withMDX = createMDX();
/** @type {import('next').NextConfig} */
const nextConfig = {
pageExtensions: ["ts", "tsx", "md", "mdx"],
};
export default withMDX(nextConfig);
+38
View File
@@ -0,0 +1,38 @@
{
"name": "@zero-native/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"
},
"dependencies": {
"@mdx-js/loader": "^3",
"@mdx-js/react": "^3",
"@next/mdx": "^16.1.6",
"clsx": "^2.1.1",
"geist": "^1.7.0",
"next": "^16.1.6",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"react": "^19",
"react-dom": "^19",
"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"
}
}
+3893
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};
Binary file not shown.
Binary file not shown.
+64
View File
@@ -0,0 +1,64 @@
import { NextRequest, NextResponse } from "next/server";
import { getSearchIndex } from "@/lib/search-index";
export async function GET(req: NextRequest) {
const q = req.nextUrl.searchParams.get("q")?.trim().toLowerCase();
if (!q) {
return NextResponse.json({ results: [] });
}
const index = await getSearchIndex();
const terms = q.split(/\s+/).filter(Boolean);
const results = index
.map((entry) => {
const titleLower = entry.title.toLowerCase();
const contentLower = entry.content.toLowerCase();
const titleMatch = terms.every((t) => titleLower.includes(t));
const contentMatch = terms.every((t) => contentLower.includes(t));
if (!titleMatch && !contentMatch) return null;
let snippet = "";
if (contentMatch) {
const firstTermIdx = Math.min(
...terms.map((t) => {
const idx = contentLower.indexOf(t);
return idx === -1 ? Infinity : idx;
})
);
if (firstTermIdx !== Infinity) {
const start = Math.max(0, firstTermIdx - 40);
const end = Math.min(entry.content.length, firstTermIdx + 120);
snippet =
(start > 0 ? "..." : "") +
entry.content.slice(start, end).replace(/\n/g, " ") +
(end < entry.content.length ? "..." : "");
}
}
return {
title: entry.title,
href: entry.href,
snippet,
score: titleMatch ? 2 : 1,
};
})
.filter(
(
r
): r is {
title: string;
href: string;
snippet: string;
score: number;
} => r !== null
)
.sort((a, b) => b.score - a.score)
.slice(0, 20)
.map(({ score: _, ...rest }) => rest);
return NextResponse.json({ results }, { headers: { "Cache-Control": "public, max-age=60" } });
}
+7
View File
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("app-model");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
+276
View File
@@ -0,0 +1,276 @@
# App Model
A zero-native app provides a name, a WebView source, and optional lifecycle callbacks. The runtime owns the event loop, windows, and native services; the platform owns the web engine.
## The App struct
<table>
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>context</code></td>
<td><code>*anyopaque</code></td>
<td>Pointer to your app state (required)</td>
</tr>
<tr>
<td><code>name</code></td>
<td><code>[]const u8</code></td>
<td>App name used in traces and automation snapshots (required)</td>
</tr>
<tr>
<td><code>source</code></td>
<td><code>WebViewSource</code></td>
<td>Initial WebView content (required)</td>
</tr>
<tr>
<td><code>source_fn</code></td>
<td><code>?fn(*anyopaque) !WebViewSource</code></td>
<td>Dynamic source resolver (overrides <code>source</code> when set)</td>
</tr>
<tr>
<td><code>start_fn</code></td>
<td><code>?fn(*anyopaque, *Runtime) !void</code></td>
<td>Called after the runtime starts and the first window is loaded</td>
</tr>
<tr>
<td><code>event_fn</code></td>
<td><code>?fn(*anyopaque, *Runtime, Event) !void</code></td>
<td>Called on every runtime event (lifecycle + commands)</td>
</tr>
<tr>
<td><code>stop_fn</code></td>
<td><code>?fn(*anyopaque, *Runtime) !void</code></td>
<td>Called before the runtime shuts down</td>
</tr>
</tbody>
</table>
All callback fields are optional. A minimal app only needs `context`, `name`, and `source`.
## WebViewSource
Three constructors for specifying what the WebView loads:
- **`.html(content)`** -- inline HTML string, served as `zero://inline`
- **`.url(address)`** -- load a remote or local URL
- **`.assets(options)`** -- serve a local file tree through a custom origin
The assets constructor takes a `WebViewAssetSource`:
```zig
.source = zero_native.WebViewSource.assets(.{
.root_path = "dist",
.entry = "index.html", // default
.origin = "zero://app", // default
.spa_fallback = true, // default
}),
```
<table>
<thead>
<tr>
<th>Field</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>root_path</code></td>
<td>required</td>
<td>Path to the directory containing frontend assets</td>
</tr>
<tr>
<td><code>entry</code></td>
<td><code>"index.html"</code></td>
<td>HTML entry point within the root path</td>
</tr>
<tr>
<td><code>origin</code></td>
<td><code>"zero://app"</code></td>
<td>Origin used for asset URLs</td>
</tr>
<tr>
<td><code>spa_fallback</code></td>
<td><code>true</code></td>
<td>Serve entry for unknown routes (SPA mode)</td>
</tr>
</tbody>
</table>
## Lifecycle events
The runtime dispatches `LifecycleEvent` values through your `event_fn`:
- **`start`** -- the app has started and the initial source is loaded
- **`frame`** -- a frame has been requested (for animations or state updates)
- **`stop`** -- the app is shutting down
## The runner pattern
The generated `src/runner.zig` wires the runtime with platform services:
1. Selects the platform (macOS, Linux, or null for headless tests)
2. Sets up trace sinks (stdout + file) via `FanoutTraceSink`
3. Installs panic capture so crashes write `last-panic.txt`
4. Initializes window state persistence from `windows.zon`
5. Creates the `Runtime` with all options and calls `runtime.run(app)`
```zig
var runtime = zero_native.Runtime.init(.{
.platform = my_platform,
.trace_sink = fanout.sink(),
.bridge = my_app.bridge(),
.builtin_bridge = .{ .enabled = true, .commands = &builtin_policies },
.security = .{
.permissions = &app_permissions,
.navigation = .{ .allowed_origins = &.{ "zero://app" } },
},
.js_window_api = true,
.window_state_store = state_store,
.automation = if (build_options.automation) automation_server else null,
});
try runtime.run(my_app.app());
```
## RuntimeOptions
<table>
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>platform</code></td>
<td><code>Platform</code></td>
<td>required</td>
<td>Platform abstraction (macOS, Linux, or NullPlatform)</td>
</tr>
<tr>
<td><code>trace_sink</code></td>
<td><code>?trace.Sink</code></td>
<td><code>null</code></td>
<td>Destination for structured trace records</td>
</tr>
<tr>
<td><code>log_path</code></td>
<td><code>?[]const u8</code></td>
<td><code>null</code></td>
<td>Path for persistent log file</td>
</tr>
<tr>
<td><code>extensions</code></td>
<td><code>?ModuleRegistry</code></td>
<td><code>null</code></td>
<td>Extension modules with lifecycle hooks</td>
</tr>
<tr>
<td><code>bridge</code></td>
<td><code>?BridgeDispatcher</code></td>
<td><code>null</code></td>
<td>App-defined bridge commands and handlers</td>
</tr>
<tr>
<td><code>builtin_bridge</code></td>
<td><code>BridgePolicy</code></td>
<td><code>.{}</code></td>
<td>Policy for built-in commands (dialogs, windows)</td>
</tr>
<tr>
<td><code>security</code></td>
<td><code>SecurityPolicy</code></td>
<td><code>.{}</code></td>
<td>Navigation allowlist, external links, permissions</td>
</tr>
<tr>
<td><code>automation</code></td>
<td><code>?automation.Server</code></td>
<td><code>null</code></td>
<td>File-based automation server for testing</td>
</tr>
<tr>
<td><code>window_state_store</code></td>
<td><code>?window_state.Store</code></td>
<td><code>null</code></td>
<td>Persistent window geometry and state</td>
</tr>
<tr>
<td><code>js_window_api</code></td>
<td><code>bool</code></td>
<td><code>false</code></td>
<td>Expose <code>window.zero.windows.*</code>; origin and <code>window</code> permission checks still apply</td>
</tr>
</tbody>
</table>
## Runtime methods
<table>
<thead>
<tr>
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>init(options) Runtime</code></td>
<td>Create a runtime</td>
</tr>
<tr>
<td><code>run(app) !void</code></td>
<td>Enter the platform event loop</td>
</tr>
<tr>
<td><code>createWindow(options) !WindowInfo</code></td>
<td>Open a new window</td>
</tr>
<tr>
<td><code>listWindows() []WindowInfo</code></td>
<td>List open windows</td>
</tr>
<tr>
<td><code>focusWindow(id) !void</code></td>
<td>Bring a window to front</td>
</tr>
<tr>
<td><code>closeWindow(id) !void</code></td>
<td>Close a window</td>
</tr>
<tr>
<td><code>invalidate()</code></td>
<td>Request a redraw</td>
</tr>
<tr>
<td><code>invalidateFor(reason, dirty_region)</code></td>
<td>Request a redraw with reason and optional dirty region</td>
</tr>
<tr>
<td><code>frameDiagnostics() FrameDiagnostics</code></td>
<td>Return stats from the last frame</td>
</tr>
<tr>
<td><code>dispatchEvent(event)</code></td>
<td>Inject a synthetic event</td>
</tr>
<tr>
<td><code>dispatchPlatformEvent(app, event)</code></td>
<td>Forward a platform event</td>
</tr>
<tr>
<td><code>automationSnapshot()</code></td>
<td>Write state to automation directory</td>
</tr>
</tbody>
</table>
+7
View File
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("app-zon");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
+156
View File
@@ -0,0 +1,156 @@
# app.zon Reference
The `app.zon` manifest declares app metadata, permissions, bridge policies, security rules, and window layout. It is read by the CLI and tooling at build, package, and validation time.
## Example
```zig
.{
.id = "dev.zero_native",
.name = "zero-native",
.display_name = "zero-native",
.version = "0.1.0",
.icons = .{ "assets/icon.icns", "assets/icon.ico" },
.platforms = .{ "macos" },
.permissions = .{ "window" },
.capabilities = .{ "webview", "js_bridge" },
.bridge = .{
.commands = .{
.{ .name = "native.ping", .origins = .{ "zero://app" } },
.{ .name = "zero-native.window.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
},
},
.security = .{
.navigation = .{
.allowed_origins = .{ "zero://app", "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 = "zero-native", .width = 720, .height = 480, .restore_state = true },
},
}
```
## Fields
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>id</code></td>
<td>Reverse-DNS bundle identifier (e.g. <code>com.example.myapp</code>)</td>
</tr>
<tr>
<td><code>name</code></td>
<td>Short machine name</td>
</tr>
<tr>
<td><code>display_name</code></td>
<td>Human-readable app name (menu bar, window title fallback)</td>
</tr>
<tr>
<td><code>version</code></td>
<td>Semver version string</td>
</tr>
<tr>
<td><code>icons</code></td>
<td>Paths to icon files for packaging</td>
</tr>
<tr>
<td><code>platforms</code></td>
<td>Target platforms: <code>macos</code>, <code>linux</code>, <code>windows</code></td>
</tr>
<tr>
<td><code>permissions</code></td>
<td>Runtime permissions (see <a href="/security">Security</a>)</td>
</tr>
<tr>
<td><code>capabilities</code></td>
<td>Feature declarations (see <a href="/security">Security</a>)</td>
</tr>
<tr>
<td><code>bridge</code></td>
<td>Bridge command policies (see <a href="/bridge">Bridge</a>)</td>
</tr>
<tr>
<td><code>security</code></td>
<td>Navigation and external link policies (see <a href="/security">Security</a>)</td>
</tr>
<tr>
<td><code>web_engine</code></td>
<td><code>system</code> or <code>chromium</code>; Chromium is currently supported for macOS builds (see <a href="/web-engines">Web Engines</a>)</td>
</tr>
<tr>
<td><code>cef</code></td>
<td>CEF runtime config for Chromium apps: <code>dir</code> and <code>auto_install</code></td>
</tr>
<tr>
<td><code>windows</code></td>
<td>Window definitions (see <a href="/windows">Windows</a>)</td>
</tr>
<tr>
<td><code>frontend</code></td>
<td>Frontend build/dev config (see <a href="/frontend">Frontend Projects</a>)</td>
</tr>
</tbody>
</table>
## `frontend.dev`
The optional `frontend.dev` block configures the managed dev server for `zero-native dev` and `zig build dev`:
```zig
.frontend = .{
.dist = "frontend/dist",
.entry = "index.html",
.spa_fallback = true,
.dev = .{
.url = "http://127.0.0.1:5173/",
.command = .{ "npm", "--prefix", "frontend", "run", "dev" },
.ready_path = "/",
.timeout_ms = 30000,
},
},
```
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>url</code></td>
<td>Dev server URL to load in the WebView during development</td>
</tr>
<tr>
<td><code>command</code></td>
<td>Command to start the dev server (spawned as a child process)</td>
</tr>
<tr>
<td><code>ready_path</code></td>
<td>HTTP path to poll until the dev server is ready (default <code>/</code>)</td>
</tr>
<tr>
<td><code>timeout_ms</code></td>
<td>Milliseconds to wait for the dev server before failing (default <code>30000</code>)</td>
</tr>
</tbody>
</table>
## Validation
```bash
zero-native validate app.zon
zero-native doctor --manifest app.zon --strict
```
+7
View File
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("automation");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
+139
View File
@@ -0,0 +1,139 @@
# Automation
The automation server exposes runtime state and accepts commands via a file-based protocol. Use it for integration testing, CI smoke tests, and inspecting running apps.
## Enabling automation
Build with the automation flag:
```bash
zig build run-webview -Dautomation=true
```
In your runner, pass an `automation.Server` to `RuntimeOptions`:
```zig
const server = zero_native.automation.Server.init(io, ".zig-cache/zero-native-automation", "My App");
var runtime = zero_native.Runtime.init(.{
.platform = my_platform,
.automation = server,
});
```
The default directory is `.zig-cache/zero-native-automation`.
## File protocol
When the runtime publishes a snapshot, it writes these files to the automation directory:
<table>
<thead>
<tr>
<th>File</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>snapshot.txt</code></td>
<td>Runtime state: app name, source kind, window metadata, <code>ready=true/false</code></td>
</tr>
<tr>
<td><code>accessibility.txt</code></td>
<td>Accessibility tree summary</td>
</tr>
<tr>
<td><code>windows.txt</code></td>
<td>Window list: <code>window @w{"{id}"} "{"{title}"}" focused={"{bool}"}</code> per line</td>
</tr>
<tr>
<td><code>screenshot.ppm</code></td>
<td>Screenshot in PPM format (currently a 2x2 placeholder)</td>
</tr>
<tr>
<td><code>command.txt</code></td>
<td>Command input: written by the CLI, consumed by the runtime</td>
</tr>
<tr>
<td><code>bridge-response.txt</code></td>
<td>JSON response from the last bridge command</td>
</tr>
</tbody>
</table>
## Commands
The runtime polls `command.txt` and processes these actions:
<table>
<thead>
<tr>
<th>Action</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>reload</code></td>
<td>Reload the WebView source</td>
</tr>
<tr>
<td><code>wait</code></td>
<td>Block until the snapshot shows <code>ready=true</code></td>
</tr>
<tr>
<td><code>bridge &lt;json&gt;</code></td>
<td>Send a bridge command with origin <code>zero://inline</code></td>
</tr>
</tbody>
</table>
After processing a command, the runtime writes `done` to `command.txt`.
## CLI usage
The `zero-native automate` subcommand interacts with the automation directory:
```bash
# Wait for the app to be ready (polls snapshot.txt for ready=true)
zero-native automate wait
# List running automation-enabled apps
zero-native automate list
# Dump the current snapshot
zero-native automate snapshot
# Capture a screenshot
zero-native automate screenshot
# Reload the WebView
zero-native automate reload
# Send a bridge command and get the response
zero-native automate bridge '{"id":"1","command":"native.ping","payload":{"source":"automation"}}'
```
## Testing with automation
The `test-webview-smoke` build step demonstrates a full automation test flow:
1. Build and start the app with `-Dautomation=true`
2. Run `zero-native automate wait` to block until the app is ready
3. Run `zero-native automate snapshot` to verify window metadata and source kind
4. Run `zero-native automate bridge '...'` to test the native bridge round-trip
5. Verify the response in `bridge-response.txt`
```bash
zig build test-webview-smoke -Dplatform=macos
```
## Custom directory
Pass a custom path to `automation.Server.init()`:
```zig
const server = zero_native.automation.Server.init(io, "/tmp/my-app-automation", "My App");
```
The CLI reads from the default `.zig-cache/zero-native-automation` unless you specify a directory via the automation subcommand.
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("bridge/builtin-commands");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
@@ -0,0 +1,112 @@
# Builtin Commands
zero-native provides built-in bridge commands for window management and native dialogs. These are controlled by the `builtin_bridge` policy in `RuntimeOptions`, separate from app-defined commands.
## Window commands
<table>
<thead>
<tr>
<th>Command</th>
<th>Required permission</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>zero-native.window.list</code></td>
<td><code>window</code></td>
<td>List all open windows</td>
</tr>
<tr>
<td><code>zero-native.window.create</code></td>
<td><code>window</code></td>
<td>Create a new window</td>
</tr>
<tr>
<td><code>zero-native.window.focus</code></td>
<td><code>window</code></td>
<td>Focus a window by ID</td>
</tr>
<tr>
<td><code>zero-native.window.close</code></td>
<td><code>window</code></td>
<td>Close a window by ID</td>
</tr>
</tbody>
</table>
Window commands are available through `window.zero.windows.*` when `js_window_api` is `true`, but the runtime still checks the request origin and the `window` permission when permissions are configured. Use an explicit `builtin_bridge` policy when you want per-command origin lists.
## Dialog commands
<table>
<thead>
<tr>
<th>Command</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>zero-native.dialog.openFile</code></td>
<td>Show a file open dialog</td>
</tr>
<tr>
<td><code>zero-native.dialog.saveFile</code></td>
<td>Show a file save dialog</td>
</tr>
<tr>
<td><code>zero-native.dialog.showMessage</code></td>
<td>Show a message dialog</td>
</tr>
</tbody>
</table>
Dialog commands are **always default-deny** and require an explicit `builtin_bridge` policy.
## Enabling builtin commands
```zig
const app_permissions = [_][]const u8{zero_native.security.permission_window};
.security = .{
.permissions = &app_permissions,
.navigation = .{ .allowed_origins = &.{ "zero://app" } },
},
.builtin_bridge = .{
.enabled = true,
.commands = &.{
.{ .name = "zero-native.window.list", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
.{ .name = "zero-native.window.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
.{ .name = "zero-native.dialog.openFile", .origins = .{ "zero://app" } },
.{ .name = "zero-native.dialog.showMessage", .origins = .{ "zero://app" } },
},
},
```
## JavaScript usage
```javascript
const win = await window.zero.windows.create({
label: "tools",
title: "Tools",
width: 420,
height: 320,
});
const files = await window.zero.invoke("zero-native.dialog.openFile", {
title: "Select a file",
allowMultiple: true,
});
const result = await window.zero.invoke("zero-native.dialog.showMessage", {
style: "warning",
title: "Confirm",
message: "Are you sure?",
primaryButton: "Yes",
secondaryButton: "No",
});
```
See also: [Dialogs](/dialogs) for the full dialog type reference, [Security](/security) for policy details.
+7
View File
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("bridge");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
+178
View File
@@ -0,0 +1,178 @@
# Bridge
The bridge connects JavaScript in the WebView to native Zig handlers via JSON messages.
## Architecture
```
WebView JS Zig Runtime
────────── ───────────
window.zero.invoke(cmd, payload)
│ │
├──── JSON message ───────────►│
│ Size check (16 KiB max)
│ Policy check (origin + permissions)
│ Handler lookup + execute
│◄─── JSON response ──────────┤
```
## Defining a handler
```zig
fn ping(context: *anyopaque, invocation: zero_native.bridge.Invocation, output: []u8) anyerror![]const u8 {
_ = invocation;
const self: *App = @ptrCast(@alignCast(context));
self.ping_count += 1;
return std.fmt.bufPrint(output, "{{\"message\":\"pong\",\"count\":{d}}}", .{self.ping_count});
}
```
The handler writes its JSON result into the provided `output` buffer (max 12 KiB) and returns a slice of it. Results must be valid JSON values; invalid raw text is rejected with `handler_failed`. When returning user data as a string, use the bridge helper so quotes and control characters are escaped:
```zig
return zero_native.bridge.writeJsonStringValue(output, user_supplied_name);
```
## Wiring the dispatcher
```zig
fn bridge(self: *App) zero_native.BridgeDispatcher {
self.handlers = .{.{ .name = "native.ping", .context = self, .invoke_fn = ping }};
return .{
.policy = .{ .enabled = true, .commands = &policies },
.registry = .{ .handlers = &self.handlers },
};
}
```
## Calling from JavaScript
```javascript
const result = await window.zero.invoke("native.ping", { source: "webview" });
console.log(result); // { message: "pong from Zig", count: 1 }
```
## Invocation
When a handler is called, it receives an `Invocation` with:
- `request.id` -- caller-provided request ID (max 64 bytes)
- `request.command` -- command name (max 128 bytes, no `/` or spaces)
- `request.payload` -- JSON payload string
- `source.origin` -- origin of the requesting page (e.g. `zero://app`)
- `source.window_id` -- which window sent the request
## Size limits
<table>
<thead>
<tr>
<th>Constant</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>max_message_bytes</code></td>
<td>16 KiB</td>
</tr>
<tr>
<td><code>max_response_bytes</code></td>
<td>16 KiB</td>
</tr>
<tr>
<td><code>max_result_bytes</code></td>
<td>12 KiB</td>
</tr>
<tr>
<td><code>max_id_bytes</code></td>
<td>64</td>
</tr>
<tr>
<td><code>max_command_bytes</code></td>
<td>128</td>
</tr>
</tbody>
</table>
## Error codes
When a bridge call fails, the JS promise rejects with an error containing a `code` field:
<table>
<thead>
<tr>
<th>Code</th>
<th>Cause</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>invalid_request</code></td>
<td>Malformed JSON message</td>
</tr>
<tr>
<td><code>unknown_command</code></td>
<td>No handler registered</td>
</tr>
<tr>
<td><code>permission_denied</code></td>
<td>Origin or permission check failed</td>
</tr>
<tr>
<td><code>handler_failed</code></td>
<td>Handler returned an error</td>
</tr>
<tr>
<td><code>payload_too_large</code></td>
<td>Message exceeds 16 KiB</td>
</tr>
<tr>
<td><code>internal_error</code></td>
<td>Unexpected runtime error</td>
</tr>
</tbody>
</table>
```javascript
try {
const result = await window.zero.invoke("native.ping", {});
} catch (error) {
console.error(error.code, error.message);
}
```
## Bridge types
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>BridgeDispatcher</code></td>
<td>Combines policy and registry</td>
</tr>
<tr>
<td><code>BridgePolicy</code></td>
<td>Whether the bridge is enabled and which commands are allowed</td>
</tr>
<tr>
<td><code>BridgeCommandPolicy</code></td>
<td>Per-command: <code>name</code>, <code>permissions</code>, <code>origins</code></td>
</tr>
<tr>
<td><code>BridgeRegistry</code></td>
<td>Maps command names to handler functions</td>
</tr>
<tr>
<td><code>BridgeHandler</code></td>
<td><code>name</code>, <code>context</code>, <code>invoke_fn</code></td>
</tr>
</tbody>
</table>
See also: [Builtin Commands](/bridge/builtin-commands) for `zero-native.window.*` and `zero-native.dialog.*`.
+7
View File
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("cli/dev");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
+68
View File
@@ -0,0 +1,68 @@
# Dev Server
Use `zero-native dev` when zero-native should own the frontend server lifecycle. It starts the configured frontend process, waits for the port to accept connections, launches the native shell with `ZERO_NATIVE_FRONTEND_URL`, sets `ZERO_NATIVE_HMR=1`, and terminates the frontend when the shell exits. Framework HMR stays owned by the dev server (Vite, Next.js, etc.) because the WebView loads the dev URL directly.
## Usage
```bash
zero-native dev --binary zig-out/bin/MyApp
zero-native dev --binary zig-out/bin/MyApp --url http://127.0.0.1:3000/ --command "npm run dev"
zero-native dev --binary zig-out/bin/MyApp --timeout-ms 60000
```
## Flags
<table>
<thead>
<tr>
<th>Flag</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>--manifest</code></td>
<td>Path to <code>app.zon</code> (default: <code>app.zon</code>)</td>
</tr>
<tr>
<td><code>--binary</code></td>
<td>Path to the built native binary</td>
</tr>
<tr>
<td><code>--url</code></td>
<td>Override dev server URL from <code>app.zon</code></td>
</tr>
<tr>
<td><code>--command</code></td>
<td>Override dev server command from <code>app.zon</code></td>
</tr>
<tr>
<td><code>--timeout-ms</code></td>
<td>Override readiness timeout (default from <code>app.zon</code>)</td>
</tr>
</tbody>
</table>
## Configuration in app.zon
```zig
.frontend = .{
.dist = "dist",
.entry = "index.html",
.spa_fallback = true,
.dev = .{
.url = "http://127.0.0.1:5173/",
.command = .{ "npm", "run", "dev", "--", "--host", "127.0.0.1" },
.ready_path = "/",
.timeout_ms = 30000,
},
}
```
## Framework recipes
**Vite**: `.url = "http://127.0.0.1:5173/"`, command `npm run dev -- --host 127.0.0.1`.
**Next.js**: `.url = "http://127.0.0.1:3000/"`, command `npm run dev -- --hostname 127.0.0.1`.
**Static preview**: point `.dist` at the build output and use any local server command.
+7
View File
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("cli");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
+283
View File
@@ -0,0 +1,283 @@
# CLI Reference
The `zero-native` CLI provides project scaffolding, validation, packaging, and debugging tools.
## Commands
<table>
<thead>
<tr>
<th>Command</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>zero-native init &lt;path&gt; --frontend &lt;next|vite|react|svelte|vue&gt;</code></td>
<td>Scaffold a new zero-native project with the specified frontend</td>
</tr>
<tr>
<td><code>zero-native dev --binary &lt;path&gt;</code></td>
<td>Start the app with a managed frontend dev server (<code>--binary</code> is required)</td>
</tr>
<tr>
<td><code>zero-native doctor</code></td>
<td>Check host environment, WebView, manifest, and CEF</td>
</tr>
<tr>
<td><code>zero-native cef install</code></td>
<td>Download, prepare, and verify the macOS CEF runtime</td>
</tr>
<tr>
<td><code>zero-native cef path</code></td>
<td>Print the default or configured CEF directory</td>
</tr>
<tr>
<td><code>zero-native cef doctor</code></td>
<td>Check only the CEF layout</td>
</tr>
<tr>
<td><code>zero-native validate [app.zon]</code></td>
<td>Validate <code>app.zon</code> against the manifest schema</td>
</tr>
<tr>
<td><code>zero-native package</code></td>
<td>Package the app for distribution</td>
</tr>
<tr>
<td><code>zero-native bundle-assets [app.zon] [assets] [output]</code></td>
<td>Copy frontend assets into the build output</td>
</tr>
<tr>
<td><code>zero-native package-windows</code></td>
<td>Package shortcut for Windows</td>
</tr>
<tr>
<td><code>zero-native package-linux</code></td>
<td>Package shortcut for Linux</td>
</tr>
<tr>
<td><code>zero-native package-ios</code></td>
<td>Package shortcut for iOS</td>
</tr>
<tr>
<td><code>zero-native package-android</code></td>
<td>Package shortcut for Android</td>
</tr>
<tr>
<td><code>zero-native automate &lt;command&gt;</code></td>
<td>Interact with the automation server</td>
</tr>
<tr>
<td><code>zero-native version</code></td>
<td>Print the zero-native version</td>
</tr>
</tbody>
</table>
## `zero-native cef` flags
<table>
<thead>
<tr>
<th>Flag</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>--dir</code></td>
<td>CEF install directory. Defaults to <code>third_party/cef/macos</code>.</td>
</tr>
<tr>
<td><code>--version</code></td>
<td>CEF binary version to download. The default is zero-native's pinned tested version.</td>
</tr>
<tr>
<td><code>--source</code></td>
<td><code>prepared</code> or <code>official</code>. Defaults to <code>prepared</code>, which downloads zero-native's no-CMake runtime from GitHub Releases.</td>
</tr>
<tr>
<td><code>--download-url</code></td>
<td>Override the prepared runtime release base URL, or the official CEF host when using <code>--source official</code>.</td>
</tr>
<tr>
<td><code>--allow-build-tools</code></td>
<td>Allow the advanced official CEF path to invoke local build tools for <code>libcef_dll_wrapper.a</code>.</td>
</tr>
<tr>
<td><code>--force</code></td>
<td>Redownload and replace the target directory.</td>
</tr>
</tbody>
</table>
Core maintainers who need to build CEF before a zero-native runtime release exists should use `tools/cef/build-from-source.sh`. The CLI's default `zero-native cef install` path remains the no-CMake app-developer path.
## `zero-native package` flags
<table>
<thead>
<tr>
<th>Flag</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>--target</code></td>
<td>Target platform (<code>macos</code>, <code>linux</code>, <code>windows</code>, <code>ios</code>, <code>android</code>)</td>
</tr>
<tr>
<td><code>--manifest</code></td>
<td>Path to <code>app.zon</code></td>
</tr>
<tr>
<td><code>--output</code></td>
<td>Output path for the package</td>
</tr>
<tr>
<td><code>--binary</code></td>
<td>Path to the built binary</td>
</tr>
<tr>
<td><code>--assets</code></td>
<td>Path to frontend assets directory</td>
</tr>
<tr>
<td><code>--optimize</code></td>
<td>Optimization level</td>
</tr>
<tr>
<td><code>--web-engine</code></td>
<td>Temporarily override <code>app.zon</code> with <code>system</code> or <code>chromium</code>; Chromium is currently wired for macOS packages</td>
</tr>
<tr>
<td><code>--cef-dir</code></td>
<td>Temporarily override the CEF distribution path from <code>app.zon</code></td>
</tr>
<tr>
<td><code>--cef-auto-install</code></td>
<td>Temporarily allow prepared CEF installation during Chromium packaging</td>
</tr>
<tr>
<td><code>--signing</code></td>
<td>Signing mode: <code>none</code>, <code>adhoc</code>, or <code>identity</code></td>
</tr>
<tr>
<td><code>--identity</code></td>
<td>Code signing identity name</td>
</tr>
<tr>
<td><code>--entitlements</code></td>
<td>Path to entitlements file</td>
</tr>
<tr>
<td><code>--team-id</code></td>
<td>Apple Developer Team ID</td>
</tr>
<tr>
<td><code>--archive</code></td>
<td>Create a distributable archive</td>
</tr>
</tbody>
</table>
## `zero-native dev` flags
<table>
<thead>
<tr>
<th>Flag</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>--binary</code> (required)</td>
<td>Path to the compiled app binary</td>
</tr>
<tr>
<td><code>--manifest</code></td>
<td>Path to <code>app.zon</code> (default: <code>app.zon</code>)</td>
</tr>
<tr>
<td><code>--url</code></td>
<td>Override the dev server URL from <code>app.zon</code></td>
</tr>
<tr>
<td><code>--command</code></td>
<td>Override the dev server command (space-separated)</td>
</tr>
<tr>
<td><code>--timeout-ms</code></td>
<td>Milliseconds to wait for the dev server (default from <code>app.zon</code> or 30000)</td>
</tr>
</tbody>
</table>
## `zero-native automate` subcommands
<table>
<thead>
<tr>
<th>Subcommand</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>automate list</code></td>
<td>List running automation-enabled apps</td>
</tr>
<tr>
<td><code>automate snapshot</code></td>
<td>Dump current app state</td>
</tr>
<tr>
<td><code>automate screenshot</code></td>
<td>Capture a screenshot</td>
</tr>
<tr>
<td><code>automate reload</code></td>
<td>Reload the WebView</td>
</tr>
<tr>
<td><code>automate wait</code></td>
<td>Wait for <code>ready=true</code> in the snapshot</td>
</tr>
<tr>
<td><code>automate bridge &lt;json&gt;</code></td>
<td>Send a bridge command (origin <code>zero://inline</code>)</td>
</tr>
</tbody>
</table>
## Environment variables
<table>
<thead>
<tr>
<th>Variable</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ZERO_NATIVE_FRONTEND_URL</code></td>
<td>Dev server URL (read by <code>frontend.sourceFromEnv</code>)</td>
</tr>
<tr>
<td><code>ZERO_NATIVE_FRONTEND_ASSETS</code></td>
<td>App convention for signaling pre-built assets</td>
</tr>
<tr>
<td><code>ZERO_NATIVE_LOG_DIR</code></td>
<td>Override log output directory</td>
</tr>
<tr>
<td><code>ZERO_NATIVE_LOG_FORMAT</code></td>
<td>Log format: <code>text</code> or <code>jsonl</code></td>
</tr>
</tbody>
</table>
+7
View File
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("debugging/doctor");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
+86
View File
@@ -0,0 +1,86 @@
# zero-native doctor
The `zero-native doctor` command checks your development environment for issues.
## What it checks
<table>
<thead>
<tr>
<th>Check</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>Host platform</td>
<td>Operating system and architecture</td>
</tr>
<tr>
<td>WebView</td>
<td>WKWebView (macOS) or WebKitGTK (Linux) availability</td>
</tr>
<tr>
<td>Manifest</td>
<td><code>app.zon</code> validation (only when <code>--manifest</code> is passed)</td>
</tr>
<tr>
<td>Log directory</td>
<td>Writability of the log output path</td>
</tr>
<tr>
<td>CEF</td>
<td>CEF distribution presence when Chromium is selected by <code>app.zon</code> or <code>--web-engine chromium</code></td>
</tr>
<tr>
<td>Signing tools</td>
<td>Code signing tool availability</td>
</tr>
</tbody>
</table>
## Usage
```bash
# Informational (always exits 0)
zero-native doctor
# Strict mode (exits non-zero on any warning)
zero-native doctor --manifest app.zon --strict
# Check CEF setup
zero-native doctor --manifest app.zon
```
## Flags
<table>
<thead>
<tr>
<th>Flag</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>--strict</code></td>
<td>Exit non-zero on any warning</td>
</tr>
<tr>
<td><code>--manifest</code></td>
<td>Path to <code>app.zon</code></td>
</tr>
<tr>
<td><code>--web-engine</code></td>
<td>Temporarily override the engine from <code>app.zon</code> with <code>system</code> or <code>chromium</code></td>
</tr>
<tr>
<td><code>--cef-dir</code></td>
<td>Temporarily override the CEF distribution path</td>
</tr>
<tr>
<td><code>--cef-auto-install</code></td>
<td>Temporarily allow automatic prepared CEF installation for Chromium checks</td>
</tr>
</tbody>
</table>
+7
View File
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("debugging");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
+175
View File
@@ -0,0 +1,175 @@
# Debugging
zero-native provides structured tracing, persistent logging, panic capture, and diagnostic tools for debugging desktop apps.
## Trace modes
The runtime emits structured trace records. Control verbosity with `TraceMode`:
<table>
<thead>
<tr>
<th>Mode</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>off</code></td>
<td>No trace output</td>
</tr>
<tr>
<td><code>events</code></td>
<td>Lifecycle and platform events only (default)</td>
</tr>
<tr>
<td><code>runtime</code></td>
<td>Runtime internals: frame timing, invalidation, window state</td>
</tr>
<tr>
<td><code>all</code></td>
<td>Everything</td>
</tr>
</tbody>
</table>
Enable at build time with `-Dtrace=true`, or parse from a string:
```zig
const mode = zero_native.debug.parseTraceMode("all"); // returns ?TraceMode
```
## Trace sinks
Trace records are routed through sinks. zero-native provides three:
**FileTraceSink** -- appends records to a file on disk:
```zig
var file_sink = zero_native.debug.FileTraceSink.init(io, log_dir, log_file, .json_lines);
```
**FanoutTraceSink** -- broadcasts to multiple child sinks (e.g. stdout + file):
```zig
var sinks = [_]trace.Sink{ stdout_sink.sink(), file_sink.sink() };
var fanout = zero_native.debug.FanoutTraceSink{ .sinks = &sinks };
```
**StdoutTraceSink** (from zero-native's trace module) -- writes to stdout for interactive development.
Wire a sink into the runtime via `RuntimeOptions.trace_sink`:
```zig
var runtime = zero_native.Runtime.init(.{
.platform = my_platform,
.trace_sink = fanout.sink(),
});
```
## Log format
The `ZERO_NATIVE_LOG_FORMAT` environment variable controls the persistent log format:
<table>
<thead>
<tr>
<th>Value</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>jsonl</code></td>
<td>JSON Lines -- one JSON object per trace record (default)</td>
</tr>
<tr>
<td><code>text</code></td>
<td>Human-readable text lines</td>
</tr>
</tbody>
</table>
## Log paths
Default log file locations by platform:
<table>
<thead>
<tr>
<th>Platform</th>
<th>Path</th>
</tr>
</thead>
<tbody>
<tr>
<td>macOS</td>
<td><code>~/Library/Logs/&lt;bundle-id&gt;/zero-native.jsonl</code></td>
</tr>
<tr>
<td>Linux</td>
<td><code>~/.local/state/&lt;bundle-id&gt;/logs/zero-native.jsonl</code></td>
</tr>
<tr>
<td>Windows</td>
<td><code>%LOCALAPPDATA%\&lt;bundle-id&gt;\Logs\zero-native.jsonl</code></td>
</tr>
</tbody>
</table>
Override with `ZERO_NATIVE_LOG_DIR`:
```bash
ZERO_NATIVE_LOG_DIR=/tmp/my-logs zig build run-webview
```
## Panic capture
zero-native captures Zig panics before the default handler runs:
1. Writes a report to `last-panic.txt` in the log directory (includes panic message and return address)
2. Appends a `fatal` trace record to the log file
3. Invokes `std.debug.defaultPanic` for the normal Zig panic output
Enable in your app:
```zig
pub const panic = std.debug.FullPanic(zero_native.debug.capturePanic);
```
Then call `installPanicCapture` during startup:
```zig
zero_native.debug.installPanicCapture(io, log_setup.paths);
```
## Debug overlay
Build with `-Ddebug-overlay=true` to enable a visual debugging overlay in the WebView. This shows frame timing, invalidation regions, and window metadata.
```bash
zig build run-webview -Ddebug-overlay=true
```
See also [zero-native doctor](/debugging/doctor) for a full diagnostic tool reference.
## Environment variables
<table>
<thead>
<tr>
<th>Variable</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ZERO_NATIVE_LOG_DIR</code></td>
<td>Override log output directory</td>
</tr>
<tr>
<td><code>ZERO_NATIVE_LOG_FORMAT</code></td>
<td>Log format: <code>text</code> or <code>jsonl</code> (default: <code>jsonl</code>)</td>
</tr>
</tbody>
</table>
+7
View File
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("dialogs");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
+169
View File
@@ -0,0 +1,169 @@
# Dialogs
zero-native provides native file and message dialogs accessible from Zig via `PlatformServices` or from JavaScript via the [builtin bridge](/bridge/builtin-commands).
## Open file dialog
<table>
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>title</code></td>
<td><code>[]const u8</code></td>
<td><code>""</code></td>
</tr>
<tr>
<td><code>default_path</code></td>
<td><code>[]const u8</code></td>
<td><code>""</code></td>
</tr>
<tr>
<td><code>filters</code></td>
<td><code>[]const FileFilter</code></td>
<td><code>&.{}</code></td>
</tr>
<tr>
<td><code>allow_directories</code></td>
<td><code>bool</code></td>
<td><code>false</code></td>
</tr>
<tr>
<td><code>allow_multiple</code></td>
<td><code>bool</code></td>
<td><code>false</code></td>
</tr>
</tbody>
</table>
Returns `OpenDialogResult` with `count` and `paths`.
## Save file dialog
<table>
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>title</code></td>
<td><code>[]const u8</code></td>
<td><code>""</code></td>
</tr>
<tr>
<td><code>default_path</code></td>
<td><code>[]const u8</code></td>
<td><code>""</code></td>
</tr>
<tr>
<td><code>default_name</code></td>
<td><code>[]const u8</code></td>
<td><code>""</code></td>
</tr>
<tr>
<td><code>filters</code></td>
<td><code>[]const FileFilter</code></td>
<td><code>&.{}</code></td>
</tr>
</tbody>
</table>
Returns an optional path string.
## Message dialog
<table>
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>style</code></td>
<td><code>MessageDialogStyle</code></td>
<td><code>.info</code></td>
</tr>
<tr>
<td><code>title</code></td>
<td><code>[]const u8</code></td>
<td><code>""</code></td>
</tr>
<tr>
<td><code>message</code></td>
<td><code>[]const u8</code></td>
<td><code>""</code></td>
</tr>
<tr>
<td><code>informative_text</code></td>
<td><code>[]const u8</code></td>
<td><code>""</code></td>
</tr>
<tr>
<td><code>primary_button</code></td>
<td><code>[]const u8</code></td>
<td><code>"OK"</code></td>
</tr>
<tr>
<td><code>secondary_button</code></td>
<td><code>[]const u8</code></td>
<td><code>""</code></td>
</tr>
<tr>
<td><code>tertiary_button</code></td>
<td><code>[]const u8</code></td>
<td><code>""</code></td>
</tr>
</tbody>
</table>
**MessageDialogStyle**: `info`, `warning`, `critical`
**MessageDialogResult**: `primary`, `secondary`, `tertiary`
## FileFilter
```zig
const filters = [_]zero_native.FileFilter{
.{ .name = "Images", .extensions = &.{ "png", "jpg", "gif" } },
.{ .name = "All Files", .extensions = &.{ "*" } },
};
```
## From JavaScript
Dialogs require the [builtin bridge](/bridge/builtin-commands) to be enabled with an explicit policy. JSON field names use camelCase:
```javascript
const files = await window.zero.invoke("zero-native.dialog.openFile", {
title: "Select a file",
defaultPath: "/home",
allowMultiple: true,
allowDirectories: false,
});
const path = await window.zero.invoke("zero-native.dialog.saveFile", {
title: "Save as",
defaultName: "untitled.txt",
});
const result = await window.zero.invoke("zero-native.dialog.showMessage", {
style: "warning",
title: "Confirm",
message: "Delete this item?",
informativeText: "This action cannot be undone.",
primaryButton: "Delete",
secondaryButton: "Cancel",
});
```
+7
View File
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("embed");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
+81
View File
@@ -0,0 +1,81 @@
# Embedded App
`EmbeddedApp` drives the runtime without the full platform event loop. Use it for embedding zero-native in an existing application, game engine, or custom render loop.
## Usage
```zig
var embedded = zero_native.embed.EmbeddedApp.init(my_app.app(), my_platform);
try embedded.start();
// In your render loop:
try embedded.frame();
// On resize:
try embedded.resize(new_surface);
// On shutdown:
try embedded.stop();
```
## Methods
<table>
<thead>
<tr>
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>init(app, platform)</code></td>
<td>Create an embedded app with a runtime</td>
</tr>
<tr>
<td><code>start()</code></td>
<td>Dispatch <code>app_start</code> event, loads the WebView source</td>
</tr>
<tr>
<td><code>resize(surface)</code></td>
<td>Dispatch <code>surface_resized</code> event</td>
</tr>
<tr>
<td><code>frame()</code></td>
<td>Dispatch <code>frame_requested</code> event</td>
</tr>
<tr>
<td><code>stop()</code></td>
<td>Dispatch <code>app_shutdown</code> event</td>
</tr>
</tbody>
</table>
## How it works
`EmbeddedApp` wraps a `Runtime` and an `App`. Each method dispatches a platform event via `runtime.dispatchPlatformEvent`, giving you full control over the event loop while still using zero-native's runtime, bridge, and window management.
## Mobile examples
The repository includes full mobile host examples:
- `examples/ios` - Xcode project with a Swift `UIViewController`, `WKWebView`, and `zero_native.h` bridge.
- `examples/android` - Gradle/Kotlin project with JNI and CMake wiring for `libzero-native.a`.
Both examples expect a local `libzero-native.a` built from the repository and copied into the path documented in each example README.
## Testing with EmbeddedApp
```zig
var null_platform = zero_native.NullPlatform.init(.{});
var state: u8 = 0;
var embedded = zero_native.embed.EmbeddedApp.init(.{
.context = &state,
.name = "embedded",
.source = zero_native.WebViewSource.html("<p>Embedded</p>"),
}, null_platform.platform());
try embedded.start();
// null_platform.loaded_source now contains the loaded HTML
```
+7
View File
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("extensions");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
+199
View File
@@ -0,0 +1,199 @@
# Extensions
The `ModuleRegistry` provides a hook-based extension system for adding modular capabilities to the runtime.
## Module structure
Each module has an info block, a context pointer, and optional lifecycle hooks:
```zig
const MyModule = struct {
data: u32 = 0,
fn start(context: *anyopaque, runtime: zero_native.extensions.RuntimeContext) anyerror!void {
_ = runtime;
const self: *@This() = @ptrCast(@alignCast(context));
self.data = 42;
}
fn command(context: *anyopaque, runtime: zero_native.extensions.RuntimeContext, cmd: zero_native.extensions.Command) anyerror!void {
_ = runtime;
const self: *@This() = @ptrCast(@alignCast(context));
if (std.mem.eql(u8, cmd.name, "reset")) self.data = 0;
}
};
```
## Registering modules
```zig
var my_module = MyModule{};
const caps = [_]zero_native.extensions.Capability{.{ .kind = .native_module }};
const modules = [_]zero_native.extensions.Module{.{
.info = .{ .id = 1, .name = "my-module", .capabilities = &caps },
.context = &my_module,
.hooks = .{ .start_fn = MyModule.start, .command_fn = MyModule.command },
}};
const registry = zero_native.extensions.ModuleRegistry{ .modules = &modules };
var runtime = zero_native.Runtime.init(.{
.platform = my_platform,
.extensions = registry,
});
```
## Module fields
<table>
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>info.id</code></td>
<td><code>u64</code></td>
<td>Unique numeric identifier (duplicates rejected at validation)</td>
</tr>
<tr>
<td><code>info.name</code></td>
<td><code>[]const u8</code></td>
<td>Human-readable module name</td>
</tr>
<tr>
<td><code>info.capabilities</code></td>
<td><code>[]const Capability</code></td>
<td>Capabilities this module provides</td>
</tr>
<tr>
<td><code>context</code></td>
<td><code>*anyopaque</code></td>
<td>Opaque pointer to module state</td>
</tr>
<tr>
<td><code>hooks.start_fn</code></td>
<td>optional</td>
<td>Called when the runtime starts</td>
</tr>
<tr>
<td><code>hooks.stop_fn</code></td>
<td>optional</td>
<td>Called when the runtime stops (reverse registration order)</td>
</tr>
<tr>
<td><code>hooks.command_fn</code></td>
<td>optional</td>
<td>Called when a command is dispatched to modules</td>
</tr>
</tbody>
</table>
## Registry methods
<table>
<thead>
<tr>
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>validate()</code></td>
<td>Check for duplicate module IDs</td>
</tr>
<tr>
<td><code>startAll(runtime)</code></td>
<td>Call <code>start_fn</code> on all modules</td>
</tr>
<tr>
<td><code>stopAll(runtime)</code></td>
<td>Call <code>stop_fn</code> on all modules (reverse order)</td>
</tr>
<tr>
<td><code>dispatchCommand(runtime, command)</code></td>
<td>Call <code>command_fn</code> on all modules</td>
</tr>
<tr>
<td><code>hasCapability(kind)</code></td>
<td>Check if any module provides a capability</td>
</tr>
</tbody>
</table>
## Capability kinds
<table>
<thead>
<tr>
<th>Kind</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>native_module</code></td>
<td>A native Zig module</td>
</tr>
<tr>
<td><code>webview</code></td>
<td>WebView rendering</td>
</tr>
<tr>
<td><code>js_bridge</code></td>
<td>JavaScript bridge</td>
</tr>
<tr>
<td><code>filesystem</code></td>
<td>File system access</td>
</tr>
<tr>
<td><code>network</code></td>
<td>Network access</td>
</tr>
<tr>
<td><code>clipboard</code></td>
<td>Clipboard access</td>
</tr>
<tr>
<td><code>custom</code></td>
<td>Custom capability (with a <code>name</code> field)</td>
</tr>
</tbody>
</table>
These map to the `capabilities` field in [app.zon](/app-zon). The runtime can query whether any module provides a given capability using `registry.hasCapability(.filesystem)`.
## Native JS engine (experimental)
The `js` module provides an abstraction layer for calling into a native JavaScript engine from Zig. This is separate from the WebView bridge and is intended for future native module integrations.
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Value</code></td>
<td>Tagged union: <code>null</code>, <code>boolean</code>, <code>number</code>, <code>string</code></td>
</tr>
<tr>
<td><code>Call</code></td>
<td>A function call: <code>module</code>, <code>function</code>, <code>args</code></td>
</tr>
<tr>
<td><code>Bridge</code></td>
<td>Validates and dispatches calls via <code>RuntimeHooks</code></td>
</tr>
<tr>
<td><code>NullEngine</code></td>
<td>Stub that returns <code>EngineUnavailable</code></td>
</tr>
</tbody>
</table>
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+7
View File
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("frontend");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
+116
View File
@@ -0,0 +1,116 @@
# Frontend Projects
For apps with a build step (React, Vue, Svelte, etc.), zero-native provides helpers to switch between a dev server and bundled assets.
## Dynamic source function
Use `source_fn` on your `App` so development uses a localhost server and production uses bundled assets:
```zig
fn source(context: *anyopaque) anyerror!zero_native.WebViewSource {
const self: *App = @ptrCast(@alignCast(context));
return zero_native.frontend.sourceFromEnv(self.env_map, .{
.dist = "dist",
.entry = "index.html",
});
}
```
`sourceFromEnv` checks `ZERO_NATIVE_FRONTEND_URL`. If set, it returns a URL source; otherwise it returns an assets source from `config.dist`.
## frontend.Config
<table>
<thead>
<tr>
<th>Field</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>dist</code></td>
<td><code>"dist"</code></td>
<td>Path to the built frontend output</td>
</tr>
<tr>
<td><code>entry</code></td>
<td><code>"index.html"</code></td>
<td>HTML entry point within dist</td>
</tr>
<tr>
<td><code>origin</code></td>
<td><code>"zero://app"</code></td>
<td>Origin for asset URLs</td>
</tr>
<tr>
<td><code>spa_fallback</code></td>
<td><code>true</code></td>
<td>Serve entry for unknown routes</td>
</tr>
<tr>
<td><code>dev_url_env</code></td>
<td><code>"ZERO_NATIVE_FRONTEND_URL"</code></td>
<td>Environment variable checked by <code>sourceFromEnv</code></td>
</tr>
</tbody>
</table>
## Configure in app.zon
```zig
.frontend = .{
.dist = "dist",
.entry = "index.html",
.spa_fallback = true,
.dev = .{
.url = "http://127.0.0.1:5173/",
.command = .{ "npm", "run", "dev", "--", "--host", "127.0.0.1" },
.ready_path = "/",
.timeout_ms = 30000,
},
}
```
## Dev server
Use `zero-native dev` to let zero-native manage the frontend server lifecycle:
```bash
zero-native dev --binary zig-out/bin/MyApp
zero-native dev --binary zig-out/bin/MyApp --url http://127.0.0.1:3000/ --command "npm run dev"
```
The command starts the frontend process, waits for the port to accept connections, launches the native shell with `ZERO_NATIVE_FRONTEND_URL`, and terminates the frontend when the shell exits. See [Dev Server](/cli/dev) for all flags.
## Framework recipes
**Vite**: `.url = "http://127.0.0.1:5173/"`, command `npm run dev -- --host 127.0.0.1`.
**Next.js**: `.url = "http://127.0.0.1:3000/"`, command `npm run dev -- --hostname 127.0.0.1`.
**Static preview**: point `.dist` at the build output and use any local server command.
## Examples
The repository includes complete frontend examples:
- `examples/next` - Next.js app with `frontend/out` production assets.
- `examples/react` - React app built with Vite.
- `examples/svelte` - Svelte app built with Vite.
- `examples/vue` - Vue app built with Vite.
Each example can be run from its directory with `zig build run`, or with `zig build dev` for the managed frontend dev server flow.
## Production source
For packaged builds that always use local assets:
```zig
return zero_native.frontend.productionSource(.{ .dist = "dist", .entry = "index.html" });
```
## ZERO_NATIVE_FRONTEND_ASSETS
`ZERO_NATIVE_FRONTEND_ASSETS` is an app-defined convention (not read by the `frontend` module). Examples use it to signal that pre-built assets should be loaded via `productionSource` instead of the default dev/prod branching.
+119
View File
@@ -0,0 +1,119 @@
@import "tailwindcss";
@plugin "tailwindcss-animate";
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, sans-serif;
--font-mono:
ui-monospace, "SF Mono", "Cascadia Mono", "Segoe UI Mono", Menlo, Consolas, monospace;
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-border: var(--border);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-sidebar: var(--sidebar);
}
:root {
--background: #fff;
--foreground: #171717;
--border: #e5e5e5;
--muted: #f5f5f5;
--muted-foreground: #737373;
--primary: #171717;
--primary-foreground: #fff;
--sidebar: #f5f5f5;
}
.dark {
--background: #0a0a0a;
--foreground: #f5f5f5;
--border: #262626;
--muted: #262626;
--muted-foreground: #a3a3a3;
--primary: #f5f5f5;
--primary-foreground: #0a0a0a;
--sidebar: #171717;
}
::selection {
background-color: #000;
color: #fff;
}
@media (prefers-color-scheme: dark) {
::selection {
background-color: #fff;
color: #000;
}
}
article table {
width: 100%;
font-size: 0.875rem;
margin-bottom: 1rem;
border-collapse: collapse;
}
article th {
border-bottom: 1px solid #e5e5e5;
padding: 0.5rem 0.75rem;
text-align: left;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #737373;
}
article td {
border-bottom: 1px solid #f5f5f5;
padding: 0.5rem 0.75rem;
color: #525252;
}
:is(.dark) article th {
border-bottom-color: #262626;
color: #a3a3a3;
}
:is(.dark) article td {
border-bottom-color: rgba(38, 38, 38, 0.5);
color: #a3a3a3;
}
.diff-add {
color: #00952d;
}
.diff-remove {
color: #f32e40;
}
.dark .diff-add {
color: #00ca50;
}
.dark .diff-remove {
color: #f32e40;
}
.shiki,
.shiki span {
color: var(--shiki-light) !important;
background-color: var(--shiki-light-bg) !important;
}
.dark .shiki,
.dark .shiki span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
}
button {
cursor: pointer;
}
+112
View File
@@ -0,0 +1,112 @@
import type { Metadata } from "next";
import Link from "next/link";
import { GeistPixelSquare } from "geist/font/pixel";
import { ThemeProvider } from "@/components/theme-provider";
import { ThemeToggle } from "@/components/theme-toggle";
import { Search } from "@/components/search";
import { DocsMobileNav } from "@/components/docs-mobile-nav";
import { DocsNav } from "@/components/docs-nav";
import "./globals.css";
export const metadata: Metadata = {
metadataBase: new URL("https://zero-native.dev"),
title: {
default: "zero-native | Desktop Apps with Zig + WebView",
template: "%s | zero-native",
},
description:
"Build desktop apps with Zig and selectable web engines. System WebView or bundled Chromium.",
openGraph: {
type: "website",
locale: "en_US",
url: "https://zero-native.dev",
siteName: "zero-native",
title: "zero-native | Desktop Apps with Zig + WebView",
description:
"Build desktop apps with Zig and selectable web engines. System WebView or bundled Chromium.",
images: [{ url: "/og", width: 1200, height: 630, alt: "zero-native" }],
},
twitter: {
card: "summary_large_image",
title: "zero-native | Desktop Apps with Zig + WebView",
description:
"Build desktop apps with Zig and selectable web engines. System WebView or bundled Chromium.",
images: ["/og"],
},
};
function Header() {
return (
<header className="sticky top-0 z-50 bg-white/90 backdrop-blur-sm dark:bg-neutral-950/90">
<div className="flex h-14 items-center justify-between px-4 gap-6">
<div className="flex items-center gap-2">
<Link href="https://vercel.com" title="Made with love by Vercel">
<svg
data-testid="geist-icon"
height="18"
strokeLinejoin="round"
viewBox="0 0 16 16"
width="18"
style={{ color: "currentcolor" }}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8 1L16 15H0L8 1Z"
fill="currentColor"
/>
</svg>
</Link>
<span className="text-neutral-300 dark:text-neutral-700">
<svg
data-testid="geist-icon"
height="16"
strokeLinejoin="round"
viewBox="0 0 16 16"
width="16"
style={{ color: "currentcolor" }}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.01526 15.3939L4.3107 14.7046L10.3107 0.704556L10.6061 0.0151978L11.9849 0.606077L11.6894 1.29544L5.68942 15.2954L5.39398 15.9848L4.01526 15.3939Z"
fill="currentColor"
/>
</svg>
</span>
<Link href="/">
<span className={`${GeistPixelSquare.className} text-lg`}>zero-native</span>
</Link>
</div>
<nav className="flex items-center gap-4">
<Search />
<a
href="https://github.com/vercel-labs/zero-native"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1.5 text-sm text-neutral-500 hover:text-neutral-900 transition-colors dark:text-neutral-400 dark:hover:text-neutral-100"
>
<svg viewBox="0 0 16 16" className="h-4 w-4" fill="currentColor" aria-hidden="true">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
</svg>
</a>
<ThemeToggle />
</nav>
</div>
</header>
);
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body className="bg-white text-neutral-900 antialiased dark:bg-neutral-950 dark:text-neutral-100">
<ThemeProvider>
<Header />
<DocsMobileNav />
<DocsNav>{children}</DocsNav>
</ThemeProvider>
</body>
</html>
);
}
+13
View File
@@ -0,0 +1,13 @@
import { NextResponse } from "next/server";
import { getPageTitle, renderOgImage } from "../og-image";
export async function GET(_request: Request, { params }: { params: Promise<{ slug: string[] }> }) {
const { slug } = await params;
const title = getPageTitle(slug.join("/"));
if (!title) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
return renderOgImage(title);
}
+111
View File
@@ -0,0 +1,111 @@
import { ImageResponse } from "next/og";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
export { getPageTitle } from "@/lib/page-titles";
let fontCache: { geistRegular: Buffer; geistPixelSquare: Buffer } | null = null;
async function loadFonts() {
if (fontCache) return fontCache;
const [geistRegular, geistPixelSquare] = await Promise.all([
readFile(join(process.cwd(), "public/Geist-Regular.ttf")),
readFile(join(process.cwd(), "public/GeistPixel-Square.ttf")),
]);
fontCache = { geistRegular, geistPixelSquare };
return fontCache;
}
export async function renderOgImage(title: string) {
const { geistRegular, geistPixelSquare } = await loadFonts();
return new ImageResponse(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
backgroundColor: "black",
padding: "60px 80px",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: "16px",
}}
>
<svg width="36" height="36" viewBox="0 0 16 16" fill="white">
<path fillRule="evenodd" clipRule="evenodd" d="M8 1L16 15H0L8 1Z" />
</svg>
<span
style={{
fontSize: 36,
color: "#666",
fontFamily: "Geist",
fontWeight: 400,
}}
>
/
</span>
<span
style={{
fontSize: 36,
fontFamily: "GeistPixelSquare",
fontWeight: 400,
color: "white",
}}
>
zero-native
</span>
</div>
<div
style={{
display: "flex",
flex: 1,
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
>
{title.split("\n").map((line, i) => (
<span
key={i}
style={{
fontSize: 72,
fontFamily: "Geist",
fontWeight: 400,
color: "white",
letterSpacing: "-0.02em",
textAlign: "center",
lineHeight: 1.2,
}}
>
{line}
</span>
))}
</div>
</div>,
{
width: 1200,
height: 630,
fonts: [
{
name: "Geist",
data: geistRegular.buffer as ArrayBuffer,
style: "normal",
weight: 400,
},
{
name: "GeistPixelSquare",
data: geistPixelSquare.buffer as ArrayBuffer,
style: "normal",
weight: 400,
},
],
}
);
}
+6
View File
@@ -0,0 +1,6 @@
import { getPageTitle, renderOgImage } from "./og-image";
export async function GET() {
const title = getPageTitle("")!;
return renderOgImage(title);
}
+7
View File
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("packages");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
+28
View File
@@ -0,0 +1,28 @@
# Package Distribution
zero-native is distributed as a Zig codebase plus a small npm wrapper package for the CLI. The former primitive modules (`geometry`, `assets`, `app_dirs`, `trace`, `app_manifest`, `diagnostics`, and `platform_info`) are now internal zero-native modules and are available through the main `zero-native` import instead of standalone Zig packages.
<table>
<thead>
<tr>
<th>Package</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>zero-native</code></td>
<td>npm package that installs the <code>zero-native</code> command and wraps the native Zig CLI binary</td>
</tr>
</tbody>
</table>
## Install
The zero-native CLI is published to npm as `zero-native`:
```bash
npm install -g zero-native
```
The npm package includes prebuilt binaries for macOS (arm64/x64), Linux (gnu/musl, arm64/x64), and Windows (x64). See `packages/zero-native/` in the repository for the wrapper scripts and packaging metadata.
+7
View File
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("packaging");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
+297
View File
@@ -0,0 +1,297 @@
# Packaging
zero-native provides tooling to bundle your app into distributable packages for macOS, Linux, and Windows. The beta distribution path is macOS-focused; Linux and Windows package support should be treated as roadmap or early support unless your app has validated those targets.
## Quick start
Build and package in two steps:
```bash
zig build package
```
Or use the CLI directly with more control:
```bash
zero-native package --target macos --manifest app.zon --binary zig-out/bin/MyApp
```
## Build options
The build system exposes options that control platform, web engine, and build features:
<table>
<thead>
<tr>
<th>Option</th>
<th>Values</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>-Dplatform</code></td>
<td><code>auto</code>, <code>null</code>, <code>macos</code>, <code>linux</code></td>
<td><code>auto</code></td>
<td>Target platform</td>
</tr>
<tr>
<td><code>-Dweb-engine</code></td>
<td><code>system</code>, <code>chromium</code></td>
<td><code>app.zon</code></td>
<td>Temporary WebView engine override</td>
</tr>
<tr>
<td><code>-Dcef-dir</code></td>
<td>path</td>
<td>--</td>
<td>Temporary CEF distribution directory override</td>
</tr>
<tr>
<td><code>-Dtrace</code></td>
<td><code>off</code>, <code>events</code>, <code>runtime</code>, <code>all</code></td>
<td><code>events</code></td>
<td>Trace output level</td>
</tr>
<tr>
<td><code>-Ddebug-overlay</code></td>
<td><code>true</code>, <code>false</code></td>
<td><code>false</code></td>
<td>Enable debug overlay in WebView</td>
</tr>
<tr>
<td><code>-Dautomation</code></td>
<td><code>true</code>, <code>false</code></td>
<td><code>false</code></td>
<td>Enable automation server</td>
</tr>
<tr>
<td><code>-Djs-bridge</code></td>
<td><code>true</code>, <code>false</code></td>
<td><code>false</code></td>
<td>Enable JavaScript bridge</td>
</tr>
</tbody>
</table>
## app.zon packaging fields
The manifest drives packaging metadata:
```zig
.{
.id = "com.example.myapp",
.name = "myapp",
.display_name = "My App",
.version = "1.0.0",
.icons = .{ "assets/icon.icns", "assets/icon.ico" },
.platforms = .{ "macos", "linux" },
.web_engine = "system",
.cef = .{ .dir = "third_party/cef/macos", .auto_install = false },
}
```
<table>
<thead>
<tr>
<th>Field</th>
<th>Used for</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>id</code></td>
<td>macOS bundle identifier, Linux desktop file, log paths</td>
</tr>
<tr>
<td><code>display_name</code></td>
<td>Menu bar name, window title fallback</td>
</tr>
<tr>
<td><code>version</code></td>
<td><code>Info.plist</code> version, package metadata</td>
</tr>
<tr>
<td><code>icons</code></td>
<td>Copied into the app bundle per platform convention</td>
</tr>
<tr>
<td><code>platforms</code></td>
<td>Which platform packages to generate</td>
</tr>
</tbody>
</table>
## macOS
### App bundle
`zig build package` creates a `.app` bundle with:
- `Contents/MacOS/<binary>` -- the compiled executable
- `Contents/Resources/icon.icns` -- the app icon
- `Contents/Info.plist` -- generated from `app.zon`
- `Contents/Resources/dist/` -- frontend assets (if configured)
See [Code Signing](/packaging/signing) for signing, notarization, and DMG creation.
## Linux
### Package structure
Linux packaging creates an install tree:
- `bin/<name>` -- the executable
- `share/applications/<name>.desktop` -- desktop entry file
- `share/icons/hicolor/.../<name>.png` -- icons at standard sizes
```bash
zero-native package --target linux --manifest app.zon --binary zig-out/bin/MyApp
```
## Windows
```bash
zero-native package --target windows --manifest app.zon --binary zig-out/bin/MyApp.exe
```
Windows packaging is in early development. The packager copies the binary and assets into a distributable directory structure.
## Frontend assets
### Bundle assets
If your app has a frontend build step, bundle the output:
```bash
zig build bundle-assets
```
This copies the configured `dist` directory into the build output. Production packages serve these through `zero://app/`, so paths like `/assets/app.js` work without `file://` URLs.
### Configure in app.zon
```zig
.frontend = .{
.dist = "dist",
.entry = "index.html",
.spa_fallback = true,
}
```
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>dist</code></td>
<td>Path to the built frontend output</td>
</tr>
<tr>
<td><code>entry</code></td>
<td>HTML entry point within <code>dist</code></td>
</tr>
<tr>
<td><code>spa_fallback</code></td>
<td>Serve <code>entry</code> for unknown routes (SPA mode)</td>
</tr>
</tbody>
</table>
## CEF bundling
When using the Chromium engine, bundle CEF alongside the app:
```bash
zig build cef-bundle -Dcef-dir=/path/to/cef
```
This copies the required CEF framework, libraries, and resources into the app bundle. The CEF distribution must match the target platform and architecture.
Use the same CEF version for install, build, package, and CI verification. The usual app-developer flow is:
```bash
zero-native cef install --version <pinned-version>
zig build
zero-native package --target macos
```
Set `.web_engine = "chromium"` and `.cef = .{ .dir = "third_party/cef/macos", .auto_install = false }` in `app.zon` for the normal Chromium package path. Use `-Dweb-engine`, `--web-engine`, `-Dcef-dir`, or `--cef-dir` only when you need a one-off override.
Verify the Chromium macOS package layout locally with:
```bash
zig build test-package-cef-layout -Dplatform=macos
```
This gated check requires a local CEF layout or `-Dcef-auto-install=true` and verifies that the packaged app contains the CEF framework and resource files.
## Icon generation
Generate platform-specific icon files from a source PNG:
```bash
zig build generate-icon
```
This produces `icon.icns` (macOS) and `icon.ico` (Windows) from `assets/icon.png`.
## Validation
Check that your manifest and environment are ready for packaging:
```bash
zero-native doctor --manifest app.zon --strict
zero-native validate app.zon
```
`doctor` checks the host environment, WebView availability, manifest validity, log paths, and optional CEF paths. Add `--strict` to fail on any warning. See [Debugging](/debugging) for details on what `zero-native doctor` checks.
## Platform shortcut commands
In addition to `zero-native package --target <platform>`, the CLI provides shortcut commands:
```bash
zero-native package-windows [--output path] [--binary path]
zero-native package-linux [--output path] [--binary path]
zero-native package-ios [--output path] [--binary path]
zero-native package-android [--output path] [--binary path]
```
## Platform targets
<table>
<thead>
<tr>
<th>Target</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>macos</code></td>
<td>Full support: <code>.app</code> bundle, signing, notarization, DMG</td>
</tr>
<tr>
<td><code>linux</code></td>
<td>Desktop entry, icon install, binary packaging</td>
</tr>
<tr>
<td><code>windows</code></td>
<td>Early support: directory-based packaging</td>
</tr>
<tr>
<td><code>ios</code></td>
<td>Experimental</td>
</tr>
<tr>
<td><code>android</code></td>
<td>Experimental</td>
</tr>
</tbody>
</table>
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("packaging/signing");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
+107
View File
@@ -0,0 +1,107 @@
# Code Signing
Sign and notarize your zero-native app for distribution.
## macOS signing
Sign the bundle with a Developer ID:
```bash
zero-native package --target macos --signing identity --identity "Developer ID Application: Your Name"
```
Signing modes:
<table>
<thead>
<tr>
<th>Mode</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>none</code></td>
<td>No signing (default)</td>
</tr>
<tr>
<td><code>adhoc</code></td>
<td>Ad-hoc signing for local testing</td>
</tr>
<tr>
<td><code>identity</code></td>
<td>Sign with a named identity (requires <code>--identity</code>)</td>
</tr>
</tbody>
</table>
## Signing flags
<table>
<thead>
<tr>
<th>Flag</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>--signing</code></td>
<td>Signing mode: <code>none</code>, <code>adhoc</code>, or <code>identity</code></td>
</tr>
<tr>
<td><code>--identity</code></td>
<td>Code signing identity name</td>
</tr>
<tr>
<td><code>--entitlements</code></td>
<td>Path to entitlements file (e.g. <code>assets/zero-native.entitlements</code>)</td>
</tr>
<tr>
<td><code>--team-id</code></td>
<td>Apple Developer Team ID</td>
</tr>
</tbody>
</table>
## Notarization
The framework repository includes a `zig build notarize` helper for local release testing:
```bash
zig build notarize
```
Generated apps should use `zero-native package --target macos --signing identity ...` unless they add their own `notarize` build step. This helper does not invoke `xcrun notarytool` directly. After the signed package is created, submit it for notarization manually:
```bash
xcrun notarytool submit zig-out/package/your-app.zip --apple-id "you@example.com" --team-id "TEAMID" --password "@keychain:AC_PASSWORD" --wait
xcrun stapler staple zig-out/package/your-app.app
```
## Chromium apps
Chromium packages include `Chromium Embedded Framework.framework` inside the `.app`. Sign and notarize the final package after CEF has been bundled so the app binary, helper executables, and embedded framework are covered by the same distribution identity.
```bash
zero-native cef install --version <pinned-version>
zig build
zero-native package --target macos --signing identity --identity "Developer ID Application: Your Name"
hdiutil create -volname "Your App" -srcfolder zig-out/package/your-app.app -ov -format UDZO zig-out/package/your-app.dmg
```
Use `.web_engine = "chromium"` and `.cef = .{ .dir = "third_party/cef/macos", .auto_install = false }` in `app.zon` for the normal signing path. `-Dweb-engine`, `--web-engine`, `-Dcef-dir`, and `--cef-dir` remain available for temporary overrides.
If Gatekeeper rejects the app, check that the CEF framework is present in `Contents/Frameworks`, that every nested helper is signed, and that the package was rebuilt after any CEF version change.
## DMG creation
Create a distributable disk image:
```bash
zig build dmg
```
## Entitlements
The project includes `assets/zero-native.entitlements` as a starting point. Customize it for your app's needs (e.g. network access, file system access, camera).
+48
View File
@@ -0,0 +1,48 @@
# zero-native
Build native desktop apps with web UI. Tiny binaries. Minimal memory. Instant rebuilds.
## Why zero-native
### Tiny and fast
zero-native apps using the system WebView produce sub-megabyte binaries and use a fraction of the memory you'd expect from a native app framework. No bundled runtime bloating your app.
### Choose your web engine
Use the system WebView for lightweight apps, or bundle Chromium via CEF when you need pixel-perfect rendering consistency. Same API, different tradeoff. You choose per project.
### Fast native rebuilds
Zig compiles fast. Change your bridge commands, system integrations, or app logic and get a rebuilt binary in seconds. Your frontend still hot-reloads instantly.
### Any C library, one import away
Zig calls C directly. No binding generation, no `unsafe` wrappers, no glue code. Native SDKs, audio codecs, ML runtimes: include the header and call it. When your app needs to go deeper than the built-in APIs, nothing is out of reach.
### Cross-platform foundation
Build macOS and Linux desktop shells from one Zig codebase today, with Windows and mobile work in progress. The native layer stays small and explicit while the WebView surface stays familiar.
### Simpler native layer
No borrow checker. No lifetimes. No fighting the compiler for 20 minutes over a string. Zig is a simple, readable systems language that web developers can pick up in an afternoon.
## Get started
```bash
zero-native init my_app --frontend next
cd my_app
zig build run
```
The first run installs the generated frontend dependencies, then opens a native window rendering your HTML. Read the full [Quick Start](/quick-start) to go from zero to a packaged app.
## Learn more
- [Quick Start](/quick-start) -- Create, run, and package your first app
- [Web Engines](/web-engines) -- System WebView vs. Chromium (CEF)
- [App Model](/app-model) -- Apps, sources, and lifecycle
- [Bridge](/bridge) -- Call native Zig from JavaScript
- [Frontend Projects](/frontend) -- Use React, Vue, or Svelte
- [Security](/security) -- Permissions, policies, and navigation rules
+7
View File
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("quick-start");
export default function QuickStartLayout({ children }: { children: React.ReactNode }) {
return children;
}
+120
View File
@@ -0,0 +1,120 @@
# Quick Start
zero-native is a Zig desktop app shell with selectable web engines. Use the system WebView (WKWebView, WebKitGTK) for lightweight apps, or bundle Chromium via CEF on macOS for predictable Chromium rendering. Create a native desktop app with a web UI in under a minute.
## Beta scope
The current beta target is macOS desktop apps. System WebView builds are available on macOS and Linux, Chromium/CEF builds and packages are macOS-only, and Windows plus Linux Chromium support are on the roadmap.
## Prerequisites
- [Zig 0.16.0+](https://ziglang.org/download/)
- Node.js with npm for the generated frontend
- macOS or Linux (Windows support is in progress)
## Create a project
```bash
zero-native init my_app --frontend next
cd my_app
```
Frontend options: `next`, `vite`, `react`, `svelte`, `vue`.
This scaffolds a complete zero-native project:
<table>
<thead>
<tr>
<th>File</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>build.zig</code></td>
<td>Zig build graph with platform, trace, debug-overlay, automation, js-bridge, and web-engine options</td>
</tr>
<tr>
<td><code>build.zig.zon</code></td>
<td>Zig package manifest, declares zero-native dependency</td>
</tr>
<tr>
<td><code>app.zon</code></td>
<td>App metadata: name, icons, permissions, bridge commands, security policy, window definitions</td>
</tr>
<tr>
<td><code>src/main.zig</code></td>
<td>App struct with <code>app()</code> and optional <code>bridge()</code> methods</td>
</tr>
<tr>
<td><code>src/runner.zig</code></td>
<td>Platform wiring: trace sinks, file logging, panic capture, state store, runtime init</td>
</tr>
<tr>
<td><code>assets/icon.icns</code></td>
<td>App icon for macOS packages</td>
</tr>
<tr>
<td><code>frontend/</code></td>
<td>Frontend starter for the framework selected with <code>--frontend</code></td>
</tr>
</tbody>
</table>
## Run it
```bash
zig build run
```
The first frontend build installs dependencies automatically. Your app opens a native window with a WebView rendering your HTML.
## macOS beta path
Use this path when validating an app for the macOS beta:
```bash
zero-native init my_app --frontend next
cd my_app
zig build run
zero-native cef install
zig build run
zero-native package --target macos --signing identity --identity "Developer ID Application: Your Name"
zero-native doctor --manifest app.zon --strict
```
Set `.web_engine = "chromium"` and `.cef = .{ .dir = "third_party/cef/macos", .auto_install = false }` in `app.zon` before the Chromium run. `-Dweb-engine` and `--web-engine` are still available for one-off overrides, but the normal app workflow reads the manifest.
For frontend frameworks, run the frontend dev server through [Dev Server](/cli/dev) during development, then package the built assets for distribution.
## Hello world
The simplest zero-native app provides a name and inline HTML:
```zig
const HelloApp = struct {
fn app(self: *@This()) zero_native.App {
return .{
.context = self,
.name = "hello",
.source = zero_native.WebViewSource.html(
\\<!doctype html>
\\<html>
\\<body style="font-family: system-ui; padding: 2rem;">
\\ <h1>Hello from zero-native</h1>
\\</body>
\\</html>
),
};
}
};
```
## Next steps
- [Web Engines](/web-engines) -- Choose between system WebView and Chromium (CEF)
- [App Model](/app-model) -- How apps, sources, and lifecycle callbacks work
- [Frontend Projects](/frontend) -- Use React, Vue, or Svelte with zero-native
- [Bridge](/bridge) -- Call native Zig code from JavaScript
- [Security](/security) -- Permissions, policies, and navigation rules
+8
View File
@@ -0,0 +1,8 @@
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: "*", allow: "/" },
sitemap: "https://zero-native.dev/sitemap.xml",
};
}
+7
View File
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("security");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
+290
View File
@@ -0,0 +1,290 @@
# Security
zero-native treats the WebView as untrusted by default. App authors opt into native power with explicit permissions, command policies, and navigation rules.
## Permissions and capabilities
`capabilities` describe broad features an app uses. `permissions` are the runtime grants checked before native commands run.
```zig
.permissions = .{ "window", "filesystem" },
.capabilities = .{ "webview", "js_bridge" },
```
### Available permissions
<table>
<thead>
<tr>
<th>Permission</th>
<th>Grants</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>window</code></td>
<td>Window create/focus/close operations</td>
</tr>
<tr>
<td><code>filesystem</code></td>
<td>File system access from bridge commands</td>
</tr>
<tr>
<td><code>clipboard</code></td>
<td>Clipboard read/write</td>
</tr>
<tr>
<td><code>network</code></td>
<td>Network requests from native code</td>
</tr>
<tr>
<td><code>camera</code></td>
<td>Camera access</td>
</tr>
<tr>
<td><code>microphone</code></td>
<td>Microphone access</td>
</tr>
<tr>
<td><code>location</code></td>
<td>Location services</td>
</tr>
<tr>
<td><code>notifications</code></td>
<td>System notifications</td>
</tr>
</tbody>
</table>
Custom permissions use reverse-DNS names (e.g. `com.example.my-permission`). Use the smallest set that covers your app.
### Available capabilities
<table>
<thead>
<tr>
<th>Capability</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>webview</code></td>
<td>WebView rendering</td>
</tr>
<tr>
<td><code>js_bridge</code></td>
<td>JavaScript bridge</td>
</tr>
<tr>
<td><code>native_module</code></td>
<td>Native Zig extension modules</td>
</tr>
<tr>
<td><code>filesystem</code></td>
<td>File system access</td>
</tr>
<tr>
<td><code>network</code></td>
<td>Network access</td>
</tr>
<tr>
<td><code>clipboard</code></td>
<td>Clipboard access</td>
</tr>
</tbody>
</table>
## Native commands
Native bridge commands are default-deny. A command must be registered by native code **and** allowed by policy before the runtime invokes it.
```zig
.bridge = .{
.commands = .{
.{
.name = "native.ping",
.origins = .{ "zero://app" },
},
.{
.name = "zero-native.window.create",
.permissions = .{ "window" },
.origins = .{ "zero://app" },
},
},
},
```
Prefer exact origins over `"*"`. Use `"*"` only for local development or commands that do not expose native state.
## Builtin bridge policy
zero-native provides built-in commands for windows (`zero-native.window.*`) and dialogs (`zero-native.dialog.*`). These are controlled separately from app-defined commands via the `builtin_bridge` field in `RuntimeOptions`.
`js_window_api` exposes the JavaScript window helper, but it does not bypass security. Window commands (`zero-native.window.list`, `create`, `focus`, `close`) must come from an allowed origin and must have the `window` permission when runtime permissions are configured. For broader control, use an explicit `builtin_bridge` policy:
Dialog commands (`zero-native.dialog.openFile`, `saveFile`, `showMessage`) are **always default-deny** and require an explicit `builtin_bridge` policy with the command listed:
```zig
.builtin_bridge = .{
.enabled = true,
.commands = &.{
.{ .name = "zero-native.window.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
.{ .name = "zero-native.dialog.openFile", .origins = .{ "zero://app" } },
.{ .name = "zero-native.dialog.showMessage", .origins = .{ "zero://app" } },
},
},
```
## Bridge error codes
When a bridge call fails, the JavaScript promise rejects with an error containing a `code` field:
<table>
<thead>
<tr>
<th>Code</th>
<th>Cause</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>invalid_request</code></td>
<td>Malformed JSON message</td>
</tr>
<tr>
<td><code>unknown_command</code></td>
<td>No handler registered for this command</td>
</tr>
<tr>
<td><code>permission_denied</code></td>
<td>Origin or permission check failed</td>
</tr>
<tr>
<td><code>handler_failed</code></td>
<td>Handler returned an error</td>
</tr>
<tr>
<td><code>payload_too_large</code></td>
<td>Message exceeds 16 KiB limit</td>
</tr>
<tr>
<td><code>internal_error</code></td>
<td>Unexpected runtime error</td>
</tr>
</tbody>
</table>
Handle errors in JavaScript:
```javascript
try {
const result = await window.zero.invoke("native.ping", {});
} catch (error) {
console.error(error.code, error.message);
}
```
## Navigation policy
Main-frame navigation is allowlisted. Packaged assets normally use `zero://app`, inline examples use `zero://inline`, and dev servers should list their exact local origin.
```zig
.security = .{
.navigation = .{
.allowed_origins = .{
"zero://app",
"zero://inline",
"http://127.0.0.1:5173",
},
},
},
```
Unknown main-frame navigations are blocked unless the external-link policy explicitly handles them.
## External links
External links are denied by default. To open links in the system browser, opt in and list URL prefixes:
```zig
.security = .{
.navigation = .{
.external_links = .{
.action = "open_system_browser",
.allowed_urls = .{ "https://example.com/docs/*" },
},
},
},
```
Do not allow broad external patterns for pages that can be influenced by remote content.
## CSP guidance
For packaged assets, start with a strict Content Security Policy:
```html
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'">
```
For inline Zig examples that embed scripts or styles, add only the minimum inline allowances:
```html
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'">
```
For dev servers, extend `connect-src` only to the local dev origin and WebSocket endpoint required by the framework. Keep production CSP separate from development CSP.
## Security model summary
<table>
<thead>
<tr>
<th>Layer</th>
<th>Default</th>
<th>Opt-in</th>
</tr>
</thead>
<tbody>
<tr>
<td>App bridge commands</td>
<td>Denied</td>
<td>Per-command policy with origin and permission checks</td>
</tr>
<tr>
<td>Builtin bridge (windows)</td>
<td>Denied unless <code>js_window_api</code> or explicit policy allows the helper and origin/permission checks pass</td>
<td><code>window</code> permission plus exact allowed origins</td>
</tr>
<tr>
<td>Builtin bridge (dialogs)</td>
<td>Denied</td>
<td>Explicit <code>builtin_bridge</code> policy required</td>
</tr>
<tr>
<td>Navigation</td>
<td>Blocked</td>
<td>Allowlisted origins</td>
</tr>
<tr>
<td>External links</td>
<td>Denied</td>
<td>Explicit action + URL prefix list</td>
</tr>
<tr>
<td>Permissions</td>
<td>None granted</td>
<td>Declared in <code>app.zon</code>, checked at runtime</td>
</tr>
<tr>
<td>CSP</td>
<td>Not enforced by zero-native</td>
<td>Set in your HTML <code>&lt;meta&gt;</code> tag</td>
</tr>
</tbody>
</table>
The goal is defense in depth: even if a command is registered in Zig, it won't execute unless the policy allows it from the requesting origin with the required permissions.
+22
View File
@@ -0,0 +1,22 @@
import type { MetadataRoute } from "next";
import { allDocsPages } from "@/lib/docs-navigation";
import { statSync } from "node:fs";
import path from "node:path";
const baseUrl = "https://zero-native.dev";
export default function sitemap(): MetadataRoute.Sitemap {
return allDocsPages.map((page) => ({
url: `${baseUrl}${page.href}`,
lastModified: lastModifiedFor(page.href),
}));
}
function lastModifiedFor(href: string): Date {
const relative = href === "/" ? "page.mdx" : path.join(href.slice(1), "page.mdx");
try {
return statSync(path.join(process.cwd(), "src", "app", relative)).mtime;
} catch {
return new Date("2026-05-08T00:00:00.000Z");
}
}
+7
View File
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("testing");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
+58
View File
@@ -0,0 +1,58 @@
# Testing
zero-native provides headless testing tools for bridge and lifecycle coverage without a GUI, plus automation-based integration tests.
## TestHarness
`TestHarness` provides a headless test driver using `NullPlatform` and a `BufferSink` for capturing trace records:
```zig
var harness: zero_native.TestHarness = undefined;
harness.init(.{});
```
The harness provides a pre-configured runtime with `NullPlatform` and a trace sink that captures records in memory. Use it to test bridge handlers, lifecycle events, and command dispatch.
`TestHarness` is the same mechanism used by the framework's own test suite to verify bridge policy enforcement, window management, and lifecycle correctness.
## Headless tests
The default test suite does not require a window server:
```bash
zig build test
zig build test-desktop
zig build test-platform-info
```
Bridge and IPC coverage lives in the headless desktop tests: they inject platform bridge events, exercise command policy and handlers, and assert the platform response without launching a WebView.
## WebView smoke tests
WebView smoke coverage is a separate macOS integration step using [Automation](/automation):
```bash
zig build test-webview-smoke -Dplatform=macos
zig build test-webview-cef-smoke -Dplatform=macos -Dweb-engine=chromium
```
This step:
1. Starts the system WebView example with automation and the JS bridge enabled
2. Waits for a published automation snapshot (`zero-native automate wait`)
3. Verifies main window/source metadata (`zero-native automate snapshot`)
4. Sends a `native.ping` request through `zero-native automate bridge`
5. Verifies the response
The CEF smoke step additionally requires a local CEF layout or `-Dcef-auto-install=true`; it exercises `native.ping` and JS window create/list/focus/close through the automation bridge. These steps are intentionally opt-in because they need a GUI-capable macOS session.
## NullPlatform
`NullPlatform` is a headless platform stub that records loaded sources and dispatched events without creating real windows. Use it in tests and with `EmbeddedApp`:
```zig
var null_platform = zero_native.NullPlatform.init(.{});
var runtime = zero_native.Runtime.init(.{
.platform = null_platform.platform(),
});
```
+7
View File
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("tray");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
+91
View File
@@ -0,0 +1,91 @@
# System Tray
zero-native supports system tray icons with menus. Tray actions dispatch as `CommandEvent` with name `"tray.action"` in the runtime.
Tray support is currently implemented on macOS. Linux returns `UnsupportedService` until a portable status notifier implementation is selected.
## TrayOptions
<table>
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>icon_path</code></td>
<td><code>[]const u8</code></td>
<td><code>""</code></td>
</tr>
<tr>
<td><code>tooltip</code></td>
<td><code>[]const u8</code></td>
<td><code>""</code></td>
</tr>
<tr>
<td><code>items</code></td>
<td><code>[]const TrayMenuItem</code></td>
<td><code>&.{}</code></td>
</tr>
</tbody>
</table>
## TrayMenuItem
<table>
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>id</code></td>
<td><code>TrayItemId</code> (<code>u32</code>)</td>
<td><code>0</code></td>
</tr>
<tr>
<td><code>label</code></td>
<td><code>[]const u8</code></td>
<td><code>""</code></td>
</tr>
<tr>
<td><code>separator</code></td>
<td><code>bool</code></td>
<td><code>false</code></td>
</tr>
<tr>
<td><code>enabled</code></td>
<td><code>bool</code></td>
<td><code>true</code></td>
</tr>
</tbody>
</table>
## PlatformServices methods
- `createTray(options)` -- create or replace the tray icon
- `updateTrayMenu(items)` -- update menu items without recreating the tray
- `removeTray()` -- remove the tray icon
## Handling tray actions
When a user clicks a tray menu item, the runtime dispatches a `CommandEvent` with the name `"tray.action"`. Use your `event_fn` to handle it:
```zig
fn event(context: *anyopaque, runtime: *Runtime, ev: Event) anyerror!void {
switch (ev) {
.command => |cmd| {
if (std.mem.eql(u8, cmd.name, "tray.action")) {
// Handle tray menu click
}
},
else => {},
}
}
```
+7
View File
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("updates");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
+38
View File
@@ -0,0 +1,38 @@
# Updates
zero-native reserves an explicit update configuration in `app.zon` so packaged apps can declare how they discover signed updates.
```zig
.updates = .{
.feed_url = "https://example.com/releases/zero-native-feed.json",
.public_key = "base64-ed25519-public-key",
.check_on_start = true,
},
```
The runtime does not silently install updates. Apps should surface update checks through their own UI, verify signatures before applying artifacts, and keep platform-specific installation behavior explicit.
## Fields
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>feed_url</code></td>
<td>HTTPS endpoint that describes available releases.</td>
</tr>
<tr>
<td><code>public_key</code></td>
<td>Public key used to verify update metadata and artifacts.</td>
</tr>
<tr>
<td><code>check_on_start</code></td>
<td>Whether the app should check for updates during startup.</td>
</tr>
</tbody>
</table>
+7
View File
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("web-engines");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
+177
View File
@@ -0,0 +1,177 @@
# Web Engines
zero-native has one app/runtime API and selectable web engine backends. The default is the platform system WebView. On macOS, apps can instead bundle Chromium through CEF.
## Support Matrix
<table>
<thead>
<tr>
<th>Platform</th>
<th><code>system</code></th>
<th><code>chromium</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>macOS</td>
<td>WKWebView</td>
<td>CEF, bundled with the app</td>
</tr>
<tr>
<td>Linux</td>
<td>WebKitGTK</td>
<td>Not wired yet; builds fail early instead of silently using WebKitGTK</td>
</tr>
<tr>
<td>Windows</td>
<td>In progress</td>
<td>In progress</td>
</tr>
</tbody>
</table>
The intended parity contract is the same Zig app model, same runtime services, same builtin bridge commands, same `window.zero` helper shape, and the same security policy semantics for every engine that a platform supports.
## System WebView
```zig
.web_engine = "system",
```
System mode has no bundled browser dependency. It uses the OS web engine, so rendering and web platform support follow the user's installed OS.
## Chromium (CEF)
Set the app engine once:
```zig
.web_engine = "chromium",
.cef = .{ .dir = "third_party/cef/macos", .auto_install = false },
```
```bash
zero-native cef install
zig build run
```
CEF mode is first-class on macOS. It uses the same zero-native runtime APIs as WKWebView, but bundles Chromium for rendering consistency and a predictable web platform.
Chromium is currently wired for macOS only. Linux continues to use WebKitGTK, and Windows support is in progress. Treat the beta as macOS-focused when choosing Chromium.
### Setup
The happy path is managed by the CLI:
```bash
zero-native cef install
zero-native doctor --manifest app.zon
```
`zero-native cef install` downloads zero-native's prepared macOS CEF runtime from GitHub Releases, verifies it, and installs a complete layout into `third_party/cef/macos`. The prepared runtime already includes `libcef_dll_wrapper.a`, so app developers do not need CMake or Chromium build knowledge.
The default install uses zero-native's pinned tested CEF version. Pin that version in your app CI or setup docs with `zero-native cef install --version <version>`, rerun the install when you intentionally update Chromium, then rebuild and repackage the app. A packaged Chromium app must bundle the same CEF layout the binary was linked against; mismatches usually show up as launch failures or missing framework/resource errors.
You can also opt into one-command local setup from the build:
```bash
zig build run
```
Set `.cef = .{ .dir = "third_party/cef/macos", .auto_install = true }` in `app.zon` to allow the build/package flow to install the prepared runtime automatically. Manual CEF layouts are still supported with `-Dcef-dir=/path/to/cef` or `.cef.dir`. Advanced users can also install directly from official CEF archives with `zero-native cef install --source official --allow-build-tools`. The expected layout is documented in `third_party/cef/README.md`.
Core maintainers can build CEF from source with `tools/cef/build-from-source.sh` when preparing the first runtime release for a version or testing a new CEF branch before publishing it.
### Build Overrides
<table>
<thead>
<tr>
<th>Flag</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>-Dweb-engine=system</code></td>
<td>Temporarily use the platform system WebView instead of the manifest default.</td>
</tr>
<tr>
<td><code>-Dweb-engine=chromium</code></td>
<td>Temporarily use CEF on supported platforms. Currently supported for macOS builds.</td>
</tr>
<tr>
<td><code>-Dcef-dir=path</code></td>
<td>Path to the CEF distribution directory.</td>
</tr>
<tr>
<td><code>-Dcef-auto-install=true</code></td>
<td>Temporarily opt into running <code>zero-native cef install</code> during Chromium builds when CEF is missing.</td>
</tr>
</tbody>
</table>
### Bundling
```bash
zig build cef-bundle -Dcef-dir=/path/to/cef
```
Local Chromium builds also copy the CEF framework into `zig-out/bin/Frameworks` when Chromium is selected through `app.zon` or a one-off flag. Packaging includes the runtime when the manifest or CLI override resolves to Chromium.
For beta distribution, verify the packaged `.app` contains `Chromium Embedded Framework.framework` under `Contents/Frameworks` and that signing covers both the app and embedded framework before notarization.
### Smoke Tests
```bash
zig build test-webview-cef-smoke -Dplatform=macos -Dweb-engine=chromium
zig build test-package-cef-layout -Dplatform=macos
```
The Chromium smoke step requires a local CEF layout or `-Dcef-auto-install=true`. It launches the example with automation, verifies `native.ping`, and exercises JS window create/list/focus/close. The package layout step verifies that a Chromium macOS package contains the framework and resources CEF expects.
## Choosing An Engine
<table>
<thead>
<tr>
<th>Consideration</th>
<th>System</th>
<th>Chromium</th>
</tr>
</thead>
<tbody>
<tr>
<td>Bundle size</td>
<td>Minimal because the browser is system-provided.</td>
<td>Large because CEF is bundled.</td>
</tr>
<tr>
<td>Rendering consistency</td>
<td>Varies by OS version.</td>
<td>Consistent for apps that ship the same CEF build.</td>
</tr>
<tr>
<td>Startup time</td>
<td>Fastest startup.</td>
<td>Slower startup because CEF initializes Chromium.</td>
</tr>
<tr>
<td>Best fit</td>
<td>Small apps, OS-native footprint, minimal dependencies.</td>
<td>Apps that need Chromium behavior, complex frontend stacks, or tighter rendering control.</td>
</tr>
</tbody>
</table>
## In app.zon
```zig
.web_engine = "system",
// or, on supported platforms:
.web_engine = "chromium",
.cef = .{
.dir = "third_party/cef/macos",
.auto_install = true,
},
```
+7
View File
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("windows");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
+150
View File
@@ -0,0 +1,150 @@
# Windows
zero-native supports multiple windows. The main window is created automatically; secondary windows can be created from Zig or JavaScript.
## Creating windows from Zig
```zig
const info = try runtime.createWindow(.{
.label = "tools",
.title = "Tools",
.default_frame = zero_native.geometry.RectF.init(80, 80, 420, 320),
});
try runtime.focusWindow(info.id);
```
## Creating windows from JavaScript
`js_window_api` exposes the `window.zero.windows.*` helper in JavaScript. Window commands still require an allowed origin and the `window` permission when runtime permissions are configured:
```zig
const app_permissions = [_][]const u8{zero_native.security.permission_window};
.security = .{
.permissions = &app_permissions,
.navigation = .{ .allowed_origins = &.{ "zero://app" } },
},
.js_window_api = true,
```
Then JavaScript can call the helper from an allowed origin:
```javascript
const win = await window.zero.windows.create({
label: "tools",
title: "Tools",
width: 420,
height: 320,
});
const all = await window.zero.windows.list();
await window.zero.windows.focus(win.id);
await window.zero.windows.close(win.id);
```
## Window types
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>WindowId</code></td>
<td>Opaque identifier (<code>u64</code>)</td>
</tr>
<tr>
<td><code>WindowCreateOptions</code></td>
<td>Options for <code>runtime.createWindow()</code>: label, title, frame</td>
</tr>
<tr>
<td><code>WindowInfo</code></td>
<td>Returned after creation: id, label, title, frame</td>
</tr>
<tr>
<td><code>WindowState</code></td>
<td>Persisted state: id, label, title, frame, open, focused, maximized, fullscreen, scale</td>
</tr>
<tr>
<td><code>WindowRestorePolicy</code></td>
<td>How restored frames are placed, such as clamping to the visible screen or centering on the primary display</td>
</tr>
</tbody>
</table>
## Platform limits
<table>
<thead>
<tr>
<th>Constant</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>max_windows</code></td>
<td>16</td>
</tr>
<tr>
<td><code>max_window_label_bytes</code></td>
<td>64</td>
</tr>
<tr>
<td><code>max_window_title_bytes</code></td>
<td>128</td>
</tr>
</tbody>
</table>
## Window state persistence
The `window_state.Store` persists geometry to `windows.zon` in the app's state directory. Startup restore uses the window `label` from the manifest and applies the saved frame; titles continue to come from `app.zon` or runtime window creation options.
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>id</code></td>
<td>Window ID</td>
</tr>
<tr>
<td><code>label</code></td>
<td>Window label (used for merge matching)</td>
</tr>
<tr>
<td><code>frame</code></td>
<td>Position and size (x, y, width, height)</td>
</tr>
<tr>
<td><code>maximized</code></td>
<td>Whether the window was maximized</td>
</tr>
<tr>
<td><code>fullscreen</code></td>
<td>Whether the window was fullscreen</td>
</tr>
<tr>
<td><code>scale</code></td>
<td>Display scale factor</td>
</tr>
</tbody>
</table>
When `saveWindow` is called, the store merges by matching on `label` or `id`, so secondary windows are preserved alongside the main window. Records with missing, malformed, or empty labels are ignored on load and omitted the next time the file is rewritten.
## Declaring windows in app.zon
```zig
.windows = .{
.{ .label = "main", .title = "zero-native", .width = 720, .height = 480, .restore_state = true },
},
```
+191
View File
@@ -0,0 +1,191 @@
import { codeToHtml } from "shiki";
const vercelDarkTheme = {
name: "vercel-dark",
type: "dark" as const,
colors: {
"editor.background": "transparent",
"editor.foreground": "#EDEDED",
},
settings: [
{
scope: ["comment", "punctuation.definition.comment"],
settings: { foreground: "#A1A1A1" },
},
{
scope: ["string", "string.quoted", "string.template", "punctuation.definition.string"],
settings: { foreground: "#00CA50" },
},
{
scope: ["constant.numeric", "constant.language.boolean", "constant.language.null"],
settings: { foreground: "#47A8FF" },
},
{
scope: ["keyword", "storage.type", "storage.modifier"],
settings: { foreground: "#FF4D8D" },
},
{
scope: ["keyword.operator", "keyword.control"],
settings: { foreground: "#FF4D8D" },
},
{
scope: ["entity.name.function", "support.function", "meta.function-call"],
settings: { foreground: "#C472FB" },
},
{
scope: ["variable", "variable.other"],
settings: { foreground: "#EDEDED" },
},
{
scope: ["variable.parameter"],
settings: { foreground: "#FF9300" },
},
{
scope: ["entity.name.tag", "support.class.component", "entity.name.type"],
settings: { foreground: "#FF4D8D" },
},
{
scope: ["punctuation", "meta.brace", "meta.bracket"],
settings: { foreground: "#EDEDED" },
},
{
scope: [
"support.type.property-name",
"entity.name.tag.json",
"meta.object-literal.key",
"punctuation.support.type.property-name",
],
settings: { foreground: "#FF4D8D" },
},
{
scope: ["entity.other.attribute-name"],
settings: { foreground: "#00CA50" },
},
{
scope: ["support.type.primitive", "entity.name.type.primitive"],
settings: { foreground: "#00CA50" },
},
],
};
const vercelLightTheme = {
name: "vercel-light",
type: "light" as const,
colors: {
"editor.background": "transparent",
"editor.foreground": "#171717",
},
settings: [
{
scope: ["comment", "punctuation.definition.comment"],
settings: { foreground: "#6B7280" },
},
{
scope: ["string", "string.quoted", "string.template", "punctuation.definition.string"],
settings: { foreground: "#067A6E" },
},
{
scope: ["constant.numeric", "constant.language.boolean", "constant.language.null"],
settings: { foreground: "#0070C0" },
},
{
scope: ["keyword", "storage.type", "storage.modifier"],
settings: { foreground: "#D6409F" },
},
{
scope: ["keyword.operator", "keyword.control"],
settings: { foreground: "#D6409F" },
},
{
scope: ["entity.name.function", "support.function", "meta.function-call"],
settings: { foreground: "#6E56CF" },
},
{
scope: ["variable", "variable.other"],
settings: { foreground: "#171717" },
},
{
scope: ["variable.parameter"],
settings: { foreground: "#B45309" },
},
{
scope: ["entity.name.tag", "support.class.component", "entity.name.type"],
settings: { foreground: "#D6409F" },
},
{
scope: ["punctuation", "meta.brace", "meta.bracket"],
settings: { foreground: "#6B7280" },
},
{
scope: [
"support.type.property-name",
"entity.name.tag.json",
"meta.object-literal.key",
"punctuation.support.type.property-name",
],
settings: { foreground: "#D6409F" },
},
{
scope: ["entity.other.attribute-name"],
settings: { foreground: "#067A6E" },
},
{
scope: ["support.type.primitive", "entity.name.type.primitive"],
settings: { foreground: "#067A6E" },
},
],
};
function DiffBlock({ children }: { children: string }) {
const lines = children.trim().split("\n");
return (
<div className="my-4 rounded-lg border border-neutral-200 bg-neutral-50 text-[13px] font-mono overflow-hidden dark:border-neutral-800 dark:bg-neutral-900">
<pre className="m-0 overflow-x-auto">
<code>
{lines.map((line, i) => {
let cls = "block px-4";
if (i === 0) cls += " pt-4";
if (i === lines.length - 1) cls += " pb-4";
if (line.startsWith("+")) cls += " diff-add";
else if (line.startsWith("-")) cls += " diff-remove";
return (
<span key={i} className={cls}>
{line}
{"\n"}
</span>
);
})}
</code>
</pre>
</div>
);
}
interface CodeProps {
children: string;
lang?: string;
}
export async function Code({ children, lang = "typescript" }: CodeProps) {
if (lang === "diff") {
return <DiffBlock>{children}</DiffBlock>;
}
const html = await codeToHtml(children.trim(), {
lang,
themes: {
light: vercelLightTheme,
dark: vercelDarkTheme,
},
defaultColor: false,
});
return (
<div className="my-4 rounded-lg border border-neutral-200 bg-neutral-50 text-[13px] font-mono overflow-hidden dark:border-neutral-800 dark:bg-neutral-900">
<div
className="overflow-x-auto [&_pre]:bg-transparent! [&_pre]:m-0! [&_pre]:p-4! [&_code]:bg-transparent! [&_.shiki]:bg-transparent!"
dangerouslySetInnerHTML={{ __html: html }}
/>
</div>
);
}
+77
View File
@@ -0,0 +1,77 @@
"use client";
import { useState, useMemo } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Sheet, SheetTrigger, SheetContent, SheetTitle } from "@/components/ui/sheet";
import { allDocsPages, navSections } from "@/lib/docs-navigation";
export function DocsMobileNav() {
const [open, setOpen] = useState(false);
const pathname = usePathname();
const currentPage = useMemo(() => {
return allDocsPages.find((page) => page.href === pathname) ?? allDocsPages[0];
}, [pathname]);
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger
aria-label="Open table of contents"
className="lg:hidden sticky top-14 z-40 w-full px-6 py-3 bg-white/80 dark:bg-neutral-950/80 backdrop-blur-sm border-b border-neutral-200 dark:border-neutral-800 flex items-center justify-between focus:outline-none"
>
<div className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
{currentPage.name}
</div>
<div className="w-8 h-8 flex items-center justify-center">
<svg
className="h-4 w-4 text-neutral-500 dark:text-neutral-400"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<line x1="8" y1="6" x2="21" y2="6" />
<line x1="8" y1="12" x2="21" y2="12" />
<line x1="8" y1="18" x2="21" y2="18" />
<line x1="3" y1="6" x2="3.01" y2="6" />
<line x1="3" y1="12" x2="3.01" y2="12" />
<line x1="3" y1="18" x2="3.01" y2="18" />
</svg>
</div>
</SheetTrigger>
<SheetContent side="left" className="overflow-y-auto p-6" showCloseButton={false}>
<SheetTitle className="mb-6">Table of Contents</SheetTitle>
<nav className="space-y-6">
{navSections.map((section) => (
<div key={section.title}>
<div className="mb-2 text-xs font-medium uppercase tracking-wider text-neutral-400 dark:text-neutral-500">
{section.title}
</div>
<ul className="space-y-0.5">
{section.items.map((item) => (
<li key={item.href}>
<Link
href={item.href}
onClick={() => setOpen(false)}
className={`text-sm block py-2 transition-colors ${
pathname === item.href
? "text-neutral-900 dark:text-neutral-100 font-medium"
: "text-neutral-500 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-100"
}`}
>
{item.name}
</Link>
</li>
))}
</ul>
</div>
))}
</nav>
</SheetContent>
</Sheet>
);
}
+54
View File
@@ -0,0 +1,54 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { navSections } from "@/lib/docs-navigation";
function Sidebar() {
const pathname = usePathname();
return (
<aside className="hidden w-56 shrink-0 lg:block">
<nav className="fixed top-14 w-56 h-[calc(100vh-3.5rem)] overflow-y-auto py-8 pr-4 space-y-6">
{navSections.map((section) => (
<div key={section.title}>
<div className="mb-2 px-3 text-xs font-medium uppercase tracking-wider text-neutral-400 dark:text-neutral-500">
{section.title}
</div>
<div className="space-y-0.5">
{section.items.map(({ href, name }) => {
const active = pathname === href;
return (
<Link
key={href}
href={href}
className={`block rounded-md px-3 py-1.5 text-sm transition-colors ${
active
? "bg-neutral-100 font-medium text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100"
: "text-neutral-600 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100"
}`}
>
{name}
</Link>
);
})}
</div>
</div>
))}
</nav>
</aside>
);
}
export function DocsNav({ children }: { children: React.ReactNode }) {
return (
<div className="mx-auto max-w-5xl px-6 py-8 lg:py-12">
<div className="flex gap-12">
<Sidebar />
<main className="min-w-0 flex-1">
<article className="max-w-none">{children}</article>
</main>
</div>
</div>
);
}
+68
View File
@@ -0,0 +1,68 @@
"use client";
import { useCallback } from "react";
function slugify(text: string): string {
return text
.toLowerCase()
.replace(/[^\w\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.trim();
}
function getTextContent(children: React.ReactNode): string {
if (typeof children === "string") return children;
if (typeof children === "number") return String(children);
if (Array.isArray(children)) return children.map(getTextContent).join("");
if (children && typeof children === "object" && "props" in children) {
const el = children as { props?: { children?: React.ReactNode } };
return getTextContent(el.props?.children);
}
return "";
}
export function HeadingLink({
as: Tag,
className,
children,
...props
}: {
as: "h1" | "h2" | "h3";
className: string;
children: React.ReactNode;
} & React.HTMLAttributes<HTMLHeadingElement>) {
const text = getTextContent(children);
const id = slugify(text);
const handleClick = useCallback(() => {
const url = `${window.location.origin}${window.location.pathname}#${id}`;
navigator.clipboard.writeText(url);
window.history.replaceState(null, "", `#${id}`);
}, [id]);
return (
<Tag id={id} className={`group relative ${className}`} {...props}>
{children}
<button
onClick={handleClick}
className="ml-2 inline-flex opacity-0 group-hover:opacity-100 transition-opacity text-neutral-300 hover:text-neutral-500 dark:text-neutral-700 dark:hover:text-neutral-400"
aria-label={`Copy link to ${text}`}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>
</button>
</Tag>
);
}
+250
View File
@@ -0,0 +1,250 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
type SearchResult = {
title: string;
href: string;
snippet: string;
};
export function Search() {
const router = useRouter();
const [open, setOpen] = useState(false);
const [query, setQuery] = useState("");
const [results, setResults] = useState<SearchResult[]>([]);
const [loading, setLoading] = useState(false);
const [activeIndex, setActiveIndex] = useState(0);
const inputRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLDivElement>(null);
const abortRef = useRef<AbortController | null>(null);
const navigate = useCallback(
(href: string) => {
setOpen(false);
setQuery("");
setResults([]);
router.push(href);
},
[router]
);
useEffect(() => {
function onKeyDown(e: KeyboardEvent) {
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
e.preventDefault();
setOpen((prev) => !prev);
}
}
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown);
}, []);
useEffect(() => {
if (open) {
setTimeout(() => inputRef.current?.focus(), 0);
} else {
setQuery("");
setResults([]);
}
}, [open]);
useEffect(() => {
const q = query.trim();
if (!q) {
setResults([]);
setLoading(false);
return;
}
setLoading(true);
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
const timeout = setTimeout(async () => {
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, {
signal: controller.signal,
});
if (res.ok) {
const data = await res.json();
setResults(data.results);
}
} catch {
// aborted or network error
} finally {
if (!controller.signal.aborted) {
setLoading(false);
}
}
}, 150);
return () => {
clearTimeout(timeout);
controller.abort();
};
}, [query]);
useEffect(() => {
setActiveIndex(0);
}, [results]);
function handleKeyDown(e: React.KeyboardEvent) {
if (e.key === "ArrowDown") {
e.preventDefault();
setActiveIndex((i) => Math.min(i + 1, results.length - 1));
} else if (e.key === "ArrowUp") {
e.preventDefault();
setActiveIndex((i) => Math.max(i - 1, 0));
} else if (e.key === "Enter" && results[activeIndex]) {
e.preventDefault();
navigate(results[activeIndex].href);
}
}
useEffect(() => {
const active = listRef.current?.querySelector("[data-active='true']");
active?.scrollIntoView({ block: "nearest" });
}, [activeIndex]);
const hasQuery = query.trim().length > 0;
return (
<>
<button
onClick={() => setOpen(true)}
className="hidden sm:flex items-center gap-2 rounded-md border border-border/50 bg-muted/50 px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground hover:border-foreground/25 transition-colors"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</svg>
Search docs
<kbd className="pointer-events-none ml-1 inline-flex items-center gap-0.5 rounded border border-border/50 bg-background px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">
<span>&#8984;</span>K
</kbd>
</button>
<button
onClick={() => setOpen(true)}
className="sm:hidden flex items-center text-muted-foreground hover:text-foreground transition-colors"
aria-label="Search docs"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</svg>
</button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent showCloseButton={false} className="gap-0 p-0 sm:max-w-lg">
<DialogTitle className="sr-only">Search documentation</DialogTitle>
<div className="flex items-center gap-2 border-b border-border/50 px-3">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="shrink-0 text-muted-foreground"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</svg>
<input
ref={inputRef}
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Search docs..."
className="flex-1 bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground"
/>
{query && (
<button
onClick={() => setQuery("")}
className="text-muted-foreground hover:text-foreground"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
)}
</div>
<div ref={listRef} className="max-h-[min(60vh,400px)] overflow-y-auto p-2">
{loading && hasQuery ? (
<div className="flex items-center justify-center py-6">
<div className="h-4 w-4 animate-spin rounded-full border-2 border-muted-foreground border-t-transparent" />
</div>
) : hasQuery && results.length === 0 ? (
<p className="py-6 text-center text-sm text-muted-foreground">No results found.</p>
) : !hasQuery ? (
<p className="py-6 text-center text-sm text-muted-foreground">
Type to search documentation...
</p>
) : (
results.map((item, i) => (
<button
key={item.href}
data-active={i === activeIndex}
onClick={() => navigate(item.href)}
onMouseEnter={() => setActiveIndex(i)}
className={cn(
"flex w-full flex-col gap-1 rounded-md px-3 py-2 text-left transition-colors",
i === activeIndex ? "bg-muted text-foreground" : "text-foreground"
)}
>
<span className="text-sm font-medium">{item.title}</span>
{item.snippet && (
<span className="line-clamp-2 text-xs text-muted-foreground leading-relaxed">
{item.snippet}
</span>
)}
</button>
))
)}
</div>
</DialogContent>
</Dialog>
</>
);
}
+16
View File
@@ -0,0 +1,16 @@
"use client";
import { ThemeProvider as NextThemesProvider } from "next-themes";
export function ThemeProvider({ children }: { children: React.ReactNode }) {
return (
<NextThemesProvider
attribute="class"
defaultTheme="dark"
enableSystem
disableTransitionOnChange
>
{children}
</NextThemesProvider>
);
}
+61
View File
@@ -0,0 +1,61 @@
"use client";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return <div className="w-8 h-8" />;
}
return (
<button
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
className="w-8 h-8 flex items-center justify-center rounded-md text-neutral-500 hover:text-neutral-900 hover:bg-neutral-100 transition-colors dark:text-neutral-400 dark:hover:text-neutral-100 dark:hover:bg-neutral-800"
aria-label="Toggle theme"
>
{theme === "dark" ? (
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2" />
<path d="M12 20v2" />
<path d="m4.93 4.93 1.41 1.41" />
<path d="m17.66 17.66 1.41 1.41" />
<path d="M2 12h2" />
<path d="M20 12h2" />
<path d="m6.34 17.66-1.41 1.41" />
<path d="m19.07 4.93-1.41 1.41" />
</svg>
) : (
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
</svg>
)}
</button>
);
}
+85
View File
@@ -0,0 +1,85 @@
"use client";
import * as React from "react";
import { Dialog as DialogPrimitive } from "radix-ui";
import { cn } from "@/lib/utils";
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
}
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
);
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean;
}) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border border-border/50 p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
);
}
function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-foreground font-semibold", className)}
{...props}
/>
);
}
export { Dialog, DialogPortal, DialogOverlay, DialogContent, DialogTitle };
+100
View File
@@ -0,0 +1,100 @@
"use client";
import * as React from "react";
import { Dialog as SheetPrimitive } from "radix-ui";
import { cn } from "@/lib/utils";
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
}
function SheetPortal({ ...props }: React.ComponentProps<typeof SheetPrimitive.Portal>) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
}
function SheetOverlay({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
return (
<SheetPrimitive.Overlay
data-slot="sheet-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
);
}
function SheetContent({
className,
children,
side = "right",
showCloseButton = true,
overlayClassName,
...props
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
side?: "top" | "right" | "bottom" | "left";
showCloseButton?: boolean;
overlayClassName?: string;
}) {
return (
<SheetPortal>
<SheetOverlay className={overlayClassName} />
<SheetPrimitive.Content
data-slot="sheet-content"
className={cn(
"bg-white dark:bg-neutral-950 data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l border-neutral-200 dark:border-neutral-800 sm:max-w-sm",
side === "left" &&
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r border-neutral-200 dark:border-neutral-800 sm:max-w-sm",
className
)}
{...props}
>
{children}
{showCloseButton && (
<SheetPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-neutral-400 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
)}
</SheetPrimitive.Content>
</SheetPortal>
);
}
function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn("font-semibold text-neutral-900 dark:text-neutral-100", className)}
{...props}
/>
);
}
function SheetTrigger({
className,
...props
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" className={cn(className)} {...props} />;
}
export { Sheet, SheetTrigger, SheetContent, SheetTitle };
+63
View File
@@ -0,0 +1,63 @@
export type NavItem = {
name: string;
href: string;
};
export type NavSection = {
title: string;
items: NavItem[];
};
export const navSections: NavSection[] = [
{
title: "Getting Started",
items: [
{ name: "Introduction", href: "/" },
{ name: "Quick Start", href: "/quick-start" },
{ name: "App Model", href: "/app-model" },
{ name: "Frontend Projects", href: "/frontend" },
],
},
{
title: "Core Concepts",
items: [
{ name: "Web Engines", href: "/web-engines" },
{ name: "Windows", href: "/windows" },
{ name: "Bridge", href: "/bridge" },
{ name: "Builtin Commands", href: "/bridge/builtin-commands" },
{ name: "Dialogs", href: "/dialogs" },
{ name: "System Tray", href: "/tray" },
{ name: "Security", href: "/security" },
],
},
{
title: "Tooling",
items: [
{ name: "CLI Reference", href: "/cli" },
{ name: "Dev Server", href: "/cli/dev" },
{ name: "Packaging", href: "/packaging" },
{ name: "Code Signing", href: "/packaging/signing" },
{ name: "Updates", href: "/updates" },
{ name: "app.zon Reference", href: "/app-zon" },
],
},
{
title: "Operations",
items: [
{ name: "Debugging", href: "/debugging" },
{ name: "zero-native doctor", href: "/debugging/doctor" },
{ name: "Automation", href: "/automation" },
{ name: "Testing", href: "/testing" },
],
},
{
title: "Advanced",
items: [
{ name: "Extensions", href: "/extensions" },
{ name: "Embedded App", href: "/embed" },
{ name: "Package Distribution", href: "/packages" },
],
},
];
export const allDocsPages: NavItem[] = navSections.flatMap((s) => s.items);
+37
View File
@@ -0,0 +1,37 @@
export function mdxToCleanMarkdown(raw: string): string {
const lines = raw.split("\n");
const out: string[] = [];
let inJsxBlock = false;
let jsxDepth = 0;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith("export ") || trimmed.startsWith("import ")) {
continue;
}
if (!inJsxBlock && trimmed.startsWith("<div ") && trimmed.includes("className=")) {
inJsxBlock = true;
jsxDepth = 1;
continue;
}
if (inJsxBlock) {
const opens = (line.match(/<div[\s>]/g) || []).length;
const closes = (line.match(/<\/div>/g) || []).length;
jsxDepth += opens - closes;
if (jsxDepth <= 0) {
inJsxBlock = false;
jsxDepth = 0;
}
continue;
}
out.push(line);
}
let result = out.join("\n");
result = result.replace(/^\n+/, "\n").trim();
return result;
}
+39
View File
@@ -0,0 +1,39 @@
import type { Metadata } from "next";
import { PAGE_TITLES } from "./page-titles";
const DESCRIPTION =
"Build desktop apps with Zig and selectable web engines. System WebView or bundled Chromium.";
export function pageMetadata(slug: string): Metadata {
const title = PAGE_TITLES[slug];
if (!title) return {};
const displayTitle = title.replace(/\n/g, " ");
const fullTitle = `${displayTitle} | zero-native`;
const ogImageUrl = slug ? `/og/${slug}` : "/og";
return {
title: displayTitle,
openGraph: {
type: "website",
locale: "en_US",
siteName: "zero-native",
title: fullTitle,
description: DESCRIPTION,
images: [
{
url: ogImageUrl,
width: 1200,
height: 630,
alt: `${displayTitle} - zero-native`,
},
],
},
twitter: {
card: "summary_large_image",
title: fullTitle,
description: DESCRIPTION,
images: [ogImageUrl],
},
};
}
+30
View File
@@ -0,0 +1,30 @@
export const PAGE_TITLES: Record<string, string> = {
"": "Build Desktop Apps\nwith Zig + WebView",
"quick-start": "Quick Start",
"app-model": "App Model",
frontend: "Frontend Projects",
windows: "Windows",
bridge: "Bridge",
"bridge/builtin-commands": "Builtin Commands",
dialogs: "Dialogs",
tray: "System Tray",
security: "Security",
cli: "CLI Reference",
"cli/dev": "Dev Server",
packaging: "Packaging",
"packaging/signing": "Code Signing",
updates: "Updates",
"app-zon": "app.zon Reference",
debugging: "Debugging",
"debugging/doctor": "zero-native doctor",
automation: "Automation",
testing: "Testing",
extensions: "Extensions",
embed: "Embedded App",
"web-engines": "Web Engines",
packages: "Package Distribution",
};
export function getPageTitle(slug: string): string | null {
return slug in PAGE_TITLES ? PAGE_TITLES[slug]! : null;
}
+46
View File
@@ -0,0 +1,46 @@
import { allDocsPages } from "./docs-navigation";
import { readFile } from "node:fs/promises";
import path from "node:path";
export type IndexEntry = {
title: string;
href: string;
content: string;
};
let cached: IndexEntry[] | null = null;
export async function getSearchIndex(): Promise<IndexEntry[]> {
if (cached) return cached;
const entries: IndexEntry[] = await Promise.all(
allDocsPages.map(async (item) => ({
title: item.name,
href: item.href,
content: await pageContent(item.href, item.name),
})),
);
cached = entries;
return entries;
}
async function pageContent(href: string, fallback: string): Promise<string> {
const relative = href === "/" ? "page.mdx" : path.join(href.slice(1), "page.mdx");
const filePath = path.join(process.cwd(), "src", "app", relative);
try {
const source = await readFile(filePath, "utf8");
return stripMdx(source);
} catch {
return fallback;
}
}
function stripMdx(source: string): string {
return source
.replace(/```[\s\S]*?```/g, " ")
.replace(/<[^>]+>/g, " ")
.replace(/[#*_`[\](){}>-]/g, " ")
.replace(/\s+/g, " ")
.trim();
}
+6
View File
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+93
View File
@@ -0,0 +1,93 @@
import type { MDXComponents } from "mdx/types";
import { Code } from "@/components/code";
import { HeadingLink } from "@/components/heading-link";
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
h1: (props) => (
<HeadingLink
as="h1"
className="mb-6 text-2xl font-semibold tracking-tight text-neutral-900 dark:text-neutral-100"
{...props}
/>
),
h2: (props) => (
<HeadingLink
as="h2"
className="mb-4 mt-12 text-lg font-semibold text-neutral-900 first:mt-0 dark:text-neutral-100"
{...props}
/>
),
h3: (props) => (
<HeadingLink
as="h3"
className="mb-3 mt-8 text-base font-semibold text-neutral-900 dark:text-neutral-100"
{...props}
/>
),
p: (props) => (
<p
className="mb-4 text-sm leading-relaxed text-neutral-600 dark:text-neutral-400"
{...props}
/>
),
ul: (props) => <ul className="mb-4 list-disc space-y-1 pl-5 text-sm" {...props} />,
ol: (props) => <ol className="mb-4 list-decimal space-y-1 pl-5 text-sm" {...props} />,
li: (props) => <li className="text-neutral-600 dark:text-neutral-400" {...props} />,
a: (props) => (
<a
className="text-neutral-900 underline decoration-neutral-300 underline-offset-2 hover:decoration-neutral-900 dark:text-neutral-100 dark:decoration-neutral-700 dark:hover:decoration-neutral-100"
{...props}
/>
),
code: ({ children, className }: { children?: React.ReactNode; className?: string }) => {
if (className) {
return <code className={className}>{children}</code>;
}
return (
<code className="rounded bg-neutral-100 px-1.5 py-0.5 text-[13px] dark:bg-neutral-800">
{children}
</code>
);
},
pre: async ({ children }: { children?: React.ReactNode }) => {
const codeElement = children as React.ReactElement<{
className?: string;
children?: string;
}>;
const className = codeElement?.props?.className || "";
const lang = className.replace("language-", "") || "typescript";
const code = codeElement?.props?.children || "";
return <Code lang={lang}>{typeof code === "string" ? code : String(code)}</Code>;
},
blockquote: (props) => (
<blockquote
className="mb-4 border-l-2 border-neutral-200 pl-4 text-sm text-neutral-500 dark:border-neutral-800 dark:text-neutral-500"
{...props}
/>
),
table: (props) => (
<div className="mb-4 overflow-x-auto">
<table className="w-full text-sm" {...props} />
</div>
),
th: (props) => (
<th
className="border-b border-neutral-200 px-3 py-2 text-left text-xs font-medium uppercase tracking-wider text-neutral-500 dark:border-neutral-800 dark:text-neutral-400"
{...props}
/>
),
td: (props) => (
<td
className="border-b border-neutral-100 px-3 py-2 text-neutral-600 dark:border-neutral-800/50 dark:text-neutral-400"
{...props}
/>
),
hr: () => <hr className="my-8 border-neutral-200 dark:border-neutral-800" />,
strong: (props) => (
<strong className="font-medium text-neutral-900 dark:text-neutral-100" {...props} />
),
...components,
};
}
+34
View File
@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"**/*.mdx",
"**/*.ts",
"**/*.tsx",
"next-env.d.ts",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}
+16
View File
@@ -0,0 +1,16 @@
.DS_Store
# Zig build output for example apps.
.zig-cache/
zig-out/
# Generated frontend dependencies and build output.
node_modules/
.next/
out/
dist/
next-env.d.ts
package-lock.json
# Local iOS build products.
Libraries/
+10
View File
@@ -0,0 +1,10 @@
# zero-native Examples
Use these examples as a progressive path through zero-native:
- `hello` is the smallest desktop shell with inline HTML.
- `webview` demonstrates bridge commands, built-in window APIs, security policy, automation, and optional CEF.
- `react`, `svelte`, `vue`, and `next` show framework projects with managed frontend assets and dev-server workflows.
- `ios` and `android` show how mobile hosts link the zero-native C ABI from `libzero-native.a`.
Start with `hello`, then move to `webview` when you need native commands or WebView policy, and use a framework example when building a real frontend.
+36
View File
@@ -0,0 +1,36 @@
# Android Example
A minimal Android host app that embeds a zero-native static library through JNI.
## Build the native library
Build or package an Android static library from the repository root, then copy it into this example:
```bash
zig build lib -Dtarget=aarch64-linux-android
mkdir -p examples/android/app/src/main/cpp/lib
cp zig-out/lib/libzero-native.a examples/android/app/src/main/cpp/lib/libzero-native.a
```
The CMake project expects the library at `app/src/main/cpp/lib/libzero-native.a` and the C header at `app/src/main/cpp/zero_native.h`.
## Run
Open `examples/android` in Android Studio, or build from the command line with a configured Android SDK:
```bash
./gradlew :app:assembleDebug
```
Install on an emulator or device:
```bash
./gradlew :app:installDebug
```
## Files
- `app/src/main/java/dev/zero_native/examples/android/MainActivity.kt` hosts a `SurfaceView` and calls the JNI bridge.
- `app/src/main/cpp/zero_native_jni.c` forwards JNI calls to the zero-native C ABI.
- `app/src/main/cpp/CMakeLists.txt` imports `libzero-native.a` and builds the JNI shared library.
- `app.zon` records the mobile example metadata for zero-native tooling.

Some files were not shown because too many files have changed in this diff Show More