Project Model #
Budo projects are intentionally plain. A project directory contains an entrypoint, optional metadata, and any assets your app needs. There is no required package manager, manifest format, bundler, or generated source tree.
That simplicity is useful only if the rules are predictable. This page explains the rules Budo applies before your first frame runs.
Entrypoints #
At startup, Budo scans the project directory in this order:
main.ts
main.js
main.lua
main.wat
main.wasmThe first file found selects the runtime. main.ts and main.js run in QuickJS. main.lua runs in Lua 5.4. main.wat and main.wasm run through Wasmtime on desktop.
Android supports QuickJS apps and Lua apps in the native runtime. Web export supports QuickJS and Lua. Wasmtime is a desktop feature; browsers cannot host it inside the Emscripten build.
When both main.ts and main.js exist, main.ts wins. This is convenient while migrating, but in a release project it is clearer to keep only the entrypoint you mean to ship.
JavaScript and TypeScript loading #
JavaScript files are evaluated once before the main loop starts. Budo detects ES module syntax in the entrypoint and chooses module evaluation when it sees import or export statements. Classic scripts still work, and runtime globals such as sys, console, fetch, and the subsystem objects are available either way.
TypeScript support is deliberately lightweight. The runtime strips type annotations, interfaces, access modifiers, generic type syntax, as casts, and related TypeScript-only constructs while preserving line and column positions as much as possible. It then runs the stripped JavaScript in QuickJS.
This gives a smooth editor experience with budo.d.ts and no build step. It does not run the TypeScript compiler, emit decorator transforms, bundle modules, or type-check before execution.
Lua loading #
Lua apps are loaded as normal Lua chunks with the standard Lua libraries opened. Budo injects sys, fetch, json_parse, and console. The rendering model is the same as JavaScript: register a function with sys.animation.requestFrame, draw, then request the next frame.
Lua has no Budo-specific module resolver. Use the Lua facilities available in the opened standard libraries and keep asset paths project-relative when you cross into Budo APIs.
WebAssembly loading #
Desktop WebAssembly apps import host functions from the env module. Budo calls an optional init export once, then calls a frame export once per frame when present. String-backed host functions require an exported memory so the host can read bytes from guest linear memory.
WAT files are useful for small examples and tests. Larger projects usually generate .wasm from a language toolchain and keep a small compatibility layer around Budo's env.* imports.
app.json #
The optional app.json file is read from the project directory. Budo uses it for app metadata and for policy decisions that must be explicit on some platforms.
{
"app_name": "My Budo App",
"author": "N. Developer",
"version": "1.0.0",
"date": "2026-05-01",
"orientation": "portrait",
"network": ["api.example.com"],
"filesystem": false,
"neural": true
}app_name may also be written as name. version may also be written as version_name. If metadata is missing, Budo falls back to the directory name, Unknown author, version 1.0, and unspecified orientation.
Network access is disabled by default. Set network to "*", a comma-separated domain string, or an array of domains to allow HTTP fetches.
On Android, external filesystem access is disabled by default. Bundled project files remain readable. Set filesystem to true only when the app genuinely needs shared storage access.
ONNX Runtime inference is opt-in at the app level. Set neural to true before calling sys.neural APIs.
Android packaging reads additional fields from app.json for identity, icons, SDK versions, permissions, signing, shrinking, store listing metadata, and output details. See Packaging and deployment for the release-oriented shape.
Asset paths #
Most APIs that load files resolve paths relative to the project directory: shaders, fonts, SVGs, textures, modules, ONNX models, and app.json policy references.
Paths must be relative. Absolute paths and traversal through .. are rejected by APIs that load project assets. This rule keeps desktop, Android packaging, and web export aligned: if a path works locally, it can usually be bundled safely.
The file API has its own root. On desktop, sys.file defaults to the project directory unless --file-root is passed. On Android, it defaults to extracted bundled assets unless external filesystem access has been granted by policy.
Capabilities #
The sys.capabilities object lets code adapt to the runtime it is actually running on.
// Check for neural inference before loading a model.
if (sys.capabilities.neural.available) {
// Use the feature only on builds that expose it.
console.log('Neural inference is available');
}
// Check for sensor support before offering tilt controls.
if (!sys.capabilities.sensors.available) {
// Pick a fallback that still works on desktop.
console.log('Using keyboard fallback for tilt controls');
}The main capability entries are neural, midi, udp, http, sensors, and shadersAdvanced. JavaScript and Lua expose the same shape on native desktop and Android builds, such as sys.capabilities.neural.available. WebAssembly uses capability_* imports.
The current web entrypoints do not expose sys.capabilities for JavaScript or Lua. When code is meant to run on web too, combine capability checks with optional chaining and normal feature fallbacks.
// This works on native builds and safely falls back on current web builds.
const canUseUdp = sys.capabilities?.udp?.available === true;Capabilities are not a substitute for normal error handling. They are a clean way to choose a path before importing or calling a feature that may not exist on the current target.
Runtime lifecycle #
For JavaScript and Lua, the app file is evaluated once, then the runtime enters a frame loop. Each frame follows the same broad shape: poll platform events, update input and timing, poll asynchronous subsystems such as MIDI, UDP, and network completion, clear or prepare the canvas, invoke your animation callback, run pending QuickJS jobs when needed, and present the frame.
The important part for app authors is that your frame callback should finish quickly and request the next frame when it wants continuous animation. Long blocking work will block rendering and input. For network and timer work, use the asynchronous APIs provided by the runtime.
Project shape #
A mature project often looks like this:
my-app/
app.json
main.ts
draw.ts
input.ts
state.ts
shader.vert
shader.frag
assets/
logo.svg
Inter-Regular.ttf
data/
seed.db
models/
classifier.onnxBudo does not require this layout. It is simply a practical one: code near the entrypoint, assets grouped by use, and paths short enough to remain readable in app code.