@madojs/mado 0.11.0 → 0.12.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 (221) hide show
  1. package/AGENTS.md +102 -25
  2. package/CHANGELOG.md +185 -1
  3. package/README.md +137 -172
  4. package/dist/src/component.d.ts +3 -2
  5. package/dist/src/component.js +98 -6
  6. package/dist/src/component.js.map +1 -1
  7. package/dist/src/css.js +34 -5
  8. package/dist/src/css.js.map +1 -1
  9. package/dist/src/head.d.ts +6 -6
  10. package/dist/src/head.js +6 -6
  11. package/dist/src/html/bindings.js +3 -3
  12. package/dist/src/html/bindings.js.map +1 -1
  13. package/dist/src/html/parser.d.ts +1 -1
  14. package/dist/src/html/parser.js +1 -1
  15. package/dist/src/html/template.js +24 -10
  16. package/dist/src/html/template.js.map +1 -1
  17. package/dist/src/index.d.ts +8 -4
  18. package/dist/src/index.js +13 -3
  19. package/dist/src/index.js.map +1 -1
  20. package/dist/src/page.d.ts +62 -28
  21. package/dist/src/page.js +5 -0
  22. package/dist/src/page.js.map +1 -1
  23. package/dist/src/resource.js +2 -1
  24. package/dist/src/resource.js.map +1 -1
  25. package/dist/src/router/base.d.ts +65 -0
  26. package/dist/src/router/base.js +168 -0
  27. package/dist/src/router/base.js.map +1 -0
  28. package/dist/src/router/manifest.js +75 -31
  29. package/dist/src/router/manifest.js.map +1 -1
  30. package/dist/src/router/match.d.ts +16 -2
  31. package/dist/src/router/match.js +41 -1
  32. package/dist/src/router/match.js.map +1 -1
  33. package/dist/src/router/navigation.js +58 -9
  34. package/dist/src/router/navigation.js.map +1 -1
  35. package/dist/src/static-runtime.d.ts +81 -0
  36. package/dist/src/static-runtime.js +209 -0
  37. package/dist/src/static-runtime.js.map +1 -0
  38. package/dist/src/vite/index.d.ts +31 -3
  39. package/dist/src/vite/index.js +49 -18
  40. package/dist/src/vite/index.js.map +1 -1
  41. package/docs/README.md +5 -8
  42. package/docs/architecture/adr/0001-browser-static-snapshots.md +132 -0
  43. package/docs/en/00-the-mado-way.md +1 -1
  44. package/docs/en/01-quickstart.md +183 -0
  45. package/docs/en/10-pages-and-components.md +271 -0
  46. package/docs/en/11-templates-and-signals.md +211 -0
  47. package/docs/en/12-routing.md +229 -0
  48. package/docs/en/13-data.md +225 -0
  49. package/docs/en/14-forms.md +244 -0
  50. package/docs/en/15-static-snapshots.md +181 -0
  51. package/docs/en/{10-app-architecture.md → 16-app-architecture.md} +53 -2
  52. package/docs/en/{13-deployment.md → 20-deployment.md} +59 -10
  53. package/docs/en/{14-testing.md → 22-testing.md} +6 -5
  54. package/docs/en/23-cookbook.md +292 -0
  55. package/docs/en/{18-api-freeze-map.md → 30-api-freeze-map.md} +12 -3
  56. package/docs/en/{20-v1-stability.md → 32-v1-stability.md} +3 -3
  57. package/docs/en/40-llm-guide.md +776 -0
  58. package/docs/en/{06-for-backenders.md → 41-for-backenders.md} +15 -16
  59. package/docs/en/{05-why-mado.md → 42-why-mado.md} +1 -1
  60. package/docs/en/README.md +97 -27
  61. package/docs/recipes/nginx/Containerfile +26 -0
  62. package/docs/recipes/nginx/nginx.conf +42 -0
  63. package/llms.txt +246 -211
  64. package/package.json +17 -10
  65. package/scripts/bake.mjs +4 -568
  66. package/scripts/cli/generate.mjs +55 -5
  67. package/scripts/cli/help.mjs +11 -5
  68. package/scripts/cli/index.mjs +20 -4
  69. package/scripts/cli/init.mjs +7 -2
  70. package/scripts/cli/release.mjs +20 -68
  71. package/scripts/docs-lint.mjs +170 -0
  72. package/scripts/package-smoke.mjs +67 -9
  73. package/scripts/preview.mjs +95 -6
  74. package/scripts/size-budget.mjs +5 -1
  75. package/scripts/static/browser.mjs +654 -0
  76. package/scripts/static/discover.mjs +281 -0
  77. package/scripts/static/output.mjs +141 -0
  78. package/scripts/static/serialize.mjs +212 -0
  79. package/scripts/static/server.mjs +167 -0
  80. package/scripts/static.mjs +191 -0
  81. package/starters/default/README.md +57 -55
  82. package/starters/default/package.json +3 -9
  83. package/starters/default/src/app.routes.ts +20 -33
  84. package/starters/default/src/components/feature-card.component.ts +40 -0
  85. package/starters/default/src/components/live-counter.component.ts +57 -0
  86. package/starters/default/src/content/guides.ts +55 -0
  87. package/starters/default/src/main.ts +9 -13
  88. package/starters/default/src/pages/app.page.ts +41 -0
  89. package/starters/default/src/pages/guide.page.ts +40 -0
  90. package/starters/default/src/pages/home.page.ts +49 -0
  91. package/starters/default/src/pages/not-found.page.ts +18 -0
  92. package/starters/default/src/styles/document.css +39 -0
  93. package/starters/default/src/styles/reset.css +37 -0
  94. package/starters/default/src/styles/tokens.css +30 -0
  95. package/starters/default/src/styles.d.ts +3 -1
  96. package/starters/default/src/vite-env.d.ts +1 -1
  97. package/starters/default/vite.config.ts +14 -1
  98. package/starters/modular/README.md +142 -0
  99. package/starters/modular/index.html +13 -0
  100. package/starters/modular/package.json +30 -0
  101. package/starters/modular/src/app.routes.ts +39 -0
  102. package/starters/{default → modular}/src/layouts/app-shell.layout.ts +8 -5
  103. package/starters/{default → modular}/src/layouts/auth-shell.layout.ts +3 -3
  104. package/starters/modular/src/main.ts +16 -0
  105. package/starters/{default → modular}/src/modules/billing/pages/invoices-list.page.ts +3 -3
  106. package/starters/{default → modular}/src/modules/home/home.page.ts +5 -7
  107. package/starters/modular/src/modules/home/not-found.page.ts +14 -0
  108. package/starters/modular/src/styles.d.ts +1 -0
  109. package/starters/modular/src/vite-env.d.ts +1 -0
  110. package/starters/modular/tsconfig.json +24 -0
  111. package/starters/modular/vite.config.ts +131 -0
  112. package/TODO.md +0 -79
  113. package/docs/en/01-routing.md +0 -152
  114. package/docs/en/02-project-layout.md +0 -124
  115. package/docs/en/03-static-bake.md +0 -249
  116. package/docs/en/04-ide-setup.md +0 -162
  117. package/docs/en/07-llm-pitfalls.md +0 -724
  118. package/docs/en/08-llm-zero-history-test.md +0 -56
  119. package/docs/en/09-shadow-vs-light-dom.md +0 -174
  120. package/docs/en/11-layouts.md +0 -113
  121. package/docs/en/12-auth-and-api.md +0 -124
  122. package/docs/en/16-bake-cookbook.md +0 -100
  123. package/docs/en/17-shadow-dom-forms.md +0 -192
  124. package/docs/fr/00-the-mado-way.md +0 -85
  125. package/docs/fr/01-routing.md +0 -120
  126. package/docs/fr/02-project-layout.md +0 -84
  127. package/docs/fr/03-static-bake.md +0 -289
  128. package/docs/fr/04-ide-setup.md +0 -162
  129. package/docs/fr/05-why-mado.md +0 -193
  130. package/docs/fr/06-for-backenders.md +0 -438
  131. package/docs/fr/07-llm-pitfalls.md +0 -625
  132. package/docs/fr/08-llm-zero-history-test.md +0 -38
  133. package/docs/fr/09-shadow-vs-light-dom.md +0 -65
  134. package/docs/fr/10-app-architecture.md +0 -138
  135. package/docs/fr/11-layouts.md +0 -47
  136. package/docs/fr/12-auth-and-api.md +0 -76
  137. package/docs/fr/13-deployment.md +0 -57
  138. package/docs/fr/14-testing.md +0 -41
  139. package/docs/fr/15-error-handling.md +0 -50
  140. package/docs/fr/16-bake-cookbook.md +0 -88
  141. package/docs/fr/17-shadow-dom-forms.md +0 -196
  142. package/docs/fr/18-api-freeze-map.md +0 -63
  143. package/docs/fr/19-reactivity-ordering.md +0 -97
  144. package/docs/fr/20-v1-stability.md +0 -88
  145. package/docs/fr/README.md +0 -27
  146. package/docs/ru/00-the-mado-way.md +0 -84
  147. package/docs/ru/01-routing.md +0 -119
  148. package/docs/ru/02-project-layout.md +0 -84
  149. package/docs/ru/03-static-bake.md +0 -250
  150. package/docs/ru/04-ide-setup.md +0 -144
  151. package/docs/ru/05-why-mado.md +0 -193
  152. package/docs/ru/06-for-backenders.md +0 -428
  153. package/docs/ru/07-llm-pitfalls.md +0 -624
  154. package/docs/ru/08-llm-zero-history-test.md +0 -57
  155. package/docs/ru/09-shadow-vs-light-dom.md +0 -63
  156. package/docs/ru/10-app-architecture.md +0 -152
  157. package/docs/ru/11-layouts.md +0 -47
  158. package/docs/ru/12-auth-and-api.md +0 -75
  159. package/docs/ru/13-deployment.md +0 -66
  160. package/docs/ru/14-testing.md +0 -50
  161. package/docs/ru/15-error-handling.md +0 -56
  162. package/docs/ru/16-bake-cookbook.md +0 -95
  163. package/docs/ru/17-shadow-dom-forms.md +0 -193
  164. package/docs/ru/18-api-freeze-map.md +0 -64
  165. package/docs/ru/19-reactivity-ordering.md +0 -95
  166. package/docs/ru/20-v1-stability.md +0 -82
  167. package/docs/ru/README.md +0 -25
  168. package/docs/uk/00-the-mado-way.md +0 -82
  169. package/docs/uk/01-routing.md +0 -76
  170. package/docs/uk/02-project-layout.md +0 -73
  171. package/docs/uk/03-static-bake.md +0 -48
  172. package/docs/uk/04-ide-setup.md +0 -26
  173. package/docs/uk/05-why-mado.md +0 -34
  174. package/docs/uk/06-for-backenders.md +0 -55
  175. package/docs/uk/07-llm-pitfalls.md +0 -145
  176. package/docs/uk/08-llm-zero-history-test.md +0 -34
  177. package/docs/uk/09-shadow-vs-light-dom.md +0 -58
  178. package/docs/uk/10-app-architecture.md +0 -97
  179. package/docs/uk/11-layouts.md +0 -47
  180. package/docs/uk/12-auth-and-api.md +0 -70
  181. package/docs/uk/13-deployment.md +0 -40
  182. package/docs/uk/14-testing.md +0 -34
  183. package/docs/uk/15-error-handling.md +0 -32
  184. package/docs/uk/16-bake-cookbook.md +0 -36
  185. package/docs/uk/17-shadow-dom-forms.md +0 -193
  186. package/docs/uk/18-api-freeze-map.md +0 -61
  187. package/docs/uk/19-reactivity-ordering.md +0 -95
  188. package/docs/uk/20-v1-stability.md +0 -83
  189. package/docs/uk/README.md +0 -27
  190. package/starters/default/src/modules/home/not-found.page.ts +0 -11
  191. /package/docs/en/{15-error-handling.md → 21-error-handling.md} +0 -0
  192. /package/docs/en/{19-reactivity-ordering.md → 31-reactivity-ordering.md} +0 -0
  193. /package/starters/{default → modular}/.editorconfig +0 -0
  194. /package/starters/{default → modular}/eslint.config.mjs +0 -0
  195. /package/starters/{default → modular}/public/favicon.svg +0 -0
  196. /package/starters/{default → modular}/src/modules/auth/_contracts/auth-api.types.ts +0 -0
  197. /package/starters/{default → modular}/src/modules/auth/auth.connector.ts +0 -0
  198. /package/starters/{default → modular}/src/modules/auth/auth.guard.ts +0 -0
  199. /package/starters/{default → modular}/src/modules/auth/auth.public.ts +0 -0
  200. /package/starters/{default → modular}/src/modules/auth/auth.routes.ts +0 -0
  201. /package/starters/{default → modular}/src/modules/auth/auth.service.ts +0 -0
  202. /package/starters/{default → modular}/src/modules/auth/auth.types.ts +0 -0
  203. /package/starters/{default → modular}/src/modules/auth/login.page.ts +0 -0
  204. /package/starters/{default → modular}/src/modules/billing/_contracts/stripe.types.ts +0 -0
  205. /package/starters/{default → modular}/src/modules/billing/api/stripe.connector.ts +0 -0
  206. /package/starters/{default → modular}/src/modules/billing/billing.public.ts +0 -0
  207. /package/starters/{default → modular}/src/modules/billing/billing.routes.ts +0 -0
  208. /package/starters/{default → modular}/src/modules/billing/billing.types.ts +0 -0
  209. /package/starters/{default → modular}/src/modules/billing/components/invoice-status-badge.component.ts +0 -0
  210. /package/starters/{default → modular}/src/modules/billing/data/invoices.resource.ts +0 -0
  211. /package/starters/{default → modular}/src/modules/billing/pages/invoice-detail.page.ts +0 -0
  212. /package/starters/{default → modular}/src/shared/http/http-client.ts +0 -0
  213. /package/starters/{default → modular}/src/shared/http/http-error.ts +0 -0
  214. /package/starters/{default → modular}/src/shared/http/interceptors.ts +0 -0
  215. /package/starters/{default → modular}/src/shared/lib/format-date.ts +0 -0
  216. /package/starters/{default → modular}/src/shared/styles/content.css +0 -0
  217. /package/starters/{default → modular}/src/shared/styles/reset.css +0 -0
  218. /package/starters/{default → modular}/src/shared/styles/shell.css +0 -0
  219. /package/starters/{default → modular}/src/shared/styles/tokens.css +0 -0
  220. /package/starters/{default → modular}/src/shared/ui/x-button.component.ts +0 -0
  221. /package/starters/{default → modular}/src/shared/ui/x-spinner.component.ts +0 -0
