From dc7d3f48a30e6ae338af5ad7d474bdecedd7a9bf Mon Sep 17 00:00:00 2001 From: Mykhailo Smolinskyi Date: Tue, 12 May 2026 10:30:00 +0300 Subject: [PATCH 1/5] Add Angular frontend init template Expose angular in Frontend with flat dist/, ng serve on 4200, Angular 20 deps, minimal router scaffold, and a template test asserting paths. --- src/tooling/templates.zig | 378 +++++++++++++++++++++++++++++++++++++- 1 file changed, 372 insertions(+), 6 deletions(-) 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| { From e4af2608050c43a208f031ac4006e262f92a94f2 Mon Sep 17 00:00:00 2001 From: Mykhailo Smolinskyi Date: Tue, 12 May 2026 14:15:00 +0300 Subject: [PATCH 2/5] Register angular frontend in zero-native init CLI Document angular alongside other --frontend choices in usage and validation. --- .github/workflows/ci.yml | 2 +- tools/zero-native/main.zig | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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/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] From fbd750d6adc04ac82a4959923cbf56a2bd2570bf Mon Sep 17 00:00:00 2001 From: Mykhailo Smolinskyi Date: Tue, 12 May 2026 18:45:00 +0300 Subject: [PATCH 3/5] Add Angular desktop example Angular 20 SPA with flat frontend/dist builds, localhost:4200 dev URL, and minimal standalone routing mirrored from framework examples. Ignore frontend/.angular cache. --- examples/.gitignore | 1 + examples/angular/README.md | 33 ++ examples/angular/app.zon | 32 ++ examples/angular/assets/icon.icns | Bin 0 -> 109387 bytes examples/angular/build.zig | 449 ++++++++++++++++++ examples/angular/build.zig.zon | 8 + examples/angular/frontend/angular.json | 48 ++ examples/angular/frontend/package.json | 25 + .../angular/frontend/src/app/app.config.ts | 16 + .../angular/frontend/src/app/app.routes.ts | 7 + examples/angular/frontend/src/app/app.ts | 10 + .../angular/frontend/src/app/home/home.css | 0 .../angular/frontend/src/app/home/home.html | 9 + .../angular/frontend/src/app/home/home.ts | 11 + examples/angular/frontend/src/index.html | 12 + examples/angular/frontend/src/main.ts | 5 + examples/angular/frontend/src/styles.css | 52 ++ examples/angular/frontend/tsconfig.app.json | 9 + examples/angular/frontend/tsconfig.json | 25 + examples/angular/src/main.zig | 47 ++ examples/angular/src/runner.zig | 217 +++++++++ 21 files changed, 1016 insertions(+) create mode 100644 examples/angular/README.md create mode 100644 examples/angular/app.zon create mode 100644 examples/angular/assets/icon.icns create mode 100644 examples/angular/build.zig create mode 100644 examples/angular/build.zig.zon create mode 100644 examples/angular/frontend/angular.json create mode 100644 examples/angular/frontend/package.json create mode 100644 examples/angular/frontend/src/app/app.config.ts create mode 100644 examples/angular/frontend/src/app/app.routes.ts create mode 100644 examples/angular/frontend/src/app/app.ts create mode 100644 examples/angular/frontend/src/app/home/home.css create mode 100644 examples/angular/frontend/src/app/home/home.html create mode 100644 examples/angular/frontend/src/app/home/home.ts create mode 100644 examples/angular/frontend/src/index.html create mode 100644 examples/angular/frontend/src/main.ts create mode 100644 examples/angular/frontend/src/styles.css create mode 100644 examples/angular/frontend/tsconfig.app.json create mode 100644 examples/angular/frontend/tsconfig.json create mode 100644 examples/angular/src/main.zig create mode 100644 examples/angular/src/runner.zig 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/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 0000000000000000000000000000000000000000..bcc99be1d3349e32c9cfe5e4efc58ee555ea8a12 GIT binary patch literal 109387 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

=-)A@#>?LHT*d{ z#{aK&Ti+7>9ZqX%m!{^)uF!l{ZFwOBc;au(;W;W7tJU+Y9103DQ5F&-lbss zb&qgwdyc57h3X%A;JmtKpm>YT$(Ti7*LYe^dB4~?e@cKnI&%iulP z5BDuCm?rsf_*a?Qm*{IIK87V~a~}jXZ%&%}xdmtUG$Y9JqvEQF*T>F%X$ebt7^gX~ z!S8U}AGYTSe(I_2_-}_R>a6+_eVX>lhpX=Takz8w!aVhvwRN|XB&LWUF0d#(0ttPFZOwWGz~ zCGVuJQi#Uj;EoxTuI}=jr;nyb&SZ4XeV-`()W)dz!?jkqG;QVAyR)XRnd|f5P{Y~A zN(ti`b`RGYR|b_oKcB09B}Gv}sdC`T>Tl(jDfi~mwYr9zJiRUc%KYUJ=(;QaX_$gu zWzysJkqPxbKG-R=+kRY$Dt`9Js-38_cA@q@gEJO#*DFYm2GUK8yN|rBf3EgIwoATx zQPsczA)xj6#|*FM`*SW!ibjTHTH~DlKK4}n)dA10o+Q8U?ubmchNR{LUhYjZsGe=v zAqHQlVOH^ti{9;P?yGG)ySGz6>XrJQQlH%RXM_3#pPjqrO$(@nTXb^nAMB1w$5j+7 z%<0tc=%FbPt-tyk>xq|Z9iQcI`u$nlvX*GnzbtH`Y3pC+?b%eF>Bmp`mR^hOEmqJt zEtW@%)XU8=pP^azht<*{6X&&*BtsV|8JnR4CQdYsSK80lPW8MpOU-ZREQ-SZ?37R0 z)T7UxhtJH6dYhA7zqn^GP*y;z~a;M=k*+gt)~sp}c|CH5Hf z$9Pvg^O9H8o@0Byy`g|Hqr@yRV7`d=JT-5o_`q9z)4GIx-C&=`jC$b4ATZZ|cm?}! z4hz@SZNTA$2*M&0#GF*N`GkgT4hnQu@eK9$-3))(tAqyyJE;VD`T2N 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; +} From 99ad9695423ed9fc658e5e80b1f3c0c2e3c3900d Mon Sep 17 00:00:00 2001 From: Mykhailo Smolinskyi Date: Wed, 13 May 2026 09:20:00 +0300 Subject: [PATCH 4/5] Add test-example-angular build step Run examples/angular Zig tests alongside the other frontend frameworks. --- build.zig | 1 + 1 file changed, 1 insertion(+) 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", &.{ From de388994991c50cf61cc076dd6bd735f03ae4fb5 Mon Sep 17 00:00:00 2001 From: Mykhailo Smolinskyi Date: Wed, 13 May 2026 14:10:00 +0300 Subject: [PATCH 5/5] Document Angular frontend scaffolding List angular in CLI skills, docs, repo READMEs, and core skill references. --- README.md | 1 + docs/src/app/cli/page.mdx | 2 +- docs/src/app/quick-start/page.mdx | 2 +- examples/README.md | 2 +- packages/zero-native/README.md | 2 +- skill-data/core/SKILL.md | 9 +++++---- skill-data/core/references/frontend-assets.md | 1 + skill-data/core/references/project-anatomy.md | 2 +- 8 files changed, 12 insertions(+), 9 deletions(-) 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/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/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/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