@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.
- package/AGENTS.md +82 -30
- package/CHANGELOG.md +208 -1
- package/dist/src/component.d.ts +17 -4
- package/dist/src/component.js +26 -4
- package/dist/src/component.js.map +1 -1
- package/dist/src/resource.js +11 -0
- package/dist/src/resource.js.map +1 -1
- package/dist/src/router/manifest.js +29 -2
- package/dist/src/router/manifest.js.map +1 -1
- package/docs/en/07-llm-pitfalls.md +197 -60
- package/docs/en/08-llm-zero-history-test.md +1 -1
- package/docs/en/17-shadow-dom-forms.md +192 -0
- package/docs/en/README.md +20 -19
- package/docs/fr/07-llm-pitfalls.md +196 -60
- package/docs/fr/17-shadow-dom-forms.md +196 -0
- package/docs/fr/README.md +20 -19
- package/docs/ru/07-llm-pitfalls.md +198 -61
- package/docs/ru/08-llm-zero-history-test.md +39 -38
- package/docs/ru/09-shadow-vs-light-dom.md +97 -81
- package/docs/ru/17-shadow-dom-forms.md +193 -0
- package/docs/ru/README.md +20 -19
- package/docs/uk/07-llm-pitfalls.md +64 -3
- package/docs/uk/17-shadow-dom-forms.md +193 -0
- package/docs/uk/README.md +20 -19
- package/llms.txt +50 -1
- package/package.json +2 -2
- package/scripts/bake.mjs +76 -22
- package/scripts/bundle.mjs +24 -1
- package/scripts/cli.mjs +98 -45
- package/scripts/preview.mjs +104 -10
- package/server/serve.mjs +80 -7
- package/starters/admin/index.html +10 -3
- package/starters/admin/package.json +3 -1
- package/starters/admin/src/components/x-button.ts +40 -13
- package/starters/admin/src/components/x-input.ts +50 -19
- package/starters/admin/src/lib/api.ts +55 -4
- package/starters/admin/src/pages/admin/order-detail.ts +4 -2
- package/starters/admin/src/pages/home.ts +10 -1
- package/starters/crud/index.html +12 -4
- package/starters/crud/package.json +3 -1
- package/starters/crud/src/pages/home.ts +16 -0
- package/starters/minimal/index.html +12 -4
- package/starters/minimal/package.json +2 -0
- 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
|
|
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
|
|
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:
|
|
240
|
+
age: { required: true, type: "number", min: 18 },
|
|
202
241
|
});
|
|
203
242
|
|
|
204
243
|
html`
|
|
205
|
-
<form
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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 {
|
|
224
|
-
|
|
225
|
-
|
|
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,
|
|
232
|
-
styles: css`x-shell header { ... }`,
|
|
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
|
|
318
|
-
|
|
319
|
-
| How does reactivity work?
|
|
320
|
-
| How are templates parsed?
|
|
321
|
-
| How does the router work?
|
|
322
|
-
| How does resource + cache work?
|
|
323
|
-
| How do forms work?
|
|
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?
|
|
326
|
-
| How should bake be used?
|
|
327
|
-
| When something goes wrong
|
|
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
|
-
|
|
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`
|
package/dist/src/component.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
}
|
package/dist/src/component.js
CHANGED
|
@@ -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 [...
|
|
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
|
-
//
|
|
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,
|
|
1
|
+
{"version":3,"file":"component.js","sourceRoot":"","sources":["../../src/component.ts"],"names":[],"mappings":"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"}
|
package/dist/src/resource.js
CHANGED
|
@@ -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
|
});
|