Getting started
Checked against 2994ef5 on 2026-11-10 by Capy (review pass: Subagent 124 and fable). The files it covers are unchanged since.
This page builds one small Svelte page and runs it from a checkout of the
repository: headless with niso-run, under Node, in a desktop window, and in
the Apple and Android hosts. There’s no package to install yet; the commands
are the repository’s own build scripts.
What you need
Section titled “What you need”- Rust (rustup’s stable) and a C compiler, for niso and QuickJS-ng.
- Node and pnpm, to build pages:
examples/build.mjscompiles Svelte and bundles with esbuild. - For the Apple platforms: macOS with Xcode (Build for Apple platforms).
- For Android: the Android SDK with the NDK, and a JDK 17 (Build for Android).
- On Linux, the Linux notes list the toolchains the checks use.
Get the source
Section titled “Get the source”git clone https://github.com/oofdere/nisocd nisogit submodule update --init # QuickJS-ngWrite a page
Section titled “Write a page”Every directory in examples/ with a main.js is a page:
examples/build.mjs builds it into examples/dist/<name>.js, the script a
host runs, and examples/dist/<name>.html, the same page for a browser.
Directoryexamples/
Directoryhello/
- App.svelte
- main.js
- build.mjs
<script> let count = $state(0); let notify = $state(true);</script>
<main> <h1>Hello, niso</h1> <p>Tapped {count} {count === 1 ? 'time' : 'times'}.</p> <button onclick={() => count++}>Tap me</button> <label><input type="checkbox" switch bind:checked={notify}> Notifications</label></main>
<style> main { padding: 16px; display: flex; flex-direction: column; gap: 12px; }</style>import { mount } from 'svelte';import App from './App.svelte';
mount(App, { target: document.body });Build the pages:
(cd examples && pnpm install --frozen-lockfile && node build.mjs)Run it
Section titled “Run it”niso-run runs scripts against a headless niso DOM: no window, the same core
and layout. It’s the quickest loop, and how niso’s own tests drive pages.
cargo build --release -p niso-quickjs --bin niso-runtarget/release/niso-run --tap 100,195 --dump examples/dist/hello.js--tap X,Y renders a frame, taps at that point in CSS pixels (here the
button) and renders again; --dump then prints the document:
<html><head><style id="svelte-1kym7j0">main.svelte-1kym7j0 {padding:16px;display:flex;flex-direction:column;gap:12px;}</style></head><body><main class="svelte-1kym7j0"><h1>Hello, niso</h1> <p>Tapped 1 time.</p> <button>Tap me</button> <label><input type="checkbox" switch=""> Notifications</label></main></body></html>To find where to tap, --boxes prints every element’s border box under
body as JSON, in CSS pixels of an 800 by 600 viewport. This is macOS’s
output; on Linux the default fonts are fontconfig’s, so the text boxes and
everything below them differ by a few pixels, which is why the tap above
aims at a point inside the button on both:
target/release/niso-run --boxes examples/dist/hello.js[{"path":"body","x":8,"y":8,"width":784,"height":255.88},{"path":"body>main:0","x":8,"y":8,"width":784,"height":255.88},{"path":"body>main:0>h1:0","x":24,"y":45.440002,"width":752,"height":32},{"path":"body>main:0>p:1","x":24,"y":126.880005,"width":752,"height":16},{"path":"body>main:0>button:2","x":24,"y":170.88,"width":752,"height":34},{"path":"body>main:0>label:3","x":24,"y":216.88,"width":752,"height":31},{"path":"body>main:0>label:3>input:0","x":24,"y":216.88,"width":51,"height":31}]The other options, all from the top of
niso-run.rs:
| Option | What it does |
|---|---|
--page FILE [--root DIR] [--url URL] |
loads an HTML page the way a browser does, with its stylesheets, classic and module scripts; with --root the page is at http://localhost/ plus its path under DIR, which serves that origin’s files |
--html FILE |
sets document.body’s content before the scripts run |
--serve URL=FILE |
answers requests for URL with FILE’s bytes, so a page that fetches sees the same answer every run |
--missing |
lists every DOM member and global the scripts looked up that niso doesn’t have (Port a web app) |
--repeat N |
repeats the taps N times |
--render FILE |
writes each frame’s render stream to FILE |
--time |
prints startup time, time per tap frame, live nodes and peak memory to stderr |
--storage DIR |
keeps localStorage and IndexedDB in DIR for the next run |
--hold |
after --time, keeps the process and its workers alive until stdin closes, for a heap tool |
--max-ms N |
how long the page’s timers may run, in virtual time (default 60000) |
Scripts run in virtual time: timers fire as fast as the page lets them, and
niso-run exits 1 on any uncaught exception.
The same page runs on V8 under Node through niso’s Node-API addon.
niso-node takes niso-run’s options:
cargo build --release -p niso-napinode --experimental-vm-modules bindings/js/napi/niso-node.js --tap 100,195 --dump examples/dist/hello.js<html><head><style id="svelte-1kym7j0">main.svelte-1kym7j0 {padding:16px;display:flex;flex-direction:column;gap:12px;}</style></head><body><main class="svelte-1kym7j0"><h1>Hello, niso</h1> <p>Tapped 1 time.</p> <button>Tap me</button> <label><input type="checkbox" switch=""> Notifications</label></main></body></html>It needs Node 20.19.5, 22.17.0, 24.2.0 or later; Deno and Bun 1.4 work too.
On main this is headless only. Opening a window from Node is on the
unmerged node-windows branch; see JavaScript.
The iced host draws the page in a desktop window. iced is niso’s Linux host; it builds on macOS too.
hosts/iced/build.sh helloIt builds niso-iced and the pages, then opens examples/dist/hello.js in a
window. Arguments after the page name go to the app: --size WxH, or
--tap X,Y to tap after launch (taps come half a second apart). The binary
stays in target/release/niso-iced, which also takes
--page FILE --root DIR like niso-run. --no-launch only builds:
hosts/iced/build.sh hello --no-launchWithout a display, the binary can render the page offscreen to a PNG
after the taps instead of opening a window. --screenshot is the switch the
screenshot tests use; this run on Linux showed “Tapped 1 time.” in the image:
target/release/niso-iced examples/dist/hello.js --size 400x300 --tap 100,195 --screenshot target/hello.pngThe SwiftUI host, here on the iOS Simulator:
hosts/apple/build.sh helloIt builds niso for the simulator (niso-app), the pages, the Swift host and
its widget extension, then installs the app on the simulator (booting it if
needed) and launches it. NISO_SIMULATOR picks the device, by name or UDID;
--platform picks macOS, tvOS, visionOS or watchOS instead; --no-launch
only builds:
hosts/apple/build.sh --no-launch helloThe app is left in target/ios/Niso.app, with the widget extension in
PlugIns/NisoWidgets.appex. Build for Apple platforms covers
every platform.
The Compose host, on the emulator or a phone:
hosts/android/build.sh helloIt builds libniso_android.so for arm64, the pages and the host app with
Gradle, starts the AVD NISO_AVD (default quilt) if nothing is attached,
installs the app and launches the page. hosts/android/build.sh hello --no-launch
only builds. Not run for this page: the machines used had no Android NDK.
See Build for Android.
What you’re looking at
Section titled “What you’re looking at”The button is the platform’s own button, and <input type="checkbox" switch>
is its switch: a SwiftUI Toggle on Apple platforms, a Material 3 Switch
on Android, iced’s toggler on desktop. Layout is niso’s, the same on every
platform and checked against Chrome’s: in the screen test cases
(tests/screens/cases.txt), every element’s box within a pixel, apart from
the known differences in tests/screens/expectations.json. Whole apps are
further off (Real apps).
Text is laid out by niso and drawn by the platform with its system fonts.
See Native controls and
Text.
- Try the examples that come with the repository:
hosts/apple/build.sh timerruns a stopwatch with home screen widgets and a Live Activity (Surfaces). - Native apps: the same timer, driving the DOM from C, Rust, Zig, Go or Java.
- JavaScript: QuickJS, Porffor and Node-API, and what each changes.
- Web platform support: what of the web platform works.