@vielzeug/forge 1.1.2 → 1.2.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/dist/devtools.cjs CHANGED
@@ -1,2 +1,2 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./_utils.cjs");let t=require("@vielzeug/arsenal");var n=!globalThis.__FORGE_PROD__;function r(r,i={}){if(!n)return()=>{};let a=`[forge:devtools:${i.label??`form`}]`,o=new Map,s=r.state.isSubmitting,c=r.state.isLoading;function l(e){let n=new Set(o.keys());for(let e of Object.keys((0,t.flattenPaths)(r.values())))n.add(e);for(let t of Object.keys(e.errors))n.add(t);for(let t of e.touchedFields)n.add(t);return n}function u(t){for(let n of l(t)){let t=r.field(n),i=o.get(n);if(o.set(n,t),!i)continue;let s=e.sanitizeForLog(n,80);i.value!==t.value&&console.debug(`${a} field "${s}" value:`,i.value,`→`,t.value),i.error!==t.error&&console.debug(`${a} field "${s}" error:`,i.error,`→`,t.error),i.touched!==t.touched&&console.debug(`${a} field "${s}" touched:`,t.touched),i.dirty!==t.dirty&&console.debug(`${a} field "${s}" dirty:`,t.dirty)}}return r.subscribe(e=>{u(e),e.isSubmitting!==s&&(s=e.isSubmitting,console.debug(`${a} submit ${e.isSubmitting?`started`:`ended`} (submitCount=${e.submitCount})`)),e.isLoading!==c&&(c=e.isLoading,console.debug(`${a} isLoading:`,e.isLoading))},{sync:!0})}exports.attachForgeDevtools=r;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./_utils.cjs");let t=require("@vielzeug/arsenal");var n=!globalThis.__FORGE_PROD__;function r(r,i={}){if(!n)return()=>{};let a=`[forge:devtools:${i.label??`form`}]`,o=new Map,s=r.state.isSubmitting,c=r.state.isLoading;function l(e){let n=new Set(o.keys());for(let e of Object.keys((0,t.flattenPaths)(r.values())))n.add(e);for(let t of Object.keys(e.errors))n.add(t);for(let t of e.touchedFields)n.add(t);return n}function u(t){for(let n of l(t)){let t=r.field(n),i=o.get(n);if(o.set(n,t),!i)continue;let s=e.sanitizeForLog(n,80);i.value!==t.value&&console.debug(`${a} field "${s}" value:`,i.value,`→`,t.value),i.error!==t.error&&console.debug(`${a} field "${s}" error:`,i.error,`→`,t.error),i.touched!==t.touched&&console.debug(`${a} field "${s}" touched:`,t.touched),i.dirty!==t.dirty&&console.debug(`${a} field "${s}" dirty:`,t.dirty)}}return r.subscribe(e=>{u(e),e.isSubmitting!==s&&(s=e.isSubmitting,console.debug(`${a} submit ${e.isSubmitting?`started`:`ended`} (submitCount=${e.submitCount})`)),e.isLoading!==c&&(c=e.isLoading,console.debug(`${a} isLoading:`,e.isLoading))},{sync:!0})}exports.debugForm=r;
2
2
  //# sourceMappingURL=devtools.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"devtools.cjs","names":[],"sources":["../src/devtools.ts"],"sourcesContent":["/**\n * @vielzeug/forge — debug utilities for form state visualisation.\n *\n * Import from the dedicated sub-path so it is tree-shaken from production bundles:\n * ```ts\n * import { attachForgeDevtools } from '@vielzeug/forge/devtools';\n * ```\n */\n\nimport type { FieldState, FlatKeyOf, Form, FormState, Unsubscribe } from './types';\n\nimport { flattenValues, sanitizeForLog } from './_utils';\n\nconst isDev = !(globalThis as { __FORGE_PROD__?: boolean }).__FORGE_PROD__;\n\n/** Options for {@link attachForgeDevtools}. */\nexport type ForgeDevtoolsOptions = {\n /** Label included in every log line, useful when debugging multiple forms at once. Default: `\"form\"`. */\n label?: string;\n};\n\n/**\n * Attaches a `console.debug`-based observer to `form` that logs a distinct line per observable\n * state transition: per-field value/error/touched/dirty changes, submit start/end (via the\n * `isSubmitting` edge), and `isLoading` edge (async `defaultValues` resolving).\n *\n * **Development only** by default — logging is a no-op when `__FORGE_PROD__` is set (the same\n * convention `_dev.ts` uses internally). Import from the dedicated `/devtools` sub-path so the\n * logging code is tree-shaken from production bundles entirely.\n *\n * Works identically on scoped sub-forms (`form.scope('address')`) — field paths logged are\n * relative to whatever `form` object is passed in, matching that form's own `state` convention.\n *\n * @example\n * ```ts\n * import { createForm } from '@vielzeug/forge';\n * import { attachForgeDevtools } from '@vielzeug/forge/devtools';\n *\n * const form = createForm({ defaultValues: { email: '' } });\n * const detach = attachForgeDevtools(form, { label: 'signup' });\n * // [forge:devtools:signup] field \"email\" value: \"\" → \"a@b.com\"\n *\n * detach(); // stop logging\n * ```\n */\nexport function attachForgeDevtools<TValues extends Record<string, unknown>>(\n form: Form<TValues>,\n options: ForgeDevtoolsOptions = {},\n): Unsubscribe {\n if (!isDev) return () => {};\n\n const prefix = `[forge:devtools:${options.label ?? 'form'}]`;\n const fieldSnapshots = new Map<string, FieldState<unknown>>();\n\n let prevIsSubmitting = form.state.isSubmitting;\n let prevIsLoading = form.state.isLoading;\n\n function knownFieldKeys(state: FormState): Set<string> {\n const keys = new Set<string>(fieldSnapshots.keys());\n\n for (const key of Object.keys(flattenValues(form.values() as Record<string, unknown>))) keys.add(key);\n for (const key of Object.keys(state.errors)) keys.add(key);\n for (const key of state.touchedFields) keys.add(key);\n\n return keys;\n }\n\n function logFieldDiffs(state: FormState): void {\n for (const key of knownFieldKeys(state)) {\n const next = form.field(key as FlatKeyOf<TValues>);\n const prev = fieldSnapshots.get(key);\n\n fieldSnapshots.set(key, next);\n\n // First observation of this field establishes a baseline — logging it would just be\n // noise (every field would \"change\" from undefined on attach).\n if (!prev) continue;\n\n // Security: `key` is a caller-chosen field path (assertSafeKey only rejects reserved\n // segments, not control characters) — sanitize before interpolating into console output\n // to prevent terminal-escape-sequence injection, mirroring the sanitization `warn()`\n // call sites already apply (see `_utils.ts` sanitizeForLog).\n const safeKey = sanitizeForLog(key, 80);\n\n if (prev.value !== next.value) console.debug(`${prefix} field \"${safeKey}\" value:`, prev.value, '→', next.value);\n\n if (prev.error !== next.error) console.debug(`${prefix} field \"${safeKey}\" error:`, prev.error, '→', next.error);\n\n if (prev.touched !== next.touched) console.debug(`${prefix} field \"${safeKey}\" touched:`, next.touched);\n\n if (prev.dirty !== next.dirty) console.debug(`${prefix} field \"${safeKey}\" dirty:`, next.dirty);\n }\n }\n\n return form.subscribe(\n (state) => {\n logFieldDiffs(state);\n\n if (state.isSubmitting !== prevIsSubmitting) {\n prevIsSubmitting = state.isSubmitting;\n console.debug(\n `${prefix} submit ${state.isSubmitting ? 'started' : 'ended'} (submitCount=${state.submitCount})`,\n );\n }\n\n if (state.isLoading !== prevIsLoading) {\n prevIsLoading = state.isLoading;\n console.debug(`${prefix} isLoading:`, state.isLoading);\n }\n },\n { sync: true },\n );\n}\n"],"mappings":"sIAaA,IAAM,EAAQ,CAAE,WAA4C,eAgC5D,SAAgB,EACd,EACA,EAAgC,CAAC,EACpB,CACb,GAAI,CAAC,EAAO,UAAa,CAAC,EAE1B,IAAM,EAAS,mBAAmB,EAAQ,OAAS,OAAO,GACpD,EAAiB,IAAI,IAEvB,EAAmB,EAAK,MAAM,aAC9B,EAAgB,EAAK,MAAM,UAE/B,SAAS,EAAe,EAA+B,CACrD,IAAM,EAAO,IAAI,IAAY,EAAe,KAAK,CAAC,EAElD,IAAK,IAAM,KAAO,OAAO,MAAA,EAAA,EAAA,aAAA,CAAmB,EAAK,OAAO,CAA4B,CAAC,EAAG,EAAK,IAAI,CAAG,EACpG,IAAK,IAAM,KAAO,OAAO,KAAK,EAAM,MAAM,EAAG,EAAK,IAAI,CAAG,EACzD,IAAK,IAAM,KAAO,EAAM,cAAe,EAAK,IAAI,CAAG,EAEnD,OAAO,CACT,CAEA,SAAS,EAAc,EAAwB,CAC7C,IAAK,IAAM,KAAO,EAAe,CAAK,EAAG,CACvC,IAAM,EAAO,EAAK,MAAM,CAAyB,EAC3C,EAAO,EAAe,IAAI,CAAG,EAMnC,GAJA,EAAe,IAAI,EAAK,CAAI,EAIxB,CAAC,EAAM,SAMX,IAAM,EAAU,EAAA,eAAe,EAAK,EAAE,EAElC,EAAK,QAAU,EAAK,OAAO,QAAQ,MAAM,GAAG,EAAO,UAAU,EAAQ,UAAW,EAAK,MAAO,IAAK,EAAK,KAAK,EAE3G,EAAK,QAAU,EAAK,OAAO,QAAQ,MAAM,GAAG,EAAO,UAAU,EAAQ,UAAW,EAAK,MAAO,IAAK,EAAK,KAAK,EAE3G,EAAK,UAAY,EAAK,SAAS,QAAQ,MAAM,GAAG,EAAO,UAAU,EAAQ,YAAa,EAAK,OAAO,EAElG,EAAK,QAAU,EAAK,OAAO,QAAQ,MAAM,GAAG,EAAO,UAAU,EAAQ,UAAW,EAAK,KAAK,CAChG,CACF,CAEA,OAAO,EAAK,UACT,GAAU,CACT,EAAc,CAAK,EAEf,EAAM,eAAiB,IACzB,EAAmB,EAAM,aACzB,QAAQ,MACN,GAAG,EAAO,UAAU,EAAM,aAAe,UAAY,QAAQ,gBAAgB,EAAM,YAAY,EACjG,GAGE,EAAM,YAAc,IACtB,EAAgB,EAAM,UACtB,QAAQ,MAAM,GAAG,EAAO,aAAc,EAAM,SAAS,EAEzD,EACA,CAAE,KAAM,EAAK,CACf,CACF"}
