@madojs/mado 0.6.0 → 0.7.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 (44) hide show
  1. package/AGENTS.md +82 -30
  2. package/CHANGELOG.md +208 -1
  3. package/dist/src/component.d.ts +17 -4
  4. package/dist/src/component.js +26 -4
  5. package/dist/src/component.js.map +1 -1
  6. package/dist/src/resource.js +11 -0
  7. package/dist/src/resource.js.map +1 -1
  8. package/dist/src/router/manifest.js +29 -2
  9. package/dist/src/router/manifest.js.map +1 -1
  10. package/docs/en/07-llm-pitfalls.md +197 -60
  11. package/docs/en/08-llm-zero-history-test.md +1 -1
  12. package/docs/en/17-shadow-dom-forms.md +192 -0
  13. package/docs/en/README.md +20 -19
  14. package/docs/fr/07-llm-pitfalls.md +196 -60
  15. package/docs/fr/17-shadow-dom-forms.md +196 -0
  16. package/docs/fr/README.md +20 -19
  17. package/docs/ru/07-llm-pitfalls.md +198 -61
  18. package/docs/ru/08-llm-zero-history-test.md +39 -38
  19. package/docs/ru/09-shadow-vs-light-dom.md +97 -81
  20. package/docs/ru/17-shadow-dom-forms.md +193 -0
  21. package/docs/ru/README.md +20 -19
  22. package/docs/uk/07-llm-pitfalls.md +64 -3
  23. package/docs/uk/17-shadow-dom-forms.md +193 -0
  24. package/docs/uk/README.md +20 -19
  25. package/llms.txt +50 -1
  26. package/package.json +2 -2
  27. package/scripts/bake.mjs +76 -22
  28. package/scripts/bundle.mjs +24 -1
  29. package/scripts/cli.mjs +98 -45
  30. package/scripts/preview.mjs +104 -10
  31. package/server/serve.mjs +80 -7
  32. package/starters/admin/index.html +10 -3
  33. package/starters/admin/package.json +3 -1
  34. package/starters/admin/src/components/x-button.ts +40 -13
  35. package/starters/admin/src/components/x-input.ts +50 -19
  36. package/starters/admin/src/lib/api.ts +55 -4
  37. package/starters/admin/src/pages/admin/order-detail.ts +4 -2
  38. package/starters/admin/src/pages/home.ts +10 -1
  39. package/starters/crud/index.html +12 -4
  40. package/starters/crud/package.json +3 -1
  41. package/starters/crud/src/pages/home.ts +16 -0
  42. package/starters/minimal/index.html +12 -4
  43. package/starters/minimal/package.json +2 -0
  44. package/starters/minimal/src/pages/home.ts +17 -0
