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

@llui/router

v0.12.1

Published

LLui router — named type-safe routes, Standard Schema codecs, history/hash mode

Readme

@llui/router

Named, type-safe URL routing for LLui, with history and hash modes, guards, and link helpers.

pnpm add @llui/router

Named routes

Routes live in one keyed registry. Its keys are stable route names, and each path template determines the exact parameters accepted by matching and generation.

import { createRouter, route, routeCodec } from '@llui/router'
import { z } from 'zod'

const integer = routeCodec(z.coerce.number().int().positive(), String)

const routes = {
  home: route('/'),
  article: route('/articles/:slug'),
  archive: route('/archive/:year?', {
    params: { year: integer },
    defaults: { year: 2026 },
  }),
  search: route('/search', {
    query: { page: integer },
    defaults: { page: 1 },
  }),
  files: route('/files/*path'),
  login: route('/login'),
  admin: route('/admin'),
  editor: route('/editor/:slug'),
}

const router = createRouter(routes)

router.href('home') // '#/'
router.href('article', { slug: 'typed-urls' }) // '#/articles/typed-urls'
router.href('search', {}) // '#/search' (the default is omitted)

router.match('/archive/2025')
// { name: 'archive', params: { year: 2025 } }

router.match('/not-a-route') // null

Templates support static segments, required :parameter segments, optional :parameter? segments at any position, and final *rest segments. Plain path parameters are strings. Rest parameters are decoded segment arrays, so an encoded slash remains inside its original segment. A template cannot combine optional and rest segments because their boundary is not bidirectionally representable.

Matching precedence is static, then parameter, then rest, independent of registry order. Equal-specificity templates that can overlap are rejected at construction with both route names. Because arbitrary validators cannot prove that two parameter domains are disjoint, otherwise identical typed-parameter templates are treated as overlapping; give them distinct static structure.

match() returns a route location containing URL identity only. Keep fetched data, drafts, loading state, and other page state in your application model.

Route codecs and Standard Schema

routeCodec(schema, format) pairs any synchronous Standard Schema v1 validator with canonical formatting. The schema's output type becomes the parameter type; the router has no runtime dependency on Zod, Valibot, or another validator.

Query parameters must be declared explicitly. Use repeatedRouteCodec() when repeated query values or a rest parameter form one semantic array:

import { repeatedRouteCodec, route } from '@llui/router'
import * as v from 'valibot'

const tags = repeatedRouteCodec(v.array(v.pipe(v.string(), v.nonEmpty())), (values) => values)

const tagged = route('/tagged', {
  query: { tag: tags },
  defaults: { tag: [] },
})

Scalar query codecs reject duplicate values. Unknown query keys do not prevent matching and are omitted by generation. Defaults are always present after matching, optional while generating, and omitted from the canonical URL; explicitly passing undefined for a defaulted parameter is equivalent to omitting it, and the rule also applies to rest defaults. Object and array defaults are cloned into each returned location, so mutating one result cannot affect another. Each default must itself round-trip through its codec; invalid or noncanonical defaults are rejected when the router is created. Generated URLs are accepted only when matching them produces the same complete normalized parameters, so a lossy formatter fails instead of silently addressing a different location.

Route locations remain JSON-roundtrip-safe URL identity. Absent nondefaulted optional parameters are omitted, while defaulted parameters remain present. Values may contain null, strings, booleans, finite numbers other than negative zero, ordinary dense arrays without owned state, and ordinary plain data objects. undefined, functions, symbols, bigints, non-finite or negative-zero numbers, sparse/stateful/subclassed arrays, cyclic values, null/custom-prototype objects, and class or built-in collection instances are rejected with a contextual error.

A route may declare refine, a synchronous Standard Schema over the complete normalized parameter object. It may reject the object, but it may not transform it or add page data. Any schema returning a Promise produces a descriptive configuration error because routing remains synchronous.

Connected routing

import { connectRouter } from '@llui/router/connect'

const routing = connectRouter(router)

routing.push('article', { slug: 'typed-urls' })
routing.replace('search', { page: 2 })
routing.navigate('home')

// In a view:
routing.link(send, 'article', { slug: 'typed-urls' }, { class: 'link' }, children)

link, push, replace, and navigate use the same route-name-specific destination type as href. Navigating to the current canonical location is a full no-op: no guards, history write, or message.

Place ...routing.listener(send) in the view to handle browser back/forward and address-bar changes. A matched URL dispatches { type: 'navigate', location }. An invalid browser URL dispatches { type: 'unmatched', url }; the application decides whether to render not-found state or navigate intentionally. After guards accept a noncanonical matched URL, the connector replaces it with the canonical URL without adding history or dispatching twice.

Guards

const routing = connectRouter(router, {
  beforeEnter(to) {
    if (to.name === 'admin' && !isLoggedIn()) return router.location('login')
  },
  beforeLeave(from) {
    if (from.name === 'editor' && hasUnsavedChanges()) {
      return confirm('Discard unsaved changes?')
    }
    return true
  },
})

Redirect targets are normalized route locations and chain until accepted, blocked, or settled at the same canonical URL, with the existing 10-hop bound. beforeLeave runs once for the originally requested navigation.

History / Location adapter

All browser reads and mutations go through RouterEnv. browserRouterEnv() is the default; inject your own adapter for tests, SSR hosts, or embedded frames. The connector stamps entries it creates so a guard-blocked browser traversal can be restored without destroying forward history. It never guesses a position for foreign or otherwise unstamped entries.

import { browserRouterEnv, connectRouter } from '@llui/router/connect'

const routing = connectRouter(router, { env: browserRouterEnv() })