@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.
Files changed (162) hide show
  1. package/AGENTS.md +291 -0
  2. package/CHANGELOG.md +23 -0
  3. package/LICENSE +21 -0
  4. package/README.md +371 -0
  5. package/ROADMAP.md +52 -0
  6. package/dist/src/component.d.ts +48 -0
  7. package/dist/src/component.js +140 -0
  8. package/dist/src/component.js.map +1 -0
  9. package/dist/src/context.d.ts +40 -0
  10. package/dist/src/context.js +67 -0
  11. package/dist/src/context.js.map +1 -0
  12. package/dist/src/css.d.ts +54 -0
  13. package/dist/src/css.js +137 -0
  14. package/dist/src/css.js.map +1 -0
  15. package/dist/src/devtools.d.ts +22 -0
  16. package/dist/src/devtools.js +63 -0
  17. package/dist/src/devtools.js.map +1 -0
  18. package/dist/src/diagnostics.d.ts +11 -0
  19. package/dist/src/diagnostics.js +28 -0
  20. package/dist/src/diagnostics.js.map +1 -0
  21. package/dist/src/each.d.ts +39 -0
  22. package/dist/src/each.js +35 -0
  23. package/dist/src/each.js.map +1 -0
  24. package/dist/src/forms.d.ts +71 -0
  25. package/dist/src/forms.js +161 -0
  26. package/dist/src/forms.js.map +1 -0
  27. package/dist/src/head.d.ts +19 -0
  28. package/dist/src/head.js +97 -0
  29. package/dist/src/head.js.map +1 -0
  30. package/dist/src/html/bindings.d.ts +78 -0
  31. package/dist/src/html/bindings.js +304 -0
  32. package/dist/src/html/bindings.js.map +1 -0
  33. package/dist/src/html/parser.d.ts +64 -0
  34. package/dist/src/html/parser.js +521 -0
  35. package/dist/src/html/parser.js.map +1 -0
  36. package/dist/src/html/template-types.d.ts +27 -0
  37. package/dist/src/html/template-types.js +8 -0
  38. package/dist/src/html/template-types.js.map +1 -0
  39. package/dist/src/html/template.d.ts +45 -0
  40. package/dist/src/html/template.js +119 -0
  41. package/dist/src/html/template.js.map +1 -0
  42. package/dist/src/html.d.ts +16 -0
  43. package/dist/src/html.js +16 -0
  44. package/dist/src/html.js.map +1 -0
  45. package/dist/src/index.d.ts +35 -0
  46. package/dist/src/index.js +39 -0
  47. package/dist/src/index.js.map +1 -0
  48. package/dist/src/lazy.d.ts +38 -0
  49. package/dist/src/lazy.js +73 -0
  50. package/dist/src/lazy.js.map +1 -0
  51. package/dist/src/lifecycle.d.ts +45 -0
  52. package/dist/src/lifecycle.js +66 -0
  53. package/dist/src/lifecycle.js.map +1 -0
  54. package/dist/src/page.d.ts +161 -0
  55. package/dist/src/page.js +38 -0
  56. package/dist/src/page.js.map +1 -0
  57. package/dist/src/persisted.d.ts +47 -0
  58. package/dist/src/persisted.js +119 -0
  59. package/dist/src/persisted.js.map +1 -0
  60. package/dist/src/resource.d.ts +120 -0
  61. package/dist/src/resource.js +275 -0
  62. package/dist/src/resource.js.map +1 -0
  63. package/dist/src/router/manifest.d.ts +56 -0
  64. package/dist/src/router/manifest.js +302 -0
  65. package/dist/src/router/manifest.js.map +1 -0
  66. package/dist/src/router/match.d.ts +62 -0
  67. package/dist/src/router/match.js +117 -0
  68. package/dist/src/router/match.js.map +1 -0
  69. package/dist/src/router/navigation.d.ts +89 -0
  70. package/dist/src/router/navigation.js +263 -0
  71. package/dist/src/router/navigation.js.map +1 -0
  72. package/dist/src/router.d.ts +13 -0
  73. package/dist/src/router.js +13 -0
  74. package/dist/src/router.js.map +1 -0
  75. package/dist/src/signal.d.ts +67 -0
  76. package/dist/src/signal.js +238 -0
  77. package/dist/src/signal.js.map +1 -0
  78. package/docs/README.md +12 -0
  79. package/docs/en/00-the-mado-way.md +106 -0
  80. package/docs/en/01-routing.md +204 -0
  81. package/docs/en/02-project-layout.md +58 -0
  82. package/docs/en/03-static-bake.md +251 -0
  83. package/docs/en/04-ide-setup.md +162 -0
  84. package/docs/en/05-why-mado.md +193 -0
  85. package/docs/en/06-for-backenders.md +422 -0
  86. package/docs/en/07-llm-pitfalls.md +486 -0
  87. package/docs/en/08-llm-zero-history-test.md +56 -0
  88. package/docs/en/09-shadow-vs-light-dom.md +122 -0
  89. package/docs/en/README.md +16 -0
  90. package/docs/fr/00-the-mado-way.md +108 -0
  91. package/docs/fr/01-routing.md +202 -0
  92. package/docs/fr/02-project-layout.md +58 -0
  93. package/docs/fr/03-static-bake.md +290 -0
  94. package/docs/fr/04-ide-setup.md +162 -0
  95. package/docs/fr/05-why-mado.md +193 -0
  96. package/docs/fr/06-for-backenders.md +432 -0
  97. package/docs/fr/07-llm-pitfalls.md +487 -0
  98. package/docs/fr/08-llm-zero-history-test.md +60 -0
  99. package/docs/fr/09-shadow-vs-light-dom.md +121 -0
  100. package/docs/fr/README.md +16 -0
  101. package/docs/ru/00-the-mado-way.md +93 -0
  102. package/docs/ru/01-routing.md +194 -0
  103. package/docs/ru/02-project-layout.md +57 -0
  104. package/docs/ru/03-static-bake.md +251 -0
  105. package/docs/ru/04-ide-setup.md +144 -0
  106. package/docs/ru/05-why-mado.md +193 -0
  107. package/docs/ru/06-for-backenders.md +422 -0
  108. package/docs/ru/07-llm-pitfalls.md +485 -0
  109. package/docs/ru/08-llm-zero-history-test.md +56 -0
  110. package/docs/ru/09-shadow-vs-light-dom.md +122 -0
  111. package/docs/ru/README.md +14 -0
  112. package/docs/uk/00-the-mado-way.md +54 -0
  113. package/docs/uk/01-routing.md +82 -0
  114. package/docs/uk/02-project-layout.md +46 -0
  115. package/docs/uk/03-static-bake.md +49 -0
  116. package/docs/uk/04-ide-setup.md +26 -0
  117. package/docs/uk/05-why-mado.md +34 -0
  118. package/docs/uk/06-for-backenders.md +50 -0
  119. package/docs/uk/07-llm-pitfalls.md +82 -0
  120. package/docs/uk/08-llm-zero-history-test.md +31 -0
  121. package/docs/uk/09-shadow-vs-light-dom.md +40 -0
  122. package/docs/uk/README.md +16 -0
  123. package/llms.txt +155 -0
  124. package/package.json +81 -0
  125. package/scripts/bake.mjs +406 -0
  126. package/scripts/bundle.mjs +146 -0
  127. package/scripts/cli.mjs +382 -0
  128. package/scripts/new.mjs +80 -0
  129. package/scripts/preview.mjs +176 -0
  130. package/scripts/release-notes.mjs +66 -0
  131. package/scripts/showcase-regression.mjs +392 -0
  132. package/server/serve.mjs +292 -0
  133. package/starters/crud/README.md +21 -0
  134. package/starters/crud/index.html +20 -0
  135. package/starters/crud/package.json +17 -0
  136. package/starters/crud/src/components/app-shell.ts +51 -0
  137. package/starters/crud/src/components/ticket-detail.ts +33 -0
  138. package/starters/crud/src/components/ticket-form.ts +69 -0
  139. package/starters/crud/src/components/ticket-list.ts +66 -0
  140. package/starters/crud/src/lib/api.ts +76 -0
  141. package/starters/crud/src/main.ts +12 -0
  142. package/starters/crud/src/pages/home.ts +18 -0
  143. package/starters/crud/src/pages/not-found.ts +12 -0
  144. package/starters/crud/src/pages/ticket-detail.ts +6 -0
  145. package/starters/crud/src/pages/ticket-new.ts +6 -0
  146. package/starters/crud/src/pages/tickets.ts +6 -0
  147. package/starters/crud/src/routes.ts +9 -0
  148. package/starters/crud/src/styles/global.ts +155 -0
  149. package/starters/crud/tsconfig.json +15 -0
  150. package/starters/minimal/README.md +19 -0
  151. package/starters/minimal/index.html +20 -0
  152. package/starters/minimal/package.json +17 -0
  153. package/starters/minimal/src/components/app-counter.ts +31 -0
  154. package/starters/minimal/src/main.ts +9 -0
  155. package/starters/minimal/src/pages/home.ts +18 -0
  156. package/starters/minimal/src/pages/not-found.ts +14 -0
  157. package/starters/minimal/src/routes.ts +6 -0
  158. package/starters/minimal/src/styles/global.ts +60 -0
  159. package/starters/minimal/tsconfig.json +15 -0
  160. package/templates/page-detail.ts +63 -0
  161. package/templates/page-form.ts +94 -0
  162. 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é.