Frameworks
PocketJS supports two first-class app frameworks over the same native tree and Rust core:
| Framework | Build id | JSX transform | Runtime renderer | Output suffix |
|---|---|---|---|---|
| Solid | solid |
babel-preset-solid universal mode |
renderer-solid.ts |
none |
| Vue Vapor | vue-vapor |
vue-jsx-vapor |
renderer-vue-vapor.ts |
.vue-vapor |
Solid is the default so existing apps keep building to dist/<app>.js and
dist/<app>.pak. Vue Vapor builds next to it:
bun scripts/build.ts hero-main # dist/hero-main.js
bun scripts/build.ts hero-vue-vapor-main --framework=vue-vapor
# dist/hero-vue-vapor-main.vue-vapor.jsThere is no environment-variable switch for framework selection. Product
builds declare it in pocket.json; low-level compiler work can still use a
project config or one-command override.
Manifest selection
{
"app": {
"framework": "solid"
}
}Use "vue-vapor" for the Vue adapter. pocket check|compile|build --target …
resolves this value once and all compiler/native stages consume the same plan.
Do not also put framework in pocket.config.ts for a manifest build.
Low-level project config
pocket.config.ts is the low-level script default:
import { definePocketConfig } from "@pocketjs/framework/config";
export default definePocketConfig({
framework: "solid",
});Use Vue Vapor by changing the file:
export default definePocketConfig({
framework: "vue-vapor",
});The direct compiler/dev scripts read the config by default. Use
--framework=solid or --framework=vue-vapor to override it for one
invocation. --config=<path> selects a different config file, and
--no-config ignores config entirely.
The same flag works through the dev and PSP entry points:
bun scripts/dev.ts --framework=vue-vapor hero-vue-vapor-main
bun scripts/psp.ts hero-vue-vapor --framework=vue-vapor --releaseFramework app imports
Apps import state and component lifecycle from the selected framework directly.
PocketJS does not wrap createSignal, ref, onMount, or onMounted.
Solid app:
import { mount, frameworkName } from "@pocketjs/framework/solid";
import { View, Text, type NodeMirror } from "@pocketjs/framework/solid/components";
import { createSignal, onMount, Show } from "solid-js";
export default function App() {
const [count, setCount] = createSignal(0);
let marker: NodeMirror | undefined;
onMount(() => {
console.log(frameworkName(), marker?.id);
});
return (
<View class="p-4 flex-col gap-2">
<Text class="text-base text-slate-950">Framework: {frameworkName()}</Text>
<View nodeRef={(node) => (marker = node ?? undefined)} 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 app:
import { mount, frameworkName } from "@pocketjs/framework/vue-vapor";
import { View, Text, type NodeMirror } from "@pocketjs/framework/vue-vapor/components";
import { onMounted, ref } from "vue";
export default function App() {
const count = ref(0);
let marker: NodeMirror | undefined;
onMounted(() => {
console.log(frameworkName(), marker?.id);
});
return (
<View class="p-4 flex-col gap-2">
<Text class="text-base text-slate-950">Framework: {frameworkName()}</Text>
<View nodeRef={(node) => (marker = node ?? undefined)} 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);The generic public subpaths remain Solid-first defaults. Use explicit framework subpaths when an example or app is tied to a framework:
| Import | Solid build | Vue Vapor build |
|---|---|---|
@pocketjs/framework |
src/index.ts |
src/index-vue-vapor.ts |
@pocketjs/framework/components |
src/components.ts |
src/components-vue-vapor.ts |
@pocketjs/framework/lifecycle |
Solid lifecycle hooks | Vue Vapor lifecycle hooks |
Use nodeRef when a component should look similar across framework examples. Solid still supports
ref, but nodeRef avoids framework-specific ref semantics.
Explicit framework subpaths
When you intentionally want one framework, import it directly:
import { mount } from "@pocketjs/framework/solid";
import { View } from "@pocketjs/framework/solid/components";import { mount } from "@pocketjs/framework/vue-vapor";
import { View } from "@pocketjs/framework/vue-vapor/components";Explicit subpaths are useful for framework-specific examples, tests, and integration code. Most apps should prefer the generic PocketJS subpaths and keep framework state imports native.
What stays shared
Both frameworks use the same Tailwind-subset compiler, generated style table,
font atlas baker, .pak asset container, host detection, input/focus system,
overlay layer, animation API, PSP/Vita native build paths, browser dev host, and
PPSSPP/Vita3K capture paths. Switching frameworks changes only the JS
component/reactivity layer and renderer adapter.