@madojs/mado 0.8.0 → 0.9.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.
@@ -1,6 +1,7 @@
1
1
  # Mado for Backend Developers
2
2
 
3
- > You write in Go / Rust / .NET / Java / Python and you need to build a web UI.
3
+ > You write in Go / Rust / .NET / Java / Python and you need to build a web UI
4
+ > for an admin panel, internal tool or dashboard.
4
5
  > This page is the mental model of Mado in 10 minutes, in your language.
5
6
 
6
7
  ---
@@ -9,17 +10,17 @@
9
10
 
10
11
  Mado is structured **like an HTTP server**. Seriously:
11
12
 
12
- | Server world | Mado |
13
- |---|---|
14
- | HTTP router (chi, axum, mux) | `routes()` — path manifest |
15
- | Handler `func(req, resp)` | `page({ view: (ctx) => html\`...\` })` |
16
- | Middleware | `layout` in `nested()` (wraps the handler) |
17
- | Template engine (Jinja, Handlebars) | `html\`\`` tagged template |
18
- | HTTP client with cache | `resource()` — fetch + cache + invalidation |
19
- | Reactive variable / atom | `signal()` — reactive getter |
20
- | Background goroutine / task | `effect()` — auto-reruns when a signal changes |
21
- | `defer cleanup()` | `ctx.onDispose(fn)` in component setup |
22
- | ENV variables | `createContext()` + `provide()`/`inject()` |
13
+ | Server world | Mado |
14
+ | ----------------------------------- | ---------------------------------------------- |
15
+ | HTTP router (chi, axum, mux) | `routes()` — path manifest |
16
+ | Handler `func(req, resp)` | `page({ view: (ctx) => html\`...\` })` |
17
+ | Middleware | `layout` in `nested()` (wraps the handler) |
18
+ | Template engine (Jinja, Handlebars) | `html\`\`` tagged template |
19
+ | HTTP client with cache | `resource()` — fetch + cache + invalidation |
20
+ | Reactive variable / atom | `signal()` — reactive getter |
21
+ | Background goroutine / task | `effect()` — auto-reruns when a signal changes |
22
+ | `defer cleanup()` | `ctx.onDispose(fn)` in component setup |
23
+ | ENV variables | `createContext()` + `provide()`/`inject()` |
23
24
 
24
25
  If you understand an HTTP server, you understand Mado.
25
26
 
@@ -130,20 +131,23 @@ import { resource, mutation, jsonFetcher, invalidate } from "@madojs/mado";
130
131
  const userId = signal(1);
131
132
 
132
133
  const user = resource(
133
- () => `/api/users/${userId()}`, // cache key (reactive!)
134
- jsonFetcher<User>(), // how to load
135
- { staleTime: 60_000 }, // 60-second cache
134
+ () => `/api/users/${userId()}`, // cache key (reactive!)
135
+ jsonFetcher<User>(), // how to load
136
+ { staleTime: 60_000 }, // 60-second cache
136
137
  );
137
138
 
138
139
  // in the component:
139
- user.data(); // User | undefined
140
- user.error(); // Error | null
141
- user.loading(); // boolean
140
+ user.data(); // User | undefined
141
+ user.error(); // Error | null
142
+ user.loading(); // boolean
142
143
 
143
144
  // mutation (like POST/PUT)
144
145
  const save = mutation<User, User>(
145
- (u) => fetch("/api/users", { method: "POST", body: JSON.stringify(u) }).then(r => r.json()),
146
- { invalidates: ["/api/users*"] }, // glob invalidation like `cache.Drop("users:*")`
146
+ (u) =>
147
+ fetch("/api/users", { method: "POST", body: JSON.stringify(u) }).then((r) =>
148
+ r.json(),
149
+ ),
150
+ { invalidates: ["/api/users*"] }, // glob invalidation — like `cache.Drop("users:*")`
147
151
  );
148
152
 
149
153
  await save.run(newUser);
@@ -169,9 +173,7 @@ component("x-counter", () => {
169
173
  const count = signal(0);
170
174
 
171
175
  return () => html`
172
- <button @click=${() => count.update(n => n + 1)}>
173
- Clicks: ${count}
174
- </button>
176
+ <button @click=${() => count.update((n) => n + 1)}>Clicks: ${count}</button>
175
177
  `;
176
178
  });
