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

@valentinkolb/ssr

v0.5.1

Published

Minimal SSR framework for SolidJS and Bun

Readme

@valentinkolb/SSR

A minimal server-side rendering framework for SolidJS and Bun with islands architecture.

Overview

This framework provides SSR capabilities for SolidJS applications using Bun's runtime. It follows the islands architecture pattern where you can selectively hydrate interactive components while keeping the rest of your page static HTML.

Size & Philosophy

This framework is intentionally minimal. The entire codebase:

| Component | Lines | Raw | Gzipped | |-----------|-------|-----|---------| | Core (index, transform, build) | ~490 | 15 KB | 4.7 KB | | Client hydration (dev only) | ~200 | 6 KB | 2 KB | | Adapters | ~50 each | — | — |

Important: These sizes reflect the framework source code, which runs at build-time and on the server only. The browser receives:

  • Your island components
  • SolidJS runtime (~7 KB gzipped)
  • seroval's deserialize function (~2 KB gzipped)
  • A tiny hydration snippet (~150 bytes per island)

Minimal framework overhead in the client bundle.

What's not included (by design):

  • No client-side routing
  • No state management
  • No CSS-in-JS
  • No build tool abstractions

Use the libraries you already know. This framework just handles SSR and islands hydration.

Features

  • Islands architecture: *.island.tsx for hydrated components, *.client.tsx for client-only
  • Framework agnostic: Works with Bun's native server, Elysia, or Hono (easy to write your own adapter)
  • Fast: Built on Bun's runtime with optimized bundling
  • Dev experience: Hot reload, source maps, and TypeScript support

Example

See github.com/valentinkolb/ssr-example for a complete working example with all three adapters, including Tailwind CSS integration.

Installation

Core dependencies (always required):

bun add @valentinkolb/ssr solid-js
bun add -d @babel/core @babel/preset-typescript babel-preset-solid

Plus one adapter depending on your framework:

# Bun native - no extra dependencies

# Hono
bun add hono

# Elysia
bun add elysia @elysiajs/static

Note: Dependencies like solid-js, hono, and elysia are peer dependencies. This lets you control the exact versions in your project and avoids version conflicts.

Quick Start

Create a configuration file (optional - has sensible defaults):

// config.ts
import { createConfig } from "@valentinkolb/ssr";

export const { config, plugin, html } = createConfig({
  dev: process.env.NODE_ENV === "development",
});

Create an interactive island component:

// components/Counter.island.tsx
import { createSignal } from "solid-js";

export default function Counter({ initialCount = 0 }) {
  const [count, setCount] = createSignal(initialCount);

  return (
    <button onClick={() => setCount(count() + 1)}>
      Count: {count()}
    </button>
  );
}

Use it in a page:

// pages/Home.tsx
import Counter from "../components/Counter.island";

export default function Home() {
  return (
    <div>
      <h1>My Page</h1>
      <Counter initialCount={5} />
    </div>
  );
}

Adapter Usage

Bun Native Server

import { Bun } from "bun";
import { routes } from "@valentinkolb/ssr/adapter/bun";
import { config, html } from "./config";
import Home from "./pages/Home";

Bun.serve({
  port: 3000,
  routes: {
    ...routes(config),
    "/": () => html(<Home />),
  },
});

Hono

import { Hono } from "hono";
import { routes } from "@valentinkolb/ssr/adapter/hono";
import { config, html } from "./config";
import Home from "./pages/Home";

const app = new Hono()
  .route("/_ssr", routes(config))
  .get("/", async (c) => {
    const response = await html(<Home />);
    return c.html(await response.text());
  });

export default app;

Elysia

import { Elysia } from "elysia";
import { routes } from "@valentinkolb/ssr/adapter/elysia";
import { config, html } from "./config";
import Home from "./pages/Home";

new Elysia()
  .use(routes(config))
  .get("/", () => html(<Home />))
  .listen(3000);

Build Configuration

Add the plugin to your build script:

// scripts/build.ts
import { plugin } from "./config";

await Bun.build({
  entrypoints: ["src/server.tsx"],
  outdir: "dist",
  target: "bun",
  plugins: [plugin()],
});

For development with watch mode:

// scripts/preload.ts
import { plugin } from "./config";

Bun.plugin(plugin());
{
  "scripts": {
    "dev": "bun --watch --preload=./scripts/preload.ts run src/server.tsx",
    "build": "bun run scripts/build.ts",
    "start": "bun run dist/server.js"
  }
}

Component Types

Island Components (*.island.tsx)

Island components are server-rendered and then hydrated on the client. They should be used for interactive UI elements that need JavaScript.

// Sidebar.island.tsx
import { createSignal } from "solid-js";

