@ovineko/react-router 0.0.1

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.md ADDED
@@ -0,0 +1,287 @@
1
+ # @ovineko/react-router
2
+
3
+ Type-safe wrapper for React Router v7 with valibot schema validation, automatic error handling, and typed params.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @ovineko/react-router valibot
9
+ ```
10
+
11
+ Peer dependencies: `react@^19`, `react-router@^7`, `valibot@^1`.
12
+
13
+ ## Features
14
+
15
+ - ✅ **Valibot schema validation** - Runtime validation for URL params and search params
16
+ - ✅ **Two hook variants** - Normal hooks (auto-redirect on error) and Raw hooks (manual error handling)
17
+ - ✅ **Smart validation** - Ignores extra query params and invalid optional params
18
+ - ✅ **Global error handling** - Configure fallback redirect URLs globally or per-route
19
+ - ✅ **Type-safe** - Full TypeScript support with inferred types
20
+ - ✅ **Hash support** - Generate paths with hash fragments
21
+
22
+ ## Quick Start
23
+
24
+ ```tsx
25
+ import { createRouteWithParams } from "@ovineko/react-router";
26
+ import * as v from "valibot";
27
+
28
+ const userRoute = createRouteWithParams("/users/:id", {
29
+ params: v.object({ id: v.pipe(v.string(), v.uuid()) }),
30
+ searchParams: v.object({
31
+ tab: v.optional(v.string()),
32
+ page: v.optional(v.pipe(v.string(), v.transform(Number), v.number())),
33
+ }),
34
+ errorRedirect: "/404", // Optional: redirect on validation error
35
+ });
36
+
37
+ // Normal hooks - auto-redirect on validation error
38
+ function UserPage() {
39
+ const params = userRoute.useParams(); // Readonly<{ id: string }>
40
+ const [searchParams, setSearchParams] = userRoute.useSearchParams();
41
+
42
+ return (
43
+ <div>
44
+ User {params.id}, Page {searchParams.page ?? 1}
45
+ </div>
46
+ );
47
+ }
48
+
49
+ // Raw hooks - manual error handling
50
+ function AdvancedUserPage() {
51
+ const [params, error] = userRoute.useParamsRaw();
52
+ const { data, error: searchError } = userRoute.useSearchParamsRaw();
53
+
54
+ if (error) return <div>Invalid user ID</div>;
55
+ if (searchError) console.warn("Invalid search params", searchError);
56
+
57
+ return <div>User {params?.id}</div>;
58
+ }
59
+ ```
60
+
61
+ ## Global Error Redirect
62
+
63
+ Configure a global fallback URL for validation errors:
64
+
65
+ ```tsx
66
+ import { setGlobalErrorRedirect } from "@ovineko/react-router";
67
+
68
+ // In your app entry point
69
+ setGlobalErrorRedirect("/error");
70
+
71
+ // Priority: route-level > global > default "/"
72
+ const route = createRouteWithParams("/users/:id", {
73
+ params: v.object({ id: v.string() }),
74
+ errorRedirect: "/404", // Overrides global
75
+ });
76
+ ```
77
+
78
+ ## Validation Behavior
79
+
80
+ ### Path Params
81
+
82
+ - Always strict validation
83
+ - Redirect on any validation error
84
+
85
+ ### Search Params
86
+
87
+ - **Extra params** (not in schema) → ignored
88
+ - **Invalid optional params** → ignored (treated as undefined)
89
+ - **Invalid required params** → error (triggers redirect)
90
+
91
+ ```tsx
92
+ const route = createRouteWithParams("/search", {
93
+ params: v.object({}),
94
+ searchParams: v.object({
95
+ q: v.string(), // required
96
+ page: v.optional(v.pipe(v.string(), v.transform(Number), v.number())),
97
+ }),
98
+ });
99
+
100
+ // URL: /search?q=react&page=invalid&debug=true
101
+ // Result: { q: "react", page: undefined }
102
+ // - page=invalid ignored (optional + invalid)
103
+ // - debug=true ignored (not in schema)
104
+ ```
105
+
106
+ ## API Reference
107
+
108
+ ### Route Creation
109
+
110
+ #### `createRouteWithParams`
111
+
112
+ ```tsx
113
+ const route = createRouteWithParams<TParams, TSearchParams>(pattern, config);
114
+ ```
115
+
116
+ **Config:**
117
+
118
+ - `params` - Valibot schema for URL params (required)
119
+ - `searchParams?` - Valibot schema for search params (optional)
120
+ - `errorRedirect?` - Redirect URL on validation error (optional)
121
+
122
+ **Returns:**
123
+
124
+ - `path(params, searchParams?, hash?)` - Generate URL path
125
+ - `parseURLParams(url)` - Parse and validate URL params
126
+ - `Link` - Type-safe Link component
127
+ - `useParams()` - Get validated params (auto-redirect on error)
128
+ - `useParamsRaw()` - Get params with error info `[data, error]`
129
+ - `useSearchParams()` - Get validated search params (auto-redirect on error)
130
+ - `useSearchParamsRaw()` - Get search params with error info `{data, error, setter}`
131
+ - `pattern` - Original route pattern
132
+
133
+ #### `createRouteWithoutParams`
134
+
135
+ ```tsx
136
+ const route = createRouteWithoutParams<TSearchParams>(pattern, config?);
137
+ ```
138
+
139
+ **Config:**
140
+
141
+ - `searchParams?` - Valibot schema for search params
142
+ - `errorRedirect?` - Redirect URL on validation error
143
+
144
+ **Returns:**
145
+
146
+ - `path(searchParams?, hash?)` - Generate URL path
147
+ - `Link` - Type-safe Link component
148
+ - `useSearchParams()` - Get validated search params
149
+ - `useSearchParamsRaw()` - Get search params with error info
150
+ - `pattern` - Original route pattern
151
+
152
+ ### Examples
153
+
154
+ #### Path Generation
155
+
156
+ ```tsx
157
+ const userRoute = createRouteWithParams("/users/:id", {
158
+ params: v.object({ id: v.string() }),
159
+ searchParams: v.object({
160
+ tab: v.optional(v.string()),
161
+ }),
162
+ });
163
+
164
+ userRoute.path({ id: "42" });
165
+ // "/users/42"
166
+
167
+ userRoute.path({ id: "42" }, { tab: "settings" });
168
+ // "/users/42?tab=settings"
169
+
170
+ userRoute.path({ id: "42" }, { tab: "settings" }, "profile");
171
+ // "/users/42?tab=settings#profile"
172
+ ```
173
+
174
+ #### Type-safe Links
175
+
176
+ ```tsx
177
+ <userRoute.Link params={{ id: "42" }} queryParams={{ tab: "profile" }}>
178
+ View Profile
179
+ </userRoute.Link>
180
+
181
+ // Automatically includes prevPath in navigation state
182
+ ```
183
+
184
+ #### Parse URL Params
185
+
186
+ ```tsx
187
+ const params = userRoute.parseURLParams("https://example.com/users/42");
188
+ // { id: "42" }
189
+
190
+ // Throws URLParseError on invalid URL or validation error
191
+ ```
192
+
193
+ #### Update Search Params
194
+
195
+ ```tsx
196
+ const [searchParams, setSearchParams] = route.useSearchParams();
197
+
198
+ // Set new params
199
+ setSearchParams({ q: "react", page: 1 });
200
+
201
+ // Update based on previous
202
+ setSearchParams((prev) => ({ ...prev, page: prev.page + 1 }));
203
+
204
+ // With navigation options
205
+ setSearchParams({ q: "vue" }, { replace: true });
206
+ ```
207
+
208
+ ### Utilities
209
+
210
+ #### `setGlobalErrorRedirect(url)`
211
+
212
+ Set global fallback redirect URL for validation errors.
213
+
214
+ ```tsx
215
+ setGlobalErrorRedirect("/error");
216
+ ```
217
+
218
+ #### `replaceState(state)`
219
+
220
+ Update browser history state without navigation.
221
+
222
+ ```tsx
223
+ import { replaceState } from "@ovineko/react-router";
224
+
225
+ replaceState({ scrollTop: window.scrollY });
226
+ ```
227
+
228
+ #### `URLParseError`
229
+
230
+ Error class thrown when URL parsing fails.
231
+
232
+ ```tsx
233
+ try {
234
+ route.parseURLParams("invalid-url");
235
+ } catch (error) {
236
+ if (error instanceof URLParseError) {
237
+ console.log(error.context); // { pattern, url, ... }
238
+ }
239
+ }
240
+ ```
241
+
242
+ ## TypeScript
243
+
244
+ All types are automatically inferred from valibot schemas:
245
+
246
+ ```tsx
247
+ const route = createRouteWithParams("/users/:id", {
248
+ params: v.object({ id: v.string() }),
249
+ searchParams: v.object({
250
+ page: v.optional(v.pipe(v.string(), v.transform(Number), v.number())),
251
+ }),
252
+ });
253
+
254
+ // Inferred types:
255
+ const params = route.useParams(); // Readonly<{ id: string }>
256
+ const [searchParams] = route.useSearchParams(); // Readonly<{ page?: number }>
257
+ ```
258
+
259
+ ## Migration from v0.x
260
+
261
+ The library now uses valibot for validation instead of TypeScript generics:
262
+
263
+ **Before:**
264
+
265
+ ```tsx
266
+ const route = createRouteWithParams<{ id: string }, { q: string }>("/users/:id");
267
+ ```
268
+
269
+ **After:**
270
+
271
+ ```tsx
272
+ const route = createRouteWithParams("/users/:id", {
273
+ params: v.object({ id: v.string() }),
274
+ searchParams: v.object({ q: v.string() }),
275
+ });
276
+ ```
277
+
278
+ **Benefits:**
279
+
280
+ - Runtime validation
281
+ - Automatic error handling
282
+ - Schema transformations (e.g., string → number)
283
+ - Better type inference
284
+
285
+ ## License
286
+
287
+ MIT
@@ -0,0 +1,15 @@
1
+ import * as v from "valibot";
2
+ import type { InferSearchParams, RouteWithoutParams, RouteWithParams, State } from "./types";
3
+ export type { RouteBase, RouteWithoutParams, RouteWithParams, State, UseParamsRawResult, UseSearchParamsRawResult, } from "./types";
4
+ export { URLParseError } from "./validation";
5
+ export declare const setGlobalErrorRedirect: (url: string) => void;
6
+ export declare const createRouteWithParams: <TParams extends v.GenericSchema, TSearchParams extends undefined | v.GenericSchema = undefined>(pattern: string, config: {
7
+ errorRedirect?: string;
8
+ params: TParams;
9
+ searchParams?: TSearchParams;
10
+ }) => Readonly<RouteWithParams<v.InferOutput<TParams>, InferSearchParams<TSearchParams>>>;
11
+ export declare const createRouteWithoutParams: <TSearchParams extends undefined | v.GenericSchema = undefined>(pattern: string, config?: {
12
+ errorRedirect?: string;
13
+ searchParams?: TSearchParams;
14
+ }) => Readonly<RouteWithoutParams<InferSearchParams<TSearchParams>>>;
15
+ export declare const replaceState: (state: State) => void;
package/dist/index.js ADDED
@@ -0,0 +1,303 @@
1
+ // src/index.tsx
2
+ import { memo, useCallback, useEffect, useMemo } from "react";
3
+ import {
4
+ generatePath,
5
+ Link,
6
+ useLocation,
7
+ useNavigate,
8
+ useParams,
9
+ useSearchParams
10
+ } from "react-router";
11
+ import * as v2 from "valibot";
12
+
13
+ // src/utils.ts
14
+ import * as v from "valibot";
15
+
16
+ // src/validation.ts
17
+ var URLParseError = class extends Error {
18
+ context;
19
+ constructor(message, context = {}) {
20
+ super(message);
21
+ this.name = "URLParseError";
22
+ this.context = context;
23
+ }
24
+ };
25
+ function safeDecodeURIComponent(value) {
26
+ try {
27
+ return decodeURIComponent(value);
28
+ } catch (error) {
29
+ console.warn(`Failed to decode URI component: ${value}`, error);
30
+ return value;
31
+ }
32
+ }
33
+
34
+ // src/utils.ts
35
+ var queryParamsToString = (params) => {
36
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
37
+ return "";
38
+ }
39
+ const query = new URLSearchParams();
40
+ Object.entries(params).forEach(([key, value]) => {
41
+ if (value === void 0 || value === null) {
42
+ return;
43
+ }
44
+ if (Array.isArray(value)) {
45
+ value.forEach((el) => {
46
+ if (el !== void 0 && el !== null) {
47
+ query.append(key, String(el));
48
+ }
49
+ });
50
+ return;
51
+ }
52
+ query.set(key, String(value));
53
+ });
54
+ const queryString = query.toString();
55
+ return queryString ? `?${queryString}` : "";
56
+ };
57
+ var searchParamsToObject = (params) => {
58
+ const obj = {};
59
+ for (const key of new Set(params.keys())) {
60
+ const values = params.getAll(key);
61
+ obj[key] = values.length > 1 ? values : values[0];
62
+ }
63
+ return obj;
64
+ };
65
+ var objectToSearchParams = (obj) => {
66
+ const params = new URLSearchParams();
67
+ for (const [key, value] of Object.entries(obj)) {
68
+ if (value === void 0 || value === null) {
69
+ continue;
70
+ }
71
+ if (Array.isArray(value)) {
72
+ value.forEach((item) => {
73
+ if (item !== void 0 && item !== null) {
74
+ params.append(key, String(item));
75
+ }
76
+ });
77
+ } else {
78
+ params.set(key, String(value));
79
+ }
80
+ }
81
+ return params;
82
+ };
83
+ var parseURLParamsRaw = (pattern, url) => {
84
+ let pathname;
85
+ try {
86
+ pathname = new URL(url).pathname;
87
+ } catch {
88
+ throw new URLParseError(`Invalid URL: "${url}"`, { pattern, url });
89
+ }
90
+ const urlParts = pathname.split("/").filter(Boolean);
91
+ const patternParts = pattern.split("/").filter(Boolean);
92
+ const params = {};
93
+ for (const [i, patternPart] of patternParts.entries()) {
94
+ const urlValue = urlParts[i];
95
+ if (patternPart.startsWith(":")) {
96
+ const paramKey = patternPart.slice(1);
97
+ if (!urlValue) {
98
+ throw new URLParseError(`Missing required parameter "${paramKey}" in URL "${pathname}"`, {
99
+ paramKey,
100
+ pathname,
101
+ pattern,
102
+ url
103
+ });
104
+ }
105
+ params[paramKey] = safeDecodeURIComponent(urlValue);
106
+ } else if (patternPart !== urlValue) {
107
+ throw new URLParseError(
108
+ `URL path "${pathname}" doesn't match pattern "${pattern}": expected "${patternPart}" at position ${i}, got "${urlValue}"`,
109
+ { actual: urlValue, expected: patternPart, pathname, pattern, position: i, url }
110
+ );
111
+ }
112
+ }
113
+ return params;
114
+ };
115
+ var filterBySchemaKeys = (obj, schema) => {
116
+ const entries = schema.entries;
117
+ const filtered = {};
118
+ for (const key in entries) {
119
+ if (key in obj) {
120
+ filtered[key] = obj[key];
121
+ }
122
+ }
123
+ return filtered;
124
+ };
125
+ var cleanOptionalParams = (obj, schema) => {
126
+ const entries = schema.entries;
127
+ const cleaned = {};
128
+ for (const [key, value] of Object.entries(obj)) {
129
+ const fieldSchema = entries[key];
130
+ if (fieldSchema) {
131
+ const isOptional = fieldSchema.type === "optional";
132
+ if (isOptional) {
133
+ const result = v.safeParse(fieldSchema, value);
134
+ if (result.success) {
135
+ cleaned[key] = value;
136
+ }
137
+ } else {
138
+ cleaned[key] = value;
139
+ }
140
+ }
141
+ }
142
+ return cleaned;
143
+ };
144
+
145
+ // src/index.tsx
146
+ import { jsx } from "react/jsx-runtime";
147
+ var globalErrorRedirect = "/";
148
+ var setGlobalErrorRedirect = (url) => {
149
+ globalErrorRedirect = url;
150
+ };
151
+ var getErrorRedirectUrl = (routeLevel) => {
152
+ return routeLevel ?? globalErrorRedirect;
153
+ };
154
+ var createSearchParamsHook = (schema) => {
155
+ const useSearchParamsRaw = () => {
156
+ const [rawSearchParams, setRawSearchParams] = useSearchParams();
157
+ const { data, error } = useMemo(() => {
158
+ const obj = searchParamsToObject(rawSearchParams);
159
+ const filtered = filterBySchemaKeys(obj, schema);
160
+ const cleaned = cleanOptionalParams(filtered, schema);
161
+ const result = v2.safeParse(schema, cleaned);
162
+ if (result.success) {
163
+ return { data: result.output, error: void 0 };
164
+ }
165
+ return { data: void 0, error: result.issues };
166
+ }, [rawSearchParams]);
167
+ const setter = useCallback(
168
+ (nextInit, navigateOpts) => {
169
+ if (typeof nextInit === "function") {
170
+ setRawSearchParams((prev) => {
171
+ const prevObj = searchParamsToObject(prev);
172
+ const filtered = filterBySchemaKeys(
173
+ prevObj,
174
+ schema
175
+ );
176
+ const cleaned = cleanOptionalParams(
177
+ filtered,
178
+ schema
179
+ );
180
+ const result = v2.safeParse(schema, cleaned);
181
+ const prevParsed = result.success ? result.output : {};
182
+ const nextObj = nextInit(prevParsed);
183
+ return objectToSearchParams(nextObj);
184
+ }, navigateOpts);
185
+ } else {
186
+ setRawSearchParams(
187
+ objectToSearchParams(nextInit),
188
+ navigateOpts
189
+ );
190
+ }
191
+ },
192
+ [setRawSearchParams]
193
+ );
194
+ return { data, error, setter };
195
+ };
196
+ const useSearchParamsHook = (errorRedirectUrl) => {
197
+ const navigate = useNavigate();
198
+ const { data, error, setter } = useSearchParamsRaw();
199
+ useEffect(() => {
200
+ if (error) {
201
+ const redirectUrl = getErrorRedirectUrl(errorRedirectUrl);
202
+ navigate(redirectUrl, { replace: true });
203
+ }
204
+ }, [error, navigate, errorRedirectUrl]);
205
+ return [data ?? {}, setter];
206
+ };
207
+ return { useSearchParamsHook, useSearchParamsRaw };
208
+ };
209
+ var createRouteWithParams = (pattern, config) => {
210
+ const getPath = (params, searchParams, hash) => `${generatePath(pattern, params)}${queryParamsToString(searchParams)}${hash ? `#${hash}` : ""}`;
211
+ const searchParamsHooks = config.searchParams ? createSearchParamsHook(config.searchParams) : null;
212
+ const LinkComponent = memo(
213
+ ({
214
+ children,
215
+ params,
216
+ queryParams,
217
+ state,
218
+ ...props
219
+ }) => {
220
+ const { pathname } = useLocation();
221
+ const mergedState = useMemo(
222
+ () => ({ prevPath: pathname, ...state }),
223
+ [pathname, state]
224
+ );
225
+ return /* @__PURE__ */ jsx(Link, { ...props, state: mergedState, to: getPath(params, queryParams), children });
226
+ }
227
+ );
228
+ const useParamsRaw = () => {
229
+ const params = useParams();
230
+ const result = v2.safeParse(config.params, params);
231
+ if (result.success) {
232
+ return [result.output, void 0];
233
+ }
234
+ return [void 0, result.issues];
235
+ };
236
+ const useParamsHook = () => {
237
+ const navigate = useNavigate();
238
+ const [params, error] = useParamsRaw();
239
+ useEffect(() => {
240
+ if (error) {
241
+ const redirectUrl = getErrorRedirectUrl(config.errorRedirect);
242
+ navigate(redirectUrl, { replace: true });
243
+ }
244
+ }, [error, navigate]);
245
+ return params ?? {};
246
+ };
247
+ return {
248
+ Link: LinkComponent,
249
+ parseURLParams: (url) => {
250
+ const rawParams = parseURLParamsRaw(pattern, url);
251
+ return v2.parse(config.params, rawParams);
252
+ },
253
+ path: getPath,
254
+ pattern,
255
+ useParams: useParamsHook,
256
+ useParamsRaw,
257
+ useSearchParams: searchParamsHooks ? () => searchParamsHooks.useSearchParamsHook(config.errorRedirect) : void 0,
258
+ useSearchParamsRaw: searchParamsHooks ? searchParamsHooks.useSearchParamsRaw : void 0
259
+ };
260
+ };
261
+ var createRouteWithoutParams = (pattern, config) => {
262
+ const getPath = (searchParams, hash) => `${pattern}${queryParamsToString(searchParams)}${hash ? `#${hash}` : ""}`;
263
+ const searchParamsHooks = config?.searchParams ? createSearchParamsHook(config.searchParams) : null;
264
+ const LinkComponent = memo(
265
+ ({ children, queryParams, state, ...props }) => {
266
+ const { pathname } = useLocation();
267
+ const mergedState = useMemo(
268
+ () => ({ prevPath: pathname, ...state }),
269
+ [pathname, state]
270
+ );
271
+ return /* @__PURE__ */ jsx(Link, { ...props, state: mergedState, to: getPath(queryParams), children });
272
+ }
273
+ );
274
+ return {
275
+ Link: LinkComponent,
276
+ path: getPath,
277
+ pattern,
278
+ useSearchParams: searchParamsHooks ? () => searchParamsHooks.useSearchParamsHook(config?.errorRedirect) : void 0,
279
+ useSearchParamsRaw: searchParamsHooks ? searchParamsHooks.useSearchParamsRaw : void 0
280
+ };
281
+ };
282
+ var replaceState = (state) => {
283
+ if (!globalThis.window) {
284
+ return;
285
+ }
286
+ globalThis.window.history.replaceState(
287
+ {
288
+ ...globalThis.window.history.state,
289
+ usr: {
290
+ ...globalThis.window.history.state?.usr,
291
+ ...state
292
+ }
293
+ },
294
+ ""
295
+ );
296
+ };
297
+ export {
298
+ URLParseError,
299
+ createRouteWithParams,
300
+ createRouteWithoutParams,
301
+ replaceState,
302
+ setGlobalErrorRedirect
303
+ };
@@ -0,0 +1,47 @@
1
+ import type { LinkProps as LinkPropsLib, NavigateOptions } from "react-router";
2
+ import type * as v from "valibot";
3
+ export type InferSearchParams<T> = T extends v.GenericSchema ? v.InferOutput<T> : undefined;
4
+ export interface LinkPropsWithoutParams<SearchParams> extends Omit<LinkPropsLib, "to"> {
5
+ children: React.ReactNode | string;
6
+ className?: string;
7
+ queryParams?: SearchParams;
8
+ state?: State;
9
+ style?: React.CSSProperties;
10
+ }
11
+ export interface LinkPropsWithParams<Params, SearchParams> extends LinkPropsWithoutParams<SearchParams> {
12
+ params: Params;
13
+ }
14
+ export interface RouteBase {
15
+ pattern: string;
16
+ }
17
+ export interface RouteWithoutParams<SearchParams = undefined> extends RouteBase {
18
+ Link: (props: LinkPropsWithoutParams<SearchParams>) => React.ReactNode;
19
+ path: (searchParams?: SearchParams, hash?: string) => string;
20
+ useSearchParams: SearchParamsHook<SearchParams>;
21
+ useSearchParamsRaw: SearchParamsHook<SearchParams> extends undefined ? undefined : () => UseSearchParamsRawResult<SearchParams>;
22
+ }
23
+ export interface RouteWithParams<Params, SearchParams = undefined> extends RouteBase {
24
+ Link: (props: LinkPropsWithParams<Params, SearchParams>) => React.ReactNode;
25
+ parseURLParams: (url: string) => Params;
26
+ path: (params: Params, searchParams?: SearchParams, hash?: string) => string;
27
+ useParams: () => Readonly<Params>;
28
+ useParamsRaw: () => UseParamsRawResult<Params>;
29
+ useSearchParams: SearchParamsHook<SearchParams>;
30
+ useSearchParamsRaw: SearchParamsHook<SearchParams> extends undefined ? undefined : () => UseSearchParamsRawResult<SearchParams>;
31
+ }
32
+ export type SearchParamsHook<SearchParams> = SearchParams extends undefined ? undefined : () => readonly [Readonly<SearchParams>, SetSearchParams<SearchParams>];
33
+ export type SearchParamsInput = Record<string, string | string[]>;
34
+ export type SetSearchParams<SearchParams> = (nextInit: ((prev: Readonly<SearchParams>) => SearchParams) | SearchParams, navigateOpts?: NavigateOptions) => void;
35
+ export interface State {
36
+ prevPath?: string;
37
+ scrollTop?: number;
38
+ }
39
+ export type UseParamsRawResult<Params> = readonly [
40
+ Readonly<Params> | undefined,
41
+ undefined | v.BaseIssue<unknown>[]
42
+ ];
43
+ export interface UseSearchParamsRawResult<SearchParams> {
44
+ data: Readonly<SearchParams> | undefined;
45
+ error: undefined | v.BaseIssue<unknown>[];
46
+ setter: SetSearchParams<SearchParams>;
47
+ }
@@ -0,0 +1,17 @@
1
+ import * as v from "valibot";
2
+ import type { SearchParamsInput } from "./types";
3
+ export declare const queryParamsToString: (params?: SearchParamsInput) => string;
4
+ export declare const searchParamsToObject: (params: URLSearchParams) => Record<string, string | string[]>;
5
+ export declare const objectToSearchParams: (obj: Record<string, unknown>) => URLSearchParams;
6
+ export declare const parseURLParamsRaw: (pattern: string, url: string) => Record<string, string>;
7
+ /**
8
+ * Filters an object to only include keys that exist in the valibot object schema.
9
+ * Ignores extra query parameters not defined in the schema.
10
+ */
11
+ export declare const filterBySchemaKeys: <T extends v.ObjectSchema<any, any>>(obj: Record<string, unknown>, schema: T) => Record<string, unknown>;
12
+ /**
13
+ * Removes invalid optional parameters from an object.
14
+ * Optional parameters with invalid values are ignored (treated as undefined).
15
+ * Required parameters are kept as-is for subsequent validation.
16
+ */
17
+ export declare const cleanOptionalParams: <T extends v.ObjectSchema<any, any>>(obj: Record<string, unknown>, schema: T) => Record<string, unknown>;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Custom error class for URL parsing failures
3
+ */
4
+ export declare class URLParseError extends Error {
5
+ readonly context: Record<string, unknown>;
6
+ constructor(message: string, context?: Record<string, unknown>);
7
+ }
8
+ /**
9
+ * Decodes URL component safely, handling special characters
10
+ */
11
+ export declare function safeDecodeURIComponent(value: string): string;
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@ovineko/react-router",
3
+ "version": "0.0.1",
4
+ "description": "Type-safe wrapper for React Router v7 with typed params, query params, and Link components",
5
+ "keywords": [
6
+ "react",
7
+ "react-router",
8
+ "router",
9
+ "type-safe",
10
+ "typed-routes",
11
+ "typed-params",
12
+ "query-params",
13
+ "routing",
14
+ "typescript",
15
+ "react19",
16
+ "link"
17
+ ],
18
+ "homepage": "https://github.com/ovineko/ovineko/tree/main/packages/react-router",
19
+ "bugs": {
20
+ "url": "https://github.com/ovineko/ovineko/issues"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/ovineko/ovineko.git",
25
+ "directory": "packages/react-router"
26
+ },
27
+ "license": "MIT",
28
+ "author": "Alexander Svinarev <shibanet0@gmail.com> (shibanet0.com)",
29
+ "type": "module",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ }
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "README.md"
39
+ ],
40
+ "peerDependencies": {
41
+ "react": "^19",
42
+ "react-router": "^7",
43
+ "valibot": "^1"
44
+ },
45
+ "engines": {
46
+ "node": ">=20"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ }
51
+ }