remix 3.0.0-beta.0 → 3.0.0-beta.10

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.
Files changed (258) hide show
  1. package/README.md +4 -2
  2. package/dist/assets/types/hmr.d.ts +2 -0
  3. package/dist/cli-entry.js +1 -1
  4. package/dist/data-table/cli.d.ts +2 -0
  5. package/dist/data-table/cli.d.ts.map +1 -0
  6. package/dist/{ui/scroll-lock.js → data-table/cli.js} +1 -1
  7. package/dist/fetch-router.d.ts +7 -0
  8. package/dist/fetch-router.d.ts.map +1 -1
  9. package/dist/headers/accept-encoding.d.ts +2 -0
  10. package/dist/headers/accept-encoding.d.ts.map +1 -0
  11. package/dist/headers/accept-encoding.js +2 -0
  12. package/dist/headers/accept-language.d.ts +2 -0
  13. package/dist/headers/accept-language.d.ts.map +1 -0
  14. package/dist/headers/accept-language.js +2 -0
  15. package/dist/headers/accept.d.ts +2 -0
  16. package/dist/headers/accept.d.ts.map +1 -0
  17. package/dist/headers/accept.js +2 -0
  18. package/dist/headers/cache-control.d.ts +2 -0
  19. package/dist/headers/cache-control.d.ts.map +1 -0
  20. package/dist/headers/cache-control.js +2 -0
  21. package/dist/headers/content-disposition.d.ts +2 -0
  22. package/dist/headers/content-disposition.d.ts.map +1 -0
  23. package/dist/headers/content-disposition.js +2 -0
  24. package/dist/headers/content-range.d.ts +2 -0
  25. package/dist/headers/content-range.d.ts.map +1 -0
  26. package/dist/headers/content-range.js +2 -0
  27. package/dist/headers/content-type.d.ts +2 -0
  28. package/dist/headers/content-type.d.ts.map +1 -0
  29. package/dist/headers/content-type.js +2 -0
  30. package/dist/headers/cookie.d.ts +2 -0
  31. package/dist/headers/cookie.d.ts.map +1 -0
  32. package/dist/headers/cookie.js +2 -0
  33. package/dist/headers/if-match.d.ts +2 -0
  34. package/dist/headers/if-match.d.ts.map +1 -0
  35. package/dist/headers/if-match.js +2 -0
  36. package/dist/headers/if-none-match.d.ts +2 -0
  37. package/dist/headers/if-none-match.d.ts.map +1 -0
  38. package/dist/headers/if-none-match.js +2 -0
  39. package/dist/headers/if-range.d.ts +2 -0
  40. package/dist/headers/if-range.d.ts.map +1 -0
  41. package/dist/headers/if-range.js +2 -0
  42. package/dist/headers/range.d.ts +2 -0
  43. package/dist/headers/range.d.ts.map +1 -0
  44. package/dist/headers/range.js +2 -0
  45. package/dist/headers/raw-headers.d.ts +2 -0
  46. package/dist/headers/raw-headers.d.ts.map +1 -0
  47. package/dist/headers/raw-headers.js +2 -0
  48. package/dist/headers/set-cookie.d.ts +2 -0
  49. package/dist/headers/set-cookie.d.ts.map +1 -0
  50. package/dist/headers/set-cookie.js +2 -0
  51. package/dist/headers/vary.d.ts +2 -0
  52. package/dist/headers/vary.d.ts.map +1 -0
  53. package/dist/{ui/separator.js → headers/vary.js} +1 -1
  54. package/dist/node-hmr/runtime.d.ts +2 -0
  55. package/dist/node-hmr/runtime.d.ts.map +1 -0
  56. package/dist/node-hmr/runtime.js +2 -0
  57. package/dist/node-hmr/types.d.ts +2 -0
  58. package/dist/node-hmr.d.ts +2 -0
  59. package/dist/node-hmr.d.ts.map +1 -0
  60. package/dist/{ui/glyph.js → node-hmr.js} +1 -1
  61. package/dist/node-tsx/load-module.d.ts +2 -0
  62. package/dist/node-tsx/load-module.d.ts.map +1 -0
  63. package/dist/node-tsx/load-module.js +2 -0
  64. package/dist/node-tsx.d.ts +3 -0
  65. package/dist/node-tsx.d.ts.map +1 -0
  66. package/dist/node-tsx.js +3 -0
  67. package/dist/render-middleware.d.ts +2 -0
  68. package/dist/render-middleware.d.ts.map +1 -0
  69. package/dist/render-middleware.js +2 -0
  70. package/dist/route-pattern/href.d.ts +2 -0
  71. package/dist/route-pattern/href.d.ts.map +1 -0
  72. package/dist/route-pattern/href.js +2 -0
  73. package/dist/route-pattern/join.d.ts +2 -0
  74. package/dist/route-pattern/join.d.ts.map +1 -0
  75. package/dist/route-pattern/join.js +2 -0
  76. package/dist/route-pattern/match.d.ts +2 -0
  77. package/dist/route-pattern/match.d.ts.map +1 -0
  78. package/dist/route-pattern/match.js +2 -0
  79. package/dist/ui/accordion/primitives.d.ts +2 -0
  80. package/dist/ui/accordion/primitives.d.ts.map +1 -0
  81. package/dist/ui/accordion/primitives.js +2 -0
  82. package/dist/ui/button.d.ts +1 -0
  83. package/dist/ui/button.d.ts.map +1 -1
  84. package/dist/ui/button.js +1 -0
  85. package/dist/ui/checkbox.d.ts +3 -0
  86. package/dist/ui/checkbox.d.ts.map +1 -0
  87. package/dist/ui/checkbox.js +3 -0
  88. package/dist/ui/combobox/primitives.d.ts +2 -0
  89. package/dist/ui/combobox/primitives.d.ts.map +1 -0
  90. package/dist/ui/combobox/primitives.js +2 -0
  91. package/dist/ui/dev/refresh.d.ts +2 -0
  92. package/dist/ui/dev/refresh.d.ts.map +1 -0
  93. package/dist/ui/dev/refresh.js +2 -0
  94. package/dist/ui/input.d.ts +3 -0
  95. package/dist/ui/input.d.ts.map +1 -0
  96. package/dist/ui/input.js +3 -0
  97. package/dist/ui/menu/primitives.d.ts +2 -0
  98. package/dist/ui/menu/primitives.d.ts.map +1 -0
  99. package/dist/ui/menu/primitives.js +2 -0
  100. package/dist/ui/radio.d.ts +3 -0
  101. package/dist/ui/radio.d.ts.map +1 -0
  102. package/dist/ui/radio.js +3 -0
  103. package/dist/ui/select/primitives.d.ts +2 -0
  104. package/dist/ui/select/primitives.d.ts.map +1 -0
  105. package/dist/ui/select/primitives.js +2 -0
  106. package/dist/ui/tabs/primitives.d.ts +2 -0
  107. package/dist/ui/tabs/primitives.d.ts.map +1 -0
  108. package/dist/ui/tabs/primitives.js +2 -0
  109. package/dist/ui/tabs.d.ts +2 -0
  110. package/dist/ui/tabs.d.ts.map +1 -0
  111. package/{src/ui/theme.ts → dist/ui/tabs.js} +1 -1
  112. package/dist/ui/toggle/primitives.d.ts +2 -0
  113. package/dist/ui/toggle/primitives.d.ts.map +1 -0
  114. package/dist/ui/toggle/primitives.js +2 -0
  115. package/dist/ui/toggle.d.ts +3 -0
  116. package/dist/ui/toggle.d.ts.map +1 -0
  117. package/dist/ui/toggle.js +3 -0
  118. package/dist/ui-hmr/assets.d.ts +2 -0
  119. package/dist/ui-hmr/assets.d.ts.map +1 -0
  120. package/dist/ui-hmr/assets.js +2 -0
  121. package/dist/ui-hmr/node.d.ts +3 -0
  122. package/dist/ui-hmr/node.d.ts.map +1 -0
  123. package/dist/ui-hmr/node.js +3 -0
  124. package/dist/ui-hmr/runtime/browser.d.ts +2 -0
  125. package/dist/ui-hmr/runtime/browser.d.ts.map +1 -0
  126. package/dist/ui-hmr/runtime/browser.js +2 -0
  127. package/dist/ui-hmr/runtime/server.d.ts +2 -0
  128. package/dist/ui-hmr/runtime/server.d.ts.map +1 -0
  129. package/dist/ui-hmr/runtime/server.js +2 -0
  130. package/dist/ui-hmr.d.ts +2 -0
  131. package/dist/ui-hmr.d.ts.map +1 -0
  132. package/{src/ui/glyph.ts → dist/ui-hmr.js} +1 -1
  133. package/package.json +284 -129
  134. package/src/assert/README.md +116 -0
  135. package/src/assets/README.md +805 -0
  136. package/src/assets/types/hmr.d.ts +2 -0
  137. package/src/async-context-middleware/README.md +100 -0
  138. package/src/auth/README.md +445 -0
  139. package/src/auth-middleware/README.md +246 -0
  140. package/src/cli/README.md +182 -0
  141. package/src/compression-middleware/README.md +176 -0
  142. package/src/cookie/README.md +106 -0
  143. package/src/cop-middleware/README.md +117 -0
  144. package/src/cors-middleware/README.md +174 -0
  145. package/src/csrf-middleware/README.md +92 -0
  146. package/src/data-schema/README.md +416 -0
  147. package/src/data-table/README.md +678 -0
  148. package/src/data-table/cli.ts +2 -0
  149. package/src/data-table-mysql/README.md +120 -0
  150. package/src/data-table-postgres/README.md +98 -0
  151. package/src/data-table-sqlite/README.md +100 -0
  152. package/src/fetch-proxy/README.md +70 -0
  153. package/src/fetch-router/README.md +982 -0
  154. package/src/fetch-router.ts +7 -0
  155. package/src/file-storage/README.md +57 -0
  156. package/src/file-storage-s3/README.md +46 -0
  157. package/src/form-data-middleware/README.md +108 -0
  158. package/src/form-data-parser/README.md +158 -0
  159. package/src/fs/README.md +60 -0
  160. package/src/headers/README.md +639 -0
  161. package/src/headers/accept-encoding.ts +2 -0
  162. package/src/headers/accept-language.ts +2 -0
  163. package/src/headers/accept.ts +2 -0
  164. package/src/headers/cache-control.ts +2 -0
  165. package/src/headers/content-disposition.ts +2 -0
  166. package/src/headers/content-range.ts +2 -0
  167. package/src/headers/content-type.ts +2 -0
  168. package/src/headers/cookie.ts +2 -0
  169. package/src/headers/if-match.ts +2 -0
  170. package/src/headers/if-none-match.ts +2 -0
  171. package/src/headers/if-range.ts +2 -0
  172. package/src/headers/range.ts +2 -0
  173. package/src/headers/raw-headers.ts +2 -0
  174. package/src/headers/set-cookie.ts +2 -0
  175. package/src/{node-serve.ts → headers/vary.ts} +1 -1
  176. package/src/html-template/README.md +101 -0
  177. package/src/lazy-file/README.md +109 -0
  178. package/src/logger-middleware/README.md +132 -0
  179. package/src/method-override-middleware/README.md +71 -0
  180. package/src/mime/README.md +117 -0
  181. package/src/multipart-parser/README.md +241 -0
  182. package/src/node-fetch-server/README.md +377 -0
  183. package/src/node-hmr/README.md +307 -0
  184. package/src/node-hmr/runtime.ts +2 -0
  185. package/src/node-hmr/types.d.ts +2 -0
  186. package/{dist/ui/theme.js → src/node-hmr.ts} +1 -1
  187. package/src/node-tsx/README.md +74 -0
  188. package/src/node-tsx/load-module.ts +2 -0
  189. package/{dist/node-serve.js → src/node-tsx.ts} +2 -1
  190. package/src/render-middleware/README.md +99 -0
  191. package/src/render-middleware.ts +2 -0
  192. package/src/route-pattern/README.md +424 -0
  193. package/src/route-pattern/href.ts +2 -0
  194. package/src/route-pattern/join.ts +2 -0
  195. package/src/route-pattern/match.ts +2 -0
  196. package/src/session/README.md +171 -0
  197. package/src/session-middleware/README.md +111 -0
  198. package/src/session-storage-memcache/README.md +37 -0
  199. package/src/session-storage-redis/README.md +36 -0
  200. package/src/static-middleware/README.md +89 -0
  201. package/src/tar-parser/README.md +74 -0
  202. package/src/terminal/README.md +92 -0
  203. package/src/test/README.md +500 -0
  204. package/src/ui/README.md +216 -0
  205. package/src/ui/accordion/README.md +202 -0
  206. package/src/ui/accordion/primitives/README.md +202 -0
  207. package/src/ui/accordion/primitives.ts +2 -0
  208. package/src/ui/anchor/README.md +196 -0
  209. package/src/ui/animation/README.md +316 -0
  210. package/src/ui/breadcrumbs/README.md +55 -0
  211. package/src/ui/button/README.md +44 -0
  212. package/src/ui/button.ts +1 -0
  213. package/src/ui/checkbox/README.md +59 -0
  214. package/src/ui/checkbox.ts +3 -0
  215. package/src/ui/combobox/README.md +194 -0
  216. package/src/ui/combobox/primitives/README.md +194 -0
  217. package/src/ui/combobox/primitives.ts +2 -0
  218. package/src/ui/dev/refresh.ts +2 -0
  219. package/src/ui/input/README.md +52 -0
  220. package/src/ui/input.ts +3 -0
  221. package/src/ui/listbox/README.md +83 -0
  222. package/src/ui/menu/README.md +161 -0
  223. package/src/ui/menu/primitives/README.md +161 -0
  224. package/src/ui/menu/primitives.ts +2 -0
  225. package/src/ui/popover/README.md +103 -0
  226. package/src/ui/radio/README.md +53 -0
  227. package/src/ui/radio.ts +3 -0
  228. package/src/ui/select/README.md +117 -0
  229. package/src/ui/select/primitives/README.md +117 -0
  230. package/src/ui/select/primitives.ts +2 -0
  231. package/src/ui/server/README.md +103 -0
  232. package/src/ui/tabs/README.md +141 -0
  233. package/src/ui/tabs/primitives/README.md +141 -0
  234. package/src/ui/tabs/primitives.ts +2 -0
  235. package/src/ui/tabs.ts +2 -0
  236. package/src/ui/test/README.md +198 -0
  237. package/src/ui/toggle/README.md +56 -0
  238. package/src/ui/toggle/primitives/README.md +56 -0
  239. package/src/ui/toggle/primitives.ts +2 -0
  240. package/src/ui/toggle.ts +3 -0
  241. package/src/ui-hmr/README.md +119 -0
  242. package/src/ui-hmr/assets.ts +2 -0
  243. package/src/ui-hmr/node.ts +3 -0
  244. package/src/ui-hmr/runtime/browser.ts +2 -0
  245. package/src/ui-hmr/runtime/server.ts +2 -0
  246. package/src/ui-hmr.ts +2 -0
  247. package/dist/node-serve.d.ts +0 -2
  248. package/dist/node-serve.d.ts.map +0 -1
  249. package/dist/ui/glyph.d.ts +0 -2
  250. package/dist/ui/glyph.d.ts.map +0 -1
  251. package/dist/ui/scroll-lock.d.ts +0 -2
  252. package/dist/ui/scroll-lock.d.ts.map +0 -1
  253. package/dist/ui/separator.d.ts +0 -2
  254. package/dist/ui/separator.d.ts.map +0 -1
  255. package/dist/ui/theme.d.ts +0 -2
  256. package/dist/ui/theme.d.ts.map +0 -1
  257. package/src/ui/scroll-lock.ts +0 -2
  258. package/src/ui/separator.ts +0 -2
