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

headless-route

v2.5.0

Published

Generate file-based routes for a Multi-Page Application

Downloads

207

Readme

Headless Route

Generate routes for a Multi-Page Application (MPA) based on the file structure of a directory. It offers functions to create routes and navigation routes from the directory structure, allowing for easy navigation and dynamic routing.

Install

To use headless-route in your project, you can install it via npm or yarn:

npm i -D headless-route
# or
yarn add -D headless-route

Usage

Say we have the following directory structure. Refer to the example directory for further details:

./
├── pages/
│   ├── 404.md                # => /404.html
│   ├── about.md              # => /about.html
│   ├── blogs/
│   │   ├── api.js            # => the blogs api
│   │   └── :slug.md          # => /blogs/:slug
│   ├── contact.md            # => /contact.html
│   ├── _hidden/
│   │   └── hidden-page.md
│   └── index.md              # => /
├── ...
├── package-lock.json
└── package.json

▸ Create routes based on a directory structure:

import { createRoutes } from 'headless-route'
// Or for CommonJS:
// const { createRoutes } = require('headless-route')

const routes = await createRoutes({
  dir: 'pages',
  extensions: ['.html', '.md'],
  urlSuffix: '.html',
  filter(file) {
    // ignore files starting with '_'
    return !file.name.startsWith('_')
  },
  async handler(route) {
    if (route.isDynamic) {
      const dirname = route.id.split('/').slice(0, -1).join('/')
      const apifile = `./${dirname}/api.js`
      const { fetchApi } = await import(apifile)

      Object.assign(route, { context: await fetchApi() })
    }
  }
})
// for sync api:
// const routes = createRoutesSync({...})

console.log(routes)
;[
  {
    id: 'pages/404.md',
    stem: '404',
    url: '/404.html',
    index: false,
    isDynamic: false
  },
  {
    id: 'pages/about.md',
    stem: 'about',
    url: '/about.html',
    index: false,
    isDynamic: false
  },
  {
    id: 'pages/blogs/$slug.md',
    stem: 'blogs/:slug',
    url: '/blogs/:slug.html',
    index: false,
    isDynamic: true,
    context: { foo: [Object], bar: [Object] }
  },
  {
    id: 'pages/contact.md',
    stem: 'contact',
    url: '/contact.html',
    index: false,
    isDynamic: false
  },
  {
    id: 'pages/foo/bar/baz/index.md',
    stem: 'foo/bar/baz/index',
    url: '/foo/bar/baz/index.html',
    index: true,
    isDynamic: false
  },
  {
    id: 'pages/foo/bar/index.md',
    stem: 'foo/bar/index',
    url: '/foo/bar/index.html',
    index: true,
    isDynamic: false
  },
  {
    id: 'pages/foo/index.md',
    stem: 'foo/index',
    url: '/foo/index.html',
    index: true,
    isDynamic: false
  },
  {
    id: 'pages/index.md',
    stem: 'index',
    url: '/index.html',
    index: true,
    isDynamic: false
  }
]

▸ Create navigation routes from routes:

import { createNavigation } from 'headless-route'
// Or for CommonJS:
// const { createNavigation } = require('headless-route')

const navigationRoutes = await createNavigation(routes)
// for sync api:
// const navigationRoutes = createNavigationSync(routes)

console.log(navigationRoutes)
;[
  {
    stem: '404',
    url: '/404.html',
    index: false,
    isDynamic: false
  },
  {
    stem: 'about',
    url: '/about.html',
    index: false,
    isDynamic: false
  },
  {
    stem: 'blogs',
    url: '/blogs',
    index: true,
    isDynamic: false,
    children: [
      {
        stem: 'blogs/:slug',
        url: '/blogs/:slug.html',
        index: false,
        isDynamic: true,
        context: {
          foo: [Object],
          bar: [Object]
        }
      }
    ]
  },
  {
    stem: 'contact',
    url: '/contact.html',
    index: false,
    isDynamic: false
  },
  {
    stem: 'foo',
    url: '/foo',
    index: true,
    isDynamic: false,
    children: [
      {
        stem: 'foo/bar',
        url: '/foo/bar',
        index: true,
        isDynamic: false,
        children: [
          {
            stem: 'foo/bar/baz',
            url: '/foo/bar/baz',
            index: true,
            isDynamic: false,
            children: [
              {
                stem: 'foo/bar/baz/index',
                url: '/foo/bar/baz/index.html',
                index: true,
                isDynamic: false
              }
            ]
          },
          {
            stem: 'foo/bar/index',
            url: '/foo/bar/index.html',
            index: true,
            isDynamic: false
          }
        ]
      },
      {
        stem: 'foo/index',
        url: '/foo/index.html',
        index: true,
        isDynamic: false
      }
    ]
  },
  {
    stem: 'index',
    url: '/index.html',
    index: true,
    isDynamic: false
  }
]

