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

@runefw/rune

v0.1.6

Published

A compiler-driven frontend framework experiment.

Readme

@runefw/rune

Rune lets you write reactive UI with normal TypeScript variables. Write components in .rune files, write reusable logic or app entries in *.rune.ts, and Rune compiles the reactive behavior for Vite apps.

API Index

| API | Example | |-----------------------------------------------------------------|---------------------------------------------------------------------------------| | prop | Props Example | | emit | Events Example | | let / var reactive state | Reactive State Example | | $computed | Computed Example | | $effect | Effect Example | | $watch | Watch Example | | $batch | Batch And Untrack Example | | $untrack | Batch And Untrack Example | | $readonly | Readonly Views Example | | $shallowReadonly | Readonly Views Example | | $shallowAuto | Readonly Views Example | | $mounted | Lifecycle And Owned Callback Example | | $cleanup | Lifecycle And Owned Callback Example | | $owned | Lifecycle And Owned Callback Example | | template -> (...) | Template Example | | if / for / key | Template Example | | template expression functions | Template Expression Functions Example | | {await ...} | Await Blocks Example | | on:* | Directives Example | | bind:* | Directives Example | | class:* | Directives Example | | style:* | Directives Example | | use:* | DOM Action Example | | <rune:slot> | Built-in Tags Example | | <rune:fragment> | Built-in Tags Example | | <rune:component> | Built-in Tags Example | | #text | Raw Text And HTML Example | | #html | Raw Text And HTML Example | | style -> {} / style.css -> {} / style.scss / style.less | Scoped Style Example | | RuneAttrs<Tag> | DOM Attrs Example | | use* composables | Composable Example | | $render | Render Entry Example |

Related Packages

| Package | Purpose | |------------------------------------------------------------------------------------|-------------------------------------------------| | @runefw/create | Project scaffolder for Rune apps and libraries. | | @runefw/prettier-plugin | Prettier integration for .rune files. | | @runefw/formatter | Shared formatter core used by Rune tooling. |

Examples

Component Basics Example

prop string label
let count = 0

function increment() {
  count += 1
}

template -> (
  <button on:click={increment}>
    {label}: {count}
  </button>
)

Props Example

prop declares parent-to-child inputs. Props are deep readonly inside the child component.

prop string title
prop number count = 0
prop string subtitle?
prop user = {
  name: "Rune",
  age: 0
}

template -> (
  <article>
    <h2>{title}</h2>
    <p>{subtitle ?? "No subtitle"}</p>
    <p>{user.name}: {count}</p>
  </article>
)

prop name = default is inferred by TypeScript. Props without a default value must use an explicit type.

Events Example

emit declares child-to-parent events.

// TextField.rune
prop value = ""
emit string Change

function update(event: Event) {
  const input = event.currentTarget as HTMLInputElement
  Change(input.value)
}

template -> (
  <input value={value} on:input={update} />
)

Parents listen with component events.

// Parent.rune
import TextField from "./TextField.rune"

template -> (
  <TextField on:Change={(nextValue) => console.log(nextValue)} />
)

Reactive State Example

Top-level let and var bindings are reactive state. Objects, arrays, Map, and Set are deep reactive by default.

let count = 0
let user = { name: "Rune" }
let users = new Map<string, { name: string }>()

function rename() {
  count += 1
  user.name = "Compiler"
  users.set("a", { name: "Ada" })
  users.get("a")!.name = "Grace"
}

template -> (
  <button on:click={rename}>
    {user.name}: {count}
  </button>
)

WeakMap and WeakSet stay ordinary values because they cannot be traversed for deep tracking.

Computed Example

Use $computed for derived state.

let count = 0
const doubled = $computed(() => count * 2)

template -> (
  <button on:click={() => (count += 1)}>
    {count} / {doubled}
  </button>
)

Effect Example

Use $effect for side effects that track reactive reads.

let count = 0

$effect(() => {
  console.log("count changed", count)
  return () => console.log("previous effect disposed")
})

template -> (
  <button on:click={() => (count += 1)}>
    Count {count}
  </button>
)

Watch Example

Use $watch when you want a callback only after a source changes, with both the next and previous values.

let count = 0
let user = { name: "Rune" }

