@hy_ong/zod-kit 0.0.4 → 0.0.5

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 (51) hide show
  1. package/.claude/settings.local.json +23 -0
  2. package/LICENSE +21 -0
  3. package/README.md +5 -5
  4. package/debug.js +21 -0
  5. package/debug.ts +16 -0
  6. package/dist/index.cjs +1619 -145
  7. package/dist/index.d.cts +324 -25
  8. package/dist/index.d.ts +324 -25
  9. package/dist/index.js +1590 -143
  10. package/eslint.config.mts +8 -0
  11. package/package.json +10 -9
  12. package/src/config.ts +1 -1
  13. package/src/i18n/locales/en.json +99 -25
  14. package/src/i18n/locales/zh-TW.json +103 -26
  15. package/src/index.ts +13 -7
  16. package/src/validators/common/boolean.ts +97 -0
  17. package/src/validators/common/date.ts +171 -0
  18. package/src/validators/common/email.ts +200 -0
  19. package/src/validators/common/id.ts +259 -0
  20. package/src/validators/common/number.ts +194 -0
  21. package/src/validators/common/password.ts +214 -0
  22. package/src/validators/common/text.ts +151 -0
  23. package/src/validators/common/url.ts +207 -0
  24. package/src/validators/taiwan/business-id.ts +140 -0
  25. package/src/validators/taiwan/fax.ts +182 -0
  26. package/src/validators/taiwan/mobile.ts +110 -0
  27. package/src/validators/taiwan/national-id.ts +208 -0
  28. package/src/validators/taiwan/tel.ts +182 -0
  29. package/tests/common/boolean.test.ts +340 -92
  30. package/tests/common/date.test.ts +458 -0
  31. package/tests/common/email.test.ts +232 -60
  32. package/tests/common/id.test.ts +535 -0
  33. package/tests/common/number.test.ts +230 -60
  34. package/tests/common/password.test.ts +271 -44
  35. package/tests/common/text.test.ts +210 -13
  36. package/tests/common/url.test.ts +492 -67
  37. package/tests/taiwan/business-id.test.ts +240 -0
  38. package/tests/taiwan/fax.test.ts +463 -0
  39. package/tests/taiwan/mobile.test.ts +373 -0
  40. package/tests/taiwan/national-id.test.ts +435 -0
  41. package/tests/taiwan/tel.test.ts +467 -0
  42. package/eslint.config.mjs +0 -10
  43. package/src/common/boolean.ts +0 -36
  44. package/src/common/date.ts +0 -43
  45. package/src/common/email.ts +0 -44
  46. package/src/common/integer.ts +0 -46
  47. package/src/common/number.ts +0 -37
  48. package/src/common/password.ts +0 -33
  49. package/src/common/text.ts +0 -34
  50. package/src/common/url.ts +0 -37
  51. package/tests/common/integer.test.ts +0 -90
