@itcamel/routing-kit
v1.0.0-beta.1
Published
React Router v7 helpers: path resolution, layout mapping, optional RouterRoot factory, CTA helpers
Downloads
99
Maintainers
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.tsx → createRoutingBrowserRoot → getPath из того же paths → main.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 для дерева роутера.
- Сначала считается лист:
wrapContent(route, route.element)— здесь обычно оборачиваютelementвProtectedRoute/Outlet-обвязку и т.д. - Если у
routeестьlayout, возвращается узел: внешнийelement=layout, внутри один дочерний маршрут сindex: trueи полученным листом. - Если
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
- Войти в npm:
npm login(scoped-пакет@itcamel/routing-kitпубликуется сpublishConfig.access: "public"). - Проверить tarball:
npm pack --dry-run— в архиве должны бытьdist/,README.md,LICENSE,package.json. - Перед
npm publishвыполняетсяprepublishOnly(yarn lint+yarn typecheck), затемprepack(сборкаdist/). - Из корня репозитория:
npm publish --tag betaдля prerelease (например1.0.0-beta.1), чтобы тегlatestне указывал на beta. Стабильный релиз:npm publishбез тега (или с нужным тегом). Альтернатива при Yarn 4:yarn npm publish --tag beta. - Установка beta:
npm i @itcamel/routing-kit@beta.
Версию меняйте в package.json по semver до каждой публикации.
Лицензия
MIT — текст в файле LICENSE, поле license в package.json.