177
179
  ```
@@ -179,7 +181,7 @@ component("x-counter", () => {
179
181
  Usage:
180
182
 
181
183
  ```ts
182
- html`<x-counter></x-counter>`
184
+ html`<x-counter></x-counter>`;
183
185
  ```
184
186
 
185
187
  We register the `<x-counter>` tag in the browser — it becomes a "function" that can be inserted into HTML. This is a **native** browser mechanism (Web Components), Mado only glues it together with signals.
@@ -195,22 +197,29 @@ import { useForm } from "@madojs/mado";
195
197
 
196
198
  const f = useForm({
197
199
  email: { required: true, type: "email" },
198
- age: { required: true, type: "number", min: 18 },
200
+ age: { required: true, type: "number", min: 18 },
199
201
  });
200
202
 
201
203
  // in the template:
202
204
  html`
203
- <form @submit=${f.onSubmit(async (v) => {
204
- await api.save(v);
205
- f.reset();
206
- })}>
207
- <input name="email" .value=${() => f.values().email ?? ""}
208
- @input=${f.onInput} @blur=${f.onBlur} />
209
-
210
- ${() => f.errors().email && f.touched().email
211
- ? html`<small>${f.errors().email}</small>`
212
- : null}
213
-
205
+ <form
206
+ @submit=${f.onSubmit(async (v) => {
207
+ await api.save(v);
208
+ f.reset();
209
+ })}
210
+ >
211
+ <input
212
+ name="email"
213
+ .value=${() => f.values().email ?? ""}
214
+ @input=${f.onInput}
215
+ @blur=${f.onBlur}
216
+ />
217
+
218
+ ${() =>
219
+ f.errors().email && f.touched().email
220
+ ? html`<small>${f.errors().email}</small>`
221
+ : null}
222
+
214
223
  <button ?disabled=${() => !f.isValid() || f.submitting()}>Save</button>
215
224
  </form>
216
225
  `;
@@ -233,12 +242,12 @@ const ApiCtx = createContext<ApiClient>(defaultApiClient);
233
242
  // in the root component — provide
234
243
  component("x-app", ({ host }) => {
235
244
  provide(host, ApiCtx, new ApiClient("https://api.example.com"));
236
- return () => html`<x-page/>`;
245
+ return () => html`<x-page />`;
237
246
  });
238
247
 
239
248
  // in any child — consume
240
249
  component("x-page", ({ host }) => {
241
- const api = inject(host, ApiCtx); // signal<ApiClient>
250
+ const api = inject(host, ApiCtx); // signal<ApiClient>
242
251
  return () => html`<div>API version: ${() => api().version}</div>`;
243
252
  });
244
253
  ```
@@ -255,7 +264,7 @@ If you're used to server-side rendering for SEO, in Mado this is solved differen
255
264
  // src/pages/product.ts
256
265
  export default page({
257
266
  bake: {
258
- paths: () => api.allProductSlugs(), // build-time fetch
267
+ paths: () => api.allProductSlugs(), // build-time fetch
259
268
  data: ({ slug }) => api.getProduct(slug),
260
269
  revalidate: 3600,
261
270
  },
@@ -264,7 +273,7 @@ export default page({
264
273
  canonical: `/product/${slug}`,
265
274
  og: { title: data.name, image: data.image },
266
275
  }),
267
- view: ({ params }) => html`<x-product data-slug=${params.slug}/>`,
276
+ view: ({ params }) => html`<x-product data-slug=${params.slug} />`,
268
277
  });
269
278
  ```
270
279
 
@@ -288,14 +297,20 @@ import { page, html, resource, each, signal } from "@madojs/mado";
288
297
  export default page({
289
298
  view: () => {
290
299
  const users = resource(() => "/api/users", jsonFetcher<User[]>());
291
-
300
+
292
301
  return html`
293
- ${() => users.loading() ? html`<p>Loading…</p>` : null}
294
- ${() => users.error() ? html`<p>Error: ${users.error()!.message}</p>` : null}
302
+ ${() => (users.loading() ? html`<p>Loading…</p>` : null)}
303
+ ${() =>
304
+ users.error() ? html`<p>Error: ${users.error()!.message}</p>` : null}
295
305
  <ul>
