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

gallic

v0.0.0

Published

Write components in plain TypeScript and TSX. gallic compiles them ahead of time to fine-grained reactive DOM updates. No virtual DOM, no custom file format, and **no framework imports inside components**. Labels like `$:`, `mount:`, `cleanup:`, and `untr

Readme

gallic

Write components in plain TypeScript and TSX. gallic compiles them ahead of time to fine-grained reactive DOM updates. No virtual DOM, no custom file format, and no framework imports inside components. Labels like $:, mount:, cleanup:, and untrack: are the only directives.

export function Counter() {
  let count = 0;

  return <button onclick={() => count++}>count is {count}</button>;
}

count becomes a signal because it is a let that is assigned and read in JSX. Everything else stays a normal variable.

Install

bun add gallic
bun add -d vite @typescript/typescript6

@typescript/typescript6 is required by the Vite plugin / compiler. Optional: install typescript ≥ 7 in your app if you want native tsc typecheck alongside tsc6.

tsconfig.json

{
  "compilerOptions": {
    "jsx": "preserve",
    "allowUnusedLabels": true,
    "types": ["gallic/jsx"],
    "strict": true
  }
}
  • jsx: "preserve" — Vite + gallic compile the JSX; TypeScript does not emit a factory.
  • allowUnusedLabels: true — silences stock tsc unused-label noise for $: / mount: / cleanup: / untrack:. gallic still warns on typos like mout:.
  • types: ["gallic/jsx"] — ambient JSX types only; nothing is imported at runtime.

vite.config.ts

import gallic from "gallic/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [gallic()],
});

Entry point

Component files import nothing from gallica. The app entry is the one place that touches the runtime:

// main.ts
import { mount } from "gallic/runtime";
import { Counter } from "./Counter";

mount(Counter, document.getElementById("root")!);

Language guide

State

A let or var becomes a signal when it is written after init and read in a reactive place (JSX, $: block, or as a live prop). const never does.

export function Counter() {
  let count = 0; // signal
  const label = "count is"; // plain

  return (
    <button onclick={() => count++}>
      {label} {count}
    </button>
  );
}

Compound updates like count++ do not create a feedback loop from the embedded read.

Derived values and effects ($:)

A $: that assigns to a binding is a derived value. A $: with only side effects is an effect. One-liners are fine:

let doubled: number;
$: doubled = count * 2;

$: {
  document.title = `count ${count}`;
}
export function Temperature() {
  let celsius = 20;
  let fahrenheit: number;

  $: fahrenheit = (celsius * 9) / 5 + 32;

  $: {
    document.title = `It's ${fahrenheit}°F`;
  }

  return (
    <label>
      <input
        type="number"
        value={celsius}
        oninput={(e) => (celsius = e.currentTarget.valueAsNumber)}
      />
      °C = {fahrenheit}°F
    </label>
  );
}

Declare derived bindings outside (let fahrenheit: number), then assign in $:. Labels cannot sit on let/const in JavaScript; use a block with var if you want declaration and derive in one place: $: { var doubled = count * 2; }.

Props and children

Props are a typed parameter. Destructuring stays live: reading count below tracks the parent.

type BadgeProps = { count: number; children: JSX.Element | string };

function Badge(props: BadgeProps) {
  const { count } = props;

  return (
    <span class="badge">
      {props.children}
      {count > 0 && <sup>{count}</sup>}
    </span>
  );
}

export function Inbox() {
  let unread = 3;

  return (
    <button onclick={() => (unread = 0)}>
      <Badge count={unread}>Inbox</Badge>
    </button>
  );
}

Conditionals use ordinary JSX: {cond && <Node/>}, {a ? <A/> : <B/>}. Only the active branch is mounted.

Lists

Map over data with a key. Same key reuses the row; changing the key remounts it. Row fields that you read reactively update in place when the item changes under a stable key.

type Todo = { id: string; text: string; done: boolean };