export default function Sidebar() {
  const [open, setOpen] = createSignal(false);
  return <div>{open() ? "Open" : "Closed"}</div>;
}

Client-Only Components (*.client.tsx)

Client-only components are not rendered on the server. They render only in the browser, useful for components that depend on browser APIs.

// ThemeToggle.client.tsx
import { createSignal, onMount } from "solid-js";

export default function ThemeToggle() {
  const [theme, setTheme] = createSignal("light");
  
  onMount(() => {
    setTheme(localStorage.getItem("theme") || "light");
  });
  
  return <button onClick={() => setTheme(theme() === "light" ? "dark" : "light")}>
    {theme()}
  </button>;
}

Regular Components

Standard Solid components that are only rendered on the server. No client-side JavaScript is shipped for these.

// Header.tsx
export default function Header() {
  return <header><h1>My Site</h1></header>;
}

Props Serialization

The framework uses seroval for props serialization, which supports complex JavaScript types that JSON cannot handle:

<Island
  date={new Date()}
  map={new Map([["key", "value"]])}
  set={new Set([1, 2, 3])}
  regex={/test/gi}
  bigint={123n}
  undefined={undefined}
/>

Custom HTML Template

You can pass additional options to your HTML template. All options are type safe!

type PageOptions = { title: string; description?: string };

const { html } = createConfig<PageOptions>({
  template: ({
    body, scripts,     // must be provided and used for hydration
    title, description // user defined options
  }) => `
    <!DOCTYPE html>
    <html>
      <head>
        <title>${title}</title>
        ${description ? `<meta name="description" content="${description}">` : ""}
      </head>
      <body>${body}${scripts}</body>
    </html>
  `,
});

// Usage
await html(<Home />, { 
  title: "Home Page",               // type safe
  description: "Welcome to my site" // type safe
});

How It Works

  1. Build time: The framework discovers all *.island.tsx and *.client.tsx files in the project and bundles them separately for the browser
  2. During SSR: Normal components are rendered to HTML strings. Island/client components are wrapped in custom elements with data attributes containing their props
  3. At the client: Individual island bundles load and hydrate their corresponding DOM elements

The framework uses a Babel plugin to transform island imports into wrapped components during SSR. Props are serialized using seroval and embedded in data attributes. On the client, each island bundle deserializes its props and renders the component.

Babel is used since Solid only supports Babel for JSX transformation at the moment.

File Structure

src/
├── index.ts           # Core SSR logic and createConfig()
├── transform.ts       # Babel plugin for island wrapping
├── build.ts           # Island bundling with code splitting
└── adapter/
    ├── bun.ts         # Bun.serve() adapter
    ├── elysia.ts      # Elysia adapter
    ├── hono.ts        # Hono adapter
    ├── client.js      # Dev mode client (reload + dev tools)
    └── utils.ts       # Shared adapter utilities

Configuration Options

createConfig({
  dev?: boolean;                // Enable dev mode (default: false)
  verbose?: boolean;            // Enable verbose logging (default: !dev)
  template?: (context) => string; // HTML template function (optional, has default)
})

Dev Tools

In dev mode, a small [ssr] badge appears in the corner of the page. Click it to open the dev tools panel where you can:

  • Toggle auto-reload on/off
  • Highlight island components (green border)
  • Highlight client components (blue border)
  • Move the panel to any corner

Settings are persisted in localStorage.

Writing Your Own Adapter

Adapters just need to serve files from the _ssr directory. See src/adapter/utils.ts for shared helpers:

  • getSsrDir(dev) - Returns path to _ssr folder
  • getCacheHeaders(dev) - Cache headers (immutable in prod, no-cache in dev)
  • createReloadResponse() - SSE stream for hot reload
  • safePath(base, filename) - Prevents path traversal attacks

Check the existing adapters (~30 lines each) for reference.

TypeScript Config

Required tsconfig.json settings for SolidJS:

{
  "compilerOptions": {
    "lib": ["ESNext", "DOM"],
    "jsx": "preserve",
    "jsxImportSource": "solid-js",
    "moduleResolution": "bundler"
  }
}

See the example project for a full recommended config.

Limitations

  • Islands must have default export: export default function MyIsland() {}
  • Props must be serializable: seroval supports Date, Map, Set, RegExp, BigInt, but not functions or class instances
  • No shared state between islands: Each island hydrates independently. Use URL params, localStorage, or a global store for cross-island communication
  • No nested islands/clients: An island cannot import another island or client component. This is not needed anyway - once a component is an island, its entire subtree is hydrated. Just use regular components inside islands.

Contributing

Contributions are welcome! The codebase is intentionally minimal. Keep changes focused and avoid adding unnecessary complexity.

License

MIT