@madojs/mado 0.5.0 → 0.6.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 (122) hide show
  1. package/AGENTS.md +49 -1
  2. package/CHANGELOG.md +188 -0
  3. package/MADO_V1_PLAN.md +179 -0
  4. package/README.md +53 -14
  5. package/ROADMAP.md +36 -5
  6. package/TODO.md +72 -0
  7. package/dist/src/forms.d.ts +41 -7
  8. package/dist/src/forms.js +334 -59
  9. package/dist/src/forms.js.map +1 -1
  10. package/dist/src/html/bindings.d.ts +41 -0
  11. package/dist/src/html/bindings.js +163 -6
  12. package/dist/src/html/bindings.js.map +1 -1
  13. package/dist/src/html.d.ts +2 -0
  14. package/dist/src/html.js +1 -0
  15. package/dist/src/html.js.map +1 -1
  16. package/dist/src/index.d.ts +6 -6
  17. package/dist/src/index.js +2 -2
  18. package/dist/src/index.js.map +1 -1
  19. package/dist/src/page.d.ts +56 -0
  20. package/dist/src/page.js +17 -0
  21. package/dist/src/page.js.map +1 -1
  22. package/dist/src/router/manifest.d.ts +16 -1
  23. package/dist/src/router/manifest.js +181 -38
  24. package/dist/src/router/manifest.js.map +1 -1
  25. package/dist/src/router/match.d.ts +7 -2
  26. package/dist/src/router/match.js +14 -4
  27. package/dist/src/router/match.js.map +1 -1
  28. package/dist/src/router/navigation.d.ts +10 -0
  29. package/dist/src/router/navigation.js +73 -12
  30. package/dist/src/router/navigation.js.map +1 -1
  31. package/dist/src/signal.d.ts +15 -1
  32. package/dist/src/signal.js +112 -16
  33. package/dist/src/signal.js.map +1 -1
  34. package/docs/en/02-project-layout.md +99 -40
  35. package/docs/en/05-why-mado.md +1 -1
  36. package/docs/en/06-for-backenders.md +1 -1
  37. package/docs/en/07-llm-pitfalls.md +1 -1
  38. package/docs/en/09-shadow-vs-light-dom.md +60 -0
  39. package/docs/en/10-app-architecture.md +141 -0
  40. package/docs/en/11-layouts.md +115 -0
  41. package/docs/en/12-auth-and-api.md +217 -0
  42. package/docs/en/13-deployment.md +192 -0
  43. package/docs/en/14-testing.md +82 -0
  44. package/docs/en/15-error-handling.md +100 -0
  45. package/docs/en/16-bake-cookbook.md +93 -0
  46. package/docs/en/README.md +7 -0
  47. package/docs/fr/05-why-mado.md +1 -1
  48. package/docs/fr/06-for-backenders.md +1 -1
  49. package/docs/fr/07-llm-pitfalls.md +1 -1
  50. package/docs/fr/09-shadow-vs-light-dom.md +63 -0
  51. package/docs/fr/10-app-architecture.md +61 -0
  52. package/docs/fr/11-layouts.md +35 -0
  53. package/docs/fr/12-auth-and-api.md +35 -0
  54. package/docs/fr/13-deployment.md +39 -0
  55. package/docs/fr/14-testing.md +41 -0
  56. package/docs/fr/15-error-handling.md +50 -0
  57. package/docs/fr/16-bake-cookbook.md +35 -0
  58. package/docs/fr/README.md +7 -0
  59. package/docs/ru/05-why-mado.md +2 -2
  60. package/docs/ru/06-for-backenders.md +1 -1
  61. package/docs/ru/09-shadow-vs-light-dom.md +60 -0
  62. package/docs/ru/10-app-architecture.md +100 -0
  63. package/docs/ru/11-layouts.md +47 -0
  64. package/docs/ru/12-auth-and-api.md +53 -0
  65. package/docs/ru/13-deployment.md +60 -0
  66. package/docs/ru/14-testing.md +50 -0
  67. package/docs/ru/15-error-handling.md +56 -0
  68. package/docs/ru/16-bake-cookbook.md +55 -0
  69. package/docs/ru/README.md +7 -0
  70. package/docs/uk/06-for-backenders.md +2 -2
  71. package/docs/uk/09-shadow-vs-light-dom.md +91 -24
  72. package/docs/uk/10-app-architecture.md +56 -0
  73. package/docs/uk/11-layouts.md +34 -0
  74. package/docs/uk/12-auth-and-api.md +34 -0
  75. package/docs/uk/13-deployment.md +39 -0
  76. package/docs/uk/14-testing.md +34 -0
  77. package/docs/uk/15-error-handling.md +32 -0
  78. package/docs/uk/16-bake-cookbook.md +36 -0
  79. package/docs/uk/README.md +7 -0
  80. package/llms.txt +24 -1
  81. package/package.json +3 -1
  82. package/scripts/_config.mjs +224 -0
  83. package/scripts/bake.mjs +217 -120
  84. package/scripts/bundle.mjs +110 -67
  85. package/scripts/cli.mjs +127 -16
  86. package/scripts/preview.mjs +22 -12
  87. package/server/serve.mjs +101 -11
  88. package/starters/admin/README.md +63 -0
  89. package/starters/admin/index.html +21 -0
  90. package/starters/admin/mado.config.json +22 -0
  91. package/starters/admin/package.json +22 -0
  92. package/starters/admin/public/favicon.svg +4 -0
  93. package/starters/admin/src/components/x-button.ts +55 -0
  94. package/starters/admin/src/components/x-input.ts +74 -0
  95. package/starters/admin/src/layouts/app.ts +101 -0
  96. package/starters/admin/src/layouts/auth.ts +41 -0
  97. package/starters/admin/src/lib/api.ts +133 -0
  98. package/starters/admin/src/lib/auth.ts +83 -0
  99. package/starters/admin/src/main.ts +15 -0
  100. package/starters/admin/src/pages/admin/dashboard.ts +48 -0
  101. package/starters/admin/src/pages/admin/order-detail.ts +78 -0
  102. package/starters/admin/src/pages/admin/orders.ts +117 -0
  103. package/starters/admin/src/pages/home.ts +25 -0
  104. package/starters/admin/src/pages/login.ts +70 -0
  105. package/starters/admin/src/pages/not-found.ts +12 -0
  106. package/starters/admin/src/routes.ts +40 -0
  107. package/starters/admin/src/styles/global.ts +86 -0
  108. package/starters/admin/tsconfig.json +15 -0
  109. package/starters/crud/README.md +14 -2
  110. package/starters/crud/mado.config.json +20 -0
  111. package/starters/crud/package.json +9 -4
  112. package/starters/crud/src/components/app-shell.ts +13 -8
  113. package/starters/crud/src/main.ts +1 -4
  114. package/starters/crud/src/pages/ticket-detail.ts +1 -0
  115. package/starters/crud/src/pages/ticket-new.ts +1 -0
  116. package/starters/crud/src/pages/tickets.ts +1 -0
  117. package/starters/crud/src/routes.ts +4 -2
  118. package/starters/minimal/README.md +4 -2
  119. package/starters/minimal/mado.config.json +20 -0
  120. package/starters/minimal/package.json +8 -3
  121. package/starters/minimal/src/components/app-counter.ts +1 -1
  122. package/starters/minimal/src/routes.ts +4 -2
