formstand 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,264 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.22.0 — 2026-10-19
4
+
5
+ ### Fixed
6
+
7
+ - **`useFieldArray` no longer remounts the row you are typing in when rows
8
+ are identical primitives.** With `["", ""]`, a keystroke into row 0 made
9
+ the value-matching pass hand row 0's id to row 1 and mint a new id for
10
+ row 0, so React remounted the focused input. A same-index identity pass
11
+ now runs first, so an edited row keeps its id and duplicates keep theirs.
12
+ - **`debounceMs` no longer suppresses blur validation on an untouched
13
+ form.** The reset-protection guard read "values is initialValues" as
14
+ pristine, which is also every form nobody has typed in yet, so a
15
+ required field tabbed through showed nothing. A rebase is now detected
16
+ as the values reference moving onto initialValues since scheduling,
17
+ which only reset and adoptValues produce.
18
+ - **`useFormSteps.next()` and `goTo()` no longer apply a stale step** when
19
+ the user presses `back()` while the async step gate is in flight; the
20
+ user's newer position wins and `goTo` returns it.
21
+ - **`useMaskedInput` follows the text binding's clearing rule.** Clearing a
22
+ required string writes `""` (clean against a `""` initial, the author's
23
+ message) instead of `undefined`; optional, nullable, and non-string
24
+ fields still clear to `emptyValue`.
25
+ - **`clearable` and `emptyValue` read the schema through wrappers and
26
+ unions.** A leaf under an optional object and a field inside a
27
+ discriminated-union variant (`useVariantField`) now report their own
28
+ schema's blank instead of the runtime heuristic.
29
+ - **`persistForm` refuses a `null` top-level draft** (an unversioned
30
+ `"null"` or a versioned wrapper holding `values: null`) instead of
31
+ adopting it as the whole values object. Nested nulls stay legal.
32
+ - **`handleSubmit` and the action bridge never reject when `process` is
33
+ undefined.** The dev-only "nothing observed the error" warning read
34
+ `process.env.NODE_ENV` unguarded, so in a runtime without a `process`
35
+ global the never-rejects contract broke exactly on the failure path. One
36
+ shared `isDevBuild()` now guards all three sites.
37
+ - **Array ops reject tuple paths at the type level.** `arrayPush("pair", x)`
38
+ on a `z.tuple` no longer compiles; it could only ever grow a fixed-arity
39
+ value the schema rejects.
40
+ - **`flattenIssues` and the plural error writes scale linearly.** The
41
+ issue grouping rebuilt its accumulator with a spread per issue, so
42
+ `validate()` on 3000 empty rows spent 1.3 s grouping what zod parsed in
43
+ 7 ms; it now groups once through a Map. The same per-entry spread sat in
44
+ `validateFieldsAsync`'s in-flight flags and in `addErrors`, which both
45
+ now run one pass over the map.
46
+ - **Error keys named after `Object.prototype` members no longer crash or
47
+ leak.** `setError("constructor", "x")`, `addErrors({ constructor: [...] })`,
48
+ and `validate()` on a schema with a `constructor` field threw inside the
49
+ store write (a bare `prev[k]` read `Object` as the previous error list);
50
+ `getFieldState("toString").touched` and the `useField` / `useFieldArray`
51
+ selectors read prototype functions as field state. Every runtime-keyed
52
+ read of `errors`, `touched`, and `isValidating` is now an own-key read.
53
+ - **An async field verdict survives an unrelated field's change.** A
54
+ subschema `validateFieldAsync("username")` dropped its result, and
55
+ cleared its in-flight flag, the moment any other value changed while it
56
+ was in flight; it now compares its own slot, so "username taken" lands
57
+ after you move on to `email`. Full-form scopes (an ancestor refine may
58
+ read any field) keep the whole-values compare.
59
+ - **`validateFieldsAsync` takes the subschema fast path.** It always parsed
60
+ the whole schema while the sync `validateFields` parsed only the
61
+ requested slots, so `useFormSteps` paid for every other step's fields on
62
+ each `next()` and an async refine on another step's field fired every
63
+ time. The async plural now mirrors the sync routing (all paths extract:
64
+ parse those slots, in parallel; any path bails: one full parse) and
65
+ judges staleness per path.
66
+ - **`updateState` values patches release server errors.** A patch that
67
+ carries `values` now runs the same release as `setValues`: entries whose
68
+ value slice changed go, untouched siblings stay. An explicit
69
+ `serverErrors` in the same patch is taken as given.
70
+ - **The devtools gates fail closed without a readable `NODE_ENV`.**
71
+ `FormstandDevtools` rendered the full state panel and the zustand
72
+ devtools middleware stayed enabled in a CDN/ESM, Deno, or worker
73
+ production build, because "no `process`" read as a dev build. Both now
74
+ gate on `isDevToolsBuild()`, true only when `process.env.NODE_ENV` is a
75
+ readable string other than `"production"`; the console warnings keep the
76
+ fail-open `isDevBuild()`.
77
+ - **Two invalid Dates compare equal, and -0 equals 0, in dirty tracking.**
78
+ A field holding `new Date(NaN)` against an invalid initial read as
79
+ permanently dirty, and `-0` over an initial `0` was dirty while the input
80
+ rendered `"0"` either way.
81
+
82
+ ### Changed
83
+
84
+ - **Array ops and `useFieldArray` take array paths only.** `arrayRemove`,
85
+ `arrayMove`, and `arraySwap` accepted any `FieldPath`, so
86
+ `form.arrayRemove("name", 0)` on a `z.string()` compiled (and warned at
87
+ runtime), while `useFieldArray(form, "pair")` on a `z.tuple` bound with
88
+ the tuple's element union and `useFieldArray(form, "name")` bound with a
89
+ `never` item. Every array op and the hook's typed overload now constrain
90
+ the path to `ArrayFieldPath<Values, D>`: variable-length arrays, optional
91
+ and nullable included, tuples and scalars excluded, template paths like
92
+ `` `rows.${number}.tags` `` kept. Type-level only: the code it rejects was
93
+ already a runtime no-op or a schema failure. `ArrayFieldPath`,
94
+ `ArrayItemOf` (the one item type, tuple arm included, now shared by the
95
+ core and the hook), and the form-level `ArrayPathsOf<F>` are exported
96
+ beside `PathsOf`.
97
+ - **`PathsAccepting` and `SelectValueAt` are actually exported**, as the
98
+ bound-components entry below says they are; `BoundUseFields` and
99
+ `BoundUseVariantField` join the other `Bound*` types.
100
+ - **`FormstandDevtoolsProps` carries the form's depth.**
101
+ `<FormstandDevtools form={createForm(s, { pathDepth: 12 })} />` was a
102
+ type error against the depth-less `Form<TSchema>` prop; the props now
103
+ take `D extends PathDepth = DefaultPathDepth`. Widening only.
104
+ - **Bound components type their `path` by value.** `TextField` accepts
105
+ string-valued paths, `NumberField` number-valued, `DateField` Date-valued,
106
+ `CheckboxField` boolean-valued, `SelectField` string-valued (optional and
107
+ nullable fields qualify everywhere). `<TextField path="age" />` on a
108
+ `z.number()` used to compile and store `"42"`; it is a compile error now,
109
+ as `textInputProps(useField(form, "age"))` always was. Type-level only:
110
+ the code it rejects was already a runtime bug. `PathsAccepting<F, V>`
111
+ and `SelectValueAt<F, P>` are exported beside `PathsOf`.
112
+ - **`SelectField` checks its options against the field.** An option whose
113
+ `value` is not in the path's `z.enum` is a compile error at the option;
114
+ a free `z.string()` path still takes any string. `SelectFieldProps` is
115
+ now generic over the form and the path (`SelectFieldProps<F, P>`) rather
116
+ than the option value type.
117
+ - **`fillFields` types each value from its key**, the contract `fillField`
118
+ already had; a wrong-typed fixture value is a compile error.
119
+ - **`persistForm`'s `apply: "edit"`** is the new spelling for "load the
120
+ draft as an edit, dirty against the initial values". `"restore"` keeps
121
+ working as an alias; it collided with `form.restore(snapshot)` and with
122
+ the former `restore()` method.
123
+
124
+ ### Docs
125
+
126
+ - Every em dash is gone from the documentation pages and the playground
127
+ sources (comments included, since they show in the View code panel).
128
+ - API pages match the shipped surface: `useFields`, `useIsValidating`,
129
+ and `usePersistForm` rows; `firstError` on `UseFieldReturn`; `path`,
130
+ `firstError`, `setError`, `clearError` on `UseFieldArrayReturn`;
131
+ `addErrors`, `SubmitOptions`, and the `devtools` option on the API
132
+ index; the missing exported types and the `formstand/testing` subpath
133
+ on the utilities page.
134
+ - The ssr.md provider snippet destructures `Provider` (there is no
135
+ `FormProvider`); components.md documents why a reusable field component
136
+ must be generic over its value type; multi-step.md notes that
137
+ `useFormSteps` memoizes on the steps array's identity.
138
+ - CLI pages: a `--tests` flag row, the current version floors in the CLI
139
+ README, three input modes everywhere, and a working link for the UI-kit
140
+ matrix.
141
+ - Playground: "Open in StackBlitz" seeds a project that installs and boots
142
+ (vite and plugin-react agree, react-dom is always a dependency), with a
143
+ test pinning the seed to examples/package.json. Array-row errors are
144
+ styled, textareas follow the theme, buttons show a keyboard focus ring,
145
+ row inputs carry aria-labels, and the nested-arrays tab says how to
146
+ start.
147
+ - Docs site: reference tables stop stacking at phone width, and the
148
+ dark-mode "Get started" button meets AA contrast.
149
+
150
+ ## formstand-cli 0.23.0 — 2026-10-19
151
+
152
+ ### Fixed
153
+
154
+ - `--watch` regenerated from a stale module: jiti's runtime module cache
155
+ survives a fresh `createJiti` per run, so a field added while watching
156
+ never reached the output. The loader now disables that cache.
157
+ - The config file's `tests` block (`runners`, `renderWrapper`,
158
+ `playwright.baseURL` / `route`) was read and then dropped as an unknown
159
+ key, so it never reached the spec emitter. It is now validated with the
160
+ same loud errors as the other keys and applied: `runners` is the
161
+ `--tests` default, kit specs render through the configured wrapper, and
162
+ the Playwright spec is emitted when a base URL is configured.
163
+ - Generated specs no longer fail on their own valid sample. A number
164
+ bounded on both sides by an exclusive window narrower than one unit
165
+ (`z.number().gt(0).lt(1)`) sampled the bound itself (0); it now samples
166
+ the midpoint (0.5), and the bound probe fills distinct bad and good
167
+ values. A schema with a degraded leaf (a `z.record`, a callable, an
168
+ unsupported kind) emits its round-trip case as `it.skip` with a comment
169
+ naming the path, since the stand-in's text sample can never parse.
170
+ - The module layout's field file for a degraded leaf (for example a
171
+ `z.record`) did not compile: the bound hook inferred the real value type
172
+ and spread it into a text control. The binding is now pinned to the text
173
+ control's value type, with the TODO kept above it.
174
+ - OpenAPI 3.0 discriminated unions: a branch tag spelled as a single-value
175
+ `enum` (3.0 has no `const`) or supplied only through
176
+ `discriminator.mapping` degraded to a string. All three spellings now
177
+ walk to the same union, and a branch that still lacks a tag is named in
178
+ the TODO.
179
+ - Every walker TODO (unsupported zod kinds such as `z.record`, callable
180
+ types, non-string enums, recursive schemas) is now mirrored on stderr as
181
+ `warning: field "<path>": <todo>`; only the depth, truncation, dotted-key,
182
+ and dropped-default degradations were mirrored before.
183
+ - Type mode: a function-typed member was dropped silently (the generated
184
+ schema lost a key of the type it mirrors); it now becomes the
185
+ string-plus-TODO placeholder. An index-signature-only object type
186
+ (`Record<string, number>`) walked to an empty section and an empty
187
+ `z.object({})`; it now degrades to a TODO like `z.record` does.
188
+ - Discriminant-only union variants (a branch with no fields beyond the
189
+ tag) emitted an empty `{x.value === "pickup" && (<></>)}` block in every
190
+ kit and in the module layout. The block is replaced by a one-line comment
191
+ naming the variant.
192
+ - Generated probe-case titles were duplicated when two sections shared a
193
+ field label; a repeated label now carries its path in the title
194
+ (`Email (billing.email) rejects ...`).
195
+ - Plain single-file output did not compile for any degraded leaf (a
196
+ `z.record`, `z.bigint`, `z.lazy`, `z.any`, a non-string literal or enum,
197
+ `z.map`): the stand-in was bound as `<TextField path="meta" />`, and the
198
+ library's bound components now type `path` by value, so a non-text leaf is
199
+ a compile error. The stand-in now renders an in-file `PlaceholderField`
200
+ bound through `useField` pinned to the text control's value type, the TODO
201
+ kept above it. Degraded leaves inside union variants are pinned at their
202
+ hoisted binding in every single-file backend (the kits failed there too).
203
+ - `z.coerce.number()` / `.date()` / `.boolean()` emitted a typed control
204
+ whose path the input type (`unknown`) refuses, in both layouts. Coerced
205
+ schemas now degrade to a TODO stand-in naming the reason.
206
+ - The module layout bound a degraded leaf inside an array row (an object
207
+ row, a tuple row, a union row) and in a union section without the field
208
+ file's pin, so those files did not compile. Every row and variant binding
209
+ now carries the same cast, and the section file imports the type. A
210
+ variant whose only field is degraded also gets the fragment its TODO
211
+ comment needs (two JSX nodes under `&& (...)` did not parse).
212
+ - `z.enum(Color)` / `z.nativeEnum(Color)` over a TypeScript enum did not
213
+ compile in the plain single-file layout: `SelectField` checks each option
214
+ literal against the field's type, which is the enum itself. The walker
215
+ marks such enums and plain renders them through an in-file
216
+ `EnumSelectField` on the raw `selectProps` builder, options unchanged.
217
+ - The generated spec's "filled with valid values" round trip ran live over
218
+ checks the walker never modeled (`.regex()`, `.startsWith()`,
219
+ `.multipleOf()`, a `.refine()`, `z.date().min()`, formats such as
220
+ `z.ipv4()`), and failed on its own sample. The walker now records the
221
+ unmodeled check kinds per path, the case is emitted as `it.skip` with the
222
+ paths and reasons in a comment (as it already was for degraded leaves),
223
+ and `--tests` mirrors each on stderr. The emitted schema is unchanged.
224
+ The method-form `.email()` / `.url()` / `.uuid()` are now modeled like
225
+ `z.email()` (blank-invalid, a format probe, the format in the schema).
226
+ - Security: schema text could break out of a `//` comment into generated
227
+ TypeScript. A JSON Schema `$ref` or `type` string carrying a line
228
+ terminator (`\n`, `\r`, U+2028, U+2029) reached `// TODO: ...` lines in
229
+ the emitted schema and module field files, so the text after the
230
+ terminator became code (a `$ref` of `"#/x\n injected: z.string(), //"`
231
+ minted a real property). Every comment and every stderr warning built
232
+ from schema text now collapses line terminators to a space, so a hostile
233
+ document can neither add code nor forge a `wrote ...` line.
234
+ - An enum option or union tag containing `"` or `\` broke the generated
235
+ spec's valid-values literal (interpolated raw where everything else was
236
+ JSON-quoted).
237
+ - Module layout file names that differed only in case (`email` / `eMail`
238
+ to `EmailField.tsx` / `EMailField.tsx`) were one file on Windows and
239
+ macOS, the second overwriting the first. Component names are now deduped
240
+ case-insensitively (`EMailField2`).
241
+ - The `--template` with `--layout module` error no longer promises module
242
+ support; it says templates apply to the single-file layout only.
243
+
244
+ ### Changed
245
+
246
+ - Optional and nullable objects start empty. `billing: address.optional()`
247
+ used to seed `{ street: "", zip: "" }`, so a blank form carried a billing
248
+ object nobody filled and could not submit; it now seeds `undefined`
249
+ (nullable: `null`), like an optional union. This is a behavior change for
250
+ generated forms: an untouched optional section passes validation on
251
+ submit. Paths under it stay addressable and materialize on the first
252
+ write, the `initialValues` cast is no longer forced by their members, and
253
+ the generated spec's blank-submit case no longer lists them.
254
+ - The module layout's form body is emitted in schema order, interleaving
255
+ root fields and sections as declared, instead of all root fields first and
256
+ every section after.
257
+ - The generated-file header now reads
258
+ `// Generated by formstand-cli: edit freely, this file is yours.`; it and
259
+ every other emitted string (TODO text, spec titles, help, wizard labels,
260
+ error messages) no longer contain an em dash.
261
+
3
262
  ## 0.21.0 — 2026-09-23