@@ -0,0 +1,194 @@
1
+ import { z, ZodNullable, ZodNumber } from "zod"
2
+ import { t } from "../../i18n"
3
+ import { getLocale, type Locale } from "../../config"
4
+
5
+ export type NumberMessages = {
6
+ required?: string
7
+ invalid?: string
8
+ integer?: string
9
+ float?: string
10
+ min?: string
11
+ max?: string
12
+ positive?: string
13
+ negative?: string
14
+ nonNegative?: string
15
+ nonPositive?: string
16
+ multipleOf?: string
17
+ finite?: string
18
+ precision?: string
19
+ }
20
+
21
+ export type NumberOptions<IsRequired extends boolean = true> = {
22
+ required?: IsRequired
23
+ min?: number
24
+ max?: number
25
+ defaultValue?: IsRequired extends true ? number : number | null
26
+ type?: "integer" | "float" | "both"
27
+ positive?: boolean
28
+ negative?: boolean
29
+ nonNegative?: boolean
30
+ nonPositive?: boolean
31
+ multipleOf?: number
32
+ precision?: number
33
+ finite?: boolean
34
+ transform?: (value: number) => number
35
+ parseCommas?: boolean // Parse "1,234" as 1234
36
+ i18n?: Record<Locale, NumberMessages>
37
+ }
38
+
39
+ export type NumberSchema<IsRequired extends boolean> = IsRequired extends true ? ZodNumber : ZodNullable<ZodNumber>
40
+
41
+ export function number<IsRequired extends boolean = true>(options?: NumberOptions<IsRequired>): NumberSchema<IsRequired> {
42
+ const {
43
+ required = true,
44
+ min,
45
+ max,
46
+ defaultValue,
47
+ type = "both",
48
+ positive,
49
+ negative,
50
+ nonNegative,
51
+ nonPositive,
52
+ multipleOf,
53
+ precision,
54
+ finite = true,
55
+ transform,
56
+ parseCommas = false,
57
+ i18n,
58
+ } = options ?? {}
59
+
60
+ // Helper function to get custom message or fallback to default i18n
61
+ const getMessage = (key: keyof NumberMessages, params?: Record<string, any>) => {
62
+ if (i18n) {
63
+ const currentLocale = getLocale()
64
+ const customMessages = i18n[currentLocale]
65
+ if (customMessages && customMessages[key]) {
66
+ const template = customMessages[key]!
67
+ return template.replace(/\$\{(\w+)}/g, (_, k) => params?.[k] ?? "")
68
+ }
69
+ }
70
+ return t(`common.number.${key}`, params)
71
+ }
72
+
73
+ // Set appropriate default value based on required flag
74
+ const actualDefaultValue = defaultValue ?? null
75
+
76
+ const schema = z
77
+ .preprocess(
78
+ (val) => {
79
+ if (val === "" || val === undefined || val === null) {
80
+ return actualDefaultValue
81
+ }
82
+
83
+ // Handle string input
84
+ if (typeof val === "string") {
85
+ let processedVal = val.trim()
86
+
87
+ // Parse comma-separated numbers like "1,234.56"
88
+ if (parseCommas) {
89
+ processedVal = processedVal.replace(/,/g, "")
90
+ }
91
+
92
+ const parsed = Number(processedVal)
93
+
94
+ // Return NaN as is so it can be caught in refine
95
+ if (isNaN(parsed)) {
96
+ return parsed
97
+ }
98
+
99
+ if (transform) {
100
+ return transform(parsed)
101
+ }
102
+
103
+ return parsed
104
+ }
105
+
106
+ // Handle existing numbers (including Infinity)
107
+ if (typeof val === "number") {
108
+ if (transform && Number.isFinite(val)) {
109
+ return transform(val)
110
+ }
111
+ return val
112
+ }
113
+
114
+ return val
115
+ },
116
+ z.union([z.number(), z.null(), z.nan(), z.custom<number>((val) => val === Infinity || val === -Infinity)])
117
+ )
118
+ .refine((val) => {
119
+ // Required check first
120
+ if (required && val === null) {
121
+ throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
122
+ }
123
+
124
+ if (val === null) return true
125
+
126
+ // Type validation for invalid inputs (NaN)
127
+ if (typeof val === "number" && isNaN(val)) {
128
+ if (type === "integer") {
129
+ throw new z.ZodError([{ code: "custom", message: getMessage("integer"), path: [] }])
130
+ } else if (type === "float") {
131
+ throw new z.ZodError([{ code: "custom", message: getMessage("float"), path: [] }])
132
+ } else {
133
+ throw new z.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }])
134
+ }
135
+ }
136
+
137
+ // Invalid number check for non-numbers
138
+ if (typeof val !== "number") {
139
+ throw new z.ZodError([{ code: "custom", message: getMessage("invalid"), path: [] }])
140
+ }
141
+
142
+ // Finite check
143
+ if (finite && !Number.isFinite(val)) {
144
+ throw new z.ZodError([{ code: "custom", message: getMessage("finite"), path: [] }])
145
+ }
146
+
147
+ // Type validation for valid numbers
148
+ if (type === "integer" && !Number.isInteger(val)) {
149
+ throw new z.ZodError([{ code: "custom", message: getMessage("integer"), path: [] }])
150
+ }
151
+ if (type === "float" && Number.isInteger(val)) {
152
+ throw new z.ZodError([{ code: "custom", message: getMessage("float"), path: [] }])
153
+ }
154
+
155
+ // Sign checks (more specific, should come first)
156
+ if (positive && val <= 0) {
157
+ throw new z.ZodError([{ code: "custom", message: getMessage("positive"), path: [] }])
158
+ }
159
+ if (negative && val >= 0) {
160
+ throw new z.ZodError([{ code: "custom", message: getMessage("negative"), path: [] }])
161
+ }
162
+ if (nonNegative && val < 0) {
163
+ throw new z.ZodError([{ code: "custom", message: getMessage("nonNegative"), path: [] }])
164
+ }
165
+ if (nonPositive && val > 0) {
166
+ throw new z.ZodError([{ code: "custom", message: getMessage("nonPositive"), path: [] }])
167
+ }
168
+
169
+ // Range checks
170
+ if (min !== undefined && val < min) {
171
+ throw new z.ZodError([{ code: "custom", message: getMessage("min", { min }), path: [] }])
172
+ }
173
+ if (max !== undefined && val > max) {
174
+ throw new z.ZodError([{ code: "custom", message: getMessage("max", { max }), path: [] }])
175
+ }
176
+
177
+ // Multiple of check
178
+ if (multipleOf !== undefined && val % multipleOf !== 0) {
179
+ throw new z.ZodError([{ code: "custom", message: getMessage("multipleOf", { multipleOf }), path: [] }])
180
+ }
181
+
182
+ // Precision check
183
+ if (precision !== undefined) {
184
+ const decimalPlaces = (val.toString().split(".")[1] || "").length
185
+ if (decimalPlaces > precision) {
186
+ throw new z.ZodError([{ code: "custom", message: getMessage("precision", { precision }), path: [] }])
187
+ }
188
+ }
189
+
190
+ return true
191
+ })
192
+
193
+ return schema as unknown as NumberSchema<IsRequired>
194
+ }
@@ -0,0 +1,214 @@
1
+ import { z, ZodNullable, ZodString } from "zod"
2
+ import { t } from "../../i18n"
3
+ import { getLocale, type Locale } from "../../config"
4
+
5
+ export type PasswordMessages = {
6
+ required?: string
7
+ min?: string
8
+ max?: string
9
+ uppercase?: string
10
+ lowercase?: string
11
+ digits?: string
12
+ special?: string
13
+ noRepeating?: string
14
+ noSequential?: string
15
+ noCommonWords?: string
16
+ minStrength?: string
17
+ excludes?: string
18
+ includes?: string
19
+ invalid?: string
20
+ }
21
+
22
+ export type PasswordStrength = "weak" | "medium" | "strong" | "very-strong"
23
+
24
+ export type PasswordOptions<IsRequired extends boolean = true> = {
25
+ required?: IsRequired
26
+ min?: number
27
+ max?: number
28
+ uppercase?: boolean
29
+ lowercase?: boolean
30
+ digits?: boolean
31
+ special?: boolean
32
+ noRepeating?: boolean
33
+ noSequential?: boolean
34
+ noCommonWords?: boolean
35
+ minStrength?: PasswordStrength
36
+ excludes?: string | string[]
37
+ includes?: string
38
+ regex?: RegExp
39
+ transform?: (value: string) => string
40
+ defaultValue?: IsRequired extends true ? string : string | null
41
+ i18n?: Record<Locale, PasswordMessages>
42
+ }
43
+
44
+ export type PasswordSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
45
+
46
+ // Common weak passwords to check against
47
+ const COMMON_PASSWORDS = [
48
+ "password",
49
+ "123456",
50
+ "123456789",
51
+ "12345678",
52
+ "12345",
53
+ "1234567",
54
+ "admin",
55
+ "qwerty",
56
+ "abc123",
57
+ "password123",
58
+ "letmein",
59
+ "welcome",
60
+ "monkey",
61
+ "dragon",
62
+ "sunshine",
63
+ "princess",
64
+ ]
65
+
66
+ // Helper function to calculate password strength
67
+ const calculatePasswordStrength = (password: string): PasswordStrength => {
68
+ let score = 0
69
+
70
+ // Length bonus
71
+ if (password.length >= 8) score += 1
72
+ if (password.length >= 12) score += 1
73
+ if (password.length >= 16) score += 1
74
+
75
+ // Character variety
76
+ if (/[a-z]/.test(password)) score += 1
77
+ if (/[A-Z]/.test(password)) score += 1
78
+ if (/[0-9]/.test(password)) score += 1
79
+ if (/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password)) score += 1
80
+
81
+ // Deductions
82
+ if (/(.)\1{2,}/.test(password)) score -= 1 // Repeating characters
83
+ if (/(?:abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz|012|123|234|345|456|567|678|789)/i.test(password)) score -= 1 // Sequential
84
+
85
+ if (score <= 2) return "weak"
86
+ if (score <= 4) return "medium"
87
+ if (score <= 6) return "strong"
88
+ return "very-strong"
89
+ }
90
+
91
+ export function password<IsRequired extends boolean = true>(options?: PasswordOptions<IsRequired>): PasswordSchema<IsRequired> {
92
+ const {
93
+ required = true,
94
+ min,
95
+ max,
96
+ uppercase,
97
+ lowercase,
98
+ digits,
99
+ special,
100
+ noRepeating,
101
+ noSequential,
102
+ noCommonWords,
103
+ minStrength,
104
+ excludes,
105
+ includes,
106
+ regex,
107
+ transform,
108
+ defaultValue,
109
+ i18n,
110
+ } = options ?? {}
111
+
112
+ // Set appropriate default value based on required flag
113
+ const actualDefaultValue = defaultValue ?? (required ? "" : null)
114
+
115
+ // Helper function to get custom message or fallback to default i18n
116
+ const getMessage = (key: keyof PasswordMessages, params?: Record<string, any>) => {
117
+ if (i18n) {
118
+ const currentLocale = getLocale()
119
+ const customMessages = i18n[currentLocale]
120
+ if (customMessages && customMessages[key]) {
121
+ const template = customMessages[key]!
122
+ return template.replace(/\$\{(\w+)}/g, (_, k) => params?.[k] ?? "")
123
+ }
124
+ }
125
+ return t(`common.password.${key}`, params)
126
+ }
127
+
128
+ // Preprocessing function
129
+ const preprocessFn = (val: unknown) => {
130
+ if (val === "" || val === null || val === undefined) {
131
+ return actualDefaultValue
132
+ }
133
+
134
+ let processed = String(val)
135
+ if (transform) {
136
+ processed = transform(processed)
137
+ }
138
+
139
+ return processed
140
+ }
141
+
142
+ const baseSchema = required ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
143
+
144
+ const schema = baseSchema.refine((val) => {
145
+ if (val === null) return true
146
+
147
+ // Required check
148
+ if (required && (val === "" || val === "null" || val === "undefined")) {
149
+ throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
150
+ }
151
+
152
+ // Length checks
153
+ if (val !== null && min !== undefined && val.length < min) {
154
+ throw new z.ZodError([{ code: "custom", message: getMessage("min", { min }), path: [] }])
155
+ }
156
+ if (val !== null && max !== undefined && val.length > max) {
157
+ throw new z.ZodError([{ code: "custom", message: getMessage("max", { max }), path: [] }])
158
+ }
159
+
160
+ // Character requirements
161
+ if (val !== null && uppercase && !/[A-Z]/.test(val)) {
162
+ throw new z.ZodError([{ code: "custom", message: getMessage("uppercase"), path: [] }])
163
+ }
164
+ if (val !== null && lowercase && !/[a-z]/.test(val)) {
165
+ throw new z.ZodError([{ code: "custom", message: getMessage("lowercase"), path: [] }])
166
+ }
167
+ if (val !== null && digits && !/[0-9]/.test(val)) {
168
+ throw new z.ZodError([{ code: "custom", message: getMessage("digits"), path: [] }])
169
+ }
170
+ if (val !== null && special && !/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(val)) {
171
+ throw new z.ZodError([{ code: "custom", message: getMessage("special"), path: [] }])
172
+ }
173
+
174
+ // Advanced security checks
175
+ if (val !== null && noRepeating && /(.)\1{2,}/.test(val)) {
176
+ throw new z.ZodError([{ code: "custom", message: getMessage("noRepeating"), path: [] }])
177
+ }
178
+ if (val !== null && noSequential && /(?:abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz|012|123|234|345|456|567|678|789)/i.test(val)) {
179
+ throw new z.ZodError([{ code: "custom", message: getMessage("noSequential"), path: [] }])
180
+ }
181
+ if (val !== null && noCommonWords && COMMON_PASSWORDS.some((common) => val.toLowerCase().includes(common.toLowerCase()))) {
182
+ throw new z.ZodError([{ code: "custom", message: getMessage("noCommonWords"), path: [] }])
183
+ }
184
+ if (val !== null && minStrength) {
185
+ const strength = calculatePasswordStrength(val)
186
+ const strengthLevels = ["weak", "medium", "strong", "very-strong"]
187
+ const currentLevel = strengthLevels.indexOf(strength)
188
+ const requiredLevel = strengthLevels.indexOf(minStrength)
189
+ if (currentLevel < requiredLevel) {
190
+ throw new z.ZodError([{ code: "custom", message: getMessage("minStrength", { minStrength }), path: [] }])
191
+ }
192
+ }
193
+
194
+ // Content checks
195
+ if (val !== null && includes !== undefined && !val.includes(includes)) {
196
+ throw new z.ZodError([{ code: "custom", message: getMessage("includes", { includes }), path: [] }])
197
+ }
198
+ if (val !== null && excludes !== undefined) {
199
+ const excludeList = Array.isArray(excludes) ? excludes : [excludes]
200
+ for (const exclude of excludeList) {
201
+ if (val.includes(exclude)) {
202
+ throw new z.ZodError([{ code: "custom", message: getMessage("excludes", { excludes: exclude }), path: [] }])
203
+ }
204
+ }
205
+ }
206
+ if (val !== null && regex !== undefined && !regex.test(val)) {
207
+ throw new z.ZodError([{ code: "custom", message: getMessage("invalid", { regex }), path: [] }])
208
+ }
209
+
210
+ return true
211
+ })
212
+
213
+ return schema as unknown as PasswordSchema<IsRequired>
214
+ }
@@ -0,0 +1,151 @@
1
+ import { z, ZodNullable, ZodString } from "zod"
2
+ import { t } from "../../i18n"
3
+ import { getLocale, type Locale } from "../../config"
4
+
5
+ export type TextMessages = {
6
+ required?: string
7
+ notEmpty?: string
8
+ minLength?: string
9
+ maxLength?: string
10
+ startsWith?: string
11
+ endsWith?: string
12
+ includes?: string
13
+ excludes?: string
14
+ invalid?: string
15
+ }
16
+
17
+ export type TextOptions<IsRequired extends boolean = true> = {
18
+ required?: IsRequired
19
+ minLength?: number
20
+ maxLength?: number
21
+ startsWith?: string
22
+ endsWith?: string
23
+ includes?: string
24
+ excludes?: string | string[]
25
+ regex?: RegExp
26
+ trimMode?: "trim" | "trimStart" | "trimEnd" | "none"
27
+ casing?: "upper" | "lower" | "title" | "none"
28
+ transform?: (value: string) => string
29
+ notEmpty?: boolean
30
+ defaultValue?: IsRequired extends true ? string : string | null
31
+ i18n?: Record<Locale, TextMessages>
32
+ }
33
+
34
+ export type TextSchema<IsRequired extends boolean> = IsRequired extends true ? ZodString : ZodNullable<ZodString>
35
+
36
+ export function text<IsRequired extends boolean = true>(options?: TextOptions<IsRequired>): TextSchema<IsRequired> {
37
+ const { required = true, minLength, maxLength, startsWith, endsWith, includes, excludes, regex, trimMode = "trim", casing = "none", transform, notEmpty, defaultValue, i18n } = options ?? {}
38
+
39
+ // Set appropriate default value based on required flag
40
+ const actualDefaultValue = defaultValue ?? (required ? "" : null)
41
+
42
+ // Helper function to get custom message or fallback to default i18n
43
+ const getMessage = (key: keyof TextMessages, params?: Record<string, any>) => {
44
+ if (i18n) {
45
+ const currentLocale = getLocale()
46
+ const customMessages = i18n[currentLocale]
47
+ if (customMessages && customMessages[key]) {
48
+ const template = customMessages[key]!
49
+ return template.replace(/\$\{(\w+)}/g, (_, k) => params?.[k] ?? "")
50
+ }
51
+ }
52
+ return t(`common.text.${key}`, params)
53
+ }
54
+
55
+ // Helper functions to apply trimming based on the mode
56
+ const applyTrim = (str: string): string => {
57
+ switch (trimMode) {
58
+ case "trimStart":
59
+ return str.trimStart()
60
+ case "trimEnd":
61
+ return str.trimEnd()
62
+ case "none":
63
+ return str
64
+ default:
65
+ return str.trim()
66
+ }
67
+ }
68
+
69
+ // Helper function to apply casing
70
+ const applyCasing = (str: string): string => {
71
+ switch (casing) {
72
+ case "upper":
73
+ return str.toUpperCase()
74
+ case "lower":
75
+ return str.toLowerCase()
76
+ case "title":
77
+ return str.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substring(1).toLowerCase())
78
+ default:
79
+ return str
80
+ }
81
+ }
82
+
83
+ // Preprocessing function with optimized transformations
84
+ const preprocessFn = (val: unknown) => {
85
+ if (val === "" || val === null || val === undefined) {
86
+ return actualDefaultValue
87
+ }
88
+
89
+ let processed = String(val)
90
+ processed = applyTrim(processed)
91
+ processed = applyCasing(processed)
92
+
93
+ if (transform) {
94
+ processed = transform(processed)
95
+ }
96
+
97
+ return processed
98
+ }
99
+
100
+ const baseSchema = required ? z.preprocess(preprocessFn, z.string()) : z.preprocess(preprocessFn, z.string().nullable())
101
+
102
+ // Single refine with all validations for better performance
103
+ const schema = baseSchema.refine((val) => {
104
+ if (val === null) return true
105
+
106
+ // Required check
107
+ if (required && (val === "" || val === "null" || val === "undefined")) {
108
+ throw new z.ZodError([{ code: "custom", message: getMessage("required"), path: [] }])
109
+ }
110
+
111
+ // Not empty check (different from required - checks whitespace)
112
+ // For notEmpty, we need to check if the original string (before processing) was only whitespace
113
+ if (notEmpty && val !== null && val.trim() === "") {
114
+ throw new z.ZodError([{ code: "custom", message: getMessage("notEmpty"), path: [] }])
115
+ }
116
+
117
+ // Length checks
118
+ if (val !== null && minLength !== undefined && val.length < minLength) {
119
+ throw new z.ZodError([{ code: "custom", message: getMessage("minLength", { minLength }), path: [] }])
120
+ }
121
+ if (val !== null && maxLength !== undefined && val.length > maxLength) {
122
+ throw new z.ZodError([{ code: "custom", message: getMessage("maxLength", { maxLength }), path: [] }])
123
+ }
124
+
125
+ // String content checks
126
+ if (val !== null && startsWith !== undefined && !val.startsWith(startsWith)) {
127
+ throw new z.ZodError([{ code: "custom", message: getMessage("startsWith", { startsWith }), path: [] }])
128
+ }
129
+ if (val !== null && endsWith !== undefined && !val.endsWith(endsWith)) {
130
+ throw new z.ZodError([{ code: "custom", message: getMessage("endsWith", { endsWith }), path: [] }])
131
+ }
132
+ if (val !== null && includes !== undefined && !val.includes(includes)) {
133
+ throw new z.ZodError([{ code: "custom", message: getMessage("includes", { includes }), path: [] }])
134
+ }
135
+ if (val !== null && excludes !== undefined) {
136
+ const excludeList = Array.isArray(excludes) ? excludes : [excludes]
137
+ for (const exclude of excludeList) {
138
+ if (val.includes(exclude)) {
139
+ throw new z.ZodError([{ code: "custom", message: getMessage("excludes", { excludes: exclude }), path: [] }])
140
+ }
141
+ }
142
+ }
143
+ if (val !== null && regex !== undefined && !regex.test(val)) {
144
+ throw new z.ZodError([{ code: "custom", message: getMessage("invalid", { regex }), path: [] }])
145
+ }
146
+
147
+ return true
148
+ })
149
+
150
+ return schema as unknown as TextSchema<IsRequired>
151
+ }