296
- ${() => each(users.data() ?? [], u => u.id, u => html`
297
- <li><a href="/users/${u.id}" data-link>${u.name}</a></li>
298
- `)}
306
+ ${() =>
307
+ each(
308
+ users.data() ?? [],
309
+ (u) => u.id,
310
+ (u) => html`
311
+ <li><a href="/users/${u.id}" data-link>${u.name}</a></li>
312
+ `,
313
+ )}
299
314
  </ul>
300
315
  `;
301
316
  },
@@ -308,7 +323,10 @@ export default page({
308
323
  import { useForm, mutation } from "@madojs/mado";
309
324
 
310
325
  const createUser = mutation<NewUser, User>(
311
- (u) => fetch("/api/users", { method: "POST", body: JSON.stringify(u) }).then(r => r.json()),
326
+ (u) =>
327
+ fetch("/api/users", { method: "POST", body: JSON.stringify(u) }).then((r) =>
328
+ r.json(),
329
+ ),
312
330
  { invalidates: ["/api/users*"] },
313
331
  );
314
332
 
@@ -316,11 +334,13 @@ const createUser = mutation<NewUser, User>(
316
334
  const f = useForm({ name: { required: true } });
317
335
 
318
336
  html`
319
- <form @submit=${f.onSubmit(async (v) => {
320
- await createUser.run(v);
321
- navigate("/users");
322
- })}>
323
- <input name="name" @input=${f.onInput}>
337
+ <form
338
+ @submit=${f.onSubmit(async (v) => {
339
+ await createUser.run(v);
340
+ navigate("/users");
341
+ })}
342
+ >
343
+ <input name="name" @input=${f.onInput} />
324
344
  <button>Create</button>
325
345
  </form>
326
346
  `;
@@ -353,8 +373,8 @@ export default routes({
353
373
  "/app/*": nested({
354
374
  layout: () => import("./layouts/auth-layout.js"),
355
375
  routes: {
356
- "dashboard": () => import("./pages/dashboard.js"),
357
- "users": () => import("./pages/users.js"),
376
+ dashboard: () => import("./pages/dashboard.js"),
377
+ users: () => import("./pages/users.js"),
358
378
  },
359
379
  }),
360
380
  });
@@ -367,7 +387,7 @@ export default routes({
367
387
  export class ApiClient {
368
388
  constructor(private base: string) {}
369
389
  get<T>(path: string): Promise<T> {
370
- return fetch(this.base + path).then(r => r.json());
390
+ return fetch(this.base + path).then((r) => r.json());
371
391
  }
372
392
  }
373
393
 
@@ -598,6 +598,105 @@ See [`17-shadow-dom-forms.md`](./17-shadow-dom-forms.md) for the full pattern.
598
598
 
599
599
  ---
600
600
 
601
+ ## Pitfall #23: signal reads in async functions called from `view()` create effect cycles
602
+
603
+ **Symptom:** `[mado] effect cycle detected: subscriber re-ran more than 100 times in one flush.`
604
+
605
+ The router calls `page.view()` inside a reactive effect. Any signal read
606
+ **synchronously** during `view()` subscribes the router's render effect. If that
607
+ signal is then written (e.g. `loading.set(true)`) — the router re-runs `view()`,
608
+ which reads the signal again → infinite loop.
609
+
610
+ ```ts
611
+ // ❌ INFINITE LOOP — loadMore reads signals inside the router's effect
612
+ export default page({
613
+ view: () => {
614
+ const cursor = signal<string | null>("start");
615
+ const loading = signal(false);
616
+
617
+ const loadMore = async () => {
618
+ if (cursor() === null || loading()) return; // ← subscribes render effect!
619
+ loading.set(true); // ← re-triggers render → loadMore() → ∞
620
+ const res = await fetch(`/api/items?cursor=${cursor()}`);
621
+ // ...
622
+ };
623
+
624
+ loadMore(); // called synchronously during view()
625
+ return html`...`;
626
+ },
627
+ });
628
+
629
+ // ✅ CORRECT — wrap signal reads in untracked()
630
+ import { untracked } from "@madojs/mado";
631
+
632
+ export default page({
633
+ view: () => {
634
+ const cursor = signal<string | null>("start");
635
+ const loading = signal(false);
636
+
637
+ const loadMore = async () => {
638
+ const c = untracked(() => cursor());
639
+ if (c === null || untracked(() => loading())) return;
640
+ loading.set(true);
641
+ const res = await fetch(`/api/items?cursor=${c}`);
642
+ // ...
643
+ };
644
+
645
+ loadMore();
646
+ return html`...`;
647
+ },
648
+ });
649
+ ```
650
+
651
+ **Rule:** Any function that reads signals AND is called synchronously during
652
+ `view()` initialization must use `untracked()` for those reads. This includes:
653
+
654
+ - Data fetching / loadMore functions
655
+ - IntersectionObserver callbacks set up during init
656
+ - Timer/polling setup functions that check state
657
+
658
+ Signals read inside the **returned template** (`html\`...\``) are fine — they are
659
+ wrapped in a child-binding function `${() => ...}` which creates its own effect.
660
+
661
+ ---
662
+
663
+ ## Pitfall #24: `setInterval` / manual subscriptions in `page()` view without cleanup
664
+
665
+ **Symptom:** After navigating away, timers/subscriptions keep running (zombie intervals,
666
+ server logs show polling requests from pages the user already left).
667
+
668
+ ```ts
669
+ // ❌ ZOMBIE — interval survives navigation
670
+ export default page({
671
+ view: () => {
672
+ const tick = signal(0);
673
+ setInterval(() => tick.update((n) => n + 1), 3000); // never cleaned up!
674
+ return html`<div>${tick}</div>`;
675
+ },
676
+ });
677
+
678
+ // ✅ CORRECT — use onDispose for cleanup
679
+ export default page({
680
+ view: ({ onDispose }) => {
681
+ const tick = signal(0);
682
+ const id = setInterval(() => tick.update((n) => n + 1), 3000);
683
+ onDispose(() => clearInterval(id));
684
+ return html`<div>${tick}</div>`;
685
+ },
686
+ });
687
+ ```
688
+
689
+ **Note:** `resource()` and `effect()` created inside `view()` are automatically
690
+ cleaned up on navigation (they register with the page lifecycle). Only raw
691
+ browser APIs need explicit `onDispose()`:
692
+
693
+ - `setInterval` / `setTimeout`
694
+ - `addEventListener` (on window/document)
695
+ - `WebSocket` / `EventSource`
696
+ - `IntersectionObserver` / `ResizeObserver`
697
+
698
+ ---
699
+
601
700
  ## Cheat-sheet for AI