@@ -0,0 +1,35 @@
1
+ # Bake cookbook
2
+
3
+ `mado bake` rend certaines routes en HTML statique. C'est pour le SEO et un
4
+ premier rendu rapide, pas pour SSR + hydration.
5
+
6
+ ```ts
7
+ export default page({
8
+ head: () => ({ title: "Products", description: "Catalog" }),
9
+ view: ({ data }) => html`
10
+ <main>
11
+ <h1>Products</h1>
12
+ ${data.products.map((p) => html`<article><h2>${p.name}</h2></article>`)}
13
+ </main>
14
+ `,
15
+ bake: {
16
+ paths: () => [{}],
17
+ data: async () => ({ products: await api.products() }),
18
+ revalidate: 3600,
19
+ },
20
+ });
21
+ ```
22
+
23
+ Dans les vues baked, préfère les tableaux simples (`items.map(...)`). Les
24
+ directives runtime comme `each()` sont pour le navigateur.
25
+
26
+ ```ts
27
+ export const manifest = {
28
+ "/": () => import("./pages/home.js"),
29
+ "/blog/:slug": () => import("./pages/blog-post.js"),
30
+ };
31
+ export default routes(manifest);
32
+ ```
33
+
34
+ `mado release` produit l'artefact déployable dans `out/`. Si bake signale une
35
+ valeur non supportée, remplace la partie runtime-only ou rends-la côté client.
package/docs/fr/README.md CHANGED
@@ -14,3 +14,10 @@ Documentation française.
14
14
  | LLM pitfalls | [07-llm-pitfalls.md](./07-llm-pitfalls.md) |