4
263
 
5
264
  ### Fixed
package/README.md CHANGED
@@ -83,7 +83,8 @@ npx formstand-gen src/profileSchema.ts --ui mui --sections panel --columns 2 --o
83
83
 
84
84
  You get typed `initialValues`, a bound control per field, sections for nested objects,
85
85
  `useFieldArray` blocks with add and remove buttons, and a wired submit. Point it at a
86
- TypeScript type instead (`--type Profile`) and it generates the zod schema too. `--ui`
86
+ TypeScript type instead (`--type Profile`), or at a JSON Schema or OpenAPI document
87
+ (`api.json --schema Profile`), and it generates the zod schema too. `--ui`
87
88
  targets plain formstand components, Material UI 5 through 9, shadcn/ui, Chakra 3,
88
89
  Mantine 9, or Ant Design 6.
89
90
 
@@ -9,8 +9,10 @@ var isPlainObject = (v) => {
9
9
  };
10
10
  var valuesEqual = (a, b) => {
11
11
  if (Object.is(a, b)) return true;
12
+ if (typeof a === "number" && typeof b === "number") return a === b;
12
13
  if (a instanceof Date && b instanceof Date) {
13
- return a.getTime() === b.getTime();
14
+ const [ta, tb] = [a.getTime(), b.getTime()];
15
+ return ta === tb || Number.isNaN(ta) && Number.isNaN(tb);
14
16
  }
15
17
  if (Array.isArray(a) && Array.isArray(b)) {
16
18
  return a.length === b.length && a.every((v, i) => valuesEqual(v, b[i]));
@@ -146,6 +148,14 @@ function useFormSelectorShallow(form, selector) {
146
148
  return useStore(form.store, useShallow(selector));
147
149
  }
148
150
 
149
- export { getAtPath, hasOwnSlot, isFieldDirty, isPlainObject, parsePath, setAtPath, slotAtPath, useFormSelector, useFormSelectorShallow, valuesEqual };
150
- //# sourceMappingURL=chunk-OB23TUKY.js.map
151
- //# sourceMappingURL=chunk-OB23TUKY.js.map
151
+ // src/core/env.ts
152
+ var isDevBuild = () => typeof process === "undefined" || process.env?.["NODE_ENV"] !== "production";
153
+ var isDevToolsBuild = () => {
154
+ if (typeof process === "undefined") return false;
155
+ const env = process.env?.["NODE_ENV"];
156
+ return typeof env === "string" && env !== "production";
157
+ };
158
+
159
+ export { getAtPath, hasOwnSlot, isDevBuild, isDevToolsBuild, isFieldDirty, isPlainObject, parsePath, setAtPath, slotAtPath, useFormSelector, useFormSelectorShallow, valuesEqual };
160
+ //# sourceMappingURL=chunk-BMXIZEI4.js.map
161
+ //# sourceMappingURL=chunk-BMXIZEI4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/equality.ts","../src/core/path.ts","../src/react/useFormSelector.ts","../src/core/env.ts"],"names":[],"mappings":"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","file":"chunk-BMXIZEI4.js","sourcesContent":["export const isPlainObject = (v: unknown): v is Record<string, unknown> => {\n if (typeof v !== \"object\" || v === null) return false;\n const proto = Object.getPrototypeOf(v);\n return proto === Object.prototype || proto === null;\n};\n\n// Structural equality used to decide whether a field still matches its initial\n// value. Recurses into arrays and plain objects; Dates compare by timestamp\n// (re-picking the same date must not leave the field permanently dirty);\n// other exotic objects fall back to Object.is so a real change is never\n// mistaken for \"unchanged\".\nexport const valuesEqual = (a: unknown, b: unknown): boolean => {\n if (Object.is(a, b)) return true;\n // Numbers compare with `===` semantics on top of Object.is: Object.is\n // alone keeps NaN equal to itself (wanted) but tells -0 from 0, and a\n // number input renders both as \"0\", so a field holding -0 against an\n // initial 0 read as permanently dirty with nothing visible to undo.\n if (typeof a === \"number\" && typeof b === \"number\") return a === b;\n if (a instanceof Date && b instanceof Date) {\n // Two invalid Dates are equal: their timestamps are both NaN, which\n // `===` denies, so a cleared or half-typed date against an invalid\n // initial read as dirty forever.\n const [ta, tb] = [a.getTime(), b.getTime()];\n return ta === tb || (Number.isNaN(ta) && Number.isNaN(tb));\n }\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((v, i) => valuesEqual(v, b[i]));\n }\n if (isPlainObject(a) && isPlainObject(b)) {\n const aKeys = Object.keys(a);\n const bKeys = Object.keys(b);\n // hasOwn matters: equal counts don't imply equal key SETS. Without it,\n // { realname: undefined } vs { nickname: \"Ann\" } passes (1 === 1, and\n // a.realname === missing b.realname === undefined) without b's real key\n // ever being inspected — dirty tracking would read a real change as\n // clean. With hasOwn plus the length check the key sets must match, so\n // the compare is also symmetric.\n return (\n aKeys.length === bKeys.length &&\n aKeys.every((k) => Object.hasOwn(b, k) && valuesEqual(a[k], b[k]))\n );\n }\n return false;\n};\n\n// The deep compare for a container-valued field runs inside store selectors\n// and watch subscriptions, i.e. on every store change — cache verdicts per\n// value object, keyed by the initial-slice reference each was computed for.\n// A small list (not a single slot) so distinct fields/forms sharing one value\n// object (a module-level default, a shared sentinel array) coexist instead of\n// evicting each other; bounded so no entry grows without limit. Form state is\n// immutable, so identical references always mean an identical verdict, and\n// the WeakMap releases everything when the value object dies.\ntype DirtyVerdict = Readonly<{ initial: unknown; dirty: boolean }>;\nconst dirtyCache = new WeakMap<object, readonly DirtyVerdict[]>();\nconst MAX_VERDICTS = 4;\n\n// The single definition of dirtiness: the current value slice differs\n// structurally from its initial-values slice. Derived (rather than tracked by\n// writers) so reads are accurate at any depth.\nexport const isFieldDirty = (value: unknown, initialValue: unknown): boolean => {\n if (Object.is(value, initialValue)) return false;\n if (typeof value !== \"object\" || value === null) {\n return !valuesEqual(value, initialValue);\n }\n const cached = dirtyCache.get(value) ?? [];\n const hit = cached.find((v) => Object.is(v.initial, initialValue));\n if (hit !== undefined) return hit.dirty;\n const dirty = !valuesEqual(value, initialValue);\n dirtyCache.set(\n value,\n [{ initial: initialValue, dirty }, ...cached].slice(0, MAX_VERDICTS),\n );\n return dirty;\n};\n","import { isPlainObject } from \"./equality\";\n\nexport type PathSegment = string | number;\n\n// Segment keys containing \".\" are not addressable (paths are split on dots);\n// use nested objects or index-free keys instead.\n\n// Writes above this index are refused — a typo'd path like \"items.4294967296\"\n// must not allocate a multi-gigabyte array.\nconst MAX_ARRAY_INDEX = 100_000;\n\nconst isIndexSegment = (s: string): boolean => {\n const n = Number(s);\n return Number.isInteger(n) && n >= 0 && String(n) === s;\n};\n\n// Hot path: every field subscription re-resolves its path on every store\n// notification (getAtPath in selectors), so parsing is memoized — a form's\n// paths are a small fixed set. Bounded as an LRU over Map insertion order:\n// a hit re-inserts its key (moving it to the back) and an overflow evicts\n// the front, so an app whose LIVE path set exceeds the cap keeps its hot\n// paths cached instead of re-parsing every one of them after each reset\n// (the old wholesale clear). Eviction only costs re-parsing, never\n// correctness. Same delete-then-set recency idiom as useFieldArray's\n// per-path id cache.\nconst PARSE_CACHE_MAX = 4096;\nconst parseCache = new Map<string, readonly PathSegment[]>();\n\nexport const parsePath = (path: string): readonly PathSegment[] => {\n const cached = parseCache.get(path);\n if (cached !== undefined) {\n parseCache.delete(path);\n parseCache.set(path, cached);\n return cached;\n }\n // Frozen because the array is now SHARED across every caller (and cached):\n // parsePath is a public export, and pre-cache each call returned a private\n // array that was safe to mutate. Freezing enforces the `readonly` type at\n // runtime so an external `.sort()`/`.reverse()`/`.push()` throws instead of\n // silently corrupting the cache for every later lookup of this path.\n const segments = Object.freeze(\n path === \"\"\n ? []\n : path.split(\".\").map((s) => (isIndexSegment(s) ? Number(s) : s)),\n );\n if (parseCache.size >= PARSE_CACHE_MAX) {\n const oldest = parseCache.keys().next().value;\n if (oldest !== undefined) parseCache.delete(oldest);\n }\n parseCache.set(path, segments);\n return segments;\n};\n\nconst isPlainRecord = (value: unknown): value is Record<string, unknown> =>\n value !== null && typeof value === \"object\" && !Array.isArray(value);\n\n// The existing container decides how a segment is interpreted: arrays take\n// numeric segments as indices, records take any segment as a string key (so a\n// z.record keyed \"0\" reads/writes the record key, not an array index). Only\n// when the container is absent does the segment type pick what gets created.\nconst readSegment = (obj: unknown, segment: PathSegment): unknown => {\n if (obj == null) return undefined;\n if (Array.isArray(obj)) {\n return typeof segment === \"number\" ? obj[segment] : undefined;\n }\n // Own properties only: a runtime-built path like \"lookup.constructor\" on a\n // z.record must read as absent, not leak Object.prototype members as field\n // values.\n return isPlainRecord(obj) && Object.hasOwn(obj, String(segment))\n ? obj[String(segment)]\n : undefined;\n};\n\nexport const getAtPath = (obj: unknown, path: string): unknown =>\n parsePath(path).reduce<unknown>(readSegment, obj);\n\nexport type SlotAtPath =\n | Readonly<{ exists: true; value: unknown }>\n | Readonly<{ exists: false }>;\n\n// Resolve `path` while checking that every segment is read from a real\n// container: an in-range array index, or a key on a plain record (containers\n// decide, mirroring readSegment). A segment into an absent or non-container\n// value addresses no slot at all — a full-form parse would key that error at\n// the missing ANCESTOR, not here, so field-scoped validation must skip it\n// rather than parse the leaf's subschema against undefined (which would\n// fabricate a spurious \"required\" error the full pass never produces). Note\n// this checks the CONTAINER, not the value: a leaf that is undefined at the\n// FINAL step through a real record is still an addressable (legitimately\n// empty) slot. Returns the resolved value so callers don't walk twice.\nexport const slotAtPath = (obj: unknown, path: string): SlotAtPath => {\n const walked = parsePath(path).reduce<\n Readonly<{ ok: boolean; current: unknown }>\n >(\n (acc, segment) => {\n const container = acc.current;\n const addressable =\n typeof segment === \"number\"\n ? Array.isArray(container)\n ? segment < container.length\n : isPlainRecord(container)\n : isPlainRecord(container);\n return !acc.ok || !addressable\n ? { ok: false, current: undefined }\n : { ok: true, current: readSegment(container, segment) };\n },\n { ok: true, current: obj },\n );\n return walked.ok ? { exists: true, value: walked.current } : { exists: false };\n};\n\n// Whether the path addresses an OWN slot: every step reads a real\n// container's own key / in-range array index, the final segment included.\n// This is stricter than slotAtPath, which treats a MISSING final key on a\n// real record as an addressable (legitimately empty) slot — here it reads\n// false. The consumer is setValue's identity guard: writing `undefined`\n// over an ABSENT key is NOT a no-op (it creates the key, a real state\n// change — key-count dirtiness, persisted shape), while writing it over a\n// key that exists holding undefined is.\nexport const hasOwnSlot = (obj: unknown, path: string): boolean =>\n parsePath(path).reduce<Readonly<{ ok: boolean; current: unknown }>>(\n (acc, segment) => {\n const container = acc.current;\n const owns = Array.isArray(container)\n ? typeof segment === \"number\" && segment < container.length\n : isPlainRecord(container) && Object.hasOwn(container, String(segment));\n return !acc.ok || !owns\n ? { ok: false, current: undefined }\n : { ok: true, current: readSegment(container, segment) };\n },\n { ok: true, current: obj },\n ).ok;\n\nconst writeIntoArray = (\n arr: readonly unknown[],\n index: number,\n rest: readonly PathSegment[],\n value: unknown,\n): readonly unknown[] => {\n const length = Math.max(arr.length, index + 1);\n return Array.from({ length }, (_, i) =>\n i === index ? writeSegments(arr[i], rest, value) : arr[i],\n );\n};\n\nconst refusesHugeIndex = (index: number): boolean => {\n if (index > MAX_ARRAY_INDEX) {\n console.warn(\n `[formstand] refusing to write array index ${index} (max ${MAX_ARRAY_INDEX}); value left unchanged.`,\n );\n return true;\n }\n return false;\n};\n\nconst writeSegments = (\n obj: unknown,\n segments: readonly PathSegment[],\n value: unknown,\n): unknown => {\n const [head, ...rest] = segments;\n if (head === undefined) return value;\n\n if (Array.isArray(obj)) {\n if (typeof head !== \"number\") {\n console.warn(\n `[formstand] cannot write string key \"${head}\" into an array; value left unchanged.`,\n );\n return obj;\n }\n if (refusesHugeIndex(head)) return obj;\n return writeIntoArray(obj, head, rest, value);\n }\n\n // Records take any segment as a string key (mirroring readSegment), so a\n // z.record keyed by a large numeric id is writable — the array-index cap\n // only applies where an array actually exists or would be created.\n if (isPlainRecord(obj)) {\n // Spreading is only safe for PLAIN objects: spreading a Date/Map/Set or\n // class instance keeps own enumerable props but drops the prototype (and\n // for Map/Set, every entry) — the original would be silently destroyed.\n // Refuse, like the other unwritable shapes above.\n if (!isPlainObject(obj)) {\n console.warn(\n `[formstand] cannot write key \"${String(head)}\" through a non-plain object (Date/Map/Set/class instance); value left unchanged.`,\n );\n return obj;\n }\n const key = String(head);\n return { ...obj, [key]: writeSegments(obj[key], rest, value) };\n }\n\n // Absent (or non-container) value: create a container from the segment type.\n if (typeof head === \"number\") {\n if (refusesHugeIndex(head)) return obj;\n return writeIntoArray([], head, rest, value);\n }\n return { [head]: writeSegments(undefined, rest, value) };\n};\n\nexport const setAtPath = <T>(obj: T, path: string, value: unknown): T =>\n writeSegments(obj, parsePath(path), value) as T;\n","import type { z } from \"zod\";\nimport type { StoreApi } from \"zustand/vanilla\";\nimport { useStore } from \"zustand/react\";\nimport { useShallow } from \"zustand/react/shallow\";\nimport type { Form } from \"../core/createForm\";\nimport type { DefaultPathDepth, PathDepth } from \"../core/fieldPath\";\nimport type { FormState } from \"../core/types\";\n\ntype ReadonlyStore<T> = Pick<\n StoreApi<T>,\n \"getState\" | \"getInitialState\" | \"subscribe\"\n>;\n\nexport type FormStateApi = Readonly<{\n store: ReadonlyStore<FormState<unknown>>;\n}>;\n\nexport function useFormSelector<\n TSchema extends z.ZodType,\n U,\n D extends PathDepth = DefaultPathDepth,\n>(\n form: Form<TSchema, D>,\n selector: (state: FormState<z.input<TSchema>>) => U,\n): U;\n// The `schema?: undefined` brand forces Form<TSchema> (which has a real\n// `schema` property) to bind only the typed overload above. Without\n// it, Form<TSchema> would structurally satisfy FormStateApi too and a\n// bad selector could silently widen to FormState<unknown>.\nexport function useFormSelector<U>(\n form: FormStateApi & { readonly schema?: undefined },\n selector: (state: FormState<unknown>) => U,\n): U;\nexport function useFormSelector<U>(\n form: FormStateApi,\n selector: (state: FormState<unknown>) => U,\n): U {\n return useStore(form.store, selector);\n}\n\nexport function useFormSelectorShallow<\n TSchema extends z.ZodType,\n U,\n D extends PathDepth = DefaultPathDepth,\n>(\n form: Form<TSchema, D>,\n selector: (state: FormState<z.input<TSchema>>) => U,\n): U;\nexport function useFormSelectorShallow<U>(\n form: FormStateApi & { readonly schema?: undefined },\n selector: (state: FormState<unknown>) => U,\n): U;\nexport function useFormSelectorShallow<U>(\n form: FormStateApi,\n selector: (state: FormState<unknown>) => U,\n): U {\n return useStore(form.store, useShallow(selector));\n}\n","// The dev-build test the library consults before emitting a console\n// warning. Bundlers that define NODE_ENV fold it to a constant and drop the\n// dev-only branches; the `typeof process` guard keeps the SAME expression\n// from throwing in a runtime that defines no `process` at all (a CDN/ESM\n// consumer, a test runner without the define). That last case matters most\n// on the submit path: handleSubmit promises never to reject, and the\n// warning that fires only when a save failed was the one line that could\n// break the promise exactly on the failure path. It FAILS OPEN on purpose:\n// an unknowable environment gets the warnings, which cost nothing but a\n// console line.\nexport const isDevBuild = (): boolean =>\n typeof process === \"undefined\" ||\n process.env?.[\"NODE_ENV\"] !== \"production\";\n\n// The gate for the two sites that EXPOSE form state: the FormstandDevtools\n// panel (renders every value and error into the page) and the zustand\n// devtools middleware (streams every write to the Redux extension). These\n// must FAIL CLOSED: isDevBuild's \"no process means dev\" reading let a\n// CDN/ESM, Deno, or worker production build mount the panel and keep the\n// middleware live for any end user with the extension installed. Only a\n// readable NODE_ENV that is a string other than \"production\" counts as\n// development; no process, no env, or an unset NODE_ENV all read as\n// production here.\nexport const isDevToolsBuild = (): boolean => {\n if (typeof process === \"undefined\") return false;\n const env = process.env?.[\"NODE_ENV\"];\n return typeof env === \"string\" && env !== \"production\";\n};\n"]}
@@ -1,3 +1,7 @@
1
+ // src/core/maps.ts
2
+ var entryAt = (map, key) => Object.hasOwn(map, key) ? map[key] : void 0;
3
+ var flagAt = (map, key) => Object.hasOwn(map, key) && map[key] === true;
4
+
1
5
  // src/core/mode.ts
2
6
  var shouldValidateOn = (trigger, mode, reValidateMode, submitAttempted, touched = false) => {
3
7
  const effective = submitAttempted ? reValidateMode : mode;
@@ -15,6 +19,6 @@ var shouldValidateOn = (trigger, mode, reValidateMode, submitAttempted, touched
15
19
  }
16
20
  };
17
21
 
18
- export { shouldValidateOn };
19
- //# sourceMappingURL=chunk-MQ6262S3.js.map
20
- //# sourceMappingURL=chunk-MQ6262S3.js.map
22
+ export { entryAt, flagAt, shouldValidateOn };
23
+ //# sourceMappingURL=chunk-V3PDMTPP.js.map
24
+ //# sourceMappingURL=chunk-V3PDMTPP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/maps.ts","../src/core/mode.ts"],"names":[],"mappings":";AAYO,IAAM,OAAA,GAAU,CACrB,GAAA,EACA,GAAA,KACmB,MAAA,CAAO,MAAA,CAAO,GAAA,EAAK,GAAG,CAAA,GAAI,GAAA,CAAI,GAAG,CAAA,GAAI;AAEnD,IAAM,MAAA,GAAS,CAAC,GAAA,EAAc,GAAA,KACnC,MAAA,CAAO,MAAA,CAAO,GAAA,EAAK,GAAG,CAAA,IAAK,GAAA,CAAI,GAAG,CAAA,KAAM;;;ACPnC,IAAM,mBAAmB,CAC9B,OAAA,EACA,MACA,cAAA,EACA,eAAA,EACA,UAAmB,KAAA,KACP;AACZ,EAAA,MAAM,SAAA,GAAY,kBAAkB,cAAA,GAAiB,IAAA;AACrD,EAAA,QAAQ,SAAA;AAAW,IACjB,KAAK,UAAA;AACH,MAAA,OAAO,OAAA,KAAY,QAAA;AAAA,IACrB,KAAK,QAAA;AACH,MAAA,OAAO,OAAA,KAAY,MAAA;AAAA,IACrB,KAAK,UAAA;AACH,MAAA,OAAO,KAAA;AAAA,IACT,KAAK,WAAA;AACH,MAAA,OAAO,YAAY,MAAA,IAAU,OAAA;AAAA,IAC/B,KAAK,KAAA;AACH,MAAA,OAAO,IAAA;AAAA;AAEb","file":"chunk-V3PDMTPP.js","sourcesContent":["import type { BoolMap } from \"./types\";\n\n// Own-key reads for the path-keyed maps (errors, touched, isValidating).\n// Their keys are user-controlled strings: a schema may declare a field\n// named `constructor`, a server response may name `toString`, and\n// JSON.parse yields own `__proto__` keys. A bare `map[key]` on a plain\n// object walks the prototype chain for those, so an EMPTY error map read\n// `Object` as the error list at \"constructor\" (and `stringArraysEqual`\n// then threw inside setState on `.every`), while `touched[\"toString\"]` was\n// a truthy function. Every read of these maps by a runtime key goes\n// through here; writes are already safe (computed keys and\n// Object.fromEntries define own properties).\nexport const entryAt = <V>(\n map: Readonly<Record<string, V>>,\n key: string,\n): V | undefined => (Object.hasOwn(map, key) ? map[key] : undefined);\n\nexport const flagAt = (map: BoolMap, key: string): boolean =>\n Object.hasOwn(map, key) && map[key] === true;\n","export type ValidationMode =\n | \"onChange\"\n | \"onBlur\"\n | \"onSubmit\"\n // Validate on blur always; on change only once the field has been touched.\n | \"onTouched\"\n // Validate on both change and blur.\n | \"all\";\n\nexport type ValidationTrigger = \"change\" | \"blur\";\n\nexport const shouldValidateOn = (\n trigger: ValidationTrigger,\n mode: ValidationMode,\n reValidateMode: ValidationMode,\n submitAttempted: boolean,\n touched: boolean = false,\n): boolean => {\n const effective = submitAttempted ? reValidateMode : mode;\n switch (effective) {\n case \"onChange\":\n return trigger === \"change\";\n case \"onBlur\":\n return trigger === \"blur\";\n case \"onSubmit\":\n return false;\n case \"onTouched\":\n return trigger === \"blur\" || touched;\n case \"all\":\n return true;\n }\n};\n"]}
@@ -50,6 +50,10 @@ type FieldPathRec<T, D extends number> = [D] extends [0] ? never : IsTuple<T> ex
50
50
  }[keyof T & string] : never;
51
51
  type StepValue<T, K extends string> = T extends readonly (infer U)[] ? K extends keyof T ? T[K] : K extends `${number}` ? U : never : T extends object ? K extends keyof T ? T[K] : never : never;
52
52
  type FieldValue<T, P extends string> = [T] extends [NonNullable<T>] ? P extends `${infer Head}.${infer Tail}` ? FieldValue<StepValue<T, Head>, Tail> : StepValue<T, P> : FieldValue<NonNullable<T>, P> | undefined;
53
+ type ArrayItemOf<T> = [NonNullable<T>] extends [readonly (infer U)[]] ? number extends NonNullable<T>["length"] ? U : never : never;
54
+ type IsListValue<V> = [NonNullable<V>] extends [readonly unknown[]] ? number extends NonNullable<V>["length"] ? true : false : false;
55
+ type ArrayFieldPath<T, D extends number = DefaultPathDepth> = ArrayPathsWhere<T, FieldPath<T, D>>;
56
+ type ArrayPathsWhere<T, P extends string> = P extends unknown ? IsListValue<FieldValue<T, P>> extends true ? P : never : never;
53
57
 
54
58
  type ValidationMode = "onChange" | "onBlur" | "onSubmit" | "onTouched" | "all";
55
59
  type ValidationTrigger = "change" | "blur";
@@ -137,7 +141,6 @@ type SubmitResult<TOutput> = Readonly<{
137
141
  kind: "error";
138
142
  error: unknown;
139
143
  }>;
140
- type ArrayItemOf<T> = T extends readonly (infer U)[] ? U : never;
141
144
  type ErrorPath<TValues, D extends PathDepth = DefaultPathDepth> = FieldPath<TValues, D> | "";
142
145
  type ResetOptions = Readonly<{
143
146
  keepErrors?: boolean;
@@ -183,11 +186,11 @@ type Form<TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth> = R
183
186
  validateFieldAsync: (path: ErrorPath<z.input<TSchema>, D>) => Promise<SettledFieldValidationResult>;
184
187
  validateFieldsAsync: (paths: readonly FieldPath<z.input<TSchema>, D>[]) => Promise<SettledFieldsValidationResult>;
185
188
  submit: (onValid: SubmitHandler<TSchema>, onInvalid?: InvalidSubmitHandler, options?: SubmitOptions) => Promise<SubmitResult<z.output<TSchema>>>;
186
- arrayPush: <P extends FieldPath<z.input<TSchema>, D>>(path: P, item: ArrayItemOf<NonNullable<FieldValue<z.input<TSchema>, P>>>) => void;
187
- arrayRemove: (path: FieldPath<z.input<TSchema>, D>, index: number) => void;
188
- arrayInsert: <P extends FieldPath<z.input<TSchema>, D>>(path: P, index: number, item: ArrayItemOf<NonNullable<FieldValue<z.input<TSchema>, P>>>) => void;
189
- arrayMove: (path: FieldPath<z.input<TSchema>, D>, from: number, to: number) => void;
190
- arraySwap: (path: FieldPath<z.input<TSchema>, D>, a: number, b: number) => void;
189
+ arrayPush: <P extends ArrayFieldPath<z.input<TSchema>, D>>(path: P, item: ArrayItemOf<FieldValue<z.input<TSchema>, P>>) => void;
190
+ arrayRemove: (path: ArrayFieldPath<z.input<TSchema>, D>, index: number) => void;
191
+ arrayInsert: <P extends ArrayFieldPath<z.input<TSchema>, D>>(path: P, index: number, item: ArrayItemOf<FieldValue<z.input<TSchema>, P>>) => void;
192
+ arrayMove: (path: ArrayFieldPath<z.input<TSchema>, D>, from: number, to: number) => void;
193
+ arraySwap: (path: ArrayFieldPath<z.input<TSchema>, D>, a: number, b: number) => void;
191
194
  handleSubmit: (onValid: SubmitHandler<TSchema>, onInvalid?: InvalidSubmitHandler, options?: SubmitOptions) => (event?: {
192
195
  preventDefault: () => void;
193
196
  }) => Promise<SubmitResult<z.output<TSchema>>>;
@@ -198,4 +201,4 @@ type Form<TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth> = R
198
201
  }>;
199
202
  declare const createForm: <TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth>(schema: TSchema, options: CreateFormOptions<TSchema, D>) => Form<TSchema, D>;
200
203
 
201
- export { type BoolMap as B, type CreateFormOptions as C, type DefaultPathDepth as D, type ErrorMap as E, type Form as F, type InvalidSubmitHandler as I, type PathDepth as P, type ReadonlyStoreApi as R, type SettledFieldValidationResult as S, type ValidationMode as V, type FieldValidationResult as a, type FormState as b, type FieldPath as c, type FieldValue as d, type FieldSnapshot as e, type FieldsValidationResult as f, type ResetOptions as g, type SettledFieldsValidationResult as h, type SettledValidationResult as i, type SubmitHandler as j, type SubmitOptions as k, type SubmitResult as l, type ValidationResult as m, type ValidationTrigger as n, createForm as o, emptyValueForSchema as p, flattenIssues as q, isAsyncRequiredError as r, isClearableSchema as s, shouldValidateOn as t, validateSync as u, validateAsync as v };
204
+ export { type ArrayFieldPath as A, type BoolMap as B, type CreateFormOptions as C, type DefaultPathDepth as D, type ErrorMap as E, type Form as F, type InvalidSubmitHandler as I, type PathDepth as P, type ReadonlyStoreApi as R, type SettledFieldValidationResult as S, type ValidationMode as V, type FormState as a, type FieldValidationResult as b, type FieldPath as c, type FieldValue as d, type ArrayItemOf as e, type FieldSnapshot as f, type FieldsValidationResult as g, type ResetOptions as h, type SettledFieldsValidationResult as i, type SettledValidationResult as j, type SubmitHandler as k, type SubmitOptions as l, type SubmitResult as m, type ValidationResult as n, type ValidationTrigger as o, createForm as p, emptyValueForSchema as q, flattenIssues as r, isAsyncRequiredError as s, isClearableSchema as t, shouldValidateOn as u, validateAsync as v, validateSync as w };
@@ -50,6 +50,10 @@ type FieldPathRec<T, D extends number> = [D] extends [0] ? never : IsTuple<T> ex
50
50
  }[keyof T & string] : never;
