Build and package#
Layout#
ohm/
├─ apps/
│ ├─ desktop/ Electron main + preload (the composition root)
│ └─ ui/ React renderer — engine-agnostic, talks only to a HostBridge
├─ packages/
│ ├─ engine-api/ the interfaces a browser engine must implement
│ ├─ engine-electron/ the only code allowed to import electron
│ ├─ protocol/ typed UI ↔ core JSON-RPC contracts
│ ├─ core/ tabs, scripts, projects, models, AI
│ ├─ ohm-lang/ splitter, LaTeX lexer/parser, Lua codegen
│ ├─ ohm-runtime/ wasmoon VM, async scheduler, worker runner
│ ├─ curves/ curves, expressions, typed sweep variables
│ ├─ graph/ Rete components, profiles, graph→Ohm compiler
│ └─ docview/ slide and spreadsheet parsing for the viewers
├─ installers/ Windows and macOS installers (own workspace)
├─ docs/ this documentation, and docs/specs
└─ scripts/ dev loop, builds, checks
Commands#
| Command | What it does |
|---|---|
npm run dev |
Vite dev server for the UI + esbuild watch for the main process; Electron restarts on change |
npm run build |
Production build: apps/ui/dist and apps/desktop/dist |
npm start |
npm run build, then launch Electron |
npm run check |
check:engine + check:specs + check:docs + typecheck + test — run this before committing |
npm run specs |
Regenerate docs/specs/*.spec.{json,md} from docs/specs/src |
npm run docs |
Compile docs/**.md into the in-app help site at apps/ui/public/help |
npm run shots |
Recapture the documentation screenshots from a real running app |
npm run docs:serve |
Serve the built site on http://127.0.0.1:4173 to read it as a visitor |
npm test |
Vitest |
The engine boundary#
Ohm is written so the engine can be replaced — Electron today, a Chromium fork later.
packages/engine-api defines the interfaces; packages/engine-electron is the only
package permitted to import 'electron', and scripts/check-engine-imports.mjs fails the
build if anything else does. npm run check runs it first, so the rule is enforced rather
than merely documented.
Practically: if you need a new capability from the engine (a dialog, a scheme, a way to
capture a page), add it to engine-api as an interface, implement it in engine-electron,
and let core depend only on the interface.
The production build#
Three steps, all in npm run build:
Help —
scripts/build-operators.mjsregenerates the operator reference from the registry, thenscripts/build-docs.mjsrenders every page underdocs/to HTML inapps/ui/public/help, with a contents sidebar, a search index and the screenshots fromdocs/images. Vite copiespublic/verbatim, so the Help tab is part of the bundle and a shipped build always carries the documentation that matches it.The output is a plain static site:
index.htmlis a landing page generated from what the documentation actually contains — the guide and reference counts, the number of operators and specification nodes are all read at build time rather than written down — anddocs.htmlis the index of everything. It can be published as-is to any static host;npm run docs:serveshows it the way a visitor sees it, which matters becausefile://blocks the fetch the search box makes.UI — Vite builds
apps/uiintoapps/ui/distand injects the production Content-Security-Policy meta tag. The dev server does not use that CSP, because React refresh needs inline scripts; if something works innpm run devbut not innpm start, suspect the CSP first.Desktop — esbuild bundles three CommonJS entries into
apps/desktop/dist:main.js,preload.jsandohm-worker.js.
Four packages stay external to the bundle: electron, wasmoon, node-llama-cpp and
@node-llama-cpp/*. wasmoon loads its .wasm next to its own module, and node-llama-cpp
picks a native binary package at runtime, so both have to exist as real files under
node_modules.
Installers#
Installers live in installers/, outside the app's workspaces, so electron-builder and its dependencies never enter the app's dependency tree.
cd installers
npm install
npm run assets # render the branding once
npm run build:win # on Windows → installers/windows/dist
npm run build:mac # on macOS → installers/macos/dist
npm run build:mac -- --validate # anywhere: check config and staging without building
Two things are worth repeating here because they surprise people:
- The package is not asar'd. The app serves its UI with
net.fetch(file://…)and runs the interpreter in a worker thread from a file path; neither can read inside an asar archive. - Only the CPU llama.cpp binary ships by default. The Vulkan and CUDA builds add
~600 MB. Pass
--backends cpu,vulkan,cudawhen you want them.
See the packaging spec for the full contract, including the staging layout, the branding pipeline and the install/uninstall rules.
Testing an installed build#
The app is drivable over CDP, which makes an end-to-end check cheap:
Ohm.exe --remote-debugging-port=9333
Then attach to the ohm-app://ui/index.html target and run
print("ok", $\sqrt{144}$) in the console. If it prints 12, the compiler, the worker
thread and wasmoon are all working from the packaged layout.
One caveat when driving a background window: Chromium throttles
requestAnimationFrame when the window is not visible, and pdf.js renders through it, so a
PDF render promise can stall until a frame is forced. Take a screenshot (or front the
window) before measuring anything in a document viewer.