JSX in. Custom elements out.
Compile-time templates, signal reactivity, file-system routing, SSR + real hydration — in 4.5 kB gzipped. Components ship as native web components, so anything that speaks HTML can consume them.
npm install @jjordy/rogue
// no virtual DOM
JSX compiles to a hoisted template once. Dynamic slots are bound by path. No re-renders, only the bytes that change.
// fine-grained signals
Signals + effects with a unified Scope lifetime that auto-cascades cleanup. Solid-style reactivity, no diff.
// SSR + real hydration
Server renders to Declarative Shadow DOM. Client adopts existing nodes via pair-anchor markers — text mutates in place.
// file-system routing
src/pages/ → URLs. [slug].jsx, [...rest].jsx, _layout.jsx, _404.jsx, _error.jsx. Each route code-splits.
// web standards out
defineComponent registers a real custom element. Shadow DOM, adopted stylesheets, form-association via ElementInternals.
// forms that work without JS
useForm + defineActions: schema validation, server-side mutations, auto-revalidated loaders, real form POST when JS hasn't loaded.
// end-to-end types
The router plugin emits typed Params, LoaderArgs, and schema-derived ActionArgs per route. No annotation that duplicates the file path or schema.
// 4.2 kB gzipped
Initial-load bundle for a multi-page app. Forms, validation, and SSR helpers tree-shake or lazy-load.
The full example
// src/components/my-counter.jsx
import { defineComponent, signal } from '@jjordy/rogue'
defineComponent(({ start = 0 }) => {
const [count, setCount] = signal(start)
return (
<div>
<button onClick={() => setCount(count() - 1)}>−button>
<span>Count: {count()}span>
<button onClick={() => setCount(count() + 1)}>+button>
div>
)
})
// Use anywhere:
//
That's it. Filename → tag name. Destructured defaults → observed attributes with inferred types. No imports to wire up, no class to define, no bundler config beyond the Vite plugin.