Overview
PocketJS is a portable application runtime that turns modern component code
into native pixels across radically different hardware. You write Solid,
Vue Vapor, or Octane components; the build compiles class strings and
font glyphs into binary tables, and a compact no_std Rust core renders
flexbox layout, sub-pixel text, and native animation from them. One application
manifest resolves into one target-specific artifact — a PSP EBOOT, a Vita VPK,
a native window, a browser bundle — and each of them drives the same logical UI
through the same HostOps op set.
If you know Solid, Vue, or React, you know most of PocketJS. The primitives are
View, Text, and Image; state comes from the native framework package
(solid-js, vue, or octane); layout and color come from class strings like
flex-col items-center gap-4 bg-slate-50. What is different is what happens
underneath: there is no browser DOM and no runtime CSS.
One core, every host
Everything renders through one Rust core, pocketjs-core — a
platform-agnostic #![no_std] library that owns the retained node tree,
taffy flexbox layout, the style table,
animation tracks, baked text, and a DrawList. Each host compiles that core
for its architecture and pairs it with a backend: sceGu on PSP, vita2d/GXM on
Vita, gpui on macOS and Linux, and one deterministic software rasterizer shared
by the browser host and headless Bun.
Two places in the repo decide what you can build for:
hosts/holds one directory per host implementation, from consoles and e-readers through phones, ESP-IDF firmware, and the browser.contracts/spec/platforms.tsregisters the stock targets thatpocket build --target <id>accepts, each with its host ABI, logical viewport, raster density, presentations, and capability set. A host outside that registry builds through its own host profile or a resolved build plan — see Platform contracts.
Layout runs in one place — the Rust core — so a screen lays out the same
everywhere. The browser and Bun hosts share one rasterizer, which is what makes
byte-exact golden images possible. See
Architecture for the full picture and
Native contract for the ui.* op set that bridges JS
and Rust.
Framework adapters over one native tree
Solid is the default adapter and uses babel-preset-solid universal mode. Vue
Vapor uses vue-jsx-vapor. Octane — React's programming model, compiled — uses
the Octane universal compiler, which lowers JSX to static host plans plus
dynamic slots. All three target the same retained native tree and HostOps
surface, so switching framework changes the JS component and reactivity layer,
not the Rust core, styling pipeline, input model, or asset pack. See
Frameworks.
A build-time Tailwind subset, with no runtime CSS
Class strings are parsed at build time. A literal like class="p-2 rounded-md bg-blue-600" compiles to a numeric style record iff every whitespace-
separated token is a supported utility; the compiler writes a binary style
table (styles.bin) plus a generated lookup, and at runtime a class is a
styleId. There is no CSS engine on the device. Dynamic styling is expressed
as ternaries of whole class literals, style={{…}} objects, or animate().
classList, hover:, and template-interpolated class fragments are compile
errors, not silent no-ops. See Styling.
Baked font atlases
Text draws from atlases baked at build time. The default faces in
assets/fonts/ are Inter for the regular and bold slots and JetBrains
Mono for the mono slots; bun tools/build.ts --font-regular= and
--font-bold= swap the proportional faces. The build scans your source for the
characters and font sizes you use and rasterizes only those atlas slots —
supersampled 8-bit coverage cells with proportional advances and a cmap.
Drawing text composites pre-baked coverage; the bundle carries no rasterizer.
Hosts that register the text.glyphs.runtime capability extend the atlases at
runtime for codepoints outside the baked set, and hosts that register
text.layout.native measure and shape through the platform text system. See
Build pipeline.
Current boundaries
PocketJS does not include:
hover:variants — a console has no pointer, andhover:inside an otherwise valid class literal is a compile error- percentage sizes other than
-full rounded-fullon runtime-sized nodes — it requires build-time-knownw-N h-Nin the same class literal- a runtime-resizable logical viewport outside the window and widget forms; console profiles bake one fixed logical size (PSP and Vita both 480×272)
- render-to-texture opacity groups — opacity multiplies vertex alpha down the subtree, which is wrong where siblings overlap
- kerning
These are omissions, not silent failures: unsupported class tokens and disallowed patterns surface as compile-time or dev errors. See the full list in Styling.