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

melina

v2.5.4

Published

A lightweight web framework for Bun with Next.js-style routing and zero-config builds.

Readme

Melina.js 🦊

Bun-native web framework — server-rendered JSX + lightweight client runtime

npm version Tests Bun

Melina.js is a web framework built for Bun. Pages are server-rendered JSX — write components that run on the server, render to HTML, and arrive at the browser instantly. Client interactivity is added via mount scripts — small .client.tsx files that hydrate specific parts of the page with a zero-dependency ~2KB VDOM runtime.

No React on the client. No hydration mismatch. No bundle bloat.

  Server (Bun)                 Browser
┌──────────────┐          ┌──────────────┐
│  page.tsx    │──HTML──▶ │  Static DOM  │
│  layout.tsx  │          │              │
│  api/route.ts│          │  .client.tsx │
│  middleware  │          │  mount()     │
│  SSG cache   │          │  VDOM render │
└──────────────┘          └──────────────┘

Features

  • File-based routing — Next.js App Router convention (app/page.tsx/)
  • Nested layoutslayout.tsx at any level, composed automatically
  • Mount scriptspage.client.tsx adds interactivity without shipping React
  • API routesapp/api/*/route.ts with GET, POST, etc.
  • Dynamic routesapp/post/[id]/page.tsx/post/:id
  • SSGexport const ssg = true to pre-render at startup, serve from memory
  • <Head> component — Declarative <title>, <meta> per page during SSR
  • Error boundarieserror.tsx catches render errors with layout chrome
  • Middlewaremiddleware.ts at any route level, runs root→leaf
  • Scoped CSSpage.css or style.css scoped to route segments
  • Tailwind CSS v4 — Built-in PostCSS + @tailwindcss/postcss support
  • Streaming — Return AsyncGenerator from API routes for SSE
  • In-memory builds — No dist/ folder — assets built and served from RAM
  • Import maps — Browser-native module resolution for client dependencies
  • Pluggable reconcilers — Keyed, sequential, or replace strategies for VDOM diffing
  • Hot reload — Dev-only SSE-based live reload. Watches client script dep trees, reloads browser on save (v2.5.0)
  • Auto server-only stubbing — Scans node_modules for bun:* imports and auto-stubs them for browser builds
  • Build error reporting — Surfaces Bun build errors/warnings with file:line:column positions
  • Observability — All operations instrumented with measure-fn

Quick Start

# Create a new project
npx melina init my-app
cd my-app
bun install
bun run server.ts

Or from scratch:

// server.ts
import { start } from 'melina';

await start({
  appDir: './app',
  port: 3000,
  defaultTitle: 'My App',
  // hotReload: true, // opt-in in dev
});

Project Structure

my-app/
├── app/
│   ├── layout.tsx              # Root layout (wraps all pages)
│   ├── layout.client.tsx       # Persistent client JS (survives navigation)
│   ├── globals.css             # Global styles (Tailwind or plain CSS)
│   ├── page.tsx                # Home page (/)
│   ├── page.client.tsx         # Home page mount script
│   ├── page.css                # Scoped CSS for home page
│   ├── middleware.ts           # Root middleware (runs on every request)
│   ├── error.tsx               # Error boundary
│   ├── about/
│   │   └── page.tsx            # /about
│   ├── post/[id]/
│   │   └── page.tsx            # /post/:id (dynamic route)
│   └── api/
│       └── messages/
│           └── route.ts        # API: /api/messages
├── server.ts
└── package.json

Architecture

Server Pages

Pages export a default function that returns JSX. These run only on the server — you can access databases, read files, call APIs directly:

// app/page.tsx
export default function HomePage() {
  const posts = db.query('SELECT * FROM posts LIMIT 10');

  return (
    <main>
      <h1>Latest Posts</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </main>
  );
}

Mount Scripts (Client Interactivity)

A page.client.tsx file adds interactivity to server-rendered HTML. Export a default mount() function — it receives the DOM after SSR:

// app/counter/page.client.tsx
import { render } from 'melina/client';

function Counter({ count, onIncrement }: { count: number; onIncrement: () => void }) {
  return (
    <div>
      <span>{count}</span>
      <button onClick={onIncrement}>+1</button>
    </div>
  );
}

export default function mount() {
  const root = document.getElementById('counter-root');
  if (!root) return;

  let count = 0;
  const update = () => {
    render(<Counter count={count} onIncrement={() => { count++; update(); }} />, root);
  };
  update();
}

Key design decisions:

  • No hooks — Logic is explicit, not hidden behind magic closures
  • No framework lock-inrender(vnode, container) is the entire API
  • Works with XState — Mount scripts are the perfect place for state machines

Layouts

Layouts wrap pages and compose automatically from root to leaf:

// app/layout.tsx
export default function RootLayout({ children }: { children: any }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <title>My App</title>
      </head>
      <body>
        <nav><a href="/">Home</a></nav>
        <main>{children}</main>
      </body>
    </html>
  );
}

layout.client.tsx is a persistent mount script — it survives page navigations, ideal for global UI like nav highlights or notification systems.

<Head> Component

Declarative per-page head management during SSR:

import { Head } from 'melina/web';

export default function AboutPage() {
  return (
    <>
      <Head>
        <title>About Us — My App</title>
        <meta name="description" content="Learn about our team" />
        <link rel="canonical" href="https://example.com/about" />
      </Head>
      <main><h1>About Us</h1></main>
    </>
  );
}

API Routes

Export HTTP method handlers:

// app/api/messages/route.ts
export async function GET(req: Request) {
  const messages = await db.getMessages();
  return Response.json(messages);
}

export async function POST(req: Request) {
  const body = await req.json();
  await db.createMessage(body);
  return Response.json({ ok: true });
}

Streaming — Return an AsyncGenerator for Server-Sent Events:

export async function* GET(req: Request) {
  for (let i = 0; i < 10; i++) {
    yield `data: ${JSON.stringify({ count: i })}\n\n`;
    await new Promise(r => setTimeout(r, 1000));
  }
}

SSG (Static Site Generation)

Opt in per page — pre-render at startup, serve from memory:

// Pre-render once, serve forever
export const ssg = true;

// Or with TTL (re-render after expiry)
export const ssg = { revalidate: 60 }; // seconds

export default function PricingPage() {
  return <main><h1>Pricing</h1></main>;
}

Middleware

middleware.ts files run before the page renders, root→leaf:

// app/middleware.ts
export default async function middleware(req: Request) {
  const token = req.headers.get('authorization');
  if (!token) {
    return new Response('Unauthorized', { status: 401 });
  }
  // Return nothing to continue to the page
}

Error Boundaries

error.tsx catches render errors and displays them with full layout chrome:

// app/error.tsx
export default function ErrorPage({ error }: { error: { message: string } }) {
  return (
    <div>
      <h1>Something went wrong</h1>
      <p>{error.message}</p>
    </div>
  );
}

Dynamic Routes

app/post/[id]/page.tsx      → /post/:id
app/user/[userId]/page.tsx  → /user/:userId
export default function PostPage({ params }: { params: { id: string } }) {
  return <h1>Post #{params.id}</h1>;
}

Scoped CSS

Add page.css or style.css alongside any page — it's automatically injected only for that route:

/* app/dashboard/page.css */
.metric-card {
  background: linear-gradient(135deg, #1a1a2e, #16213e);
  border-radius: 12px;
  padding: 24px;
}

Styling

Built-in Tailwind CSS v4 + PostCSS. Add globals.css in the app directory:

@import "tailwindcss";

@theme {
  --color-primary: #0a0a0f;
  --color-accent: #6366f1;
}

Melina auto-discovers globals.css, global.css, or app.css.

API Reference

start(options)

High-level entry point:

import { start } from 'melina';

await start({
  appDir: './app',
  port: 3000,
  defaultTitle: 'My App',
});

serve(handler, options) + createAppRouter(options)

Lower-level API for custom setups:

import { serve, createAppRouter } from 'melina';

const handler = createAppRouter({
  appDir: './app',
  defaultTitle: 'My App',
  globalCss: './app/globals.css',
  hotReload: true,
});

serve(handler, { port: 3000, hotReload: true });

Client: render(vnode, container)

The entire client API:

import { render, createElement } from 'melina/client';

render(<MyComponent />, document.getElementById('root'));

CLI

npx melina init <project-name>   # Create new project from template
npx melina start                 # Start dev server

Showcase

Run the built-in showcase to see every feature in action:

git clone https://github.com/7flash/melina.js.git
cd melina.js
bun install
bun run examples/showcase/server.ts
# → http://localhost:3000

The showcase includes:

  • SSR demo with live timestamps
  • Counter with VDOM rendering
  • XState state machine integration
  • Reconciler strategy comparison and benchmarks
  • SSG benchmark (SSR vs Cached SSR vs SSG response times)
  • Error boundaries, middleware, scoped CSS, <Head> component
  • Streaming API with animated progress
  • Server throughput stress test

For Contributors

Design Philosophy

Melina is intentionally small. We don't add features unless they solve a real problem that the existing primitives can't handle. Two features we've explicitly decided against:

Why no Cached SSR

The comparison table on the SSG page shows three strategies: SSR, Cached SSR, and SSG. Cached SSR does not exist as a framework feature — and we don't plan to add it.

The pitch for Cached SSR is: "Render on the first request, cache the HTML, serve the cache for subsequent requests until TTL expires." But SSG with revalidation already does this — better:

// This is all you need. No Cached SSR required.
export const ssg = { revalidate: 60 }; // re-render every 60 seconds

export default function PricingPage() {
    const prices = db.getPrices(); // fresh data on each revalidation
    return <main><PriceTable prices={prices} /></main>;
}

Here's the concrete comparison:

| | Cached SSR | SSG with revalidate | |---|---|---| | When cached | After first visitor requests | At startup (before any visitor) | | First visitor | Pays full render cost | Instant response | | Storage | JS string in memory (GC pressure) | ArrayBuffer (zero-copy, no GC) | | Cache refresh | Next request after TTL expires triggers re-render | Background revalidation on timer | | Invalidation | TTL only | TTL via revalidate, or manual via clearSSGCache() | | Cold start | Slow (uncached) | Fast (pre-rendered) |

The critical difference: Cached SSR penalizes the first visitor with a full server render. SSG pre-renders at startup, so every visitor — including the first — gets an instant response. The revalidate option handles staleness automatically, and clearSSGCache() handles on-demand invalidation (e.g., after a webhook from your CMS).

If you need truly dynamic, per-request data (user-specific content, authenticated pages), use SSR. If you want caching, use SSG with revalidate. There's no use case where "SSR + cache the response" beats "SSG + periodic revalidation" — SSG is strictly better because it eliminates the cold-start penalty entirely.

Hot Reload (v2.5.0)

In dev mode, Melina watches your client script dependency trees and auto-reloads the browser on save:

  • hot-reload.ts uses fs.watch() on directories containing client scripts and their imports
  • When a file changes, an SSE event is sent to the browser via /__melina_hmr
  • A reconnecting EventSource client in the page triggers window.location.reload()
  • 150ms debounce handles editors that write multiple times per save
  • Dep trees are walked using Bun.Transpiler.scanImports() — only local imports are followed
  • Completely no-op in production

Apps can also configure server-only packages via package.json:

{
  "melina": {
    "serverOnly": ["my-db-adapter", "internal-auth-lib"]
  }
}

These packages will be stubbed with a Proxy in browser builds, preventing bun:* import errors.

Project Structure

src/
├── server/
│   ├── app-router.ts      # Route matching, SSR pipeline, error boundaries
│   ├── build.ts            # Asset build pipeline (JS, CSS, static files)
│   ├── serve.ts            # HTTP server with measure-fn observability
│   ├── router.ts           # File-based route discovery
│   ├── ssg.ts              # Static site generation (pre-render + memory serve)
│   ├── ssr.ts              # renderToString (VNode → HTML)
│   ├── head.ts             # <Head> component (side-channel collection)
│   ├── imports.ts          # Import map generation
│   ├── hot-reload.ts       # Dev-only SSE hot reload + file watcher
│   └── types.ts            # Shared types
├── client/
│   ├── render.ts           # VDOM renderer + Fiber reconciler (~2KB)
│   ├── reconcilers/        # Pluggable diffing strategies
│   │   ├── keyed.ts        # O(n log n) key-based with LIS
│   │   ├── sequential.ts   # O(n) index-based
│   │   └── replace.ts      # Full replace (baseline)
│   ├── jsx-runtime.ts      # JSX transform for client bundles
│   ├── jsx-dom.ts          # JSX-to-real-DOM for mount scripts
│   └── types.ts            # VNode, Component, Props types
└── web.ts                  # Main entry point

Observability

Every operation is instrumented with measure-fn:

[a] ✓ Discover routes 8.10ms → 17 routes
[b] ... GET http://localhost:3000/
[b-a] ... Middleware: app
[b-a] ✓ Middleware: app 0.12ms
[b-b] ... Import page
[b-b] ✓ Import page 0.04ms
[b-c] ... SSR renderToString
[b-c] ✓ SSR renderToString 0.31ms
[build:d] ... Style: globals.css
[build:d] ✓ Style: globals.css 0.10ms
[b] ✓ GET http://localhost:3000/ 2.14ms

Running Tests

bun test

License

MIT © 7flash Import page 0.04ms [b-c] ... SSR renderToString [b-c] ✓ SSR renderToString 0.31ms [build:d] ... Style: globals.css [build:d] ✓ Style: globals.css 0.10ms [b] ✓ GET http://localhost:3000/ 2.14ms


### Running Tests

```bash
bun test

License

MIT © 7flash