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

@place-ts/routing

v0.1.2

Published

Client-side routing for @place-ts/reactivity. v0.1 ships a hash-based router and a memoryRouter for tests, exposed as the Router capability so consumers can swap implementations without touching call sites.

Readme

Routing System

URL ↔ state mapping for place. Reactive path / segments / query, navigate / replace / link / url, typed route(pattern) paths and typed searchParams schemas (no codegen), three implementations (hash, history-API, in-memory), and a RouterCap capability so apps swap implementations without touching call sites.

Status: v0.4 shipping. 86 tests across hash, path, memory, link, url, typed routes, typed search params, normalization, capability behaviour. Full audit comparison against Next / Nuxt / React Router / TanStack — see docs/journal.

Why this is different

What every other router does:

  • React Router: <NavLink> component, useNavigate() hook, useLocation() hook — three concepts for what's logically one navigation thing
  • Next: global router singleton; <Link> is a component you import; modifier-click handling implicit
  • TanStack: typed routes (real win), but everything is still a component or a hook
  • Nuxt: file-based routing baked into the build tool

What we do:

| Concern | Conventional approach | This system | |---|---|---| | Navigate programmatically | useNavigate() hook | router.navigate('/x') — capability, no hook | | Render an anchor | <NavLink> / <Link> component | <a {...router.link('/x')}> — spread props | | Active styling | callback className / NavLink magic | aria-current="page" + CSS variants | | Modifier-click → new tab | manual or implicit in component | automatic, native <a href> flow | | Active flag in app code | useLocation() + compare | link.active() reactive, on the link itself | | Boot ceremony | <RouterProvider> wrapper | mount(view, '#app', { provide: [router] }) | | Test routing | mock the singleton | memoryRouter() cap, fully isolated | | Embed two apps | impossible w/ globals | each app passes its own router cap |

Quick start

import { mount } from '@place-ts/component'
import { hashRouter } from '@place-ts/routing'

mount(<App />, '#app', { provide: [hashRouter()] })

hashRouter() returns a RouterHandle that's also a Provision — pass it straight into provide:[…], no provide(RouterCap, router) wrapper.

In components, pull the cap:

import { RouterCap } from '@place-ts/routing'

const Sidebar = component(() => {
  const router = RouterCap.use()
  return (
    <nav>
      <a {...router.link('/')}>Home</a>
      <a {...router.link('/about')}>About</a>
    </nav>
  )
})

CSS handles the active state — no JS class composition needed:

nav a[aria-current="page"] { font-weight: 600; }

API surface