15
15
  | LLM zero-history test | [08-llm-zero-history-test.md](./08-llm-zero-history-test.md) |
16
16
  | Shadow DOM vs Light DOM | [09-shadow-vs-light-dom.md](./09-shadow-vs-light-dom.md) |
17
+ | Architecture d'application | [10-app-architecture.md](./10-app-architecture.md) |
18
+ | Layouts | [11-layouts.md](./11-layouts.md) |
19
+ | Auth et API | [12-auth-and-api.md](./12-auth-and-api.md) |
20
+ | Déploiement | [13-deployment.md](./13-deployment.md) |
21
+ | Tests | [14-testing.md](./14-testing.md) |
22
+ | Gestion des erreurs | [15-error-handling.md](./15-error-handling.md) |
23
+ | Bake cookbook | [16-bake-cookbook.md](./16-bake-cookbook.md) |
@@ -33,7 +33,7 @@ Mado — не «убийца» React/Vue/Svelte. Это узкоспециали
33
33
  | Реактивность | декораторы `@property` + ручной `requestUpdate` | сигналы (`signal`/`computed`/`effect`) из коробки |
34
34
  | Роутер | нет, нужно искать (`@lit-labs/router`, etc) | в комплекте: `routes()` + nested + prefetch + sync-cache |
35
35
  | Data fetching | нет, нужно собирать | `resource()` + `mutation()` + glob-инвалидация |
36
- | Формы | нет | `useForm()` на нативной HTML5-валидации |
36
+ | Формы | нет | `useForm()` с HTML-like constraints |
37
37
  | SEO / static | сложно (`@lit-labs/ssr`) | `bake` (linkedom) + edge-prerender |
38
38
  | Билд | нужен esbuild/rollup/webpack | хватает `tsc` |
39
39
  | Стиль кода | классы + декораторы | функции + tagged templates |
@@ -190,4 +190,4 @@ Mado — это **узкий** инструмент с честным позиц
190
190
 
191
191
  Если хоть один пункт не про вас — берите альтернативу из таблицы выше. Не миритесь с инструментом, который не подходит.
192
192
 
193
- — Автор Mado, бывший React-разработчик, перешедший на бекенд и теперь склеивающий фронт в свободное время.
193
+ — Автор Mado, бывший React-разработчик, перешедший на бекенд и теперь склеивающий фронт в свободное время.
@@ -188,7 +188,7 @@ html`<x-counter></x-counter>`
188
188
 
189
189
  ## Формы — как `form.Validate()` на бекенде
190
190
 
191
- Mado использует **нативную HTML5-валидацию**, plus добавляет state-tracking.
191
+ Mado использует **schema-based validation в духе HTML constraints**, плюс добавляет state-tracking.
192
192
 
