@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.
- package/AGENTS.md +49 -1
- package/CHANGELOG.md +188 -0
- package/MADO_V1_PLAN.md +179 -0
- package/README.md +53 -14
- package/ROADMAP.md +36 -5
- package/TODO.md +72 -0
- package/dist/src/forms.d.ts +41 -7
- package/dist/src/forms.js +334 -59
- package/dist/src/forms.js.map +1 -1
- package/dist/src/html/bindings.d.ts +41 -0
- package/dist/src/html/bindings.js +163 -6
- package/dist/src/html/bindings.js.map +1 -1
- package/dist/src/html.d.ts +2 -0
- package/dist/src/html.js +1 -0
- package/dist/src/html.js.map +1 -1
- package/dist/src/index.d.ts +6 -6
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/page.d.ts +56 -0
- package/dist/src/page.js +17 -0
- package/dist/src/page.js.map +1 -1
- package/dist/src/router/manifest.d.ts +16 -1
- package/dist/src/router/manifest.js +181 -38
- package/dist/src/router/manifest.js.map +1 -1
- package/dist/src/router/match.d.ts +7 -2
- package/dist/src/router/match.js +14 -4
- package/dist/src/router/match.js.map +1 -1
- package/dist/src/router/navigation.d.ts +10 -0
- package/dist/src/router/navigation.js +73 -12
- package/dist/src/router/navigation.js.map +1 -1
- package/dist/src/signal.d.ts +15 -1
- package/dist/src/signal.js +112 -16
- package/dist/src/signal.js.map +1 -1
- package/docs/en/02-project-layout.md +99 -40
- package/docs/en/05-why-mado.md +1 -1
- package/docs/en/06-for-backenders.md +1 -1
- package/docs/en/07-llm-pitfalls.md +1 -1
- package/docs/en/09-shadow-vs-light-dom.md +60 -0
- package/docs/en/10-app-architecture.md +141 -0
- package/docs/en/11-layouts.md +115 -0
- package/docs/en/12-auth-and-api.md +217 -0
- package/docs/en/13-deployment.md +192 -0
- package/docs/en/14-testing.md +82 -0
- package/docs/en/15-error-handling.md +100 -0
- package/docs/en/16-bake-cookbook.md +93 -0
- package/docs/en/README.md +7 -0
- package/docs/fr/05-why-mado.md +1 -1
- package/docs/fr/06-for-backenders.md +1 -1
- package/docs/fr/07-llm-pitfalls.md +1 -1
- package/docs/fr/09-shadow-vs-light-dom.md +63 -0
- package/docs/fr/10-app-architecture.md +61 -0
- package/docs/fr/11-layouts.md +35 -0
- package/docs/fr/12-auth-and-api.md +35 -0
- package/docs/fr/13-deployment.md +39 -0
- package/docs/fr/14-testing.md +41 -0
- package/docs/fr/15-error-handling.md +50 -0
- package/docs/fr/16-bake-cookbook.md +35 -0
- package/docs/fr/README.md +7 -0
- package/docs/ru/05-why-mado.md +2 -2
- package/docs/ru/06-for-backenders.md +1 -1
- package/docs/ru/09-shadow-vs-light-dom.md +60 -0
- package/docs/ru/10-app-architecture.md +100 -0
- package/docs/ru/11-layouts.md +47 -0
- package/docs/ru/12-auth-and-api.md +53 -0
- package/docs/ru/13-deployment.md +60 -0
- package/docs/ru/14-testing.md +50 -0
- package/docs/ru/15-error-handling.md +56 -0
- package/docs/ru/16-bake-cookbook.md +55 -0
- package/docs/ru/README.md +7 -0
- package/docs/uk/06-for-backenders.md +2 -2
- package/docs/uk/09-shadow-vs-light-dom.md +91 -24
- package/docs/uk/10-app-architecture.md +56 -0
- package/docs/uk/11-layouts.md +34 -0
- package/docs/uk/12-auth-and-api.md +34 -0
- package/docs/uk/13-deployment.md +39 -0
- package/docs/uk/14-testing.md +34 -0
- package/docs/uk/15-error-handling.md +32 -0
- package/docs/uk/16-bake-cookbook.md +36 -0
- package/docs/uk/README.md +7 -0
- package/llms.txt +24 -1
- package/package.json +3 -1
- package/scripts/_config.mjs +224 -0
- package/scripts/bake.mjs +217 -120
- package/scripts/bundle.mjs +110 -67
- package/scripts/cli.mjs +127 -16
- package/scripts/preview.mjs +22 -12
- package/server/serve.mjs +101 -11
- package/starters/admin/README.md +63 -0
- package/starters/admin/index.html +21 -0
- package/starters/admin/mado.config.json +22 -0
- package/starters/admin/package.json +22 -0
- package/starters/admin/public/favicon.svg +4 -0
- package/starters/admin/src/components/x-button.ts +55 -0
- package/starters/admin/src/components/x-input.ts +74 -0
- package/starters/admin/src/layouts/app.ts +101 -0
- package/starters/admin/src/layouts/auth.ts +41 -0
- package/starters/admin/src/lib/api.ts +133 -0
- package/starters/admin/src/lib/auth.ts +83 -0
- package/starters/admin/src/main.ts +15 -0
- package/starters/admin/src/pages/admin/dashboard.ts +48 -0
- package/starters/admin/src/pages/admin/order-detail.ts +78 -0
- package/starters/admin/src/pages/admin/orders.ts +117 -0
- package/starters/admin/src/pages/home.ts +25 -0
- package/starters/admin/src/pages/login.ts +70 -0
- package/starters/admin/src/pages/not-found.ts +12 -0
- package/starters/admin/src/routes.ts +40 -0
- package/starters/admin/src/styles/global.ts +86 -0
- package/starters/admin/tsconfig.json +15 -0
- package/starters/crud/README.md +14 -2
- package/starters/crud/mado.config.json +20 -0
- package/starters/crud/package.json +9 -4
- package/starters/crud/src/components/app-shell.ts +13 -8
- package/starters/crud/src/main.ts +1 -4
- package/starters/crud/src/pages/ticket-detail.ts +1 -0
- package/starters/crud/src/pages/ticket-new.ts +1 -0
- package/starters/crud/src/pages/tickets.ts +1 -0
- package/starters/crud/src/routes.ts +4 -2
- package/starters/minimal/README.md +4 -2
- package/starters/minimal/mado.config.json +20 -0
- package/starters/minimal/package.json +8 -3
- package/starters/minimal/src/components/app-counter.ts +1 -1
- 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) |
|
package/docs/ru/05-why-mado.md
CHANGED
|
@@ -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()`
|
|
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 использует
|
|
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
|
|
38
|
-
|
|
37
|
+
Mado використовує schema-based validation, близьку до HTML constraints, а не
|
|
38
|
+
окрему validation ecosystem.
|
|
39
39
|
|
|
40
40
|
## Auth
|
|
41
41
|
|