npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jjordy/rogue

v0.7.0

Published

JSX → web components compiler with fine-grained reactivity, file-system routing, and SSR/hydration. Vite plugin + tiny runtime.

Downloads

1,976

Readme

rogue

JSX → web components compiler with fine-grained reactivity, file-system routing, and SSR/hydration. Vite plugin + tiny runtime.

npm install @jjordy/rogue

Why

  • Web components are the output. Each defineComponent registers a real <my-thing> custom element with shadow DOM, constructible stylesheets, and form-association via ElementInternals. No virtual DOM, no diff. Your components are just custom elements other tools can consume.
  • Compile-time templates. JSX compiles to a hoisted <template> once, then clones per render. Dynamic spots are bound via firstChild/nextSibling paths computed at build time — no runtime walking, no key prop, no reconciliation.
  • Fine-grained reactivity. Signals + effects wrapped in a Scope lifetime that auto-cascades cleanup. No re-renders; only the bytes that need to change, change.
  • SSR + real hydration. server/render.js SSRs to Declarative Shadow DOM. The runtime adopts existing DOM via pair-anchor markers (<!--[-->...<!---->) — text nodes mutate in place, structure is preserved.
  • File-system routing. src/pages/ with [slug].jsx / [...rest].jsx / _layout.jsx / _404.jsx / _error.jsx. Each route is its own code-split chunk.
  • Forms that work without JavaScript. useForm + defineActions schema validation, server-side mutations, auto-revalidated loaders. With JS the form posts via fetch and patches state in place; without JS, native form POST + server re-render with errors embedded. One declaration site for client + server.
  • End-to-end types. The router plugin emits typed ./<stem>.types modules per page so params and data are inferred from the file path and the action's schema, with no annotation that duplicates either.

Quickstart

vite.config.js:

import { defineConfig } from 'vite'
import { rogue, rogueRouter, rogueSsr } from '@jjordy/rogue/vite'

export default defineConfig({
  plugins: [rogue(), rogueRouter(), rogueSsr()],
  esbuild: { jsx: 'preserve' }, // let the rogue plugin handle JSX
})

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>
      <style>{`
        button { width: 2rem }
        span { margin: 0 .5rem }
      `}</style>
    </div>
  )
})

src/pages/index.jsx:

export default function Home() {
  return (
    <div>
      <h1>Hello</h1>
      <my-counter start={5} />
    </div>
  )
}

src/main.ts:

import { mount } from '@jjordy/rogue/router'

const target = document.getElementById('app')
if (!target) throw new Error('mount target #app not found')

const ssr = window.__JSX_WC_DATA__
mount(target, ssr ? { initialData: ssr.data, initialParams: ssr.params } : {})

Forms

// src/pages/signup.tsx
import { useForm, defineActions, redirect, invalid } from '@jjordy/rogue/forms'

export const actions = defineActions({
  signup: {
    schema: {
      email:    { type: 'email', required: 'Email is required' },
      password: { type: 'password', required: true, minLength: 8 },
    },
    async run({ data }) {
      if (await isTaken(data.email)) return invalid({ email: 'already registered' })
      await createUser(data)
      return redirect('/welcome')
    },
  },
})

export default function Signup() {
  const form = useForm(actions.signup)
  return (
    <form {...form.props}>
      {form.formError() && <div class="alert">{form.formError()}</div>}
      <input {...form.field('email')} />
      <span class="err">{form.errors().email}</span>
      <input {...form.field('password')} />
      <span class="err">{form.errors().password}</span>
      <button disabled={form.submitting()}>
        {form.submitting() ? 'Creating…' : 'Sign up'}
      </button>
    </form>
  )
}

Schemas are validated client-side (in useForm) and server-side (in the action dispatcher) from one definition. The form works without JavaScript — native form POST, server re-render with errors embedded — and the auto-revalidated loader means the page picks up fresh data after a successful mutation.

API surface

