@madojs/mado 0.5.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 +291 -0
- package/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +371 -0
- package/ROADMAP.md +52 -0
- package/dist/src/component.d.ts +48 -0
- package/dist/src/component.js +140 -0
- package/dist/src/component.js.map +1 -0
- package/dist/src/context.d.ts +40 -0
- package/dist/src/context.js +67 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/css.d.ts +54 -0
- package/dist/src/css.js +137 -0
- package/dist/src/css.js.map +1 -0
- package/dist/src/devtools.d.ts +22 -0
- package/dist/src/devtools.js +63 -0
- package/dist/src/devtools.js.map +1 -0
- package/dist/src/diagnostics.d.ts +11 -0
- package/dist/src/diagnostics.js +28 -0
- package/dist/src/diagnostics.js.map +1 -0
- package/dist/src/each.d.ts +39 -0
- package/dist/src/each.js +35 -0
- package/dist/src/each.js.map +1 -0
- package/dist/src/forms.d.ts +71 -0
- package/dist/src/forms.js +161 -0
- package/dist/src/forms.js.map +1 -0
- package/dist/src/head.d.ts +19 -0
- package/dist/src/head.js +97 -0
- package/dist/src/head.js.map +1 -0
- package/dist/src/html/bindings.d.ts +78 -0
- package/dist/src/html/bindings.js +304 -0
- package/dist/src/html/bindings.js.map +1 -0
- package/dist/src/html/parser.d.ts +64 -0
- package/dist/src/html/parser.js +521 -0
- package/dist/src/html/parser.js.map +1 -0
- package/dist/src/html/template-types.d.ts +27 -0
- package/dist/src/html/template-types.js +8 -0
- package/dist/src/html/template-types.js.map +1 -0
- package/dist/src/html/template.d.ts +45 -0
- package/dist/src/html/template.js +119 -0
- package/dist/src/html/template.js.map +1 -0
- package/dist/src/html.d.ts +16 -0
- package/dist/src/html.js +16 -0
- package/dist/src/html.js.map +1 -0
- package/dist/src/index.d.ts +35 -0
- package/dist/src/index.js +39 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/lazy.d.ts +38 -0
- package/dist/src/lazy.js +73 -0
- package/dist/src/lazy.js.map +1 -0
- package/dist/src/lifecycle.d.ts +45 -0
- package/dist/src/lifecycle.js +66 -0
- package/dist/src/lifecycle.js.map +1 -0
- package/dist/src/page.d.ts +161 -0
- package/dist/src/page.js +38 -0
- package/dist/src/page.js.map +1 -0
- package/dist/src/persisted.d.ts +47 -0
- package/dist/src/persisted.js +119 -0
- package/dist/src/persisted.js.map +1 -0
- package/dist/src/resource.d.ts +120 -0
- package/dist/src/resource.js +275 -0
- package/dist/src/resource.js.map +1 -0
- package/dist/src/router/manifest.d.ts +56 -0
- package/dist/src/router/manifest.js +302 -0
- package/dist/src/router/manifest.js.map +1 -0
- package/dist/src/router/match.d.ts +62 -0
- package/dist/src/router/match.js +117 -0
- package/dist/src/router/match.js.map +1 -0
- package/dist/src/router/navigation.d.ts +89 -0
- package/dist/src/router/navigation.js +263 -0
- package/dist/src/router/navigation.js.map +1 -0
- package/dist/src/router.d.ts +13 -0
- package/dist/src/router.js +13 -0
- package/dist/src/router.js.map +1 -0
- package/dist/src/signal.d.ts +67 -0
- package/dist/src/signal.js +238 -0
- package/dist/src/signal.js.map +1 -0
- package/docs/README.md +12 -0
- package/docs/en/00-the-mado-way.md +106 -0
- package/docs/en/01-routing.md +204 -0
- package/docs/en/02-project-layout.md +58 -0
- package/docs/en/03-static-bake.md +251 -0
- package/docs/en/04-ide-setup.md +162 -0
- package/docs/en/05-why-mado.md +193 -0
- package/docs/en/06-for-backenders.md +422 -0
- package/docs/en/07-llm-pitfalls.md +486 -0
- package/docs/en/08-llm-zero-history-test.md +56 -0
- package/docs/en/09-shadow-vs-light-dom.md +122 -0
- package/docs/en/README.md +16 -0
- package/docs/fr/00-the-mado-way.md +108 -0
- package/docs/fr/01-routing.md +202 -0
- package/docs/fr/02-project-layout.md +58 -0
- package/docs/fr/03-static-bake.md +290 -0
- package/docs/fr/04-ide-setup.md +162 -0
- package/docs/fr/05-why-mado.md +193 -0
- package/docs/fr/06-for-backenders.md +432 -0
- package/docs/fr/07-llm-pitfalls.md +487 -0
- package/docs/fr/08-llm-zero-history-test.md +60 -0
- package/docs/fr/09-shadow-vs-light-dom.md +121 -0
- package/docs/fr/README.md +16 -0
- package/docs/ru/00-the-mado-way.md +93 -0
- package/docs/ru/01-routing.md +194 -0
- package/docs/ru/02-project-layout.md +57 -0
- package/docs/ru/03-static-bake.md +251 -0
- package/docs/ru/04-ide-setup.md +144 -0
- package/docs/ru/05-why-mado.md +193 -0
- package/docs/ru/06-for-backenders.md +422 -0
- package/docs/ru/07-llm-pitfalls.md +485 -0
- package/docs/ru/08-llm-zero-history-test.md +56 -0
- package/docs/ru/09-shadow-vs-light-dom.md +122 -0
- package/docs/ru/README.md +14 -0
- package/docs/uk/00-the-mado-way.md +54 -0
- package/docs/uk/01-routing.md +82 -0
- package/docs/uk/02-project-layout.md +46 -0
- package/docs/uk/03-static-bake.md +49 -0
- package/docs/uk/04-ide-setup.md +26 -0
- package/docs/uk/05-why-mado.md +34 -0
- package/docs/uk/06-for-backenders.md +50 -0
- package/docs/uk/07-llm-pitfalls.md +82 -0
- package/docs/uk/08-llm-zero-history-test.md +31 -0
- package/docs/uk/09-shadow-vs-light-dom.md +40 -0
- package/docs/uk/README.md +16 -0
- package/llms.txt +155 -0
- package/package.json +81 -0
- package/scripts/bake.mjs +406 -0
- package/scripts/bundle.mjs +146 -0
- package/scripts/cli.mjs +382 -0
- package/scripts/new.mjs +80 -0
- package/scripts/preview.mjs +176 -0
- package/scripts/release-notes.mjs +66 -0
- package/scripts/showcase-regression.mjs +392 -0
- package/server/serve.mjs +292 -0
- package/starters/crud/README.md +21 -0
- package/starters/crud/index.html +20 -0
- package/starters/crud/package.json +17 -0
- package/starters/crud/src/components/app-shell.ts +51 -0
- package/starters/crud/src/components/ticket-detail.ts +33 -0
- package/starters/crud/src/components/ticket-form.ts +69 -0
- package/starters/crud/src/components/ticket-list.ts +66 -0
- package/starters/crud/src/lib/api.ts +76 -0
- package/starters/crud/src/main.ts +12 -0
- package/starters/crud/src/pages/home.ts +18 -0
- package/starters/crud/src/pages/not-found.ts +12 -0
- package/starters/crud/src/pages/ticket-detail.ts +6 -0
- package/starters/crud/src/pages/ticket-new.ts +6 -0
- package/starters/crud/src/pages/tickets.ts +6 -0
- package/starters/crud/src/routes.ts +9 -0
- package/starters/crud/src/styles/global.ts +155 -0
- package/starters/crud/tsconfig.json +15 -0
- package/starters/minimal/README.md +19 -0
- package/starters/minimal/index.html +20 -0
- package/starters/minimal/package.json +17 -0
- package/starters/minimal/src/components/app-counter.ts +31 -0
- package/starters/minimal/src/main.ts +9 -0
- package/starters/minimal/src/pages/home.ts +18 -0
- package/starters/minimal/src/pages/not-found.ts +14 -0
- package/starters/minimal/src/routes.ts +6 -0
- package/starters/minimal/src/styles/global.ts +60 -0
- package/starters/minimal/tsconfig.json +15 -0
- package/templates/page-detail.ts +63 -0
- package/templates/page-form.ts +94 -0
- package/templates/page-list.ts +79 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# La voie Mado
|
|
2
|
+
|
|
3
|
+
> Une seule bonne façon. Des contrats stricts. Pas de magie.
|
|
4
|
+
|
|
5
|
+
Mado n'est pas seulement un framework — c'est un **ensemble de conventions**. Si vous les
|
|
6
|
+
respectez, le projet reste compréhensible même avec 200 écrans et 5 développeurs. Si vous
|
|
7
|
+
les enfreignez — les types et le linter vous le diront immédiatement.
|
|
8
|
+
|
|
9
|
+
## Principes
|
|
10
|
+
|
|
11
|
+
1. **Une seule voie.** Pour chaque tâche, il existe un seul chemin correct, pas cinq. Si vous
|
|
12
|
+
écrivez quelque chose d'inhabituel — demandez-vous si un helper idiomatique n'existe pas déjà.
|
|
13
|
+
2. **L'explicite plutôt que la magie.** Pas de scanners de système de fichiers, pas de globals
|
|
14
|
+
implicites, pas d'effets de bord cachés. Tout ce que fait le framework peut être lu dans un
|
|
15
|
+
seul fichier.
|
|
16
|
+
3. **La plateforme d'abord.** Si le navigateur propose déjà une fonctionnalité — utilisez-la
|
|
17
|
+
directement. Pas d'abstractions personnalisées sur `fetch`, `<form>`, l'History API, ou
|
|
18
|
+
Shadow DOM.
|
|
19
|
+
4. **Types stricts.** `tsc --strict --noUncheckedIndexedAccess` toujours. Si quelque chose ne
|
|
20
|
+
peut pas être typé — c'est un signal que l'API est mauvaise.
|
|
21
|
+
5. **Pas de dépendances runtime.** Chaque dépendance est un engagement sur des années ;
|
|
22
|
+
l'écosystème Web Components n'en a pas besoin.
|
|
23
|
+
|
|
24
|
+
## Conventions
|
|
25
|
+
|
|
26
|
+
### Structure du projet
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
src/
|
|
30
|
+
├── routes.ts ← manifeste de routes, un fichier par projet
|
|
31
|
+
├── main.ts ← point d'entrée : providers + montage de <x-app>
|
|
32
|
+
├── pages/ ← une page = un fichier = `export default page({...})`
|
|
33
|
+
├── components/ ← composants réutilisables, enregistrement des effets de bord
|
|
34
|
+
├── lib/ ← contextes, clients API, logique métier sans UI
|
|
35
|
+
└── styles/ ← styles partagés (si nécessaire), fichiers .ts avec css``
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
C'est **obligatoire**, pas optionnel. Si un projet a 10 développeurs — ils doivent
|
|
39
|
+
tous écrire de la même manière.
|
|
40
|
+
|
|
41
|
+
### Un composant = un fichier
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
// src/components/user-card.ts
|
|
45
|
+
import { component, html, css } from '@madojs/mado';
|
|
46
|
+
|
|
47
|
+
component('x-user-card', () => {
|
|
48
|
+
return () => html`<div class="card"><slot/></div>`;
|
|
49
|
+
}, {
|
|
50
|
+
styles: css`.card { padding: 1rem; }`,
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`import './components/user-card.js'` **enregistre** le composant via
|
|
55
|
+
`customElements.define`. C'est un effet de bord. Importez là où le composant est nécessaire.
|
|
56
|
+
|
|
57
|
+
### Une seule façon de charger les données
|
|
58
|
+
|
|
59
|
+
❌ N'appelez pas `fetch()` directement depuis un composant. Utilisez toujours :
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// lecture → resource
|
|
63
|
+
const user = resource(() => `/api/users/${id()}`, jsonFetcher());
|
|
64
|
+
|
|
65
|
+
// écriture → mutation
|
|
66
|
+
const save = mutation(api.save, { invalidates: ['/api/users*'] });
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Cela fournit la mise en cache, l'annulation, la gestion des erreurs et l'invalidation automatique.
|
|
70
|
+
|
|
71
|
+
### Une seule façon de décrire une page
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
// src/pages/user-profile.ts
|
|
75
|
+
import { page, html, resource, jsonFetcher } from '@madojs/mado';
|
|
76
|
+
|
|
77
|
+
export default page({
|
|
78
|
+
title: ({ id }) => `Utilisateur #${id}`,
|
|
79
|
+
view: ({ params }) => html`...`,
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Trois emplacements — `title`, `load`, `view`. Pas d'autres. Vous voulez autre chose — c'est
|
|
84
|
+
un composant ou un helper.
|
|
85
|
+
|
|
86
|
+
### Une seule façon de déclarer les routes
|
|
87
|
+
|
|
88
|
+
Voir [`01-routing.md`](./01-routing.md).
|
|
89
|
+
|
|
90
|
+
## Ce que nous NE faisons PAS
|
|
91
|
+
|
|
92
|
+
- ❌ N'écrivez pas de composants sans trait d'union. C'est la règle du navigateur pour
|
|
93
|
+
les custom elements : `user-card` est correct, `usercard` ne l'est pas.
|
|
94
|
+
- `x-*` n'est qu'une convention pour les exemples et les tests Mado, pas un standard de marque.
|
|
95
|
+
En production, utilisez un préfixe de domaine : `app-*`, `crm-*`, `ticket-*`, `admin-*`.
|
|
96
|
+
- ❌ N'utilisez pas `innerHTML` directement. Uniquement via `html\`\``.
|
|
97
|
+
- ❌ N'appelez pas `setTimeout`/`setInterval` sans nettoyage. Uniquement à l'intérieur de `effect()`.
|
|
98
|
+
- ❌ Ne stockez pas d'état mutable global. Utilisez les signals et `context`.
|
|
99
|
+
- ❌ N'ajoutez pas de packages sans discussion. Chaque dépendance est un engagement.
|
|
100
|
+
|
|
101
|
+
## En cas de doute
|
|
102
|
+
|
|
103
|
+
Si vous vous demandez "quelle est la meilleure façon ici ?" — c'est un signal que :
|
|
104
|
+
1. Soit il existe un helper intégré que vous ne connaissez pas (consultez `docs/`).
|
|
105
|
+
2. Soit c'est une nouvelle situation — discutez-en et **consignez-la** dans ce document
|
|
106
|
+
comme une convention supplémentaire.
|
|
107
|
+
|
|
108
|
+
"Un 'bien' cohérent vaut mieux qu'un 'idéal' varié."
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Routage
|
|
2
|
+
|
|
3
|
+
> Un seul fichier manifeste. Pas de scanners de dossiers. Pas de caractères spéciaux.
|
|
4
|
+
|
|
5
|
+
## Pourquoi pas de routes basées sur les fichiers
|
|
6
|
+
|
|
7
|
+
Dans Next/SvelteKit/SolidStart, les routes apparaissent "magiquement" à partir des noms de
|
|
8
|
+
fichiers. Cela a des avantages (la structure d'URL visible dans `pages/`), mais en production
|
|
9
|
+
cela signifie :
|
|
10
|
+
|
|
11
|
+
- Un plugin-scanner invisible dans le build. Sans lui, les fichiers ne sont que des fichiers.
|
|
12
|
+
- Des caractères spéciaux dans les chemins : `[id]`, `(group)`, `_layout`, `+page.svelte`, `...slug`.
|
|
13
|
+
- Les routes serveur et les routes client se mélangent.
|
|
14
|
+
- Tester le routage est pénible : vous avez besoin d'un émulateur de build-tool.
|
|
15
|
+
|
|
16
|
+
Mado considère cela comme **trop de magie**. Nous procédons différemment.
|
|
17
|
+
|
|
18
|
+
## Manifeste
|
|
19
|
+
|
|
20
|
+
Un seul fichier — `src/routes.ts`. Un seul objet. Lu de haut en bas.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// src/routes.ts
|
|
24
|
+
import { routes } from '@madojs/mado';
|
|
25
|
+
|
|
26
|
+
export default routes({
|
|
27
|
+
'/': () => import('./pages/home.js'),
|
|
28
|
+
'/about': () => import('./pages/about.js'),
|
|
29
|
+
'/users/:id': () => import('./pages/user-profile.js'),
|
|
30
|
+
'/users/:id/edit':() => import('./pages/user-edit.js'),
|
|
31
|
+
'*': () => import('./pages/not-found.js'),
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Vous voulez voir toutes les routes ? Ouvrez `routes.ts`. Pas de surprises.
|
|
36
|
+
|
|
37
|
+
## Ce qui va à droite d'un chemin
|
|
38
|
+
|
|
39
|
+
Chaque entrée est **l'une de ces trois choses** :
|
|
40
|
+
|
|
41
|
+
### 1. Import lazy (recommandé)
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
'/posts': () => import('./pages/posts.js'),
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- Le navigateur crée son propre chunk lors du bundling (esbuild --bundle --splitting).
|
|
48
|
+
- Le module est chargé uniquement quand l'utilisateur visite la route.
|
|
49
|
+
- Les navigations suivantes utilisent le résultat mis en cache.
|
|
50
|
+
|
|
51
|
+
### 2. Page prête (eager)
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import about from './pages/about.js';
|
|
55
|
+
|
|
56
|
+
'/about': about,
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Dans le bundle immédiatement, sans délai. Utilisez pour les pages critiques (accueil, connexion).
|
|
60
|
+
|
|
61
|
+
### 3. Imbriqué avec layout
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { routes, nested } from '@madojs/mado';
|
|
65
|
+
|
|
66
|
+
export default routes({
|
|
67
|
+
'/': () => import('./pages/home.js'),
|
|
68
|
+
|
|
69
|
+
'/admin/*': nested({
|
|
70
|
+
layout: () => import('./layouts/admin.js'),
|
|
71
|
+
routes: {
|
|
72
|
+
'': () => import('./pages/admin/dashboard.js'),
|
|
73
|
+
'users': () => import('./pages/admin/users.js'),
|
|
74
|
+
'logs': () => import('./pages/admin/logs.js'),
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Un layout est juste une `page({...})` ordinaire qui rend `ctx.child` où elle le souhaite :
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
// src/layouts/admin.ts
|
|
84
|
+
import { page, html, css, component } from '@madojs/mado';
|
|
85
|
+
|
|
86
|
+
export default page({
|
|
87
|
+
view: ({ child }) => html`
|
|
88
|
+
<div class="admin">
|
|
89
|
+
<aside><nav>...</nav></aside>
|
|
90
|
+
<main>${child}</main>
|
|
91
|
+
</div>
|
|
92
|
+
`,
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Contrat de page
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { page, html, resource, jsonFetcher } from '@madojs/mado';
|
|
100
|
+
|
|
101
|
+
export default page({
|
|
102
|
+
title: ({ id }) => `Utilisateur #${id}`, // string | (params) => string
|
|
103
|
+
load: ({ id }) => resource(...), // optionnel, retourne Resource ou data
|
|
104
|
+
view: ({ params, data, path, child }) => html`...`, // OBLIGATOIRE
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Trois emplacements, c'est tout. Si vous exportez autre chose que `page({...})`, une simple
|
|
109
|
+
fonction par exemple — `routes()` génère une erreur claire :
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
[Mado] La route lazy n'a pas retourné page({...}) comme export par défaut.
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Paramètres d'URL
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
'/users/:id': () => import('./pages/user.js'),
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
export default page<{ id: string }>({
|
|
123
|
+
title: ({ id }) => `Utilisateur ${id}`,
|
|
124
|
+
view: ({ params }) => html`<h1>${params.id}</h1>`,
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Les types sont passés dans `page<Params>` — `tsc` vérifie que vous n'accédez pas à
|
|
129
|
+
`params.foo` qui n'existe pas dans la route.
|
|
130
|
+
|
|
131
|
+
## Options globales
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
export default routes(
|
|
135
|
+
{ '/': home, '/about': about, '*': nf },
|
|
136
|
+
{
|
|
137
|
+
titleSuffix: ' · MonApp', // → "Accueil · MonApp"
|
|
138
|
+
loading: () => html`<x-spinner/>`, // pendant le chargement du module
|
|
139
|
+
error: (err) => html`<x-fatal-error .err=${err}/>`,
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Navigation programmatique
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import route from './routes.js';
|
|
148
|
+
|
|
149
|
+
route.navigate('/posts');
|
|
150
|
+
route.navigate('/posts?page=2');
|
|
151
|
+
route.navigate('/posts', { replace: true });
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Les clics sur `<a href="/foo" data-link>` sont interceptés globalement (sans l'attribut —
|
|
155
|
+
le navigateur effectue un rechargement complet, comme prévu pour les liens externes).
|
|
156
|
+
|
|
157
|
+
## Paramètres de requête
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { queryParam } from '@madojs/mado';
|
|
161
|
+
|
|
162
|
+
const page = queryParam('page', '1');
|
|
163
|
+
page(); // '1'
|
|
164
|
+
page.set('2'); // history.replaceState + re-rendu
|
|
165
|
+
page.set(null); // supprimer le paramètre
|
|
166
|
+
page.set('3', { push: true }); // history.pushState
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`queryParam` est un signal normal. Utilisez-le n'importe où : dans les pages, les composants,
|
|
170
|
+
les computed.
|
|
171
|
+
|
|
172
|
+
## Ce qui est intentionnellement absent
|
|
173
|
+
|
|
174
|
+
- ❌ Auto-scan de `pages/`. **Un seul fichier manifeste explicite**.
|
|
175
|
+
- ❌ Caractères spéciaux dans les chemins (`[id]`, `(group)`, `_layout`). **Les paramètres sont
|
|
176
|
+
uniquement `:name`, rien d'autre**.
|
|
177
|
+
- ❌ Routage côté serveur dans le même manifeste. Mado est un framework côté client.
|
|
178
|
+
- ❌ Auto-préchargement au survol. Si vous en avez vraiment besoin — faites-le manuellement :
|
|
179
|
+
`link.addEventListener('mouseenter', loader)`. Généralement inutile.
|
|
180
|
+
|
|
181
|
+
## FAQ
|
|
182
|
+
|
|
183
|
+
**Et si j'ai 100 routes ? Le fichier ne deviendra-t-il pas énorme ?**
|
|
184
|
+
Il atteindra ~150 lignes. C'est toujours **une seule source de vérité** contre une centaine
|
|
185
|
+
de fichiers dans `pages/` avec des noms magiques. En pratique, même les grands projets (1000+
|
|
186
|
+
pages) peuvent se diviser en manifestes de fonctionnalités :
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
import { routes } from '@madojs/mado';
|
|
190
|
+
import adminRoutes from './features/admin/routes.js';
|
|
191
|
+
import billingRoutes from './features/billing/routes.js';
|
|
192
|
+
|
|
193
|
+
export default routes({
|
|
194
|
+
...adminRoutes,
|
|
195
|
+
...billingRoutes,
|
|
196
|
+
'*': () => import('./pages/not-found.js'),
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Comment tester le routage ?**
|
|
201
|
+
Importez `routes.ts` — c'est juste un objet. Substituez votre router mock. Pas besoin
|
|
202
|
+
d'émulation de build-tool.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Structure du projet
|
|
2
|
+
|
|
3
|
+
Chaque nouveau projet Mado a la même structure. C'est une convention **obligatoire**.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
my-app/
|
|
7
|
+
├── package.json # exactement 1 dép : typescript (esbuild optionnel)
|
|
8
|
+
├── tsconfig.json # avec paths "@madojs/mado" → import sans chemins relatifs
|
|
9
|
+
├── Dockerfile + nginx.conf # copiés depuis Mado/ lors du scaffold
|
|
10
|
+
├── .gitlab-ci.yml | .github/workflows/ci.yml
|
|
11
|
+
├── server/serve.mjs # dev-server de Mado, sans dépendances
|
|
12
|
+
├── scripts/
|
|
13
|
+
│ ├── bundle.mjs # bundle de production esbuild
|
|
14
|
+
│ └── new.mjs # générateur de pages
|
|
15
|
+
├── templates/ # templates pour new.mjs
|
|
16
|
+
├── docs/ # documentation du projet (vous pouvez copier nos guides)
|
|
17
|
+
├── public/ # assets statiques (favicon, manifests)
|
|
18
|
+
└── src/
|
|
19
|
+
├── main.ts # entrée : providers + montage de <x-app>
|
|
20
|
+
├── routes.ts # manifeste de routes
|
|
21
|
+
├── pages/ # une page = un fichier = `export default page({...})`
|
|
22
|
+
├── components/ # composants réutilisables (x-*)
|
|
23
|
+
├── layouts/ # pages de layout (pour nested)
|
|
24
|
+
└── lib/
|
|
25
|
+
├── api.ts # tous les wrappers fetch
|
|
26
|
+
├── contexts.ts # createContext(...)
|
|
27
|
+
├── theme.ts # thèmes
|
|
28
|
+
└── ... # utilitaires, types, règles métier
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Où mettre un nouveau fichier ?
|
|
32
|
+
|
|
33
|
+
| Quoi | Où |
|
|
34
|
+
|---|---|
|
|
35
|
+
| Page pour une nouvelle URL | `src/pages/foo.ts` + ajouter à `src/routes.ts` |
|
|
36
|
+
| Widget UI réutilisable | `src/components/foo-bar.ts` |
|
|
37
|
+
| Wrapper API | `src/lib/api.ts` (ajouter une méthode) |
|
|
38
|
+
| Contexte global (thème, utilisateur, i18n) | `src/lib/<nom>.ts` |
|
|
39
|
+
| Fonction pure sans UI | `src/lib/util/<nom>.ts` |
|
|
40
|
+
|
|
41
|
+
Si vous ne savez pas où — c'est un signal que **l'architecture souffre**.
|
|
42
|
+
Consultez l'équipe, **consignez** la réponse dans `docs/`.
|
|
43
|
+
|
|
44
|
+
## Règles de nommage
|
|
45
|
+
|
|
46
|
+
| Quoi | Style | Exemple |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| Fichier | kebab-case | `user-profile.ts` |
|
|
49
|
+
| Tag de composant | `x-` + kebab | `<x-user-profile>` |
|
|
50
|
+
| Context | PascalCase + `Ctx` | `ThemeCtx`, `AuthCtx` |
|
|
51
|
+
| Signal | camelCase | `userId`, `isLoggedIn` |
|
|
52
|
+
| Fonction de page (composant interne) | `x-<route>-page` | `<x-posts-page>` |
|
|
53
|
+
|
|
54
|
+
## Ce qui ne va PAS dans src/
|
|
55
|
+
|
|
56
|
+
- ❌ Configurations de build-tool (webpack, rollup, vite) — nous n'en avons pas.
|
|
57
|
+
- ❌ Fichiers `.env` — les variables d'environnement sont lues depuis `process.env`/`import.meta.env` dans `lib/config.ts`.
|
|
58
|
+
- ❌ Tests mélangés avec le code — tout dans `test/`.
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
# Static intelligent (`bake`)
|
|
2
|
+
|
|
3
|
+
> Générer du HTML pour le SEO sans SSR runtime. **Idée : données et vue dans un seul fichier, sortie statique.**
|
|
4
|
+
|
|
5
|
+
`bake` est un **prérendu au moment du build**, pas du SSR. La sortie est des fichiers `*.html`
|
|
6
|
+
statiques que n'importe quel nginx/Cloudflare sert comme du contenu statique ordinaire. Sur le
|
|
7
|
+
client, les Web Components s'animent et la navigation SPA continue de fonctionner.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Quand `bake` est adapté
|
|
12
|
+
|
|
13
|
+
- **Pages marketing** : landing pages, sous-landings pour des campagnes publicitaires, pages produits.
|
|
14
|
+
- **Catalogue avec un ensemble de pages relativement stable** : blog, documentation, portfolio,
|
|
15
|
+
e-commerce jusqu'à **~10k SKUs** avec des mises à jour moins d'une fois par heure.
|
|
16
|
+
- **Contenu identique pour tous les utilisateurs** : la page entière est identique pour un
|
|
17
|
+
visiteur et un utilisateur authentifié (ou l'authentification est rendue côté client via `effect()`).
|
|
18
|
+
- Vous avez besoin d'un **bon SEO** (rich snippets, OG, JSON-LD) et d'un **premier affichage
|
|
19
|
+
rapide** sans faire tourner des serveurs node en production.
|
|
20
|
+
|
|
21
|
+
## Quand `bake` n'est **pas** adapté
|
|
22
|
+
|
|
23
|
+
- **Des centaines de milliers de pages avec des changements fréquents**. `bake` parcourt tous
|
|
24
|
+
les `paths` de façon synchrone en une seule exécution. Pour 100k+ pages, cela signifie des
|
|
25
|
+
minutes de rebuild, et l'invalidation d'une page nécessite soit un rebake complet, soit une
|
|
26
|
+
logique CI séparée (voir ci-dessous sur la revalidation ciblée).
|
|
27
|
+
- **Contenu personnalisé dans le HTML**. Si la page doit afficher "Bonjour, Ivan" dans le
|
|
28
|
+
`<title>` ou dans les meta — ce n'est pas pour `bake`. Les tableaux de bord authentifiés,
|
|
29
|
+
les fils personnalisés, un panier avec des prix réels pour l'utilisateur — gardez-les en SPA.
|
|
30
|
+
- **Des API uniquement serveur sont nécessaires lors du rendu** : cookies, headers, vraies
|
|
31
|
+
requêtes réseau vers des API privées. Côté bake, seul `linkedom` est disponible, pas
|
|
32
|
+
d'environnement Node pour les composants.
|
|
33
|
+
- **Tests A/B et flags qui modifient le markup au premier affichage**. `bake` verrouillera
|
|
34
|
+
une variante. Gérez le comportement dynamique côté client via `effect()`.
|
|
35
|
+
- **Données en temps réel / qui changent fréquemment** (cours boursiers, stock entrepôt à la
|
|
36
|
+
minute). `bake.revalidate` est des métadonnées, pas du runtime : le framework ne re-bake
|
|
37
|
+
rien lui-même.
|
|
38
|
+
- **Contenu derrière une authentification** (admin, outils internes). Inutile ; utilisez le
|
|
39
|
+
mode SPA.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Concept
|
|
44
|
+
|
|
45
|
+
`page({...})` a quatre emplacements optionnels liés à `bake` :
|
|
46
|
+
|
|
47
|
+
- `head` — meta, OG, JSON-LD.
|
|
48
|
+
- `bake.paths` — liste des paramètres URL pour la génération (build-time, peut être `async`).
|
|
49
|
+
- `bake.data` — données pour une URL spécifique (build-time, peut être `async`).
|
|
50
|
+
- `bake.revalidate` — après combien de secondes le cache est périmé (écrit dans `<meta>`, la
|
|
51
|
+
vraie invalidation est gérée par votre CI/CDN).
|
|
52
|
+
|
|
53
|
+
La commande `npm run bake` parcourt toutes les entrées `page` avec `bake`, génère le HTML via
|
|
54
|
+
`linkedom`, et le place dans `out/<chemin>/index.html`. **Pas de Chromium nécessaire** —
|
|
55
|
+
`linkedom` fait ~50 Ko.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Exemple
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
// src/pages/product.ts
|
|
63
|
+
import { page, component, html } from "@madojs/mado";
|
|
64
|
+
import { findProduct, products, type Product } from "../lib/products.js";
|
|
65
|
+
|
|
66
|
+
component("x-product-page", ({ host }) => {
|
|
67
|
+
return () => {
|
|
68
|
+
const p = findProduct(host.dataset.slug);
|
|
69
|
+
return p
|
|
70
|
+
? html`<h1>${p.name}</h1><p>${p.description}</p>`
|
|
71
|
+
: html`<p>Introuvable.</p>`;
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export default page<{ slug: string }, Product | undefined>({
|
|
76
|
+
title: ({ slug }) => `${findProduct(slug)?.name} — MaBoutique`,
|
|
77
|
+
|
|
78
|
+
head: ({ slug }, baked) => {
|
|
79
|
+
const p = baked ?? findProduct(slug);
|
|
80
|
+
if (!p) return {};
|
|
81
|
+
return {
|
|
82
|
+
description: p.description,
|
|
83
|
+
canonical: `/product/${p.slug}`,
|
|
84
|
+
og: { title: p.name, image: p.image, type: "product" },
|
|
85
|
+
jsonLd: {
|
|
86
|
+
"@context": "https://schema.org",
|
|
87
|
+
"@type": "Product",
|
|
88
|
+
name: p.name,
|
|
89
|
+
offers: { "@type": "Offer", price: p.price, priceCurrency: p.currency },
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
bake: {
|
|
95
|
+
paths: () => products.map((p) => ({ slug: p.slug })),
|
|
96
|
+
data: ({ slug }) => findProduct(slug),
|
|
97
|
+
revalidate: 3600,
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
view: ({ params }) =>
|
|
101
|
+
html`<x-product-page data-slug=${params.slug}></x-product-page>`,
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
// src/routes.ts
|
|
107
|
+
import { routes, type RoutesMap } from "@madojs/mado";
|
|
108
|
+
|
|
109
|
+
// Exporter À LA FOIS default (RouterApi pour le runtime) ET manifest (pour le script bake).
|
|
110
|
+
export const manifest: RoutesMap = {
|
|
111
|
+
"/": () => import("./pages/home.js"),
|
|
112
|
+
"/product/:slug": () => import("./pages/product.js"),
|
|
113
|
+
"*": () => import("./pages/not-found.js"),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export default routes(manifest, { titleSuffix: " · MaBoutique" });
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Exécution
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npm install -D linkedom esbuild
|
|
123
|
+
npm run build
|
|
124
|
+
npm run bake
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Vous obtenez :
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
out/
|
|
131
|
+
├── product/
|
|
132
|
+
│ ├── mado-mug/index.html ← HTML avec meta + JSON-LD
|
|
133
|
+
│ ├── raw-bundler/index.html
|
|
134
|
+
│ └── shadow-dom/index.html
|
|
135
|
+
└── sitemap.xml
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Contenu du HTML généré
|
|
139
|
+
|
|
140
|
+
```html
|
|
141
|
+
<head>
|
|
142
|
+
<title>Mado Mug — MaBoutique</title>
|
|
143
|
+
<meta name="description" content="..." data-mado-head="baked">
|
|
144
|
+
<link rel="canonical" href="/product/mado-mug" data-mado-head="baked">
|
|
145
|
+
<meta property="og:title" content="Mado Mug" data-mado-head="baked">
|
|
146
|
+
<meta property="og:image" content="..." data-mado-head="baked">
|
|
147
|
+
<script type="application/ld+json" data-mado-head="baked">
|
|
148
|
+
{"@context":"https://schema.org","@type":"Product","..."}
|
|
149
|
+
</script>
|
|
150
|
+
<meta name="bake-revalidate" content="3600" data-mado-head="baked">
|
|
151
|
+
<meta name="bake-stamp" content="1234567890" data-mado-head="baked">
|
|
152
|
+
</head>
|
|
153
|
+
<body>
|
|
154
|
+
<div id="app">
|
|
155
|
+
<x-product-page data-slug="mado-mug">
|
|
156
|
+
<h1>Mado Mug</h1>
|
|
157
|
+
<p>Un mug avec l'inscription "zéro dépendances".</p>
|
|
158
|
+
<strong>12 EUR</strong>
|
|
159
|
+
</x-product-page>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<!-- données préchargées pour l'hydratation -->
|
|
163
|
+
<script id="bake" type="application/json">
|
|
164
|
+
{"slug":"mado-mug","name":"Mado Mug","price":12,"..."}
|
|
165
|
+
</script>
|
|
166
|
+
|
|
167
|
+
<script type="module" src="/dist/examples/main.js"></script>
|
|
168
|
+
</body>
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Après le chargement du JS :
|
|
172
|
+
|
|
173
|
+
1. Les Web Components `<x-product-page>` s'animent (le navigateur connaît déjà les custom elements).
|
|
174
|
+
2. `page.load()` reçoit `baked` comme initialData — pas d'appels `fetch` inutiles.
|
|
175
|
+
3. La navigation SPA continue de fonctionner normalement.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Cookbook : Scénarios typiques
|
|
180
|
+
|
|
181
|
+
### Blog (Markdown → bake)
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
// src/lib/posts.ts
|
|
185
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
186
|
+
// (ce module est importé uniquement depuis le script bake,
|
|
187
|
+
// il ne doit pas se retrouver dans le graph navigateur — mettez-le dans lib/server/ ou excluez-le)
|
|
188
|
+
|
|
189
|
+
export const allPosts = () =>
|
|
190
|
+
readdirSync("content/blog").map((file) => ({
|
|
191
|
+
slug: file.replace(/\.md$/, ""),
|
|
192
|
+
...parseFrontmatter(readFileSync(`content/blog/${file}`, "utf-8")),
|
|
193
|
+
}));
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
// src/pages/blog-post.ts
|
|
198
|
+
import { page, html } from "@madojs/mado";
|
|
199
|
+
import { allPosts } from "../lib/posts.js";
|
|
200
|
+
|
|
201
|
+
export default page<{ slug: string }>({
|
|
202
|
+
title: ({ slug }) => allPosts().find((p) => p.slug === slug)?.title ?? slug,
|
|
203
|
+
head: ({ slug }, post) => ({
|
|
204
|
+
description: post?.excerpt,
|
|
205
|
+
canonical: `/blog/${slug}`,
|
|
206
|
+
og: { title: post?.title, type: "article" },
|
|
207
|
+
}),
|
|
208
|
+
bake: {
|
|
209
|
+
paths: () => allPosts().map(({ slug }) => ({ slug })),
|
|
210
|
+
data: ({ slug }) => allPosts().find((p) => p.slug === slug),
|
|
211
|
+
},
|
|
212
|
+
view: ({ params }) =>
|
|
213
|
+
html`<x-blog-post data-slug=${params.slug}></x-blog-post>`,
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Catalogue produits (e-commerce, ≤ 10k SKUs)
|
|
218
|
+
|
|
219
|
+
- `bake.paths` → SELECT slug FROM products
|
|
220
|
+
- `bake.data` → SELECT * FROM products WHERE slug=?
|
|
221
|
+
- Invalidation complète toutes les N heures via cron + `npm run bake`
|
|
222
|
+
- Invalidation ciblée d'un seul produit : webhook → rebuild uniquement cette entrée `paths`
|
|
223
|
+
|
|
224
|
+
### Documentation
|
|
225
|
+
|
|
226
|
+
- `bake.paths` — parcours du système de fichiers `docs/**/*.md`
|
|
227
|
+
- `bake.data` — parsing Markdown
|
|
228
|
+
- `head.jsonLd` — `TechArticle`
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Revalidate / CDN
|
|
233
|
+
|
|
234
|
+
`bake.revalidate: 3600` écrit `<meta name="bake-revalidate" content="3600">` et `bake-stamp`
|
|
235
|
+
dans le HTML. C'est des **métadonnées** — le framework ne re-bake rien lui-même. Stratégies :
|
|
236
|
+
|
|
237
|
+
1. **Option la plus simple** : cron dans CI — `npm run bake && rsync out/ origin:/var/www/`.
|
|
238
|
+
2. **Via CDN** (Cloudflare/Fastly) : servir le HTML avec `Cache-Control: max-age=3600`. Le CDN
|
|
239
|
+
s'invalide lui-même.
|
|
240
|
+
3. **Déclencheur webhook** : API boutique → POST `/_revalidate?path=/product/mado-mug` → CI
|
|
241
|
+
re-bake uniquement cette page (vous pouvez implémenter `bake.paths` pour retourner une liste
|
|
242
|
+
ciblée basée sur un paramètre d'environnement).
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Comparaison avec les alternatives
|
|
247
|
+
|
|
248
|
+
| | Next.js SSG/ISR | playwright-prerender | **mado bake** |
|
|
249
|
+
|--------------------------|--------------------|----------------------|-----------------|
|
|
250
|
+
| Chrome requis en CI | non | **oui** (~300 Mo) | **non** |
|
|
251
|
+
| Node requis en production | pour ISR | non | **non** |
|
|
252
|
+
| Temps pour 1000 pages | minutes | minutes | **secondes** |
|
|
253
|
+
| Niveau de magie | élevé | faible | **zéro** |
|
|
254
|
+
| Parser HTML | moteur React | navigateur | linkedom (~50 Ko) |
|
|
255
|
+
| Configuration | next.config.js + … | script unique | script unique |
|
|
256
|
+
| Source de vérité vue+données | séparées | page | **une seule page** |
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Limitations et pièges
|
|
261
|
+
|
|
262
|
+
- **Il n'y a pas de navigateur côté bake.** Ne fonctionnent pas : `setTimeout` (techniquement
|
|
263
|
+
ça marche, mais bake se termine avant qu'il ne se déclenche), `fetch` vers des URLs relatives,
|
|
264
|
+
tous les effets de bord `effect()`/`signal()`, le vrai `requestAnimationFrame`. La fonction
|
|
265
|
+
de rendu doit être déterministe selon `params` / `data`.
|
|
266
|
+
- **`linkedom` ≠ navigateur.** Toutes les API DOM ne sont pas supportées (par exemple,
|
|
267
|
+
`HTMLElement.click()` se comporte plus simplement). La logique complexe dans les Web Components
|
|
268
|
+
ne s'exécutera dans le navigateur qu'après `connectedCallback` ; seul ce qui a été rendu
|
|
269
|
+
de façon synchrone lors du premier passage se retrouvera dans le HTML baked.
|
|
270
|
+
- **Rendre le contenu dynamique côté client.** L'heure actuelle, les tests A/B, les
|
|
271
|
+
banners géo, le panier de l'utilisateur — ceux-ci ne doivent pas être dans le HTML baked.
|
|
272
|
+
Utilisez `effect()` pour le rendu côté client.
|
|
273
|
+
- **Les imports côté serveur ne doivent pas se retrouver dans le graph client.** Si
|
|
274
|
+
`lib/posts.ts` importe `node:fs`, il ne peut pas être importé depuis `view`. Gardez ces
|
|
275
|
+
modules dans un dossier séparé (`lib/build/`) et utilisez-les uniquement depuis
|
|
276
|
+
`bake.paths`/`bake.data`.
|
|
277
|
+
- **`paths` et `data` sont exécutés à chaque exécution de bake.** S'ils impliquent une
|
|
278
|
+
requête de base de données lourde — mettez en cache au niveau du script.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## TL;DR
|
|
283
|
+
|
|
284
|
+
Si une page est **identique pour tous les utilisateurs**, a **un ensemble d'URLs relativement
|
|
285
|
+
stable**, et que **le SEO + le premier affichage** comptent — ajoutez `bake: { paths, data }`
|
|
286
|
+
et obtenez du HTML statique avec meta/JSON-LD/sitemap en millisecondes. Pas de serveur node,
|
|
287
|
+
pas de Chrome, pas de magie.
|
|
288
|
+
|
|
289
|
+
Si la page est personnalisée, ou qu'il y a des millions d'URLs, ou que le contenu change en
|
|
290
|
+
temps réel — `bake` n'est pas votre outil. Gardez-la en SPA ou intégrez un framework SSR séparé.
|