package/AGENTS.md CHANGED
@@ -91,6 +91,36 @@ component("x-timer", (ctx) => {
91
91
 
92
92
  **`resource()`, `effect()`, and subscriptions inside `setup()` hook into the lifecycle automatically** — no need to write onDispose for them.
93
93
 
94
+ ### 4b. Reactive attributes — `ctx.attr()`
95
+
96
+ ```ts
97
+ // ❌ NO (reading once, never reactive)
98
+ component("x-badge", ({ host }) => () => {
99
+ const variant = host.getAttribute("variant") ?? "default";
100
+ return html`<span class=${variant}>...</span>`;
101
+ });
102
+
103
+ // ❌ NO (MutationObserver boilerplate)
104
+ component("x-badge", ({ host, onDispose }) => {
105
+ const variant = signal(host.getAttribute("variant") ?? "default");
106
+ const obs = new MutationObserver(() =>
107
+ variant.set(host.getAttribute("variant") ?? "default"),
108
+ );
109
+ obs.observe(host, { attributes: true, attributeFilter: ["variant"] });
110
+ onDispose(() => obs.disconnect());
111
+ return () => html`<span class=${variant}>...</span>`;
112
+ });
113
+
114
+ // ✅ YES — one line, reactive, no cleanup needed
115
+ component("x-badge", ({ attr }) => {
116
+ const variant = attr("variant", "default");
117
+ return () => html`<span class=${() => `badge-${variant()}`}>...</span>`;
118
+ });
119
+ ```
120
+
121
+ `ctx.attr(name, defaultValue?)` returns a `Signal<string>` that auto-updates.
122
+ The attribute is automatically added to `observedAttributes`.
123
+
94
124
  ### 5. Reactive value in template child position = function
95
125
 
96
126
  The most common AI mistake:
@@ -99,13 +129,13 @@ The most common AI mistake:
99
129
  const count = signal(0);
100
130
 
101
131
  // ❌ NOT REACTIVE — count() is read once
102
- html`<div>${count() * 2}</div>`
132
+ html`<div>${count() * 2}</div>`;
103
133
 
104
134
  // ✅ REACTIVE — the function will be called when count changes
105
- html`<div>${() => count() * 2}</div>`
135
+ html`<div>${() => count() * 2}</div>`;
106
136
 
107
137
  // ✅ ALSO OK — the signal itself is a function, Mado recognizes it
108
- html`<div>${count}</div>`
138
+ html`<div>${count}</div>`;
109
139
  ```
110
140
 
111
141
  **Rule of thumb:** if there is a signal call (with parentheses) inside `${...}`, wrap it in `() => ...`.
@@ -114,17 +144,17 @@ html`<div>${count}</div>`
114
144
 
115
145
  ```ts
116
146
  // string/number → attribute
117
- html`<a href=${url}>...</a>`
147
+ html`<a href=${url}>...</a>`;
118
148
 
119
149
  // DOM property (objects, numbers without serialization, .value for input)
120
- html`<input .value=${user.name}>`
121
- html`<my-list .items=${arr}>`
150
+ html`<input .value=${user.name} />`;
151
+ html`<my-list .items=${arr}></my-list>`;
122
152
 
123
153
  // boolean attribute (toggle)
124
- html`<button ?disabled=${isLoading}>...</button>`
154
+ html`<button ?disabled=${isLoading}>...</button>`;
125
155
 
126
156
  // event
127
- html`<button @click=${fn}>...</button>`
157
+ html`<button @click=${fn}>...</button>`;
128
158
  ```
129
159
 
130
160
  Common mistake: `disabled=${loading()}` — this attempts to set a **string** attribute `disabled="true"` or `disabled="false"`, which does not work correctly. **Use `?disabled=`.**
@@ -135,10 +165,19 @@ Common mistake: `disabled=${loading()}` — this attempts to set a **string** at
135
165
  import { each } from "@madojs/mado";
136
166
 
137
167
  // ❌ Works, but no keyed reconciliation → loses focus on reorder
138
- html`<ul>${() => items().map(t => html`<li>${t.name}</li>`)}</ul>`
168
+ html`<ul>
169
+ ${() => items().map((t) => html`<li>${t.name}</li>`)}
170
+ </ul>`;
139
171
 
140
172
  // ✅ Correct: keyed, reuses DOM nodes
141
- html`<ul>${() => each(items(), t => t.id, t => html`<li>${t.name}</li>`)}</ul>`
173
+ html`<ul>
174
+ ${() =>
175
+ each(
176
+ items(),
177
+ (t) => t.id,
178
+ (t) => html`<li>${t.name}</li>`,
179
+ )}
180
+ </ul>`;
142
181
  ```
143
182
 
144
183
  ### 8. Routing — `routes()` + `page()`
@@ -198,14 +237,20 @@ import { useForm } from "@madojs/mado";
198
237
 
199
238
  const f = useForm({
200
239
  email: { required: true, type: "email" },
201
- age: { required: true, type: "number", min: 18 },
240
+ age: { required: true, type: "number", min: 18 },
202
241
  });
203
242
 
204
243
  html`
205
- <form @submit=${f.onSubmit(async v => { await api.save(v); })}>
206
- <input name="email" @input=${f.onInput} @blur=${f.onBlur}>
207
- ${() => f.touched().email && f.errors().email
208
- ? html`<small>${f.errors().email}</small>` : null}
244
+ <form
245
+ @submit=${f.onSubmit(async (v) => {
246
+ await api.save(v);
247
+ })}
248
+ >
249
+ <input name="email" @input=${f.onInput} @blur=${f.onBlur} />
250
+ ${() =>
251
+ f.touched().email && f.errors().email
252
+ ? html`<small>${f.errors().email}</small>`
253
+ : null}
209
254
  <button ?disabled=${() => !f.isValid() || f.submitting()}>Save</button>
210
255
  </form>
211
256
  `;
@@ -220,16 +265,23 @@ import { component, css, html } from "@madojs/mado";
220
265
 
221
266
  component("x-card", () => () => html`<div><slot></slot></div>`, {
222
267
  styles: css`
223
- :host { display: block; padding: 1rem; }
224
- div { background: var(--bg); }
225
- ::slotted(h2) { margin: 0; }
268
+ :host {
269
+ display: block;
270
+ padding: 1rem;
271
+ }
272
+ div {
273
+ background: var(--bg);
274
+ }
275
+ ::slotted(h2) {
276
+ margin: 0;
277
+ }
226
278
  `,
227
279
  });
228
280
 
229
281
  // Light DOM (without Shadow), global styles:
230
282
  component("x-shell", () => () => html`...`, {
231
- shadow: false, // disables Shadow DOM
232
- styles: css`x-shell header { ... }`, // selectors are written as usual
283
+ shadow: false, // disables Shadow DOM
284
+ styles: css`x-shell header { ... }`, // selectors are written as usual
233
285
  });
234
286
  ```
235
287
 
@@ -314,17 +366,17 @@ When generating an app, prefer the blessed production shape from
314
366
 
315
367
  ## Where to find specific answers
316
368
 
317
- | Question | File |
318
- |---|---|
319
- | How does reactivity work? | `src/signal.ts` (283 lines) |
320
- | How are templates parsed? | `src/html.ts` (1013 lines) |
321
- | How does the router work? | `src/router.ts` (~530 lines) |
322
- | How does resource + cache work? | `src/resource.ts` (297 lines) |
323
- | How do forms work? | `src/forms.ts` (212 lines) |
369
+ | Question | File |
370
+ | -------------------------------- | -------------------------------- |
371
+ | How does reactivity work? | `src/signal.ts` (283 lines) |
372
+ | How are templates parsed? | `src/html.ts` (1013 lines) |
373
+ | How does the router work? | `src/router.ts` (~530 lines) |
374
+ | How does resource + cache work? | `src/resource.ts` (297 lines) |
375
+ | How do forms work? | `src/forms.ts` (212 lines) |
324
376
  | How should an app be structured? | `docs/en/10-app-architecture.md` |
325
- | How should errors be handled? | `docs/en/15-error-handling.md` |
326
- | How should bake be used? | `docs/en/16-bake-cookbook.md` |
327
- | When something goes wrong | `docs/en/07-llm-pitfalls.md` |
377
+ | How should errors be handled? | `docs/en/15-error-handling.md` |
378
+ | How should bake be used? | `docs/en/16-bake-cookbook.md` |
379
+ | When something goes wrong | `docs/en/07-llm-pitfalls.md` |
328
380
 
329
381
  ## Before committing
330
382
 
package/CHANGELOG.md CHANGED
@@ -4,6 +4,204 @@
4
4
 
5
5
  Nothing yet.
6
6
 
7
+ ## 0.7.0
8
+
9
+ Reactive component props, Shadow DOM + Forms fixes, deterministic releases,
10
+ and `mado serve` unification. Motivated by stress-test findings in a real-world
11
+ admin panel (see `MADO_TEST_REPORT.md`).
12
+
13
+ ### Breaking Changes
14
+
15
+ - **`mado serve` in app-mode** no longer uses the legacy `serveStaticProject()`
16
+ fallback. It now always goes through `server/serve.mjs`, which means
17
+ `--host`, `--port`, `mado.config.json` dev.proxy, and HMR all work for
18
+ generated apps. If you relied on the old no-HMR behaviour, pass
19
+ `NO_HMR=1 mado serve`.
20
+
21
+ ### Added — Framework
22
+
23
+ - **`ctx.attr(name, defaultValue?)`** — reactive attribute accessor for
24
+ components. Returns a `Signal<string>` that auto-updates when the attribute
25
+ changes on the host element via `attributeChangedCallback`. No more
26
+ `MutationObserver` boilerplate in every component.
27
+
28
+ ```ts
29
+ component("x-badge", ({ attr }) => {
30
+ const variant = attr("variant", "default");
31
+ return () =>
32
+ html`<span class=${() => `badge-${variant()}`}><slot></slot></span>`;
33
+ });
34
+ ```
35
+
36
+ Attributes used with `ctx.attr()` are automatically added to
37
+ `observedAttributes`.
38
+
39
+ ### Added — Starters
40
+
41
+ - **`apiFetcher<T>()`** in `starters/admin/src/lib/api.ts` — a fetcher for
42
+ `resource()` that attaches the Bearer token from memory. Use for protected
43
+ endpoints instead of the plain `jsonFetcher()`.
44
+ - **`x-button`**: now bridges Shadow DOM → Light DOM form submit via
45
+ `form.requestSubmit()`. Buttons inside Shadow DOM cannot natively trigger
46
+ `<form>` submit in Light DOM — this is now handled automatically.
47
+ - **`x-button`**: uses `ctx.attr("disabled")` for reactive disabled state.
48
+ External `?disabled=${() => !form.isValid()}` now correctly enables/disables
49
+ the inner button.
50
+ - **`x-input`**: proxies `.name` and `.value` DOM properties on the host
51
+ element so that `useForm().onInput` works after Shadow DOM event retargeting.
52
+
53
+ ### Added — CLI / Build
54
+
55
+ - **`mado release --no-clean`**: release now cleans the entire `out/` directory
56
+ before building (deterministic artifacts). Pass `--no-clean` to opt out.
57
+ Previously stale assets, removed bake routes, and deleted public files could
58
+ linger in the deploy artifact.
59
+ - **`scripts/bake.mjs`**: `<title>` now falls back to `page.title` if
60
+ `head().title` is not explicitly set. Previously baked HTML kept the template
61
+ `<title>` from `index.html` — a critical SEO gap.
62
+
63
+ ### Added — Documentation
64
+
65
+ - **`docs/en/17-shadow-dom-forms.md`** — full recipe for using `useForm()` with
66
+ Shadow DOM components (proxy properties, form submit bridge, ctx.attr()).
67
+ - **`llms.txt`**: added `ctx.attr()` section, `apiFetcher` recipe, and Shadow
68
+ DOM + Forms guidance.
69
+
70
+ ### Fixed
71
+
72
+ - **`x-button` in starters**: the disabled state was read once from
73
+ `host.hasAttribute("disabled")` in the render function — never updating when
74
+ the attribute changed externally. Every form using `?disabled` on `x-button`
75
+ was broken from the start.
76
+ - **`x-input` in starters**: `useForm().onInput` received `undefined` for
77
+ `name` and `value` because Shadow DOM retargets `e.target` from the inner
78
+ `<input>` to `<x-input>`, which had no DOM properties.
79
+ - **`jsonFetcher()`**: the admin starter relied on `jsonFetcher()` for protected
80
+ endpoints but it sends no Authorization header. Documented the pattern and
81
+ added `apiFetcher()`.
82
+ - **`mado serve`**: app-mode did not respect `--host`, `--port`, or config
83
+ settings. All flag pass-through now goes through `server/serve.mjs`.
84
+ - **`mado release`**: stale files from deleted bake routes or removed public
85
+ assets could remain in `out/`. Now cleans `out/` fully before building.
86
+ - **`mado bake`**: `<title>` was not set in baked HTML if only `page.title`
87
+ was defined (without `head().title`).
88
+
89
+ ## 0.6.1
90
+
91
+ Starter & release-pipeline hardening pass. No public API breaks.
92
+ Identified from a lab pressure-test on `/admin/lab` plus a deep audit of the
93
+ starter / bundle / bake / dev-server contour. All fixes verified by
94
+ regression tests added in this release.
95
+
96
+ ### Fixed
97
+
98
+ - **Starters**: every `index.html` in `starters/{admin,crud,minimal}/` now
99
+ uses root-absolute paths in the importmap and entry `<script>` tag
100
+ (`/node_modules/@madojs/mado/...`, `/dist/main.js`). Relative paths
101
+ (`./node_modules/...`, `./dist/main.js`) broke hard-refresh of any nested
102
+ route (`/admin/orders/42` → browser fetched
103
+ `/admin/orders/dist/main.js` → 404 → blank page). Inline comments in each
104
+ file explain the trap so it does not get reverted.
105
+ - **Starters/admin**: `pages/admin/order-detail.ts` now uses `each(items,
106
+ key, render)` instead of `o.items.map(...)`, matching `llms.txt` rule #3
107
+ and the framework's own pitfalls documentation.
108
+ - **`scripts/bundle.mjs`**: cleans stale hashed assets before every build.
109
+ Previously each `mado bundle` / `mado release` left old `main-<hash>.js`
110
+ and `chunk-<hash>.js` in `out/assets/`; the rewriter then emitted
111
+ `<link rel="modulepreload">` for every leftover `.js` it found, so
112
+ production HTML shipped dead-code preloads without SRI. In app-mode the
113
+ whole assets directory is wiped; in repo-mode only recognisable hashed
114
+ files are removed so unrelated repo artifacts stay put.
115
+ - **`src/router/manifest.ts`**: opens a fresh component lifecycle scope
116
+ around every `page.view()` / layout `view()` call and disposes the
117
+ previous one on navigation (and on `router.dispose()`). `resource()`,
118
+ `effect()` and `persisted()` created inside `page.view()` now register
119
+ cleanup with that scope automatically — no more
120
+ `[mado:resource-outside-lifecycle]` warnings on the framework's own
121
+ canonical examples, and no more invalidator-subscription leaks across
122
+ route changes.
123
+ - **`src/resource.ts`**: guards against stale responses overwriting fresh
124
+ data on rapid key changes. The previous `AbortController` defence worked
125
+ only if the user-supplied fetcher honored `AbortSignal` — for fetchers
126
+ that ignore cancellation, a slow stale resolution for an old key could
127
+ win over a fast fresh one. Both then/catch branches now also check
128
+ `if (key !== lastKey) return`.
129
+ - **`server/serve.mjs`**: falls back to `./public/*` when a file is not
130
+ found at the project root, mirroring what `mado release` does to `out/`.
131
+ `favicon.svg`, `robots.txt`, `og-image.png` etc. no longer 404 in dev.
132
+ - **`server/serve.mjs`**: prints an actionable hint on `EPERM`/`EACCES`
133
+ pointing at `mado dev --host 127.0.0.1` (the default host changed from
134
+ the implicit `0.0.0.0` to `localhost`, which is friendlier in sandboxed
135
+ environments).
136
+ - **`scripts/preview.mjs`**: serves prerendered HTML from `<out>/baked/`
137
+ with priority over the SPA shell. Previously `mado preview` only looked
138
+ at `out/` and never saw bake's output, so prerendered routes returned
139
+ the empty SPA shell — looking like a "blank page" bug even when bake
140
+ had succeeded.
141
+
142
+ ### Added
143
+
144
+ - **`mado dev` / `mado serve` flag pass-through**: `cli.mjs` now splits
145
+ positional arguments from flags via `splitDevArgs()`, so calls like
146
+ `mado dev --host 127.0.0.1`, `mado dev showcase --port 6000` and
147
+ `mado dev -- --host 0.0.0.0` all work. Previously the CLI mistook the
148
+ flag for an example name and exited with `unknown example`.
149
+ - **`server/serve.mjs`**: tiny argv parser supporting `--host`, `--port`
150
+ and `--host=value` forms; HOST and PORT also fall back to environment
151
+ variables and `mado.config.json` (`dev.host`, `dev.port`).
152
+ - **`scripts/preview.mjs`**: same `--host` / `--port` flags as the dev
153
+ server, plus a startup banner showing `url:` / `out:` / `baked:` so it
154
+ is obvious which directories preview is serving from.
155
+ - **`scripts/bake.mjs`**: fails loudly when the manifest exists but no
156
+ page declares `bake: { paths, data }`. The previous behaviour produced
157
+ `0 pages + sitemap.xml` silently with exit code 0, making `mado
158
+ release` look successful while shipping only the SPA shell with no
159
+ SEO-friendly HTML. The new warning prints the skipped routes, a
160
+ worked example bake snippet, and exits non-zero. Override with
161
+ `MADO_BAKE_ALLOW_EMPTY=1` for intentional SPA-only deploys.
162
+ - **`scripts/bake.mjs`**: clearer "missing dev dep" errors — when
163
+ `linkedom` or `esbuild` is missing the message now tells the user
164
+ exactly which packages to `npm i -D`.
165
+ - **Starter landing pages**: `home.ts` in all three starters now declares
166
+ `bake: { paths: () => [{}], data: () => ({}) }` and a `head()` so
167
+ `mado release` actually prerenders the landing page out of the box.
168
+ - **Starter `devDependencies`**: `linkedom` and `esbuild` added to
169
+ `starters/{admin,crud,minimal}/package.json` so `mado release` works
170
+ immediately after `mado init <app>` + `npm install`, without manual
171
+ follow-up installs.
172
+ - **Regression tests** (`test/`):
173
+ - `starter-html-paths.test.mjs` — asserts every starter `index.html`
174
+ uses root-absolute paths in both the importmap and the entry script.
175
+ - `bundle-cleanup.test.mjs` — end-to-end: runs `mado bundle` twice on
176
+ a synthesized temp project (mutating source between runs) and
177
+ asserts there is exactly one `main-<hash>.js` in `out/assets/`
178
+ afterwards.
179
+ - `resource.test.mjs` (2 new cases) — stale-response races: a fetcher
180
+ that ignores `AbortSignal` with key=1 slower than key=2, plus a
181
+ rapid 3-way key thrash where the final key is the slowest fetch.
182
+ Both assert `data()` reflects the latest key, not the fastest
183
+ response.
184
+
185
+ ### Changed
186
+
187
+ - **`server/serve.mjs`** default host is now `localhost` (was implicitly
188
+ `0.0.0.0`). LAN exposure is opt-in via `mado dev --host 0.0.0.0` or
189
+ `HOST=0.0.0.0`. The startup banner shows both the bound host and a
190
+ click-friendly URL (`localhost` substituted when bound to `0.0.0.0`).
191
+
192
+ ### Notes
193
+
194
+ - No public API changes; no migrations required. Apps that previously
195
+ worked on a fresh-out-of-the-box `mado init` did so only because
196
+ someone manually fixed the starter's relative paths and dev deps —
197
+ this release closes those gaps so the documented "happy path" actually
198
+ is happy.
199
+ - If you intentionally deploy SPA-only (no prerendered HTML), drop
200
+ `mado bake` from your release pipeline or set
201
+ `MADO_BAKE_ALLOW_EMPTY=1`; otherwise bake will now fail your
202
+ CI with a clear pointer to the missing config.
203
+ - Test count: 137 pass, 0 fail, 3 skipped (Playwright e2e — unchanged).
204
+
7
205
  ## 0.6.0
8
206
 
9
207
  Product-surface release: app-mode defaults, blessed admin starter, release
@@ -12,12 +210,13 @@ pipeline, core hardening and v1 recipe docs.
12
210
  Phase 1 — Repo-vs-app split:
13
211
 
14
212
  ### Added
213
+
15
214
  - `MADO_V1_PLAN.md` — executable tracker for the v1 push.
16
215
  - `scripts/_config.mjs` — single configuration loader (defaults < `mado.config.json`
17
216
  < CLI flags). Exports `loadConfig`, `detectContext`, `parseFlags`,
18
217
  `resolveProjectPath`. [v1 F1.1]
19
218
  - `mado release` command: one-shot `typecheck + build + bundle + bake + copy
20
- public/ → out/` pipeline so apps have exactly one command to ship. [v1 F1.3]
219
+ public/ → out/` pipeline so apps have exactly one command to ship. [v1 F1.3]
21
220
  - `mado.config.json` shipped in the `minimal` and `crud` starters with the
22
221
  default app-mode layout (`src/routes.ts`, `index.html`, `out/`). [v1 F1.4]
23
222
  - Tests: `test/config-loader.test.mjs`, `test/bake-cli.test.mjs` (11 + 3
@@ -25,6 +224,7 @@ Phase 1 — Repo-vs-app split:
25
224
  flags, and the no-more-silent-`[object Object]` contract). [v1 F1.6]
26
225
 
27
226
  ### Changed
227
+
28
228
  - `scripts/bake.mjs` now reads configuration from `mado.config.json` and
29
229
  accepts `--entry`, `--template`, `--out`, `--base-url` flags. In app-mode
30
230
  defaults are `src/routes.ts` + `index.html` + `out/baked/`; the
@@ -46,6 +246,7 @@ Phase 1 — Repo-vs-app split:
46
246
  Phase 2 — One blessed way:
47
247
 
48
248
  ### Added
249
+
49
250
  - `layout()` factory in `src/page.ts` (alias of `nested()`) plus `Guard` and
50
251
  `GuardResult` types. Exported from the public API. [v1 F2.1 / F2.3]
51
252
  - Route guards: nested groups and individual pages accept `guard: Guard | Guard[]`.
@@ -70,6 +271,7 @@ Phase 2 — One blessed way:
70
271
  Phase 3 — Bake first-class + Release pipeline:
71
272
 
72
273
  ### Added
274
+
73
275
  - `mado release` writes `_redirects` (`/* /index.html 200`) and `_headers`
74
276
  (immutable for `/assets/*`, no-cache for HTML) into `out/` when they do not
75
277
  exist, so Cloudflare Pages / Netlify deploys "just work". [v1 F3.7]
@@ -83,6 +285,7 @@ Phase 3 — Bake first-class + Release pipeline:
83
285
  and copied `public/` assets are all present. [v1 F3.10]
84
286
 
85
287
  ### Changed
288
+
86
289
  - `scripts/preview.mjs` now reads `mado.config.json` (`build.out`, `dev.port`),
87
290
  refuses to auto-build by default in app-mode, and asks the user to run
88
291
  `mado release` first. Legacy auto-build is opt-in via `PREVIEW_AUTOBUILD=1`
@@ -97,6 +300,7 @@ Phase 3 — Bake first-class + Release pipeline:
97
300
  dependencies. The startup banner prints the active proxy table. [v1 F3.6]
98
301
 
99
302
  ### Deferred to v0.7
303
+
100
304
  - `mado dev` does not yet serve baked routes inline. Workaround: run
101
305
  `mado release && mado preview`. [v1 F3.2]
102
306
  - `mado check` (bake-safety scan over `bake:` routes) is not exposed yet.
@@ -106,6 +310,7 @@ Phase 3 — Bake first-class + Release pipeline:
106
310
  Phase 4 — Core hardening:
107
311
 
108
312
  ### Added
313
+
109
314
  - `computed(fn, { equals })` option to suppress subscriber reruns when an
110
315
  observed computed recomputes to an equal value. [v1 F4.3]
111
316
  - HTML directives: `unsafeHTML()`, `ref()`, `classMap()` and `styleMap()` are
@@ -127,6 +332,7 @@ Phase 4 — Core hardening:
127
332
  its regression tests. [v1 F4.9]
128
333
 
129
334
  ### Changed
335
+
130
336
  - `computed()` now releases dependency subscriptions after unobserved reads and
131
337
  after the last subscriber is disposed, avoiding long-lived stale subscriptions
132
338
  in the signal graph. [v1 F4.1]
@@ -145,6 +351,7 @@ Phase 4 — Core hardening:
145
351
  Phase 5 — Documentation:
146
352
 
147
353
  ### Added
354
+
148
355
  - `docs/en/10-app-architecture.md`, `14-testing.md`, `15-error-handling.md`
149
356
  and `16-bake-cookbook.md` complete the v1 English recipe set. [v1 F5.1-F5.4]
150
357
  - `AGENTS.md` now includes an "App architecture for LLM" section and `llms.txt`
@@ -15,13 +15,25 @@
15
15
  * Shadow DOM (open) is used by default. It can be disabled, and
16
16
  * styles will be scoped via @scope (or a tag-prefix fallback).
17
17
  */
18
- import { type Disposer } from "./signal.js";
18
+ import { type Signal, type Disposer } from "./signal.js";
19
19
  import { html, type TemplateResult } from "./html.js";
20
20
  import { type CSSResult } from "./css.js";
21
21
  export interface ComponentContext {
22
22
  host: HTMLElement;
23
23
  /** Run cleanup when the component is removed. */
24
24
  onDispose(fn: Disposer): void;
25
+ /**
26
+ * Reactive attribute accessor. Returns a Signal<string> that updates
27
+ * automatically whenever the attribute changes on the host element.
28
+ *
29
+ * const variant = ctx.attr("variant", "primary");
30
+ * return () => html`<div class=${variant()}>…</div>`;
31
+ *
32
+ * No MutationObserver boilerplate needed — the signal updates via
33
+ * attributeChangedCallback. The attribute name is automatically added to
34
+ * observedAttributes if not already listed.
35
+ */
36
+ attr(name: string, defaultValue?: string): Signal<string>;
25
37
  }
26
38
  export type SetupFn = (ctx: ComponentContext) => () => TemplateResult;
27
39
  export type StyleInput = string | CSSResult | Array<string | CSSResult>;
@@ -38,9 +50,10 @@ export interface ComponentOptions {
38
50
  /**
39
51
  * List of observed attributes.
40
52
  *
41
- * v0.3: this is plain reflection into host[attr], without a reactive props API.
42
- * If you need to re-render the component when an attribute changes, create a signal()
43
- * inside setup() and update it manually from your own wrapper/event.
53
+ * Attributes listed here are reflected to host[attr] via
54
+ * attributeChangedCallback and also power ctx.attr() reactive signals.
55
+ * You only need to list them here if you use the legacy property reflection
56
+ * pattern. ctx.attr() automatically registers any attribute it tracks.
44
57
  */
45
58
  observedAttributes?: readonly string[];
46
59
  }
@@ -15,10 +15,10 @@
15
15
  * Shadow DOM (open) is used by default. It can be disabled, and
16
16
  * styles will be scoped via @scope (or a tag-prefix fallback).
17
17
  */
18
- import { effect } from "./signal.js";
18
+ import { signal, effect } from "./signal.js";
19
19
  import { html, render } from "./html.js";
20
20
  import { adopt, scopeStyles } from "./css.js";
21
- import { createLifecycle, runInLifecycle } from "./lifecycle.js";
21
+ import { createLifecycle, runInLifecycle, } from "./lifecycle.js";
22
22
  import { warnOnce } from "./diagnostics.js";
23
23
  export function component(tagName, setup, options = {}) {
24
24
  if (!tagName.includes("-")) {
@@ -36,18 +36,22 @@ export function component(tagName, setup, options = {}) {
36
36
  }
37
37
  const useShadow = options.shadow !== false;
38
38
  const observed = options.observedAttributes ?? [];
39
+ // Collect attribute names that ctx.attr() will track. These are merged with
40
+ // options.observedAttributes to form the final static observedAttributes.
41
+ const attrSignalNames = new Set(observed);
39
42
  // Normalize styles to an array of CSSStyleSheet once.
40
43
  // Sheets are shared across all instances — memory is not duplicated.
41
44
  const stylesheets = normalizeStyles(options.styles, tagName, useShadow);
42
45
  class MadoElement extends HTMLElement {
43
46
  static get observedAttributes() {
44
- return [...observed];
47
+ return [...attrSignalNames];
45
48
  }
46
49
  #root;
47
50
  #renderer = null;
48
51
  #effectDispose = null;
49
52
  #lifecycle = null;
50
53
  #connected = false;
54
+ #attrSignals = new Map();
51
55
  constructor() {
52
56
  super();
53
57
  this.#root = useShadow ? this.attachShadow({ mode: "open" }) : this;
@@ -69,12 +73,25 @@ export function component(tagName, setup, options = {}) {
69
73
  // getCurrentLifecycle() and register its own cleanup.
70
74
  const lifecycle = createLifecycle();
71
75
  this.#lifecycle = lifecycle;
76
+ const host = this;
72
77
  const ctx = {
73
78
  host: this,
74
79
  // ctx.onDispose proxies to lifecycle — the single source of truth
75
80
  // for component cleanups (including auto-cleanup from
76
81
  // resource(), navigator listeners, etc.).
77
82
  onDispose: (fn) => lifecycle.onDispose(fn),
83
+ attr(name, defaultValue = "") {
84
+ let s = host.#attrSignals.get(name);
85
+ if (!s) {
86
+ s = signal(host.getAttribute(name) ?? defaultValue);
87
+ host.#attrSignals.set(name, s);
88
+ // Ensure the attribute is observed. For the current instance
89
+ // we already have the signal; for future registrations of
90
+ // the same tag (HMR) the set persists.
91
+ attrSignalNames.add(name);
92
+ }
93
+ return s;
94
+ },
78
95
  };
79
96
  this.#renderer = runInLifecycle(lifecycle, () => setup(ctx));
80
97
  this.#effectDispose = effect(() => {
@@ -89,7 +106,12 @@ export function component(tagName, setup, options = {}) {
89
106
  this.#connected = false;
90
107
  }
91
108
  attributeChangedCallback(name, _old, value) {
92
- // reflect attribute to property user can bind a signal to it
109
+ // Update ctx.attr() signal if it exists for this attribute.
110
+ const s = this.#attrSignals.get(name);
111
+ if (s) {
112
+ s.set(value ?? "");
113
+ }
114
+ // Legacy reflection: reflect attribute to property.
93
115
  this[name] = value;
94
116
  }
95
117
  }
@@ -1 +1 @@
1
- {"version":3,"file":"component.js","sourceRoot":"","sources":["../../src/component.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,MAAM,EAAiB,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAuB,MAAM,WAAW,CAAC;AAC9D,OAAO,EAAE,KAAK,EAAE,WAAW,EAAkB,MAAM,UAAU,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,cAAc,EAAwB,MAAM,gBAAgB,CAAC;AACvF,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAgC5C,MAAM,UAAU,SAAS,CACvB,OAAe,EACf,KAAc,EACd,UAA4B,EAAE;IAE9B,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3B,QAAQ,CACN,yBAAyB,OAAO,EAAE,EAClC,cAAc,OAAO,yDAAyD,CAC/E,CAAC;QACF,OAAO;IACT,CAAC;IAED,MAAM,YAAY,GAAG,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC7C,IAAI,cAAc,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;QAChC,IACE,CAAC,YAAY;YACb,YAAY,CAAC,KAAK,KAAK,KAAK;YAC5B,CAAC,oBAAoB,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,EACpD,CAAC;YACD,QAAQ,CACN,uBAAuB,OAAO,EAAE,EAChC,cAAc,OAAO,oFAAoF,CAC1G,CAAC;QACJ,CAAC;QACD,OAAO,CAAC,wBAAwB;IAClC,CAAC;IAED,MAAM,SAAS,GAAG,OAAO,CAAC,MAAM,KAAK,KAAK,CAAC;IAC3C,MAAM,QAAQ,GAAG,OAAO,CAAC,kBAAkB,IAAI,EAAE,CAAC;IAElD,sDAAsD;IACtD,qEAAqE;IACrE,MAAM,WAAW,GAAgB,eAAe,CAC9C,OAAO,CAAC,MAAM,EACd,OAAO,EACP,SAAS,CACV,CAAC;IAEF,MAAM,WAAY,SAAQ,WAAW;QACnC,MAAM,KAAK,kBAAkB;YAC3B,OAAO,CAAC,GAAG,QAAQ,CAAC,CAAC;QACvB,CAAC;QAED,KAAK,CAAuB;QAC5B,SAAS,GAAkC,IAAI,CAAC;QAChD,cAAc,GAAoB,IAAI,CAAC;QACvC,UAAU,GAA2B,IAAI,CAAC;QAC1C,UAAU,GAAG,KAAK,CAAC;QAEnB;YACE,KAAK,EAAE,CAAC;YACR,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACtE,CAAC;QAED,iBAAiB;YACf,IAAI,IAAI,CAAC,UAAU;gBAAE,OAAO;YAC5B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;YAEvB,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC3B,IAAI,SAAS,EAAE,CAAC;oBACd,KAAK,CAAC,IAAI,CAAC,KAAmB,EAAE,GAAG,WAAW,CAAC,CAAC;gBAClD,CAAC;qBAAM,CAAC;oBACN,mBAAmB,CAAC,WAAW,CAAC,CAAC;gBACnC,CAAC;YACH,CAAC;YAED,sDAAsD;YACtD,sDAAsD;YACtD,sDAAsD;YACtD,MAAM,SAAS,GAAG,eAAe,EAAE,CAAC;YACpC,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;YAE5B,MAAM,GAAG,GAAqB;gBAC5B,IAAI,EAAE,IAAI;gBACV,kEAAkE;gBAClE,sDAAsD;gBACtD,0CAA0C;gBAC1C,SAAS,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE,CAAC;aAC3C,CAAC;YAEF,IAAI,CAAC,SAAS,GAAG,cAAc,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;YAE7D,IAAI,CAAC,cAAc,GAAG,MAAM,CAAC,GAAG,EAAE;gBAChC,MAAM,CAAC,IAAI,CAAC,SAAU,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;YACxC,CAAC,CAAC,CAAC;QACL,CAAC;QAED,oBAAoB;YAClB,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC;YACxB,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;YAC3B,IAAI,CAAC,UAAU,EAAE,OAAO,EAAE,CAAC;YAC3B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;YACvB,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;QAC1B,CAAC;QAED,wBAAwB,CACtB,IAAY,EACZ,IAAmB,EACnB,KAAoB;YAEpB,+DAA+D;YAC9D,IAA2C,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QAC7D,CAAC;KACF;IAED,cAAc,CAAC,MAAM,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;IAC5C,UAAU,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;AAC9C,CAAC;AAED,gCAAgC;AAEhC,SAAS,eAAe,CACtB,KAA6B,EAC7B,OAAe,EACf,SAAkB;IAElB,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,CAAC;IACtB,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACnD,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACnB,IAAI,KAAgB,CAAC;QACrB,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;YAC1B,KAAK,GAAG,IAAI,aAAa,EAAE,CAAC;YAC5B,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;QACvB,CAAC;aAAM,CAAC;YACN,KAAK,GAAG,CAAC,CAAC;QACZ,CAAC;QACD,+BAA+B;QAC/B,OAAO,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,eAAe,GAAG,IAAI,OAAO,EAAa,CAAC;AACjD,MAAM,UAAU,GAAG,IAAI,GAAG,EAGvB,CAAC;AAEJ,SAAS,oBAAoB,CAC3B,CAAmB,EACnB,CAAmB;IAEnB,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACxC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACxC,MAAM,EAAE,GAAG,CAAC,CAAC,kBAAkB,IAAI,EAAE,CAAC;IACtC,MAAM,EAAE,GAAG,CAAC,CAAC,kBAAkB,IAAI,EAAE,CAAC;IACtC,IAAI,EAAE,CAAC,MAAM,KAAK,EAAE,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC1C,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,SAAS,mBAAmB,CAAC,MAAmB;IAC9C,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAC/B,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC9C,QAAQ,CAAC,kBAAkB,GAAG,CAAC,GAAG,QAAQ,CAAC,kBAAkB,EAAE,GAAG,KAAK,CAAC,CAAC;AAC3E,CAAC;AAED,yBAAyB;AACzB,OAAO,EAAE,IAAI,EAAE,CAAC"}
1
+ {"version":3,"file":"component.js","sourceRoot":"","sources":["../../src/component.ts"],"names":[],"mappings":"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"}
@@ -85,14 +85,25 @@ export function resource(keyFn, fetcher, options = {}) {
85
85
  error.set(null);
86
86
  force = false;
87
87
  fetcher(key, ac.signal).then((result) => {
88
+ // Two-layer staleness check:
89
+ // 1. ac.signal.aborted — fetcher honored the AbortSignal
90
+ // (jsonFetcher does; user fetchers may not).
91
+ // 2. key !== lastKey — defensive guard for fetchers that ignore
92
+ // the AbortSignal and resolve after a newer run() has started.
93
+ // Without this, a slow stale response can overwrite the data
94
+ // from a faster newer one when the key changes rapidly.
88
95
  if (ac.signal.aborted)
89
96
  return;
97
+ if (key !== lastKey)
98
+ return;
90
99
  cache.set(key, { data: result, timestamp: Date.now() });
91
100
  data.set(result);
92
101
  loading.set(false);
93
102
  }, (err) => {
94
103
  if (ac.signal.aborted)
95
104
  return;
105
+ if (key !== lastKey)
106
+ return;
96
107
  error.set(err instanceof Error ? err : new Error(String(err)));
97
108
  loading.set(false);
98
109
  });