react-f0rm 1.3.0 → 1.5.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/README.md +98 -1140
- package/devtools.d.ts +1 -0
- package/devtools.js +1 -0
- package/dist/array-Bu7W8BSz.d.ts +54 -0
- package/dist/devtools/index.cjs.js +1 -1
- package/dist/devtools/index.cjs.js.map +1 -1
- package/dist/devtools/index.d.cts +24 -0
- package/dist/devtools/index.d.mts +24 -0
- package/dist/devtools/index.d.ts +8 -17
- package/dist/devtools/index.mjs +1 -1
- package/dist/devtools/index.mjs.map +1 -1
- package/dist/errors-DA4ReEd9.mjs +2 -0
- package/dist/errors-DA4ReEd9.mjs.map +1 -0
- package/dist/errors-TzyWwBfw.cjs.js +2 -0
- package/dist/errors-TzyWwBfw.cjs.js.map +1 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.cts +1253 -0
- package/dist/index.d.mts +1253 -0
- package/dist/index.d.ts +570 -895
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +1256 -535
- package/dist/index.umd.js.map +1 -1
- package/dist/index.umd.min.js +2 -2
- package/dist/index.umd.min.js.map +1 -1
- package/dist/persist.cjs.js +1 -1
- package/dist/persist.cjs.js.map +1 -1
- package/dist/persist.d.cts +28 -0
- package/dist/persist.d.mts +28 -0
- package/dist/persist.d.ts +9 -30
- package/dist/persist.mjs +1 -1
- package/dist/persist.mjs.map +1 -1
- package/dist/resolvers/standard-schema.cjs.js +1 -1
- package/dist/resolvers/standard-schema.cjs.js.map +1 -1
- package/dist/resolvers/standard-schema.d.cts +2 -0
- package/dist/resolvers/standard-schema.d.mts +2 -0
- package/dist/resolvers/standard-schema.d.ts +1 -67
- package/dist/resolvers/standard-schema.mjs +1 -1
- package/dist/resolvers/standard-schema.mjs.map +1 -1
- package/dist/resolvers/yup.cjs.js +1 -1
- package/dist/resolvers/yup.cjs.js.map +1 -1
- package/dist/resolvers/yup.d.cts +6 -0
- package/dist/resolvers/yup.d.mts +6 -0
- package/dist/resolvers/yup.d.ts +0 -1
- package/dist/resolvers/yup.mjs +1 -1
- package/dist/resolvers/yup.mjs.map +1 -1
- package/dist/resolvers/zod.cjs.js +1 -1
- package/dist/resolvers/zod.cjs.js.map +1 -1
- package/dist/resolvers/zod.d.cts +34 -0
- package/dist/resolvers/zod.d.mts +34 -0
- package/dist/resolvers/zod.d.ts +30 -3
- package/dist/resolvers/zod.mjs +1 -1
- package/dist/resolvers/zod.mjs.map +1 -1
- package/dist/server/index.cjs.js +1 -1
- package/dist/server/index.cjs.js.map +1 -1
- package/dist/server/index.d.cts +38 -0
- package/dist/server/index.d.mts +38 -0
- package/dist/server/index.d.ts +26 -65
- package/dist/server/index.mjs +1 -1
- package/dist/server/index.mjs.map +1 -1
- package/dist/standard-schema-BAaTmAHh.d.ts +682 -0
- package/dist/standardSchema-5WezjHlp.mjs +2 -0
- package/dist/standardSchema-5WezjHlp.mjs.map +1 -0
- package/dist/standardSchema-DINHlsYR.cjs.js +2 -0
- package/dist/standardSchema-DINHlsYR.cjs.js.map +1 -0
- package/dist/validate-2pX-N1O6.cjs.js +2 -0
- package/dist/validate-2pX-N1O6.cjs.js.map +1 -0
- package/dist/validate-C0HOsP9v.mjs +2 -0
- package/dist/validate-C0HOsP9v.mjs.map +1 -0
- package/dist/values-BbPnLByD.cjs.js +2 -0
- package/dist/values-BbPnLByD.cjs.js.map +1 -0
- package/dist/values-CHsmcZk4.mjs +2 -0
- package/dist/values-CHsmcZk4.mjs.map +1 -0
- package/package.json +75 -28
- package/persist.d.ts +1 -0
- package/persist.js +1 -0
- package/resolvers/standard-schema.d.ts +1 -0
- package/resolvers/standard-schema.js +1 -0
- package/resolvers/yup.d.ts +1 -0
- package/resolvers/yup.js +1 -0
- package/resolvers/zod.d.ts +1 -0
- package/resolvers/zod.js +1 -0
- package/server.d.ts +1 -0
- package/server.js +1 -0
- package/dist/errors-BKrUdpfI.cjs.js +0 -2
- package/dist/errors-BKrUdpfI.cjs.js.map +0 -1
- package/dist/errors-CrQBddrJ.mjs +0 -2
- package/dist/errors-CrQBddrJ.mjs.map +0 -1
- package/dist/form-CeKSBs31.d.ts +0 -486
- package/dist/validate-CNtuUhmk.mjs +0 -2
- package/dist/validate-CNtuUhmk.mjs.map +0 -1
- package/dist/validate-Cl4ksNFu.cjs.js +0 -2
- package/dist/validate-Cl4ksNFu.cjs.js.map +0 -1
- package/dist/validate-nksgv1pR.d.ts +0 -272
- package/dist/values-Cu6awQOJ.cjs.js +0 -2
- package/dist/values-Cu6awQOJ.cjs.js.map +0 -1
- package/dist/values-DRY-a32G.mjs +0 -2
- package/dist/values-DRY-a32G.mjs.map +0 -1
package/dist/persist.cjs.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var e=require("@for-fun/event-emitter"),t=require("./values-
|
|
1
|
+
"use strict";var e=require("@for-fun/event-emitter"),t=require("./values-BbPnLByD.cjs.js"),r=require("./errors-TzyWwBfw.cjs.js");function n(e,t){return e.startsWith(`${t.slice(0,-1)},`)}function c(t,c){const{name:i,event:o="change",scope:s="branch",callback:a}=c;if(void 0===i)return e.on(t.emitter,o,a);const u=function(e){return Array.isArray(e)&&e.every(e=>"number"!=typeof e)}(i)?i:[i],l=u.map(c=>{const i=r.create(c);return"errors"===o||"touched"===o?function(t,r,n,c){return e.on(t,r,e=>{void 0!==e&&e.key!==n||c()})}(t.emitter,o,i.key,a):function(t,r,c,i,o){const{key:s}=c;return e.on(t,r,e=>{(void 0===e||e.key===s||n(s,e.key)||"branch"===i&&n(e.key,s))&&o()})}(t.emitter,o,i,s,a)});return 1===l.length?l[0]:()=>l.forEach(e=>e())}exports.persistForm=function(e,r){const{key:n}=r,i=r.storage??("undefined"!=typeof localStorage?localStorage:void 0);if(!i)return()=>{};const o=i.getItem(n);if(null!==o)try{const n=r.deserialize??(e=>JSON.parse(e));t.setInitialValues(e,n(o))}catch{}return c(e,{event:"change",callback:()=>{const c=r.serialize?r.serialize(t.getValues(e)):t.getValues(e);try{i.setItem(n,JSON.stringify(c))}catch{}}})};
|
|
2
2
|
//# sourceMappingURL=persist.cjs.js.map
|
package/dist/persist.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"persist.cjs.js","sources":["../src/subscribe.ts","../src/persist.ts"],"sourcesContent":["import {on} from './emitter';\nimport type {EventEmitter} from './emitter';\nimport createPath from './path';\nimport type {Name, Path} from './path';\nimport type {Form, FormEvents} from './form';\n\n/** Subscription granularity for {@link onPathEvent}.\n * - `'leaf'`: the subscriber reads exactly one key ({@link\n * useValueByPath}); only writes at that key or above it can change what\n * it reads.\n * - `'branch'`: the subscriber aggregates a whole subtree below a key\n * ({@link useFieldArray}); descendant writes matter too. */\nexport type WatchScope = 'leaf' | 'branch';\n\n/**\n * Is `key` a strict descendant of `ancestorKey`?\n *\n * Keys are JSON.stringify'd segment arrays ('[\"a\",\"b\"]'), so a descendant\n * key is the ancestor key minus its closing ']' followed by a ','\n * ('[\"a\",\"b\",'). The ',' separator is mandatory: a plain prefix match\n * would let the sibling '[\"tagsX\"]' pass as a descendant of '[\"tags\"]'.\n */\nfunction isDescendant(key: string, ancestorKey: string): boolean {\n return key.startsWith(`${ancestorKey.slice(0, -1)},`);\n}\n\n/**\n * Subscribe to `event`, invoking `cb` only when the emitted path is\n * relevant to `path`.\n *\n * Payload-less broadcasts (reset, setInitialValues) always invoke `cb` --\n * they are global syncs and the correctness fallback. (removeFieldByPath\n * emits with its path: its mutations are bounded to that key, so the path\n * matching below is exact.) When the emit carries a path P:\n * - `'leaf'`: P.key equals `path.key` or is one of its ancestors -- a leaf\n * read falls back to ancestor values (getValueByPath), so ancestor\n * writes must invalidate, while sibling and descendant writes cannot\n * change what the leaf reads.\n * - `'branch'`: `'leaf'` semantics plus P.key being a descendant of\n * `path.key` -- changed descendants re-aggregate the subtree.\n *\n * @param emitter emitter to subscribe to\n * @param event event name\n * @param path the watched path\n * @param scope which writes around `path` are relevant\n * @param cb listener, invoked with no arguments\n * @return unsubscribe function\n */\nexport function onPathEvent(\n emitter: EventEmitter<FormEvents>,\n event: SubscribeEvent,\n path: Path,\n scope: WatchScope,\n cb: () => void\n): () => void {\n const {key} = path;\n return on(emitter, event, (changed?: Path) => {\n if (\n changed === undefined ||\n changed.key === key ||\n isDescendant(key, changed.key) ||\n (scope === 'branch' && isDescendant(changed.key, key))\n ) {\n cb();\n }\n });\n}\n\n/**\n * Subscribe to `event`, invoking `cb` only when the emitted path's key is\n * exactly `key` (or the emit carries no payload -- a global sync).\n *\n * For state stored per exact key (errors, touched) no ancestor or\n * descendant matching is wanted: another field's key must not wake this\n * subscriber.\n *\n * @param emitter emitter to subscribe to\n * @param event event name\n * @param key exact path key to match\n * @param cb listener, invoked with no arguments\n * @return unsubscribe function\n */\nexport function onKeyEvent(\n emitter: EventEmitter<FormEvents>,\n event: SubscribeEvent,\n key: string,\n cb: () => void\n): () => void {\n return on(emitter, event, (changed?: Path) => {\n if (changed === undefined || changed.key === key) cb();\n });\n}\n\n/** Events {@link subscribe} can watch. `'errors'` and `'touched'` are\n * stored per exact key, so they match exact keys ({@link onKeyEvent});\n * `'change'`, `'validating'`, `'submitting'`, `'submitCount'`,\n * `'disabled'`, `'status'` and `'submitSuccessful'` go through\n * {@link onPathEvent}. `'validating'` carries paths (one per async\n * validator round) and matches by path exactly like `'change'`;\n * `'submitting'`, `'submitCount'`, `'disabled'`, `'status'` and\n * `'submitSuccessful'` are payload-less broadcasts, so `name` never\n * narrows them — every subscriber hears every emission. */\nexport type SubscribeEvent =\n | 'change'\n | 'errors'\n | 'touched'\n | 'validating'\n | 'submitting'\n | 'submitCount'\n | 'submitSuccessful'\n | 'disabled'\n | 'status'\n | 'loading';\n\n/** Options accepted by {@link subscribe}. */\nexport type SubscribeOptions = {\n /** Path (or list of paths) to watch. Omit to receive every emission of\n * `event`, payload-less broadcasts included. A single segments path\n * (`['tags', 0]`) and a list of names (`['tags', 'user.name']`) are told\n * apart by the same rule `trigger` uses: only a segments path can hold\n * a number. */\n name?: Name | Name[];\n /** Event to watch. Defaults to `'change'`. */\n event?: SubscribeEvent;\n /** Which writes around `name` are relevant — `'leaf'` or `'branch'`.\n * Only meaningful for the path-carrying events `'change'` and\n * `'validating'`: `'errors'`/`'touched'` match exact keys and\n * `'submitting'`/`'submitCount'`/`'disabled'`/`'status'`/\n * `'submitSuccessful'` are payload-less. Defaults to `'branch'` — the\n * intuitive linkage semantics, where subscribing to `'tags'` means the\n * whole branch. */\n scope?: WatchScope;\n /** Invoked with no arguments after each matching emission. Read fresh\n * state through the `get*` readers inside it. */\n callback: () => void;\n};\n\n/** Is `name` a list of names rather than one segments path? Numbers only\n * occur inside a segments path (`['a', 0]`), never as standalone names —\n * the same disambiguation `trigger` applies to its name argument. */\nfunction isNameList(name: Name | Name[]): name is Name[] {\n return (\n Array.isArray(name) &&\n (name as (number | unknown)[]).every(part => typeof part !== 'number')\n );\n}\n\n/**\n * Subscribe to form events imperatively — the non-render counterpart of\n * the `use*` hooks: linkages and side effects (province changed → clear\n * city, autosave, analytics) run without mounting a watching component.\n *\n * Without `name`, `callback` fires on every `event` emission, payload-less\n * broadcasts (reset, setInitialValues) included. With `name`, matching\n * follows the event's shape: `'errors'`/`'touched'` match the exact key\n * ({@link onKeyEvent}) — another field's error never wakes this\n * subscriber — while `'change'`/`'validating'`/`'submitting'`/\n * `'submitCount'`/`'disabled'`/`'submitSuccessful'` go through\n * {@link onPathEvent}, so the default `'branch'` scope wakes a `'tags'`\n * subscriber when any `tags.*` descendant is written. `'validating'`\n * carries a path per validator round and narrows by path like\n * `'change'`; `'disabled'`/`'submitSuccessful'` (like `'submitting'`)\n * are payload-less broadcasts that every named subscriber receives. A\n * `name` array builds one subscription per path and the returned\n * function unsubscribes them all.\n *\n * @param form the form to watch\n * @param options event, name(s), scope and callback\n * @return unsubscribe function\n */\nexport function subscribe(form: Form, options: SubscribeOptions): () => void {\n const {name, event = 'change', scope = 'branch', callback} = options;\n if (name === undefined) return on(form.emitter, event, callback);\n const names = isNameList(name) ? name : [name];\n const unsubscribes = names.map(one => {\n const path = createPath(one);\n return event === 'errors' || event === 'touched'\n ? onKeyEvent(form.emitter, event, path.key, callback)\n : onPathEvent(form.emitter, event, path, scope, callback);\n });\n return unsubscribes.length === 1\n ? unsubscribes[0]\n : () => unsubscribes.forEach(unsubscribe => unsubscribe());\n}\n","/**\n * Local persistence — `react-f0rm/persist`.\n *\n * Ships separately from the main entry (like the resolvers and devtools)\n * so persistence code never lands in bundles that do not use it, and\n * imports nothing but the headless core: the module is React-free and\n * works with any form instance, hook-created or not.\n */\nimport {getValues, setInitialValues} from './form';\nimport type {Form} from './form';\nimport {subscribe} from './subscribe';\n\n/** Storage surface {@link persistForm} needs — the browser's\n * `localStorage`/`sessionStorage` satisfy it as-is; pass a custom object\n * (or a framework adapter) for tests, SSR or non-DOM runtimes. */\nexport type PersistStorage = {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n};\n\n/** Options for {@link persistForm}. */\nexport type PersistOptions = {\n /** Storage key the form snapshot lives under. */\n key: string;\n /** Where to read/write. Defaults to `window.localStorage` when it\n * exists; without one (SSR, Node) persistence becomes a silent no-op\n * and the returned unsubscribe is a no-op too. */\n storage?: PersistStorage;\n /** Transform values before serialization — e.g. strip File/FileList\n * entries (JSON.stringify drops their contents anyway) or pick a\n * subset. Defaults to identity. */\n serialize?: (values: Record<string, any>) => Record<string, any>;\n /** Parse the stored string back into values. Defaults to JSON.parse;\n * a throwing parse (corrupted/foreign payload) is swallowed and the\n * stored snapshot ignored. */\n deserialize?: (raw: string) => Record<string, any>;\n};\n\n/**\n * Persist a form's values to a storage backend (localStorage by default)\n * and hydrate them back on the next session.\n *\n * Call once at form creation, before user interaction: hydration applies\n * the stored snapshot through {@link setInitialValues} — the restored\n * values become the baseline, so the form starts clean, not dirty. Every\n * 'change' event re-writes the snapshot (a keystroke writes the whole\n * values tree, JSON-stringified; browsers handle that fine for typical\n * form sizes, and `serialize` can shrink it).\n *\n * Returns the unsubscribe function — call it to stop persisting (the\n * stored snapshot stays).\n *\n * @param form form instance to persist\n * @param options storage key, backend and (de)serialization hooks\n * @return unsubscribe function (no-op when no storage exists)\n */\nexport function persistForm(form: Form, options: PersistOptions): () => void {\n const {key} = options;\n const storage: PersistStorage | undefined =\n options.storage ??\n (typeof localStorage !== 'undefined' ? localStorage : undefined);\n if (!storage) return () => {};\n\n // Hydrate before subscribing: the restored baseline must exist before\n // the first 'change' write, so the initial snapshot persisted equals\n // the snapshot the user sees.\n const raw = storage.getItem(key);\n if (raw !== null) {\n try {\n const parse = options.deserialize ?? ((s: string) => JSON.parse(s));\n setInitialValues(form, parse(raw));\n } catch {\n // Corrupted or foreign payload under our key: ignore it, start from\n // the form's own initialValues.\n }\n }\n\n return subscribe(form, {\n event: 'change',\n callback: () => {\n const values = options.serialize\n ? options.serialize(getValues(form))\n : getValues(form);\n try {\n storage.setItem(key, JSON.stringify(values));\n } catch {\n // Quota exceeded / storage disabled: persistence is best-effort.\n }\n }\n });\n}\n"],"names":["isDescendant","key","ancestorKey","startsWith","slice","subscribe","form","options","name","event","scope","callback","on","emitter","names","Array","isArray","every","part","isNameList","unsubscribes","map","one","path","createPath","cb","changed","onKeyEvent","onPathEvent","length","forEach","unsubscribe","storage","localStorage","raw","getItem","parse","deserialize","s","JSON","setInitialValues","values","serialize","getValues","setItem","stringify"],"mappings":"iIAsBA,SAASA,EAAaC,EAAaC,GACjC,OAAOD,EAAIE,WAAW,GAAGD,EAAYE,MAAM,GAAG,MAChD,CAkJO,SAASC,EAAUC,EAAYC,GACpC,MAAMC,KAACA,EAAAC,MAAMA,EAAQ,eAAUC,EAAQ,SAAAC,SAAUA,GAAYJ,EAC7D,QAAa,IAATC,EAAoB,OAAOI,EAAAA,GAAGN,EAAKO,QAASJ,EAAOE,GACvD,MAAMG,EAjCR,SAAoBN,GAClB,OACEO,MAAMC,QAAQR,IACbA,EAA8BS,MAAMC,GAAwB,iBAATA,EAExD,CA4BgBC,CAAWX,GAAQA,EAAO,CAACA,GACnCY,EAAeN,EAAMO,IAAIC,IAC7B,MAAMC,EAAOC,EAAAA,OAAWF,GACxB,MAAiB,WAAVb,GAAgC,YAAVA,EA9F1B,SACLI,EACAJ,EACAR,EACAwB,GAEA,OAAOb,KAAGC,EAASJ,EAAQiB,SACT,IAAZA,GAAyBA,EAAQzB,MAAQA,GAAKwB,KAEtD,CAsFQE,CAAWrB,EAAKO,QAASJ,EAAOc,EAAKtB,IAAKU,GAjI3C,SACLE,EACAJ,EACAc,EACAb,EACAe,GAEA,MAAMxB,IAACA,GAAOsB,EACd,OAAOX,KAAGC,EAASJ,EAAQiB,UAEX,IAAZA,GACAA,EAAQzB,MAAQA,GAChBD,EAAaC,EAAKyB,EAAQzB,MACf,WAAVS,GAAsBV,EAAa0B,EAAQzB,IAAKA,KAEjDwB,KAGN,CAgHQG,CAAYtB,EAAKO,QAASJ,EAAOc,EAAMb,EAAOC,KAEpD,OAA+B,IAAxBS,EAAaS,OAChBT,EAAa,GACb,IAAMA,EAAaU,QAAQC,GAAeA,IAChD,qBC/HO,SAAqBzB,EAAYC,GACtC,MAAMN,IAACA,GAAOM,EACRyB,EACJzB,EAAQyB,UACiB,oBAAjBC,aAA+BA,qBACzC,IAAKD,EAAS,MAAO,OAKrB,MAAME,EAAMF,EAAQG,QAAQlC,GAC5B,GAAY,OAARiC,EACF,IACE,MAAME,EAAQ7B,EAAQ8B,aAAA,CAAiBC,GAAcC,KAAKH,MAAME,IAChEE,EAAAA,iBAAiBlC,EAAM8B,EAAMF,GAC/B,CAAA,MAGA,CAGF,OAAO7B,EAAUC,EAAM,CACrBG,MAAO,SACPE,SAAU,KACR,MAAM8B,EAASlC,EAAQmC,UACnBnC,EAAQmC,UAAUC,EAAAA,UAAUrC,IAC5BqC,EAAAA,UAAUrC,GACd,IACE0B,EAAQY,QAAQ3C,EAAKsC,KAAKM,UAAUJ,GACtC,CAAA,MAEA,IAGN"}
|
|
1
|
+
{"version":3,"file":"persist.cjs.js","sources":["../src/subscribe.ts","../src/persist.ts"],"sourcesContent":["import {on} from './emitter';\nimport type {EventEmitter} from './emitter';\nimport createPath from './path';\nimport type {Name, Path} from './path';\nimport type {Form, FormEvents} from './form';\n\n/** Subscription granularity for {@link onPathEvent}: `'leaf'` reads one\n * key (only writes at it or above matter); `'branch'` aggregates a whole\n * subtree (descendant writes matter too). */\nexport type WatchScope = 'leaf' | 'branch';\n\n/**\n * Is `key` a strict descendant? Keys are JSON-stringified segment arrays,\n * so a descendant is the ancestor key minus its closing ']' plus ','; the\n * mandatory ',' keeps sibling '[\"tagsX\"]' from matching '[\"tags\"]'.\n */\nfunction isDescendant(key: string, ancestorKey: string): boolean {\n return key.startsWith(`${ancestorKey.slice(0, -1)},`);\n}\n\n/**\n * Subscribe to `event`, invoking `cb` only when the emitted path is\n * relevant. Payload-less broadcasts always invoke (global sync). With a\n * path P: `'leaf'` fires on P == path or an ancestor (leaf reads fall back\n * to ancestor values); `'branch'` also fires on descendants, which\n * re-aggregate the subtree.\n */\nexport function onPathEvent(\n emitter: EventEmitter<FormEvents>,\n event: SubscribeEvent,\n path: Path,\n scope: WatchScope,\n cb: () => void\n): () => void {\n const {key} = path;\n return on(emitter, event, (changed?: Path) => {\n if (\n changed === undefined ||\n changed.key === key ||\n isDescendant(key, changed.key) ||\n (scope === 'branch' && isDescendant(changed.key, key))\n ) {\n cb();\n }\n });\n}\n\n/**\n * Subscribe to `event`, invoking `cb` only on exact key matches (or a\n * payload-less global sync). Exact-key state (errors, touched) wants no\n * ancestor/descendant matching.\n */\nexport function onKeyEvent(\n emitter: EventEmitter<FormEvents>,\n event: SubscribeEvent,\n key: string,\n cb: () => void\n): () => void {\n return on(emitter, event, (changed?: Path) => {\n if (changed === undefined || changed.key === key) cb();\n });\n}\n\n/** Events {@link subscribe} can watch. `'errors'`/`'touched'` match exact\n * keys ({@link onKeyEvent}); `'change'`/`'validating'` match by path\n * ({@link onPathEvent}); the rest are payload-less broadcasts every\n * subscriber hears. */\nexport type SubscribeEvent =\n | 'change'\n | 'errors'\n | 'touched'\n | 'validating'\n | 'submitting'\n | 'submitCount'\n | 'submitSuccessful'\n | 'disabled'\n | 'status'\n | 'loading';\n\nexport type SubscribeOptions = {\n /** Path or paths to watch; omit to receive every emission. A segments\n * path vs a name list is told apart by `trigger`'s rule: only segments\n * hold a number. */\n name?: Name | Name[];\n /** Event to watch. Defaults to `'change'`. */\n event?: SubscribeEvent;\n /** Which writes around `name` are relevant. Only meaningful for the\n * path-carrying events; defaults to `'branch'` (subscribing to `'tags'`\n * means the whole branch). */\n scope?: WatchScope;\n /** Invoked after each matching emission; read fresh state through the\n * `get*` readers inside it. */\n callback: () => void;\n};\n\n/** A segments path can hold a number (`['a', 0]`), a name list never can —\n * the same disambiguation `trigger` applies to its name argument. */\nfunction isNameList(name: Name | Name[]): name is Name[] {\n return (\n Array.isArray(name) &&\n (name as (number | unknown)[]).every(part => typeof part !== 'number')\n );\n}\n\n/**\n * Subscribe to form events imperatively — the non-render counterpart of\n * the `use*` hooks. Without `name`, `callback` fires on every emission;\n * with `name`, matching follows the event's shape (`'errors'`/`'touched'`\n * match exact keys, the rest match by path or broadcast). A name array\n * builds one subscription per path.\n */\nexport function subscribe(form: Form, options: SubscribeOptions): () => void {\n const {name, event = 'change', scope = 'branch', callback} = options;\n if (name === undefined) return on(form.emitter, event, callback);\n const names = isNameList(name) ? name : [name];\n const unsubscribes = names.map(one => {\n const path = createPath(one);\n return event === 'errors' || event === 'touched'\n ? onKeyEvent(form.emitter, event, path.key, callback)\n : onPathEvent(form.emitter, event, path, scope, callback);\n });\n return unsubscribes.length === 1\n ? unsubscribes[0]\n : () => unsubscribes.forEach(unsubscribe => unsubscribe());\n}\n\n/** The handle {@link watch} returns: a subscribe/getSnapshot pair any\n * reactive runtime can bind to. One internal listener stays alive from\n * creation, so `getSnapshot()` is always fresh; `dispose` ends it. */\nexport type WatchHandle<T> = {\n /** Read the current snapshot, cached between events; repeated reads\n * share one reference until state changes. */\n getSnapshot: () => T;\n /** Register a change listener; fires only when the projection changed.\n * With `isEqual`, an equal verdict skips the callback; without one,\n * every event wakes it. */\n subscribe: (invalidate: () => void) => () => void;\n /** Remove the internal listener and every consumer callback; the handle\n * is dead afterwards. */\n dispose: () => void;\n};\n\n/**\n * Watch a projection of form state without React — the framework-free\n * {@link useWatch} (same `isEqual` bailout), tree-shaken when unused.\n * Returns a {@link WatchHandle}: read `getSnapshot()`, re-read/re-render\n * when `subscribe`'s listener fires. The handle subscribes eagerly, so\n * reads are never stale; `getter`/`isEqual` are captured at creation.\n */\nexport function watch<T>(\n form: Form,\n event: SubscribeEvent,\n getter: () => T,\n isEqual?: (prev: T, next: T) => boolean\n): WatchHandle<T> {\n const cache: {hasValue: boolean; value?: T} = {hasValue: false};\n const consumers = new Set<() => void>();\n const wake = () => {\n if (isEqual && cache.hasValue) {\n // Custom comparator: decide before waking consumers. Equal means\n // unchanged — keep the cache and skip; unequal stores the fresh\n // snapshot so the next read needs no recompute.\n const next = getter();\n if (isEqual(cache.value as T, next)) return;\n cache.value = next;\n } else {\n cache.value = getter();\n cache.hasValue = true;\n }\n consumers.forEach(invalidate => invalidate());\n };\n // Eager: the cache tracks the form from creation, so reads are fresh\n // even before any consumer subscribes.\n const off = on(form.emitter, event, wake);\n return {\n getSnapshot: () => {\n if (!cache.hasValue) {\n cache.value = getter();\n cache.hasValue = true;\n }\n return cache.value as T;\n },\n subscribe: (invalidate: () => void) => {\n consumers.add(invalidate);\n return () => {\n consumers.delete(invalidate);\n };\n },\n dispose: () => {\n off();\n consumers.clear();\n }\n };\n}\n","/** Local persistence — `react-f0rm/persist`. Ships separately (React-free,\n * imports only the headless core) so unused persistence never enters a\n * bundle. */\nimport {getValues, setInitialValues} from './form';\nimport type {Form} from './form';\nimport {subscribe} from './subscribe';\n\n/** Storage surface {@link persistForm} needs; localStorage/sessionStorage\n * satisfy it, pass a custom object for tests/SSR. */\nexport type PersistStorage = {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n};\n\n/** Options for {@link persistForm}. */\nexport type PersistOptions = {\n /** Storage key the form snapshot lives under. */\n key: string;\n /** Where to read/write. Defaults to localStorage; without one persistence is a silent no-op. */\n storage?: PersistStorage;\n /** Transform values before serialization (e.g. strip File entries). Defaults to identity. */\n serialize?: (values: Record<string, any>) => Record<string, any>;\n /** Parse the stored string back. Defaults to JSON.parse; a throwing parse is swallowed. */\n deserialize?: (raw: string) => Record<string, any>;\n};\n\n/** Persist a form's values and hydrate them back next session. Call once\n * before interaction: hydration applies the snapshot via setInitialValues\n * (restored values become the baseline, so the form starts clean); every\n * 'change' re-writes it. Returns the unsubscribe (no-op without storage). */\nexport function persistForm(form: Form, options: PersistOptions): () => void {\n const {key} = options;\n const storage: PersistStorage | undefined =\n options.storage ??\n (typeof localStorage !== 'undefined' ? localStorage : undefined);\n if (!storage) return () => {};\n\n // Hydrate before subscribing: the restored baseline must exist before\n // the first 'change' write, so the initial snapshot persisted equals\n // the snapshot the user sees.\n const raw = storage.getItem(key);\n if (raw !== null) {\n try {\n const parse = options.deserialize ?? ((s: string) => JSON.parse(s));\n setInitialValues(form, parse(raw));\n } catch {\n // Corrupted or foreign payload under our key: ignore it, start from\n // the form's own initialValues.\n }\n }\n\n return subscribe(form, {\n event: 'change',\n callback: () => {\n const values = options.serialize\n ? options.serialize(getValues(form))\n : getValues(form);\n try {\n storage.setItem(key, JSON.stringify(values));\n } catch {\n // Quota exceeded / storage disabled: persistence is best-effort.\n }\n }\n });\n}\n"],"names":["isDescendant","key","ancestorKey","startsWith","slice","subscribe","form","options","name","event","scope","callback","on","emitter","names","Array","isArray","every","part","isNameList","unsubscribes","map","one","path","createPath","cb","changed","onKeyEvent","onPathEvent","length","forEach","unsubscribe","storage","localStorage","raw","getItem","parse","deserialize","s","JSON","setInitialValues","values","serialize","getValues","setItem","stringify"],"mappings":"iIAgBA,SAASA,EAAaC,EAAaC,GACjC,OAAOD,EAAIE,WAAW,GAAGD,EAAYE,MAAM,GAAG,MAChD,CA6FO,SAASC,EAAUC,EAAYC,GACpC,MAAMC,KAACA,EAAAC,MAAMA,EAAQ,eAAUC,EAAQ,SAAAC,SAAUA,GAAYJ,EAC7D,QAAa,IAATC,EAAoB,OAAOI,EAAAA,GAAGN,EAAKO,QAASJ,EAAOE,GACvD,MAAMG,EAjBR,SAAoBN,GAClB,OACEO,MAAMC,QAAQR,IACbA,EAA8BS,MAAMC,GAAwB,iBAATA,EAExD,CAYgBC,CAAWX,GAAQA,EAAO,CAACA,GACnCY,EAAeN,EAAMO,IAAIC,IAC7B,MAAMC,EAAOC,EAAAA,OAAWF,GACxB,MAAiB,WAAVb,GAAgC,YAAVA,EAjE1B,SACLI,EACAJ,EACAR,EACAwB,GAEA,OAAOb,KAAGC,EAASJ,EAAQiB,SACT,IAAZA,GAAyBA,EAAQzB,MAAQA,GAAKwB,KAEtD,CAyDQE,CAAWrB,EAAKO,QAASJ,EAAOc,EAAKtB,IAAKU,GA3F3C,SACLE,EACAJ,EACAc,EACAb,EACAe,GAEA,MAAMxB,IAACA,GAAOsB,EACd,OAAOX,KAAGC,EAASJ,EAAQiB,UAEX,IAAZA,GACAA,EAAQzB,MAAQA,GAChBD,EAAaC,EAAKyB,EAAQzB,MACf,WAAVS,GAAsBV,EAAa0B,EAAQzB,IAAKA,KAEjDwB,KAGN,CA0EQG,CAAYtB,EAAKO,QAASJ,EAAOc,EAAMb,EAAOC,KAEpD,OAA+B,IAAxBS,EAAaS,OAChBT,EAAa,GACb,IAAMA,EAAaU,QAAQC,GAAeA,IAChD,qBC9FO,SAAqBzB,EAAYC,GACtC,MAAMN,IAACA,GAAOM,EACRyB,EACJzB,EAAQyB,UACiB,oBAAjBC,aAA+BA,qBACzC,IAAKD,EAAS,MAAO,OAKrB,MAAME,EAAMF,EAAQG,QAAQlC,GAC5B,GAAY,OAARiC,EACF,IACE,MAAME,EAAQ7B,EAAQ8B,aAAA,CAAiBC,GAAcC,KAAKH,MAAME,IAChEE,EAAAA,iBAAiBlC,EAAM8B,EAAMF,GAC/B,CAAA,MAGA,CAGF,OAAO7B,EAAUC,EAAM,CACrBG,MAAO,SACPE,SAAU,KACR,MAAM8B,EAASlC,EAAQmC,UACnBnC,EAAQmC,UAAUC,EAAAA,UAAUrC,IAC5BqC,EAAAA,UAAUrC,GACd,IACE0B,EAAQY,QAAQ3C,EAAKsC,KAAKM,UAAUJ,GACtC,CAAA,MAEA,IAGN"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { FormInstance as Form } from './index.js';
|
|
2
|
+
import '@for-fun/event-emitter';
|
|
3
|
+
|
|
4
|
+
/** Storage surface {@link persistForm} needs; localStorage/sessionStorage
|
|
5
|
+
* satisfy it, pass a custom object for tests/SSR. */
|
|
6
|
+
type PersistStorage = {
|
|
7
|
+
getItem(key: string): string | null;
|
|
8
|
+
setItem(key: string, value: string): void;
|
|
9
|
+
};
|
|
10
|
+
/** Options for {@link persistForm}. */
|
|
11
|
+
type PersistOptions = {
|
|
12
|
+
/** Storage key the form snapshot lives under. */
|
|
13
|
+
key: string;
|
|
14
|
+
/** Where to read/write. Defaults to localStorage; without one persistence is a silent no-op. */
|
|
15
|
+
storage?: PersistStorage;
|
|
16
|
+
/** Transform values before serialization (e.g. strip File entries). Defaults to identity. */
|
|
17
|
+
serialize?: (values: Record<string, any>) => Record<string, any>;
|
|
18
|
+
/** Parse the stored string back. Defaults to JSON.parse; a throwing parse is swallowed. */
|
|
19
|
+
deserialize?: (raw: string) => Record<string, any>;
|
|
20
|
+
};
|
|
21
|
+
/** Persist a form's values and hydrate them back next session. Call once
|
|
22
|
+
* before interaction: hydration applies the snapshot via setInitialValues
|
|
23
|
+
* (restored values become the baseline, so the form starts clean); every
|
|
24
|
+
* 'change' re-writes it. Returns the unsubscribe (no-op without storage). */
|
|
25
|
+
declare function persistForm(form: Form, options: PersistOptions): () => void;
|
|
26
|
+
|
|
27
|
+
export { persistForm };
|
|
28
|
+
export type { PersistOptions, PersistStorage };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { FormInstance as Form } from './index.js';
|
|
2
|
+
import '@for-fun/event-emitter';
|
|
3
|
+
|
|
4
|
+
/** Storage surface {@link persistForm} needs; localStorage/sessionStorage
|
|
5
|
+
* satisfy it, pass a custom object for tests/SSR. */
|
|
6
|
+
type PersistStorage = {
|
|
7
|
+
getItem(key: string): string | null;
|
|
8
|
+
setItem(key: string, value: string): void;
|
|
9
|
+
};
|
|
10
|
+
/** Options for {@link persistForm}. */
|
|
11
|
+
type PersistOptions = {
|
|
12
|
+
/** Storage key the form snapshot lives under. */
|
|
13
|
+
key: string;
|
|
14
|
+
/** Where to read/write. Defaults to localStorage; without one persistence is a silent no-op. */
|
|
15
|
+
storage?: PersistStorage;
|
|
16
|
+
/** Transform values before serialization (e.g. strip File entries). Defaults to identity. */
|
|
17
|
+
serialize?: (values: Record<string, any>) => Record<string, any>;
|
|
18
|
+
/** Parse the stored string back. Defaults to JSON.parse; a throwing parse is swallowed. */
|
|
19
|
+
deserialize?: (raw: string) => Record<string, any>;
|
|
20
|
+
};
|
|
21
|
+
/** Persist a form's values and hydrate them back next session. Call once
|
|
22
|
+
* before interaction: hydration applies the snapshot via setInitialValues
|
|
23
|
+
* (restored values become the baseline, so the form starts clean); every
|
|
24
|
+
* 'change' re-writes it. Returns the unsubscribe (no-op without storage). */
|
|
25
|
+
declare function persistForm(form: Form, options: PersistOptions): () => void;
|
|
26
|
+
|
|
27
|
+
export { persistForm };
|
|
28
|
+
export type { PersistOptions, PersistStorage };
|
package/dist/persist.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { FormInstance as Form } from './index.js';
|
|
2
2
|
import '@for-fun/event-emitter';
|
|
3
3
|
|
|
4
|
-
/** Storage surface {@link persistForm} needs
|
|
5
|
-
*
|
|
6
|
-
* (or a framework adapter) for tests, SSR or non-DOM runtimes. */
|
|
4
|
+
/** Storage surface {@link persistForm} needs; localStorage/sessionStorage
|
|
5
|
+
* satisfy it, pass a custom object for tests/SSR. */
|
|
7
6
|
type PersistStorage = {
|
|
8
7
|
getItem(key: string): string | null;
|
|
9
8
|
setItem(key: string, value: string): void;
|
|
@@ -12,37 +11,17 @@ type PersistStorage = {
|
|
|
12
11
|
type PersistOptions = {
|
|
13
12
|
/** Storage key the form snapshot lives under. */
|
|
14
13
|
key: string;
|
|
15
|
-
/** Where to read/write. Defaults to
|
|
16
|
-
* exists; without one (SSR, Node) persistence becomes a silent no-op
|
|
17
|
-
* and the returned unsubscribe is a no-op too. */
|
|
14
|
+
/** Where to read/write. Defaults to localStorage; without one persistence is a silent no-op. */
|
|
18
15
|
storage?: PersistStorage;
|
|
19
|
-
/** Transform values before serialization
|
|
20
|
-
* entries (JSON.stringify drops their contents anyway) or pick a
|
|
21
|
-
* subset. Defaults to identity. */
|
|
16
|
+
/** Transform values before serialization (e.g. strip File entries). Defaults to identity. */
|
|
22
17
|
serialize?: (values: Record<string, any>) => Record<string, any>;
|
|
23
|
-
/** Parse the stored string back
|
|
24
|
-
* a throwing parse (corrupted/foreign payload) is swallowed and the
|
|
25
|
-
* stored snapshot ignored. */
|
|
18
|
+
/** Parse the stored string back. Defaults to JSON.parse; a throwing parse is swallowed. */
|
|
26
19
|
deserialize?: (raw: string) => Record<string, any>;
|
|
27
20
|
};
|
|
28
|
-
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* Call once at form creation, before user interaction: hydration applies
|
|
33
|
-
* the stored snapshot through {@link setInitialValues} — the restored
|
|
34
|
-
* values become the baseline, so the form starts clean, not dirty. Every
|
|
35
|
-
* 'change' event re-writes the snapshot (a keystroke writes the whole
|
|
36
|
-
* values tree, JSON-stringified; browsers handle that fine for typical
|
|
37
|
-
* form sizes, and `serialize` can shrink it).
|
|
38
|
-
*
|
|
39
|
-
* Returns the unsubscribe function — call it to stop persisting (the
|
|
40
|
-
* stored snapshot stays).
|
|
41
|
-
*
|
|
42
|
-
* @param form form instance to persist
|
|
43
|
-
* @param options storage key, backend and (de)serialization hooks
|
|
44
|
-
* @return unsubscribe function (no-op when no storage exists)
|
|
45
|
-
*/
|
|
21
|
+
/** Persist a form's values and hydrate them back next session. Call once
|
|
22
|
+
* before interaction: hydration applies the snapshot via setInitialValues
|
|
23
|
+
* (restored values become the baseline, so the form starts clean); every
|
|
24
|
+
* 'change' re-writes it. Returns the unsubscribe (no-op without storage). */
|
|
46
25
|
declare function persistForm(form: Form, options: PersistOptions): () => void;
|
|
47
26
|
|
|
48
27
|
export { persistForm };
|
package/dist/persist.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{on as e}from"@for-fun/event-emitter";import{s as t,g as r}from"./values-
|
|
1
|
+
import{on as e}from"@for-fun/event-emitter";import{s as t,g as r}from"./values-CHsmcZk4.mjs";import{c as n}from"./errors-DA4ReEd9.mjs";function o(e,t){return e.startsWith(`${t.slice(0,-1)},`)}function c(t,r){const{name:c,event:i="change",scope:a="branch",callback:s}=r;if(void 0===c)return e(t.emitter,i,s);const u=function(e){return Array.isArray(e)&&e.every(e=>"number"!=typeof e)}(c)?c:[c],f=u.map(r=>{const c=n(r);return"errors"===i||"touched"===i?function(t,r,n,o){return e(t,r,e=>{void 0!==e&&e.key!==n||o()})}(t.emitter,i,c.key,s):function(t,r,n,c,i){const{key:a}=n;return e(t,r,e=>{(void 0===e||e.key===a||o(a,e.key)||"branch"===c&&o(e.key,a))&&i()})}(t.emitter,i,c,a,s)});return 1===f.length?f[0]:()=>f.forEach(e=>e())}function i(e,n){const{key:o}=n,i=n.storage??("undefined"!=typeof localStorage?localStorage:void 0);if(!i)return()=>{};const a=i.getItem(o);if(null!==a)try{const r=n.deserialize??(e=>JSON.parse(e));t(e,r(a))}catch{}return c(e,{event:"change",callback:()=>{const t=n.serialize?n.serialize(r(e)):r(e);try{i.setItem(o,JSON.stringify(t))}catch{}}})}export{i as persistForm};
|
|
2
2
|
//# sourceMappingURL=persist.mjs.map
|
package/dist/persist.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"persist.mjs","sources":["../src/subscribe.ts","../src/persist.ts"],"sourcesContent":["import {on} from './emitter';\nimport type {EventEmitter} from './emitter';\nimport createPath from './path';\nimport type {Name, Path} from './path';\nimport type {Form, FormEvents} from './form';\n\n/** Subscription granularity for {@link onPathEvent}.\n * - `'leaf'`: the subscriber reads exactly one key ({@link\n * useValueByPath}); only writes at that key or above it can change what\n * it reads.\n * - `'branch'`: the subscriber aggregates a whole subtree below a key\n * ({@link useFieldArray}); descendant writes matter too. */\nexport type WatchScope = 'leaf' | 'branch';\n\n/**\n * Is `key` a strict descendant of `ancestorKey`?\n *\n * Keys are JSON.stringify'd segment arrays ('[\"a\",\"b\"]'), so a descendant\n * key is the ancestor key minus its closing ']' followed by a ','\n * ('[\"a\",\"b\",'). The ',' separator is mandatory: a plain prefix match\n * would let the sibling '[\"tagsX\"]' pass as a descendant of '[\"tags\"]'.\n */\nfunction isDescendant(key: string, ancestorKey: string): boolean {\n return key.startsWith(`${ancestorKey.slice(0, -1)},`);\n}\n\n/**\n * Subscribe to `event`, invoking `cb` only when the emitted path is\n * relevant to `path`.\n *\n * Payload-less broadcasts (reset, setInitialValues) always invoke `cb` --\n * they are global syncs and the correctness fallback. (removeFieldByPath\n * emits with its path: its mutations are bounded to that key, so the path\n * matching below is exact.) When the emit carries a path P:\n * - `'leaf'`: P.key equals `path.key` or is one of its ancestors -- a leaf\n * read falls back to ancestor values (getValueByPath), so ancestor\n * writes must invalidate, while sibling and descendant writes cannot\n * change what the leaf reads.\n * - `'branch'`: `'leaf'` semantics plus P.key being a descendant of\n * `path.key` -- changed descendants re-aggregate the subtree.\n *\n * @param emitter emitter to subscribe to\n * @param event event name\n * @param path the watched path\n * @param scope which writes around `path` are relevant\n * @param cb listener, invoked with no arguments\n * @return unsubscribe function\n */\nexport function onPathEvent(\n emitter: EventEmitter<FormEvents>,\n event: SubscribeEvent,\n path: Path,\n scope: WatchScope,\n cb: () => void\n): () => void {\n const {key} = path;\n return on(emitter, event, (changed?: Path) => {\n if (\n changed === undefined ||\n changed.key === key ||\n isDescendant(key, changed.key) ||\n (scope === 'branch' && isDescendant(changed.key, key))\n ) {\n cb();\n }\n });\n}\n\n/**\n * Subscribe to `event`, invoking `cb` only when the emitted path's key is\n * exactly `key` (or the emit carries no payload -- a global sync).\n *\n * For state stored per exact key (errors, touched) no ancestor or\n * descendant matching is wanted: another field's key must not wake this\n * subscriber.\n *\n * @param emitter emitter to subscribe to\n * @param event event name\n * @param key exact path key to match\n * @param cb listener, invoked with no arguments\n * @return unsubscribe function\n */\nexport function onKeyEvent(\n emitter: EventEmitter<FormEvents>,\n event: SubscribeEvent,\n key: string,\n cb: () => void\n): () => void {\n return on(emitter, event, (changed?: Path) => {\n if (changed === undefined || changed.key === key) cb();\n });\n}\n\n/** Events {@link subscribe} can watch. `'errors'` and `'touched'` are\n * stored per exact key, so they match exact keys ({@link onKeyEvent});\n * `'change'`, `'validating'`, `'submitting'`, `'submitCount'`,\n * `'disabled'`, `'status'` and `'submitSuccessful'` go through\n * {@link onPathEvent}. `'validating'` carries paths (one per async\n * validator round) and matches by path exactly like `'change'`;\n * `'submitting'`, `'submitCount'`, `'disabled'`, `'status'` and\n * `'submitSuccessful'` are payload-less broadcasts, so `name` never\n * narrows them — every subscriber hears every emission. */\nexport type SubscribeEvent =\n | 'change'\n | 'errors'\n | 'touched'\n | 'validating'\n | 'submitting'\n | 'submitCount'\n | 'submitSuccessful'\n | 'disabled'\n | 'status'\n | 'loading';\n\n/** Options accepted by {@link subscribe}. */\nexport type SubscribeOptions = {\n /** Path (or list of paths) to watch. Omit to receive every emission of\n * `event`, payload-less broadcasts included. A single segments path\n * (`['tags', 0]`) and a list of names (`['tags', 'user.name']`) are told\n * apart by the same rule `trigger` uses: only a segments path can hold\n * a number. */\n name?: Name | Name[];\n /** Event to watch. Defaults to `'change'`. */\n event?: SubscribeEvent;\n /** Which writes around `name` are relevant — `'leaf'` or `'branch'`.\n * Only meaningful for the path-carrying events `'change'` and\n * `'validating'`: `'errors'`/`'touched'` match exact keys and\n * `'submitting'`/`'submitCount'`/`'disabled'`/`'status'`/\n * `'submitSuccessful'` are payload-less. Defaults to `'branch'` — the\n * intuitive linkage semantics, where subscribing to `'tags'` means the\n * whole branch. */\n scope?: WatchScope;\n /** Invoked with no arguments after each matching emission. Read fresh\n * state through the `get*` readers inside it. */\n callback: () => void;\n};\n\n/** Is `name` a list of names rather than one segments path? Numbers only\n * occur inside a segments path (`['a', 0]`), never as standalone names —\n * the same disambiguation `trigger` applies to its name argument. */\nfunction isNameList(name: Name | Name[]): name is Name[] {\n return (\n Array.isArray(name) &&\n (name as (number | unknown)[]).every(part => typeof part !== 'number')\n );\n}\n\n/**\n * Subscribe to form events imperatively — the non-render counterpart of\n * the `use*` hooks: linkages and side effects (province changed → clear\n * city, autosave, analytics) run without mounting a watching component.\n *\n * Without `name`, `callback` fires on every `event` emission, payload-less\n * broadcasts (reset, setInitialValues) included. With `name`, matching\n * follows the event's shape: `'errors'`/`'touched'` match the exact key\n * ({@link onKeyEvent}) — another field's error never wakes this\n * subscriber — while `'change'`/`'validating'`/`'submitting'`/\n * `'submitCount'`/`'disabled'`/`'submitSuccessful'` go through\n * {@link onPathEvent}, so the default `'branch'` scope wakes a `'tags'`\n * subscriber when any `tags.*` descendant is written. `'validating'`\n * carries a path per validator round and narrows by path like\n * `'change'`; `'disabled'`/`'submitSuccessful'` (like `'submitting'`)\n * are payload-less broadcasts that every named subscriber receives. A\n * `name` array builds one subscription per path and the returned\n * function unsubscribes them all.\n *\n * @param form the form to watch\n * @param options event, name(s), scope and callback\n * @return unsubscribe function\n */\nexport function subscribe(form: Form, options: SubscribeOptions): () => void {\n const {name, event = 'change', scope = 'branch', callback} = options;\n if (name === undefined) return on(form.emitter, event, callback);\n const names = isNameList(name) ? name : [name];\n const unsubscribes = names.map(one => {\n const path = createPath(one);\n return event === 'errors' || event === 'touched'\n ? onKeyEvent(form.emitter, event, path.key, callback)\n : onPathEvent(form.emitter, event, path, scope, callback);\n });\n return unsubscribes.length === 1\n ? unsubscribes[0]\n : () => unsubscribes.forEach(unsubscribe => unsubscribe());\n}\n","/**\n * Local persistence — `react-f0rm/persist`.\n *\n * Ships separately from the main entry (like the resolvers and devtools)\n * so persistence code never lands in bundles that do not use it, and\n * imports nothing but the headless core: the module is React-free and\n * works with any form instance, hook-created or not.\n */\nimport {getValues, setInitialValues} from './form';\nimport type {Form} from './form';\nimport {subscribe} from './subscribe';\n\n/** Storage surface {@link persistForm} needs — the browser's\n * `localStorage`/`sessionStorage` satisfy it as-is; pass a custom object\n * (or a framework adapter) for tests, SSR or non-DOM runtimes. */\nexport type PersistStorage = {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n};\n\n/** Options for {@link persistForm}. */\nexport type PersistOptions = {\n /** Storage key the form snapshot lives under. */\n key: string;\n /** Where to read/write. Defaults to `window.localStorage` when it\n * exists; without one (SSR, Node) persistence becomes a silent no-op\n * and the returned unsubscribe is a no-op too. */\n storage?: PersistStorage;\n /** Transform values before serialization — e.g. strip File/FileList\n * entries (JSON.stringify drops their contents anyway) or pick a\n * subset. Defaults to identity. */\n serialize?: (values: Record<string, any>) => Record<string, any>;\n /** Parse the stored string back into values. Defaults to JSON.parse;\n * a throwing parse (corrupted/foreign payload) is swallowed and the\n * stored snapshot ignored. */\n deserialize?: (raw: string) => Record<string, any>;\n};\n\n/**\n * Persist a form's values to a storage backend (localStorage by default)\n * and hydrate them back on the next session.\n *\n * Call once at form creation, before user interaction: hydration applies\n * the stored snapshot through {@link setInitialValues} — the restored\n * values become the baseline, so the form starts clean, not dirty. Every\n * 'change' event re-writes the snapshot (a keystroke writes the whole\n * values tree, JSON-stringified; browsers handle that fine for typical\n * form sizes, and `serialize` can shrink it).\n *\n * Returns the unsubscribe function — call it to stop persisting (the\n * stored snapshot stays).\n *\n * @param form form instance to persist\n * @param options storage key, backend and (de)serialization hooks\n * @return unsubscribe function (no-op when no storage exists)\n */\nexport function persistForm(form: Form, options: PersistOptions): () => void {\n const {key} = options;\n const storage: PersistStorage | undefined =\n options.storage ??\n (typeof localStorage !== 'undefined' ? localStorage : undefined);\n if (!storage) return () => {};\n\n // Hydrate before subscribing: the restored baseline must exist before\n // the first 'change' write, so the initial snapshot persisted equals\n // the snapshot the user sees.\n const raw = storage.getItem(key);\n if (raw !== null) {\n try {\n const parse = options.deserialize ?? ((s: string) => JSON.parse(s));\n setInitialValues(form, parse(raw));\n } catch {\n // Corrupted or foreign payload under our key: ignore it, start from\n // the form's own initialValues.\n }\n }\n\n return subscribe(form, {\n event: 'change',\n callback: () => {\n const values = options.serialize\n ? options.serialize(getValues(form))\n : getValues(form);\n try {\n storage.setItem(key, JSON.stringify(values));\n } catch {\n // Quota exceeded / storage disabled: persistence is best-effort.\n }\n }\n });\n}\n"],"names":["isDescendant","key","ancestorKey","startsWith","slice","subscribe","form","options","name","event","scope","callback","on","emitter","names","Array","isArray","every","part","isNameList","unsubscribes","map","one","path","createPath","cb","changed","onKeyEvent","onPathEvent","length","forEach","unsubscribe","persistForm","storage","localStorage","raw","getItem","parse","deserialize","s","JSON","setInitialValues","values","serialize","getValues","setItem","stringify"],"mappings":"uIAsBA,SAASA,EAAaC,EAAaC,GACjC,OAAOD,EAAIE,WAAW,GAAGD,EAAYE,MAAM,GAAG,MAChD,CAkJO,SAASC,EAAUC,EAAYC,GACpC,MAAMC,KAACA,EAAAC,MAAMA,EAAQ,eAAUC,EAAQ,SAAAC,SAAUA,GAAYJ,EAC7D,QAAa,IAATC,EAAoB,OAAOI,EAAGN,EAAKO,QAASJ,EAAOE,GACvD,MAAMG,EAjCR,SAAoBN,GAClB,OACEO,MAAMC,QAAQR,IACbA,EAA8BS,MAAMC,GAAwB,iBAATA,EAExD,CA4BgBC,CAAWX,GAAQA,EAAO,CAACA,GACnCY,EAAeN,EAAMO,IAAIC,IAC7B,MAAMC,EAAOC,EAAWF,GACxB,MAAiB,WAAVb,GAAgC,YAAVA,EA9F1B,SACLI,EACAJ,EACAR,EACAwB,GAEA,OAAOb,EAAGC,EAASJ,EAAQiB,SACT,IAAZA,GAAyBA,EAAQzB,MAAQA,GAAKwB,KAEtD,CAsFQE,CAAWrB,EAAKO,QAASJ,EAAOc,EAAKtB,IAAKU,GAjI3C,SACLE,EACAJ,EACAc,EACAb,EACAe,GAEA,MAAMxB,IAACA,GAAOsB,EACd,OAAOX,EAAGC,EAASJ,EAAQiB,UAEX,IAAZA,GACAA,EAAQzB,MAAQA,GAChBD,EAAaC,EAAKyB,EAAQzB,MACf,WAAVS,GAAsBV,EAAa0B,EAAQzB,IAAKA,KAEjDwB,KAGN,CAgHQG,CAAYtB,EAAKO,QAASJ,EAAOc,EAAMb,EAAOC,KAEpD,OAA+B,IAAxBS,EAAaS,OAChBT,EAAa,GACb,IAAMA,EAAaU,QAAQC,GAAeA,IAChD,CC/HO,SAASC,EAAY1B,EAAYC,GACtC,MAAMN,IAACA,GAAOM,EACR0B,EACJ1B,EAAQ0B,UACiB,oBAAjBC,aAA+BA,qBACzC,IAAKD,EAAS,MAAO,OAKrB,MAAME,EAAMF,EAAQG,QAAQnC,GAC5B,GAAY,OAARkC,EACF,IACE,MAAME,EAAQ9B,EAAQ+B,aAAA,CAAiBC,GAAcC,KAAKH,MAAME,IAChEE,EAAiBnC,EAAM+B,EAAMF,GAC/B,CAAA,MAGA,CAGF,OAAO9B,EAAUC,EAAM,CACrBG,MAAO,SACPE,SAAU,KACR,MAAM+B,EAASnC,EAAQoC,UACnBpC,EAAQoC,UAAUC,EAAUtC,IAC5BsC,EAAUtC,GACd,IACE2B,EAAQY,QAAQ5C,EAAKuC,KAAKM,UAAUJ,GACtC,CAAA,MAEA,IAGN"}
|
|
1
|
+
{"version":3,"file":"persist.mjs","sources":["../src/subscribe.ts","../src/persist.ts"],"sourcesContent":["import {on} from './emitter';\nimport type {EventEmitter} from './emitter';\nimport createPath from './path';\nimport type {Name, Path} from './path';\nimport type {Form, FormEvents} from './form';\n\n/** Subscription granularity for {@link onPathEvent}: `'leaf'` reads one\n * key (only writes at it or above matter); `'branch'` aggregates a whole\n * subtree (descendant writes matter too). */\nexport type WatchScope = 'leaf' | 'branch';\n\n/**\n * Is `key` a strict descendant? Keys are JSON-stringified segment arrays,\n * so a descendant is the ancestor key minus its closing ']' plus ','; the\n * mandatory ',' keeps sibling '[\"tagsX\"]' from matching '[\"tags\"]'.\n */\nfunction isDescendant(key: string, ancestorKey: string): boolean {\n return key.startsWith(`${ancestorKey.slice(0, -1)},`);\n}\n\n/**\n * Subscribe to `event`, invoking `cb` only when the emitted path is\n * relevant. Payload-less broadcasts always invoke (global sync). With a\n * path P: `'leaf'` fires on P == path or an ancestor (leaf reads fall back\n * to ancestor values); `'branch'` also fires on descendants, which\n * re-aggregate the subtree.\n */\nexport function onPathEvent(\n emitter: EventEmitter<FormEvents>,\n event: SubscribeEvent,\n path: Path,\n scope: WatchScope,\n cb: () => void\n): () => void {\n const {key} = path;\n return on(emitter, event, (changed?: Path) => {\n if (\n changed === undefined ||\n changed.key === key ||\n isDescendant(key, changed.key) ||\n (scope === 'branch' && isDescendant(changed.key, key))\n ) {\n cb();\n }\n });\n}\n\n/**\n * Subscribe to `event`, invoking `cb` only on exact key matches (or a\n * payload-less global sync). Exact-key state (errors, touched) wants no\n * ancestor/descendant matching.\n */\nexport function onKeyEvent(\n emitter: EventEmitter<FormEvents>,\n event: SubscribeEvent,\n key: string,\n cb: () => void\n): () => void {\n return on(emitter, event, (changed?: Path) => {\n if (changed === undefined || changed.key === key) cb();\n });\n}\n\n/** Events {@link subscribe} can watch. `'errors'`/`'touched'` match exact\n * keys ({@link onKeyEvent}); `'change'`/`'validating'` match by path\n * ({@link onPathEvent}); the rest are payload-less broadcasts every\n * subscriber hears. */\nexport type SubscribeEvent =\n | 'change'\n | 'errors'\n | 'touched'\n | 'validating'\n | 'submitting'\n | 'submitCount'\n | 'submitSuccessful'\n | 'disabled'\n | 'status'\n | 'loading';\n\nexport type SubscribeOptions = {\n /** Path or paths to watch; omit to receive every emission. A segments\n * path vs a name list is told apart by `trigger`'s rule: only segments\n * hold a number. */\n name?: Name | Name[];\n /** Event to watch. Defaults to `'change'`. */\n event?: SubscribeEvent;\n /** Which writes around `name` are relevant. Only meaningful for the\n * path-carrying events; defaults to `'branch'` (subscribing to `'tags'`\n * means the whole branch). */\n scope?: WatchScope;\n /** Invoked after each matching emission; read fresh state through the\n * `get*` readers inside it. */\n callback: () => void;\n};\n\n/** A segments path can hold a number (`['a', 0]`), a name list never can —\n * the same disambiguation `trigger` applies to its name argument. */\nfunction isNameList(name: Name | Name[]): name is Name[] {\n return (\n Array.isArray(name) &&\n (name as (number | unknown)[]).every(part => typeof part !== 'number')\n );\n}\n\n/**\n * Subscribe to form events imperatively — the non-render counterpart of\n * the `use*` hooks. Without `name`, `callback` fires on every emission;\n * with `name`, matching follows the event's shape (`'errors'`/`'touched'`\n * match exact keys, the rest match by path or broadcast). A name array\n * builds one subscription per path.\n */\nexport function subscribe(form: Form, options: SubscribeOptions): () => void {\n const {name, event = 'change', scope = 'branch', callback} = options;\n if (name === undefined) return on(form.emitter, event, callback);\n const names = isNameList(name) ? name : [name];\n const unsubscribes = names.map(one => {\n const path = createPath(one);\n return event === 'errors' || event === 'touched'\n ? onKeyEvent(form.emitter, event, path.key, callback)\n : onPathEvent(form.emitter, event, path, scope, callback);\n });\n return unsubscribes.length === 1\n ? unsubscribes[0]\n : () => unsubscribes.forEach(unsubscribe => unsubscribe());\n}\n\n/** The handle {@link watch} returns: a subscribe/getSnapshot pair any\n * reactive runtime can bind to. One internal listener stays alive from\n * creation, so `getSnapshot()` is always fresh; `dispose` ends it. */\nexport type WatchHandle<T> = {\n /** Read the current snapshot, cached between events; repeated reads\n * share one reference until state changes. */\n getSnapshot: () => T;\n /** Register a change listener; fires only when the projection changed.\n * With `isEqual`, an equal verdict skips the callback; without one,\n * every event wakes it. */\n subscribe: (invalidate: () => void) => () => void;\n /** Remove the internal listener and every consumer callback; the handle\n * is dead afterwards. */\n dispose: () => void;\n};\n\n/**\n * Watch a projection of form state without React — the framework-free\n * {@link useWatch} (same `isEqual` bailout), tree-shaken when unused.\n * Returns a {@link WatchHandle}: read `getSnapshot()`, re-read/re-render\n * when `subscribe`'s listener fires. The handle subscribes eagerly, so\n * reads are never stale; `getter`/`isEqual` are captured at creation.\n */\nexport function watch<T>(\n form: Form,\n event: SubscribeEvent,\n getter: () => T,\n isEqual?: (prev: T, next: T) => boolean\n): WatchHandle<T> {\n const cache: {hasValue: boolean; value?: T} = {hasValue: false};\n const consumers = new Set<() => void>();\n const wake = () => {\n if (isEqual && cache.hasValue) {\n // Custom comparator: decide before waking consumers. Equal means\n // unchanged — keep the cache and skip; unequal stores the fresh\n // snapshot so the next read needs no recompute.\n const next = getter();\n if (isEqual(cache.value as T, next)) return;\n cache.value = next;\n } else {\n cache.value = getter();\n cache.hasValue = true;\n }\n consumers.forEach(invalidate => invalidate());\n };\n // Eager: the cache tracks the form from creation, so reads are fresh\n // even before any consumer subscribes.\n const off = on(form.emitter, event, wake);\n return {\n getSnapshot: () => {\n if (!cache.hasValue) {\n cache.value = getter();\n cache.hasValue = true;\n }\n return cache.value as T;\n },\n subscribe: (invalidate: () => void) => {\n consumers.add(invalidate);\n return () => {\n consumers.delete(invalidate);\n };\n },\n dispose: () => {\n off();\n consumers.clear();\n }\n };\n}\n","/** Local persistence — `react-f0rm/persist`. Ships separately (React-free,\n * imports only the headless core) so unused persistence never enters a\n * bundle. */\nimport {getValues, setInitialValues} from './form';\nimport type {Form} from './form';\nimport {subscribe} from './subscribe';\n\n/** Storage surface {@link persistForm} needs; localStorage/sessionStorage\n * satisfy it, pass a custom object for tests/SSR. */\nexport type PersistStorage = {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n};\n\n/** Options for {@link persistForm}. */\nexport type PersistOptions = {\n /** Storage key the form snapshot lives under. */\n key: string;\n /** Where to read/write. Defaults to localStorage; without one persistence is a silent no-op. */\n storage?: PersistStorage;\n /** Transform values before serialization (e.g. strip File entries). Defaults to identity. */\n serialize?: (values: Record<string, any>) => Record<string, any>;\n /** Parse the stored string back. Defaults to JSON.parse; a throwing parse is swallowed. */\n deserialize?: (raw: string) => Record<string, any>;\n};\n\n/** Persist a form's values and hydrate them back next session. Call once\n * before interaction: hydration applies the snapshot via setInitialValues\n * (restored values become the baseline, so the form starts clean); every\n * 'change' re-writes it. Returns the unsubscribe (no-op without storage). */\nexport function persistForm(form: Form, options: PersistOptions): () => void {\n const {key} = options;\n const storage: PersistStorage | undefined =\n options.storage ??\n (typeof localStorage !== 'undefined' ? localStorage : undefined);\n if (!storage) return () => {};\n\n // Hydrate before subscribing: the restored baseline must exist before\n // the first 'change' write, so the initial snapshot persisted equals\n // the snapshot the user sees.\n const raw = storage.getItem(key);\n if (raw !== null) {\n try {\n const parse = options.deserialize ?? ((s: string) => JSON.parse(s));\n setInitialValues(form, parse(raw));\n } catch {\n // Corrupted or foreign payload under our key: ignore it, start from\n // the form's own initialValues.\n }\n }\n\n return subscribe(form, {\n event: 'change',\n callback: () => {\n const values = options.serialize\n ? options.serialize(getValues(form))\n : getValues(form);\n try {\n storage.setItem(key, JSON.stringify(values));\n } catch {\n // Quota exceeded / storage disabled: persistence is best-effort.\n }\n }\n });\n}\n"],"names":["isDescendant","key","ancestorKey","startsWith","slice","subscribe","form","options","name","event","scope","callback","on","emitter","names","Array","isArray","every","part","isNameList","unsubscribes","map","one","path","createPath","cb","changed","onKeyEvent","onPathEvent","length","forEach","unsubscribe","persistForm","storage","localStorage","raw","getItem","parse","deserialize","s","JSON","setInitialValues","values","serialize","getValues","setItem","stringify"],"mappings":"uIAgBA,SAASA,EAAaC,EAAaC,GACjC,OAAOD,EAAIE,WAAW,GAAGD,EAAYE,MAAM,GAAG,MAChD,CA6FO,SAASC,EAAUC,EAAYC,GACpC,MAAMC,KAACA,EAAAC,MAAMA,EAAQ,eAAUC,EAAQ,SAAAC,SAAUA,GAAYJ,EAC7D,QAAa,IAATC,EAAoB,OAAOI,EAAGN,EAAKO,QAASJ,EAAOE,GACvD,MAAMG,EAjBR,SAAoBN,GAClB,OACEO,MAAMC,QAAQR,IACbA,EAA8BS,MAAMC,GAAwB,iBAATA,EAExD,CAYgBC,CAAWX,GAAQA,EAAO,CAACA,GACnCY,EAAeN,EAAMO,IAAIC,IAC7B,MAAMC,EAAOC,EAAWF,GACxB,MAAiB,WAAVb,GAAgC,YAAVA,EAjE1B,SACLI,EACAJ,EACAR,EACAwB,GAEA,OAAOb,EAAGC,EAASJ,EAAQiB,SACT,IAAZA,GAAyBA,EAAQzB,MAAQA,GAAKwB,KAEtD,CAyDQE,CAAWrB,EAAKO,QAASJ,EAAOc,EAAKtB,IAAKU,GA3F3C,SACLE,EACAJ,EACAc,EACAb,EACAe,GAEA,MAAMxB,IAACA,GAAOsB,EACd,OAAOX,EAAGC,EAASJ,EAAQiB,UAEX,IAAZA,GACAA,EAAQzB,MAAQA,GAChBD,EAAaC,EAAKyB,EAAQzB,MACf,WAAVS,GAAsBV,EAAa0B,EAAQzB,IAAKA,KAEjDwB,KAGN,CA0EQG,CAAYtB,EAAKO,QAASJ,EAAOc,EAAMb,EAAOC,KAEpD,OAA+B,IAAxBS,EAAaS,OAChBT,EAAa,GACb,IAAMA,EAAaU,QAAQC,GAAeA,IAChD,CC9FO,SAASC,EAAY1B,EAAYC,GACtC,MAAMN,IAACA,GAAOM,EACR0B,EACJ1B,EAAQ0B,UACiB,oBAAjBC,aAA+BA,qBACzC,IAAKD,EAAS,MAAO,OAKrB,MAAME,EAAMF,EAAQG,QAAQnC,GAC5B,GAAY,OAARkC,EACF,IACE,MAAME,EAAQ9B,EAAQ+B,aAAA,CAAiBC,GAAcC,KAAKH,MAAME,IAChEE,EAAiBnC,EAAM+B,EAAMF,GAC/B,CAAA,MAGA,CAGF,OAAO9B,EAAUC,EAAM,CACrBG,MAAO,SACPE,SAAU,KACR,MAAM+B,EAASnC,EAAQoC,UACnBpC,EAAQoC,UAAUC,EAAUtC,IAC5BsC,EAAUtC,GACd,IACE2B,EAAQY,QAAQ5C,EAAKuC,KAAKM,UAAUJ,GACtC,CAAA,MAEA,IAGN"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";require("
|
|
1
|
+
"use strict";var r=require("../standardSchema-DINHlsYR.cjs.js");require("../errors-TzyWwBfw.cjs.js"),require("@for-fun/event-emitter"),exports.hasStandardProps=r.hasStandardProps,exports.standardSchemaFormValidator=r.schemaToFormValidator,exports.standardSchemaResolver=r.schemaToFieldValidator;
|
|
2
2
|
//# sourceMappingURL=standard-schema.cjs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"standard-schema.cjs.js","sources":[
|
|
1
|
+
{"version":3,"file":"standard-schema.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
|
@@ -1,68 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
import { Validator } from '../index.js';
|
|
1
|
+
export { I as InferSchemaValues, ah as StandardSchemaIssue, S as StandardSchemaV1, ai as hasStandardProps, aj as standardSchemaFormValidator, ak as standardSchemaResolver } from '../standard-schema-BAaTmAHh.js';
|
|
3
2
|
import '@for-fun/event-emitter';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Minimal copy of the Standard Schema v1 interfaces
|
|
7
|
-
* (https://standardschema.dev) so this module has zero runtime and type
|
|
8
|
-
* dependencies on any schema library. Implemented by zod v3.24+/v4,
|
|
9
|
-
* valibot v1, arktype and others.
|
|
10
|
-
*/
|
|
11
|
-
type StandardSchemaIssue = {
|
|
12
|
-
readonly message: string;
|
|
13
|
-
readonly path?: ReadonlyArray<PropertyKey | {
|
|
14
|
-
readonly key: PropertyKey;
|
|
15
|
-
}> | undefined;
|
|
16
|
-
};
|
|
17
|
-
type StandardSchemaV1<Input = unknown, Output = Input> = {
|
|
18
|
-
readonly '~standard': {
|
|
19
|
-
readonly version: 1;
|
|
20
|
-
readonly vendor: string;
|
|
21
|
-
readonly validate: (value: Input) => {
|
|
22
|
-
readonly value: Output;
|
|
23
|
-
readonly issues?: undefined;
|
|
24
|
-
} | {
|
|
25
|
-
readonly issues: ReadonlyArray<StandardSchemaIssue>;
|
|
26
|
-
} | Promise<{
|
|
27
|
-
readonly value: Output;
|
|
28
|
-
readonly issues?: undefined;
|
|
29
|
-
} | {
|
|
30
|
-
readonly issues: ReadonlyArray<StandardSchemaIssue>;
|
|
31
|
-
}>;
|
|
32
|
-
};
|
|
33
|
-
};
|
|
34
|
-
/**
|
|
35
|
-
* Does the schema implement the Standard Schema v1 props?
|
|
36
|
-
*/
|
|
37
|
-
declare function hasStandardProps(schema: any): schema is StandardSchemaV1;
|
|
38
|
-
/**
|
|
39
|
-
* Field-level Standard Schema adapter: validate a single value with any
|
|
40
|
-
* schema implementing '~standard' and map every issue to a FieldError,
|
|
41
|
-
* so a value breaking several rules surfaces all of them (setErrorByPath
|
|
42
|
-
* stores the array; error/errorObject readers still see the first).
|
|
43
|
-
*
|
|
44
|
-
* @param schema a Standard Schema v1 (zod v3.24+/v4, valibot v1, arktype...)
|
|
45
|
-
* @return field validator compatible with useField's validate option
|
|
46
|
-
*/
|
|
47
|
-
declare function standardSchemaResolver(schema: StandardSchemaV1): Validator;
|
|
48
|
-
/**
|
|
49
|
-
* Form-level Standard Schema adapter: validate the whole values object with
|
|
50
|
-
* any schema implementing '~standard' and return a ValidationOutcome. On
|
|
51
|
-
* failure `errors` carries the nested shape Options.validate expects
|
|
52
|
-
* ({a: {b: FieldError[]}}; ensureValidate flattens it back to per-field
|
|
53
|
-
* errors, keeping every issue of a path). Issues without a path are
|
|
54
|
-
* form-level errors and land on the FORM_ERROR key, whose value (the
|
|
55
|
-
* reserved _form field name) is exported from this library so consumers
|
|
56
|
-
* read the errors back via getError(form, FORM_ERROR). On success `values`
|
|
57
|
-
* carries the schema's parsed output (coerce/transform results included),
|
|
58
|
-
* which the form stores as its parsedValues baseline — the layer getValues
|
|
59
|
-
* reads above initialValues, mirroring how react-hook-form's zodResolver
|
|
60
|
-
* and TanStack's standardSchemaValidators use the parsed value.
|
|
61
|
-
*
|
|
62
|
-
* @param schema a Standard Schema v1 (zod v3.24+/v4, valibot v1, arktype...)
|
|
63
|
-
* @return form-level validator for createForm({validate: ...})
|
|
64
|
-
*/
|
|
65
|
-
declare function standardSchemaFormValidator<T extends Record<string, any>>(schema: StandardSchemaV1<T, any>): (values: T) => Promise<ValidationOutcome<T>>;
|
|
66
|
-
|
|
67
|
-
export { hasStandardProps, standardSchemaFormValidator, standardSchemaResolver };
|
|
68
|
-
export type { StandardSchemaIssue, StandardSchemaV1 };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
export{h as hasStandardProps,s as standardSchemaFormValidator,a as standardSchemaResolver}from"../standardSchema-5WezjHlp.mjs";import"../errors-DA4ReEd9.mjs";import"@for-fun/event-emitter";
|
|
2
2
|
//# sourceMappingURL=standard-schema.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"standard-schema.mjs","sources":[
|
|
1
|
+
{"version":3,"file":"standard-schema.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var r=require("
|
|
1
|
+
"use strict";var r=require("../standardSchema-DINHlsYR.cjs.js");require("../errors-TzyWwBfw.cjs.js"),require("@for-fun/event-emitter"),exports.yupResolver=function(e){return r.hasStandardProps(e)?r.schemaToFieldValidator(e):async r=>{try{return void await e.validate(r,{abortEarly:!1})}catch(r){return(Array.isArray(r?.inner)&&r.inner.length?r.inner:[r]).map(r=>({type:r?.type||"custom",message:r?.message||"Validation failed"}))}}};
|
|
2
2
|
//# sourceMappingURL=yup.cjs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"yup.cjs.js","sources":["../../src/resolvers/yup.ts"],"sourcesContent":["import type {FieldError} from '../form';\nimport type {Validator} from '../hooks/validate';\nimport {hasStandardProps, standardSchemaResolver} from './standard-schema';\n\nexport function yupResolver(schema: any): Validator {\n // Recent yup versions implement the Standard Schema props — prefer them.\n if (hasStandardProps(schema)) return standardSchemaResolver(schema);\n // Older yup: fall back to the throw-based validate API. abortEarly:false\n // makes yup aggregate every failure into err.inner instead of throwing\n // on the first, so all of a field's errors reach the form.\n return async (value: any) => {\n try {\n await schema.validate(value, {abortEarly: false});\n return undefined;\n } catch (err: any) {\n const issues =\n Array.isArray(err?.inner) && err.inner.length ? err.inner : [err];\n return issues.map((issue: any): FieldError => ({\n type: issue?.type || 'custom',\n message: issue?.message || 'Validation failed'\n }));\n }\n };\n}\n"],"names":["schema","hasStandardProps","standardSchemaResolver","async","value","validate","abortEarly","err","Array","isArray","inner","length","map","issue","type","message"],"mappings":"
|
|
1
|
+
{"version":3,"file":"yup.cjs.js","sources":["../../src/resolvers/yup.ts"],"sourcesContent":["import type {FieldError} from '../form';\nimport type {Validator} from '../hooks/validate';\nimport {hasStandardProps, standardSchemaResolver} from './standard-schema';\n\nexport function yupResolver(schema: any): Validator {\n // Recent yup versions implement the Standard Schema props — prefer them.\n if (hasStandardProps(schema)) return standardSchemaResolver(schema);\n // Older yup: fall back to the throw-based validate API. abortEarly:false\n // makes yup aggregate every failure into err.inner instead of throwing\n // on the first, so all of a field's errors reach the form.\n return async (value: any) => {\n try {\n await schema.validate(value, {abortEarly: false});\n return undefined;\n } catch (err: any) {\n const issues =\n Array.isArray(err?.inner) && err.inner.length ? err.inner : [err];\n return issues.map((issue: any): FieldError => ({\n type: issue?.type || 'custom',\n message: issue?.message || 'Validation failed'\n }));\n }\n };\n}\n"],"names":["schema","hasStandardProps","standardSchemaResolver","async","value","validate","abortEarly","err","Array","isArray","inner","length","map","issue","type","message"],"mappings":"2JAIO,SAAqBA,GAE1B,OAAIC,EAAAA,iBAAiBD,GAAgBE,EAAAA,uBAAuBF,GAIrDG,MAAOC,IACZ,IAEE,kBADMJ,EAAOK,SAASD,EAAO,CAACE,YAAY,GAE5C,OAASC,GAGP,OADEC,MAAMC,QAAQF,GAAKG,QAAUH,EAAIG,MAAMC,OAASJ,EAAIG,MAAQ,CAACH,IACjDK,IAAKC,IAAA,CACjBC,KAAMD,GAAOC,MAAQ,SACrBC,QAASF,GAAOE,SAAW,sBAE/B,EAEJ"}
|
package/dist/resolvers/yup.d.ts
CHANGED
package/dist/resolvers/yup.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{h as r,a}from"../standardSchema-5WezjHlp.mjs";import"../errors-DA4ReEd9.mjs";import"@for-fun/event-emitter";function e(e){return r(e)?a(e):async r=>{try{return void await e.validate(r,{abortEarly:!1})}catch(r){return(Array.isArray(r?.inner)&&r.inner.length?r.inner:[r]).map(r=>({type:r?.type||"custom",message:r?.message||"Validation failed"}))}}}export{e as yupResolver};
|
|
2
2
|
//# sourceMappingURL=yup.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"yup.mjs","sources":["../../src/resolvers/yup.ts"],"sourcesContent":["import type {FieldError} from '../form';\nimport type {Validator} from '../hooks/validate';\nimport {hasStandardProps, standardSchemaResolver} from './standard-schema';\n\nexport function yupResolver(schema: any): Validator {\n // Recent yup versions implement the Standard Schema props — prefer them.\n if (hasStandardProps(schema)) return standardSchemaResolver(schema);\n // Older yup: fall back to the throw-based validate API. abortEarly:false\n // makes yup aggregate every failure into err.inner instead of throwing\n // on the first, so all of a field's errors reach the form.\n return async (value: any) => {\n try {\n await schema.validate(value, {abortEarly: false});\n return undefined;\n } catch (err: any) {\n const issues =\n Array.isArray(err?.inner) && err.inner.length ? err.inner : [err];\n return issues.map((issue: any): FieldError => ({\n type: issue?.type || 'custom',\n message: issue?.message || 'Validation failed'\n }));\n }\n };\n}\n"],"names":["yupResolver","schema","hasStandardProps","standardSchemaResolver","async","value","validate","abortEarly","err","Array","isArray","inner","length","map","issue","type","message"],"mappings":"
|
|
1
|
+
{"version":3,"file":"yup.mjs","sources":["../../src/resolvers/yup.ts"],"sourcesContent":["import type {FieldError} from '../form';\nimport type {Validator} from '../hooks/validate';\nimport {hasStandardProps, standardSchemaResolver} from './standard-schema';\n\nexport function yupResolver(schema: any): Validator {\n // Recent yup versions implement the Standard Schema props — prefer them.\n if (hasStandardProps(schema)) return standardSchemaResolver(schema);\n // Older yup: fall back to the throw-based validate API. abortEarly:false\n // makes yup aggregate every failure into err.inner instead of throwing\n // on the first, so all of a field's errors reach the form.\n return async (value: any) => {\n try {\n await schema.validate(value, {abortEarly: false});\n return undefined;\n } catch (err: any) {\n const issues =\n Array.isArray(err?.inner) && err.inner.length ? err.inner : [err];\n return issues.map((issue: any): FieldError => ({\n type: issue?.type || 'custom',\n message: issue?.message || 'Validation failed'\n }));\n }\n };\n}\n"],"names":["yupResolver","schema","hasStandardProps","standardSchemaResolver","async","value","validate","abortEarly","err","Array","isArray","inner","length","map","issue","type","message"],"mappings":"mHAIO,SAASA,EAAYC,GAE1B,OAAIC,EAAiBD,GAAgBE,EAAuBF,GAIrDG,MAAOC,IACZ,IAEE,kBADMJ,EAAOK,SAASD,EAAO,CAACE,YAAY,GAE5C,OAASC,GAGP,OADEC,MAAMC,QAAQF,GAAKG,QAAUH,EAAIG,MAAMC,OAASJ,EAAIG,MAAQ,CAACH,IACjDK,IAAKC,IAAA,CACjBC,KAAMD,GAAOC,MAAQ,SACrBC,QAASF,GAAOE,SAAW,sBAE/B,EAEJ"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var e=require("
|
|
1
|
+
"use strict";var e=require("../standardSchema-DINHlsYR.cjs.js");function t(e){const t=e?._def?.typeName||e?._zod?.def?.type||e?.def?.type;return"string"!=typeof t?"":t.startsWith("Zod")&&t.length>3?t[3].toLowerCase()+t.slice(4):t}function n(e){return e?._def?.innerType||e?._zod?.def?.innerType||e?._zod?.innerType||e?.def?.innerType||e?._def?.getter?.()||e?.schema||("function"==typeof e?.unwrap?e.unwrap():void 0)}function r(e){if(e?.shape&&"object"==typeof e.shape)return e.shape;const t=e?._def?.shape;return"function"==typeof t?t():t&&"object"==typeof t?t:void 0}function o(e){return e?._def?.type||e?._zod?.def?.element||e?.def?.element||e?.element}require("../errors-TzyWwBfw.cjs.js"),require("@for-fun/event-emitter");const i={greater_than:"min",less_than:"max",min_length:"min",max_length:"max",length_equals:"length",string_format:"regex"};function s(e){if(!e||"object"!=typeof e)return;if("string"==typeof e.kind)return e;const t=e._zod?.def;return t&&"string"==typeof t.check?{kind:i[t.check]||t.check,value:t.value??t.minimum??t.maximum??t.length,regex:t.pattern,message:"string"==typeof t.error?t.error:void 0}:void 0}function a(e,i,f,d,c){if(!e||d.has(e))return;const u=t(e);if(d.add(e),"optional"===u||"nullable"===u||"default"===u||"lazy"===u)a(n(e),i,f||"lazy"!==u,d,c);else{const t=function(e,t,n){const r=e?._def?.checks||e?._zod?.checks;if(!Array.isArray(r)||0===r.length)return;const o={};let i=!1;for(const e of r){const t=s(e);if(t)if(i=!0,"regex"===t.kind){const e=t.regex instanceof RegExp?t.regex:"string"==typeof t.regex?new RegExp(t.regex):void 0;e&&(o.pattern={value:e,message:t.message||""})}else if("number"==typeof t.value)if("min"===t.kind||"max"===t.kind){const e=t.kind;o["number"===n?e:"min"===e?"minLength":"maxLength"]=t.value}else"length"===t.kind&&(o.minLength=o.maxLength=t.value)}return i?(t||(o.required=!0),o):void 0}(e,f,u);if(t&&i&&(c[i]=c[i]?{...c[i],...t}:t),"object"===u){const t=r(e);if(t)for(const e in t)a(t[e],i?`${i}.${e}`:e,f,d,c)}else"array"===u&&a(o(e),i,f,d,c)}d.delete(e)}function f(e,i){if(!e||i.has(e))return;const s=t(e);if("default"===s){const t=e._def?.defaultValue??e._zod?.def?.defaultValue??e?.def?.defaultValue;if(void 0!==t)return"function"==typeof t?t.call(e):t}if("optional"===s||"nullable"===s||"default"===s||"lazy"===s){i.add(e);const t=f(n(e),i);return i.delete(e),t}if("object"===s){i.add(e);const t=r(e),n={};let o=!0;if(t)for(const e in t){const r=f(t[e],i);void 0!==r&&(n[e]=r,o=!1)}return i.delete(e),o?void 0:n}if("array"===s){i.add(e);const t=f(o(e),i);return i.delete(e),void 0===t?void 0:[t]}}exports.constraintsFromSchema=function(e){const t={};return a(e,"",!1,new Set,t),t},exports.defaultsFromSchema=function(e){return f(e,new Set)??{}},exports.zodResolver=function(t){return e.hasStandardProps(t)?e.schemaToFieldValidator(t):async e=>{const n=await t.safeParseAsync(e);if(n.success)return;const{issues:r}=n.error;return r?.length?r.map(e=>({type:e?.code||"custom",message:e?.message||"Validation failed"})):[{type:"custom",message:"Validation failed"}]}};
|
|
2
2
|
//# sourceMappingURL=zod.cjs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zod.cjs.js","sources":["../../src/resolvers/zod.ts"],"sourcesContent":["import type {FieldError} from '../form';\nimport type {Validator} from '../hooks/validate';\nimport {hasStandardProps, standardSchemaResolver} from './standard-schema';\n\nexport function zodResolver(schema: any): Validator {\n // zod v3.24+/v4 schemas carry the Standard Schema props — prefer them.\n if (hasStandardProps(schema)) return standardSchemaResolver(schema);\n // Older zod: fall back to the legacy safeParseAsync API. It aggregates\n // every issue (no abortEarly), so map them all — a value breaking\n // several rules surfaces all of its errors.\n return async (value: any) => {\n const result = await schema.safeParseAsync(value);\n if (result.success) return undefined;\n const {issues} = result.error;\n if (!issues?.length) {\n return [{type: 'custom', message: 'Validation failed'}];\n }\n return issues.map((issue: any): FieldError => ({\n type: issue?.code || 'custom',\n message: issue?.message || 'Validation failed'\n }));\n };\n}\n"],"names":["schema","hasStandardProps","standardSchemaResolver","async","value","result","safeParseAsync","success","issues","error","length","map","issue","type","code","message"],"mappings":"kJAIO,SAAqBA,GAE1B,OAAIC,EAAAA,iBAAiBD,GAAgBE,EAAAA,uBAAuBF,GAIrDG,MAAOC,IACZ,MAAMC,QAAeL,EAAOM,eAAeF,GAC3C,GAAIC,EAAOE,QAAS,OACpB,MAAMC,OAACA,GAAUH,EAAOI,MACxB,OAAKD,GAAQE,OAGNF,EAAOG,IAAKC,IAAA,CACjBC,KAAMD,GAAOE,MAAQ,SACrBC,QAASH,GAAOG,SAAW,uBAJpB,CAAC,CAACF,KAAM,SAAUE,QAAS,sBAOxC"}
|
|
1
|
+
{"version":3,"file":"zod.cjs.js","sources":["../../src/resolvers/zod.ts"],"sourcesContent":["import type {FieldError} from '../form';\nimport type {Validator} from '../hooks/validate';\nimport type {FieldRules} from '../rules';\nimport {hasStandardProps, standardSchemaResolver} from './standard-schema';\n\n/** The {@link FieldRules} subset a zod schema can describe declaratively. */\nexport type SchemaConstraints = Pick<\n FieldRules,\n 'required' | 'min' | 'max' | 'minLength' | 'maxLength' | 'pattern'\n>;\n\nexport function zodResolver(schema: any): Validator {\n // zod v3.24+/v4 schemas carry the Standard Schema props — prefer them.\n if (hasStandardProps(schema)) return standardSchemaResolver(schema);\n // Older zod: fall back to the legacy safeParseAsync API. It aggregates\n // every issue (no abortEarly), so map them all — a value breaking\n // several rules surfaces all of its errors.\n return async (value: any) => {\n const result = await schema.safeParseAsync(value);\n if (result.success) return undefined;\n const {issues} = result.error;\n if (!issues?.length) {\n return [{type: 'custom', message: 'Validation failed'}];\n }\n return issues.map((issue: any): FieldError => ({\n type: issue?.code || 'custom',\n message: issue?.message || 'Validation failed'\n }));\n };\n}\n\n// ---------------------------------------------------------------------------\n// Schema introspection — zod stays an optional peer, so everything below is\n// duck-typed over both generations' internal shapes (best effort: nodes we\n// fail to recognize are silently skipped, never guessed at, never thrown).\n//\n// v3: `_def.typeName` ('ZodString'), `_def.checks` (`{kind: 'min'|'max'|\n// 'length'|'regex', value|regex}`), `_def.innerType`, `_def.shape()`,\n// `_def.type` (array element), `_def.defaultValue()`\n// v4: `def.type` / `_zod.def.type` ('string'), `_zod.checks` (`{_zod:\n// {def: {check: 'greater_than'|'less_than'|'min_length'|'max_length'|\n// 'length_equals'|'string_format', value|minimum|maximum|length|\n// pattern}}}`), `def.innerType`, `def.shape`, `def.element`,\n// `def.defaultValue` (getter or plain value)\n// ---------------------------------------------------------------------------\n\n/** Normalized node type: v3 'ZodString' and v4 'string' both become\n * 'string'; `''` when the shape is unrecognized. */\nfunction zodType(node: any): string {\n const t = node?._def?.typeName || node?._zod?.def?.type || node?.def?.type;\n return typeof t !== 'string'\n ? ''\n : t.startsWith('Zod') && t.length > 3\n ? t[3].toLowerCase() + t.slice(4)\n : t;\n}\n\n/** Inner schema of wrapping nodes (optional/nullable/default/lazy): v3\n * `_def.innerType`/`_def.getter()`, v4 `def.innerType`, `.unwrap()` last. */\nfunction unwrap(node: any): any {\n return (\n node?._def?.innerType ||\n node?._zod?.def?.innerType ||\n node?._zod?.innerType ||\n node?.def?.innerType ||\n node?._def?.getter?.() ||\n node?.schema ||\n (typeof node?.unwrap === 'function' ? node.unwrap() : undefined)\n );\n}\n\n/** Shape record of a ZodObject: `.shape` (v3/v4 getter) or v3's\n * `_def.shape()` / plain `_def.shape`. */\nfunction shapeOf(node: any): Record<string, any> | undefined {\n if (node?.shape && typeof node.shape === 'object') return node.shape;\n const shape = node?._def?.shape;\n if (typeof shape === 'function') return shape();\n return shape && typeof shape === 'object' ? shape : undefined;\n}\n\n/** Element schema of a ZodArray: v3 `_def.type`, v4 `def.element`. */\nfunction elementOf(node: any): any {\n return (\n node?._def?.type ||\n node?._zod?.def?.element ||\n node?.def?.element ||\n node?.element\n );\n}\n\n/** v4 check ids translated to the v3-flavoured kinds the leaf mapper\n * understands (numbers and lengths both resolve to min/max per node type —\n * zod itself guarantees which kind lands on which type). */\nconst CHECK_KINDS: Record<string, string> = {\n greater_than: 'min',\n less_than: 'max',\n min_length: 'min',\n max_length: 'max',\n length_equals: 'length',\n string_format: 'regex'\n};\n\n/** One normalized check `{kind, value, regex, message}`: a v3 entry\n * (`_def.checks[i]`, already `{kind, …}`) passes through; a v4 entry\n * (`_zod.checks[i]._zod.def`) is translated. Unrecognized → undefined. */\nfunction checkOf(check: any): any {\n if (!check || typeof check !== 'object') return undefined;\n if (typeof check.kind === 'string') return check;\n const def = check._zod?.def;\n if (!def || typeof def.check !== 'string') return undefined;\n return {\n kind: CHECK_KINDS[def.check] || def.check,\n value: def.value ?? def.minimum ?? def.maximum ?? def.length,\n regex: def.pattern,\n message: typeof def.error === 'string' ? def.error : undefined\n };\n}\n\n/** The constraints one node contributes at its own path. `required: true`\n * only for non-optional nodes carrying at least one check — a bare\n * `z.string()` adds nothing, so `required` is not sprayed over the tree;\n * unmappable checks (email/int/multiple_of…) still count as \"checked\". */\nfunction leafConstraints(\n node: any,\n optional: boolean,\n type: string\n): SchemaConstraints | undefined {\n const raw = node?._def?.checks || node?._zod?.checks;\n if (!Array.isArray(raw) || raw.length === 0) return undefined;\n const constraints: SchemaConstraints = {};\n let hasCheck = false;\n for (const entry of raw) {\n const check = checkOf(entry);\n if (!check) continue;\n hasCheck = true;\n if (check.kind === 'regex') {\n const regex =\n check.regex instanceof RegExp\n ? check.regex\n : typeof check.regex === 'string'\n ? new RegExp(check.regex)\n : undefined;\n if (regex) {\n constraints.pattern = {value: regex, message: check.message || ''};\n }\n } else if (typeof check.value === 'number') {\n if (check.kind === 'min' || check.kind === 'max') {\n const kind: 'min' | 'max' = check.kind;\n constraints[\n type === 'number' ? kind : kind === 'min' ? 'minLength' : 'maxLength'\n ] = check.value;\n } else if (check.kind === 'length') {\n constraints.minLength = constraints.maxLength = check.value;\n }\n }\n }\n if (!hasCheck) return undefined;\n if (!optional) constraints.required = true;\n return constraints;\n}\n\n/**\n * Read declarative {@link FieldRules} constraints out of a zod v3/v4 object\n * schema (best effort — see the shape notes above), so form builders don't\n * re-declare rules next to the schema. Returns a record keyed by dotted\n * field path (`'user.name'`, matching `useErrors` keys): string/array\n * checks become minLength/maxLength, number checks min/max, regex checks\n * `pattern` (carrying the check's message when the schema declares one).\n * Array elements recurse onto the de-indexed path (`'items.name'`) — one\n * schema row stands for every index. Optional/nullable/default-wrapped\n * fields keep their bounds but never get `required`; bare nodes without\n * checks and unrecognized shapes are silently skipped. Pure, never throws,\n * circular references terminate.\n */\nexport function constraintsFromSchema(\n schema: any\n): Record<string, SchemaConstraints> {\n const out: Record<string, SchemaConstraints> = {};\n collectConstraints(schema, '', false, new Set(), out);\n return out;\n}\n\n/** Depth walk for {@link constraintsFromSchema}; `seen` holds the current\n * recursion stack only (delete on exit), so shared subschema instances used\n * at several paths still walk everywhere. */\nfunction collectConstraints(\n node: any,\n path: string,\n optional: boolean,\n seen: Set<any>,\n out: Record<string, SchemaConstraints>\n): void {\n if (!node || seen.has(node)) return;\n const type = zodType(node);\n seen.add(node);\n if (\n type === 'optional' ||\n type === 'nullable' ||\n type === 'default' ||\n type === 'lazy'\n ) {\n collectConstraints(\n unwrap(node),\n path,\n optional || type !== 'lazy',\n seen,\n out\n );\n } else {\n const constraints = leafConstraints(node, optional, type);\n if (constraints && path) {\n out[path] = out[path] ? {...out[path], ...constraints} : constraints;\n }\n if (type === 'object') {\n const shape = shapeOf(node);\n if (shape) {\n for (const key in shape) {\n collectConstraints(\n shape[key],\n path ? `${path}.${key}` : key,\n optional,\n seen,\n out\n );\n }\n }\n } else if (type === 'array') {\n // The element's constraints land on the de-indexed path: the schema\n // describes one row, not the indices of a live array.\n collectConstraints(elementOf(node), path, optional, seen, out);\n }\n }\n seen.delete(node);\n}\n\n/**\n * Collect `z.default(...)` values out of a zod v3/v4 object schema (best\n * effort) as a nested values tree ready for `useForm({initialValues})` /\n * `setInitialValues`. Keys with no default anywhere below are omitted; an\n * array without its own default seeds a single row from its element's\n * defaults (`{items: [{qty: 1}]}`) — more rows are the user's to add.\n * A wrapper's own default is taken literally (v3 `defaultValue()`, v4\n * `def.defaultValue` as getter or value). Pure, never throws, circular\n * references terminate.\n */\nexport function defaultsFromSchema(schema: any): Record<string, any> {\n return (collectDefaults(schema, new Set()) ?? {}) as Record<string, any>;\n}\n\n/** Depth walk for {@link defaultsFromSchema}; same stack-only `seen`\n * discipline as {@link collectConstraints}. */\nfunction collectDefaults(node: any, seen: Set<any>): any {\n if (!node || seen.has(node)) return undefined;\n const type = zodType(node);\n if (type === 'default') {\n const dv =\n node._def?.defaultValue ??\n node._zod?.def?.defaultValue ??\n node?.def?.defaultValue;\n if (dv !== undefined) return typeof dv === 'function' ? dv.call(node) : dv;\n }\n if (\n type === 'optional' ||\n type === 'nullable' ||\n type === 'default' ||\n type === 'lazy'\n ) {\n seen.add(node);\n const value = collectDefaults(unwrap(node), seen);\n seen.delete(node);\n return value;\n }\n if (type === 'object') {\n seen.add(node);\n const shape = shapeOf(node);\n const values: Record<string, any> = {};\n let empty = true;\n if (shape) {\n for (const key in shape) {\n const value = collectDefaults(shape[key], seen);\n if (value !== undefined) {\n values[key] = value;\n empty = false;\n }\n }\n }\n seen.delete(node);\n return empty ? undefined : values;\n }\n if (type === 'array') {\n seen.add(node);\n const row = collectDefaults(elementOf(node), seen);\n seen.delete(node);\n return row === undefined ? undefined : [row];\n }\n return undefined;\n}\n"],"names":["zodType","node","t","_def","typeName","_zod","def","type","startsWith","length","toLowerCase","slice","unwrap","innerType","getter","schema","shapeOf","shape","elementOf","element","CHECK_KINDS","greater_than","less_than","min_length","max_length","length_equals","string_format","checkOf","check","kind","value","minimum","maximum","regex","pattern","message","error","collectConstraints","path","optional","seen","out","has","add","constraints","raw","checks","Array","isArray","hasCheck","entry","RegExp","minLength","maxLength","required","leafConstraints","key","delete","collectDefaults","dv","defaultValue","call","values","empty","row","Set","hasStandardProps","standardSchemaResolver","async","result","safeParseAsync","success","issues","map","issue","code"],"mappings":"gEAgDA,SAASA,EAAQC,GACf,MAAMC,EAAID,GAAME,MAAMC,UAAYH,GAAMI,MAAMC,KAAKC,MAAQN,GAAMK,KAAKC,KACtE,MAAoB,iBAANL,EACV,GACAA,EAAEM,WAAW,QAAUN,EAAEO,OAAS,EAChCP,EAAE,GAAGQ,cAAgBR,EAAES,MAAM,GAC7BT,CACR,CAIA,SAASU,EAAOX,GACd,OACEA,GAAME,MAAMU,WACZZ,GAAMI,MAAMC,KAAKO,WACjBZ,GAAMI,MAAMQ,WACZZ,GAAMK,KAAKO,WACXZ,GAAME,MAAMW,YACZb,GAAMc,SACmB,mBAAjBd,GAAMW,OAAwBX,EAAKW,cAAW,EAE1D,CAIA,SAASI,EAAQf,GACf,GAAIA,GAAMgB,OAA+B,iBAAfhB,EAAKgB,aAA2BhB,EAAKgB,MAC/D,MAAMA,EAAQhB,GAAME,MAAMc,MAC1B,MAAqB,mBAAVA,EAA6BA,IACjCA,GAA0B,iBAAVA,EAAqBA,OAAQ,CACtD,CAGA,SAASC,EAAUjB,GACjB,OACEA,GAAME,MAAMI,MACZN,GAAMI,MAAMC,KAAKa,SACjBlB,GAAMK,KAAKa,SACXlB,GAAMkB,OAEV,wEAKA,MAAMC,EAAsC,CAC1CC,aAAc,MACdC,UAAW,MACXC,WAAY,MACZC,WAAY,MACZC,cAAe,SACfC,cAAe,SAMjB,SAASC,EAAQC,GACf,IAAKA,GAA0B,iBAAVA,EAAoB,OACzC,GAA0B,iBAAfA,EAAMC,KAAmB,OAAOD,EAC3C,MAAMtB,EAAMsB,EAAMvB,MAAMC,IACxB,OAAKA,GAA4B,iBAAdA,EAAIsB,MAChB,CACLC,KAAMT,EAAYd,EAAIsB,QAAUtB,EAAIsB,MACpCE,MAAOxB,EAAIwB,OAASxB,EAAIyB,SAAWzB,EAAI0B,SAAW1B,EAAIG,OACtDwB,MAAO3B,EAAI4B,QACXC,QAA8B,iBAAd7B,EAAI8B,MAAqB9B,EAAI8B,WAAQ,QALvD,CAOF,CAqEA,SAASC,EACPpC,EACAqC,EACAC,EACAC,EACAC,GAEA,IAAKxC,GAAQuC,EAAKE,IAAIzC,GAAO,OAC7B,MAAMM,EAAOP,EAAQC,GAErB,GADAuC,EAAKG,IAAI1C,GAEE,aAATM,GACS,aAATA,GACS,YAATA,GACS,SAATA,EAEA8B,EACEzB,EAAOX,GACPqC,EACAC,GAAqB,SAAThC,EACZiC,EACAC,OAEG,CACL,MAAMG,EAvFV,SACE3C,EACAsC,EACAhC,GAEA,MAAMsC,EAAM5C,GAAME,MAAM2C,QAAU7C,GAAMI,MAAMyC,OAC9C,IAAKC,MAAMC,QAAQH,IAAuB,IAAfA,EAAIpC,OAAc,OAC7C,MAAMmC,EAAiC,CAAA,EACvC,IAAIK,GAAW,EACf,IAAA,MAAWC,KAASL,EAAK,CACvB,MAAMjB,EAAQD,EAAQuB,GACtB,GAAKtB,EAEL,GADAqB,GAAW,EACQ,UAAfrB,EAAMC,KAAkB,CAC1B,MAAMI,EACJL,EAAMK,iBAAiBkB,OACnBvB,EAAMK,MACiB,iBAAhBL,EAAMK,MACX,IAAIkB,OAAOvB,EAAMK,YACjB,EACJA,IACFW,EAAYV,QAAU,CAACJ,MAAOG,EAAOE,QAASP,EAAMO,SAAW,IAEnE,MAAA,GAAkC,iBAAhBP,EAAME,MACtB,GAAmB,QAAfF,EAAMC,MAAiC,QAAfD,EAAMC,KAAgB,CAChD,MAAMA,EAAsBD,EAAMC,KAClCe,EACW,WAATrC,EAAoBsB,EAAgB,QAATA,EAAiB,YAAc,aACxDD,EAAME,KACZ,KAA0B,WAAfF,EAAMC,OACfe,EAAYQ,UAAYR,EAAYS,UAAYzB,EAAME,MAG5D,CACA,OAAKmB,GACAV,IAAUK,EAAYU,UAAW,GAC/BV,QAFP,CAGF,CAkDwBW,CAAgBtD,EAAMsC,EAAUhC,GAIpD,GAHIqC,GAAeN,IACjBG,EAAIH,GAAQG,EAAIH,GAAQ,IAAIG,EAAIH,MAAUM,GAAeA,GAE9C,WAATrC,EAAmB,CACrB,MAAMU,EAAQD,EAAQf,GACtB,GAAIgB,EACF,IAAA,MAAWuC,KAAOvC,EAChBoB,EACEpB,EAAMuC,GACNlB,EAAO,GAAGA,KAAQkB,IAAQA,EAC1BjB,EACAC,EACAC,EAIR,KAAoB,UAATlC,GAGT8B,EAAmBnB,EAAUjB,GAAOqC,EAAMC,EAAUC,EAAMC,EAE9D,CACAD,EAAKiB,OAAOxD,EACd,CAkBA,SAASyD,EAAgBzD,EAAWuC,GAClC,IAAKvC,GAAQuC,EAAKE,IAAIzC,GAAO,OAC7B,MAAMM,EAAOP,EAAQC,GACrB,GAAa,YAATM,EAAoB,CACtB,MAAMoD,EACJ1D,EAAKE,MAAMyD,cACX3D,EAAKI,MAAMC,KAAKsD,cAChB3D,GAAMK,KAAKsD,aACb,YAAID,EAAkB,MAAqB,mBAAPA,EAAoBA,EAAGE,KAAK5D,GAAQ0D,CAC1E,CACA,GACW,aAATpD,GACS,aAATA,GACS,YAATA,GACS,SAATA,EACA,CACAiC,EAAKG,IAAI1C,GACT,MAAM6B,EAAQ4B,EAAgB9C,EAAOX,GAAOuC,GAE5C,OADAA,EAAKiB,OAAOxD,GACL6B,CACT,CACA,GAAa,WAATvB,EAAmB,CACrBiC,EAAKG,IAAI1C,GACT,MAAMgB,EAAQD,EAAQf,GAChB6D,EAA8B,CAAA,EACpC,IAAIC,GAAQ,EACZ,GAAI9C,EACF,IAAA,MAAWuC,KAAOvC,EAAO,CACvB,MAAMa,EAAQ4B,EAAgBzC,EAAMuC,GAAMhB,QAC5B,IAAVV,IACFgC,EAAON,GAAO1B,EACdiC,GAAQ,EAEZ,CAGF,OADAvB,EAAKiB,OAAOxD,GACL8D,OAAQ,EAAYD,CAC7B,CACA,GAAa,UAATvD,EAAkB,CACpBiC,EAAKG,IAAI1C,GACT,MAAM+D,EAAMN,EAAgBxC,EAAUjB,GAAOuC,GAE7C,OADAA,EAAKiB,OAAOxD,QACG,IAAR+D,OAAoB,EAAY,CAACA,EAC1C,CAEF,+BA1HO,SACLjD,GAEA,MAAM0B,EAAyC,CAAA,EAE/C,OADAJ,EAAmBtB,EAAQ,IAAI,EAAO,IAAIkD,IAAOxB,GAC1CA,CACT,6BAiEO,SAA4B1B,GACjC,OAAQ2C,EAAgB3C,EAAQ,IAAIkD,MAAU,CAAA,CAChD,sBA5OO,SAAqBlD,GAE1B,OAAImD,EAAAA,iBAAiBnD,GAAgBoD,EAAAA,uBAAuBpD,GAIrDqD,MAAOtC,IACZ,MAAMuC,QAAetD,EAAOuD,eAAexC,GAC3C,GAAIuC,EAAOE,QAAS,OACpB,MAAMC,OAACA,GAAUH,EAAOjC,MACxB,OAAKoC,GAAQ/D,OAGN+D,EAAOC,IAAKC,IAAA,CACjBnE,KAAMmE,GAAOC,MAAQ,SACrBxC,QAASuC,GAAOvC,SAAW,uBAJpB,CAAC,CAAC5B,KAAM,SAAU4B,QAAS,sBAOxC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { FieldRules, Validator } from '../index.js';
|
|
2
|
+
import '@for-fun/event-emitter';
|
|
3
|
+
|
|
4
|
+
/** The {@link FieldRules} subset a zod schema can describe declaratively. */
|
|
5
|
+
type SchemaConstraints = Pick<FieldRules, 'required' | 'min' | 'max' | 'minLength' | 'maxLength' | 'pattern'>;
|
|
6
|
+
declare function zodResolver(schema: any): Validator;
|
|
7
|
+
/**
|
|
8
|
+
* Read declarative {@link FieldRules} constraints out of a zod v3/v4 object
|
|
9
|
+
* schema (best effort — see the shape notes above), so form builders don't
|
|
10
|
+
* re-declare rules next to the schema. Returns a record keyed by dotted
|
|
11
|
+
* field path (`'user.name'`, matching `useErrors` keys): string/array
|
|
12
|
+
* checks become minLength/maxLength, number checks min/max, regex checks
|
|
13
|
+
* `pattern` (carrying the check's message when the schema declares one).
|
|
14
|
+
* Array elements recurse onto the de-indexed path (`'items.name'`) — one
|
|
15
|
+
* schema row stands for every index. Optional/nullable/default-wrapped
|
|
16
|
+
* fields keep their bounds but never get `required`; bare nodes without
|
|
17
|
+
* checks and unrecognized shapes are silently skipped. Pure, never throws,
|
|
18
|
+
* circular references terminate.
|
|
19
|
+
*/
|
|
20
|
+
declare function constraintsFromSchema(schema: any): Record<string, SchemaConstraints>;
|
|
21
|
+
/**
|
|
22
|
+
* Collect `z.default(...)` values out of a zod v3/v4 object schema (best
|
|
23
|
+
* effort) as a nested values tree ready for `useForm({initialValues})` /
|
|
24
|
+
* `setInitialValues`. Keys with no default anywhere below are omitted; an
|
|
25
|
+
* array without its own default seeds a single row from its element's
|
|
26
|
+
* defaults (`{items: [{qty: 1}]}`) — more rows are the user's to add.
|
|
27
|
+
* A wrapper's own default is taken literally (v3 `defaultValue()`, v4
|
|
28
|
+
* `def.defaultValue` as getter or value). Pure, never throws, circular
|
|
29
|
+
* references terminate.
|
|
30
|
+
*/
|
|
31
|
+
declare function defaultsFromSchema(schema: any): Record<string, any>;
|
|
32
|
+
|
|
33
|
+
export { constraintsFromSchema, defaultsFromSchema, zodResolver };
|
|
34
|
+
export type { SchemaConstraints };
|