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 🙏

© 2024 – Pkg Stats / Ryan Hefner

single-page-app-router

v0.1.0

Published

## Installation

Downloads

59

Readme

single-page-app-router

Installation

npm i -S single-page-app-router

You can use it with any history solution, the most common being history I’ll showcase this one.

// <repo>/history.ts
import { createBrowserHistory } from 'history'

export const history = createBrowserHistory() // or Memory, or…

// <repo>/library/router.ts
import { RouterFactory } from 'single-page-app-router'
import { PathToRegexpAdapter } from 'single-page-app-router/adapter/PathToRegexp'
import { match } from 'path-to-regexp'
import { history } from '<repo>/history'

export const Router = RouterFactory({
  adapter: PathToRegexpAdapter(match),
  getPathname: () => history.location.pathname,
  onHistoryChange: history.listen,
  // Optional: you can provide a global route equality check
  isSameRoute: (a, b) => isDeepEqual(a, b),
})

Enforcing a route shape

// Optional: You can force a general route shape.
// This is useful to force a stable discriminant.
// Let’s our discriminant is "name"
type RouteShape = { name: string }

export const Router = RouterFactory<RouteShape>(…)

Recipes

Top-Level Routes

import { Router } from '<repo>/router'

type YourRoute =
  | { name: 'Home' }
  | { name: 'Product', id: number }
  | { name: 'NotFound', matchPathname?: string }

const router = Router<YourRoute>({
  NotFound: () => ({ name: 'NotFound' })
})({
  '/': () => ({ name: 'Home' }),
  '/product/:id{/*}?': ({ params, pathname }) => {
    pathname // "/product/:id{/*}"
    const id = Number(params.id)
    // For whatever reason, you can return `undefined`
    // it will resolve to "not found"
    // In our case, if the product id is not a number, let’s return `undefined`
    return Number.isNaN(id) ? undefined : { name: 'Product', id }
  },
})


router.route // YourRoute
router.onRouteChanged((newRoute, oldRoute) => {…})
router.destroy() // remove all `onRouteChanged` listeners.

Nested Routes

import { Router } from '<repo>/router'

type YourRoute =
  | { name: 'Home' }
  | { name: 'Product', id: number }
  | { name: 'NotFound', matchPathname?: string }

const basePath = '/:locale'

const router = Router<YourRoute>({
  NotFound: () => ({ name: 'NotFound' }),
})({
  basePath,
  routes: {
    '{/}?': ({ params }) => ({ name: 'Home' }),
    '/product/:id{/*}?': ({ params, pathname }) => {
      pathname // "/:locale/product/:id{/*}?"
      const id = Number(params.id)
      return Number.isNaN(id) ? undefined : { name: 'Product', id }
    },
  }
})


router.route // YourRoute
router.onRouteChanged((newRoute, oldRoute) => {})
router.destroy() // remove all `onRouteChanged` listeners.

Framework integrations

React Hook

const useRouter = (router: Router) => {
	const [route, setRoute] = useState(router.route)
	useEffect(() => {
		const unsubscribe = router.onRouteChanged((newRoute) => {
			setRoute(newRoute)
		})
		return unsubscribe
	}, [router])
	return route
}

Svelte

import { readable } from 'svelte'

const RouterToSvelteStore = (router: Router) => {
	return readable(router.route, (set) => {
		const unsubscribe = router.onRouteChanged((newRoute) => {
			set(newRoute)
		})
		return unsubscribe
	})
}

Reference

Router

type Unsubscribe = () => void

export interface Router<Route extends object> {
  route: Route
  onRouteChanged: (
    listener: (newRoute: Route, previousRoute: Route) => unknown,
  ) => Unsubscribe
  destroy: () => void
}

RouteData<Path>

What is injected in the route callback.

export interface RouteData<BasePath extends string, Path extends string> {
  params: PathParameters<`${BasePath}${Path}`>
  pathname: string
}

const router = Router<MyRoute>(…)({
  basePath: '/:locale',
  routes: {
    '{/}?': (data: RouteData<'/:locale', '{/}?'>) => {
      data // { params: { locale: string }, pathname: '/:locale{/}?' }
    },
    '/product/:id': (data: RouteData<'/:locale', '/product/:id'>) => {
      data // { params: { locale: string, id: string }, pathname: '/:locale/product/:id' }
    },
  }
})

Path Syntax

I based the library on URLPattern, which itself is based on path-to-regexp. Therefore, their syntax prevails.

The MDN website is an excellent place to start. Here are a few tips though:

  • /post/* will match /post/, /post/1 & /post/1/2 ; but not /post :warning: To match /post => post{/*}?
  • /post{/:id}? matches /post & /post/1, not /post/1/2
  • Regex groups like /books/(\\d+) can be used but break intellisense of path parameters
  • For nested routers, type the home as {/}? :wink:

Why yet-another X ?

Because I never encountered one that made sense to me:

[!Important] Routing and history are separate concerns. A history can be unique across the client-side app. Or you can nest them. I don’t care. Routing solves another problem anyway

You want routing? Fine: provide the history to watch changes, you'll get the active route in return.

You want some nested routing? Perfect, provide the history and a base path, you'll get the active route in return.

All in pure JS, testable with no framework, adaptable to every framework.

Testable: No jsdom needed, no {your framework}-library, no nothing. Aim at that 3ms test 😉.

Fully type-safe and type-driven for mad-typers. It comes with a double-function cost, but still worth it!

Now you have the treat of typed path parameters :stuck_out_tongue:

Contributing

Any contribution is welcome, fork and PR :grin:

# clone the repo
npm ci
npm run test