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

@playfast/echoform

v1.0.9

Published

run your react app logic on the server

Readme

fullstack

Build web UIs where all logic stays on the server. The browser is just a screen.

echoform is for dev tools, local apps, and anywhere you want a web interface without building an API layer. You write React on the server — state, callbacks, streaming — and echoform handles the rest.

How it works

The opposite of server-side rendering: the client renders UI components, the server runs business logic. State management, data fetching, and layout decisions happen on the server. User interactions (clicks, input) run on the client and are forwarded to the server as callbacks.

Data flow: client actionserver callbackserver state updateclient view update (over WebSocket)

This eliminates the HTTP request/response cycle for data fetching — the server pushes updates directly. For data-heavy views this is faster than traditional React apps. For purely client-side interactions with no server state, a traditional approach may be faster.

Use cases

"web-desktop-environment" is a project built on top of "echoform" that benefits from the tight connection between server and client. Moving the entire server logic to React components made the codebase more readable and organized.

@playfast/wmux is a web terminal multiplexer for dev servers built with "echoform". It uses server-side React components to manage PTY terminals, iframe tabs, and file browsing sessions, streaming terminal output to the client via echoform's stream primitives while handling user input through callbacks — all over a single WebSocket connection.

Getting Started

An echoform app has three parts: a shared view contract, a server that runs logic, and a client that renders UI.

bun add @playfast/echoform @playfast/echoform-render
bun add @playfast/echoform-bun-ws-server  # server transport
bun add @playfast/echoform-bun-ws-client   # client transport
bun add zod  # or valibot, arktype — any Standard Schema library

1. Define the contract

// shared/views.ts
import { view, callback, createViews } from "@playfast/echoform";
import { z } from "zod";

export const Home = view("Home", {
  input: { username: z.string() },
  callbacks: { logout: callback() },
});

export const Login = view("Login", {
  callbacks: {
    login: callback({ input: z.object({ username: z.string(), password: z.string() }) }),
  },
});

export const Prompt = view("Prompt", {
  input: { message: z.string() },
  callbacks: { onOk: callback() },
});

export const views = createViews({ Home, Login, Prompt });

2. Server — all logic lives here

// server/index.tsx
import { useState } from "react";
import { Render } from "@playfast/echoform-render";
import { Server } from "@playfast/echoform/server";
import { createBunWebSocketServer } from "@playfast/echoform-bun-ws-server";
import { Home, Login, Prompt } from "../shared/views";

function App() {
  const [location, setLocation] = useState<"home" | "error" | "login">("login");
  const [name, setName] = useState("");

  return (
    <>
      {location === "login" && (
        <Login
          login={({ username, password }) => {
            if (password === "0000") {
              setName(username);
              setLocation("home");
            } else {
              setLocation("error");
            }
          }}
        />
      )}
      {location === "home" && (
        <Home username={name} logout={() => setLocation("login")} />
      )}
      {location === "error" && (
        <Prompt message="Wrong password" onOk={() => setLocation("login")} />
      )}
    </>
  );
}

const { transport, start } = createBunWebSocketServer({ port: 8485, path: "/ws" });
start();

Render(
  <Server transport={transport}>
    {() => <App />}
  </Server>
);

3. Client — just renders what the server sends

// client/index.tsx
import { Client } from "@playfast/echoform/client";
import { useWebSocketTransport } from "@playfast/echoform-bun-ws-client";
import type { InferClientProps } from "@playfast/echoform/client";
import { Home as HomeDef, Login as LoginDef, Prompt as PromptDef } from "../shared/views";

function Home({ username, logout }: InferClientProps<typeof HomeDef>) {
  return (
    <div>
      <h1>Hello - {username}</h1>
      <button onClick={() => logout.mutate()}>Logout</button>
    </div>
  );
}

function Login({ login }: InferClientProps<typeof LoginDef>) {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  return (
    <div>
      <input type="text" onChange={(e) => setUsername(e.target.value)} placeholder="username" />
      <input type="password" onChange={(e) => setPassword(e.target.value)} placeholder="password" />
      <button onClick={() => login.mutate({ username, password })}>Log In</button>
    </div>
  );
}

function Prompt({ message, onOk }: InferClientProps<typeof PromptDef>) {
  return (
    <div>
      <h1>{message}</h1>
      <button onClick={() => onOk.mutate()}>OK</button>
    </div>
  );
}

function App() {
  const { transport } = useWebSocketTransport("ws://localhost:8485/ws");
  if (!transport) return <div>Connecting...</div>;
  return <Client transport={transport} views={{ Home, Login, Prompt }} />;
}

Callbacks use .mutate() on the client and return promises. Streams use .subscribe(). See the root README for a full example with streams.