[!NOTE] In navigation routes, a file named index serves as a Layout routes. It participates in UI nesting, but it does not add any segments to the URL.

▸ Finds a route that matches the provided request URL:

import { findRoute } from 'headless-route'

const requestUrl = '/blogs/foo.html'
const route = findRoute(requestUrl, routes)

if (route?.isDynamic) {
  // match params
  const params = route.matchParams(requestUrl)
  // yields: { slug: foo }

  // generate url path
  const urlpath = route.generatePath({ slug: 'bar' })
  // yields: /blogs/bar.html
}

Best practices

When structuring your project, adhere to the following best practices:

▸ Files or directories starting with an underscore character (_) should be ignored:

const routes = await createRoutes({
  filter(file) {
    // ignore files starting with '_'
    return !file.name.startsWith('_')
  }
})

▸ File or directory names starting with a dollar character ($) or colon (:), or conclude with a question mark (?), or are enclosed within square brackets ([]), will be treated as “dynamic segments”.

[!CAUTION] Please note that the colon (:) and question mark (?) characters are invalid for file names on Windows.

▸ Dynamic segments should adhere to the following formatting guidelines:

  • 🚫 Avoid: /users-:id.md (partial paths should be avoided)
  • ✅ Prefer: /users/:id.md or /users/$id.md
  • ✅ Acceptable: /users/:id?.md or /users/[id].md (for optional segments)
  • 🚫 Avoid: /posts/:categories--:id.md (partial paths should be avoided)
  • ✅ Prefer: /posts/:categories/:id.md or /posts/$categories/$id.md
  • ✅ Acceptable: /posts/[lang]/categories.md (for optional segments)
  • ✅ Acceptable: /files/*.md (for splat segments)
  • ✅ Acceptable: /foo/:bar*.md (for named splat segments)
  • ✅ Acceptable: /foo/:bar+.md (for required splat segments)

▸ Follow a consistent pattern in CRUD operations. Instead of naming files like foo/$id.edit.tsx, use foo/$id/edit.tsx:

🚫 Avoid:

  • pages/users/$id.create.tsx
  • pages/users/$id.edit.tsx
  • pages/users/$id.delete.tsx
  • pages/users/$id.view.tsx

✅ Prefer:

  • pages/users/$id/create.tsx
  • pages/users/$id/edit.tsx
  • pages/users/$id/delete.tsx
  • pages/users/$id/view.tsx
  • pages/users/api.ts
  • pages/users/index.tsx

API

createRoutes(options: Options): Promise<Route[]>

Creates routes based on the specified options:

  • dir: The directory to scan for routes. Defaults to the current working directory (process.cwd()).

  • extensions: The file extensions to include when scanning for routes. Defaults (['.html', '.md']).

  • urlPrefix: Defines the prefix to prepend to route URLs. Defaults '/'.

    Acceptable values include:

    • Absolute URL pathname, e.g., /foo/
    • Full URL, e.g., https://foo.com/
    • Empty string or ./
  • urlSuffix: Defines the suffix to append to route URLs. Defaults ''.

  • cache: Indicates whether to cache routes. Defaults to false.

  • filter: A filter function for filtering Dirent objects. It automatically disregards files and directories listed in the project's .gitignore file, ensuring they are consistently excluded from consideration.

    const routes = await createRoutes({
      filter(file) {
        // ignore files starting with '_' or ending with '.data.js'
        return !file.name.startsWith('_') && !file.name.endsWith('.data.js')
      }
    })
  • handler: A handler function called for each route.

    await createRoutes({
      dir: 'pages',
      async handler(route) {
        if (route.id.endsWith('.js')) {
          // attach a lazy route for JavaScript files
          route.lazy = import(route.id)
        }
      }
    })

createRoutesSync(options: OptionsSync): Route[]

Creates routes based on the specified options synchronously.

createNavigation(routes: Route[], handler?: NavigationHandlerFn): Promise<NavigationRoute[]>

Creates navigation routes based on the specified routes. A navigation route object has the same structure as a route object, excluding the id property. It may also contain children property, representing the children routes of the navigation route.

  • routes: An array of routes.
  • handler?: A navigation route handler function.
const navigationRoutes = await createNavigation(routes, route => {
  const segments = route.stem.split('/')
  const lastSegment = String(segments.pop())

  // assign 'text' prop for each route and layout routes
  Object.assign(route, {
    text: lastSegment[0].toUpperCase() + lastSegment.slice(1).toLowerCase()
  })
})

createNavigationSync(routes: Route[], handler?: NavigationHandlerFnSync): NavigationRoute[]

Creates navigation routes based on the specified routes synchronously.

createRoute(id: string, options: { root: string, urlPrefix: string }): Route

A utility to create a route object based on the provided ID and options.

import { createRoute } from 'headless-route'

const route = createRoute('pages/users/:id.md', {
  root: 'pages',
  urlSuffix: '.html'
})

// Yields:
// { id: 'pages/users/$id.md', stem: 'users/:id', url: '/users/:id.html', index: false, isDynamic: true }

findRoute(requestUrl: string, routes: Route[]): Route | undefined

A utility to Find a route that matches the provided request URL.

import { findRoute } from 'headless-route'

const matchedRoute = findRoute('/contact.html', routes)

// Yields:
// { id: 'pages/contact.md', stem: 'contact', url: '/contact.html', index: false, isDynamic: false }

routeSegments(id: string, root?: string): string[]

A utility to extract segments from a route id relative to a root directory.

import { routeSegments } from 'headless-route'

const segments = routeSegments('foo/bar/baz.html', 'foo')

// Yields:
// ['bar', 'baz']

Types

Route

Represents a single route in the MPA.

/**
 * Represents a route, which can be either a base route or a dynamic route.
 */
