formstand 0.22.0 → 0.22.1

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,176 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.22.1 — 2026-10-22
4
+
5
+ ### Fixed
6
+
7
+ - Nested `useFieldArray` row ids survive an outer reorder. Inner ids are
8
+ stored per path (`teams.0.members`), so an outer `move`, `swap`, `remove`,
9
+ `insert`, or whole-array write left the entry at `teams.2.members` meeting
10
+ team A's members with team C's history: the same-index fallback handed the
11
+ first rows the other team's ids, the rest reminted, and React remounted
12
+ inputs a user may have been typing in. The outer hook now re-seats every
13
+ descendant path's id entry to follow its row (by id, so an exact op
14
+ mapping and a by-value reconciliation re-seat the same way), and drops the
15
+ entries of a removed row's slot. Inner-only trees that reorder the outer
16
+ array imperatively keep the value fallback.
17
+
18
+ ### Docs
19
+
20
+ - Field arrays: the array-op revalidation sentence now matches the gate.
21
+ Ops run under the change trigger, so in the default `onBlur` mode (and
22
+ `onSubmit`) they stay silent until the first failed submit switches to
23
+ `reValidateMode`; `onTouched` needs the array path itself touched;
24
+ `onChange` and `all` revalidate at once. A "Render cost" note says the
25
+ hook re-renders its caller on every keystroke in any row and how to
26
+ keep rows cheap.
27
+ - Persistence: a loaded draft is adopted without validation, so a draft
28
+ that fails the schema reads valid until something validates;
29
+ `validateOnMount` runs before `persistForm` applies the draft and does
30
+ not cover it, so call `form.validate()` after the load.
31
+ - `useFormValues` re-renders on every keystroke anywhere in the form; keep
32
+ it out of per-row components.
33
+ - A `"use server"` action must parse `data` with the schema on the server
34
+ on every path, not only the no-JS one.
35
+ - Masked inputs: `"empty"` writes `emptyValue` for clearable and non-string
36
+ fields and `""` for a required string, as the 0.22.0 binding does.
37
+ - Components: the `SelectField` props row names the real option type, and
38
+ the guide states the by-value `path` rule (`TextField` on a `z.number()`
39
+ is a compile error) that the API page already had.
40
+ - CLI: the schema module and `formstand.config.ts` are executed through
41
+ jiti, so point the CLI only at files you would run with `node`; the
42
+ overview names all three inputs.
43
+ - Snippets import every name they use (`Form`, `FormStateApi`,
44
+ `createForm`, `textInputProps`); the `onSubmit` snippet is in a tsx
45
+ fence; `fillFields` is documented as typed per key.
46
+ - A "Security notes" section on validation.md collects server
47
+ re-validation, untrusted drafts, and the CLI executing modules, with
48
+ links to the sections that hold the detail.
49
+ - Roadmap: `--path-depth` moves to the shipped list. The remaining em
50
+ dashes in the roadmap and the VitePress theme comments are reworded.
51
+ - Playground: every demo is a lazy chunk. The main bundle drops from
52
+ 2.95 MB (779 kB gzip) to 381 kB (116 kB gzip) and the main stylesheet
53
+ from 601 kB to 34 kB; each tab's component, kit and stylesheet load on
54
+ first visit, and the View code panel fetches source text on demand
55
+ behind a static file tree. Mantine's sheet loads as its `@layer` variant
56
+ so the shell's unlayered rules keep winning regardless of load order.
57
+ - Playground: the Conditional demo is a `z.discriminatedUnion` bound with
58
+ `useField` for the discriminant and `useVariantField` for the branch
59
+ fields, and the payload carries only the active branch (the flat version
60
+ submitted the hidden card number alongside the bank fields).
61
+ - Playground: the hand-rolled demos wire each error line to its input
62
+ (`aria-describedby`, `aria-invalid`, `aria-live`) through one shared
63
+ helper and focus the first errored control after a failed submit; the
64
+ View state / View code toggles expose `aria-pressed`; the MUI Invoice,
65
+ shadcn Team and shadcn Onboarding contact rows label their
66
+ placeholder-only cells.
67
+ - Playground: the Autosave status reads "Draft restored" after a restore
68
+ instead of "Unsaved changes"; the Schema builder's Zod paste tab says
69
+ that pasted code runs in the page and its View code list includes
70
+ `ImportModal.tsx` and `parseZod.ts`; the Perf demo's inputs follow the
71
+ theme; the light theme's eyebrow (6.76:1) and muted text (4.88:1) clear
72
+ the AA floor; the theme preference is written only when the user
73
+ toggles, so a docs visitor's "auto" survives opening the playground.
74
+ - Playground: the Onboarding module's field files, its emergency-contact
75
+ rows, and the CLI command builder wire each error line to its control
76
+ through the shared `fieldA11y` helper, like the other hand-rolled
77
+ demos since 0.22.0; the contacts array's own error line is a live
78
+ region. The command builder's file set ships the helper, so it opens
79
+ in StackBlitz.
80
+ - Playground: the generated kitchen sink's input schema carries human
81
+ messages on every check, so an empty submit no longer shows zod's raw
82
+ messages, and the regenerated plain output carries the CLI's new aria
83
+ wiring. `examples/src` is now linted with the library's rule set.
84
+
85
+ ## formstand-cli 0.24.0 — 2026-10-20
86
+
87
+ ### Added
88
+
89
+ - `--no-live` and `--no-form-prop`: an explicit flag turns the mode off for
90
+ one run when the config file sets `live: true` / `formProp: true`. Before,
91
+ nothing could override a configured mode (`--no-live` was an unknown
92
+ flag).
93
+ - JSON Schema mode carries `pattern` and `multipleOf` into the generated
94
+ validator, as `.regex(new RegExp("..."))` and `.multipleOf(n)`. A pattern
95
+ JavaScript's `RegExp` refuses degrades to a TODO instead of a module that
96
+ throws on load. Both, and any `format` outside email/uri/uuid/date, are
97
+ recorded as unmodeled checks, so the generated spec skips its blind round
98
+ trip over them and `--tests` warns on stderr; they were dropped silently
99
+ before. The IR gains `StringChecks.regex` and `NumberChecks.multipleOf`.
100
+ - Type mode walks intersections (`A & B`, the merged member set, later wins
101
+ on a collision, a TODO where two members disagree on a key) and
102
+ discriminated unions of object types sharing a string-literal key, to
103
+ the same IR union the zod and JSON Schema walks produce. A union of
104
+ numeric literals names the string-only enum as its reason instead of
105
+ "unsupported union type".
106
+
107
+ ### Fixed
108
+
109
+ - Type mode had no recursion guard: `type Tree = { value: string; left?:
110
+ Tree; right?: Tree }` expanded to the depth budget (6142 IR nodes, a
111
+ 188 KB file, 518 module files). The walk tracks the declared types on the
112
+ current path and cuts a repeat with a "recursive type" TODO, the way the
113
+ zod walker treats its getter idiom. A member typed as the global
114
+ `Function` (no call signature of its own) walked Function.prototype to
115
+ the budget; it now degrades like a method.
116
+ - `--type someConst` (a value, not a type) generated an "any" TODO form; it
117
+ is now an error naming the symbol kind. An index signature beside named
118
+ members (`{ a: string; [k: string]: string }`) was dropped without a
119
+ trace; the members bind and a TODO sits on the section, mirrored on
120
+ stderr and above the generated schema.
121
+ - OpenAPI 3.0 idioms in JSON Schema mode: `{ $ref, nullable: true }` was
122
+ not nullable (the ref walk returned before the flag applied);
123
+ `{ allOf: [{ $ref }], nullable: true }` lost the outer keywords in the
124
+ merge; `allOf: [{ $ref: StringEnum }]` (the 3.0 way to annotate a ref)
125
+ was "allOf with non-object branches"; `enum: ["a", "b", null]` with
126
+ `nullable: true` (3.0) or `type: ["string", "null"]` (3.1) was a
127
+ "non-string enum" TODO; an `allOf` branch that is itself an `allOf`, and
128
+ `$ref` chains, degraded; `#/$defs/My%20Type` was not percent-decoded.
129
+ All six now walk to the expected IR, and a self-referring `allOf` branch
130
+ is named in its TODO.
131
+ - Draft-04 `exclusiveMinimum: true` / `exclusiveMaximum: true` beside
132
+ `minimum` / `maximum` silently read as an inclusive bound; the flag now
133
+ makes the bound exclusive.
134
+ - An empty enum list (`enum: []` in a document, `z.enum([])` in a schema,
135
+ which zod constructs and then rejects every value for) emitted a select
136
+ with no option; it degrades to a TODO in both walkers.
137
+ - `--out some/dir/` (a trailing separator, or an existing directory) in the
138
+ single-file layout wrote a file literally named after the folder, or
139
+ refused to "overwrite" it. It is refused with an error that says to name
140
+ a `.tsx` file or use `--layout module`.
141
+ - `--name profileForm` passed validation and the generated spec rendered
142
+ `<profileForm />`, which JSX reads as a DOM tag; the name must now start
143
+ with an uppercase letter.
144
+ - `--force` over a module folder left the previous run's files in place
145
+ silently (a removed section's `sections/GoneSection.tsx` survived and
146
+ still compiled). Files that carry the generator header on their first
147
+ line and are not in the new set are now named on stderr as
148
+ `warning: stale: <path>`, so the leftover is visible. They are not
149
+ deleted: the header says the file is yours to edit, and a folder can hold
150
+ a single-file component from another run under the same header, so
151
+ removal is left to you.
152
+ - `--schema constructor` (or `toString`, `__proto__`) resolved a prototype
153
+ member of the component store and generated a degraded form named after
154
+ it; lookups and pointer segments use own keys only, so it reports "no
155
+ schema named".
156
+ - A string-literal export name (`export { inner as "my schema" }`) emitted
157
+ `import { my schema } from ...`; it is refused with the rule, for an
158
+ explicit `--export` and for the sole-export fallback.
159
+ - The generated spec's blank-submit case picked its control among the
160
+ blank-invalid fields only, so a required "Age" beside an optional "Age"
161
+ in another section made `getByLabelText` throw. Uniqueness is now judged
162
+ against every label the blank render shows, for the label fills too.
163
+ - **The plain backend's error lines are described to their controls.** The
164
+ in-file `AutocompleteField`, `TextareaField`, `PlaceholderField` and
165
+ `EnumSelectField`, the union variant leaves, and the module layout's field
166
+ files and array rows emitted a bare `<p role="alert">` with no id and a
167
+ control with no `aria-describedby`, so assistive tech heard the field but
168
+ never its message. The error line now owns `${field.path}-error` and the
169
+ control points at it while the error shows (the same runtime derivation
170
+ the autocomplete datalist id already used, unique per array row with no
171
+ `useId`); `aria-invalid` keeps coming from the prop builders. The kit
172
+ backends were already wired through their own components.
173
+
3
174
  ## 0.22.0 — 2026-10-19