1
+ {"version":3,"file":"devtools.cjs","names":[],"sources":["../src/devtools.ts"],"sourcesContent":["/**\n * @vielzeug/forge — debug utilities for form state visualisation.\n *\n * Import from the dedicated sub-path so it is tree-shaken from production bundles:\n * ```ts\n * import { debugForm } from '@vielzeug/forge/devtools';\n * ```\n */\n\nimport type { FieldState, FlatKeyOf, Form, FormState, Unsubscribe } from './types';\n\nimport { flattenValues, sanitizeForLog } from './_utils';\n\nconst isDev = !(globalThis as { __FORGE_PROD__?: boolean }).__FORGE_PROD__;\n\n/** Options for {@link debugForm}. */\nexport type ForgeDevtoolsOptions = {\n /** Label included in every log line, useful when debugging multiple forms at once. Default: `\"form\"`. */\n label?: string;\n};\n\n/**\n * Attaches a `console.debug`-based observer to `form` that logs a distinct line per observable\n * state transition: per-field value/error/touched/dirty changes, submit start/end (via the\n * `isSubmitting` edge), and `isLoading` edge (async `defaultValues` resolving).\n *\n * **Development only** by default — logging is a no-op when `__FORGE_PROD__` is set (the same\n * convention `_dev.ts` uses internally). Import from the dedicated `/devtools` sub-path so the\n * logging code is tree-shaken from production bundles entirely.\n *\n * Works identically on scoped sub-forms (`form.scope('address')`) — field paths logged are\n * relative to whatever `form` object is passed in, matching that form's own `state` convention.\n *\n * @example\n * ```ts\n * import { createForm } from '@vielzeug/forge';\n * import { debugForm } from '@vielzeug/forge/devtools';\n *\n * const form = createForm({ defaultValues: { email: '' } });\n * const detach = debugForm(form, { label: 'signup' });\n * // [forge:devtools:signup] field \"email\" value: \"\" → \"a@b.com\"\n *\n * detach(); // stop logging\n * ```\n */\nexport function debugForm<TValues extends Record<string, unknown>>(\n form: Form<TValues>,\n options: ForgeDevtoolsOptions = {},\n): Unsubscribe {\n if (!isDev) return () => {};\n\n const prefix = `[forge:devtools:${options.label ?? 'form'}]`;\n const fieldSnapshots = new Map<string, FieldState<unknown>>();\n\n let prevIsSubmitting = form.state.isSubmitting;\n let prevIsLoading = form.state.isLoading;\n\n function knownFieldKeys(state: FormState): Set<string> {\n const keys = new Set<string>(fieldSnapshots.keys());\n\n for (const key of Object.keys(flattenValues(form.values() as Record<string, unknown>))) keys.add(key);\n for (const key of Object.keys(state.errors)) keys.add(key);\n for (const key of state.touchedFields) keys.add(key);\n\n return keys;\n }\n\n function logFieldDiffs(state: FormState): void {\n for (const key of knownFieldKeys(state)) {\n const next = form.field(key as FlatKeyOf<TValues>);\n const prev = fieldSnapshots.get(key);\n\n fieldSnapshots.set(key, next);\n\n // First observation of this field establishes a baseline — logging it would just be\n // noise (every field would \"change\" from undefined on attach).\n if (!prev) continue;\n\n // Security: `key` is a caller-chosen field path (assertSafeKey only rejects reserved\n // segments, not control characters) — sanitize before interpolating into console output\n // to prevent terminal-escape-sequence injection, mirroring the sanitization `warn()`\n // call sites already apply (see `_utils.ts` sanitizeForLog).\n const safeKey = sanitizeForLog(key, 80);\n\n if (prev.value !== next.value) console.debug(`${prefix} field \"${safeKey}\" value:`, prev.value, '→', next.value);\n\n if (prev.error !== next.error) console.debug(`${prefix} field \"${safeKey}\" error:`, prev.error, '→', next.error);\n\n if (prev.touched !== next.touched) console.debug(`${prefix} field \"${safeKey}\" touched:`, next.touched);\n\n if (prev.dirty !== next.dirty) console.debug(`${prefix} field \"${safeKey}\" dirty:`, next.dirty);\n }\n }\n\n return form.subscribe(\n (state) => {\n logFieldDiffs(state);\n\n if (state.isSubmitting !== prevIsSubmitting) {\n prevIsSubmitting = state.isSubmitting;\n console.debug(\n `${prefix} submit ${state.isSubmitting ? 'started' : 'ended'} (submitCount=${state.submitCount})`,\n );\n }\n\n if (state.isLoading !== prevIsLoading) {\n prevIsLoading = state.isLoading;\n console.debug(`${prefix} isLoading:`, state.isLoading);\n }\n },\n { sync: true },\n );\n}\n"],"mappings":"sIAaA,IAAM,EAAQ,CAAE,WAA4C,eAgC5D,SAAgB,EACd,EACA,EAAgC,CAAC,EACpB,CACb,GAAI,CAAC,EAAO,UAAa,CAAC,EAE1B,IAAM,EAAS,mBAAmB,EAAQ,OAAS,OAAO,GACpD,EAAiB,IAAI,IAEvB,EAAmB,EAAK,MAAM,aAC9B,EAAgB,EAAK,MAAM,UAE/B,SAAS,EAAe,EAA+B,CACrD,IAAM,EAAO,IAAI,IAAY,EAAe,KAAK,CAAC,EAElD,IAAK,IAAM,KAAO,OAAO,MAAA,EAAA,EAAA,aAAA,CAAmB,EAAK,OAAO,CAA4B,CAAC,EAAG,EAAK,IAAI,CAAG,EACpG,IAAK,IAAM,KAAO,OAAO,KAAK,EAAM,MAAM,EAAG,EAAK,IAAI,CAAG,EACzD,IAAK,IAAM,KAAO,EAAM,cAAe,EAAK,IAAI,CAAG,EAEnD,OAAO,CACT,CAEA,SAAS,EAAc,EAAwB,CAC7C,IAAK,IAAM,KAAO,EAAe,CAAK,EAAG,CACvC,IAAM,EAAO,EAAK,MAAM,CAAyB,EAC3C,EAAO,EAAe,IAAI,CAAG,EAMnC,GAJA,EAAe,IAAI,EAAK,CAAI,EAIxB,CAAC,EAAM,SAMX,IAAM,EAAU,EAAA,eAAe,EAAK,EAAE,EAElC,EAAK,QAAU,EAAK,OAAO,QAAQ,MAAM,GAAG,EAAO,UAAU,EAAQ,UAAW,EAAK,MAAO,IAAK,EAAK,KAAK,EAE3G,EAAK,QAAU,EAAK,OAAO,QAAQ,MAAM,GAAG,EAAO,UAAU,EAAQ,UAAW,EAAK,MAAO,IAAK,EAAK,KAAK,EAE3G,EAAK,UAAY,EAAK,SAAS,QAAQ,MAAM,GAAG,EAAO,UAAU,EAAQ,YAAa,EAAK,OAAO,EAElG,EAAK,QAAU,EAAK,OAAO,QAAQ,MAAM,GAAG,EAAO,UAAU,EAAQ,UAAW,EAAK,KAAK,CAChG,CACF,CAEA,OAAO,EAAK,UACT,GAAU,CACT,EAAc,CAAK,EAEf,EAAM,eAAiB,IACzB,EAAmB,EAAM,aACzB,QAAQ,MACN,GAAG,EAAO,UAAU,EAAM,aAAe,UAAY,QAAQ,gBAAgB,EAAM,YAAY,EACjG,GAGE,EAAM,YAAc,IACtB,EAAgB,EAAM,UACtB,QAAQ,MAAM,GAAG,EAAO,aAAc,EAAM,SAAS,EAEzD,EACA,CAAE,KAAM,EAAK,CACf,CACF"}
@@ -3,11 +3,11 @@
3
3
  *
