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

@itcamel/routing-kit

v1.0.0-beta.1

Published

React Router v7 helpers: path resolution, layout mapping, optional RouterRoot factory, CTA helpers

Downloads

99

Readme

@itcamel/routing-kit

Небольшая библиотека поверх React Router v7 (react-router): типизированная таблица маршрутов, построение URL по id и параметрам, обёртка layout + произвольная обёртка контента (например auth), опционально createRoutingBrowserRoot (готовый RouterProvider + createBrowserRouter), вспомогательные функции для CTA с якорем #hash.

Пакет не подключает страницы, layout-компоненты и guard’ы — они остаются в приложении. Сюда вынесены только повторяющиеся паттерны.


Требования

| Зависимость | Назначение | | -------------- | ------------------------------------ | | react | peer, ^18 или ^19 | | react-router | peer, ^7 (как в документации RR v7) | | Node | >= 20 (см. engines в package.json) |

Установка:

yarn add @itcamel/routing-kit react react-router

Что остаётся в приложении, а что даёт пакет

В приложении вы по-прежнему держите:

  • string enum идентификаторов маршрутов (например EPathID) — им же задаётся id в paths и первый аргумент getPath для ссылок и навигации (см. ниже). Технически подойдёт и union строковых литералов, но ориентир — enum как единый источник правды;
  • массив paths с element, path, при необходимости layout;
  • компоненты ProtectedRoute, AdminRoute, корневой AppLayout, страницу 404 (если не заведена в paths с path: '*').

Точка входа роутинга: либо один вызов createRoutingBrowserRoot на уровне модуля (пакет внутри создаёт createBrowserRouter и компонент RouterRoot с RouterProvider — в main.tsx рендерите только <RouterRoot />), либо вручную собираете дерево через createBrowserRouter + RouterProvider, если нужна нестандартная структура.

Пакет даёт тип AppRouteObject, mapWithLayout / mapAppRoutes, createProtectedWrap, pathDefinitionsFromRoutes, createGetPathFromRoutes (один вызов вместо pathDefinitionsFromRoutes + createGetPath), createRoutingBrowserRoot (при linkDefaultId сразу отдаёт и getPath), createGetPath, createCtaHelpers.


Рекомендуемая модель: один paths.tsx

Не нужны отдельные adminPaths, второй массив «для ссылок» и вложенный родитель path: 'admin' в createBrowserRouter, если вас устраивает плоский список маршрутов с полными путями:

  • публичные страницы: path: '/catalog', path: '/terms';
  • админка: path: '/admin', path: '/admin/orders', path: '/admin/users/:id' — те же поля element, при необходимости layout: <AdminLayout />, isProtected: true;
  • один экспорт paths: AppRouteObject<EPathID>[] и один вызов mapAppRoutes(paths, wrapContent) в AppRouter;
  • функцию getPath: либо createGetPathFromRoutes(paths, EPathID.HOME), либо вместе с роутером — createRoutingBrowserRoot({ …, linkDefaultId: EPathID.HOME }) и деструктуризация getPath; дальше везде строка URL: <Link to={getPath(EPathID.CATALOG)} />, navigate(...), параметры сегментов вторым аргументом.

Стандартный флоу (один сквозной пример)

Ниже — типичное приложение: enum маршрутоводин paths.tsxcreateRoutingBrowserRootgetPath из того же pathsmain.tsx только с <RouterRoot /> → ссылка через enum.

1. src/app/router/model/EPathID.enum.ts

export enum EPathID {
	HOME = 'home',
	CATALOG = 'catalog',
	PROFILE = 'profile',
}

2. src/app/router/model/paths.tsx — страницы и layout остаются вашими; здесь только каркас.

Поле layout задаётся только там, где нужна обёртка (шапка/сайдбар/Outlet); у лендинга без общей оболочки его можно не указывать — тогда element вешается на маршрут напрямую (см. mapWithLayout в API).

import type { AppRouteObject } from '@itcamel/routing-kit';

// import { HomePage, CatalogPage, ProfilePage } from '@/pages/...';
// import { PublicLayout, ShopLayout } from '@/app/layouts/...';

import { EPathID } from './EPathID.enum';

export const paths: AppRouteObject<EPathID>[] = [
	{
		id: EPathID.HOME,
		path: '/',
		isProtected: false,
		// без `layout` — страница сразу на маршруте
		element: <>{/* <HomePage /> */}</>,
	},
	{
		id: EPathID.CATALOG,
		path: '/catalog',
		isProtected: false,
		layout: <>{/* <ShopLayout /> */}</>,
		element: <>{/* <CatalogPage /> */}</>,
	},
	{
		id: EPathID.PROFILE,
		path: '/profile',
		isProtected: true,
		layout: <>{/* тот же <ShopLayout /> или отдельный layout кабинета */}</>,
		element: <>{/* <ProfilePage /> */}</>,
	},
];

