@madojs/mado 0.12.0 → 0.13.1

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.
Files changed (154) hide show
  1. package/AGENTS.md +15 -8
  2. package/CHANGELOG.md +66 -0
  3. package/README.md +25 -3
  4. package/dist/src/component.d.ts +3 -1
  5. package/dist/src/component.d.ts.map +1 -0
  6. package/dist/src/component.js +65 -30
  7. package/dist/src/component.js.map +1 -1
  8. package/dist/src/context.d.ts +13 -32
  9. package/dist/src/context.d.ts.map +1 -0
  10. package/dist/src/context.js +73 -48
  11. package/dist/src/context.js.map +1 -1
  12. package/dist/src/css.d.ts +1 -0
  13. package/dist/src/css.d.ts.map +1 -0
  14. package/dist/src/css.js.map +1 -1
  15. package/dist/src/devtools-hook.d.ts +14 -0
  16. package/dist/src/devtools-hook.d.ts.map +1 -0
  17. package/dist/src/devtools-hook.js +27 -0
  18. package/dist/src/devtools-hook.js.map +1 -0
  19. package/dist/src/devtools.d.ts +24 -22
  20. package/dist/src/devtools.d.ts.map +1 -0
  21. package/dist/src/devtools.js +230 -58
  22. package/dist/src/devtools.js.map +1 -1
  23. package/dist/src/diagnostics.d.ts +12 -0
  24. package/dist/src/diagnostics.d.ts.map +1 -0
  25. package/dist/src/diagnostics.js +68 -8
  26. package/dist/src/diagnostics.js.map +1 -1
  27. package/dist/src/each.d.ts +1 -5
  28. package/dist/src/each.d.ts.map +1 -0
  29. package/dist/src/each.js +0 -8
  30. package/dist/src/each.js.map +1 -1
  31. package/dist/src/forms.d.ts +27 -96
  32. package/dist/src/forms.d.ts.map +1 -0
  33. package/dist/src/forms.js +184 -394
  34. package/dist/src/forms.js.map +1 -1
  35. package/dist/src/head.d.ts +1 -0
  36. package/dist/src/head.d.ts.map +1 -0
  37. package/dist/src/head.js +2 -1
  38. package/dist/src/head.js.map +1 -1
  39. package/dist/src/html/bindings.d.ts +1 -0
  40. package/dist/src/html/bindings.d.ts.map +1 -0
  41. package/dist/src/html/bindings.js.map +1 -1
  42. package/dist/src/html/parser.d.ts +3 -4
  43. package/dist/src/html/parser.d.ts.map +1 -0
  44. package/dist/src/html/parser.js +63 -14
  45. package/dist/src/html/parser.js.map +1 -1
  46. package/dist/src/html/template-types.d.ts +1 -0
  47. package/dist/src/html/template-types.d.ts.map +1 -0
  48. package/dist/src/html/template-types.js.map +1 -1
  49. package/dist/src/html/template.d.ts +5 -1
  50. package/dist/src/html/template.d.ts.map +1 -0
  51. package/dist/src/html/template.js +78 -3
  52. package/dist/src/html/template.js.map +1 -1
  53. package/dist/src/index.d.ts +7 -8
  54. package/dist/src/index.d.ts.map +1 -0
  55. package/dist/src/index.js +5 -7
  56. package/dist/src/index.js.map +1 -1
  57. package/dist/src/json.d.ts +3 -0
  58. package/dist/src/json.d.ts.map +1 -0
  59. package/dist/src/json.js +14 -0
  60. package/dist/src/json.js.map +1 -0
  61. package/dist/src/lifecycle.d.ts +1 -0
  62. package/dist/src/lifecycle.d.ts.map +1 -0
  63. package/dist/src/lifecycle.js +3 -2
  64. package/dist/src/lifecycle.js.map +1 -1
  65. package/dist/src/page.d.ts +1 -0
  66. package/dist/src/page.d.ts.map +1 -0
  67. package/dist/src/page.js.map +1 -1
  68. package/dist/src/persisted.d.ts +6 -1
  69. package/dist/src/persisted.d.ts.map +1 -0
  70. package/dist/src/persisted.js +17 -11
  71. package/dist/src/persisted.js.map +1 -1
  72. package/dist/src/resource.d.ts +2 -1
  73. package/dist/src/resource.d.ts.map +1 -0
  74. package/dist/src/resource.js +126 -49
  75. package/dist/src/resource.js.map +1 -1
  76. package/dist/src/router/base.d.ts +1 -0
  77. package/dist/src/router/base.d.ts.map +1 -0
  78. package/dist/src/router/base.js.map +1 -1
  79. package/dist/src/router/manifest.d.ts +1 -0
  80. package/dist/src/router/manifest.d.ts.map +1 -0
  81. package/dist/src/router/manifest.js +89 -148
  82. package/dist/src/router/manifest.js.map +1 -1
  83. package/dist/src/router/match.d.ts +1 -0
  84. package/dist/src/router/match.d.ts.map +1 -0
  85. package/dist/src/router/match.js +19 -5
  86. package/dist/src/router/match.js.map +1 -1
  87. package/dist/src/router/navigation.d.ts +1 -0
  88. package/dist/src/router/navigation.d.ts.map +1 -0
  89. package/dist/src/router/navigation.js.map +1 -1
  90. package/dist/src/signal.d.ts +1 -0
  91. package/dist/src/signal.d.ts.map +1 -0
  92. package/dist/src/signal.js +46 -17
  93. package/dist/src/signal.js.map +1 -1
  94. package/dist/src/static-runtime.d.ts +1 -0
  95. package/dist/src/static-runtime.d.ts.map +1 -0
  96. package/dist/src/static-runtime.js.map +1 -1
  97. package/dist/src/vite/index.d.ts +1 -0
  98. package/dist/src/vite/index.d.ts.map +1 -0
  99. package/dist/src/vite/index.js +4 -3
  100. package/dist/src/vite/index.js.map +1 -1
  101. package/docs/architecture/v1-roadmap.md +19 -0
  102. package/docs/en/10-pages-and-components.md +9 -1
  103. package/docs/en/13-data.md +8 -6
  104. package/docs/en/14-forms.md +66 -219
  105. package/docs/en/15-static-snapshots.md +4 -4
  106. package/docs/en/16-app-architecture.md +3 -2
  107. package/docs/en/20-deployment.md +21 -8
  108. package/docs/en/21-error-handling.md +9 -6
  109. package/docs/en/24-devtools-and-diagnostics.md +45 -0
  110. package/docs/en/30-api-freeze-map.md +8 -9
  111. package/docs/en/32-v1-stability.md +2 -2
  112. package/docs/en/33-migration-0.12-0.13.md +56 -0
  113. package/docs/en/40-llm-guide.md +7 -9
  114. package/docs/en/41-for-backenders.md +13 -9
  115. package/docs/en/42-why-mado.md +2 -2
  116. package/docs/en/README.md +2 -0
  117. package/docs/recipes/nginx/nginx.conf +2 -2
  118. package/llms.txt +25 -7
  119. package/package.json +37 -16
  120. package/scripts/clean.mjs +8 -0
  121. package/scripts/cli/generate.mjs +11 -14
  122. package/scripts/cli/help.mjs +2 -0
  123. package/scripts/cli/index.mjs +14 -6
  124. package/scripts/cli/init.mjs +13 -18
  125. package/scripts/cli/release.mjs +22 -19
  126. package/scripts/cli/run.mjs +5 -3
  127. package/scripts/logger.mjs +117 -0
  128. package/scripts/output-guard.mjs +128 -0
  129. package/scripts/preview.mjs +11 -19
  130. package/scripts/static/browser.mjs +58 -8
  131. package/scripts/static/discover.mjs +4 -1
  132. package/scripts/static.mjs +21 -7
  133. package/starters/default/package.json +4 -4
  134. package/starters/default/tsconfig.node.json +13 -0
  135. package/starters/default/vite.config.ts +0 -3
  136. package/starters/modular/README.md +4 -4
  137. package/starters/modular/package.json +4 -4
  138. package/starters/modular/src/layouts/app-shell.layout.ts +1 -2
  139. package/starters/modular/src/modules/auth/login.page.ts +5 -12
  140. package/starters/modular/src/modules/billing/pages/invoice-detail.page.ts +3 -4
  141. package/starters/modular/src/modules/billing/pages/invoices-list.page.ts +1 -1
  142. package/starters/modular/src/shared/styles/reset.css +21 -1
  143. package/starters/modular/tsconfig.json +10 -1
  144. package/starters/modular/tsconfig.node.json +13 -0
  145. package/starters/modular/vite.config.ts +6 -5
  146. package/dist/src/lazy.d.ts +0 -38
  147. package/dist/src/lazy.js +0 -73
  148. package/dist/src/lazy.js.map +0 -1
  149. package/scripts/bake.mjs +0 -5
  150. package/scripts/docs-lint.mjs +0 -170
  151. package/scripts/package-smoke.mjs +0 -135
  152. package/scripts/release-notes.mjs +0 -66
  153. package/scripts/size-budget.mjs +0 -95
  154. package/starters/modular/src/shared/ui/x-button.component.ts +0 -49
