formstand 0.7.0 → 0.9.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,368 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.9.0 — 2026-07-10
6
+
7
+ ### Fixed
8
+
9
+ - `validateFields` no longer fabricates a "required" error for a field
10
+ whose required ancestor is `undefined` (the field-scoped fast path parsed
11
+ the leaf against undefined where the full-form parse keys the error at the
12
+ ancestor) — sync and async agreed again. `slotAtPath` now checks the
13
+ container at each step.
14
+ - `useVariantField` keeps its type guard for **optional/nullable**
15
+ discriminated unions (a missing `NonNullable` let the nullish member
16
+ collapse `keyof` to `never` and leak every key, including the
17
+ discriminant).
18
+ - `devtools` is active only in non-production builds
19
+ (`NODE_ENV !== "production"`), so opting in during development never
20
+ streams a shipped form's state to an end user with the extension.
21
+ - `parseDateText` accepts calendar years under 100 (the Date constructor
22
+ maps 0–99 to 1900–1999; the literal year is forced back on).
23
+ - `DateField` / `dateInputProps` preserve the existing value's time-of-day,
24
+ so re-picking the same day on a timestamped value is no longer spuriously
25
+ dirty (and changing the day keeps the time).
26
+ - `parsePath`'s cached segment array is frozen — it is shared across callers
27
+ now, so external mutation throws instead of corrupting the cache.
28
+ - `useVariantField` docs the write-gating contract (gate variant-field
29
+ writes on the discriminant, like rendering).
30
+
31
+ ### Added
32
+
33
+ - `useVariantField(form, unionPath, field)`: typed access to the
34
+ variant-specific fields of a `z.discriminatedUnion` — the ones
35
+ `FieldPath` omits because they exist in only some union members (the
36
+ discriminant stays a common key, bound with plain `useField`). Typed as
37
+ the field's value across the variants that declare it, widened with
38
+ `| undefined`; a non-variant field or the discriminant itself is a
39
+ compile error. `createFormHooks` gains a bound `use{Name}VariantField`.
40
+ - `persistForm(form, { key })`: the autosave recipe as a first-class
41
+ helper — debounced JSON drafts to any `{ getItem, setItem, removeItem }`
42
+ storage, draft auto-apply on start (`adopt` rebases clean, `restore`
43
+ loads dirty, `manual` waits for you), `clear()` that also cancels
44
+ pending writes, and guarded storage access throughout.
45
+ - Redux DevTools: `createForm(schema, { devtools: "checkout" })` connects
46
+ the form's store to the extension via zustand's middleware — every write
47
+ named, inspectable, and time-travelable. Off by default and inert
48
+ without the extension.
49
+ - Hot-path performance (the 2026-07 review's efficiency findings, no API
50
+ changes): `parsePath` is memoized (every field subscription re-parsed
51
+ its path per store notification); `validateFields` parses just the
52
+ requested subschemas when they're extractable — cross-field rules still
53
+ force the full parse, because extraction bails on refined levels — so a
54
+ wizard step stops paying for the whole schema per click; `diff()`/
55
+ `dirtyFields()` deep-compare each node once instead of once per ancestor
56
+ level; `focusFirstError` runs its focusability DOM walks only on
57
+ name-matched candidates instead of every named control in the document.
58
+ - Dates are a first-class field kind: `DateField` (an `<input type="date">`
59
+ bound to Date-typed paths) joins the shipped components, with
60
+ `dateInputProps` / `dateToInputText` / `parseDateText` exported for
61
+ custom markup and UI-kit adapters. Local calendar-date semantics
62
+ throughout (never `toISOString()` — June 1 stays June 1 west of UTC),
63
+ the `emptyValue` round-trip on clear, and rollover rejection
64
+ (`2026-02-31` is invalid, not March 3).
65
+
66
+ ### CI (no package changes)
67
+
68
+ - Dependabot triage: esbuild pinned to the patched 0.28.1 line via npm
69
+ overrides in the root and cli (dev-server advisories), scoped to spare
70
+ vitepress 1.x's nested vite-5 toolchain (esbuild 0.28 can't downlevel to
71
+ vite 5's browser targets). The remaining vitepress-nested alerts are
72
+ dismissed as tolerable risk — local docs dev/build only — with the
73
+ VitePress 2 migration tracked on the roadmap as the real fix.
74
+ - Workflows declare least-privilege GITHUB_TOKEN permissions (CodeQL:
75
+ actions/missing-workflow-permissions).
76
+
77
+ ### Docs & examples (no package changes)
78
+
79
+ - The Schema builder gains a **Paste a TypeScript type** mode: paste an
80
+ interface or type alias and the same in-browser emitters generate the
81
+ form — no TypeScript compiler in the bundle, a focused ~250-line parser
82
+ covers the CLI's type-mode subset (string/number/boolean/Date, arrays,
83
+ nested objects, string-literal unions, optional/nullable) and degrades
84
+ the rest to a text field with a TODO. Both input modes share one emit
85
+ path.
86
+ - ROADMAP.md: a living plan for the library and the CLI — the 0.9/cli-0.6
87
+ cycle (dates as a first-class field kind, hot-path performance,
88
+ DevTools, CLI config + watch), the next tier, the parking lot, and the
89
+ review's deferred cleanup items.
90
+ - The theme is one preference across the docs site and the playground:
91
+ the playground stores its light/dark choice under VitePress's own
92
+ localStorage key (same origin), so flipping either surface's toggle
93
+ follows you to the other.
94
+ - The demo body caps at a readable 760px measure; legitimately wide demos
95
+ (Perf's grid, the Schema builder, the Invoice table) opt out via an
96
+ explicit WIDE_DEMOS set.
97
+ - The demo card gets a real header: group eyebrow, title, a one-line
98
+ description of what each demo shows (exhaustive over the tab keys, like
99
+ the group map), and the View state / View code toggles plus a copy-link
100
+ button as a right-aligned action cluster — shell chrome separated from
101
+ demo content by a hairline. The nav groups show demo counts, and the
102
+ mobile bottom sheet gains its own Close button (the header toggles sit
103
+ behind it).
104
+
105
+ - Mobile playground shell: a top app bar (menu / title / controls), the
106
+ demo list as an off-canvas drawer that closes on pick, and the View
107
+ state / View code panel as a bottom sheet — all CSS-driven, desktop
108
+ layout unchanged.
109
+ - Light theme: a toggle in the app bar (and sidebar on desktop), defaulting
110
+ to the OS preference and persisted per browser. One variable palette in
111
+ styles.css drives the shell; the MUI bridge and the shadcn scope follow
112
+ the same html[data-theme] switch. Terminal/code surfaces deliberately
113
+ stay dark in both themes.
114
+ - The GitHub link in the shell shows the repo's star count (best-effort,
115
+ cached per session, skipped under tests).
116
+
117
+ ## formstand-cli 0.6.0 — 2026-07-10
118
+
119
+ - Discriminated unions (`z.discriminatedUnion`) at a field position: the
120
+ generator emits a discriminant select plus per-variant conditional
121
+ blocks, binding variant fields through the library's `useVariantField`
122
+ (single-file) or bound `use{Name}VariantField` (module layout) — fully
123
+ typed, no casts. All six emit paths (plain/mui/shadcn × single-file/
124
+ module) typecheck against the real helper. Initial values pick the
125
+ first variant; the schema round-trips. Nested-in-section unions,
126
+ unions in array rows, and non-scalar variant fields keep a TODO.
127
+ - Nested-array row extraction (`--layout module`): an array inside a
128
+ section object, or an array inside an array row, now generates its own
129
+ Row/Rows component pair with a bound `useFieldArray` on the template
130
+ path (two numeric holes for arrays-in-rows,
131
+ `contacts.${parentIndex}.phones.${index}.number`) instead of a TODO
132
+ comment. One extraction level per array — deeper nesting keeps a TODO.
133
+ - `formstand.config.ts` (also `.mts`/`.js`/`.mjs`, or `--config <file>`):
134
+ project defaults for `ui`/`layout`/`sections`/`columns`, written with the
135
+ typed `defineConfig` export. Precedence is flags > config > built-ins,
136
+ and config typos fail as loudly as flag typos.
137
+ - `--watch`: regenerate whenever the input file changes (requires
138
+ `--out`; reruns overwrite their own output). Watches the parent
139
+ directory so editors that save via rename don't kill the watcher.
140
+ - A kind × optional × nullable matrix test pins `emitInitialValues` and
141
+ `blankNeedsCast` agreement in both directions: the no-cast combos must
142
+ typecheck with the checked annotation, and every cast combo is proven
143
+ to genuinely need it.
144
+ - Date fields emit real bindings instead of a text input with a cast and
145
+ a TODO: `DateField` (plain), `muiDateFieldProps` (MUI `type="date"`
146
+ TextField with a floated label), `shadcnDateInputProps` (shadcn Input).
147
+ `DATE_CAST` and the per-backend date TODOs are gone from generated
148
+ output — forms with dates submit successfully out of the box. Requires
149
+ formstand ≥ 0.9 for date fields.
150
+
151
+ ## 0.8.0 — 2026-07-10
152
+
153
+ #### Fixed (2026-07 full-repo review)
154
+
155
+ - `valuesEqual` compared only the first object's keys, so two objects with
156
+ equal key counts but different key sets (one holding `undefined`) read as
157
+ equal — dirty flags, `diff()`, and server-error release could all treat a
158
+ real change as clean. Key sets must now match (`Object.hasOwn`), and the
159
+ compare is symmetric.
160
+ - `setAtPath` no longer spreads a `Date`/`Map`/`Set`/class instance at an
161
+ intermediate path into a plain object (silently destroying it); the write
162
+ is refused with a console warning, like other unwritable shapes.
163
+ - Path reads return own properties only: `getAtPath(values,
164
+ "lookup.constructor")` on a `z.record` is `undefined` instead of leaking
165
+ `Object.prototype` members as field values.
166
+ - A throwing `onInvalid` handler now resolves `submit()` with
167
+ `{ kind: "error" }` like a throwing `onValid`, instead of rejecting out of
168
+ a DOM event handler.
169
+ - `restore()` no longer resurrects a snapshotted `isSubmitting` — the flag
170
+ reflects live submit passes, so a mid-submit snapshot restored later can't
171
+ disable submit buttons forever (same rule the validation flags already
172
+ followed).
173
+ - `SelectField` on a nullable field keeps its empty option visible and
174
+ selectable after a choice, so the field can be cleared back to `null`
175
+ through the UI (the `emptyValue` round-trip `selectProps` always
176
+ supported). Non-nullable fields keep the disabled placeholder.
177
+ - `useFieldArray` no longer hands a removed row's id to a row appended in
178
+ the same update (React treated it as a reorder and resurrected the dead
179
+ row's DOM state); the same-index fallback still keeps an edited row's id.
180
+ - `useForm` now also warns when the CONTENT of `initialValues` changes after
181
+ mount (the async-fetch pattern with a module-hoisted schema — previously
182
+ silently ignored with no warning at all). Inline literals with identical
183
+ content stay silent.
184
+ - Docs: `field-arrays.md` claimed array ops re-key `isValidating`; they
185
+ deliberately drop in-flight flags (the pass that set them is stale) — the
186
+ guide now says so. `components.md` documents the `checkboxProps`
187
+ unchecked-writes-`false` tradeoff for nullable booleans.
188
+
189
+ ### Docs & examples (no package changes)
190
+
191
+ - The docs nav gains a "Built on" menu and the footer links to
192
+ [zod](https://zod.dev) and [zustand](https://zustand.docs.pmnd.rs).
193
+
194
+ - A "Schema builder" tab: formstand-gen running in the browser. A formstand
195
+ form describes a schema (fields, sections, arrays, enum options — with
196
+ duplicate-name and enum-needs-options cross-field validation), its values
197
+ map straight onto the CLI's IR, and the REAL emitters (imported from
198
+ cli/src — pure string builders, no Node APIs) regenerate the output files
199
+ on every keystroke, shown in a file tree with copy-file / copy-all. All
200
+ the CLI axes are selects: --ui, --layout, --sections, --columns.
201
+ - The Schema builder's "Copy all files" is now "Download .zip" — a
202
+ dependency-free STORE-only zip writer (~50 lines, in the demo's own
203
+ source) bundles the generated files under a folder named for the
204
+ component, exactly the tree `--out` would have written.
205
+ - Every demo has a direct link: hash routes like
206
+ `examples/#/schema-builder` (kebab-cased tab keys), synced both ways so
207
+ back/forward and hand-edited URLs work. Hash routing because GitHub Pages
208
+ is static hosting.
209
+ - The CLI command builder gains Sections and Field columns selects, and the
210
+ Generated Onboarding tab now showcases `--sections panel --columns 2`
211
+ (still CI-verified as the CLI's untouched output).
212
+
213
+ - A "CLI command builder" tab: a formstand form (cross-field superRefine,
214
+ conditional fields) whose live-updating formstand-gen command line is
215
+ useFormSelector-derived state, with a copy button.
216
+ - A "Generated" playground group whose Onboarding tab is formstand-gen's
217
+ untouched `--layout module --ui mui` output for the Onboarding schema —
218
+ CI regenerates it and fails on drift, so the demo is provably what the
219
+ current CLI emits (scripts/generate-cli-demos.mjs).
220
+
221
+ - MUI and shadcn variants of the Onboarding feature module (31 demos now):
222
+ same shared schema, option lists, and blank draft as the plain module
223
+ (re-exported, so the three can't drift), each with its own form instance
224
+ and pre-wired hooks — MUI sections are Accordions with dirty/valid chips,
225
+ shadcn sections are collapsible with badges. The CLI generates the same
226
+ architecture from the same schema (`--layout module --ui mui|shadcn`).
227
+ - The sidebar demo list is a tree view (x-tree-view): brass group folders,
228
+ form-check leaf icons, brass selection.
229
+
230
+ - The playground gets a real app layout: grouped demo navigation in a
231
+ sidebar (Core / Patterns / Material UI / shadcn/ui — the group map is
232
+ exhaustive over the tab keys, so an unassigned demo is a compile error),
233
+ a sticky scrollable sidebar from tablet width up, and responsive tiers
234
+ through HD to 4K; on mobile the nav becomes wrapping chips above the
235
+ content. `/guide/` on the docs site now lands on a guide index instead
236
+ of a 404.
237
+ - Fixed: clicking a checkbox's label made the field and everything below
238
+ it jump down for a frame and snap back (Chrome's label-forwarded
239
+ activation briefly rendered the checkbox as a padded text-input box —
240
+ reproduced and verified fixed in real Chromium via Playwright). The
241
+ playground's text-input chrome now excludes checkboxes, which also gain
242
+ a brass accent-color and a focus-visible ring.
243
+ - View state / View code move to a panel on the demo's right (stacked
244
+ below on narrow screens), toggled by the same two buttons; multi-file
245
+ demos (Onboarding) get a file-tree navigator (@mui/x-tree-view with
246
+ folder/file icons) opening on hooks.ts; the sidebar brand links back to
247
+ the docs and stays pinned while the demo list scrolls.
248
+ - The Perf demo registers its form like every other tab, so View state
249
+ works (the shell only mounts the panel while open, so a closed panel
250
+ costs the benchmark nothing — the copy says to close it while running).
251
+ The benchmark writes `field0` instead of the mid-grid field, so the
252
+ change is visible without scrolling.
253
+
254
+ ## formstand-cli 0.5.3 — 2026-07-10
255
+
256
+ ### Fixed
257
+
258
+ - Emitted string literals escape U+2028/U+2029 (JSON.stringify leaves the
259
+ JS line separators raw — a syntax error for pre-ES2019 parsers of the
260
+ GENERATED file, and CodeQL's js/bad-code-sanitization).
261
+
262
+ ## formstand-cli 0.5.2 — 2026-07-10
263
+
264
+ #### Fixed (2026-07 full-repo review)
265
+
266
+ - Digit-leading schema keys ("2fa", "2ndOwners") emitted invalid
267
+ identifiers (`const 2ndOwnersArray`); pascalCase/camelCase results are
268
+ now underscore-prefixed when they'd start with a digit.
269
+ - Array-row fields named JS reserved words ("new", "delete") emitted
270
+ reserved-word `const` declarations in module sections; row bindings now
271
+ use an identifier-safe variant (`new_`).
272
+ - A field named after the module prefix (field "contact" in ContactForm)
273
+ collided with the bound `useContactField` hook from `hooks.ts` — a
274
+ duplicate declaration plus self-recursion; component names are now
275
+ deduped against the hook names too (`ContactField2`).
276
+ - Boolean-only schemas with `--ui mui --layout module` emitted an
277
+ `adapter.ts` using `ChangeEvent` without importing it.
278
+ - `--name` is validated as an identifier (reserved words rejected) instead
279
+ of interpolating verbatim into declarations and file names.
280
+ - Cross-drive input/`--out` on Windows produced an unresolvable
281
+ `"./D:/..."` import specifier; the CLI now fails loudly at generation
282
+ time.
283
+ - zod's `.nonoptional()` unwrapped transparently, letting an inner
284
+ `.optional()` win — the field's checked `initialValues` annotation then
285
+ failed to typecheck; `.nonoptional()` now re-requires the field.
286
+ - A field literally named `__proto__` silently vanished from emitted object
287
+ literals (prototype-setter semantics); it's now emitted as a computed key.
288
+ - `--sections panel` with `--ui mui` rendered different chrome per
289
+ `--layout` (single-file used `CardHeader`, module used `Typography` in
290
+ `CardContent`); both emitters now share the module shape, the grid
291
+ strings come from one set of helpers instead of six inline copies, and
292
+ `emitInitialValues`/`blankNeedsCast` read one shared blank-value table so
293
+ they can't drift.
294
+
295
+ ## formstand-cli 0.5.1 — 2026-07-09
296
+
297
+ ### Fixed
298
+
299
+ - Generated `initialValues` (and array empty-item constants) only use the
300
+ `as unknown as` escape hatch when the blank draft genuinely can't
301
+ typecheck (a required number/date/enum starts `undefined`); every other
302
+ schema gets a checked type annotation, so typos in edited initial values
303
+ are compile errors again.
304
+ - `--layout module`: the generated `index.ts` now re-exports `./schema` and
305
+ `./types` alongside `./hooks`, so consumers can type submit handlers and
306
+ server code (`ProfileValues`, `profileSchema`) off the module's public
307
+ API instead of deep-importing its files.
308
+
309
+ ## formstand-cli 0.5.0 — 2026-07-09
310
+
311
+ ### Added
312
+
313
+ - `--sections flat|panel|collapsible` and `--columns 1|2|3`: minimal visual
314
+ layout options, working with both `--layout single` and `--layout module`
315
+ and all three uis. Sections render as flat headings (default, unchanged
316
+ output), bordered panels, or collapsible sections; fields inside each
317
+ section flow into 1–3 evenly spaced columns, with nested sections
318
+ spanning the full row. Each ui speaks its own dialect: inline styles for
319
+ `plain` (`<details>`/`<summary>` when collapsible), `Card`/`Accordion` +
320
+ `sx` grids for `mui`, Tailwind classes (`md:grid-cols-2`,
321
+ `bg-card … shadow-sm`, `<details>`) for `shadcn`. The defaults emit
322
+ byte-identical output to 0.4.0.
323
+
324
+ ## formstand-cli 0.4.0 — 2026-07-09
325
+
326
+ ### Added
327
+
328
+ - `--layout module` works with `--ui mui` and `--ui shadcn`: kit modules
329
+ get a shared `adapter.ts` / `adapter.tsx` exporting the adapter the
330
+ single-file backends inline, and field/section files import it. The
331
+ emitted prop builders are now generic over the field's value type
332
+ (matching the documented example adapters), because the module layout's
333
+ typed hooks surfaced what the single-file layout's widened hooks masked —
334
+ `UseFieldReturn<string>` wasn't assignable to a monomorphic builder.
335
+ Emitted kit modules are typechecked in CI against the MUI stub and both
336
+ the shadcn stub and the repo's real Radix components.
337
+
338
+ ### Docs & examples (no package changes)
339
+
340
+ - Brand pass on the docs site and playground: the formstand mark (a form
341
+ with its green check, resting on a music stand) as logo and favicon, a
342
+ brass/ink palette replacing the stock VitePress indigo, hand-drawn SVG
343
+ feature icons replacing the emoji, and restrained motion (hero float,
344
+ card lift, tab/button transitions, demo fade-in) — all disabled under
345
+ prefers-reduced-motion. The playground gains a branded header and
346
+ focus-visible states.
347
+
348
+ ## formstand-cli 0.3.0 — 2026-07-09
349
+
350
+ ### Added
351
+
352
+ - `--layout module`: emits a feature-module folder instead of one file —
353
+ `schema.ts` (re-exported in zod mode, generated in type mode), `types.ts`,
354
+ `hooks.ts` (`createForm` + `createFormHooks`, so the whole module shares
355
+ one pre-wired hook API), one file per scalar field (props type + field
356
+ hook + component), one file per top-level object/array section (props
357
+ type + section hook built on the path-scoped dirty/valid flags +
358
+ component), the form body, and `index.ts` — the shape of the Onboarding
359
+ playground demo. `--out` names the folder with all destinations
360
+ pre-checked; without it the files stream to stdout with `// --- file:`
361
+ headers. Emitted modules are typechecked in CI against the library
362
+ source, including hostile-name, colliding-name, and leaf-free schemas.
363
+ Requires formstand ≥ 0.7; `--ui plain` only for now.
364
+ - `emitModuleForm` / `joinModuleFiles` join the programmatic API.
365
+
3
366
  ## 0.7.0 — 2026-07-09
4
367
 
5
368
  ### Added
package/README.md CHANGED
@@ -429,7 +429,8 @@ form.handleSubmit(async (data) => {
429
429
 
430
430
  See [CONTRIBUTING.md](./CONTRIBUTING.md) — note the repo has three
431
431
  independent npm roots (`.`, `examples/`, `cli/`), and the root test suite
432
- needs `examples/` installed.
432
+ needs `examples/` installed. Where the project is headed lives in
433
+ [ROADMAP.md](./ROADMAP.md).
433
434
 
434
435
  ## License
435
436