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.

PocketJS runs your JSX app through one of two framework adapters — Solid or Vue Vapor — each of which compiles your component code and drives the same retained-mode Rust UI tree. Choosing a framework changes only the JavaScript reactivity layer and the JSX transform; the Rust core, the Tailwind-subset compiler, font atlases, input model, animation API, PSP native build, and .pak asset container are all completely shared. The compiled output (a .js bundle and a .pak file) is byte-for-byte the same shape regardless of which adapter you picked.

The adapter model

Both frameworks target the same HostOps surface — the ui.* op set (createNode, setStyle, insertBefore, setText, and so on) that the Rust core exposes to JavaScript. On the PSP that surface is QuickJS FFI bindings; in the browser and headless Bun it is the WebAssembly wrapper. A framework adapter is the thin renderer layer that translates JSX tree mutations into those ops. Solid does it through babel-preset-solid’s universal renderer output; Vue Vapor does it through vue-jsx-vapor plus a small DOM facade the Vapor runtime expects. The JS mirror tree, focus manager, animation scheduler, overlay root, and every component in @pocketjs/framework/components are shared across both adapters. Switching frameworks costs nothing at the Rust boundary.
The built artifact — dist/<app>.js plus dist/<app>.pak — is structurally identical regardless of which framework compiled it. The .pak always contains the same ui:styles, ui:font.*, and ui:img.* entries baked from the same class/charset scan. Only the JS layer differs.

Solid (default)

Solid is the default adapter. Apps import reactive primitives directly from solid-js and mount with @pocketjs/framework.
  • JSX transform: babel-preset-solid { generate: 'universal', moduleName: <absolute path to src/renderer-solid.ts> } — compiled into universal renderer calls that create native nodes instead of DOM elements.
  • Types: @babel/preset-typescript strips types in the same Babel pass.
  • Main entry: @pocketjs/framework (maps to src/index.ts)
  • Components: @pocketjs/framework/components (maps to src/components.ts)
  • Lifecycle: onMount, onCleanup, createEffect — imported directly from solid-js.
  • Control flow: Show, For, Index, Switch, Match — imported from solid-js as usual.
  • Output suffix: none — produces dist/<app>.js and dist/<app>.pak.
import { createSignal, onMount, Show } from "solid-js";
import { mount, frameworkName } from "@pocketjs/framework";
import { View, Text } from "@pocketjs/framework/components";

export default function App() {
  const [count, setCount] = createSignal(0);

  return (
    <View class="p-4 flex-col gap-2">
      <Text class="text-base text-slate-950">Framework: {frameworkName()}</Text>
      <View focusable onPress={() => setCount(count() + 1)}>
        <Text class="text-sm text-blue-600">Count: {count()}</Text>
      </View>
      <Show when={count() > 2}>
        <Text class="text-sm text-emerald-600">Solid, native tree.</Text>
      </Show>
    </View>
  );
}

mount(() => <App />);

Vue Vapor

The Vue Vapor adapter uses vue-jsx-vapor to compile JSX into Vapor renderer calls. Components return render functions (() => JSX.Element) rather than JSX directly. Reactive state is Vue ref/reactive; lifecycle hooks come from vue.
  • JSX transform: vue-jsx-vapor — compiles JSX with the Vue Vapor codegen, bundled against src/renderer-vue-vapor.ts plus the Vapor runtime facade.
  • Main entry: @pocketjs/framework/vue-vapor (maps to src/index-vue-vapor.ts)
  • Components: @pocketjs/framework/vue-vapor/components (maps to src/components-vue-vapor.ts)
  • Lifecycle: onMounted, onScopeDispose, watchEffect — imported from vue.
  • Control flow: Ternaries and .map() instead of <Show>/<For> (standard Vue Vapor pattern).
  • Output suffix: .vue-vapor — produces dist/<app>.vue-vapor.js and dist/<app>.vue-vapor.pak.
import { ref, onMounted } from "vue";
import { mount, frameworkName } from "@pocketjs/framework/vue-vapor";
import { View, Text } from "@pocketjs/framework/vue-vapor/components";

export default function App() {
  const count = ref(0);

  return () => (
    <View class="p-4 flex-col gap-2">
      <Text class="text-base text-slate-950">Framework: {frameworkName()}</Text>
      <View focusable onPress={() => count.value++}>
        <Text class="text-sm text-blue-600">Count: {count.value}</Text>
      </View>
      {count.value > 2 ? (
        <Text class="text-sm text-emerald-600">Vue Vapor, native tree.</Text>
      ) : null}
    </View>
  );
}

mount(App);
Vue Vapor components must return a render function (() => JSX.Element), not JSX directly. The Vapor runtime calls the render function on each reactive update. If you return JSX from the component body itself, reactivity will not track correctly.

Framework selection

There are three ways to pick a framework, in order of precedence:

1. pocket.config.ts (project default)

