@bb-labs/next-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 +291 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/z-page/index.d.ts +21 -0
- package/dist/client/z-page/index.js +52 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/z-page/fns/z-page.d.ts +81 -0
- package/dist/server/z-page/fns/z-page.js +18 -0
- package/dist/server/z-page/helpers/infer-props.d.ts +12 -0
- package/dist/server/z-page/helpers/infer-props.js +1 -0
- package/dist/server/z-page/helpers/resolve-params.d.ts +14 -0
- package/dist/server/z-page/helpers/resolve-params.js +31 -0
- package/dist/server/z-page/index.d.ts +3 -0
- package/dist/server/z-page/index.js +2 -0
- package/dist/server/z-page/types.d.ts +10 -0
- package/dist/server/z-page/types.js +1 -0
- package/dist/server/z-page/utils/helper.d.ts +17 -0
- package/dist/server/z-page/utils/helper.js +35 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
## Introduction
|
|
2
|
+
|
|
3
|
+
Type-safe route and search params for Next.js App Router with Zod validation.
|
|
4
|
+
|
|
5
|
+
- 🔒 **Type-safe** — Full TypeScript inference from your Zod schemas
|
|
6
|
+
- ✅ **Validated** — Params are validated at runtime with Zod
|
|
7
|
+
- 🎯 **Flexible** — Use promises or pre-awaited values, your choice
|
|
8
|
+
- 🪝 **Hooks included** — First-class support for client components
|
|
9
|
+
- 📦 **Zero dependencies** — Only requires Zod
|
|
10
|
+
- 🛡️ **Error handling** — Built-in error handling with customizable callbacks
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @bb-labs/next-router
|
|
16
|
+
# or
|
|
17
|
+
yarn add @bb-labs/next-router
|
|
18
|
+
# or
|
|
19
|
+
bun add @bb-labs/next-router
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Server Components
|
|
23
|
+
|
|
24
|
+
### `zPage`
|
|
25
|
+
|
|
26
|
+
A unified function for creating page components with type-safe params. Use the `resolve` parameter to control how params are handled.
|
|
27
|
+
|
|
28
|
+
#### Resolved Params (Default)
|
|
29
|
+
|
|
30
|
+
When `resolve: true` (or omitted), params are **pre-awaited** with built-in Suspense.
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { zPage } from "@bb-labs/next-router";
|
|
34
|
+
import { z } from "zod";
|
|
35
|
+
|
|
36
|
+
export default zPage({
|
|
37
|
+
params: {
|
|
38
|
+
searchParams: z.object({
|
|
39
|
+
email: z.string().email(),
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
handler: ({ searchParams }) => {
|
|
43
|
+
// Already awaited — use directly
|
|
44
|
+
return <div>Email: {searchParams.email}</div>;
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Options for resolved mode:**
|
|
50
|
+
|
|
51
|
+
- `onError?: (error: z.ZodError) => void` — Custom error handler (defaults to redirecting to "/404")
|
|
52
|
+
- `loadingHandler?: React.ReactNode` — Custom Suspense fallback (defaults to `null`)
|
|
53
|
+
|
|
54
|
+
#### Promise Params
|
|
55
|
+
|
|
56
|
+
When `resolve: false`, params are passed as **promises** — you control when to await.
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import { zPage, resolveParams } from "@bb-labs/next-router";
|
|
60
|
+
import { z } from "zod";
|
|
61
|
+
|
|
62
|
+
export default zPage({
|
|
63
|
+
params: {
|
|
64
|
+
routeParams: z.object({ id: z.string() }),
|
|
65
|
+
searchParams: z.object({ page: z.coerce.number().optional() }),
|
|
66
|
+
},
|
|
67
|
+
resolve: false,
|
|
68
|
+
handler: async (paramsPromise) => {
|
|
69
|
+
const { routeParams, searchParams } = await resolveParams(paramsPromise);
|
|
70
|
+
return (
|
|
71
|
+
<div>
|
|
72
|
+
Product {routeParams.id}, Page {searchParams.page}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Note:** `onError` and `loadingHandler` are not available when `resolve: false`.
|
|
80
|
+
|
|
81
|
+
### `resolveParams`
|
|
82
|
+
|
|
83
|
+
Helper function to await and validate params from promise-based handlers. Optionally accepts an `onError` callback.
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { zPage, resolveParams } from "@bb-labs/next-router";
|
|
87
|
+
import { redirect } from "next/navigation";
|
|
88
|
+
import { z } from "zod";
|
|
89
|
+
|
|
90
|
+
export default zPage({
|
|
91
|
+
params: {
|
|
92
|
+
searchParams: z.object({
|
|
93
|
+
email: z.string().email(),
|
|
94
|
+
}),
|
|
95
|
+
},
|
|
96
|
+
resolve: false,
|
|
97
|
+
handler: async (paramsPromise) => {
|
|
98
|
+
const { searchParams } = await resolveParams(paramsPromise, {
|
|
99
|
+
onError: (error) => {
|
|
100
|
+
console.error("Validation failed:", error);
|
|
101
|
+
redirect("/custom-error");
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
return <div>{searchParams.email}</div>;
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Flexible Params Configuration
|
|
110
|
+
|
|
111
|
+
You can provide both, only `routeParams`, or only `searchParams` — but never neither:
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
// ✅ Both
|
|
115
|
+
{ params: { routeParams: z.object({...}), searchParams: z.object({...}) } }
|
|
116
|
+
|
|
117
|
+
// ✅ Only routeParams
|
|
118
|
+
{ params: { routeParams: z.object({...}) } }
|
|
119
|
+
|
|
120
|
+
// ✅ Only searchParams
|
|
121
|
+
{ params: { searchParams: z.object({...}) } }
|
|
122
|
+
|
|
123
|
+
// ❌ Neither — TypeScript error
|
|
124
|
+
{ params: {} }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Client Components
|
|
130
|
+
|
|
131
|
+
### `useRouteParams` / `useSearchParams`
|
|
132
|
+
|
|
133
|
+
Hooks for accessing validated params in client components with `onSuccess` and `onError` callbacks.
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
"use client";
|
|
137
|
+
|
|
138
|
+
import { useSearchParams } from "@bb-labs/next-router/client";
|
|
139
|
+
import { z } from "zod";
|
|
140
|
+
|
|
141
|
+
const searchParamsSchema = z.object({
|
|
142
|
+
email: z.string().email(),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
export function MyComponent() {
|
|
146
|
+
const { data } = useSearchParams(searchParamsSchema, {
|
|
147
|
+
onSuccess: (data) => {
|
|
148
|
+
console.log("Valid params:", data.email);
|
|
149
|
+
},
|
|
150
|
+
onError: (error) => {
|
|
151
|
+
console.error("Validation failed:", error);
|
|
152
|
+
// Custom error handling (defaults to redirecting to "/404")
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (!data) return <div>Loading...</div>;
|
|
157
|
+
|
|
158
|
+
return <div>Email: {data.email}</div>;
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Hook API
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
const { data } = useRouteParams(schema, options?);
|
|
166
|
+
const { data } = useSearchParams(schema, options?);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Options:**
|
|
170
|
+
|
|
171
|
+
- `onSuccess?: (data: T) => void` — Called when params are successfully validated
|
|
172
|
+
- `onError?: (error: Error) => void` — Called when validation fails (defaults to redirecting to "/404")
|
|
173
|
+
|
|
174
|
+
**Returns:**
|
|
175
|
+
|
|
176
|
+
- `data` — The validated params, or `null` if validation failed
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Schema Types
|
|
181
|
+
|
|
182
|
+
**Important:** URL params are always strings. Use Zod's coercion methods for non-string types:
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
// ✅ Correct — use coercion for non-string types
|
|
186
|
+
z.object({
|
|
187
|
+
id: z.string(), // strings work directly
|
|
188
|
+
page: z.coerce.number(), // coerce string → number
|
|
189
|
+
active: z.coerce.boolean(), // coerce string → boolean
|
|
190
|
+
count: z.coerce.number().optional(), // optional coerced number
|
|
191
|
+
tags: z.array(z.string()), // array of strings (for ?tags=a&tags=b)
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ❌ Wrong — will fail because raw input is always a string
|
|
195
|
+
z.object({
|
|
196
|
+
page: z.number(), // Error: expected number, received string
|
|
197
|
+
active: z.boolean(), // Error: expected boolean, received string
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
You can also use transforms for custom parsing:
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
z.object({
|
|
205
|
+
date: z.string().transform((s) => new Date(s)),
|
|
206
|
+
ids: z.string().transform((s) => s.split(",")),
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### JSON-Encoded Objects
|
|
211
|
+
|
|
212
|
+
For complex objects, encode them as JSON in the URL (URL-escaped). Use `.transform()` with `JSON.parse()` and `.pipe()` to validate:
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
// URL: ?filter={"category":"books","price":{"min":10,"max":50},"inStock":true}
|
|
216
|
+
// (URL-encoded: ?filter=%7B%22category%22%3A%22books%22...%7D)
|
|
217
|
+
|
|
218
|
+
z.object({
|
|
219
|
+
filter: z
|
|
220
|
+
.string()
|
|
221
|
+
.transform((s) => JSON.parse(s))
|
|
222
|
+
.pipe(
|
|
223
|
+
z.object({
|
|
224
|
+
category: z.string(),
|
|
225
|
+
price: z.object({ min: z.number(), max: z.number() }),
|
|
226
|
+
tags: z.array(z.string()).optional(),
|
|
227
|
+
inStock: z.boolean(),
|
|
228
|
+
})
|
|
229
|
+
),
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**How it works:**
|
|
234
|
+
|
|
235
|
+
1. The raw URL param is a JSON string: `'{"category":"books",...}'`
|
|
236
|
+
2. `.transform((s) => JSON.parse(s))` parses it into a JS object
|
|
237
|
+
3. `.pipe(z.object({...}))` validates the parsed object's structure
|
|
238
|
+
|
|
239
|
+
**Example usage:**
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
export default zPage({
|
|
243
|
+
params: {
|
|
244
|
+
searchParams: z.object({
|
|
245
|
+
filter: z
|
|
246
|
+
.string()
|
|
247
|
+
.transform((s) => JSON.parse(s))
|
|
248
|
+
.pipe(
|
|
249
|
+
z.object({
|
|
250
|
+
category: z.string(),
|
|
251
|
+
price: z.object({ min: z.number(), max: z.number() }),
|
|
252
|
+
})
|
|
253
|
+
),
|
|
254
|
+
}),
|
|
255
|
+
},
|
|
256
|
+
handler: ({ searchParams }) => {
|
|
257
|
+
// searchParams.filter is fully typed!
|
|
258
|
+
return (
|
|
259
|
+
<div>
|
|
260
|
+
Category: {searchParams.filter.category}
|
|
261
|
+
Price: ${searchParams.filter.price.min} - ${searchParams.filter.price.max}
|
|
262
|
+
</div>
|
|
263
|
+
);
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Client-side encoding:**
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
const filter = { category: "books", price: { min: 10, max: 50 } };
|
|
272
|
+
const url = `/products?filter=${encodeURIComponent(JSON.stringify(filter))}`;
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Error Handling
|
|
278
|
+
|
|
279
|
+
By default, validation errors redirect to "/404":
|
|
280
|
+
|
|
281
|
+
- **Server components**: `zPage` and `resolveParams` with `resolve: true` redirects on validation failure
|
|
282
|
+
- **Client components**: `useRouteParams` and `useSearchParams` redirect on validation failure
|
|
283
|
+
|
|
284
|
+
You can customize error handling:
|
|
285
|
+
|
|
286
|
+
- **Server**: Provide an `onError` callback to `zPage` (only available when `resolve: true`)
|
|
287
|
+
- **Client**: Provide an `onError` callback to the hooks
|
|
288
|
+
|
|
289
|
+
## License
|
|
290
|
+
|
|
291
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./z-page";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./z-page";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z, type ZodRawShape } from "zod";
|
|
2
|
+
type ZodObject = z.ZodObject<ZodRawShape>;
|
|
3
|
+
type HookOptions<T> = {
|
|
4
|
+
onSuccess?: (data: T) => void;
|
|
5
|
+
onError?: (error: Error) => void;
|
|
6
|
+
};
|
|
7
|
+
export declare function useRouteParams<T extends ZodObject>(schema: T, options?: HookOptions<z.output<T>>): {
|
|
8
|
+
data: z.output<T>;
|
|
9
|
+
error: null;
|
|
10
|
+
} | {
|
|
11
|
+
data: null;
|
|
12
|
+
error: z.ZodError<z.core.output<T>>;
|
|
13
|
+
};
|
|
14
|
+
export declare function useSearchParams<T extends ZodObject>(schema: T, options?: HookOptions<z.output<T>>): {
|
|
15
|
+
data: z.output<T>;
|
|
16
|
+
error: null;
|
|
17
|
+
} | {
|
|
18
|
+
data: null;
|
|
19
|
+
error: z.ZodError<z.core.output<T>>;
|
|
20
|
+
};
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useRef, useMemo } from "react";
|
|
3
|
+
import { useParams, useSearchParams as useNextSearchParams, useRouter } from "next/navigation";
|
|
4
|
+
export function useRouteParams(schema, options) {
|
|
5
|
+
const rawParams = useParams();
|
|
6
|
+
const router = useRouter();
|
|
7
|
+
const calledRef = useRef(false);
|
|
8
|
+
const result = useMemo(() => {
|
|
9
|
+
const parsed = schema.safeParse(rawParams);
|
|
10
|
+
return parsed.success ? { data: parsed.data, error: null } : { data: null, error: parsed.error };
|
|
11
|
+
}, [rawParams, schema]);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (calledRef.current)
|
|
14
|
+
return;
|
|
15
|
+
calledRef.current = true;
|
|
16
|
+
if (result.error) {
|
|
17
|
+
console.log(result.error);
|
|
18
|
+
options?.onError ? options.onError(result.error) : router.push("/404");
|
|
19
|
+
}
|
|
20
|
+
else if (result.data) {
|
|
21
|
+
options?.onSuccess?.(result.data);
|
|
22
|
+
}
|
|
23
|
+
}, [result, options, router]);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
export function useSearchParams(schema, options) {
|
|
27
|
+
const nextSearchParams = useNextSearchParams();
|
|
28
|
+
const router = useRouter();
|
|
29
|
+
const calledRef = useRef(false);
|
|
30
|
+
const result = useMemo(() => {
|
|
31
|
+
const raw = {};
|
|
32
|
+
nextSearchParams.forEach((v, k) => {
|
|
33
|
+
const existing = raw[k];
|
|
34
|
+
raw[k] = existing ? (Array.isArray(existing) ? [...existing, v] : [existing, v]) : v;
|
|
35
|
+
});
|
|
36
|
+
const parsed = schema.safeParse(raw);
|
|
37
|
+
return parsed.success ? { data: parsed.data, error: null } : { data: null, error: parsed.error };
|
|
38
|
+
}, [nextSearchParams, schema]);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (calledRef.current)
|
|
41
|
+
return;
|
|
42
|
+
calledRef.current = true;
|
|
43
|
+
if (result.error) {
|
|
44
|
+
console.log(result.error);
|
|
45
|
+
options?.onError ? options.onError(result.error) : router.push("/404");
|
|
46
|
+
}
|
|
47
|
+
else if (result.data) {
|
|
48
|
+
options?.onSuccess?.(result.data);
|
|
49
|
+
}
|
|
50
|
+
}, [result, options, router]);
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./z-page";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./z-page";
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { type ZodObject, type OnError } from "../utils/helper";
|
|
4
|
+
import type { NextPageProps } from "../types";
|
|
5
|
+
/** Return type for zPage */
|
|
6
|
+
type ZPageReturn = (props: NextPageProps) => React.ReactNode | Promise<React.ReactNode>;
|
|
7
|
+
export declare function zPage<RP extends ZodObject, SP extends ZodObject>(cfg: {
|
|
8
|
+
params: {
|
|
9
|
+
routeParams: RP;
|
|
10
|
+
searchParams: SP;
|
|
11
|
+
};
|
|
12
|
+
resolve?: true;
|
|
13
|
+
onError?: OnError;
|
|
14
|
+
loadingHandler?: React.ReactNode;
|
|
15
|
+
handler: (input: {
|
|
16
|
+
routeParams: z.output<RP>;
|
|
17
|
+
searchParams: z.output<SP>;
|
|
18
|
+
}) => React.ReactNode | Promise<React.ReactNode>;
|
|
19
|
+
}): ZPageReturn;
|
|
20
|
+
export declare function zPage<RP extends ZodObject>(cfg: {
|
|
21
|
+
params: {
|
|
22
|
+
routeParams: RP;
|
|
23
|
+
searchParams?: undefined;
|
|
24
|
+
};
|
|
25
|
+
resolve?: true;
|
|
26
|
+
onError?: OnError;
|
|
27
|
+
loadingHandler?: React.ReactNode;
|
|
28
|
+
handler: (input: {
|
|
29
|
+
routeParams: z.output<RP>;
|
|
30
|
+
}) => React.ReactNode | Promise<React.ReactNode>;
|
|
31
|
+
}): ZPageReturn;
|
|
32
|
+
export declare function zPage<SP extends ZodObject>(cfg: {
|
|
33
|
+
params: {
|
|
34
|
+
routeParams?: undefined;
|
|
35
|
+
searchParams: SP;
|
|
36
|
+
};
|
|
37
|
+
resolve?: true;
|
|
38
|
+
onError?: OnError;
|
|
39
|
+
loadingHandler?: React.ReactNode;
|
|
40
|
+
handler: (input: {
|
|
41
|
+
searchParams: z.output<SP>;
|
|
42
|
+
}) => React.ReactNode | Promise<React.ReactNode>;
|
|
43
|
+
}): ZPageReturn;
|
|
44
|
+
export declare function zPage<RP extends ZodObject, SP extends ZodObject>(cfg: {
|
|
45
|
+
params: {
|
|
46
|
+
routeParams: RP;
|
|
47
|
+
searchParams: SP;
|
|
48
|
+
};
|
|
49
|
+
resolve: false;
|
|
50
|
+
onError?: never;
|
|
51
|
+
loadingHandler?: never;
|
|
52
|
+
handler: (input: {
|
|
53
|
+
routeParams: Promise<z.output<RP>>;
|
|
54
|
+
searchParams: Promise<z.output<SP>>;
|
|
55
|
+
}) => React.ReactNode | Promise<React.ReactNode>;
|
|
56
|
+
}): ZPageReturn;
|
|
57
|
+
export declare function zPage<RP extends ZodObject>(cfg: {
|
|
58
|
+
params: {
|
|
59
|
+
routeParams: RP;
|
|
60
|
+
searchParams?: undefined;
|
|
61
|
+
};
|
|
62
|
+
resolve: false;
|
|
63
|
+
onError?: never;
|
|
64
|
+
loadingHandler?: never;
|
|
65
|
+
handler: (input: {
|
|
66
|
+
routeParams: Promise<z.output<RP>>;
|
|
67
|
+
}) => React.ReactNode | Promise<React.ReactNode>;
|
|
68
|
+
}): ZPageReturn;
|
|
69
|
+
export declare function zPage<SP extends ZodObject>(cfg: {
|
|
70
|
+
params: {
|
|
71
|
+
routeParams?: undefined;
|
|
72
|
+
searchParams: SP;
|
|
73
|
+
};
|
|
74
|
+
resolve: false;
|
|
75
|
+
onError?: never;
|
|
76
|
+
loadingHandler?: never;
|
|
77
|
+
handler: (input: {
|
|
78
|
+
searchParams: Promise<z.output<SP>>;
|
|
79
|
+
}) => React.ReactNode | Promise<React.ReactNode>;
|
|
80
|
+
}): ZPageReturn;
|
|
81
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Suspense } from "react";
|
|
3
|
+
import { setup } from "../utils/helper";
|
|
4
|
+
// ------------- IMPLEMENTATION -------------
|
|
5
|
+
export function zPage(cfg) {
|
|
6
|
+
const shouldResolve = cfg.resolve !== false;
|
|
7
|
+
const { createValidatedPromises, resolveParams } = setup(cfg.params, shouldResolve ? cfg.onError : undefined);
|
|
8
|
+
if (!shouldResolve) {
|
|
9
|
+
return async (props) => cfg.handler(createValidatedPromises(props));
|
|
10
|
+
}
|
|
11
|
+
async function Inner({ promises }) {
|
|
12
|
+
const input = await resolveParams(promises);
|
|
13
|
+
if (!Object.keys(input).length)
|
|
14
|
+
return null;
|
|
15
|
+
return cfg.handler(input);
|
|
16
|
+
}
|
|
17
|
+
return (props) => (_jsx(Suspense, { fallback: cfg.loadingHandler ?? null, children: _jsx(Inner, { promises: createValidatedPromises(props) }) }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z, type ZodObject, type ZodRawShape } from "zod";
|
|
2
|
+
type InferParamsPromises<T> = ("routeParams" extends keyof T ? (T["routeParams"] extends ZodObject<ZodRawShape> ? {
|
|
3
|
+
routeParams: Promise<z.output<T["routeParams"]>>;
|
|
4
|
+
} : {}) : {}) & ("searchParams" extends keyof T ? (T["searchParams"] extends ZodObject<ZodRawShape> ? {
|
|
5
|
+
searchParams: Promise<z.output<T["searchParams"]>>;
|
|
6
|
+
} : {}) : {});
|
|
7
|
+
type InferParams<T> = ("routeParams" extends keyof T ? (T["routeParams"] extends ZodObject<ZodRawShape> ? {
|
|
8
|
+
routeParams: z.output<T["routeParams"]>;
|
|
9
|
+
} : {}) : {}) & ("searchParams" extends keyof T ? (T["searchParams"] extends ZodObject<ZodRawShape> ? {
|
|
10
|
+
searchParams: z.output<T["searchParams"]>;
|
|
11
|
+
} : {}) : {});
|
|
12
|
+
export type { InferParamsPromises, InferParams };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type OnError } from "../utils/helper";
|
|
2
|
+
type ResolveParamsInput<RP, SP> = {
|
|
3
|
+
routeParams?: Promise<RP>;
|
|
4
|
+
searchParams?: Promise<SP>;
|
|
5
|
+
};
|
|
6
|
+
type ResolveParamsResult<RP, SP> = {
|
|
7
|
+
routeParams: RP;
|
|
8
|
+
searchParams: SP;
|
|
9
|
+
};
|
|
10
|
+
type ResolveParamsOptions = {
|
|
11
|
+
onError?: OnError;
|
|
12
|
+
};
|
|
13
|
+
export declare function resolveParams<RP, SP>(paramsPromise: ResolveParamsInput<RP, SP>, options?: ResolveParamsOptions): Promise<ResolveParamsResult<RP, SP>>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defaultOnError } from "../utils/helper";
|
|
3
|
+
export async function resolveParams(paramsPromise, options = {}) {
|
|
4
|
+
const result = {};
|
|
5
|
+
const onError = options.onError ?? defaultOnError;
|
|
6
|
+
if (paramsPromise.routeParams) {
|
|
7
|
+
try {
|
|
8
|
+
result.routeParams = await paramsPromise.routeParams;
|
|
9
|
+
}
|
|
10
|
+
catch (e) {
|
|
11
|
+
if (e instanceof z.ZodError) {
|
|
12
|
+
onError(e);
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
throw e;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (paramsPromise.searchParams) {
|
|
19
|
+
try {
|
|
20
|
+
result.searchParams = await paramsPromise.searchParams;
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
if (e instanceof z.ZodError) {
|
|
24
|
+
onError(e);
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
throw e;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { z, type ZodRawShape } from "zod";
|
|
2
|
+
/** Next.js page props type */
|
|
3
|
+
export type NextPageProps = {
|
|
4
|
+
params: Promise<Record<string, string | string[]>>;
|
|
5
|
+
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
|
6
|
+
};
|
|
7
|
+
/** Enforces that routeParams/searchParams must be z.object(...) */
|
|
8
|
+
export type ZodObject = z.ZodObject<ZodRawShape>;
|
|
9
|
+
/** Error handler type */
|
|
10
|
+
export type OnError = (error: z.ZodError) => never | void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { NextPageProps, ZodObject, OnError } from "../types";
|
|
3
|
+
export type { ZodObject, OnError };
|
|
4
|
+
export declare const defaultOnError: OnError;
|
|
5
|
+
export declare function setup<RP extends ZodObject, SP extends ZodObject>(params: {
|
|
6
|
+
routeParams?: RP;
|
|
7
|
+
searchParams?: SP;
|
|
8
|
+
}, onError?: OnError): {
|
|
9
|
+
createValidatedPromises: (props: NextPageProps) => {
|
|
10
|
+
routeParams: Promise<z.core.output<RP>> | undefined;
|
|
11
|
+
searchParams: Promise<z.core.output<SP>> | undefined;
|
|
12
|
+
};
|
|
13
|
+
resolveParams: (promises: {
|
|
14
|
+
routeParams: Promise<z.core.output<RP>> | undefined;
|
|
15
|
+
searchParams: Promise<z.core.output<SP>> | undefined;
|
|
16
|
+
}) => Promise<Record<string, unknown>>;
|
|
17
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { redirect } from "next/navigation";
|
|
3
|
+
export const defaultOnError = (e) => {
|
|
4
|
+
console.log(e);
|
|
5
|
+
redirect("/404");
|
|
6
|
+
};
|
|
7
|
+
export function setup(params, onError = defaultOnError) {
|
|
8
|
+
const rpSchema = params.routeParams;
|
|
9
|
+
const spSchema = params.searchParams;
|
|
10
|
+
/** Creates validated promises from Next.js page props */
|
|
11
|
+
function createValidatedPromises(props) {
|
|
12
|
+
return {
|
|
13
|
+
routeParams: rpSchema ? props.params.then((raw) => rpSchema.parse(raw)) : undefined,
|
|
14
|
+
searchParams: spSchema ? props.searchParams.then((raw) => spSchema.parse(raw)) : undefined,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** Awaits all params in parallel, handling errors */
|
|
18
|
+
async function resolveParams(promises) {
|
|
19
|
+
const result = {};
|
|
20
|
+
try {
|
|
21
|
+
const [rp, sp] = await Promise.all([promises.routeParams, promises.searchParams]);
|
|
22
|
+
if (rpSchema)
|
|
23
|
+
result.routeParams = rp;
|
|
24
|
+
if (spSchema)
|
|
25
|
+
result.searchParams = sp;
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
if (e instanceof z.ZodError)
|
|
29
|
+
return onError(e), {};
|
|
30
|
+
throw e;
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
return { createValidatedPromises, resolveParams };
|
|
35
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bb-labs/next-router",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"author": "Beepbop",
|
|
5
|
+
"homepage": "https://github.com/beepbop-labs/next-router",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"next-router",
|
|
8
|
+
"router",
|
|
9
|
+
"next",
|
|
10
|
+
"nextjs"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/beepbop-labs/next-router.git"
|
|
16
|
+
},
|
|
17
|
+
"description": "A library for routing in Next.js",
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"exports": {
|
|
23
|
+
".": "./dist/server/index.js",
|
|
24
|
+
"./client": "./dist/client/index.js"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"clean": "rm -rf dist",
|
|
28
|
+
"build": "npm run clean && tsc -p tsconfig.json",
|
|
29
|
+
"pack": "npm run build && npm pack --pack-destination ./archive/"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"zod": "^4.2.1"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/bun": "latest",
|
|
36
|
+
"@types/react": "^19.2.7"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"typescript": "^5",
|
|
40
|
+
"react": "^19",
|
|
41
|
+
"next": "^16.0.4"
|
|
42
|
+
}
|
|
43
|
+
}
|