@madojs/mado 0.8.0 → 0.10.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.
Files changed (83) hide show
  1. package/AGENTS.md +81 -4
  2. package/CHANGELOG.md +202 -1
  3. package/README.md +184 -242
  4. package/ROADMAP.md +174 -79
  5. package/TODO.md +8 -5
  6. package/dist/src/component.d.ts +2 -12
  7. package/dist/src/component.js +30 -29
  8. package/dist/src/component.js.map +1 -1
  9. package/dist/src/diagnostics.d.ts +0 -4
  10. package/dist/src/diagnostics.js +1 -0
  11. package/dist/src/diagnostics.js.map +1 -1
  12. package/dist/src/forms.js +17 -0
  13. package/dist/src/forms.js.map +1 -1
  14. package/dist/src/html/bindings.js +35 -3
  15. package/dist/src/html/bindings.js.map +1 -1
  16. package/dist/src/html/parser.js +60 -3
  17. package/dist/src/html/parser.js.map +1 -1
  18. package/dist/src/lifecycle.js +18 -0
  19. package/dist/src/lifecycle.js.map +1 -1
  20. package/dist/src/persisted.js +43 -9
  21. package/dist/src/persisted.js.map +1 -1
  22. package/dist/src/resource.d.ts +13 -6
  23. package/dist/src/resource.js +83 -16
  24. package/dist/src/resource.js.map +1 -1
  25. package/dist/src/router/manifest.d.ts +0 -3
  26. package/dist/src/router/manifest.js +23 -2
  27. package/dist/src/router/manifest.js.map +1 -1
  28. package/dist/src/router/navigation.js +56 -2
  29. package/dist/src/router/navigation.js.map +1 -1
  30. package/dist/src/router.d.ts +1 -1
  31. package/dist/src/router.js +1 -1
  32. package/dist/src/router.js.map +1 -1
  33. package/dist/src/signal.d.ts +0 -4
  34. package/dist/src/signal.js +56 -7
  35. package/dist/src/signal.js.map +1 -1
  36. package/docs/en/00-the-mado-way.md +23 -12
  37. package/docs/en/03-static-bake.md +1 -2
  38. package/docs/en/05-why-mado.md +78 -68
  39. package/docs/en/06-for-backenders.md +80 -55
  40. package/docs/en/07-llm-pitfalls.md +101 -0
  41. package/docs/en/08-llm-zero-history-test.md +5 -0
  42. package/docs/en/18-api-freeze-map.md +63 -0
  43. package/docs/en/19-reactivity-ordering.md +93 -0
  44. package/docs/en/20-v1-stability.md +83 -0
  45. package/docs/en/README.md +3 -0
  46. package/docs/fr/00-the-mado-way.md +25 -13
  47. package/docs/fr/03-static-bake.md +1 -2
  48. package/docs/fr/06-for-backenders.md +6 -0
  49. package/docs/fr/07-llm-pitfalls.md +2 -0
  50. package/docs/fr/08-llm-zero-history-test.md +5 -0
  51. package/docs/fr/18-api-freeze-map.md +63 -0
  52. package/docs/fr/19-reactivity-ordering.md +97 -0
  53. package/docs/fr/20-v1-stability.md +88 -0
  54. package/docs/fr/README.md +3 -0
  55. package/docs/ru/00-the-mado-way.md +24 -11
  56. package/docs/ru/03-static-bake.md +2 -3
  57. package/docs/ru/06-for-backenders.md +6 -0
  58. package/docs/ru/07-llm-pitfalls.md +2 -0
  59. package/docs/ru/08-llm-zero-history-test.md +5 -0
  60. package/docs/ru/18-api-freeze-map.md +62 -0
  61. package/docs/ru/19-reactivity-ordering.md +95 -0
  62. package/docs/ru/20-v1-stability.md +82 -0
  63. package/docs/ru/README.md +3 -0
  64. package/docs/uk/00-the-mado-way.md +3 -1
  65. package/docs/uk/06-for-backenders.md +5 -0
  66. package/docs/uk/07-llm-pitfalls.md +2 -0
  67. package/docs/uk/08-llm-zero-history-test.md +5 -0
  68. package/docs/uk/18-api-freeze-map.md +61 -0
  69. package/docs/uk/19-reactivity-ordering.md +95 -0
  70. package/docs/uk/20-v1-stability.md +83 -0
  71. package/docs/uk/README.md +3 -0
  72. package/llms.txt +63 -7
  73. package/package.json +10 -5
  74. package/scripts/bake.mjs +0 -1
  75. package/scripts/bundle.mjs +6 -6
  76. package/scripts/cli.mjs +17 -0
  77. package/scripts/llm-zero-history-smoke.mjs +93 -0
  78. package/scripts/new.mjs +1 -1
  79. package/scripts/package-smoke.mjs +74 -0
  80. package/scripts/size-budget.mjs +88 -0
  81. package/starters/admin/package.json +2 -2
  82. package/starters/crud/package.json +2 -2
  83. package/starters/minimal/package.json +2 -2
