formstand 0.12.0 → 0.14.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,22 +1,290 @@
1
1
  # Changelog
2
2
 
3
- ## 0.12.0 — 2026-07-29
3
+ ## 0.14.0 — 2026-07-31
4
+
5
+ ### Added
6
+
7
+ - **`formstand/devtools`**: a form-aware debugging panel, `<FormstandDevtools
8
+ form={form} />`, shipped as a subpath of the existing package so there is
9
+ nothing extra to install and no version to keep in step. It renders one row
10
+ per leaf path with the value, error, and touched/dirty/validating flags;
11
+ surfaces the errors that belong to no field (the root `""` key, array-level
12
+ messages, server verdicts on paths that hold no value yet); shows the two
13
+ error channels separately, including whether a server verdict is visible or
14
+ sitting behind a schema message at the same key; renders `diff()` as a live
15
+ PATCH preview; and wires `snapshot()`/`restore()` to buttons for time
16
+ travel. Renders nothing in production builds, on the same `NODE_ENV` gate
17
+ `createForm`'s `devtools` option uses.
18
+
19
+ It complements rather than replaces that option: the Redux DevTools
20
+ extension gives a cross-store action log but only ever sees the merged error
21
+ map, which is exactly the distinction this panel exists to show. Dirtiness
22
+ comes from `getFieldState` so the panel can't disagree with `useIsDirty`,
23
+ and styling is inline so no host stylesheet can restyle it into lying.
24
+
25
+ ## formstand-cli 0.10.1 — 2026-07-30
26
+
27
+ ### Fixed
28
+
29
+ - **The template-hole regex is linear again** (CodeQL `js/polynomial-redos`,
30
+ high). `pathSegmentCount` strips holes like `${index}` before counting dot
31
+ segments, and its pattern excluded only the closing brace, so an
32
+ unterminated run such as `"${{${{${{..."` rescanned to the end of the
33
+ string from every `${` start: 1.8 seconds at 32k repetitions, growing
34
+ quadratically. Excluding both braces bounds each scan at the next brace
35
+ and makes the same input flat. A hole is always a plain identifier, so
36
+ real paths are unaffected and every committed demo regenerates
37
+ byte-identical. Matters because `formstand-cli/codegen` is the documented
38
+ browser-safe surface: the playground's Schema builder runs it on pasted
39
+ input, and anything built on it may accept a schema it did not author. The
40
+ binary alone was never really exposed, since generating from a hostile
41
+ schema already executes that module.
42
+
43
+ ## 0.13.0 — 2026-07-29
44
+
45
+ ### Added
46
+
47
+ - **`useFormValues(form)`** — whole-values subscription in render: returns
48
+ the form's current values object, typed `z.input<TSchema>` (structural
49
+ `FormStateApi` forms read `unknown`). Sugar for
50
+ `useFormSelector(form, (s) => s.values)`, reference-compared — the store
51
+ replaces the values object immutably, so the hook re-renders exactly when
52
+ some value changes and never on touched/error/submit churn. The motivating
53
+ case: form values driving derived rendering (a map re-rendering from live
54
+ coordinates). The playground's three whole-values subscriptions (Autosave,
55
+ the MUI checkout's review step, the Schema builder's live preview) now use
56
+ it, and the MUI adapter's number binding now goes through the exported
57
+ `useNumberInput` (as `useMuiNumberFieldProps`) — the same shape
58
+ `formstand-gen --ui mui` emits.
59
+
60
+ ### Fixed
61
+
62
+ - **Explicit type arguments on a schema-typed form now fail with a readable
63
+ error.** `useField<Values>(form, "email")` (any explicit generics on a
64
+ `Form` — the value type infers from the path, so they were always an
65
+ error) used to resolve onto the schema-less overload and die with a
66
+ baffling `"... not assignable to type 'undefined'"` brand mismatch. A
67
+ trap-guard overload now intercepts that shape and blames the path
68
+ argument with instructions: `Argument of type '"email"' is not assignable
69
+ to parameter of type '"Remove the explicit type argument: a schema-typed
70
+ form infers the value type from the path"'`. Same guard on
71
+ `useFieldArray` (`"... infers the item type from the path"`) and
72
+ `useVariantField` (`"... infers the variant value from the union path
73
+ and field"`). Inferred calls, structural-form explicit generics,
74
+ selector paths, and explicit instantiation expressions
75
+ (`typeof useField<Schema, "name">`,
76
+ `typeof useVariantField<Schema, "payment", "cardNumber">`) are all
77
+ unaffected — the guard's type parameter defaults to an unexported
78
+ sentinel no inferred call can produce.
79
+ - **`setValue` with the identical value is now a full no-op.** Writing the
80
+ value already at the path used to rebuild the values object anyway, so
81
+ `useFormValues` re-rendered and `watchValues`/`onValuesChange` fired on
82
+ writes that changed nothing — and a two-way sync that echoes values back
83
+ (each side writing what it just received) recursed until the stack
84
+ overflowed. `form.setValue` now identity-guards (`Object.is`) the write:
85
+ the state reference is unchanged, no dirty/error recompute runs, no
86
+ subscribers fire, and `useField().setValue` skips its revalidation gate
87
+ too (nothing changed, so the current error state already reflects the
88
+ value). The no-op is deliberately FULL: a no-op write also no longer
89
+ releases server errors on the path or clears array-op records — those
90
+ side effects belonged to an actual value change. One carve-out: writing
91
+ `undefined` over an ABSENT key is still a real write (it creates the
92
+ key — `{}` vs `{ nickname: undefined }` differ in key count, which
93
+ dirtiness and the persisted shape observe). `setValues`, `adoptValues`,
94
+ and `updateState` replace wholesale by design and are unchanged.
95
+
96
+ ## formstand-cli 0.10.0 — 2026-07-29
97
+
98
+ ### Added
99
+
100
+ - **Per-field component overrides — `fields` in `formstand.config.ts`,
101
+ with an `autocomplete` flavor and a generated options prop.** The
102
+ motivating case (straight from the dogfood project): an ICAO airport
103
+ field is a plain `z.string()` whose suggestion list is DATA — an airport
104
+ list, not a zod enum — so the CLI emitted a bare text input and the page
105
+ hand-swapped it for the kit's autocomplete on every regeneration. Now
106
+ the config names the field and the component:
107
+
108
+ ```ts
109
+ export default defineConfig({
110
+ fields: {
111
+ "icao": { component: "autocomplete", optionsProp: true },
112
+ "crew.*.role": { component: "autocomplete", optionsProp: true },
113
+ },
114
+ });
115
+ ```
116
+
117
+ Paths are exact dot paths against the walked schema, `*` matching one
118
+ array-index segment. The semantic is **free text with suggestions** (the
119
+ one autocomplete meaning every kit shares): the field stays a string the
120
+ user can type freely; strict select-from-list remains the enum/Select
121
+ path. Overrides apply to **string** fields (`optionsProp: true` is
122
+ required — there is no other options source; the generated component
123
+ gains `{camelPath}Options: readonly string[]`, e.g. `"crew.*.role"` →
124
+ `crewRoleOptions`, collision-suffixed like every derived identifier) and
125
+ **enum** fields (the select upgrades to a combobox seeded with the enum
126
+ values baked in; `optionsProp: true` REPLACES them with the prop).
127
+ Everything else is a loud generation-time ERROR, exit 1, nothing
128
+ written: an unknown path (with near-miss suggestions), a non-string/enum
129
+ target, a string without `optionsProp`, a walker-degraded or
130
+ over-FieldPath-budget field, an override under a row-nested object with
131
+ `--layout module` (that layout degrades those to a TODO). Per-kit
132
+ bindings, each proven against the real installed `.d.ts` by the matrix:
133
+ MUI `Autocomplete` `freeSolo` bound through the INPUT value
134
+ (`inputValue`/`onInputChange`, so typing and picking both update the
135
+ form); Mantine `Autocomplete` (natively this semantic — value-shaped
136
+ `onChange`, `data` accepts readonly arrays); antd `AutoComplete`
137
+ (value-shaped like its Select, `status` for errors, `id={path}` for the
138
+ focus-helper fallback — no `name` exists); plain, shadcn, and Chakra ride
139
+ an `Input` + native `<datalist>` (shadcn's combobox is a copy-paste
140
+ recipe, not an installable component; Chakra 3's `Combobox` is Ark's
141
+ collection-API compound component — disproportionate for generated
142
+ free-text suggestions, so the DOM-shaped datalist is the honest
143
+ binding). Both layouts thread the options prop root → section → row/field
144
+ files (nested-array extractions included); composes with `--live` and
145
+ `--form-prop` (the props join the same props type); an overridden field
146
+ wins over a custom `--template`'s per-kind renderer (templates own
147
+ per-KIND rendering; an override opts the field out of its kind). Each
148
+ override site carries a short generated comment naming the options
149
+ source. `component: "autocomplete"` is the only flavor this cycle; the
150
+ config shape is a discriminated union so future flavors (textarea,
151
+ slider, ...) slot in. The playground's flight-search demo now generates
152
+ its origin/destination ICAO fields through the override (the suggestion
153
+ list passed by the page), making the dogfood project's exact page real
154
+ CLI output.
155
+ - **`--live` — live/no-submit scaffold mode** (config key `live`). For
156
+ forms nothing ever submits (a map or preview re-rendering from every
157
+ value change): the submit scaffold — `handleSubmit`, the submit button,
158
+ `useIsSubmitting` — is omitted entirely; the generated component instead
159
+ accepts an optional `onValuesChange?: (values) => void` prop wired
160
+ through `form.watchValues` in a `useEffect` (watchValues has shipped
161
+ since formstand 0.2 and returns its own unsubscribe, so generated output
162
+ keeps a low version floor; the emitted comment names `useFormValues` as
163
+ the post-0.12 render-side one-liner). The root element stays a `<form>`
164
+ (label association, the form landmark, kit chrome unchanged) with a
165
+ `preventDefault` `onSubmit` so the browser's implicit Enter-key
166
+ submission can't navigate the page. The emitted validation mode defaults
167
+ to `"onChange"` — a live consumer wants validity that tracks the values,
168
+ not the library-default `"onBlur"` lag (noted in the emitted comment).
169
+ Kit `Button` imports stay usage-gated: array add/remove buttons keep
170
+ them, an array-free live form drops them. Works with all six `--ui`
171
+ kits, custom `--template`s, and both layouts (`--layout module` defaults
172
+ the singleton's `createForm` mode and rewires the form file; the
173
+ pre-wired hook API is unchanged).
174
+ - **`--form-prop` — page-owned-form scaffold mode** (config key
175
+ `formProp`). The generated component's props gain
176
+ `form: Form<typeof schema>` and it stops creating its own form; the
177
+ `useForm` scaffold is still emitted, as an exported `use{Name}Form()`
178
+ hook the page calls — so one instance can feed the generated UI and any
179
+ other consumer (the dogfood case: a page-level form driving a map AND
180
+ the form UI). In `--layout module` the component takes the same prop for
181
+ its shell (submit/subscription) while the field hooks stay pre-wired to
182
+ the module singleton — the emitted props comment says to pass that
183
+ exported instance. Composes with `--live`: the component becomes pure
184
+ rendering and `onValuesChange` subscribes to the passed form.
185
+ - Internal: the per-kit form shells (single-file backends and the module
186
+ layout) are restructured into shared open/onSubmit/submit-button/close
187
+ pieces, so the submit and `--live` shapes are composed from one
188
+ production and cannot drift per kit. Default-flag output is
189
+ byte-identical.
190
+ - **Playground: the two modes as a living demo** — a new Generated tab
191
+ (**Gen: live + form prop**) renders a compact flight-search schema
192
+ generated with `--live --form-prop` (`--ui plain`, single-file,
193
+ committed untouched and drift-checked via
194
+ `scripts/generate-cli-demos.mjs` like the other generated demos) next to
195
+ a hand-written consumer page: the page owns the form via the exported
196
+ `useFlightSearchForm()` hook and a live values panel renders only what
197
+ `onValuesChange` delivers — updating per keystroke, the map-driven case
198
+ in miniature. Smoke-tested in the root suite (mount, type, assert the
199
+ panel reflects the keystroke; no submit scaffold in the output).
200
+ - **zod `.describe()` / `.meta({ description })` → generated helper text.**
201
+ The walkers capture a field's description into the IR — zod v4 stores
202
+ both spellings in ONE registry entry (`z.globalRegistry`, surfaced by the
203
+ schema's `description` getter; nothing on `def`), so the rule is simply
204
+ "the last `.describe()`/`.meta()` call wins", and across wrappers
205
+ (`z.number().describe("x").optional()` vs `.optional().describe("x")`)
206
+ the outermost entry present wins. Type mode captures the member's leading
207
+ JSDoc description the same way (and the generated zod schema re-emits it
208
+ as `.describe()`, outermost, so it round-trips). Emission per kit, both
209
+ layouts, union variant fields and tuple elements included: mui routes it
210
+ into `helperText` as `fieldError(field) ?? description` (the error keeps
211
+ the one slot while present), shadcn a muted
212
+ `<p className="text-sm text-muted-foreground">` and antd a
213
+ `Typography.Text type="secondary"` line — each rendered only while the
214
+ error line is not — chakra a `Field.HelperText` under the same guard,
215
+ mantine the native `description` prop (its own slot, coexists with
216
+ `error`), and plain an always-visible `<p className="zf-help">` line
217
+ (formstand's built-in components own the error slot internally).
218
+ Booleans are skipped where the control has no slot (MUI's
219
+ FormControlLabel/Switch, chakra's Switch.Root, antd's bare Checkbox);
220
+ shadcn/mantine/plain booleans render it. Custom templates receive the
221
+ description as `ctx.description` (an expression like `ctx.label`; `""`
222
+ when absent). Description-free schemas emit byte-identical output.
223
+ Adornments from `.meta` (unit prefixes/suffixes) are deliberately NOT
224
+ generated — the four kits' adornment APIs are value-shaped and mutually
225
+ incompatible (InputAdornment / rightSection / InputElement / suffix);
226
+ helper text covers the units case. Visible in the playground: the
227
+ flight-search demo's `cruiseAltitude` now carries `.describe("feet MSL")`.
4
228
 
5
229
  ### Fixed
6
230
 
231
+ - **`--layout module`: autocomplete-only schemas now compile for every
232
+ kit.** The module adapter file's import gates missed
233
+ `usage.autocomplete`: a schema whose every string/enum field carries the
234
+ autocomplete override emitted chakra/shadcn adapters using `ChangeEvent`
235
+ without importing it (their override rides the kits' text adapter), and
236
+ the antd adapter's `FieldError` used `Typography` without importing it.
237
+ The single-file backends already gated these correctly; the module gates
238
+ now mirror them (mui/mantine/plain were already correct — mui's
239
+ autocomplete types `SyntheticEvent`, mantine's and antd's are
240
+ value-shaped).
241
+ - **`--layout module --form-prop`: the emitted component now warns in dev
242
+ when the passed form is not the module singleton.** The module's field
243
+ hooks are pre-wired to the exported singleton; a different form of the
244
+ same schema compiled and rendered but silently split state (the shell on
245
+ the prop, the fields on the singleton). The generated component now
246
+ compares the prop against the singleton and `console.warn`s outside
247
+ production (`NODE_ENV` gate, same shape as the library's own dev
248
+ warnings), naming the instance to pass. Documented as an emphasized
249
+ contract note in the CLI README's flags section.
250
+ - **Override near-miss suggestions no longer offer paths the validator
251
+ would reject.** The "did you mean ...?" candidate list for an unknown
252
+ `fields` path now filters out dot-containing (unaddressable) names,
253
+ walker-degraded leaves, and paths past the FieldPath depth budget — the
254
+ same predicates `applyFieldOverrides` errors on, so a suggestion can
255
+ always actually be used.
256
+ - Internal: `ownerHookName` and `hasStaticDescriptions` are no longer
257
+ exported from codegen (no external consumer), and `overridablePaths`
258
+ left the `formstand-cli/codegen` public surface (it is the validator's
259
+ internal near-miss walk).
260
+
261
+ ## 0.12.0 — 2026-07-29
262
+
263
+ ### Changed
264
+
7
265
  - **`useFieldArray` ops revalidate the array path** — push/remove/insert/
8
266
  move/swap now re-run field validation on the array's own path after the
9
267
  op, under the exact change-trigger gate `useField.setValue` applies
10
- (`shouldValidateOn` over mode/reValidateMode/submitCount/touched). A
11
- visible array-level error (`z.array().min(1)` after a failed submit) used
12
- to go stale: adding the row didn't clear the message, and removing below
13
- `.min` raised nothing until the next submit. The gate means `onSubmit`
14
- forms stay silent before the first submit, exactly like typing in a
15
- field. `FieldArrayFormApi` gains an OPTIONAL `validateField` member
16
- (probed before use) — hand-rolled implementations without it keep
17
- working, their ops simply skip revalidation. The IMPERATIVE
18
- `form.arrayPush`/`arrayRemove` stay non-revalidating on purpose, matching
19
- the `form.setValue` vs `useField.setValue` layering.
268
+ (`shouldValidateOn` over mode/reValidateMode/submitCount/touched). This
269
+ fixes a visible array-level error (`z.array().min(1)` after a failed
270
+ submit) going stale: adding the row didn't clear the message, and
271
+ removing below `.min` raised nothing until the next submit. Upgraders
272
+ note: in `onChange` mode (or after the first submit) array ops now
273
+ trigger a validation pass — including async refines — where 0.11 stayed
274
+ silent; `onSubmit` forms remain silent before the first submit, exactly
275
+ like typing in a field. The IMPERATIVE `form.arrayPush`/`arrayRemove`
276
+ stay non-revalidating on purpose, matching the `form.setValue` vs
277
+ `useField.setValue` layering.
278
+
279
+ ### Added
280
+
281
+ - **`FieldArrayFormApi.validateField` (optional)** — the member the hook
282
+ probes for the revalidation above. A `Form<TSchema>` always provides it;
283
+ hand-rolled `FieldArrayFormApi` implementations without it keep working,
284
+ their ops simply skip revalidation (documented in the API reference's
285
+ implementing-these-interfaces notes).
286
+
287
+ ### Fixed
20
288
  - **`focusFirstError` / `focusField`: a hidden name-carrier no longer
21
289
  suppresses the `[id=path]` fallback.** Composite widgets like
22
290
  react-select render a hidden `<input name={path}>` for form posting next