3. src/app/router.tsx — роутер и строки путей для ссылок из одного источника paths.

import {
	createProtectedWrap,
	createRoutingBrowserRoot,
} from '@itcamel/routing-kit';

import AppLayout from '@/app/layouts/AppLayout';
import ProtectedRoute from '@/app/router/ProtectedRoute';
import NotFound from '@/pages/404/NotFound';

import { EPathID } from './router/model/EPathID.enum';
import { paths } from './router/model/paths';

const wrapContent = createProtectedWrap<EPathID>(ProtectedRoute);

export const { RouterRoot, router, getPath } = createRoutingBrowserRoot({
	paths,
	wrapContent,
	rootLayout: <AppLayout />,
	notFoundElement: <NotFound />,
	linkDefaultId: EPathID.HOME,
});

Если 404 уже описан в paths с path: '*', не передавайте notFoundElement — catch-all не дублируется.

4. src/app/main.tsx

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';

import { RouterRoot } from '@/app/router';

createRoot(document.getElementById('root')!).render(
	<StrictMode>
		<RouterRoot />
	</StrictMode>,
);

5. Любой компонент — навигация только через getPath и enum.

import { Link } from 'react-router';

import { getPath } from '@/app/router';
import { EPathID } from '@/app/router/model/EPathID.enum';

export function TopNav() {
	return <Link to={getPath(EPathID.CATALOG)}>Каталог</Link>;
}

CTA с якорем (по желанию): после getPath добавьте createCtaHelpers(rows, getPath, { ctaId, pathId }) — строки rows ссылаются на те же значения EPathID, что и в paths.


API

AppRouteObject<PathId>

Расширение RouteObject из react-router:

  • id: PathId — как правило ваш string enum (EPathID); тот же тип использует возвращаемая createGetPath функция getPath(id?, params?) при подстановке в ссылки и вызовы навигации;
  • layout?: ReactNode — если задан, маршрут превращается в родителя с element: layout и одним ребёнком { index: true, element: ... };
  • isProtected: boolean — для вашей логики и для типового createProtectedWrap(Guard) (обёртка только при true); свой wrapContent может игнорировать поле;
  • navLabel?: string, legalKey?: string — опциональные метаданные для меню / футера.

Остальные поля (path, index, element, children, …) — как в обычном RouteObject.


mapWithLayout(route, wrapContent)

Превращает AppRouteObject в RouteObject для дерева роутера.

  1. Сначала считается лист: wrapContent(route, route.element) — здесь обычно оборачивают element в ProtectedRoute / Outlet-обвязку и т.д.
  2. Если у route есть layout, возвращается узел: внешний element = layout, внутри один дочерний маршрут с index: true и полученным листом.
  3. Если layout нет, возвращается { ...route, element } с уже обёрнутым element.

Так повторяется типичный паттерн «публичный layout вокруг страницы» без дублирования JSX в каждом проекте.


mapAppRoutes(routes, wrapContent)

Эквивалент routes.map((r) => mapWithLayout(r, wrapContent)) — удобно, когда весь UI описан одним массивом paths.


createProtectedWrap(Guard)

Возвращает готовый wrapContent для mapAppRoutes и createRoutingBrowserRoot: если у маршрута isProtected: true, рендерит content внутри переданного Guard (обычно ваш ProtectedRoute с children); иначе возвращает content без обёртки.

Тип выводится как WrapRouteContent<PathId> — без ручной аннотации import('…').WrapRouteContent<…>.

Нестандартная логика (несколько ролей, разные guard’ы по route.id) — по-прежнему через свой callback wrapContent и mapWithLayout.


pathDefinitionsFromRoutes(routes, options?)

Строит массив PathDefinition для createGetPath из того же paths, что уходит в роутер: не дублируете таблицу путей.

По умолчанию из выборки убираются записи без строкового path, с пустым path и с path: '*'. Дополнительный предикат options.exclude может отфильтровать, например, служебные маршруты, по которым не строите ссылки.


createGetPathFromRoutes(routes, defaultId, pathOptions?)

Композиция pathDefinitionsFromRoutes + createGetPath: один вызов, тот же getPath(id?, params?), если роутер собираете сами и не используете linkDefaultId в createRoutingBrowserRoot.

Третий аргумент — те же опции, что у pathDefinitionsFromRoutes (например exclude).


createRoutingBrowserRoot(options)

