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

atollic

v0.1.0

Published

Island architecture for WinterCG-compatible runtimes. Bring-your-own server (Elysia, Hono, …) and UI framework, powered by Vite.

Readme

npm version CI License: MIT Bundle size

Island architecture for WinterCG-compatible runtimes. Bring your own server (Elysia, Hono, …) and your own UI framework, powered by Vite.

Status: experimental. Atollic is pre-1.0 (v0.0.x). The API may change between minor versions until 1.0.

  • Server-agnostic — any WinterCG runtime that speaks Request/Response (Elysia, Hono, Bun, Node via adapter, Workers, …).
  • UI-framework-agnostic — ships with Solid and React adapters; Preact and others pluggable via FrameworkAdapter. Each island picks its framework via the JSX pragma, so one page can host Solid and React islands side by side.
  • Zero-JS by default — pages render to HTML strings on the server; only "use client" islands ship JavaScript.
  • HMR with state preserved — server changes morph the DOM via idiomorph, keeping mounted islands alive.

How it works

Server (Elysia, Hono, ...)     Client (Browser)
─────────────────────────────   ─────────────────────────
1. Route handler returns JSX    4. Find [data-island] elements
2. Islands SSR to real HTML     5. Lazy-import component module
3. Full page sent to browser    6. Hydrate with matching props
  • Pages are server-rendered JSX using Atollic's built-in HTML runtime — no virtual DOM, just strings.
  • Components marked with "use client" become islands — they SSR on the server and hydrate on the client.
  • Everything else is zero-JS static HTML.

Quick start

bun add atollic elysia solid-js vite-plugin-solid

Project structure

my-app/
  src/
    app.tsx          # Server entry — routes and layouts
    islands/
      Counter.tsx    # Interactive island component
  vite.config.ts

vite.config.ts

import { defineConfig } from "vite";
import { solid } from "atollic/solid";
import { atollic } from "atollic/vite";

export default defineConfig({
  plugins: [
    atollic({
      entry: "./src/app.tsx",
      frameworks: [solid()],
    }),
  ],
});

src/app.tsx — Server entry (Elysia)

import { Elysia } from "elysia";
import { Head } from "atollic/head";
import { atollic } from "atollic/elysia";
import Counter from "./islands/Counter.js";

const app = new Elysia()
  .use(atollic())
  .get("/", () => (
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <Head />
      </head>
      <body>
        <h1>Hello from the server</h1>
        <Counter initial={0} />
      </body>
    </html>
  ));

export default app.handle;

src/islands/Counter.tsx — Island component

/** @jsxImportSource solid-js */
"use client";

import { createSignal } from "solid-js";

export default function Counter(props: { initial: number }) {
  const [count, setCount] = createSignal(props.initial);
  return (
    <button onClick={() => setCount((c) => c + 1)}>
      Count: {count()}
    </button>
  );
}

Run

bunx --bun vite        # Dev server with HMR
bunx --bun vite build  # Production build
bun dist/server/app.js # Start production server

Server adapters

Atollic is decoupled from any specific server. Your entry file exports a fetch function — Atollic handles the rest.

Elysia

import { Elysia } from "elysia";
import { atollic } from "atollic/elysia";

const app = new Elysia()
  .use(atollic())
  .get("/", () => <h1>Hello</h1>);

export default app.handle;

The Elysia adapter intercepts responses via mapResponse, extracts HTML, ensures DOCTYPE, and injects production assets.

The HTML-extraction step is registry-based — each FrameworkAdapter contributes an extractor function (via the extractHtml field) that knows how to convert its framework's SSR output (e.g. Solid's { t: "..." } shape) into a plain HTML string. The Atollic Vite plugin wires these registrations up automatically before the first request, so the core stays framework-agnostic. Plain strings are always recognized as a fallback. See Writing a framework adapter and the registerHtmlExtractor API.

Hono

import { Hono } from "hono";
import { atollic } from "atollic/hono";
import { html } from "atollic";

const app = new Hono();
app.use(atollic());

app.get("/", () => html(<h1>Hello</h1>));

export default app.fetch;

The Hono adapter is middleware that processes text/html responses. Use the html() helper to wrap JSX into a proper Response.

Islands

Any .tsx or .jsx file with "use client" at the top becomes an island:

/** @jsxImportSource solid-js */
"use client";

// This component SSRs on the server and hydrates on the client

How islands work

  1. Discovery — The Vite plugin scans for files with "use client" and identifies PascalCase component exports
  2. SSR stub — During SSR, island files are replaced with stubs that render the component to HTML inside a <div data-island="Name"> wrapper with serialized props
  3. Client entry — A virtual module registers all discovered islands with lazy imports
  4. Hydration — The client runtime finds [data-island] elements and:
    • If SSR content exists: hydrates with matching renderId (reuses existing DOM)
    • If empty (dynamically added): falls back to full client-side render

