@akb2/react-router 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ru.md ADDED
@@ -0,0 +1,268 @@
1
+ # @akb2/react-router
2
+
3
+ [English](README.md)
4
+
5
+ Объединение конфигураций маршрутов и инструменты React-навигации на основе Wouter.
6
+
7
+ ## Установка
8
+
9
+ ```sh
10
+ pnpm add @akb2/react-router react@^18.3.1 wouter@^3.13.0 @akb2/types-tools@^1.5.0
11
+ ```
12
+
13
+ React, Wouter и `@akb2/types-tools` обязательны как peer dependencies. В репозитории используется pnpm 12.5.1.
14
+
15
+ ## Публичный API
16
+
17
+ Точка входа пакета сейчас экспортирует:
18
+
19
+ | Экспорт | Назначение |
20
+ | ------------------------- | ----------------------------------------------------------------------- |
21
+ | `reactRouterFabric` | Объединение конфигураций маршрутов. |
22
+ | `routerContextFabric` | Создание провайдера роутера, хуков и функций построения URL. |
23
+ | `componentsContextFabric` | Создание провайдера компонентов и хука доступа к ним. |
24
+ | `ReactComponents` | Преобразование набора пропсов в типизированный набор React-компонентов. |
25
+ | `RouterComponents` | Обязательные компоненты роутера: `FullPageLoader` с пустыми пропсами. |
26
+ | `RoutesDefinition` | Тип определений отображения. |
27
+ | `RoutesKeys` | Ключи маршрутов в виде массивов или строк с точками. |
28
+ | `RoutesPathKeys` | Ключи маршрутов в виде строк с точками. |
29
+
30
+ Все три фабрики импортируются напрямую из `@akb2/react-router`.
31
+
32
+ ## Объединение конфигураций
33
+
34
+ ```tsx
35
+ import { reactRouterFabric } from "@akb2/react-router";
36
+
37
+ const baseRoutes = {
38
+ home: { path: "", Template: () => <h1>Home</h1> },
39
+ users: { path: "users", Template: () => <h1>Users</h1> },
40
+ notFound: { isNotFound: true, Template: () => <h1>Not found</h1> },
41
+ };
42
+
43
+ const userRoutes = {
44
+ detail: {
45
+ putInKey: "users",
46
+ path: ":id",
47
+ Template: () => <h1>User details</h1>,
48
+ },
49
+ };
50
+
51
+ const routes = reactRouterFabric(baseRoutes, userRoutes);
52
+ // routes.users.children.detail → /users/:id
53
+ ```
54
+
55
+ Результат — объект конфигурации. Без `putInKey` маршрут добавляется в корень. С `putInKey` маршрут помещается в `children` указанного корневого маршрута. Цель — корневой ключ, а не путь с точками. Повторяющиеся ключи и неразрешённые ссылки на родителей вызывают ошибки; для отсутствующего родителя сейчас используется сообщение о циклической зависимости.
56
+
57
+ ## Конфигурация маршрута
58
+
59
+ | Поле | Значение |
60
+ | ------------------ | ------------------------------------------------------------- |
61
+ | `path` | Один путь; `""` обозначает корень. |
62
+ | `paths` | Альтернативные пути одного маршрута. |
63
+ | `isNotFound` | Пометка запасного маршрута. |
64
+ | `children` | Вложенные маршруты, пути которых добавляются к родительскому. |
65
+ | `Template` | Компонент активной страницы. |
66
+ | `Module` | Ленивый React-компонент, отображаемый через Suspense. |
67
+ | `redirectUrl` | Адрес для `Redirect` из Wouter. |
68
+ | `props` | Пропсы активной страницы. |
69
+ | `SuspenseTemplate` | Компонент загрузки для конкретного маршрута. |
70
+ | `putInKey` | Корневой ключ родителя для объединения конфигураций. |
71
+
72
+ Динамические сегменты записываются как `:name`, например `users/:id`.
73
+
74
+ `routerContextFabric` проверяет конфигурацию при создании. Каждый маршрут должен содержать ровно одно из полей `path`, `paths`, `isNotFound` и ровно одно из полей `Template`, `Module`, `redirectUrl`. Массив альтернативных путей не должен быть пустым. В корне обязателен единственный запасной маршрут без дочерних элементов. Повторяющиеся сгенерированные URL запрещены.
75
+
76
+ ## Создание и отображение роутера
77
+
78
+ Используя `routes` из примера объединения:
79
+
80
+ ```tsx
81
+ import {
82
+ routerContextFabric,
83
+ componentsContextFabric,
84
+ type ReactComponents,
85
+ type RouterComponents,
86
+ } from "@akb2/react-router";
87
+
88
+ type AppComponents = ReactComponents<RouterComponents>;
89
+
90
+ const router = routerContextFabric(routes);
91
+ const { ComponentsProvider } = componentsContextFabric<AppComponents>();
92
+ const components: AppComponents = { FullPageLoader: () => <p>Loading…</p> };
93
+
94
+ export function App() {
95
+ return (
96
+ <ComponentsProvider components={components}>
97
+ <router.RouterProvider />
98
+ </ComponentsProvider>
99
+ );
100
+ }
101
+ ```
102
+
103
+ Создавайте фабрики вне компонентов. `ComponentsProvider` предоставляет `FullPageLoader`; он необходим для `RouterProvider`, даже когда текущая страница синхронная. Поле `SuspenseTemplate` маршрута заменяет этот загрузчик.
104
+
105
+ `RouterProvider` отображает активную страницу и предоставляет конфигурацию через `RouterContext`. Он не отображает произвольные дочерние элементы как обёртка приложения. Размещайте навигацию и макеты в компонентах маршрутов. Шаблоны родительских маршрутов автоматически не объединяются во вложенные макеты.
106
+
107
+ Адресом управляет Wouter. Для настройки окружения маршрутизации разместите его `Router` выше потребителей.
108
+
109
+ ### Общие компоненты
110
+
111
+ `componentsContextFabric()` возвращает `ComponentsProvider`, `ComponentsContext` и `useComponents`. Передайте провайдеру набор компонентов с обязательным `FullPageLoader`. Хук `useComponents()` читает этот набор внутри потомков провайдера; вызов вне провайдера вызывает ошибку.
112
+
113
+ `RouterComponents` описывает пропсы обязательных компонентов роутера. `ReactComponents<T>` преобразует объекты пропсов в типы `FC`, сохраняя уже заданные типы функций компонентов. Передайте результат как generic-аргумент фабрики.
114
+
115
+ Для дополнительных общих компонентов расширьте набор пропсов:
116
+
117
+ ```tsx
118
+ type ExtendedComponents = ReactComponents<
119
+ RouterComponents & {
120
+ PageTitle: { title: string };
121
+ }
122
+ >;
123
+
124
+ const shared = componentsContextFabric<ExtendedComponents>();
125
+ const extendedComponents: ExtendedComponents = {
126
+ FullPageLoader: () => <p>Загрузка…</p>,
127
+ PageTitle: ({ title }) => <h1>{title}</h1>,
128
+ };
129
+
130
+ function SharedPageTitle() {
131
+ const { PageTitle } = shared.useComponents();
132
+ return <PageTitle title="Пользователи" />;
133
+ }
134
+
135
+ function SharedComponentsExample() {
136
+ return (
137
+ <shared.ComponentsProvider components={extendedComponents}>
138
+ <SharedPageTitle />
139
+ </shared.ComponentsProvider>
140
+ );
141
+ }
142
+ ```
143
+
144
+ В приложении этот набор можно передать провайдеру, который оборачивает `RouterProvider`, и читать его через `shared.useComponents()` в компонентах страниц.
145
+
146
+ ## Построение URL
147
+
148
+ ```ts
149
+ router.createUrlFromRouteConfig("users.detail", { id: 42 });
150
+ // "/users/42"
151
+ router.createUrlFromRouteConfig(["users", "detail"], { id: 42 }, { tab: "profile" });
152
+ // "/users/42?tab=profile"
153
+ router.createUrlsFromRouteConfig("users.detail", { id: 42 });
154
+ // ["/users/42"]
155
+ ```
156
+
157
+ Обе функции принимают `(key, pathParams?, queryParams?, safePathParams?)`. Их можно вызывать вне React.
158
+
159
+ - `createUrlFromRouteConfig` использует первый альтернативный путь.
160
+ - `createUrlsFromRouteConfig` возвращает все комбинации альтернатив родителей и потомков.
161
+ - Ключи и значения query-параметров кодируются; значения `null` пропускаются.
162
+ - Значения path-параметров подставляются напрямую, без кодирования. Отсутствующие значения заменяются на `*`.
163
+ - При `safePathParams: true` сохраняются плейсхолдеры вроде `:id`.
164
+
165
+ ## Хуки навигации и сопоставления
166
+
167
+ Вызывайте хуки внутри компонентов или собственных хуков:
168
+
169
+ ```tsx
170
+ function UserNavigation() {
171
+ const [pathname, navigate] = router.useLocation<{ source: string }>();
172
+ const [matches, params] = router.useRoute("users.detail");
173
+ const active = router.useActivatedRoute();
174
+
175
+ return (
176
+ <button
177
+ onClick={() =>
178
+ navigate("users.detail", { id: 42 }, undefined, {
179
+ replace: true,
180
+ state: { source: "navigation" },
181
+ })
182
+ }
183
+ >
184
+ {pathname}: {matches ? params?.id : active.routeKeyString}
185
+ </button>
186
+ );
187
+ }
188
+ ```
189
+
190
+ | Хук | Результат |
191
+ | --------------------- | ---------------------------------------------------------------------------------------------- |
192
+ | `useLocation<S>()` | `[pathname, navigate]`; pathname не содержит query string. |
193
+ | `useRoute(key)` | Результат сопоставления Wouter с извлечёнными параметрами; проверяет первую альтернативу пути. |
194
+ | `useActivatedRoute()` | Метаданные активного маршрута, включая конфигурации родителей. |
195
+
196
+ `navigate(key, pathParams?, queryParams?, options?)` передаёт опции `replace`, `state`, `transition` в Wouter. Выбирается первая альтернатива пути со всеми переданными ключами path-параметров либо первая альтернатива, если подходящей нет.
197
+
198
+ `useActivatedRoute()` возвращает `activatedRoute`, `routeKey`, `routeKeyString`, `routeUrlMask`, `parents`, `pathParams`, `queryParams`. Параметры из URL являются строками. При выборе среди совпавших кандидатов статические сегменты приоритетнее параметризованных. При переходе к запасному маршруту метаданные ключей и родителей могут сохранять совпавшую часть дерева. Сейчас `routeUrlMask` содержит накопленный несовпавший путь и пуст после полного совпадения; полной маски найденного маршрута в нём нет.
199
+
200
+ ## Хуки состояния URL
201
+
202
+ Внутри компонента:
203
+
204
+ ```tsx
205
+ function UserIdControl() {
206
+ const [id, setId] = router.useRoutePathState<string>("id", "unknown");
207
+
208
+ return <button onClick={() => setId("43")}>Текущий пользователь: {id}</button>;
209
+ }
210
+
211
+ function TabControl() {
212
+ const [tab, setTab] = router.useRouteQueryState<string>("tab", "overview");
213
+
214
+ return (
215
+ <div>
216
+ <span>{tab}</span>
217
+ <button onClick={() => setTab("settings")}>Настройки</button>
218
+ <button onClick={() => setTab("overview")}>Сбросить вкладку</button>
219
+ </div>
220
+ );
221
+ }
222
+ ```
223
+
224
+ `useRoutePathState` читает параметр пути и возвращает fallback при его отсутствии. Сеттер влияет на URL только при наличии параметра в текущем пути маршрута.
225
+
226
+ `useRouteQueryState` читает query-параметр. Установка fallback или пустой строки удаляет параметр; установка текущего значения не вызывает навигацию.
227
+
228
+ Оба сеттера заменяют текущую запись истории и пытаются сохранить остальные параметры. Оба хука принимают `string | number`, но существующие значения URL остаются строками: числовой generic не преобразует их в числа.
229
+
230
+ ## Типы
231
+
232
+ ```ts
233
+ import type { RoutesKeys, RoutesPathKeys } from "@akb2/react-router";
234
+
235
+ type Key = RoutesKeys<typeof routes>;
236
+ type PathKey = RoutesPathKeys<typeof routes>;
237
+ type Router = typeof router;
238
+ type ActiveRoute = ReturnType<typeof router.useActivatedRoute>;
239
+ ```
240
+
241
+ Сохраняйте выведенный тип объекта маршрутов, чтобы не потерять конкретные ключи. `RoutesDefinition` описывает определения отображения, а не полную конфигурацию путей. Остальные внутренние типы не экспортируются из корня пакета.
242
+
243
+ ## Известные ограничения
244
+
245
+ Четыре теста с `it.fails` фиксируют две ошибки реализации:
246
+
247
+ - При навигации с динамическими path-параметрами и query-параметрами последний параметр пути может заменяться на `*`. Это также затрагивает хуки состояния.
248
+ - Короткая альтернатива пути может совпасть слишком рано, из-за чего определение активного маршрута возвращает запасной маршрут для корректной длинной альтернативы.
249
+
250
+ Контексты роутера глобально разделяются между вызовами фабрики, а функции замыкают конфигурацию своей фабрики. Избегайте вложенных провайдеров с несвязанными конфигурациями.
251
+
252
+ ## Разработка
253
+
254
+ ```sh
255
+ pnpm install
256
+ pnpm test
257
+ pnpm test:watch
258
+ pnpm exec tsc -p tsconfig.test.json --noEmit
259
+ pnpm build
260
+ ```
261
+
262
+ В `tests/router-context-fabric` каждая возможность проверяется в отдельном файле. Тесты ожидаемых ошибок выполняются; после исправления соответствующей ошибки нужно удалить пометку `it.fails`.
263
+
264
+ `pnpm build` создаёт ESM, CommonJS и декларации TypeScript в `.dist` из публичной точки входа.
265
+
266
+ ## Лицензия
267
+
268
+ [MPL-2.0](LICENSE)
package/package.json CHANGED
@@ -1,6 +1,83 @@
1
1
  {
2
2
  "name": "@akb2/react-router",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.1.0",
4
+ "description": "The context for react router",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./.dist/index.d.ts",
8
+ "import": "./.dist/index.js",
9
+ "require": "./.dist/index.cjs"
10
+ }
11
+ },
12
+ "files": [
13
+ ".dist"
14
+ ],
15
+ "keywords": [
16
+ "react",
17
+ "translate",
18
+ "translates",
19
+ "context"
20
+ ],
21
+ "author": "akb2",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/akb2/react-router.git"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/akb2/react-router/issues"
28
+ },
29
+ "license": "MPL-2.0",
30
+ "devEngines": {
31
+ "packageManager": {
32
+ "name": "pnpm",
33
+ "version": "12.5.1",
34
+ "onFail": "download"
35
+ }
36
+ },
37
+ "type": "module",
38
+ "devDependencies": {
39
+ "@testing-library/dom": "^10.4.2",
40
+ "@testing-library/react": "^16.3.3",
41
+ "@types/node": "^26.6.2",
42
+ "@types/react": "^18.3.31",
43
+ "@types/react-dom": "^18.3.7",
44
+ "eslint-config-prettier": "^10.1.8",
45
+ "jsdom": "^30.1.0",
46
+ "prettier": "^3.9.7",
47
+ "react-dom": "^18.3.1",
48
+ "tsup": "^8.5.1",
49
+ "typescript": "^5.9.3",
50
+ "vitest": "^5.0.1"
51
+ },
52
+ "dependencies": {
53
+ "@akb2/types-tools": "^1.5.0",
54
+ "react": "^18.3.1",
55
+ "wouter": "^3.13.0"
56
+ },
57
+ "peerDependencies": {
58
+ "@akb2/types-tools": "^1.3.0",
59
+ "react": "^18.3.1",
60
+ "wouter": "^3.13.0"
61
+ },
62
+ "peerDependenciesMeta": {
63
+ "@akb2/types-tools": {
64
+ "optional": false
65
+ },
66
+ "react": {
67
+ "optional": false
68
+ },
69
+ "wouter": {
70
+ "optional": false
71
+ }
72
+ },
73
+ "scripts": {
74
+ "build": "tsup src/index.ts --inject ./src/react.inject.ts --dts --format esm,cjs --clean --out-dir .dist --minify",
75
+ "release": "pnpm run build && pnpm publish --access public",
76
+ "test": "vitest run",
77
+ "test:watch": "vitest",
78
+ "lint": "eslint .",
79
+ "lint:fix": "eslint . --fix",
80
+ "format": "prettier . --write",
81
+ "format:check": "prettier . --check"
82
+ }
6
83
  }