@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 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
@@ -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.ZodOptional<z.ZodPipe<z.ZodUnknown, z.ZodTransform<string | undefined, unknown>>>;
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.ZodOptional<z.ZodPipe<z.ZodUnknown, z.ZodTransform<T | undefined, unknown>>>;
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
@@ -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.67.0",
46
- "oxlint": "1.82.0",
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-rohit-fork-ci.1",
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
+ });
@@ -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.ZodOptional<
94
- z.ZodPipe<z.ZodUnknown, z.ZodTransform<string | undefined, unknown>>
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.ZodOptional<
114
- z.ZodPipe<z.ZodUnknown, z.ZodTransform<T | undefined, unknown>>
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.unknown().transform((value) => toSearchOption(value, allowed)),
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
  }
@@ -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