602
701
 
603
702
  | If you want to do… | Correct in Mado |
@@ -619,5 +718,7 @@ See [`17-shadow-dom-forms.md`](./17-shadow-dom-forms.md) for the full pattern.
619
718
  | `@customElement('x')` | `component('x-name', setup)` |
620
719
  | `host.getAttribute('x')` in render | `ctx.attr('x', default)` (reactive) |
621
720
  | `jsonFetcher()` with auth | `apiFetcher()` (attaches Bearer token) |
721
+ | `setInterval` in page view | `onDispose(() => clearInterval(id))` |
722
+ | signal read in view() async init | `untracked(() => cursor())` |
622
723
 
623
724
  If something doesn't fit this list — open `src/` and **read 500 lines**. Seriously. Mado is intentionally small to be readable.
@@ -2,9 +2,12 @@
2
2
 
3
3
  > Une seule bonne façon. Des contrats stricts. Pas de magie.
4
4
 
5
- Mado n'est pas seulement un framework c'est un **ensemble de conventions**. Si vous les
6
- respectez, le projet reste compréhensible même avec 200 écrans et 5 développeurs. Si vous
7
- les enfreignez les types et le linter vous le diront immédiatement.
5
+ Mado est un framework pour les équipes qui construisent des panneaux d'admin,
6
+ des outils internes et des SPA métier des apps qui doivent être simples à
7
+ créer et ennuyeuses à maintenir. Pour cela, il impose un **ensemble de
8
+ conventions**. Si vous les respectez, le projet reste compréhensible même avec
9
+ 200 écrans et 5 développeurs. Si vous les enfreignez — les types et le linter
10
+ vous le diront immédiatement.
8
11
 
9
12
  ## Principes
10
13
 
@@ -42,13 +45,21 @@ tous écrire de la même manière.
42
45
 
