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

@dldc/chemin

v11.0.4

Published

A type-safe pattern builder & route matching library written in TypeScript

Downloads

7

Readme

🥾 Chemin

A type-safe pattern builder & route matching library written in TypeScript

Gist

import { chemin, pNumber, pOptionalConst } from '@dldc/chemin';

// admin/post/:postId(number)/delete?
const path = chemin('admin', 'post', pNumber('postId'), pOptionalConst('delete'));

console.log(path.match('/no/valid'));
// => null

const match = path.match('/admin/post/45');
console.log(match);
// => { rest: [], exact: true, params: { postId: 45, delete: false } }
// match.params is typed as { postId: number, delete: boolean } !

Composition

You can use a Chemin inside another one to easily compose your routes !

import { chemin, pNumber, pString } from '@dldc/chemin';

const postFragment = chemin('post', pNumber('postId'));
const postAdmin = chemin('admin', pString('userId'), postFragment, 'edit');

console.log(postAdmin.stringify()); // /admin/:userId/post/:postId(number)/edit

Build-in params

The following params are build-in and exported from @dldc/chemin.

pNumber(name)

A number using parseFloat(x)

const chemin = chemin(pNumber('myNum'));
matchExact(chemin, '/3.1415'); // { myNum: 3.1415 }

NOTE: Because it uses parseFloat this will also accept Infinity, 10e2...

pInteger(name, options?)

A integer using parseInt(x, 10)

const chemin = chemin(pInteger('myInt'));
matchExact(chemin, '/42'); // { myInt: 42 }

The options parameter is optional and accepts a strict boolean property (true by default). When strict is set to true (the default) it will only match if the parsed number is the same as the raw value (so 1.0 or 42blabla will not match).

const chemin = chemin(pInteger('myInt', { strict: false }));
matchExact(chemin, '/42fooo'); // { myInt: 42 }

pString(name)

Any non-empty string

const chemin = chemin(pString('myStr'));
matchExact(chemin, '/cat'); // { myStr: 'cat' }

pConstant(name)

A constant string

const chemin = chemin(pConstant('edit'));
matchExact(chemin, '/edit'); // {}
matchExact(chemin, '/'); // false

pOptional(param)

Make any Param optional

const chemin = chemin(pOptional(pInteger('myInt')));
matchExact(chemin, '/42'); // { myInt: { present: true, value: 42 } }
matchExact(chemin, '/'); // { myInt: { present: false } }

pOptionalConst(name, path?)

An optional contant string

const chemin = chemin(pOptionalConst('isEditing', 'edit'));
matchExact(chemin, '/edit'); // { isEditing: true }
matchExact(chemin, '/'); // { isEditing: false }

If path is omitted then the name is used as the path.

const chemin = chemin(pOptionalConst('edit'));
matchExact(chemin, '/edit'); // { edit: true }
matchExact(chemin, '/'); // { edit: false }

pOptionalString(name)

An optional string parameter

const chemin = chemin(pOptionalString('name'));
matchExact(chemin, '/paul'); // { name: 'paul' }
matchExact(chemin, '/'); // { name: false }

pMultiple(param, atLeastOne?)

Allow a params to be repeated any number of time

const chemin = chemin(pMultiple(pString('categories')));
matchExact(chemin, '/'); // { categories: [] }
matchExact(chemin, '/foo/bar'); // { categories: ['foo', 'bar'] }
const chemin = chemin(pMultiple(pString('categories'), true));
matchExact(chemin, '/'); // false because atLeastOne is true
matchExact(chemin, '/foo/bar'); // { categories: ['foo', 'bar'] }

Custom Param

You can create your own Param to better fit your application while keeping full type-safety !

import { chemin, type TCheminParam } from '@dldc/chemin';

// match only string of 4 char [a-z0-9]
function pFourCharStringId<N extends string>(name: N): TCheminParam<N, string> {
  const reg = /^[a-z0-9]{4}$/;
  return {
    factory: pFourCharStringId,
    name,
    meta: null,
    isEqual: (other) => other.name === name,
    match: (...all) => {
      if (all[0].match(reg)) {
        return { match: true, value: all[0], next: all.slice(1) };
      }
      return { match: false, next: all };
    },
    serialize: (value) => value,
    stringify: () => `:${name}(id4)`,
  };
}

const path = chemin('item', pFourCharStringId('itemId'));
console.log(path.match('/item/a4e3t')); // null (5 char)
console.log(path.match('/item/A4e3')); // null (because A is uppercase)
console.log(path.match('/item/a4e3')); // { rest: [], exact: true, params: { itemId: 'a4e3' } }

Take a look a the custom-advanced.test.ts example. and the build-in Params.

API

chemin(...parts)

Create a Chemin

Accepts any number or arguments of type string, TCheminParam or IChemin.

Note: strings are converted to pConstant.

chemin('admin', pNumber('userId'), pOptionalConst('edit'));

The chemin function returns an object with the following properties:

  • parts: an array of the parts (other Chemins or Params), this is what was passed to the chemin function except that strings are converted to pConstant.
  • match(pathname): test a chemin against a pathname, see match for more details.
  • matchExact(pathname): test a chemin against a pathname for an exact match, see matchExact for more details.
  • stringify(params?, options?): serialize a chemin, see stringify for more details.
  • serialize(params?, options?): serialize a chemin, see serialize for more details.
  • extract(): return an array of all the Chemin it contains (as well as the Chemin itself), see extract for more details.
  • flatten(): return all the Param it contains, see flatten for more details.

