Skip to content

Real apps

Generated from docs/compat/apps.md when the site was built at 7aa23e2.

niso’s compatibility target is real apps that run and look right on every host (DECISIONS.md, Priorities). WPT stays the regression net. The corpus:

App Build Driver’s flow
TodoMVC, Svelte, React and Vue (todomvc-svelte, todomvc-react, todomvc-vue) tastejs/todomvc at ff43b02, each example built as upstream builds it, from its lockfile: Vite for Svelte and Vue, webpack for React add three todos, complete one, filter Active, Completed and All through the filter links, edit one by double-click, delete one, reload
Excalidraw (excalidraw) @excalidraw/excalidraw 0.18.1 in a minimal Vite page (tests/apps/excalidraw/host, every version pinned by its package-lock.json), with its fonts served beside it pick tools on the toolbar, draw a rectangle and an arrow by dragging with pointer events, add text in the text editor, select the rectangle and drag it, undo with Ctrl+Z, redo with the toolbar’s button, export to a canvas and to PNG
VS Code for the web (vscode-web) the vscode-web 1.145.2 standalone build (commit c4453f8) open an untitled JavaScript file, type a function, check its tokens are highlighted (vscode-web.md)
Bluesky’s social-app (web build) not in the harness yet (bluesky.md)
niso’s docs site (Astro Starlight, docs/site/ on docs-study) not in the harness yet
Terminal window
tests/apps/run.sh NAME --fetch
tests/apps/run.sh NAME [--engine quickjs|node|bun] [--screenshot]

--fetch fetches and builds the app with tests/apps/<app>/fetch.sh into $NISO_APPS/NAME (target/apps by default), whose root/ is the static serve root. It is the only step that uses the network, so runs work offline. It stamps the build with a hash of what the app pins (fetch.sh, host/ with its lockfile, make-index.mjs) and the variant; a run whose build is missing or stamped differently, as after a pin is bumped, stops and asks for --fetch. The vscode-web tarball is checked against its sha256. Each run serves it at http://localhost/ and loads its index.html with the app’s driver.js added as the last module script. The driver acts the way a user’s input arrives (DOM events: clicks, pointer drags, typing, keys), checks the app’s state after each step, and prints NISO-APP ok STEP, NISO-APP FAIL STEP: why and NISO-APP note ... lines, then NISO-APP done. A driver that prints NISO-APP reload runs again as a reload: a second process with the same storage directory (--storage, in niso-run and niso-node). run.sh prints the driver’s lines and exits 1 on a FAIL or when the driver never finishes. --screenshot renders the page with niso-iced into NAME/ENGINE.png: on QuickJS niso-iced runs the page and its driver itself; on Node and Bun it renders the DOM the run ended with, with scripts removed, so canvases are blank there, with a storage directory of its own (NISO_STORAGE_DIR). Each engine’s runs write pages of their own (root/niso-app-ENGINE-N.html), so runs on different engines can share a build at the same time. NISO_PROFILE=debug uses debug builds, and NISO_BUILD=0 uses what is already built (for machines where builds take a shared lock).

An app is a directory with fetch.sh WORKDIR [VARIANT], driver.js and an optional app.conf (max_ms, the page time each run gets, and size, the screenshot’s). todomvc-svelte is tests/apps/todomvc with the variant svelte.

Checked on 2026-11-09 on Linux (yachiyo) at 5589c475, now on main, with debug builds for Node, Bun and niso-iced. TodoMVC Svelte on QuickJS was run again on 2026-11-10 on macOS with main at 621af1a6: edit and delete still do nothing.

App QuickJS (niso-run) Node 26.10 (V8) Bun 1.4.4 (JSC) niso-iced (QuickJS) iOS, Android
TodoMVC Svelte edit and delete do nothing (TodoMVC 1) passes passes runs; looks wrong (TodoMVC 2-6) not run
TodoMVC React passes passes passes runs; looks wrong (TodoMVC 2-6) not run
TodoMVC Vue passes passes passes runs; looks wrong (TodoMVC 2-6) not run
Excalidraw nothing can be drawn; the whole flow passes behind three probes the whole flow passes behind two probes as Node draws; its UI looks wrong (Excalidraw 4-5) not run
VS Code highlights highlights highlights see vscode-web.md not run

“Passes” means every driver step says ok. TodoMVC’s reload step checks that localStorage survives a reload, with a key of the driver’s own: tastejs’s current examples keep their todos in memory, so each shows an empty list after the reload, as it does in a browser.

When a step fails on a missing API, Excalidraw’s driver logs the failure, then installs probes and retries the step, to see what breaks next. Probes are logged (NISO-APP note probe: ...) and stand in for what niso lacks. They are not fixes, and a run that needs one fails. Today’s: document.getSelection returning null, FontFace and document.fonts stand-ins that load nothing and, on QuickJS, RegExp.$1-$9 set by RegExp.prototype.exec.

QuickJS in niso-iced (Vue) Chrome, the same DOM

The other screenshots: Svelte (its edit and delete failed, so it still has three todos), React, and Vue’s DOM after a Node run, rendered by niso-iced: Vue on Node.