@@ -1,244 +1,91 @@
1
1
  # Forms
2
2
 
3
- > `useForm()` is a thin signal-wrapper around native HTML5 form
4
- > validation. For 90% of cases you write a `<form>`, attach
5
- > `onInput / onBlur / onSubmit` and you are done.
3
+ `useForm()` adds reactive state to the browser's form and constraint-validation
4
+ model. HTML remains the source of truth for `required`, `type`, `min`, `max`,
5
+ `minlength`, `maxlength` and `pattern`.
6
6
 
7
7
  ```ts
8
- import { html, page, useForm } from "@madojs/mado";
9
-
10
- export default page({
11
- view: () => {
12
- const f = useForm({
13
- email: { required: true, type: "email" },
14
- age: { required: true, type: "number", min: 18 },
15
- });
16
-
17
- return html`
18
- <form @submit=${f.onSubmit(async (v) => { await api.save(v); f.reset(); })}>
19
- <input name="email" type="email"
20
- @input=${f.onInput} @blur=${f.onBlur} />
21
- ${() =>
22
- f.touched().email && f.errors().email
23
- ? html`<small class="err">${f.errors().email}</small>`
24
- : null}
25
-
26
- <input name="age" type="number" @input=${f.onInput} />
27
-
28
- <button ?disabled=${() => !f.isValid() || f.submitting()}>Save</button>
29
- </form>
30
- `;
31
- },
32
- });
33
- ```
34
-
35
- ## The signals it exposes
36
-
37
- | Reader | Value |
38
- | ---------------------- | ---------------------------------------------------------------- |
39
- | `f.values()` | object of current field values |
40
- | `f.errors()` | object of `field → error string` (validators + HTML5 constraints)|
41
- | `f.touched()` | object of `field → boolean` (true after first blur) |
42
- | `f.isValid()` | true when `errors()` is empty |
43
- | `f.submitting()` | true while the `onSubmit` async handler is running |
44
- | `f.dirty()` | true when any value differs from initial |
45
-
46
- Writers: `f.setValue(name, v)`, `f.setError(name, msg)`,
47
- `f.reset(values?)`, `f.touch(name)`.
48
-
49
- ## Schema
50
-
51
- The schema mirrors HTML5 constraints, with a few extras:
52
-
53
- ```ts
54
- useForm({
55
- email: { required: true, type: "email" },
56
- age: { type: "number", min: 18, max: 120 },
57
- name: { required: true, minLength: 2, maxLength: 80 },
58
- website: { type: "url" },
59
- tags: { custom: (v) => Array.isArray(v) && v.length > 0 ? null : "pick at least one" },
60
- });
61
- ```
62
-
63
- For cross-field or async rules use `validate`:
64
-
65
- ```ts
66
- useForm({
67
- password: { required: true, minLength: 8 },
68
- confirm: { required: true },
69
- }, {
70
- validate: (values) =>
71
- values.password !== values.confirm
72
- ? { confirm: "passwords do not match" }
73
- : null,
74
- });
75
- ```
76
-
77
- Return `null` (everything is valid) or a partial `field → error`
78
- object.
79
-
80
- ## Field arrays
81
-
82
- ```ts
83
- const f = useForm({ tags: { custom: (v) => v.length > 0 ? null : "required" } });
84
- const tags = f.fieldArray<string>("tags");
85
-
86
- tags.append("rust");
87
- tags.remove(0);
88
- tags.move(0, 2);
89
- tags.items(); // current array (signal)
90
- ```
91
-
92
- ## With `mutation()`
8
+ import { html, useForm } from "@madojs/mado";
93
9
 