4
175
 
5
176
  ### Fixed
package/dist/index.cjs CHANGED
@@ -1937,10 +1937,13 @@ var mappedIdState = (prev, mapping, liveItems) => {
1937
1937
  };
1938
1938
  var MAX_ID_PATHS = 4096;
1939
1939
  var sharedIdEntries = /* @__PURE__ */ new WeakMap();
1940
- var sharedIdEntry = (store, path) => {
1941
- const existingStore = sharedIdEntries.get(store);
1942
- const forStore = existingStore ?? { byPath: /* @__PURE__ */ new Map(), seq: 1 };
1943
- if (existingStore === void 0) sharedIdEntries.set(store, forStore);
1940
+ var storeIdEntries = (store) => {
1941
+ const existing = sharedIdEntries.get(store);
1942
+ const forStore = existing ?? { byPath: /* @__PURE__ */ new Map(), seq: 1 };
1943
+ if (existing === void 0) sharedIdEntries.set(store, forStore);
1944
+ return forStore;
1945
+ };
1946
+ var sharedIdEntry = (forStore, path) => {
1944
1947
  const existing = forStore.byPath.get(path);
1945
1948
  if (existing !== void 0) {
1946
1949
  forStore.byPath.delete(path);
@@ -1970,12 +1973,40 @@ var walkOps = (start, items, ops) => {
1970
1973
  }, start);
1971
1974
  return walked.items === items ? walked : reconcileIds(walked, items);
1972
1975
  };
1976
+ var reseatDescendants = (forStore, path, prev, next) => {
1977
+ if (prev.ids.length === 0) return;
1978
+ const newIndexById = new Map(next.ids.map((id, i) => [id, i]));
1979
+ const moves = prev.ids.map((id) => newIndexById.get(id));
1980
+ if (moves.every((to, from) => to === from)) return;
1981
+ const prefix = `${path}.`;
1982
+ const descendants = [...forStore.byPath.keys()].filter(
1983
+ (key) => key.startsWith(prefix)
1984
+ );
1985
+ const relocations = descendants.flatMap((key) => {
1986
+ const rest = key.slice(prefix.length);
1987
+ const dot = rest.indexOf(".");
1988
+ const segment = dot === -1 ? rest : rest.slice(0, dot);
1989
+ const from = Number(segment);
1990
+ if (!/^\d+$/.test(segment) || from >= prev.ids.length) return [];
1991
+ const entry = forStore.byPath.get(key);
1992
+ if (entry === void 0) return [];
1993
+ const to = moves[from];
1994
+ const target = to === void 0 ? void 0 : `${prefix}${to}${dot === -1 ? "" : rest.slice(dot)}`;
1995
+ return [{ key, target, entry }];
1996
+ });
1997
+ relocations.forEach(({ key }) => forStore.byPath.delete(key));
1998
+ relocations.forEach(({ target, entry }) => {
1999
+ if (target !== void 0) forStore.byPath.set(target, entry);
2000
+ });
2001
+ };
1973
2002
  var deriveIds = (store, path, items) => {
1974
- const entry = sharedIdEntry(store, path);
2003
+ const forStore = storeIdEntries(store);
2004
+ const entry = sharedIdEntry(forStore, path);
1975
2005
  if (entry.state.items === items) return entry.state;
1976
2006
  const ops = arrayOpsFor(store, path);
1977
2007
  if (ops.length > 0) clearArrayOps(store, path);
1978
2008
  const next = walkOps(entry.state, items, ops);
2009
+ reseatDescendants(forStore, path, entry.state, next);
1979
2010
  entry.state = next;
1980
2011
  return next;
1981
2012
  };