package/AGENTS.md CHANGED
@@ -9,9 +9,11 @@
9
9
 
10
10
  ## Project at a glance
11
11
 
12
- - **Mado** — SPA framework built on Web Components + signals + tagged-template `html`.
13
- - No build system (only `tsc`), no runtime dependencies.
14
- - Small TypeScript core in `src/`; bundled/minified full API is roughly 11 KB gzip.
12
+ - **Mado** — a calm browser-native SPA framework for internal tools, admin panels and business apps.
13
+ - Built on Web Components + signals + tagged-template `html`.
14
+ - Zero runtime dependencies. Generated apps use dev tooling (`typescript`,
15
+ `esbuild`, `linkedom`) for build/bundle/bake/release.
16
+ - Small TypeScript core in `src/`; production size budgets are enforced in CI.
15
17
 
16
18
  ## HARD RULES — violation = bug
17
19
 
@@ -119,7 +121,9 @@ component("x-badge", ({ attr }) => {
119
121
  ```
120
122
 
121
123
  `ctx.attr(name, defaultValue?)` returns a `Signal<string>` that auto-updates.
122
- The attribute is automatically added to `observedAttributes`.
124
+ Internally Mado uses a per-instance `MutationObserver` for attributes registered
125
+ during `setup()`. The observer auto-disconnects on component removal via
126
+ lifecycle cleanup.
123
127
 
124
128
  ### 5. Reactive value in template child position = function
125
129
 
@@ -180,6 +184,29 @@ html`<ul>
180
184
  </ul>`;
181
185
  ```
182
186
 
187
+ ### 7b. Parser hard errors
188
+
189
+ Mado fails loudly for template shapes that cannot be represented safely.
190
+
191
+ ```ts
192
+ // ❌ NO — slots inside RAW_TEXT elements are a parser error
193
+ html`<textarea>${draft}</textarea>`;
194
+ html`<title>${title}</title>`;
195
+
196
+ // ✅ YES — use properties or page/head APIs
197
+ html`<textarea .value=${draft}></textarea>`;
198
+ page({ title: ({ id }) => `User ${id}`, view: () => html`<main></main>` });
199
+
200
+ // ❌ NO — nested SVG-only templates lose namespace context
201
+ html`<svg>${html`<path d=${d}></path>`}</svg>`;
202
+
203
+ // ✅ YES — keep the SVG in one template or in its own component
204
+ html`<svg viewBox="0 0 10 10"><path d=${d}></path></svg>`;
205
+ ```
206
+
207
+ No dynamic `${...}` child slots inside `<script>`, `<style>`, `<textarea>`,
208
+ or `<title>`. Keep SVG internals in one `<svg>...</svg>` template.
209
+
183
210
  ### 8. Routing — `routes()` + `page()`
184
211
 
185
212
  ```ts
@@ -202,6 +229,31 @@ export default page<{ id: string }>({
202
229
  - Each page is a **separate file** in `pages/` with `export default page({...})`.
203
230
  - Import via `() => import("./pages/foo.js")` — this enables code-splitting via ESM.
204
231
  - Programmatic navigation: `import { navigate } from "@madojs/mado"; navigate("/users/42")`.
232
+ - Layouts are declared in the route manifest via `layout()`. Treat
233
+ `layout.view({ child })` as a stateless wrapper around `${child}` and shared
234
+ chrome. Put per-page state in pages/components/resources, not in layout view
235
+ locals that depend on route identity.
236
+ - **`onDispose`** — cleanup hook for page views. Use for `setInterval`, `WebSocket`, `EventSource`. `resource()` and `effect()` are auto-cleaned.
237
+ - **`untracked()`** — required when reading signals inside async functions called synchronously from `view()`. Without it, the signal subscribes the router's render effect → infinite loop.
238
+
239
+ ```ts
240
+ // page with polling and cleanup
241
+ import { page, html, signal, untracked } from "@madojs/mado";
242
+ export default page({
243
+ view: ({ onDispose }) => {
244
+ const data = signal(null);
245
+ const poll = async () => {
246
+ // untracked: don't subscribe the router's render effect
247
+ const res = await fetch("/api/status");
248
+ data.set(await res.json());
249
+ };
250
+ const id = setInterval(poll, 5000);
251
+ onDispose(() => clearInterval(id)); // ← cleaned up on navigation
252
+ poll(); // initial call
253
+ return html`<div>${() => JSON.stringify(data())}</div>`;
254
+ },
255
+ });
256
+ ```
205
257
 
206
258
  ### 9. Data fetching — `resource()` / `mutation()`
207
259
 
@@ -226,6 +278,14 @@ const save = mutation<User, User>(
226
278
  await save.run(newUser);
227
279
  ```
228
280
 
281
+ - A resource key is the cache identity. Same key means shared cache and deduped
282
+ in-flight request; use distinct keys for distinct data or auth scope.
283
+ - `mutation().run()` is concurrent by default. `loading()` stays true while any
284
+ run is in flight. Use `{ abortPrevious: true }` only for search-as-you-type or
285
+ "latest request wins" flows.
286
+ - Invalidation is best-effort after a successful mutation; invalidation errors
287
+ are logged but do not turn the mutation itself into a failure.
288
+
229
289
  ### 10. Forms — `useForm()`
230
290
 
231
291
  ```ts
@@ -325,10 +385,24 @@ Rules:
325
385
  - Tiny leaf components used everywhere → importing in `main.ts` is acceptable.
326
386
  - Do **not** bulk-import every component "just in case".
327
387
 
388
+ ### 14. Bake — meta shell, not SSR/SSG runtime
389
+
390
+ `mado bake` is a static meta-shell/prerender pass for SEO and first paint. It is
391
+ not SSR with hydration and not a Next-style SSG runtime.
392
+
393
+ - Baked HTML must be deterministic from `params`, `bake.data`, and plain values.
394
+ - Do not rely on browser-only effects, timers, relative `fetch`, or keyed
395
+ runtime directives such as `each()` during bake.
396
+ - Use `page({ head, bake })` for meta/JSON-LD/sitemap data; render dynamic,
397
+ personalized, real-time, or auth-dependent content in the client SPA.
398
+
328
399
  ## SOFT GUIDELINES — recommended, but not critical
329
400
 
330
401
  - **TypeScript strict.** Use `noUncheckedIndexedAccess`-aware code (with `!` or a type guard).
331
402
  - **Import using `.js`** (not `.ts`) — this is required by ES modules in the browser: `import { foo } from "./bar.js"`.
403
+ - **Public imports only.** App code imports from `@madojs/mado` and, when
404
+ needed, side-effect `@madojs/mado/devtools.js`. Other package subpaths and
405
+ `dist/src/*` are internal.
332
406
  - **One file = one responsibility.** Don't put 5 components in one file "because they're all small".
333
407
  - **Do not add runtime dependencies** (`npm install` in `dependencies`). This violates the framework's principle.
334
408
  - **JSDoc on public functions** is required. Comments explain "why", not "what".
@@ -376,6 +450,9 @@ When generating an app, prefer the blessed production shape from
376
450
  | How should an app be structured? | `docs/en/10-app-architecture.md` |
377
451
  | How should errors be handled? | `docs/en/15-error-handling.md` |
378
452
  | How should bake be used? | `docs/en/16-bake-cookbook.md` |
453
+ | What API is stable? | `docs/en/18-api-freeze-map.md` |
454
+ | What ordering is guaranteed? | `docs/en/19-reactivity-ordering.md` |
455
+ | What does v1 stability mean? | `docs/en/20-v1-stability.md` |
379
456
  | When something goes wrong | `docs/en/07-llm-pitfalls.md` |
380
457
 
381
458
  ## Before committing
package/CHANGELOG.md CHANGED
@@ -2,7 +2,208 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
- Nothing yet.
5
+ ## 0.10.0 - 2026-06-12
6
+
7
+ Surface-cleanup and API-lock release from the v1 tracker Phase B: legacy public
8
+ surface is removed, package exports are closed, docs/LLM guidance match the real
9
+ API, and CI now protects package, size, release and LLM-smoke contracts.
10
+
11
+ ### Fixed
12
+
13
+ - **Component attribute changes no longer clobber host properties (B1).**
14
+ Legacy `observedAttributes` reflection used to write `this[name] = value`
15
+ from `attributeChangedCallback`, overwriting `.prop=` bindings and custom
16
+ host state such as `.value`. Attribute changes now update only `ctx.attr()`
17
+ signals; `ctx.attr()` is the canonical reactive attribute API.
18
+
19
+ - **Removed `component(..., { observedAttributes })` (B2).** `ctx.attr()` is now
20
+ the single reactive-attribute API. It installs a per-instance observer for the
21
+ attributes used during setup, so component options no longer carry a second
22
+ attribute mechanism.
23
+
24
+ - **Package exports are explicit (B3).** The npm package no longer exports
25
+ `./*`. Public imports are limited to `@madojs/mado` and
26
+ `@madojs/mado/devtools.js`; internal files such as `lifecycle.js` are no
27
+ longer package subpaths.
28
+
29
+ - **Internal `_testHooks` are stripped from declarations (B4).** Runtime hooks
30
+ remain available to the repository's own tests, but emitted `.d.ts` files no
31
+ longer advertise them as public API; the router barrel also no longer
32
+ re-exports manifest test hooks.
33
+
34
+ - **README now states the explicit No list (B5).** The project boundary is
35
+ documented: no SSR hydration, template compiler, separate store library,
36
+ Suspense, router plugin system, built-in i18n/animation/virtual-scroll
37
+ primitives, or non-evergreen browser support. Browser baseline is pinned to
38
+ Baseline 2023.
39
+
40
+ - **`resource()` dedupes in-flight requests by key (B6).** Concurrent resources
41
+ with the same key now share one fetch. If the same in-flight key is used with
42
+ different fetcher functions, Mado warns once because the cache key is likely
43
+ too broad. README/docs now spell out resource key discipline.
44
+
45
+ - **`each()` warns on duplicate keys (B7).** The positional-suffix fallback is
46
+ preserved so every item still renders, but duplicate keys now produce a
47
+ `warnOnce` diagnostic because they are almost always a data bug.
48
+
49
+ - **API freeze map published (B8).** `docs/en/18-api-freeze-map.md` now defines
50
+ the stable root API, the public devtools subpath, and the internal modules
51
+ that are not protected by SemVer.
52
+
53
+ - **Reactivity ordering contract published (B9).** `docs/en/19-reactivity-ordering.md`
54
+ documents signal/effect/batch ordering, nested-template update reuse and
55
+ component teardown timing. A new invariant test pins nested-batch effect
56
+ scheduling.
57
+
58
+ - **v1 stability contract published (B10).** `docs/en/20-v1-stability.md`
59
+ defines what SemVer protects after v1 and what remains internal or
60
+ implementation-specific, including bundle byte output and internal module
61
+ layout.
62
+
63
+ - **Agent and LLM guidance synced to the real API (B11).** `AGENTS.md`,
64
+ `.clinerules`, `.cursorrules` and `llms.txt` now document C7 parser hard
65
+ errors, C6 mutation concurrency, stateless `layout.view` wrappers, public
66
+ package imports and `bake` as a static meta-shell rather than SSR/SSG runtime.
67
+
68
+ - **`mado init` writes required dev dependencies (B12).** Generated apps now
69
+ include `typescript`, `esbuild` and `linkedom` as dev dependencies, sourced
70
+ from the package's own tool versions. README/agent wording now says zero
71
+ **runtime** dependencies instead of implying no build tooling exists.
72
+
73
+ - **Size budgets are enforced in CI (B13).** `npm run size` bundles the full
74
+ public API and the showcase app, then fails on gzip regressions above the
75
+ current budgets: public API < 16 KiB, showcase app < 42 KiB.
76
+
77
+ - **Published tarball smoke test added (B14).** `npm run package:smoke` packs
78
+ the package, installs the tarball in a temp project, checks that public
79
+ imports work and `@madojs/mado/lifecycle.js` is blocked with
80
+ `ERR_PACKAGE_PATH_NOT_EXPORTED`, then scaffolds a clean app and runs
81
+ `mado release`.
82
+
83
+ - **Release output is deterministic (B15).** `bake-stamp` was removed from baked
84
+ HTML, and the release pipeline test now runs `mado release` twice on the same
85
+ input and compares the entire `out/` tree byte-for-byte.
86
+
87
+ - **LLM zero-history test is a CI smoke (B16).** `npm run llm:smoke` validates
88
+ that `llms.txt` retains the key Mado guidance, checks the committed
89
+ `examples/tickets` artifact for required APIs and forbidden React-shaped
90
+ patterns, then builds and runs the tickets smoke test.
91
+
92
+ - **Localized docs synced for Phase B (B17).** RU/FR/UK docs now include the
93
+ API freeze map, reactivity ordering and v1 stability pages, plus the Phase B
94
+ updates for resource key discipline, deterministic bake metadata and the
95
+ LLM-smoke CI proxy.
96
+
97
+ ## 0.9.0 - 2026-06-12
98
+
99
+ Correctness release from the v1 tracker Phase A: C1-C8 are closed with focused
100
+ regression tests.
101
+
102
+ ### Changed
103
+
104
+ - **`mutation().run()` is concurrent by default (C6).** Previously a `run()`
105
+ aborted any previous in-flight run, so two quick submits of different entities
106
+ through one mutation cancelled the first POST client-side — its `invalidates`
107
+ never fired even though the server had likely applied it. Mutations are now
108
+ concurrent: each `run()` has its own `AbortController`, `loading` is an
109
+ in-flight counter (true until the last run settles), and aborting the previous
110
+ run is opt-in via `mutation(fetcher, { abortPrevious: true })` for
111
+ search-as-you-type. `reset()` aborts all in-flight runs. **Behavioural change**
112
+ (done before the v1 API freeze). Regression test:
113
+ `test/mutation-concurrent.test.mjs`.
114
+
115
+ ### Fixed
116
+
117
+ - **Lifecycle/router defect pack is closed (C8).** `onDispose()` registered
118
+ after a lifecycle was already disposed now runs immediately instead of being
119
+ dropped. SPA link interception now respects `target="_blank"` and `download`.
120
+ Same-path `#hash` navigation scrolls to its anchor instead of being swallowed
121
+ by signal deduplication. Guard redirects now have a per-tick loop detector
122
+ that reports and halts mutually-redirecting routes. Regression test:
123
+ `test/lifecycle-router-pack.test.mjs`.
124
+
125
+ - **Parser fails loudly instead of silently dropping bindings (C7).** A `${}`
126
+ slot inside a RAW_TEXT element (`<textarea>`/`<title>`/`<style>`/`<script>`)
127
+ was silently ignored — an LLM writing `<textarea>${draft}</textarea>` got
128
+ neither an error nor a render. And a nested `html\`<path …>\`` for `<svg>` was
129
+ parsed in the HTML namespace, producing an invisible element. The parser now
130
+ throws a clear, fixable error in both cases (the RAW_TEXT message points at
131
+ `.value=`; the SVG message says to keep SVG content in one `<svg>…</svg>`
132
+ template). A self-contained `<svg>` still works. Regression test:
133
+ `test/html-rawtext-svg.test.mjs`.
134
+
135
+ - **Forms: stale async validation no longer lands on a shifted field-array row
136
+
137
+
138
+ (C5).** `useForm().array()` mutations (`remove`/`move`/`replace`/…) shift
139
+ indices, but an in-flight `validateAsync` for e.g. `items.2.title` still
140
+ matched its per-path generation guard and wrote its result onto a row that had
141
+ moved — a red error "jumping" onto a neighbouring valid row after a delete.
142
+ Array writes now bump the validation generation for every in-flight path under
143
+ the array prefix, so stale results are discarded. Row identity remains
144
+ positional. Regression test: `test/forms-array-stale-async.test.mjs`.
145
+
146
+ - **`computed({ equals })` no longer breaks `batch()` atomicity (C4).** An
147
+
148
+ observed `equals`-computed recomputed eagerly inside `set()`, so during
149
+ `batch(() => { x.set(2); y.set(2) })` a computed reading both `x` and `y` ran
150
+ on half-applied state `(new x, old y)` — observing an inconsistent snapshot,
151
+ potentially notifying with it, and running once per `set()`. An observed
152
+ `equals`-computed invalidated inside a batch now defers its recompute+compare
153
+ to a queue drained at the end of the outermost `batch()`, so it runs once on
154
+ fully-applied state. Behaviour outside a batch is unchanged. Regression test:
155
+ `test/signal-batch-equals.test.mjs`.
156
+
157
+ - **`update()` reuses nested templates instead of recreating them (C3).** A
158
+
159
+ renderer returning a nested `html\`\`` (e.g. a conditional form block) rebuilt
160
+ its entire subtree on every change of any signal it read, because `renderChild`
161
+ always did clear + re-instantiate for a single `TemplateResult` — unlike
162
+ `each()` and `render()`, which compare `_strings` and patch in place. Focus and
163
+ `<input>` values inside such blocks were lost and listeners re-attached.
164
+ `renderChild` now reuses the existing instance via `update()` when the new
165
+ value is a single `TemplateResult` with matching `_strings`, preserving DOM
166
+ identity; a structurally different template still rebuilds. Regression test:
167
+ `test/update-nested-reuse.test.mjs`.
168
+
169
+ - **`persisted()` cross-tab sync no longer ping-pongs, and `destroy()` is
170
+
171
+ complete (C2).** For object/array values, every cross-tab message produced a
172
+ new structured-clone identity, so the publisher effect re-broadcast each
173
+ received value forever (a single change generated 80+ messages in the
174
+ regression test). Cross-tab values are now echo-suppressed by their serialized
175
+ form, so an arriving value is never re-published. `destroy()` previously only
176
+ closed the channel and cleared storage while leaving the write/publish effects
177
+ alive — so the next `set()` re-created the key; it now disposes both effects,
178
+ clears the debounce timer, and marks the signal inert. `persisted()` also
179
+ registers `destroy()` with the active component/page lifecycle, so a persisted
180
+ signal created inside `setup()` no longer leaks. Regression test:
181
+ `test/persisted-crosstab.test.mjs`.
182
+
183
+ - **`each()` reorder no longer destroys custom-element state (C1).** Moving a
184
+ connected node (which keyed `each()` does via `insertBefore`) fires
185
+ `disconnectedCallback` → `connectedCallback` synchronously, and the old
186
+
187
+ `disconnectedCallback` tore the component down immediately — re-running
188
+ `setup()` and wiping every signal/resource/timer plus focus and `<input>`
189
+ values on a shuffle. Teardown is now deferred to a microtask and cancelled if
190
+ the element is re-inserted in the same tick, so a keyed move preserves state;
191
+ a genuine removal still disposes. `each()` also uses `Node.prototype.moveBefore`
192
+ (Chrome 133+) when available, which relocates connected nodes without firing
193
+ lifecycle callbacks at all. Regression test: `test/each-component-state.test.mjs`.
194
+
195
+ ### Docs / planning
196
+
197
+
198
+ - **Road to v1 re-sequenced around correctness.** Added
199
+ [`MADO_V1_TRACKER.md`](./MADO_V1_TRACKER.md), the active task-by-task tracker
200
+ derived from the `FABLE_REPORT.md` audit: phase A `v0.9` (correctness fixes
201
+ C1–C8, TDD: reproducing test → fix), phase B `v0.10` (surface cleanup, explicit
202
+ exports map, API freeze map), phase C `v1.0-rc` (live demo + external
203
+ dogfooding), phase D `v1.0` (freeze). `ROADMAP.md` now gates its
204
+ product-surface milestones behind a new "Milestone 0"; `TODO.md` points at the
205
+ tracker and drops items it now owns (exports policy, size reporting).
206
+
6
207
 
7
208
  ## 0.8.0
8
209