Getting started
This is the fastest path from an empty checkout to JSX running on screen. You'll write a component, mount it, build it, and see it in the browser dev host — the same bundle that also runs on a real Sony PSP, PPSSPP, and headless Bun.
If you only want to try PocketJS, skip the toolchain entirely and open the online Playground: it runs the Rust core as WebAssembly in your browser, so you can edit JSX and see it render with nothing installed. Everything below is the local workflow.
Prerequisites
The JavaScript workflow needs one tool. The Rust toolchains are only required for the targets that compile the core natively — you don't need them to write UI.
| You want to… | You need |
|---|---|
| Write components, build bundles | Bun (drives the build, tests, and dev host) |
| Run the local browser dev host | Bun + Rust with the wasm target (rustup target add wasm32-unknown-unknown) |
| Ship a PSP EBOOT / run on hardware | Rust nightly + cargo-psp |
The Rust core is no_std and gets built once per platform. For this guide we
stay on the JS side and let the dev host compile the wasm core for us.
Install
git clone https://github.com/pocketjs/pocketjs
cd pocketjs
bun installThat pulls solid-js plus the build-time tooling (the Babel + Tailwind-subset
compiler, the font baker, and the dev host). There is no separate runtime to
install — the framework is the @pocketjs/core package in this repo, exposed
through subpath imports like @pocketjs/core/components.
Write your first component
A component is a function that returns JSX. You lay out with View, draw text
with Text, and style with class — a build-time subset of Tailwind, not
runtime CSS. State comes from createSignal, exactly like Solid.
Here's a focusable counter. Put it in demos/hero/app.tsx:
import { Show, Text, View } from "@pocketjs/core/components";
import { createSignal } from "@pocketjs/core/reactivity";
export default function App() {
const [count, setCount] = createSignal(0);
return (
<View class="w-full h-full flex-col items-center gap-4 p-4 bg-slate-50">
<Text class="text-xl text-slate-950 font-bold">Count: {count()}</Text>
<View
class="px-4 py-2 rounded-xl shadow-md bg-blue-600 focus:bg-blue-500 active:bg-blue-700 transition-colors duration-150"
focusable
onPress={() => setCount(count() + 1)}
>
<Text class="text-base text-white font-bold">Press Circle</Text>
</View>
<Show when={count() > 3}>
<Text class="text-sm text-emerald-600">Reactive on real hardware.</Text>
</Show>
</View>
);
}What's happening:
- Layout is flexbox.
flex-col,items-center,gap-4,p-4compile to a layout the Rust core runs through taffy. See Components for the full element set. - Styling is class literals only. Each utility (
bg-blue-600,rounded-xl,text-white, …) is resolved at build time into a style table — there is no CSS at runtime. Thefocus:andactive:variants swap styles based on input state. Details in Styling and the exact supported utilities in Tailwind subset. focusableopts theViewinto d-pad focus, andonPressfires when the focused node is confirmed (the Circle button on a PSP). Focus and input are covered in Input & focus.{count()}is a reactive read. WhensetCountruns, only thatTextupdates — no re-render of the tree.Showmounts its children only whilewhenis truthy. More in Reactivity.
The mount entry
app.tsx exports a component but doesn't put anything on screen. The mount
entry does that. Keep it tiny — this is just app bootstrap. Put it in
demos/hero/main.tsx:
// @title PocketJS: Hero
import App from "./app.tsx";
import { mount } from "@pocketjs/core";
mount(() => <App />);mount is imported from the package root, @pocketjs/core. It handles host
detection (PSP vs. PPSSPP vs. browser vs. Bun), wiring the generated style table,
uploading images from the packed asset file, and installing the per-frame host
callback — you don't manage any of that yourself. (mount builds on the
lower-level render export from the same module; mount is what you want for an
app.)
Build it
One command transforms your app, compiles the styles it actually uses, bakes only the glyphs it actually renders, and bundles everything:
bun scripts/build.ts heroThis produces two files in dist/:
| File | What it is |
|---|---|
dist/hero.js |
Your app bundled to a single IIFE (unminified) that any host loads |
dist/hero.dcpak |
The packed asset file: the compiled style table, font atlases, and images |
A few notes on the command:
The argument resolves against
demos/.hero→demos/hero/app.tsx. To build the mounted entry instead, targetmain.tsx— eitherbun scripts/build.ts demos/hero/main.tsxor the shorthandbun scripts/build.ts hero-main, which emitsdist/hero-main.js. The dev host runs the mounted-mainbundle.--extra-chars=<string>forces extra codepoints into every font atlas — useful when text is data-driven and not present in the source:bun scripts/build.ts hero --extra-chars="0123456789€"
Run it
In the browser dev host
The dev host builds the wasm core, builds the mounted demo, and serves it:
bun scripts/dev.ts # builds the wasm core + hero-main, then serves
# or: bun run devOpen the printed URL, http://127.0.0.1:8130/. Pass demo names to build
specific ones, or set PORT:
bun scripts/dev.ts hero-main cards
PORT=9000 bun scripts/dev.tsRebuild-on-change is deliberately manual: after editing a component, re-run
bun scripts/build.ts <app> (or the whole dev script) and reload the page.
The first run compiles the Rust core to wasm with cargo, so it takes a moment;
subsequent runs are fast.
In the Playground
No local build at all: open the Playground, which loads the same wasm core in your browser. Edit JSX in the editor and it renders live — the quickest way to explore the component and styling surface before wiring up a local project.
What the build just did
bun scripts/build.ts is a two-pass build:
- Transform & collect. Babel (Solid's universal preset + TypeScript) runs
over every module reachable from your entry, content-hash cached in
.cache/. As it goes it collects every class literal and every text codepoint from the AST. The Tailwind-subset compiler turns the collected classes intostyles.bin, the font baker rasterizes an Inter atlas containing only the characters your app uses, images are decoded, and it's all packed intodist/<app>.dcpak. - Bundle. Bun bundles the app (IIFE, targeting the browser, unminified) from
the cached pass-1 transforms into
dist/<app>.js.
Because styles and fonts are derived from your source, a class literal only compiles if every token is a supported utility, and the atlas only holds glyphs you reference. The full mechanics — caching, the class/codepoint collection, the dcpak format — are in Build pipeline.
Next steps
- Architecture — how one Rust core drives every host.
- Components —
View,Text,Image, control flow, and the app-shell primitives. - Styling and Tailwind subset — the compile-time class rules.
- Reactivity and Animation — signals, effects, and native tweens.
- Input & focus — d-pad traversal, buttons, and focus scopes.