@r0hitsharma/router-kit 0.12.0-rohit-fork-ci.1 → 0.12.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/DESIGN.md +22 -0
- package/README.md +4 -0
- package/dist/search-params.d.ts +3 -3
- package/dist/search-params.js +3 -3
- package/package.json +3 -3
- package/src/search-params.test.ts +29 -0
- package/src/search-params.ts +13 -7
- package/src/search-params.types.test.ts +17 -0
- package/src/test-fixtures.ts +22 -0
package/DESIGN.md
CHANGED
|
@@ -91,6 +91,28 @@ shape — and when they are not, the failure reads as an inscrutable variance er
|
|
|
91
91
|
at the route definition rather than as a duplicate dependency. A peer makes it
|
|
92
92
|
one install and the question does not arise.
|
|
93
93
|
|
|
94
|
+
#### Built on `zod/mini`, composable into either entry point
|
|
95
|
+
|
|
96
|
+
The builders import from `zod/mini`, not from `zod`. Both entry points are thin
|
|
97
|
+
layers over the same `zod/v4/core` schema model, so a mini schema nests in a
|
|
98
|
+
classic `z.object` and the reverse, at runtime and in `z.infer`. What differs is
|
|
99
|
+
bundle cost: a classic schema's constructor attaches the whole method table
|
|
100
|
+
(`.email`, `.toJSONSchema`, and the rest), so constructing even one pulls most
|
|
101
|
+
of classic zod into the app. Measured on a real app's search schemas, minified:
|
|
102
|
+
about 57 kB (17 kB gzip) on classic against 14 kB (5 kB gzip) on mini.
|
|
103
|
+
|
|
104
|
+
That saving only exists if *every* schema in the bundle is mini, which is why
|
|
105
|
+
the call is made here: an app that moved its own schemas to mini while these
|
|
106
|
+
builders stayed classic would still ship about 49 kB. An app still on classic
|
|
107
|
+
pays about 1 kB for the mini wrappers alongside.
|
|
108
|
+
|
|
109
|
+
The cost is that the returned schemas carry mini's surface, not classic's:
|
|
110
|
+
`textParam().default(...)` or `.describe(...)` is not available. Wrap them the
|
|
111
|
+
mini way (`z._default(textParam(), ...)`), or build a param of your own on
|
|
112
|
+
`toSearchText` under the rules at the foot of `search-params.ts`.
|
|
113
|
+
|
|
114
|
+
`zod/mini` has been exported since zod 4.0.0, so the peer floor does not move.
|
|
115
|
+
|
|
94
116
|
### `@r0hitsharma/design-system` — not a dependency at all
|
|
95
117
|
|
|
96
118
|
Not a dependency, not an optional peer. `UrlSyncedTableStateAdapter` is restated
|
package/README.md
CHANGED
|
@@ -85,6 +85,10 @@ export const sharedSearchSchema = z.object({
|
|
|
85
85
|
value only if it is in the set — pass the set `as const` so the inferred type is
|
|
86
86
|
the literal union rather than `string`.
|
|
87
87
|
|
|
88
|
+
The builders are built on `zod/mini` and compose into an object from either
|
|
89
|
+
`zod` or `zod/mini`. If nothing else in your app builds a classic schema,
|
|
90
|
+
classic zod drops out of the bundle; see [DESIGN.md](./DESIGN.md).
|
|
91
|
+
|
|
88
92
|
Every builder here is **total** and **idempotent**:
|
|
89
93
|
|
|
90
94
|
- **Total** — no input fails, so `validateSearch` never rejects a URL. A
|
package/dist/search-params.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
1
|
+
import { z } from 'zod/mini';
|
|
2
2
|
/**
|
|
3
3
|
* Collapses a raw URL search value to the text a param means, or `undefined`
|
|
4
4
|
* when it means nothing.
|
|
@@ -59,7 +59,7 @@ export declare function toSearchOption<T extends string>(value: unknown, allowed
|
|
|
59
59
|
* Schema shape returned by {@link textParam}. Named so the built `.d.ts` states
|
|
60
60
|
* the param's output type instead of inlining zod's internal generics.
|
|
61
61
|
*/
|
|
62
|
-
export type SearchTextParam = z.
|
|
62
|
+
export type SearchTextParam = z.ZodMiniOptional<z.ZodMiniPipe<z.ZodMiniUnknown, z.ZodMiniTransform<string | undefined, unknown>>>;
|
|
63
63
|
/**
|
|
64
64
|
* A free-text search param: trimmed, with empty and unusable values degrading
|
|
65
65
|
* to absent. Use for identifiers, query strings, and timestamps — anything
|
|
@@ -73,7 +73,7 @@ export declare function textParam(): SearchTextParam;
|
|
|
73
73
|
* Schema shape returned by {@link oneOfParam}, carrying the closed option set
|
|
74
74
|
* through to the inferred search type.
|
|
75
75
|
*/
|
|
76
|
-
export type SearchOptionParam<T extends string> = z.
|
|
76
|
+
export type SearchOptionParam<T extends string> = z.ZodMiniOptional<z.ZodMiniPipe<z.ZodMiniUnknown, z.ZodMiniTransform<T | undefined, unknown>>>;
|
|
77
77
|
/**
|
|
78
78
|
* A closed-set param: one of `allowed`, or absent. Use for tabs, modes, sort
|
|
79
79
|
* directions, and flags whose spellings are fixed — a hand-edited or stale
|
package/dist/search-params.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
1
|
+
import { z } from 'zod/mini';
|
|
2
2
|
/**
|
|
3
3
|
* Collapses a raw URL search value to the text a param means, or `undefined`
|
|
4
4
|
* when it means nothing.
|
|
@@ -85,7 +85,7 @@ export function toSearchOption(value, allowed) {
|
|
|
85
85
|
* builders below.
|
|
86
86
|
*/
|
|
87
87
|
export function textParam() {
|
|
88
|
-
return z.optional(z.unknown().transform(toSearchText));
|
|
88
|
+
return z.optional(z.pipe(z.unknown(), z.transform(toSearchText)));
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
91
|
* A closed-set param: one of `allowed`, or absent. Use for tabs, modes, sort
|
|
@@ -97,7 +97,7 @@ export function textParam() {
|
|
|
97
97
|
* search type is the literal union rather than `string`.
|
|
98
98
|
*/
|
|
99
99
|
export function oneOfParam(allowed) {
|
|
100
|
-
return z.optional(z.unknown().transform((value) => toSearchOption(value, allowed)));
|
|
100
|
+
return z.optional(z.pipe(z.unknown(), z.transform((value) => toSearchOption(value, allowed))));
|
|
101
101
|
}
|
|
102
102
|
/*
|
|
103
103
|
* ## Writing a param of your own
|
package/package.json
CHANGED
|
@@ -42,12 +42,12 @@
|
|
|
42
42
|
"@r0hitsharma/oxlint-config": "*",
|
|
43
43
|
"@r0hitsharma/tsconfig": "*",
|
|
44
44
|
"@tanstack/react-router": "1.170.33",
|
|
45
|
-
"oxfmt": "0.
|
|
46
|
-
"oxlint": "1.
|
|
45
|
+
"oxfmt": "0.68.0",
|
|
46
|
+
"oxlint": "1.83.0",
|
|
47
47
|
"vitest": "^4.1.9",
|
|
48
48
|
"zod": "4.5.4"
|
|
49
49
|
},
|
|
50
|
-
"version": "0.12.0
|
|
50
|
+
"version": "0.12.0",
|
|
51
51
|
"repository": {
|
|
52
52
|
"url": "https://github.com/r0hitsharma/uikit"
|
|
53
53
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { z as classic } from 'zod';
|
|
3
|
+
import { z as mini } from 'zod/mini';
|
|
2
4
|
|
|
3
5
|
import {
|
|
4
6
|
oneOfParam,
|
|
@@ -7,6 +9,7 @@ import {
|
|
|
7
9
|
toSearchText,
|
|
8
10
|
} from './search-params.js';
|
|
9
11
|
import {
|
|
12
|
+
createMiniRouter,
|
|
10
13
|
jsonParseSearch,
|
|
11
14
|
jsonStringifySearch,
|
|
12
15
|
parseSearch,
|
|
@@ -14,6 +17,7 @@ import {
|
|
|
14
17
|
stringifySearch,
|
|
15
18
|
TABS,
|
|
16
19
|
} from './test-fixtures.js';
|
|
20
|
+
import { settleEntryUrl } from './testing.js';
|
|
17
21
|
|
|
18
22
|
/**
|
|
19
23
|
* Renders a normalized value into a query string and decodes it back through
|
|
@@ -173,3 +177,28 @@ describe('oneOfParam', () => {
|
|
|
173
177
|
expect(oneOfParam(TABS).parse('overview')).toBe('overview');
|
|
174
178
|
});
|
|
175
179
|
});
|
|
180
|
+
|
|
181
|
+
describe('composing into either zod entry point', () => {
|
|
182
|
+
// The builders are built on `zod/mini` so an app on mini does not pull the
|
|
183
|
+
// classic API back in, and an app on classic `zod` still composes them into
|
|
184
|
+
// its own `z.object`. Both share `zod/v4/core`, so it is one schema model.
|
|
185
|
+
const shape = { q: textParam(), tab: oneOfParam(TABS) };
|
|
186
|
+
const raw = { q: ' hello ', tab: 'archive', stray: 1 };
|
|
187
|
+
|
|
188
|
+
it('applies the same rule under a classic and a mini object', () => {
|
|
189
|
+
const expected = { q: 'hello', tab: undefined };
|
|
190
|
+
|
|
191
|
+
expect(classic.object(shape).parse(raw)).toEqual(expected);
|
|
192
|
+
expect(mini.object(shape).parse(raw)).toEqual(expected);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('settles an entry URL through a route whose schema is a mini object', async () => {
|
|
196
|
+
const settled = await settleEntryUrl(
|
|
197
|
+
createMiniRouter().options,
|
|
198
|
+
'/plain?q=%20hi%20&tab=archive',
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
expect(settled.url).toBe('/plain?q=hi');
|
|
202
|
+
expect(settled.result.search).toEqual({ q: 'hi' });
|
|
203
|
+
});
|
|
204
|
+
});
|
package/src/search-params.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
1
|
+
import { z } from 'zod/mini';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Collapses a raw URL search value to the text a param means, or `undefined`
|
|
@@ -90,8 +90,11 @@ export function toSearchOption<T extends string>(
|
|
|
90
90
|
* Schema shape returned by {@link textParam}. Named so the built `.d.ts` states
|
|
91
91
|
* the param's output type instead of inlining zod's internal generics.
|
|
92
92
|
*/
|
|
93
|
-
export type SearchTextParam = z.
|
|
94
|
-
z.
|
|
93
|
+
export type SearchTextParam = z.ZodMiniOptional<
|
|
94
|
+
z.ZodMiniPipe<
|
|
95
|
+
z.ZodMiniUnknown,
|
|
96
|
+
z.ZodMiniTransform<string | undefined, unknown>
|
|
97
|
+
>
|
|
95
98
|
>;
|
|
96
99
|
|
|
97
100
|
/**
|
|
@@ -103,15 +106,15 @@ export type SearchTextParam = z.ZodOptional<
|
|
|
103
106
|
* builders below.
|
|
104
107
|
*/
|
|
105
108
|
export function textParam(): SearchTextParam {
|
|
106
|
-
return z.optional(z.unknown().transform(toSearchText));
|
|
109
|
+
return z.optional(z.pipe(z.unknown(), z.transform(toSearchText)));
|
|
107
110
|
}
|
|
108
111
|
|
|
109
112
|
/**
|
|
110
113
|
* Schema shape returned by {@link oneOfParam}, carrying the closed option set
|
|
111
114
|
* through to the inferred search type.
|
|
112
115
|
*/
|
|
113
|
-
export type SearchOptionParam<T extends string> = z.
|
|
114
|
-
z.
|
|
116
|
+
export type SearchOptionParam<T extends string> = z.ZodMiniOptional<
|
|
117
|
+
z.ZodMiniPipe<z.ZodMiniUnknown, z.ZodMiniTransform<T | undefined, unknown>>
|
|
115
118
|
>;
|
|
116
119
|
|
|
117
120
|
/**
|
|
@@ -127,7 +130,10 @@ export function oneOfParam<T extends string>(
|
|
|
127
130
|
allowed: readonly T[],
|
|
128
131
|
): SearchOptionParam<T> {
|
|
129
132
|
return z.optional(
|
|
130
|
-
z.
|
|
133
|
+
z.pipe(
|
|
134
|
+
z.unknown(),
|
|
135
|
+
z.transform((value) => toSearchOption(value, allowed)),
|
|
136
|
+
),
|
|
131
137
|
);
|
|
132
138
|
}
|
|
133
139
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expectTypeOf, it } from 'vitest';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import { z as mini } from 'zod/mini';
|
|
3
4
|
|
|
4
5
|
import { oneOfParam, textParam, toSearchOption } from './search-params.js';
|
|
5
6
|
|
|
@@ -57,4 +58,20 @@ export function typeAssertions(): void {
|
|
|
57
58
|
expectTypeOf(toSearchOption('detail', mutableSet)).toEqualTypeOf<
|
|
58
59
|
string | undefined
|
|
59
60
|
>();
|
|
61
|
+
|
|
62
|
+
// The same inference through a mini object: an app on `zod/mini` gets the
|
|
63
|
+
// literal union and the absent case too, not a widened `unknown`.
|
|
64
|
+
const miniSchema = mini.object({ q: textParam(), tab: oneOfParam(TABS) });
|
|
65
|
+
type MiniSearch = mini.infer<typeof miniSchema>;
|
|
66
|
+
expectTypeOf<MiniSearch>().toEqualTypeOf<Search>();
|
|
67
|
+
|
|
68
|
+
// A classic object with a classic transform on top — the shape an app's
|
|
69
|
+
// shared schema usually takes — still infers through the mini children.
|
|
70
|
+
const transformed = z
|
|
71
|
+
.object({ q: textParam() })
|
|
72
|
+
.transform(({ q }) => ({ q, hasQuery: q !== undefined }));
|
|
73
|
+
expectTypeOf<z.infer<typeof transformed>>().toEqualTypeOf<{
|
|
74
|
+
q: string | undefined;
|
|
75
|
+
hasQuery: boolean;
|
|
76
|
+
}>();
|
|
60
77
|
}
|
package/src/test-fixtures.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
stringifySearchWith,
|
|
8
8
|
} from '@tanstack/react-router';
|
|
9
9
|
import { z } from 'zod';
|
|
10
|
+
import { z as mini } from 'zod/mini';
|
|
10
11
|
|
|
11
12
|
import { oneOfParam, textParam } from './search-params.js';
|
|
12
13
|
import {
|
|
@@ -81,6 +82,27 @@ export function createToyRouter() {
|
|
|
81
82
|
});
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
/**
|
|
86
|
+
* The toy router's cleanup with the root schema built from `zod/mini` instead
|
|
87
|
+
* of classic `zod` — the shape an app on mini writes. The router takes it as a
|
|
88
|
+
* Standard Schema, so nothing else about the tree changes.
|
|
89
|
+
*/
|
|
90
|
+
export function createMiniRouter() {
|
|
91
|
+
const rootRoute = createRootRoute({
|
|
92
|
+
validateSearch: mini.object({ q: textParam(), tab: oneOfParam(TABS) }),
|
|
93
|
+
beforeLoad: createValidatedSearchRedirect({ stringifySearch }),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return createRouter({
|
|
97
|
+
routeTree: rootRoute.addChildren([
|
|
98
|
+
createRoute({ getParentRoute: () => rootRoute, path: '/plain' }),
|
|
99
|
+
]),
|
|
100
|
+
trailingSlash: 'never',
|
|
101
|
+
parseSearch,
|
|
102
|
+
stringifySearch,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
84
106
|
/**
|
|
85
107
|
* The same tree as {@link createToyRouter}, plus an allowlist for the two keys
|
|
86
108
|
* an OAuth provider appends on the way back. Neither is declared by any schema
|