Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/pocket-stack/pocketjs/llms.txt

Use this file to discover all available pages before exploring further.

This guide takes you from an empty checkout to a reactive JSX component running in the browser dev host — the same bundle that also runs on real Sony PSP hardware, PPSSPP, and headless Bun. If you want to explore PocketJS without installing anything, skip directly to the Playground and come back here when you’re ready for the local workflow.
The Playground loads the same wasm32 Rust core in your browser. Pick Solid or Vue Vapor in the toolbar, edit JSX in the editor, and it renders live — no local install required.
1

Prerequisites

The JavaScript workflow needs one tool. The Rust toolchains are only required when you want to compile the core natively — you don’t need them to write UI or run the browser dev host.
You want to…You need
Write components, run the build pipelineBun
Run the browser dev hostBun + Rust with rustup target add wasm32-unknown-unknown
Ship a PSP EBOOT and run on hardware or PPSSPPRust nightly (nightly-2026-05-28) + cargo-psp
The first bun scripts/dev.ts run compiles the Rust core to wasm32 with cargo build. That takes a moment. Subsequent runs skip it and are fast.
2

Clone and install

git clone https://github.com/pocket-stack/pocketjs
cd pocketjs
bun install
bun install pulls solid-js, the Vue Vapor packages, and all build-time tooling: the Babel + Tailwind-subset compiler, the font baker, and the dev host. There is no separate runtime to install — @pocketjs/framework is the package in this repo, exposed through subpath imports like @pocketjs/framework/components.
3

Write your first component

Create demos/hero/app.tsx. Layout is flexbox via View, text via Text, and styling via class — a build-time subset of Tailwind, not runtime CSS. State comes from the selected framework.
// demos/hero/app.tsx
import { createSignal, Show } from "solid-js";
import { Text, View } from "@pocketjs/framework/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>
  );
}
What is happening here:
  • View is the layout primitive — a flexbox node. Text is the text primitive. Both are imported from the framework’s /components subpath.
  • focusable opts the View into d-pad focus traversal. onPress fires when that focused node is confirmed (Circle button on PSP, Enter or Space in the browser host).
  • focus:bg-blue-500 and active:bg-blue-700 are style variants baked into the binary style record. Focus changes swap style IDs natively — zero JS per transition.
  • transition-colors duration-150 declares a color tween. The tween ticks in Rust at a fixed dt = 1/60 s; JS only declares it.
  • Show / the conditional expression is standard framework control flow. PocketJS handles node insertion and removal on both sides of the FFI.
  • Count: {count()} / Count: {count.value} is a mixed text run. The static string and the reactive value are laid out as one inline run by the text engine.
4

Write the mount entry

app.tsx exports a component but doesn’t put anything on screen. The mount entry does that. Put it in demos/hero/main.tsx:
// demos/hero/main.tsx
// @title PocketJS: Hero
import App from "./app.tsx";
import { mount } from "@pocketjs/framework";

mount(() => <App />);
mount is imported from @pocketjs/framework (the package root). It:
  • Detects the current host (PSP / browser / headless Bun)
  • Registers the generated style table
  • Uploads images from the packed asset file on injected hosts (browser / Bun)
  • Installs the per-frame host callback
You never manage any of that yourself. Both adapters take a render function: mount(() => <App />).
5

Build your app

One command transforms your JSX, compiles the styles it actually uses, bakes only the glyphs it actually renders, and bundles everything:
bun scripts/build.ts hero
This produces two files in dist/:
FileWhat it is
dist/hero.jsYour app bundled to a single IIFE (unminified) that any host loads
dist/hero.pakThe packed asset file: compiled style table, baked font atlases, and images
Vue Vapor builds use a .vue-vapor suffix — dist/hero.vue-vapor.js and dist/hero.vue-vapor.pak.The build is two-pass:
  1. Transform & collect. Babel runs over every module reachable from your entry (Solid’s universal preset + TypeScript), content-hash cached in .cache/. As it goes it collects every class literal and every text codepoint from the AST. The Tailwind-subset compiler validates tokens and writes styles.bin; the font baker rasterizes an Inter atlas containing only the characters your app uses; everything is packed into dist/hero.pak.
  2. Bundle. Bun bundles the app (IIFE, target: "browser", unminified) from the cached pass-1 transforms into dist/hero.js.
A class literal only compiles if every whitespace-separated token is a supported utility. A single unsupported token silently drops the entire literal at compile time. Check the supported utilities in the Tailwind subset reference, and run the build to see which classes were rejected.
You can force extra characters into every font atlas when text content is data-driven and not visible in your source:
bun scripts/build.ts hero --extra-chars="0123456789€"
6

Run in the browser dev host

The dev host builds the wasm32 Rust core, builds the mounted demo, and serves it all in one command:
bun scripts/dev.ts
# or: bun run dev
Open http://127.0.0.1:8130/ in your browser. You’ll see a 480×272 canvas running the demo with virtual buttons for input.To target a specific demo or change the port:
bun scripts/dev.ts hero-main
PORT=9000 bun scripts/dev.ts
There is no live reload. After editing a component, re-run bun scripts/build.ts hero (or the full bun scripts/dev.ts) and reload the page manually. The first run compiles the Rust core with cargo build; subsequent runs skip that step and are much faster.
7

(Optional) Ship to PSP hardware

With Rust nightly and cargo-psp installed, one command wraps your bundle into a PSP EBOOT:
bun scripts/psp.ts hero
This runs the same two-pass build, then calls cargo psp with the correct environment for the mipsel-sony-psp target. The output is a EBOOT.PBP ready for ms0:/PSP/GAME/. For Vue Vapor:
bun scripts/psp.ts hero --framework=vue-vapor
To run on real hardware over PSPLINK:
bun run hw hero --trace
The PSP build requires Rust toolchain nightly-2026-05-28 exactly. Other nightly versions are not guaranteed to produce a working MIPS binary. Pin the toolchain with rustup override set nightly-2026-05-28 inside the repo.

Framework selection

Framework selection is explicit. The default (from pocket.config.ts) is Solid:
// pocket.config.ts
import { definePocketConfig } from "@pocketjs/framework/config";

export default definePocketConfig({
  framework: "solid",
});
Switch to Vue Vapor either by changing the config file or by passing --framework=vue-vapor to any of the scripts:
bun scripts/build.ts hero --framework=vue-vapor
bun scripts/dev.ts --framework=vue-vapor
bun scripts/psp.ts hero --framework=vue-vapor

What’s next

  • Architecture — how one Rust core drives all four hosts, and why the JS mirror tree never reads across FFI.
  • ComponentsView, Text, Image, and app-shell primitives like FocusGrid, Modal, and Portal.
  • Styling — the full supported Tailwind utility set, variant syntax, and dynamic styling patterns.
  • Animation — the animate() and spring() APIs, transition-* classes, and fixed-dt animation tracks.
  • Input & focus — d-pad traversal, onPress, FocusScope, and what happens when a focused node is removed.

Build docs developers (and LLMs) love