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 🙏

© 2025 – Pkg Stats / Ryan Hefner

astrojs-hyperapp

v3.0.10

Published

Hyperapp integration for astro.build

Readme

Astro-Hyperapp Integration

Static/server rendered Hyperapp components with Astro. JSX/TSX-support, Hot-Module-Reloading, Server-Side-Rendering & Synchronized Components

Astro is a static-site-generator – it lets you generate a website as static files from various content-sources. It can also run as a server, rendering your html/css at runtime. Unlike similar tools, Astro is agnostic about the client side, allowing any frontend framework (or none!) by way of integrations.

Hyperapp is an extremely tiny, fast and simple client-side UI framework. With this integration, you get to use Hyperapp in your Astro site, along with:

  • JSX/TSX support – The lingua franca for writing view components
  • Server-Side-Rendering – Even before your component is mounted, the initial view is pre-rendered in the html. This makes your pages feel faster, and prevents jarring layout shifts.
  • Hot-Module-Reloading – While developing, see the changes to your components reflected live on save - without losing their internal state
  • Synchronized Components – Have multiple, separate Hyperapp components spread out over your otherwise static site – sharing the same state, thanks to the synchronized-islands utility provided by this integration.

Have a look at a demo at https://codesandbox.io/p/github/zaceno/astrojs-hyperapp-demo, or read on to try for yourself

Get Started

If you don't already have an astro project, set one up following the instructions at: https://docs.astro.build/en/install-and-setup/

Install and enable astrojs-hyperapp

Open a shell and change directory to the root of your astro project, then run:

> npm install hyperapp astrojs-hyperapp

Enable the integration by including it in your astro.config.mjs file:

import { defineConfig } from "astro/config"
import hyperapp from "astrojs-hyperapp"

export default defineConfig({
  integrations: [hyperapp()],
})

Create a first island

In Astro, an "island" is a mount-point for a client-side component is rendered.

Make a new folder called client inside src/ to keep your client-side code separate from the server-side code. In that folder, create a file Counter.jsx:

const increment = state => ({ ...state, count: state.count + 1 })

const decrement = state => ({ ...state, count: state.count - 1 })

export default () => ({
  init: { count: 0 },
  view: state => (
    <div>
      <h1>{state.count}</h1>
      <button onclick={decrement}>-</button>
      <button onclick={increment}>+</button>
    </div>
  ),
})

Use the island in an astro page

Open up src/pages/index.astro and

  • add an import for the counter in the frontmatter,
  • place the component somewhere in the body
---
import Counter from '../client/Counter' //<-- add this
---

<html lang="en">
  <head>
    ...
    <title>Astro</title>
  </head>
  <body>
    <h1>Astro</h1>

    <!-- and add this: -->
    <p>A hyperapp counter:</p>
    <Counter client:load />
    <!-- // -->
  </body>
</html>

"client:load" tells astro to hydrate the component as soon as the page is loaded. See https://docs.astro.build/en/reference/directives-reference/#client-directives for more info

Check it out!

Start Astro's dev server by opening your shell, changing directory to your Astro project, and running:

> npm run dev

Then open up a browser to http://localhost:4321/. You should see your standard Astro index page, and in it you should see your counter-component rendered, and working (click the buttons to try it out!)

Hyperapp Islands

Hyperapp islands are functions that return objects with the same properties you would pass to Hyperapp's app({...}) call (init, view, subscriptions, dispatch) – all except the node property (which astro provides for us). Upon hydration, a hyperapp-instance is started on the island mount-point. Therefore a view is required, and you probably want an init, but subscriptions and dispatch are optional.

Server Props

An Astro-page can send props to your island, in the first argument to your island function. For example:

//some-page.astro

<Counter client:load startCount={5} />

will send {startCount: 5} as the first argument to your island function, so you can handle adapt this particular instance to the given props:

//Counter.jsx
export default (serverProps) => ({
  init: {count: serverProps.startCount},
  view: state => ...
})

Static content

You can pass static html content to you islands, from Astro pages:

---
import ContentToggle from '../components/ContentToggle.jsx'
---
<p> Click to reveal contents:</p>
<ContentToggle client:load>
  <p class="red">
    This text his hidden by default. And red.
  </p>
</ContentToggle>
<style>
.red {
  color: red;
}
</style>

This static content is provided to the island as a Hyperapp virtual-node in the second argument:

export default (_, content) => ({
  init: false,
  view: showing => (
    <div>
        <p>
          <button onclick={showing => !showing}>
            {showing ? 'Hide' : 'Show'}
          </button>
        </p>
      {showing && content}
    </div>
  )
})

Slots

Sometimes you might want to pass more than one set of static html to an island. You can do that by setting the slot attribute to a name you choose, on an element in the static content being passed in, that you want to distinguish from the rest:

---
import ContentToggle from '../components/ContentToggle.jsx'
---
<p> Click to reveal contents:</p>
<ContentToggle client:load>
  <p class="red">
    This text his hidden by default. And red.
  </p>
  <p slot="footer">
    This footer text is always visible
  </p>
</ContentToggle>
<style>
.red {
  color: red;
}
</style>

Slotted content is not included in the second content argument to your island, but is instead passed as a prop in the first argument, with the same name you gave the slot.