export type Route = BaseRoute | DynamicRoute

/**
 * Represents the base structure of a route.
 */
export interface BaseRoute {
  /**
   * The unique identifier for the route.
   */
  id: string

  /**
   * The stem of the route URL.
   */
  stem: string

  /**
   * The URL of the route.
   */
  url: string

  /**
   * Indicates whether the route is an index page.
   */
  index: boolean

  /**
   * Indicates whether the route is dynamic.
   */
  isDynamic: false
}

/**
 * Represents a dynamic route, which can match and generate URLs dynamically.
 */
export interface DynamicRoute extends Omit<BaseRoute, 'isDynamic'> {
  /**
   * Indicates whether the route is dynamic.
   */
  isDynamic: true

  /**
   * Function to check if the given input matches the route.
   *
   * @param input The input to match against the route.
   * @returns A boolean indicating whether the input matches the route.
   */
  isMatch: (input: string) => boolean

  /**
   * Function to extract parameters from the given input if it matches the route.
   *
   * @template Params The type of parameters extracted from the input.
   * @param input The input to extract parameters from.
   * @returns The extracted parameters if the input matches the route, otherwise false.
   */
  matchParams: <Params extends object = object>(input: string) => false | Params

  /**
   * Function to generate a URL using the provided parameters.
   *
   * @template Params The type of parameters used to generate the URL.
   * @param params The parameters used to generate the URL.
   * @returns The generated URL.
   */
  generatePath: <Params extends object = object>(params: Params) => string
}

NavigationRoute

Represents a navigation route with additional data. It inherits all properties from Route except for id.

/**
 * Represents a navigation route, which extends the base route structure and
 * can have children routes.
 */
export interface NavigationRoute extends Omit<Route, 'id'> {
  /**
   * Children routes of the navigation route.
   */
  children?: NavigationRoute[]
}

Related

Contributing

We 💛  issues.

When committing, please conform to the semantic-release commit standards. Please install commitizen and the adapter globally, if you have not already.

npm i -g commitizen cz-conventional-changelog

Now you can use git cz or just cz instead of git commit when committing. You can also use git-cz, which is an alias for cz.

git add . && git cz

License

GitHub

A project by Stilearning © 2024.