$watch(count, (next, previous) => {
  console.log("count", next, previous)
})

$watch(() => user.name, (name, oldName) => {
  console.log("name", name, oldName)
})

$watch(user, (nextUser) => {
  console.log("deep by default", nextUser.name)
})

$watch(user, (nextUser) => {
  console.log("reference changes only", nextUser)
}, { deep: false })

$watch(user, () => {
  console.log("direct fields changed")
}, { deep: 1 })

$watch.post(count, (next) => {
  console.log("after DOM update", next)
})

template -> (
  <button on:click={() => (count += 1)}>
    {user.name}: {count}
  </button>
)

Batch And Untrack Example

Use $batch to group writes and $untrack to read without subscribing the current effect.

let first = "Ada"
let last = "Lovelace"

function rename() {
  $batch(() => {
    first = "Grace"
    last = "Hopper"
  })
}

$effect(() => {
  console.log("tracked", first)
  console.log("snapshot", $untrack(() => last))
})

template -> (
  <button on:click={rename}>
    {first} {last}
  </button>
)

Readonly Views Example

Use readonly views when you want to share reactive data without exposing mutation.

let state = {
  user: { name: "Rune" },
  count: 0
}

const readonlyState = $readonly(state)
const shallowState = $shallowReadonly(state)
const shallowReactive = $shallowAuto({ active: true })

template -> (
  <p>
    {readonlyState.user.name}: {shallowState.count} / {shallowReactive.active ? "active" : "idle"}
  </p>
)

Lifecycle And Owned Callback Example

Use $mounted after DOM insertion, $cleanup for owner cleanup, and $owned when a callback runs later but still needs the current cleanup owner.

let button: HTMLButtonElement | undefined
let clicks = 0

const subscribeLater = $owned(() => {
  const timer = window.setInterval(() => {
    clicks += 1
  }, 1000)

  $cleanup(() => window.clearInterval(timer))
})

$mounted(() => {
  button?.addEventListener("click", subscribeLater)
  return () => button?.removeEventListener("click", subscribeLater)
})

template -> (
  <button bind:this={button}>
    Started {clicks} timers
  </button>
)

Template Example

Templates use HTML-like markup with JavaScript expressions and control flow.

let items = [
  { id: 1, label: "Compiler" },
  { id: 2, label: "Runtime" }
]

template -> (
  <section>
    if (items.length === 0) {
      <p>No items</p>
    } else {
      for (const item of items) {
        <p key={item.id}>{item.label}</p>
      }
    }
  </section>
)

key={expr} is compiler-only list identity. It is not emitted as a DOM attribute or component prop.

Template Expression Functions Example

Template expressions can use TSX-like arrow callbacks, ordinary function callbacks, and IIFEs that return Rune template nodes. Returning null, false, or undefined renders an empty branch.

type User = { id: string; name: string; visible: boolean }

let users: User[] = [
  { id: "ada", name: "Ada", visible: true },
  { id: "grace", name: "Grace", visible: false }
]

let user: User = { id: "lin", name: "Lin", visible: true }

template -> (
  <section>
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>

    <ul>
      {users.map(function (user) {
        return user.visible ? <li key={user.id}>{user.name}</li> : null
      })}
    </ul>

    {(() => {
      if (!user.visible) return null
      return <article>{user.name}</article>
    })()}
  </section>
)

These are Rune template expression forms. Rune does not enable a TSX runtime for ordinary TypeScript value positions.

Await Blocks Example

Use {await ...} as a template expression block for Promise state. The pending, resolved, and rejected branches all render real Rune template nodes.

type User = { name: string }

let userId = "ada"

function fetchUser(id: string): Promise<User> {
  return Promise.resolve({ name: id })
}

template -> (
  <section>
    {await fetchUser(userId) {
      <p>Loading...</p>
    } then (user) {
      <p>{user.name}</p>
    } catch (error) {
      <p>Failed</p>
    }}
  </section>
)

then receives the resolved value. catch receives an unknown error value. When the Promise source changes, stale resolve or reject results are ignored.

Unsupported forms include naked await children, <p>{await promise}</p>, async .map(...) callbacks that return templates, and loading-only await blocks without then.

Directives Example

Rune template directives are compiler features for native DOM elements.

