@validex/core 1.0.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.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +583 -0
  3. package/dist/checks/index.cjs +51 -0
  4. package/dist/checks/index.d.cts +207 -0
  5. package/dist/checks/index.d.ts +207 -0
  6. package/dist/checks/index.js +51 -0
  7. package/dist/chunk-2OTGUKO7.cjs +26 -0
  8. package/dist/chunk-4HUGF4EZ.js +0 -0
  9. package/dist/chunk-5CYBSNNG.cjs +33 -0
  10. package/dist/chunk-5DOFRLSB.js +30 -0
  11. package/dist/chunk-64X6D23X.cjs +24 -0
  12. package/dist/chunk-6MGS4JWP.js +152 -0
  13. package/dist/chunk-AGJWIOFF.js +24 -0
  14. package/dist/chunk-BAFEYOPS.js +43 -0
  15. package/dist/chunk-BYMZTDYD.js +56 -0
  16. package/dist/chunk-D232V332.cjs +30 -0
  17. package/dist/chunk-FD77PZXB.js +9 -0
  18. package/dist/chunk-H3XHQLZF.cjs +43 -0
  19. package/dist/chunk-ISY3F7TI.cjs +239 -0
  20. package/dist/chunk-JEKRBIPN.cjs +1 -0
  21. package/dist/chunk-KTN4NQQL.js +33 -0
  22. package/dist/chunk-LSQNDPFQ.cjs +9 -0
  23. package/dist/chunk-NDLUDRZJ.js +24 -0
  24. package/dist/chunk-OCTLUBGT.cjs +24 -0
  25. package/dist/chunk-OFT3FQPJ.cjs +152 -0
  26. package/dist/chunk-OOFMB7K5.js +34 -0
  27. package/dist/chunk-OTPQTLPM.js +50 -0
  28. package/dist/chunk-P3FRVJ3U.cjs +50 -0
  29. package/dist/chunk-PFPNNQGJ.js +30 -0
  30. package/dist/chunk-PQ4TUE2Q.cjs +2688 -0
  31. package/dist/chunk-SMDC2EAD.js +26 -0
  32. package/dist/chunk-TB6J73U7.js +239 -0
  33. package/dist/chunk-TBVAKZA5.js +2688 -0
  34. package/dist/chunk-TSPTIW3V.cjs +34 -0
  35. package/dist/chunk-WE2OD5XD.cjs +30 -0
  36. package/dist/chunk-WKVMDEA3.js +26 -0
  37. package/dist/chunk-ZAUX2RGL.cjs +56 -0
  38. package/dist/chunk-ZWIO2MJX.cjs +26 -0
  39. package/dist/cli/index.cjs +120 -0
  40. package/dist/cli/index.d.cts +1 -0
  41. package/dist/cli/index.d.ts +1 -0
  42. package/dist/cli/index.js +120 -0
  43. package/dist/commonPasswords-3BYUBARZ.cjs +10 -0
  44. package/dist/commonPasswords-ZYOEI6PG.js +10 -0
  45. package/dist/countryCodes-EKJKVHR5.cjs +10 -0
  46. package/dist/countryCodes-RTJZVCLB.js +10 -0
  47. package/dist/countryCodes-VY56VZPT.cjs +255 -0
  48. package/dist/countryCodes-YRY75MQP.js +255 -0
  49. package/dist/creditCardPrefixes-EXMJZGE7.cjs +10 -0
  50. package/dist/creditCardPrefixes-EZK7T4IZ.js +10 -0
  51. package/dist/creditCardPrefixes-HKWKCHNU.cjs +48 -0
  52. package/dist/creditCardPrefixes-QP3S4ZAU.js +48 -0
  53. package/dist/currencyCodes-GU6W3HSN.cjs +171 -0
  54. package/dist/currencyCodes-P67AASLW.js +171 -0
  55. package/dist/currencyCodes-RMRLGDME.cjs +10 -0
  56. package/dist/currencyCodes-U6TSAWDR.js +10 -0
  57. package/dist/disposableDomains-DCXSV422.js +10 -0
  58. package/dist/disposableDomains-USU2JQSF.cjs +10 -0
  59. package/dist/ibanPatterns-2PM32RIY.cjs +85 -0
  60. package/dist/ibanPatterns-BSQUWKLY.js +85 -0
  61. package/dist/ibanPatterns-KTLY6TZY.cjs +10 -0
  62. package/dist/ibanPatterns-LJRPR7FV.js +10 -0
  63. package/dist/index-Cid7Ygr_.d.cts +950 -0
  64. package/dist/index-Cid7Ygr_.d.ts +950 -0
  65. package/dist/index.cjs +361 -0
  66. package/dist/index.d.cts +132 -0
  67. package/dist/index.d.ts +132 -0
  68. package/dist/index.js +361 -0
  69. package/dist/locales/en.cjs +10 -0
  70. package/dist/locales/en.d.cts +234 -0
  71. package/dist/locales/en.d.ts +234 -0
  72. package/dist/locales/en.js +10 -0
  73. package/dist/passwordsTier1-NAZLSHKW.cjs +105 -0
  74. package/dist/passwordsTier1-OYRMLOWD.js +105 -0
  75. package/dist/passwordsTier2-GYJTYGY6.cjs +906 -0
  76. package/dist/passwordsTier2-JAEO5AYY.js +906 -0
  77. package/dist/passwordsTier3-BAPUFHZM.cjs +9006 -0
  78. package/dist/passwordsTier3-E6WBK5OB.js +9006 -0
  79. package/dist/phoneDetection-AFSSD4IB.cjs +6 -0
  80. package/dist/phoneDetection-G23LZ6MU.js +6 -0
  81. package/dist/phoneParser-2RTXDB6H.js +10 -0
  82. package/dist/phoneParser-CGRP2OUN.cjs +10 -0
  83. package/dist/postalCodes-4EZVDT2N.cjs +10 -0
  84. package/dist/postalCodes-ZPAJB3P5.js +10 -0
  85. package/dist/reservedUsernames-3QPPKUXR.cjs +246 -0
  86. package/dist/reservedUsernames-GIK6NX3J.js +246 -0
  87. package/dist/reservedUsernames-QR4ONXSL.js +10 -0
  88. package/dist/reservedUsernames-W65FGT6A.cjs +10 -0
  89. package/dist/rules/index.cjs +66 -0
  90. package/dist/rules/index.d.cts +2 -0
  91. package/dist/rules/index.d.ts +2 -0
  92. package/dist/rules/index.js +66 -0
  93. package/dist/utilities/index.cjs +8 -0
  94. package/dist/utilities/index.d.cts +47 -0
  95. package/dist/utilities/index.d.ts +47 -0
  96. package/dist/utilities/index.js +8 -0
  97. package/dist/vatPatterns-BLRXHNCP.js +36 -0
  98. package/dist/vatPatterns-DNVZJPTW.js +10 -0
  99. package/dist/vatPatterns-NPN6SV2Y.cjs +36 -0
  100. package/dist/vatPatterns-RRHUTA3U.cjs +10 -0
  101. package/package.json +133 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 validex contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,583 @@
