diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4556d6901..09c142e6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: version: 0.16.0 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 # TypeScript cores compile through the external core compiler at # build/test time; the compiler and the frontend's toolchain both # arrive with this one install (without it the ts-core suites skip @@ -44,7 +44,7 @@ jobs: version: 0.16.0 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 # The exact-pinned external core compiler and the frontend's # toolchain, one install (packages/core/package.json is the one # place the pin lives). @@ -65,7 +65,7 @@ jobs: version: 0.16.0 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 # gpu-components is a TypeScript-core app, so its smoke build needs # the frontend compiler and exact-pinned TypeScript toolchain. - run: npm ci --prefix packages/core @@ -74,6 +74,9 @@ jobs: - run: zig build test-example-mobile-canvas-lib-ios-store - run: zig build test-webview-system-link - run: zig build test-webview-smoke + # The zero-config TypeScript runner must load app manifest menus before + # automation can select their registered command ids. + - run: zig build test-menu-bar-smoke # Signed-package seal pin: an ad-hoc signed package must pass # codesign --verify --strict (macOS runners are the only tier with # codesign; the step skips loudly anywhere else). @@ -181,7 +184,7 @@ jobs: version: 0.16.0 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 # The TypeScript core suites compile through the external core # compiler at build/test time; without the install they skip # silently, so CI must provide it. @@ -221,7 +224,7 @@ jobs: version: 0.16.0 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 # The TypeScript examples compile through the external core # compiler at build time; the compiler and the frontend toolchain # arrive with this install. @@ -355,7 +358,7 @@ jobs: version: 0.16.0 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 # The scaffold default is the TypeScript core; its frontend and # compiler run at build time from this checkout's packages/core. - run: npm ci --prefix packages/core @@ -460,7 +463,7 @@ jobs: version: 0.16.0 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 # The default scaffold is a TypeScript core: its build runs the # @native-sdk/core frontend and the external core compiler from # this checkout's own install. @@ -495,7 +498,7 @@ jobs: app=".zig-cache/scaffold-${frontend}" rm -rf "$app" ./zig-out/bin/native init "$app" --frontend "$frontend" --full - (cd "$app" && zig build test -Dplatform=null && ../../zig-out/bin/native validate app.zon) + (cd "$app" && zig build test -Dplatform=null && ../../zig-out/bin/native validate app.json) # Every scaffold ships a CI workflow; parse it as real YAML. test -s "$app/.github/workflows/ci.yml" python3 -c 'import sys, yaml; yaml.safe_load(open(sys.argv[1]))' "$app/.github/workflows/ci.yml" diff --git a/AGENTS.md b/AGENTS.md index 2b79e5e6b..f4a9e4e9e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ Guidance for agents (and humans) working on this repository. Native SDK itself is implemented in Zig, but Native SDK **apps are authored in TypeScript + Native markup by default**. Do not infer the app-authoring language from this repository's implementation language or from older Zig-core examples. -- For a new app, use `native init ` and expect `src/core.ts`, `src/app.native`, and `app.zon`. Ordinary compiled TypeScript work that needs filesystem, process, JSON, regex, classes, or other static-tier APIs belongs under optional `src/services/`, reached from the core with `Cmd.request`; do not import a service from the core. Do not add Zig app code unless the user explicitly chooses `--template zig-core` or the feature requires a toolkit extension. +- For a new app, use `native init ` and expect `src/core.ts`, `src/app.native`, and `app.json`. `app.zon` remains a supported legacy/alternative manifest. Ordinary compiled TypeScript work that needs filesystem, process, JSON, regex, classes, or other static-tier APIs belongs under optional `src/services/`, reached from the core with `Cmd.request`; do not import a service from the core. Do not add Zig app code unless the user explicitly chooses `--template zig-core` or the feature requires a toolkit extension. - Before changing an existing app, inspect its tree. A `src/core.ts` app stays TypeScript; a `src/main.zig` app stays Zig unless the task is specifically a migration. - For default app work, read `skill-data/native-ui/SKILL.md` and `skill-data/ts-core/SKILL.md`; also read `skill-data/ts-services/SKILL.md` when the tree has `src/services/` or the task needs ordinary TypeScript beyond the core subset. `skill-data/core/SKILL.md` covers shared/runtime concerns; `skill-data/zig/SKILL.md` is for Zig-core apps and SDK implementation work. - The `-ts` suffix on a few examples only distinguishes ports from older Zig originals. New TypeScript apps need no suffix because TypeScript is the default. @@ -15,7 +15,7 @@ Native SDK itself is implemented in Zig, but Native SDK **apps are authored in T ```bash zig build test # root engine + runtime suites -zig build validate # sample app.zon manifest check +zig build validate # framework's legacy app.zon manifest check zig build test-example- # one example's suite (e.g. test-example-notes) scripts/gate.sh fast [ref] # affected-only local gate for your diff (default base: main) scripts/gate.sh full # everything CI-shaped that runs locally @@ -34,7 +34,8 @@ Do not edit `CHANGELOG.md` as part of regular feature or fix work. The release a ## Where things live - `src/` — the engine and runtime; `src/primitives/canvas/` holds the widget, markup, and vector core. -- `examples/` — the showcase apps, most zero-config (`app.zon` + `src/`). +- `apps/schema/` — the standalone static Vercel project for `schema.native-sdk.dev`. +- `examples/` — the showcase apps, many predating the JSON default (`app.zon` + `src/`). - `docs/` — the documentation site; `docs/AGENTS.md` has its MDX conventions. - `skills/` and `skill-data/` — the agent skills the CLI ships (`native skills list`). - `tools/` and `scripts/` — dev tooling and the local gate. diff --git a/CHANGELOG.md b/CHANGELOG.md index 172d1aa62..cf83a1ebe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,91 @@ All notable changes to the Native SDK (formerly zero-native) will be documented in this file. -## 0.9.1 +## 0.9.5 ### New Features +- **JSON manifests by default**: New TypeScript, Zig, web, full, and ejected apps now scaffold with `app.json`, backed by full parsing, discovery, build conversion, validation, vendoring, a published versioned schema, and seamless `app.zon` fallback for existing projects (#385). +- **Registered-image source cropping**: Canvas image options and Native markup can now select atomic source rectangles from registered images for texture-atlas rendering, with schema, compiler, validation, documentation, and sampling-bleed coverage (#390). + +### Bug Fixes + +- **Reliable installed TypeScript toolchains**: Core and service builds now resolve ScriptC across nested, hoisted, and global sibling npm layouts, generate the complete SQLite SDK module family, and validate library imports against their actual directories (#389). + +### Improvements + +- **Focused schema hosting**: `schema.native-sdk.dev` now serves only the versioned app schema and its current-version alias, redirecting every non-schema route to the main Native SDK site (#388). + +### Contributors + +- @ctate + + + +## 0.9.4 + +### New Features + +- **Model-driven window restore policies**: TypeScript apps can now declare whether each model-driven window restores saved geometry or opens fresh, including center-on-primary placement, with matching defaults, validation, runtime forwarding, tests, and documentation (#381). + +### Improvements + +- **Faster, more predictable iterative rebuilds**: Generated core and service ABI artifacts now change only when their contents do, markup and app code compile into independently cached objects, SDK module edits invalidate the right inputs, and rebuild diagnostics expose phase timing, memory use, and cache decisions across platforms (#382). +- **Updated TypeScript compiler integration**: ScriptC advances to 0.0.33 with published compile-cache bootstrapping, explicit development and release library profiles, synchronized compiler-surface artifacts, and Node 24 throughout the TypeScript build and CI toolchain (#384). + +### Contributors + +- @ctate + +## 0.9.3 + +### New Features + +- **Model-driven TypeScript theme state**: Zero-config TypeScript apps can now derive the built-in pack, color scheme, and accent from committed model state while preserving manifest fallback, live system accessibility settings, deterministic replay, and the existing `themePack` helper (#378). +- **Platform-correct line deletion**: Command+Backspace on macOS now deletes to the start of a field or logical textarea line across every editable canvas control, with matching TypeScript text helpers, controlled-state behavior, undo, and replay (#377). + +### Bug Fixes + +- **Precise macOS file-drop routing**: AppKit drops now retain labeled canvas and WebView targets with top-left, view-local coordinates, while unlabeled window regions fall back to content coordinates (#374). +- **Manifest menus in generated runners**: Zero-config TypeScript and Zig-core apps now load `app.zon` commands, shortcuts, and menus consistently in live and replay runners, including ejected-runner fallbacks (#376). +- **Large TypeScript message unions compile reliably**: Generated shims now derive comptime scan quotas from message shape and identifier size, allowing wide unions to compile across persistence, channels, environment routing, and the full external-core pipeline (#375). +- **Correct combobox Enter precedence**: A bound `on-submit` now handles Enter before trigger activation, so query submission no longer opens the picker or dispatches the wrong command (#373). + +### Contributors + +- @ctate +- @MohakBajaj + +## 0.9.2 + +### New Features + +- **Flash-free accessory startup**: Apps can opt into accessory activation from `app.zon` to launch without a Dock icon or foreground flash, with tray-affordance validation, runtime composition, packaging support, and an updated menu-bar example (#358). +- **Logical canvas radio groups**: Nested radios now form accessible single-selection groups with roving focus and consistent keyboard, pointer, handler, and naming semantics (#361). +- **Budget-aware photo decoding**: Dynamic encoded images are downsampled across desktop and mobile codecs to fit a configurable registered-pixel budget, with independent source bounds, deterministic replay, and platform-level regression coverage (#366). + +### Bug Fixes + +- **Correct anchored surfaces**: Floating and modal surfaces now dismiss without requiring focus, relayout after scroll restoration, resolve against the correct root, and behave consistently across window contexts (#363). +- **Reliable autofocus and caret reveal**: Keyboard focus, autofocus, and automation now transactionally reveal offscreen targets while preserving collapsed end-caret selections in text editors (#364). +- **Explicit link decoration**: Linked text spans now honor their underline flag while Markdown-generated links retain conventional underlines (#368). +- **Stable macOS window geometry**: Fresh windows now distinguish restored, explicit, and default placement, while AppKit and CEF frame events consistently report content geometry without titlebar drift (#369, #370). + +### Improvements + +- **Consistent canvas controls and surfaces**: Checkbox and radio labels can contain markup consistently, while actionable states, disabled colors, variant accents, selection geometry, compact layouts, and zero-width strokes now render uniformly across the schema, runtime, accessibility tree, and documentation (#367). + +### Contributors + +- @ctate +- @sepehr-safari + +## 0.9.1 + +### New Features + - **Multi-item macOS menu bars**: Apps can now manage independent, keyed status items with model-driven updates, events, automation, journaling, and regression coverage (#343). - **Complete TypeScript file effects**: Secure, permission-gated effects now support bounded streaming reads, atomic writes, stat, append, and deletion while preserving deterministic record and replay behavior (#339, #350). - **Actionable desktop notifications**: Notification replacement identifiers and actions dispatch through the ordinary command path on macOS, Windows, and Linux (#347). @@ -31,8 +110,6 @@ All notable changes to the Native SDK (formerly zero-native) will be documented - @ElSebas41 - @johnlindquist - - ## 0.9.0 ### New Features diff --git a/README.md b/README.md index 258d3038c..14e9ee669 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ Read the full guide at [native-sdk.dev/quick-start](https://native-sdk.dev/quick ## Examples -The apps pictured above live in [examples/](./examples), most as zero-config projects — `app.zon` plus `src/`, no build files — run straight from their directory with `native dev`. Start with the TypeScript examples when learning the primary authoring path. The `-ts` suffix on `soundboard-ts` and `system-monitor-ts` is historical because those apps are ports kept beside older Zig originals. Chatbot is TypeScript-only and follows the unsuffixed naming used by new apps created with `native init`. +The apps pictured above live in [examples/](./examples), most as zero-config projects with a manifest plus `src/` and no build files, run straight from their directory with `native dev`. Many examples predate the current `app.json` default and retain `app.zon`; both formats have the same capabilities. Start with the TypeScript examples when learning the primary authoring path. The `-ts` suffix on `soundboard-ts` and `system-monitor-ts` is historical because those apps are ports kept beside older Zig originals. Chatbot is TypeScript-only and follows the unsuffixed naming used by new apps created with `native init`. | Example | What it shows | | --- | --- | diff --git a/apps/schema/README.md b/apps/schema/README.md new file mode 100644 index 000000000..3834b40d2 --- /dev/null +++ b/apps/schema/README.md @@ -0,0 +1,19 @@ +# Native SDK schemas + +Static JSON Schemas published at `schema.native-sdk.dev`. + +- `/app/v1.json` is the stable-major schema URL scaffolded into `app.json`. +- `/app.json` is the short-lived current-version alias. + +Only those schema URLs are served from this deployment. Every other path +redirects (302) to `https://native-sdk.dev`, so the domain stays a single- +purpose home for the manifest rather than hosting arbitrary content. + +Create the Vercel project as `native-schema`, set its root directory to +`apps/schema`, leave the framework preset as Other with no build command, and +attach `schema.native-sdk.dev`. `vercel.json` sets the output directory to +`public`. Add a new versioned file only for a breaking manifest contract; +backward-compatible additions update the current major. + +The original `https://native-sdk.dev/schemas/app.schema.json` URL remains a +byte-identical compatibility copy owned by the docs deployment. diff --git a/apps/schema/public/app/v1.json b/apps/schema/public/app/v1.json new file mode 100644 index 000000000..001d25544 --- /dev/null +++ b/apps/schema/public/app/v1.json @@ -0,0 +1,435 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://schema.native-sdk.dev/app/v1.json", + "title": "Native SDK app manifest", + "description": "Complete app.json manifest for a Native SDK application. app.zon remains supported as a legacy alternative.", + "type": "object", + "additionalProperties": false, + "required": ["id", "name", "version"], + "properties": { + "$schema": { "type": "string", "format": "uri-reference" }, + "id": { "type": "string", "minLength": 1, "maxLength": 128, "description": "Reverse-DNS application identifier." }, + "name": { "type": "string", "minLength": 1, "description": "Short machine-readable app name." }, + "display_name": { "type": "string", "minLength": 1, "description": "Human-readable app name." }, + "description": { "type": "string", "minLength": 1, "maxLength": 256 }, + "version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" }, + "icons": { "$ref": "#/$defs/stringArray" }, + "platforms": { + "type": "array", + "uniqueItems": true, + "items": { "enum": ["macos", "linux", "windows", "ios", "android", "web"] } + }, + "permissions": { "$ref": "#/$defs/stringArray" }, + "capabilities": { + "type": "array", + "uniqueItems": true, + "items": { + "enum": [ + "native_module", "webview", "js_bridge", "native_views", "gpu_surfaces", + "menus", "shortcuts", "tray", "filesystem", "network", "notifications", + "dialog", "clipboard", "credentials", "persist", "store", "sqlite", + "open_url", "reveal_path", "recent_documents", "file_drops", + "app_activation_events", "file_associations", "url_schemes" + ] + } + }, + "dock_visible": { "type": "boolean", "default": true }, + "persist": { "$ref": "#/$defs/persist" }, + "images": { "$ref": "#/$defs/images" }, + "service_packages": { + "type": "array", + "items": { "$ref": "#/$defs/servicePackage" } + }, + "service_carrier": { "enum": ["auto", "in_process", "child"], "default": "auto" }, + "service_pool_size": { "type": "integer", "minimum": 1, "maximum": 16 }, + "bridge": { "$ref": "#/$defs/bridge" }, + "web_engine": { "enum": ["system", "chromium"], "default": "system" }, + "webview_layer": { "enum": ["auto", "include", "exclude"], "default": "auto" }, + "core_compiler": { "const": "external", "default": "external" }, + "theme": { "enum": ["house", "geist"] }, + "theme_accent": { "type": "string", "pattern": "^#[0-9A-Fa-f]{6}$" }, + "cef": { "$ref": "#/$defs/cef" }, + "frontend": { "$ref": "#/$defs/frontend" }, + "security": { "$ref": "#/$defs/security" }, + "assets": { "$ref": "#/$defs/assets" }, + "windows": { + "type": "array", + "items": { "$ref": "#/$defs/window" } + }, + "shell": { "$ref": "#/$defs/shell" }, + "commands": { + "type": "array", + "items": { "$ref": "#/$defs/command" } + }, + "menus": { + "type": "array", + "items": { "$ref": "#/$defs/menu" } + }, + "shortcuts": { + "type": "array", + "items": { "$ref": "#/$defs/shortcut" } + }, + "file_associations": { + "type": "array", + "items": { "$ref": "#/$defs/fileAssociation" } + }, + "url_schemes": { + "type": "array", + "items": { "$ref": "#/$defs/urlScheme" } + }, + "dmg": { "$ref": "#/$defs/dmg" } + }, + "$defs": { + "stringArray": { + "type": "array", + "uniqueItems": true, + "items": { "type": "string" } + }, + "position": { + "type": "object", + "additionalProperties": false, + "required": ["x", "y"], + "properties": { + "x": { "type": "integer", "minimum": 0, "maximum": 65535 }, + "y": { "type": "integer", "minimum": 0, "maximum": 65535 } + } + }, + "persist": { + "type": "object", + "additionalProperties": false, + "required": ["version", "restore"], + "properties": { + "version": { "type": "integer", "minimum": 1 }, + "debounce_ms": { "type": "integer", "minimum": 0, "maximum": 60000, "default": 500 }, + "restore": { + "type": "object", + "additionalProperties": false, + "required": ["ok", "none", "err"], + "properties": { + "ok": { "type": "string", "minLength": 1 }, + "none": { "type": "string", "minLength": 1 }, + "err": { "type": "string", "minLength": 1 } + } + } + } + }, + "images": { + "type": "object", + "additionalProperties": false, + "properties": { + "max_image_pixel_bytes": { "type": "integer", "minimum": 1048576, "maximum": 8388608, "default": 1048576 } + } + }, + "servicePackage": { + "type": "object", + "additionalProperties": false, + "required": ["name", "version", "content_hash"], + "properties": { + "name": { "type": "string", "minLength": 1 }, + "version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" }, + "content_hash": { "type": "string", "pattern": "^[0-9a-f]{64}$" } + } + }, + "bridge": { + "type": "object", + "additionalProperties": false, + "properties": { + "commands": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["name"], + "properties": { + "name": { "type": "string", "minLength": 1 }, + "permissions": { "$ref": "#/$defs/stringArray" }, + "origins": { "$ref": "#/$defs/stringArray" } + } + } + } + } + }, + "cef": { + "type": "object", + "additionalProperties": false, + "properties": { + "dir": { "type": "string", "default": "third_party/cef/macos" }, + "auto_install": { "type": "boolean", "default": false } + } + }, + "frontend": { + "type": "object", + "additionalProperties": false, + "properties": { + "dist": { "type": "string", "default": "dist" }, + "entry": { "type": "string", "default": "index.html" }, + "spa_fallback": { "type": "boolean", "default": true }, + "dev": { + "type": "object", + "additionalProperties": false, + "required": ["url"], + "properties": { + "url": { "type": "string", "format": "uri" }, + "command": { "$ref": "#/$defs/stringArray" }, + "ready_path": { "type": "string", "default": "/" }, + "timeout_ms": { "type": "integer", "minimum": 1, "maximum": 4294967295, "default": 30000 } + } + } + } + }, + "security": { + "type": "object", + "additionalProperties": false, + "properties": { + "navigation": { + "type": "object", + "additionalProperties": false, + "properties": { + "allowed_origins": { "$ref": "#/$defs/stringArray" }, + "external_links": { + "type": "object", + "additionalProperties": false, + "properties": { + "action": { "enum": ["deny", "open_system_browser"], "default": "deny" }, + "allowed_urls": { "$ref": "#/$defs/stringArray" } + } + } + } + } + } + }, + "assets": { + "type": "object", + "additionalProperties": false, + "properties": { + "images": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "path"], + "properties": { + "id": { "type": "integer", "minimum": 1 }, + "path": { "type": "string", "minLength": 1 } + } + } + } + } + }, + "windowBase": { + "type": "object", + "properties": { + "label": { "type": "string", "default": "main" }, + "title": { "type": "string" }, + "width": { "type": "number", "exclusiveMinimum": 0, "default": 720 }, + "height": { "type": "number", "exclusiveMinimum": 0, "default": 480 }, + "x": { "type": "number" }, + "y": { "type": "number" }, + "resizable": { "type": "boolean", "default": true }, + "restore_state": { "type": "boolean", "default": true }, + "titlebar": { "enum": ["standard", "hidden_inset", "hidden_inset_tall", "chromeless"], "default": "standard" }, + "transparent": { "type": "boolean", "default": false }, + "always_on_top": { "type": "boolean", "default": false }, + "click_through": { "type": "boolean", "default": false }, + "activate_on_show": { "type": "boolean", "default": true }, + "initially_hidden": { "type": "boolean", "default": false }, + "allows_fullscreen": { "type": "boolean", "default": true }, + "min_width": { "type": "number", "minimum": 0, "default": 0 }, + "min_height": { "type": "number", "minimum": 0, "default": 0 }, + "close_policy": { "enum": ["quit", "hide"], "default": "quit" } + } + }, + "window": { + "allOf": [{ "$ref": "#/$defs/windowBase" }], + "unevaluatedProperties": false + }, + "shell": { + "type": "object", + "additionalProperties": false, + "properties": { + "windows": { + "type": "array", + "items": { "$ref": "#/$defs/shellWindow" } + }, + "chrome": { "$ref": "#/$defs/shellChrome" } + } + }, + "shellWindow": { + "allOf": [ + { "$ref": "#/$defs/windowBase" }, + { + "type": "object", + "properties": { + "restore_policy": { "enum": ["clamp_to_visible_screen", "center_on_primary"], "default": "clamp_to_visible_screen" }, + "views": { "type": "array", "items": { "$ref": "#/$defs/shellView" } } + } + } + ], + "unevaluatedProperties": false + }, + "shellChrome": { + "type": "object", + "additionalProperties": false, + "properties": { + "tabs": { + "type": "array", + "items": { "$ref": "#/$defs/shellTab" } + }, + "primary_action": { "$ref": "#/$defs/shellTab" } + } + }, + "shellTab": { + "type": "object", + "additionalProperties": false, + "required": ["id", "label"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "label": { "type": "string", "minLength": 1 }, + "icon": { "type": "string", "default": "" } + } + }, + "shellView": { + "type": "object", + "additionalProperties": false, + "required": ["label", "kind"], + "properties": { + "label": { "type": "string", "minLength": 1 }, + "kind": { + "enum": [ + "webview", "toolbar", "titlebar_accessory", "sidebar", "statusbar", "split", "stack", + "button", "icon_button", "list_item", "checkbox", "toggle", "segmented_control", + "text_field", "search_field", "label", "spacer", "gpu_surface", "progress_indicator" + ] + }, + "parent": { "type": "string" }, + "edge": { "enum": ["top", "right", "bottom", "left"] }, + "axis": { "enum": ["row", "horizontal", "column", "vertical"] }, + "x": { "type": "number" }, + "y": { "type": "number" }, + "width": { "type": "number" }, + "height": { "type": "number" }, + "min_width": { "type": "number" }, + "min_height": { "type": "number" }, + "max_width": { "type": "number" }, + "max_height": { "type": "number" }, + "fill": { "type": "boolean", "default": false }, + "layer": { "type": "integer", "minimum": -2147483648, "maximum": 2147483647, "default": 0 }, + "visible": { "type": "boolean", "default": true }, + "enabled": { "type": "boolean", "default": true }, + "role": { "type": "string" }, + "accessibility_label": { "type": "string" }, + "url": { "type": "string" }, + "text": { "type": "string" }, + "command": { "type": "string" }, + "gpu_backend": { "enum": ["none", "metal", "software"] }, + "gpu_pixel_format": { "enum": ["none", "bgra8_unorm"] }, + "gpu_present_mode": { "enum": ["none", "timer"] }, + "gpu_alpha_mode": { "enum": ["none", "opaque", "premultiplied"] }, + "gpu_color_space": { "enum": ["none", "srgb", "display_p3"] }, + "gpu_vsync": { "type": "boolean" } + } + }, + "command": { + "type": "object", + "additionalProperties": false, + "required": ["id"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "title": { "type": "string", "default": "" }, + "enabled": { "type": "boolean", "default": true }, + "checked": { "type": "boolean", "default": false } + } + }, + "menu": { + "type": "object", + "additionalProperties": false, + "required": ["title"], + "properties": { + "title": { "type": "string", "minLength": 1 }, + "items": { + "type": "array", + "items": { "$ref": "#/$defs/menuItem" } + } + } + }, + "menuItem": { + "type": "object", + "additionalProperties": false, + "properties": { + "label": { "type": "string", "default": "" }, + "command": { "type": "string", "default": "" }, + "key": { "type": "string", "default": "" }, + "modifiers": { "$ref": "#/$defs/modifiers" }, + "separator": { "type": "boolean", "default": false }, + "enabled": { "type": "boolean", "default": true }, + "checked": { "type": "boolean", "default": false } + } + }, + "modifiers": { + "type": "array", + "uniqueItems": true, + "items": { "enum": ["primary", "command", "control", "option", "alt", "shift"] } + }, + "shortcut": { + "type": "object", + "additionalProperties": false, + "required": ["id", "key"], + "properties": { + "id": { "type": "string", "minLength": 1 }, + "key": { "type": "string", "minLength": 1 }, + "modifiers": { "$ref": "#/$defs/modifiers" } + } + }, + "fileAssociation": { + "type": "object", + "additionalProperties": false, + "required": ["name"], + "properties": { + "name": { "type": "string", "minLength": 1 }, + "role": { "$ref": "#/$defs/associationRole" }, + "extensions": { "$ref": "#/$defs/stringArray" }, + "mime_types": { "$ref": "#/$defs/stringArray" }, + "icon": { "type": "string" } + } + }, + "urlScheme": { + "type": "object", + "additionalProperties": false, + "required": ["scheme"], + "properties": { + "scheme": { "type": "string", "minLength": 1 }, + "role": { "$ref": "#/$defs/associationRole" } + } + }, + "associationRole": { "enum": ["viewer", "editor", "shell", "none"], "default": "viewer" }, + "dmg": { + "type": "object", + "additionalProperties": false, + "properties": { + "volume_name": { "type": "string" }, + "background": { "type": "string" }, + "window_width": { "type": "integer", "minimum": 320, "maximum": 2000, "default": 660 }, + "window_height": { "type": "integer", "minimum": 240, "maximum": 1400, "default": 400 }, + "icon_size": { "type": "integer", "minimum": 32, "maximum": 256, "default": 128 }, + "app_position": { "$ref": "#/$defs/position" }, + "applications_position": { "$ref": "#/$defs/position" }, + "applications_link": { "type": "boolean", "default": true }, + "items": { + "type": "array", + "items": { "$ref": "#/$defs/dmgItem" } + } + } + }, + "dmgItem": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "position"], + "properties": { + "kind": { "enum": ["app", "applications", "file", "link"] }, + "path": { "type": "string" }, + "name": { "type": "string" }, + "position": { "$ref": "#/$defs/position" } + } + } + } +} diff --git a/apps/schema/vercel.json b/apps/schema/vercel.json new file mode 100644 index 000000000..8444b7a99 --- /dev/null +++ b/apps/schema/vercel.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "outputDirectory": "public", + "rewrites": [ + { "source": "/app.json", "destination": "/app/v1.json" } + ], + "redirects": [ + { + "source": "/((?!app(?:\\.json|/v1\\.json)$).*)", + "destination": "https://native-sdk.dev", + "statusCode": 302 + } + ], + "headers": [ + { + "source": "/app/v1.json", + "headers": [ + { "key": "Content-Type", "value": "application/schema+json; charset=utf-8" }, + { "key": "Cache-Control", "value": "public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400" }, + { "key": "Access-Control-Allow-Origin", "value": "*" } + ] + }, + { + "source": "/app.json", + "headers": [ + { "key": "Content-Type", "value": "application/schema+json; charset=utf-8" }, + { "key": "Cache-Control", "value": "public, max-age=300, s-maxage=300, stale-while-revalidate=86400" }, + { "key": "Access-Control-Allow-Origin", "value": "*" } + ] + } + ] +} diff --git a/build.zig b/build.zig index 49785b4d8..a35965f2d 100644 --- a/build.zig +++ b/build.zig @@ -1,6 +1,13 @@ const std = @import("std"); const web_engine_tool = @import("src/tooling/web_engine.zig"); +fn repositoryScriptcBin(b: *std.Build) []const u8 { + return b.pathFromRoot(if (b.graph.host.result.os.tag == .windows) + "packages/core/node_modules/.bin/scriptc.cmd" + else + "packages/core/node_modules/.bin/scriptc"); +} + const PlatformOption = enum { auto, null, @@ -206,7 +213,38 @@ pub fn build(b: *std.Build) void { else sqliteCompileFlags(), }); + const app_runner_window_placement_mod = module(b, target, optimize, "src/app_runner/window_placement.zig"); + app_runner_window_placement_mod.addImport("native_sdk", desktop_mod); + const app_runner_window_placement_tests = testArtifact(b, app_runner_window_placement_mod); + const app_runner_options = b.addOptions(); + app_runner_options.addOption([]const u8, "platform", "null"); + app_runner_options.addOption([]const u8, "trace", "off"); + app_runner_options.addOption([]const u8, "web_engine", "system"); + app_runner_options.addOption(bool, "debug_overlay", false); + app_runner_options.addOption(bool, "automation", false); + app_runner_options.addOption(bool, "web_layer", false); + const app_runner_mod = module(b, target, optimize, "src/app_runner/root.zig"); + app_runner_mod.addImport("native_sdk", desktop_mod); + app_runner_mod.addImport("build_options", app_runner_options.createModule()); + app_runner_mod.addImport("app_manifest_zon", b.createModule(.{ .root_source_file = b.path("tests/app-runner/menu_commands_fixture.zon") })); + const app_runner_migrations_mod = module(b, target, optimize, "src/app_runner/no_migrations.zig"); + app_runner_migrations_mod.addImport("native_sdk", desktop_mod); + app_runner_mod.addImport("relational_migrations", app_runner_migrations_mod); + const app_runner_tests = testArtifact(b, app_runner_mod); + const app_runner_test_run = b.addRunArtifact(app_runner_tests); + const app_runner_test_step = b.step("test-app-runner", "Run framework app-runner manifest fallback tests"); + app_runner_test_step.dependOn(&app_runner_test_run.step); desktop_mod.link_libc = true; + if (target.result.os.tag == .macos) { + const flags: []const []const u8 = if (b.sysroot) |sysroot| + &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC", "-mmacosx-version-min=11.0", "-isysroot", sysroot, b.fmt("-I{s}/usr/include", .{sysroot}) } + else + &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC", "-mmacosx-version-min=11.0" }; + desktop_mod.addCSourceFile(.{ .file = b.path("src/platform/macos/image_fit_test.m"), .flags = flags }); + desktop_mod.linkFramework("Foundation", .{}); + desktop_mod.linkFramework("ImageIO", .{}); + desktop_mod.linkSystemLibrary("objc", .{}); + } const desktop_tests = testArtifact(b, desktop_mod); const desktop_test_shards = desktopTestShardArtifacts(b, desktop_mod); // Tier-5 crash battery: a child uses the public streamed sink, signals @@ -582,6 +620,17 @@ pub fn build(b: *std.Build) void { .windows => "windows", }; + const appkit_cell_grid_host_test_run = if (b.graph.host.result.os.tag == .macos) blk: { + const run = b.addSystemCommand(&.{ "sh", "scripts/test-appkit-cell-grid-host.sh" }); + run.setCwd(b.path(".")); + const step = b.step( + "test-appkit-cell-grid-host", + "Run the real AppKit binary cell-grid decoder, CoreText raster, and cache tests", + ); + step.dependOn(&run.step); + break :blk run; + } else null; + const test_step = b.step("test", "Run package and framework tests"); test_step.dependOn(&b.addRunArtifact(build_graph_tests).step); test_step.dependOn(&b.addRunArtifact(geometry_tests).step); @@ -593,9 +642,12 @@ pub fn build(b: *std.Build) void { test_step.dependOn(&b.addRunArtifact(platform_info_tests).step); test_step.dependOn(&b.addRunArtifact(json_tests).step); test_step.dependOn(&b.addRunArtifact(app_runner_assets_tests).step); + test_step.dependOn(&b.addRunArtifact(app_runner_window_placement_tests).step); + test_step.dependOn(&app_runner_test_run.step); test_step.dependOn(&b.addRunArtifact(canvas_tests).step); test_step.dependOn(&b.addRunArtifact(record_store_tests).step); test_step.dependOn(&file_crash_run.step); + if (appkit_cell_grid_host_test_run) |run| test_step.dependOn(&run.step); for (desktop_test_shards) |shard_tests| { test_step.dependOn(&b.addRunArtifact(shard_tests).step); } @@ -799,6 +851,43 @@ pub fn build(b: *std.Build) void { .{ .path = "build.zig", .pattern = "if (spec.emit_services) check.addFileInput(b.path(\"packages/core/package.json\"));" }, .{ .path = "build/app.zig", .pattern = "if (has_services) check.addFileInput(dep.path(\"packages/core/package.json\"));" }, }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-ts-build-invalidation-boundaries", "Verify generated TypeScript contracts stabilize by content and service implementation inputs cannot dirty the core compiler lane", &.{ + .{ .path = "build/app.zig", .pattern = "pub fn stabilizeGeneratedFile" }, + .{ .path = "build/app.zig", .pattern = "const contract = stabilizeGeneratedFile(b, contract_raw, \"core.contract.json\", build_trace);" }, + .{ .path = "build/app.zig", .pattern = "break :services_contract stabilizeGeneratedFile(b, raw, \"services.contract.json\", build_trace);" }, + .{ .path = "build/app.zig", .pattern = "addAppCoreTsDirInputs(b, stage_run, appPath(b, app_root, \"src\"));" }, + .{ .path = "build/app.zig", .pattern = "addStagedCoreSdkInputs(b, dep.builder, stage_run);" }, + .{ .path = "build/app.zig", .pattern = "for ([_][]const u8{ \"text.ts\", \"events.ts\" }) |source|" }, + .{ .path = "build.zig", .pattern = "app_build.addStagedCoreSdkInputs(b, b, stage_run);" }, + .{ .path = "packages/core/scripts/stage_external_core.mjs", .pattern = "for (const sdkFile of [\"text.ts\", \"events.ts\"])" }, + .{ .path = "build/app.zig", .pattern = "if (std.mem.startsWith(u8, normalized, \"services/\")) continue;" }, + .{ .path = "tools/corewire/emit_service.zig", .pattern = "native-sdk.services.abi.v3" }, + .{ .path = "tools/corewire/emit_service.zig", .pattern = "implementation-only fingerprint" }, + .{ .path = "build/app.zig", .pattern = "link_mod.addObject(markupDataObject(b, target, app_optimize, stage.markup_c));" }, + .{ .path = "build/app.zig", .pattern = "addPlatformLinkSearchPaths(b, selected_platform, web_engine, cef_dir, link_mod);" }, + .{ .path = "build/app.zig", .pattern = "mod.addFrameworkPath(.{ .cwd_relative = b.pathJoin(&.{ sysroot, \"System/Library/Frameworks\" }) });" }, + .{ .path = "build/app.zig", .pattern = "b.fmt(\"{s}-app-code\", .{app_options.name})" }, + .{ .path = "src/app_runner/ts_core_main.zig", .pattern = "extern const native_sdk_app_markup: u8;" }, + .{ .path = "packages/core/scripts/embed_markup_c.mjs", .pattern = "const unsigned char native_sdk_app_markup[]" }, + .{ .path = "build/app.zig", .pattern = "node_modules\", \"scriptc\", \"dist\", \"bootstrap.js\"" }, + .{ .path = "build/app.zig", .pattern = "setEnvironmentVariable(\"SCRIPTC_TIMING\", \"1\")" }, + .{ .path = "build/app.zig", .pattern = "service_compile.addArg(\"--compiler-package-origin\")" }, + .{ .path = "build/app.zig", .pattern = "compile.addArg(\"--compiler-package-origin\")" }, + .{ .path = "packages/core/scripts/run_external_core_compiler.mjs", .pattern = "publishedScriptcArgv(args[\"compiler-package-origin\"])" }, + .{ .path = "packages/core/scripts/run_external_service_compiler.mjs", .pattern = "publishedScriptcArgv(args[\"compiler-package-origin\"])" }, + .{ .path = "build/app.zig", .pattern = "addFileInput(dep.path(\"packages/core/scripts/compiler_command.mjs\"))" }, + .{ .path = "packages/core/scripts/run_external_core_compiler.mjs", .pattern = "compilerArgv(args.compiler)" }, + .{ .path = "packages/core/scripts/run_external_service_compiler.mjs", .pattern = "compilerArgv(args.compiler)" }, + .{ .path = "packages/core/scripts/compiler_command.mjs", .pattern = "npmTarget !== null" }, + .{ .path = "build.zig", .pattern = "fn repositoryScriptcBin" }, + .{ .path = "build.zig", .pattern = "packages/core/node_modules/.bin/scriptc.cmd" }, + .{ .path = "build.zig", .pattern = "compile.addArgs(&.{ \"--compiler\", repositoryScriptcBin(b) });" }, + .{ .path = "src/tooling/verbs.zig", .pattern = "Zig's full summary reports each named build step's duration" }, + .{ .path = "src/tooling/verbs.zig", .pattern = "fn rebuildPathIgnored" }, + .{ .path = "src/tooling/verbs.zig", .pattern = "var walker = try root.walkSelectively(allocator);" }, + .{ .path = "src/tooling/verbs.zig", .pattern = "if (!rebuildPathIgnored(path)) try walker.enter(io, entry);" }, + .{ .path = "tools/native-sdk/main.zig", .pattern = "--explain-rebuild" }, + }); addFileContainsCheckStep(b, file_contains_checker, test_step, "test-scriptc-cross-target-plumbing", "Verify core and service archives share the target-aware ScriptC lane, and every direct Windows archive consumer links its runtime import libraries", &.{ .{ .path = "build/app.zig", .pattern = ".linux => !cross or target.result.cpu.arch == .x86_64 or target.result.cpu.arch == .aarch64" }, .{ .path = "build/app.zig", .pattern = ".windows => !cross or target.result.abi == .gnu" }, @@ -824,6 +913,31 @@ pub fn build(b: *std.Build) void { .{ .path = "build/app.zig", .pattern = "test_step.dependOn(&analysis_obj.step);" }, .{ .path = "src/runtime/ui_app.zig", .pattern = "has no default value - give every Model field a default" }, }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-owned-runner-window-placement", "Verify owned example runners preserve explicit origins and distinguish successful state restoration", &.{ + .{ .path = "examples/hello/src/runner.zig", .pattern = ".initial_placement = if (@hasField(@TypeOf(window), \"x\") or @hasField(@TypeOf(window), \"y\")) .explicit else .default" }, + .{ .path = "examples/hello/src/runner.zig", .pattern = "window.initial_placement = .restored;" }, + .{ .path = "examples/hello/src/runner.zig", .pattern = "app_info.main_window.initial_placement = .restored;" }, + .{ .path = "examples/capabilities/src/runner.zig", .pattern = "info.main_window.default_frame = manifestShellStartupFrame(info.main_window.default_frame);" }, + .{ .path = "examples/capabilities/src/runner.zig", .pattern = "info.main_window.restore_state = manifestShellStartupRestoreState(info.main_window.restore_state);" }, + .{ .path = "examples/capabilities/src/runner.zig", .pattern = "info.main_window.restore_policy = manifestShellStartupRestorePolicy(info.main_window.restore_policy);" }, + .{ .path = "examples/capabilities/src/runner.zig", .pattern = "info.main_window.initial_placement = manifestShellStartupInitialPlacement(info.main_window.initial_placement);" }, + }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-macos-window-placement-contracts", "Verify both macOS hosts use the primary display and restore persisted content frames without titlebar growth", &.{ + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "return [NSScreen screens].firstObject ?: [NSScreen mainScreen];" }, + .{ .path = "src/platform/macos/cef_host.mm", .pattern = "return [NSScreen screens].firstObject ?: [NSScreen mainScreen];" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "[window frameRectForContentRect:restoredContentFrame]" }, + .{ .path = "src/platform/macos/cef_host.mm", .pattern = "[window frameRectForContentRect:restoredContentFrame]" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "[window setFrame:restoredWindowFrame display:NO];" }, + .{ .path = "src/platform/macos/cef_host.mm", .pattern = "[window setFrame:restoredWindowFrame display:NO];" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "[window setFrame:NativeSdkCenterFrameOnScreen(window.frame, primaryScreen) display:NO];" }, + .{ .path = "src/platform/macos/cef_host.mm", .pattern = "[window setFrame:NativeSdkCenterFrameOnScreen(window.frame, primaryScreen) display:NO];" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "else if (initialPlacement == 1) {\n // Fresh authored dimensions are content size" }, + .{ .path = "src/platform/macos/cef_host.mm", .pattern = "else if (initialPlacement == 1) {\n // AppKit adds titlebar chrome" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "[window setFrame:NativeSdkConstrainFrame(window.frame) display:NO];" }, + .{ .path = "src/platform/macos/cef_host.mm", .pattern = "[window setFrame:NativeSdkConstrainFrame(window.frame) display:NO];" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "initWithFrame:window.contentView.bounds" }, + .{ .path = "src/platform/macos/cef_host.mm", .pattern = "initWithFrame:window.contentView.bounds" }, + }); addFileContainsCheckStep(b, file_contains_checker, test_step, "test-bridge-view-selector-helpers", "Verify injected view helpers accept string selectors", &.{ .{ .path = "src/platform/macos/appkit_host.m", .pattern = "viewSelectorPayload(options)" }, .{ .path = "src/platform/macos/cef_host.mm", .pattern = "viewSelectorPayload(options)" }, @@ -1200,10 +1314,10 @@ pub fn build(b: *std.Build) void { // this step until the encoder comment, the host decoder comment, and // the patterns below move with it. addFileContainsCheckStep(b, file_contains_checker, test_step, "test-wire-format-version-prose", "Verify wire-format version prose matches the packet version constant", &.{ - .{ .path = "src/primitives/canvas/serialization.zig", .pattern = "pub const binary_packet_version: u8 = 5;" }, - .{ .path = "src/primitives/canvas/serialization.zig", .pattern = "Compact binary gpu-surface packet encoding (wire format v5)." }, - .{ .path = "src/platform/macos/appkit_host.m", .pattern = "Compact binary gpu-surface packet decoding (wire format v5)." }, - .{ .path = "src/platform/windows/gpu_surface_renderer.cpp", .pattern = "Compact binary gpu-surface packet decoding (wire format v5)." }, + .{ .path = "src/primitives/canvas/serialization.zig", .pattern = "pub const binary_packet_version: u8 = 7;" }, + .{ .path = "src/primitives/canvas/serialization.zig", .pattern = "Compact binary gpu-surface packet encoding (wire format v7)." }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "Compact binary gpu-surface packet decoding (wire format v7)." }, + .{ .path = "src/platform/windows/gpu_surface_renderer.cpp", .pattern = "Compact binary gpu-surface packet decoding (wire format v7)." }, }); addFileContainsCheckStep(b, file_contains_checker, test_step, "test-windows-gpu-packet-presenter", "Verify Windows uses retained Direct2D packets with recovery, bounded resources, and dirty-region pixel fallback", &.{ .{ .path = "src/platform/windows/root.zig", .pattern = ".present_gpu_surface_packet_binary_fn = presentGpuSurfacePacketBinary" }, @@ -1458,6 +1572,13 @@ pub fn build(b: *std.Build) void { .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NativeSdkTextNavigationNeedsRawKeyEvent(event)" }, .{ .path = "src/platform/macos/appkit_host.m", .pattern = "NSEventModifierFlagCommand | NSEventModifierFlagOption" }, }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-widget-accessibility-hierarchy", "Verify AppKit preserves retained-widget accessibility parentage", &.{ + .{ .path = "src/platform/macos/appkit_host.h", .pattern = "uint64_t parent_id;" }, + .{ .path = "src/platform/macos/root.zig", .pattern = ".parent_id = node.parent_id orelse 0" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "element.accessibilityParent = parent;" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "parent.accessibilityChildren = [childrenByParentId objectForKey:parentId];" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "return self.widgetAccessibilityRootElements ?: @[];" }, + }); addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-appearance-bridge", "Verify AppKit reports system light and dark appearance changes", &.{ .{ .path = "src/platform/macos/appkit_host.m", .pattern = "effectiveAppearance" }, .{ .path = "src/platform/macos/appkit_host.m", .pattern = "accessibilityDisplayShouldReduceMotion" }, @@ -1467,6 +1588,14 @@ pub fn build(b: *std.Build) void { .{ .path = "src/platform/macos/root.zig", .pattern = ".high_contrast = event.high_contrast != 0" }, .{ .path = "src/platform/macos/root.zig", .pattern = ".appearance_changed => state.emit" }, }); + addFileContainsCheckStep(b, file_contains_checker, test_step, "test-appkit-tray-segment-source-selection", "Verify both macOS tray hosts keep segmented selection model-owned", &.{ + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "@property(nonatomic, assign) NSInteger sourceSelectedSegment;" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "if (options[i].selected != 0) control.sourceSelectedSegment = (NSInteger)i;" }, + .{ .path = "src/platform/macos/appkit_host.m", .pattern = "for (NSInteger index = 0; index < control.segmentCount; index++) {\n [control setSelected:index == sourceSelected forSegment:index];\n }\n self.trayCallback" }, + .{ .path = "src/platform/macos/cef_host.mm", .pattern = "@property(nonatomic, assign) NSInteger sourceSelectedSegment;" }, + .{ .path = "src/platform/macos/cef_host.mm", .pattern = "if (options[i].selected != 0) control.sourceSelectedSegment = (NSInteger)i;" }, + .{ .path = "src/platform/macos/cef_host.mm", .pattern = "for (NSInteger index = 0; index < control.segmentCount; index++) {\n [control setSelected:index == sourceSelected forSegment:index];\n }\n self.trayCallback" }, + }); addFileContainsCheckStep(b, file_contains_checker, test_step, "test-docs-builtin-bridge-policy", "Verify bridge policy docs include guarded dialog commands", &.{ .{ .path = "docs/src/app/docs/security/page.mdx", .pattern = ".{ .name = \"native-sdk.dialog.saveFile\"" }, .{ .path = "docs/src/app/docs/bridge/builtin-commands/page.mdx", .pattern = ".{ .name = \"native-sdk.dialog.saveFile\"" }, @@ -1480,6 +1609,7 @@ pub fn build(b: *std.Build) void { addTestStep(b, "test-diagnostics", "Run diagnostics module tests", diagnostics_tests); addTestStep(b, "test-platform-info", "Run platform info module tests", platform_info_tests); addTestStep(b, "test-json", "Run JSON primitive tests", json_tests); + addTestStep(b, "test-app-runner-window-placement", "Run app-runner window placement decision tests", app_runner_window_placement_tests); addTestStep(b, "test-canvas", "Run canvas display list tests", canvas_tests); addTestStep(b, "test-desktop", "Run Native SDK framework tests", desktop_tests); for (desktop_test_shard_specs, desktop_test_shards) |spec, shard_tests| { @@ -1652,6 +1782,7 @@ pub fn build(b: *std.Build) void { addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-menu-bar", "Run menu-bar lifecycle example tests", "examples/menu-bar", .managed), addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-feed", "Run feed example tests", "examples/feed", .managed), addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-service-feed-reader", "Run TypeScript service feed-reader example tests", "examples/service-feed-reader", .managed), + addExampleTestStep(b, host_cli_exe, native_examples_step, "test-native-extension-app", "Run the TypeScript native runner extension contract fixture", "tests/native-extension-app", .owned), addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-canvas-preview", "Run canvas preview example tests", "examples/canvas-preview", .managed), addExampleTestStep(b, host_cli_exe, native_examples_step, "test-example-capabilities", "Run capabilities example tests", "examples/capabilities", .owned), }; @@ -1713,6 +1844,7 @@ pub fn build(b: *std.Build) void { "examples/calculator/zig-out/package/test-ios-layout/calculator.xcodeproj/xcshareddata/xcschemes/calculator.xcscheme", "examples/calculator/zig-out/package/test-ios-layout/Host/uikit_host.m", "examples/calculator/zig-out/package/test-ios-layout/Host/native_sdk_app.h", + "examples/calculator/zig-out/package/test-ios-layout/Host/apple_image_fit.h", "examples/calculator/zig-out/package/test-ios-layout/Host/Info.plist", "examples/calculator/zig-out/package/test-ios-layout/Assets.xcassets/AppIcon.appiconset/AppIcon.png", "examples/calculator/zig-out/package/test-ios-layout/Assets.xcassets/AppIcon.appiconset/Contents.json", @@ -1813,13 +1945,20 @@ pub fn build(b: *std.Build) void { }); addFileContainsCheckStep(b, file_contains_checker, mobile_examples_step, "test-example-mobile-widget-abi", "Verify mobile examples use stable widget ABI lookups", &.{ .{ .path = "examples/ios/NativeSdkIOSExample/native_sdk.h", .pattern = "native_sdk_viewport_state_t" }, + .{ .path = "examples/ios/NativeSdkIOSExample/native_sdk.h", .pattern = "NATIVE_SDK_WIDGET_ROLE_RADIOGROUP = 21" }, .{ .path = "examples/ios/NativeSdkIOSExample/native_sdk.h", .pattern = "native_sdk_app_scroll" }, .{ .path = "examples/ios/NativeSdkIOSExample/native_sdk.h", .pattern = "native_sdk_app_set_text_measure" }, + .{ .path = "examples/android/app/src/main/cpp/native_sdk.h", .pattern = "NATIVE_SDK_WIDGET_ROLE_RADIOGROUP = 21" }, .{ .path = "examples/android/app/src/main/cpp/native_sdk.h", .pattern = "native_sdk_app_set_text_measure" }, .{ .path = "examples/mobile-canvas/ios/native_sdk_app.h", .pattern = "native_sdk_app_set_text_measure" }, .{ .path = "examples/ios/NativeSdkIOSExample/NativeSdkHostViewController.swift", .pattern = "native_sdk_app_widget_semantics_by_id" }, + .{ .path = "examples/ios/NativeSdkIOSExample/NativeSdkHostViewController.swift", .pattern = "NATIVE_SDK_WIDGET_ROLE_RADIO" }, + .{ .path = "examples/ios/NativeSdkIOSExample/NativeSdkHostViewController.swift", .pattern = "childrenByParentId[node.parentId, default: []].append(element)" }, + .{ .path = "examples/ios/NativeSdkIOSExample/NativeSdkHostViewController.swift", .pattern = "parent.accessibilityContainerType = .semanticGroup" }, + .{ .path = "examples/ios/NativeSdkIOSExample/NativeSdkHostViewController.swift", .pattern = "parent.isAccessibilityElement = false" }, .{ .path = "examples/android/app/src/main/cpp/native_sdk.h", .pattern = "native_sdk_app_widget_semantics_by_id" }, .{ .path = "examples/android/app/src/main/java/dev/native_sdk/examples/android/MainActivity.kt", .pattern = "nativeScroll(nativeApp" }, + .{ .path = "examples/android/app/src/main/java/dev/native_sdk/examples/android/MainActivity.kt", .pattern = "WIDGET_ROLE_RADIOGROUP -> \"android.widget.RadioGroup\"" }, .{ .path = "examples/android/app/src/main/java/dev/native_sdk/examples/android/MainActivity.kt", .pattern = "nativeWidgetSemanticsByIdFields" }, .{ .path = "examples/android/app/src/main/cpp/native_sdk_jni.c", .pattern = "native_sdk_app_widget_semantics_by_id" }, .{ .path = "examples/android/app/src/main/cpp/native_sdk_jni.c", .pattern = "native_sdk_app_scroll" }, @@ -2103,6 +2242,52 @@ pub fn build(b: *std.Build) void { native_shell_smoke_run.step.dependOn(&cli_exe.step); native_shell_smoke_step.dependOn(&native_shell_smoke_run.step); + const menu_bar_smoke_step = b.step("test-menu-bar-smoke", "Run zero-config TypeScript app-menu automation smoke test"); + const menu_bar_smoke_build = managedExampleRun(b, cli_exe, &.{ "build", "-Dplatform=macos", "-Dweb-engine=system", "-Dautomation=true", "-Doptimize=Debug" }); + menu_bar_smoke_build.setCwd(b.path("examples/menu-bar")); + const menu_bar_smoke_run = b.addSystemCommand(&.{ + "sh", "-c", + \\set -eu + \\cd examples/menu-bar + \\app="zig-out/bin/menu-bar" + \\cli="$1" + \\case "$cli" in /*) ;; *) cli="../../$cli" ;; esac + \\automation_dir=".zig-cache/native-sdk-automation" + \\mkdir -p "$automation_dir" + \\rm -f "$automation_dir/snapshot.txt" "$automation_dir/accessibility.txt" "$automation_dir/windows.txt" "$automation_dir"/command*.txt + \\"$app" > .zig-cache/native-sdk-menu-bar-smoke.log 2>&1 & + \\pid=$! + \\trap 'status=$?; kill "$pid" >/dev/null 2>&1 || true; wait "$pid" >/dev/null 2>&1 || true; if [ "$status" -ne 0 ]; then echo "---- app log (.zig-cache/native-sdk-menu-bar-smoke.log) ----" >&2; cat .zig-cache/native-sdk-menu-bar-smoke.log >&2 2>/dev/null || true; fi' EXIT + \\ready="$("$cli" automate wait 2>&1)" + \\case "$ready" in *"ready=true"*) ;; *) echo "menu-bar automation snapshot was not ready" >&2; exit 1 ;; esac + \\before="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\case "$before" in *'command id="player.next" title="Next Track" enabled=true checked=false'*) ;; *) echo "app.zon command catalog was not loaded by the zero-config runner" >&2; exit 1 ;; esac + \\case "$before" in *'app-menu title="Player" items=6'*) ;; *) echo "app.zon Player menu was not loaded by the zero-config runner" >&2; exit 1 ;; esac + \\case "$before" in *'app-menu-item label="Next Track" command="player.next" enabled=true checked=false key="n" modifiers=(primary=true,command=false,control=false,option=false,shift=false)'*) ;; *) echo "app.zon player.next menu item was not loaded by the zero-config runner" >&2; exit 1 ;; esac + \\case "$before" in + \\ *'Ambient Coast'*) expected='Night Drive' ;; + \\ *'Night Drive'*) expected='Paper Planes' ;; + \\ *'Paper Planes'*) expected='Ambient Coast' ;; + \\ *) echo "menu-bar snapshot did not expose the current TypeScript model track" >&2; exit 1 ;; + \\esac + \\"$cli" automate menu-command player.next >/dev/null 2>&1 + \\attempts=0 + \\while [ "$attempts" -lt 50 ]; do + \\ snapshot="$(cat "$automation_dir/snapshot.txt" 2>/dev/null || true)" + \\ case "$snapshot" in *"$expected"*) break ;; esac + \\ attempts=$((attempts + 1)) + \\ sleep 0.1 + \\done + \\case "$snapshot" in *"$expected"*) ;; *) echo "app.zon menu command did not reach the zero-config TypeScript commandMsg mapper" >&2; exit 1 ;; esac + \\echo "menu-bar smoke ok" + , + "sh", + }); + menu_bar_smoke_run.addFileArg(cli_exe.getEmittedBin()); + menu_bar_smoke_run.step.dependOn(&menu_bar_smoke_build.step); + menu_bar_smoke_run.step.dependOn(&cli_exe.step); + menu_bar_smoke_step.dependOn(&menu_bar_smoke_run.step); + const gpu_surface_smoke_step = b.step("test-gpu-surface-smoke", "Run macOS GPU surface automation smoke test"); // The GPU smoke apps are managed examples (no build.zig of their own), // so their binaries come from the CLI verb. -Doptimize=Debug keeps the @@ -2593,7 +2778,7 @@ pub fn build(b: *std.Build) void { \\case "$ready_snapshot" in *'view @w1/components-canvas kind=gpu_surface'*'gpu_nonblank=true'*'canvas_frame_gpu_packet_representable=true'*) ;; *) echo "component gallery GPU surface was not ready" >&2; exit 1 ;; esac \\case "$ready_snapshot" in *'view @w1/main kind=webview'*) echo "component gallery created an implicit WebView" >&2; exit 1 ;; *) ;; esac \\"$cli" automate assert 'role=tree name="Components"' 'role=treeitem name="Components".*state=\[expanded\]' 'role=treeitem name="Accordion".*state=\[selected\]' 'role=group name="Details".*state=\[selected,expanded\]' 'name="Accordion details are visible. The model owns this expanded state."' - \\"$cli" automate assert 'role=group name="Theme"' 'role=button name="Default".*state=\[selected\]' 'role=button name="Geist"' + \\"$cli" automate assert 'role=group name="Theme pack"' 'role=group name="Color scheme"' 'role=group name="Theme accent"' 'role=button name="Default".*state=\[selected\]' 'role=button name="Geist"' 'role=button name="System".*state=\[selected\]' 'role=button name="Pink"' 'role=button name="Teal"' \\"$cli" automate screenshot components-canvas >/dev/null 2>&1 \\cp "$automation_dir/screenshot-components-canvas.png" "$automation_dir/screenshot-components-house.png" \\rm -f "$automation_dir/screenshot-components-canvas.png" @@ -3115,7 +3300,7 @@ fn tsCoreE2eArtifact( if (b.graph.environ_map.get("NATIVE_SDK_CORE_COMPILER") == null) { b.build_root.handle.access( b.graph.io, - "packages/core/node_modules/scriptc/dist/main.js", + repositoryScriptcBin(b), .{}, ) catch return null; } @@ -3460,6 +3645,9 @@ fn tsCoreE2eArtifact( // against (the same module the generated shims stage). conformance_mod.addImport("corewire_rt", module(b, target, optimize, "tools/corewire/shim_rt.zig")); conformance_mod.addImport("shim_markup_core", sidecarShimModule(b, target, optimize, corewire_exe, b.path("tests/sidecar/markup_fixture.contract.json"))); + // Compile-cost guard: this generated mirror carries 160 realistically + // named Msg arms and must need no quota setting in app or test code. + conformance_mod.addImport("shim_wide_core", sidecarShimModule(b, target, optimize, corewire_exe, b.path("tests/sidecar/wide_msg_fixture.contract.json"))); // The integer-class fixture: a hand-written sidecar attesting mixed // i64/u64 slot classes, so the suite drives boundary and full-range // integer values through a generated mirror's decode paths. @@ -3636,6 +3824,7 @@ fn externalServiceFixture( const compile = b.addSystemCommand(&.{node}); compile.addFileArg(b.path("packages/core/scripts/run_external_service_compiler.mjs")); + compile.addFileInput(b.path("packages/core/scripts/compiler_command.mjs")); compile.addArg("--stage"); compile.addDirectoryArg(stage_dir); compile.addArg("--manifest"); @@ -3664,8 +3853,7 @@ fn externalServiceFixture( if (b.graph.environ_map.get("NATIVE_SDK_CORE_COMPILER")) |override| { compile.addArgs(&.{ "--compiler", override }); } else { - compile.addArg("--compiler-js"); - compile.addFileArg(b.path("packages/core/node_modules/scriptc/dist/main.js")); + compile.addArgs(&.{ "--compiler", repositoryScriptcBin(b) }); } return .{ @@ -3812,12 +4000,14 @@ fn externalCoreFixtureModule( check.addFileArg(b.path("packages/core/src/cli.ts")); check.addFileArg(b.path(spec.entry)); check.addArg("--contract"); - const contract = check.addOutputFileArg("core.contract.json"); + const contract_raw = check.addOutputFileArg("core.contract.json"); + const contract = app_build.stabilizeGeneratedFile(b, contract_raw, "core.contract.json", false); check.addArg("--contract-entry"); check.addArg(spec.entry); const services_contract: ?std.Build.LazyPath = if (spec.emit_services) services: { check.addArg("--services-contract"); - break :services check.addOutputFileArg("services.contract.json"); + const raw = check.addOutputFileArg("services.contract.json"); + break :services app_build.stabilizeGeneratedFile(b, raw, "services.contract.json", false); } else null; for (spec.service_packages) |package_entry| check.addArgs(&.{ "--service-package", package_entry }); const services_client: ?std.Build.LazyPath = if (services_contract) |service_contract| client: { @@ -3825,7 +4015,8 @@ fn externalCoreFixtureModule( service_project.addArg("--services-sidecar"); service_project.addFileArg(service_contract); service_project.addArg("--service-client"); - break :client service_project.addOutputFileArg("services.gen.ts"); + const client_raw = service_project.addOutputFileArg("services.gen.ts"); + break :client app_build.stabilizeGeneratedFile(b, client_raw, "services.gen.ts", false); } else null; if (spec.persist_capability) check.addArgs(&.{ "--capability", "persist" }); if (spec.store_capability) check.addArgs(&.{ "--capability", "store" }); @@ -3878,6 +4069,8 @@ fn externalCoreFixtureModule( stage_run.addArg("--services-client"); stage_run.addFileArg(client); } + tsCoreAddCoreDirInputs(b, stage_run, std.fs.path.dirname(spec.entry) orelse "."); + app_build.addStagedCoreSdkInputs(b, b, stage_run); stage_run.addArg("--out"); const stage_dir = stage_run.addOutputDirectoryArg("stage"); @@ -3885,6 +4078,7 @@ fn externalCoreFixtureModule( // pin, archive normalized, the co-emitted sidecar captured. const compile = b.addSystemCommand(&.{node}); compile.addFileArg(b.path("packages/core/scripts/run_external_core_compiler.mjs")); + compile.addFileInput(b.path("packages/core/scripts/compiler_command.mjs")); compile.addArg("--stage"); compile.addDirectoryArg(stage_dir); compile.addArgs(&.{ "--name", spec.name }); @@ -3911,8 +4105,7 @@ fn externalCoreFixtureModule( // driver still refuses a release other than the SDK's pin. compile.addArgs(&.{ "--compiler", override }); } else { - compile.addArg("--compiler-js"); - compile.addFileArg(b.path("packages/core/node_modules/scriptc/dist/main.js")); + compile.addArgs(&.{ "--compiler", repositoryScriptcBin(b) }); } // The mirror, generated from the archive's OWN co-emitted contract, @@ -3960,6 +4153,24 @@ fn tsCoreAddDirInputs(b: *std.Build, transpile: *std.Build.Step.Run, dir_path: [ } } +/// The source set stage_external_core.mjs copies: every ordinary `.ts` file, +/// excluding the independent services compiler class and declaration files. +fn tsCoreAddCoreDirInputs(b: *std.Build, stage: *std.Build.Step.Run, dir_path: []const u8) void { + var dir = b.build_root.handle.openDir(b.graph.io, dir_path, .{ .iterate = true }) catch return; + defer dir.close(b.graph.io); + var walker = dir.walk(b.allocator) catch return; + defer walker.deinit(); + while (walker.next(b.graph.io) catch null) |entry| { + if (entry.kind != .file or !std.mem.endsWith(u8, entry.basename, ".ts") or std.mem.endsWith(u8, entry.basename, ".d.ts")) continue; + const normalized = b.dupe(entry.path); + for (normalized) |*char| if (char.* == '\\') { + char.* = '/'; + }; + if (std.mem.startsWith(u8, normalized, "services/")) continue; + stage.addFileInput(b.path(b.fmt("{s}/{s}", .{ dir_path, entry.path }))); + } +} + fn filteredTestArtifact(b: *std.Build, mod: *std.Build.Module, name: []const u8, filters: []const []const u8) *std.Build.Step.Compile { // use_llvm: Zig 0.16.0's self-hosted x86_64 backend miscompiles the // SysV C ABI for f32-heavy signatures (native_sdk_app_viewport); see diff --git a/build/app.zig b/build/app.zig index 3f643d7a4..d1b58fd61 100644 --- a/build/app.zig +++ b/build/app.zig @@ -5,6 +5,95 @@ const std = @import("std"); const builtin = @import("builtin"); +const json_to_zon = @import("../src/tooling/json_to_zon.zig"); + +/// Canonicalize a generated file by its CONTENT before another build step +/// consumes it. `std.Build.Step.Run` normally places outputs under a cache +/// directory keyed by every declared input. That is correct for the producer, +/// but it means an unrelated input can change the output PATH even when the +/// file's bytes are identical; downstream Run steps hash that path and miss +/// their own caches. TypeScript's combined frontend is exactly that shape: +/// service implementation edits re-run the checker while often leaving the +/// core ABI contract byte-identical. +/// +/// This narrow adapter gives equal bytes one immutable cache path. Consumers +/// still invalidate whenever the bytes change, while producer-only churn +/// stops here. It is public so the repository's fixture graph can exercise +/// the same boundary as app builds. +pub fn stabilizeGeneratedFile(b: *std.Build, source: std.Build.LazyPath, basename: []const u8, trace: bool) std.Build.LazyPath { + return StableGeneratedFile.create(b, source, basename, trace).lazyPath(); +} + +const StableGeneratedFile = struct { + step: std.Build.Step, + source: std.Build.LazyPath, + basename: []const u8, + trace: bool, + generated: std.Build.GeneratedFile, + + fn create(b: *std.Build, source: std.Build.LazyPath, basename: []const u8, trace: bool) *StableGeneratedFile { + const stable = b.allocator.create(StableGeneratedFile) catch @panic("OOM"); + stable.* = .{ + .step = std.Build.Step.init(.{ + .id = .custom, + .name = b.fmt("stabilize generated {s}", .{basename}), + .owner = b, + .makeFn = make, + }), + .source = source.dupe(b), + .basename = b.dupePath(basename), + .trace = trace, + .generated = undefined, + }; + stable.generated = .{ .step = &stable.step }; + source.addStepDependencies(&stable.step); + return stable; + } + + fn lazyPath(self: *StableGeneratedFile) std.Build.LazyPath { + return .{ .generated = .{ .file = &self.generated } }; + } + + fn make(step: *std.Build.Step, options: std.Build.Step.MakeOptions) !void { + _ = options; + const self: *StableGeneratedFile = @fieldParentPtr("step", step); + const b = step.owner; + const io = b.graph.io; + const arena = b.allocator; + const source_path = self.source.getPath3(b, step); + const bytes = source_path.root_dir.handle.readFileAlloc(io, source_path.sub_path, arena, .limited(64 * 1024 * 1024)) catch |err| + return step.fail("cannot stabilize generated {s}: {t}", .{ self.basename, err }); + + var digest: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined; + std.crypto.hash.sha2.Sha256.hash(bytes, &digest, .{}); + const hex = std.fmt.bytesToHex(digest, .lower); + const output_dir = b.pathJoin(&.{ "o", "native-stable", &hex }); + const output_path = b.pathJoin(&.{ output_dir, self.basename }); + self.generated.path = try b.cache_root.join(arena, &.{output_path}); + + var reused = false; + if (b.cache_root.handle.readFileAlloc(io, output_path, arena, .limited(64 * 1024 * 1024))) |existing| { + reused = std.mem.eql(u8, existing, bytes); + } else |_| {} + if (!reused) { + b.cache_root.handle.createDirPath(io, output_dir) catch |err| + return step.fail("cannot create the stable generated-output directory for {s}: {t}", .{ self.basename, err }); + var atomic = b.cache_root.handle.createFileAtomic(io, output_path, .{ .replace = true }) catch |err| + return step.fail("cannot stage stable generated {s}: {t}", .{ self.basename, err }); + defer atomic.deinit(io); + atomic.file.writeStreamingAll(io, bytes) catch |err| + return step.fail("cannot write stable generated {s}: {t}", .{ self.basename, err }); + atomic.replace(io) catch |err| + return step.fail("cannot publish stable generated {s}: {t}", .{ self.basename, err }); + } + step.result_cached = reused; + if (self.trace) std.debug.print("native build trace: {s} content {s} {s}\n", .{ + self.basename, + &hex, + if (reused) "reused" else "changed", + }); + } +}; /// The shared web-layer inference contract: this build graph is one thin /// adapter over it (the CLI's manifest tooling and the app runner are the @@ -33,6 +122,9 @@ const WebLayerOption = web_layer_contract.WebViewLayer; pub const AppOptions = struct { name: []const u8, + /// Explicit manifest path. Null auto-detects app.json first, then app.zon, + /// so older owned build.zig files can adopt JSON without a build edit. + manifest: ?[]const u8 = null, /// App entry point; defaults to src/main.zig (relative to `app_root`). main: []const u8 = "src/main.zig", /// Root of the app source tree, relative to the build root. "." for a @@ -52,6 +144,14 @@ pub const AppOptions = struct { /// step walks wuffs/translate_c and whose full build pulls harfbuzz) /// — the load-bearing property for scaffolded apps. terminal_sessions: bool = false, + /// Optional app-owned Zig module that customizes the generated desktop + /// runner for a `src/core.ts` app. The module implements the narrow + /// `native_extension` contract documented in + /// `src/app_runner/native_extension.zig`: configure the generated + /// `TsUiApp` core/options, then optionally wrap its `native_sdk.App`. + /// Paths are relative to `app_root`. Zig-core apps own their entry point + /// directly and must leave this null. + native_extension: ?[]const u8 = null, }; /// Which core the app tree carries. No flag and no config anywhere: the @@ -75,6 +175,34 @@ fn appFileExists(b: *std.Build, app_root: []const u8, sub_path: []const u8) bool return true; } +fn appManifestName(b: *std.Build, app_root: []const u8, requested: ?[]const u8) []const u8 { + if (requested) |name| return name; + if (appFileExists(b, app_root, "app.json")) return "app.json"; + return "app.zon"; +} + +fn appManifestPath(b: *std.Build, app_root: []const u8, manifest_name: []const u8) []const u8 { + return appPath(b, app_root, manifest_name); +} + +/// Produce the Zig module consumed by the existing comptime manifest wiring. +/// ZON manifests are already modules; JSON manifests are converted losslessly +/// into a generated module, keeping one runtime feature path for both formats. +fn appManifestModule(b: *std.Build, app_root: []const u8, manifest_name: []const u8) *std.Build.Module { + const path = appManifestPath(b, app_root, manifest_name); + if (!json_to_zon.isJsonPath(path)) { + return b.createModule(.{ .root_source_file = b.path(path) }); + } + const source = b.build_root.handle.readFileAlloc(b.graph.io, path, b.allocator, .limited(1024 * 1024)) catch + @panic("cannot read app.json"); + const zon = json_to_zon.convertAlloc(b.allocator, source) catch |err| switch (err) { + error.NullNotAllowed => @panic("app.json cannot contain null values; omit optional fields instead"), + else => @panic("cannot convert app.json into the build-time manifest module; run `native check` for a precise diagnostic"), + }; + const generated = b.addWriteFiles().add("app_manifest.zon", zon); + return b.createModule(.{ .root_source_file = generated }); +} + const TsWindowView = struct { label: []const u8, source_path: []const u8, @@ -156,7 +284,7 @@ fn tsWindowRegistrySource(b: *std.Build, registry: TsWindowViews) []const u8 { \\//! Generated by build/app.zig from src/windows/