4
4
  * Import from the dedicated sub-path so it is tree-shaken from production bundles:
5
5
  * ```ts
6
- * import { attachForgeDevtools } from '@vielzeug/forge/devtools';
6
+ * import { debugForm } from '@vielzeug/forge/devtools';
7
7
  * ```
8
8
  */
9
9
  import type { Form, Unsubscribe } from './types';
10
- /** Options for {@link attachForgeDevtools}. */
10
+ /** Options for {@link debugForm}. */
11
11
  export type ForgeDevtoolsOptions = {
12
12
  /** Label included in every log line, useful when debugging multiple forms at once. Default: `"form"`. */
13
13
  label?: string;
@@ -27,14 +27,14 @@ export type ForgeDevtoolsOptions = {
27
27
  * @example
28
28
  * ```ts
29
29
  * import { createForm } from '@vielzeug/forge';
30
- * import { attachForgeDevtools } from '@vielzeug/forge/devtools';
30
+ * import { debugForm } from '@vielzeug/forge/devtools';
31
31
  *
32
32
  * const form = createForm({ defaultValues: { email: '' } });
33
- * const detach = attachForgeDevtools(form, { label: 'signup' });
33
+ * const detach = debugForm(form, { label: 'signup' });
34
34
  * // [forge:devtools:signup] field "email" value: "" → "a@b.com"
35
35
  *
36
36
  * detach(); // stop logging
37
37
  * ```
38
38
  */
39
- export declare function attachForgeDevtools<TValues extends Record<string, unknown>>(form: Form<TValues>, options?: ForgeDevtoolsOptions): Unsubscribe;
39
+ export declare function debugForm<TValues extends Record<string, unknown>>(form: Form<TValues>, options?: ForgeDevtoolsOptions): Unsubscribe;
40
40
  //# sourceMappingURL=devtools.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"devtools.d.ts","sourceRoot":"","sources":["../src/devtools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAyB,IAAI,EAAa,WAAW,EAAE,MAAM,SAAS,CAAC;AAMnF,+CAA+C;AAC/C,MAAM,MAAM,oBAAoB,GAAG;IACjC,yGAAyG;IACzG,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACzE,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,EACnB,OAAO,GAAE,oBAAyB,GACjC,WAAW,CAgEb"}
1
+ {"version":3,"file":"devtools.d.ts","sourceRoot":"","sources":["../src/devtools.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAyB,IAAI,EAAa,WAAW,EAAE,MAAM,SAAS,CAAC;AAMnF,qCAAqC;AACrC,MAAM,MAAM,oBAAoB,GAAG;IACjC,yGAAyG;IACzG,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,SAAS,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,EACnB,OAAO,GAAE,oBAAyB,GACjC,WAAW,CAgEb"}
package/dist/devtools.js CHANGED
@@ -24,6 +24,6 @@ function r(r, i = {}) {
24
24
  }, { sync: !0 });
25
25
  }
26
26
  //#endregion
27
- export { r as attachForgeDevtools };
27
+ export { r as debugForm };
28
28
 
29
29
  //# sourceMappingURL=devtools.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"devtools.js","names":[],"sources":["../src/devtools.ts"],"sourcesContent":["/**\n * @vielzeug/forge — debug utilities for form state visualisation.\n *\n * Import from the dedicated sub-path so it is tree-shaken from production bundles:\n * ```ts\n * import { attachForgeDevtools } from '@vielzeug/forge/devtools';\n * ```\n */\n\nimport type { FieldState, FlatKeyOf, Form, FormState, Unsubscribe } from './types';\n\nimport { flattenValues, sanitizeForLog } from './_utils';\n\nconst isDev = !(globalThis as { __FORGE_PROD__?: boolean }).__FORGE_PROD__;\n\n/** Options for {@link attachForgeDevtools}. */\nexport type ForgeDevtoolsOptions = {\n /** Label included in every log line, useful when debugging multiple forms at once. Default: `\"form\"`. */\n label?: string;\n};\n\n/**\n * Attaches a `console.debug`-based observer to `form` that logs a distinct line per observable\n * state transition: per-field value/error/touched/dirty changes, submit start/end (via the\n * `isSubmitting` edge), and `isLoading` edge (async `defaultValues` resolving).\n *\n * **Development only** by default — logging is a no-op when `__FORGE_PROD__` is set (the same\n * convention `_dev.ts` uses internally). Import from the dedicated `/devtools` sub-path so the\n * logging code is tree-shaken from production bundles entirely.\n *\n * Works identically on scoped sub-forms (`form.scope('address')`) — field paths logged are\n * relative to whatever `form` object is passed in, matching that form's own `state` convention.\n *\n * @example\n * ```ts\n * import { createForm } from '@vielzeug/forge';\n * import { attachForgeDevtools } from '@vielzeug/forge/devtools';\n *\n * const form = createForm({ defaultValues: { email: '' } });\n * const detach = attachForgeDevtools(form, { label: 'signup' });\n * // [forge:devtools:signup] field \"email\" value: \"\" → \"a@b.com\"\n *\n * detach(); // stop logging\n * ```\n */\nexport function attachForgeDevtools<TValues extends Record<string, unknown>>(\n form: Form<TValues>,\n options: ForgeDevtoolsOptions = {},\n): Unsubscribe {\n if (!isDev) return () => {};\n\n const prefix = `[forge:devtools:${options.label ?? 'form'}]`;\n const fieldSnapshots = new Map<string, FieldState<unknown>>();\n\n let prevIsSubmitting = form.state.isSubmitting;\n let prevIsLoading = form.state.isLoading;\n\n function knownFieldKeys(state: FormState): Set<string> {\n const keys = new Set<string>(fieldSnapshots.keys());\n\n for (const key of Object.keys(flattenValues(form.values() as Record<string, unknown>))) keys.add(key);\n for (const key of Object.keys(state.errors)) keys.add(key);\n for (const key of state.touchedFields) keys.add(key);\n\n return keys;\n }\n\n function logFieldDiffs(state: FormState): void {\n for (const key of knownFieldKeys(state)) {\n const next = form.field(key as FlatKeyOf<TValues>);\n const prev = fieldSnapshots.get(key);\n\n fieldSnapshots.set(key, next);\n\n // First observation of this field establishes a baseline — logging it would just be\n // noise (every field would \"change\" from undefined on attach).\n if (!prev) continue;\n\n // Security: `key` is a caller-chosen field path (assertSafeKey only rejects reserved\n // segments, not control characters) — sanitize before interpolating into console output\n // to prevent terminal-escape-sequence injection, mirroring the sanitization `warn()`\n // call sites already apply (see `_utils.ts` sanitizeForLog).\n const safeKey = sanitizeForLog(key, 80);\n\n if (prev.value !== next.value) console.debug(`${prefix} field \"${safeKey}\" value:`, prev.value, '→', next.value);\n\n if (prev.error !== next.error) console.debug(`${prefix} field \"${safeKey}\" error:`, prev.error, '→', next.error);\n\n if (prev.touched !== next.touched) console.debug(`${prefix} field \"${safeKey}\" touched:`, next.touched);\n\n if (prev.dirty !== next.dirty) console.debug(`${prefix} field \"${safeKey}\" dirty:`, next.dirty);\n }\n }\n\n return form.subscribe(\n (state) => {\n logFieldDiffs(state);\n\n if (state.isSubmitting !== prevIsSubmitting) {\n prevIsSubmitting = state.isSubmitting;\n console.debug(\n `${prefix} submit ${state.isSubmitting ? 'started' : 'ended'} (submitCount=${state.submitCount})`,\n );\n }\n\n if (state.isLoading !== prevIsLoading) {\n prevIsLoading = state.isLoading;\n console.debug(`${prefix} isLoading:`, state.isLoading);\n }\n },\n { sync: true },\n );\n}\n"],"mappings":";;AAaA,IAAM,IAAQ,CAAE,WAA4C;AAgC5D,SAAgB,EACd,GACA,IAAgC,CAAC,GACpB;CACb,IAAI,CAAC,GAAO,aAAa,CAAC;CAE1B,IAAM,IAAS,mBAAmB,EAAQ,SAAS,OAAO,IACpD,oBAAiB,IAAI,IAAiC,GAExD,IAAmB,EAAK,MAAM,cAC9B,IAAgB,EAAK,MAAM;CAE/B,SAAS,EAAe,GAA+B;EACrD,IAAM,IAAO,IAAI,IAAY,EAAe,KAAK,CAAC;EAElD,KAAK,IAAM,KAAO,OAAO,KAAK,EAAc,EAAK,OAAO,CAA4B,CAAC,GAAG,EAAK,IAAI,CAAG;EACpG,KAAK,IAAM,KAAO,OAAO,KAAK,EAAM,MAAM,GAAG,EAAK,IAAI,CAAG;EACzD,KAAK,IAAM,KAAO,EAAM,eAAe,EAAK,IAAI,CAAG;EAEnD,OAAO;CACT;CAEA,SAAS,EAAc,GAAwB;EAC7C,KAAK,IAAM,KAAO,EAAe,CAAK,GAAG;GACvC,IAAM,IAAO,EAAK,MAAM,CAAyB,GAC3C,IAAO,EAAe,IAAI,CAAG;GAMnC,IAJA,EAAe,IAAI,GAAK,CAAI,GAIxB,CAAC,GAAM;GAMX,IAAM,IAAU,EAAe,GAAK,EAAE;GAQtC,AANI,EAAK,UAAU,EAAK,SAAO,QAAQ,MAAM,GAAG,EAAO,UAAU,EAAQ,WAAW,EAAK,OAAO,KAAK,EAAK,KAAK,GAE3G,EAAK,UAAU,EAAK,SAAO,QAAQ,MAAM,GAAG,EAAO,UAAU,EAAQ,WAAW,EAAK,OAAO,KAAK,EAAK,KAAK,GAE3G,EAAK,YAAY,EAAK,WAAS,QAAQ,MAAM,GAAG,EAAO,UAAU,EAAQ,aAAa,EAAK,OAAO,GAElG,EAAK,UAAU,EAAK,SAAO,QAAQ,MAAM,GAAG,EAAO,UAAU,EAAQ,WAAW,EAAK,KAAK;EAChG;CACF;CAEA,OAAO,EAAK,WACT,MAAU;EAUT,AATA,EAAc,CAAK,GAEf,EAAM,iBAAiB,MACzB,IAAmB,EAAM,cACzB,QAAQ,MACN,GAAG,EAAO,UAAU,EAAM,eAAe,YAAY,QAAQ,gBAAgB,EAAM,YAAY,EACjG,IAGE,EAAM,cAAc,MACtB,IAAgB,EAAM,WACtB,QAAQ,MAAM,GAAG,EAAO,cAAc,EAAM,SAAS;CAEzD,GACA,EAAE,MAAM,GAAK,CACf;AACF"}
1
+ {"version":3,"file":"devtools.js","names":[],"sources":["../src/devtools.ts"],"sourcesContent":["/**\n * @vielzeug/forge — debug utilities for form state visualisation.\n *\n * Import from the dedicated sub-path so it is tree-shaken from production bundles:\n * ```ts\n * import { debugForm } from '@vielzeug/forge/devtools';\n * ```\n */\n\nimport type { FieldState, FlatKeyOf, Form, FormState, Unsubscribe } from './types';\n\nimport { flattenValues, sanitizeForLog } from './_utils';\n\nconst isDev = !(globalThis as { __FORGE_PROD__?: boolean }).__FORGE_PROD__;\n\n/** Options for {@link debugForm}. */\nexport type ForgeDevtoolsOptions = {\n /** Label included in every log line, useful when debugging multiple forms at once. Default: `\"form\"`. */\n label?: string;\n};\n\n/**\n * Attaches a `console.debug`-based observer to `form` that logs a distinct line per observable\n * state transition: per-field value/error/touched/dirty changes, submit start/end (via the\n * `isSubmitting` edge), and `isLoading` edge (async `defaultValues` resolving).\n *\n * **Development only** by default — logging is a no-op when `__FORGE_PROD__` is set (the same\n * convention `_dev.ts` uses internally). Import from the dedicated `/devtools` sub-path so the\n * logging code is tree-shaken from production bundles entirely.\n *\n * Works identically on scoped sub-forms (`form.scope('address')`) — field paths logged are\n * relative to whatever `form` object is passed in, matching that form's own `state` convention.\n *\n * @example\n * ```ts\n * import { createForm } from '@vielzeug/forge';\n * import { debugForm } from '@vielzeug/forge/devtools';\n *\n * const form = createForm({ defaultValues: { email: '' } });\n * const detach = debugForm(form, { label: 'signup' });\n * // [forge:devtools:signup] field \"email\" value: \"\" → \"a@b.com\"\n *\n * detach(); // stop logging\n * ```\n */\nexport function debugForm<TValues extends Record<string, unknown>>(\n form: Form<TValues>,\n options: ForgeDevtoolsOptions = {},\n): Unsubscribe {\n if (!isDev) return () => {};\n\n const prefix = `[forge:devtools:${options.label ?? 'form'}]`;\n const fieldSnapshots = new Map<string, FieldState<unknown>>();\n\n let prevIsSubmitting = form.state.isSubmitting;\n let prevIsLoading = form.state.isLoading;\n\n function knownFieldKeys(state: FormState): Set<string> {\n const keys = new Set<string>(fieldSnapshots.keys());\n\n for (const key of Object.keys(flattenValues(form.values() as Record<string, unknown>))) keys.add(key);\n for (const key of Object.keys(state.errors)) keys.add(key);\n for (const key of state.touchedFields) keys.add(key);\n\n return keys;\n }\n\n function logFieldDiffs(state: FormState): void {\n for (const key of knownFieldKeys(state)) {\n const next = form.field(key as FlatKeyOf<TValues>);\n const prev = fieldSnapshots.get(key);\n\n fieldSnapshots.set(key, next);\n\n // First observation of this field establishes a baseline — logging it would just be\n // noise (every field would \"change\" from undefined on attach).\n if (!prev) continue;\n\n // Security: `key` is a caller-chosen field path (assertSafeKey only rejects reserved\n // segments, not control characters) — sanitize before interpolating into console output\n // to prevent terminal-escape-sequence injection, mirroring the sanitization `warn()`\n // call sites already apply (see `_utils.ts` sanitizeForLog).\n const safeKey = sanitizeForLog(key, 80);\n\n if (prev.value !== next.value) console.debug(`${prefix} field \"${safeKey}\" value:`, prev.value, '→', next.value);\n\n if (prev.error !== next.error) console.debug(`${prefix} field \"${safeKey}\" error:`, prev.error, '→', next.error);\n\n if (prev.touched !== next.touched) console.debug(`${prefix} field \"${safeKey}\" touched:`, next.touched);\n\n if (prev.dirty !== next.dirty) console.debug(`${prefix} field \"${safeKey}\" dirty:`, next.dirty);\n }\n }\n\n return form.subscribe(\n (state) => {\n logFieldDiffs(state);\n\n if (state.isSubmitting !== prevIsSubmitting) {\n prevIsSubmitting = state.isSubmitting;\n console.debug(\n `${prefix} submit ${state.isSubmitting ? 'started' : 'ended'} (submitCount=${state.submitCount})`,\n );\n }\n\n if (state.isLoading !== prevIsLoading) {\n prevIsLoading = state.isLoading;\n console.debug(`${prefix} isLoading:`, state.isLoading);\n }\n },\n { sync: true },\n );\n}\n"],"mappings":";;AAaA,IAAM,IAAQ,CAAE,WAA4C;AAgC5D,SAAgB,EACd,GACA,IAAgC,CAAC,GACpB;CACb,IAAI,CAAC,GAAO,aAAa,CAAC;CAE1B,IAAM,IAAS,mBAAmB,EAAQ,SAAS,OAAO,IACpD,oBAAiB,IAAI,IAAiC,GAExD,IAAmB,EAAK,MAAM,cAC9B,IAAgB,EAAK,MAAM;CAE/B,SAAS,EAAe,GAA+B;EACrD,IAAM,IAAO,IAAI,IAAY,EAAe,KAAK,CAAC;EAElD,KAAK,IAAM,KAAO,OAAO,KAAK,EAAc,EAAK,OAAO,CAA4B,CAAC,GAAG,EAAK,IAAI,CAAG;EACpG,KAAK,IAAM,KAAO,OAAO,KAAK,EAAM,MAAM,GAAG,EAAK,IAAI,CAAG;EACzD,KAAK,IAAM,KAAO,EAAM,eAAe,EAAK,IAAI,CAAG;EAEnD,OAAO;CACT;CAEA,SAAS,EAAc,GAAwB;EAC7C,KAAK,IAAM,KAAO,EAAe,CAAK,GAAG;GACvC,IAAM,IAAO,EAAK,MAAM,CAAyB,GAC3C,IAAO,EAAe,IAAI,CAAG;GAMnC,IAJA,EAAe,IAAI,GAAK,CAAI,GAIxB,CAAC,GAAM;GAMX,IAAM,IAAU,EAAe,GAAK,EAAE;GAQtC,AANI,EAAK,UAAU,EAAK,SAAO,QAAQ,MAAM,GAAG,EAAO,UAAU,EAAQ,WAAW,EAAK,OAAO,KAAK,EAAK,KAAK,GAE3G,EAAK,UAAU,EAAK,SAAO,QAAQ,MAAM,GAAG,EAAO,UAAU,EAAQ,WAAW,EAAK,OAAO,KAAK,EAAK,KAAK,GAE3G,EAAK,YAAY,EAAK,WAAS,QAAQ,MAAM,GAAG,EAAO,UAAU,EAAQ,aAAa,EAAK,OAAO,GAElG,EAAK,UAAU,EAAK,SAAO,QAAQ,MAAM,GAAG,EAAO,UAAU,EAAQ,WAAW,EAAK,KAAK;EAChG;CACF;CAEA,OAAO,EAAK,WACT,MAAU;EAUT,AATA,EAAc,CAAK,GAEf,EAAM,iBAAiB,MACzB,IAAmB,EAAM,cACzB,QAAQ,MACN,GAAG,EAAO,UAAU,EAAM,eAAe,YAAY,QAAQ,gBAAgB,EAAM,YAAY,EACjG,IAGE,EAAM,cAAc,MACtB,IAAgB,EAAM,WACtB,QAAQ,MAAM,GAAG,EAAO,cAAc,EAAM,SAAS;CAEzD,GACA,EAAE,MAAM,GAAK,CACf;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vielzeug/forge",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Type-safe form state — field-level validation, submission, dirty tracking, field arrays, and async default values",
5
5
  "type": "module",
6
6
  "files": [
@@ -43,6 +43,9 @@
43
43
  "access": "public",
44
44
  "registry": "https://registry.npmjs.org/"
45
45
  },
46
+ "engines": {
47
+ "node": ">=18"
48
+ },
46
49
  "dependencies": {
47
50
  "@vielzeug/arsenal": "workspace:*",
48
51
  "@vielzeug/ripple": "workspace:*"