Note: Most of these functions are also exported as standalone functions (see below). The only difference is that extract and flatten are cached when called on a Chemin itself, but you should rarely need to use them anyway.

isChemin(maybe)

Test wether an object is a Chemin or not

Accepts one argument and return true if it's a Chemin, false otherwise.

isChemin(chemin('admin')); // true

cheminFactory(defaultSerializeOptions)

The cheminFactory function returns a function that works exactly like chemin but with a default serialize / stringify options.

The defaultSerializeOptions parameter is optional and accepts two boolean properties:

  • leadingSlash (default true): Add a slash at the begining
  • trailingSlash (default: false): Add a slash at the end

match(chemin, pathname)

Test a chemin against a pathname

Returns null or ICheminMatch.

  • pathname can be either a string (/admin/user/5) or an array of strings (['admin', 'user', '5'])
  • ICheminMatch is an object with three properties
    • rest: an array of string of the remaining parts of the pathname once the matching is done
    • exact: a boolean indicating if the match is exact or not (if rest is empty or not)
    • params: an object of params extracted from the matching

Note: When pathname is a string, it is splitted using the splitPathname function. This function is exported so you can use it to split your pathnames in the same way.

import { chemin, pNumber, pOptionalConst, match } from '@dldc/chemin';

const chemin = chemin('admin', pNumber('userId'), pOptionalConst('edit'));
match(chemin, '/admin/42/edit'); // { rest: [], exact: true, params: { userId: 42, edit: true } }
match(chemin, '/admin/42/edit/rest'); // { rest: ['rest'], exact: false, params: { userId: 42, edit: true } }
match(chemin, '/noop'); // null

matchExact(chemin pathname)

Accepts the same arguments as match but return null if the path does not match or if rest is not empty, otherwise it returns the params object directly.

serialize(chemin, params?, options?)

Print a chemin from its params.

Accepts a chemin some params (an object or null) and an optional option object.

The option object accepts two boolean properties:

  • leadingSlash (default true): Add a slash at the begining
  • trailingSlash (default: false): Add a slash at the end
const chemin = chemin('admin', pNumber('userId'), pOptionalConst('edit'));
serialize(chemin, { userId: 42, edit: true }); // /admin/42/edit

splitPathname(pathname)

Split a pathname and prevent empty parts

Accepts a string and returns an array of strings.

splitPathname('/admin/user/5'); // ['admin', 'user', '5']

partialMatch(chemin, match, part)

This function let you extract the params of a chemin that is part of another one

const workspaceBase = chemin('workspace', pString('tenant'));

const routes = [
  chemin('home'), // home
  chemin('settings'), // settings
  chemin(workspaceBase, 'home'), // workspace home
  chemin(workspaceBase, 'settings'), // workspace settings
];

function app(pathname: string) {
  const route = matchFirst(routes, pathname);
  if (!route) {
    return { route: null };
  }
  const { chemin, match } = route;
  // extract the tenant from the workspace if it's a workspace route
  const params = partialMatch(chemin, match, workspaceBase);
  // params is typed as { tenant: string } | null
  if (params) {
    return { tenant: params.tenant, route: chemin.stringify() };
  }
  return { route: chemin.stringify() };
}

Note: This is based on reference equality so it will not work if you create a new Chemin with the same parts: chemin('workspace', pString('tenant')) !

Note 2: In reality this function simply returns the match.params object if the part is contained in chemin or null otherwise. This mean that you might get more properties that what the type gives you (but this is quite commoin in TypeScript).

matchAll(chemins, pathname)

Given an object of Chemin and a pathname return an new object with the result of match for each keys

const chemins = {
  home: chemin('home'),
  workspace: chemin('workspace', pString('tenant')),
  workspaceSettings: chemin('workspace', pString('tenant'), 'settings'),
};

const match = matchAll(chemins, '/workspace/123/settings');
expect(match).toEqual({
  home: null,
  workspace: { rest: ['settings'], exact: false, params: { tenant: '123' } },
  workspaceSettings: { rest: [], exact: true, params: { tenant: '123' } },
});

matchAllNested(chemins, pathname)

Same as matchAll but also match nested objects

extract(chemin)

Return an array of all the Chemin it contains (as well as the Chemin itself).

import { Chemin } from '@dldc/chemin';

const admin = chemin('admin');
const adminUser = chemin(admin, 'user');

adminUser.extract(); // [adminUser, admin];

Note: You probably don't need this but it's used internally in partialMatch

stringify(chemin, options)

Return a string representation of the chemin.

import { Chemin, pNumber, pString, stringify } from '@dldc/chemin';

const postFragment = chemin('post', pNumber('postId'));
const postAdmin = chemin('admin', pString('userId'), postFragment, 'edit');

console.log(stringify(postAdmin)); // /admin/:userId/post/:postId(number)/edit

The option object accepts two boolean properties:

  • leadingSlash (default true): Add a slash at the begining
  • trailingSlash (default: false): Add a slash at the end

matchFirst(chemins, pathname)

matchFirstExact(chemins, pathname)

namespace(base, chemins)

prefix(prefix, chemins)