Picking a framework per island

Atollic selects the adapter for each island from the file's JSX pragma. The @jsxImportSource comment at the top of the file tells Atollic (and Vite) which UI framework this island uses:

/** @jsxImportSource solid-js */
"use client";
// Rendered and hydrated by the Solid adapter
/** @jsxImportSource react */
"use client";
// Rendered and hydrated by the React adapter

Both can coexist in the same project and on the same page. If a file has "use client" without a pragma, Atollic uses the first registered framework.

Named exports

A single file can export multiple island components. Each PascalCase export becomes its own island boundary:

"use client";

export function SearchBar(props: { placeholder: string }) { /* ... */ }
export function TagCloud(props: { tags: string[] }) { /* ... */ }

Both are independently hydratable islands.

Server-rendered children

Islands can accept children that are rendered on the server as plain HTML and then spliced into the island's SSR output. The children stay zero-JS — only the island itself ships hydration code.

// src/app.tsx — server entry
import Card from "./islands/Card.js";
import ProductList from "./components/ProductList.js"; // plain server component, no "use client"

<Card title="Featured">
  <ProductList items={products} />
</Card>
// src/islands/Card.tsx
/** @jsxImportSource solid-js */
"use client";

import { createSignal, type JSX } from "solid-js";

export default function Card(props: { title: string; children: JSX.Element }) {
  const [open, setOpen] = createSignal(true);
  return (
    <section>
      <button onClick={() => setOpen((v) => !v)}>{props.title}</button>
      {open() && <div>{props.children}</div>}
    </section>
  );
}

<ProductList /> renders to HTML on the server using Atollic's JSX runtime. That HTML is injected into the island's SSR output via a sentinel substitution, so the island sees its children as pre-rendered markup rather than re-rendering them through the UI framework. After hydration the island still controls its own interactivity, but the children are just static DOM.

Client scripts

Non-JSX files (.ts, .js) with "use client" are bundled as client-side scripts — no framework, just plain JS:

"use client";

document.addEventListener("click", (e) => {
  // Runs only in the browser
});

Import the script from your server entry — it's skipped during SSR and loaded in the browser.

Cross-island state

Islands are independent roots, but you can share reactive state between them using module-level signals:

// shared.ts
import { createSignal } from "solid-js";
export const [count, setCount] = createSignal(0);
// Increment.tsx
"use client";
import { setCount } from "./shared";
export default () => <button onClick={() => setCount((c) => c + 1)}>+</button>;
// Display.tsx
"use client";
import { count } from "./shared";
export default () => <p>Count: {count()}</p>;

Both islands share the same signal — no context provider needed.

JSX runtime

Atollic includes a server-side JSX runtime (atollic/jsx-runtime) that compiles JSX to HTML strings:

  • All standard HTML elements and attributes
  • Async components (Promise<string> return values)
  • Boolean attributes (disabled, checked, etc.)
  • Automatic XSS escaping
  • Void elements (<br />, <img />, etc.)
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "atollic"
  }
}

<Head />

Place <Head /> in your document <head> to mark where Atollic injects CSS and script tags:

import { Head } from "atollic/head";

<head>
  <meta charset="UTF-8" />
  <Head />
</head>

In dev, Atollic injects the hydration bootstrap, collected CSS, and the client entry. In production, it injects the built asset tags. If <Head /> is omitted, assets are injected before </head> as a fallback.

HMR

Dual HMR strategy for instant feedback:

  • Server file changes — Atollic sends a custom atollic:reload event. The client refetches the page and uses idiomorph to morph the DOM, preserving mounted island state.
  • Island file changes — handled by the framework's own HMR (e.g., solid-refresh).

Events

Listen to lifecycle events on document:

| Event | Detail | Description | |---|---|---| | atollic:ready | — | All initial islands mounted | | atollic:before-morph | { newDoc, mountedIslands } | Cancelable, before HMR page morph | | atollic:after-morph | { defaultSwap } | After HMR page morph |

htmx, Alpine, and other DOM-mutating libraries

Islands added to the DOM after the initial mount — by htmx swaps, Alpine templates, or any other source — are picked up automatically. The client runtime installs a MutationObserver on document.body and mounts any newly inserted [data-island] element. No library-specific event listener is required.

CSS handling

CSS files imported in your server entry (or its transitive imports) are automatically discovered:

  • Dev: collected from Vite's module graph and injected as <link> tags via <Head />
  • Prod: included in the client build, hashed, and referenced in the build manifest