// @jjordy/rogue
import {
  signal, effect, memo, untrack, batch,
  onCleanup, onMount,
  createContext, provide, useContext,
  defineComponent, emit,
  For,
} from '@jjordy/rogue'

// @jjordy/rogue/router — client-side routing
import { useRoute, navigate, mount, findMatch, renderRoute } from '@jjordy/rogue/router'

// @jjordy/rogue/testing — linkedom-backed DOM + mount/query/fireEvent for tests
import { setupDOM, mount, unmount, fireEvent, query, queryAll } from '@jjordy/rogue/testing'

// @jjordy/rogue/forms — form state, validation, submit lifecycle, server actions
import { useForm, defineActions, redirect, invalid, formError } from '@jjordy/rogue/forms'

// @jjordy/rogue/vite — Vite plugins
import { rogue, rogueRouter, rogueSsr } from '@jjordy/rogue/vite'

// @jjordy/rogue/server — dev SSR + action handler + JSON loader endpoint
import { render, handleAction, loadData } from '@jjordy/rogue/server'

// @jjordy/rogue/server/prod — production renderer (no Vite at request time)
import { loadProdRenderer } from '@jjordy/rogue/server/prod'

See runtime/index.d.ts and the .d.ts siblings for full types.

Conventions

  • Components live in src/components/<name>.jsx. The filename is the kebab-case tag (my-counter.jsx<my-counter>).
  • defineComponent shorthand infers the tag name from the filename: defineComponent((props) => …) is equivalent to defineComponent('my-counter', (props) => …).
  • Defaults from destructuring: ({ count = 0, label = 'x' }) makes count and label observed attributes with those defaults. Type inferred from the default value.
  • Co-located CSS: a my-counter.css next to my-counter.jsx is auto-imported as a constructible stylesheet.
  • Hoisted styles: <style> children of the component root are extracted to a sheet shared across instances.
  • Auto-import: kebab tags used in JSX auto-import their component file; PascalCase tags auto-import as named imports.

SSR

In dev, rogueSsr() (the Vite plugin) handles SSR + server actions inside the dev server — npm run dev does it all. For one-off renders:

import { createServer } from 'vite'
import { render } from '@jjordy/rogue/server'

const vite = await createServer({ server: { middlewareMode: true }, appType: 'custom' })
const { html, status } = await render('/blog/hello', vite)

The output uses Declarative Shadow DOM; the runtime hydrates by adopting existing DOM via pair-anchor markers — text nodes mutate in place, no rebuild.

Production deployment

Node only — Workers / Lambda / edge runtimes are deliberately out of scope (ADR-0002). Two vite build invocations plus a thin node:http shell:

vite build --outDir dist/client                              # browser bundle
vite build --ssr src/entry-server.ts --outDir dist/server    # SSR bundle
node server.js                                               # the prod server

Where src/entry-server.ts is one line:

export * from 'virtual:jsx-wc/routes'

And server.js wires the renderer into Node's http module:

import { loadProdRenderer } from '@jjordy/rogue/server/prod'
const { render, handleAction, loadData } = await loadProdRenderer({
  clientDir: 'dist/client',
  serverDir: 'dist/server',
})
// dispatch: POST ?_action=… → handleAction
//           GET text/html  → render
//           GET application/json → loadData (client-side router fetches data via JSON)
//           GET /assets/*  → static file

A copy-pasteable reference shell lives at rogue-lab/server/serve.prod.js (~120 LOC, zero dependencies).

Security

Server-only code in page modules (loader bodies, actions[*].run bodies) is automatically stripped from the client bundle at build time (ADR-0003). The transform refuses unfamiliar AST shapes rather than best-effort handling them — conservative refusal beats permissive leak. See SECURITY.md for vulnerability reporting.

More

  • Docs site — concepts, conventions, API reference, live showcase
  • AGENTS.md — project conventions for AI coding agents
  • CONTEXT.md — domain glossary
  • docs/adr/ — architectural decision records

License

MIT