@@ -1,624 +0,0 @@
1
- # Mado · LLM pitfalls
2
-
3
- > Типичные ошибки, которые AI-ассистенты (Copilot, Claude, ChatGPT, Cursor)
4
- > делают при генерации Mado-кода. И как их исправлять.
5
-
6
- Этот документ — для **двух аудиторий**:
7
-
8
- 1. **AI-агентов в IDE**, которые читают `AGENTS.md` / `.cursorrules` / `.github/copilot-instructions.md`. Здесь больше деталей по типичным граблям.
9
- 2. **Людей**, которые получили от AI код с этими ошибками и не понимают, что не так.
10
-
11
- ---
12
-
13
- ## Pitfall #1: `${signal()}` вместо `${() => signal()}`
14
-
15
- **Симптом:** значение в шаблоне отображается, но не обновляется при изменении сигнала.
16
-
17
- ```ts
18
- const count = signal(0);
19
-
20
- // ❌ AI генерирует это часто
21
- html`<div>Count: ${count() * 2}</div>`;
22
- // → Отрисует "Count: 0", и больше никогда не обновится.
23
- // count() прочитан один раз в момент создания TemplateResult.
24
-
25
- // ✅ Правильно — функция-геттер
26
- html`<div>Count: ${() => count() * 2}</div>`;
27
- // → Mado создаст effect() на эту функцию, при изменении count перерисует.
28
-
29
- // ✅ Тоже правильно — сам сигнал является функцией
30
- html`<div>Count: ${count}</div>`;
31
- ```
32
-
33
- **Правило:**
34
-
35
- - Если в `${...}` есть **выражение** (что-то делает с сигналом) — оборачивай в `() => ...`.
36
- - Если в `${...}` **сам сигнал** — можно как есть.
37
-
38
- Это работает для **child-биндингов** (текст внутри тегов) и для **value-атрибутов** (`@click`, `.prop`, `?attr`, обычных атрибутов).
39
-
40
- ---
41
-
42
- ## Pitfall #2: `<button disabled=${loading}>` вместо `?disabled`
43
-
44
- **Симптом:** кнопка не disable'ится либо disable'ится всегда.
45
-
46
- ```ts
47
- const loading = signal(false);
48
-
49
- // ❌ Это setAttribute("disabled", "false") — DOM воспринимает это как disabled
50
- html`<button disabled=${loading()}>Save</button>`;
51
-
52
- // ✅ Правильно — boolean-биндинг (toggle attribute)
53
- html`<button ?disabled=${loading}>Save</button>`;
54
- ```
55
-
56
- **Правило для атрибутов:**
57
- | Префикс | Что делает | Когда использовать |
58
- |---|---|---|
59
- | `attr=` | `setAttribute("attr", value)` | строки, числа, URL |
60
- | `.attr=` | `el.attr = value` (DOM property) | объекты, массивы, `.value` инпута |
61
- | `?attr=` | toggle attribute (по truthy) | `disabled`, `hidden`, `checked`, etc |
62
- | `@evt=` | `addEventListener("evt", fn)` | обработчики событий |
63
-
64
- ---
65
-
66
- ## Pitfall #3: useState / useEffect-стиль
67
-
68
- **Симптом:** AI генерирует React-подобный код, который не работает в Mado.
69
-
70
- ```ts
71
- // ❌ AI часто это пишет
72
- function Counter() {
73
- const [count, setCount] = useState(0);
74
- useEffect(() => { console.log(count); }, [count]);
75
- return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
76
- }
77
-
78
- // ✅ Правильно в Mado
79
- import { component, signal, effect, html } from "@madojs/mado";
80
-
81
- component("x-counter", () => {
82
- const count = signal(0);
83
- effect(() => console.log(count())); // auto-subscribe, dispose автоматически
84
- return () => html`
85
- <button @click=${() => count.update(c => c + 1)}>${count}</button>
86
- `;
87
- });
88
- ```
89
-
90
- **Ключевые отличия:**
91
-
92
- - Нет хуков, нет правил хуков.
93
- - `signal()` можно создавать где угодно — в setup, в effect, в обработчике.
94
- - `effect()` сам видит, что прочитал — не нужен dependency array.
95
- - Компонент = `component("x-name", setup)`, не JSX-функция.
96
-
97
- ---
98
-
99
- ## Pitfall #4: `useEffect(() => { ... return cleanup })`
100
-
101
- **Симптом:** AI пишет `return cleanup` в effect, ожидая что это сработает как в React.
102
-
103
- ```ts
104
- // ❌ AI пытается это написать
105
- component("x-timer", () => {
106
- effect(() => {
107
- const id = setInterval(..., 1000);
108
- return () => clearInterval(id); // НЕ сработает, нужно через onDispose
109
- });
110
- return () => html`...`;
111
- });
112
-
113
- // ✅ Правильно: cleanup через ctx.onDispose
114
- component("x-timer", (ctx) => {
115
- const id = setInterval(..., 1000);
116
- ctx.onDispose(() => clearInterval(id));
117
- return () => html`...`;
118
- });
119
- ```
120
-
121
- **Примечание:** `effect()` действительно поддерживает `return cleanup`, но это **per-run cleanup** (выполнится при следующем прогоне effect'а), а не при unmount. Для unmount-cleanup используй `ctx.onDispose`.
122
-
123
- ---
124
-
125
- ## Pitfall #5: Компонент как класс или с декоратором
126
-
127
- **Симптом:** AI генерирует Lit-style или vanilla WebComponent класс.
128
-
129
- ```ts
130
- // ❌ AI: "сделаем как в Lit"
131
- import { LitElement, html } from "lit";
132
- import { customElement, property } from "lit/decorators.js";
133
-
134
- @customElement('x-counter')
135
- class XCounter extends LitElement { ... }
136
-
137
- // ❌ AI: "сделаем как vanilla"
138
- class XCounter extends HTMLElement {
139
- connectedCallback() { ... }
140
- }
141
- customElements.define("x-counter", XCounter);
142
-
143
- // ✅ Правильно: функциональный component()
144
- import { component, html, signal } from "@madojs/mado";
145
-
146
- component("x-counter", () => {
147
- const count = signal(0);
148
- return () => html`<button @click=${() => count.update(n => n + 1)}>${count}</button>`;
149
- });
150
- ```
151
-
152
- ---
153
-
154
- ## Pitfall #6: импорт без расширения `.js`
155
-
156
- **Симптом:** TypeScript компилирует, но в браузере 404.
157
-
158
- ```ts
159
- // ❌ AI часто опускает расширение
160
- import { foo } from "./bar";
161
- import { Home } from "./pages/home";
162
-
163
- // ✅ Правильно: ES-модули в браузере требуют расширение
164
- import { foo } from "./bar.js";
165
- import { Home } from "./pages/home.js";
166
- ```
167
-
168
- **Почему `.js`, а не `.ts`:** в браузер уходит уже скомпилированный JS. TypeScript достаточно умён, чтобы понимать `./bar.js` как ссылку на `./bar.ts` при компиляции.
169
-
170
- ---
171
-
172
- ## Pitfall #7: списки через `.map()` без ключей
173
-
174
- **Симптом:** при перестановке элементов теряется фокус инпутов / ломаются CSS-анимации / тормозит на больших списках.
175
-
176
- ```ts
177
- // ❌ Работает, но не keyed: пересоздаёт DOM на каждое изменение
178
- html`<ul>
179
- ${() => items().map((t) => html`<li>${t.name}</li>`)}
180
- </ul>`;
181
-
182
- // ✅ Правильно: each() с key-функцией
183
- import { each } from "@madojs/mado";
184
- html`<ul>
185
- ${() =>
186
- each(
187
- items(),
188
- (t) => t.id,
189
- (t) => html`<li>${t.name}</li>`,
190
- )}
191
- </ul>`;
192
- ```
193
-
194
- **Правило:** всегда используй `each()` для списков из массивов с устойчивыми ID. `.map()` оставь только для статичных списков.
195
-
196
- ---
197
-
198
- ## Pitfall #8: `signal.value` или `count.get()`
199
-
200
- **Симптом:** AI пишет API в стиле Vue или Solid pre-v1.
201
-
202
- ```ts
203
- const count = signal(0);
204
-
205
- // ❌ Нет такого API
206
- count.value;
207
- count.value = 5;
208
- count.get();
209
-
210
- // ✅ Правильно
211
- count(); // прочитать
212
- count.set(5); // записать
213
- count.update((n) => n + 1);
214
- count.peek(); // прочитать без подписки
215
- ```
216
-
217
- ---
218
-
219
- ## Pitfall #9: `provide(ApiCtx, value)` без host
220
-
221
- **Симптом:** TypeError при попытке поднять контекст.
222
-
223
- ```ts
224
- // ❌ AI забывает host
225
- provide(ApiCtx, myApi);
226
- inject(ApiCtx);
227
-
228
- // ✅ Правильно: первый аргумент — host (текущий компонент)
229
- component("x-app", ({ host }) => {
230
- provide(host, ApiCtx, myApi);
231
- return () => html`...`;
232
- });
233
-
234
- component("x-child", ({ host }) => {
235
- const api = inject(host, ApiCtx); // signal<value>
236
- return () => html`...`;
237
- });
238
- ```
239
-
240
- ---
241
-
242
- ## Pitfall #10: ожидание SSR
243
-
244
- **Симптом:** AI пишет код, предполагая, что страница пререндерится на сервере.
245
-
246
- ```ts
247
- // ❌ Это работает только в браузере
248
- const userId = location.pathname.split("/")[2];
249
-
250
- // ❌ Это тоже только в браузере
251
- if (typeof window !== "undefined") { ... } // в Mado window есть ВСЕГДА
252
- ```
253
-
254
- Mado **не делает SSR с гидрацией**. На сервере код не выполняется — есть только `bake` (статический prerender на build) и edge-prerender. Оба заменяют user code на linkedom-окружение, но это **только** для генерации HTML с meta-тегами, не для выполнения логики страницы.
255
-
256
- Это значит:
257
-
258
- - ✅ `window`, `document`, `location`, `fetch` — доступны без проверок.
259
- - ❌ Не пиши код, который пытается «универсально работать на сервере и клиенте».
260
- - ❌ Не используй паттерны Next.js (`getServerSideProps`, `headers()`).
261
-
262
- ---
263
-
264
- ## Pitfall #11: `useForm()` с zod/yup-резолвером
265
-
266
- **Симптом:** AI хочет подключить валидатор.
267
-
268
- ```ts
269
- // ❌ Нет такого API
270
- const f = useForm({ resolver: zodResolver(schema) });
271
-
272
- // ✅ Правильно: HTML5-валидация атрибутами
273
- const f = useForm({
274
- email: { required: true, type: "email" },
275
- age: { required: true, type: "number", min: 18 },
276
- });
277
-
278
- // ✅ Или кастомная функция, если HTML5 не хватает
279
- const f = useForm(
280
- { name: { required: true } },
281
- {
282
- validate: (values) => {
283
- const errors: Record<string, string> = {};
284
- if (values.name && /\d/.test(values.name as string)) {
285
- errors.name = "Имя не должно содержать цифры";
286
- }
287
- return Object.keys(errors).length ? errors : null;
288
- },
289
- },
290
- );
291
- ```
292
-
293
- ---
294
-
295
- ## Pitfall #12: Tailwind / styled-components / CSS Modules
296
-
297
- **Симптом:** AI предлагает стандартные React-CSS-решения.
298
-
299
- Mado использует **Shadow DOM + `css\`\`` + CSS variables**. Глобальные UI-фреймворки (Tailwind, Bootstrap-через-классы) **работают только в light DOM** (`shadow: false`):
300
-
301
- ```ts
302
- // Light-DOM page/screen компонент, Tailwind-классы работают
303
- component(
304
- "x-admin-page",
305
- () => () => html`
306
- <section class="bg-white shadow-lg rounded-lg p-4">...</section>
307
- `,
308
- { shadow: false },
309
- );
310
-
311
- // Shadow-DOM компонент (default) — Tailwind НЕ работает.
312
- // Используй css`` или ::part() для внешней стилизации.
313
- component("x-button", () => () => html`<button><slot></slot></button>`, {
314
- styles: css`
315
- button {
316
- background: var(--button-bg, #2563eb);
317
- color: white;
318
- padding: 0.5rem 1rem;
319
- border-radius: 6px;
320
- }
321
- `,
322
- });
323
- ```
324
-
325
- **Темы и кастомизация — через CSS variables**, а не классы.
326
-
327
- ---
328
-
329
- ## Pitfall #13: `import * as Mado from "@madojs/mado"`
330
-
331
- **Симптом:** AI хочет namespace-import.
332
-
333
- Это работает, но дублирует имена и плохо tree-shake'ится. Лучше named-import:
334
-
335
- ```ts
336
- // ✅ Канонично
337
- import { signal, html, component, css, page } from "@madojs/mado";
338
-
339
- // ⚠️ Работает, но избыточно
340
- import * as Mado from "@madojs/mado";
341
- Mado.signal(0);
342
- ```
343
-
344
- ---
345
-
346
- ## Pitfall #14: попытка добавить runtime-зависимость
347
-
348
- **Симптом:** AI предлагает `npm install lodash` / `npm install date-fns` / etc.
349
-
350
- Mado — **zero runtime deps** by design. Если AI хочет добавить:
351
-
352
- - **lodash** → используй нативный JS (`Object.entries`, `Array.prototype`, `structuredClone`);
353
- - **date-fns** → используй `Intl.DateTimeFormat` и `Intl.RelativeTimeFormat`;
354
- - **uuid** → `crypto.randomUUID()`;
355
- - **axios** → нативный `fetch` + `jsonFetcher()` из Mado;
356
- - **classnames** → нативный template literal или объект-mапа.
357
-
358
- Любая runtime-зависимость — **нарушение принципа фреймворка**. Если без неё реально нельзя — добавляй в пользовательский проект, не в Mado core.
359
-
360
- ---
361
-
362
- ## Pitfall #15: inline `<style>` внутри page-шаблонов
363
-
364
- **Симптом:** AI кладёт большой `<style>` прямо в `html\`\`` страницы.
365
-
366
- ```ts
367
- // ❌ Работает, но плохо масштабируется и усложняет cleanup
368
- page({
369
- view: () => html`
370
- <style>
371
- .panel {
372
- padding: 1rem;
373
- }
374
- </style>
375
- <section class="panel">...</section>
376
- `,
377
- });
378
-
379
- // ✅ Правильно: стили компонента через css``
380
- component(
381
- "x-admin-panel",
382
- () => () => html` <section class="panel">...</section> `,
383
- {
384
- styles: css`
385
- .panel {
386
- padding: 1rem;
387
- }
388
- `,
389
- },
390
- );
391
- ```
392
-
393
- Для backend-admin route/page экранов часто уместен `shadow: false`, чтобы
394
- глобальные layout/form/table utilities работали как обычная админка. Но если
395
- layout использует `<slot>` для проекции страницы внутрь shell, оставь layout в
396
- Shadow DOM и держи shell-стили в `styles: css\`\``.
397
-
398
- ---
399
-
400
- ## Pitfall #16: Shadow DOM links без `data-link`
401
-
402
- **Симптом:** ссылка внутри Web Component перезагружает страницу или не
403
- prefetch'ится.
404
-
405
- ```ts
406
- // ❌ Обычная ссылка: браузер сделает full reload
407
- html`<a href="/tickets/42">Open</a>`;
408
-
409
- // ✅ SPA-навигация: router() перехватит click даже через Shadow DOM
410
- html`<a href="/tickets/42" data-link>Open</a>`;
411
- ```
412
-
413
- Mado ищет ссылку через `event.composedPath()`, поэтому `data-link` работает
414
- и внутри Shadow DOM. Hover-prefetch использует тот же путь; `data-no-prefetch`
415
- отключает prefetch для конкретной ссылки.
416
-
417
- ---
418
-
419
- ## Pitfall #17: `resource()` вне component setup
420
-
421
- **Симптом:** AI создаёт resource в module scope, чтобы "переиспользовать"
422
- данные между страницами.
423
-
424
- ```ts
425
- // ❌ Нет lifecycle cleanup, будет dev-warning
426
- const tickets = resource(
427
- () => "tickets",
428
- () => api.listTickets(),
429
- );
430
-
431
- component("x-tickets", () => {
432
- return () => html`${() => tickets.data()?.length ?? 0}`;
433
- });
434
-
435
- // ✅ Создавай resource внутри setup компонента
436
- component("x-tickets", () => {
437
- const tickets = resource(
438
- () => "tickets",
439
- () => api.listTickets(),
440
- );
441
- return () => html`${() => tickets.data()?.length ?? 0}`;
442
- });
443
- ```
444
-
445
- Так подписки на invalidation, abort controller и effects будут очищены при
446
- disconnect компонента.
447
-
448
- ---
449
-
450
- ## Pitfall #18: предположение, что nested templates не требуют cleanup
451
-
452
- **Симптом:** AI собирает route outlet или conditional UI из вложенных
453
- `TemplateResult`, а потом старые элементы продолжают жить ниже новой страницы.
454
-
455
- ```ts
456
- const view = signal(html`<x-home></x-home>`);
457
-
458
- // ✅ Нормальный паттерн: вложенный TemplateResult можно возвращать из child-binding
459
- html`${view}`;
460
- ```
461
-
462
- Начиная с v0.3 это закреплено регрессиями: при замене child-binding Mado
463
- dispose'ит вложенные template instances/effects рекурсивно. Если видишь
464
- накопление страниц в `#app`, это баг ядра, а не "нужно руками чистить DOM".
465
-
466
- ---
467
-
468
- ## Pitfall #19: global CSS utilities внутри Shadow DOM
469
-
470
- **Симптом:** страница выглядит “без стилей”: `.page-head`, `.btn`,
471
- `.form-grid`, `.metric-grid` не применяются.
472
-
473
- ```ts
474
- // ❌ .page-head объявлен глобально, но x-dashboard по умолчанию Shadow DOM
475
- component(
476
- "x-dashboard",
477
- () => () => html`
478
- <header class="page-head">...</header>
479
- <div class="metric-grid">...</div>
480
- `,
481
- );
482
-
483
- // ✅ Page/layout/admin-shell компоненты часто должны быть Light DOM
484
- component(
485
- "x-dashboard",
486
- () => () => html`
487
- <header class="page-head">...</header>
488
- <div class="metric-grid">...</div>
489
- `,
490
- { shadow: false },
491
- );
492
- ```
493
-
494
- Правило: Shadow DOM — для leaf widgets и slot-based layouts, Light DOM — для
495
- route/page/admin-screen компонентов, которые намеренно используют общие
496
- layout/form/table utilities. Не забывай: `<slot>` проецирует детей только в
497
- Shadow DOM; при `shadow: false` это обычный элемент.
498
- Подробнее: [`09-shadow-vs-light-dom.md`](./09-shadow-vs-light-dom.md).
499
-
500
- ---
501
-
502
- ## Pitfall #20: `host.getAttribute()` в render = не реактивно
503
-
504
- **Симптом:** внешний вид компонента не обновляется при изменении атрибута родителем.
505
-
506
- ```ts
507
- // ❌ host.getAttribute() в render-функции читается один раз, но
508
- // render перезапускается только при изменении его собственных сигналов.
509
- // Внешние изменения атрибута не триггерят перерисовку.
510
- component("x-badge", ({ host }) => () => {
511
- const variant = host.getAttribute("variant") ?? "default";
512
- return html`<span class=${variant}>...</span>`;
513
- });
514
-
515
- // ✅ Правильно: ctx.attr() — возвращает реактивный Signal<string>
516
- component("x-badge", ({ attr }) => {
517
- const variant = attr("variant", "default");
518
- return () => html`<span class=${() => `badge-${variant()}`}>...</span>`;
519
- });
520
- ```
521
-
522
- **Правило:** никогда не читайте `host.getAttribute()` или `host.hasAttribute()` внутри
523
- render-функции для значений, которые могут измениться снаружи. Используйте `ctx.attr()` —
524
- он возвращает Signal, который автоматически обновляется через `attributeChangedCallback`.
525
-
526
- ---
527
-
528
- ## Pitfall #21: Shadow DOM `<button>` не сабмитит формы
529
-
530
- **Симптом:** клик по `<x-button type="submit">` внутри `<form>` ничего не делает.
531
-
532
- `<button>` внутри Shadow DOM не участвует в алгоритме form-owner для
533
- `<form>` в Light DOM — это ограничение спецификации, не баг Mado.
534
-
535
- ```ts
536
- // ❌ Внутренняя <button type="submit"> не может триггерить родительскую <form>
537
- component("x-button", ({ host }) => {
538
- return () => html`<button type="submit"><slot></slot></button>`;
539
- });
540
-
541
- // ✅ Мост через requestSubmit()
542
- component("x-button", ({ host, attr }) => {
543
- const disabled = attr("disabled");
544
-
545
- const handleClick = () => {
546
- const typeAttr = host.getAttribute("type");
547
- if (typeAttr === "button" || typeAttr === "reset") return;
548
- const form = host.closest("form");
549
- if (form && !host.hasAttribute("disabled")) form.requestSubmit();
550
- };
551
-
552
- return () => html`
553
- <button ?disabled=${() => disabled() !== ""} @click=${handleClick}>
554
- <slot></slot>
555
- </button>
556
- `;
557
- });
558
- ```
559
-
560
- Подробнее: [`17-shadow-dom-forms.md`](./17-shadow-dom-forms.md).
561
-
562
- ---
563
-
564
- ## Pitfall #22: `useForm()` с Shadow DOM кастомными input
565
-
566
- **Симптом:** `form.onInput` получает `undefined` для name/value от `<x-input>`.
567
-
568
- Когда Shadow DOM input диспатчит `input` событие, браузер ретаргетирует
569
- `e.target` с внутреннего `<input>` на host `<x-input>`. Но `<x-input>`
570
- (HTMLElement) не имеет `.name` или `.value` — поэтому `useForm` ничего не получает.
571
-
572
- ```ts
573
- // ❌ Нет proxy-свойств — useForm тихо игнорирует события
574
- component("x-input", ({ host, attr }) => {
575
- const name = attr("name", "");
576
- return () => html`<input name=${name} />`;
577
- });
578
-
579
- // ✅ Добавить proxy-свойства для совместимости с useForm
580
- component("x-input", ({ host, attr }) => {
581
- const name = attr("name", "");
582
-
583
- Object.defineProperty(host, "name", {
584
- get: () => host.getAttribute("name") ?? "",
585
- configurable: true,
586
- });
587
- Object.defineProperty(host, "value", {
588
- get: () => host.shadowRoot?.querySelector("input")?.value ?? "",
589
- configurable: true,
590
- });
591
-
592
- return () => html`<input name=${name} />`;
593
- });
594
- ```
595
-
596
- Подробнее: [`17-shadow-dom-forms.md`](./17-shadow-dom-forms.md).
597
-
598
- ---
599
-
600
- ## Cheat-sheet для AI
601
-
602
- | Если хочешь сделать… | Правильно в Mado |
603
- | ------------------------------------- | ------------------------------------------- |
604
- | `useState(0)` | `signal(0)` |
605
- | `useEffect(() => {...}, [a, b])` | `effect(() => {...})` (auto-deps) |
606
- | `useEffect(() => return cleanup, [])` | `ctx.onDispose(cleanup)` |
607
- | `useMemo(() => x, [a])` | `computed(() => x)` |
608
- | `useCallback(fn, [])` | обычная функция |
609
- | `useContext(Ctx)` | `inject(host, Ctx)` |
610
- | `useQuery(['key'], fn)` | `resource(() => 'key', fn)` |
611
- | `useMutation(fn)` | `mutation(fn, { invalidates: [...] })` |
612
- | `useRouter().push('/')` | `navigate('/')` |
613
- | `useRouter().query.q` | `queryParam('q')` |
614
- | `<input value={v} onChange={...}>` | `<input .value=${v} @input=${...}>` |
615
- | `{items.map(x => ...)}` | `${() => each(items, x => x.id, x => ...)}` |
616
- | `useForm({ resolver: zodResolver })` | `useForm({...}, { validate: (v) => ... })` |
617
- | `class extends HTMLElement` | `component('x-name', setup)` |
618
- | `@customElement('x')` | `component('x-name', setup)` |
619
- | `host.getAttribute('x')` в render | `ctx.attr('x', default)` (реактивно) |
620
- | `jsonFetcher()` с авторизацией | `apiFetcher()` (прикрепляет Bearer токен) |
621
- | `setInterval` в page view | `onDispose(() => clearInterval(id))` |
622
- | чтение сигнала в async init view() | `untracked(() => cursor())` |
623
-
624
- Если что-то не подходит из этого списка — открой `src/` и **прочитай 500 строк**. Это серьёзно. Mado специально маленький, чтобы быть читаемым.
@@ -1,57 +0,0 @@
1
- # Тест LLM с нулевой историей
2
-
3
- Этот документ определяет практический валидационный тест для Mado.
4
-
5
- Вопрос не в том, «может ли LLM генерировать фронтенд-код?» — может. Вопрос в том:
6
- может ли свежая LLM написать идиоматичный Mado-код без скатывания в React-подобные
7
- паттерны?
8
-
9
- ## Допустимый контекст
10
-
11
- При первом проходе предоставьте агенту только:
12
-
13
- - `AGENTS.md`
14
- - `README.md`
15
- - `docs/en/07-llm-pitfalls.md`
16
- - файлы из внешнего workspace `madojs-examples` только если агент сам попросит
17
- паттерн более крупного приложения
18
-
19
- Агент может искать целевые API в `src/` когда заблокирован, но не должен
20
- загружать весь фреймворк в контекст.
21
-
22
- ## Задание
23
-
24
- Построить маленький ticket-admin SPA для соло/бекенд-разработчика.
25
-
26
- Требуемое поведение:
27
-
28
- - маршруты: `/`, `/tickets`, `/tickets/new`, `/tickets/:id`, `*`;
29
- - in-memory mock API с реалистичными асинхронными задержками;
30
- - страница списка с `resource()`, `queryParam()` фильтрами поиска/статуса,
31
- `computed()` и `each()` с ключами по строкам;
32
- - сценарии создания и редактирования с `useForm()` + `mutation()` + `invalidates`;
33
- - локальный UI-стейт через `signal()`;
34
- - slotted shell, metric и badge компоненты для более реалистичного admin UI;
35
- - smoke-тест, импортирующий собранный пример.
36
-
37
- ## Чеклист ошибок
38
-
39
- Ищите следующее после реализации:
40
-
41
- - JSX, `useState`, `useEffect`, `ref`, `$state` или class-style компоненты;
42
- - `${signal()}` или `${signal() + 1}` где требуется реактивный child thunk;
43
- - `disabled=${...}` вместо `?disabled=${...}`;
44
- - динамические списки, отрендеренные через unkeyed `.map()` вместо `each()`;
45
- - ESM-импорты в браузере без `.js`;
46
- - `resource()`, созданный вне component setup;
47
- - новые runtime-зависимости или новые публичные API фреймворка.
48
-
49
- ## Заметки по результатам
50
-
51
- Историческая реализация tickets живет во внешнем workspace `madojs-examples`.
52
- Core-репозиторий больше не поставляет этот артефакт и отдельную smoke-команду;
53
- используйте этот документ как ручной сценарий оценки при обновлении LLM-guidance.
54
-
55
- Основная болевая точка в документации остается lifecycle: примеры не должны
56
- создавать впечатление, что long-lived `resource()` можно случайно держать на
57
- module scope или в route-коде без cleanup.