react-formesh 0.1.4
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/README.md +191 -0
- package/dist/array.cjs +4 -0
- package/dist/array.cjs.map +1 -0
- package/dist/array.d.cts +2 -0
- package/dist/array.d.ts +2 -0
- package/dist/array.js +3 -0
- package/dist/array.js.map +1 -0
- package/dist/field-CLlwd7BC.d.ts +134 -0
- package/dist/field-DvWKUaTD.d.cts +134 -0
- package/dist/file.cjs +4 -0
- package/dist/file.cjs.map +1 -0
- package/dist/file.d.cts +2 -0
- package/dist/file.d.ts +2 -0
- package/dist/file.js +3 -0
- package/dist/file.js.map +1 -0
- package/dist/index.cjs +431 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +209 -0
- package/dist/index.d.ts +209 -0
- package/dist/index.js +419 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +487 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +144 -0
- package/dist/react.d.ts +144 -0
- package/dist/react.js +482 -0
- package/dist/react.js.map +1 -0
- package/dist/validation-gIT3Xn9h.d.cts +125 -0
- package/dist/validation-gIT3Xn9h.d.ts +125 -0
- package/dist/validators.cjs +99 -0
- package/dist/validators.cjs.map +1 -0
- package/dist/validators.d.cts +20 -0
- package/dist/validators.d.ts +20 -0
- package/dist/validators.js +89 -0
- package/dist/validators.js.map +1 -0
- package/package.json +70 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A form's values are always a plain, serializable-shaped JS object.
|
|
3
|
+
* Nesting is allowed (and is how multi-section forms compose), but every
|
|
4
|
+
* leaf is a value the consumer owns — the store never wraps values in
|
|
5
|
+
* framework-specific containers.
|
|
6
|
+
*/
|
|
7
|
+
type FormValues = Record<string, unknown>;
|
|
8
|
+
/**
|
|
9
|
+
* A dot-separated path into a (possibly nested) FormValues object.
|
|
10
|
+
* e.g. "employeeInfo.firstName"
|
|
11
|
+
*
|
|
12
|
+
* Kept as a plain `string` (not a template-literal-typed path) in Phase 1.
|
|
13
|
+
* Full compile-time path inference is a Phase 6 (DX) concern — see the
|
|
14
|
+
* project roadmap's note on avoiding excessive type complexity before the
|
|
15
|
+
* runtime behavior it would describe actually exists.
|
|
16
|
+
*/
|
|
17
|
+
type FieldPath = string;
|
|
18
|
+
/**
|
|
19
|
+
* A listener is notified after a change with the fields that actually
|
|
20
|
+
* changed (as dot-paths), so subscribers can decide for themselves whether
|
|
21
|
+
* a change is relevant to them. The store computes this list; individual
|
|
22
|
+
* hooks decide what to do with it (this is what makes fine-grained
|
|
23
|
+
* subscription possible without the store knowing about React).
|
|
24
|
+
*/
|
|
25
|
+
type StoreListener = (changedPaths: readonly FieldPath[]) => void;
|
|
26
|
+
type Unsubscribe = () => void;
|
|
27
|
+
/**
|
|
28
|
+
* Callback for `watch(path, listener)`. Fires only when the value at the
|
|
29
|
+
* watched path actually changed (by deep equality, matching `deepEqual`),
|
|
30
|
+
* receiving the new value and the value immediately before the change. It
|
|
31
|
+
* deliberately does NOT fire on subscribe — consumers who need an initial
|
|
32
|
+
* derivation can compute it during render from `getValues()`.
|
|
33
|
+
*/
|
|
34
|
+
type WatchListener = (value: unknown, previousValue: unknown) => void;
|
|
35
|
+
interface FormStore<TValues extends FormValues = FormValues> {
|
|
36
|
+
/**
|
|
37
|
+
* Returns the current values object. Never mutated in place.
|
|
38
|
+
*
|
|
39
|
+
* NOTE: the members of this interface are deliberately declared with
|
|
40
|
+
* *method syntax* (`getValues()`, not `getValues: () => ...`). Under
|
|
41
|
+
* `strictFunctionTypes`, property-syntax function members are checked
|
|
42
|
+
* contravariantly, which makes `FormStore<SomeTypedShape>` NOT assignable
|
|
43
|
+
* to `FormStore<FormValues>` — silently breaking every composition that
|
|
44
|
+
* hands a typed store/section/sync wrapper to something expecting the
|
|
45
|
+
* loose `FormValues` shape (hooks, `createFormSection`,
|
|
46
|
+
* `createDebouncedSync`). Method syntax restores bivariance for these
|
|
47
|
+
* members, which is exactly what the structural composability story
|
|
48
|
+
* (store → section → debounced sync → React hooks) relies on.
|
|
49
|
+
*/
|
|
50
|
+
getValues(): TValues;
|
|
51
|
+
/** Reads a single value by dot-path. Returns `undefined` if not present. */
|
|
52
|
+
getValue(path: FieldPath): unknown;
|
|
53
|
+
/** Writes a single value by dot-path, notifying relevant subscribers. */
|
|
54
|
+
setValue(path: FieldPath, value: unknown): void;
|
|
55
|
+
/**
|
|
56
|
+
* Merges a partial values object into the store (shallow per top-level
|
|
57
|
+
* key, matching how independent sections merge into a parent form).
|
|
58
|
+
*/
|
|
59
|
+
setValues(partial: Partial<TValues>): void;
|
|
60
|
+
/** Restores the store to its initial values (or a new baseline, if given). */
|
|
61
|
+
reset(nextInitialValues?: TValues): void;
|
|
62
|
+
/**
|
|
63
|
+
* Subscribes to store changes. The listener fires after every commit with
|
|
64
|
+
* the list of dot-paths that changed. Returns an unsubscribe function.
|
|
65
|
+
*/
|
|
66
|
+
subscribe(listener: StoreListener): Unsubscribe;
|
|
67
|
+
/**
|
|
68
|
+
* Observes one path (including its whole subtree) across commits,
|
|
69
|
+
* invoking `listener(value, previousValue)` only when the value actually
|
|
70
|
+
* changed (deep equality). Returns an unsubscribe function.
|
|
71
|
+
*/
|
|
72
|
+
watch(path: FieldPath, listener: WatchListener): Unsubscribe;
|
|
73
|
+
/** The values the store was created with, or last reset to. */
|
|
74
|
+
getInitialValues(): TValues;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Context handed to every validator alongside the value being checked.
|
|
79
|
+
* `values` is the complete values object in scope (whole store or section),
|
|
80
|
+
* which is what cross-field rules compare against — e.g. `matches`
|
|
81
|
+
* (confirm-password) or a form-level date-range check.
|
|
82
|
+
*/
|
|
83
|
+
interface ValidationContext {
|
|
84
|
+
/** The dot-path of the field being validated (scope-relative). */
|
|
85
|
+
path: FieldPath;
|
|
86
|
+
/** The complete values object in scope at validation time. */
|
|
87
|
+
values: FormValues;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A validator returns an error message string when the value is invalid,
|
|
91
|
+
* and `undefined`/`null`/nothing when it is valid. Pure functions of
|
|
92
|
+
* (value, context) — no side effects, no async (debounced async validation
|
|
93
|
+
* is a later-phase concern built on top of this).
|
|
94
|
+
*/
|
|
95
|
+
type Validator = (value: unknown, context: ValidationContext) => string | null | undefined | void;
|
|
96
|
+
/** One field's rules: a single validator or a list (first failure wins). */
|
|
97
|
+
type FieldRules = Validator | readonly Validator[];
|
|
98
|
+
/**
|
|
99
|
+
* A form-level validator receives the whole values object in scope and
|
|
100
|
+
* returns a partial errors map (paths → messages) — for rules no single
|
|
101
|
+
* field can express: date ranges, start ≤ end, budget caps, etc.
|
|
102
|
+
*/
|
|
103
|
+
type FormLevelValidator = (values: FormValues) => Record<string, string> | null | undefined | void;
|
|
104
|
+
interface ValidationSchema {
|
|
105
|
+
/**
|
|
106
|
+
* Per-field rules, keyed by dot-path (scope-relative; nested paths like
|
|
107
|
+
* "employee.firstName" work). A path may map to one validator or a list —
|
|
108
|
+
* for lists, the first failing rule's message is used.
|
|
109
|
+
*/
|
|
110
|
+
fields?: Record<string, FieldRules>;
|
|
111
|
+
/**
|
|
112
|
+
* Form-level validator(s) run after field rules. Their errors are merged
|
|
113
|
+
* in for paths that don't already have a field-level error, so the more
|
|
114
|
+
* specific per-field message wins.
|
|
115
|
+
*/
|
|
116
|
+
form?: FormLevelValidator | readonly FormLevelValidator[];
|
|
117
|
+
}
|
|
118
|
+
interface ValidationResult {
|
|
119
|
+
/** Errors keyed by field path; empty object when everything validates. */
|
|
120
|
+
errors: Record<string, string>;
|
|
121
|
+
/** `true` when `errors` is empty. */
|
|
122
|
+
isValid: boolean;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type { FormValues as F, StoreListener as S, Unsubscribe as U, Validator as V, WatchListener as W, FieldPath as a, ValidationResult as b, ValidationSchema as c, FormStore as d, FieldRules as e, FormLevelValidator as f, ValidationContext as g };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A form's values are always a plain, serializable-shaped JS object.
|
|
3
|
+
* Nesting is allowed (and is how multi-section forms compose), but every
|
|
4
|
+
* leaf is a value the consumer owns — the store never wraps values in
|
|
5
|
+
* framework-specific containers.
|
|
6
|
+
*/
|
|
7
|
+
type FormValues = Record<string, unknown>;
|
|
8
|
+
/**
|
|
9
|
+
* A dot-separated path into a (possibly nested) FormValues object.
|
|
10
|
+
* e.g. "employeeInfo.firstName"
|
|
11
|
+
*
|
|
12
|
+
* Kept as a plain `string` (not a template-literal-typed path) in Phase 1.
|
|
13
|
+
* Full compile-time path inference is a Phase 6 (DX) concern — see the
|
|
14
|
+
* project roadmap's note on avoiding excessive type complexity before the
|
|
15
|
+
* runtime behavior it would describe actually exists.
|
|
16
|
+
*/
|
|
17
|
+
type FieldPath = string;
|
|
18
|
+
/**
|
|
19
|
+
* A listener is notified after a change with the fields that actually
|
|
20
|
+
* changed (as dot-paths), so subscribers can decide for themselves whether
|
|
21
|
+
* a change is relevant to them. The store computes this list; individual
|
|
22
|
+
* hooks decide what to do with it (this is what makes fine-grained
|
|
23
|
+
* subscription possible without the store knowing about React).
|
|
24
|
+
*/
|
|
25
|
+
type StoreListener = (changedPaths: readonly FieldPath[]) => void;
|
|
26
|
+
type Unsubscribe = () => void;
|
|
27
|
+
/**
|
|
28
|
+
* Callback for `watch(path, listener)`. Fires only when the value at the
|
|
29
|
+
* watched path actually changed (by deep equality, matching `deepEqual`),
|
|
30
|
+
* receiving the new value and the value immediately before the change. It
|
|
31
|
+
* deliberately does NOT fire on subscribe — consumers who need an initial
|
|
32
|
+
* derivation can compute it during render from `getValues()`.
|
|
33
|
+
*/
|
|
34
|
+
type WatchListener = (value: unknown, previousValue: unknown) => void;
|
|
35
|
+
interface FormStore<TValues extends FormValues = FormValues> {
|
|
36
|
+
/**
|
|
37
|
+
* Returns the current values object. Never mutated in place.
|
|
38
|
+
*
|
|
39
|
+
* NOTE: the members of this interface are deliberately declared with
|
|
40
|
+
* *method syntax* (`getValues()`, not `getValues: () => ...`). Under
|
|
41
|
+
* `strictFunctionTypes`, property-syntax function members are checked
|
|
42
|
+
* contravariantly, which makes `FormStore<SomeTypedShape>` NOT assignable
|
|
43
|
+
* to `FormStore<FormValues>` — silently breaking every composition that
|
|
44
|
+
* hands a typed store/section/sync wrapper to something expecting the
|
|
45
|
+
* loose `FormValues` shape (hooks, `createFormSection`,
|
|
46
|
+
* `createDebouncedSync`). Method syntax restores bivariance for these
|
|
47
|
+
* members, which is exactly what the structural composability story
|
|
48
|
+
* (store → section → debounced sync → React hooks) relies on.
|
|
49
|
+
*/
|
|
50
|
+
getValues(): TValues;
|
|
51
|
+
/** Reads a single value by dot-path. Returns `undefined` if not present. */
|
|
52
|
+
getValue(path: FieldPath): unknown;
|
|
53
|
+
/** Writes a single value by dot-path, notifying relevant subscribers. */
|
|
54
|
+
setValue(path: FieldPath, value: unknown): void;
|
|
55
|
+
/**
|
|
56
|
+
* Merges a partial values object into the store (shallow per top-level
|
|
57
|
+
* key, matching how independent sections merge into a parent form).
|
|
58
|
+
*/
|
|
59
|
+
setValues(partial: Partial<TValues>): void;
|
|
60
|
+
/** Restores the store to its initial values (or a new baseline, if given). */
|
|
61
|
+
reset(nextInitialValues?: TValues): void;
|
|
62
|
+
/**
|
|
63
|
+
* Subscribes to store changes. The listener fires after every commit with
|
|
64
|
+
* the list of dot-paths that changed. Returns an unsubscribe function.
|
|
65
|
+
*/
|
|
66
|
+
subscribe(listener: StoreListener): Unsubscribe;
|
|
67
|
+
/**
|
|
68
|
+
* Observes one path (including its whole subtree) across commits,
|
|
69
|
+
* invoking `listener(value, previousValue)` only when the value actually
|
|
70
|
+
* changed (deep equality). Returns an unsubscribe function.
|
|
71
|
+
*/
|
|
72
|
+
watch(path: FieldPath, listener: WatchListener): Unsubscribe;
|
|
73
|
+
/** The values the store was created with, or last reset to. */
|
|
74
|
+
getInitialValues(): TValues;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Context handed to every validator alongside the value being checked.
|
|
79
|
+
* `values` is the complete values object in scope (whole store or section),
|
|
80
|
+
* which is what cross-field rules compare against — e.g. `matches`
|
|
81
|
+
* (confirm-password) or a form-level date-range check.
|
|
82
|
+
*/
|
|
83
|
+
interface ValidationContext {
|
|
84
|
+
/** The dot-path of the field being validated (scope-relative). */
|
|
85
|
+
path: FieldPath;
|
|
86
|
+
/** The complete values object in scope at validation time. */
|
|
87
|
+
values: FormValues;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A validator returns an error message string when the value is invalid,
|
|
91
|
+
* and `undefined`/`null`/nothing when it is valid. Pure functions of
|
|
92
|
+
* (value, context) — no side effects, no async (debounced async validation
|
|
93
|
+
* is a later-phase concern built on top of this).
|
|
94
|
+
*/
|
|
95
|
+
type Validator = (value: unknown, context: ValidationContext) => string | null | undefined | void;
|
|
96
|
+
/** One field's rules: a single validator or a list (first failure wins). */
|
|
97
|
+
type FieldRules = Validator | readonly Validator[];
|
|
98
|
+
/**
|
|
99
|
+
* A form-level validator receives the whole values object in scope and
|
|
100
|
+
* returns a partial errors map (paths → messages) — for rules no single
|
|
101
|
+
* field can express: date ranges, start ≤ end, budget caps, etc.
|
|
102
|
+
*/
|
|
103
|
+
type FormLevelValidator = (values: FormValues) => Record<string, string> | null | undefined | void;
|
|
104
|
+
interface ValidationSchema {
|
|
105
|
+
/**
|
|
106
|
+
* Per-field rules, keyed by dot-path (scope-relative; nested paths like
|
|
107
|
+
* "employee.firstName" work). A path may map to one validator or a list —
|
|
108
|
+
* for lists, the first failing rule's message is used.
|
|
109
|
+
*/
|
|
110
|
+
fields?: Record<string, FieldRules>;
|
|
111
|
+
/**
|
|
112
|
+
* Form-level validator(s) run after field rules. Their errors are merged
|
|
113
|
+
* in for paths that don't already have a field-level error, so the more
|
|
114
|
+
* specific per-field message wins.
|
|
115
|
+
*/
|
|
116
|
+
form?: FormLevelValidator | readonly FormLevelValidator[];
|
|
117
|
+
}
|
|
118
|
+
interface ValidationResult {
|
|
119
|
+
/** Errors keyed by field path; empty object when everything validates. */
|
|
120
|
+
errors: Record<string, string>;
|
|
121
|
+
/** `true` when `errors` is empty. */
|
|
122
|
+
isValid: boolean;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type { FormValues as F, StoreListener as S, Unsubscribe as U, Validator as V, WatchListener as W, FieldPath as a, ValidationResult as b, ValidationSchema as c, FormStore as d, FieldRules as e, FormLevelValidator as f, ValidationContext as g };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/core/utils/deepEqual.ts
|
|
4
|
+
function deepEqual(a, b) {
|
|
5
|
+
if (Object.is(a, b)) return true;
|
|
6
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
7
|
+
if (a.length !== b.length) return false;
|
|
8
|
+
return a.every((item, index) => deepEqual(item, b[index]));
|
|
9
|
+
}
|
|
10
|
+
const aIsPlainObject = typeof a === "object" && a !== null && !Array.isArray(a) && a.constructor === Object;
|
|
11
|
+
const bIsPlainObject = typeof b === "object" && b !== null && !Array.isArray(b) && b.constructor === Object;
|
|
12
|
+
if (aIsPlainObject && bIsPlainObject) {
|
|
13
|
+
const aKeys = Object.keys(a);
|
|
14
|
+
const bKeys = Object.keys(b);
|
|
15
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
16
|
+
return aKeys.every(
|
|
17
|
+
(key) => deepEqual(a[key], b[key])
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/core/utils/paths.ts
|
|
24
|
+
function splitPath(path) {
|
|
25
|
+
return path.split(".").filter(Boolean);
|
|
26
|
+
}
|
|
27
|
+
function getAtPath(source, path) {
|
|
28
|
+
const segments = splitPath(path);
|
|
29
|
+
let current = source;
|
|
30
|
+
for (const segment of segments) {
|
|
31
|
+
if (current === null || typeof current !== "object") {
|
|
32
|
+
return void 0;
|
|
33
|
+
}
|
|
34
|
+
current = current[segment];
|
|
35
|
+
}
|
|
36
|
+
return current;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/validators.ts
|
|
40
|
+
function required(message = "This field is required.") {
|
|
41
|
+
return (value) => {
|
|
42
|
+
if (value === void 0 || value === null) return message;
|
|
43
|
+
if (typeof value === "string" && value.trim() === "") return message;
|
|
44
|
+
if (Array.isArray(value) && value.length === 0) return message;
|
|
45
|
+
return void 0;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function minLength(limit, message) {
|
|
49
|
+
const msg = message ?? `Must be at least ${limit} characters.`;
|
|
50
|
+
return (value) => {
|
|
51
|
+
if (value === void 0 || value === null) return void 0;
|
|
52
|
+
const length = typeof value === "string" || Array.isArray(value) ? value.length : void 0;
|
|
53
|
+
return length !== void 0 && length < limit ? msg : void 0;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function maxLength(limit, message) {
|
|
57
|
+
const msg = message ?? `Must be at most ${limit} characters.`;
|
|
58
|
+
return (value) => {
|
|
59
|
+
if (value === void 0 || value === null) return void 0;
|
|
60
|
+
const length = typeof value === "string" || Array.isArray(value) ? value.length : void 0;
|
|
61
|
+
return length !== void 0 && length > limit ? msg : void 0;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function pattern(regex, message = "Invalid format.") {
|
|
65
|
+
return (value) => typeof value === "string" && value !== "" && !regex.test(value) ? message : void 0;
|
|
66
|
+
}
|
|
67
|
+
function email(message = "Enter a valid email address.") {
|
|
68
|
+
return pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, message);
|
|
69
|
+
}
|
|
70
|
+
function min(bound, message) {
|
|
71
|
+
const msg = message ?? `Must be ${bound} or more.`;
|
|
72
|
+
return (value) => typeof value === "number" && Number.isFinite(value) && value < bound ? msg : void 0;
|
|
73
|
+
}
|
|
74
|
+
function max(bound, message) {
|
|
75
|
+
const msg = message ?? `Must be ${bound} or less.`;
|
|
76
|
+
return (value) => typeof value === "number" && Number.isFinite(value) && value > bound ? msg : void 0;
|
|
77
|
+
}
|
|
78
|
+
function oneOf(allowed, message = "Invalid selection.") {
|
|
79
|
+
return (value) => {
|
|
80
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
81
|
+
return allowed.includes(value) ? void 0 : message;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function matches(otherPath, message) {
|
|
85
|
+
const msg = message ?? `Must match ${otherPath}.`;
|
|
86
|
+
return (value, { values }) => deepEqual(value, getAtPath(values, otherPath)) ? void 0 : msg;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
exports.email = email;
|
|
90
|
+
exports.matches = matches;
|
|
91
|
+
exports.max = max;
|
|
92
|
+
exports.maxLength = maxLength;
|
|
93
|
+
exports.min = min;
|
|
94
|
+
exports.minLength = minLength;
|
|
95
|
+
exports.oneOf = oneOf;
|
|
96
|
+
exports.pattern = pattern;
|
|
97
|
+
exports.required = required;
|
|
98
|
+
//# sourceMappingURL=validators.cjs.map
|
|
99
|
+
//# sourceMappingURL=validators.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/utils/deepEqual.ts","../src/core/utils/paths.ts","../src/validators.ts"],"names":[],"mappings":"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","file":"validators.cjs","sourcesContent":["/**\r\n * Structural equality for plain JSON-like form values (objects, arrays,\r\n * primitives). Non-plain values (Date, File, custom classes) fall back to\r\n * `Object.is` rather than field-by-field inspection — sufficient for dirty\r\n * tracking, since those values are typically replaced wholesale rather than\r\n * mutated in place. Revisit only if a real use case needs otherwise.\r\n */\r\nexport function deepEqual(a: unknown, b: unknown): boolean {\r\n if (Object.is(a, b)) return true;\r\n\r\n if (Array.isArray(a) && Array.isArray(b)) {\r\n if (a.length !== b.length) return false;\r\n return a.every((item, index) => deepEqual(item, b[index]));\r\n }\r\n\r\n const aIsPlainObject =\r\n typeof a === \"object\" && a !== null && !Array.isArray(a) && a.constructor === Object;\r\n const bIsPlainObject =\r\n typeof b === \"object\" && b !== null && !Array.isArray(b) && b.constructor === Object;\r\n\r\n if (aIsPlainObject && bIsPlainObject) {\r\n const aKeys = Object.keys(a as Record<string, unknown>);\r\n const bKeys = Object.keys(b as Record<string, unknown>);\r\n if (aKeys.length !== bKeys.length) return false;\r\n return aKeys.every((key) =>\r\n deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),\r\n );\r\n }\r\n\r\n return false;\r\n}\r\n","/**\r\n * Minimal, dependency-free dot-path helpers.\r\n *\r\n * Deliberately not using lodash here (get/set/isEqual) even though the\r\n * reference application relies on it — the whole store is small enough that\r\n * shipping ~30 lines here beats pulling in a runtime dependency for a\r\n * library meant to stay near-zero-dependency (see roadmap: \"Dependency\r\n * Philosophy\").\r\n */\r\n\r\nexport function splitPath(path: string): string[] {\r\n return path.split(\".\").filter(Boolean);\r\n}\r\n\r\nexport function getAtPath(source: unknown, path: string): unknown {\r\n const segments = splitPath(path);\r\n let current: unknown = source;\r\n for (const segment of segments) {\r\n if (current === null || typeof current !== \"object\") {\r\n return undefined;\r\n }\r\n current = (current as Record<string, unknown>)[segment];\r\n }\r\n return current;\r\n}\r\n\r\n/**\r\n * Returns a new object with `value` written at `path`, cloning only the\r\n * objects along the path (structural sharing everywhere else). This is what\r\n * lets subscribers cheaply detect \"did the branch I care about change?\" via\r\n * reference equality, without deep-cloning the whole form on every keystroke.\r\n */\r\nexport function setAtPath<T extends Record<string, unknown>>(\r\n source: T,\r\n path: string,\r\n value: unknown,\r\n): T {\r\n const segments = splitPath(path);\r\n if (segments.length === 0) return source;\r\n\r\n const [head, ...rest] = segments as [string, ...string[]];\r\n\r\n if (rest.length === 0) {\r\n if (Object.is((source as Record<string, unknown>)[head], value)) {\r\n return source;\r\n }\r\n return { ...source, [head]: value };\r\n }\r\n\r\n const currentChild = (source as Record<string, unknown>)[head];\r\n const childSource =\r\n currentChild !== null && typeof currentChild === \"object\"\r\n ? (currentChild as Record<string, unknown>)\r\n : {};\r\n\r\n const nextChild = setAtPath(childSource, rest.join(\".\"), value);\r\n\r\n if (Object.is(currentChild, nextChild)) {\r\n return source;\r\n }\r\n\r\n return { ...source, [head]: nextChild };\r\n}\r\n\r\n/**\r\n * Shallow-merges `partial` into `source` at the top level, one key at a\r\n * time, reusing setAtPath so each key's structural-sharing behavior stays\r\n * consistent with single-field writes.\r\n */\r\nexport function mergeAtRoot<T extends Record<string, unknown>>(\r\n source: T,\r\n partial: Partial<T>,\r\n): T {\r\n let next: T = source;\r\n for (const key of Object.keys(partial)) {\r\n next = setAtPath(next, key, (partial as Record<string, unknown>)[key]);\r\n }\r\n return next;\r\n}\r\n\r\n/**\r\n * Returns every dot-path whose leaf value differs (by reference, via\r\n * Object.is) between `prev` and `next`, walking both objects together.\r\n * Used to compute exactly which paths to notify subscribers about.\r\n */\r\nexport function diffPaths(\r\n prev: unknown,\r\n next: unknown,\r\n basePath = \"\",\r\n seen: Set<string> = new Set(),\r\n): string[] {\r\n if (Object.is(prev, next)) {\r\n return [];\r\n }\r\n\r\n const prevIsObject = prev !== null && typeof prev === \"object\" && !Array.isArray(prev);\r\n const nextIsObject = next !== null && typeof next === \"object\" && !Array.isArray(next);\r\n\r\n if (!prevIsObject || !nextIsObject) {\r\n return basePath ? [basePath] : [];\r\n }\r\n\r\n const keys = new Set([\r\n ...Object.keys(prev as Record<string, unknown>),\r\n ...Object.keys(next as Record<string, unknown>),\r\n ]);\r\n\r\n const changed: string[] = [];\r\n for (const key of keys) {\r\n const childPath = basePath ? `${basePath}.${key}` : key;\r\n if (seen.has(childPath)) continue;\r\n seen.add(childPath);\r\n changed.push(\r\n ...diffPaths(\r\n (prev as Record<string, unknown>)[key],\r\n (next as Record<string, unknown>)[key],\r\n childPath,\r\n seen,\r\n ),\r\n );\r\n }\r\n return changed;\r\n}\r\n","// Phase 2: ready-made validators for the validation schema consumed by\r\n// `useForm({ validation })` / `validateValues`. Every factory returns a\r\n// pure `Validator` — (value, context) => message | undefined — so custom\r\n// rules compose with these on equal footing.\r\n//\r\n// Conventions shared by all of them:\r\n// - An ABSENT value (undefined/null) is only `required()`'s concern; every\r\n// other rule passes absent values through so \"optional but validated\"\r\n// fields work (e.g. optional email that must be well-formed if given).\r\n// - Rules that don't apply to a value's type pass (e.g. `min` ignores\r\n// strings) — type enforcement belongs to the field's normalizer.\r\n// - Messages are plain English defaults; every factory accepts an override\r\n// (i18n/error-message catalogs are a later-phase concern).\r\n\r\nimport type { Validator } from \"./core/types/validation\";\r\nimport { deepEqual } from \"./core/utils/deepEqual\";\r\nimport { getAtPath } from \"./core/utils/paths\";\r\n\r\n/** Fails on undefined, null, empty string, whitespace-only string, empty array. */\r\nexport function required(message = \"This field is required.\"): Validator {\r\n return (value) => {\r\n if (value === undefined || value === null) return message;\r\n if (typeof value === \"string\" && value.trim() === \"\") return message;\r\n if (Array.isArray(value) && value.length === 0) return message;\r\n return undefined;\r\n };\r\n}\r\n\r\nexport function minLength(limit: number, message?: string): Validator {\r\n const msg = message ?? `Must be at least ${limit} characters.`;\r\n return (value) => {\r\n if (value === undefined || value === null) return undefined;\r\n const length = typeof value === \"string\" || Array.isArray(value) ? value.length : undefined;\r\n return length !== undefined && length < limit ? msg : undefined;\r\n };\r\n}\r\n\r\nexport function maxLength(limit: number, message?: string): Validator {\r\n const msg = message ?? `Must be at most ${limit} characters.`;\r\n return (value) => {\r\n if (value === undefined || value === null) return undefined;\r\n const length = typeof value === \"string\" || Array.isArray(value) ? value.length : undefined;\r\n return length !== undefined && length > limit ? msg : undefined;\r\n };\r\n}\r\n\r\nexport function pattern(regex: RegExp, message = \"Invalid format.\"): Validator {\r\n return (value) =>\r\n typeof value === \"string\" && value !== \"\" && !regex.test(value) ? message : undefined;\r\n}\r\n\r\nexport function email(message = \"Enter a valid email address.\"): Validator {\r\n return pattern(/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/, message);\r\n}\r\n\r\nexport function min(bound: number, message?: string): Validator {\r\n const msg = message ?? `Must be ${bound} or more.`;\r\n return (value) =>\r\n typeof value === \"number\" && Number.isFinite(value) && value < bound ? msg : undefined;\r\n}\r\n\r\nexport function max(bound: number, message?: string): Validator {\r\n const msg = message ?? `Must be ${bound} or less.`;\r\n return (value) =>\r\n typeof value === \"number\" && Number.isFinite(value) && value > bound ? msg : undefined;\r\n}\r\n\r\n/** Value must be one of the given options (selects, dropdowns). */\r\nexport function oneOf(allowed: readonly unknown[], message = \"Invalid selection.\"): Validator {\r\n return (value) => {\r\n if (value === undefined || value === null || value === \"\") return undefined;\r\n return allowed.includes(value) ? undefined : message;\r\n };\r\n}\r\n\r\n/**\r\n * Cross-field equality: the field's value must deeply equal the value at\r\n * `otherPath` (scope-relative) — confirm-password, confirm-email, and the\r\n * equality flavor of date-range checks.\r\n */\r\nexport function matches(otherPath: string, message?: string): Validator {\r\n const msg = message ?? `Must match ${otherPath}.`;\r\n return (value, { values }) =>\r\n deepEqual(value, getAtPath(values, otherPath)) ? undefined : msg;\r\n}\r\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { V as Validator } from './validation-gIT3Xn9h.cjs';
|
|
2
|
+
|
|
3
|
+
/** Fails on undefined, null, empty string, whitespace-only string, empty array. */
|
|
4
|
+
declare function required(message?: string): Validator;
|
|
5
|
+
declare function minLength(limit: number, message?: string): Validator;
|
|
6
|
+
declare function maxLength(limit: number, message?: string): Validator;
|
|
7
|
+
declare function pattern(regex: RegExp, message?: string): Validator;
|
|
8
|
+
declare function email(message?: string): Validator;
|
|
9
|
+
declare function min(bound: number, message?: string): Validator;
|
|
10
|
+
declare function max(bound: number, message?: string): Validator;
|
|
11
|
+
/** Value must be one of the given options (selects, dropdowns). */
|
|
12
|
+
declare function oneOf(allowed: readonly unknown[], message?: string): Validator;
|
|
13
|
+
/**
|
|
14
|
+
* Cross-field equality: the field's value must deeply equal the value at
|
|
15
|
+
* `otherPath` (scope-relative) — confirm-password, confirm-email, and the
|
|
16
|
+
* equality flavor of date-range checks.
|
|
17
|
+
*/
|
|
18
|
+
declare function matches(otherPath: string, message?: string): Validator;
|
|
19
|
+
|
|
20
|
+
export { email, matches, max, maxLength, min, minLength, oneOf, pattern, required };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { V as Validator } from './validation-gIT3Xn9h.js';
|
|
2
|
+
|
|
3
|
+
/** Fails on undefined, null, empty string, whitespace-only string, empty array. */
|
|
4
|
+
declare function required(message?: string): Validator;
|
|
5
|
+
declare function minLength(limit: number, message?: string): Validator;
|
|
6
|
+
declare function maxLength(limit: number, message?: string): Validator;
|
|
7
|
+
declare function pattern(regex: RegExp, message?: string): Validator;
|
|
8
|
+
declare function email(message?: string): Validator;
|
|
9
|
+
declare function min(bound: number, message?: string): Validator;
|
|
10
|
+
declare function max(bound: number, message?: string): Validator;
|
|
11
|
+
/** Value must be one of the given options (selects, dropdowns). */
|
|
12
|
+
declare function oneOf(allowed: readonly unknown[], message?: string): Validator;
|
|
13
|
+
/**
|
|
14
|
+
* Cross-field equality: the field's value must deeply equal the value at
|
|
15
|
+
* `otherPath` (scope-relative) — confirm-password, confirm-email, and the
|
|
16
|
+
* equality flavor of date-range checks.
|
|
17
|
+
*/
|
|
18
|
+
declare function matches(otherPath: string, message?: string): Validator;
|
|
19
|
+
|
|
20
|
+
export { email, matches, max, maxLength, min, minLength, oneOf, pattern, required };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// src/core/utils/deepEqual.ts
|
|
2
|
+
function deepEqual(a, b) {
|
|
3
|
+
if (Object.is(a, b)) return true;
|
|
4
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
5
|
+
if (a.length !== b.length) return false;
|
|
6
|
+
return a.every((item, index) => deepEqual(item, b[index]));
|
|
7
|
+
}
|
|
8
|
+
const aIsPlainObject = typeof a === "object" && a !== null && !Array.isArray(a) && a.constructor === Object;
|
|
9
|
+
const bIsPlainObject = typeof b === "object" && b !== null && !Array.isArray(b) && b.constructor === Object;
|
|
10
|
+
if (aIsPlainObject && bIsPlainObject) {
|
|
11
|
+
const aKeys = Object.keys(a);
|
|
12
|
+
const bKeys = Object.keys(b);
|
|
13
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
14
|
+
return aKeys.every(
|
|
15
|
+
(key) => deepEqual(a[key], b[key])
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/core/utils/paths.ts
|
|
22
|
+
function splitPath(path) {
|
|
23
|
+
return path.split(".").filter(Boolean);
|
|
24
|
+
}
|
|
25
|
+
function getAtPath(source, path) {
|
|
26
|
+
const segments = splitPath(path);
|
|
27
|
+
let current = source;
|
|
28
|
+
for (const segment of segments) {
|
|
29
|
+
if (current === null || typeof current !== "object") {
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
32
|
+
current = current[segment];
|
|
33
|
+
}
|
|
34
|
+
return current;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/validators.ts
|
|
38
|
+
function required(message = "This field is required.") {
|
|
39
|
+
return (value) => {
|
|
40
|
+
if (value === void 0 || value === null) return message;
|
|
41
|
+
if (typeof value === "string" && value.trim() === "") return message;
|
|
42
|
+
if (Array.isArray(value) && value.length === 0) return message;
|
|
43
|
+
return void 0;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function minLength(limit, message) {
|
|
47
|
+
const msg = message ?? `Must be at least ${limit} characters.`;
|
|
48
|
+
return (value) => {
|
|
49
|
+
if (value === void 0 || value === null) return void 0;
|
|
50
|
+
const length = typeof value === "string" || Array.isArray(value) ? value.length : void 0;
|
|
51
|
+
return length !== void 0 && length < limit ? msg : void 0;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function maxLength(limit, message) {
|
|
55
|
+
const msg = message ?? `Must be at most ${limit} characters.`;
|
|
56
|
+
return (value) => {
|
|
57
|
+
if (value === void 0 || value === null) return void 0;
|
|
58
|
+
const length = typeof value === "string" || Array.isArray(value) ? value.length : void 0;
|
|
59
|
+
return length !== void 0 && length > limit ? msg : void 0;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function pattern(regex, message = "Invalid format.") {
|
|
63
|
+
return (value) => typeof value === "string" && value !== "" && !regex.test(value) ? message : void 0;
|
|
64
|
+
}
|
|
65
|
+
function email(message = "Enter a valid email address.") {
|
|
66
|
+
return pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, message);
|
|
67
|
+
}
|
|
68
|
+
function min(bound, message) {
|
|
69
|
+
const msg = message ?? `Must be ${bound} or more.`;
|
|
70
|
+
return (value) => typeof value === "number" && Number.isFinite(value) && value < bound ? msg : void 0;
|
|
71
|
+
}
|
|
72
|
+
function max(bound, message) {
|
|
73
|
+
const msg = message ?? `Must be ${bound} or less.`;
|
|
74
|
+
return (value) => typeof value === "number" && Number.isFinite(value) && value > bound ? msg : void 0;
|
|
75
|
+
}
|
|
76
|
+
function oneOf(allowed, message = "Invalid selection.") {
|
|
77
|
+
return (value) => {
|
|
78
|
+
if (value === void 0 || value === null || value === "") return void 0;
|
|
79
|
+
return allowed.includes(value) ? void 0 : message;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function matches(otherPath, message) {
|
|
83
|
+
const msg = message ?? `Must match ${otherPath}.`;
|
|
84
|
+
return (value, { values }) => deepEqual(value, getAtPath(values, otherPath)) ? void 0 : msg;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { email, matches, max, maxLength, min, minLength, oneOf, pattern, required };
|
|
88
|
+
//# sourceMappingURL=validators.js.map
|
|
89
|
+
//# sourceMappingURL=validators.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/utils/deepEqual.ts","../src/core/utils/paths.ts","../src/validators.ts"],"names":[],"mappings":"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","file":"validators.js","sourcesContent":["/**\r\n * Structural equality for plain JSON-like form values (objects, arrays,\r\n * primitives). Non-plain values (Date, File, custom classes) fall back to\r\n * `Object.is` rather than field-by-field inspection — sufficient for dirty\r\n * tracking, since those values are typically replaced wholesale rather than\r\n * mutated in place. Revisit only if a real use case needs otherwise.\r\n */\r\nexport function deepEqual(a: unknown, b: unknown): boolean {\r\n if (Object.is(a, b)) return true;\r\n\r\n if (Array.isArray(a) && Array.isArray(b)) {\r\n if (a.length !== b.length) return false;\r\n return a.every((item, index) => deepEqual(item, b[index]));\r\n }\r\n\r\n const aIsPlainObject =\r\n typeof a === \"object\" && a !== null && !Array.isArray(a) && a.constructor === Object;\r\n const bIsPlainObject =\r\n typeof b === \"object\" && b !== null && !Array.isArray(b) && b.constructor === Object;\r\n\r\n if (aIsPlainObject && bIsPlainObject) {\r\n const aKeys = Object.keys(a as Record<string, unknown>);\r\n const bKeys = Object.keys(b as Record<string, unknown>);\r\n if (aKeys.length !== bKeys.length) return false;\r\n return aKeys.every((key) =>\r\n deepEqual((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]),\r\n );\r\n }\r\n\r\n return false;\r\n}\r\n","/**\r\n * Minimal, dependency-free dot-path helpers.\r\n *\r\n * Deliberately not using lodash here (get/set/isEqual) even though the\r\n * reference application relies on it — the whole store is small enough that\r\n * shipping ~30 lines here beats pulling in a runtime dependency for a\r\n * library meant to stay near-zero-dependency (see roadmap: \"Dependency\r\n * Philosophy\").\r\n */\r\n\r\nexport function splitPath(path: string): string[] {\r\n return path.split(\".\").filter(Boolean);\r\n}\r\n\r\nexport function getAtPath(source: unknown, path: string): unknown {\r\n const segments = splitPath(path);\r\n let current: unknown = source;\r\n for (const segment of segments) {\r\n if (current === null || typeof current !== \"object\") {\r\n return undefined;\r\n }\r\n current = (current as Record<string, unknown>)[segment];\r\n }\r\n return current;\r\n}\r\n\r\n/**\r\n * Returns a new object with `value` written at `path`, cloning only the\r\n * objects along the path (structural sharing everywhere else). This is what\r\n * lets subscribers cheaply detect \"did the branch I care about change?\" via\r\n * reference equality, without deep-cloning the whole form on every keystroke.\r\n */\r\nexport function setAtPath<T extends Record<string, unknown>>(\r\n source: T,\r\n path: string,\r\n value: unknown,\r\n): T {\r\n const segments = splitPath(path);\r\n if (segments.length === 0) return source;\r\n\r\n const [head, ...rest] = segments as [string, ...string[]];\r\n\r\n if (rest.length === 0) {\r\n if (Object.is((source as Record<string, unknown>)[head], value)) {\r\n return source;\r\n }\r\n return { ...source, [head]: value };\r\n }\r\n\r\n const currentChild = (source as Record<string, unknown>)[head];\r\n const childSource =\r\n currentChild !== null && typeof currentChild === \"object\"\r\n ? (currentChild as Record<string, unknown>)\r\n : {};\r\n\r\n const nextChild = setAtPath(childSource, rest.join(\".\"), value);\r\n\r\n if (Object.is(currentChild, nextChild)) {\r\n return source;\r\n }\r\n\r\n return { ...source, [head]: nextChild };\r\n}\r\n\r\n/**\r\n * Shallow-merges `partial` into `source` at the top level, one key at a\r\n * time, reusing setAtPath so each key's structural-sharing behavior stays\r\n * consistent with single-field writes.\r\n */\r\nexport function mergeAtRoot<T extends Record<string, unknown>>(\r\n source: T,\r\n partial: Partial<T>,\r\n): T {\r\n let next: T = source;\r\n for (const key of Object.keys(partial)) {\r\n next = setAtPath(next, key, (partial as Record<string, unknown>)[key]);\r\n }\r\n return next;\r\n}\r\n\r\n/**\r\n * Returns every dot-path whose leaf value differs (by reference, via\r\n * Object.is) between `prev` and `next`, walking both objects together.\r\n * Used to compute exactly which paths to notify subscribers about.\r\n */\r\nexport function diffPaths(\r\n prev: unknown,\r\n next: unknown,\r\n basePath = \"\",\r\n seen: Set<string> = new Set(),\r\n): string[] {\r\n if (Object.is(prev, next)) {\r\n return [];\r\n }\r\n\r\n const prevIsObject = prev !== null && typeof prev === \"object\" && !Array.isArray(prev);\r\n const nextIsObject = next !== null && typeof next === \"object\" && !Array.isArray(next);\r\n\r\n if (!prevIsObject || !nextIsObject) {\r\n return basePath ? [basePath] : [];\r\n }\r\n\r\n const keys = new Set([\r\n ...Object.keys(prev as Record<string, unknown>),\r\n ...Object.keys(next as Record<string, unknown>),\r\n ]);\r\n\r\n const changed: string[] = [];\r\n for (const key of keys) {\r\n const childPath = basePath ? `${basePath}.${key}` : key;\r\n if (seen.has(childPath)) continue;\r\n seen.add(childPath);\r\n changed.push(\r\n ...diffPaths(\r\n (prev as Record<string, unknown>)[key],\r\n (next as Record<string, unknown>)[key],\r\n childPath,\r\n seen,\r\n ),\r\n );\r\n }\r\n return changed;\r\n}\r\n","// Phase 2: ready-made validators for the validation schema consumed by\r\n// `useForm({ validation })` / `validateValues`. Every factory returns a\r\n// pure `Validator` — (value, context) => message | undefined — so custom\r\n// rules compose with these on equal footing.\r\n//\r\n// Conventions shared by all of them:\r\n// - An ABSENT value (undefined/null) is only `required()`'s concern; every\r\n// other rule passes absent values through so \"optional but validated\"\r\n// fields work (e.g. optional email that must be well-formed if given).\r\n// - Rules that don't apply to a value's type pass (e.g. `min` ignores\r\n// strings) — type enforcement belongs to the field's normalizer.\r\n// - Messages are plain English defaults; every factory accepts an override\r\n// (i18n/error-message catalogs are a later-phase concern).\r\n\r\nimport type { Validator } from \"./core/types/validation\";\r\nimport { deepEqual } from \"./core/utils/deepEqual\";\r\nimport { getAtPath } from \"./core/utils/paths\";\r\n\r\n/** Fails on undefined, null, empty string, whitespace-only string, empty array. */\r\nexport function required(message = \"This field is required.\"): Validator {\r\n return (value) => {\r\n if (value === undefined || value === null) return message;\r\n if (typeof value === \"string\" && value.trim() === \"\") return message;\r\n if (Array.isArray(value) && value.length === 0) return message;\r\n return undefined;\r\n };\r\n}\r\n\r\nexport function minLength(limit: number, message?: string): Validator {\r\n const msg = message ?? `Must be at least ${limit} characters.`;\r\n return (value) => {\r\n if (value === undefined || value === null) return undefined;\r\n const length = typeof value === \"string\" || Array.isArray(value) ? value.length : undefined;\r\n return length !== undefined && length < limit ? msg : undefined;\r\n };\r\n}\r\n\r\nexport function maxLength(limit: number, message?: string): Validator {\r\n const msg = message ?? `Must be at most ${limit} characters.`;\r\n return (value) => {\r\n if (value === undefined || value === null) return undefined;\r\n const length = typeof value === \"string\" || Array.isArray(value) ? value.length : undefined;\r\n return length !== undefined && length > limit ? msg : undefined;\r\n };\r\n}\r\n\r\nexport function pattern(regex: RegExp, message = \"Invalid format.\"): Validator {\r\n return (value) =>\r\n typeof value === \"string\" && value !== \"\" && !regex.test(value) ? message : undefined;\r\n}\r\n\r\nexport function email(message = \"Enter a valid email address.\"): Validator {\r\n return pattern(/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/, message);\r\n}\r\n\r\nexport function min(bound: number, message?: string): Validator {\r\n const msg = message ?? `Must be ${bound} or more.`;\r\n return (value) =>\r\n typeof value === \"number\" && Number.isFinite(value) && value < bound ? msg : undefined;\r\n}\r\n\r\nexport function max(bound: number, message?: string): Validator {\r\n const msg = message ?? `Must be ${bound} or less.`;\r\n return (value) =>\r\n typeof value === \"number\" && Number.isFinite(value) && value > bound ? msg : undefined;\r\n}\r\n\r\n/** Value must be one of the given options (selects, dropdowns). */\r\nexport function oneOf(allowed: readonly unknown[], message = \"Invalid selection.\"): Validator {\r\n return (value) => {\r\n if (value === undefined || value === null || value === \"\") return undefined;\r\n return allowed.includes(value) ? undefined : message;\r\n };\r\n}\r\n\r\n/**\r\n * Cross-field equality: the field's value must deeply equal the value at\r\n * `otherPath` (scope-relative) — confirm-password, confirm-email, and the\r\n * equality flavor of date-range checks.\r\n */\r\nexport function matches(otherPath: string, message?: string): Validator {\r\n const msg = message ?? `Must match ${otherPath}.`;\r\n return (value, { values }) =>\r\n deepEqual(value, getAtPath(values, otherPath)) ? undefined : msg;\r\n}\r\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-formesh",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "Framework-agnostic, fine-grained form state for complex multi-section forms.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"require": "./dist/index.cjs"
|
|
16
|
+
},
|
|
17
|
+
"./react": {
|
|
18
|
+
"types": "./dist/react.d.ts",
|
|
19
|
+
"import": "./dist/react.js",
|
|
20
|
+
"require": "./dist/react.cjs"
|
|
21
|
+
},
|
|
22
|
+
"./validators": {
|
|
23
|
+
"types": "./dist/validators.d.ts",
|
|
24
|
+
"import": "./dist/validators.js",
|
|
25
|
+
"require": "./dist/validators.cjs"
|
|
26
|
+
},
|
|
27
|
+
"./file": {
|
|
28
|
+
"types": "./dist/file.d.ts",
|
|
29
|
+
"import": "./dist/file.js",
|
|
30
|
+
"require": "./dist/file.cjs"
|
|
31
|
+
},
|
|
32
|
+
"./array": {
|
|
33
|
+
"types": "./dist/array.d.ts",
|
|
34
|
+
"import": "./dist/array.js",
|
|
35
|
+
"require": "./dist/array.cjs"
|
|
36
|
+
},
|
|
37
|
+
"./package.json": "./package.json"
|
|
38
|
+
},
|
|
39
|
+
"main": "./dist/index.cjs",
|
|
40
|
+
"module": "./dist/index.js",
|
|
41
|
+
"types": "./dist/index.d.ts",
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsup",
|
|
44
|
+
"dev": "tsup --watch",
|
|
45
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
46
|
+
"test": "vitest",
|
|
47
|
+
"clean": "rimraf dist"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"react": ">=18.0.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"react": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/node": "^20.11.0",
|
|
59
|
+
"@testing-library/react": "^15.0.0",
|
|
60
|
+
"@types/react": "^18.2.0",
|
|
61
|
+
"@vitejs/plugin-react": "^4.2.0",
|
|
62
|
+
"jsdom": "^24.0.0",
|
|
63
|
+
"react": "^18.2.0",
|
|
64
|
+
"react-dom": "^18.2.0",
|
|
65
|
+
"rimraf": "^5.0.5",
|
|
66
|
+
"tsup": "^8.0.0",
|
|
67
|
+
"typescript": "^5.4.0",
|
|
68
|
+
"vitest": "^1.5.0"
|
|
69
|
+
}
|
|
70
|
+
}
|