//ContentToggle.jsx
export default (props, content) => ({
  init: false,
  view: showing => (
    <div style={{border: '1px black solid'}}>
      <button onclick={showing => !showing}>
        {showing ? 'Hide' : 'Show'}
      </button>
      <div>
        {showing && content}
        {props.footer}
    </div>
  )
})

Slot-names need to be given as snake/kebab case (e.g slot="kebab-case" or slot="snake_case") in .astro files, in order to be html-compliant. But for your convenience, they are transformed to camelCase (e.g. props.kebabCase or props.snakeCase) in the props passed to the island.

Synchronizing Islands

Every island in an Astro page is ... an island, unto itself. It has its own state and doesn't know what's going on in any other island unless you build in some signalling/sharing mechanism of your own. This is true for any choice of client-side framework.

Astro's recommended solution to sharing state between islands is nanostores. It is a fine solution, but will (for now) require you to implement your own effects & subscriptions for interacting with nanostores.

As a convenient alternative, this integration offers a mechanism for you to define synchronized islands. Islands that share the same state.

Start by definining a synchronizer, where you pass in an init prop, and optionally subscriptions & dispatch props. Again, these are the same as you would pass to a Hyperapp app({...}) call.

//chat-master.js
import makeSynchronizer from 'astrojs-hyperapp/synced-islands'
export const chatSynchronizer = makeSynchronizer({
  init: ...          // initial states and effects
  subscriptions: ... // optional
  dispatch: ...      // optional
})

Now, for every island that you want to share this state, import the synchronizer, and use it to define your island by giving it only a view (the rest of the props are provided by the synchronizer):

//chat-messages.jsx
import { chatSynchronizer } from "./chat-master.js"
export default props =>
  chatSynchronizer(state => (
    <div class="chat-messages">{/* define chat-messages view here */}</div>
  ))
//chat-status.jsx
import { chatSynchronizer } from "./chat-master.js"
export default props =>
  chatSynchronizer(state => (
    <div class="chat-status">{/* define chat-status view here */}</div>
  ))
//chat-notifications.jsx
import { chatSynchronizer } from "./chat-master.js"
export default props =>
  chatSynchronizer(state => (
    <div class="chat-notifications">
      {/* define chat-notifications view here */}
    </div>
  ))

Each island will technically have it's own hyperapp-instance running. But they are wired up so as to immediately synchronize their state between them. Therefore, they can also share and reuse the same actions.

Important Notes and Caveats

JSX children

When you pass children to a component, like:

<MyComponent>
  <p>foo</p>
  <p>bar</p>
</MyComponent>

they will arrive as an array of virtual nodes in the second argument to the component.

const MyComponent = (props, children) => {
  //children will be: [<p>foo</p>, <p>bar</p>]
}

It's ok to put this array of children among other children, because the jsx transform will flatten the child-list:

const MyComponent = (props, children) => (
  <div>
    <h1>Header</h1>
    {children} {/* <--- this is fine */}
  </div>
)

Using TSX

In order to use TSX (type-aware JSX), you will need to add the following lines to your tsconfig.json file under "compilerOptions": { ... }:

"jsx": "preserve",
"jsxFactory": "jsx",
"jsxFragmentFactory": "jsxFragment"

You should also create a src/env.d.ts file - a recommended place to put global type definitions for your code - and in it, add the line:

/// <reference types="astrojs-hyperapp/jsx" />

Limiting Scope of the integration

By default, this integration will affect every .js, .ts, .jsx and .tsx file in your project, which might cause undesired effects. You can limit what files this integration processes, by passing include and/or exclude options to the integration in astro.config.mjs.

import { defineConfig } from "astro/config"
import hyperapp from "astrojs-hyperapp"

export default defineConfig({
  integrations: [hyperapp({
    include?: String | RegExp | Array[...String|RegExp],
    exclude?: String | RegExp | Array[...String|RegExp]
  })],
})

Where strings are taken as picomatch patterns.

Mixing client-frameworks

If you want to use Hyperapp alongside another client-side UI-framework that also uses .jsx, like React, you will need to separate their integration scope using the include/exclude options mentioned above.

A recommended practice is to keep components for different frameworks in named folders, for example: src/client/hyperapp/ for everything using Hyperapp, and src/client/react/ for everything using React. Then astro.config.mjs could look like this:

import { defineConfig } from "astro/config"
import hyperapp from "astrojs-hyperapp"
import react from "@astrojs/react"
export default defineConfig({
  integrations: [
    hyperapp({ include: "**/hyperapp/*" }),
    react({ include: "**/react/*" }),
  ],
})

Also, since we need to set our own compilerOptions for jsx in tsconfig.json, which breaks react's jsx processing, you will need to add the following pragma at the top of all React components:

/** @jsxRuntime automatic */
/** @jsxImportSource react */

client:only issue

Due to a current limitation in astro, if you are using another client-side framework like React alongside Hyperapp, the client:only directive is unusable for Hyperapp islands.

If you try, you will get an error about a missing renderer-hint. Even if you specify client:only="hyperapp". This is due to the fact that "hyperapp" is not listed in the internal, hard-coded list of 'sanctioned' renderers.