51
51
  type StepValue<T, K extends string> = T extends readonly (infer U)[] ? K extends keyof T ? T[K] : K extends `${number}` ? U : never : T extends object ? K extends keyof T ? T[K] : never : never;
52
52
  type FieldValue<T, P extends string> = [T] extends [NonNullable<T>] ? P extends `${infer Head}.${infer Tail}` ? FieldValue<StepValue<T, Head>, Tail> : StepValue<T, P> : FieldValue<NonNullable<T>, P> | undefined;
53
+ type ArrayItemOf<T> = [NonNullable<T>] extends [readonly (infer U)[]] ? number extends NonNullable<T>["length"] ? U : never : never;
54
+ type IsListValue<V> = [NonNullable<V>] extends [readonly unknown[]] ? number extends NonNullable<V>["length"] ? true : false : false;
55
+ type ArrayFieldPath<T, D extends number = DefaultPathDepth> = ArrayPathsWhere<T, FieldPath<T, D>>;
56
+ type ArrayPathsWhere<T, P extends string> = P extends unknown ? IsListValue<FieldValue<T, P>> extends true ? P : never : never;
53
57
 
54
58
  type ValidationMode = "onChange" | "onBlur" | "onSubmit" | "onTouched" | "all";
55
59
  type ValidationTrigger = "change" | "blur";
