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

@emkodev/emroute

v1.12.4

Published

File-based (but storage-agnostic) router with triple rendering (SPA, SSR HTML, SSR Markdown). Zero dependencies.

Readme


Every route renders three ways from the same component: as a Single Page App in the browser, as server-rendered HTML, and as plain Markdown. No separate API layer needed — prefix any route with /md/ and get text that LLMs, scripts, and curl can consume directly.

GET /projects/42          → SPA (hydrated in browser)
GET /html/projects/42     → pre-rendered HTML
GET /md/projects/42       → plain Markdown

Install

npm add @emkodev/emroute    # or bun add, pnpm add, yarn add

Works on Node, Bun, and Deno. Node uses compiled JS; Bun and Deno use TypeScript source directly.

For markdown rendering, add @emkodev/emkoma (built for emroute) or bring your own — marked and markdown-it both work.

How It Works

One component, three rendering paths:

The SPA and SSR HTML flows both call renderHTML() — same output, different delivery. The SSR Markdown flow calls renderMarkdown() instead, bypassing HTML entirely for plain text output.

Routes are files. The filesystem is the config.

routes/
  index.page.md              → /
  about.page.html            → /about
  projects.page.md           → /projects
  projects/
    [id].page.ts             → /projects/:id
    [id]/
      tasks.page.ts          → /projects/:id/tasks
  404.page.html              → not found
  index.error.ts             → root error handler

A route can be a .md file, an .html template, a .ts component, or a combination. When a .page.ts exists, it controls data fetching and rendering. When it doesn't, the framework renders the .html or .md file directly.

import { PageComponent } from '@emkodev/emroute';

class ProjectPage extends PageComponent<{ id: string }, ProjectData> {
  override readonly name = 'project';

  override async getData({ params }: this['DataArgs']) {
    const res = await fetch(`/api/projects/${params.id}`);
    return res.json();
  }

  override renderHTML({ data, params, context }: this['RenderArgs']) {
    // context.files.html has the companion .page.html template if it exists
    const template = context.files?.html ?? `<h1>${data.name}</h1>`;
    return template.replaceAll('{{id}}', params.id) + '<router-slot></router-slot>';
  }

  override renderMarkdown({ data, context }: this['RenderArgs']) {
    // context.files.md has the companion .page.md content if it exists
    return context.files?.md ?? `# ${data.name}\n\nStatus: ${data.status}`;
  }
}

export default new ProjectPage();

Features

  • File-based routing with dynamic segments ([id]), catch-all directories, and nested layouts via <router-slot>. Routes follow REST conventions: exact routes are terminal resources, catch-all directories own their namespace
  • Triple rendering — SPA, SSR HTML, SSR Markdown from one component
  • Companion files.page.html, .page.md, .page.css loaded automatically and passed through context
  • Widgets — interactive islands with their own data lifecycle, error handling, and optional file companions (.html, .md, .css). Auto-discovered from a widgets/ directory or registered manually. this.element gives opt-in DOM access in the browser. <widget-foo lazy> defers loading until visible via IntersectionObserver
  • View Transitions — SPA route changes animate via document.startViewTransition(). Progressive enhancement with CSS-only customization
  • Scoped CSS — companion .widget.css files auto-wrapped in @scope (widget-{name}) { ... }
  • Shadow DOM — unified Declarative Shadow DOM architecture for SSR and SPA. Widgets render into shadow roots for true CSS encapsulation and Web Components spec compliance
  • SSR hydration — server-rendered HTML adopted by the SPA without re-rendering. Widgets can implement hydrate(args) to attach event listeners after SSR adoption, receiving { data, params, context }
  • Error boundaries — scoped error handlers per route prefix, plus status pages (404.page.html) and a root fallback
  • Extensible context — inject app-level services (RPC clients, auth, feature flags) into every component via extendContext on the router. Type-safe access through module augmentation or a per-component generic
  • Declarative overlays — popovers, modals, and toasts with zero JS via Invoker Commands API and CSS keyframe animations. Programmatic API available for dynamic content
  • Zero dependencies — native APIs only (URLPattern, custom elements, Navigation API). No framework runtime, no virtual DOM, no build-time magic
  • Pluggable markdown<mark-down> custom element with a swappable parser interface; bring your own renderer
  • Redirects — declarative .redirect.ts files with 301/302 support
  • Configurable base paths/html/ and /md/ prefixes are configurable via BasePath
  • SPA modes'root' (default), 'leaf', 'none', or 'only' to control how the server handles non-file requests and SSR endpoints
  • Sitemap generation — opt-in sitemap.xml from the routes manifest with support for dynamic route enumerators
  • On-the-fly transpilationBunFsRuntime serves .ts files as transpiled JavaScript with companion files inlined. No build step required for development. buildClientBundles() is an optional production optimization

Runtimes

The router is storage-agnostic — it reads routes through a Runtime abstraction, not the filesystem directly. emroute ships four runtimes:

  • BunFsRuntime — Bun-native APIs (Bun.file(), Bun.write(), Bun.Transpiler). The default choice for Bun projects.
  • UniversalFsRuntimenode: APIs only. Works on Node, Deno, and Bun.
  • BunSqliteRuntime — stores routes in a SQLite database. Proves the storage-agnostic design: no filesystem needed.
  • FetchRuntime — browser runtime that fetches files from a remote server. Powers the SPA in root and only modes.

Bun runs TypeScript source directly. Node and Deno use the compiled JS from dist/. See ADR-0017 for the full analysis.

Getting Started

Pick your runtime: Bun | Node | Deno

Documentation

  • Setup — Bun, Node, Deno
  • First route — route files and rendering modes
  • Pages — page components, companion files, data fetching
  • Routing — dynamic segments, catch-all, redirects
  • Nesting — layouts, slots, passthrough pages, tips and tricks
  • Widgets — interactive islands with data lifecycle
  • ServerEmroute.create, composition, static files
  • Markdown renderers — pluggable parser interface and setup
  • Runtime — abstract runtime, UniversalFsRuntime, BunFsRuntime, BunSqliteRuntime
  • SPA modes — none, leaf, root, only
  • Error handling — widget errors, boundaries, status pages
  • Shadow DOM — unified architecture, SSR hydration
  • Hono integration — using emroute with Hono

For contributors and architects