export function TodoApp() {
  let todos: Todo[] = [];
  let text = "";
  let hideDone = false;
  let visible: Todo[];
  let remaining: number;

  $: {
    visible = hideDone ? todos.filter((t) => !t.done) : todos;
    remaining = todos.filter((t) => !t.done).length;
  }

  function add(e: SubmitEvent) {
    e.preventDefault();
    if (!text.trim()) return;
    todos = [...todos, { id: crypto.randomUUID(), text, done: false }];
    text = "";
  }

  function toggle(id: string) {
    todos = todos.map((t) => (t.id === id ? { ...t, done: !t.done } : t));
  }

  return (
    <main>
      <form onsubmit={add}>
        <input
          value={text}
          oninput={(e) => (text = e.currentTarget.value)}
        />
      </form>

      <label>
        <input
          type="checkbox"
          checked={hideDone}
          oninput={(e) => (hideDone = e.currentTarget.checked)}
        />
        Hide completed
      </label>

      <ul>
        {visible.map((todo) => (
          <li key={todo.id} class={["todo", todo.done && "done"]}>
            <input
              type="checkbox"
              checked={todo.done}
              oninput={() => toggle(todo.id)}
            />
            {todo.text}
          </li>
        ))}
      </ul>

      {todos.length > 0 && <footer>{remaining} remaining</footer>}
    </main>
  );
}

Lifecycle (mount:, cleanup:)

mount: runs after the component's DOM is connected. Nest cleanup: inside mount: (or at component top level) for disposal.

ref={el} assigns the element when the template is wired — before $: effects first run, and before mount:. So an effect can guard on the ref and still subscribe to signals read inside the branch:

$: {
  if (el) el.textContent = String(count);
}

Use mount: for connect-time work (listeners, measuring layout, timers). Use $: for reactive updates against a ref that already exists.

export function Clock() {
  let now = new Date().toLocaleTimeString();
  let el!: HTMLTimeElement;

  mount: {
    el.classList.add("ticking");
    const id = setInterval(() => {
      now = new Date().toLocaleTimeString();
    }, 1000);
    cleanup: {
      clearInterval(id);
    }
  }

  return <time ref={el}>{now}</time>;
}

Untracked reads (untrack:)

Signal reads inside untrack: do not subscribe. Use it when you need the current value without creating a dependency (for example, sampling state inside an effect that should not re-run on that signal).

Compound updates like count++ already avoid self-subscription; you only need the label for explicit non-tracking regions.

export function WatchA() {
  let a = 0;
  let b = 0;

  $: {
    console.log("a is", a);
    untrack: {
      // Reading `b` does not subscribe — this effect only re-runs when `a` changes.
      console.log("b is", b);
    }
  }

  return (
    <>
      <button onclick={() => a++}>a++</button>
      <button onclick={() => b++}>b++</button>
    </>
  );
}

Module-scope stores

A module-level let that is assigned anywhere in the module becomes a shared signal. Import it from components with no special API.

// stores/theme.ts
export let theme: "light" | "dark" = "light";

export function toggleTheme() {
  theme = theme === "light" ? "dark" : "light";
}
// ThemeButton.tsx
import { theme, toggleTheme } from "./stores/theme";

export function ThemeButton() {
  $: {
    document.documentElement.setAttribute("data-theme", theme);
    document.documentElement.style.colorScheme = theme;
  }

  return <button onclick={toggleTheme}>Theme: {theme}</button>;
}

Class and style

class accepts strings, arrays, and objects (clsx-style). Falsy array entries drop; object keys apply when truthy.

export function ClassConditionals() {
  let primary = true;
  let disabled = false;

  return (
    <button
      class={[
        "btn",
        primary ? "btn-primary" : "btn-secondary",
        disabled && "btn-disabled",
      ]}
      onclick={() => (primary = !primary)}
    >
      variant: {primary ? "primary" : "secondary"}
    </button>
  );
}

style accepts a string or an object. Object keys become per-property updates (including custom properties):

export function Progress() {
  let hue = 200;
  let progress = 30;

  return (
    <div
      style={{
        color: "white",
        background: `hsl(${hue}, 70%, 45%)`,
        "--progress": `${progress}%`,
      }}
    />
  );
}

Events

Lowercase DOM events on elements: onclick, oninput, onsubmit, and so on. Handlers are plain functions; call preventDefault yourself when you need it.


Package exports

| Import | Role | | --- | --- | | gallic / gallic/runtime | mount, flushSync | | gallic/vite | Vite plugin | | gallic/jsx | Ambient JSX types |

Components import nothing from gallica. Call mount from your app entry.