Implementations

  • hashRouter()location.hash-based. Works on any static host (file://, S3, GitHub Pages). Subscribes to hashchange. Use this when you don't control the server.
  • pathRouter() — History API mode. Clean URLs (/about, not /#/about). Subscribes to popstate. Requires the server to serve index.html for unknown routes (Vite dev does this; configure your prod host).
  • memoryRouter(initial = '/') — no global side effects. back/forward are no-ops. For tests, SSR, embedded contexts.

All three return the same RouterHandle:

interface RouterHandle extends Router, Provision {
  dispose(): void  // tests call; apps ignore
}

Router

interface Router {
  // Reactive reads — re-run watchers / reactive children on change
  path(): string                                  // current path, normalized ('/' for empty)
  segments(): readonly string[]                   // URL-decoded, cached parse
  segment(i: number): string | null               // single-segment shortcut
  query(): URLSearchParams                        // defensive clone per call
  param(key: string): string | null               // single-param shortcut

  // Navigation
  navigate(path: string, options?: { replace?: boolean; preserveQuery?: boolean }): void
  replace(path: string): void
  updateQuery(changes: Record<string, string | null>, options?: { replace?: boolean }): void
  back(): void
  forward(): void

  // Composition
  link(to: string, options?: { replace?: boolean; preserveQuery?: boolean }): Link
  url(to?: string): string                         // shareable absolute URL
}

Link

A reactive value that doubles as JSX props, programmatic navigator, and active-state accessor. Spread on any <a>; the spread only enumerates the DOM-safe properties (href, onClick, aria-current).

interface Link {
  // Spreadable on <a>
  readonly href: string
  readonly onClick: (e: MouseEvent) => void
  readonly 'aria-current': () => 'page' | undefined

  // Direct access (non-enumerable — won't leak via spread)
  readonly active: () => boolean
  go(): void
}

The onClick defers to the browser for modifier-clicks (Cmd/Ctrl/Shift/Alt) and middle/right-clicks, so "open in new tab" works natively.

parsePath(path)

Free utility for off-router parsing. Returns { segments, query }. URL-decodes; preserves the raw segment on a malformed escape rather than throwing.

route(pattern) — typed paths

const userPost = route('/users/:id/posts/:postId')
//      ^? Route<{ id: string; postId: string }>

userPost({ id: 'a', postId: '42' })          // '/users/a/posts/42'
userPost({ wrong: 'x' })                      // ❌ TS error
userPost.match('/users/a/posts/42')           // { id: 'a', postId: '42' }
userPost.match('/users/a/posts')              // null

router.navigate(userPost({ id: 'a', postId: '42' }))   // route returns a string
router.link(userPost({ id: 'a', postId: '42' }))       // same

Param shape is inferred at compile time from the pattern string via TS template-literal types. No codegen, no plugin, no CLI — just tsc. Compare to TanStack which ships a Vite/Rspack plugin to generate route trees.

URL-encodes param values when building, decodes when matching. The :name syntax captures whole segments only.

searchParams(schema) — typed query-param schemas

const filters = searchParams({
  tag:  (raw) => raw,                              // string | null
  page: (raw) => raw ? Number(raw) : 1,            // number, default 1
  sort: (raw) => raw === 'desc' ? 'desc' : 'asc',  // 'asc' | 'desc'
})

const { tag, page, sort } = filters.read(router)
//                          ^? { tag: string | null; page: number; sort: 'asc' | 'desc' }

filters.update(router, { tag: 'react' })            // typed
filters.update(router, { tag: null })               // remove the key
filters.update(router, { typo: 'x' })               // ❌ TS error
filters.update(router, { sort: 'asc' }, { replace: true })  // don't grow back stack

No Zod or other validator dependency — the schema is { key: parseFn } and TS infers the result type from each parser's return. Compare to TanStack which integrates with Zod for validation.

read() is reactive — call it inside a watch / reactive child and it re-runs on path change. update() String()-coerces non-null values; null/undefined deletes the key.

Patterns

Derive selection from the URL

const selectedId = (): string | null => router.segment(0)

Deep-linking, refresh-survival, back-button correctness all fall out — no separate state, no separate persistence.

Filter UI without growing history

const setTag = (tag: string | null) => router.updateQuery({ tag }, { replace: true })

Every filter click would otherwise add to the back stack; replace keeps it flat.

"Selecting an item" preserving filter

const onSelect = (id: string) => router.navigate(`/${id}`, { preserveQuery: true })

Switches the path but keeps ?tag=react etc. — the user's "I'm browsing this slice" intent doesn't reset.

Won't ship (anti-bloat)

These are real features in other libraries; we deliberately don't include them:

  • Scroll restoration. Real UX gap, real implementation cost (history.state coordination, per-route scroll capture). Defer until concrete trigger.
  • Route loaders (loader: async () => …). @place-ts/reactivity's resource() already covers async data; framework integration would be glue.
  • File-based routing (pages/about.tsx/about). Build-tool concern; contradicts our minimal-surface charter.
  • Nested route trees (<Route>s with <Outlet>). JSX composition + a small dispatch() function does this without DSL.
  • Route guards / middleware. A component that calls router.replace('/login') in a watch does the job without a guard concept.
  • Codec libraries for search params. If your parse function needs validation, write the validation in the parse function; if it needs Zod, call Zod from the parse function. The schema doesn't dictate the validator.