193
193
  ```ts
194
194
  import { useForm } from "@madojs/mado";
@@ -6,6 +6,19 @@ an application.
6
6
 
7
7
  ## Rule of Thumb
8
8
 
9
+ В Mado layout — это тоже component. Если файл описывает видимую переиспользуемую
10
+ часть UI-дерева — app shell, sidebar, modal, table, page section — по умолчанию
11
+ делайте Web Component через `component()`.
12
+
13
+ Обычные функции оставляйте для маленьких inline helpers:
14
+
15
+ ```ts
16
+ const money = (value: number) => html`<span>${formatMoney(value)}</span>`;
17
+ ```
18
+
19
+ Не стоит делать app shell функцией в публичных примерах. Это работает, но
20
+ прячет browser model вместо того, чтобы ее объяснять.
21
+
9
22
  Use **Shadow DOM** for leaf widgets:
10
23
 
11
24
  - buttons, badges, cards, metrics;
@@ -22,6 +35,15 @@ global CSS utilities:
22
35
  - components that intentionally share global layout, form and table utilities;
23
36
  - places where children should simply remain normal document DOM.
24
37
 
38
+ Use **Shadow DOM** для slot-based layouts:
39
+
40
+ - app shells с `<slot>`;
41
+ - sidebar/content wrappers;
42
+ - reusable layout frames, которые владеют своим grid/header/sidebar CSS.
43
+
44
+ `<slot>` — это feature Shadow DOM. В компоненте с `shadow: false` тег `<slot>`
45
+ становится обычным DOM-элементом и не переносит children в это место layout.
46
+
25
47
  ## The Footgun
26
48
 
27
49
  Global CSS does not cross a Shadow DOM boundary.
@@ -90,6 +112,18 @@ component("x-toast-stack", setup);
90
112
  This gives backend-admin screens predictable CSS while preserving encapsulation
91
113
  for reusable widgets and slot-based shells.
92
114
 
115
+ Import model специально browser-native:
116
+
117
+ ```ts
118
+ import "./components/app-layout.js";
119
+
120
+ render(html`<x-app-layout>${router.view}</x-app-layout>`, app);
121
+ ```
122
+
123
+ Import регистрирует custom element через `customElements.define()`. Template
124
+ создает `<x-app-layout>` element. Дальше браузер сам связывает тег с классом
125
+ компонента. Тут нет React-style component value, который передается как функция.
126
+
93
127
  If a layout does not need slot projection and should be styled entirely by
94
128
  global CSS, `shadow: false` can still be a good choice. If it contains
95
129
  `<slot>`, keep Shadow DOM and put the shell styles in that component.
@@ -107,6 +141,32 @@ component("x-card-link", () => () => html`
107
141
 
108
142
  The link can be in Shadow DOM; navigation still stays SPA.
109
143
 
144
+ ## Где импортировать компоненты
145
+
146
+ Custom elements становятся глобальными после регистрации, но регистрация все
147
+ равно остается явным JavaScript import.
148
+
149
+ ```ts
150
+ // main.ts: global app frame
151
+ import "./components/app-shell.js";
152
+
153
+ // pages/tickets.ts: component, которым владеет эта page
154
+ import "../components/ticket-list.js";
155
+ ```
156
+
157
+ Браузер **не** скачивает `ticket-list.js` только потому, что увидел
158
+ `<ticket-list>`. Файл должен быть где-то импортирован. После import он вызывает
159
+ `customElements.define(...)`, и тег становится известен текущему document.
160
+
161
+ Не стоит bulk-import всех компонентов в `main.ts` "just in case". Для маленьких
162
+ demo это работает, но прячет ownership и ломает lazy route loading. Лучше:
163
+
164
+ - global app shell/providers импортировать в `main.ts`;
165
+ - components, которыми владеет одна page, импортировать в этой page;
166
+ - shared components feature-а импортировать в feature entry page;
167
+ - truly global leaf components импортировать в `main.ts` только если они реально
168
+ используются везде.
169
+
110
170
  ## Showcase Lesson
111
171
 
112
172
  `examples/showcase` uses this split deliberately:
