The same counter on a phone
Two hand-written proofs drive a counter on iOS UIKit and macOS AppKit through JavaScriptCore, from a graph-shaped artifact instead of the DOM.
Last page, the graph and its symbols turned out to be data plus small functions. Does a browser have to run them?
The Markless README states the goal: one web-like component model for web and native apps. Today, two small proofs test that idea. Neither one is a product.
The problem
The web build ends in DOM operations, such as setText. A phone has no DOM. A native button needs its own control, its own event, and its own way to set a title.
So the question is narrow. Can a graph-shaped description of a counter drive real native controls, with the handler still written in JavaScript?
The mental model
Think of sheet music. That is an analogy. The score says which notes play and when. Each instrument plays the same score in its own way.
Here the score is the artifact. It lists state cells, host nodes, event records, text bindings, and symbol bodies. A small Swift runtime is the instrument. It builds UIKit or AppKit controls from the host nodes.
Press Count 0. Then pick another host and press it again.
What stays the same on a native host, and what changes?
Press Count 0. Then pick another host and press it again.
The event record for this host
{ hostNodeId: 'h0', eventName: 'click', (changes) symbolIds: ['symbol:click'] }Event record in the view payload. Shape from packages/web/test/render.test.ts.
import { state } from '@markless/core'; export function Counter() @{ let count = state(0); <main> <button onClick={() => count++}>Count {count}</button> </main>}Same in every hostsame
- State
state:count, starts at 0- Event record
- The button's
onClickruns the handler - Text binding
- Button text
Count {count} - Symbol
- The handler: add one to
count
Changes with the host
- Control
<button>- Event name
click- Listens with
- One capture listener on the root:
root.addEventListener(eventName, listener, { capture: true }) - Sets the title
- Journal entry
setTextsetstextContent - Records written by
- The Markless compiler
- JavaScript runs in
- The page
- noteReady. Press the button in the window.
Simplified. The browser record has the shape that @markless/web reads. This one comes from packages/web/test/render.test.ts. The iOS and macOS hosts are hand-written proofs in poc/fixtures/proofs/. A person wrote their artifact.json, and no Markless package emits it. The proofs add an <h1> heading that is not shown here.
Notice that state:count, the handler, and the text binding stay the same. Only the control, its event name, and the title write change.
The proof
The iOS proof starts from this component. The macOS proof differs only in its heading.
import { state } from '@markless/core';
export function Counter() @{
let count = state(0);
<main>
<h1>Markless iOS Proof</h1>
<button onClick={() => count++}>Count {count}</button>
</main>
}
The hand-written artifact maps the authored onClick to a semantic activate event. Then it names the host event. On iOS that is touchUpInside. On macOS it is action.
{
"node": "host:button",
"authoredEvent": "onClick",
"semanticEvent": "activate",
"nativeEvent": "touchUpInside",
"symbolId": "symbol:counter.increment"
}
The symbol body is also hand-written JavaScript:
graph["state:count"] = graph["state:count"] + 1;
What the Swift runtime does
- It creates a
JSContextfrom JavaScriptCore, and a plaingraphobject withstate:countset to0. - It turns each host node into a control. On iOS,
mainbecomes aUIStackViewandbuttonbecomes aUIButton. - It connects the native event to the symbol ID.
- On a tap, it runs the symbol in JavaScriptCore.
- Then it re-reads every text binding and sets the button title, such as
Count 1.
The macOS proof does the same with an NSButton and its action event. In both proofs, an XCTest checks that the title changes from Count 0 to Count 1.
What the proofs do not show
The proofs are narrow on purpose. Keep these limits in mind:
- They do not use
@markless/runtime. The graph is a plain JavaScript object. - They update every text binding after each tap. They do not use path subscriptions.
- They do not cover styling, Android, Windows, Linux, or production packaging.
- A check script in each proof makes sure that the files name no web view or cross-platform shell.
Misconception: Markless ships native apps today. It does not. The proofs show that the artifact shape can drive native controls. The compiler does not produce that artifact yet.
Next: That is the whole engine, from compiler to native proof. Where does each part live in the source? Repo tour →
