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 +171 -0
- package/dist/index.cjs +36 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +36 -5
- package/dist/index.js.map +1 -1
- package/package.json +17 -17
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
|
|
1941
|
-
const
|
|
1942
|
-
const forStore =
|
|
1943
|
-
if (
|
|
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
|
|
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
|
};
|