diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index dc0ff3ea..c01b4113 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -107,7 +107,7 @@ jobs:
- name: Scaffold and test frontend templates
run: |
set -euo pipefail
- for frontend in next vite react svelte vue; do
+ for frontend in next vite react svelte vue angular; do
app=".zig-cache/scaffold-${frontend}"
rm -rf "$app"
./zig-out/bin/zero-native init "$app" --frontend "$frontend"
diff --git a/README.md b/README.md
index 9d715103..a2873ce7 100644
--- a/README.md
+++ b/README.md
@@ -107,6 +107,7 @@ Framework-specific starter examples live in `examples/`:
- `examples/react`
- `examples/svelte`
- `examples/vue`
+- `examples/angular`
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.
diff --git a/build.zig b/build.zig
index 705703f3..56d289b8 100644
--- a/build.zig
+++ b/build.zig
@@ -215,6 +215,7 @@ pub fn build(b: *std.Build) void {
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");
+ addExampleTestStep(b, frontend_examples_step, "test-example-angular", "Run Angular example tests", "examples/angular");
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", &.{
diff --git a/docs/src/app/cli/page.mdx b/docs/src/app/cli/page.mdx
index 588d00a4..92199843 100644
--- a/docs/src/app/cli/page.mdx
+++ b/docs/src/app/cli/page.mdx
@@ -13,7 +13,7 @@ The `zero-native` CLI provides project scaffolding, validation, packaging, and d
- zero-native init [path] --frontend <next|vite|react|svelte|vue> |
+ zero-native init [path] --frontend <next|vite|react|svelte|vue|angular> |
Scaffold a new zero-native project with the specified frontend. Omit path to scaffold into the current directory. |
diff --git a/docs/src/app/quick-start/page.mdx b/docs/src/app/quick-start/page.mdx
index cd9946f8..7bbd4fff 100644
--- a/docs/src/app/quick-start/page.mdx
+++ b/docs/src/app/quick-start/page.mdx
@@ -19,7 +19,7 @@ zero-native init my_app --frontend next
cd my_app
```
-Frontend options: `next`, `vite`, `react`, `svelte`, `vue`.
+Frontend options: `next`, `vite`, `react`, `svelte`, `vue`, `angular`.
This scaffolds a complete zero-native project:
diff --git a/examples/.gitignore b/examples/.gitignore
index 86268824..cebacf9b 100644
--- a/examples/.gitignore
+++ b/examples/.gitignore
@@ -11,6 +11,7 @@ out/
dist/
next-env.d.ts
package-lock.json
+.angular/
# Local iOS build products.
Libraries/
diff --git a/examples/README.md b/examples/README.md
index 88cffdef..543c5877 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -5,7 +5,7 @@ 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.
- `browser` is a vanilla no-build shell that uses layered WebViews for isolated page content on macOS and Linux system WebViews.
-- `react`, `svelte`, `vue`, and `next` show framework projects with managed frontend assets and dev-server workflows.
+- `react`, `svelte`, `vue`, `angular`, 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. Use `browser` when you want to see layered native WebViews, and use a framework example when building a real frontend.
diff --git a/examples/angular/README.md b/examples/angular/README.md
new file mode 100644
index 00000000..32fa6500
--- /dev/null
+++ b/examples/angular/README.md
@@ -0,0 +1,33 @@
+# Angular Example
+
+A super basic zero-native example using Angular for the frontend and Zig for the native shell.
+
+## Run
+
+```bash
+zig build run
+```
+
+The build installs frontend dependencies, builds the frontend, and opens the native WebView shell.
+
+## Dev Server
+
+```bash
+zig build dev
+```
+
+This starts the Angular dev server from `app.zon`, waits for `http://127.0.0.1:4200/`, and launches the native shell with `ZERO_NATIVE_FRONTEND_URL`.
+
+## Frontend
+
+- Frontend: `angular` (Angular 20)
+- Production assets: `frontend/dist`
+- Dev URL: `http://127.0.0.1:4200/`
+
+## Using Outside The Repo
+
+This example references zero-native via relative path (`../../`). To use it standalone, override the path:
+
+```bash
+zig build run -Dzero-native-path=/path/to/zero-native
+```
diff --git a/examples/angular/app.zon b/examples/angular/app.zon
new file mode 100644
index 00000000..c48e03cb
--- /dev/null
+++ b/examples/angular/app.zon
@@ -0,0 +1,32 @@
+.{
+ .id = "dev.zero_native.angular-example",
+ .name = "angular-example",
+ .display_name = "Angular Example",
+ .version = "0.1.0",
+ .icons = .{ "assets/icon.icns" },
+ .platforms = .{ "macos", "linux" },
+ .permissions = .{},
+ .capabilities = .{ "webview" },
+ .frontend = .{
+ .dist = "frontend/dist",
+ .entry = "index.html",
+ .spa_fallback = true,
+ .dev = .{
+ .url = "http://127.0.0.1:4200/",
+ .command = .{ "npm", "--prefix", "frontend", "run", "dev", "--", "--host", "127.0.0.1" },
+ .ready_path = "/",
+ .timeout_ms = 30000,
+ },
+ },
+ .security = .{
+ .navigation = .{
+ .allowed_origins = .{ "zero://app", "zero://inline", "http://127.0.0.1:4200" },
+ .external_links = .{ .action = "deny" },
+ },
+ },
+ .web_engine = "system",
+ .cef = .{ .dir = "third_party/cef/macos", .auto_install = false },
+ .windows = .{
+ .{ .label = "main", .title = "Angular Example", .width = 720, .height = 480, .restore_state = true },
+ },
+}
diff --git a/examples/angular/assets/icon.icns b/examples/angular/assets/icon.icns
new file mode 100644
index 00000000..bcc99be1
Binary files /dev/null and b/examples/angular/assets/icon.icns differ
diff --git a/examples/angular/build.zig b/examples/angular/build.zig
new file mode 100644
index 00000000..93f2cda6
--- /dev/null
+++ b/examples/angular/build.zig
@@ -0,0 +1,449 @@
+const std = @import("std");
+
+const PlatformOption = enum {
+ auto,
+ null,
+ macos,
+ linux,
+ windows,
+};
+
+const TraceOption = enum {
+ off,
+ events,
+ runtime,
+ all,
+};
+
+const WebEngineOption = enum {
+ system,
+ chromium,
+};
+
+const PackageTarget = enum {
+ macos,
+ windows,
+ linux,
+};
+
+const default_zero_native_path = "../..";
+const app_exe_name = "angular";
+
+pub fn build(b: *std.Build) void {
+ const target = zeroNativeTarget(b);
+ const optimize = b.standardOptimizeOption(.{});
+ const platform_option = b.option(PlatformOption, "platform", "Desktop backend: auto, null, macos, linux, windows") orelse .auto;
+ const trace_option = b.option(TraceOption, "trace", "Trace output: off, events, runtime, all") orelse .events;
+ const debug_overlay = b.option(bool, "debug-overlay", "Enable debug overlay output") orelse false;
+ const automation_enabled = b.option(bool, "automation", "Enable zero-native automation artifacts") orelse false;
+ const js_bridge_enabled = b.option(bool, "js-bridge", "Enable optional JavaScript bridge stubs") orelse false;
+ const web_engine_override = b.option(WebEngineOption, "web-engine", "Override app.zon web engine: system, chromium");
+ const cef_dir_override = b.option([]const u8, "cef-dir", "Override CEF root directory for Chromium builds");
+ const cef_auto_install_override = b.option(bool, "cef-auto-install", "Override app.zon CEF auto-install setting");
+ const package_target = b.option(PackageTarget, "package-target", "Package target: macos, windows, linux") orelse .macos;
+ const zero_native_path = b.option([]const u8, "zero-native-path", "Path to the zero-native framework checkout") orelse default_zero_native_path;
+ const optimize_name = @tagName(optimize);
+ const selected_platform: PlatformOption = switch (platform_option) {
+ .auto => if (target.result.os.tag == .macos) .macos else if (target.result.os.tag == .linux) .linux else if (target.result.os.tag == .windows) .windows else .null,
+ else => platform_option,
+ };
+ if (selected_platform == .macos and target.result.os.tag != .macos) {
+ @panic("-Dplatform=macos requires a macOS target");
+ }
+ if (selected_platform == .linux and target.result.os.tag != .linux) {
+ @panic("-Dplatform=linux requires a Linux target");
+ }
+ if (selected_platform == .windows and target.result.os.tag != .windows) {
+ @panic("-Dplatform=windows requires a Windows target");
+ }
+ const app_web_engine = appWebEngineConfig();
+ const web_engine = web_engine_override orelse app_web_engine.web_engine;
+ const cef_dir = cef_dir_override orelse defaultCefDir(selected_platform, app_web_engine.cef_dir);
+ const cef_auto_install = cef_auto_install_override orelse app_web_engine.cef_auto_install;
+ if (web_engine == .chromium and selected_platform == .null) {
+ @panic("-Dweb-engine=chromium requires -Dplatform=macos, linux, or windows");
+ }
+
+ const zero_native_mod = zeroNativeModule(b, target, optimize, zero_native_path);
+ const options = b.addOptions();
+ options.addOption([]const u8, "platform", switch (selected_platform) {
+ .auto => unreachable,
+ .null => "null",
+ .macos => "macos",
+ .linux => "linux",
+ .windows => "windows",
+ });
+ options.addOption([]const u8, "trace", @tagName(trace_option));
+ options.addOption([]const u8, "web_engine", @tagName(web_engine));
+ options.addOption(bool, "debug_overlay", debug_overlay);
+ options.addOption(bool, "automation", automation_enabled);
+ options.addOption(bool, "js_bridge", js_bridge_enabled);
+ const options_mod = options.createModule();
+
+ const runner_mod = localModule(b, target, optimize, "src/runner.zig");
+ runner_mod.addImport("zero-native", zero_native_mod);
+ runner_mod.addImport("build_options", options_mod);
+
+ const app_mod = localModule(b, target, optimize, "src/main.zig");
+ app_mod.addImport("zero-native", zero_native_mod);
+ app_mod.addImport("runner", runner_mod);
+ const exe = b.addExecutable(.{
+ .name = app_exe_name,
+ .root_module = app_mod,
+ });
+ linkPlatform(b, target, app_mod, exe, selected_platform, web_engine, zero_native_path, cef_dir, cef_auto_install);
+ b.installArtifact(exe);
+
+ const frontend_install = b.addSystemCommand(&.{ "npm", "install", "--prefix", "frontend" });
+ const frontend_install_step = b.step("frontend-install", "Install frontend dependencies");
+ frontend_install_step.dependOn(&frontend_install.step);
+
+ const frontend_build = b.addSystemCommand(&.{ "npm", "--prefix", "frontend", "run", "build" });
+ frontend_build.step.dependOn(&frontend_install.step);
+ const frontend_step = b.step("frontend-build", "Build the frontend");
+ frontend_step.dependOn(&frontend_build.step);
+
+ const run = b.addRunArtifact(exe);
+ run.step.dependOn(&frontend_build.step);
+ addCefRuntimeRunFiles(b, target, run, exe, web_engine, cef_dir);
+ const run_step = b.step("run", "Run the app");
+ run_step.dependOn(&run.step);
+
+ const dev = b.addSystemCommand(&.{ "zero-native", "dev", "--manifest", "app.zon", "--binary" });
+ dev.addFileArg(exe.getEmittedBin());
+ dev.step.dependOn(&exe.step);
+ dev.step.dependOn(&frontend_install.step);
+ const dev_step = b.step("dev", "Run the frontend dev server and native shell");
+ dev_step.dependOn(&dev.step);
+
+ const package = b.addSystemCommand(&.{
+ "zero-native",
+ "package",
+ "--target",
+ @tagName(package_target),
+ "--manifest",
+ "app.zon",
+ "--assets",
+ "frontend/dist",
+ "--optimize",
+ optimize_name,
+ "--output",
+ b.fmt("zig-out/package/{s}-0.1.0-{s}-{s}{s}", .{ app_exe_name, @tagName(package_target), optimize_name, packageSuffix(package_target) }),
+ "--binary",
+ });
+ package.addFileArg(exe.getEmittedBin());
+ package.addArgs(&.{ "--web-engine", @tagName(web_engine), "--cef-dir", cef_dir });
+ if (cef_auto_install) package.addArg("--cef-auto-install");
+ package.step.dependOn(&exe.step);
+ package.step.dependOn(&frontend_build.step);
+ const package_step = b.step("package", "Create a local package artifact");
+ package_step.dependOn(&package.step);
+
+ const tests = b.addTest(.{ .root_module = app_mod });
+ const test_step = b.step("test", "Run tests");
+ test_step.dependOn(&b.addRunArtifact(tests).step);
+}
+
+fn zeroNativeTarget(b: *std.Build) std.Build.ResolvedTarget {
+ const target = b.standardTargetOptions(.{});
+ if (target.result.os.tag != .macos) return target;
+
+ if (b.sysroot == null) {
+ b.sysroot = macosSdkPath(b) orelse b.sysroot;
+ }
+
+ var query = target.query;
+ query.os_tag = .macos;
+ query.os_version_min = .{ .semver = .{ .major = 11, .minor = 0, .patch = 0 } };
+ return b.resolveTargetQuery(query);
+}
+
+fn macosSdkPath(b: *std.Build) ?[]const u8 {
+ if (b.graph.environ_map.get("SDKROOT")) |sdkroot| {
+ if (sdkroot.len > 0) return sdkroot;
+ }
+
+ const result = std.process.run(b.allocator, b.graph.io, .{
+ .argv = &.{ "xcrun", "--sdk", "macosx", "--show-sdk-path" },
+ .stdout_limit = .limited(4096),
+ .stderr_limit = .limited(4096),
+ }) catch return null;
+ defer b.allocator.free(result.stderr);
+ if (result.term != .exited or result.term.exited != 0) {
+ b.allocator.free(result.stdout);
+ return null;
+ }
+ return std.mem.trimEnd(u8, result.stdout, "\r\n");
+}
+
+fn localModule(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, path: []const u8) *std.Build.Module {
+ return b.createModule(.{
+ .root_source_file = b.path(path),
+ .target = target,
+ .optimize = optimize,
+ });
+}
+
+fn zeroNativePath(b: *std.Build, zero_native_path: []const u8, sub_path: []const u8) std.Build.LazyPath {
+ return .{ .cwd_relative = b.pathJoin(&.{ zero_native_path, sub_path }) };
+}
+
+fn zeroNativeModule(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, zero_native_path: []const u8) *std.Build.Module {
+ const geometry_mod = externalModule(b, target, optimize, zero_native_path, "src/primitives/geometry/root.zig");
+ const assets_mod = externalModule(b, target, optimize, zero_native_path, "src/primitives/assets/root.zig");
+ const app_dirs_mod = externalModule(b, target, optimize, zero_native_path, "src/primitives/app_dirs/root.zig");
+ const trace_mod = externalModule(b, target, optimize, zero_native_path, "src/primitives/trace/root.zig");
+ const app_manifest_mod = externalModule(b, target, optimize, zero_native_path, "src/primitives/app_manifest/root.zig");
+ const diagnostics_mod = externalModule(b, target, optimize, zero_native_path, "src/primitives/diagnostics/root.zig");
+ const platform_info_mod = externalModule(b, target, optimize, zero_native_path, "src/primitives/platform_info/root.zig");
+ const json_mod = externalModule(b, target, optimize, zero_native_path, "src/primitives/json/root.zig");
+ const debug_mod = externalModule(b, target, optimize, zero_native_path, "src/debug/root.zig");
+ debug_mod.addImport("app_dirs", app_dirs_mod);
+ debug_mod.addImport("trace", trace_mod);
+
+ const zero_native_mod = externalModule(b, target, optimize, zero_native_path, "src/root.zig");
+ zero_native_mod.addImport("geometry", geometry_mod);
+ zero_native_mod.addImport("assets", assets_mod);
+ zero_native_mod.addImport("app_dirs", app_dirs_mod);
+ zero_native_mod.addImport("trace", trace_mod);
+ zero_native_mod.addImport("app_manifest", app_manifest_mod);
+ zero_native_mod.addImport("diagnostics", diagnostics_mod);
+ zero_native_mod.addImport("platform_info", platform_info_mod);
+ zero_native_mod.addImport("json", json_mod);
+ return zero_native_mod;
+}
+
+fn externalModule(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, zero_native_path: []const u8, path: []const u8) *std.Build.Module {
+ return b.createModule(.{
+ .root_source_file = zeroNativePath(b, zero_native_path, path),
+ .target = target,
+ .optimize = optimize,
+ });
+}
+
+fn linkPlatform(b: *std.Build, target: std.Build.ResolvedTarget, app_mod: *std.Build.Module, exe: *std.Build.Step.Compile, platform: PlatformOption, web_engine: WebEngineOption, zero_native_path: []const u8, cef_dir: []const u8, cef_auto_install: bool) void {
+ if (platform == .macos) {
+ switch (web_engine) {
+ .system => {
+ const sdk_include = if (b.sysroot) |sysroot| b.fmt("-I{s}/usr/include", .{sysroot}) else "";
+ const flags: []const []const u8 = if (b.sysroot) |sysroot| &.{ "-fobjc-arc", "-ObjC", "-mmacosx-version-min=11.0", "-isysroot", sysroot, sdk_include } else &.{ "-fobjc-arc", "-ObjC", "-mmacosx-version-min=11.0" };
+ app_mod.addCSourceFile(.{ .file = zeroNativePath(b, zero_native_path, "src/platform/macos/appkit_host.m"), .flags = flags });
+ app_mod.linkFramework("WebKit", .{});
+ },
+ .chromium => {
+ const cef_check = addCefCheck(b, target, cef_dir);
+ if (cef_auto_install) {
+ const cef_auto = b.addSystemCommand(&.{ "zero-native", "cef", "install", "--dir", cef_dir });
+ cef_check.step.dependOn(&cef_auto.step);
+ }
+ exe.step.dependOn(&cef_check.step);
+ const include_arg = b.fmt("-I{s}", .{cef_dir});
+ const define_arg = b.fmt("-DZERO_NATIVE_CEF_DIR=\"{s}\"", .{cef_dir});
+ const sdk_include = if (b.sysroot) |sysroot| b.fmt("-I{s}/usr/include", .{sysroot}) else "";
+ const flags: []const []const u8 = if (b.sysroot) |sysroot| &.{ "-fobjc-arc", "-ObjC++", "-std=c++17", "-stdlib=libc++", "-mmacosx-version-min=11.0", "-isysroot", sysroot, sdk_include, include_arg, define_arg } else &.{ "-fobjc-arc", "-ObjC++", "-std=c++17", "-stdlib=libc++", "-mmacosx-version-min=11.0", include_arg, define_arg };
+ app_mod.addCSourceFile(.{ .file = zeroNativePath(b, zero_native_path, "src/platform/macos/cef_host.mm"), .flags = flags });
+ app_mod.addObjectFile(b.path(b.fmt("{s}/libcef_dll_wrapper/libcef_dll_wrapper.a", .{cef_dir})));
+ app_mod.addFrameworkPath(b.path(b.fmt("{s}/Release", .{cef_dir})));
+ app_mod.linkFramework("Chromium Embedded Framework", .{});
+ app_mod.addRPath(.{ .cwd_relative = "@executable_path/Frameworks" });
+ },
+ }
+ if (b.sysroot) |sysroot| {
+ app_mod.addFrameworkPath(.{ .cwd_relative = b.pathJoin(&.{ sysroot, "System/Library/Frameworks" }) });
+ }
+ app_mod.linkFramework("AppKit", .{});
+ app_mod.linkFramework("Foundation", .{});
+ app_mod.linkFramework("UniformTypeIdentifiers", .{});
+ app_mod.linkSystemLibrary("c", .{});
+ if (web_engine == .chromium) app_mod.linkSystemLibrary("c++", .{});
+ } else if (platform == .linux) {
+ switch (web_engine) {
+ .system => {
+ app_mod.addCSourceFile(.{ .file = zeroNativePath(b, zero_native_path, "src/platform/linux/gtk_host.c"), .flags = &.{} });
+ app_mod.linkSystemLibrary("gtk4", .{});
+ app_mod.linkSystemLibrary("webkitgtk-6.0", .{});
+ },
+ .chromium => {
+ const cef_check = addCefCheck(b, target, cef_dir);
+ if (cef_auto_install) {
+ const cef_auto = b.addSystemCommand(&.{ "zero-native", "cef", "install", "--dir", cef_dir });
+ cef_check.step.dependOn(&cef_auto.step);
+ }
+ exe.step.dependOn(&cef_check.step);
+ const include_arg = b.fmt("-I{s}", .{cef_dir});
+ const define_arg = b.fmt("-DZERO_NATIVE_CEF_DIR=\"{s}\"", .{cef_dir});
+ app_mod.addCSourceFile(.{ .file = zeroNativePath(b, zero_native_path, "src/platform/linux/cef_host.cpp"), .flags = &.{ "-std=c++17", include_arg, define_arg } });
+ app_mod.addObjectFile(b.path(b.fmt("{s}/libcef_dll_wrapper/libcef_dll_wrapper.a", .{cef_dir})));
+ app_mod.addLibraryPath(b.path(b.fmt("{s}/Release", .{cef_dir})));
+ app_mod.linkSystemLibrary("cef", .{});
+ app_mod.addRPath(.{ .cwd_relative = "$ORIGIN" });
+ },
+ }
+ app_mod.linkSystemLibrary("c", .{});
+ if (web_engine == .chromium) app_mod.linkSystemLibrary("stdc++", .{});
+ } else if (platform == .windows) {
+ switch (web_engine) {
+ .system => app_mod.addCSourceFile(.{ .file = zeroNativePath(b, zero_native_path, "src/platform/windows/webview2_host.cpp"), .flags = &.{"-std=c++17"} }),
+ .chromium => {
+ const cef_check = addCefCheck(b, target, cef_dir);
+ if (cef_auto_install) {
+ const cef_auto = b.addSystemCommand(&.{ "zero-native", "cef", "install", "--dir", cef_dir });
+ cef_check.step.dependOn(&cef_auto.step);
+ }
+ exe.step.dependOn(&cef_check.step);
+ const include_arg = b.fmt("-I{s}", .{cef_dir});
+ const define_arg = b.fmt("-DZERO_NATIVE_CEF_DIR=\"{s}\"", .{cef_dir});
+ app_mod.addCSourceFile(.{ .file = zeroNativePath(b, zero_native_path, "src/platform/windows/cef_host.cpp"), .flags = &.{ "-std=c++17", include_arg, define_arg } });
+ app_mod.addObjectFile(b.path(b.fmt("{s}/libcef_dll_wrapper/libcef_dll_wrapper.lib", .{cef_dir})));
+ app_mod.addLibraryPath(b.path(b.fmt("{s}/Release", .{cef_dir})));
+ },
+ }
+ app_mod.linkSystemLibrary("c", .{});
+ app_mod.linkSystemLibrary("c++", .{});
+ app_mod.linkSystemLibrary("user32", .{});
+ app_mod.linkSystemLibrary("ole32", .{});
+ app_mod.linkSystemLibrary("shell32", .{});
+ if (web_engine == .chromium) app_mod.linkSystemLibrary("libcef", .{});
+ }
+}
+
+fn addCefRuntimeRunFiles(b: *std.Build, target: std.Build.ResolvedTarget, run: *std.Build.Step.Run, exe: *std.Build.Step.Compile, web_engine: WebEngineOption, cef_dir: []const u8) void {
+ if (web_engine != .chromium) return;
+ if (target.result.os.tag != .macos) return;
+ const copy = b.addSystemCommand(&.{
+ "sh", "-c",
+ b.fmt(
+ \\set -e
+ \\exe="$0"
+ \\exe_dir="$(dirname "$exe")"
+ \\rm -rf "zig-out/Frameworks/Chromium Embedded Framework.framework" "zig-out/bin/Frameworks/Chromium Embedded Framework.framework" ".zig-cache/o/Frameworks/Chromium Embedded Framework.framework" &&
+ \\mkdir -p "zig-out/Frameworks" "zig-out/bin/Frameworks" ".zig-cache/o/Frameworks" "$exe_dir" &&
+ \\cp -R "{s}/Release/Chromium Embedded Framework.framework" "zig-out/Frameworks/" &&
+ \\cp -R "{s}/Release/Chromium Embedded Framework.framework" "zig-out/bin/Frameworks/" &&
+ \\cp -R "{s}/Release/Chromium Embedded Framework.framework" ".zig-cache/o/Frameworks/" &&
+ \\cp "{s}/Release/Chromium Embedded Framework.framework/Libraries/libEGL.dylib" "$exe_dir/" &&
+ \\cp "{s}/Release/Chromium Embedded Framework.framework/Libraries/libGLESv2.dylib" "$exe_dir/" &&
+ \\cp "{s}/Release/Chromium Embedded Framework.framework/Libraries/libvk_swiftshader.dylib" "$exe_dir/" &&
+ \\cp "{s}/Release/Chromium Embedded Framework.framework/Libraries/vk_swiftshader_icd.json" "$exe_dir/"
+ , .{ cef_dir, cef_dir, cef_dir, cef_dir, cef_dir, cef_dir, cef_dir }),
+ });
+ copy.addFileArg(exe.getEmittedBin());
+ run.step.dependOn(©.step);
+}
+
+fn addCefCheck(b: *std.Build, target: std.Build.ResolvedTarget, cef_dir: []const u8) *std.Build.Step.Run {
+ const script = switch (target.result.os.tag) {
+ .macos => b.fmt(
+ \\test -f "{s}/include/cef_app.h" &&
+ \\test -d "{s}/Release/Chromium Embedded Framework.framework" &&
+ \\test -f "{s}/libcef_dll_wrapper/libcef_dll_wrapper.a" || {{
+ \\ echo "missing CEF dependency for -Dweb-engine=chromium" >&2
+ \\ echo "Expected:" >&2
+ \\ echo " {s}/include/cef_app.h" >&2
+ \\ echo " {s}/Release/Chromium Embedded Framework.framework" >&2
+ \\ echo " {s}/libcef_dll_wrapper/libcef_dll_wrapper.a" >&2
+ \\ echo "Fix with: zero-native cef install --dir {s}" >&2
+ \\ echo "Or rerun with: -Dcef-auto-install=true" >&2
+ \\ echo "Pass -Dcef-dir=/path/to/cef if your bundle lives elsewhere." >&2
+ \\ exit 1
+ \\}}
+ , .{ cef_dir, cef_dir, cef_dir, cef_dir, cef_dir, cef_dir, cef_dir }),
+ .linux => b.fmt(
+ \\test -f "{s}/include/cef_app.h" &&
+ \\test -f "{s}/Release/libcef.so" &&
+ \\test -f "{s}/libcef_dll_wrapper/libcef_dll_wrapper.a" || {{
+ \\ echo "missing CEF dependency for -Dweb-engine=chromium" >&2
+ \\ echo "Fix with: zero-native cef install --dir {s}" >&2
+ \\ exit 1
+ \\}}
+ , .{ cef_dir, cef_dir, cef_dir, cef_dir }),
+ .windows => b.fmt(
+ \\test -f "{s}/include/cef_app.h" &&
+ \\test -f "{s}/Release/libcef.dll" &&
+ \\test -f "{s}/libcef_dll_wrapper/libcef_dll_wrapper.lib" || {{
+ \\ echo "missing CEF dependency for -Dweb-engine=chromium" >&2
+ \\ echo "Fix with: zero-native cef install --dir {s}" >&2
+ \\ exit 1
+ \\}}
+ , .{ cef_dir, cef_dir, cef_dir, cef_dir }),
+ else => "echo unsupported CEF target >&2; exit 1",
+ };
+ return b.addSystemCommand(&.{ "sh", "-c", script });
+}
+
+fn packageSuffix(target: PackageTarget) []const u8 {
+ return switch (target) {
+ .macos => ".app",
+ .windows, .linux => "",
+ };
+}
+
+const AppWebEngineConfig = struct {
+ web_engine: WebEngineOption = .system,
+ cef_dir: []const u8 = "third_party/cef/macos",
+ cef_auto_install: bool = false,
+};
+
+fn defaultCefDir(platform: PlatformOption, configured: []const u8) []const u8 {
+ if (!std.mem.eql(u8, configured, "third_party/cef/macos")) return configured;
+ return switch (platform) {
+ .linux => "third_party/cef/linux",
+ .windows => "third_party/cef/windows",
+ else => configured,
+ };
+}
+
+fn appWebEngineConfig() AppWebEngineConfig {
+ const source = @embedFile("app.zon");
+ var config: AppWebEngineConfig = .{};
+ if (stringField(source, ".web_engine")) |value| {
+ config.web_engine = parseWebEngine(value) orelse .system;
+ }
+ if (objectSection(source, ".cef")) |cef| {
+ if (stringField(cef, ".dir")) |value| config.cef_dir = value;
+ if (boolField(cef, ".auto_install")) |value| config.cef_auto_install = value;
+ }
+ return config;
+}
+
+fn parseWebEngine(value: []const u8) ?WebEngineOption {
+ if (std.mem.eql(u8, value, "system")) return .system;
+ if (std.mem.eql(u8, value, "chromium")) return .chromium;
+ return null;
+}
+
+fn stringField(source: []const u8, field: []const u8) ?[]const u8 {
+ const field_index = std.mem.indexOf(u8, source, field) orelse return null;
+ const equals = std.mem.indexOfScalarPos(u8, source, field_index, '=') orelse return null;
+ const start_quote = std.mem.indexOfScalarPos(u8, source, equals, '"') orelse return null;
+ const end_quote = std.mem.indexOfScalarPos(u8, source, start_quote + 1, '"') orelse return null;
+ return source[start_quote + 1 .. end_quote];
+}
+
+fn objectSection(source: []const u8, field: []const u8) ?[]const u8 {
+ const field_index = std.mem.indexOf(u8, source, field) orelse return null;
+ const open = std.mem.indexOfScalarPos(u8, source, field_index, '{') orelse return null;
+ var depth: usize = 0;
+ var index = open;
+ while (index < source.len) : (index += 1) {
+ switch (source[index]) {
+ '{' => depth += 1,
+ '}' => {
+ depth -= 1;
+ if (depth == 0) return source[open + 1 .. index];
+ },
+ else => {},
+ }
+ }
+ return null;
+}
+
+fn boolField(source: []const u8, field: []const u8) ?bool {
+ const field_index = std.mem.indexOf(u8, source, field) orelse return null;
+ const equals = std.mem.indexOfScalarPos(u8, source, field_index, '=') orelse return null;
+ var index = equals + 1;
+ while (index < source.len and std.ascii.isWhitespace(source[index])) : (index += 1) {}
+ if (std.mem.startsWith(u8, source[index..], "true")) return true;
+ if (std.mem.startsWith(u8, source[index..], "false")) return false;
+ return null;
+}
diff --git a/examples/angular/build.zig.zon b/examples/angular/build.zig.zon
new file mode 100644
index 00000000..e5155d08
--- /dev/null
+++ b/examples/angular/build.zig.zon
@@ -0,0 +1,8 @@
+.{
+ .name = .angular,
+ .fingerprint = 0x768075445a707070,
+ .version = "0.1.0",
+ .minimum_zig_version = "0.16.0",
+ .dependencies = .{},
+ .paths = .{ "build.zig", "build.zig.zon", "src", "assets", "frontend", "app.zon", "README.md" },
+}
diff --git a/examples/angular/frontend/angular.json b/examples/angular/frontend/angular.json
new file mode 100644
index 00000000..7fe4b1f5
--- /dev/null
+++ b/examples/angular/frontend/angular.json
@@ -0,0 +1,48 @@
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "newProjectRoot": "projects",
+ "projects": {
+ "angular_example": {
+ "projectType": "application",
+ "root": "",
+ "sourceRoot": "src",
+ "prefix": "app",
+ "architect": {
+ "build": {
+ "builder": "@angular/build:application",
+ "options": {
+ "outputPath": { "base": "dist", "browser": "" },
+ "browser": "src/main.ts",
+ "tsConfig": "tsconfig.app.json",
+ "styles": ["src/styles.css"]
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ { "type": "initial", "maximumWarning": "500kB", "maximumError": "1MB" },
+ { "type": "anyComponentStyle", "maximumWarning": "4kB", "maximumError": "8kB" }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "optimization": false,
+ "extractLicenses": false,
+ "sourceMap": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "builder": "@angular/build:dev-server",
+ "options": { "host": "127.0.0.1" },
+ "configurations": {
+ "production": { "buildTarget": "angular_example:build:production" },
+ "development": { "buildTarget": "angular_example:build:development" }
+ },
+ "defaultConfiguration": "development"
+ }
+ }
+ }
+ }
+}
diff --git a/examples/angular/frontend/package.json b/examples/angular/frontend/package.json
new file mode 100644
index 00000000..9d04ac8b
--- /dev/null
+++ b/examples/angular/frontend/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "angular-example",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "ng serve",
+ "build": "ng build"
+ },
+ "dependencies": {
+ "@angular/common": "^20.3.0",
+ "@angular/compiler": "^20.3.0",
+ "@angular/core": "^20.3.0",
+ "@angular/forms": "^20.3.0",
+ "@angular/platform-browser": "^20.3.0",
+ "@angular/router": "^20.3.0",
+ "rxjs": "~7.8.0",
+ "tslib": "^2.3.0"
+ },
+ "devDependencies": {
+ "@angular/build": "^20.3.26",
+ "@angular/cli": "^20.3.26",
+ "@angular/compiler-cli": "^20.3.0",
+ "typescript": "~5.9.2"
+ }
+}
diff --git a/examples/angular/frontend/src/app/app.config.ts b/examples/angular/frontend/src/app/app.config.ts
new file mode 100644
index 00000000..69a1a93b
--- /dev/null
+++ b/examples/angular/frontend/src/app/app.config.ts
@@ -0,0 +1,16 @@
+import {
+ ApplicationConfig,
+ provideBrowserGlobalErrorListeners,
+ provideZonelessChangeDetection,
+} from '@angular/core';
+import { provideRouter } from '@angular/router';
+
+import { routes } from './app.routes';
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideBrowserGlobalErrorListeners(),
+ provideZonelessChangeDetection(),
+ provideRouter(routes),
+ ],
+};
diff --git a/examples/angular/frontend/src/app/app.routes.ts b/examples/angular/frontend/src/app/app.routes.ts
new file mode 100644
index 00000000..7e9e7798
--- /dev/null
+++ b/examples/angular/frontend/src/app/app.routes.ts
@@ -0,0 +1,7 @@
+import { Routes } from '@angular/router';
+
+import { Home } from './home/home';
+
+export const routes: Routes = [
+ { path: '', pathMatch: 'full', component: Home },
+];
diff --git a/examples/angular/frontend/src/app/app.ts b/examples/angular/frontend/src/app/app.ts
new file mode 100644
index 00000000..d0321f20
--- /dev/null
+++ b/examples/angular/frontend/src/app/app.ts
@@ -0,0 +1,10 @@
+import { ChangeDetectionStrategy, Component } from '@angular/core';
+import { RouterOutlet } from '@angular/router';
+
+@Component({
+ selector: 'app-root',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [RouterOutlet],
+ template: '',
+})
+export class App {}
diff --git a/examples/angular/frontend/src/app/home/home.css b/examples/angular/frontend/src/app/home/home.css
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/angular/frontend/src/app/home/home.html b/examples/angular/frontend/src/app/home/home.html
new file mode 100644
index 00000000..5144a3b0
--- /dev/null
+++ b/examples/angular/frontend/src/app/home/home.html
@@ -0,0 +1,9 @@
+
+ zero-native + Angular
+ Angular Example
+ An Angular frontend running inside the system WebView.
+
+ Native bridge
+ {{ bridge() }}
+
+
diff --git a/examples/angular/frontend/src/app/home/home.ts b/examples/angular/frontend/src/app/home/home.ts
new file mode 100644
index 00000000..86dfc5c7
--- /dev/null
+++ b/examples/angular/frontend/src/app/home/home.ts
@@ -0,0 +1,11 @@
+import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
+
+@Component({
+ selector: 'app-home',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ templateUrl: './home.html',
+ styleUrl: './home.css',
+})
+export class Home {
+ protected readonly bridge = signal('zero' in globalThis ? 'available' : 'not enabled');
+}
diff --git a/examples/angular/frontend/src/index.html b/examples/angular/frontend/src/index.html
new file mode 100644
index 00000000..7f3e66ff
--- /dev/null
+++ b/examples/angular/frontend/src/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+ Angular Example
+
+
+
+
+
+
+
diff --git a/examples/angular/frontend/src/main.ts b/examples/angular/frontend/src/main.ts
new file mode 100644
index 00000000..190f3418
--- /dev/null
+++ b/examples/angular/frontend/src/main.ts
@@ -0,0 +1,5 @@
+import { bootstrapApplication } from '@angular/platform-browser';
+import { appConfig } from './app/app.config';
+import { App } from './app/app';
+
+bootstrapApplication(App, appConfig).catch((err) => console.error(err));
diff --git a/examples/angular/frontend/src/styles.css b/examples/angular/frontend/src/styles.css
new file mode 100644
index 00000000..0f107fc5
--- /dev/null
+++ b/examples/angular/frontend/src/styles.css
@@ -0,0 +1,52 @@
+:root {
+ color: #0f172a;
+ background: #f8fafc;
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+}
+
+body {
+ min-width: 320px;
+ min-height: 100vh;
+ margin: 0;
+ display: grid;
+ place-items: center;
+}
+
+main {
+ width: min(560px, calc(100vw - 48px));
+ padding: 32px;
+ border-radius: 24px;
+ background: white;
+ box-shadow: 0 24px 60px rgba(15, 23, 42, 0.14);
+}
+
+h1 {
+ margin: 0 0 12px;
+ font-size: clamp(2rem, 8vw, 4rem);
+ line-height: 1;
+}
+
+.eyebrow {
+ margin: 0 0 12px;
+ color: #2563eb;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.lede {
+ margin: 0 0 24px;
+ color: #475569;
+ line-height: 1.6;
+}
+
+.card {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ padding: 16px;
+ border: 1px solid #e2e8f0;
+ border-radius: 16px;
+ background: #f8fafc;
+}
diff --git a/examples/angular/frontend/tsconfig.app.json b/examples/angular/frontend/tsconfig.app.json
new file mode 100644
index 00000000..8426ad95
--- /dev/null
+++ b/examples/angular/frontend/tsconfig.app.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/app",
+ "types": []
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": ["src/**/*.spec.ts"]
+}
diff --git a/examples/angular/frontend/tsconfig.json b/examples/angular/frontend/tsconfig.json
new file mode 100644
index 00000000..b1b0d297
--- /dev/null
+++ b/examples/angular/frontend/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "skipLibCheck": true,
+ "isolatedModules": true,
+ "experimentalDecorators": true,
+ "importHelpers": true,
+ "target": "ES2022",
+ "module": "preserve"
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "typeCheckHostBindings": true,
+ "strictTemplates": true
+ },
+ "files": [],
+ "references": [{ "path": "./tsconfig.app.json" }]
+}
diff --git a/examples/angular/src/main.zig b/examples/angular/src/main.zig
new file mode 100644
index 00000000..8482f70b
--- /dev/null
+++ b/examples/angular/src/main.zig
@@ -0,0 +1,47 @@
+const std = @import("std");
+const runner = @import("runner");
+const zero_native = @import("zero-native");
+
+pub const panic = std.debug.FullPanic(zero_native.debug.capturePanic);
+
+const App = struct {
+ env_map: *std.process.Environ.Map,
+
+ fn app(self: *@This()) zero_native.App {
+ return .{
+ .context = self,
+ .name = "angular-example",
+ .source = zero_native.frontend.productionSource(.{ .dist = "frontend/dist" }),
+ .source_fn = source,
+ };
+ }
+
+ fn source(context: *anyopaque) anyerror!zero_native.WebViewSource {
+ const self: *@This() = @ptrCast(@alignCast(context));
+ return zero_native.frontend.sourceFromEnv(self.env_map, .{
+ .dist = "frontend/dist",
+ .entry = "index.html",
+ });
+ }
+};
+
+const dev_origins = [_][]const u8{ "zero://app", "zero://inline", "http://127.0.0.1:4200" };
+
+pub fn main(init: std.process.Init) !void {
+ var app = App{ .env_map = init.environ_map };
+ try runner.runWithOptions(app.app(), .{
+ .app_name = "Angular Example",
+ .window_title = "Angular Example",
+ .bundle_id = "dev.zero_native.angular-example",
+ .icon_path = "assets/icon.icns",
+ .security = .{
+ .navigation = .{ .allowed_origins = &dev_origins },
+ },
+ }, init);
+}
+
+test "production source points at Angular build output" {
+ const source = zero_native.frontend.productionSource(.{ .dist = "frontend/dist" });
+ try std.testing.expectEqual(zero_native.WebViewSourceKind.assets, source.kind);
+ try std.testing.expectEqualStrings("frontend/dist", source.asset_options.?.root_path);
+}
diff --git a/examples/angular/src/runner.zig b/examples/angular/src/runner.zig
new file mode 100644
index 00000000..c2f9f033
--- /dev/null
+++ b/examples/angular/src/runner.zig
@@ -0,0 +1,217 @@
+const std = @import("std");
+const build_options = @import("build_options");
+const zero_native = @import("zero-native");
+
+pub const StdoutTraceSink = struct {
+ pub fn sink(self: *StdoutTraceSink) zero_native.trace.Sink {
+ return .{ .context = self, .write_fn = write };
+ }
+
+ fn write(context: *anyopaque, record: zero_native.trace.Record) zero_native.trace.WriteError!void {
+ _ = context;
+ if (!shouldTrace(record)) return;
+ var buffer: [1024]u8 = undefined;
+ var writer = std.Io.Writer.fixed(&buffer);
+ zero_native.trace.formatText(record, &writer) catch return error.OutOfSpace;
+ std.debug.print("{s}\n", .{writer.buffered()});
+ }
+};
+
+pub const RunOptions = struct {
+ app_name: []const u8,
+ window_title: []const u8 = "",
+ bundle_id: []const u8,
+ icon_path: []const u8 = "assets/icon.icns",
+ bridge: ?zero_native.BridgeDispatcher = null,
+ builtin_bridge: zero_native.BridgePolicy = .{},
+ security: zero_native.SecurityPolicy = .{},
+
+ fn appInfo(self: RunOptions) zero_native.AppInfo {
+ return .{
+ .app_name = self.app_name,
+ .window_title = self.window_title,
+ .bundle_id = self.bundle_id,
+ .icon_path = self.icon_path,
+ };
+ }
+};
+
+pub fn runWithOptions(app: zero_native.App, options: RunOptions, init: std.process.Init) !void {
+ if (build_options.debug_overlay) {
+ std.debug.print("debug-overlay=true backend={s} web-engine={s} trace={s}\n", .{ build_options.platform, build_options.web_engine, build_options.trace });
+ }
+ if (comptime std.mem.eql(u8, build_options.platform, "macos")) {
+ try runMacos(app, options, init);
+ } else if (comptime std.mem.eql(u8, build_options.platform, "linux")) {
+ try runLinux(app, options, init);
+ } else if (comptime std.mem.eql(u8, build_options.platform, "windows")) {
+ try runWindows(app, options, init);
+ } else {
+ try runNull(app, options, init);
+ }
+}
+
+fn runNull(app: zero_native.App, options: RunOptions, init: std.process.Init) !void {
+ var buffers: StateBuffers = undefined;
+ var app_info = options.appInfo();
+ const store = prepareStateStore(init.io, init.environ_map, &app_info, &buffers);
+ var null_platform = zero_native.NullPlatform.initWithOptions(.{}, webEngine(), app_info);
+ var trace_sink = StdoutTraceSink{};
+ var log_buffers: zero_native.debug.LogPathBuffers = .{};
+ const log_setup = zero_native.debug.setupLogging(init.io, init.environ_map, app_info.bundle_id, &log_buffers) catch null;
+ if (log_setup) |setup| zero_native.debug.installPanicCapture(init.io, setup.paths);
+ var file_trace_sink: zero_native.debug.FileTraceSink = undefined;
+ var fanout_sinks: [2]zero_native.trace.Sink = undefined;
+ var fanout_sink: zero_native.debug.FanoutTraceSink = undefined;
+ var runtime_trace_sink = trace_sink.sink();
+ if (log_setup) |setup| {
+ file_trace_sink = zero_native.debug.FileTraceSink.init(init.io, setup.paths.log_dir, setup.paths.log_file, setup.format);
+ fanout_sinks = .{ trace_sink.sink(), file_trace_sink.sink() };
+ fanout_sink = .{ .sinks = &fanout_sinks };
+ runtime_trace_sink = fanout_sink.sink();
+ }
+ var runtime = zero_native.Runtime.init(.{
+ .platform = null_platform.platform(),
+ .trace_sink = runtime_trace_sink,
+ .log_path = if (log_setup) |setup| setup.paths.log_file else null,
+ .bridge = options.bridge,
+ .builtin_bridge = options.builtin_bridge,
+ .security = options.security,
+ .automation = if (build_options.automation) zero_native.automation.Server.init(init.io, ".zig-cache/zero-native-automation", app_info.resolvedWindowTitle()) else null,
+ .window_state_store = store,
+ });
+
+ try runtime.run(app);
+}
+
+fn runMacos(app: zero_native.App, options: RunOptions, init: std.process.Init) !void {
+ var buffers: StateBuffers = undefined;
+ var app_info = options.appInfo();
+ const store = prepareStateStore(init.io, init.environ_map, &app_info, &buffers);
+ var mac_platform = try zero_native.platform.macos.MacPlatform.initWithOptions(zero_native.geometry.SizeF.init(720, 480), webEngine(), app_info);
+ defer mac_platform.deinit();
+ var trace_sink = StdoutTraceSink{};
+ var log_buffers: zero_native.debug.LogPathBuffers = .{};
+ const log_setup = zero_native.debug.setupLogging(init.io, init.environ_map, app_info.bundle_id, &log_buffers) catch null;
+ if (log_setup) |setup| zero_native.debug.installPanicCapture(init.io, setup.paths);
+ var file_trace_sink: zero_native.debug.FileTraceSink = undefined;
+ var fanout_sinks: [2]zero_native.trace.Sink = undefined;
+ var fanout_sink: zero_native.debug.FanoutTraceSink = undefined;
+ var runtime_trace_sink = trace_sink.sink();
+ if (log_setup) |setup| {
+ file_trace_sink = zero_native.debug.FileTraceSink.init(init.io, setup.paths.log_dir, setup.paths.log_file, setup.format);
+ fanout_sinks = .{ trace_sink.sink(), file_trace_sink.sink() };
+ fanout_sink = .{ .sinks = &fanout_sinks };
+ runtime_trace_sink = fanout_sink.sink();
+ }
+ var runtime = zero_native.Runtime.init(.{
+ .platform = mac_platform.platform(),
+ .trace_sink = runtime_trace_sink,
+ .log_path = if (log_setup) |setup| setup.paths.log_file else null,
+ .bridge = options.bridge,
+ .builtin_bridge = options.builtin_bridge,
+ .security = options.security,
+ .automation = if (build_options.automation) zero_native.automation.Server.init(init.io, ".zig-cache/zero-native-automation", app_info.resolvedWindowTitle()) else null,
+ .window_state_store = store,
+ });
+
+ try runtime.run(app);
+}
+
+fn runLinux(app: zero_native.App, options: RunOptions, init: std.process.Init) !void {
+ var buffers: StateBuffers = undefined;
+ var app_info = options.appInfo();
+ const store = prepareStateStore(init.io, init.environ_map, &app_info, &buffers);
+ var linux_platform = try zero_native.platform.linux.LinuxPlatform.initWithOptions(zero_native.geometry.SizeF.init(720, 480), webEngine(), app_info);
+ defer linux_platform.deinit();
+ var trace_sink = StdoutTraceSink{};
+ var log_buffers: zero_native.debug.LogPathBuffers = .{};
+ const log_setup = zero_native.debug.setupLogging(init.io, init.environ_map, app_info.bundle_id, &log_buffers) catch null;
+ if (log_setup) |setup| zero_native.debug.installPanicCapture(init.io, setup.paths);
+ var file_trace_sink: zero_native.debug.FileTraceSink = undefined;
+ var fanout_sinks: [2]zero_native.trace.Sink = undefined;
+ var fanout_sink: zero_native.debug.FanoutTraceSink = undefined;
+ var runtime_trace_sink = trace_sink.sink();
+ if (log_setup) |setup| {
+ file_trace_sink = zero_native.debug.FileTraceSink.init(init.io, setup.paths.log_dir, setup.paths.log_file, setup.format);
+ fanout_sinks = .{ trace_sink.sink(), file_trace_sink.sink() };
+ fanout_sink = .{ .sinks = &fanout_sinks };
+ runtime_trace_sink = fanout_sink.sink();
+ }
+ var runtime = zero_native.Runtime.init(.{
+ .platform = linux_platform.platform(),
+ .trace_sink = runtime_trace_sink,
+ .log_path = if (log_setup) |setup| setup.paths.log_file else null,
+ .bridge = options.bridge,
+ .builtin_bridge = options.builtin_bridge,
+ .security = options.security,
+ .automation = if (build_options.automation) zero_native.automation.Server.init(init.io, ".zig-cache/zero-native-automation", app_info.resolvedWindowTitle()) else null,
+ .window_state_store = store,
+ });
+
+ try runtime.run(app);
+}
+
+fn runWindows(app: zero_native.App, options: RunOptions, init: std.process.Init) !void {
+ var buffers: StateBuffers = undefined;
+ var app_info = options.appInfo();
+ const store = prepareStateStore(init.io, init.environ_map, &app_info, &buffers);
+ var windows_platform = try zero_native.platform.windows.WindowsPlatform.initWithOptions(zero_native.geometry.SizeF.init(720, 480), webEngine(), app_info);
+ defer windows_platform.deinit();
+ var trace_sink = StdoutTraceSink{};
+ var log_buffers: zero_native.debug.LogPathBuffers = .{};
+ const log_setup = zero_native.debug.setupLogging(init.io, init.environ_map, app_info.bundle_id, &log_buffers) catch null;
+ if (log_setup) |setup| zero_native.debug.installPanicCapture(init.io, setup.paths);
+ var file_trace_sink: zero_native.debug.FileTraceSink = undefined;
+ var fanout_sinks: [2]zero_native.trace.Sink = undefined;
+ var fanout_sink: zero_native.debug.FanoutTraceSink = undefined;
+ var runtime_trace_sink = trace_sink.sink();
+ if (log_setup) |setup| {
+ file_trace_sink = zero_native.debug.FileTraceSink.init(init.io, setup.paths.log_dir, setup.paths.log_file, setup.format);
+ fanout_sinks = .{ trace_sink.sink(), file_trace_sink.sink() };
+ fanout_sink = .{ .sinks = &fanout_sinks };
+ runtime_trace_sink = fanout_sink.sink();
+ }
+ var runtime = zero_native.Runtime.init(.{
+ .platform = windows_platform.platform(),
+ .trace_sink = runtime_trace_sink,
+ .log_path = if (log_setup) |setup| setup.paths.log_file else null,
+ .bridge = options.bridge,
+ .builtin_bridge = options.builtin_bridge,
+ .security = options.security,
+ .automation = if (build_options.automation) zero_native.automation.Server.init(init.io, ".zig-cache/zero-native-automation", app_info.resolvedWindowTitle()) else null,
+ .window_state_store = store,
+ });
+
+ try runtime.run(app);
+}
+
+fn shouldTrace(record: zero_native.trace.Record) bool {
+ if (comptime std.mem.eql(u8, build_options.trace, "off")) return false;
+ if (comptime std.mem.eql(u8, build_options.trace, "all")) return true;
+ if (comptime std.mem.eql(u8, build_options.trace, "events")) return true;
+ return std.mem.indexOf(u8, record.name, build_options.trace) != null;
+}
+
+fn webEngine() zero_native.WebEngine {
+ if (comptime std.mem.eql(u8, build_options.web_engine, "chromium")) return .chromium;
+ return .system;
+}
+
+const StateBuffers = struct {
+ state_dir: [1024]u8 = undefined,
+ file_path: [1200]u8 = undefined,
+ read: [8192]u8 = undefined,
+ restored_windows: [zero_native.platform.max_windows]zero_native.WindowOptions = undefined,
+};
+
+fn prepareStateStore(io: std.Io, env_map: *std.process.Environ.Map, app_info: *zero_native.AppInfo, buffers: *StateBuffers) ?zero_native.window_state.Store {
+ const paths = zero_native.window_state.defaultPaths(&buffers.state_dir, &buffers.file_path, app_info.bundle_id, zero_native.debug.envFromMap(env_map)) catch return null;
+ const store = zero_native.window_state.Store.init(io, paths.state_dir, paths.file_path);
+ if (app_info.main_window.restore_state) {
+ if (store.loadWindow(app_info.main_window.label, &buffers.read) catch null) |saved| {
+ app_info.main_window.default_frame = saved.frame;
+ }
+ }
+ return store;
+}
diff --git a/packages/zero-native/README.md b/packages/zero-native/README.md
index ac551bac..cfcf853c 100644
--- a/packages/zero-native/README.md
+++ b/packages/zero-native/README.md
@@ -22,7 +22,7 @@ The first run installs the generated frontend dependencies automatically.
| Command | Description |
|---------|-------------|
-| `zero-native init [name] --frontend ` | Scaffold a new zero-native project |
+| `zero-native init [name] --frontend ` | Scaffold a new zero-native project |
| `zero-native dev --binary ` | Start the app with a managed frontend dev server |
| `zero-native doctor` | Check host environment, WebView, manifest, and CEF |
| `zero-native validate` | Validate `app.zon` against the manifest schema |
diff --git a/skill-data/core/SKILL.md b/skill-data/core/SKILL.md
index c7cf021e..0c53c10c 100644
--- a/skill-data/core/SKILL.md
+++ b/skill-data/core/SKILL.md
@@ -25,7 +25,7 @@ These references are included by `zero-native skills get core --full`. Use them
- Project creation, generated files, build steps: `references/project-anatomy.md`
- `App`, `Runtime`, callbacks, embedding, tests: `references/app-model-runtime.md`
-- React/Vue/Svelte/Next/Vite, dev server, bundled assets: `references/frontend-assets.md`
+- React/Vue/Svelte/Angular/Next/Vite, dev server, bundled assets: `references/frontend-assets.md`
- App-defined bridge commands, builtin commands, permissions, windows, WebViews, dialogs: `references/bridge-security-native-capabilities.md`
- Web engine choice, CEF, packaging, signing, doctor, logs, debugging: `references/web-engines-packaging-debugging.md`
- Running-app inspection and smoke tests: `zero-native skills get automation`
@@ -41,7 +41,7 @@ cd my_app
zig build run
```
-Frontend choices are `next`, `vite`, `react`, `svelte`, and `vue`. The first `zig build run` installs frontend dependencies, builds the native shell, and opens a desktop window.
+Frontend choices are `next`, `vite`, `react`, `svelte`, `vue`, and `angular`. The first `zig build run` installs frontend dependencies, builds the native shell, and opens a desktop window.
## Workflow for existing apps
@@ -141,7 +141,7 @@ Use exact local origins for dev servers. Add `zero://inline` only for inline HTM
### Add a new framework app
-Use `zero-native init --frontend `. Then inspect the generated `app.zon`, `src/main.zig`, and `build.zig` before customizing. For framework behavior, keep frontend work in `frontend/` and use `sourceFromEnv` so development and packaged builds share one app shell.
+Use `zero-native init --frontend `. Then inspect the generated `app.zon`, `src/main.zig`, and `build.zig` before customizing. For framework behavior, keep frontend work in `frontend/` and use `sourceFromEnv` so development and packaged builds share one app shell.
### Add a native bridge command
@@ -186,7 +186,7 @@ Or run the CLI directly after building the binary:
zero-native dev --manifest app.zon --binary zig-out/bin/MyApp
```
-Vite usually uses `http://127.0.0.1:5173/`; Next.js usually uses `http://127.0.0.1:3000/`. The app WebView loads the dev URL directly, so framework HMR remains owned by Vite, Next.js, or the selected dev server.
+Vite usually uses `http://127.0.0.1:5173/`; Angular usually uses `http://127.0.0.1:4200/`; Next.js usually uses `http://127.0.0.1:3000/`. The app WebView loads the dev URL directly, so framework HMR remains owned by Vite, Next.js, Angular, or the selected dev server.
## Security defaults
@@ -232,6 +232,7 @@ When changing app behavior, add focused Zig tests when the code can run headless
- `examples/browser`: layered WebView/browser-style example.
- `examples/next`: Next.js with production assets.
- `examples/react`, `examples/svelte`, `examples/vue`: Vite frontend apps.
+- `examples/angular`: Angular 20 SPA.
- `examples/ios`, `examples/android`: mobile host embedding examples.
## When answering users
diff --git a/skill-data/core/references/frontend-assets.md b/skill-data/core/references/frontend-assets.md
index 5d7bd370..d408caab 100644
--- a/skill-data/core/references/frontend-assets.md
+++ b/skill-data/core/references/frontend-assets.md
@@ -120,5 +120,6 @@ For builtin windows/WebViews helpers, enable `js_window_api` and policy in the r
- `examples/react`: React with Vite.
- `examples/svelte`: Svelte with Vite.
- `examples/vue`: Vue with Vite.
+- `examples/angular`: Angular 20 SPA with `ng serve`/`ng build` and flat assets under `frontend/dist`.
When unsure about frontend build commands or output directories, inspect the matching example before editing.
diff --git a/skill-data/core/references/project-anatomy.md b/skill-data/core/references/project-anatomy.md
index 9a8c3867..4ac66066 100644
--- a/skill-data/core/references/project-anatomy.md
+++ b/skill-data/core/references/project-anatomy.md
@@ -10,7 +10,7 @@ Use this when creating, orienting in, or restructuring a zero-native app.
- `src/main.zig`: app state, `app()` method, source resolver, optional bridge dispatcher, lifecycle callbacks.
- `src/runner.zig`: platform and runtime setup: native backend, trace sinks, panic capture, log paths, state store, security policy, builtin bridge policy, automation.
- `assets/`: icons and other package resources.
-- `frontend/`: framework app when using Next, Vite, React, Svelte, or Vue.
+- `frontend/`: framework app when using Next, Vite, React, Svelte, Vue, or Angular.
## app.zon responsibilities
diff --git a/src/tooling/templates.zig b/src/tooling/templates.zig
index 5230ccaf..f441dd2c 100644
--- a/src/tooling/templates.zig
+++ b/src/tooling/templates.zig
@@ -8,6 +8,7 @@ pub const Frontend = enum {
react,
svelte,
vue,
+ angular,
pub fn parse(value: []const u8) ?Frontend {
if (std.mem.eql(u8, value, "next")) return .next;
@@ -15,13 +16,14 @@ pub const Frontend = enum {
if (std.mem.eql(u8, value, "react")) return .react;
if (std.mem.eql(u8, value, "svelte")) return .svelte;
if (std.mem.eql(u8, value, "vue")) return .vue;
+ if (std.mem.eql(u8, value, "angular")) return .angular;
return null;
}
pub fn distDir(self: Frontend) []const u8 {
return switch (self) {
.next => "frontend/out",
- .vite, .react, .svelte, .vue => "frontend/dist",
+ .vite, .react, .svelte, .vue, .angular => "frontend/dist",
};
}
@@ -29,6 +31,7 @@ pub const Frontend = enum {
return switch (self) {
.next => "3000",
.vite, .react, .svelte, .vue => "5173",
+ .angular => "4200",
};
}
@@ -36,6 +39,7 @@ pub const Frontend = enum {
return switch (self) {
.next => "http://127.0.0.1:3000/",
.vite, .react, .svelte, .vue => "http://127.0.0.1:5173/",
+ .angular => "http://127.0.0.1:4200/",
};
}
};
@@ -740,7 +744,7 @@ fn runnerZig() []const u8 {
\\ bridge: ?zero_native.BridgeDispatcher = null,
\\ builtin_bridge: zero_native.BridgePolicy = .{},
\\ security: zero_native.SecurityPolicy = .{},
- \\ js_window_api: bool = false,
+ \\ js_window_api: bool = false,
\\
\\ fn appInfo(self: RunOptions) zero_native.AppInfo {
\\ return .{
@@ -793,7 +797,7 @@ fn runnerZig() []const u8 {
\\ .bridge = options.bridge,
\\ .builtin_bridge = options.builtin_bridge,
\\ .security = options.security,
- \\ .js_window_api = options.js_window_api,
+ \\ .js_window_api = options.js_window_api,
\\ .automation = if (build_options.automation) zero_native.automation.Server.init(init.io, ".zig-cache/zero-native-automation", app_info.resolvedWindowTitle()) else null,
\\ .window_state_store = store,
\\ });
@@ -828,7 +832,7 @@ fn runnerZig() []const u8 {
\\ .bridge = options.bridge,
\\ .builtin_bridge = options.builtin_bridge,
\\ .security = options.security,
- \\ .js_window_api = options.js_window_api,
+ \\ .js_window_api = options.js_window_api,
\\ .automation = if (build_options.automation) zero_native.automation.Server.init(init.io, ".zig-cache/zero-native-automation", app_info.resolvedWindowTitle()) else null,
\\ .window_state_store = store,
\\ });
@@ -863,7 +867,7 @@ fn runnerZig() []const u8 {
\\ .bridge = options.bridge,
\\ .builtin_bridge = options.builtin_bridge,
\\ .security = options.security,
- \\ .js_window_api = options.js_window_api,
+ \\ .js_window_api = options.js_window_api,
\\ .automation = if (build_options.automation) zero_native.automation.Server.init(init.io, ".zig-cache/zero-native-automation", app_info.resolvedWindowTitle()) else null,
\\ .window_state_store = store,
\\ });
@@ -898,7 +902,7 @@ fn runnerZig() []const u8 {
\\ .bridge = options.bridge,
\\ .builtin_bridge = options.builtin_bridge,
\\ .security = options.security,
- \\ .js_window_api = options.js_window_api,
+ \\ .js_window_api = options.js_window_api,
\\ .automation = if (build_options.automation) zero_native.automation.Server.init(init.io, ".zig-cache/zero-native-automation", app_info.resolvedWindowTitle()) else null,
\\ .window_state_store = store,
\\ });
@@ -1026,9 +1030,319 @@ fn writeFrontendFiles(allocator: std.mem.Allocator, io: std.Io, app_dir: std.Io.
.react => try writeReactFrontend(allocator, io, app_dir, names),
.svelte => try writeSvelteFrontend(allocator, io, app_dir, names),
.vue => try writeVueFrontend(allocator, io, app_dir, names),
+ .angular => try writeAngularFrontend(allocator, io, app_dir, names),
}
}
+fn angularProjectKey(allocator: std.mem.Allocator, package_name: []const u8) ![]const u8 {
+ var out: std.ArrayList(u8) = .empty;
+ errdefer out.deinit(allocator);
+ if (package_name.len > 0 and !isAsciiAlpha(package_name[0])) {
+ try out.appendSlice(allocator, "app_");
+ }
+ for (package_name) |ch| {
+ const normalized = if (isAsciiAlpha(ch) or isAsciiDigit(ch)) ch else '_';
+ if (normalized == '_' and (out.items.len == 0 or out.items[out.items.len - 1] == '_')) continue;
+ try out.append(allocator, normalized);
+ }
+ while (out.items.len > 0 and out.items[out.items.len - 1] == '_') _ = out.pop();
+ if (out.items.len == 0) try out.appendSlice(allocator, "app");
+ return out.toOwnedSlice(allocator);
+}
+
+fn writeAngularFrontend(allocator: std.mem.Allocator, io: std.Io, app_dir: std.Io.Dir, names: TemplateNames) !void {
+ try app_dir.createDirPath(io, "frontend/src/app/home");
+ const project_key = try angularProjectKey(allocator, names.package_name);
+ defer allocator.free(project_key);
+
+ const angular_json = try angularAngularJson(allocator, project_key);
+ defer allocator.free(angular_json);
+
+ const package_json = try angularPackageJson(allocator, names);
+ defer allocator.free(package_json);
+
+ const index_html = try angularIndexHtml(allocator, names.display_name);
+ defer allocator.free(index_html);
+
+ const home_html = try angularHomeHtml(allocator, names.display_name);
+ defer allocator.free(home_html);
+
+ try writeFile(app_dir, io, "frontend/angular.json", angular_json);
+ try writeFile(app_dir, io, "frontend/package.json", package_json);
+ try writeFile(app_dir, io, "frontend/tsconfig.json", angularTsConfigJson());
+ try writeFile(app_dir, io, "frontend/tsconfig.app.json", angularTsConfigAppJson());
+ try writeFile(app_dir, io, "frontend/src/main.ts", angularMainTs());
+ try writeFile(app_dir, io, "frontend/src/index.html", index_html);
+ try writeFile(app_dir, io, "frontend/src/styles.css", frontendStylesCss());
+ try writeFile(app_dir, io, "frontend/src/app/app.config.ts", angularAppConfigTs());
+ try writeFile(app_dir, io, "frontend/src/app/app.routes.ts", angularAppRoutesTs());
+ try writeFile(app_dir, io, "frontend/src/app/app.ts", angularAppTs());
+ try writeFile(app_dir, io, "frontend/src/app/home/home.ts", angularHomeTs());
+ try writeFile(app_dir, io, "frontend/src/app/home/home.html", home_html);
+ try writeFile(app_dir, io, "frontend/src/app/home/home.css", "");
+}
+
+fn angularAngularJson(allocator: std.mem.Allocator, project_key: []const u8) ![]const u8 {
+ return std.fmt.allocPrint(allocator,
+ \\{{
+ \\ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ \\ "version": 1,
+ \\ "newProjectRoot": "projects",
+ \\ "projects": {{
+ \\ "{s}": {{
+ \\ "projectType": "application",
+ \\ "root": "",
+ \\ "sourceRoot": "src",
+ \\ "prefix": "app",
+ \\ "architect": {{
+ \\ "build": {{
+ \\ "builder": "@angular/build:application",
+ \\ "options": {{
+ \\ "outputPath": {{ "base": "dist", "browser": "" }},
+ \\ "browser": "src/main.ts",
+ \\ "tsConfig": "tsconfig.app.json",
+ \\ "styles": ["src/styles.css"]
+ \\ }},
+ \\ "configurations": {{
+ \\ "production": {{
+ \\ "budgets": [
+ \\ {{ "type": "initial", "maximumWarning": "500kB", "maximumError": "1MB" }},
+ \\ {{ "type": "anyComponentStyle", "maximumWarning": "4kB", "maximumError": "8kB" }}
+ \\ ],
+ \\ "outputHashing": "all"
+ \\ }},
+ \\ "development": {{
+ \\ "optimization": false,
+ \\ "extractLicenses": false,
+ \\ "sourceMap": true
+ \\ }}
+ \\ }},
+ \\ "defaultConfiguration": "production"
+ \\ }},
+ \\ "serve": {{
+ \\ "builder": "@angular/build:dev-server",
+ \\ "options": {{ "host": "127.0.0.1" }},
+ \\ "configurations": {{
+ \\ "production": {{ "buildTarget": "{s}:build:production" }},
+ \\ "development": {{ "buildTarget": "{s}:build:development" }}
+ \\ }},
+ \\ "defaultConfiguration": "development"
+ \\ }}
+ \\ }}
+ \\ }}
+ \\ }}
+ \\}}
+ \\
+ , .{ project_key, project_key, project_key });
+}
+
+fn angularPackageJson(allocator: std.mem.Allocator, names: TemplateNames) ![]const u8 {
+ var out: std.ArrayList(u8) = .empty;
+ errdefer out.deinit(allocator);
+ try out.appendSlice(allocator, "{\n \"name\": ");
+ try appendJsonString(&out, allocator, names.package_name);
+ try out.appendSlice(allocator,
+ \\,
+ \\ "version": "0.1.0",
+ \\ "private": true,
+ \\ "scripts": {
+ \\ "dev": "ng serve",
+ \\ "build": "ng build"
+ \\ },
+ \\ "dependencies": {
+ \\ "@angular/common": "^20.3.0",
+ \\ "@angular/compiler": "^20.3.0",
+ \\ "@angular/core": "^20.3.0",
+ \\ "@angular/forms": "^20.3.0",
+ \\ "@angular/platform-browser": "^20.3.0",
+ \\ "@angular/router": "^20.3.0",
+ \\ "rxjs": "~7.8.0",
+ \\ "tslib": "^2.3.0"
+ \\ },
+ \\ "devDependencies": {
+ \\ "@angular/build": "^20.3.26",
+ \\ "@angular/cli": "^20.3.26",
+ \\ "@angular/compiler-cli": "^20.3.0",
+ \\ "typescript": "~5.9.2"
+ \\ }
+ \\}
+ \\
+ );
+ return out.toOwnedSlice(allocator);
+}
+
+fn angularTsConfigJson() []const u8 {
+ return
+ \\{
+ \\ "compileOnSave": false,
+ \\ "compilerOptions": {
+ \\ "strict": true,
+ \\ "noImplicitOverride": true,
+ \\ "noPropertyAccessFromIndexSignature": true,
+ \\ "noImplicitReturns": true,
+ \\ "noFallthroughCasesInSwitch": true,
+ \\ "skipLibCheck": true,
+ \\ "isolatedModules": true,
+ \\ "experimentalDecorators": true,
+ \\ "importHelpers": true,
+ \\ "target": "ES2022",
+ \\ "module": "preserve"
+ \\ },
+ \\ "angularCompilerOptions": {
+ \\ "enableI18nLegacyMessageIdFormat": false,
+ \\ "strictInjectionParameters": true,
+ \\ "strictInputAccessModifiers": true,
+ \\ "typeCheckHostBindings": true,
+ \\ "strictTemplates": true
+ \\ },
+ \\ "files": [],
+ \\ "references": [{ "path": "./tsconfig.app.json" }]
+ \\}
+ \\
+ ;
+}
+
+fn angularTsConfigAppJson() []const u8 {
+ return
+ \\{
+ \\ "extends": "./tsconfig.json",
+ \\ "compilerOptions": {
+ \\ "outDir": "./out-tsc/app",
+ \\ "types": []
+ \\ },
+ \\ "include": ["src/**/*.ts"],
+ \\ "exclude": ["src/**/*.spec.ts"]
+ \\}
+ \\
+ ;
+}
+
+fn angularMainTs() []const u8 {
+ return
+ \\import { bootstrapApplication } from '@angular/platform-browser';
+ \\import { appConfig } from './app/app.config';
+ \\import { App } from './app/app';
+ \\
+ \\bootstrapApplication(App, appConfig).catch((err) => console.error(err));
+ \\
+ ;
+}
+
+fn angularAppConfigTs() []const u8 {
+ return
+ \\import {
+ \\ ApplicationConfig,
+ \\ provideBrowserGlobalErrorListeners,
+ \\ provideZonelessChangeDetection,
+ \\} from '@angular/core';
+ \\import { provideRouter } from '@angular/router';
+ \\
+ \\import { routes } from './app.routes';
+ \\
+ \\export const appConfig: ApplicationConfig = {
+ \\ providers: [
+ \\ provideBrowserGlobalErrorListeners(),
+ \\ provideZonelessChangeDetection(),
+ \\ provideRouter(routes),
+ \\ ],
+ \\};
+ \\
+ ;
+}
+
+fn angularAppRoutesTs() []const u8 {
+ return
+ \\import { Routes } from '@angular/router';
+ \\
+ \\import { Home } from './home/home';
+ \\
+ \\export const routes: Routes = [
+ \\ { path: '', pathMatch: 'full', component: Home },
+ \\];
+ \\
+ ;
+}
+
+fn angularAppTs() []const u8 {
+ return
+ \\import { ChangeDetectionStrategy, Component } from '@angular/core';
+ \\import { RouterOutlet } from '@angular/router';
+ \\
+ \\@Component({
+ \\ selector: 'app-root',
+ \\ changeDetection: ChangeDetectionStrategy.OnPush,
+ \\ imports: [RouterOutlet],
+ \\ template: '',
+ \\})
+ \\export class App {}
+ \\
+ ;
+}
+
+fn angularHomeTs() []const u8 {
+ return
+ \\import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
+ \\
+ \\@Component({
+ \\ selector: 'app-home',
+ \\ changeDetection: ChangeDetectionStrategy.OnPush,
+ \\ templateUrl: './home.html',
+ \\ styleUrl: './home.css',
+ \\})
+ \\export class Home {
+ \\ protected readonly bridge = signal('zero' in globalThis ? 'available' : 'not enabled');
+ \\}
+ \\
+ ;
+}
+
+fn angularIndexHtml(allocator: std.mem.Allocator, display_name: []const u8) ![]const u8 {
+ var out: std.ArrayList(u8) = .empty;
+ errdefer out.deinit(allocator);
+ try out.appendSlice(allocator,
+ \\
+ \\
+ \\
+ \\
+ \\
+ );
+ try appendXmlText(&out, allocator, display_name);
+ try out.appendSlice(allocator,
+ \\
+ \\
+ \\
+ \\
+ \\
+ \\
+ \\
+ \\
+ \\
+ );
+ return out.toOwnedSlice(allocator);
+}
+
+fn angularHomeHtml(allocator: std.mem.Allocator, display_name: []const u8) ![]const u8 {
+ var out: std.ArrayList(u8) = .empty;
+ errdefer out.deinit(allocator);
+ try out.appendSlice(allocator,
+ \\
+ \\ zero-native + Angular
+ \\
+ );
+ try appendXmlText(&out, allocator, display_name);
+ try out.appendSlice(allocator,
+ \\
+ \\ An Angular frontend running inside the system WebView.
+ \\
+ \\ Native bridge
+ \\ {{ bridge() }}
+ \\
+ \\
+ \\
+ );
+ return out.toOwnedSlice(allocator);
+}
+
fn writeNextFrontend(allocator: std.mem.Allocator, io: std.Io, app_dir: std.Io.Dir, names: TemplateNames) !void {
try app_dir.createDirPath(io, "frontend/app");
const package_json = try nextPackageJson(allocator, names);
@@ -1873,6 +2187,45 @@ test "writeDefaultApp emits frontend-specific Next paths" {
try std.testing.expect(std.mem.indexOf(u8, tsconfig_text, "\"@/*\": [\"./app/*\"]") != null);
}
+test "writeDefaultApp emits frontend-specific Angular paths" {
+ const destination = ".zig-cache/test-angular-init-template";
+ try writeDefaultApp(std.testing.allocator, std.testing.io, destination, .{ .app_name = "Angular App", .framework_path = ".", .frontend = .angular });
+
+ const app_zon_text = try readTestFile(std.testing.allocator, std.testing.io, destination, "app.zon");
+ defer std.testing.allocator.free(app_zon_text);
+ const angular_json_text = try readTestFile(std.testing.allocator, std.testing.io, destination, "frontend/angular.json");
+ defer std.testing.allocator.free(angular_json_text);
+ const main_zig_text = try readTestFile(std.testing.allocator, std.testing.io, destination, "src/main.zig");
+ defer std.testing.allocator.free(main_zig_text);
+ const package_json_text = try readTestFile(std.testing.allocator, std.testing.io, destination, "frontend/package.json");
+ defer std.testing.allocator.free(package_json_text);
+ const app_ts_text = try readTestFile(std.testing.allocator, std.testing.io, destination, "frontend/src/app/app.ts");
+ defer std.testing.allocator.free(app_ts_text);
+ const home_html_text = try readTestFile(std.testing.allocator, std.testing.io, destination, "frontend/src/app/home/home.html");
+ defer std.testing.allocator.free(home_html_text);
+
+ try std.testing.expect(std.mem.indexOf(u8, app_zon_text, "frontend/dist") != null);
+ try std.testing.expect(std.mem.indexOf(u8, app_zon_text, "127.0.0.1:4200") != null);
+ try std.testing.expect(std.mem.indexOf(u8, main_zig_text, "frontend/dist") != null);
+ try std.testing.expect(std.mem.indexOf(u8, main_zig_text, "127.0.0.1:4200") != null);
+ try std.testing.expect(std.mem.indexOf(u8, angular_json_text, "\"base\": \"dist\"") != null);
+ try std.testing.expect(std.mem.indexOf(u8, angular_json_text, "\"browser\": \"\"") != null);
+ try std.testing.expect(std.mem.indexOf(u8, angular_json_text, "\"host\": \"127.0.0.1\"") != null);
+ try std.testing.expect(std.mem.indexOf(u8, angular_json_text, "zone.js") == null);
+ try std.testing.expect(std.mem.indexOf(u8, app_ts_text, "ChangeDetectionStrategy.OnPush") != null);
+ try std.testing.expect(std.mem.indexOf(u8, app_ts_text, "standalone: true") == null);
+ try std.testing.expect(std.mem.indexOf(u8, home_html_text, "role=\"status\" aria-live=\"polite\"") != null);
+ try std.testing.expect(std.mem.indexOf(u8, package_json_text, "\"@angular/core\"") != null);
+ try std.testing.expect(std.mem.indexOf(u8, package_json_text, "\"zone.js\"") == null);
+}
+
+test "angularProjectKey emits Angular-safe target names" {
+ const key = try angularProjectKey(std.testing.allocator, "123 bad.name/@scope");
+ defer std.testing.allocator.free(key);
+
+ try std.testing.expectEqualStrings("app_123_bad_name_scope", key);
+}
+
fn normalizePackageName(allocator: std.mem.Allocator, value: []const u8) ![]const u8 {
var out: std.ArrayList(u8) = .empty;
errdefer out.deinit(allocator);
@@ -1982,6 +2335,19 @@ fn appendJsonString(out: *std.ArrayList(u8), allocator: std.mem.Allocator, value
try appendEscapedString(out, allocator, value);
}
+fn appendXmlText(out: *std.ArrayList(u8), allocator: std.mem.Allocator, value: []const u8) !void {
+ for (value) |ch| {
+ switch (ch) {
+ '&' => try out.appendSlice(allocator, "&"),
+ '<' => try out.appendSlice(allocator, "<"),
+ '>' => try out.appendSlice(allocator, ">"),
+ '"' => try out.appendSlice(allocator, """),
+ '\'' => try out.appendSlice(allocator, "'"),
+ else => try out.append(allocator, ch),
+ }
+ }
+}
+
fn appendEscapedString(out: *std.ArrayList(u8), allocator: std.mem.Allocator, value: []const u8) !void {
try out.append(allocator, '"');
for (value) |ch| {
diff --git a/tools/zero-native/main.zig b/tools/zero-native/main.zig
index ef7c9462..ad3bba35 100644
--- a/tools/zero-native/main.zig
+++ b/tools/zero-native/main.zig
@@ -17,8 +17,8 @@ pub fn main(init: std.process.Init) !void {
std.debug.print("zero-native {s}\n", .{version});
} else if (std.mem.eql(u8, command, "init")) {
const destination = positionalArg(args[2..]) orelse ".";
- const frontend_str = flagValue(args, "--frontend") catch fail("--frontend requires a value: next, vite, react, svelte, vue") orelse fail("--frontend is required: next, vite, react, svelte, vue");
- const frontend = tooling.templates.Frontend.parse(frontend_str) orelse fail("invalid --frontend value: use next, vite, react, svelte, or vue");
+ const frontend_str = flagValue(args, "--frontend") catch fail("--frontend requires a value: next, vite, react, svelte, vue, angular") orelse fail("--frontend is required: next, vite, react, svelte, vue, angular");
+ const frontend = tooling.templates.Frontend.parse(frontend_str) orelse fail("invalid --frontend value: use next, vite, react, svelte, vue, or angular");
const app_name, const free_app_name = try initAppName(allocator, init.io, destination);
defer if (free_app_name) allocator.free(app_name);
const framework_path, const free_framework_path = try initFrameworkPath(allocator, init.io);
@@ -146,7 +146,7 @@ fn usage() void {
\\usage: zero-native
\\
\\commands:
- \\ init [path] --frontend
+ \\ init [path] --frontend
\\ cef install|path|doctor [--dir path] [--version version] [--source prepared|official] [--force]
\\ doctor [--strict] [--manifest app.zon] [--web-engine system|chromium] [--cef-dir path] [--cef-auto-install]
\\ validate [app.zon]