attaform 0.21.2 → 0.23.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 +7 -10
- package/dist/chunks/dev-key-collision-warnings.cjs +0 -33
- package/dist/chunks/dev-key-collision-warnings.cjs.map +1 -1
- package/dist/chunks/dev-key-collision-warnings.mjs +1 -33
- package/dist/chunks/dev-key-collision-warnings.mjs.map +1 -1
- package/dist/chunks/devtools.cjs +3 -5
- package/dist/chunks/devtools.cjs.map +1 -1
- package/dist/chunks/devtools.mjs +3 -5
- package/dist/chunks/devtools.mjs.map +1 -1
- package/dist/chunks/fingerprint2.cjs +1 -1
- package/dist/chunks/fingerprint2.mjs +1 -1
- package/dist/index.cjs +3 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -41
- package/dist/index.d.mts +6 -41
- package/dist/index.d.ts +6 -41
- package/dist/index.mjs +5 -5
- package/dist/nuxt.d.cts +1 -1
- package/dist/nuxt.d.mts +1 -1
- package/dist/nuxt.d.ts +1 -1
- package/dist/runtime/components/AttaformDevtoolsPanel.vue +3 -11
- package/dist/runtime/plugins/attaform.cjs +2 -2
- package/dist/runtime/plugins/attaform.mjs +2 -2
- package/dist/shared/{attaform.BGf_J22U.d.ts → attaform.BGMRvckW.d.ts} +11 -70
- package/dist/shared/{attaform.ory-3WhV.d.mts → attaform.BJnNK75Y.d.cts} +41 -491
- package/dist/shared/{attaform.ory-3WhV.d.ts → attaform.BJnNK75Y.d.mts} +41 -491
- package/dist/shared/{attaform.ory-3WhV.d.cts → attaform.BJnNK75Y.d.ts} +41 -491
- package/dist/shared/{attaform.DP-u7_tk.mjs → attaform.BhI9Icek.mjs} +17 -291
- package/dist/shared/attaform.BhI9Icek.mjs.map +1 -0
- package/dist/shared/{attaform.BwLp9KM7.cjs → attaform.BibT5AS_.cjs} +2 -2
- package/dist/shared/{attaform.BwLp9KM7.cjs.map → attaform.BibT5AS_.cjs.map} +1 -1
- package/dist/shared/{attaform.DkA5J8NW.d.cts → attaform.CO0e7YVY.d.cts} +1 -46
- package/dist/shared/{attaform.DkA5J8NW.d.ts → attaform.CO0e7YVY.d.mts} +1 -46
- package/dist/shared/{attaform.DkA5J8NW.d.mts → attaform.CO0e7YVY.d.ts} +1 -46
- package/dist/shared/{attaform.BwrowMp2.cjs → attaform.CaYj3ZfY.cjs} +3 -3
- package/dist/shared/{attaform.BwrowMp2.cjs.map → attaform.CaYj3ZfY.cjs.map} +1 -1
- package/dist/shared/{attaform.BBDIKtKY.cjs → attaform.Cmb_LCie.cjs} +4 -4
- package/dist/shared/{attaform.BVeLgfEh.mjs.map → attaform.Cmb_LCie.cjs.map} +1 -1
- package/dist/shared/{attaform.CTheKoTc.mjs → attaform.CtJOd7ox.mjs} +446 -525
- package/dist/shared/attaform.CtJOd7ox.mjs.map +1 -0
- package/dist/shared/{attaform.CrD73S4m.mjs → attaform.CzVta5o2.mjs} +116 -47
- package/dist/shared/attaform.CzVta5o2.mjs.map +1 -0
- package/dist/shared/{attaform.CnEl--PF.d.mts → attaform.D52oJiYC.d.cts} +1 -1
- package/dist/shared/{attaform.BoY6RZUl.d.cts → attaform.DCkSNnPr.d.mts} +1 -1
- package/dist/shared/{attaform.B5LNzqQh.cjs → attaform.Db4E4IW6.cjs} +18 -297
- package/dist/shared/attaform.Db4E4IW6.cjs.map +1 -0
- package/dist/shared/{attaform.CcnF1AKJ.cjs → attaform.DbyTD8N2.cjs} +116 -47
- package/dist/shared/attaform.DbyTD8N2.cjs.map +1 -0
- package/dist/shared/{attaform.D6GYGshL.mjs → attaform.Dd1Kmmaj.mjs} +3 -3
- package/dist/shared/{attaform.D6GYGshL.mjs.map → attaform.Dd1Kmmaj.mjs.map} +1 -1
- package/dist/shared/{attaform.BCcrLApm.d.mts → attaform.DrY8srOp.d.mts} +11 -70
- package/dist/shared/{attaform.D2ZuIOCf.cjs → attaform.DsQkXE3o.cjs} +445 -534
- package/dist/shared/attaform.DsQkXE3o.cjs.map +1 -0
- package/dist/shared/{attaform.BkjJfMvJ.d.cts → attaform.DuPneYR0.d.cts} +11 -70
- package/dist/shared/{attaform.C41gjp-a.mjs → attaform.Dx9-QQE2.mjs} +2 -2
- package/dist/shared/{attaform.C41gjp-a.mjs.map → attaform.Dx9-QQE2.mjs.map} +1 -1
- package/dist/shared/{attaform.BYgioWLF.d.ts → attaform.WEwfXcHq.d.ts} +1 -1
- package/dist/shared/{attaform.BVeLgfEh.mjs → attaform.alpG7rT7.mjs} +4 -4
- package/dist/shared/{attaform.BBDIKtKY.cjs.map → attaform.alpG7rT7.mjs.map} +1 -1
- package/dist/zod-v3.cjs +2 -2
- package/dist/zod-v3.d.cts +3 -3
- package/dist/zod-v3.d.mts +3 -3
- package/dist/zod-v3.d.ts +3 -3
- package/dist/zod-v3.mjs +2 -2
- package/dist/zod-v4.cjs +2 -2
- package/dist/zod-v4.d.cts +5 -5
- package/dist/zod-v4.d.mts +5 -5
- package/dist/zod-v4.d.ts +5 -5
- package/dist/zod-v4.mjs +2 -2
- package/dist/zod.cjs +5 -5
- package/dist/zod.d.cts +5 -5
- package/dist/zod.d.mts +5 -5
- package/dist/zod.d.ts +5 -5
- package/dist/zod.mjs +5 -5
- package/package.json +2 -2
- package/dist/chunks/indexeddb.cjs +0 -119
- package/dist/chunks/indexeddb.cjs.map +0 -1
- package/dist/chunks/indexeddb.mjs +0 -117
- package/dist/chunks/indexeddb.mjs.map +0 -1
- package/dist/chunks/local-storage.cjs +0 -58
- package/dist/chunks/local-storage.cjs.map +0 -1
- package/dist/chunks/local-storage.mjs +0 -56
- package/dist/chunks/local-storage.mjs.map +0 -1
- package/dist/chunks/multi-tab-sync.cjs +0 -367
- package/dist/chunks/multi-tab-sync.cjs.map +0 -1
- package/dist/chunks/multi-tab-sync.mjs +0 -364
- package/dist/chunks/multi-tab-sync.mjs.map +0 -1
- package/dist/chunks/session-storage.cjs +0 -58
- package/dist/chunks/session-storage.cjs.map +0 -1
- package/dist/chunks/session-storage.mjs +0 -56
- package/dist/chunks/session-storage.mjs.map +0 -1
- package/dist/chunks/wire-persistence.cjs +0 -396
- package/dist/chunks/wire-persistence.cjs.map +0 -1
- package/dist/chunks/wire-persistence.mjs +0 -394
- package/dist/chunks/wire-persistence.mjs.map +0 -1
- package/dist/shared/attaform.B5LNzqQh.cjs.map +0 -1
- package/dist/shared/attaform.CTheKoTc.mjs.map +0 -1
- package/dist/shared/attaform.CcnF1AKJ.cjs.map +0 -1
- package/dist/shared/attaform.CrD73S4m.mjs.map +0 -1
- package/dist/shared/attaform.D2ZuIOCf.cjs.map +0 -1
- package/dist/shared/attaform.DP-u7_tk.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -121,7 +121,7 @@ It catches stale `form.values.contacts[N]` reads at compile time. Nuxt 3 / 4 set
|
|
|
121
121
|
- **`form.history`**: consolidated undo/redo namespace (`undo()`, `redo()`, `clear()`, `canUndo`, `canRedo`, `size`).
|
|
122
122
|
- **`form.register(path)`**: typed two-way binding. Pair with `v-register` on `<input>` / `<textarea>` / `<select>`.
|
|
123
123
|
- **`form.handleSubmit(onValid, onInvalid?)`**: runs validation, dispatches. The valid callback receives the strict Zod-inferred type.
|
|
124
|
-
- **`form.setValue(path, value)`**, **`form.reset()`**, field-array helpers
|
|
124
|
+
- **`form.setValue(path, value)`**, **`form.reset()`**, field-array helpers: see [Entry points](https://www.attaform.com/docs/reference/entry-points).
|
|
125
125
|
|
|
126
126
|
> **Try it live.** Tweak this schema, edit the template, and watch the form rebind in the browser at [attaform.com/demos](https://www.attaform.com/demos). No install needed.
|
|
127
127
|
|
|
@@ -136,18 +136,16 @@ It catches stale `form.values.contacts[N]` reads at compile time. Nuxt 3 / 4 set
|
|
|
136
136
|
Typed two-way binding to `form.values.email`, with schema-driven coercion at the directive layer.
|
|
137
137
|
|
|
138
138
|
```vue
|
|
139
|
-
<input v-register="form.register('
|
|
139
|
+
<input v-register="form.register('slug', { transforms: [lowercase] })" />
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
-
Same line. The field now
|
|
142
|
+
Same line. The field now runs a sync write-time transform, normalizing the value before it reaches form state.
|
|
143
143
|
|
|
144
144
|
```vue
|
|
145
|
-
<input
|
|
146
|
-
v-register="form.register('email', { persist: true, transforms: [lowercase], multiTab: false })"
|
|
147
|
-
/>
|
|
145
|
+
<input v-register="form.register('slug', { transforms: [lowercase, dashify], autoAria: false })" />
|
|
148
146
|
```
|
|
149
147
|
|
|
150
|
-
Same line.
|
|
148
|
+
Same line. Compose a transform pipeline and opt this field out of automatic aria wiring, all without touching the markup elsewhere.
|
|
151
149
|
|
|
152
150
|
## Features
|
|
153
151
|
|
|
@@ -155,12 +153,11 @@ Same line. Add a sync DOM-input transform, opt out of multi-tab sync, all withou
|
|
|
155
153
|
- **Live validation.** `validateOn: 'change'` by default with synchronous `debounceMs: 0`. `'blur'` and `'submit'` (opt-out) modes available. Async refinements await before submit dispatches.
|
|
156
154
|
- **Schema-driven coercion.** String DOM input flows into the schema's typed slot (`string→number`, `string→boolean`) at the directive layer. Default-on; pass `useForm({ coerce: false })` to disable or a custom `CoercionRegistry` to extend.
|
|
157
155
|
- **Register transforms.** `form.register('slug', { transforms: [lowercase, dashify] })` runs sync write-time normalization before storage commit.
|
|
158
|
-
- **First-class multistep.** `useWizard` composes `useForm` instances into a flow with shared navigation, per-step validation,
|
|
156
|
+
- **First-class multistep.** `useWizard` composes `useForm` instances into a flow with shared navigation, per-step validation, state retained across steps, and deep-link restore.
|
|
159
157
|
- **DevTools panel.** Auto-wired in Nuxt. Walk history, edit values live, inspect every form on the page. No probes to install.
|
|
160
158
|
- **Discriminated-union variant memory.** Switching a discriminator (`notify.channel: 'email' → 'sms' → 'email'`) restores the previous variant's typed subtree by default. Pass `useForm({ rememberVariants: false })` to drop on switch.
|
|
161
159
|
- **Field arrays.** `append` / `prepend` / `insert` / `remove` / `swap` / `move` / `replace`, fully typed at the call site.
|
|
162
|
-
- **
|
|
163
|
-
- **Multi-tab sync.** Same-keyed `useForm` callsites in same-origin tabs auto-pair over `BroadcastChannel` and mirror every mutation in near real-time. Sensitive paths filtered both directions; HTTPS-or-localhost required.
|
|
160
|
+
- **Undo / redo.** A bounded undo stack via `form.history` (`undo()` / `redo()` / `canUndo` / `canRedo`).
|
|
164
161
|
- **Server errors.** `parseApiErrors(payload, { formKey: form.key })` normalizes any API envelope into the same `ValidationError` shape your template already reads. Pair with `form.setFieldErrors(...)`; user errors survive schema revalidation.
|
|
165
162
|
- **Stable error codes.** Every `ValidationError` carries `code: string`. Attaform codes (`atta:`) live on the exported `AttaformErrorCode` enum; adapter codes use a `zod:` prefix; consumers pick their own (`api:`, `auth:`, …).
|
|
166
163
|
- **Clearable required fields.** The `unset` sentinel marks a field displayed-empty while storage holds the schema's slim default. Submit fails with `'No value supplied'` for required schemas; `.optional()` / `.nullable()` / `.default(N)` opt out.
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const injectWizard = require('../shared/attaform.D2ZuIOCf.cjs');
|
|
4
|
-
|
|
5
3
|
async function warnOnSchemaFingerprintMismatch(key, existing, incoming) {
|
|
6
4
|
let existingFp;
|
|
7
5
|
let incomingFp;
|
|
@@ -22,37 +20,6 @@ async function warnOnSchemaFingerprintMismatch(key, existing, incoming) {
|
|
|
22
20
|
incoming: ${incomingFp}`
|
|
23
21
|
);
|
|
24
22
|
}
|
|
25
|
-
function warnOnPersistDivergence(key, existing, incomingPersist) {
|
|
26
|
-
if (incomingPersist === void 0) return;
|
|
27
|
-
const wired = existing.modules.get(injectWizard.PERSISTENCE_MODULE_KEY);
|
|
28
|
-
const incomingNormalized = injectWizard.normalizePersistConfig(incomingPersist);
|
|
29
|
-
if (wired === void 0) {
|
|
30
|
-
console.warn(
|
|
31
|
-
`[attaform] useForm({ key: "${key}" }) passed a persist config but the first useForm({ key }) call didn't wire persistence; the new config is silently dropped. Pass persist on the first call, or remove persist here to make the inheritance explicit.`
|
|
32
|
-
);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
if (persistConfigsEquivalent(wired.config, incomingNormalized)) return;
|
|
36
|
-
console.warn(
|
|
37
|
-
`[attaform] useForm({ key: "${key}" }) passed a persist config that differs from the first useForm({ key }) call's; first wins, this one is ignored.
|
|
38
|
-
wired: ${describePersist(wired.config)}
|
|
39
|
-
incoming: ${describePersist(incomingNormalized)}`
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
function persistConfigsEquivalent(a, b) {
|
|
43
|
-
if (a.storage !== b.storage) return false;
|
|
44
|
-
if ((a.key ?? void 0) !== (b.key ?? void 0)) return false;
|
|
45
|
-
if ((a.debounceMs ?? void 0) !== (b.debounceMs ?? void 0)) return false;
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
function describePersist(config) {
|
|
49
|
-
const storage = typeof config.storage === "string" ? config.storage : "custom-adapter";
|
|
50
|
-
const parts = [`storage=${storage}`];
|
|
51
|
-
if (config.key !== void 0) parts.push(`key=${config.key}`);
|
|
52
|
-
if (config.debounceMs !== void 0) parts.push(`debounceMs=${config.debounceMs}`);
|
|
53
|
-
return `{ ${parts.join(", ")} }`;
|
|
54
|
-
}
|
|
55
23
|
|
|
56
|
-
exports.warnOnPersistDivergence = warnOnPersistDivergence;
|
|
57
24
|
exports.warnOnSchemaFingerprintMismatch = warnOnSchemaFingerprintMismatch;
|
|
58
25
|
//# sourceMappingURL=dev-key-collision-warnings.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev-key-collision-warnings.cjs","sources":["../../src/runtime/core/dev-key-collision-warnings.ts"],"sourcesContent":["/**\n * Dev-only shared-key collision diagnostics for `useForm`. When two\n * `useForm({ key })` calls land on the same key, they resolve to one\n * `FormStore` by design (the shared-store semantic), and the second\n * call's schema
|
|
1
|
+
{"version":3,"file":"dev-key-collision-warnings.cjs","sources":["../../src/runtime/core/dev-key-collision-warnings.ts"],"sourcesContent":["/**\n * Dev-only shared-key collision diagnostics for `useForm`. When two\n * `useForm({ key })` calls land on the same key, they resolve to one\n * `FormStore` by design (the shared-store semantic), and the second\n * call's schema is silently dropped in favour of the first's wiring.\n * The warnings here surface that divergence so a genuine collision (two\n * unrelated call sites that happen to agree on a key) is diagnosable\n * rather than silent.\n *\n * This whole module is loaded behind a `__DEV__`-gated dynamic import in\n * `use-abstract-form.ts`. A production build folds that gate to `false`,\n * dead-code-eliminates the `import()` call, and leaves this module an\n * unreferenced chunk the consumer's bundler drops. Keeping the warnings\n * here (rather than as plain functions called from the gated block)\n * matters because esbuild removes the inline dead branch but NOT a\n * top-level function it is the sole caller of: tree-shaking runs before\n * the define-fold, so the function survives. A separately-imported\n * module sidesteps that and keeps the cluster out of every consumer's\n * production bundle.\n */\nimport type { AbstractSchema, FormKey } from '../types/types-api'\nimport type { GenericForm } from '../types/types-core'\n\n/**\n * Dev-only: warn when a second `useForm` lands on the same key with\n * a structurally-different schema. Two schemas resolve their own\n * fingerprints; we compare the strings and flag mismatches. An adapter\n * `fingerprint()` that rejects is caught (never crashes the form) and\n * surfaced as a `console.error` in dev: the mismatch check is skipped,\n * matching the \"allow the inconsistency\" failure mode. See\n * `AbstractSchema.fingerprint()` in types-api.ts for the contract.\n */\nexport async function warnOnSchemaFingerprintMismatch(\n key: FormKey,\n existing: AbstractSchema<GenericForm, GenericForm>,\n incoming: AbstractSchema<GenericForm, GenericForm>\n): Promise<void> {\n let existingFp: string\n let incomingFp: string\n try {\n existingFp = await existing.fingerprint()\n incomingFp = await incoming.fingerprint()\n } catch (error) {\n console.error(\n `[attaform] fingerprint() rejected for key \"${key}\"; skipping mismatch check.`,\n error\n )\n return\n }\n if (existingFp === incomingFp) return\n console.warn(\n `[attaform] useForm() calls with key \"${key}\" use different schemas; first wins, second is ignored. Use identical schemas or unique keys.\\n existing: ${existingFp}\\n incoming: ${incomingFp}`\n )\n}\n"],"names":[],"mappings":";;AAgCA,eAAsB,+BAAA,CACpB,GAAA,EACA,QAAA,EACA,QAAA,EACe;AACf,EAAA,IAAI,UAAA;AACJ,EAAA,IAAI,UAAA;AACJ,EAAA,IAAI;AACF,IAAA,UAAA,GAAa,MAAM,SAAS,WAAA,EAAY;AACxC,IAAA,UAAA,GAAa,MAAM,SAAS,WAAA,EAAY;AAAA,EAC1C,SAAS,KAAA,EAAO;AACd,IAAA,OAAA,CAAQ,KAAA;AAAA,MACN,8CAA8C,GAAG,CAAA,2BAAA,CAAA;AAAA,MACjD;AAAA,KACF;AACA,IAAA;AAAA,EACF;AACA,EAAA,IAAI,eAAe,UAAA,EAAY;AAC/B,EAAA,OAAA,CAAQ,IAAA;AAAA,IACN,wCAAwC,GAAG,CAAA;AAAA,YAAA,EAA8G,UAAU;AAAA,YAAA,EAAiB,UAAU,CAAA;AAAA,GAChM;AACF;;;;"}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { P as PERSISTENCE_MODULE_KEY, n as normalizePersistConfig } from '../shared/attaform.CTheKoTc.mjs';
|
|
2
|
-
|
|
3
1
|
async function warnOnSchemaFingerprintMismatch(key, existing, incoming) {
|
|
4
2
|
let existingFp;
|
|
5
3
|
let incomingFp;
|
|
@@ -20,36 +18,6 @@ async function warnOnSchemaFingerprintMismatch(key, existing, incoming) {
|
|
|
20
18
|
incoming: ${incomingFp}`
|
|
21
19
|
);
|
|
22
20
|
}
|
|
23
|
-
function warnOnPersistDivergence(key, existing, incomingPersist) {
|
|
24
|
-
if (incomingPersist === void 0) return;
|
|
25
|
-
const wired = existing.modules.get(PERSISTENCE_MODULE_KEY);
|
|
26
|
-
const incomingNormalized = normalizePersistConfig(incomingPersist);
|
|
27
|
-
if (wired === void 0) {
|
|
28
|
-
console.warn(
|
|
29
|
-
`[attaform] useForm({ key: "${key}" }) passed a persist config but the first useForm({ key }) call didn't wire persistence; the new config is silently dropped. Pass persist on the first call, or remove persist here to make the inheritance explicit.`
|
|
30
|
-
);
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
if (persistConfigsEquivalent(wired.config, incomingNormalized)) return;
|
|
34
|
-
console.warn(
|
|
35
|
-
`[attaform] useForm({ key: "${key}" }) passed a persist config that differs from the first useForm({ key }) call's; first wins, this one is ignored.
|
|
36
|
-
wired: ${describePersist(wired.config)}
|
|
37
|
-
incoming: ${describePersist(incomingNormalized)}`
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
function persistConfigsEquivalent(a, b) {
|
|
41
|
-
if (a.storage !== b.storage) return false;
|
|
42
|
-
if ((a.key ?? void 0) !== (b.key ?? void 0)) return false;
|
|
43
|
-
if ((a.debounceMs ?? void 0) !== (b.debounceMs ?? void 0)) return false;
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
function describePersist(config) {
|
|
47
|
-
const storage = typeof config.storage === "string" ? config.storage : "custom-adapter";
|
|
48
|
-
const parts = [`storage=${storage}`];
|
|
49
|
-
if (config.key !== void 0) parts.push(`key=${config.key}`);
|
|
50
|
-
if (config.debounceMs !== void 0) parts.push(`debounceMs=${config.debounceMs}`);
|
|
51
|
-
return `{ ${parts.join(", ")} }`;
|
|
52
|
-
}
|
|
53
21
|
|
|
54
|
-
export {
|
|
22
|
+
export { warnOnSchemaFingerprintMismatch };
|
|
55
23
|
//# sourceMappingURL=dev-key-collision-warnings.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dev-key-collision-warnings.mjs","sources":["../../src/runtime/core/dev-key-collision-warnings.ts"],"sourcesContent":["/**\n * Dev-only shared-key collision diagnostics for `useForm`. When two\n * `useForm({ key })` calls land on the same key, they resolve to one\n * `FormStore` by design (the shared-store semantic), and the second\n * call's schema
|
|
1
|
+
{"version":3,"file":"dev-key-collision-warnings.mjs","sources":["../../src/runtime/core/dev-key-collision-warnings.ts"],"sourcesContent":["/**\n * Dev-only shared-key collision diagnostics for `useForm`. When two\n * `useForm({ key })` calls land on the same key, they resolve to one\n * `FormStore` by design (the shared-store semantic), and the second\n * call's schema is silently dropped in favour of the first's wiring.\n * The warnings here surface that divergence so a genuine collision (two\n * unrelated call sites that happen to agree on a key) is diagnosable\n * rather than silent.\n *\n * This whole module is loaded behind a `__DEV__`-gated dynamic import in\n * `use-abstract-form.ts`. A production build folds that gate to `false`,\n * dead-code-eliminates the `import()` call, and leaves this module an\n * unreferenced chunk the consumer's bundler drops. Keeping the warnings\n * here (rather than as plain functions called from the gated block)\n * matters because esbuild removes the inline dead branch but NOT a\n * top-level function it is the sole caller of: tree-shaking runs before\n * the define-fold, so the function survives. A separately-imported\n * module sidesteps that and keeps the cluster out of every consumer's\n * production bundle.\n */\nimport type { AbstractSchema, FormKey } from '../types/types-api'\nimport type { GenericForm } from '../types/types-core'\n\n/**\n * Dev-only: warn when a second `useForm` lands on the same key with\n * a structurally-different schema. Two schemas resolve their own\n * fingerprints; we compare the strings and flag mismatches. An adapter\n * `fingerprint()` that rejects is caught (never crashes the form) and\n * surfaced as a `console.error` in dev: the mismatch check is skipped,\n * matching the \"allow the inconsistency\" failure mode. See\n * `AbstractSchema.fingerprint()` in types-api.ts for the contract.\n */\nexport async function warnOnSchemaFingerprintMismatch(\n key: FormKey,\n existing: AbstractSchema<GenericForm, GenericForm>,\n incoming: AbstractSchema<GenericForm, GenericForm>\n): Promise<void> {\n let existingFp: string\n let incomingFp: string\n try {\n existingFp = await existing.fingerprint()\n incomingFp = await incoming.fingerprint()\n } catch (error) {\n console.error(\n `[attaform] fingerprint() rejected for key \"${key}\"; skipping mismatch check.`,\n error\n )\n return\n }\n if (existingFp === incomingFp) return\n console.warn(\n `[attaform] useForm() calls with key \"${key}\" use different schemas; first wins, second is ignored. Use identical schemas or unique keys.\\n existing: ${existingFp}\\n incoming: ${incomingFp}`\n )\n}\n"],"names":[],"mappings":"AAgCA,eAAsB,+BAAA,CACpB,GAAA,EACA,QAAA,EACA,QAAA,EACe;AACf,EAAA,IAAI,UAAA;AACJ,EAAA,IAAI,UAAA;AACJ,EAAA,IAAI;AACF,IAAA,UAAA,GAAa,MAAM,SAAS,WAAA,EAAY;AACxC,IAAA,UAAA,GAAa,MAAM,SAAS,WAAA,EAAY;AAAA,EAC1C,SAAS,KAAA,EAAO;AACd,IAAA,OAAA,CAAQ,KAAA;AAAA,MACN,8CAA8C,GAAG,CAAA,2BAAA,CAAA;AAAA,MACjD;AAAA,KACF;AACA,IAAA;AAAA,EACF;AACA,EAAA,IAAI,eAAe,UAAA,EAAY;AAC/B,EAAA,OAAA,CAAQ,IAAA;AAAA,IACN,wCAAwC,GAAG,CAAA;AAAA,YAAA,EAA8G,UAAU;AAAA,YAAA,EAAiB,UAAU,CAAA;AAAA,GAChM;AACF;;;;"}
|
package/dist/chunks/devtools.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const paths = require('../shared/attaform.
|
|
3
|
+
const paths = require('../shared/attaform.Db4E4IW6.cjs');
|
|
4
4
|
|
|
5
5
|
const INSPECTOR_ID = "attaform";
|
|
6
6
|
const TIMELINE_LAYER_ID = "attaform:events";
|
|
@@ -145,10 +145,8 @@ function wire(api, app, registry) {
|
|
|
145
145
|
const section = payload.path[0];
|
|
146
146
|
if (section !== "Form value") return;
|
|
147
147
|
const segments = payload.path.slice(2);
|
|
148
|
-
const { segments: canonicalPath
|
|
149
|
-
state.setValueAtPath(canonicalPath, payload.state.value
|
|
150
|
-
persist: state.persistOptIns.hasAnyOptInForPath(canonicalKey)
|
|
151
|
-
});
|
|
148
|
+
const { segments: canonicalPath } = paths.canonicalizePath(segments);
|
|
149
|
+
state.setValueAtPath(canonicalPath, payload.state.value);
|
|
152
150
|
refreshState();
|
|
153
151
|
});
|
|
154
152
|
syncForms();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"devtools.cjs","sources":["../../src/runtime/core/devtools.ts"],"sourcesContent":["import type { App } from 'vue'\nimport type { FormStore } from './create-form-store'\nimport type { AttaformRegistry } from './registry'\nimport type { GenericForm } from '../types/types-core'\nimport type { FormKey } from '../types/types-api'\nimport { canonicalizePath } from './paths'\n\n/**\n * Vue DevTools plugin wiring for attaform. Lazy-imported by\n * `createAttaform` under dev-mode guards so the production\n * bundle tree-shakes it out entirely.\n *\n * Registers:\n * - An inspector (per-app) that lists every registered form, with\n * nodes for form value / errors / aggregates / history.\n * - A timeline layer that emits events on submit start/success/\n * failure, reset, undo, redo, and form mutations.\n * - State editing — modifying a leaf inside the inspector tree\n * pushes through `state.setValueAtPath`, mutating the form.\n *\n * Tolerant of missing `@vue/devtools-api` — the peer dep is marked\n * optional. If the import fails, `setupAttaformDevtools` silently\n * no-ops so production builds / users without DevTools installed\n * don't see errors.\n */\n\nconst INSPECTOR_ID = 'attaform'\nconst TIMELINE_LAYER_ID = 'attaform:events'\n\ntype UnsafeDevtoolsApi = {\n addInspector(opts: { id: string; label: string; icon?: string; app: App }): void\n addTimelineLayer(opts: { id: string; label: string; color: number }): void\n sendInspectorTree(inspectorId: string): void\n sendInspectorState(inspectorId: string): void\n addTimelineEvent(payload: {\n layerId: string\n event: {\n time: number\n title: string\n subtitle?: string\n data?: Record<string, unknown>\n groupId?: string | number\n }\n }): void\n on: {\n getInspectorTree(\n handler: (payload: {\n inspectorId: string\n filter: string\n rootNodes: Array<{ id: string; label: string; tags?: unknown[] }>\n }) => void\n ): void\n getInspectorState(\n handler: (payload: {\n inspectorId: string\n nodeId: string\n state: Record<string, Array<{ key: string; value: unknown; editable?: boolean }>>\n }) => void\n ): void\n editInspectorState(\n handler: (payload: {\n inspectorId: string\n nodeId: string\n path: string[]\n state: { value: unknown; newKey?: string | null; remove?: boolean }\n }) => void\n ): void\n }\n}\n\ntype SetupDevtoolsPluginFn = (\n descriptor: {\n id: string\n label: string\n packageName?: string\n homepage?: string\n componentStateTypes?: string[]\n app: App\n },\n setup: (api: UnsafeDevtoolsApi) => void\n) => void\n\n/**\n * Install the DevTools plugin for the given Vue app + registry. Safe\n * to call in production — if `@vue/devtools-api` isn't installed, the\n * dynamic import fails and we log nothing. Returns `true` when\n * DevTools was wired successfully, `false` otherwise — useful for\n * tests.\n */\nexport async function setupAttaformDevtools(\n app: App,\n registry: AttaformRegistry\n): Promise<boolean> {\n let mod: { setupDevtoolsPlugin?: SetupDevtoolsPluginFn }\n try {\n mod = (await import('@vue/devtools-api')) as {\n setupDevtoolsPlugin?: SetupDevtoolsPluginFn\n }\n } catch {\n // Peer dep not installed — silently skip. Production builds pass\n // `{ devtools: false }` explicitly, but this catch covers the\n // \"dev without the peer dep\" case without a noisy warning.\n return false\n }\n const setupDevtoolsPlugin = mod.setupDevtoolsPlugin\n if (typeof setupDevtoolsPlugin !== 'function') return false\n\n setupDevtoolsPlugin(\n {\n id: INSPECTOR_ID,\n label: 'Attaform',\n packageName: 'attaform',\n homepage: 'https://github.com/attaform/Attaform',\n app,\n componentStateTypes: ['Attaform form'],\n },\n (api) => wire(api, app, registry)\n )\n return true\n}\n\nfunction wire(api: UnsafeDevtoolsApi, app: App, registry: AttaformRegistry): void {\n // Per-form subscriber bookkeeping — we keep the unsubscribers so\n // the registry's eviction path can detach them when a form is\n // disposed. Using a Map keyed by FormKey mirrors the registry.\n const subscriberUnsubs = new Map<FormKey, () => void>()\n\n api.addInspector({ id: INSPECTOR_ID, label: 'Attaform', app })\n api.addTimelineLayer({ id: TIMELINE_LAYER_ID, label: 'Attaform', color: 0x5b8def })\n\n function refreshTree(): void {\n api.sendInspectorTree(INSPECTOR_ID)\n }\n\n function refreshState(): void {\n api.sendInspectorState(INSPECTOR_ID)\n }\n\n function subscribeForm(state: FormStore<GenericForm>): void {\n if (subscriberUnsubs.has(state.formKey)) return\n const unsubChange = state.onFormChange(() => {\n refreshState()\n api.addTimelineEvent({\n layerId: TIMELINE_LAYER_ID,\n event: {\n time: Date.now(),\n title: 'form.change',\n subtitle: state.formKey,\n // Devtools is dev-only — emit raw values. Consumers worried\n // about screen-share leaks should close the panel before\n // sharing, same as they would for the browser DevTools\n // console.\n data: { form: state.form.value as Record<string, unknown> },\n },\n })\n })\n const unsubSubmit = state.onSubmitSuccess(() => {\n api.addTimelineEvent({\n layerId: TIMELINE_LAYER_ID,\n event: {\n time: Date.now(),\n title: 'submit.success',\n subtitle: state.formKey,\n data: { form: state.form.value as Record<string, unknown> },\n },\n })\n })\n const unsubReset = state.onReset(() => {\n refreshState()\n api.addTimelineEvent({\n layerId: TIMELINE_LAYER_ID,\n event: {\n time: Date.now(),\n title: 'reset',\n subtitle: state.formKey,\n },\n })\n })\n subscriberUnsubs.set(state.formKey, () => {\n unsubChange()\n unsubSubmit()\n unsubReset()\n })\n }\n\n // Subscribe all currently-registered forms + register as they're\n // added. The registry's `forms` Map is shallowReactive — we poll\n // once per render on refresh; for live change detection, each\n // useForm call that adds a new form triggers a tree/state refresh\n // via the form's own onFormChange emission on the first\n // applyFormReplacement.\n function syncForms(): void {\n for (const [, state] of registry.forms) {\n subscribeForm(state)\n }\n // Drop subscribers for forms that were evicted.\n for (const [formKey, unsub] of subscriberUnsubs) {\n if (!registry.forms.has(formKey)) {\n unsub()\n subscriberUnsubs.delete(formKey)\n }\n }\n }\n\n api.on.getInspectorTree((payload) => {\n if (payload.inspectorId !== INSPECTOR_ID) return\n syncForms()\n payload.rootNodes = [...registry.forms.keys()].map((key) => ({\n id: `form:${key}`,\n label: key,\n tags: [],\n }))\n })\n\n api.on.getInspectorState((payload) => {\n if (payload.inspectorId !== INSPECTOR_ID) return\n if (!payload.nodeId.startsWith('form:')) return\n const formKey = payload.nodeId.slice('form:'.length)\n const state = registry.forms.get(formKey)\n if (state === undefined) return\n // Devtools is dev-only — render raw values for everything,\n // including sensitive-named paths. Consumers concerned about\n // screen-share leaks should close the panel before sharing.\n payload.state['Form value'] = [\n {\n key: 'form',\n value: state.form.value,\n editable: true,\n },\n ]\n // Schema-driven and user-injected errors land in separate inspector\n // sections so devs can see the source distinction at a glance — a\n // user-injected entry surviving a successful submit, or a schema\n // entry that should have cleared after a value fix, are immediately\n // visible without cross-referencing call sites.\n payload.state['Schema Errors'] = [\n ...[...state.schemaErrors.entries()].map(([k, v]) => ({\n key: String(k),\n value: v as unknown,\n })),\n ]\n payload.state['User Errors'] = [\n ...[...state.userErrors.entries()].map(([k, v]) => ({\n key: String(k),\n value: v as unknown,\n })),\n ]\n payload.state['Aggregates'] = [\n { key: 'submitting', value: state.submitting.value },\n { key: 'submissionAttempts', value: state.submissionAttempts.value },\n { key: 'departAttempts', value: state.departAttempts.value },\n { key: 'submitError', value: state.submitError.value },\n { key: 'activeValidations', value: state.activeValidations.value },\n ]\n })\n\n api.on.editInspectorState((payload) => {\n if (payload.inspectorId !== INSPECTOR_ID) return\n if (!payload.nodeId.startsWith('form:')) return\n const formKey = payload.nodeId.slice('form:'.length)\n const state = registry.forms.get(formKey)\n if (state === undefined) return\n // payload.path is `['Form value', 'form', ...pathSegments]` — the\n // first two segments are the inspector section + key, the rest is\n // the target form path the user edited. Pass the segment array\n // directly to `canonicalizePath`: join('.') would collapse a\n // literal-dot field key (`{\"user.email\": ...}`) into two segments,\n // writing to the wrong leaf.\n if (payload.path.length < 3) return\n const section = payload.path[0]\n if (section !== 'Form value') return\n const segments = payload.path.slice(2)\n const { segments: canonicalPath, key: canonicalKey } = canonicalizePath(segments)\n // A devtools edit on a path that any element has opted in to should\n // persist (matches the user's expectation: editing via the inspector\n // should be indistinguishable from typing into the bound input).\n // No opt-in for this path → no write.\n state.setValueAtPath(canonicalPath, payload.state.value, {\n persist: state.persistOptIns.hasAnyOptInForPath(canonicalKey),\n })\n refreshState()\n })\n\n // Initial sync so existing forms show up.\n syncForms()\n refreshTree()\n}\n"],"names":["canonicalizePath"],"mappings":";;;;AA0BA,MAAM,YAAA,GAAe,UAAA;AACrB,MAAM,iBAAA,GAAoB,iBAAA;AA8D1B,eAAsB,qBAAA,CACpB,KACA,QAAA,EACkB;AAClB,EAAA,IAAI,GAAA;AACJ,EAAA,IAAI;AACF,IAAA,GAAA,GAAO,MAAM,OAAO,mBAAmB,CAAA;AAAA,EAGzC,CAAA,CAAA,MAAQ;AAIN,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,MAAM,sBAAsB,GAAA,CAAI,mBAAA;AAChC,EAAA,IAAI,OAAO,mBAAA,KAAwB,UAAA,EAAY,OAAO,KAAA;AAEtD,EAAA,mBAAA;AAAA,IACE;AAAA,MACE,EAAA,EAAI,YAAA;AAAA,MACJ,KAAA,EAAO,UAAA;AAAA,MACP,WAAA,EAAa,UAAA;AAAA,MACb,QAAA,EAAU,sCAAA;AAAA,MACV,GAAA;AAAA,MACA,mBAAA,EAAqB,CAAC,eAAe;AAAA,KACvC;AAAA,IACA,CAAC,GAAA,KAAQ,IAAA,CAAK,GAAA,EAAK,KAAK,QAAQ;AAAA,GAClC;AACA,EAAA,OAAO,IAAA;AACT;AAEA,SAAS,IAAA,CAAK,GAAA,EAAwB,GAAA,EAAU,QAAA,EAAkC;AAIhF,EAAA,MAAM,gBAAA,uBAAuB,GAAA,EAAyB;AAEtD,EAAA,GAAA,CAAI,aAAa,EAAE,EAAA,EAAI,cAAc,KAAA,EAAO,UAAA,EAAY,KAAK,CAAA;AAC7D,EAAA,GAAA,CAAI,gBAAA,CAAiB,EAAE,EAAA,EAAI,iBAAA,EAAmB,OAAO,UAAA,EAAY,KAAA,EAAO,SAAU,CAAA;AAElF,EAAA,SAAS,WAAA,GAAoB;AAC3B,IAAA,GAAA,CAAI,kBAAkB,YAAY,CAAA;AAAA,EACpC;AAEA,EAAA,SAAS,YAAA,GAAqB;AAC5B,IAAA,GAAA,CAAI,mBAAmB,YAAY,CAAA;AAAA,EACrC;AAEA,EAAA,SAAS,cAAc,KAAA,EAAqC;AAC1D,IAAA,IAAI,gBAAA,CAAiB,GAAA,CAAI,KAAA,CAAM,OAAO,CAAA,EAAG;AACzC,IAAA,MAAM,WAAA,GAAc,KAAA,CAAM,YAAA,CAAa,MAAM;AAC3C,MAAA,YAAA,EAAa;AACb,MAAA,GAAA,CAAI,gBAAA,CAAiB;AAAA,QACnB,OAAA,EAAS,iBAAA;AAAA,QACT,KAAA,EAAO;AAAA,UACL,IAAA,EAAM,KAAK,GAAA,EAAI;AAAA,UACf,KAAA,EAAO,aAAA;AAAA,UACP,UAAU,KAAA,CAAM,OAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAKhB,IAAA,EAAM,EAAE,IAAA,EAAM,KAAA,CAAM,KAAK,KAAA;AAAiC;AAC5D,OACD,CAAA;AAAA,IACH,CAAC,CAAA;AACD,IAAA,MAAM,WAAA,GAAc,KAAA,CAAM,eAAA,CAAgB,MAAM;AAC9C,MAAA,GAAA,CAAI,gBAAA,CAAiB;AAAA,QACnB,OAAA,EAAS,iBAAA;AAAA,QACT,KAAA,EAAO;AAAA,UACL,IAAA,EAAM,KAAK,GAAA,EAAI;AAAA,UACf,KAAA,EAAO,gBAAA;AAAA,UACP,UAAU,KAAA,CAAM,OAAA;AAAA,UAChB,IAAA,EAAM,EAAE,IAAA,EAAM,KAAA,CAAM,KAAK,KAAA;AAAiC;AAC5D,OACD,CAAA;AAAA,IACH,CAAC,CAAA;AACD,IAAA,MAAM,UAAA,GAAa,KAAA,CAAM,OAAA,CAAQ,MAAM;AACrC,MAAA,YAAA,EAAa;AACb,MAAA,GAAA,CAAI,gBAAA,CAAiB;AAAA,QACnB,OAAA,EAAS,iBAAA;AAAA,QACT,KAAA,EAAO;AAAA,UACL,IAAA,EAAM,KAAK,GAAA,EAAI;AAAA,UACf,KAAA,EAAO,OAAA;AAAA,UACP,UAAU,KAAA,CAAM;AAAA;AAClB,OACD,CAAA;AAAA,IACH,CAAC,CAAA;AACD,IAAA,gBAAA,CAAiB,GAAA,CAAI,KAAA,CAAM,OAAA,EAAS,MAAM;AACxC,MAAA,WAAA,EAAY;AACZ,MAAA,WAAA,EAAY;AACZ,MAAA,UAAA,EAAW;AAAA,IACb,CAAC,CAAA;AAAA,EACH;AAQA,EAAA,SAAS,SAAA,GAAkB;AACzB,IAAA,KAAA,MAAW,GAAG,KAAK,CAAA,IAAK,SAAS,KAAA,EAAO;AACtC,MAAA,aAAA,CAAc,KAAK,CAAA;AAAA,IACrB;AAEA,IAAA,KAAA,MAAW,CAAC,OAAA,EAAS,KAAK,CAAA,IAAK,gBAAA,EAAkB;AAC/C,MAAA,IAAI,CAAC,QAAA,CAAS,KAAA,CAAM,GAAA,CAAI,OAAO,CAAA,EAAG;AAChC,QAAA,KAAA,EAAM;AACN,QAAA,gBAAA,CAAiB,OAAO,OAAO,CAAA;AAAA,MACjC;AAAA,IACF;AAAA,EACF;AAEA,EAAA,GAAA,CAAI,EAAA,CAAG,gBAAA,CAAiB,CAAC,OAAA,KAAY;AACnC,IAAA,IAAI,OAAA,CAAQ,gBAAgB,YAAA,EAAc;AAC1C,IAAA,SAAA,EAAU;AACV,IAAA,OAAA,CAAQ,SAAA,GAAY,CAAC,GAAG,QAAA,CAAS,KAAA,CAAM,MAAM,CAAA,CAAE,GAAA,CAAI,CAAC,GAAA,MAAS;AAAA,MAC3D,EAAA,EAAI,QAAQ,GAAG,CAAA,CAAA;AAAA,MACf,KAAA,EAAO,GAAA;AAAA,MACP,MAAM;AAAC,KACT,CAAE,CAAA;AAAA,EACJ,CAAC,CAAA;AAED,EAAA,GAAA,CAAI,EAAA,CAAG,iBAAA,CAAkB,CAAC,OAAA,KAAY;AACpC,IAAA,IAAI,OAAA,CAAQ,gBAAgB,YAAA,EAAc;AAC1C,IAAA,IAAI,CAAC,OAAA,CAAQ,MAAA,CAAO,UAAA,CAAW,OAAO,CAAA,EAAG;AACzC,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,MAAA,CAAO,KAAA,CAAM,QAAQ,MAAM,CAAA;AACnD,IAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,KAAA,CAAM,GAAA,CAAI,OAAO,CAAA;AACxC,IAAA,IAAI,UAAU,MAAA,EAAW;AAIzB,IAAA,OAAA,CAAQ,KAAA,CAAM,YAAY,CAAA,GAAI;AAAA,MAC5B;AAAA,QACE,GAAA,EAAK,MAAA;AAAA,QACL,KAAA,EAAO,MAAM,IAAA,CAAK,KAAA;AAAA,QAClB,QAAA,EAAU;AAAA;AACZ,KACF;AAMA,IAAA,OAAA,CAAQ,KAAA,CAAM,eAAe,CAAA,GAAI;AAAA,MAC/B,GAAG,CAAC,GAAG,KAAA,CAAM,YAAA,CAAa,OAAA,EAAS,CAAA,CAAE,GAAA,CAAI,CAAC,CAAC,CAAA,EAAG,CAAC,CAAA,MAAO;AAAA,QACpD,GAAA,EAAK,OAAO,CAAC,CAAA;AAAA,QACb,KAAA,EAAO;AAAA,OACT,CAAE;AAAA,KACJ;AACA,IAAA,OAAA,CAAQ,KAAA,CAAM,aAAa,CAAA,GAAI;AAAA,MAC7B,GAAG,CAAC,GAAG,KAAA,CAAM,UAAA,CAAW,OAAA,EAAS,CAAA,CAAE,GAAA,CAAI,CAAC,CAAC,CAAA,EAAG,CAAC,CAAA,MAAO;AAAA,QAClD,GAAA,EAAK,OAAO,CAAC,CAAA;AAAA,QACb,KAAA,EAAO;AAAA,OACT,CAAE;AAAA,KACJ;AACA,IAAA,OAAA,CAAQ,KAAA,CAAM,YAAY,CAAA,GAAI;AAAA,MAC5B,EAAE,GAAA,EAAK,YAAA,EAAc,KAAA,EAAO,KAAA,CAAM,WAAW,KAAA,EAAM;AAAA,MACnD,EAAE,GAAA,EAAK,oBAAA,EAAsB,KAAA,EAAO,KAAA,CAAM,mBAAmB,KAAA,EAAM;AAAA,MACnE,EAAE,GAAA,EAAK,gBAAA,EAAkB,KAAA,EAAO,KAAA,CAAM,eAAe,KAAA,EAAM;AAAA,MAC3D,EAAE,GAAA,EAAK,aAAA,EAAe,KAAA,EAAO,KAAA,CAAM,YAAY,KAAA,EAAM;AAAA,MACrD,EAAE,GAAA,EAAK,mBAAA,EAAqB,KAAA,EAAO,KAAA,CAAM,kBAAkB,KAAA;AAAM,KACnE;AAAA,EACF,CAAC,CAAA;AAED,EAAA,GAAA,CAAI,EAAA,CAAG,kBAAA,CAAmB,CAAC,OAAA,KAAY;AACrC,IAAA,IAAI,OAAA,CAAQ,gBAAgB,YAAA,EAAc;AAC1C,IAAA,IAAI,CAAC,OAAA,CAAQ,MAAA,CAAO,UAAA,CAAW,OAAO,CAAA,EAAG;AACzC,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,MAAA,CAAO,KAAA,CAAM,QAAQ,MAAM,CAAA;AACnD,IAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,KAAA,CAAM,GAAA,CAAI,OAAO,CAAA;AACxC,IAAA,IAAI,UAAU,MAAA,EAAW;AAOzB,IAAA,IAAI,OAAA,CAAQ,IAAA,CAAK,MAAA,GAAS,CAAA,EAAG;AAC7B,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,IAAA,CAAK,CAAC,CAAA;AAC9B,IAAA,IAAI,YAAY,YAAA,EAAc;AAC9B,IAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,IAAA,CAAK,KAAA,CAAM,CAAC,CAAA;AACrC,IAAA,MAAM,EAAE,QAAA,EAAU,aAAA,EAAe,KAAK,YAAA,EAAa,GAAIA,uBAAiB,QAAQ,CAAA;AAKhF,IAAA,KAAA,CAAM,cAAA,CAAe,aAAA,EAAe,OAAA,CAAQ,KAAA,CAAM,KAAA,EAAO;AAAA,MACvD,OAAA,EAAS,KAAA,CAAM,aAAA,CAAc,kBAAA,CAAmB,YAAY;AAAA,KAC7D,CAAA;AACD,IAAA,YAAA,EAAa;AAAA,EACf,CAAC,CAAA;AAGD,EAAA,SAAA,EAAU;AACV,EAAA,WAAA,EAAY;AACd;;;;"}
|
|
1
|
+
{"version":3,"file":"devtools.cjs","sources":["../../src/runtime/core/devtools.ts"],"sourcesContent":["import type { App } from 'vue'\nimport type { FormStore } from './create-form-store'\nimport type { AttaformRegistry } from './registry'\nimport type { GenericForm } from '../types/types-core'\nimport type { FormKey } from '../types/types-api'\nimport { canonicalizePath } from './paths'\n\n/**\n * Vue DevTools plugin wiring for attaform. Lazy-imported by\n * `createAttaform` under dev-mode guards so the production\n * bundle tree-shakes it out entirely.\n *\n * Registers:\n * - An inspector (per-app) that lists every registered form, with\n * nodes for form value / errors / aggregates / history.\n * - A timeline layer that emits events on submit start/success/\n * failure, reset, undo, redo, and form mutations.\n * - State editing — modifying a leaf inside the inspector tree\n * pushes through `state.setValueAtPath`, mutating the form.\n *\n * Tolerant of missing `@vue/devtools-api` — the peer dep is marked\n * optional. If the import fails, `setupAttaformDevtools` silently\n * no-ops so production builds / users without DevTools installed\n * don't see errors.\n */\n\nconst INSPECTOR_ID = 'attaform'\nconst TIMELINE_LAYER_ID = 'attaform:events'\n\ntype UnsafeDevtoolsApi = {\n addInspector(opts: { id: string; label: string; icon?: string; app: App }): void\n addTimelineLayer(opts: { id: string; label: string; color: number }): void\n sendInspectorTree(inspectorId: string): void\n sendInspectorState(inspectorId: string): void\n addTimelineEvent(payload: {\n layerId: string\n event: {\n time: number\n title: string\n subtitle?: string\n data?: Record<string, unknown>\n groupId?: string | number\n }\n }): void\n on: {\n getInspectorTree(\n handler: (payload: {\n inspectorId: string\n filter: string\n rootNodes: Array<{ id: string; label: string; tags?: unknown[] }>\n }) => void\n ): void\n getInspectorState(\n handler: (payload: {\n inspectorId: string\n nodeId: string\n state: Record<string, Array<{ key: string; value: unknown; editable?: boolean }>>\n }) => void\n ): void\n editInspectorState(\n handler: (payload: {\n inspectorId: string\n nodeId: string\n path: string[]\n state: { value: unknown; newKey?: string | null; remove?: boolean }\n }) => void\n ): void\n }\n}\n\ntype SetupDevtoolsPluginFn = (\n descriptor: {\n id: string\n label: string\n packageName?: string\n homepage?: string\n componentStateTypes?: string[]\n app: App\n },\n setup: (api: UnsafeDevtoolsApi) => void\n) => void\n\n/**\n * Install the DevTools plugin for the given Vue app + registry. Safe\n * to call in production — if `@vue/devtools-api` isn't installed, the\n * dynamic import fails and we log nothing. Returns `true` when\n * DevTools was wired successfully, `false` otherwise — useful for\n * tests.\n */\nexport async function setupAttaformDevtools(\n app: App,\n registry: AttaformRegistry\n): Promise<boolean> {\n let mod: { setupDevtoolsPlugin?: SetupDevtoolsPluginFn }\n try {\n mod = (await import('@vue/devtools-api')) as {\n setupDevtoolsPlugin?: SetupDevtoolsPluginFn\n }\n } catch {\n // Peer dep not installed — silently skip. Production builds pass\n // `{ devtools: false }` explicitly, but this catch covers the\n // \"dev without the peer dep\" case without a noisy warning.\n return false\n }\n const setupDevtoolsPlugin = mod.setupDevtoolsPlugin\n if (typeof setupDevtoolsPlugin !== 'function') return false\n\n setupDevtoolsPlugin(\n {\n id: INSPECTOR_ID,\n label: 'Attaform',\n packageName: 'attaform',\n homepage: 'https://github.com/attaform/Attaform',\n app,\n componentStateTypes: ['Attaform form'],\n },\n (api) => wire(api, app, registry)\n )\n return true\n}\n\nfunction wire(api: UnsafeDevtoolsApi, app: App, registry: AttaformRegistry): void {\n // Per-form subscriber bookkeeping — we keep the unsubscribers so\n // the registry's eviction path can detach them when a form is\n // disposed. Using a Map keyed by FormKey mirrors the registry.\n const subscriberUnsubs = new Map<FormKey, () => void>()\n\n api.addInspector({ id: INSPECTOR_ID, label: 'Attaform', app })\n api.addTimelineLayer({ id: TIMELINE_LAYER_ID, label: 'Attaform', color: 0x5b8def })\n\n function refreshTree(): void {\n api.sendInspectorTree(INSPECTOR_ID)\n }\n\n function refreshState(): void {\n api.sendInspectorState(INSPECTOR_ID)\n }\n\n function subscribeForm(state: FormStore<GenericForm>): void {\n if (subscriberUnsubs.has(state.formKey)) return\n const unsubChange = state.onFormChange(() => {\n refreshState()\n api.addTimelineEvent({\n layerId: TIMELINE_LAYER_ID,\n event: {\n time: Date.now(),\n title: 'form.change',\n subtitle: state.formKey,\n // Devtools is dev-only — emit raw values. Consumers worried\n // about screen-share leaks should close the panel before\n // sharing, same as they would for the browser DevTools\n // console.\n data: { form: state.form.value as Record<string, unknown> },\n },\n })\n })\n const unsubSubmit = state.onSubmitSuccess(() => {\n api.addTimelineEvent({\n layerId: TIMELINE_LAYER_ID,\n event: {\n time: Date.now(),\n title: 'submit.success',\n subtitle: state.formKey,\n data: { form: state.form.value as Record<string, unknown> },\n },\n })\n })\n const unsubReset = state.onReset(() => {\n refreshState()\n api.addTimelineEvent({\n layerId: TIMELINE_LAYER_ID,\n event: {\n time: Date.now(),\n title: 'reset',\n subtitle: state.formKey,\n },\n })\n })\n subscriberUnsubs.set(state.formKey, () => {\n unsubChange()\n unsubSubmit()\n unsubReset()\n })\n }\n\n // Subscribe all currently-registered forms + register as they're\n // added. The registry's `forms` Map is shallowReactive — we poll\n // once per render on refresh; for live change detection, each\n // useForm call that adds a new form triggers a tree/state refresh\n // via the form's own onFormChange emission on the first\n // applyFormReplacement.\n function syncForms(): void {\n for (const [, state] of registry.forms) {\n subscribeForm(state)\n }\n // Drop subscribers for forms that were evicted.\n for (const [formKey, unsub] of subscriberUnsubs) {\n if (!registry.forms.has(formKey)) {\n unsub()\n subscriberUnsubs.delete(formKey)\n }\n }\n }\n\n api.on.getInspectorTree((payload) => {\n if (payload.inspectorId !== INSPECTOR_ID) return\n syncForms()\n payload.rootNodes = [...registry.forms.keys()].map((key) => ({\n id: `form:${key}`,\n label: key,\n tags: [],\n }))\n })\n\n api.on.getInspectorState((payload) => {\n if (payload.inspectorId !== INSPECTOR_ID) return\n if (!payload.nodeId.startsWith('form:')) return\n const formKey = payload.nodeId.slice('form:'.length)\n const state = registry.forms.get(formKey)\n if (state === undefined) return\n // Devtools is dev-only — render raw values for everything,\n // including sensitive-named paths. Consumers concerned about\n // screen-share leaks should close the panel before sharing.\n payload.state['Form value'] = [\n {\n key: 'form',\n value: state.form.value,\n editable: true,\n },\n ]\n // Schema-driven and user-injected errors land in separate inspector\n // sections so devs can see the source distinction at a glance — a\n // user-injected entry surviving a successful submit, or a schema\n // entry that should have cleared after a value fix, are immediately\n // visible without cross-referencing call sites.\n payload.state['Schema Errors'] = [\n ...[...state.schemaErrors.entries()].map(([k, v]) => ({\n key: String(k),\n value: v as unknown,\n })),\n ]\n payload.state['User Errors'] = [\n ...[...state.userErrors.entries()].map(([k, v]) => ({\n key: String(k),\n value: v as unknown,\n })),\n ]\n payload.state['Aggregates'] = [\n { key: 'submitting', value: state.submitting.value },\n { key: 'submissionAttempts', value: state.submissionAttempts.value },\n { key: 'departAttempts', value: state.departAttempts.value },\n { key: 'submitError', value: state.submitError.value },\n { key: 'activeValidations', value: state.activeValidations.value },\n ]\n })\n\n api.on.editInspectorState((payload) => {\n if (payload.inspectorId !== INSPECTOR_ID) return\n if (!payload.nodeId.startsWith('form:')) return\n const formKey = payload.nodeId.slice('form:'.length)\n const state = registry.forms.get(formKey)\n if (state === undefined) return\n // payload.path is `['Form value', 'form', ...pathSegments]` — the\n // first two segments are the inspector section + key, the rest is\n // the target form path the user edited. Pass the segment array\n // directly to `canonicalizePath`: join('.') would collapse a\n // literal-dot field key (`{\"user.email\": ...}`) into two segments,\n // writing to the wrong leaf.\n if (payload.path.length < 3) return\n const section = payload.path[0]\n if (section !== 'Form value') return\n const segments = payload.path.slice(2)\n const { segments: canonicalPath } = canonicalizePath(segments)\n state.setValueAtPath(canonicalPath, payload.state.value)\n refreshState()\n })\n\n // Initial sync so existing forms show up.\n syncForms()\n refreshTree()\n}\n"],"names":["canonicalizePath"],"mappings":"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"}
|
package/dist/chunks/devtools.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as canonicalizePath } from '../shared/attaform.
|
|
1
|
+
import { a as canonicalizePath } from '../shared/attaform.BhI9Icek.mjs';
|
|
2
2
|
|
|
3
3
|
const INSPECTOR_ID = "attaform";
|
|
4
4
|
const TIMELINE_LAYER_ID = "attaform:events";
|
|
@@ -143,10 +143,8 @@ function wire(api, app, registry) {
|
|
|
143
143
|
const section = payload.path[0];
|
|
144
144
|
if (section !== "Form value") return;
|
|
145
145
|
const segments = payload.path.slice(2);
|
|
146
|
-
const { segments: canonicalPath
|
|
147
|
-
state.setValueAtPath(canonicalPath, payload.state.value
|
|
148
|
-
persist: state.persistOptIns.hasAnyOptInForPath(canonicalKey)
|
|
149
|
-
});
|
|
146
|
+
const { segments: canonicalPath } = canonicalizePath(segments);
|
|
147
|
+
state.setValueAtPath(canonicalPath, payload.state.value);
|
|
150
148
|
refreshState();
|
|
151
149
|
});
|
|
152
150
|
syncForms();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"devtools.mjs","sources":["../../src/runtime/core/devtools.ts"],"sourcesContent":["import type { App } from 'vue'\nimport type { FormStore } from './create-form-store'\nimport type { AttaformRegistry } from './registry'\nimport type { GenericForm } from '../types/types-core'\nimport type { FormKey } from '../types/types-api'\nimport { canonicalizePath } from './paths'\n\n/**\n * Vue DevTools plugin wiring for attaform. Lazy-imported by\n * `createAttaform` under dev-mode guards so the production\n * bundle tree-shakes it out entirely.\n *\n * Registers:\n * - An inspector (per-app) that lists every registered form, with\n * nodes for form value / errors / aggregates / history.\n * - A timeline layer that emits events on submit start/success/\n * failure, reset, undo, redo, and form mutations.\n * - State editing — modifying a leaf inside the inspector tree\n * pushes through `state.setValueAtPath`, mutating the form.\n *\n * Tolerant of missing `@vue/devtools-api` — the peer dep is marked\n * optional. If the import fails, `setupAttaformDevtools` silently\n * no-ops so production builds / users without DevTools installed\n * don't see errors.\n */\n\nconst INSPECTOR_ID = 'attaform'\nconst TIMELINE_LAYER_ID = 'attaform:events'\n\ntype UnsafeDevtoolsApi = {\n addInspector(opts: { id: string; label: string; icon?: string; app: App }): void\n addTimelineLayer(opts: { id: string; label: string; color: number }): void\n sendInspectorTree(inspectorId: string): void\n sendInspectorState(inspectorId: string): void\n addTimelineEvent(payload: {\n layerId: string\n event: {\n time: number\n title: string\n subtitle?: string\n data?: Record<string, unknown>\n groupId?: string | number\n }\n }): void\n on: {\n getInspectorTree(\n handler: (payload: {\n inspectorId: string\n filter: string\n rootNodes: Array<{ id: string; label: string; tags?: unknown[] }>\n }) => void\n ): void\n getInspectorState(\n handler: (payload: {\n inspectorId: string\n nodeId: string\n state: Record<string, Array<{ key: string; value: unknown; editable?: boolean }>>\n }) => void\n ): void\n editInspectorState(\n handler: (payload: {\n inspectorId: string\n nodeId: string\n path: string[]\n state: { value: unknown; newKey?: string | null; remove?: boolean }\n }) => void\n ): void\n }\n}\n\ntype SetupDevtoolsPluginFn = (\n descriptor: {\n id: string\n label: string\n packageName?: string\n homepage?: string\n componentStateTypes?: string[]\n app: App\n },\n setup: (api: UnsafeDevtoolsApi) => void\n) => void\n\n/**\n * Install the DevTools plugin for the given Vue app + registry. Safe\n * to call in production — if `@vue/devtools-api` isn't installed, the\n * dynamic import fails and we log nothing. Returns `true` when\n * DevTools was wired successfully, `false` otherwise — useful for\n * tests.\n */\nexport async function setupAttaformDevtools(\n app: App,\n registry: AttaformRegistry\n): Promise<boolean> {\n let mod: { setupDevtoolsPlugin?: SetupDevtoolsPluginFn }\n try {\n mod = (await import('@vue/devtools-api')) as {\n setupDevtoolsPlugin?: SetupDevtoolsPluginFn\n }\n } catch {\n // Peer dep not installed — silently skip. Production builds pass\n // `{ devtools: false }` explicitly, but this catch covers the\n // \"dev without the peer dep\" case without a noisy warning.\n return false\n }\n const setupDevtoolsPlugin = mod.setupDevtoolsPlugin\n if (typeof setupDevtoolsPlugin !== 'function') return false\n\n setupDevtoolsPlugin(\n {\n id: INSPECTOR_ID,\n label: 'Attaform',\n packageName: 'attaform',\n homepage: 'https://github.com/attaform/Attaform',\n app,\n componentStateTypes: ['Attaform form'],\n },\n (api) => wire(api, app, registry)\n )\n return true\n}\n\nfunction wire(api: UnsafeDevtoolsApi, app: App, registry: AttaformRegistry): void {\n // Per-form subscriber bookkeeping — we keep the unsubscribers so\n // the registry's eviction path can detach them when a form is\n // disposed. Using a Map keyed by FormKey mirrors the registry.\n const subscriberUnsubs = new Map<FormKey, () => void>()\n\n api.addInspector({ id: INSPECTOR_ID, label: 'Attaform', app })\n api.addTimelineLayer({ id: TIMELINE_LAYER_ID, label: 'Attaform', color: 0x5b8def })\n\n function refreshTree(): void {\n api.sendInspectorTree(INSPECTOR_ID)\n }\n\n function refreshState(): void {\n api.sendInspectorState(INSPECTOR_ID)\n }\n\n function subscribeForm(state: FormStore<GenericForm>): void {\n if (subscriberUnsubs.has(state.formKey)) return\n const unsubChange = state.onFormChange(() => {\n refreshState()\n api.addTimelineEvent({\n layerId: TIMELINE_LAYER_ID,\n event: {\n time: Date.now(),\n title: 'form.change',\n subtitle: state.formKey,\n // Devtools is dev-only — emit raw values. Consumers worried\n // about screen-share leaks should close the panel before\n // sharing, same as they would for the browser DevTools\n // console.\n data: { form: state.form.value as Record<string, unknown> },\n },\n })\n })\n const unsubSubmit = state.onSubmitSuccess(() => {\n api.addTimelineEvent({\n layerId: TIMELINE_LAYER_ID,\n event: {\n time: Date.now(),\n title: 'submit.success',\n subtitle: state.formKey,\n data: { form: state.form.value as Record<string, unknown> },\n },\n })\n })\n const unsubReset = state.onReset(() => {\n refreshState()\n api.addTimelineEvent({\n layerId: TIMELINE_LAYER_ID,\n event: {\n time: Date.now(),\n title: 'reset',\n subtitle: state.formKey,\n },\n })\n })\n subscriberUnsubs.set(state.formKey, () => {\n unsubChange()\n unsubSubmit()\n unsubReset()\n })\n }\n\n // Subscribe all currently-registered forms + register as they're\n // added. The registry's `forms` Map is shallowReactive — we poll\n // once per render on refresh; for live change detection, each\n // useForm call that adds a new form triggers a tree/state refresh\n // via the form's own onFormChange emission on the first\n // applyFormReplacement.\n function syncForms(): void {\n for (const [, state] of registry.forms) {\n subscribeForm(state)\n }\n // Drop subscribers for forms that were evicted.\n for (const [formKey, unsub] of subscriberUnsubs) {\n if (!registry.forms.has(formKey)) {\n unsub()\n subscriberUnsubs.delete(formKey)\n }\n }\n }\n\n api.on.getInspectorTree((payload) => {\n if (payload.inspectorId !== INSPECTOR_ID) return\n syncForms()\n payload.rootNodes = [...registry.forms.keys()].map((key) => ({\n id: `form:${key}`,\n label: key,\n tags: [],\n }))\n })\n\n api.on.getInspectorState((payload) => {\n if (payload.inspectorId !== INSPECTOR_ID) return\n if (!payload.nodeId.startsWith('form:')) return\n const formKey = payload.nodeId.slice('form:'.length)\n const state = registry.forms.get(formKey)\n if (state === undefined) return\n // Devtools is dev-only — render raw values for everything,\n // including sensitive-named paths. Consumers concerned about\n // screen-share leaks should close the panel before sharing.\n payload.state['Form value'] = [\n {\n key: 'form',\n value: state.form.value,\n editable: true,\n },\n ]\n // Schema-driven and user-injected errors land in separate inspector\n // sections so devs can see the source distinction at a glance — a\n // user-injected entry surviving a successful submit, or a schema\n // entry that should have cleared after a value fix, are immediately\n // visible without cross-referencing call sites.\n payload.state['Schema Errors'] = [\n ...[...state.schemaErrors.entries()].map(([k, v]) => ({\n key: String(k),\n value: v as unknown,\n })),\n ]\n payload.state['User Errors'] = [\n ...[...state.userErrors.entries()].map(([k, v]) => ({\n key: String(k),\n value: v as unknown,\n })),\n ]\n payload.state['Aggregates'] = [\n { key: 'submitting', value: state.submitting.value },\n { key: 'submissionAttempts', value: state.submissionAttempts.value },\n { key: 'departAttempts', value: state.departAttempts.value },\n { key: 'submitError', value: state.submitError.value },\n { key: 'activeValidations', value: state.activeValidations.value },\n ]\n })\n\n api.on.editInspectorState((payload) => {\n if (payload.inspectorId !== INSPECTOR_ID) return\n if (!payload.nodeId.startsWith('form:')) return\n const formKey = payload.nodeId.slice('form:'.length)\n const state = registry.forms.get(formKey)\n if (state === undefined) return\n // payload.path is `['Form value', 'form', ...pathSegments]` — the\n // first two segments are the inspector section + key, the rest is\n // the target form path the user edited. Pass the segment array\n // directly to `canonicalizePath`: join('.') would collapse a\n // literal-dot field key (`{\"user.email\": ...}`) into two segments,\n // writing to the wrong leaf.\n if (payload.path.length < 3) return\n const section = payload.path[0]\n if (section !== 'Form value') return\n const segments = payload.path.slice(2)\n const { segments: canonicalPath, key: canonicalKey } = canonicalizePath(segments)\n // A devtools edit on a path that any element has opted in to should\n // persist (matches the user's expectation: editing via the inspector\n // should be indistinguishable from typing into the bound input).\n // No opt-in for this path → no write.\n state.setValueAtPath(canonicalPath, payload.state.value, {\n persist: state.persistOptIns.hasAnyOptInForPath(canonicalKey),\n })\n refreshState()\n })\n\n // Initial sync so existing forms show up.\n syncForms()\n refreshTree()\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"devtools.mjs","sources":["../../src/runtime/core/devtools.ts"],"sourcesContent":["import type { App } from 'vue'\nimport type { FormStore } from './create-form-store'\nimport type { AttaformRegistry } from './registry'\nimport type { GenericForm } from '../types/types-core'\nimport type { FormKey } from '../types/types-api'\nimport { canonicalizePath } from './paths'\n\n/**\n * Vue DevTools plugin wiring for attaform. Lazy-imported by\n * `createAttaform` under dev-mode guards so the production\n * bundle tree-shakes it out entirely.\n *\n * Registers:\n * - An inspector (per-app) that lists every registered form, with\n * nodes for form value / errors / aggregates / history.\n * - A timeline layer that emits events on submit start/success/\n * failure, reset, undo, redo, and form mutations.\n * - State editing — modifying a leaf inside the inspector tree\n * pushes through `state.setValueAtPath`, mutating the form.\n *\n * Tolerant of missing `@vue/devtools-api` — the peer dep is marked\n * optional. If the import fails, `setupAttaformDevtools` silently\n * no-ops so production builds / users without DevTools installed\n * don't see errors.\n */\n\nconst INSPECTOR_ID = 'attaform'\nconst TIMELINE_LAYER_ID = 'attaform:events'\n\ntype UnsafeDevtoolsApi = {\n addInspector(opts: { id: string; label: string; icon?: string; app: App }): void\n addTimelineLayer(opts: { id: string; label: string; color: number }): void\n sendInspectorTree(inspectorId: string): void\n sendInspectorState(inspectorId: string): void\n addTimelineEvent(payload: {\n layerId: string\n event: {\n time: number\n title: string\n subtitle?: string\n data?: Record<string, unknown>\n groupId?: string | number\n }\n }): void\n on: {\n getInspectorTree(\n handler: (payload: {\n inspectorId: string\n filter: string\n rootNodes: Array<{ id: string; label: string; tags?: unknown[] }>\n }) => void\n ): void\n getInspectorState(\n handler: (payload: {\n inspectorId: string\n nodeId: string\n state: Record<string, Array<{ key: string; value: unknown; editable?: boolean }>>\n }) => void\n ): void\n editInspectorState(\n handler: (payload: {\n inspectorId: string\n nodeId: string\n path: string[]\n state: { value: unknown; newKey?: string | null; remove?: boolean }\n }) => void\n ): void\n }\n}\n\ntype SetupDevtoolsPluginFn = (\n descriptor: {\n id: string\n label: string\n packageName?: string\n homepage?: string\n componentStateTypes?: string[]\n app: App\n },\n setup: (api: UnsafeDevtoolsApi) => void\n) => void\n\n/**\n * Install the DevTools plugin for the given Vue app + registry. Safe\n * to call in production — if `@vue/devtools-api` isn't installed, the\n * dynamic import fails and we log nothing. Returns `true` when\n * DevTools was wired successfully, `false` otherwise — useful for\n * tests.\n */\nexport async function setupAttaformDevtools(\n app: App,\n registry: AttaformRegistry\n): Promise<boolean> {\n let mod: { setupDevtoolsPlugin?: SetupDevtoolsPluginFn }\n try {\n mod = (await import('@vue/devtools-api')) as {\n setupDevtoolsPlugin?: SetupDevtoolsPluginFn\n }\n } catch {\n // Peer dep not installed — silently skip. Production builds pass\n // `{ devtools: false }` explicitly, but this catch covers the\n // \"dev without the peer dep\" case without a noisy warning.\n return false\n }\n const setupDevtoolsPlugin = mod.setupDevtoolsPlugin\n if (typeof setupDevtoolsPlugin !== 'function') return false\n\n setupDevtoolsPlugin(\n {\n id: INSPECTOR_ID,\n label: 'Attaform',\n packageName: 'attaform',\n homepage: 'https://github.com/attaform/Attaform',\n app,\n componentStateTypes: ['Attaform form'],\n },\n (api) => wire(api, app, registry)\n )\n return true\n}\n\nfunction wire(api: UnsafeDevtoolsApi, app: App, registry: AttaformRegistry): void {\n // Per-form subscriber bookkeeping — we keep the unsubscribers so\n // the registry's eviction path can detach them when a form is\n // disposed. Using a Map keyed by FormKey mirrors the registry.\n const subscriberUnsubs = new Map<FormKey, () => void>()\n\n api.addInspector({ id: INSPECTOR_ID, label: 'Attaform', app })\n api.addTimelineLayer({ id: TIMELINE_LAYER_ID, label: 'Attaform', color: 0x5b8def })\n\n function refreshTree(): void {\n api.sendInspectorTree(INSPECTOR_ID)\n }\n\n function refreshState(): void {\n api.sendInspectorState(INSPECTOR_ID)\n }\n\n function subscribeForm(state: FormStore<GenericForm>): void {\n if (subscriberUnsubs.has(state.formKey)) return\n const unsubChange = state.onFormChange(() => {\n refreshState()\n api.addTimelineEvent({\n layerId: TIMELINE_LAYER_ID,\n event: {\n time: Date.now(),\n title: 'form.change',\n subtitle: state.formKey,\n // Devtools is dev-only — emit raw values. Consumers worried\n // about screen-share leaks should close the panel before\n // sharing, same as they would for the browser DevTools\n // console.\n data: { form: state.form.value as Record<string, unknown> },\n },\n })\n })\n const unsubSubmit = state.onSubmitSuccess(() => {\n api.addTimelineEvent({\n layerId: TIMELINE_LAYER_ID,\n event: {\n time: Date.now(),\n title: 'submit.success',\n subtitle: state.formKey,\n data: { form: state.form.value as Record<string, unknown> },\n },\n })\n })\n const unsubReset = state.onReset(() => {\n refreshState()\n api.addTimelineEvent({\n layerId: TIMELINE_LAYER_ID,\n event: {\n time: Date.now(),\n title: 'reset',\n subtitle: state.formKey,\n },\n })\n })\n subscriberUnsubs.set(state.formKey, () => {\n unsubChange()\n unsubSubmit()\n unsubReset()\n })\n }\n\n // Subscribe all currently-registered forms + register as they're\n // added. The registry's `forms` Map is shallowReactive — we poll\n // once per render on refresh; for live change detection, each\n // useForm call that adds a new form triggers a tree/state refresh\n // via the form's own onFormChange emission on the first\n // applyFormReplacement.\n function syncForms(): void {\n for (const [, state] of registry.forms) {\n subscribeForm(state)\n }\n // Drop subscribers for forms that were evicted.\n for (const [formKey, unsub] of subscriberUnsubs) {\n if (!registry.forms.has(formKey)) {\n unsub()\n subscriberUnsubs.delete(formKey)\n }\n }\n }\n\n api.on.getInspectorTree((payload) => {\n if (payload.inspectorId !== INSPECTOR_ID) return\n syncForms()\n payload.rootNodes = [...registry.forms.keys()].map((key) => ({\n id: `form:${key}`,\n label: key,\n tags: [],\n }))\n })\n\n api.on.getInspectorState((payload) => {\n if (payload.inspectorId !== INSPECTOR_ID) return\n if (!payload.nodeId.startsWith('form:')) return\n const formKey = payload.nodeId.slice('form:'.length)\n const state = registry.forms.get(formKey)\n if (state === undefined) return\n // Devtools is dev-only — render raw values for everything,\n // including sensitive-named paths. Consumers concerned about\n // screen-share leaks should close the panel before sharing.\n payload.state['Form value'] = [\n {\n key: 'form',\n value: state.form.value,\n editable: true,\n },\n ]\n // Schema-driven and user-injected errors land in separate inspector\n // sections so devs can see the source distinction at a glance — a\n // user-injected entry surviving a successful submit, or a schema\n // entry that should have cleared after a value fix, are immediately\n // visible without cross-referencing call sites.\n payload.state['Schema Errors'] = [\n ...[...state.schemaErrors.entries()].map(([k, v]) => ({\n key: String(k),\n value: v as unknown,\n })),\n ]\n payload.state['User Errors'] = [\n ...[...state.userErrors.entries()].map(([k, v]) => ({\n key: String(k),\n value: v as unknown,\n })),\n ]\n payload.state['Aggregates'] = [\n { key: 'submitting', value: state.submitting.value },\n { key: 'submissionAttempts', value: state.submissionAttempts.value },\n { key: 'departAttempts', value: state.departAttempts.value },\n { key: 'submitError', value: state.submitError.value },\n { key: 'activeValidations', value: state.activeValidations.value },\n ]\n })\n\n api.on.editInspectorState((payload) => {\n if (payload.inspectorId !== INSPECTOR_ID) return\n if (!payload.nodeId.startsWith('form:')) return\n const formKey = payload.nodeId.slice('form:'.length)\n const state = registry.forms.get(formKey)\n if (state === undefined) return\n // payload.path is `['Form value', 'form', ...pathSegments]` — the\n // first two segments are the inspector section + key, the rest is\n // the target form path the user edited. Pass the segment array\n // directly to `canonicalizePath`: join('.') would collapse a\n // literal-dot field key (`{\"user.email\": ...}`) into two segments,\n // writing to the wrong leaf.\n if (payload.path.length < 3) return\n const section = payload.path[0]\n if (section !== 'Form value') return\n const segments = payload.path.slice(2)\n const { segments: canonicalPath } = canonicalizePath(segments)\n state.setValueAtPath(canonicalPath, payload.state.value)\n refreshState()\n })\n\n // Initial sync so existing forms show up.\n syncForms()\n refreshTree()\n}\n"],"names":[],"mappings":";;AA0BA,MAAM,YAAA,GAAe,UAAA;AACrB,MAAM,iBAAA,GAAoB,iBAAA;AA8D1B,eAAsB,qBAAA,CACpB,KACA,QAAA,EACkB;AAClB,EAAA,IAAI,GAAA;AACJ,EAAA,IAAI;AACF,IAAA,GAAA,GAAO,MAAM,OAAO,mBAAmB,CAAA;AAAA,EAGzC,CAAA,CAAA,MAAQ;AAIN,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,MAAM,sBAAsB,GAAA,CAAI,mBAAA;AAChC,EAAA,IAAI,OAAO,mBAAA,KAAwB,UAAA,EAAY,OAAO,KAAA;AAEtD,EAAA,mBAAA;AAAA,IACE;AAAA,MACE,EAAA,EAAI,YAAA;AAAA,MACJ,KAAA,EAAO,UAAA;AAAA,MACP,WAAA,EAAa,UAAA;AAAA,MACb,QAAA,EAAU,sCAAA;AAAA,MACV,GAAA;AAAA,MACA,mBAAA,EAAqB,CAAC,eAAe;AAAA,KACvC;AAAA,IACA,CAAC,GAAA,KAAQ,IAAA,CAAK,GAAA,EAAK,KAAK,QAAQ;AAAA,GAClC;AACA,EAAA,OAAO,IAAA;AACT;AAEA,SAAS,IAAA,CAAK,GAAA,EAAwB,GAAA,EAAU,QAAA,EAAkC;AAIhF,EAAA,MAAM,gBAAA,uBAAuB,GAAA,EAAyB;AAEtD,EAAA,GAAA,CAAI,aAAa,EAAE,EAAA,EAAI,cAAc,KAAA,EAAO,UAAA,EAAY,KAAK,CAAA;AAC7D,EAAA,GAAA,CAAI,gBAAA,CAAiB,EAAE,EAAA,EAAI,iBAAA,EAAmB,OAAO,UAAA,EAAY,KAAA,EAAO,SAAU,CAAA;AAElF,EAAA,SAAS,WAAA,GAAoB;AAC3B,IAAA,GAAA,CAAI,kBAAkB,YAAY,CAAA;AAAA,EACpC;AAEA,EAAA,SAAS,YAAA,GAAqB;AAC5B,IAAA,GAAA,CAAI,mBAAmB,YAAY,CAAA;AAAA,EACrC;AAEA,EAAA,SAAS,cAAc,KAAA,EAAqC;AAC1D,IAAA,IAAI,gBAAA,CAAiB,GAAA,CAAI,KAAA,CAAM,OAAO,CAAA,EAAG;AACzC,IAAA,MAAM,WAAA,GAAc,KAAA,CAAM,YAAA,CAAa,MAAM;AAC3C,MAAA,YAAA,EAAa;AACb,MAAA,GAAA,CAAI,gBAAA,CAAiB;AAAA,QACnB,OAAA,EAAS,iBAAA;AAAA,QACT,KAAA,EAAO;AAAA,UACL,IAAA,EAAM,KAAK,GAAA,EAAI;AAAA,UACf,KAAA,EAAO,aAAA;AAAA,UACP,UAAU,KAAA,CAAM,OAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAKhB,IAAA,EAAM,EAAE,IAAA,EAAM,KAAA,CAAM,KAAK,KAAA;AAAiC;AAC5D,OACD,CAAA;AAAA,IACH,CAAC,CAAA;AACD,IAAA,MAAM,WAAA,GAAc,KAAA,CAAM,eAAA,CAAgB,MAAM;AAC9C,MAAA,GAAA,CAAI,gBAAA,CAAiB;AAAA,QACnB,OAAA,EAAS,iBAAA;AAAA,QACT,KAAA,EAAO;AAAA,UACL,IAAA,EAAM,KAAK,GAAA,EAAI;AAAA,UACf,KAAA,EAAO,gBAAA;AAAA,UACP,UAAU,KAAA,CAAM,OAAA;AAAA,UAChB,IAAA,EAAM,EAAE,IAAA,EAAM,KAAA,CAAM,KAAK,KAAA;AAAiC;AAC5D,OACD,CAAA;AAAA,IACH,CAAC,CAAA;AACD,IAAA,MAAM,UAAA,GAAa,KAAA,CAAM,OAAA,CAAQ,MAAM;AACrC,MAAA,YAAA,EAAa;AACb,MAAA,GAAA,CAAI,gBAAA,CAAiB;AAAA,QACnB,OAAA,EAAS,iBAAA;AAAA,QACT,KAAA,EAAO;AAAA,UACL,IAAA,EAAM,KAAK,GAAA,EAAI;AAAA,UACf,KAAA,EAAO,OAAA;AAAA,UACP,UAAU,KAAA,CAAM;AAAA;AAClB,OACD,CAAA;AAAA,IACH,CAAC,CAAA;AACD,IAAA,gBAAA,CAAiB,GAAA,CAAI,KAAA,CAAM,OAAA,EAAS,MAAM;AACxC,MAAA,WAAA,EAAY;AACZ,MAAA,WAAA,EAAY;AACZ,MAAA,UAAA,EAAW;AAAA,IACb,CAAC,CAAA;AAAA,EACH;AAQA,EAAA,SAAS,SAAA,GAAkB;AACzB,IAAA,KAAA,MAAW,GAAG,KAAK,CAAA,IAAK,SAAS,KAAA,EAAO;AACtC,MAAA,aAAA,CAAc,KAAK,CAAA;AAAA,IACrB;AAEA,IAAA,KAAA,MAAW,CAAC,OAAA,EAAS,KAAK,CAAA,IAAK,gBAAA,EAAkB;AAC/C,MAAA,IAAI,CAAC,QAAA,CAAS,KAAA,CAAM,GAAA,CAAI,OAAO,CAAA,EAAG;AAChC,QAAA,KAAA,EAAM;AACN,QAAA,gBAAA,CAAiB,OAAO,OAAO,CAAA;AAAA,MACjC;AAAA,IACF;AAAA,EACF;AAEA,EAAA,GAAA,CAAI,EAAA,CAAG,gBAAA,CAAiB,CAAC,OAAA,KAAY;AACnC,IAAA,IAAI,OAAA,CAAQ,gBAAgB,YAAA,EAAc;AAC1C,IAAA,SAAA,EAAU;AACV,IAAA,OAAA,CAAQ,SAAA,GAAY,CAAC,GAAG,QAAA,CAAS,KAAA,CAAM,MAAM,CAAA,CAAE,GAAA,CAAI,CAAC,GAAA,MAAS;AAAA,MAC3D,EAAA,EAAI,QAAQ,GAAG,CAAA,CAAA;AAAA,MACf,KAAA,EAAO,GAAA;AAAA,MACP,MAAM;AAAC,KACT,CAAE,CAAA;AAAA,EACJ,CAAC,CAAA;AAED,EAAA,GAAA,CAAI,EAAA,CAAG,iBAAA,CAAkB,CAAC,OAAA,KAAY;AACpC,IAAA,IAAI,OAAA,CAAQ,gBAAgB,YAAA,EAAc;AAC1C,IAAA,IAAI,CAAC,OAAA,CAAQ,MAAA,CAAO,UAAA,CAAW,OAAO,CAAA,EAAG;AACzC,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,MAAA,CAAO,KAAA,CAAM,QAAQ,MAAM,CAAA;AACnD,IAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,KAAA,CAAM,GAAA,CAAI,OAAO,CAAA;AACxC,IAAA,IAAI,UAAU,MAAA,EAAW;AAIzB,IAAA,OAAA,CAAQ,KAAA,CAAM,YAAY,CAAA,GAAI;AAAA,MAC5B;AAAA,QACE,GAAA,EAAK,MAAA;AAAA,QACL,KAAA,EAAO,MAAM,IAAA,CAAK,KAAA;AAAA,QAClB,QAAA,EAAU;AAAA;AACZ,KACF;AAMA,IAAA,OAAA,CAAQ,KAAA,CAAM,eAAe,CAAA,GAAI;AAAA,MAC/B,GAAG,CAAC,GAAG,KAAA,CAAM,YAAA,CAAa,OAAA,EAAS,CAAA,CAAE,GAAA,CAAI,CAAC,CAAC,CAAA,EAAG,CAAC,CAAA,MAAO;AAAA,QACpD,GAAA,EAAK,OAAO,CAAC,CAAA;AAAA,QACb,KAAA,EAAO;AAAA,OACT,CAAE;AAAA,KACJ;AACA,IAAA,OAAA,CAAQ,KAAA,CAAM,aAAa,CAAA,GAAI;AAAA,MAC7B,GAAG,CAAC,GAAG,KAAA,CAAM,UAAA,CAAW,OAAA,EAAS,CAAA,CAAE,GAAA,CAAI,CAAC,CAAC,CAAA,EAAG,CAAC,CAAA,MAAO;AAAA,QAClD,GAAA,EAAK,OAAO,CAAC,CAAA;AAAA,QACb,KAAA,EAAO;AAAA,OACT,CAAE;AAAA,KACJ;AACA,IAAA,OAAA,CAAQ,KAAA,CAAM,YAAY,CAAA,GAAI;AAAA,MAC5B,EAAE,GAAA,EAAK,YAAA,EAAc,KAAA,EAAO,KAAA,CAAM,WAAW,KAAA,EAAM;AAAA,MACnD,EAAE,GAAA,EAAK,oBAAA,EAAsB,KAAA,EAAO,KAAA,CAAM,mBAAmB,KAAA,EAAM;AAAA,MACnE,EAAE,GAAA,EAAK,gBAAA,EAAkB,KAAA,EAAO,KAAA,CAAM,eAAe,KAAA,EAAM;AAAA,MAC3D,EAAE,GAAA,EAAK,aAAA,EAAe,KAAA,EAAO,KAAA,CAAM,YAAY,KAAA,EAAM;AAAA,MACrD,EAAE,GAAA,EAAK,mBAAA,EAAqB,KAAA,EAAO,KAAA,CAAM,kBAAkB,KAAA;AAAM,KACnE;AAAA,EACF,CAAC,CAAA;AAED,EAAA,GAAA,CAAI,EAAA,CAAG,kBAAA,CAAmB,CAAC,OAAA,KAAY;AACrC,IAAA,IAAI,OAAA,CAAQ,gBAAgB,YAAA,EAAc;AAC1C,IAAA,IAAI,CAAC,OAAA,CAAQ,MAAA,CAAO,UAAA,CAAW,OAAO,CAAA,EAAG;AACzC,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,MAAA,CAAO,KAAA,CAAM,QAAQ,MAAM,CAAA;AACnD,IAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,KAAA,CAAM,GAAA,CAAI,OAAO,CAAA;AACxC,IAAA,IAAI,UAAU,MAAA,EAAW;AAOzB,IAAA,IAAI,OAAA,CAAQ,IAAA,CAAK,MAAA,GAAS,CAAA,EAAG;AAC7B,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,IAAA,CAAK,CAAC,CAAA;AAC9B,IAAA,IAAI,YAAY,YAAA,EAAc;AAC9B,IAAA,MAAM,QAAA,GAAW,OAAA,CAAQ,IAAA,CAAK,KAAA,CAAM,CAAC,CAAA;AACrC,IAAA,MAAM,EAAE,QAAA,EAAU,aAAA,EAAc,GAAI,iBAAiB,QAAQ,CAAA;AAC7D,IAAA,KAAA,CAAM,cAAA,CAAe,aAAA,EAAe,OAAA,CAAQ,KAAA,CAAM,KAAK,CAAA;AACvD,IAAA,YAAA,EAAa;AAAA,EACf,CAAC,CAAA;AAGD,EAAA,SAAA,EAAU;AACV,EAAA,WAAA,EAAY;AACd;;;;"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const canonicalStringify = require('../shared/attaform.CQN9R62B.cjs');
|
|
4
|
-
const index = require('../shared/attaform.
|
|
4
|
+
const index = require('../shared/attaform.Cmb_LCie.cjs');
|
|
5
5
|
|
|
6
6
|
const cyclicSentinel = "<cyclic>";
|
|
7
7
|
function fingerprintZodSchema(schema) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { c as canonicalStringify } from '../shared/attaform.BJGA_UOS.mjs';
|
|
2
|
-
import { k as kindOf, g as getIntersectionLeft, b as getIntersectionRight, c as unwrapLazy, d as unwrapInner, e as unwrapPipe, h as getDiscriminator, i as getDiscriminatedOptions, j as getUnionOptions, l as getRecordKeyType, m as getRecordValueType, n as getTupleItems, o as getSetValueType, p as getArrayElement, q as getObjectShape, r as getEnumValues, s as getLiteralValues, t as getChecks, v as getCatchDefault, x as getDefaultValue } from '../shared/attaform.
|
|
2
|
+
import { k as kindOf, g as getIntersectionLeft, b as getIntersectionRight, c as unwrapLazy, d as unwrapInner, e as unwrapPipe, h as getDiscriminator, i as getDiscriminatedOptions, j as getUnionOptions, l as getRecordKeyType, m as getRecordValueType, n as getTupleItems, o as getSetValueType, p as getArrayElement, q as getObjectShape, r as getEnumValues, s as getLiteralValues, t as getChecks, v as getCatchDefault, x as getDefaultValue } from '../shared/attaform.alpG7rT7.mjs';
|
|
3
3
|
|
|
4
4
|
const cyclicSentinel = "<cyclic>";
|
|
5
5
|
function fingerprintZodSchema(schema) {
|
package/dist/index.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const paths = require('./shared/attaform.
|
|
4
|
-
const devtoolsShared = require('./shared/attaform.
|
|
5
|
-
const injectWizard = require('./shared/attaform.
|
|
3
|
+
const paths = require('./shared/attaform.Db4E4IW6.cjs');
|
|
4
|
+
const devtoolsShared = require('./shared/attaform.BibT5AS_.cjs');
|
|
5
|
+
const injectWizard = require('./shared/attaform.DsQkXE3o.cjs');
|
|
6
6
|
|
|
7
7
|
function escapeForInlineScript(json) {
|
|
8
8
|
return json.replace(/[<>&\u2028\u2029]/g, (char) => {
|
|
@@ -167,9 +167,7 @@ function isDetailsRecord(value) {
|
|
|
167
167
|
return true;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
exports.AnonPersistError = paths.AnonPersistError;
|
|
171
170
|
exports.AttaformError = paths.AttaformError;
|
|
172
|
-
exports.DEFAULT_SENSITIVE_NAMES = paths.DEFAULT_SENSITIVE_NAMES;
|
|
173
171
|
exports.InvalidPathError = paths.InvalidPathError;
|
|
174
172
|
exports.InvalidUseFormConfigError = paths.InvalidUseFormConfigError;
|
|
175
173
|
exports.OutsideSetupError = paths.OutsideSetupError;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":["../src/runtime/core/serialize-script.ts","../src/runtime/core/parse-api-errors.ts"],"sourcesContent":["/**\n * Escape a JSON string so it's safe to embed inside an inline\n * `<script>` tag during SSR. Plain `JSON.stringify` is not safe — a\n * form value containing the literal substring `</script>` would\n * break out of the script tag.\n *\n * ```ts\n * const payload = escapeForInlineScript(JSON.stringify(renderAttaformState(app)))\n * // `<script>window.__ATTAFORM_STATE__ = ${payload}</script>` is safe.\n * ```\n *\n * Output remains valid JSON — `JSON.parse` round-trips back to the\n * original value on the client.\n */\nexport function escapeForInlineScript(json: string): string {\n return json.replace(/[<>&\\u2028\\u2029]/g, (char) => {\n switch (char) {\n case '<':\n return '\\\\u003c'\n case '>':\n return '\\\\u003e'\n case '&':\n return '\\\\u0026'\n case '\\u2028':\n return '\\\\u2028'\n case '\\u2029':\n return '\\\\u2029'\n default:\n return char\n }\n })\n}\n","import type {\n ApiErrorDetails,\n ApiErrorEntry,\n ApiErrorEnvelope,\n FormKey,\n ValidationError,\n} from '../types/types-api'\nimport { normalizeNumericOption } from './defaults'\nimport { InvalidPathError } from './errors'\nimport { canonicalizePath } from './paths'\n\n/**\n * Result of `parseApiErrors`. Branch on `ok` to handle the two cases:\n *\n * ```ts\n * const result = parseApiErrors(payload, { formKey: form.key })\n * if (result.ok) {\n * form.setFieldErrors(result.errors)\n * } else {\n * console.warn('Bad error payload:', result.rejected)\n * }\n * ```\n *\n * `ok: true` means the payload was recognised — `errors` may still be\n * empty if the payload was valid but had no actual errors.\n * `ok: false` means the payload didn't match a known shape; `rejected`\n * carries a one-line description of why.\n */\nexport type ParseApiErrorsResult = {\n /** `true` when the payload was recognised; `false` when the shape was unfamiliar. */\n readonly ok: boolean\n /** Errors extracted from the payload. May be empty even when `ok: true`. */\n readonly errors: ValidationError[]\n /** When `ok: false`, a one-line description of why the payload was rejected. */\n readonly rejected?: string\n}\n\n/**\n * Options for `parseApiErrors`. The size caps protect against\n * misbehaving or hostile servers — exceeding any cap causes the\n * parser to reject the payload wholesale rather than partially apply.\n */\nexport type ParseApiErrorsOptions = {\n /**\n * The form's identifier — pass `form.key`. Stamped on every\n * produced `ValidationError` so errors route to the right form.\n */\n readonly formKey: FormKey\n /**\n * Code stamped on `ValidationError`s synthesized from bare-string\n * entries (the Rails / DRF / Laravel `{ field: [\"msg\"] }` shape).\n * Default `'api:unknown'`. Pick something more specific\n * (`'api:server-validation'`, `'myapp:legacy'`, …) when you know\n * the source.\n *\n * Structured `{ message, code }` entries forward their `code`\n * verbatim and ignore this option.\n */\n readonly defaultCode?: string\n /**\n * Maximum number of distinct keys to accept. Default `1000`.\n * Raise for trusted backends that legitimately produce more.\n */\n readonly maxEntries?: number\n /**\n * Maximum number of path segments per key. Default `32`. Keys\n * deeper than this are dropped (the rest of the payload still\n * applies if it stays under the other caps).\n */\n readonly maxPathDepth?: number\n /**\n * Maximum total path segments summed across every accepted key.\n * Default `10000`. Bounds the worst-case traversal cost.\n */\n readonly maxTotalSegments?: number\n}\n\n/**\n * Default size caps + default fallback code used by `parseApiErrors`.\n * Conservative; pass larger values (or a more specific code) via the\n * options bag for trusted-backend integrations.\n */\nexport const PARSE_API_ERRORS_DEFAULTS = {\n maxEntries: 1000,\n maxPathDepth: 32,\n maxTotalSegments: 10000,\n defaultCode: 'api:unknown',\n} as const\n\n/**\n * Normalise a server-side validation error payload into\n * `ValidationError[]`. Pair with `form.setFieldErrors` /\n * `form.addFieldErrors` to surface server errors on the form:\n *\n * ```ts\n * const response = await fetch('/api/signup', { … })\n * if (!response.ok) {\n * const payload = await response.json()\n * const result = parseApiErrors(payload, { formKey: form.key })\n * if (result.ok) form.setFieldErrors(result.errors)\n * }\n * ```\n *\n * Recognised payload shapes:\n *\n * - Wrapped envelope:\n * `{ error: { details: { email: { message: 'taken', code: 'api:duplicate-email' } } } }`\n * - Unwrapped envelope:\n * `{ details: { email: { message: 'taken', code: 'api:duplicate-email' } } }`\n * - Raw details record:\n * `{ email: { message: 'taken', code: 'api:duplicate-email' } }`\n * - **Bare-string Rails / DRF / Laravel shape:**\n * `{ email: ['Email already taken.'], username: 'too short' }`\n * - `null` / `undefined` — returns `{ ok: true, errors: [] }`\n *\n * Two entry shapes are accepted:\n *\n * 1. **Structured** — `{ message: string, code: string }`. The `code`\n * is forwarded verbatim onto the produced `ValidationError`.\n * 2. **Bare-string** — a plain string. Synthesized into\n * `{ message: <string>, code: <defaultCode> }` where `defaultCode`\n * comes from `options.defaultCode` (default `'api:unknown'`).\n * Useful for the Rails / Django REST Framework / FastAPI / Laravel\n * JSON shape that doesn't carry a per-field code.\n *\n * Each detail key's value can be a single entry, an array, or a mix\n * of structured and bare-string entries; arrays expand into one\n * `ValidationError` per entry. Pick a prefix on the server (`api:`,\n * `auth:`, etc.) and stay consistent so error renderers can branch\n * on `code` — or rely on `defaultCode` when the wire shape is\n * message-only.\n *\n * Dotted keys (`\"address.line1\"`) are split into structured paths\n * automatically. Use a custom server response shape outside these\n * patterns? Build the `ValidationError[]` array yourself and pass\n * it to `setFieldErrors` directly — `parseApiErrors` is just a\n * convenience for the common shapes.\n */\nexport function parseApiErrors(\n payload: ApiErrorEnvelope | ApiErrorDetails | null | undefined | unknown,\n options: ParseApiErrorsOptions\n): ParseApiErrorsResult {\n const { maxEntries, maxPathDepth, maxTotalSegments, defaultCode } = normalizeParseCaps(options)\n\n if (payload === null || payload === undefined) {\n return { ok: true, errors: [] }\n }\n if (typeof payload !== 'object') {\n return { ok: false, errors: [], rejected: `payload was ${typeof payload}, expected object` }\n }\n\n const extraction = extractDetails(payload as Record<string, unknown>)\n if (!extraction.ok) {\n return { ok: false, errors: [], rejected: extraction.reason }\n }\n\n const { details } = extraction\n const entryCount = Object.keys(details).length\n // Enforce the guardrails before we spend time walking the payload.\n // Rejecting wholesale (not partial-applying) keeps the failure visible\n // so consumers can tune the caps or investigate the server payload.\n if (entryCount > maxEntries) {\n return {\n ok: false,\n errors: [],\n rejected: `payload has ${entryCount} entries, exceeds maxEntries=${maxEntries}`,\n }\n }\n\n return convertDetailsToErrors(\n details,\n options.formKey,\n maxPathDepth,\n maxTotalSegments,\n defaultCode\n )\n}\n\n/** Resolved, sanitised size caps plus the fallback code for one `parseApiErrors` call. */\ntype NormalizedParseCaps = {\n readonly maxEntries: number\n readonly maxPathDepth: number\n readonly maxTotalSegments: number\n readonly defaultCode: string\n}\n\n/**\n * Resolve the optional size caps against `PARSE_API_ERRORS_DEFAULTS` and\n * sanitise them into safe integers.\n *\n * Comparison gates (`>` against the count / depth) yield `false` for\n * `NaN`, so without sanitisation a hostile or malformed `NaN` cap would\n * let pathological payloads run unbounded. `Infinity` would do the same.\n * Negatives and non-integers would discard legitimate entries. Falls\n * back to the library default on garbage.\n */\nfunction normalizeParseCaps(options: ParseApiErrorsOptions): NormalizedParseCaps {\n const maxEntries = normalizeNumericOption({\n value: options.maxEntries ?? PARSE_API_ERRORS_DEFAULTS.maxEntries,\n source: 'parseApiErrors.maxEntries',\n allowInfinity: false,\n min: 0,\n defaultValue: PARSE_API_ERRORS_DEFAULTS.maxEntries,\n })\n const maxPathDepth = normalizeNumericOption({\n value: options.maxPathDepth ?? PARSE_API_ERRORS_DEFAULTS.maxPathDepth,\n source: 'parseApiErrors.maxPathDepth',\n allowInfinity: false,\n min: 0,\n defaultValue: PARSE_API_ERRORS_DEFAULTS.maxPathDepth,\n })\n const maxTotalSegments = normalizeNumericOption({\n value: options.maxTotalSegments ?? PARSE_API_ERRORS_DEFAULTS.maxTotalSegments,\n source: 'parseApiErrors.maxTotalSegments',\n allowInfinity: false,\n min: 0,\n defaultValue: PARSE_API_ERRORS_DEFAULTS.maxTotalSegments,\n })\n const defaultCode = options.defaultCode ?? PARSE_API_ERRORS_DEFAULTS.defaultCode\n return { maxEntries, maxPathDepth, maxTotalSegments, defaultCode }\n}\n\n/**\n * Walk an already-extracted, already-entry-capped details record into\n * `ValidationError[]`. Per-key paths are canonicalised, depth-capped,\n * and bounded by a running total-segment budget; each key's entries\n * expand to one `ValidationError` per non-empty message. Returns\n * `ok: false` only when the total-segment budget overflows mid-walk\n * (rejected wholesale, never partially applied).\n */\nfunction convertDetailsToErrors(\n details: ApiErrorDetails,\n formKey: FormKey,\n maxPathDepth: number,\n maxTotalSegments: number,\n defaultCode: string\n): ParseApiErrorsResult {\n const errors: ValidationError[] = []\n let totalSegments = 0\n for (const [key, value] of Object.entries(details)) {\n const entryList: ReadonlyArray<string | ApiErrorEntry> = Array.isArray(value) ? value : [value]\n // `canonicalizePath` throws `InvalidPathError` for dotted strings with\n // empty segments (e.g. `'. '`, `'a..b'`). A misbehaving server can\n // genuinely emit such a key; the hydrator is a normaliser, not a\n // validator, so we drop offending keys rather than let the exception\n // escape. Well-formed keys continue as normal.\n let segments: readonly (string | number)[]\n try {\n segments = canonicalizePath(key).segments\n } catch (err) {\n if (err instanceof InvalidPathError) continue\n throw err\n }\n // Per-path depth cap. We drop the offending key (rather than\n // rejecting the whole payload) because a single stray deep path\n // in an otherwise legitimate error set is still worth surfacing\n // the rest. Consumers who want strict rejection can post-filter\n // on `result.errors.length < details entryCount`.\n if (segments.length > maxPathDepth) continue\n // Total-segment cap. Enforced wholesale (not per-key) so a payload\n // that passes the per-key gate but stacks into a pathological\n // total still fails visibly. Mirrors `maxEntries` strictness.\n totalSegments += segments.length\n if (totalSegments > maxTotalSegments) {\n return {\n ok: false,\n errors: [],\n rejected: `payload total path segments exceeds maxTotalSegments=${maxTotalSegments}`,\n }\n }\n for (const entry of entryList) {\n // Bare-string entries (Rails / DRF / Laravel shape) synthesize a\n // `code` from `defaultCode`; structured `{ message, code }`\n // entries forward `code` verbatim. Empty messages drop silently\n // (`{ message: '' }` or `''`) — same recoverable-malformed-server\n // policy as before.\n const message = typeof entry === 'string' ? entry : entry.message\n const code = typeof entry === 'string' ? defaultCode : entry.code\n if (message.length === 0) continue\n errors.push({\n message,\n path: Array.from(segments),\n formKey,\n code,\n })\n }\n }\n return { ok: true, errors }\n}\n\ntype ExtractResult = { ok: true; details: ApiErrorDetails } | { ok: false; reason: string }\n\nfunction extractDetails(payload: Record<string, unknown>): ExtractResult {\n const wrappedError = payload['error']\n if (wrappedError !== null && wrappedError !== undefined && typeof wrappedError === 'object') {\n const inner = (wrappedError as { details?: unknown }).details\n if (inner === undefined) {\n // A wrapped envelope without details is considered \"no errors\" — valid shape.\n return { ok: true, details: {} }\n }\n if (isDetailsRecord(inner)) return { ok: true, details: inner }\n return {\n ok: false,\n reason: 'error.details entries must be strings or { message, code } objects',\n }\n }\n\n // `{ error: 'oops' }` / `{ error: 42 }` is a malformed wrapped envelope —\n // the server meant an error object but sent a scalar. Without this guard\n // the payload would fall through to the raw-details branch below, where\n // `{ error: 'oops' }` satisfies `isDetailsRecord` and silently produces\n // a phantom `ValidationError` at path `['error']`.\n if (wrappedError !== null && wrappedError !== undefined && typeof wrappedError !== 'object') {\n return {\n ok: false,\n reason: `payload.error was ${typeof wrappedError}, expected an object with { details }`,\n }\n }\n\n if ('details' in payload) {\n const inner = payload['details']\n if (inner === undefined) return { ok: true, details: {} }\n if (isDetailsRecord(inner)) return { ok: true, details: inner }\n return { ok: false, reason: 'details entries must be strings or { message, code } objects' }\n }\n\n if (isDetailsRecord(payload)) return { ok: true, details: payload }\n\n // Heuristic: if the payload has keys but none of them look like details,\n // it's probably a completely different shape. Reject.\n if (Object.keys(payload).length === 0) return { ok: true, details: {} }\n return { ok: false, reason: 'unrecognised payload shape' }\n}\n\nfunction isStructuredEntry(value: unknown): value is ApiErrorEntry {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return false\n const obj = value as { message?: unknown; code?: unknown }\n return typeof obj.message === 'string' && typeof obj.code === 'string'\n}\n\n/**\n * Accepts either a structured `{ message, code }` entry OR a bare\n * string. Bare strings synthesize a `code` at parse time\n * (`options.defaultCode`) and are useful for the Rails / Django REST\n * Framework / Laravel JSON shape that doesn't carry a per-field code.\n */\nfunction isAcceptedEntry(value: unknown): value is string | ApiErrorEntry {\n return typeof value === 'string' || isStructuredEntry(value)\n}\n\n/**\n * A record is a \"details\" record when every value is either an\n * accepted entry or an array of accepted entries (mixing structured +\n * bare-string in the same array is fine; the parser normalises per\n * entry). Half-structured objects (e.g. `{ message: 'x' }` missing\n * `code`) are still rejected so the bug surfaces — see the\n * `'rejects entries that are objects but missing required fields'`\n * test for the rationale.\n */\nfunction isDetailsRecord(value: unknown): value is ApiErrorDetails {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return false\n // Reject prototype-polluted keys — we don't use them here, but downstream\n // spreads shouldn't have to worry about this input.\n const record = value as Record<string, unknown>\n for (const k of Object.keys(record)) {\n const v = record[k]\n if (isAcceptedEntry(v)) continue\n if (Array.isArray(v) && v.every((entry) => isAcceptedEntry(entry))) continue\n return false\n }\n return true\n}\n"],"names":["normalizeNumericOption","canonicalizePath","InvalidPathError"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../src/runtime/core/serialize-script.ts","../src/runtime/core/parse-api-errors.ts"],"sourcesContent":["/**\n * Escape a JSON string so it's safe to embed inside an inline\n * `<script>` tag during SSR. Plain `JSON.stringify` is not safe — a\n * form value containing the literal substring `</script>` would\n * break out of the script tag.\n *\n * ```ts\n * const payload = escapeForInlineScript(JSON.stringify(renderAttaformState(app)))\n * // `<script>window.__ATTAFORM_STATE__ = ${payload}</script>` is safe.\n * ```\n *\n * Output remains valid JSON — `JSON.parse` round-trips back to the\n * original value on the client.\n */\nexport function escapeForInlineScript(json: string): string {\n return json.replace(/[<>&\\u2028\\u2029]/g, (char) => {\n switch (char) {\n case '<':\n return '\\\\u003c'\n case '>':\n return '\\\\u003e'\n case '&':\n return '\\\\u0026'\n case '\\u2028':\n return '\\\\u2028'\n case '\\u2029':\n return '\\\\u2029'\n default:\n return char\n }\n })\n}\n","import type {\n ApiErrorDetails,\n ApiErrorEntry,\n ApiErrorEnvelope,\n FormKey,\n ValidationError,\n} from '../types/types-api'\nimport { normalizeNumericOption } from './defaults'\nimport { InvalidPathError } from './errors'\nimport { canonicalizePath } from './paths'\n\n/**\n * Result of `parseApiErrors`. Branch on `ok` to handle the two cases:\n *\n * ```ts\n * const result = parseApiErrors(payload, { formKey: form.key })\n * if (result.ok) {\n * form.setFieldErrors(result.errors)\n * } else {\n * console.warn('Bad error payload:', result.rejected)\n * }\n * ```\n *\n * `ok: true` means the payload was recognised — `errors` may still be\n * empty if the payload was valid but had no actual errors.\n * `ok: false` means the payload didn't match a known shape; `rejected`\n * carries a one-line description of why.\n */\nexport type ParseApiErrorsResult = {\n /** `true` when the payload was recognised; `false` when the shape was unfamiliar. */\n readonly ok: boolean\n /** Errors extracted from the payload. May be empty even when `ok: true`. */\n readonly errors: ValidationError[]\n /** When `ok: false`, a one-line description of why the payload was rejected. */\n readonly rejected?: string\n}\n\n/**\n * Options for `parseApiErrors`. The size caps protect against\n * misbehaving or hostile servers — exceeding any cap causes the\n * parser to reject the payload wholesale rather than partially apply.\n */\nexport type ParseApiErrorsOptions = {\n /**\n * The form's identifier — pass `form.key`. Stamped on every\n * produced `ValidationError` so errors route to the right form.\n */\n readonly formKey: FormKey\n /**\n * Code stamped on `ValidationError`s synthesized from bare-string\n * entries (the Rails / DRF / Laravel `{ field: [\"msg\"] }` shape).\n * Default `'api:unknown'`. Pick something more specific\n * (`'api:server-validation'`, `'myapp:legacy'`, …) when you know\n * the source.\n *\n * Structured `{ message, code }` entries forward their `code`\n * verbatim and ignore this option.\n */\n readonly defaultCode?: string\n /**\n * Maximum number of distinct keys to accept. Default `1000`.\n * Raise for trusted backends that legitimately produce more.\n */\n readonly maxEntries?: number\n /**\n * Maximum number of path segments per key. Default `32`. Keys\n * deeper than this are dropped (the rest of the payload still\n * applies if it stays under the other caps).\n */\n readonly maxPathDepth?: number\n /**\n * Maximum total path segments summed across every accepted key.\n * Default `10000`. Bounds the worst-case traversal cost.\n */\n readonly maxTotalSegments?: number\n}\n\n/**\n * Default size caps + default fallback code used by `parseApiErrors`.\n * Conservative; pass larger values (or a more specific code) via the\n * options bag for trusted-backend integrations.\n */\nexport const PARSE_API_ERRORS_DEFAULTS = {\n maxEntries: 1000,\n maxPathDepth: 32,\n maxTotalSegments: 10000,\n defaultCode: 'api:unknown',\n} as const\n\n/**\n * Normalise a server-side validation error payload into\n * `ValidationError[]`. Pair with `form.setFieldErrors` /\n * `form.addFieldErrors` to surface server errors on the form:\n *\n * ```ts\n * const response = await fetch('/api/signup', { … })\n * if (!response.ok) {\n * const payload = await response.json()\n * const result = parseApiErrors(payload, { formKey: form.key })\n * if (result.ok) form.setFieldErrors(result.errors)\n * }\n * ```\n *\n * Recognised payload shapes:\n *\n * - Wrapped envelope:\n * `{ error: { details: { email: { message: 'taken', code: 'api:duplicate-email' } } } }`\n * - Unwrapped envelope:\n * `{ details: { email: { message: 'taken', code: 'api:duplicate-email' } } }`\n * - Raw details record:\n * `{ email: { message: 'taken', code: 'api:duplicate-email' } }`\n * - **Bare-string Rails / DRF / Laravel shape:**\n * `{ email: ['Email already taken.'], username: 'too short' }`\n * - `null` / `undefined` — returns `{ ok: true, errors: [] }`\n *\n * Two entry shapes are accepted:\n *\n * 1. **Structured** — `{ message: string, code: string }`. The `code`\n * is forwarded verbatim onto the produced `ValidationError`.\n * 2. **Bare-string** — a plain string. Synthesized into\n * `{ message: <string>, code: <defaultCode> }` where `defaultCode`\n * comes from `options.defaultCode` (default `'api:unknown'`).\n * Useful for the Rails / Django REST Framework / FastAPI / Laravel\n * JSON shape that doesn't carry a per-field code.\n *\n * Each detail key's value can be a single entry, an array, or a mix\n * of structured and bare-string entries; arrays expand into one\n * `ValidationError` per entry. Pick a prefix on the server (`api:`,\n * `auth:`, etc.) and stay consistent so error renderers can branch\n * on `code` — or rely on `defaultCode` when the wire shape is\n * message-only.\n *\n * Dotted keys (`\"address.line1\"`) are split into structured paths\n * automatically. Use a custom server response shape outside these\n * patterns? Build the `ValidationError[]` array yourself and pass\n * it to `setFieldErrors` directly — `parseApiErrors` is just a\n * convenience for the common shapes.\n */\nexport function parseApiErrors(\n payload: ApiErrorEnvelope | ApiErrorDetails | null | undefined | unknown,\n options: ParseApiErrorsOptions\n): ParseApiErrorsResult {\n const { maxEntries, maxPathDepth, maxTotalSegments, defaultCode } = normalizeParseCaps(options)\n\n if (payload === null || payload === undefined) {\n return { ok: true, errors: [] }\n }\n if (typeof payload !== 'object') {\n return { ok: false, errors: [], rejected: `payload was ${typeof payload}, expected object` }\n }\n\n const extraction = extractDetails(payload as Record<string, unknown>)\n if (!extraction.ok) {\n return { ok: false, errors: [], rejected: extraction.reason }\n }\n\n const { details } = extraction\n const entryCount = Object.keys(details).length\n // Enforce the guardrails before we spend time walking the payload.\n // Rejecting wholesale (not partial-applying) keeps the failure visible\n // so consumers can tune the caps or investigate the server payload.\n if (entryCount > maxEntries) {\n return {\n ok: false,\n errors: [],\n rejected: `payload has ${entryCount} entries, exceeds maxEntries=${maxEntries}`,\n }\n }\n\n return convertDetailsToErrors(\n details,\n options.formKey,\n maxPathDepth,\n maxTotalSegments,\n defaultCode\n )\n}\n\n/** Resolved, sanitised size caps plus the fallback code for one `parseApiErrors` call. */\ntype NormalizedParseCaps = {\n readonly maxEntries: number\n readonly maxPathDepth: number\n readonly maxTotalSegments: number\n readonly defaultCode: string\n}\n\n/**\n * Resolve the optional size caps against `PARSE_API_ERRORS_DEFAULTS` and\n * sanitise them into safe integers.\n *\n * Comparison gates (`>` against the count / depth) yield `false` for\n * `NaN`, so without sanitisation a hostile or malformed `NaN` cap would\n * let pathological payloads run unbounded. `Infinity` would do the same.\n * Negatives and non-integers would discard legitimate entries. Falls\n * back to the library default on garbage.\n */\nfunction normalizeParseCaps(options: ParseApiErrorsOptions): NormalizedParseCaps {\n const maxEntries = normalizeNumericOption({\n value: options.maxEntries ?? PARSE_API_ERRORS_DEFAULTS.maxEntries,\n source: 'parseApiErrors.maxEntries',\n allowInfinity: false,\n min: 0,\n defaultValue: PARSE_API_ERRORS_DEFAULTS.maxEntries,\n })\n const maxPathDepth = normalizeNumericOption({\n value: options.maxPathDepth ?? PARSE_API_ERRORS_DEFAULTS.maxPathDepth,\n source: 'parseApiErrors.maxPathDepth',\n allowInfinity: false,\n min: 0,\n defaultValue: PARSE_API_ERRORS_DEFAULTS.maxPathDepth,\n })\n const maxTotalSegments = normalizeNumericOption({\n value: options.maxTotalSegments ?? PARSE_API_ERRORS_DEFAULTS.maxTotalSegments,\n source: 'parseApiErrors.maxTotalSegments',\n allowInfinity: false,\n min: 0,\n defaultValue: PARSE_API_ERRORS_DEFAULTS.maxTotalSegments,\n })\n const defaultCode = options.defaultCode ?? PARSE_API_ERRORS_DEFAULTS.defaultCode\n return { maxEntries, maxPathDepth, maxTotalSegments, defaultCode }\n}\n\n/**\n * Walk an already-extracted, already-entry-capped details record into\n * `ValidationError[]`. Per-key paths are canonicalised, depth-capped,\n * and bounded by a running total-segment budget; each key's entries\n * expand to one `ValidationError` per non-empty message. Returns\n * `ok: false` only when the total-segment budget overflows mid-walk\n * (rejected wholesale, never partially applied).\n */\nfunction convertDetailsToErrors(\n details: ApiErrorDetails,\n formKey: FormKey,\n maxPathDepth: number,\n maxTotalSegments: number,\n defaultCode: string\n): ParseApiErrorsResult {\n const errors: ValidationError[] = []\n let totalSegments = 0\n for (const [key, value] of Object.entries(details)) {\n const entryList: ReadonlyArray<string | ApiErrorEntry> = Array.isArray(value) ? value : [value]\n // `canonicalizePath` throws `InvalidPathError` for dotted strings with\n // empty segments (e.g. `'. '`, `'a..b'`). A misbehaving server can\n // genuinely emit such a key; the hydrator is a normaliser, not a\n // validator, so we drop offending keys rather than let the exception\n // escape. Well-formed keys continue as normal.\n let segments: readonly (string | number)[]\n try {\n segments = canonicalizePath(key).segments\n } catch (err) {\n if (err instanceof InvalidPathError) continue\n throw err\n }\n // Per-path depth cap. We drop the offending key (rather than\n // rejecting the whole payload) because a single stray deep path\n // in an otherwise legitimate error set is still worth surfacing\n // the rest. Consumers who want strict rejection can post-filter\n // on `result.errors.length < details entryCount`.\n if (segments.length > maxPathDepth) continue\n // Total-segment cap. Enforced wholesale (not per-key) so a payload\n // that passes the per-key gate but stacks into a pathological\n // total still fails visibly. Mirrors `maxEntries` strictness.\n totalSegments += segments.length\n if (totalSegments > maxTotalSegments) {\n return {\n ok: false,\n errors: [],\n rejected: `payload total path segments exceeds maxTotalSegments=${maxTotalSegments}`,\n }\n }\n for (const entry of entryList) {\n // Bare-string entries (Rails / DRF / Laravel shape) synthesize a\n // `code` from `defaultCode`; structured `{ message, code }`\n // entries forward `code` verbatim. Empty messages drop silently\n // (`{ message: '' }` or `''`) — same recoverable-malformed-server\n // policy as before.\n const message = typeof entry === 'string' ? entry : entry.message\n const code = typeof entry === 'string' ? defaultCode : entry.code\n if (message.length === 0) continue\n errors.push({\n message,\n path: Array.from(segments),\n formKey,\n code,\n })\n }\n }\n return { ok: true, errors }\n}\n\ntype ExtractResult = { ok: true; details: ApiErrorDetails } | { ok: false; reason: string }\n\nfunction extractDetails(payload: Record<string, unknown>): ExtractResult {\n const wrappedError = payload['error']\n if (wrappedError !== null && wrappedError !== undefined && typeof wrappedError === 'object') {\n const inner = (wrappedError as { details?: unknown }).details\n if (inner === undefined) {\n // A wrapped envelope without details is considered \"no errors\" — valid shape.\n return { ok: true, details: {} }\n }\n if (isDetailsRecord(inner)) return { ok: true, details: inner }\n return {\n ok: false,\n reason: 'error.details entries must be strings or { message, code } objects',\n }\n }\n\n // `{ error: 'oops' }` / `{ error: 42 }` is a malformed wrapped envelope —\n // the server meant an error object but sent a scalar. Without this guard\n // the payload would fall through to the raw-details branch below, where\n // `{ error: 'oops' }` satisfies `isDetailsRecord` and silently produces\n // a phantom `ValidationError` at path `['error']`.\n if (wrappedError !== null && wrappedError !== undefined && typeof wrappedError !== 'object') {\n return {\n ok: false,\n reason: `payload.error was ${typeof wrappedError}, expected an object with { details }`,\n }\n }\n\n if ('details' in payload) {\n const inner = payload['details']\n if (inner === undefined) return { ok: true, details: {} }\n if (isDetailsRecord(inner)) return { ok: true, details: inner }\n return { ok: false, reason: 'details entries must be strings or { message, code } objects' }\n }\n\n if (isDetailsRecord(payload)) return { ok: true, details: payload }\n\n // Heuristic: if the payload has keys but none of them look like details,\n // it's probably a completely different shape. Reject.\n if (Object.keys(payload).length === 0) return { ok: true, details: {} }\n return { ok: false, reason: 'unrecognised payload shape' }\n}\n\nfunction isStructuredEntry(value: unknown): value is ApiErrorEntry {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return false\n const obj = value as { message?: unknown; code?: unknown }\n return typeof obj.message === 'string' && typeof obj.code === 'string'\n}\n\n/**\n * Accepts either a structured `{ message, code }` entry OR a bare\n * string. Bare strings synthesize a `code` at parse time\n * (`options.defaultCode`) and are useful for the Rails / Django REST\n * Framework / Laravel JSON shape that doesn't carry a per-field code.\n */\nfunction isAcceptedEntry(value: unknown): value is string | ApiErrorEntry {\n return typeof value === 'string' || isStructuredEntry(value)\n}\n\n/**\n * A record is a \"details\" record when every value is either an\n * accepted entry or an array of accepted entries (mixing structured +\n * bare-string in the same array is fine; the parser normalises per\n * entry). Half-structured objects (e.g. `{ message: 'x' }` missing\n * `code`) are still rejected so the bug surfaces — see the\n * `'rejects entries that are objects but missing required fields'`\n * test for the rationale.\n */\nfunction isDetailsRecord(value: unknown): value is ApiErrorDetails {\n if (value === null || typeof value !== 'object' || Array.isArray(value)) return false\n // Reject prototype-polluted keys — we don't use them here, but downstream\n // spreads shouldn't have to worry about this input.\n const record = value as Record<string, unknown>\n for (const k of Object.keys(record)) {\n const v = record[k]\n if (isAcceptedEntry(v)) continue\n if (Array.isArray(v) && v.every((entry) => isAcceptedEntry(entry))) continue\n return false\n }\n return true\n}\n"],"names":["normalizeNumericOption","canonicalizePath","InvalidPathError"],"mappings":";;;;;;AAcO,SAAS,sBAAsB,IAAA,EAAsB;AAC1D,EAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,oBAAA,EAAsB,CAAC,IAAA,KAAS;AAClD,IAAA,QAAQ,IAAA;AAAM,MACZ,KAAK,GAAA;AACH,QAAA,OAAO,SAAA;AAAA,MACT,KAAK,GAAA;AACH,QAAA,OAAO,SAAA;AAAA,MACT,KAAK,GAAA;AACH,QAAA,OAAO,SAAA;AAAA,MACT,KAAK,QAAA;AACH,QAAA,OAAO,SAAA;AAAA,MACT,KAAK,QAAA;AACH,QAAA,OAAO,SAAA;AAAA,MACT;AACE,QAAA,OAAO,IAAA;AAAA;AACX,EACF,CAAC,CAAA;AACH;;ACmDO,MAAM,yBAAA,GAA4B;AAAA,EACvC,UAAA,EAAY,GAAA;AAAA,EACZ,YAAA,EAAc,EAAA;AAAA,EACd,gBAAA,EAAkB,GAAA;AAAA,EAClB,WAAA,EAAa;AACf;AAmDO,SAAS,cAAA,CACd,SACA,OAAA,EACsB;AACtB,EAAA,MAAM,EAAE,UAAA,EAAY,YAAA,EAAc,kBAAkB,WAAA,EAAY,GAAI,mBAAmB,OAAO,CAAA;AAE9F,EAAA,IAAI,OAAA,KAAY,IAAA,IAAQ,OAAA,KAAY,MAAA,EAAW;AAC7C,IAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,MAAA,EAAQ,EAAC,EAAE;AAAA,EAChC;AACA,EAAA,IAAI,OAAO,YAAY,QAAA,EAAU;AAC/B,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,IAAI,QAAA,EAAU,CAAA,YAAA,EAAe,OAAO,OAAO,CAAA,iBAAA,CAAA,EAAoB;AAAA,EAC7F;AAEA,EAAA,MAAM,UAAA,GAAa,eAAe,OAAkC,CAAA;AACpE,EAAA,IAAI,CAAC,WAAW,EAAA,EAAI;AAClB,IAAA,OAAO,EAAE,IAAI,KAAA,EAAO,MAAA,EAAQ,EAAC,EAAG,QAAA,EAAU,WAAW,MAAA,EAAO;AAAA,EAC9D;AAEA,EAAA,MAAM,EAAE,SAAQ,GAAI,UAAA;AACpB,EAAA,MAAM,UAAA,GAAa,MAAA,CAAO,IAAA,CAAK,OAAO,CAAA,CAAE,MAAA;AAIxC,EAAA,IAAI,aAAa,UAAA,EAAY;AAC3B,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,QAAQ,EAAC;AAAA,MACT,QAAA,EAAU,CAAA,YAAA,EAAe,UAAU,CAAA,6BAAA,EAAgC,UAAU,CAAA;AAAA,KAC/E;AAAA,EACF;AAEA,EAAA,OAAO,sBAAA;AAAA,IACL,OAAA;AAAA,IACA,OAAA,CAAQ,OAAA;AAAA,IACR,YAAA;AAAA,IACA,gBAAA;AAAA,IACA;AAAA,GACF;AACF;AAoBA,SAAS,mBAAmB,OAAA,EAAqD;AAC/E,EAAA,MAAM,aAAaA,mCAAA,CAAuB;AAAA,IACxC,KAAA,EAAO,OAAA,CAAQ,UAAA,IAAc,yBAAA,CAA0B,UAAA;AAAA,IACvD,MAAA,EAAQ,2BAAA;AAAA,IACR,aAAA,EAAe,KAAA;AAAA,IACf,GAAA,EAAK,CAAA;AAAA,IACL,cAAc,yBAAA,CAA0B;AAAA,GACzC,CAAA;AACD,EAAA,MAAM,eAAeA,mCAAA,CAAuB;AAAA,IAC1C,KAAA,EAAO,OAAA,CAAQ,YAAA,IAAgB,yBAAA,CAA0B,YAAA;AAAA,IACzD,MAAA,EAAQ,6BAAA;AAAA,IACR,aAAA,EAAe,KAAA;AAAA,IACf,GAAA,EAAK,CAAA;AAAA,IACL,cAAc,yBAAA,CAA0B;AAAA,GACzC,CAAA;AACD,EAAA,MAAM,mBAAmBA,mCAAA,CAAuB;AAAA,IAC9C,KAAA,EAAO,OAAA,CAAQ,gBAAA,IAAoB,yBAAA,CAA0B,gBAAA;AAAA,IAC7D,MAAA,EAAQ,iCAAA;AAAA,IACR,aAAA,EAAe,KAAA;AAAA,IACf,GAAA,EAAK,CAAA;AAAA,IACL,cAAc,yBAAA,CAA0B;AAAA,GACzC,CAAA;AACD,EAAA,MAAM,WAAA,GAAc,OAAA,CAAQ,WAAA,IAAe,yBAAA,CAA0B,WAAA;AACrE,EAAA,OAAO,EAAE,UAAA,EAAY,YAAA,EAAc,gBAAA,EAAkB,WAAA,EAAY;AACnE;AAUA,SAAS,sBAAA,CACP,OAAA,EACA,OAAA,EACA,YAAA,EACA,kBACA,WAAA,EACsB;AACtB,EAAA,MAAM,SAA4B,EAAC;AACnC,EAAA,IAAI,aAAA,GAAgB,CAAA;AACpB,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAA,EAAG;AAClD,IAAA,MAAM,YAAmD,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,GAAI,KAAA,GAAQ,CAAC,KAAK,CAAA;AAM9F,IAAA,IAAI,QAAA;AACJ,IAAA,IAAI;AACF,MAAA,QAAA,GAAWC,sBAAA,CAAiB,GAAG,CAAA,CAAE,QAAA;AAAA,IACnC,SAAS,GAAA,EAAK;AACZ,MAAA,IAAI,eAAeC,sBAAA,EAAkB;AACrC,MAAA,MAAM,GAAA;AAAA,IACR;AAMA,IAAA,IAAI,QAAA,CAAS,SAAS,YAAA,EAAc;AAIpC,IAAA,aAAA,IAAiB,QAAA,CAAS,MAAA;AAC1B,IAAA,IAAI,gBAAgB,gBAAA,EAAkB;AACpC,MAAA,OAAO;AAAA,QACL,EAAA,EAAI,KAAA;AAAA,QACJ,QAAQ,EAAC;AAAA,QACT,QAAA,EAAU,wDAAwD,gBAAgB,CAAA;AAAA,OACpF;AAAA,IACF;AACA,IAAA,KAAA,MAAW,SAAS,SAAA,EAAW;AAM7B,MAAA,MAAM,OAAA,GAAU,OAAO,KAAA,KAAU,QAAA,GAAW,QAAQ,KAAA,CAAM,OAAA;AAC1D,MAAA,MAAM,IAAA,GAAO,OAAO,KAAA,KAAU,QAAA,GAAW,cAAc,KAAA,CAAM,IAAA;AAC7D,MAAA,IAAI,OAAA,CAAQ,WAAW,CAAA,EAAG;AAC1B,MAAA,MAAA,CAAO,IAAA,CAAK;AAAA,QACV,OAAA;AAAA,QACA,IAAA,EAAM,KAAA,CAAM,IAAA,CAAK,QAAQ,CAAA;AAAA,QACzB,OAAA;AAAA,QACA;AAAA,OACD,CAAA;AAAA,IACH;AAAA,EACF;AACA,EAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,MAAA,EAAO;AAC5B;AAIA,SAAS,eAAe,OAAA,EAAiD;AACvE,EAAA,MAAM,YAAA,GAAe,QAAQ,OAAO,CAAA;AACpC,EAAA,IAAI,iBAAiB,IAAA,IAAQ,YAAA,KAAiB,MAAA,IAAa,OAAO,iBAAiB,QAAA,EAAU;AAC3F,IAAA,MAAM,QAAS,YAAA,CAAuC,OAAA;AACtD,IAAA,IAAI,UAAU,MAAA,EAAW;AAEvB,MAAA,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,OAAA,EAAS,EAAC,EAAE;AAAA,IACjC;AACA,IAAA,IAAI,eAAA,CAAgB,KAAK,CAAA,EAAG,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,SAAS,KAAA,EAAM;AAC9D,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,MAAA,EAAQ;AAAA,KACV;AAAA,EACF;AAOA,EAAA,IAAI,iBAAiB,IAAA,IAAQ,YAAA,KAAiB,MAAA,IAAa,OAAO,iBAAiB,QAAA,EAAU;AAC3F,IAAA,OAAO;AAAA,MACL,EAAA,EAAI,KAAA;AAAA,MACJ,MAAA,EAAQ,CAAA,kBAAA,EAAqB,OAAO,YAAY,CAAA,qCAAA;AAAA,KAClD;AAAA,EACF;AAEA,EAAA,IAAI,aAAa,OAAA,EAAS;AACxB,IAAA,MAAM,KAAA,GAAQ,QAAQ,SAAS,CAAA;AAC/B,IAAA,IAAI,KAAA,KAAU,QAAW,OAAO,EAAE,IAAI,IAAA,EAAM,OAAA,EAAS,EAAC,EAAE;AACxD,IAAA,IAAI,eAAA,CAAgB,KAAK,CAAA,EAAG,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,SAAS,KAAA,EAAM;AAC9D,IAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,8DAAA,EAA+D;AAAA,EAC7F;AAEA,EAAA,IAAI,eAAA,CAAgB,OAAO,CAAA,EAAG,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,SAAS,OAAA,EAAQ;AAIlE,EAAA,IAAI,MAAA,CAAO,IAAA,CAAK,OAAO,CAAA,CAAE,MAAA,KAAW,CAAA,EAAG,OAAO,EAAE,EAAA,EAAI,IAAA,EAAM,OAAA,EAAS,EAAC,EAAE;AACtE,EAAA,OAAO,EAAE,EAAA,EAAI,KAAA,EAAO,MAAA,EAAQ,4BAAA,EAA6B;AAC3D;AAEA,SAAS,kBAAkB,KAAA,EAAwC;AACjE,EAAA,IAAI,KAAA,KAAU,QAAQ,OAAO,KAAA,KAAU,YAAY,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG,OAAO,KAAA;AAChF,EAAA,MAAM,GAAA,GAAM,KAAA;AACZ,EAAA,OAAO,OAAO,GAAA,CAAI,OAAA,KAAY,QAAA,IAAY,OAAO,IAAI,IAAA,KAAS,QAAA;AAChE;AAQA,SAAS,gBAAgB,KAAA,EAAiD;AACxE,EAAA,OAAO,OAAO,KAAA,KAAU,QAAA,IAAY,iBAAA,CAAkB,KAAK,CAAA;AAC7D;AAWA,SAAS,gBAAgB,KAAA,EAA0C;AACjE,EAAA,IAAI,KAAA,KAAU,QAAQ,OAAO,KAAA,KAAU,YAAY,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG,OAAO,KAAA;AAGhF,EAAA,MAAM,MAAA,GAAS,KAAA;AACf,EAAA,KAAA,MAAW,CAAA,IAAK,MAAA,CAAO,IAAA,CAAK,MAAM,CAAA,EAAG;AACnC,IAAA,MAAM,CAAA,GAAI,OAAO,CAAC,CAAA;AAClB,IAAA,IAAI,eAAA,CAAgB,CAAC,CAAA,EAAG;AACxB,IAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA,IAAK,CAAA,CAAE,KAAA,CAAM,CAAC,KAAA,KAAU,eAAA,CAAgB,KAAK,CAAC,CAAA,EAAG;AACpE,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,OAAO,IAAA;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|