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 source and pocket.json can also be compiled into target-specific packages
for PSP, PS Vita, and the other registered targets. Your app declares one
logical viewport in pocket.json and the target profile decides whether that
size is admissible: PSP and Vita are fixed at 480×272, while window and widget
targets carry a default size and resize the core live. Each target owns its
native renderer, raster density, and HostOps ABI.
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 | bun run bootstrap (pinned Rust, cargo-psp, LLVM, and verified SDK) |
| Ship a PS Vita VPK | VitaSDK, cargo-vita 0.2.2, and Rust nightly 2026-05-28 with rust-src |
| Embed PocketJS in ESP-IDF | ESP-IDF 6.0 or 6.1; Registry releases include the P4/S3 native core archives |
| Hot-reload on real PSP hardware | The build toolchain above + optional PSPLINK host tools |
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.
Existing P4 and S3 firmware projects use the independent workflow in the
ESP-IDF guide.
Install
git clone https://github.com/pocket-stack/pocketjs
cd pocketjs
bun install
bun run bootstrap # one-time PSP setup; omit for browser-only developmentThe @pocketjs/cli package can
check (and mostly install) the toolchain for you, flutter-doctor style:
npm install -g @pocketjs/cli
pocket doctor # diagnoses bun, the Rust targets, and the PSP toolchain
pocket setup # runs the checkout's pinned, idempotent bootstrapThe PSP setup is self-contained in PocketJS; it does not inspect DreamCart or
any sibling source checkout. Its exact revisions and SDK checksum live in
tools/cli/psp-toolchain.json. Artifacts are shared through
${XDG_CACHE_HOME:-~/.cache}/pocket-stack; POCKET_STACK_CACHE_DIR overrides
that root. For a custom SDK, set PSP_SDK or PSPDEV (in that precedence
order). The build validates an explicit path and then exports both names to the
selected SDK, so a typo fails instead of falling through to a different cached
toolchain.
It also wraps the day-to-day commands. pocket create <name> scaffolds a
manifest-first demo; pocket check|compile|build --target psp|vita delegate to
the canonical resolver; pocket dev|psp|vita|hw|psplink retain the low-level
host-development paths; and pocket devtools [app] opens the
DevTools panel with the USB debug bridge.
That pulls solid-js, the Vue Vapor and Octane dependencies, and 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/framework package
in this repo, exposed through subpath imports like
@pocketjs/framework/components.
Create an app and validate it against both stock profiles:
pocket create my-app
pocket check --target psp --manifest apps/my-app/pocket.json
pocket check --target vita --manifest apps/my-app/pocket.jsonThe generated pocket.json is strict application intent:
{
"$schema": "https://pocketjs.dev/schema/pocket-2.json",
"pocket": 2,
"id": "dev.example.my-app",
"name": "my-app",
"title": "My App",
"version": "0.1.0",
"engine": {
"capabilities": {
"requires": ["text.glyphs.baked", "input.buttons"]
}
},
"app": {
"entry": "main.tsx",
"output": "my-app-main",
"framework": "solid",
"viewport": {
"logical": [480, 272],
"presentation": "integer-fit"
}
}
}Keep id stable across releases: the Vita backend derives the installed Title
ID from it. Put optional APIs such as input.touch under enhances and retain
a controller fallback; unsupported entries under requires fail before the
compiler runs.
Write your first component
A component 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 directly from the selected framework: createSignal in Solid,
ref in Vue Vapor, useState in Octane.
Solid is the default low-level framework. Manifest builds select Solid, Vue
Vapor, or Octane with app.framework in pocket.json; see
Frameworks for the full selection model.
pocket create writes an apps/my-app/app.tsx that counts CROSS presses
through onButtonPress from @pocketjs/framework/lifecycle. Replace it with
this focusable counter, which routes the same increment through d-pad focus and
onPress:
import { createSignal, Show } from "solid-js";
import { Text, View } from "@pocketjs/framework/solid/components";
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>
);
}import { ref } from "vue";
import { Text, View } from "@pocketjs/framework/vue-vapor/components";
export default function App() {
const count = ref(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.value}</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={() => {
count.value++;
}}
>
<Text class="text-base text-white font-bold">Press Circle</Text>
</View>
{count.value > 3 ? (
<Text class="text-sm text-emerald-600">Reactive on real hardware.</Text>
) : null}
</View>
);
}import { useState } from "octane";
import { Text, View } from "@pocketjs/framework/octane/components";
export default function App() {
const [count, setCount] = useState(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>
{count > 3 ? (
<Text class="text-sm text-emerald-600">Reactive on real hardware.</Text>
) : null}
</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 and the supported utilities are in Styling. 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()}/{count.value}/{`…${count}`}is a reactive read. When the setter or ref write runs, only thatTextupdates — no re-render of the whole native tree. (In Octane, a text run that mixes static and dynamic segments is written as one template literal.) 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 small — this is app bootstrap. pocket create
writes the Solid spelling to apps/my-app/main.tsx; the other two differ in
the import:
// @title PocketJS: My App
import App from "./app.tsx";
import { mount } from "@pocketjs/framework/solid";
mount(() => <App />);// @title PocketJS: My App
import App from "./app.tsx";
import { mount } from "@pocketjs/framework/vue-vapor";
mount(App);// @title PocketJS: My App
import App from "./app.tsx";
import { mount } from "@pocketjs/framework/octane";
// Pass the component itself: in Octane, JSX inside a call-argument arrow
// (mount(() => <App />)) is a compile error.
mount(App);mount comes from the selected framework runtime subpath. It handles host
detection (native PSP/Vita vs. injected browser/headless hosts), 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
Use the manifest path for product builds. It validates the app, resolves the target once, compiles target-specific assets, and dispatches the native backend:
pocket build --target psp --manifest apps/my-app/pocket.json -- --release
# hosts/psp/target/mipsel-sony-psp/release/EBOOT.PBP
export VITASDK="$HOME/vitasdk"
export PATH="$VITASDK/bin:$HOME/.cargo/bin:$PATH"
pocket build --target vita --manifest apps/my-app/pocket.json -- --release
# apps/my-app/dist/vita/my-app-main.vpkVita VPKs include PocketJS's default black 128x128 bubble icon and complete
LiveArea background/startup artwork, so a newly built application does not get
a blank bubble or generic launch gate. Custom native hosts call
packageVitaVpk() from @pocketjs/framework/vita-package; VPK-relative app
assets override matching defaults while missing artwork continues to inherit
PocketJS's complete set.
pocket compile --target … stops after the JS/pak artifacts for a custom native
host. pocket check --target … is read-only. Arguments after -- belong to the
selected native backend.
For framework work, the lower-level compiler remains available:
bun tools/build.ts herobun tools/build.ts hero --framework=vue-vaporbun tools/build.ts hero --framework=octaneThat density-1 development command writes the bundle and its packed assets to
dist/; the per-framework file names are in
Build pipeline. The dev host and the sim
are development paths rather than stock targets — see
Transitional dev targets.
A few notes on the low-level command:
The argument resolves against
apps/.hero→apps/hero/app.tsx. To build the mounted entry instead, targetmain.tsx— eitherbun tools/build.ts apps/hero/main.tsxor the shorthandbun tools/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 tools/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 tools/dev.ts # builds the wasm core + hero-main, then serves
# or: bun run devbun tools/dev.ts hero-mainbun tools/dev.ts --framework=vue-vapor hero-mainbun tools/dev.ts --framework=octane hero-mainOpen the printed URL, http://127.0.0.1:8130/. Pass demo names to build
specific ones, or set PORT:
bun tools/dev.ts hero-main cards
PORT=9000 bun tools/dev.tsRebuild-on-change is manual: after editing a component, re-run
bun tools/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 Vita3K
For stock demos, one command builds a manifest-driven VPK, installs it under a stable per-demo Title ID, and launches it:
pocket play vita hero
pocket play vita gallery --fullscreenVita3K is interactive here; --fullscreen controls the emulator window. The
application still uses the profile's 480×272 logical viewport rendered at
960×544 density 2. See the Vita host guide
for toolchain setup, key mappings, real-device installation, and the golden E2E.
Next steps
- Architecture — how one Rust core drives every host.
- Frameworks — switch between Solid, Vue Vapor, and Octane.
- Components —
View,Text,Image, control flow, and the app-shell primitives. - Styling — the compile-time class rules and the utility tables.
- Animation — native tweens, baked timelines, and sprite atlases.
- Input & focus — d-pad traversal, buttons, and focus scopes.