@@ -137,7 +141,6 @@ type SubmitResult<TOutput> = Readonly<{
137
141
  kind: "error";
138
142
  error: unknown;
139
143
  }>;
140
- type ArrayItemOf<T> = T extends readonly (infer U)[] ? U : never;
141
144
  type ErrorPath<TValues, D extends PathDepth = DefaultPathDepth> = FieldPath<TValues, D> | "";
142
145
  type ResetOptions = Readonly<{
143
146
  keepErrors?: boolean;
@@ -183,11 +186,11 @@ type Form<TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth> = R
183
186
  validateFieldAsync: (path: ErrorPath<z.input<TSchema>, D>) => Promise<SettledFieldValidationResult>;
184
187
  validateFieldsAsync: (paths: readonly FieldPath<z.input<TSchema>, D>[]) => Promise<SettledFieldsValidationResult>;
185
188
  submit: (onValid: SubmitHandler<TSchema>, onInvalid?: InvalidSubmitHandler, options?: SubmitOptions) => Promise<SubmitResult<z.output<TSchema>>>;
186
- arrayPush: <P extends FieldPath<z.input<TSchema>, D>>(path: P, item: ArrayItemOf<NonNullable<FieldValue<z.input<TSchema>, P>>>) => void;
187
- arrayRemove: (path: FieldPath<z.input<TSchema>, D>, index: number) => void;
188
- arrayInsert: <P extends FieldPath<z.input<TSchema>, D>>(path: P, index: number, item: ArrayItemOf<NonNullable<FieldValue<z.input<TSchema>, P>>>) => void;
189
- arrayMove: (path: FieldPath<z.input<TSchema>, D>, from: number, to: number) => void;
190
- arraySwap: (path: FieldPath<z.input<TSchema>, D>, a: number, b: number) => void;
189
+ arrayPush: <P extends ArrayFieldPath<z.input<TSchema>, D>>(path: P, item: ArrayItemOf<FieldValue<z.input<TSchema>, P>>) => void;
190
+ arrayRemove: (path: ArrayFieldPath<z.input<TSchema>, D>, index: number) => void;
191
+ arrayInsert: <P extends ArrayFieldPath<z.input<TSchema>, D>>(path: P, index: number, item: ArrayItemOf<FieldValue<z.input<TSchema>, P>>) => void;
192
+ arrayMove: (path: ArrayFieldPath<z.input<TSchema>, D>, from: number, to: number) => void;
193
+ arraySwap: (path: ArrayFieldPath<z.input<TSchema>, D>, a: number, b: number) => void;
191
194
  handleSubmit: (onValid: SubmitHandler<TSchema>, onInvalid?: InvalidSubmitHandler, options?: SubmitOptions) => (event?: {
192
195
  preventDefault: () => void;
193
196
  }) => Promise<SubmitResult<z.output<TSchema>>>;
@@ -198,4 +201,4 @@ type Form<TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth> = R
198
201
  }>;
