attaform 0.17.1 → 0.18.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 +77 -36
- package/dist/chunks/devtools.cjs +10 -37
- package/dist/chunks/devtools.cjs.map +1 -1
- package/dist/chunks/devtools.mjs +10 -37
- package/dist/chunks/devtools.mjs.map +1 -1
- package/dist/chunks/indexeddb.cjs +4 -4
- package/dist/chunks/indexeddb.cjs.map +1 -1
- package/dist/chunks/indexeddb.mjs +1 -1
- package/dist/chunks/local-storage.cjs +2 -2
- package/dist/chunks/local-storage.cjs.map +1 -1
- package/dist/chunks/local-storage.mjs +1 -1
- package/dist/chunks/session-storage.cjs +2 -2
- package/dist/chunks/session-storage.cjs.map +1 -1
- package/dist/chunks/session-storage.mjs +1 -1
- package/dist/index.cjs +42 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +159 -196
- package/dist/index.d.mts +159 -196
- package/dist/index.d.ts +159 -196
- package/dist/index.mjs +5 -7
- package/dist/index.mjs.map +1 -1
- package/dist/nuxt.cjs +31 -40
- package/dist/nuxt.cjs.map +1 -1
- package/dist/nuxt.d.cts +8 -1
- package/dist/nuxt.d.mts +8 -1
- package/dist/nuxt.d.ts +8 -1
- package/dist/nuxt.mjs +32 -41
- package/dist/nuxt.mjs.map +1 -1
- package/dist/runtime/components/AttaformDevtoolsPanel.d.vue.ts +7 -0
- package/dist/runtime/components/AttaformDevtoolsPanel.vue +453 -0
- package/dist/runtime/components/AttaformDevtoolsPanel.vue.d.ts +7 -0
- package/dist/runtime/components/DevtoolsValueTree.d.vue.ts +37 -0
- package/dist/runtime/components/DevtoolsValueTree.vue +192 -0
- package/dist/runtime/components/DevtoolsValueTree.vue.d.ts +37 -0
- package/dist/runtime/plugins/attaform.cjs +17 -6
- package/dist/runtime/plugins/attaform.cjs.map +1 -1
- package/dist/runtime/plugins/attaform.mjs +15 -4
- package/dist/runtime/plugins/attaform.mjs.map +1 -1
- package/dist/shared/attaform.5UhpSVFI.cjs +63 -0
- package/dist/shared/attaform.5UhpSVFI.cjs.map +1 -0
- package/dist/shared/attaform.BDdFdjeX.mjs +57 -0
- package/dist/shared/attaform.BDdFdjeX.mjs.map +1 -0
- package/dist/shared/attaform.Bgu9l6OG.d.cts +1651 -0
- package/dist/shared/attaform.BmDBu4ql.d.ts +1651 -0
- package/dist/shared/{attaform.Dee2rU1P.cjs → attaform.BqK_L4gK.cjs} +310 -24
- package/dist/shared/attaform.BqK_L4gK.cjs.map +1 -0
- package/dist/shared/{attaform.C_5aB6EQ.d.mts → attaform.BsMdl-35.d.cts} +754 -146
- package/dist/shared/{attaform.C_5aB6EQ.d.ts → attaform.BsMdl-35.d.mts} +754 -146
- package/dist/shared/{attaform.C_5aB6EQ.d.cts → attaform.BsMdl-35.d.ts} +754 -146
- package/dist/shared/attaform.Bubm_slq.cjs.map +1 -1
- package/dist/shared/attaform.C3x1hKJC.d.mts +53 -0
- package/dist/shared/{attaform.CuE-bS1C.d.mts → attaform.CWs1Z3p7.d.ts} +57 -23
- package/dist/shared/attaform.CXpzmj38.mjs.map +1 -1
- package/dist/shared/attaform.CjmJpfLH.d.ts +53 -0
- package/dist/shared/{attaform.CVCmBKZX.mjs → attaform.CtNUB9nf.mjs} +74 -72
- package/dist/shared/attaform.CtNUB9nf.mjs.map +1 -0
- package/dist/shared/{attaform.C0iFnTN0.d.ts → attaform.D-hDvb98.d.cts} +57 -23
- package/dist/shared/attaform.DAH3kvav.d.mts +1651 -0
- package/dist/shared/{attaform.B5qiXQwN.cjs → attaform.DUHru0OF.cjs} +83 -81
- package/dist/shared/attaform.DUHru0OF.cjs.map +1 -0
- package/dist/shared/{attaform.BV40t5y2.cjs → attaform.Dlk1jMuv.cjs} +245 -108
- package/dist/shared/attaform.Dlk1jMuv.cjs.map +1 -0
- package/dist/shared/{attaform.B3ZaPIzS.mjs → attaform.DsC3rZHG.mjs} +1804 -219
- package/dist/shared/attaform.DsC3rZHG.mjs.map +1 -0
- package/dist/shared/attaform.Dzi89x8N.d.cts +53 -0
- package/dist/shared/{attaform.Cer8JO_P.cjs → attaform.II89Pcf4.cjs} +1860 -272
- package/dist/shared/attaform.II89Pcf4.cjs.map +1 -0
- package/dist/shared/{attaform.CIEQgJnM.mjs → attaform.Xhg0AYNa.mjs} +300 -26
- package/dist/shared/attaform.Xhg0AYNa.mjs.map +1 -0
- package/dist/shared/{attaform.CpERWz3u.mjs → attaform.Xt0A3QUd.mjs} +232 -95
- package/dist/shared/attaform.Xt0A3QUd.mjs.map +1 -0
- package/dist/shared/{attaform.CHorcsIU.d.cts → attaform.bH7WvNad.d.mts} +57 -23
- package/dist/vite.cjs +270 -2
- package/dist/vite.cjs.map +1 -1
- package/dist/vite.mjs +266 -2
- package/dist/vite.mjs.map +1 -1
- package/dist/zod-v3.cjs +11 -8
- package/dist/zod-v3.cjs.map +1 -1
- package/dist/zod-v3.d.cts +36 -84
- package/dist/zod-v3.d.mts +36 -84
- package/dist/zod-v3.d.ts +36 -84
- package/dist/zod-v3.mjs +3 -3
- package/dist/zod-v4.cjs +11 -8
- package/dist/zod-v4.cjs.map +1 -1
- 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 +3 -3
- package/dist/zod.cjs +13 -10
- package/dist/zod.cjs.map +1 -1
- package/dist/zod.d.cts +127 -15
- package/dist/zod.d.mts +127 -15
- package/dist/zod.d.ts +127 -15
- package/dist/zod.mjs +5 -5
- package/dist/zod.mjs.map +1 -1
- package/package.json +19 -5
- package/dist/shared/attaform.B1jvxsOF.d.mts +0 -156
- package/dist/shared/attaform.B3ZaPIzS.mjs.map +0 -1
- package/dist/shared/attaform.B5qiXQwN.cjs.map +0 -1
- package/dist/shared/attaform.BBM2muQ9.cjs +0 -101
- package/dist/shared/attaform.BBM2muQ9.cjs.map +0 -1
- package/dist/shared/attaform.BV40t5y2.cjs.map +0 -1
- package/dist/shared/attaform.C6qzEdIM.d.cts +0 -156
- package/dist/shared/attaform.C8LVFVVe.cjs +0 -32
- package/dist/shared/attaform.C8LVFVVe.cjs.map +0 -1
- package/dist/shared/attaform.CIEQgJnM.mjs.map +0 -1
- package/dist/shared/attaform.CTwNcpLE.d.ts +0 -156
- package/dist/shared/attaform.CVCmBKZX.mjs.map +0 -1
- package/dist/shared/attaform.Cer8JO_P.cjs.map +0 -1
- package/dist/shared/attaform.CpERWz3u.mjs.map +0 -1
- package/dist/shared/attaform.Dee2rU1P.cjs.map +0 -1
- package/dist/shared/attaform.Vo-Kft0t.mjs +0 -29
- package/dist/shared/attaform.Vo-Kft0t.mjs.map +0 -1
- package/dist/shared/attaform.h1sq3BFu.mjs +0 -92
- package/dist/shared/attaform.h1sq3BFu.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -8,17 +8,25 @@
|
|
|
8
8
|
|
|
9
9
|
A type-safe, schema-driven form library for Vue 3 and Nuxt with first-class Zod support.
|
|
10
10
|
|
|
11
|
+
> Hand a Zod schema to `useForm` and Attaform turns it into a reactive form, typed end-to-end, with live errors and SSR out of the box.
|
|
12
|
+
>
|
|
13
|
+
> It scales from the simplest forms to the most comprehensive multistep wizards while keeping the core experience clear and focused.
|
|
14
|
+
>
|
|
15
|
+
> Because Vue and Nuxt devs deserve nice things, too.
|
|
16
|
+
|
|
17
|
+
**Try it live.** Tweak a schema, edit the template, and watch the form rebind in the browser at [attaform.com/demos](https://www.attaform.com/demos). No install needed.
|
|
18
|
+
|
|
11
19
|
## Installation
|
|
12
20
|
|
|
13
21
|
```bash
|
|
14
22
|
npm install attaform zod
|
|
15
23
|
```
|
|
16
24
|
|
|
17
|
-
That's it for client-side rendering. Forms render and validate the moment you call `useForm
|
|
25
|
+
That's it for client-side rendering. Forms render and validate the moment you call `useForm`; the registry self-installs on first use.
|
|
18
26
|
|
|
19
27
|
### Going further
|
|
20
28
|
|
|
21
|
-
**Nuxt 3 / 4
|
|
29
|
+
**Nuxt 3 / 4.** Add the module:
|
|
22
30
|
|
|
23
31
|
```ts
|
|
24
32
|
// nuxt.config.ts
|
|
@@ -27,7 +35,7 @@ export default defineNuxtConfig({
|
|
|
27
35
|
})
|
|
28
36
|
```
|
|
29
37
|
|
|
30
|
-
**Bare Vue + SSR
|
|
38
|
+
**Bare Vue + SSR.** Add the Vite plugin so server-rendered HTML matches the hydrated client (the plugin injects `:value` / `:checked` bindings at compile time):
|
|
31
39
|
|
|
32
40
|
```ts
|
|
33
41
|
// vite.config.ts
|
|
@@ -39,9 +47,9 @@ export default defineConfig({
|
|
|
39
47
|
})
|
|
40
48
|
```
|
|
41
49
|
|
|
42
|
-
The Vite plugin also rewrites `attaform/zod` imports at build time to `attaform/zod-v3` or `attaform/zod-v4
|
|
50
|
+
The Vite plugin also rewrites `attaform/zod` imports at build time to `attaform/zod-v3` or `attaform/zod-v4`, so your bundle ships only the adapter you actually use.
|
|
43
51
|
|
|
44
|
-
**App-wide options
|
|
52
|
+
**App-wide options.** Install the Vue plugin if you want to set defaults or disable devtools:
|
|
45
53
|
|
|
46
54
|
```ts
|
|
47
55
|
// main.ts
|
|
@@ -55,7 +63,7 @@ createApp(App)
|
|
|
55
63
|
|
|
56
64
|
### Recommended tsconfig
|
|
57
65
|
|
|
58
|
-
|
|
66
|
+
Attaform pairs well with `noUncheckedIndexedAccess: true`:
|
|
59
67
|
|
|
60
68
|
```json
|
|
61
69
|
{
|
|
@@ -68,6 +76,8 @@ We pair well with `noUncheckedIndexedAccess: true`:
|
|
|
68
76
|
|
|
69
77
|
It catches stale `form.values.contacts[N]` reads at compile time. Nuxt 3 / 4 sets this for you.
|
|
70
78
|
|
|
79
|
+
> **Try it live.** Skip the install and tweak a schema in your browser at [attaform.com/demos](https://www.attaform.com/demos).
|
|
80
|
+
|
|
71
81
|
## Quick start
|
|
72
82
|
|
|
73
83
|
```vue
|
|
@@ -95,53 +105,84 @@ It catches stale `form.values.contacts[N]` reads at compile time. Nuxt 3 / 4 set
|
|
|
95
105
|
<input v-register="form.register('password')" type="password" placeholder="Password" />
|
|
96
106
|
<small v-if="form.errors.password?.[0]">{{ form.errors.password[0].message }}</small>
|
|
97
107
|
|
|
98
|
-
<button :disabled="form.meta.
|
|
108
|
+
<button :disabled="form.meta.submitting">Sign up</button>
|
|
99
109
|
</form>
|
|
100
110
|
</template>
|
|
101
111
|
```
|
|
102
112
|
|
|
103
|
-
`useForm({ schema, key })` returns a Pinia-style reactive
|
|
113
|
+
`useForm({ schema, key })` returns a Pinia-style reactive form. Read leaves directly, no `.value`:
|
|
114
|
+
|
|
115
|
+
- **`form.values`**: current values. `form.values.username`, `form.values.address.city`.
|
|
116
|
+
- **`form.errors`**: per-field errors, keyed by dotted path. `form.errors.username?.[0]?.message`.
|
|
117
|
+
- **`form.fields`**: per-field flags (`dirty`, `touched`, `errors`, `blank`, …). `form.fields.username.dirty`.
|
|
118
|
+
- **`form.meta`**: form-level flags and counters (`submitting`, `valid`, `dirty`, `submissionAttempts`, the flat `meta.errors` aggregate, the per-mount `instanceId`, …).
|
|
119
|
+
- **`form.history`**: consolidated undo/redo namespace (`undo()`, `redo()`, `clear()`, `canUndo`, `canRedo`, `size`).
|
|
120
|
+
- **`form.register(path)`**: typed two-way binding. Pair with `v-register` on `<input>` / `<textarea>` / `<select>`.
|
|
121
|
+
- **`form.handleSubmit(onValid, onInvalid?)`**: runs validation, dispatches. The valid callback receives the strict Zod-inferred type.
|
|
122
|
+
- **`form.setValue(path, value)`**, **`form.reset()`**, field-array helpers, persistence: see [Entry points](https://www.attaform.com/docs/reference/entry-points).
|
|
123
|
+
|
|
124
|
+
> **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.
|
|
125
|
+
|
|
126
|
+
## `v-register`: one line, every option
|
|
127
|
+
|
|
128
|
+
`v-register` stays on the same native `<input>`. Every option you add opts into another runtime feature without touching the template structure.
|
|
129
|
+
|
|
130
|
+
```vue
|
|
131
|
+
<input v-register="form.register('email')" />
|
|
132
|
+
```
|
|
104
133
|
|
|
105
|
-
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
134
|
+
Typed two-way binding to `form.values.email`, with schema-driven coercion at the directive layer.
|
|
135
|
+
|
|
136
|
+
```vue
|
|
137
|
+
<input v-register="form.register('email', { persist: true })" />
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Same line. The field now writes through to the form's persistence backend on every keystroke, with the sensitive-name guard catching accidental `password`-style opt-ins.
|
|
141
|
+
|
|
142
|
+
```vue
|
|
143
|
+
<input
|
|
144
|
+
v-register="form.register('email', { persist: true, transforms: [lowercase], multiTab: false })"
|
|
145
|
+
/>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Same line. Add a sync DOM-input transform, opt out of multi-tab sync, all without touching the markup elsewhere.
|
|
113
149
|
|
|
114
150
|
## Features
|
|
115
151
|
|
|
116
|
-
- **Schema-driven types
|
|
117
|
-
- **Live validation
|
|
118
|
-
- **Schema-driven coercion
|
|
119
|
-
- **Register transforms
|
|
120
|
-
- **
|
|
121
|
-
- **
|
|
122
|
-
- **
|
|
123
|
-
- **
|
|
124
|
-
- **
|
|
125
|
-
- **
|
|
126
|
-
- **
|
|
127
|
-
- **
|
|
152
|
+
- **Schema-driven types.** Every path, value, and error is inferred from your Zod schema. No `any`, no manual type plumbing.
|
|
153
|
+
- **Live validation.** `validateOn: 'change'` by default with synchronous `debounceMs: 0`. `'blur'` and `'submit'` (opt-out) modes available. Async refinements await before submit dispatches.
|
|
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.
|
|
155
|
+
- **Register transforms.** `form.register('slug', { transforms: [lowercase, dashify] })` runs sync write-time normalization before storage commit.
|
|
156
|
+
- **First-class multistep.** `useWizard` composes `useForm` instances into a flow with shared navigation, per-step validation, persistence across steps, and deep-link restore.
|
|
157
|
+
- **DevTools panel.** Auto-wired in Nuxt. Walk history, edit values live, inspect every form on the page. No probes to install.
|
|
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.
|
|
159
|
+
- **Field arrays.** `append` / `prepend` / `insert` / `remove` / `swap` / `move` / `replace`, fully typed at the call site.
|
|
160
|
+
- **Drafts and undo / redo.** Per-field opt-in persistence (`localStorage` / `sessionStorage` / IndexedDB / custom backend) and a bounded undo stack.
|
|
161
|
+
- **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.
|
|
162
|
+
- **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.
|
|
163
|
+
- **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:`, …).
|
|
164
|
+
- **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.
|
|
165
|
+
- **SSR.** Nuxt handles the payload round-trip automatically; bare Vue uses `renderAttaformState` / `hydrateAttaformState`.
|
|
128
166
|
|
|
129
167
|
## Documentation
|
|
130
168
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
- [**
|
|
134
|
-
- [**
|
|
135
|
-
- [**
|
|
136
|
-
- [**
|
|
169
|
+
Full docs live at [www.attaform.com](https://www.attaform.com):
|
|
170
|
+
|
|
171
|
+
- [**Quick start**](https://www.attaform.com/docs/getting-started/quick-start): schema, form, submit, in one page.
|
|
172
|
+
- [**Why Attaform**](https://www.attaform.com/docs/getting-started/why-attaform): the convictions behind the design.
|
|
173
|
+
- [**Entry points**](https://www.attaform.com/docs/reference/entry-points): every public export, grouped by subpath.
|
|
174
|
+
- [**useWizard**](https://www.attaform.com/docs/multistep/use-wizard): multistep flows with shared state and validation.
|
|
175
|
+
- [**Performance**](https://www.attaform.com/docs/server-and-ssr/performance): how it scales; when to worry.
|
|
176
|
+
- [**Troubleshooting**](https://www.attaform.com/docs/devtools-and-debugging/troubleshooting): common gotchas and fixes.
|
|
177
|
+
- [**Changelog**](./CHANGELOG.md): full release history.
|
|
137
178
|
|
|
138
179
|
## Status
|
|
139
180
|
|
|
140
|
-
Pre-1.0. SemVer applies from `v1.0` onward; 0.x minor bumps may still include breaking changes, each documented
|
|
181
|
+
Pre-1.0. SemVer applies from `v1.0` onward; 0.x minor bumps may still include breaking changes, each documented in the [Changelog](./CHANGELOG.md).
|
|
141
182
|
|
|
142
183
|
## License
|
|
143
184
|
|
|
144
|
-
MIT
|
|
185
|
+
MIT; see [LICENSE](./LICENSE).
|
|
145
186
|
|
|
146
187
|
<!-- Badges -->
|
|
147
188
|
|
package/dist/chunks/devtools.cjs
CHANGED
|
@@ -1,31 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const paths = require('../shared/attaform.
|
|
3
|
+
const paths = require('../shared/attaform.BqK_L4gK.cjs');
|
|
4
4
|
|
|
5
5
|
const INSPECTOR_ID = "attaform";
|
|
6
6
|
const TIMELINE_LAYER_ID = "attaform:events";
|
|
7
|
-
const REDACTED = "[redacted]";
|
|
8
|
-
function redactSensitiveLeaves(value, matchSensitive) {
|
|
9
|
-
return redactImpl(value, false, matchSensitive);
|
|
10
|
-
}
|
|
11
|
-
function redactImpl(value, inSensitiveSubtree, matchSensitive) {
|
|
12
|
-
if (value === null || value === void 0) return value;
|
|
13
|
-
if (typeof value !== "object") {
|
|
14
|
-
return inSensitiveSubtree ? REDACTED : value;
|
|
15
|
-
}
|
|
16
|
-
if (Array.isArray(value)) {
|
|
17
|
-
return value.map((item) => redactImpl(item, inSensitiveSubtree, matchSensitive));
|
|
18
|
-
}
|
|
19
|
-
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
|
|
20
|
-
return inSensitiveSubtree ? REDACTED : value;
|
|
21
|
-
}
|
|
22
|
-
const out = {};
|
|
23
|
-
for (const key of Object.keys(value)) {
|
|
24
|
-
const childSensitive = inSensitiveSubtree || matchSensitive(key);
|
|
25
|
-
out[key] = redactImpl(value[key], childSensitive, matchSensitive);
|
|
26
|
-
}
|
|
27
|
-
return out;
|
|
28
|
-
}
|
|
29
7
|
async function setupAttaformDevtools(app, registry) {
|
|
30
8
|
let mod;
|
|
31
9
|
try {
|
|
@@ -68,14 +46,11 @@ function wire(api, app, registry) {
|
|
|
68
46
|
time: Date.now(),
|
|
69
47
|
title: "form.change",
|
|
70
48
|
subtitle: state.formKey,
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
data: {
|
|
77
|
-
form: redactSensitiveLeaves(state.form.value, state.segmentMatchesSensitive)
|
|
78
|
-
}
|
|
49
|
+
// Devtools is dev-only — emit raw values. Consumers worried
|
|
50
|
+
// about screen-share leaks should close the panel before
|
|
51
|
+
// sharing, same as they would for the browser DevTools
|
|
52
|
+
// console.
|
|
53
|
+
data: { form: state.form.value }
|
|
79
54
|
}
|
|
80
55
|
});
|
|
81
56
|
});
|
|
@@ -86,9 +61,7 @@ function wire(api, app, registry) {
|
|
|
86
61
|
time: Date.now(),
|
|
87
62
|
title: "submit.success",
|
|
88
63
|
subtitle: state.formKey,
|
|
89
|
-
data: {
|
|
90
|
-
form: redactSensitiveLeaves(state.form.value, state.segmentMatchesSensitive)
|
|
91
|
-
}
|
|
64
|
+
data: { form: state.form.value }
|
|
92
65
|
}
|
|
93
66
|
});
|
|
94
67
|
});
|
|
@@ -138,7 +111,7 @@ function wire(api, app, registry) {
|
|
|
138
111
|
payload.state["Form value"] = [
|
|
139
112
|
{
|
|
140
113
|
key: "form",
|
|
141
|
-
value:
|
|
114
|
+
value: state.form.value,
|
|
142
115
|
editable: true
|
|
143
116
|
}
|
|
144
117
|
];
|
|
@@ -156,7 +129,8 @@ function wire(api, app, registry) {
|
|
|
156
129
|
];
|
|
157
130
|
payload.state["Aggregates"] = [
|
|
158
131
|
{ key: "submitting", value: state.submitting.value },
|
|
159
|
-
{ key: "
|
|
132
|
+
{ key: "submissionAttempts", value: state.submissionAttempts.value },
|
|
133
|
+
{ key: "departAttempts", value: state.departAttempts.value },
|
|
160
134
|
{ key: "submitError", value: state.submitError.value },
|
|
161
135
|
{ key: "activeValidations", value: state.activeValidations.value }
|
|
162
136
|
];
|
|
@@ -172,7 +146,6 @@ function wire(api, app, registry) {
|
|
|
172
146
|
if (section !== "Form value") return;
|
|
173
147
|
const segments = payload.path.slice(2);
|
|
174
148
|
const { segments: canonicalPath, key: canonicalKey } = paths.canonicalizePath(segments);
|
|
175
|
-
if (state.isSensitivePath([...canonicalPath])) return;
|
|
176
149
|
state.setValueAtPath(canonicalPath, payload.state.value, {
|
|
177
150
|
persist: state.persistOptIns.hasAnyOptInForPath(canonicalKey)
|
|
178
151
|
});
|
|
@@ -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, type Segment } 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\nconst REDACTED = '[redacted]'\n\n/**\n * Walk `value` and replace any leaf whose enclosing path matches the\n * sensitive-name heuristic with the string `'[redacted]'`. Returns a\n * new tree (no mutation of the input). Object keys + array indices\n * are preserved; only the leaf payloads change.\n *\n * Applied to BOTH the DevTools timeline events and the inspector\n * `Form value` panel — leaks via either surface are treatable as\n * \"any developer with the panel open during user testing can read\n * a customer's password,\" which is exactly the failure mode the\n * sensitive-name guard exists to prevent on the storage side.\n *\n * Leaves whose path doesn't match a pattern pass through untouched.\n * `acknowledgeSensitive: true` on persistence does NOT bypass this —\n * if the consumer opted into persisting the value, they still\n * shouldn't see it in DevTools timelines that grow unbounded.\n *\n * Implementation note: tracks an `inSensitiveSubtree` flag through\n * the recursion instead of allocating a fresh path array per node\n * + calling `isSensitivePath` per leaf. Once any ancestor segment\n * matches the heuristic, the flag stays set for every descendant —\n * the leaf simply returns `REDACTED` without re-scanning the path.\n * For a 100-leaf form: ~100 path allocations + ~100 full-path regex\n * sweeps → 0 path allocations + ~100 single-segment regex sweeps,\n * with whole-subtree short-circuit when sensitive ancestors are\n * found early.\n */\nfunction redactSensitiveLeaves(\n value: unknown,\n matchSensitive: (segment: Segment) => boolean\n): unknown {\n return redactImpl(value, false, matchSensitive)\n}\n\nfunction redactImpl(\n value: unknown,\n inSensitiveSubtree: boolean,\n matchSensitive: (segment: Segment) => boolean\n): unknown {\n if (value === null || value === undefined) return value\n if (typeof value !== 'object') {\n return inSensitiveSubtree ? REDACTED : value\n }\n if (Array.isArray(value)) {\n // Numeric segments never match the sensitive-name heuristic\n // (segmentMatchesSensitive rejects non-string segments), so the\n // flag passes through unchanged when descending into arrays.\n return value.map((item) => redactImpl(item, inSensitiveSubtree, matchSensitive))\n }\n // Non-plain object (Map / Set / Date / class instance) — redact\n // wholesale if we're already in a sensitive subtree; otherwise pass\n // through. DevTools rendering of these is already heuristic, so we\n // don't try to descend into them.\n if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {\n return inSensitiveSubtree ? REDACTED : value\n }\n const out: Record<string, unknown> = {}\n for (const key of Object.keys(value as Record<string, unknown>)) {\n const childSensitive = inSensitiveSubtree || matchSensitive(key)\n out[key] = redactImpl((value as Record<string, unknown>)[key], childSensitive, matchSensitive)\n }\n return out\n}\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 // Redact sensitive-named leaves before they land in the\n // timeline event log — events accumulate for the whole\n // session and a screen-share / paired-debugging session\n // would otherwise expose any password / token / etc. the\n // user typed since DevTools was opened.\n data: {\n form: redactSensitiveLeaves(state.form.value, state.segmentMatchesSensitive) as Record<\n string,\n unknown\n >,\n },\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: {\n form: redactSensitiveLeaves(state.form.value, state.segmentMatchesSensitive) as Record<\n string,\n unknown\n >,\n },\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 // Redact sensitive-named leaves in the inspector panel for the\n // same reason as the timeline events: a screen-share with an\n // open DevTools panel shouldn't expose passwords / tokens.\n // Editing stays enabled at the section level — the editInspector\n // handler refuses sensitive-path edits at write time so a dev\n // can't accidentally write the literal string `'[redacted]'` over\n // a real value.\n payload.state['Form value'] = [\n {\n key: 'form',\n value: redactSensitiveLeaves(state.form.value, state.segmentMatchesSensitive),\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: 'submitCount', value: state.submitCount.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 // Refuse edits on sensitive-named paths. The inspector renders\n // them as `'[redacted]'`, so a dev who confirms the field would\n // overwrite the real value with the literal masked string. Edits\n // to sensitive paths must go through the bound input element.\n if (state.isSensitivePath([...canonicalPath])) return\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":"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"}
|
|
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":"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"}
|
package/dist/chunks/devtools.mjs
CHANGED
|
@@ -1,29 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as canonicalizePath } from '../shared/attaform.Xhg0AYNa.mjs';
|
|
2
2
|
|
|
3
3
|
const INSPECTOR_ID = "attaform";
|
|
4
4
|
const TIMELINE_LAYER_ID = "attaform:events";
|
|
5
|
-
const REDACTED = "[redacted]";
|
|
6
|
-
function redactSensitiveLeaves(value, matchSensitive) {
|
|
7
|
-
return redactImpl(value, false, matchSensitive);
|
|
8
|
-
}
|
|
9
|
-
function redactImpl(value, inSensitiveSubtree, matchSensitive) {
|
|
10
|
-
if (value === null || value === void 0) return value;
|
|
11
|
-
if (typeof value !== "object") {
|
|
12
|
-
return inSensitiveSubtree ? REDACTED : value;
|
|
13
|
-
}
|
|
14
|
-
if (Array.isArray(value)) {
|
|
15
|
-
return value.map((item) => redactImpl(item, inSensitiveSubtree, matchSensitive));
|
|
16
|
-
}
|
|
17
|
-
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
|
|
18
|
-
return inSensitiveSubtree ? REDACTED : value;
|
|
19
|
-
}
|
|
20
|
-
const out = {};
|
|
21
|
-
for (const key of Object.keys(value)) {
|
|
22
|
-
const childSensitive = inSensitiveSubtree || matchSensitive(key);
|
|
23
|
-
out[key] = redactImpl(value[key], childSensitive, matchSensitive);
|
|
24
|
-
}
|
|
25
|
-
return out;
|
|
26
|
-
}
|
|
27
5
|
async function setupAttaformDevtools(app, registry) {
|
|
28
6
|
let mod;
|
|
29
7
|
try {
|
|
@@ -66,14 +44,11 @@ function wire(api, app, registry) {
|
|
|
66
44
|
time: Date.now(),
|
|
67
45
|
title: "form.change",
|
|
68
46
|
subtitle: state.formKey,
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
data: {
|
|
75
|
-
form: redactSensitiveLeaves(state.form.value, state.segmentMatchesSensitive)
|
|
76
|
-
}
|
|
47
|
+
// Devtools is dev-only — emit raw values. Consumers worried
|
|
48
|
+
// about screen-share leaks should close the panel before
|
|
49
|
+
// sharing, same as they would for the browser DevTools
|
|
50
|
+
// console.
|
|
51
|
+
data: { form: state.form.value }
|
|
77
52
|
}
|
|
78
53
|
});
|
|
79
54
|
});
|
|
@@ -84,9 +59,7 @@ function wire(api, app, registry) {
|
|
|
84
59
|
time: Date.now(),
|
|
85
60
|
title: "submit.success",
|
|
86
61
|
subtitle: state.formKey,
|
|
87
|
-
data: {
|
|
88
|
-
form: redactSensitiveLeaves(state.form.value, state.segmentMatchesSensitive)
|
|
89
|
-
}
|
|
62
|
+
data: { form: state.form.value }
|
|
90
63
|
}
|
|
91
64
|
});
|
|
92
65
|
});
|
|
@@ -136,7 +109,7 @@ function wire(api, app, registry) {
|
|
|
136
109
|
payload.state["Form value"] = [
|
|
137
110
|
{
|
|
138
111
|
key: "form",
|
|
139
|
-
value:
|
|
112
|
+
value: state.form.value,
|
|
140
113
|
editable: true
|
|
141
114
|
}
|
|
142
115
|
];
|
|
@@ -154,7 +127,8 @@ function wire(api, app, registry) {
|
|
|
154
127
|
];
|
|
155
128
|
payload.state["Aggregates"] = [
|
|
156
129
|
{ key: "submitting", value: state.submitting.value },
|
|
157
|
-
{ key: "
|
|
130
|
+
{ key: "submissionAttempts", value: state.submissionAttempts.value },
|
|
131
|
+
{ key: "departAttempts", value: state.departAttempts.value },
|
|
158
132
|
{ key: "submitError", value: state.submitError.value },
|
|
159
133
|
{ key: "activeValidations", value: state.activeValidations.value }
|
|
160
134
|
];
|
|
@@ -170,7 +144,6 @@ function wire(api, app, registry) {
|
|
|
170
144
|
if (section !== "Form value") return;
|
|
171
145
|
const segments = payload.path.slice(2);
|
|
172
146
|
const { segments: canonicalPath, key: canonicalKey } = canonicalizePath(segments);
|
|
173
|
-
if (state.isSensitivePath([...canonicalPath])) return;
|
|
174
147
|
state.setValueAtPath(canonicalPath, payload.state.value, {
|
|
175
148
|
persist: state.persistOptIns.hasAnyOptInForPath(canonicalKey)
|
|
176
149
|
});
|
|
@@ -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, type Segment } 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\nconst REDACTED = '[redacted]'\n\n/**\n * Walk `value` and replace any leaf whose enclosing path matches the\n * sensitive-name heuristic with the string `'[redacted]'`. Returns a\n * new tree (no mutation of the input). Object keys + array indices\n * are preserved; only the leaf payloads change.\n *\n * Applied to BOTH the DevTools timeline events and the inspector\n * `Form value` panel — leaks via either surface are treatable as\n * \"any developer with the panel open during user testing can read\n * a customer's password,\" which is exactly the failure mode the\n * sensitive-name guard exists to prevent on the storage side.\n *\n * Leaves whose path doesn't match a pattern pass through untouched.\n * `acknowledgeSensitive: true` on persistence does NOT bypass this —\n * if the consumer opted into persisting the value, they still\n * shouldn't see it in DevTools timelines that grow unbounded.\n *\n * Implementation note: tracks an `inSensitiveSubtree` flag through\n * the recursion instead of allocating a fresh path array per node\n * + calling `isSensitivePath` per leaf. Once any ancestor segment\n * matches the heuristic, the flag stays set for every descendant —\n * the leaf simply returns `REDACTED` without re-scanning the path.\n * For a 100-leaf form: ~100 path allocations + ~100 full-path regex\n * sweeps → 0 path allocations + ~100 single-segment regex sweeps,\n * with whole-subtree short-circuit when sensitive ancestors are\n * found early.\n */\nfunction redactSensitiveLeaves(\n value: unknown,\n matchSensitive: (segment: Segment) => boolean\n): unknown {\n return redactImpl(value, false, matchSensitive)\n}\n\nfunction redactImpl(\n value: unknown,\n inSensitiveSubtree: boolean,\n matchSensitive: (segment: Segment) => boolean\n): unknown {\n if (value === null || value === undefined) return value\n if (typeof value !== 'object') {\n return inSensitiveSubtree ? REDACTED : value\n }\n if (Array.isArray(value)) {\n // Numeric segments never match the sensitive-name heuristic\n // (segmentMatchesSensitive rejects non-string segments), so the\n // flag passes through unchanged when descending into arrays.\n return value.map((item) => redactImpl(item, inSensitiveSubtree, matchSensitive))\n }\n // Non-plain object (Map / Set / Date / class instance) — redact\n // wholesale if we're already in a sensitive subtree; otherwise pass\n // through. DevTools rendering of these is already heuristic, so we\n // don't try to descend into them.\n if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {\n return inSensitiveSubtree ? REDACTED : value\n }\n const out: Record<string, unknown> = {}\n for (const key of Object.keys(value as Record<string, unknown>)) {\n const childSensitive = inSensitiveSubtree || matchSensitive(key)\n out[key] = redactImpl((value as Record<string, unknown>)[key], childSensitive, matchSensitive)\n }\n return out\n}\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 // Redact sensitive-named leaves before they land in the\n // timeline event log — events accumulate for the whole\n // session and a screen-share / paired-debugging session\n // would otherwise expose any password / token / etc. the\n // user typed since DevTools was opened.\n data: {\n form: redactSensitiveLeaves(state.form.value, state.segmentMatchesSensitive) as Record<\n string,\n unknown\n >,\n },\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: {\n form: redactSensitiveLeaves(state.form.value, state.segmentMatchesSensitive) as Record<\n string,\n unknown\n >,\n },\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 // Redact sensitive-named leaves in the inspector panel for the\n // same reason as the timeline events: a screen-share with an\n // open DevTools panel shouldn't expose passwords / tokens.\n // Editing stays enabled at the section level — the editInspector\n // handler refuses sensitive-path edits at write time so a dev\n // can't accidentally write the literal string `'[redacted]'` over\n // a real value.\n payload.state['Form value'] = [\n {\n key: 'form',\n value: redactSensitiveLeaves(state.form.value, state.segmentMatchesSensitive),\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: 'submitCount', value: state.submitCount.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 // Refuse edits on sensitive-named paths. The inspector renders\n // them as `'[redacted]'`, so a dev who confirms the field would\n // overwrite the real value with the literal masked string. Edits\n // to sensitive paths must go through the bound input element.\n if (state.isSensitivePath([...canonicalPath])) return\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, 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,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const paths = require('../shared/attaform.BqK_L4gK.cjs');
|
|
4
4
|
|
|
5
5
|
const DB_NAME = "attaform";
|
|
6
6
|
const STORE_NAME = "kv";
|
|
@@ -24,7 +24,7 @@ function openDb() {
|
|
|
24
24
|
};
|
|
25
25
|
request.onsuccess = () => resolve(request.result);
|
|
26
26
|
request.onerror = () => {
|
|
27
|
-
if (
|
|
27
|
+
if (paths.__DEV__ && !warnedOnOpenFailure) {
|
|
28
28
|
warnedOnOpenFailure = true;
|
|
29
29
|
console.warn(
|
|
30
30
|
"[attaform] IndexedDB open failed; persistence disabled. Common causes: private-mode disabled IDB, browser quota policy.",
|
|
@@ -34,7 +34,7 @@ function openDb() {
|
|
|
34
34
|
resolve(null);
|
|
35
35
|
};
|
|
36
36
|
request.onblocked = () => {
|
|
37
|
-
if (
|
|
37
|
+
if (paths.__DEV__ && !warnedOnOpenFailure) {
|
|
38
38
|
warnedOnOpenFailure = true;
|
|
39
39
|
console.warn(
|
|
40
40
|
"[attaform] IndexedDB open blocked (another tab holds an older version); persistence disabled until the conflict resolves."
|
|
@@ -77,7 +77,7 @@ function runWriteOp(fn) {
|
|
|
77
77
|
fn(tx.objectStore(STORE_NAME));
|
|
78
78
|
tx.oncomplete = () => resolve();
|
|
79
79
|
tx.onabort = () => {
|
|
80
|
-
if (
|
|
80
|
+
if (paths.__DEV__ && !warnedOnWriteFailure) {
|
|
81
81
|
warnedOnWriteFailure = true;
|
|
82
82
|
console.warn(
|
|
83
83
|
"[attaform] IndexedDB transaction aborted; subsequent writes will silently no-op. Common cause: storage quota exceeded.",
|