JSPM

  • Created
  • Published
  • Downloads 757
  • Score
    100M100P100Q115952F
  • License MIT

Isomorphic multimodal HTTP framework built for humans and machines in a single Bun runtime

Package Exports

  • @abide/abide/build
  • @abide/abide/bundle/BundleMenu
  • @abide/abide/bundle/BundleMenuItem
  • @abide/abide/bundle/BundleWindow
  • @abide/abide/bundle/bundled
  • @abide/abide/bundle/onMenu
  • @abide/abide/compile
  • @abide/abide/mcp/createMcpServer
  • @abide/abide/preload
  • @abide/abide/resolver-plugin
  • @abide/abide/server/AppModule
  • @abide/abide/server/DELETE
  • @abide/abide/server/GET
  • @abide/abide/server/HEAD
  • @abide/abide/server/InspectorContext
  • @abide/abide/server/PATCH
  • @abide/abide/server/POST
  • @abide/abide/server/PUT
  • @abide/abide/server/agent
  • @abide/abide/server/appDataDir
  • @abide/abide/server/cookies
  • @abide/abide/server/env
  • @abide/abide/server/error
  • @abide/abide/server/json
  • @abide/abide/server/jsonl
  • @abide/abide/server/prompts/definePrompt
  • @abide/abide/server/prompts/renderPromptTemplate
  • @abide/abide/server/reachable
  • @abide/abide/server/redirect
  • @abide/abide/server/request
  • @abide/abide/server/rpc/defineVerb
  • @abide/abide/server/server
  • @abide/abide/server/socket
  • @abide/abide/server/sockets/defineSocket
  • @abide/abide/server/sse
  • @abide/abide/shared/HttpError
  • @abide/abide/shared/cache
  • @abide/abide/shared/createSubscriber
  • @abide/abide/shared/health
  • @abide/abide/shared/html
  • @abide/abide/shared/log
  • @abide/abide/shared/online
  • @abide/abide/shared/page
  • @abide/abide/shared/pending
  • @abide/abide/shared/refreshing
  • @abide/abide/shared/snippet
  • @abide/abide/shared/trace
  • @abide/abide/shared/url
  • @abide/abide/shared/withJsonSchema
  • @abide/abide/test/assertAgentFrameConformance
  • @abide/abide/test/createScriptedSurface
  • @abide/abide/test/createTestApp
  • @abide/abide/tsconfig
  • @abide/abide/ui-plugin
  • @abide/abide/ui/dom/anchorCursor
  • @abide/abide/ui/dom/appendSnippet
  • @abide/abide/ui/dom/appendStatic
  • @abide/abide/ui/dom/appendText
  • @abide/abide/ui/dom/appendTextAt
  • @abide/abide/ui/dom/applyResolved
  • @abide/abide/ui/dom/attach
  • @abide/abide/ui/dom/attr
  • @abide/abide/ui/dom/awaitBlock
  • @abide/abide/ui/dom/cloneStatic
  • @abide/abide/ui/dom/each
  • @abide/abide/ui/dom/eachAsync
  • @abide/abide/ui/dom/hydrate
  • @abide/abide/ui/dom/mergeProps
  • @abide/abide/ui/dom/mount
  • @abide/abide/ui/dom/mountChild
  • @abide/abide/ui/dom/mountSlot
  • @abide/abide/ui/dom/on
  • @abide/abide/ui/dom/outlet
  • @abide/abide/ui/dom/readCall
  • @abide/abide/ui/dom/restProps
  • @abide/abide/ui/dom/skeleton
  • @abide/abide/ui/dom/spreadAttrs
  • @abide/abide/ui/dom/spreadProps
  • @abide/abide/ui/dom/switchBlock
  • @abide/abide/ui/dom/text
  • @abide/abide/ui/dom/tryBlock
  • @abide/abide/ui/dom/when
  • @abide/abide/ui/effect
  • @abide/abide/ui/enterScope
  • @abide/abide/ui/exitScope
  • @abide/abide/ui/navigate
  • @abide/abide/ui/outbox
  • @abide/abide/ui/remoteProxy
  • @abide/abide/ui/renderToStream
  • @abide/abide/ui/router
  • @abide/abide/ui/runtime/enterRenderPass
  • @abide/abide/ui/runtime/escapeKey
  • @abide/abide/ui/runtime/exitRenderPass
  • @abide/abide/ui/runtime/nextBlockId
  • @abide/abide/ui/scope
  • @abide/abide/ui/socketProxy
  • @abide/abide/ui/startClient
  • @abide/abide/ui/tail

Readme

abide

One typed declaration fans out to HTTP, a CLI, an MCP tool, and an OpenAPI spec — and the bundler swaps the runtime per side.

abide is an isomorphic framework on Bun and web standards: you declare a verb, a socket, or a component once, and the same callable runs server-side, in the browser, from the terminal, and from an agent — the bundler decides which runtime each side gets. Built for humans and machines.

  • One direct dependency — the TypeScript compiler, used by abide check. Tailwind is an optional peer. Everything runs on a single Bun runtime (≥ 1.3.0); no second toolchain.