1
+ # @validex/core
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@validex/core)](https://www.npmjs.com/package/@validex/core)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@validex/core)](https://www.npmjs.com/package/@validex/core)
5
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/@validex/core)](https://bundlephobia.com/package/@validex/core)
6
+ [![build](https://img.shields.io/github/actions/workflow/status/chiptoma/validex/ci.yml)](https://github.com/chiptoma/validex/actions)
7
+ [![TypeScript 5.0+](https://img.shields.io/badge/TypeScript-5.0%2B-blue)](https://www.typescriptlang.org/)
8
+ [![license MIT](https://img.shields.io/npm/l/@validex/core)](https://github.com/chiptoma/validex/blob/main/LICENSE)
9
+
10
+ **Type-safe validation rules built on Zod** — tree-shakeable, so you only ship what you use.
11
+
12
+ ---
13
+
14
+ - [Why validex?](#why-validex)
15
+ - [Install](#install)
16
+ - [Quick Start](#quick-start)
17
+ - [Rules](#rules) — all 25 rules
18
+ - [Bundle Size](#bundle-size)
19
+ - [Configuration](#configuration) — global defaults, three-tier merge, preloading
20
+ - [Cross-Field Validation](#cross-field-validation) — sameAs, requiredWhen
21
+ - [Chainable Methods](#chainable-methods) — checks and transforms
22
+ - [Check Functions](#check-functions) — standalone pure functions
23
+ - [Error Handling](#error-handling) — structured errors, getParams
24
+ - [i18n](#i18n) — translations, CLI
25
+ - [Custom Rules](#custom-rules) — createRule, customFn
26
+ - [Framework Adapters](#framework-adapters) — Nuxt, Fastify
27
+
28
+ ---
29
+
30
+ ## Why validex?
31
+
32
+ I built validex because I was **fed up** writing the same validation rules over and over again in every project.
33
+
34
+ Different teams, different defaults, forgetting what I had configured last time, and ending up with inconsistent behavior across the codebase. Sound familiar?
35
+
36
+ Validex was created to solve that pain once and for all:
37
+
38
+ - **One config system** — `setup()` lets you define your defaults globally, per-rule, or per-call. Three-tier merge (built-in defaults → global config → per-call options) so you never repeat yourself again.
39
+ - **One consistent error surface** — `validate()` always returns the same clean shape — flat errors, nested errors, first-per-field, raw issues. One function, one result, every time.
40
+ - **Every error is validex-owned** — no raw Zod messages leak to your users. Every error carries a namespace, code, and label for precise routing.
41
+ - **25 production-ready rules** covering the fields you actually use: identity, auth, networking, finance, and text.
42
+ - **Tree-shakeable & lightweight** — 5–6 kB Brotli per rule (shared core included). All 25 rules together = 13 kB. Heavy data loads on demand.
43
+ - **i18n-ready out of the box** — key mode, `t()` function support, label/message transforms, and a CLI that generates ready-to-translate locale files.
44
+ - **First-class framework adapters** — Nuxt and Fastify integrations that feel native.
45
+
46
+ Stop copy-pasting rules. Get consistent, maintainable validation with sensible defaults — and only ship what you actually use.
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pnpm add @validex/core zod
52
+ ```
53
+
54
+ ## Quick Start
55
+
56
+ ### Single rule
57
+
58
+ ```ts
59
+ import { Email } from '@validex/core'
60
+
61
+ const schema = Email()
62
+ schema.parse('hello@example.com') // OK
63
+ schema.parse('not-an-email') // throws ZodError
64
+ ```
65
+
66
+ ### Rule with options
67
+
68
+ ```ts
69
+ import { Password } from '@validex/core'
70
+
71
+ const schema = Password({
72
+ length: { min: 10 },
73
+ uppercase: { min: 2 },
74
+ blockCommon: 'basic',
75
+ })
76
+
77
+ schema.parse('ABcdefgh1!') // OK — 10+ chars, 2 uppercase, 1 digit, 1 special
78
+ ```
79
+
80
+ ### Composed schema with `validate()`
81
+
82
+ ```ts
83
+ import { z } from 'zod'
84
+ import { Email, Password, validate } from '@validex/core'
85
+
86
+ const schema = z.object({
87
+ email: Email(),
88
+ password: Password(),
89
+ })
90
+
91
+ const result = await validate(schema, {
92
+ email: 'user@example.com',
93
+ password: 'Str0ng!Pass',
94
+ })
95
+
96
+ if (result.success) {
97
+ console.log(result.data) // typed as { email: string; password: string }
98
+ } else {
99
+ console.log(result.errors) // { email: ['...'], password: ['...'] }
100
+ console.log(result.firstErrors) // { email: '...', password: '...' }
101
+ }
102
+ ```
103
+
104
+ ## Rules
105
+
106
+ | Rule | Description |
107
+ |------|-------------|
108
+ | Email | Email address with domain filtering, plus-alias blocking, and disposable detection |
109
+ | Password | Strength rules: length, casing, digits, specials, consecutive limits, common-password ban |
110
+ | PasswordConfirmation | Confirms two password fields match |
111
+ | PersonName | Human name with unicode support, word count, and boundary rules |
112
+ | BusinessName | Company/organization name with boundary and consecutive limits |
113
+ | Phone | International phone via libphonenumber-js |
114
+ | Website | URL restricted to http/https with optional www and domain filtering |
115
+ | Url | General URL with protocol, TLD, and domain validation |
116
+ | Username | Alphanumeric with configurable separators and reserved-word ban |
117
+ | Slug | URL-safe slug (lowercase, hyphens, length limits) |
118
+ | PostalCode | Country-aware postal/ZIP code |
119
+ | LicenseKey | Software license key format (segments, separators, charset) |
120
+ | Uuid | UUID v1-v7 validation |
121
+ | Jwt | JSON Web Token structure with optional expiry checks |
122
+ | DateTime | Date/time string with format and range constraints |
123
+ | Token | Generic token validation (hex, base64, nanoid, etc.) |
124
+ | Text | Free text with length, word count, content detection, and regex override |
125
+ | Country | ISO 3166 country code (alpha-2, alpha-3) |
126
+ | Currency | ISO 4217 currency code |
127
+ | Color | Hex, RGB, HSL, and named CSS color formats |
128
+ | CreditCard | Card number with Luhn check and issuer detection |
129
+ | Iban | International Bank Account Number with country patterns |
130
+ | VatNumber | EU VAT identification number |
131
+ | MacAddress | MAC address (colon, hyphen, and dot notations) |
132
+ | IpAddress | IPv4 and IPv6 with optional CIDR notation |
133
+
134
+ ## Bundle Size
135
+
136
+ Every rule shares a ~5 kB core (Brotli). Each additional rule adds 0.1-0.8 kB. Measured with esbuild `--splitting` + Brotli, excluding `zod` peer dependency.
137
+
138
+ | Rule | Initial (Brotli) | On-demand data | Trigger |
139
+ |------|-----------------|----------------|---------|
140
+ | Email | 5.7 kB | — | — |
141
+ | Password | 5.6 kB | +0.5 kB / +3.8 kB / +35.5 kB | `blockCommon: 'basic'` / `'moderate'` / `'strict'` |
142
+ | PasswordConfirmation | 5.7 kB | — | — |
143
+ | PersonName | 5.7 kB | — | — |
144
+ | BusinessName | 5.7 kB | — | — |
145
+ | Phone | 5.7 kB | external | libphonenumber-js peer dep |
146
+ | Website | 5.7 kB | — | — |
147
+ | Url | 5.6 kB | — | — |
148
+ | Username | 5.9 kB | +0.8 kB | `blockReserved: true` |
149
+ | Slug | 5.5 kB | — | — |
150
+ | PostalCode | 5.4 kB | external | postcode-validator peer dep |
151
+ | LicenseKey | 5.5 kB | — | — |
152
+ | Uuid | 5.3 kB | — | — |
153
+ | Jwt | 5.6 kB | — | — |
154
+ | DateTime | 5.6 kB | — | — |
155
+ | Token | 5.5 kB | — | — |
156
+ | Text | 5.5 kB | — | — |
157
+ | Country | 5.4 kB | +2.4 kB | First use |
158
+ | Currency | 5.4 kB | +0.3 kB | First use |
159
+ | Color | 5.5 kB | — | — |
160
+ | CreditCard | 5.6 kB | +0.3 kB | First use |
161
+ | Iban | 5.5 kB | +0.7 kB | First use |
162
+ | VatNumber | 5.5 kB | +0.3 kB | First use |
163
+ | MacAddress | 5.3 kB | — | — |
164
+ | IpAddress | 5.6 kB | — | — |
165
+
166
+ | Combination | Initial (Brotli) |
167
+ |-------------|-----------------|
168
+ | Email + Password | 6.0 kB |
169
+ | Form (Email + Password + PersonName + Phone) | 6.9 kB |
170
+ | All 25 rules | 13.0 kB |
171
+
172
+ "On-demand data" loads asynchronously on first use or when the listed option is enabled. Not included in the initial bundle.
173
+
174
+ ## Configuration
175
+
176
+ ### Global defaults with `setup()`
177
+
178
+ ```ts
179
+ import { setup, Email, Password } from '@validex/core'
180
+
181
+ setup({
182
+ rules: {
183
+ email: { blockDisposable: true },
184
+ password: { length: { min: 10 }, special: { min: 2 } },
185
+ },
186
+ i18n: {
187
+ enabled: true,
188
+ t: (key, params) => translate(key, params),
189
+ },
190
+ })
191
+
192
+ // Rules now use your defaults — no need to pass options every time
193
+ const emailSchema = Email()
194
+ const passwordSchema = Password()
195
+ ```
196
+
197
+ ### Three-tier merge
198
+
199
+ ```
200
+ built-in defaults < setup() config < per-call options
201
+ ```
202
+
203
+ Per-call options override `setup()` config, which overrides built-in defaults. Passing `undefined` for a per-call option removes the global setting for that field.
204
+
205
+ ```ts
206
+ import { setup, Email } from '@validex/core'
207
+
208
+ setup({ rules: { email: { blockDisposable: true } } })
209
+
210
+ Email() // blockDisposable: true (from setup)
211
+ Email({ blockPlusAlias: true }) // blockDisposable: true + blockPlusAlias: true
212
+ Email({ blockDisposable: undefined }) // blockDisposable removed for this call
213
+ ```
214
+
215
+ ### `resetConfig()`
216
+
217
+ ```ts
218
+ import { resetConfig } from '@validex/core'
219
+
220
+ resetConfig() // resets to built-in defaults
221
+ ```
222
+
223
+ ### `preloadData()`
224
+
225
+ Preload async data files at startup so first validation has no delay:
226
+
227
+ ```ts
228
+ import { preloadData } from '@validex/core'
229
+
230
+ await preloadData({
231
+ disposable: true,
232
+ passwords: 'moderate',
233
+ reserved: true,
234
+ phone: 'mobile',
235
+ countryCodes: true,
236
+ currencyCodes: true,
237
+ ibanPatterns: true,
238
+ vatPatterns: true,
239
+ creditCardPrefixes: true,
240
+ postalCodes: true,
241
+ })
242
+ ```
243
+
244
+ ## Cross-Field Validation
245
+
246
+ ### `sameAs`
247
+
248
+ Creates a `superRefine` callback that verifies two fields hold the same value:
249
+
250
+ ```ts
251
+ import { z } from 'zod'
252
+ import { Password, sameAs } from '@validex/core'
253
+
254
+ const schema = z.object({
255
+ password: Password(),
256
+ confirmPassword: z.string(),
257
+ }).superRefine(sameAs('confirmPassword', 'password', {
258
+ message: 'Passwords do not match',
259
+ }))
260
+ ```
261
+
262
+ `PasswordConfirmation` auto-wires this — it registers a `sameAs: 'password'` constraint automatically:
263
+
264
+ ```ts
265
+ import { z } from 'zod'
266
+ import { Password, PasswordConfirmation, validate } from '@validex/core'
267
+
268
+ const schema = z.object({
269
+ password: Password(),
270
+ confirmPassword: PasswordConfirmation(),
271
+ })
272
+
273
+ const result = await validate(schema, {
274
+ password: 'Str0ng!Pass',
275
+ confirmPassword: 'different',
276
+ })
277
+ // result.firstErrors.confirmPassword → "Password Confirmation must match Password"
278
+ ```
279
+
280
+ ### `requiredWhen`
281
+
282
+ Creates a `superRefine` callback that marks a field as required when a condition is met:
283
+
284
+ ```ts
285
+ import { z } from 'zod'
286
+ import { requiredWhen } from '@validex/core'
287
+
288
+ const schema = z.object({
289
+ accountType: z.string(),
290
+ companyName: z.string().optional(),
291
+ }).superRefine(requiredWhen(
292
+ 'companyName',
293
+ (data) => data['accountType'] === 'business',
294
+ { message: 'Company name is required for business accounts' },
295
+ ))
296
+ ```
297
+
298
+ ### `validate()` resolves cross-field
299
+
300
+ `schema.safeParse()` only runs field-level validation. `validate()` adds cross-field checks (`sameAs`, `requiredWhen`) after Zod parsing:
301
+
302
+ ```ts
303
+ // safeParse — field-level only, no cross-field
304
+ const zodResult = schema.safeParse(data)
305
+
306
+ // validate — runs field-level + cross-field
307
+ const result = await validate(schema, data)
308
+ ```
309
+
310
+ ## Chainable Methods
311
+
312
+ Import `@validex/core` and all Zod string schemas get these methods:
313
+
314
+ ### Checks (return same type, add refinement)
315
+
316
+ | Method | Options | Description |
317
+ |--------|---------|-------------|
318
+ | `.hasUppercase(opts?)` | `min?, max?` | Requires uppercase letters |
319
+ | `.hasLowercase(opts?)` | `min?, max?` | Requires lowercase letters |
320
+ | `.hasDigits(opts?)` | `min?, max?` | Requires digits |
321
+ | `.hasSpecial(opts?)` | `min?, max?` | Requires special characters |
322
+ | `.noEmails(opts?)` | — | Blocks email addresses |
323
+ | `.noUrls(opts?)` | — | Blocks URLs |
324
+ | `.noHtml(opts?)` | — | Blocks HTML tags |
325
+ | `.noPhoneNumbers(opts?)` | — | Blocks phone numbers |
326
+ | `.noSpaces(opts?)` | — | Blocks whitespace |
327
+ | `.onlyAlpha(opts?)` | — | Letters only |
328
+ | `.onlyNumeric(opts?)` | — | Digits only |
329
+ | `.onlyAlphanumeric(opts?)` | — | Letters + digits |
330
+ | `.onlyAlphaSpaceHyphen(opts?)` | — | Letters, spaces, hyphens |
331
+ | `.onlyAlphanumericSpaceHyphen(opts?)` | — | Letters, digits, spaces, hyphens |
332
+ | `.maxWords(opts)` | `max` | Maximum word count |
333
+ | `.minWords(opts)` | `min` | Minimum word count |
334
+ | `.maxConsecutive(opts)` | `max` | Max consecutive identical chars |
335
+
336
+ ### Transforms (return ZodPipe)
337
+
338
+ | Method | Description |
339
+ |--------|-------------|
340
+ | `.toTitleCase()` | Converts to Title Case |
341
+ | `.toSlug()` | Converts to url-safe-slug |
342
+ | `.stripHtml()` | Removes HTML tags |
343
+ | `.collapseWhitespace()` | Collapses multiple spaces to single |
344
+ | `.emptyToUndefined()` | Converts `""` to `undefined` |
345
+
346
+ ```ts
347
+ import { z } from 'zod'
348
+ import '@validex/core'
349
+
350
+ const schema = z.string().hasUppercase({ min: 2 }).noSpaces().toSlug()
351
+ ```
352
+
353
+ ## Check Functions
354
+
355
+ Pure functions, no Zod dependency. Import from `@validex/core/checks`.
356
+
357
+ ### Composition
358
+
359
+ | Function | Signature | Description |
360
+ |----------|-----------|-------------|
361
+ | `hasUppercase` | `(value: string, min: number, max?: number) => boolean` | Uppercase letter count within `[min, max]` |
362
+ | `hasLowercase` | `(value: string, min: number, max?: number) => boolean` | Lowercase letter count within `[min, max]` |
363
+ | `hasDigits` | `(value: string, min: number, max?: number) => boolean` | Digit count within `[min, max]` |
364
+ | `hasSpecial` | `(value: string, min: number, max?: number) => boolean` | Special character count within `[min, max]` |
365
+
366
+ ### Detection
367
+
368
+ | Function | Signature | Description |
369
+ |----------|-----------|-------------|
370
+ | `containsEmail` | `(value: string) => boolean` | Detects email-like patterns |
371
+ | `containsUrl` | `(value: string) => boolean` | Detects URL-like patterns |
372
+ | `containsHtml` | `(value: string) => boolean` | Detects HTML tags |
373
+ | `containsPhoneNumber` | `(value: string) => Promise<boolean>` | Detects phone numbers (async, uses libphonenumber-js) |
374
+
375
+ ### Restriction
376
+
377
+ | Function | Signature | Description |
378
+ |----------|-----------|-------------|
379
+ | `onlyAlpha` | `(value: string) => boolean` | Every character is a unicode letter |
380
+ | `onlyNumeric` | `(value: string) => boolean` | Every character is a digit |
381
+ | `onlyAlphanumeric` | `(value: string) => boolean` | Every character is a letter or digit |
382
+ | `onlyAlphaSpaceHyphen` | `(value: string) => boolean` | Letters, spaces, hyphens only |
383
+ | `onlyAlphanumericSpaceHyphen` | `(value: string) => boolean` | Letters, digits, spaces, hyphens only |
384
+
385
+ ### Limits
386
+
387
+ | Function | Signature | Description |
388
+ |----------|-----------|-------------|
389
+ | `maxWords` | `(value: string, max: number) => boolean` | At most `max` words |
390
+ | `minWords` | `(value: string, min: number) => boolean` | At least `min` words |
391
+ | `maxConsecutive` | `(value: string, max: number) => boolean` | No character repeats more than `max` times |
392
+ | `noSpaces` | `(value: string) => boolean` | No whitespace characters |
393
+
394
+ ### Transforms
395
+
396
+ | Function | Signature | Description |
397
+ |----------|-----------|-------------|
398
+ | `emptyToUndefined` | `(value: unknown) => unknown` | `""` and `null` to `undefined` |
399
+ | `toTitleCase` | `(value: string) => string` | Title Case with hyphen/apostrophe handling |
400
+ | `toSlug` | `(value: string) => string` | URL-safe slug |
401
+ | `stripHtml` | `(value: string) => string` | Removes HTML tags |
402
+ | `collapseWhitespace` | `(value: string) => string` | Collapses whitespace, trims |
403
+
404
+ ```ts
405
+ import { hasUppercase, containsEmail, toSlug } from '@validex/core/checks'
406
+
407
+ hasUppercase('Hello', 1) // true
408
+ containsEmail('hi@test.com') // true
409
+ toSlug('Hello World!') // 'hello-world'
410
+ ```
411
+
412
+ ## Error Handling
413
+
414
+ ### Error structure
415
+
416
+ Every validex error carries structured metadata via Zod's custom error params:
417
+
418
+ ```ts
419
+ ctx.addIssue({
420
+ code: 'custom',
421
+ params: {
422
+ code: 'disposableBlocked',
423
+ namespace: 'email',
424
+ label: 'Email',
425
+ domain: 'tempmail.com',
426
+ },
427
+ })
428
+ ```
429
+
430
+ ### `getParams(issue)`
431
+
432
+ Extract structured metadata from any Zod issue:
433
+
434
+ ```ts
435
+ import { Email, getParams } from '@validex/core'
436
+
437
+ const schema = Email()
438
+ const result = schema.safeParse('user@tempmail.com')
439
+
440
+ if (!result.success) {
441
+ const params = getParams(result.error.issues[0])
442
+ // { code: 'disposableBlocked', namespace: 'email', label: 'Email',
443
+ // key: 'validation.messages.email.disposableBlocked', path: [], ... }
444
+ }
445
+ ```
446
+
447
+ ### Error code pattern
448
+
449
+ Keys follow: `validation.messages.{namespace}.{code}`
450
+
451
+ - `validation.messages.email.disposableBlocked`
452
+ - `validation.messages.password.commonBlocked`
453
+ - `validation.messages.username.reservedBlocked`
454
+
455
+ ### `validate()` result
456
+
457
+ ```ts
458
+ interface ValidationResult<T> {
459
+ success: boolean
460
+ data?: T // typed parsed data (when success)
461
+ errors: Record<string, readonly string[]> // dot-path to all messages
462
+ firstErrors: Record<string, string> // dot-path to first message
463
+ nestedErrors: NestedErrors // nested object matching schema shape
464
+ issues: ReadonlyArray<unknown> // raw Zod issues (escape hatch)
465
+ }
466
+ ```
467
+
468
+ ## i18n
469
+
470
+ ### Setup
471
+
472
+ ```ts
473
+ import { setup } from '@validex/core'
474
+
475
+ setup({
476
+ i18n: {
477
+ enabled: true,
478
+ prefix: 'validation', // default
479
+ separator: '.', // default
480
+ t: (key, params) => i18next.t(key, params),
481
+ },
482
+ })
483
+ ```
484
+
485
+ ### Key pattern
486
+
487
+ `validation.messages.{namespace}.{code}`
488
+
489
+ When `i18n.enabled` is `true` and `t()` is provided, validex calls `t()` automatically for every error message and field label.
490
+
491
+ ### Label transforms
492
+
493
+ ```ts
494
+ setup({
495
+ label: {
496
+ fallback: 'derived', // 'derived' | 'generic' | 'none'
497
+ transform: ({ path, fieldName, defaultLabel }) => {
498
+ return myLabelLookup(fieldName) ?? defaultLabel
499
+ },
500
+ },
501
+ })
502
+ ```
503
+
504
+ ### CLI
505
+
506
+ ```bash
507
+ npx validex fr de --output ./locales
508
+ npx validex ja --empty --output ./locales
509
+ ```
510
+
511
+ Full guide with all 141 error codes: [Translation Guide](https://github.com/chiptoma/validex/blob/main/docs/I18N.md)
512
+
513
+ ## Custom Rules
514
+
515
+ ### `createRule()`
516
+
517
+ ```ts
518
+ import { createRule } from '@validex/core'
519
+ import { z } from 'zod'
520
+
521
+ interface HexColorOptions {
522
+ label?: string
523
+ emptyToUndefined?: boolean
524
+ normalize?: boolean
525
+ customFn?: (value: string) => true | string | Promise<true | string>
526
+ allowAlpha?: boolean
527
+ }
528
+
529
+ const HexColor = createRule<HexColorOptions>({
530
+ name: 'hexColor',
531
+ defaults: { allowAlpha: false },
532
+ build: (opts) => {
533
+ const pattern = opts.allowAlpha
534
+ ? /^#[\da-f]{6,8}$/i
535
+ : /^#[\da-f]{6}$/i
536
+ return z.string().regex(pattern)
537
+ },
538
+ messages: {
539
+ invalid: '{{label}} is not a valid hex color',
540
+ },
541
+ })
542
+
543
+ const schema = HexColor({ allowAlpha: true })
544
+ schema.parse('#ff00aacc') // OK
545
+ ```
546
+
547
+ ### `customFn`
548
+
549
+ Every rule accepts a `customFn` that runs after built-in checks. Return `true` to pass or a string to fail:
550
+
551
+ ```ts
552
+ import { Email } from '@validex/core'
553
+
554
+ const schema = Email({
555
+ customFn: (value) => value.endsWith('.org') || 'Must be a .org domain',
556
+ })
557
+
558
+ schema.parse('info@example.org') // OK
559
+ schema.parse('info@example.com') // throws — "Must be a .org domain"
560
+ ```
561
+
562
+ ### Custom regex
563
+
564
+ Rules that extend `FormatRuleOptions` (like `Text`) accept a `regex` property:
565
+
566
+ ```ts
567
+ import { Text } from '@validex/core'
568
+
569
+ const schema = Text({
570
+ regex: /^[^<>]+$/,
571
+ })
572
+ ```
573
+
574
+ Full reference: [API Reference](https://github.com/chiptoma/validex/blob/main/docs/API.md)
575
+
576
+ ## Framework Adapters
577
+
578
+ - [@validex/nuxt](https://github.com/chiptoma/validex/tree/main/packages/nuxt) — Nuxt module with auto-imports and `useValidation` composable
579
+ - [@validex/fastify](https://github.com/chiptoma/validex/tree/main/packages/fastify) — Fastify plugin with request validation decorators
580
+
581
+ ## License
582
+
583
+ [MIT](https://github.com/chiptoma/validex/blob/main/LICENSE)
@@ -0,0 +1,51 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});require('../chunk-JEKRBIPN.cjs');
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+ var _chunkOFT3FQPJcjs = require('../chunk-OFT3FQPJ.cjs');
25
+
26
+
27
+ var _chunkLSQNDPFQcjs = require('../chunk-LSQNDPFQ.cjs');
28
+
29
+
30
+
31
+
32
+
33
+
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+
47
+
48
+
49
+
50
+
51
+ exports.collapseWhitespace = _chunkOFT3FQPJcjs.collapseWhitespace; exports.containsEmail = _chunkOFT3FQPJcjs.containsEmail; exports.containsHtml = _chunkOFT3FQPJcjs.containsHtml; exports.containsPhoneNumber = _chunkLSQNDPFQcjs.containsPhoneNumber; exports.containsUrl = _chunkOFT3FQPJcjs.containsUrl; exports.emptyToUndefined = _chunkOFT3FQPJcjs.emptyToUndefined; exports.hasDigits = _chunkOFT3FQPJcjs.hasDigits; exports.hasLowercase = _chunkOFT3FQPJcjs.hasLowercase; exports.hasSpecial = _chunkOFT3FQPJcjs.hasSpecial; exports.hasUppercase = _chunkOFT3FQPJcjs.hasUppercase; exports.maxConsecutive = _chunkOFT3FQPJcjs.maxConsecutive; exports.maxWords = _chunkOFT3FQPJcjs.maxWords; exports.minWords = _chunkOFT3FQPJcjs.minWords; exports.noSpaces = _chunkOFT3FQPJcjs.noSpaces; exports.onlyAlpha = _chunkOFT3FQPJcjs.onlyAlpha; exports.onlyAlphaSpaceHyphen = _chunkOFT3FQPJcjs.onlyAlphaSpaceHyphen; exports.onlyAlphanumeric = _chunkOFT3FQPJcjs.onlyAlphanumeric; exports.onlyAlphanumericSpaceHyphen = _chunkOFT3FQPJcjs.onlyAlphanumericSpaceHyphen; exports.onlyNumeric = _chunkOFT3FQPJcjs.onlyNumeric; exports.stripHtml = _chunkOFT3FQPJcjs.stripHtml; exports.toSlug = _chunkOFT3FQPJcjs.toSlug; exports.toTitleCase = _chunkOFT3FQPJcjs.toTitleCase;