@granite-js/react-native 0.1.29 → 0.1.31
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/CHANGELOG.md +33 -0
- package/dist/router/createRoute.d.ts +29 -8
- package/dist/router/createRoute.test-d.d.ts +26 -0
- package/dist/router/types/RouteScreen.d.ts +6 -0
- package/dist/router/utils/standardSchema.d.ts +13 -0
- package/dist/router/utils/validateRouteParams.d.ts +10 -0
- package/package.json +12 -10
- package/src/router/createRoute.test-d.ts +96 -1
- package/src/router/createRoute.ts +64 -17
- package/src/router/hooks/useRouterControls.tsx +2 -2
- package/src/router/types/RouteScreen.ts +6 -0
- package/src/router/utils/screen.tsx +29 -0
- package/src/router/utils/standardSchema.ts +18 -0
- package/src/router/utils/validateRouteParams.ts +40 -0
- package/src/scroll-view-inertial-background/ScrollViewInertialBackground.tsx +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# @granite-js/react-native
|
|
2
2
|
|
|
3
|
+
## 0.1.31
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0ff9b13: feat(router): screenOptions in Route
|
|
8
|
+
- 37ae3f3: feat(router): supports standard schema
|
|
9
|
+
- Updated dependencies [0ff9b13]
|
|
10
|
+
- Updated dependencies [9bf8b50]
|
|
11
|
+
- Updated dependencies [e957833]
|
|
12
|
+
- Updated dependencies [37ae3f3]
|
|
13
|
+
- @granite-js/native@0.1.31
|
|
14
|
+
- @granite-js/plugin-core@0.1.31
|
|
15
|
+
- @granite-js/mpack@0.1.31
|
|
16
|
+
- @granite-js/image@0.1.31
|
|
17
|
+
- @granite-js/jest@0.1.31
|
|
18
|
+
- @granite-js/lottie@0.1.31
|
|
19
|
+
- @granite-js/style-utils@0.1.31
|
|
20
|
+
- @granite-js/cli@0.1.31
|
|
21
|
+
|
|
22
|
+
## 0.1.30
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- Updated dependencies [9e9ea71]
|
|
27
|
+
- @granite-js/mpack@0.1.30
|
|
28
|
+
- @granite-js/cli@0.1.30
|
|
29
|
+
- @granite-js/image@0.1.30
|
|
30
|
+
- @granite-js/jest@0.1.30
|
|
31
|
+
- @granite-js/lottie@0.1.30
|
|
32
|
+
- @granite-js/native@0.1.30
|
|
33
|
+
- @granite-js/plugin-core@0.1.30
|
|
34
|
+
- @granite-js/style-utils@0.1.30
|
|
35
|
+
|
|
3
36
|
## 0.1.29
|
|
4
37
|
|
|
5
38
|
### Patch Changes
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { type ParamListBase } from '@granite-js/native/@react-navigation/native';
|
|
2
|
-
import { NativeStackNavigationProp } from '@granite-js/native/@react-navigation/native-stack';
|
|
2
|
+
import { NativeStackNavigationOptions, NativeStackNavigationProp } from '@granite-js/native/@react-navigation/native-stack';
|
|
3
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
3
4
|
import { defaultParserParams } from './utils/defaultParserParams';
|
|
5
|
+
import { type InferOutput, type InferInput } from './utils/standardSchema';
|
|
4
6
|
export interface RouteOptions<T extends Readonly<object | undefined>> {
|
|
5
7
|
parserParams?: (params: Record<string, unknown>) => Record<string, unknown>;
|
|
6
|
-
validateParams?: (params: Readonly<object | undefined>) => T
|
|
8
|
+
validateParams?: ((params: Readonly<object | undefined>) => T) | StandardSchemaV1<any, T>;
|
|
7
9
|
component: React.FC<any>;
|
|
10
|
+
screenOptions?: NativeStackNavigationOptions | ((context: {
|
|
11
|
+
params: T;
|
|
12
|
+
}) => NativeStackNavigationOptions);
|
|
13
|
+
}
|
|
14
|
+
export interface RegisterScreenInput {
|
|
8
15
|
}
|
|
9
|
-
export type NavigationProps = NativeStackNavigationProp<keyof RegisterScreen extends never ? ParamListBase : RegisterScreen>;
|
|
10
16
|
export interface RegisterScreen {
|
|
11
17
|
}
|
|
18
|
+
export type NavigationProps = NativeStackNavigationProp<keyof RegisterScreenInput extends never ? ParamListBase : RegisterScreenInput>;
|
|
12
19
|
export declare function useNavigation(): NavigationProps;
|
|
13
20
|
export type RouteHooksOptions<TScreen extends keyof RegisterScreen> = {
|
|
14
21
|
from: TScreen;
|
|
@@ -17,13 +24,16 @@ export type RouteHooksOptions<TScreen extends keyof RegisterScreen> = {
|
|
|
17
24
|
strict: false;
|
|
18
25
|
from?: never;
|
|
19
26
|
};
|
|
20
|
-
export declare const routeMap: Map<
|
|
21
|
-
options: Omit<RouteOptions<any>, "component">;
|
|
27
|
+
export declare const routeMap: Map<keyof RegisterScreenInput, {
|
|
28
|
+
options: Omit<RouteOptions<any>, "component" | "screenOptions">;
|
|
22
29
|
component: React.FC<any>;
|
|
30
|
+
screenOptions?: NativeStackNavigationOptions | ((context: {
|
|
31
|
+
params: any;
|
|
32
|
+
}) => NativeStackNavigationOptions);
|
|
23
33
|
}>;
|
|
24
34
|
export declare function useMatchOptions<TScreen extends keyof RegisterScreen>(options: RouteHooksOptions<TScreen>): {
|
|
25
35
|
parserParams: ((params: Record<string, unknown>) => Record<string, unknown>) | typeof defaultParserParams;
|
|
26
|
-
validateParams?: ((params: Readonly<object | undefined>) => any) | undefined;
|
|
36
|
+
validateParams?: StandardSchemaV1<any, any> | ((params: Readonly<object | undefined>) => any) | undefined;
|
|
27
37
|
} | null;
|
|
28
38
|
export declare function useParams<TScreen extends keyof RegisterScreen>(options: {
|
|
29
39
|
from: TScreen;
|
|
@@ -32,8 +42,19 @@ export declare function useParams<TScreen extends keyof RegisterScreen>(options:
|
|
|
32
42
|
export declare function useParams(options: {
|
|
33
43
|
strict: false;
|
|
34
44
|
}): Readonly<object | undefined>;
|
|
35
|
-
export declare
|
|
36
|
-
|
|
45
|
+
export declare function createRoute<TSchema extends StandardSchemaV1<any, any>>(path: keyof RegisterScreenInput, options: Omit<RouteOptions<any>, 'validateParams'> & {
|
|
46
|
+
validateParams: TSchema;
|
|
47
|
+
}): {
|
|
48
|
+
_path: keyof RegisterScreenInput;
|
|
49
|
+
useNavigation: typeof useNavigation;
|
|
50
|
+
useParams: () => InferOutput<TSchema>;
|
|
51
|
+
_inputType?: InferInput<TSchema>;
|
|
52
|
+
_outputType?: InferOutput<TSchema>;
|
|
53
|
+
};
|
|
54
|
+
export declare function createRoute<T extends Readonly<object | undefined>>(path: keyof RegisterScreenInput, options: RouteOptions<T>): {
|
|
55
|
+
_path: keyof RegisterScreenInput;
|
|
37
56
|
useNavigation: typeof useNavigation;
|
|
38
57
|
useParams: () => T;
|
|
58
|
+
_inputType?: T;
|
|
59
|
+
_outputType?: T;
|
|
39
60
|
};
|
|
@@ -1,9 +1,35 @@
|
|
|
1
1
|
declare module './createRoute' {
|
|
2
|
+
interface RegisterScreenInput {
|
|
3
|
+
'/test': {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
};
|
|
7
|
+
'/test-schema': {
|
|
8
|
+
id: string;
|
|
9
|
+
count: number;
|
|
10
|
+
};
|
|
11
|
+
'/test-transform': {
|
|
12
|
+
id: string;
|
|
13
|
+
};
|
|
14
|
+
'/test-with-defaults': {
|
|
15
|
+
animation?: boolean;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
2
18
|
interface RegisterScreen {
|
|
3
19
|
'/test': {
|
|
4
20
|
id: string;
|
|
5
21
|
name: string;
|
|
6
22
|
};
|
|
23
|
+
'/test-schema': {
|
|
24
|
+
id: string;
|
|
25
|
+
count: number;
|
|
26
|
+
};
|
|
27
|
+
'/test-transform': {
|
|
28
|
+
id: number;
|
|
29
|
+
};
|
|
30
|
+
'/test-with-defaults': {
|
|
31
|
+
animation: boolean;
|
|
32
|
+
};
|
|
7
33
|
}
|
|
8
34
|
}
|
|
9
35
|
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { NativeStackNavigationOptions } from '@granite-js/native/@react-navigation/native-stack';
|
|
1
2
|
import { Screen } from './Screen';
|
|
2
3
|
/**
|
|
3
4
|
* @name RouteScreen
|
|
@@ -13,4 +14,9 @@ export interface RouteScreen {
|
|
|
13
14
|
* @description Screen component
|
|
14
15
|
*/
|
|
15
16
|
component: Screen;
|
|
17
|
+
/**
|
|
18
|
+
* @name screenOptions
|
|
19
|
+
* @description Screen options for React Navigation (can be static or a function that receives route params)
|
|
20
|
+
*/
|
|
21
|
+
screenOptions?: NativeStackNavigationOptions;
|
|
16
22
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
/**
|
|
3
|
+
* Type guard to check if a value is a StandardSchema v1 compliant schema
|
|
4
|
+
*/
|
|
5
|
+
export declare function isStandardSchema(value: unknown): value is StandardSchemaV1;
|
|
6
|
+
/**
|
|
7
|
+
* Infer the output type from a StandardSchema
|
|
8
|
+
*/
|
|
9
|
+
export type InferOutput<TSchema> = TSchema extends StandardSchemaV1<any, infer Output> ? Output : never;
|
|
10
|
+
/**
|
|
11
|
+
* Infer the input type from a StandardSchema
|
|
12
|
+
*/
|
|
13
|
+
export type InferInput<TSchema> = TSchema extends StandardSchemaV1<infer Input, any> ? Input : never;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
/**
|
|
3
|
+
* Validates route parameters using either a StandardSchema or a validation function.
|
|
4
|
+
*
|
|
5
|
+
* @param validateParams - The validation schema or function to apply
|
|
6
|
+
* @param parsedParams - The parsed parameters to validate
|
|
7
|
+
* @returns The validated parameters
|
|
8
|
+
* @throws Error if validation fails or async validation is attempted
|
|
9
|
+
*/
|
|
10
|
+
export declare function validateRouteParams(validateParams: ((params: Readonly<object | undefined>) => any) | StandardSchemaV1<any, any>, parsedParams: Readonly<object | undefined>): any;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@granite-js/react-native",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"description": "The Granite Framework",
|
|
5
5
|
"bin": {
|
|
6
6
|
"granite": "./bin/cli.js"
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"@babel/core": "^7.24.9",
|
|
92
92
|
"@babel/preset-env": "^7.24.8",
|
|
93
93
|
"@babel/preset-typescript": "^7.24.7",
|
|
94
|
-
"@granite-js/native": "0.1.
|
|
94
|
+
"@granite-js/native": "0.1.31",
|
|
95
95
|
"@testing-library/dom": "^10.4.0",
|
|
96
96
|
"@testing-library/react": "^16.1.0",
|
|
97
97
|
"@types/babel__core": "^7",
|
|
@@ -108,7 +108,8 @@
|
|
|
108
108
|
"react-native": "0.72.6",
|
|
109
109
|
"tsup": "^8.5.0",
|
|
110
110
|
"typescript": "5.8.3",
|
|
111
|
-
"vitest": "^2.1.8"
|
|
111
|
+
"vitest": "^2.1.8",
|
|
112
|
+
"zod": "^4.1.12"
|
|
112
113
|
},
|
|
113
114
|
"peerDependencies": {
|
|
114
115
|
"@granite-js/native": "*",
|
|
@@ -117,13 +118,14 @@
|
|
|
117
118
|
"react-native": "*"
|
|
118
119
|
},
|
|
119
120
|
"dependencies": {
|
|
120
|
-
"@granite-js/cli": "0.1.
|
|
121
|
-
"@granite-js/image": "0.1.
|
|
122
|
-
"@granite-js/jest": "0.1.
|
|
123
|
-
"@granite-js/lottie": "0.1.
|
|
124
|
-
"@granite-js/mpack": "0.1.
|
|
125
|
-
"@granite-js/plugin-core": "0.1.
|
|
126
|
-
"@granite-js/style-utils": "0.1.
|
|
121
|
+
"@granite-js/cli": "0.1.31",
|
|
122
|
+
"@granite-js/image": "0.1.31",
|
|
123
|
+
"@granite-js/jest": "0.1.31",
|
|
124
|
+
"@granite-js/lottie": "0.1.31",
|
|
125
|
+
"@granite-js/mpack": "0.1.31",
|
|
126
|
+
"@granite-js/plugin-core": "0.1.31",
|
|
127
|
+
"@granite-js/style-utils": "0.1.31",
|
|
128
|
+
"@standard-schema/spec": "^1.0.0",
|
|
127
129
|
"es-toolkit": "^1.39.8",
|
|
128
130
|
"react-native-url-polyfill": "3.0.0"
|
|
129
131
|
},
|
|
@@ -1,12 +1,40 @@
|
|
|
1
1
|
import { assertType, describe, it } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { createRoute, useNavigation, useParams } from './createRoute';
|
|
3
4
|
|
|
4
5
|
declare module './createRoute' {
|
|
6
|
+
interface RegisterScreenInput {
|
|
7
|
+
'/test': {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
};
|
|
11
|
+
'/test-schema': {
|
|
12
|
+
id: string;
|
|
13
|
+
count: number;
|
|
14
|
+
};
|
|
15
|
+
'/test-transform': {
|
|
16
|
+
id: string;
|
|
17
|
+
};
|
|
18
|
+
'/test-with-defaults': {
|
|
19
|
+
animation?: boolean;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
5
23
|
interface RegisterScreen {
|
|
6
24
|
'/test': {
|
|
7
25
|
id: string;
|
|
8
26
|
name: string;
|
|
9
27
|
};
|
|
28
|
+
'/test-schema': {
|
|
29
|
+
id: string;
|
|
30
|
+
count: number;
|
|
31
|
+
};
|
|
32
|
+
'/test-transform': {
|
|
33
|
+
id: number;
|
|
34
|
+
};
|
|
35
|
+
'/test-with-defaults': {
|
|
36
|
+
animation: boolean;
|
|
37
|
+
};
|
|
10
38
|
}
|
|
11
39
|
}
|
|
12
40
|
|
|
@@ -50,3 +78,70 @@ describe('createRoute', () => {
|
|
|
50
78
|
assertType(useParams({ from: '/test', strict: false }));
|
|
51
79
|
});
|
|
52
80
|
});
|
|
81
|
+
|
|
82
|
+
describe('createRoute with StandardSchema', () => {
|
|
83
|
+
it('should infer correct type from StandardSchema', () => {
|
|
84
|
+
const RouteWithSchema = createRoute('/test-schema', {
|
|
85
|
+
component: () => null,
|
|
86
|
+
validateParams: z.object({
|
|
87
|
+
id: z.string(),
|
|
88
|
+
count: z.number(),
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
assertType<{
|
|
93
|
+
id: string;
|
|
94
|
+
count: number;
|
|
95
|
+
}>(RouteWithSchema.useParams());
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should infer output type from transformation', () => {
|
|
99
|
+
const RouteWithTransform = createRoute('/test-transform', {
|
|
100
|
+
component: () => null,
|
|
101
|
+
validateParams: z.object({
|
|
102
|
+
id: z.string().transform((v) => parseInt(v)),
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Should be number (output), not string (input)
|
|
107
|
+
assertType<{
|
|
108
|
+
id: number;
|
|
109
|
+
}>(RouteWithTransform.useParams());
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should work with useParams hook', () => {
|
|
113
|
+
createRoute('/test-schema', {
|
|
114
|
+
component: () => null,
|
|
115
|
+
validateParams: z.object({
|
|
116
|
+
id: z.string(),
|
|
117
|
+
count: z.number(),
|
|
118
|
+
}),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
assertType<{
|
|
122
|
+
id: string;
|
|
123
|
+
count: number;
|
|
124
|
+
}>(useParams({ from: '/test-schema' }));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should separate input and output types with defaults', () => {
|
|
128
|
+
const RouteWithDefaults = createRoute('/test-with-defaults', {
|
|
129
|
+
component: () => null,
|
|
130
|
+
validateParams: z.object({
|
|
131
|
+
animation: z.boolean().default(true),
|
|
132
|
+
}),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// useParams returns output type (required)
|
|
136
|
+
assertType<{ animation: boolean }>(RouteWithDefaults.useParams());
|
|
137
|
+
|
|
138
|
+
// navigation.navigate should accept input type (optional)
|
|
139
|
+
const navigation = useNavigation();
|
|
140
|
+
|
|
141
|
+
// Should accept with animation parameter
|
|
142
|
+
navigation.navigate('/test-with-defaults', { animation: false });
|
|
143
|
+
|
|
144
|
+
// Should accept without animation parameter (default will be used)
|
|
145
|
+
navigation.navigate('/test-with-defaults', {});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -3,25 +3,35 @@ import {
|
|
|
3
3
|
useNavigation as useNavigationNative,
|
|
4
4
|
useRoute,
|
|
5
5
|
} from '@granite-js/native/@react-navigation/native';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
NativeStackNavigationOptions,
|
|
8
|
+
NativeStackNavigationProp,
|
|
9
|
+
} from '@granite-js/native/@react-navigation/native-stack';
|
|
10
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
7
11
|
import { useMemo } from 'react';
|
|
8
12
|
import { RESERVED_PATHS } from './constants';
|
|
9
13
|
import { defaultParserParams } from './utils/defaultParserParams';
|
|
14
|
+
import { type InferOutput, type InferInput } from './utils/standardSchema';
|
|
15
|
+
import { validateRouteParams } from './utils/validateRouteParams';
|
|
10
16
|
|
|
11
17
|
export interface RouteOptions<T extends Readonly<object | undefined>> {
|
|
12
18
|
parserParams?: (params: Record<string, unknown>) => Record<string, unknown>;
|
|
13
|
-
validateParams?: (params: Readonly<object | undefined>) => T
|
|
19
|
+
validateParams?: ((params: Readonly<object | undefined>) => T) | StandardSchemaV1<any, T>;
|
|
14
20
|
component: React.FC<any>;
|
|
21
|
+
screenOptions?: NativeStackNavigationOptions | ((context: { params: T }) => NativeStackNavigationOptions);
|
|
15
22
|
}
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
keyof RegisterScreen extends never ? ParamListBase : RegisterScreen
|
|
20
|
-
>;
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
25
|
+
export interface RegisterScreenInput {}
|
|
21
26
|
|
|
22
27
|
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
23
28
|
export interface RegisterScreen {}
|
|
24
29
|
|
|
30
|
+
export type NavigationProps = NativeStackNavigationProp<
|
|
31
|
+
// @ts-expect-error - override type
|
|
32
|
+
keyof RegisterScreenInput extends never ? ParamListBase : RegisterScreenInput
|
|
33
|
+
>;
|
|
34
|
+
|
|
25
35
|
export function useNavigation() {
|
|
26
36
|
return useNavigationNative<NavigationProps>();
|
|
27
37
|
}
|
|
@@ -37,8 +47,12 @@ export type RouteHooksOptions<TScreen extends keyof RegisterScreen> =
|
|
|
37
47
|
};
|
|
38
48
|
|
|
39
49
|
export const routeMap = new Map<
|
|
40
|
-
keyof
|
|
41
|
-
{
|
|
50
|
+
keyof RegisterScreenInput,
|
|
51
|
+
{
|
|
52
|
+
options: Omit<RouteOptions<any>, 'component' | 'screenOptions'>;
|
|
53
|
+
component: React.FC<any>;
|
|
54
|
+
screenOptions?: NativeStackNavigationOptions | ((context: { params: any }) => NativeStackNavigationOptions);
|
|
55
|
+
}
|
|
42
56
|
>();
|
|
43
57
|
|
|
44
58
|
export function useMatchOptions<TScreen extends keyof RegisterScreen>(options: RouteHooksOptions<TScreen>) {
|
|
@@ -140,23 +154,56 @@ export function useParams<TScreen extends keyof RegisterScreen>(
|
|
|
140
154
|
}
|
|
141
155
|
|
|
142
156
|
const parsedParams = routeOptions.parserParams(route.params as Record<string, string>);
|
|
143
|
-
|
|
157
|
+
|
|
158
|
+
if (!isStrict || !routeOptions.validateParams) {
|
|
159
|
+
return parsedParams;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return validateRouteParams(routeOptions.validateParams, parsedParams);
|
|
144
163
|
}, [routeOptions, route.params, isStrict]);
|
|
145
164
|
|
|
146
165
|
return params;
|
|
147
166
|
}
|
|
148
167
|
|
|
149
|
-
|
|
150
|
-
|
|
168
|
+
// Overload 1: StandardSchema pattern
|
|
169
|
+
export function createRoute<TSchema extends StandardSchemaV1<any, any>>(
|
|
170
|
+
path: keyof RegisterScreenInput,
|
|
171
|
+
options: Omit<RouteOptions<any>, 'validateParams'> & {
|
|
172
|
+
validateParams: TSchema;
|
|
173
|
+
}
|
|
174
|
+
): {
|
|
175
|
+
_path: keyof RegisterScreenInput;
|
|
176
|
+
useNavigation: typeof useNavigation;
|
|
177
|
+
useParams: () => InferOutput<TSchema>;
|
|
178
|
+
_inputType?: InferInput<TSchema>;
|
|
179
|
+
_outputType?: InferOutput<TSchema>;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Overload 2: Function pattern
|
|
183
|
+
export function createRoute<T extends Readonly<object | undefined>>(
|
|
184
|
+
path: keyof RegisterScreenInput,
|
|
151
185
|
options: RouteOptions<T>
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
|
|
186
|
+
): {
|
|
187
|
+
_path: keyof RegisterScreenInput;
|
|
188
|
+
useNavigation: typeof useNavigation;
|
|
189
|
+
useParams: () => T;
|
|
190
|
+
_inputType?: T;
|
|
191
|
+
_outputType?: T;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Implementation
|
|
195
|
+
export function createRoute(path: keyof RegisterScreenInput, options: RouteOptions<any>) {
|
|
196
|
+
const { component, screenOptions, ...restOptions } = options;
|
|
197
|
+
routeMap.set(path, {
|
|
198
|
+
options: restOptions,
|
|
199
|
+
component,
|
|
200
|
+
screenOptions: screenOptions,
|
|
201
|
+
});
|
|
155
202
|
|
|
156
|
-
const _path = path as keyof
|
|
203
|
+
const _path = path as keyof RegisterScreenInput;
|
|
157
204
|
return {
|
|
158
205
|
_path,
|
|
159
206
|
useNavigation,
|
|
160
|
-
useParams: () => useParams({ from: _path, strict: true })
|
|
207
|
+
useParams: () => useParams({ from: _path as keyof RegisterScreen, strict: true }),
|
|
161
208
|
};
|
|
162
|
-
}
|
|
209
|
+
}
|
|
@@ -50,9 +50,9 @@ export function useRouterControls({
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
const routePath = routeScreen.path;
|
|
53
|
-
const
|
|
53
|
+
const options = routeScreen.screenOptions ?? {};
|
|
54
54
|
|
|
55
|
-
return <StackNavigator.Screen key={routePath} name={routePath} component={Component} options={
|
|
55
|
+
return <StackNavigator.Screen key={routePath} name={routePath} component={Component} options={options} />;
|
|
56
56
|
});
|
|
57
57
|
}, [registerScreens, layoutScreenMap, ScreenContainer]);
|
|
58
58
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { NativeStackNavigationOptions } from '@granite-js/native/@react-navigation/native-stack';
|
|
1
2
|
import { Screen } from './Screen';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -14,4 +15,9 @@ export interface RouteScreen {
|
|
|
14
15
|
* @description Screen component
|
|
15
16
|
*/
|
|
16
17
|
component: Screen;
|
|
18
|
+
/**
|
|
19
|
+
* @name screenOptions
|
|
20
|
+
* @description Screen options for React Navigation (can be static or a function that receives route params)
|
|
21
|
+
*/
|
|
22
|
+
screenOptions?: NativeStackNavigationOptions;
|
|
17
23
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getRoutePath } from './path';
|
|
2
2
|
import { routeMap } from '../createRoute';
|
|
3
3
|
import { RequireContext, RouteScreen } from '../types';
|
|
4
|
+
import { validateRouteParams } from './validateRouteParams';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @kind function
|
|
@@ -33,9 +34,37 @@ export function getRouteScreens(context: RequireContext): RouteScreen[] {
|
|
|
33
34
|
throw new Error(`Page component not found in ${key}.`);
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
// Retrieve route configuration from routeMap
|
|
38
|
+
const routeMapEntry = routeMap.get(context(key)?.Route?._path);
|
|
39
|
+
|
|
40
|
+
// Get screenOptions from routeMap or component (routeMap takes priority)
|
|
41
|
+
const rawScreenOptions = routeMapEntry?.screenOptions ?? component.screenOptions;
|
|
42
|
+
|
|
43
|
+
// If screenOptions is a function, wrap it to apply parserParams and validateParams
|
|
44
|
+
const screenOptions =
|
|
45
|
+
typeof rawScreenOptions === 'function'
|
|
46
|
+
? ({ route }: { route: { params?: Record<string, unknown> }; navigation: unknown }) => {
|
|
47
|
+
const rawParams = route.params ?? {};
|
|
48
|
+
const parserParams = routeMapEntry?.options?.parserParams;
|
|
49
|
+
const validateParams = routeMapEntry?.options?.validateParams;
|
|
50
|
+
|
|
51
|
+
// Apply parserParams if available
|
|
52
|
+
const parsedParams = parserParams ? parserParams(rawParams) : rawParams;
|
|
53
|
+
|
|
54
|
+
// Apply validateParams if available
|
|
55
|
+
try {
|
|
56
|
+
const validatedParams = validateParams ? validateRouteParams(validateParams, parsedParams) : parsedParams;
|
|
57
|
+
return rawScreenOptions({ params: validatedParams });
|
|
58
|
+
} catch {
|
|
59
|
+
return rawScreenOptions({ params: { _error: true } });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
: rawScreenOptions;
|
|
63
|
+
|
|
36
64
|
return {
|
|
37
65
|
path,
|
|
38
66
|
component,
|
|
67
|
+
screenOptions,
|
|
39
68
|
};
|
|
40
69
|
});
|
|
41
70
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type guard to check if a value is a StandardSchema v1 compliant schema
|
|
5
|
+
*/
|
|
6
|
+
export function isStandardSchema(value: unknown): value is StandardSchemaV1 {
|
|
7
|
+
return typeof value === 'object' && value !== null && '~standard' in value;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Infer the output type from a StandardSchema
|
|
12
|
+
*/
|
|
13
|
+
export type InferOutput<TSchema> = TSchema extends StandardSchemaV1<any, infer Output> ? Output : never;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Infer the input type from a StandardSchema
|
|
17
|
+
*/
|
|
18
|
+
export type InferInput<TSchema> = TSchema extends StandardSchemaV1<infer Input, any> ? Input : never;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
+
import { isStandardSchema } from './standardSchema';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validates route parameters using either a StandardSchema or a validation function.
|
|
6
|
+
*
|
|
7
|
+
* @param validateParams - The validation schema or function to apply
|
|
8
|
+
* @param parsedParams - The parsed parameters to validate
|
|
9
|
+
* @returns The validated parameters
|
|
10
|
+
* @throws Error if validation fails or async validation is attempted
|
|
11
|
+
*/
|
|
12
|
+
export function validateRouteParams(
|
|
13
|
+
validateParams: ((params: Readonly<object | undefined>) => any) | StandardSchemaV1<any, any>,
|
|
14
|
+
parsedParams: Readonly<object | undefined>
|
|
15
|
+
): any {
|
|
16
|
+
// Check if validateParams is a StandardSchema
|
|
17
|
+
if (isStandardSchema(validateParams)) {
|
|
18
|
+
const result = validateParams['~standard'].validate(parsedParams);
|
|
19
|
+
|
|
20
|
+
// Handle async results
|
|
21
|
+
if (result instanceof Promise) {
|
|
22
|
+
throw new Error('Async validation is not supported');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Handle validation failures
|
|
26
|
+
if ('issues' in result && result.issues) {
|
|
27
|
+
const messages = result.issues.map((i) => i.message).join(', ');
|
|
28
|
+
throw new Error(`Parameter validation failed: ${messages}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return result.value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Function pattern
|
|
35
|
+
if (typeof validateParams === 'function') {
|
|
36
|
+
return validateParams(parsedParams);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return parsedParams;
|
|
40
|
+
}
|
|
@@ -96,4 +96,4 @@ function HiddenView({ style, pointerEvents = 'none', ...props }: ComponentProps<
|
|
|
96
96
|
* Gradient value for BottomCTA. Set as a fixed value to avoid peerDeps.
|
|
97
97
|
*/
|
|
98
98
|
const GRADIENT_HEIGHT = 37;
|
|
99
|
-
const DEFAULT_BACKGROUND_COLOR = '
|
|
99
|
+
const DEFAULT_BACKGROUND_COLOR = 'transparent';
|