Quick start

bunx abide scaffold my-app   # copies the template, installs, starts dev

Or clone the kitchen-sink and run it:

git clone https://github.com/briancray/abide
cd abide/examples/kitchen-sink
bun install
bun run dev

RPCs

An RPC is one export per file under src/server/rpc/. The file path is the URL. The schema validates arguments and projects the MCP tool, the CLI flags, and the OpenAPI operation. The contract is Standard Schema — zod, valibot, or arktype, unadapted.

// src/server/rpc/getMessages.ts — file path is the route: GET /getMessages
import { GET } from '@abide/abide/server/GET'
import { json } from '@abide/abide/server/json'
import { z } from 'zod'

const inputSchema = z.object({ room: z.string() })

export const getMessages = GET(({ room }) => json(board(room)), { inputSchema })

One declaration, five surfaces:

              getMessages = GET(fn, { inputSchema })
                              │
   ┌───────────┬─────────────┼────────────┬──────────────┐
 SSR call   browser fetch   MCP tool    CLI command    OpenAPI op
cache(fn)() typed proxy()  (read-only)  abide ... cli  /openapi.json

A schema unlocks the CLI everywhere and MCP for read-only verbs; a mutating verb never auto-exposes to MCP — it needs explicit clients: { mcp: true }. Consume the verb four ways: cache(getMessages)({ room }) resolves in-process during SSR, the same call hits a swapped fetch in the browser, getMessages.raw(args) returns the undecoded Response, and getMessages.stream(args) is an iterable view of the body.

Query args travel as strings — validate with z.coerce.*. The per-verb timeout (504 on every surface) is distinct from ABIDE_CLIENT_TIMEOUT.

Sockets

A socket is one broadcast topic per file under src/server/sockets/. A Socket<T> is an isomorphic AsyncIterable<T>, and every socket multiplexes onto one websocket at /__abide/sockets.

// src/server/sockets/messages.ts — topic name = file name: `messages`
import { socket } from '@abide/abide/server/socket'
import { z } from 'zod'

const schema = z.object({ id: z.string(), room: z.string(), from: z.string(), text: z.string() })

// retain the last 50 frames; evict any older than an hour
export const messages = socket({ schema, tail: 50, ttl: 3_600_000 })
export type Message = z.infer<typeof schema>

The socket also has an HTTP face at /__abide/sockets/messages, for clients that can't speak the ws multiplex: GET returns the retained tail, POST publishes — gated by clientPublish (off by default, so the POST 403s).

Components

A .abide component is the payoff: it imports the verb and the socket above and ties them together with the native <template> grammar. Reactive state is reached only through scope()scope().state() is writable, scope().computed() is read-only. props() and effect() are in-scope, no import.

<script>
import { cache } from '@abide/abide/shared/cache'
import { tail } from '@abide/abide/ui/tail'
import { getMessages } from '$server/rpc/getMessages.ts'
import { postMessage } from '$server/rpc/postMessage.ts'
import { messages } from '$server/sockets/messages.ts'
import Avatar from '$ui/Avatar.abide'

const { room = 'lobby' } = props<{ room?: string }>()

// SSR-warm history; live frames then stream over the ws
const seed = cache(getMessages)({ room })
const live = scope().computed(() => tail(messages, { last: 50 }))

let from = scope().state('alice')
let text = scope().state('')
let pinned = scope().state(false)
let sort = scope().state('newest')

async function send() {
    await postMessage({ room, from, text })
    text = ''
}
</script>

<template name="bubble" args={msg}>
    <li class="flex gap-2"><Avatar name={msg.from} /> <b>{msg.from}</b> {msg.text}</li>
</template>

<form onsubmit={send} class="flex gap-2">
    <input bind:value={from} class="border px-2" />
    <input bind:value={text} placeholder="message" class="flex-1 border px-2" />
    <label><input type="checkbox" bind:checked={pinned} /> pin</label>
    <label><input type="radio" bind:group={sort} value="newest" /> newest</label>
    <button disabled={!text} class="border px-3">send</button>
</form>

<template if={pinned}>
    <p class="text-xs text-amber-700">room pinned</p>
    <template else><p class="text-xs text-slate-400">not pinned</p></template>
</template>

<template switch={sort}>
    <template case={'newest'}><p class="text-xs">newest first</p></template>
    <template default><p class="text-xs">oldest first</p></template>
</template>

<template await={seed}>
    <p class="text-xs text-slate-500">loading…</p>
    <template then="history">
        <ul class="mt-3 space-y-1">
            <template each={live ?? history} as="msg" key="msg.id">
                {bubble(msg)}
            </template>
        </ul>
    </template>
    <template catch="err"><p class="text-rose-700">{err.message}</p></template>
</template>

<style>
    li { font-size: 0.875rem; }
</style>

MIT