import "./styles.css"; // discovered automatically

Production build

bunx --bun vite build

Produces:

dist/
  client/          # Static assets (JS, CSS) with content-hashed filenames
    assets/
  server/
    app.js         # Self-contained server entry

The generated server entry calls setProductionAssets() with the built CSS/JS tags, imports your app, and starts Bun.serve() with static file serving from dist/client/.

PORT=3000 bun dist/server/app.js

Writing a framework adapter

Implement FrameworkAdapter to add support for any UI framework:

import type { FrameworkAdapter } from "atollic/adapter";

export function myFramework(): FrameworkAdapter {
  return {
    name: "my-framework",

    // Vite plugins for JSX transform
    plugins: () => [myFrameworkVitePlugin()],

    // Generate SSR stub for "use client" components
    ssrStub(rawImportPath, fileExports) {
      return `/* SSR stub that renders to HTML string */`;
    },

    // Client-side hydrate/render functions
    clientRuntime: `
      export function hydrateIsland(el, Component, props, id) {
        // Hydrate existing SSR content — return dispose function
      }
      export function renderIsland(el, Component, props) {
        // Render into empty container — return dispose function
      }
    `,

    // Optional: script tag for hydration bootstrap (e.g., Solid's _$HY)
    hydrationScript: `<script>/* bootstrap */</script>`,

    // Optional: source code for an extractor function `(value) => string | null`.
    // Atollic includes this in the server boot module so the runtime knows
    // how to convert this framework's SSR output (e.g. Solid's `{ t: "..." }`
    // shape) into a plain HTML string. Frameworks whose SSR output is already
    // a string can omit this — plain strings are always recognized.
    extractHtml: `(value) => {
      if (value && typeof value === "object" && "t" in value) return value.t;
      return null;
    }`,
  };
}

The extractHtml strings from every registered adapter are emitted into a generated server-boot module that runs once before the first request. Each one calls registerHtmlExtractor() on the Atollic core, which extractHtml (used internally by the Elysia and Hono adapters) iterates in order. This is how the core stays decoupled from any specific UI framework's SSR output shape.

API reference

atollic/vite

atollic(options: AtollicOptions): Plugin[]

| Option | Type | Description | |---|---|---| | entry | string | Path to server entry that default-exports a fetch function | | frameworks | FrameworkAdapter[] | UI framework adapters (e.g., [solid()]) |

atollic

// Wrap an HTML string (or async JSX) in a Response with DOCTYPE.
// Returns a Promise<Response> when given a Promise<string>.
html(input: string | Promise<string>): Response | Promise<Response>

setProductionAssets(assets: string): void   // Set production asset tags
getProductionAssets(): string | undefined   // Get production asset tags

// Register a function that converts a framework-specific SSR output value
// into an HTML string (or returns null if it doesn't recognize the shape).
// Normally wired up automatically by the Vite plugin from each adapter's
// `extractHtml` field — call this directly only if you need a custom one.
// Returns a dispose function.
type HtmlExtractor = (value: unknown) => string | null
registerHtmlExtractor(fn: HtmlExtractor): () => void

atollic/elysia

atollic(): Elysia  // Elysia plugin — intercepts HTML responses, injects assets

atollic/hono

atollic(): MiddlewareHandler  // Hono middleware — processes HTML responses, injects assets

atollic/head

Head(): string  // Returns marker for asset injection

atollic/solid

solid(): FrameworkAdapter  // Solid.js framework adapter

atollic/react

react(): FrameworkAdapter  // React framework adapter

atollic/html

Types for server-side JSX:

type Children =
  | string
  | number
  | bigint
  | boolean
  | null
  | undefined
  | Promise<Children>
  | Children[]

type Component<T = {}> = (
  props: T & { children?: Children },
) => string | Promise<string>

Exports

| Export | Description | |---|---| | atollic | Core — html(), setProductionAssets(), getProductionAssets(), registerHtmlExtractor() | | atollic/vite | Vite plugin | | atollic/client | Client runtime (auto-imported) | | atollic/adapter | FrameworkAdapter type | | atollic/head | <Head /> component | | atollic/solid | Solid.js adapter | | atollic/react | React adapter | | atollic/elysia | Elysia server adapter | | atollic/hono | Hono server adapter | | atollic/jsx-runtime | Server JSX runtime | | atollic/html | HTML types (Component, Children, JSX namespace) |

Requirements

  • A WinterCG-compatible runtime — Bun, Node (via fetch adapter), Deno, or Cloudflare Workers
  • Vite ^8.0.0
  • Examples and the bundled production server template currently target Bun; other runtimes work but require providing your own server entry

License

MIT