Собирает типичное приложение: родитель с path: '/' и element: rootLayout, в children — результат mapAppRoutes(paths, wrapContent) и при необходимости catch-all path: '*'.

| Поле | Назначение | | ------------------------- | ----------------------------------------------------------------------------------------- | | paths | Тот же массив, что в paths.tsx | | wrapContent | Как для mapAppRoutes | | rootLayout | Обычно <AppLayout /><Outlet />) | | notFoundElement? | Подставляется как *, если в paths ещё нет маршрута с path: '*' | | basename? | Второй аргумент createBrowserRouter | | linkDefaultId? | Если задан — в результате есть getPath по тем же paths (тип знает, что поле есть) | | pathDefinitionsOptions? | Уходит в pathDefinitionsFromRoutes при сборке getPath (например exclude) |

Возвращает RouterRoot, router, а при переданном linkDefaultId — ещё getPath для <Link to={getPath(...)} /> без отдельного вызова в приложении.

Вызывайте фабрику на уровне модуля, а не внутри React-компонента, чтобы не пересоздавать роутер при каждом рендере.


createGetPath(paths, defaultId)

Низкоуровневая связка: на вход уже готовый массив { id, path }. В приложении чаще удобнее createGetPathFromRoutes или createRoutingBrowserRoot с linkDefaultId.

Возвращает функцию getPath(id?, params?) — строка пути по enum для <Link to={…} />, href, navigate и т.д.

  • paths — массив { id, path } (часто из pathDefinitionsFromRoutes(paths)), где path — строка шаблона React Router (например /talent/:uuid?, /admin/users/:userUuid).
  • defaultId — значение того же enum, что и id в таблице (например EPathID.HOME), если вызываете getPath() без первого аргумента.

Подстановка параметров (params: Record<string, string | undefined>):

  • для каждого ключа подставляется значение в сегменты :key и :key?;
  • пустые и undefined пропускаются;
  • значения кодируются через encodeURIComponent;
  • незаполненные опциональные сегменты вида /:something? удаляются из строки;
  • схлопываются лишние /, убирается хвостовой / (кроме корня /).

Если для id записи в таблице нет, используется путь '/'.


createCtaHelpers(rows, getPath, defaults)

rows — массив { id: CtaId, pathId: PathId, hash: string }.
getPath — из createGetPathFromRoutes, из createRoutingBrowserRoot (поле getPath при linkDefaultId) или из пары createGetPath + pathDefinitionsFromRoutes.
defaults{ ctaId, pathId } для подстановки, когда в таблице нет строки.

Возвращает объект:

| Метод | Описание | | ------------------------------ | ------------------------------------------ | | getCTAPathId(id?) | pathId для CTA или defaults.pathId | | getCTAHash(id?) | строка hash (может быть пустой) | | getCTAFullPath(id?, params?) | `${getPath(pathId, params)}#${hash}` |

params пробрасываются в getPath, если целевому маршруту нужны динамические сегменты.


Структура исходников (src/)

| Папка / файлы | Назначение | | --------------------- | ----------------------------------------------------------------- | | types/ | Публичные типы (AppRouteObject, PathDefinition, опции и т.д.) | | utils/ | Внутренние чистые функции (шаблон пути, признак catch-all *) | | *.ts в корне src/ | Публичные фабрики и index.ts (barrel для npm) |

Потребители пакета по-прежнему импортируют только из @itcamel/routing-kit, внутренние пути не часть контракта.


Разработка пакета

Из корня репозитория:

| Команда | Действие | | ---------------- | ------------------------------- | | yarn install | зависимости | | yarn build | сборка dist/ (tsup + .d.ts) | | yarn typecheck | tsc --noEmit | | yarn format | Prettier write | | yarn lint | Prettier check |

Перед публикацией yarn pack / публикация в registry запускают prepack → сборка.


Публикация в npm

  1. Войти в npm: npm login (scoped-пакет @itcamel/routing-kit публикуется с publishConfig.access: "public").
  2. Проверить tarball: npm pack --dry-run — в архиве должны быть dist/, README.md, LICENSE, package.json.
  3. Перед npm publish выполняется prepublishOnly (yarn lint + yarn typecheck), затем prepack (сборка dist/).
  4. Из корня репозитория: npm publish --tag beta для prerelease (например 1.0.0-beta.1), чтобы тег latest не указывал на beta. Стабильный релиз: npm publish без тега (или с нужным тегом). Альтернатива при Yarn 4: yarn npm publish --tag beta.
  5. Установка beta: npm i @itcamel/routing-kit@beta.

Версию меняйте в package.json по semver до каждой публикации.


Лицензия

MIT — текст в файле LICENSE, поле license в package.json.