199
202
  declare const createForm: <TSchema extends z.ZodType, D extends PathDepth = DefaultPathDepth>(schema: TSchema, options: CreateFormOptions<TSchema, D>) => Form<TSchema, D>;
200
203
 
201
- export { type BoolMap as B, type CreateFormOptions as C, type DefaultPathDepth as D, type ErrorMap as E, type Form as F, type InvalidSubmitHandler as I, type PathDepth as P, type ReadonlyStoreApi as R, type SettledFieldValidationResult as S, type ValidationMode as V, type FieldValidationResult as a, type FormState as b, type FieldPath as c, type FieldValue as d, type FieldSnapshot as e, type FieldsValidationResult as f, type ResetOptions as g, type SettledFieldsValidationResult as h, type SettledValidationResult as i, type SubmitHandler as j, type SubmitOptions as k, type SubmitResult as l, type ValidationResult as m, type ValidationTrigger as n, createForm as o, emptyValueForSchema as p, flattenIssues as q, isAsyncRequiredError as r, isClearableSchema as s, shouldValidateOn as t, validateSync as u, validateAsync as v };
204
+ export { type ArrayFieldPath as A, type BoolMap as B, type CreateFormOptions as C, type DefaultPathDepth as D, type ErrorMap as E, type Form as F, type InvalidSubmitHandler as I, type PathDepth as P, type ReadonlyStoreApi as R, type SettledFieldValidationResult as S, type ValidationMode as V, type FormState as a, type FieldValidationResult as b, type FieldPath as c, type FieldValue as d, type ArrayItemOf as e, type FieldSnapshot as f, type FieldsValidationResult as g, type ResetOptions as h, type SettledFieldsValidationResult as i, type SettledValidationResult as j, type SubmitHandler as k, type SubmitOptions as l, type SubmitResult as m, type ValidationResult as n, type ValidationTrigger as o, createForm as p, emptyValueForSchema as q, flattenIssues as r, isAsyncRequiredError as s, isClearableSchema as t, shouldValidateOn as u, validateAsync as v, validateSync as w };
@@ -7,6 +7,13 @@ var jsxRuntime = require('react/jsx-runtime');
7
7
 