@@ -0,0 +1,100 @@
1
+ # Архитектура приложения
2
+
3
+ Базовая форма production-приложения на Mado должна быть скучной: один route
4
+ manifest, один shell, один API-клиент, один auth-модуль и страницы, которые
5
+ импортируют свои компоненты.
6
+
7
+ ## Структура
8
+
9
+ ```txt
10
+ src/
11
+ ├── main.ts
12
+ ├── routes.ts
13
+ ├── layouts/
14
+ │ ├── app.ts
15
+ │ └── auth.ts
16
+ ├── pages/
17
+ │ ├── home.ts
18
+ │ ├── login.ts
19
+ │ ├── not-found.ts
20
+ │ └── admin/
21
+ ├── components/
22
+ ├── lib/
23
+ │ ├── api.ts
24
+ │ └── auth.ts
25
+ └── styles/
26
+ ```
27
+
28
+ `lib/` хранит бизнес-логику, `layouts/` оборачивает группы роутов,
29
+ `components/` хранит переиспользуемые UI-теги, а `pages/` содержит один файл
30
+ на страницу.
31
+
32
+ ## Entry point
33
+
34
+ ```ts
35
+ import { html, render } from "@madojs/mado";
36
+ import "./styles/global.js";
37
+ import "./components/x-button.js";
38
+ import routesApi from "./routes.js";
39
+
40
+ render(html`${routesApi.view}`, document.getElementById("app")!);
41
+ ```
42
+
43
+ В `main.ts` импортируй только глобальные провайдеры, стили и маленькие общие
44
+ компоненты. Feature-компоненты импортирует страница, которая их рендерит.
45
+
46
+ ## Routes
47
+
48
+ ```ts
49
+ import { layout, routes } from "@madojs/mado";
50
+ import { requireAuth } from "./lib/auth.js";
51
+
52
+ export const manifest = {
53
+ "/": () => import("./pages/home.js"),
54
+ "/login": layout({
55
+ layout: () => import("./layouts/auth.js"),
56
+ routes: { "/": () => import("./pages/login.js") },
57
+ }),
58
+ "/admin": layout({
59
+ layout: () => import("./layouts/app.js"),
60
+ guard: requireAuth,
61
+ routes: {
62
+ "/": () => import("./pages/admin/dashboard.js"),
63
+ "/orders": () => import("./pages/admin/orders.js"),
64
+ },
65
+ }),
66
+ "*": () => import("./pages/not-found.js"),
67
+ };
68
+
69
+ export default routes(manifest);
70
+ ```
71
+
72
+ `export const manifest` нужен для `mado bake`.
73
+
74
+ ## API, forms, release
75
+
76
+ Держи один API-клиент и один auth-модуль. Для списков используй `resource()`,
77
+ для записей `mutation(..., { invalidates })`, для форм `useForm()`.
78
+
79
+ ```ts
80
+ const form = useForm({
81
+ email: { required: true, type: "email" },
82
+ "items.*.title": { required: true },
83
+ });
84
+ const items = form.array("items");
85
+ ```
86
+
87
+ Разработка:
88
+
89
+ ```bash
90
+ mado dev
91
+ ```
92
+
93
+ Продакшен:
94
+
95
+ ```bash
96
+ mado release
97
+ rsync -avz out/ user@server:/var/www/app/
98
+ ```
99
+
100
+ Деплоится только `out/`. `dist/` — внутренний результат `tsc`.
@@ -0,0 +1,47 @@
1
+ # Layouts
2
+
3
+ В Mado blessed-способ для layout — nested route group в `routes.ts`.
4
+ Не заворачивай каждую страницу вручную и не клади общий shell в `main.ts`,
5
+ если в приложении есть разные зоны вроде public/login/admin.
6
+
7
+ ```ts
8
+ import { layout, routes } from "@madojs/mado";
9
+ import { requireAuth } from "./lib/auth.js";
10
+
11
+ export const manifest = {
12
+ "/": () => import("./pages/home.js"),
13
+ "/login": layout({
14
+ layout: () => import("./layouts/auth.js"),
15
+ routes: { "/": () => import("./pages/login.js") },
16
+ }),
17
+ "/admin": layout({
18
+ layout: () => import("./layouts/app.js"),
19
+ guard: requireAuth,
20
+ routes: {
21
+ "/": () => import("./pages/admin/dashboard.js"),
22
+ "/orders": () => import("./pages/admin/orders.js"),
23
+ },
24
+ }),
25
+ "*": () => import("./pages/not-found.js"),
26
+ };
27
+
28
+ export default routes(manifest);
29
+ ```
30
+
31
+ Layout — это обычная `page({ view })`, которая рендерит `child`:
32
+
33
+ ```ts
34
+ export default page({
35
+ view: ({ child }) => html`<x-app-shell>${child}</x-app-shell>`,
36
+ });
37
+ ```
38
+
39
+ Правила:
40
+
41
+ - один shell на группу, не на каждую страницу;
42
+ - outer layouts оборачивают inner layouts;
43
+ - guard на группе защищает всю поддеревянную часть;
44
+ - layout можно lazy-load через `() => import(...)`.
45
+
46
+ Single-shell wrapper в `main.ts` допустим только для приложений, где абсолютно
47
+ все роуты живут в одной оболочке.
@@ -0,0 +1,53 @@
1
+ # Auth and API
2
+
3
+ Mado не навязывает auth, но starter `admin` дает blessed recipe:
4
+
5
+ - `src/lib/api.ts` — один HTTP boundary, `ApiError`, refresh-on-401;
6
+ - `src/lib/auth.ts` — `accessToken`, `restoreSession()`, `login()`,
7
+ `logout()`, `requireAuth` guard.
8
+
9
+ Модель:
10
+
11
+ - access token хранится в памяти через `signal`, не в `localStorage`;
12
+ - HttpOnly refresh cookie восстанавливает сессию после reload;
13
+ - все запросы идут через один API-клиент;
14
+ - protected routes закрываются group guard.
15
+
16
+ ```ts
17
+ export const requireAuth: Guard = async ({ path }) => {
18
+ if (accessToken()) return;
19
+ if (await restoreSession()) return;
20
+ return { redirect: `/login?return=${encodeURIComponent(path)}`, replace: true };
21
+ };
22
+ ```
23
+
24
+ В route manifest:
25
+
26
+ ```ts
27
+ "/admin": layout({
28
+ layout: () => import("./layouts/app.js"),
29
+ guard: requireAuth,
30
+ routes: { "/": () => import("./pages/admin/dashboard.js") },
31
+ }),
32
+ ```
33
+
34
+ Backend contract по умолчанию:
35
+
36
+ | Endpoint | Response | Notes |
37
+ |---|---|---|
38
+ | `POST /api/auth/login` | `{ accessToken }` | ставит HttpOnly refresh cookie |
39
+ | `POST /api/auth/refresh` | `{ accessToken }` | читает refresh cookie |
40
+ | `POST /api/auth/logout` | `204` | очищает cookie |
41
+
42
+ Для dev proxy:
43
+
44
+ ```jsonc
45
+ {
46
+ "dev": {
47
+ "proxy": { "/api": "http://localhost:3000" }
48
+ }
49
+ }
50
+ ```
51
+
52
+ Если backend использует другую схему auth, меняй только `api.ts`/`auth.ts`.
53
+ Страницы должны оставаться невинными.
@@ -0,0 +1,60 @@
1
+ # Deployment
2
+
3
+ Один command, один artifact:
4
+
5
+ ```bash
6
+ mado release
7
+ ```
8
+
9
+ Результат:
10
+
11
+ ```txt
12
+ out/
13
+ ├── index.html
14
+ ├── assets/
15
+ ├── baked/
16
+ ├── _redirects
17
+ └── _headers
18
+ ```
19
+
20
+ `out/` можно деплоить на nginx, Cloudflare Pages, Netlify, S3/CloudFront или
21
+ GitHub Pages.
22
+
23
+ ## Preview
24
+
25
+ ```bash
26
+ mado release
27
+ mado preview
28
+ ```
29
+
30
+ `mado preview` сервит `out/` как статический хост: baked HTML имеет приоритет,
31
+ а неизвестные пути падают в SPA fallback.
32
+
33
+ ## VPS + nginx
34
+
35
+ ```bash
36
+ mado release
37
+ rsync -avz --delete out/ user@server:/var/www/myapp/
38
+ ```
39
+
40
+ В репозитории есть production `nginx.conf`: hashed bundles кешируются
41
+ immutably, HTML идет с `no-cache`, deep links работают через SPA fallback.
42
+
43
+ ## Cloudflare / Netlify
44
+
45
+ ```bash
46
+ mado release
47
+ npx wrangler pages deploy out --project-name=myapp
48
+ ```
49
+
50
+ `_redirects` и `_headers` генерируются автоматически.
51
+
52
+ ## Cache rules
53
+
54
+ | Path | Cache-Control |
55
+ |---|---|
56
+ | `/assets/main-*.js` | `public, max-age=31536000, immutable` |
57
+ | `/*.html` | `no-cache, must-revalidate` |
58
+ | other static files | `public, max-age=86400` |
59
+
60
+ Если hard refresh на deep link дает 404, проблема в fallback настройке хоста.
@@ -0,0 +1,50 @@
1
+ # Тестирование
2
+
3
+ Mado — это обычный TypeScript и browser APIs. В репозитории фреймворка тесты
4
+ идут через Node test runner и `linkedom`.
5
+
6
+ ## Команды
7
+
8
+ ```bash
9
+ npm run typecheck
10
+ npm run build
11
+ npm test
12
+ ```
13
+
14
+ Перед merge/release прогоняй все три.
15
+
16
+ ## DOM-тест
17
+
18
+ ```js
19
+ import test from "node:test";
20
+ import assert from "node:assert/strict";
21
+
22
+ const { parseHTML } = await import("linkedom");
23
+ const { window } = parseHTML("<!doctype html><html><body></body></html>");
24
+ globalThis.window = window;
25
+ globalThis.document = window.document;
26
+ globalThis.Node = window.Node;
27
+ globalThis.HTMLElement = window.HTMLElement;
28
+
29
+ const { html, render } = await import("../dist/src/html.js");
30
+
31
+ test("renders a value", () => {
32
+ const root = document.createElement("div");
33
+ render(html`<p>${"hello"}</p>`, root);
34
+ assert.equal(root.querySelector("p").textContent, "hello");
35
+ });
36
+ ```
37
+
38
+ Сначала `npm run build`, потом импорт из `dist/`.
39
+
40
+ ## Что покрывать
41
+
42
+ - signals/computed/effect: scheduling и cleanup;
43
+ - template bindings: children, attrs, events, directives;
44
+ - routes: guards, redirects, scroll/focus, error boundaries;
45
+ - forms: sync/async validation, races, field arrays;
46
+ - resources/mutations: cache keys, invalidation, lifecycle cleanup;
47
+ - CLI: `mado release`, `mado bake`, `mado preview`.
48
+
49
+ Для реального браузера оставляй маленькие smoke-тесты: открыть страницу,
50
+ кликнуть ссылку или отправить форму, проверить видимый результат.
@@ -0,0 +1,56 @@
1
+ # Обработка ошибок
2
+
3
+ В Mado есть три практичных слоя ошибок: загрузка роутов, загрузка данных и
4
+ действия пользователя.
5
+
6
+ ## Route errors
7
+
8
+ Глобальная граница:
9
+
10
+ ```ts
11
+ export default routes(manifest, {
12
+ errorPage: (err) => html`
13
+ <main>
14
+ <h1>Что-то пошло не так</h1>
15
+ <pre>${err.message}</pre>
16
+ <a data-link href="/">На главную</a>
17
+ </main>
18
+ `,
19
+ });
20
+ ```
21
+
22
+ Локальная `page({ errorView })` имеет приоритет над `errorPage`.
23
+
24
+ ## Resource errors
25
+
26
+ `resource()` дает `error()` и `loading()`. Показывай retry рядом с данными.
27
+
28
+ ```ts
29
+ const users = resource(() => "/api/users", jsonFetcher<User[]>());
30
+
31
+ html`
32
+ ${() => users.error()
33
+ ? html`<p role="alert">${users.error()!.message}</p>
34
+ <button @click=${users.refresh}>Повторить</button>`
35
+ : null}
36
+ `;
37
+ ```
38
+
39
+ ## Forms and mutations
40
+
41
+ Ошибки валидации — в `useForm()`. Ошибки записи — рядом с submit-кнопкой.
42
+
43
+ ```ts
44
+ const form = useForm(
45
+ { email: { required: true, type: "email" } },
46
+ { validateAsync: (values) => api.validateUser(values) },
47
+ );
48
+ const save = mutation((values) => api.post("/users", values), {
49
+ invalidates: ["/api/users*"],
50
+ });
51
+ ```
52
+
53
+ ## Cleanup
54
+
55
+ Внешние browser subscriptions чисти через `ctx.onDispose()`. `resource()`,
56
+ `effect()` и signals внутри component setup уже привязаны к lifecycle.
@@ -0,0 +1,55 @@
1
+ # Bake cookbook
2
+
3
+ `mado bake` рендерит выбранные роуты в статический HTML. Это для SEO и быстрого
4
+ первого ответа, не SSR с hydration.
5
+
6
+ ## Минимальная страница
7
+
8
+ ```ts
9
+ export default page({
10
+ head: () => ({ title: "Products", description: "Catalog" }),
11
+ view: ({ data }) => html`
12
+ <main>
13
+ <h1>Products</h1>
14
+ ${data.products.map((p) => html`<article><h2>${p.name}</h2></article>`)}
15
+ </main>
16
+ `,
17
+ bake: {
18
+ paths: () => [{}],
19
+ data: async () => ({ products: await api.products() }),
20
+ revalidate: 3600,
21
+ },
22
+ });
23
+ ```
24
+
25
+ В baked views лучше использовать обычные массивы (`items.map(...)`). Runtime
26
+ директивы вроде `each()` нужны браузеру.
27
+
28
+ ## Dynamic routes
29
+
30
+ ```ts
31
+ export default page<{ slug: string }>({
32
+ head: ({ slug }, data) => ({ title: data.title, canonical: `/blog/${slug}` }),
33
+ view: ({ data }) => html`<article>${unsafeHTML(data.html)}</article>`,
34
+ bake: {
35
+ paths: async () => (await api.posts()).map((p) => ({ slug: p.slug })),
36
+ data: ({ slug }) => api.post(slug),
37
+ },
38
+ });
39
+ ```
40
+
41
+ `unsafeHTML()` используй только для доверенного или заранее очищенного HTML.
42
+
43
+ ## Manifest и output
44
+
45
+ ```ts
46
+ export const manifest = {
47
+ "/": () => import("./pages/home.js"),
48
+ "/blog/:slug": () => import("./pages/blog-post.js"),
49
+ };
50
+ export default routes(manifest);
51
+ ```
52
+
53
+ `mado release` создает deploy artifact в `out/`. Если bake ругается на
54
+ unsupported value, значит в статическую страницу попало runtime-only значение:
55
+ замени `each()` на `items.map(...)` или вынеси интерактивный кусок в клиент.
package/docs/ru/README.md CHANGED
@@ -12,3 +12,10 @@
12
12
  | LLM pitfalls | [07-llm-pitfalls.md](./07-llm-pitfalls.md) |
13
13
  | LLM zero-history test | [08-llm-zero-history-test.md](./08-llm-zero-history-test.md) |
14
14
  | Shadow DOM vs Light DOM | [09-shadow-vs-light-dom.md](./09-shadow-vs-light-dom.md) |
15
+ | Архитектура приложения | [10-app-architecture.md](./10-app-architecture.md) |
16
+ | Layouts | [11-layouts.md](./11-layouts.md) |
17
+ | Auth and API | [12-auth-and-api.md](./12-auth-and-api.md) |
18
+ | Deployment | [13-deployment.md](./13-deployment.md) |
19
+ | Тестирование | [14-testing.md](./14-testing.md) |
20
+ | Обработка ошибок | [15-error-handling.md](./15-error-handling.md) |
21
+ | Bake cookbook | [16-bake-cookbook.md](./16-bake-cookbook.md) |
@@ -34,8 +34,8 @@ const form = useForm({
34
34
  });
35
35
  ```
36
36
 
37
- Mado спирається на Constraint Validation API браузера, а не на окрему validation
38
- екосистему.
37
+ Mado використовує schema-based validation, близьку до HTML constraints, а не
38
+ окрему validation ecosystem.
39
39
 
40
40
  ## Auth
41
41