43
46
  ```ts
44
47
  // src/components/user-card.ts
45
- import { component, html, css } from '@madojs/mado';
46
-
47
- component('x-user-card', () => {
48
- return () => html`<div class="card"><slot/></div>`;
49
- }, {
50
- styles: css`.card { padding: 1rem; }`,
51
- });
48
+ import { component, html, css } from "@madojs/mado";
49
+
50
+ component(
51
+ "x-user-card",
52
+ () => {
53
+ return () => html`<div class="card"><slot /></div>`;
54
+ },
55
+ {
56
+ styles: css`
57
+ .card {
58
+ padding: 1rem;
59
+ }
60
+ `,
61
+ },
62
+ );
52
63
  ```
53
64
 
54
65
  `import './components/user-card.js'` **enregistre** le composant via
@@ -63,7 +74,7 @@ component('x-user-card', () => {
63
74
  const user = resource(() => `/api/users/${id()}`, jsonFetcher());
64
75
 
65
76
  // écriture → mutation
66
- const save = mutation(api.save, { invalidates: ['/api/users*'] });
77
+ const save = mutation(api.save, { invalidates: ["/api/users*"] });
67
78
  ```
68
79
 
69
80
  Cela fournit la mise en cache, l'annulation, la gestion des erreurs et l'invalidation automatique.
@@ -72,11 +83,11 @@ Cela fournit la mise en cache, l'annulation, la gestion des erreurs et l'invalid
72
83
 
73
84
  ```ts
74
85
  // src/pages/user-profile.ts
75
- import { page, html, resource, jsonFetcher } from '@madojs/mado';
86
+ import { page, html, resource, jsonFetcher } from "@madojs/mado";
76
87
 
77
88
  export default page({
78
89
  title: ({ id }) => `Utilisateur #${id}`,
79
- view: ({ params }) => html`...`,
90
+ view: ({ params }) => html`...`,
80
91
  });
81
92
  ```
82
93
 
@@ -101,6 +112,7 @@ Voir [`01-routing.md`](./01-routing.md).
101
112
  ## En cas de doute
102
113
 
103
114
  Si vous vous demandez "quelle est la meilleure façon ici ?" — c'est un signal que :
115
+
104
116
  1. Soit il existe un helper intégré que vous ne connaissez pas (consultez `docs/`).
105
117
  2. Soit c'est une nouvelle situation — discutez-en et **consignez-la** dans ce document
106
118
  comme une convention supplémentaire.
@@ -619,5 +619,7 @@ Plus de détails : [`17-shadow-dom-forms.md`](./17-shadow-dom-forms.md).
619
619
  | `@customElement('x')` | `component('x-name', setup)` |
620
620
  | `host.getAttribute('x')` dans render | `ctx.attr('x', default)` (réactif) |
621
621
  | `jsonFetcher()` avec auth | `apiFetcher()` (attache le Bearer token) |
622
+ | `setInterval` dans page view | `onDispose(() => clearInterval(id))` |
623
+ | lecture signal dans async init view() | `untracked(() => cursor())` |
622
624
 
623
625
  Si quelque chose ne rentre pas dans cette liste — ouvrez `src/` et **lisez 500 lignes**. Sérieusement. Mado est intentionnellement petit pour être lisible.
@@ -2,7 +2,11 @@
2
2
 
3
3
  > Один правильный путь. Жёсткие контракты. Никакой магии.
4
4
 
5
- Mado — это не просто фреймворк, это **набор соглашений**. Если ты следуешь им, проект остаётся понятным даже когда в нём 200 экранов и 5 разработчиков. Если нарушаешь — типы и линтер скажут об этом сразу.
5
+ Mado — фреймворк для команд, которые строят админки, внутренние инструменты
6
+ и бизнес-SPA — приложения, которые должны быть просты в разработке и скучны
7
+ в поддержке. Для этого он задаёт **набор соглашений**. Если ты следуешь им,
8
+ проект остаётся понятным даже когда в нём 200 экранов и 5 разработчиков. Если
9
+ нарушаешь — типы и линтер скажут об этом сразу.
6
10
 
7
11
  ## Принципы
8
12
 
@@ -32,13 +36,21 @@ src/
32
36
 
33
37
  ```ts
34
38
  // src/components/user-card.ts
35
- import { component, html, css } from '@madojs/mado';
36
-
37
- component('x-user-card', () => {
38
- return () => html`<div class="card"><slot/></div>`;
39
- }, {
40
- styles: css`.card { padding: 1rem; }`,
41
- });
39
+ import { component, html, css } from "@madojs/mado";
40
+
41
+ component(
42
+ "x-user-card",
43
+ () => {
44
+ return () => html`<div class="card"><slot /></div>`;
45
+ },
46
+ {
47
+ styles: css`
48
+ .card {
49
+ padding: 1rem;
50
+ }
51
+ `,
52
+ },
53
+ );
42
54
  ```
43
55
 
44
56
  Импорт `import './components/user-card.js'` **регистрирует** компонент через `customElements.define`. Это side-effect. Где компонент нужен — там и импортируем.
@@ -52,7 +64,7 @@ component('x-user-card', () => {
52
64
  const user = resource(() => `/api/users/${id()}`, jsonFetcher());
53
65
 
54
66
  // запись → mutation
55
- const save = mutation(api.save, { invalidates: ['/api/users*'] });
67
+ const save = mutation(api.save, { invalidates: ["/api/users*"] });
56
68
  ```
57
69
 
58
70
  Это даёт кеш, отмену, обработку ошибок, авто-инвалидацию.
@@ -61,11 +73,11 @@ const save = mutation(api.save, { invalidates: ['/api/users*'] });
61
73
 
62
74
  ```ts
63
75
  // src/pages/user-profile.ts
64
- import { page, html, resource, jsonFetcher } from '@madojs/mado';
76
+ import { page, html, resource, jsonFetcher } from "@madojs/mado";
65
77
 
66
78
  export default page({
67
79
  title: ({ id }) => `User #${id}`,
68
- view: ({ params }) => html`...`,
80
+ view: ({ params }) => html`...`,
69
81
  });
