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

solarbuild

v0.2.3

Published

A runtime UI framework built for AI-generated code. Typed prop contracts, structured errors with fix instructions, and a machine-readable component registry.

Readme

Solar

A runtime UI framework built for AI-generated code.

When an AI agent builds UI with React or Vue, it guesses. It guesses prop names, types, and valid values. When it gets something wrong, it gets a stack trace. Stack traces are not machine-readable feedback.

Solar gives agents a clear path. Call registry.manifest() and you get a JSON schema of every registered component, including what props it takes and what values are valid. Mount a component with wrong props and you get a ContractError, a structured object with expected, received, and fix fields. The agent reads fix and retries. No human required.


Core principles

  • Explicit contracts over conventions: props are typed and validated at the component boundary, not inferred
  • Component registry: models read a machine-readable manifest before generating any composition code
  • Structured errors: validation failures return parseable JSON an agent can act on directly
  • Declared side effects: effects declare what they depend on and what they touch; no implicit subscriptions
  • Typed composition: slot props enforce which component can fill them, with runtime validation
  • Runtime-based: no compiler required; debuggable in the browser as-is
  • Small surface area: fewer primitives means fewer ways to generate something wrong

How it works

Defining a component

Every component declares its shape explicitly. Register it so other components and models can discover it.

import { createElement, defineComponent, registry } from './framework/index.js'

const Button = defineComponent({
  name: 'Button',
  props: {
    label: { type: 'string', required: true },
    onClick: { type: 'function', required: true },
    variant: { type: 'string', enum: ['primary', 'secondary'], default: 'primary' },
  },
  render({ label, onClick, variant }) {
    return createElement('button', { class: variant, onclick: onClick }, label)
  }
})

registry.register(Button)

Structured errors

Pass the wrong type and you get a structured error, not a string. An agent can parse and self-correct without regex.

Button({ label: 42, onClick: () => {} })
// throws ContractError:
{
  "error": "ContractError",
  "component": "Button",
  "prop": "label",
  "expected": "string",
  "received": "number",
  "fix": "Pass a string value for \"label\""
}

Component registry

Before generating composition code, a model reads the full catalog:

registry.manifest()
// →
[
  {
    "name": "Button",
    "props": {
      "label": { "type": "string", "required": true },
      "onClick": { "type": "function", "required": true },
      "variant": { "type": "string", "enum": ["primary", "secondary"], "default": "primary" }
    }
  },
  ...
]

Typed slots (component composition)

A parent component declares which component type can fill a slot. Passing anything else throws a ContractError.

const Card = defineComponent({
  name: 'Card',
  props: {
    title: { type: 'string', required: true },
    action: { type: 'slot', accepts: 'Button', required: true },
  },
  render({ title, action }) {
    return createElement('div', { class: 'card' },
      createElement('h3', {}, title),
      action,
    )
  }
})

// valid
Card({ title: 'Hello', action: Button({ label: 'Go', onClick: () => {} }) })

// throws: Card: prop "action": expected slot(Button), got vnode with no _source
Card({ title: 'Hello', action: createElement('button', {}, 'Go') })

Compact h() notation

h() parses a dense array format into a vnode tree. It's registry-aware, so component names resolve automatically.

import { h } from './framework/index.js'

// plain DOM nodes
h(['div', { class: 'row' },
  ['p', {}, 'Hello'],
  ['button', { class: 'btn' }, 'Click'],
])

// registered component by name, dispatches to Button()
h(['Button', { label: 'Save', onClick: handleSave, variant: 'primary' }])

Hooks and effects

State is useState. Side effects use three explicit primitives instead of a general useEffect:

  • useResource: async data fetching with automatic AbortController cancellation on key change
  • useSubscription: event listener that attaches and detaches when source/event/handler changes
  • onMount / onUnmount: lifecycle callbacks that run once, not on every render
const UserCard = defineComponent({
  name: 'UserCard',
  props: { userId: { type: 'number', required: true } },
  render({ userId }) {
    const [width, setWidth] = useState(window.innerWidth)

    // cancels the previous fetch automatically when userId changes
    const { data, loading, error } = useResource({
      key: userId,
      fetch: async (signal) => {
        const res = await fetch(`/api/user/${userId}`, { signal })
        return res.json()
      },
    })

    // attaches once, re-attaches only if source/event/handler changes
    useSubscription({ source: window, event: 'resize', handler: () => setWidth(window.innerWidth) })

    onMount(() => analytics.track('UserCard mounted'))
    onUnmount(() => analytics.track('UserCard unmounted'))

    if (loading) return createElement('p', {}, 'Loading...')
    return createElement('p', {}, `${data.name} - viewport: ${width}px`)
  }
})

Architecture

framework/
├── core/
│   ├── createElement.js     # vnode factory
│   ├── render.js            # mount vnode tree to DOM
│   ├── diff.js              # reconcile old and new vnode trees
│   ├── hooks.js             # useState, useMemo, useResource, useSubscription, onMount, onUnmount
│   └── scheduler.js         # batch and defer re-renders
├── contract/
│   ├── defineComponent.js   # strict component definition with prop schema
│   ├── ContractError.js     # structured error class with toJSON()
│   ├── validate.js          # runtime prop validation
│   └── types.js             # primitive type definitions + slot type
├── runtime/
│   ├── reconciler.js        # component lifecycle and mount/unmount
│   └── events.js            # delegated event handling
├── registry.js              # component catalog and manifest
├── h.js                     # compact array notation parser
└── index.js                 # public API

Components live in components/, one per file, default export is the defineComponent call, self-registered on import. Rules a model can follow without inference.


Live demo

Open in StackBlitz — runs a 3-step agent loop in the browser: discover components via registry.manifest(), catch a ContractError on a bad mount, self-correct using the fix field.


Getting started

npm create solarbuild@latest my-app
cd my-app
npm run dev

Cursor plugin: cursor.directory/plugins/framework-solar — adds Solar rules to Cursor so your AI assistant knows the API out of the box.

Or use the CDN directly. No install, no build step:

<!DOCTYPE html>
<html>
<body>
  <div id="app"></div>
  <script type="module">
    import {
      defineComponent, mountComponent, registry,
      useState, createElement
    } from 'https://cdn.jsdelivr.net/npm/solarbuild/framework/index.js'

    const Counter = defineComponent({
      name: 'Counter',
      props: {},
      render() {
        const [count, setCount] = useState(0)
        return createElement('button', { onclick: () => setCount(n => n + 1) }, `Clicked ${count} times`)
      }
    })

    registry.register(Counter)
    mountComponent(Counter, {}, document.getElementById('app'))
  </script>
</body>
</html>

Running the demo

npm run dev

Open http://localhost:3456/demo/ (trailing slash required; it's a static server quirk).

The demo covers all framework features: static rendering, diffing, hooks, contract validation, declared effects, batched updates, registry manifest, compact h() notation, and typed slots.


Docs

Full documentation at docs.solarbuild.dev


What this is not

  • Not a meta-framework. No SSR, no routing, no build pipeline.
  • Not a React replacement. Narrower scope, different constraints.
  • Not optimized for humans writing components by hand, though it works fine for that too.

The thesis: the next wave of frameworks won't be designed for developers. They'll be designed for the models developers use to write code. This is an early attempt at that.