What breaks, most harmful first:

  1. Svelte 5 loses its event handlers on QuickJS, so edit and delete do nothing. Svelte keeps delegated handlers (click, dblclick, keydown, input, …) as an expando on each element, set while the element is still in a cloned template, and looks them up on the element when an event comes. niso’s wrapper of a node that isn’t connected is weak (bindings/js/glue/wrappers.js; the known limitation in docs/engines/napi.md), so a label or button that no script refers to gets a new wrapper without the handlers once the old one is collected, which QuickJS does at once. V8 and JSC collect later, so Node and Bun pass by luck of timing. Every Svelte 5 app is affected. Medium: a wrapper with expandos has to live while its node can still be reached, which the core can tell the binding (DECISIONS, Core: the core notifies bindings when a node becomes unreachable).
  2. No text decorations in the render stream (fixed on main since this check by 725ab097, which sends decoration lines with the text and has every host draw them; TodoMVC hasn’t been run again with it): the completed todo wasn’t struck through.
  3. No CSS background images or SVG images in the render stream: the todos’ check circles (an SVG data URL in background-image) are missing, so nothing shows which todos are done. render.rs sends only <img> rasters and canvases (IMAGE); ImageData::Svg and backgrounds are dropped. Large: background layers (position, size, repeat) and SVG rasterizing, for every host.
  4. Inline boxes lose their padding, margins and borders: the filter links run together (“AllActiveCompleted” in Vue, whose compiled template has no spaces between them) and the selected filter has no border. Either the inline layout (blitz and Parley) or the render stream; not narrowed down. Medium.
  5. niso-iced draws no rotations: the toggle-all chevron stays unrotated over the input (“iced draws only translations and uniform scales of a transform” in its log). hosts/iced. Small to medium.
  6. Box shadows (the card’s shadow and the stacked-paper footer) and the placeholder’s italic are missing in niso-iced. Small each; not narrowed down.

Fixed before this check: a click on a link to a fragment did nothing, so Svelte’s and React’s filters never changed, and fragment navigations fired no popstate, which React Router’s hash history listens for.

QuickJS in niso-iced, after the flow Chrome, the same DOM (canvases blank) Node’s DOM in niso-iced

With the probes, the flow passes on all three engines: the rectangle and the arrow are drawn on the static canvas, text goes in through the text editor, the rectangle moves, Ctrl+Z and the redo button work, and export gives a 420x255 canvas with ink and a valid PNG of it. What breaks, most harmful first:

  1. No Selection API: every pointerdown on the canvas throws at once (document.getSelection is not a function: right after capturing the pointer, Excalidraw clears the document’s selection), so nothing can be drawn, with real input too, on every engine and host. niso has no Selection, no window.getSelection or document.getSelection, and no Range or StaticRange. Large: the Selection API and Range on the core’s DOM (DECISIONS, Editing), in a crate of their own; WPT’s selection/ and dom/ranges/ are the checklist.
  2. No CSS Font Loading API: FontFace and document.fonts don’t exist. Excalidraw’s font loading rejects at startup, and the text tool and export throw (“FontFace is not defined”), so there is no text and no export. Medium to large: FontFace and FontFaceSet over the fonts @font-face already loads, in their own crate; WPT’s css/css-font-loading/ is the checklist.
  3. QuickJS-ng has no RegExp legacy statics (RegExp.$1): Rough.js parses every shape’s SVG path with them, so on QuickJS the first shape throws inside React’s render and the whole editor unmounts. An engine gap (third_party/quickjs); Node and Bun have them. Small to medium in QuickJS-ng’s regexp exec (the last match kept on the realm), best done upstream.
  4. Inline SVG isn’t drawn: every toolbar and panel icon is an empty button. The render stream has no op for <svg> content (crates/niso-core/src/render.rs). Large: SVG rendering for every host (rasterized with usvg as a first step).
  5. Visually hidden headings show (“Shapes”, “Actions”, “Canvas actions”): Excalidraw’s .visually-hidden (1x1 px, overflow: hidden, clip: rect(1px, 1px, 1px, 1px)) doesn’t hide them in niso-iced. Either clip or the overflow clip of a 1 px box; not narrowed down. Small to medium.
  6. Missing UI Events members that React and Excalidraw look up (niso-run --missing): MouseEvent.relatedTarget, movementX, movementY and getModifierState, and FocusEvent, CompositionEvent, ClipboardEvent, DragEvent, HTMLElement.contentEditable and HTMLLinkElement.relList. React synthesizes mouseenter and mouseleave from relatedTarget, so hover states can go wrong. Small to medium each.

Fixed before this check: under Node and Bun, a module script’s own module wasn’t in the module map, so Vite’s chunks, which import back from the entry, ran it a second time; Excalidraw mounted twice into one container, React’s second mount failed in removeChild, and the editor never came up. setPointerCapture threw NotFoundError for the mouse until a host reported it; the mouse is now pointer 1 and always active, as in Chrome (DECISIONS, Editing). Native radio groups replaced Excalidraw’s own radio buttons, which it hides (opacity: 0) to draw its icons; such a group now stays plain (DECISIONS, Elements). The screenshots above predate both.

  • Drivers dispatch DOM events from page script, so their events are untrusted and skip the host’s input path (hit testing, pointer capture, focus, the compatibility mouse events). niso-wpt’s testdriver drives WPT’s tests through niso-actions (niso_dom_pointer and the other input calls); a page-facing bridge to it in niso-run and niso-node would let drivers give real input.
  • niso-iced runs only QuickJS, so Node’s and Bun’s screenshots show their final DOM without scripts or canvas content.