let name = ""
let active = true
let accent = "#2563eb"

template -> (
  <label class:active style:color={accent}>
    Name
    <input
      bind:value={name}
      on:keydown.enter={() => (active = !active)}
    />
  </label>
)

When the directive name is a JavaScript identifier, class:active means class:active={active} and style:color means style:color={color}. Use an explicit value for kebab-case names and CSS custom properties, such as class:is-active={active} or style:--accent={accent}.

DOM Action Example

use:name calls the same-named action function with the DOM node. If the directive has a value, its second argument is a getter that can be passed to $watch.

let message = "Save"

function tooltip(node: HTMLElement, text: () => string) {
  $watch(text, (value) => {
    node.dataset.tooltip = value
  }, { immediate: true })

  $cleanup(() => {
    delete node.dataset.tooltip
  })
}

template -> (
  <button use:tooltip={message}>Save</button>
)

Actions use $watch or $effect for updates and $cleanup for disposal; they do not return an update / destroy object.

Built-in Tags Example

Rune built-in tags use the rune: namespace.

// Panel.rune
template -> (
  <article>
    <header><rune:slot name="header">Fallback header</rune:slot></header>
    <main><rune:slot /></main>
  </article>
)
// DetailsPanel.rune
prop string title

template -> (
  <p>{title}</p>
)
// Page.rune
import Panel from "./Panel.rune"
import DetailsPanel from "./DetailsPanel.rune"

let title = "Overview"
let CurrentPanel = DetailsPanel

template -> (
  <Panel>
    <rune:fragment slot="header">
      <h2>{title}</h2>
    </rune:fragment>

    <rune:component is={CurrentPanel} title={title} key={title} />
  </Panel>
)

Raw Text And HTML Example

Use #text for safe raw text and #html for trusted raw HTML.

let safeText = "<strong>shown as text</strong>"
let trustedHtml = "<strong>rendered as HTML</strong>"

template -> (
  <article>
    <pre>#text { $1 }(safeText)</pre>
    <div>#html { $1 }(trustedHtml)</div>
  </article>
)

#html does not sanitize or escape content. Only use it with trusted or application-sanitized HTML.

Scoped Style Example

style -> {} and style.css -> {} define component-scoped CSS. style.scss, style.less, and other Vite CSS preprocessor blocks are compiled to CSS first, then scoped by Rune.

let accent = "#2563eb"

template -> (
  <article class="card">
    Scoped styles
  </article>
)

style -> {
  .card {
    border: 1px solid #d1d5db;
    color: {accent};
  }

  :global(body) {
    margin: 0;
  }
}

Selectors are scoped to native DOM elements declared by the component template. Styles do not pierce child components.

Preprocessor syntax belongs to the preprocessor, not Rune:

template -> (<button class="button" style:--button-color={accent}>Save</button>)

style.scss -> {
  @use "./tokens.scss" as *;

  .button {
    color: var(--button-color);
    border-color: $border;
  }
}

Rune {expr} style interpolation is only supported in style -> {} / style.css -> {}. In preprocessor blocks, pass reactive values through CSS custom properties with style:*.

DOM Attrs Example

Use RuneAttrs<Tag> for DOM attribute passthrough props.

prop string label
prop RuneAttrs<"input"> inputProps = {}

template -> (
  <label>
    {label}
    <input {...inputProps} />
  </label>
)

Composable Example

*.rune.ts files can define composables. Functions named use* follow React Hooks-style stable call rules.

// useCounter.rune.ts
export function useCounter(initial = 0) {
  let count = initial;
  const doubled = $computed(() => count * 2);

  function increment() {
    count += 1;
  }

  return { count, doubled, increment };
}

Import the composable with a Rune specifier.

import { useCounter } from "./useCounter.rune";

const counter = useCounter();

template -> (
  <button on:click={counter.increment}>
    {counter.count} / {counter.doubled}
  </button>
)

Do not call composables in conditions, loops, event handlers, template expressions, or ordinary nested functions.

Render Entry Example

Applications render from a *.rune.ts entry file.

// main.rune.ts
import App from "./App.rune";

$render(App, "#app");

$render is only valid at the top level of a Rune-aware TypeScript entry module.