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

@pyreon/core

v0.25.1

Published

Core component model and lifecycle for Pyreon

Readme

@pyreon/core

Component model, JSX runtime, lifecycle, context, and control-flow components for Pyreon.

@pyreon/core provides h(), the JSX automatic runtime, lifecycle hooks (onMount/onUnmount/onUpdate/onErrorCaptured), a two-tier context system (static vs reactive), control-flow components (Show, Switch/Match, For, Suspense, ErrorBoundary, Portal, Dynamic), code-splitting via lazy(), and props utilities that preserve reactivity through HOC pipelines. Components run ONCE — re-rendering on signal change is not the model; reactivity is per-binding via accessors read inside JSX text thunks, effects, or computeds. Sits one layer above @pyreon/reactivity and is consumed by both runtime-dom (CSR) and runtime-server (SSR).

Install

bun add @pyreon/core @pyreon/reactivity

TypeScript / JSX setup

In your tsconfig.json:

{
  "compilerOptions": {
    "jsx": "preserve",
    "jsxImportSource": "@pyreon/core"
  }
}

The compiler (@pyreon/compiler, via @pyreon/vite-plugin) then transforms JSX into _tpl() + _bind() templates against this runtime.

Quick start

import {
  onMount, createContext, createReactiveContext, provide, useContext,
  Show, Switch, Match, For, Suspense, ErrorBoundary, lazy,
} from '@pyreon/core'
import { signal } from '@pyreon/reactivity'

const ModeCtx = createReactiveContext<'light' | 'dark'>('light')

function Timer() {
  const count = signal(0)
  onMount(() => {
    const id = setInterval(() => count.update(n => n + 1), 1000)
    return () => clearInterval(id)
  })
  return <div>{() => count()}</div>
}

function Page(props: { items: { id: number; name: string }[] }) {
  const mode = signal<'light' | 'dark'>('dark')
  provide(ModeCtx, () => mode())

  return (
    <Switch fallback={<p>None</p>}>
      <Match when={() => props.items.length > 0}>
        <For each={props.items} by={i => i.id}>{i => <li>{i.name}</li>}</For>
      </Match>
    </Switch>
  )
}

const Heavy = lazy(() => import('./Heavy'))
function App() {
  return (
    <ErrorBoundary fallback={(e) => <p>{String(e)}</p>}>
      <Suspense fallback={<div>Loading…</div>}>
        <Heavy />
      </Suspense>
    </ErrorBoundary>
  )
}

The reactive-vs-static rule

Components run once. What's reactive depends on where you read a signal:

// REACTIVE — compiler wraps DOM text in an accessor
<div>{name()}</div>

// REACTIVE — explicit accessor
<div>{() => `Hi ${name()}`}</div>

// REACTIVE — props read inside a reactive scope
<Comp title={name()} />

// STATIC — destructured at component setup, captured once
const { items } = props
return <For each={items} ...>...</For>   // items is frozen at first read

// REACTIVE — read live
return <For each={props.items} ...>...</For>

const x = props.y IS reactive: the compiler inlines props.y back at the use site when x is a const. let x = props.y is static (mutable, not safe to inline).

Lifecycle

onMount(() => {
  const ws = new WebSocket(url)
  return () => ws.close()  // cleanup runs on unmount
})

onUnmount(() => { /* … */ })
onUpdate(() => { /* … */ })
onErrorCaptured((err, info) => { /* return true to stop propagation */ })

onMount's return value is the cleanup function — there's no separate useEffect-style pair. Hook arrays are lazy-allocated; components with no hooks pay zero cost.

Context

Two flavors, deliberately distinct:

// Static context: useContext returns T directly, safe to destructure
const ThemeCtx = createContext<'light' | 'dark'>('light')
const theme = useContext(ThemeCtx)  // 'light' | 'dark'

// Reactive context: useContext returns () => T, call it inside reactive scopes
const ModeCtx = createReactiveContext<'light' | 'dark'>('light')
const getMode = useContext(ModeCtx)
return <div>{() => getMode()}</div>

provide(ctx, value) pushes a context frame and auto-cleans up on unmount. withContext(ctx, value, fn) is the bounded form for non-component scopes.

Control flow

<Show when={isReady()}>{() => <Page />}</Show>
<Show when={count} fallback={<Loading />}>{(n) => <p>{n}</p>}</Show>

<Switch fallback={<NotFound />}>
  <Match when={isAdmin()}><AdminPanel /></Match>
  <Match when={isUser()}><UserPanel /></Match>
</Switch>

<For each={items} by={item => item.id}>
  {(item) => <li>{item.name}</li>}
</For>

<Portal mount={document.body}><Modal /></Portal>
<Dynamic component={tag()} {...props} />
<Defer>{() => <Heavy />}</Defer>   // mount after first paint

<For> uses by (not key) — JSX reserves key as a VNode reconciliation prop. Show / Match accept either a value (when={isOpen()}) or an accessor (when={() => isOpen()}) — both work, but only the accessor form re-evaluates on signal change.

Suspense + lazy

const Heavy = lazy(() => import('./Heavy'))

<Suspense fallback={<div>Loading…</div>}>
  <Heavy />
</Suspense>

lazy() integrates with Suspense — async work inside the lazy module pauses rendering until resolved. SSR streams the fallback then patches in the resolved subtree.

Props utilities

import { splitProps, mergeProps, cx, createUniqueId } from '@pyreon/core'

function Button(props: ButtonProps) {
  const [local, rest] = splitProps(props, ['variant', 'size'])
  const merged = mergeProps({ type: 'button' }, rest)
  const id = createUniqueId()  // 'pyreon-1', SSR-safe
  return (
    <button id={id} {...merged} class={cx('btn', `btn-${local.variant}`, local.size && `size-${local.size}`)}>
      {props.children}
    </button>
  )
}

splitProps and mergeProps copy property descriptors (not values), so getter-shaped reactive props survive. Plain result[key] = source[key] fires the getter at copy time and collapses reactivity — use these helpers instead.

ErrorBoundary

<ErrorBoundary fallback={(err, reset) => (
  <div role="alert">
    <p>{String(err)}</p>
    <button onClick={reset}>Retry</button>
  </div>
)}>
  <App />
</ErrorBoundary>

Captures any error thrown in descendants. Pair with registerErrorHandler / reportError for telemetry.

Compiler-emitted helpers

_rp(fn), _wrapSpread(source), makeReactiveProps(raw), REACTIVE_PROP — emitted by @pyreon/compiler and consumed by runtime-dom / runtime-server. Not user-facing in normal code. If you write a manual HOC pipeline that copies props in plain JS (not via JSX spread), reach for splitProps/mergeProps — descriptor preservation is load-bearing for reactivity.

nativeCompat(Component) — marker that tells @pyreon/{react,preact,vue,solid}-compat jsx() runtimes to route the component through h(type, props) directly, skipping the compat wrapper. Only relevant for hand-rolled Pyreon-flavored helpers used inside compat-mode apps.

Common conventions

  • class, not className
  • for, not htmlFor
  • onInput, not onChange, for per-keystroke input updates
  • style={{ … }} accepts a CSS-object; style="…" accepts a CSS string
  • data-* / aria-* attributes typed via template-literal index signatures (catches typos)

Documentation

Full docs: docs.pyreon.dev/docs/core (or docs/docs/core.md in this repo).

License

MIT