init
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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`.
|
||||
@@ -0,0 +1,176 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -0,0 +1,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).
|
||||
@@ -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 },
|
||||
},
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,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(¬arize_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,
|
||||
};
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
.next/
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,22 @@
|
||||
# Docs Site Conventions
|
||||
|
||||
## MDX Tables
|
||||
|
||||
Always use HTML `<table>` syntax in MDX pages, never markdown pipe tables. This ensures consistent styling and avoids MDX parsing edge cases.
|
||||
|
||||
```html
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Column</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>field</code></td>
|
||||
<td>What it does</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
```
|
||||
@@ -0,0 +1,10 @@
|
||||
import createMDX from "@next/mdx";
|
||||
|
||||
const withMDX = createMDX();
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
pageExtensions: ["ts", "tsx", "md", "mdx"],
|
||||
};
|
||||
|
||||
export default withMDX(nextConfig);
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Generated
+3893
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
Binary file not shown.
Binary file not shown.
@@ -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" } });
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
```
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 <json></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.
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.*`.
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 <path> --frontend <next|vite|react|svelte|vue></code></td>
|
||||
<td>Scaffold a new zero-native project with the specified frontend</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>zero-native dev --binary <path></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 <command></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 <json></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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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/<bundle-id>/zero-native.jsonl</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Linux</td>
|
||||
<td><code>~/.local/state/<bundle-id>/logs/zero-native.jsonl</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Windows</td>
|
||||
<td><code>%LOCALAPPDATA%\<bundle-id>\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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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",
|
||||
});
|
||||
```
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
```
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 |
@@ -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;
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { getPageTitle, renderOgImage } from "./og-image";
|
||||
|
||||
export async function GET() {
|
||||
const title = getPageTitle("")!;
|
||||
return renderOgImage(title);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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).
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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",
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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><meta></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.
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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(),
|
||||
});
|
||||
```
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 => {},
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
```
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 },
|
||||
},
|
||||
```
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>⌘</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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/
|
||||
@@ -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.
|
||||
@@ -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
Reference in New Issue
Block a user