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

history-manager

v3.5.0

Published

A small, dependency‑light client‑side router built directly on top of the browser **History API** (`pushState`/`popstate`). It adds the pieces a single page application actually needs on top of raw history:

Readme

history-manager

A small, dependency‑light client‑side router built directly on top of the browser History API (pushState/popstate). It adds the pieces a single page application actually needs on top of raw history:

  • path routing with named parameters (powered by path-to-regexp)
  • contexts — named groups of routes with their own default and fallback paths
  • navigation locks — trap the back button to guard unsaved work or modal flows
  • redirections, query‑string helpers and multiple independent sub‑routers
  • works both in hash mode (#/path) and in clean‑URL mode (/path, via the History API)

Navigation is fully serialized through an internal work queue, so go, lock/unlock and popstate never interleave in surprising ways.

Installation

npm install history-manager

The package ships two builds:

| build | file | format | | ----- | ---- | ------ | | ES module | dist/index.es.js | es | | UMD (browser global historyManager) | dist/index.js | umd |

// ES module
import { Router } from "history-manager";
<!-- UMD: exposes window.historyManager -->
<script src="history-manager/dist/index.js"></script>
<script>
    const { Router } = window.historyManager;
</script>

Quick start

import { Router } from "history-manager";

// 1. declare routes: a path pattern -> a callback
Router.route("/user/:id", (location, keymap) => {
    console.log("user page", keymap.get("id"), location.href);
});
Router.route("/me", location => {
    console.log("my profile");
});

// 2. (optional) group routes into a context
Router.setContext({
    name: "profile",
    paths: [
        { path: "me" },
        { path: "user/:id", fallback: true } // used when the context can't be resolved
    ],
    default: "me" // where the context lands by default
});

// 3. start the router (registers the popstate listener and emits the current route)
await Router.start("profile");

// 4. navigate
await Router.go("/user/12");   // push a new url
await Router.go(-1);           // go back one entry, like history.back()

By default urls are hash‑based (https://app/#/user/12). To use clean urls via the History API, set the base to / before starting:

Router.setBase("/");
await Router.start();
// -> https://app/user/12

Core concepts

Routes

A route pairs a path pattern with a callback. Patterns use path-to-regexp syntax, so :name segments become named parameters.

Router.route("/product/:category/:id", (location, keymap, redirection) => {
    keymap.get("category"); // matched param
    keymap.get("id");
    location.getQueryParam("ref"); // read the query string
});

The callback receives:

| argument | type | description | | -------- | ---- | ----------- | | location | Location | the resolved location (see below) | | keymap | Map<string, any> | matched path parameters | | redirection | { location, keymap } \| null | set when the route was reached through a redirection |

Remove a route with Router.unroute(path).

The Location object

Every callback (and Router.getLocation()) gives you a Location:

type Location = {
    href: string;
    pathname: string;
    hash: string;
    query: string;
    parsedQuery: any;

    hasQueryParam(param: string): boolean;
    getQueryParam(param: string): string | null | undefined;
    addQueryParam(param: string, value: string | null): void;
    removeQueryParam(param: string): void;

    hrefIf(go: string): string; // resolve a relative navigation without performing it
};

Contexts

A context is a named set of paths with a default landing url and optional fallbacks. Contexts let you preserve "where the user was" across sections of the app (e.g. keep the current profile when jumping between tabs) and recover gracefully when a url can't be matched.

Router.setContext({
    name: "search",
    paths: [{ path: "search" }],
    default: "search?recent"
});

Router.getContext();                 // context of the current url
Router.restoreContext("search");     // navigate back to a context's last/known url
Router.getContextDefaultOf("search");// "search?recent"

You can also build contexts incrementally:

Router.addContextPath("profile", "user/:id", /* isFallback */ true);
Router.setContextDefaultHref("profile", "me");

Path precedence and catch‑all fallbacks

When resolving which context a url belongs to (e.g. on a deep‑link or a page refresh), paths are matched in two tiers:

  1. primary paths of every context — a primary path always wins over any fallback, in any context;
  2. fallback paths — only if no primary matched.

Within each tier the declaration order decides: the first context whose path matches wins. This matters as soon as you use a catch‑all fallback such as {/*route} (path‑to‑regexp v8 optional group), which matches every url:

// ⚠️ order-sensitive
Router.setContext({
    name: "home",
    paths: [{ path: "/home" }, { path: "{/*route}", fallback: true }], // catch‑all
    default: "/home"
});
Router.setContext({
    name: "profile",
    paths: [{ path: "/me" }, { path: "/users/:id", fallback: true }],
    default: "/me"
});

Router.getContext("/users/2"); // -> "home"  (home's catch‑all matched first)

Because both /users/:id and the catch‑all are fallbacks, tier 2 falls back to declaration order, and the catch‑all — declared first — shadows the more specific /users/:id. Declare catch‑all fallbacks last (and, more generally, more specific fallbacks before broader ones) so they only catch what nothing else does:

// ✅ profile (specific) declared before home (catch‑all)
Router.setContext({ name: "profile", paths: [{ path: "/me" }, { path: "/users/:id", fallback: true }], default: "/me" });
Router.setContext({ name: "home",    paths: [{ path: "/home" }, { path: "{/*route}", fallback: true }], default: "/home" });

Router.getContext("/users/2"); // -> "profile"

Note that a catch‑all only ever competes as a fallback: a primary path in any context (even one declared later) still wins over it.

Navigation locks

A lock traps the back button. This is useful to protect a form with unsaved changes or to keep a modal/flow open until it is explicitly dismissed.

const lock = await Router.lock();

Router.isLocked(); // true

By default a lock is hard: back navigation is trapped and a non‑forced unlock() is refused. Only a forced unlock releases it.

await Router.unlock();       // force = true (default) -> always releases
await Router.unlock(false);  // cooperative -> refused by a hard lock, returns false

To make a lock cooperative, attach a listener. When a release is attempted (a non‑forced unlock, or the trapped back button), the listener receives a cancelable "navigation" event. Call event.preventDefault() to approve the release; do nothing to keep the lock held.

lock.listen(event => {
    if (userConfirmedLeaving()) {
        event.preventDefault(); // approve: allow the release / navigation
    }
    // otherwise the lock stays active
});

// You can also release through the returned manager:
lock.unlock();

Note the inverted convention: on the lock's "navigation" event preventDefault() means approve, not veto.

Locks are stacked LIFO: locking three times requires three unlocks to fully release, and a balanced lock/unlock sequence always lands back on the original url.

Redirections

Router.redirect("/old-page", "/new-page");
Router.unredirect("/old-page");

Query parameters

await Router.setQueryParam("tab", "settings", { emit: true });
await Router.setQueryParam("tab", undefined, { emit: true }); // remove it

Sub‑routers

Router is the main instance. Create additional independent routers with the same routing API when you need isolated route tables (e.g. per‑widget):

const widgetRouter = Router.create();
widgetRouter.route("/panel/:name", location => { /* ... */ });

API reference (Router)

| method | description | | ------ | ----------- | | route(path, cb) / unroute(path) | register / remove a route | | redirect(path, to) / unredirect(path) | register / remove a redirection | | setContext({ name, paths, default }) | declare a context | | addContextPath(context, href, isFallback?) | add a path to a context | | setContextDefaultHref(context, href) | set a context's default url | | getContext(href?) / getContextDefaultOf(context) | inspect contexts | | restoreContext(context) | navigate to a context | | start(startingContext?) | register listeners and emit the current route | | go(direction, { emit?, replace? }) | navigate to a url (string) or by delta (number) | | setQueryParam(param, value, { emit, replace? }) | add/remove a query param and navigate | | getLocation() / getLocationAt(index) / index() | inspect the current / historic location | | emit(emitOnlyMain?) | re‑run the matching route callbacks for the current url | | lock() / unlock(force = true) / isLocked() | navigation locks | | getBase() / setBase(base) | read / set the url base ("/" for clean urls) | | create() | create an independent sub‑router |

The package also exports the lower‑level building blocks the Router is built on: HistoryManager, NavigationLock, ContextManager, OptionsManager, PathGenerator and URLManager.

Events

The router dispatches a couple of window events you can hook into:

  • historylanded — fired after a navigation has fully settled (used, for example, to await a history.back() completing).
  • router:going — a cancelable CustomEvent fired before a navigation; call preventDefault() to abort it.

Development

npm run build        # bundle with rollup into dist/
npm test             # run the Playwright + Mocha suite on chromium, firefox and webkit
npm run test:manual  # serve the manual test page for interactive checking

Tests drive a real browser through Playwright against an Express dev server, so the full History API behaviour (including popstate and the back‑button traps) is exercised end to end.

License

MIT © Giuliano Collacchioni