8
8
  // src/devtools/FormstandDevtools.tsx
9
9
 
10
+ // src/core/env.ts
11
+ var isDevToolsBuild = () => {
12
+ if (typeof process === "undefined") return false;
13
+ const env = process.env?.["NODE_ENV"];
14
+ return typeof env === "string" && env !== "production";
15
+ };
16
+
10
17
  // src/core/path.ts
11
18
  var isIndexSegment = (s) => {
12
19
  const n = Number(s);
@@ -156,7 +163,6 @@ var styles = {
156
163
  cursor: "pointer"
157
164
  }
158
165
  };
159
- var isDevBuild = () => typeof process === "undefined" || process.env?.["NODE_ENV"] !== "production";
160
166
  var preview = (value) => {
161
167
  if (value === void 0) return "undefined";
162
168
  if (value instanceof Date) return value.toISOString();
@@ -311,7 +317,7 @@ var DevtoolsPanel = ({
311
317
  }
312
318
  ) });
313
319
  };
314
- var FormstandDevtools = (props) => isDevBuild() ? /* @__PURE__ */ jsxRuntime.jsx(DevtoolsPanel, { ...props }) : null;
320
+ var FormstandDevtools = (props) => isDevToolsBuild() ? /* @__PURE__ */ jsxRuntime.jsx(DevtoolsPanel, { ...props }) : null;
315
321
 
316
322
  exports.FormstandDevtools = FormstandDevtools;
317
323
  exports.leafPaths = leafPaths;