70
82
  ```
71
83
 
@@ -87,6 +99,7 @@ export default page({
87
99
  ## Когда сомневаешься
88
100
 
89
101
  Если ты задаёшься вопросом "а как тут лучше?" — это сигнал, что:
102
+
90
103
  1. Либо есть встроенный хелпер, который ты не знаешь (загляни в `docs/`).
91
104
  2. Либо это новая ситуация — её надо обсудить и **зафиксировать** в этом документе как ещё одно соглашение.
92
105
 
@@ -618,5 +618,7 @@ component("x-input", ({ host, attr }) => {
618
618
  | `@customElement('x')` | `component('x-name', setup)` |
619
619
  | `host.getAttribute('x')` в render | `ctx.attr('x', default)` (реактивно) |
620
620
  | `jsonFetcher()` с авторизацией | `apiFetcher()` (прикрепляет Bearer токен) |
621
+ | `setInterval` в page view | `onDispose(() => clearInterval(id))` |
622
+ | чтение сигнала в async init view() | `untracked(() => cursor())` |
621
623
 
622
624
  Если что-то не подходит из этого списка — открой `src/` и **прочитай 500 строк**. Это серьёзно. Mado специально маленький, чтобы быть читаемым.
@@ -2,7 +2,9 @@
2
2
 
3
3
  > Один зрозумілий шлях. Жорсткі контракти. Мінімум магії.
4
4
 
5
- Mado — це не лише набір API, а набір домовленостей. Якщо їх дотримуватись,
5
+ Mado — фреймворк для команд, що будують адмін-панелі, внутрішні інструменти
6
+ та бізнес-SPA — застосунки, які мають бути простими у розробці та нудними в
7
+ підтримці. Для цього він задає **набір домовленостей**. Якщо їх дотримуватись,
6
8
  проєкт залишається читабельним навіть тоді, коли в ньому десятки сторінок і
7
9
  кілька розробників.
8
10
 
@@ -141,3 +141,5 @@ Object.defineProperty(host, "value", {
141
141
  | `class extends HTMLElement` | `component('x-name', setup)` |
142
142
  | `host.getAttribute('x')` | `ctx.attr('x', default)` |
143
143
  | `jsonFetcher()` з auth | `apiFetcher()` |
144
+ | `setInterval` в page view | `onDispose(() => clearInterval(id))` |
145
+ | читання сигналу в async init | `untracked(() => cursor())` |
package/llms.txt CHANGED
@@ -1,9 +1,9 @@
1
1
  # Mado
2
2
 
3
- > Small native-web SPA framework on the platform (Web Components + signals + tagged-template `html`).
4
- > No build step (only `tsc`), no runtime dependencies, readable in an evening.
3
+ > A calm browser-native SPA framework for internal tools, admin panels and business apps.
4
+ > Routing, forms, state, data fetching and prerendering. TypeScript-only build (`tsc`), zero runtime dependencies.
5
5
 
6
- Mado is a narrowly-focused frontend framework that deliberately avoids React patterns (no JSX, no hooks, no VDOM, no Vite). Target audience: backend developers and senior frontend engineers who are tired of the infrastructure complexity of React/Next.
6
+ Mado is a focused frontend framework for admin panels, internal tools and CRUD-heavy SPA. It deliberately avoids React patterns (no JSX, no hooks, no VDOM, no Vite). Target audience: backend developers and small teams who want a complete app stack without frontend infrastructure overhead.
7
7
 
8
8
  ## Key things an AI assistant needs to know
9
9
 
@@ -13,7 +13,9 @@ Mado is a narrowly-focused frontend framework that deliberately avoids React pat
13
13
  - **Components are Web Components.** Registered via `component('x-name', setupFn, options)`. Names must include a hyphen (`x-foo`, `my-button`).
14
14
  - **Component files register tags as side effects.** The browser does not auto-import files by tag name. If `<x-card>` works, some imported module already ran `customElements.define("x-card", ...)`.
15
15
  - **Cleanup via `ctx.onDispose(fn)`** in setup, not via return from effect.
16
- - **Reactive attributes via `ctx.attr(name, default?)`** returns a Signal<string> that auto-updates when the attribute changes. No MutationObserver needed.
16
+ - **Page cleanup via `onDispose`** in view: `view: ({ onDispose }) => { ... onDispose(() => cleanup()); }`. Only needed for raw APIs (setInterval, WebSocket). `resource()`/`effect()` auto-cleanup.
17
+ - **`untracked()` in page view async** — functions called synchronously in `view()` that read signals must wrap reads in `untracked()` to avoid effect cycles with the router.
18
+ - **Reactive attributes via `ctx.attr(name, default?)`** — returns a Signal<string> that auto-updates when the attribute changes. Uses `observedAttributes` when available, falls back to a per-instance `MutationObserver` for attrs registered during setup. No boilerplate needed.
17
19
 
18
20
  ## Critical template rules
19
21
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@madojs/mado",
3
- "version": "0.8.0",
4
- "description": "Mado — a small native-web SPA framework with Web Components, signals, tagged-template html, router, resources, and forms. TypeScript-only build, zero runtime dependencies.",
3
+ "version": "0.9.0",
4
+ "description": "Mado — a calm browser-native SPA framework for internal tools, admin panels and business apps. Routing, forms, state and data fetching without frontend infrastructure overhead.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -80,4 +80,4 @@
80
80
  "engines": {
81
81
  "node": ">=20"
82
82
  }
83
- }
83
+ }
@@ -120,14 +120,14 @@ const result = await build({
120
120
  legalComments: "none",
121
121
  });
122
122
 
123
- const entryOutput = Object.entries(result.metafile.outputs).find(
124
- ([name, info]) => info.entryPoint && name.endsWith(".js"),
125
- );
126
- if (!entryOutput) {
127
- console.error("[bundle] entry not found in outputs");
123
+ // With splitting: true, esbuild marks all dynamic-import chunks as having
124
+ // entryPoint. We identify the real app entry by the `entryNames` prefix "main-".
125
+ const mainBundle = (await readdir(ASSETS_DIR))
126
+ .find((f) => f.startsWith("main-") && f.endsWith(".js") && !f.endsWith(".js.map"));
127
+ if (!mainBundle) {
128
+ console.error("[bundle] entry not found in outputs (no main-*.js in assets dir)");
128
129
  process.exit(1);
129
130
  }
130
- const mainBundle = basename(entryOutput[0]);
131
131
 
132
132
  // Collect all js chunks in the assets dir.
133
133
  const allJs = (await readdir(ASSETS_DIR)).filter((f) => f.endsWith(".js"));