@madojs/mado 0.5.1 → 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 +26 -0
- package/CHANGELOG.md +153 -0
- package/MADO_V1_PLAN.md +179 -0
- package/README.md +31 -13
- package/ROADMAP.md +28 -7
- package/TODO.md +72 -0
- package/dist/src/forms.d.ts +37 -4
- package/dist/src/forms.js +331 -57
- 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 +71 -3
- 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/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/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/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/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 +9 -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 +119 -15
- package/scripts/preview.mjs +22 -12
- package/server/serve.mjs +82 -4
- 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/mado.config.json +20 -0
- package/starters/crud/package.json +8 -4
- package/starters/crud/src/routes.ts +4 -2
- package/starters/minimal/mado.config.json +20 -0
- package/starters/minimal/package.json +7 -3
- package/starters/minimal/src/routes.ts +4 -2
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Architecture d'application
|
|
2
|
+
|
|
3
|
+
La forme recommandée d'une app Mado en production est volontairement simple :
|
|
4
|
+
un manifeste de routes, un shell, un client API, un module auth, et des pages
|
|
5
|
+
qui importent leurs propres composants.
|
|
6
|
+
|
|
7
|
+
## Structure
|
|
8
|
+
|
|
9
|
+
```txt
|
|
10
|
+
src/
|
|
11
|
+
├── main.ts
|
|
12
|
+
├── routes.ts
|
|
13
|
+
├── layouts/
|
|
14
|
+
├── pages/
|
|
15
|
+
├── components/
|
|
16
|
+
├── lib/
|
|
17
|
+
└── styles/
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`lib/` contient la logique métier, `layouts/` enveloppe les groupes de routes,
|
|
21
|
+
`components/` contient les tags réutilisables, et `pages/` contient un fichier
|
|
22
|
+
par page.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { html, render } from "@madojs/mado";
|
|
26
|
+
import "./styles/global.js";
|
|
27
|
+
import routesApi from "./routes.js";
|
|
28
|
+
|
|
29
|
+
render(html`${routesApi.view}`, document.getElementById("app")!);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
N'importe pas tous les composants dans `main.ts`. Une page importe les
|
|
33
|
+
composants qu'elle rend.
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { layout, routes } from "@madojs/mado";
|
|
37
|
+
import { requireAuth } from "./lib/auth.js";
|
|
38
|
+
|
|
39
|
+
export const manifest = {
|
|
40
|
+
"/": () => import("./pages/home.js"),
|
|
41
|
+
"/admin": layout({
|
|
42
|
+
layout: () => import("./layouts/app.js"),
|
|
43
|
+
guard: requireAuth,
|
|
44
|
+
routes: { "/": () => import("./pages/admin/dashboard.js") },
|
|
45
|
+
}),
|
|
46
|
+
"*": () => import("./pages/not-found.js"),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default routes(manifest);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Exporte `manifest` pour `mado bake`. Utilise `resource()` pour les lectures,
|
|
53
|
+
`mutation(..., { invalidates })` pour les écritures, et `useForm()` pour les
|
|
54
|
+
workflows utilisateur.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
mado dev
|
|
58
|
+
mado release
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Le dossier déployable est `out/`. `dist/` est interne.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Layouts
|
|
2
|
+
|
|
3
|
+
Le chemin recommandé pour les layouts Mado est un groupe de routes imbriqué
|
|
4
|
+
dans `routes.ts`.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { layout, routes } from "@madojs/mado";
|
|
8
|
+
import { requireAuth } from "./lib/auth.js";
|
|
9
|
+
|
|
10
|
+
export const manifest = {
|
|
11
|
+
"/": () => import("./pages/home.js"),
|
|
12
|
+
"/login": layout({
|
|
13
|
+
layout: () => import("./layouts/auth.js"),
|
|
14
|
+
routes: { "/": () => import("./pages/login.js") },
|
|
15
|
+
}),
|
|
16
|
+
"/admin": layout({
|
|
17
|
+
layout: () => import("./layouts/app.js"),
|
|
18
|
+
guard: requireAuth,
|
|
19
|
+
routes: { "/": () => import("./pages/admin/dashboard.js") },
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default routes(manifest);
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Un layout est une `page({ view })` qui rend `child` :
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
export default page({
|
|
30
|
+
view: ({ child }) => html`<x-app-shell>${child}</x-app-shell>`,
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Règles : un shell par groupe, pas par page ; les layouts externes enveloppent
|
|
35
|
+
les layouts internes ; un guard sur le groupe protège tout le sous-arbre.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Auth et API
|
|
2
|
+
|
|
3
|
+
Le starter `admin` contient la recette recommandée :
|
|
4
|
+
|
|
5
|
+
- `src/lib/api.ts` — un seul client HTTP, `ApiError`, refresh après 401 ;
|
|
6
|
+
- `src/lib/auth.ts` — `accessToken`, `restoreSession()`, `login()`,
|
|
7
|
+
`logout()`, `requireAuth`.
|
|
8
|
+
|
|
9
|
+
Modèle :
|
|
10
|
+
|
|
11
|
+
- access token en mémoire via `signal`, pas dans `localStorage` ;
|
|
12
|
+
- refresh cookie HttpOnly pour restaurer la session ;
|
|
13
|
+
- toutes les requêtes passent par le client API ;
|
|
14
|
+
- les routes protégées utilisent un 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
|
+
Dev proxy :
|
|
25
|
+
|
|
26
|
+
```jsonc
|
|
27
|
+
{
|
|
28
|
+
"dev": {
|
|
29
|
+
"proxy": { "/api": "http://localhost:3000" }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Si ton backend a une autre forme, modifie `api.ts` et `auth.ts`. Les pages ne
|
|
35
|
+
doivent pas connaître les détails d'authentification.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Déploiement
|
|
2
|
+
|
|
3
|
+
Une commande, un artefact :
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
mado release
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Résultat :
|
|
10
|
+
|
|
11
|
+
```txt
|
|
12
|
+
out/
|
|
13
|
+
├── index.html
|
|
14
|
+
├── assets/
|
|
15
|
+
├── baked/
|
|
16
|
+
├── _redirects
|
|
17
|
+
└── _headers
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Déploie `out/` sur nginx, Cloudflare Pages, Netlify, S3/CloudFront ou GitHub
|
|
21
|
+
Pages.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
mado release
|
|
25
|
+
mado preview
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`mado preview` sert `out/` comme un hébergeur statique : HTML baked d'abord,
|
|
29
|
+
fallback SPA ensuite.
|
|
30
|
+
|
|
31
|
+
Pour VPS + nginx :
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
mado release
|
|
35
|
+
rsync -avz --delete out/ user@server:/var/www/myapp/
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Le `nginx.conf` fourni gère cache immutable pour les bundles hashés, no-cache
|
|
39
|
+
pour HTML, et fallback SPA pour les deep links.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Tests
|
|
2
|
+
|
|
3
|
+
Mado utilise TypeScript et les APIs du navigateur. Le dépôt du framework teste
|
|
4
|
+
avec le test runner de Node et `linkedom`.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm run typecheck
|
|
8
|
+
npm run build
|
|
9
|
+
npm test
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Un test DOM minimal :
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import test from "node:test";
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
|
|
18
|
+
const { parseHTML } = await import("linkedom");
|
|
19
|
+
const { window } = parseHTML("<!doctype html><html><body></body></html>");
|
|
20
|
+
globalThis.window = window;
|
|
21
|
+
globalThis.document = window.document;
|
|
22
|
+
globalThis.Node = window.Node;
|
|
23
|
+
globalThis.HTMLElement = window.HTMLElement;
|
|
24
|
+
|
|
25
|
+
const { html, render } = await import("../dist/src/html.js");
|
|
26
|
+
|
|
27
|
+
test("renders", () => {
|
|
28
|
+
const root = document.createElement("div");
|
|
29
|
+
render(html`<p>${"hello"}</p>`, root);
|
|
30
|
+
assert.equal(root.querySelector("p").textContent, "hello");
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
À couvrir :
|
|
35
|
+
|
|
36
|
+
- signals/computed/effect : scheduling et nettoyage ;
|
|
37
|
+
- bindings HTML : children, attributs, événements, directives ;
|
|
38
|
+
- routes : guards, redirects, scroll/focus, error boundaries ;
|
|
39
|
+
- formulaires : validation sync/async, races, field arrays ;
|
|
40
|
+
- resources/mutations : clés de cache, invalidation, lifecycle ;
|
|
41
|
+
- CLI : `mado release`, `mado bake`, `mado preview`.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Gestion des erreurs
|
|
2
|
+
|
|
3
|
+
Traite les erreurs au niveau où l'utilisateur peut récupérer : routes, données,
|
|
4
|
+
actions utilisateur.
|
|
5
|
+
|
|
6
|
+
## Routes
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
export default routes(manifest, {
|
|
10
|
+
errorPage: (err) => html`
|
|
11
|
+
<main>
|
|
12
|
+
<h1>Une erreur est survenue</h1>
|
|
13
|
+
<pre>${err.message}</pre>
|
|
14
|
+
<a data-link href="/">Accueil</a>
|
|
15
|
+
</main>
|
|
16
|
+
`,
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`page({ errorView })` a priorité sur cette boundary globale.
|
|
21
|
+
|
|
22
|
+
## Données
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
const users = resource(() => "/api/users", jsonFetcher<User[]>());
|
|
26
|
+
|
|
27
|
+
html`
|
|
28
|
+
${() => users.error()
|
|
29
|
+
? html`<p role="alert">${users.error()!.message}</p>
|
|
30
|
+
<button @click=${users.refresh}>Réessayer</button>`
|
|
31
|
+
: null}
|
|
32
|
+
`;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Formulaires et mutations
|
|
36
|
+
|
|
37
|
+
La validation va dans `useForm()`. Les erreurs serveur d'écriture restent près
|
|
38
|
+
du bouton de soumission.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const form = useForm(
|
|
42
|
+
{ email: { required: true, type: "email" } },
|
|
43
|
+
{ validateAsync: (values) => api.validateUser(values) },
|
|
44
|
+
);
|
|
45
|
+
const save = mutation((values) => api.post("/users", values), {
|
|
46
|
+
invalidates: ["/api/users*"],
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Nettoie les subscriptions navigateur externes avec `ctx.onDispose()`.
|
|
@@ -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) |
|
|
@@ -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.
|