94
- Forms compose with `mutation()` for typed write-paths:
95
-
96
- ```ts
97
- import { mutation, navigate } from "@madojs/mado";
98
-
99
- const save = mutation((u: User) => api.save(u), {
100
- invalidates: ["/api/users*"],
10
+ const form = useForm({
11
+ initial: { email: "", age: "" as number | "", newsletter: false },
12
+ validate: async (values, { signal }) => {
13
+ const available = await api.emailAvailable(values.email, { signal });
14
+ return available ? null : { email: "Email is already registered" };
15
+ },
101
16
  });
102
17
 
103
- // inside view()
104
- const f = useForm({ name: { required: true } });
105
- return html`
106
- <form @submit=${f.onSubmit(async (v) => {
107
- await save.run(v);
108
- navigate("/users");
18
+ html`
19
+ <form @submit=${form.onSubmit(async (values, event) => {
20
+ await api.save(values, new FormData(event.currentTarget as HTMLFormElement));
109
21
  })}>
110
- <input name="name" @input=${f.onInput} />
111
- <button ?disabled=${() => !f.isValid() || save.loading()}>Create</button>
112
- ${() => save.error() ? html`<p class="err">${save.error()!.message}</p>` : null}
22
+ <label>
23
+ Email
24
+ <input name="email" type="email" required
25
+ @input=${form.onInput} @blur=${form.onBlur} />
26
+ </label>
27
+ ${() => form.touched().email && form.errors().email
28
+ ? html`<small role="alert">${form.errors().email}</small>`
29
+ : null}
30
+
31
+ <input name="age" type="number" min="18" max="120" @input=${form.onInput} />
32
+ <label><input name="newsletter" type="checkbox" @input=${form.onInput} /> Subscribe</label>
33
+ <button type="submit" ?disabled=${() => form.submitting() || form.validating()}>
34
+ Save
35
+ </button>
113
36
  </form>
114
37
  `;
115
38
  ```
116
39
 
117
- `save.loading()` reflects the request; `f.submitting()` reflects the
118
- `onSubmit` handler runtime. Usually only one of them is interesting.
119
-
120
- ## Shadow DOM inputs — when you need them
40
+ ## API
121
41
 
122
- Most apps build forms from native `<input>` / `<select>` /
123
- `<textarea>` directly inside the `<form>`. `useForm()` reads
124
- `e.target.name` and `e.target.value` on the events, which works
125
- out-of-the-box for native controls.
42
+ `useForm<T>({ initial, validate? })` returns:
126
43
 
127
- You only need a custom Web Component when the input has its own
128
- shadow tree (a date picker, a tag editor, a rich text field, …). Two
129
- browser-level facts make that path slightly different:
44
+ | Member | Meaning |
45
+ | --- | --- |
46
+ | `values()` | current typed values |
47
+ | `errors()` | native and custom errors by field; `$form` is available for form-wide errors |
48
+ | `touched()` | fields that have blurred or participated in submit |
49
+ | `dirty()` | whether values differ from the current initial values |
50
+ | `isValid()` | whether the merged error map is empty |
51
+ | `submitting()` / `validating()` | current async activity |
52
+ | `setField(name, value)` | update one field programmatically |
53
+ | `validate(form?)` | run native and custom validation; returns `Promise<boolean>` |
54
+ | `reset(nextInitial?)` | abort validation, clear state and reset native controls |
55
+ | `onInput`, `onBlur`, `onSubmit(handler)` | DOM event handlers |
130
56
 
131
- 1. **Event retargeting** events that bubble out of an open shadow
132
- root have their `target` retargeted to the host. `useForm()` reads
133
- `e.target.name` and `e.target.value` — the host element must
134
- expose both.
135
- 2. **Form association** — a `<button type="submit">` inside a shadow
136
- root is NOT part of the form-owner algorithm. Clicking it does
137
- not submit the surrounding `<form>`.
57
+ Custom validation receives `{ signal, form }`. Starting another validation or
58
+ calling `reset()` aborts the previous run. Treat abort as normal cancellation.
138
59
 
139
- The two patterns below cover both. Reach for them **only** if a real
140
- custom input forces you to. For everything else, see "Light-DOM
141
- inputs" further down.
60
+ ## Native controls
142
61
 
143
- ### Custom input proxy `name` / `value`
62
+ - Checkbox groups produce an array when the initial field value is an array;
63
+ a single checkbox produces a boolean.
64
+ - Radio groups produce the checked value.
65
+ - `<select multiple>` produces a string array.
66
+ - Number/range inputs produce a number, or `""` when empty.
67
+ - File inputs produce `File | null`, or `File[]` when `multiple` is present.
68
+ - The submit callback receives the native `SubmitEvent`; construct `FormData`
69
+ from its form when the backend expects multipart or repeated fields.
144
70
 
145
- ```ts
146
- import { component, html } from "@madojs/mado";
147
-
148
- component("x-input", ({ host, attr }) => {
149
- const type = attr("type", "text");
150
-
151
- // After Shadow DOM retargets e.target from the inner <input> to
152
- // <x-input>, useForm reads e.target.name / e.target.value. These
153
- // getters bridge the gap.
154
- Object.defineProperty(host, "name", {
155
- get: () => host.getAttribute("name") ?? "",
156
- configurable: true,
157
- });
158
- Object.defineProperty(host, "value", {
159
- get: () => host.shadowRoot?.querySelector("input")?.value ?? "",
160
- set: (v: string) => {
161
- const input = host.shadowRoot?.querySelector("input");
162
- if (input) input.value = v;
163
- },
164
- configurable: true,
165
- });
166
-
167
- return () => html`<input type=${type} />`;
168
- });
169
- ```
71
+ Use a native `<button type="submit">`. A button inside another element's
72
+ Shadow DOM is not a submit control for the outer form and keyboard submission
73
+ will not discover it.
170
74
 
171
- The inner `input` event already bubbles through the shadow boundary
172
- (`composed: true` by default), so `<form @input=${f.onInput}>` keeps
173
- working.
174
-
175
- ### Custom submit button — `requestSubmit()`
75
+ ## Mutations
176
76
 
177
77
  ```ts
178
- import { component, html } from "@madojs/mado";
179
-
180
- component("x-button", ({ host, attr }) => {
181
- const disabled = attr("disabled");
182
-
183
- const onClick = () => {
184
- const t = host.getAttribute("type");
185
- if (t === "button" || t === "reset") return;
186
- if (host.hasAttribute("disabled")) return;
187
- host.closest("form")?.requestSubmit();
188
- };
189
-
190
- return () => html`
191
- <button ?disabled=${() => disabled() !== ""} @click=${onClick}>
192
- <slot></slot>
193
- </button>
194
- `;
78
+ const save = mutation((values: Profile) => api.save(values), {
79
+ invalidates: ["/api/profile"],
195
80
  });
196
- ```
197
-
198
- `host.closest("form")` works because the host itself lives in the
199
- light DOM. `requestSubmit()` triggers HTML5 validation and the
200
- `submit` event exactly like a native click.
201
-
202
- ### Light-DOM input — the simpler alternative
203
-
204
- If a custom element is just a styled wrapper around a native control
205
- and does not need style encapsulation, use `{ shadow: false }`. The
206
- native `<input>` is then part of the document; no retargeting, no
207
- proxy properties, no submit bridge.
81
+ const form = useForm({ initial: { displayName: "" } });
208
82
 
209
- ```ts
210
- component(
211
- "x-field",
212
- ({ attr }) => {
213
- const label = attr("label", "");
214
- return () => html`
215
- <label>
216
- <span>${label}</span>
217
- <slot></slot>
218
- </label>
219
- `;
220
- },
221
- { shadow: false },
222
- );
83
+ html`<form @submit=${form.onSubmit((values) => save.run(values))}>
84
+ <input name="displayName" required minlength="2" @input=${form.onInput} />
85
+ <button type="submit" ?disabled=${() => form.submitting()}>Save</button>
86
+ </form>`;
223
87
  ```
224
88
 
225
- This is the only situation where `shadow: false` is the recommended
226
- default and even then a `page()` is usually a better fit if the
227
- wrapper is layout-shaped rather than reusable.
228
-
229
- ## Summary
230
-
231
- | Concern | Native form | Custom component with Shadow DOM |
232
- | -------------------------- | ------------------------------------ | ----------------------------------------- |
233
- | `useForm` integration | works out of the box | proxy `name` / `value` on the host |
234
- | Submit button | `<button type="submit">` works | `host.closest("form")?.requestSubmit()` |
235
- | Reactive attributes | not needed | `ctx.attr(name)` returns a Signal |
236
- | Style encapsulation | global CSS | Shadow DOM CSS via `css\`\`` |
237
-
238
- ## Further reading
239
-
240
- - [10-pages-and-components.md](./10-pages-and-components.md) — when
241
- to reach for `component({ shadow: false })`.
242
- - [13-data.md](./13-data.md) — `mutation()` for typed writes.
243
- - [21-error-handling.md](./21-error-handling.md) — surfacing form
244
- errors from network failures.
89
+ Write failures belong to the mutation; field and cross-field validation belong
90
+ to the form. See the [0.12 0.13 migration guide](./33-migration-0.12-0.13.md)
91
+ for the removed schema and field-array APIs.
@@ -151,9 +151,9 @@ same shape the snapshot captured.
151
151
  resource(), custom element definition) fails the snapshot. A small
152
152
  allow-list covers `/favicon.ico`, `/favicon.svg`, `/robots.txt`
153
153
  and `data:` URLs.
154
- - **All network calls happen against an internal capture server** that
155
- serves the freshly built `out/` tree. There is no real network at
156
- capture time.
154
+ - **All network calls must target the internal capture origin** serving the
155
+ freshly built `out/` tree. Undeclared external HTTP(S) requests are blocked;
156
+ provide deterministic `static.initialData()` instead.
157
157
 
158
158
  ## Comparison
159
159
 
@@ -178,4 +178,4 @@ multi-locale builds live in [23-cookbook.md](./23-cookbook.md).
178
178
  - Declare `static: true | { paths, initialData }` on `page({ ... })`.
179
179
  - Run `mado release`.
180
180
  - Ship `out/` to any static host.
181
- - The live SPA takes over atomically on first paint.
181
+ - The live SPA takes over atomically on first paint.
@@ -190,8 +190,9 @@ Reusable leaf components keep their own styles in ``css`...` `` inside
190
190
  `component()` options. They should depend on tokens (`var(--color-text)`)
191
191
  rather than global classes.
192
192
 
193
- `vite.config.ts` opts into Vite's Lightning CSS transformer. Mado does not own
194
- prefixing, CSS lowering or minification.
193
+ Vite 8's standard CSS pipeline owns transforms and minification. Mado adds no
194
+ CSS transformer; applications can configure Vite directly when they have a
195
+ measured need.
195
196
 
196
197
  ## Growth
197
198
 
@@ -62,7 +62,7 @@ Key lines of the recipe `nginx.conf`:
62
62
  - `gzip_static on;` — serves the precompressed `.gz` files written by
63
63
  `mado release`. Zero CPU at request time.
64
64
  - `/assets/*` should be cached immutable; Vite filenames are content hashed.
65
- - `try_files $uri $uri/ /index.html;` — SPA fallback so deep links work
65
+ - `try_files $uri $uri/ /_mado/spa.html;` — SPA fallback so deep links work
66
66
  after a hard refresh.
67
67
 
68
68
  Enable HTTPS with Let's Encrypt / Certbot. Add HSTS once you have it.
@@ -76,7 +76,7 @@ mado release
76
76
  npx wrangler pages deploy out --project-name=myapp
77
77
  ```
78
78
 
79
- - The generated `_redirects` (`/* /index.html 200`) gives you SPA fallback.
79
+ - The generated `_redirects` (`/* /_mado/spa.html 200`) gives you SPA fallback.
80
80
  - The generated `_headers` (immutable cache for `/assets/*`, `no-cache` for
81
81
  HTML) is honored by CF Pages.
82
82
  - Baked routes are promoted to real route files (`out/<route>/index.html`),
@@ -116,16 +116,15 @@ aws s3 sync out/ s3://my-bucket/ \
116
116
  --cache-control "no-cache, must-revalidate" --include '*.html'
117
117
  ```
118
118
  Configure CloudFront's "Default root object" to `index.html` and add a custom
119
- error response: 403/404 → `/index.html` with status 200 (SPA fallback).
119
+ error response: 403/404 → `/_mado/spa.html` with status 200 (SPA fallback).
120
120
 
121
121
  **GitHub Pages**
122
122
  ```bash
123
123
  mado release
124
124
  # Push out/ into the gh-pages branch (or use actions/upload-pages-artifact)
125
125
  ```
126
- Pages handles `index.html` automatically. There is no native SPA fallback;
127
- add a `404.html` that loads the SPA, or use the
128
- [`spa-github-pages`](https://github.com/rafgraph/spa-github-pages) trick.
126
+ Pages handles route `index.html` files automatically. `mado release` also writes
127
+ `404.html` from the noindex SPA shell for client-only routes.
129
128
 
130
129
  ---
131
130
 
@@ -177,7 +176,7 @@ jobs:
177
176
 
178
177
  - **404 on hard refresh of a deep link.** Your host did not pick up SPA
179
178
  fallback. nginx: check `try_files`. CF/Netlify: `_redirects` is present?
180
- S3+CloudFront: configure the 404 → `/index.html` (200) error response.
179
+ S3+CloudFront: configure the 404 → `/_mado/spa.html` (200) error response.
181
180
  - **HTML is cached forever.** Either your host sent a default
182
181
  `Cache-Control: public, max-age=...` or you are sitting behind a CDN that
183
182
  ignores `no-cache`. Add an explicit rule mirroring the matrix above.
@@ -189,6 +188,20 @@ jobs:
189
188
  field. If you still see it, upgrade `@madojs/mado` and re-run
190
189
  `mado release`.
191
190
 
191
+ ## CSP and trusted content
192
+
193
+ Mado serialises JSON-LD and static seed JSON with `<`, U+2028 and U+2029
194
+ escaped, so data cannot close its script element. `unsafeHTML()` is different:
195
+ it is an explicit trust boundary and must receive only audited or sanitised
196
+ markup.
197
+
198
+ Start with a restrictive policy such as `default-src 'self'; script-src
199
+ 'self'; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self'`
200
+ and tighten styles with nonces/hashes when your deployment supports them.
201
+ Trusted Types can enforce the same application boundary around any code that
202
+ produces trusted HTML. The devtools overlay is development-only and should not
203
+ be imported in production entrypoints.
204
+
192
205
  ## Sub-path deployments and `base`
193
206
 
194
207
  A deployment under a sub-path (`https://your.site/docs/`, GitHub
@@ -226,7 +239,7 @@ html`<a data-link href=${routeUrl("/guides/intro")}>Intro</a>`;
226
239
 
227
240
  On the production host:
228
241
 
229
- - **nginx** — keep `try_files $uri $uri/ /index.html;` but mount the
242
+ - **nginx** — keep `try_files $uri $uri/ /_mado/spa.html;` but mount the
230
243
  app under the matching prefix (`location /docs/ { ... }`).
231
244
  - **Cloudflare Pages / Netlify** — `_redirects` is generated with the
232
245
  base prefix already baked in.
@@ -24,7 +24,7 @@ For a specific page, `page({ errorView })` wins over the global route boundary.
24
24
 
25
25
  ```ts
26
26
  export default page({
27
- load: async () => api.get("/reports"),
27
+ load: () => resource(() => "/reports", jsonFetcher<Report[]>()),
28
28
  errorView: (err) => html`<x-report-error .error=${err}></x-report-error>`,
29
29
  view: ({ data }) => html`<x-report .data=${data}></x-report>`,
30
30
  });
@@ -53,10 +53,10 @@ Validation errors belong in `useForm()`. Server errors from writes belong near
53
53
  the submit button.
54
54
 
55
55
  ```ts
56
- const form = useForm(
57
- { email: { required: true, type: "email" } },
58
- { validateAsync: (values) => api.validateUser(values) },
59
- );
56
+ const form = useForm({
57
+ initial: { email: "" },
58
+ validate: (values, { signal }) => api.validateUser(values, { signal }),
59
+ });
60
60
 
61
61
  const save = mutation((values) => api.post("/users", values), {
62
62
  invalidates: ["/api/users*"],
@@ -66,6 +66,7 @@ html`
66
66
  <form @submit=${form.onSubmit(async values => {
67
67
  await save.run(values);
68
68
  })}>
69
+ <input name="email" type="email" required @input=${form.onInput} />
69
70
  <button ?disabled=${() => form.validating() || form.submitting()}>
70
71
  Save
71
72
  </button>
@@ -97,4 +98,6 @@ component("x-online", (ctx) => {
97
98
 
98
99
  Log once at the boundary that owns recovery. Avoid logging the same failure in
99
100
  the API client, resource, page and component. The user should get one visible
100
- message and developers should get one useful console error.
101
+ message and developers should get one structured diagnostic. Runtime records
102
+ are visible in the devtools Timeline/Errors panel and as `mado:diagnostic`
103
+ events; CLI automation can select `--log-format=json`.
@@ -0,0 +1,45 @@
1
+ # Devtools and diagnostics
2
+
3
+ Development instrumentation is opt-in and compiled out of production builds.
4
+ Import the stable devtools subpath before application startup:
5
+
6
+ ```ts
7
+ import { devtools } from "@madojs/mado/devtools.js";
8
+
9
+ devtools.open();
10
+ ```
11
+
12
+ `Alt+Shift+M` toggles the Shadow DOM overlay. Its Overview, Reactivity,
13
+ Components, Router/Data and Timeline/Errors views are fed by a versioned
14
+ internal hook. The controller also exposes `close()`, `toggle()`, `clear()`,
15
+ `setLogLevel()` and `snapshot()`. Snapshots contain safe previews rather than
16
+ live application objects.
17
+
18
+ `localStorage.madoDebug = "1"` is accepted during 0.13 migration only and emits
19
+ a deprecation warning.
20
+
21
+ ## Runtime diagnostics
22
+
23
+ Every record has `level`, `scope`, `code`, `message`, optional `data`, and a
24
+ timestamp. Set browser verbosity with:
25
+
26
+ ```js
27
+ localStorage.setItem("mado:log-level", "debug");
28
+ ```
29
+
30
+ Allowed levels are `debug`, `info`, `warn`, `error` and `silent`. Records are
31
+ also dispatched as `mado:diagnostic` events and appear in devtools.
32
+
33
+ ## CLI diagnostics
34
+
35
+ ```bash
36
+ mado release --log-level=debug --log-format=pretty
37
+ mado release --log-format=plain
38
+ mado release --log-format=json
39
+ ```
40
+
41
+ The CLI honours `MADO_LOG_LEVEL`, `MADO_LOG_FORMAT` and `NO_COLOR`. Non-TTY
42
+ output automatically becomes plain. JSON mode writes one structured record per
43
+ line, suitable for CI ingestion.
44
+
45
+ The logger is internal infrastructure, not a public logging API for apps.
@@ -9,11 +9,11 @@ package root:
9
9
  import { component, html, resource, routes, signal } from "@madojs/mado";
10
10
  ```
11
11
 
12
- The public package subpaths are the side-effect devtools module and the Vite
13
- tooling integration:
12
+ The public package subpaths are the devtools controller and the Vite tooling
13
+ integration:
14
14
 
15
15
  ```ts
16
- import "@madojs/mado/devtools.js";
16
+ import { devtools } from "@madojs/mado/devtools.js";
17
17
  import { mado } from "@madojs/mado/vite";
18
18
  ```
19
19
 
@@ -26,7 +26,7 @@ These names are public and protected by SemVer once v1 ships:
26
26
 
27
27
  - Reactivity: `signal`, `computed`, `effect`, `untracked`, `batch`,
28
28
  `flushSync`.
29
- - Templates and directives: `html`, `render`, `each`, `list`, `unsafeHTML`,
29
+ - Templates and directives: `html`, `render`, `unmount`, `each`, `unsafeHTML`,
30
30
  `ref`, `classMap`, `styleMap`.
31
31
  - Components and CSS: `component`, `css`, `cssVars`.
32
32
  - Routing and pages: `routes`, `router`, `page`, `layout`,
@@ -37,9 +37,8 @@ These names are public and protected by SemVer once v1 ships:
37
37
  - Context: `createContext`, `provide`, `inject`.
38
38
  - Advanced lifecycle helpers: `createLifecycle`, `runInLifecycle`,
39
39
  `getCurrentLifecycle`.
40
- - Public TypeScript types exported from `@madojs/mado`, including
41
- `StaticPageConfig` (the `static: true | { ... }` option on `page()`)
42
- and `HeadMeta` (the head metadata shape returned by pages).
40
+ - Public TypeScript types exported from `@madojs/mado`, including `FormApi`,
41
+ `Resource`, `ComponentContext`, `StaticPageConfig` and `HeadMeta`.
43
42
 
44
43
  ## Internal or unstable
45
44
 
@@ -66,8 +65,8 @@ Application code should not.
66
65
 
67
66
  ## What can change
68
67
 
69
- Patch and minor releases may add new root exports, options, diagnostics, docs,
70
- or starter files. They may also change internals, emitted bundle shape, and
68
+ Patch and minor releases may add compatible options, diagnostics, docs or
69
+ starter files. New root exports require an explicit API review. Releases may change internals, emitted bundle shape, and
71
70
  implementation details as long as the stable API and documented behavior remain
72
71
  compatible.
73
72
 
@@ -17,7 +17,7 @@ After v1, Mado treats these as SemVer-protected:
17
17
 
18
18
  - Public exports from `@madojs/mado`.
19
19
  - Public TypeScript types exported from `@madojs/mado`.
20
- - The `@madojs/mado/devtools.js` side-effect subpath.
20
+ - The `@madojs/mado/devtools.js` controller subpath and `@madojs/mado/vite`.
21
21
  - Template binding syntax: child `${}`, `@event`, `.prop`, `?boolean`,
22
22
  attribute bindings, directives and `each()`.
23
23
  - Signal semantics documented in the reactivity ordering guide.
@@ -33,7 +33,7 @@ Breaking these requires a major version.
33
33
 
34
34
  Minor releases may add:
35
35
 
36
- - New root exports.
36
+ - New root exports after explicit API review and golden-file update.
37
37
  - New options on existing APIs.
38
38
  - New diagnostics and warnings.
39
39
  - New starters, examples, docs and CLI flags.
@@ -0,0 +1,56 @@
1
+ # Migrating from 0.12 to 0.13
2
+
3
+ 0.13 is the final planned pre-v1 breaking release. It narrows the root API and
4
+ makes lifecycle, forms, resources and router behaviour explicit.
5
+
6
+ ## Forms
7
+
8
+ <!-- docs-lint:allow-legacy-mention -->
9
+
10
+ The 0.12 schema-shaped call:
11
+
12
+ ```ts
13
+ useForm({ email: { required: true, type: "email" } });
14
+ ```
15
+
16
+ becomes:
17
+
18
+ ```ts
19
+ useForm({ initial: { email: "" } });
20
+ ```
21
+
22
+ Move `required`, `type`, `min`, `max` and `pattern` to the native controls.
23
+ Replace `setValue` with `setField`. The separate field-array helper was removed;
24
+ use ordinary array values and `setField`. Custom/async validation now belongs
25
+ to `validate(values, { signal, form })` in the options object.
26
+
27
+ <!-- /docs-lint:allow-legacy-mention -->
28
+
29
+ ## Removed root exports
30
+
31
+ `lazy`, `list`, `instantiate`, `isHtmlDirective`, `adopt`, `scopeStyles`,
32
+ `isPage` and `isLayoutGroup` are no longer root exports. Use dynamic route
33
+ imports, `each()`, `render()` and the documented high-level APIs instead.
34
+ Only the package root, `/vite` and `/devtools.js` are public subpaths.
35
+
36
+ ## Behaviour changes
37
+
38
+ - `render()` returns an idempotent disposer; `unmount(container)` explicitly
39
+ releases bindings, events, nested templates and child lifecycles.
40
+ - `page.load` is synchronous. Return a value or a `Resource`; returning a
41
+ Promise throws a diagnostic error. Put async work in `resource()`.
42
+ - `resource().refresh()` returns `Promise<T>`. `staleTime: 0` deduplicates only
43
+ an in-flight request and never reuses completed data.
44
+ - Cache identity includes the fetcher, and `invalidate()` reaches cached and
45
+ uncached live resources.
46
+ - Persisted signals separate `dispose()` from explicit storage deletion.
47
+ - Context now uses the Web Components `context-request` protocol and cleans up
48
+ subscriptions with the current lifecycle.
49
+ - Static capture blocks undeclared external requests and `mado build` writes
50
+ the `out/` artifact consumed by `mado static`.
51
+
52
+ ## Devtools and logging
53
+
54
+ Import `@madojs/mado/devtools.js` in development and use `Alt+Shift+M`.
55
+ `localStorage.madoDebug` is deprecated; use `mado:log-level` or the controller.
56
+ CLI automation should prefer `--log-format=json`.