@@ -0,0 +1,416 @@
1
+ # data-schema
2
+
3
+ Tiny, standards-aligned data validation for Remix and the wider TypeScript ecosystem.
4
+
5
+ - [Standard Schema](https://standardschema.dev/) v1 compatible
6
+ - Sync-first, minimal API surface
7
+ - Runs anywhere JavaScript runs (browser, Node.js, Bun, Deno, Workers)
8
+
9
+ ## Quick start
10
+
11
+ ```ts
12
+ import { enum_, literal, number, object, parse, string, variant } from 'remix/data-schema'
13
+ import { email, maxLength, min, minLength } from 'remix/data-schema/checks'
14
+ import * as coerce from 'remix/data-schema/coerce'
15
+
16
+ let User = object({
17
+ id: string(),
18
+ email: string().pipe(email()),
19
+ username: string().pipe(minLength(3), maxLength(20)),
20
+ age: coerce.number().pipe(min(13)),
21
+ role: enum_(['admin', 'member', 'guest']),
22
+ flags: object({
23
+ beta: coerce.boolean(),
24
+ }),
25
+ })
26
+
27
+ let Event = variant('type', {
28
+ created: object({ type: literal('created'), id: string() }),
29
+ updated: object({ type: literal('updated'), id: string(), version: number() }),
30
+ })
31
+
32
+ let user = parse(User, {
33
+ id: 'u1',
34
+ email: 'ada@example.com',
35
+ username: 'ada',
36
+ age: '37',
37
+ role: 'admin',
38
+ flags: { beta: 'true' },
39
+ })
40
+
41
+ let event = parse(Event, { type: 'created', id: 'evt_1' })
42
+ ```
43
+
44
+ ## Parsing
45
+
46
+ Use `parse()` when you want a typed value or an exception.
47
+
48
+ ```ts
49
+ import { object, string, number, parse } from 'remix/data-schema'
50
+
51
+ let User = object({ name: string(), age: number() })
52
+
53
+ let user = parse(User, { name: 'Ada', age: 37 })
54
+ ```
55
+
56
+ Use `parseSafe()` when you prefer explicit branching over exceptions.
57
+
58
+ ```ts
59
+ import { object, string, number, parseSafe } from 'remix/data-schema'
60
+
61
+ let User = object({ name: string(), age: number() })
62
+
63
+ let result = parseSafe(User, input)
64
+
65
+ if (!result.success) {
66
+ // result.issues — array of { message, path? }
67
+ } else {
68
+ let user = result.value
69
+ }
70
+ ```
71
+
72
+ Both `parse` and `parseSafe` accept any [Standard Schema](https://standardschema.dev/) v1 schema, not just data-schema's own schemas. You can pass a Zod, Valibot, or ArkType schema and they'll work.
73
+
74
+ For `FormData` and `URLSearchParams`, use the `remix/data-schema/form-data` helpers to build schemas that plug into the same `parse()` / `parseSafe()` flow:
75
+
76
+ ```ts
77
+ import * as s from 'remix/data-schema'
78
+ import * as f from 'remix/data-schema/form-data'
79
+ import * as checks from 'remix/data-schema/checks'
80
+ import * as coerce from 'remix/data-schema/coerce'
81
+
82
+ let Login = f.object({
83
+ email: f.field(coerce.string().pipe(checks.email())),
84
+ password: f.field(s.string().pipe(checks.minLength(8))),
85
+ })
86
+
87
+ let credentials = s.parse(Login, await request.formData())
88
+ let filters = s.parse(
89
+ f.object({
90
+ query: f.field(s.defaulted(s.string(), '')),
91
+ tags: f.fields(s.array(s.string())),
92
+ }),
93
+ new URL(request.url).searchParams,
94
+ )
95
+ ```
96
+
97
+ `f.object(...)` is the root schema for `FormData` and `URLSearchParams`. Use `f.field(...)` for one text value, `f.fields(...)` for repeated text values, `f.file(...)` for one uploaded file, and `f.files(...)` for repeated files. When you want a fallback value, prefer `s.defaulted(s.string(), '')`. File helpers are intended for `FormData`; `URLSearchParams` only supports text values.
98
+
99
+ You can also customize built-in validation messages with `errorMap`:
100
+
101
+ ```ts
102
+ import { object, parseSafe, string } from 'remix/data-schema'
103
+ import { minLength } from 'remix/data-schema/checks'
104
+
105
+ let User = object({
106
+ name: string(),
107
+ username: string().pipe(minLength(3)),
108
+ })
109
+ let result = parseSafe(User, input, {
110
+ locale: 'es',
111
+ errorMap(context) {
112
+ if (context.code === 'type.string') {
113
+ return 'Se esperaba texto'
114
+ }
115
+
116
+ if (context.code === 'string.min_length') {
117
+ return (
118
+ 'Debe tener al menos ' + String((context.values as { min: number }).min) + ' caracteres'
119
+ )
120
+ }
121
+ },
122
+ })
123
+ ```
124
+
125
+ `errorMap` receives `{ code, defaultMessage, path, values, input, locale }`. Return `undefined` to keep the default message.
126
+
127
+ ## Primitives
128
+
129
+ ```ts
130
+ import { string, number, boolean, bigint, symbol, null_, undefined_ } from 'remix/data-schema'
131
+
132
+ string() // validates typeof === 'string'
133
+ number() // validates finite numbers (rejects NaN, Infinity)
134
+ boolean() // validates typeof === 'boolean'
135
+ bigint() // validates typeof === 'bigint'
136
+ symbol() // validates typeof === 'symbol'
137
+ null_() // validates value === null
138
+ undefined_() // validates value === undefined
139
+ ```
140
+
141
+ ## Literals, enums, and unions
142
+
143
+ ```ts
144
+ import { literal, enum_, union } from 'remix/data-schema'
145
+
146
+ // Exact value match
147
+ let yes = literal('yes')
148
+
149
+ // One of several allowed values
150
+ let Status = enum_(['active', 'inactive', 'pending'])
151
+
152
+ // First schema that matches wins
153
+ let StringOrNumber = union([string(), number()])
154
+ ```
155
+
156
+ ## Objects
157
+
158
+ ```ts
159
+ import { object, string, number, optional, defaulted } from 'remix/data-schema'
160
+
161
+ let User = object({
162
+ name: string(),
163
+ bio: optional(string()), // accepts undefined
164
+ role: defaulted(string(), 'user'), // fills in 'user' when undefined
165
+ age: number(),
166
+ })
167
+ ```
168
+
169
+ Unknown keys are stripped by default. Change this with `unknownKeys`:
170
+
171
+ ```ts
172
+ object({ name: string() }, { unknownKeys: 'passthrough' }) // keeps unknown keys
173
+ object({ name: string() }, { unknownKeys: 'error' }) // rejects unknown keys
174
+ ```
175
+
176
+ ## Collections
177
+
178
+ ```ts
179
+ import { array, tuple, record, map, set, string, number, boolean } from 'remix/data-schema'
180
+
181
+ array(number()) // number[]
182
+ tuple([string(), number(), boolean()]) // [string, number, boolean]
183
+ record(string(), number()) // Record<string, number>
184
+ map(string(), number()) // Map<string, number>
185
+ set(number()) // Set<number>
186
+ ```
187
+
188
+ ## Modifiers
189
+
190
+ ```ts
191
+ import { nullable, optional, defaulted, string, number } from 'remix/data-schema'
192
+
193
+ nullable(string()) // string | null
194
+ optional(number()) // number | undefined
195
+ defaulted(string(), 'n/a') // fills 'n/a' when undefined
196
+ ```
197
+
198
+ ## Instance checks
199
+
200
+ ```ts
201
+ import { instanceof_, object } from 'remix/data-schema'
202
+
203
+ let Schema = object({
204
+ created: instanceof_(Date),
205
+ pattern: instanceof_(RegExp),
206
+ })
207
+ ```
208
+
209
+ ## Any
210
+
211
+ Accept any value without validation. Useful when part of a structure is opaque.
212
+
213
+ ```ts
214
+ import { any, object, string } from 'remix/data-schema'
215
+
216
+ let Envelope = object({
217
+ type: string(),
218
+ payload: any(),
219
+ })
220
+ ```
221
+
222
+ ## Custom rules with `.refine()`
223
+
224
+ Add domain-specific validation logic inline. The predicate runs after the schema validates.
225
+
226
+ ```ts
227
+ import { number, string, object } from 'remix/data-schema'
228
+
229
+ let Profile = object({
230
+ username: string().refine((s) => s.length >= 3, 'Too short'),
231
+ age: number().refine((n) => n >= 18, 'Must be an adult'),
232
+ })
233
+ ```
234
+
235
+ ## Output transforms with `.transform()`
236
+
237
+ Map a validated value into the shape your app wants. The transformer runs after the schema validates and changes the parsed output type.
238
+
239
+ ```ts
240
+ import { object, parse, string } from 'remix/data-schema'
241
+
242
+ let Event = object({
243
+ id: string(),
244
+ createdAt: string()
245
+ .refine((value) => !Number.isNaN(Date.parse(value)), 'Expected valid date')
246
+ .transform((value) => new Date(value)),
247
+ })
248
+
249
+ let event = parse(Event, {
250
+ id: 'evt_1',
251
+ createdAt: '2026-04-25T00:00:00.000Z',
252
+ })
253
+
254
+ event.createdAt // Date
255
+ ```
256
+
257
+ Use `.refine()` for checks that reject values without changing them. Use `.transform()` for safe, synchronous mappings; thrown errors are propagated instead of converted into validation issues.
258
+
259
+ ## Validation pipelines with `.pipe()`
260
+
261
+ Compose reusable `Check` objects for common constraints.
262
+
263
+ ```ts
264
+ import { object, string, number } from 'remix/data-schema'
265
+ import { minLength, maxLength, email, min, max } from 'remix/data-schema/checks'
266
+
267
+ let Credentials = object({
268
+ username: string().pipe(minLength(3), maxLength(20)),
269
+ email: string().pipe(email()),
270
+ age: number().pipe(min(13), max(130)),
271
+ })
272
+ ```
273
+
274
+ Built-in checks: `minLength`, `maxLength`, `email`, `url`, `min`, `max`.
275
+
276
+ ## Coercing input values
277
+
278
+ Turn stringly-typed inputs (like form data or query strings) into real types at the schema boundary.
279
+
280
+ ```ts
281
+ import { object, parse } from 'remix/data-schema'
282
+ import * as coerce from 'remix/data-schema/coerce'
283
+
284
+ let Query = object({
285
+ page: coerce.number(),
286
+ includeArchived: coerce.boolean(),
287
+ since: coerce.date(),
288
+ limit: coerce.bigint(),
289
+ search: coerce.string(),
290
+ })
291
+
292
+ let query = parse(Query, {
293
+ page: '2',
294
+ includeArchived: 'true',
295
+ since: '2025-01-01',
296
+ limit: '100',
297
+ search: 42,
298
+ })
299
+ ```
300
+
301
+ ## Discriminated unions
302
+
303
+ Pick the right schema based on a discriminator property.
304
+
305
+ ```ts
306
+ import { literal, number, object, string, variant } from 'remix/data-schema'
307
+
308
+ let Event = variant('type', {
309
+ created: object({ type: literal('created'), id: string() }),
310
+ updated: object({ type: literal('updated'), id: string(), version: number() }),
311
+ })
312
+ ```
313
+
314
+ ## Recursive schemas
315
+
316
+ Model trees and self-referencing structures. `lazy()` defers schema resolution to avoid circular references.
317
+
318
+ ```ts
319
+ import { array, object, string } from 'remix/data-schema'
320
+ import { lazy } from 'remix/data-schema/lazy'
321
+ import type { Schema } from 'remix/data-schema'
322
+
323
+ type TreeNode = { id: string; children: TreeNode[] }
324
+
325
+ let Node: Schema<unknown, TreeNode> = lazy(() => object({ id: string(), children: array(Node) }))
326
+ ```
327
+
328
+ ## Aborting early
329
+
330
+ By default, validation collects all issues in a single pass. To stop at the first issue, enable `abortEarly`.
331
+
332
+ ```ts
333
+ import { object, string, number, parseSafe } from 'remix/data-schema'
334
+
335
+ let result = parseSafe(
336
+ object({ name: string(), age: number() }),
337
+ { name: 123, age: 'x' },
338
+ { abortEarly: true },
339
+ )
340
+
341
+ if (!result.success) {
342
+ console.log(result.issues) // only the first issue
343
+ }
344
+ ```
345
+
346
+ ## Type inference
347
+
348
+ Extract input and output types from any Standard Schema-compatible schema.
349
+
350
+ ```ts
351
+ import { object, string, number } from 'remix/data-schema'
352
+ import type { InferInput, InferOutput } from 'remix/data-schema'
353
+
354
+ let User = object({ name: string(), age: number() })
355
+
356
+ type UserInput = InferInput<typeof User> // unknown
357
+ type UserOutput = InferOutput<typeof User> // { name: string; age: number }
358
+ ```
359
+
360
+ ## Extending data-schema
361
+
362
+ Build custom schemas using `createSchema`, `createIssue`, and `fail`. These are the same primitives used internally by every built-in schema.
363
+
364
+ ```ts
365
+ import { createSchema, createIssue, fail } from 'remix/data-schema'
366
+ import type { Schema } from 'remix/data-schema'
367
+
368
+ // A schema that validates a non-empty trimmed string
369
+ function trimmedString(): Schema<unknown, string> {
370
+ return createSchema(function validate(value, context) {
371
+ if (typeof value !== 'string') {
372
+ return fail('Expected string', context.path)
373
+ }
374
+
375
+ let trimmed = value.trim()
376
+
377
+ if (trimmed.length === 0) {
378
+ return fail('Expected non-empty string', context.path)
379
+ }
380
+
381
+ return { value: trimmed }
382
+ })
383
+ }
384
+
385
+ // A schema that validates a [lat, lng] coordinate pair
386
+ function latLng(): Schema<unknown, [number, number]> {
387
+ return createSchema(function validate(value, context) {
388
+ if (!Array.isArray(value) || value.length !== 2) {
389
+ return fail('Expected [lat, lng] pair', context.path)
390
+ }
391
+
392
+ let issues = []
393
+ let [lat, lng] = value
394
+
395
+ if (typeof lat !== 'number' || lat < -90 || lat > 90) {
396
+ issues.push(createIssue('Latitude must be between -90 and 90', [...context.path, 0]))
397
+ }
398
+
399
+ if (typeof lng !== 'number' || lng < -180 || lng > 180) {
400
+ issues.push(createIssue('Longitude must be between -180 and 180', [...context.path, 1]))
401
+ }
402
+
403
+ if (issues.length > 0) {
404
+ return { issues }
405
+ }
406
+
407
+ return { value: [lat, lng] }
408
+ })
409
+ }
410
+ ```
411
+
412
+ The validator function receives the raw value and a context with the current `path` and `options`. Return `{ value }` on success or `{ issues: [...] }` on failure. The returned schema is fully Standard Schema v1-compatible and supports `.pipe()` and `.refine()` out of the box.
413
+
414
+ ## License
415
+
416
+ See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)