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

@opsydyn/astro-foldkit

v0.2.1

Published

Astro integration and renderer for FoldKit.

Readme

@opsydyn/astro-foldkit

Astro integration and renderer for FoldKit.

FoldKit is an Elm Architecture runtime built on Effect. This package registers FoldKit as an Astro renderer so you can drop any FoldKit app into a .astro page as a component and hydrate it with client:load.

Installation

npm install @opsydyn/astro-foldkit
# peer deps
npm install astro foldkit

Setup

Add the integration to astro.config.ts:

import { defineConfig } from 'astro/config'
import foldkit from '@opsydyn/astro-foldkit'

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

Defining an app

Use defineApp to register a FoldKit app for lazy loading. The loader returns your main.ts module which must export a value that satisfies AppConfig.

// src/apps/counter/app.ts
import { defineApp } from '@opsydyn/astro-foldkit/define-app'

export default defineApp(() => import('./main'))
// src/apps/counter/main.ts
export const Model = null
export const init = () => [0, []] as const
export const update = (model: number, message: 'Inc' | 'Dec') =>
  [message === 'Inc' ? model + 1 : model - 1, []] as const
export const view = (model: number) => ({ /* foldkit view tree */ })

Use the app in an Astro page:

---
import Counter from '../apps/counter/app'
---
<Counter client:load />

Passing props

defineApp accepts a type parameter for the props your FoldKit app expects. This makes the component callable with typed attributes in .astro files.

// src/apps/greeting/app.ts
import { defineApp } from '@opsydyn/astro-foldkit/define-app'
import type { Name } from './model'

export default defineApp<{ name: Name }>(() => import('./main'))

Props are forwarded from Astro's <astro-island> serialisation into your init function. Declare init to accept props: unknown and validate at the boundary with Effect Schema:

// src/apps/greeting/model.ts
import { Schema } from 'effect'

export const Name = Schema.String.pipe(Schema.brand('Name'))
export type Name = typeof Name.Type

const Props = Schema.Struct({ name: Name })

export const init = (props: unknown): readonly [Model, readonly []] => {
  const { name } = Schema.decodeUnknownSync(Props)(props)
  return [name, []]
}

Pass the branded value from the Astro page:

---
import { Schema } from 'effect'
import GreetingApp from '../apps/greeting/app'
import { Name } from '../apps/greeting/model'

const name = Schema.decodeSync(Name)(Astro.url.searchParams.get('name') ?? 'World')
---
<GreetingApp client:load name={name} />

The TypeScript types flow end-to-end: the Name brand is required at the Astro call site, and Schema.decodeUnknownSync re-validates the serialised value at the client hydration boundary.

Built-in props

The renderer intercepts one reserved prop before forwarding anything to your app. It is never seen by init, update, or view.

noMeta

Prevents the FoldKit program from overwriting document.title. Use this whenever you embed an app as an island on a page that manages its own title — without it, the island's view tree will update the browser tab title on every render tick.

<DashboardChart client:load noMeta data={chartData} />

Both noMeta and noMeta={true} are accepted. The prop is stripped before init receives the rest of your props.

Architecture

Imperative shell, functional core

The Astro page is the imperative shell: it performs side effects (HTTP fetches, query-param reads, URL parsing) and validates raw values into branded types via Schema.decodeSync. The shell hands only clean, typed values into the component.

The FoldKit app is the functional core: its init, update, and view functions are pure. They receive already-validated props, run a closed message loop, and produce a view tree — with no side effects outside of declared Commands and Subscriptions.

Data flow

flowchart TD
    subgraph shell ["Astro SSR (imperative shell)"]
        A[fetch / query params] --> B["Schema.decodeSync(Brand)"]
        B --> C[branded prop]
    end

    subgraph island ["Astro serialisation"]
        C --> D["&lt;astro-island props='...'&gt;"]
    end

    subgraph hydration ["Client hydration — client.ts"]
        D --> E["config.init(props)"]
        E --> F["Schema.decodeUnknownSync(Props)"]
        F --> G[initial Model]
    end

    subgraph core ["FoldKit runtime (functional core)"]
        G --> H[update loop]
        H --> I[view]
        I --> J[DOM patch]
        K["Subscription\n(animationFrame, events)"] --> H
    end

The double-decode is intentional: Schema.decodeSync at the Astro boundary ensures the page cannot render with invalid data; Schema.decodeUnknownSync at the FoldKit boundary re-validates after JSON round-trip through the island serialisation, so the functional core never receives unverified input.

AppConfig

The module returned by your loader must export:

| Export | Type | Description | | :------- | :------------------------------------------------------------- | :---------------------------------------------- | | Model | unknown | Initial model type marker | | init | (props: unknown) => readonly [Model, ReadonlyArray<Command>] | Initial state from props and startup commands | | update | (model, message) => readonly [Model, ReadonlyArray<Command>] | Pure state transition | | view | (model) => Document | Render the current model to a FoldKit view tree |

Exports

| Entry point | Description | | :---------------------------------- | :-------------------------------------- | | @opsydyn/astro-foldkit | Default Astro integration (foldkit()) | | @opsydyn/astro-foldkit/define-app | defineApp helper and AppConfig type |

Embedding

Runtime.embed gives you a typed handle to the running program so you can clean it up on unmount and push live data in without remounting. This integration uses embed internally — the astro:unmount event calls handle.dispose() to tear down subscriptions and animation-frame loops when Astro navigates away.

Declaring ports

Ports are typed channels declared in your app's main.ts. Pass a Schema for each port so values are validated at the boundary.

// src/apps/dashboard/main.ts
import { Port } from 'foldkit'
import { Schema } from 'effect'

export const ports = {
  inbound: {
    data: Port.inbound(Schema.Array(DataPointSchema)),
  },
  outbound: {
    selection: Port.outbound(Schema.NullOr(Schema.String)),
  },
}

Pass the ports map to makeApplication alongside your init, update, and view:

import { Runtime } from 'foldkit'

const program = Runtime.makeApplication({
  Model,
  init,
  update,
  view,
  container,
  devTools: false,
  ports,
})

Live prop updates

Once embedded, push new data through an inbound port instead of remounting the component. This is particularly useful in Astro View Transition flows where the page shell re-runs but the island should preserve its running state:

const handle = Runtime.embed(program)

// Called when Astro soft-navigates back to this page with new data
handle.ports.data.send(nextDataPoints)

Outbound subscriptions

Subscribe to outbound ports to react to events inside the program from the host page. Port names are flat on handle.ports regardless of whether they were declared under inbound or outbound:

const unsubscribe = handle.ports.selection.subscribe((id) => {
  // sync selection state to the URL or another island
  history.replaceState({}, '', `?selected=${id ?? ''}`)
})

element.addEventListener('astro:unmount', () => {
  unsubscribe()
  handle.dispose()
}, { once: true })

Peer dependencies

| Package | Version | | :-------- | :--------- | | astro | ≥ 5.0 | | foldkit | ≥ 0.108 |

License

MIT