Skip to content

Commit 37ae3f3

Browse files
feat(router): supports standard schema (#158)
* feat(router): supports standard schema * fix * fix: types * fix * chore: add changeset * fix: isStandard * fix: template * fix: docs * docs * fix: docs * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 9bf8b50 commit 37ae3f3

File tree

18 files changed

+555
-82
lines changed

18 files changed

+555
-82
lines changed

.changeset/ten-fans-pull.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@granite-js/plugin-router': patch
3+
'@granite-js/react-native': patch
4+
'@granite-js/native': patch
5+
---
6+
7+
feat(router): supports standard schema

docs/guides/granite-router/params.md

Lines changed: 120 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -118,15 +118,10 @@ import * as v from 'valibot';
118118

119119
export const Route = createRoute('/', {
120120
component: Index,
121-
validateParams: (params) => {
122-
return v.parse(
123-
v.object({
124-
name: v.string(),
125-
age: v.number(),
126-
}),
127-
params
128-
);
129-
},
121+
validateParams: v.object({
122+
name: v.string(),
123+
age: v.number(),
124+
}),
130125
});
131126

132127
function Index() {
@@ -148,14 +143,10 @@ import { z } from 'zod';
148143

149144
export const Route = createRoute('/', {
150145
component: Index,
151-
validateParams: (params) => {
152-
return z
153-
.object({
154-
name: z.string(),
155-
age: z.number(),
156-
})
157-
.parse(params);
158-
},
146+
validateParams: z.object({
147+
name: z.string(),
148+
age: z.number(),
149+
}),
159150
});
160151

161152
function Index() {
@@ -174,6 +165,118 @@ function Index() {
174165

175166
</UsageSection>
176167

168+
## Advanced Validation
169+
170+
Validation libraries provide powerful features for handling validation errors and transforming values.
171+
172+
::: code-group
173+
174+
```tsx [valibot]
175+
import { createRoute } from '@granite-js/react-native';
176+
import * as v from 'valibot';
177+
178+
// Using v.fallback() for error resilience
179+
export const Route = createRoute('/profile', {
180+
component: Profile,
181+
validateParams: v.object({
182+
name: v.fallback(v.string(), 'Anonymous'),
183+
age: v.fallback(v.number(), 0),
184+
isActive: v.fallback(v.boolean(), true),
185+
}),
186+
});
187+
```
188+
189+
```tsx [valibot - optional]
190+
import { createRoute } from '@granite-js/react-native';
191+
import * as v from 'valibot';
192+
193+
// Using v.optional() with default values
194+
export const Route = createRoute('/settings', {
195+
component: Settings,
196+
validateParams: v.object({
197+
theme: v.optional(v.picklist(['light', 'dark']), 'light'),
198+
animation: v.optional(v.boolean(), true),
199+
fontSize: v.optional(v.number(), 16),
200+
}),
201+
});
202+
```
203+
204+
```tsx [valibot - transform]
205+
import { createRoute } from '@granite-js/react-native';
206+
import * as v from 'valibot';
207+
208+
// Using v.transform() for type conversion
209+
export const Route = createRoute('/user', {
210+
component: User,
211+
validateParams: v.object({
212+
// Converts string to number
213+
id: v.pipe(
214+
v.string(),
215+
v.transform((v) => parseInt(v))
216+
),
217+
// Converts ISO string to Date object
218+
createdAt: v.pipe(
219+
v.string(),
220+
v.transform((v) => new Date(v))
221+
),
222+
}),
223+
});
224+
```
225+
226+
```tsx [zod]
227+
import { createRoute } from '@granite-js/react-native';
228+
import { z } from 'zod';
229+
230+
// Using .catch() for error resilience
231+
export const Route = createRoute('/profile', {
232+
component: Profile,
233+
validateParams: z.object({
234+
name: z.string().catch('Anonymous'),
235+
age: z.number().catch(0),
236+
isActive: z.boolean().catch(true),
237+
}),
238+
});
239+
```
240+
241+
```tsx [zod - default]
242+
import { createRoute } from '@granite-js/react-native';
243+
import { z } from 'zod';
244+
245+
// Using .default() for optional parameters
246+
export const Route = createRoute('/settings', {
247+
component: Settings,
248+
validateParams: z.object({
249+
theme: z.enum(['light', 'dark']).default('light'),
250+
animation: z.boolean().default(true),
251+
fontSize: z.number().default(16),
252+
}),
253+
});
254+
```
255+
256+
```tsx [zod - transform]
257+
import { createRoute } from '@granite-js/react-native';
258+
import { z } from 'zod';
259+
260+
// Using .transform() for type conversion
261+
export const Route = createRoute('/user', {
262+
component: User,
263+
validateParams: z.object({
264+
// Converts string to number
265+
id: z.string().transform((v) => parseInt(v)),
266+
// Converts ISO string to Date object
267+
createdAt: z.string().transform((v) => new Date(v)),
268+
}),
269+
});
270+
271+
function User() {
272+
const params = Route.useParams();
273+
// params.id is number (not string)
274+
// params.createdAt is Date object
275+
}
276+
```
277+
278+
:::
279+
177280
## Transforming Parameter Values
178281

179282
The `createRoute.parserParams` option allows you to convert `string` values passed as query strings to your desired types.

docs/guides/granite-router/plugin-router.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,16 @@ import { Route as _PageBRoute } from '../pages/page-b';
9797
import { Route as _PageCRoute } from '../pages/page-c';
9898

9999
declare module '@granite-js/react-native' {
100+
interface RegisterScreenInput {
101+
'/page-a': (typeof _PageARoute)['_inputType'];
102+
'/page-b': (typeof _PageBRoute)['_inputType'];
103+
'/page-c': (typeof _PageCRoute)['_inputType'];
104+
}
105+
100106
interface RegisterScreen {
101-
'/page-a': ReturnType<typeof _PageARoute.useParams>;
102-
'/page-b': ReturnType<typeof _PageBRoute.useParams>;
103-
'/page-c': ReturnType<typeof _PageCRoute.useParams>;
107+
'/page-a': (typeof _PageARoute)['_outputType'];
108+
'/page-b': (typeof _PageBRoute)['_outputType'];
109+
'/page-c': (typeof _PageCRoute)['_outputType'];
104110
}
105111
}
106112
```

docs/ko/guides/granite-router/params.md

Lines changed: 120 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -118,15 +118,10 @@ import * as v from 'valibot';
118118

119119
export const Route = createRoute('/', {
120120
component: Index,
121-
validateParams: (params) => {
122-
return v.parse(
123-
v.object({
124-
name: v.string(),
125-
age: v.number(),
126-
}),
127-
params
128-
);
129-
},
121+
validateParams: v.object({
122+
name: v.string(),
123+
age: v.number(),
124+
}),
130125
});
131126

132127
function Index() {
@@ -148,14 +143,10 @@ import { z } from 'zod';
148143

149144
export const Route = createRoute('/', {
150145
component: Index,
151-
validateParams: (params) => {
152-
return z
153-
.object({
154-
name: z.string(),
155-
age: z.number(),
156-
})
157-
.parse(params);
158-
},
146+
validateParams: z.object({
147+
name: z.string(),
148+
age: z.number(),
149+
}),
159150
});
160151

161152
function Index() {
@@ -174,6 +165,118 @@ function Index() {
174165

175166
</UsageSection>
176167

168+
## 고급 검증 기능
169+
170+
검증 라이브러리는 검증 에러를 처리하고 값을 변환하는 강력한 기능을 제공해요.
171+
172+
::: code-group
173+
174+
```tsx [valibot]
175+
import { createRoute } from '@granite-js/react-native';
176+
import * as v from 'valibot';
177+
178+
// v.fallback()으로 에러 안전성 확보
179+
export const Route = createRoute('/profile', {
180+
component: Profile,
181+
validateParams: v.object({
182+
name: v.fallback(v.string(), 'Anonymous'),
183+
age: v.fallback(v.number(), 0),
184+
isActive: v.fallback(v.boolean(), true),
185+
}),
186+
});
187+
```
188+
189+
```tsx [valibot - optional]
190+
import { createRoute } from '@granite-js/react-native';
191+
import * as v from 'valibot';
192+
193+
// v.optional()로 기본값 설정
194+
export const Route = createRoute('/settings', {
195+
component: Settings,
196+
validateParams: v.object({
197+
theme: v.optional(v.picklist(['light', 'dark']), 'light'),
198+
animation: v.optional(v.boolean(), true),
199+
fontSize: v.optional(v.number(), 16),
200+
}),
201+
});
202+
```
203+
204+
```tsx [valibot - transform]
205+
import { createRoute } from '@granite-js/react-native';
206+
import * as v from 'valibot';
207+
208+
// v.transform()으로 타입 변환
209+
export const Route = createRoute('/user', {
210+
component: User,
211+
validateParams: v.object({
212+
// 문자열을 숫자로 변환
213+
id: v.pipe(
214+
v.string(),
215+
v.transform((v) => parseInt(v))
216+
),
217+
// ISO 문자열을 Date 객체로 변환
218+
createdAt: v.pipe(
219+
v.string(),
220+
v.transform((v) => new Date(v))
221+
),
222+
}),
223+
});
224+
```
225+
226+
```tsx [zod]
227+
import { createRoute } from '@granite-js/react-native';
228+
import { z } from 'zod';
229+
230+
// .catch()로 에러 안전성 확보
231+
export const Route = createRoute('/profile', {
232+
component: Profile,
233+
validateParams: z.object({
234+
name: z.string().catch('Anonymous'),
235+
age: z.number().catch(0),
236+
isActive: z.boolean().catch(true),
237+
}),
238+
});
239+
```
240+
241+
```tsx [zod - default]
242+
import { createRoute } from '@granite-js/react-native';
243+
import { z } from 'zod';
244+
245+
// .default()로 기본값 설정
246+
export const Route = createRoute('/settings', {
247+
component: Settings,
248+
validateParams: z.object({
249+
theme: z.enum(['light', 'dark']).default('light'),
250+
animation: z.boolean().default(true),
251+
fontSize: z.number().default(16),
252+
}),
253+
});
254+
```
255+
256+
```tsx [zod - transform]
257+
import { createRoute } from '@granite-js/react-native';
258+
import { z } from 'zod';
259+
260+
// .transform()으로 타입 변환
261+
export const Route = createRoute('/user', {
262+
component: User,
263+
validateParams: z.object({
264+
// 문자열을 숫자로 변환
265+
id: z.string().transform((v) => parseInt(v)),
266+
// ISO 문자열을 Date 객체로 변환
267+
createdAt: z.string().transform((v) => new Date(v)),
268+
}),
269+
});
270+
271+
function User() {
272+
const params = Route.useParams();
273+
// params.id는 number 타입 (string이 아님)
274+
// params.createdAt은 Date 객체
275+
}
276+
```
277+
278+
:::
279+
177280
## 파라미터 값 변환하기
178281

179282
`createRoute.parserParams` 옵션을 사용하면 스트링으로 전달된 `string` 값을 원하는 타입으로 변환할 수 있어요.

docs/ko/guides/granite-router/plugin-router.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,16 @@ import { Route as _PageBRoute } from '../pages/page-b';
9797
import { Route as _PageCRoute } from '../pages/page-c';
9898

9999
declare module '@granite-js/react-native' {
100+
interface RegisterScreenInput {
101+
'/page-a': (typeof _PageARoute)['_inputType'];
102+
'/page-b': (typeof _PageBRoute)['_inputType'];
103+
'/page-c': (typeof _PageCRoute)['_inputType'];
104+
}
105+
100106
interface RegisterScreen {
101-
'/page-a': ReturnType<typeof _PageARoute.useParams>;
102-
'/page-b': ReturnType<typeof _PageBRoute.useParams>;
103-
'/page-c': ReturnType<typeof _PageCRoute.useParams>;
107+
'/page-a': (typeof _PageARoute)['_outputType'];
108+
'/page-b': (typeof _PageBRoute)['_outputType'];
109+
'/page-c': (typeof _PageCRoute)['_outputType'];
104110
}
105111
}
106112
```

packages/create-granite-app/templates/granite-app/src/router.gen.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@ import { Route as _AboutRoute } from '../pages/about';
44
import { Route as _IndexRoute } from '../pages/';
55

66
declare module '@granite-js/react-native' {
7+
interface RegisterScreenInput {
8+
'/about': (typeof _AboutRoute)['_inputType'];
9+
'/': (typeof _IndexRoute)['_inputType'];
10+
}
11+
712
interface RegisterScreen {
8-
'/about': ReturnType<typeof _AboutRoute.useParams>;
9-
'/': ReturnType<typeof _IndexRoute.useParams>;
13+
'/about': (typeof _AboutRoute)['_outputType'];
14+
'/': (typeof _IndexRoute)['_outputType'];
1015
}
1116
}

0 commit comments

Comments
 (0)