Create pocket.config.ts at the repo root to set the default for all build commands:
import { definePocketConfig } from "@pocketjs/framework/config";

export default definePocketConfig({
  framework: "solid",
});
Switch to Vue Vapor:
import { definePocketConfig } from "@pocketjs/framework/config";

export default definePocketConfig({
  framework: "vue-vapor",
});

2. --framework CLI flag (per-invocation override)

Pass --framework=solid or --framework=vue-vapor to any build script to override the config file for a single command. The flag works across all three entry points:
bun scripts/build.ts hero --framework=solid
bun scripts/build.ts hero-vue-vapor-main --framework=vue-vapor

bun scripts/dev.ts --framework=vue-vapor hero-vue-vapor-main

bun scripts/psp.ts hero --framework=solid --release
bun scripts/psp.ts hero-vue-vapor --framework=vue-vapor --release

3. CLI flag wins

When both pocket.config.ts and --framework are present, --framework always wins. Use --no-config to ignore the config file entirely (defaults to Solid unless --framework is also set). Use --config=<path> to load a different config file.

Import path reference

Both frameworks export the same logical surface through different subpaths. The generic paths (without solid/ or vue-vapor/ prefix) resolve to Solid in a Solid build and to Vue Vapor in a Vue Vapor build — the compiler rewrites them at bundle time.
PurposeSolidVue Vapor
Mount@pocketjs/framework@pocketjs/framework/vue-vapor
Components@pocketjs/framework/components@pocketjs/framework/vue-vapor/components
Lifecycle@pocketjs/framework/lifecycle@pocketjs/framework/vue-vapor/lifecycle
Animation@pocketjs/framework/animation@pocketjs/framework/vue-vapor/animation
Input@pocketjs/framework/input@pocketjs/framework/vue-vapor/input
You can also import a specific framework explicitly regardless of the build setting:
// Always Solid, regardless of pocket.config.ts
import { mount } from "@pocketjs/framework/solid";
import { View } from "@pocketjs/framework/solid/components";

// Always Vue Vapor
import { mount } from "@pocketjs/framework/vue-vapor";
import { View } from "@pocketjs/framework/vue-vapor/components";
Explicit subpaths are useful for framework-specific demos, tests, and integration code. Most apps should prefer the generic paths and keep framework state imports (createSignal, ref) native to whichever framework they chose.

Side-by-side counter example

The same counter component written in both frameworks:
import { createSignal, Show } from "solid-js";
import { mount } from "@pocketjs/framework";
import { View, Text, Screen } from "@pocketjs/framework/components";

function Counter() {
  const [count, setCount] = createSignal(0);
  return (
    <Screen>
      <View class="flex-col items-center justify-center gap-4 w-full h-full">
        <Text class="text-2xl text-slate-950 font-bold">Count: {count()}</Text>
        <View
          class="px-4 py-2 rounded-xl bg-blue-600 border-blue-500 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() >= 5}>
          <Text class="text-sm text-emerald-600">Five or more!</Text>
        </Show>
      </View>
    </Screen>
  );
}

mount(() => <Counter />);

QuickJS constraints on PSP

The PSP runs your bundle through QuickJS (Bellard 2025, ~ES2023). QuickJS supports Proxy, WeakMap, WeakRef, FinalizationRegistry, and logical assignment, but it is missing setTimeout, MessageChannel, and performance. A polyfill provides queueMicrotask via Promise.resolve().then() for the basic microtask case. As a result, certain Solid APIs that rely on the scheduler are off-limits on PSP and produce a hard compile error if imported:
Banned importReason
createResourceRequires async scheduling (no setTimeout)
useTransitionRequires concurrent scheduling
startTransitionSame
Use signals + createEffect for reactive data fetching, and animate() from @pocketjs/framework/animation for motion. These patterns work identically across PSP, browser, and headless Bun.
The compiler throws a code frame error — not a warning — if any of these are imported from solid-js. This is intentional: a miscompile that silently no-ops on the PSP is far harder to debug than a build failure. If you are targeting browser-only builds, you can suppress specific checks via --no-config and a custom entry point, but the PSP EBOOT build will always enforce them.

What stays the same across both frameworks

Switching from Solid to Vue Vapor (or back) does not change:
  • The Rust no_std core, taffy layout engine, and sceGu backend
  • The Tailwind-subset compiler and generated styles.generated.ts
  • Font atlas baking and the baked charset
  • The .pak container format and its ui:styles, ui:font.*, ui:img.* entries
  • Host detection and the HostOps interface
  • The input and focus manager
  • The overlay root and portal mechanism
  • The animation API (animate(), spring())
  • The PSP EBOOT build pipeline
  • Every component in @pocketjs/framework/components
  • The browser dev host and PPSSPP capture path
The only thing that changes is the JS component/reactivity layer and the renderer adapter that bridges it to the native tree.

Build docs developers (and LLMs) love