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

perun

v2.0.0

Published

Simple 100% typesafe router for Preact using Preact signals and Zod

Downloads

26

Readme

perun

NPM Build status

Simple 100% typesafe router for Preact using Preact signals and Zod.

Table of contents

Install

npm install perun

Or, if you are using yarn:

yarn add perun

Usage

Example with three routes

export const routes = {
  plyersCountry: createRoute({
    routePattern: "/players/[country]/[playername]",
    renderComponent: (props) => (
      <TestComponent2
        country={props.country}
        player={props.playername}
        queryParams={props.queryParams}
      />
    ),
    searchParamsValidator: z.object({
      ime: z.string(),
      prezime: z.string().optional(),
    }),
  }),

  lastNameId: createRoute({
    routePattern: "/[id?]/ime/[lastname]",
    renderComponent: (props) => (
      <TestComponent lastname={props.lastname} id={props.id ?? "name id"} />
    ),
    searchParamsValidator: z.object({}),
  }),

  asyncRoute: createAsyncRoute({
    routePattern: "/async/[route]",
    renderComponent: (props) =>
      import("./async").then((module) => (
        <module.AsyncComponent route={props.route} />
      )),
    searchParamsValidator: z.object({}),
  }),
};

const NoRoutesMatch = () => {
  return <div>404, requested route is not defined :(</div>;
};

export const App = () => {
  const toPersonWithId = useCallback(() => {
    routes.lastNameId.routeTo({ id: "marko", lastname: "jerkic" });
  }, []);

  const toPerson = useCallback(() => {
    routes.lastNameId.routeTo({ lastname: "jerkic" });
  }, []);

  const toPlayer = useCallback(() => {
    routes.plyersCountry.routeTo({
      playername: "stipe",
      country: "hrv",
      queryParams: {
        godine: 22,
        ime: "Stipe",
        prezime: "Stipić",
      },
    });
  }, []);

  const toAsyncRoute = useCallback(() => {
    routes.asyncRoute.routeTo({ route: "neka" });
  }, []);

  return (
    <>
      <p>Bok, ovo je moj router :)</p>
      <div class="flex space-x-4 my-4">
        <routes.plyersCountry.Link
          playername="Marko"
          country="Hrvatska"
          queryParams={{ ime: "Marko", prezime: "Jerkić", godine: 22 }}
        >
          Na igrač marko ajde
        </routes.plyersCountry.Link>
        <button className="bg-red-300" onClick={() => toPlayer()}>
          Idemo na igrač stipe iz hrv
        </button>
        <button className="bg-blue-300" onClick={() => toPerson()}>
          Idemo na osobu jerkic
        </button>
        <button className="bg-blue-300" onClick={() => toPersonWithId()}>
          Idemo na osobu jerkic s identifikatorom
        </button>
        <button className="bg-fuchsia-300" onClick={() => toAsyncRoute()}>
          Idemo na async rutu
        </button>
      </div>
      <Router routes={routes}>
        <NoRoutesMatch />
      </Router>
    </>
  );
};

Api

createRoute

createRoute function takes three parameters:

  • routePatter: string
    • This is a string representation of the route. The route must start with /, and must not end with /
    • Dynamic route parts are indicated by: [variableName]
      • If this is an optional part of the route, you should put an ? at the end of the variable name. This does not mean that the variable will be called e.g. variableName. Instead the type of the variable will just be string | undefind
  • searchParamsValidator: ZodObject
    • This prop contains a Zod validator. The validator object should only be one dimensional (nesting is not supported).
    • If query params are not required, you should pass an empty zod object, e.g. z.object({})
  • renderComponent: (props: RouteParamsWithOptionalQueryParams<TRoute, ...> ) => Component
    • This is a callback function which should return a Preact component. The props contain dynamic parts of the route which will be of type string or string | undefined (if indicated that the route part is optional), and queryParams object which will contain the query params validated by the zod validator passed through searchParamsValidator.
{
        routePattern: "/players/[country]/[playername]",
        renderComponent: (({ country, playername, queryParams })) => (
            <TestComponent2
                country={country}
                player={playername}
                queryParams={queryParams}
            />
        ),
        searchParamsValidator: z.object({
            ime: z.string(),
            prezime: z.string().optional(),
        }),
    }

createAsyncRoute

createAsyncRoute function takes three parameters:

  • routePatter: string
    • This is a string representation of the route. The route must start with /, and must not end with /
    • Dynamic route parts are indicated by: [variableName]
      • If this is an optional part of the route, you should put an ? at the end of the variable name. This does not mean that the variable will be called, e.g. variableName. Instead, the type of the variable will just be string | undefind
  • searchParamsValidator: ZodObject
    • This prop contains a Zod validator. The validator object should only be one dimensional (nesting is not supported).
    • If query params are not required, you should pass an empty zod object, e.g. z.object({})
  • renderComponent: (props: RouteParamsWithOptionalQueryParams<TRoute, ...> ) => Promise<Component>
    • This is a callback function which imports a component async. The imported component should be in a separate file, and should be imported as displayed in the example below.
    • The props contain dynamic parts of the route which will be of type string or string | undefined (if indicated that the route part is optional), and queryParams object which will contain the query params vlidated by the zod validator passed through searchParamsValidator.
{
        routePattern: "/players/[country]/[playername]",
        renderComponent: (props) =>
          import("./async").then((module) => (
            <module.TestComponent2
                country={country}
                player={playername}
                queryParams={queryParams}
                route={props.route} />
        ),
        searchParamsValidator: z.object({
            ime: z.string(),
            prezime: z.string().optional(),
        }),
    }

Link

The Link is a very handy typesafe wrapper around classic HTML <a href="http://...">Link</a> tag. You use it as such:

<routes.plyersCountry.Link
  playername="Marko"
  country="Hrvatska"
  queryParams={{ ime: "Marko", prezime: "Jerkić", godine: 22 }}
>
  Na igrač marko ajde
</routes.plyersCountry.Link>
  • The routes is the variable containing created routes as shown in this section. This handy Link component is why you should not inline the routes creation, rather create them as a static variable and export them.
  • Dynamic route variables are referenced each individually (in the example above, those would be playername and country), and query parameters are bundled together, and they are always optional.
  • Anything passed as children to this component will be rendered as the contents of the underlying <a> tag.

routeTo

Similarly to the Link component, this a typesafe way to change the route.

routes.plyersCountry.routeTo({
  playername: "stipe",
  country: "hrv",
  queryParams: {
    godine: 22,
    ime: "Stipe",
    prezime: "Stipić",
  },
});
  • Unlike Link, this does not render anything, as this is meant to be used in a callback of some sorts.
  • The routes is the variable containing created routes as shown in this section. This handy Link component is why you should not inline the routes' creation, rather create them as a static variable and export them.
  • Dynamic route variables are referenced each individually (in the example above, those would be playername and country), and query parameters are bundled together, and they are always optional.

Router

The Router component is set where you want to bootstrap your components selected by the current route.

<Router routes={routes}>
  <NoRoutesMatch />
</Router>
  • routes
    • Object containing created routes. This could be inlined, but if you want to use the typesafe Link component or the typesafe routeTo function, you should save the route in a separate route, which you would ideally export.
  • The Router component also takes children components, which will be displayed if no matching route is found. It is esentially used as the 404 section.