@nodefony/framework 10.0.0-alpha.1

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 (96) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +50 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/index.js +211 -0
  6. package/dist/nodefony/config/config.js +61 -0
  7. package/dist/nodefony/config/defineModuleConfig.js +36 -0
  8. package/dist/nodefony/controller/AdminApiController.js +163 -0
  9. package/dist/nodefony/controller/ApiKeyController.js +151 -0
  10. package/dist/nodefony/controller/BenchController.js +132 -0
  11. package/dist/nodefony/controller/IssuerMetadataController.js +148 -0
  12. package/dist/nodefony/controller/OAuth2Controller.js +133 -0
  13. package/dist/nodefony/controller/ProtectedResourceMetadataController.js +221 -0
  14. package/dist/nodefony/controller/SessionAuthController.js +141 -0
  15. package/dist/nodefony/controller/TokenAuthController.js +113 -0
  16. package/dist/nodefony/controller/TotpController.js +129 -0
  17. package/dist/nodefony/controller/WebAuthnController.js +242 -0
  18. package/dist/nodefony/controller/oauthAuthority.js +74 -0
  19. package/dist/nodefony/decorators/routerDecorators.js +967 -0
  20. package/dist/nodefony/interfaces/IAdminBroker.js +1 -0
  21. package/dist/nodefony/interfaces/IController.js +1 -0
  22. package/dist/nodefony/interfaces/IIdempotencyStore.js +1 -0
  23. package/dist/nodefony/interfaces/IResolver.js +1 -0
  24. package/dist/nodefony/interfaces/IRoute.js +1 -0
  25. package/dist/nodefony/interfaces/index.js +1 -0
  26. package/dist/nodefony/service/AdminBroker.js +106 -0
  27. package/dist/nodefony/service/Eta.js +68 -0
  28. package/dist/nodefony/service/IdempotencyStore.js +136 -0
  29. package/dist/nodefony/service/router.js +243 -0
  30. package/dist/nodefony/src/Controller.js +515 -0
  31. package/dist/nodefony/src/FrameworkAdminApi.js +268 -0
  32. package/dist/nodefony/src/KernelAdminApi.js +1243 -0
  33. package/dist/nodefony/src/PlaygroundAdminApi.js +97 -0
  34. package/dist/nodefony/src/RedisIdempotencyStore.js +254 -0
  35. package/dist/nodefony/src/Resolver.js +416 -0
  36. package/dist/nodefony/src/ResourceController.js +148 -0
  37. package/dist/nodefony/src/Route.js +476 -0
  38. package/dist/nodefony/src/SyslogAdminApi.js +466 -0
  39. package/dist/nodefony/src/Template.js +15 -0
  40. package/dist/nodefony/src/configMutation.js +186 -0
  41. package/dist/nodefony/src/docsReader.js +929 -0
  42. package/dist/nodefony/src/idempotency.js +137 -0
  43. package/dist/nodefony/src/idempotencyGc.js +32 -0
  44. package/dist/nodefony/src/idempotencyStoreRegistry.js +36 -0
  45. package/dist/nodefony/src/scopeCatalog.js +40 -0
  46. package/dist/nodefony/src/syslogFilters.js +51 -0
  47. package/dist/types/index.d.ts +96 -0
  48. package/dist/types/nodefony/config/config.d.ts +41 -0
  49. package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
  50. package/dist/types/nodefony/controller/AdminApiController.d.ts +70 -0
  51. package/dist/types/nodefony/controller/ApiKeyController.d.ts +49 -0
  52. package/dist/types/nodefony/controller/BenchController.d.ts +45 -0
  53. package/dist/types/nodefony/controller/IssuerMetadataController.d.ts +86 -0
  54. package/dist/types/nodefony/controller/OAuth2Controller.d.ts +81 -0
  55. package/dist/types/nodefony/controller/ProtectedResourceMetadataController.d.ts +147 -0
  56. package/dist/types/nodefony/controller/SessionAuthController.d.ts +74 -0
  57. package/dist/types/nodefony/controller/TokenAuthController.d.ts +56 -0
  58. package/dist/types/nodefony/controller/TotpController.d.ts +42 -0
  59. package/dist/types/nodefony/controller/WebAuthnController.d.ts +123 -0
  60. package/dist/types/nodefony/controller/oauthAuthority.d.ts +54 -0
  61. package/dist/types/nodefony/decorators/routerDecorators.d.ts +632 -0
  62. package/dist/types/nodefony/interfaces/IAdminBroker.d.ts +79 -0
  63. package/dist/types/nodefony/interfaces/IController.d.ts +38 -0
  64. package/dist/types/nodefony/interfaces/IIdempotencyStore.d.ts +1 -0
  65. package/dist/types/nodefony/interfaces/IResolver.d.ts +25 -0
  66. package/dist/types/nodefony/interfaces/IRoute.d.ts +31 -0
  67. package/dist/types/nodefony/interfaces/index.d.ts +5 -0
  68. package/dist/types/nodefony/service/AdminBroker.d.ts +37 -0
  69. package/dist/types/nodefony/service/Eta.d.ts +25 -0
  70. package/dist/types/nodefony/service/IdempotencyStore.d.ts +45 -0
  71. package/dist/types/nodefony/service/router.d.ts +53 -0
  72. package/dist/types/nodefony/src/Controller.d.ts +193 -0
  73. package/dist/types/nodefony/src/FrameworkAdminApi.d.ts +35 -0
  74. package/dist/types/nodefony/src/KernelAdminApi.d.ts +71 -0
  75. package/dist/types/nodefony/src/PlaygroundAdminApi.d.ts +98 -0
  76. package/dist/types/nodefony/src/RedisIdempotencyStore.d.ts +104 -0
  77. package/dist/types/nodefony/src/Resolver.d.ts +165 -0
  78. package/dist/types/nodefony/src/ResourceController.d.ts +171 -0
  79. package/dist/types/nodefony/src/Route.d.ts +192 -0
  80. package/dist/types/nodefony/src/SyslogAdminApi.d.ts +38 -0
  81. package/dist/types/nodefony/src/Template.d.ts +8 -0
  82. package/dist/types/nodefony/src/configMutation.d.ts +109 -0
  83. package/dist/types/nodefony/src/docsReader.d.ts +369 -0
  84. package/dist/types/nodefony/src/idempotency.d.ts +96 -0
  85. package/dist/types/nodefony/src/idempotencyGc.d.ts +30 -0
  86. package/dist/types/nodefony/src/idempotencyStoreRegistry.d.ts +59 -0
  87. package/dist/types/nodefony/src/scopeCatalog.d.ts +26 -0
  88. package/dist/types/nodefony/src/syslogFilters.d.ts +52 -0
  89. package/docs/admin.md +451 -0
  90. package/docs/controller.md +645 -0
  91. package/docs/decorateurs.md +845 -0
  92. package/docs/idempotence.md +741 -0
  93. package/docs/index.md +151 -0
  94. package/docs/routing.md +648 -0
  95. package/docs/templates.md +380 -0
  96. package/package.json +83 -0
@@ -0,0 +1,380 @@
1
+ ---
2
+ title: "Templates — le moteur de rendu de vues (Eta)"
3
+ navTitle: Templates
4
+ lang: fr
5
+ module: "@nodefony/framework"
6
+ topic: templates
7
+ section: "Cœur runtime"
8
+ audience: [developer]
9
+ tags: [templates, eta, vues, rendu, html, xss, echappement, ssr]
10
+ version: "doc"
11
+ status: stable
12
+ updated: 2026-07-21
13
+ source: "src/packages/@nodefony/framework/docs/templates.md"
14
+ coverageModule: framework
15
+ coverageFiles: Template.ts,Eta.ts,Controller.ts
16
+ ---
17
+
18
+ # Templates — le moteur de rendu de vues (Eta)
19
+
20
+ > Quand une route doit renvoyer une **page HTML** plutôt que du JSON, il faut coller des données dans
21
+ > du texte : c'est le rôle du moteur de templates. Nodefony n'en a qu'un — **Eta** — et le branche au
22
+ > minimum : ton contrôleur appelle `renderView()`, le framework lit le fichier `.eta`, l'exécute avec
23
+ > tes variables et pose `Content-Type: text/html`. La défense clé est l'**échappement HTML par
24
+ > défaut** (anti-XSS). Ancré sur `Eta.ts`, `Template.ts` et `Controller.renderView()`.
25
+
26
+ 📍 [Documentation](../../../../../docs/index.md) › [Framework](index.md) › **Templates**
27
+
28
+ ## 🧠 Le modèle mental — un formulaire à trous
29
+
30
+ Un template est un texte **à trous** (le HTML fixe) que le moteur remplit avec des **données** (les
31
+ locals) pour produire la page finale. Nodefony fait ce remplissage **côté serveur** (SSR), à chaque
32
+ requête, puis renvoie le résultat comme n'importe quel corps de réponse.
33
+
34
+ ```mermaid
35
+ flowchart LR
36
+ A["ton action<br/>renderView(chemin, locals)"] --> B["FileClass<br/>lit le fichier .eta"]
37
+ B --> C["Eta.render(source, locals)<br/>exécute le template"]
38
+ C -->|"&lt;%= %&gt; échappé (anti-XSS)"| D["HTML produit"]
39
+ D --> E["renderResponse<br/>Content-Type: text/html"]
40
+ E --> OUT["réponse HTTP<br/>ou frame WebSocket"]
41
+ FE["service frontend<br/>(optionnel)"] -.->|"frontendTags · asset"| C
42
+ ```
43
+
44
+ Deux idées à retenir :
45
+
46
+ 1. **Le contrôleur lit le fichier, le moteur ne fait que rendre une chaîne.** `renderView()` résout le
47
+ chemin, lit l'octet, puis passe la **source** à Eta (`Controller.renderView()`, `Controller.ts:308`).
48
+ Il n'y a **pas** de dossier `views/` magique connu du moteur.
49
+ 2. **L'échappement est automatique.** Une donnée interpolée par `<%= %>` est neutralisée (`<` devient
50
+ `&lt;`) avant d'entrer dans le HTML — c'est la protection XSS, active par défaut
51
+ (`autoEscape`, `Eta.ts:16`).
52
+
53
+ ## 📖 Lexique
54
+
55
+ | Terme | Sens (dans cette page) |
56
+ | ---------------- | ----------------------------------------------------------------------------------------------------- |
57
+ | Template / vue | Un fichier `.eta` : du HTML fixe + des balises qui insèrent des données. |
58
+ | Moteur de vues | Le composant qui exécute le template avec des données → produit la page. Ici **Eta**. |
59
+ | Eta | Moteur de templates écrit en TypeScript, syntaxe `<% %>` façon EJS. Unique moteur de Nodefony. |
60
+ | Local(s) | Les variables passées au template (`{ name, nodefony }`) — les « données » qui remplissent les trous. |
61
+ | SSR | _Server-Side Rendering_ : la page HTML est fabriquée sur le serveur, pas dans le navigateur. |
62
+ | Interpolation | Insérer une valeur dans la sortie : `<%= valeur %>` (échappée) ou `<%~ valeur %>` (brute). |
63
+ | Échappement HTML | Transformer `< > & " '` en entités (`&lt;`…) pour qu'une donnée soit **affichée**, jamais exécutée. |
64
+ | XSS | _Cross-Site Scripting_ : un attaquant injecte du HTML/JS via une donnée ; l'échappement le désamorce. |
65
+ | `autoEscape` | L'option Eta qui échappe `<%= %>` par défaut (activée dans Nodefony). |
66
+ | `useWith` | L'option Eta qui expose les locals **nus** (`<%= name %>`) au lieu de `<%= it.name %>`. |
67
+ | Phase `render` | Le temps chronométré de production du corps par le moteur (visible dans la debug bar). |
68
+
69
+ ## Qu'est-ce que c'est ?
70
+
71
+ Imagine une lettre type avec des blancs : « Bonjour **\___**, ta commande **\___** est prête. » Le moteur
72
+ de templates prend cette lettre (le fichier `.eta`) et les données (`{ nom, commande }`), remplit les
73
+ blancs, et te rend la lettre finie. C'est exactement ce qu'un serveur fait pour produire une page HTML
74
+ personnalisée à partir d'un gabarit unique.
75
+
76
+ Le piège de cette opération, c'est la **sécurité**. Si une donnée vient de l'utilisateur (un pseudo,
77
+ un commentaire) et qu'on la recolle **nue** dans le HTML, un attaquant peut y glisser
78
+ `<script>vole_le_cookie()</script>` : le navigateur de la **victime** l'exécutera comme du code de ton
79
+ site. C'est une faille **XSS**. La parade est l'**échappement** : on remplace `<` par `&lt;`, `>` par
80
+ `&gt;`, etc. — le navigateur **affiche** alors le texte au lieu de l'**exécuter**.
81
+
82
+ > [!IMPORTANT]
83
+ > Eta échappe **par défaut** avec `<%= %>`. Tu ne désactives cette protection **que** volontairement,
84
+ > avec `<%~ %>` (sortie brute) — à réserver à du HTML que **tu** as produit et en qui tu as confiance,
85
+ > jamais à une donnée utilisateur.
86
+
87
+ ## La vision Nodefony
88
+
89
+ Nodefony a **un seul** moteur de vues : **Eta** (il remplace Twig et EJS, retirés). Le choix est
90
+ documenté au source (`Eta`, `Eta.ts:34`) : écrit en TypeScript (types fournis, pas de `@types/*`), ESM
91
+ natif, échappement natif, et surtout des délimiteurs `<% %>` qui **n'entrent pas en collision** avec la
92
+ syntaxe TS/JSON/JSX — décisif car le même moteur sert aussi à générer du code (le scaffold `create`).
93
+
94
+ Le branchement est **délibérément minimal** :
95
+
96
+ - Le service Eta est enregistré au boot sous le nom `template` (`@services([Router, Eta, …])`,
97
+ `nodefony/framework/index.ts:76`) ; chaque contrôleur le récupère à sa construction
98
+ (`this.get<Eta>("template")`, `Controller.ts:253`).
99
+ - Le moteur ne connaît **que le rendu d'une chaîne** : `Eta.render(source, data)` appelle
100
+ `renderStringAsync` (`Eta.ts:51`). C'est le contrôleur qui lit le fichier — pas Eta.
101
+ - Deux options seulement sont posées, plus le cache : `autoEscape`, `useWith`, `cache`
102
+ (`defaultOption`, `Eta.ts:15`). Il n'y a **pas** de racine `views/`, donc **pas** de résolution
103
+ d'`include`/layout par nom (voir Pièges).
104
+
105
+ > [!NOTE]
106
+ > Le rendu **d'erreurs** ne passe **pas** par les templates : une exception devient un corps **JSON
107
+ > structuré** (`ErrorRenderer.renderHttp()`, `error-renderer.ts:348`), jamais une page Eta. Le moteur
108
+ > de vues ne sert que le HTML **que tu rends explicitement**.
109
+
110
+ ## 🚀 Démarrage rapide
111
+
112
+ Une vue Eta est un fichier `.eta` ; l'action la rend avec `renderView(chemin, locals)`. Voici le tout —
113
+ le contrôleur, la vue, et ce qu'on observe.
114
+
115
+ ### 1. La vue — `nodefony/views/hello.eta`
116
+
117
+ ```eta
118
+ <!doctype html>
119
+ <html lang="fr">
120
+ <head>
121
+ <meta charset="utf-8" />
122
+ <title><%= nodefony.name %></title>
123
+ </head>
124
+ <body>
125
+ <!-- <%= %> ÉCHAPPE : si name vaut "<b>x</b>", la page affiche le texte, ne l'exécute pas -->
126
+ <h1>Bonjour <%= name %></h1>
127
+ <% if (name === "cci") { %>
128
+ <p>Salut l'auteur.</p>
129
+ <% } %>
130
+ </body>
131
+ </html>
132
+ ```
133
+
134
+ ### 2. Le contrôleur — rend la vue, renvoie du HTML
135
+
136
+ ```typescript
137
+ // nodefony/controllers/HelloController.ts — compile tel quel
138
+ import { Controller, controller, Get, Param } from "@nodefony/framework";
139
+ import type { ContextType } from "@nodefony/http";
140
+ import { resolve } from "node:path";
141
+
142
+ @controller("/hello")
143
+ class HelloController extends Controller {
144
+ constructor(context: ContextType) {
145
+ super("hello", context);
146
+ }
147
+
148
+ // GET /hello/:name → rend `hello.eta` avec le local `name`.
149
+ @Get("/{name}")
150
+ async index(@Param("name") name: string) {
151
+ // TU résous le chemin de la vue : pas de dossier `views/` implicite.
152
+ const view = resolve(
153
+ this.module?.path as string,
154
+ "nodefony",
155
+ "views",
156
+ "hello.eta",
157
+ );
158
+ // renderView lit le fichier, appelle Eta, pose Content-Type: text/html.
159
+ // `nodefony.*` (name, requestId…) vient de metaData ; `name` est à toi et
160
+ // prime sur les locals frontend (spread en dernier).
161
+ return this.renderView(view, { name, ...this.context?.metaData });
162
+ }
163
+ }
164
+
165
+ export default HelloController;
166
+ ```
167
+
168
+ Câblage : `@controllers([HelloController])` sur ta classe `Module` (fait par
169
+ `nodefony create controller`). Aucune config à écrire — le service `template` existe déjà.
170
+
171
+ ### 3. Ce qu'on observe
172
+
173
+ ```bash
174
+ # 1) La donnée est interpolée ET échappée
175
+ curl -s http://localhost:5151/hello/cci
176
+ # <!doctype html> … <h1>Bonjour cci</h1> <p>Salut l'auteur.</p> …
177
+
178
+ # 2) En-tête posé automatiquement par renderView()
179
+ curl -si http://localhost:5151/hello/cci | grep -i content-type
180
+ # Content-Type: text/html
181
+
182
+ # 3) Une donnée « piégée » est neutralisée (anti-XSS) : le <b> devient du texte
183
+ curl -s 'http://localhost:5151/hello/%3Cb%3Ex%3C%2Fb%3E'
184
+ # <h1>Bonjour &lt;b&gt;x&lt;/b&gt;</h1>
185
+ ```
186
+
187
+ > [!TIP]
188
+ > Tu n'as écrit **aucun** appel d'envoi (`send`, `res.end`). `renderView()` produit le corps **et**
189
+ > l'envoie. Pour piloter l'envoi toi-même, retourne plutôt une chaîne via `render()`
190
+ > (`Controller.render()`, `Controller.ts:290`).
191
+
192
+ ## 🏗️ Architecture interne — le parcours d'un `renderView()`
193
+
194
+ Deux classes, une responsabilité chacune :
195
+
196
+ - **`Template`** (`Template.ts:2`) — la base : elle étend `Service` (donc container + logs), garde une
197
+ référence au `module` et **décide du cache** selon l'environnement (`this.cache` vrai en `prod`,
198
+ `Template.ts:20`).
199
+ - **`Eta`** (`Eta.ts:34`) — l'implémentation : elle instancie le moteur `eta` (`new EtaEngine()`,
200
+ `Eta.ts:38`), applique le cache calculé par `Template` (`this.engine.configure()`, `Eta.ts:41`), et
201
+ expose deux méthodes de rendu.
202
+
203
+ Le trajet d'un appel, étape par étape :
204
+
205
+ ```mermaid
206
+ sequenceDiagram
207
+ participant C as TON Controller
208
+ participant F as FileClass
209
+ participant E as Eta (service "template")
210
+ participant Ctx as Context
211
+ C->>F: FileClass.from(chemin) + readAsync()
212
+ C->>Ctx: phaseStart("render")
213
+ C->>E: render(source, withFrontendLocals(locals))
214
+ E-->>C: HTML (renderStringAsync)
215
+ C->>Ctx: phaseEnd("render")
216
+ C->>Ctx: setContextHtml() puis renderResponse(html)
217
+ ```
218
+
219
+ | # | Étape | Où |
220
+ | --- | -------------------------------------------- | ------------------------------------------------------------- |
221
+ | 1 | Résolution + lecture async du fichier | `FileClass` dans `renderView()` (`Controller.ts:316`) |
222
+ | 2 | Ouverture de la phase mesurée `render` | `phaseStart("render")` (`Controller.ts:322`) |
223
+ | 3 | Injection des aides frontend dans les locals | `withFrontendLocals()` (`Controller.ts:345`) |
224
+ | 4 | Rendu de la source par le moteur | `Eta.render()` → `renderStringAsync` (`Eta.ts:51`) |
225
+ | 5 | `Content-Type: text/html` puis envoi | `setContextHtml()` + `renderResponse()` (`Controller.ts:331`) |
226
+
227
+ Le point notable de l'étape 3 : `withFrontendLocals()` ajoute automatiquement `frontendTags`,
228
+ `frontendDocument` et `asset` aux locals **si** le service `frontend` est présent — et **tes** valeurs
229
+ priment (spread `param` en dernier, `Controller.ts:345`). Si le module frontend n'est pas chargé, la
230
+ fonction retourne les locals inchangés : zéro couplage dur.
231
+
232
+ ## 🔐 Sécurité — échappement HTML et XSS
233
+
234
+ La règle Eta se lit sur les délimiteurs. Trois formes, trois comportements :
235
+
236
+ | Balise | Rôle | Échappé ? | Pour… |
237
+ | ---------- | ------------------------- | :-------: | ----------------------------------------------- |
238
+ | `<%= v %>` | interpole la valeur `v` | **oui** | **toute donnée** — le cas par défaut, sûr |
239
+ | `<%~ v %>` | interpole `v` **brut** | non | du HTML de confiance que TU produis (fragments) |
240
+ | `<% … %>` | exécute du code (if/for…) | n/a | logique de template (pas de sortie directe) |
241
+
242
+ L'échappement par défaut vient de l'option `autoEscape: true` posée dans `defaultOption` (`Eta.ts:16`).
243
+ Concrètement, `<%= %>` passe la valeur dans la fonction d'échappement d'Eta, qui remplace `& < > " '`
244
+ par leurs entités HTML. Une chaîne d'attaque comme `<script>alert(1)</script>` ressort donc en texte
245
+ inerte `&lt;script&gt;alert(1)&lt;/script&gt;`.
246
+
247
+ > [!WARNING]
248
+ > `<%~ %>` **désactive** la protection. Ne l'emploie **jamais** sur une donnée qui a pu être influencée
249
+ > par un utilisateur (pseudo, commentaire, champ de formulaire, paramètre d'URL). Réserve-le à des
250
+ > fragments HTML que ton propre code a construits.
251
+
252
+ ## 🧰 API publique
253
+
254
+ Deux niveaux : ce que le **service Eta** expose, et ce que le **contrôleur** t'offre au-dessus.
255
+
256
+ ### Le service `Eta` (nom d'injection `template`)
257
+
258
+ | Méthode | Rôle | Ancre |
259
+ | ------------------------- | ---------------------------------------------------------- | ----------- |
260
+ | `render(source, data?)` | Rend un template depuis une **chaîne** (chemin chaud) | `Eta.ts:51` |
261
+ | `renderFile(path, data?)` | Lit un fichier `.eta` **puis** le rend (usages CLI/outils) | `Eta.ts:66` |
262
+
263
+ `render()` est ce qu'appelle le contrôleur ; `renderFile()` lit lui-même le fichier
264
+ (`readFile` + `renderStringAsync`, `Eta.ts:71`) pour les usages qui partent d'un chemin (générateurs,
265
+ scaffold). Les deux sont **asynchrones** (I/O non bloquante).
266
+
267
+ ### Les helpers du contrôleur
268
+
269
+ | Helper | Pour… | Ancre |
270
+ | ----------------------------------- | ------------------------------------------------------- | ------------------- |
271
+ | `renderView(path, locals, status?)` | Rendre une vue `.eta` (lit le fichier + aides frontend) | `Controller.ts:308` |
272
+ | `render(data, encoding?, status?)` | Envoyer un corps quelconque (ex. HTML déjà prêt) | `Controller.ts:273` |
273
+ | `renderJson(obj, status?)` | Réponse JSON explicite (pas un template) | `Controller.ts:392` |
274
+
275
+ Les signatures exactes vivent dans le graphe symbolique `.ai/symbols.json` — jamais recopiées ici.
276
+
277
+ ## ⚙️ Configuration et modes
278
+
279
+ Le moteur est configuré **en dur**, pas via un bloc Zod exposé à `use()`. Trois réglages seulement :
280
+
281
+ | Réglage | Valeur Nodefony | Effet | Ancre |
282
+ | ------------ | ----------------------------- | ------------------------------------------------------------------- | ---------------- |
283
+ | `autoEscape` | `true` | `<%= %>` échappe le HTML par défaut (anti-XSS) | `Eta.ts:16` |
284
+ | `useWith` | `true` | locals exposés nus (`<%= name %>`) — DX façon EJS | `Eta.ts:17` |
285
+ | `cache` | `true` en prod, `false` sinon | compile-once des templates en production ; recompile à chaud en dev | `Template.ts:20` |
286
+
287
+ Le cache n'est **pas** un booléen figé : `Template` le dérive de l'environnement du kernel
288
+ (`environment === "prod"`, `Template.ts:20`) puis `Eta` l'applique au moteur (`Eta.ts:41`). En
289
+ développement, un template modifié est donc pris en compte sans redémarrer.
290
+
291
+ ## 🔌 HTTP et WebSocket — le même rendu
292
+
293
+ `renderView()` est agnostique du transport : sur une action WebSocket, le rendu produit une **frame**
294
+ au lieu d'un corps HTTP. Le module de test le fait avec un template `.eta` qui produit du JSON, renvoyé
295
+ comme frame au client :
296
+
297
+ ```eta
298
+ {
299
+ "nodefony" : "<%= nodefony.name %>",
300
+ "name" : "<%= name %>"
301
+ }
302
+ ```
303
+
304
+ L'action WS appelle `renderView(view, { name, ...this.context?.metaData })` exactement comme en HTTP —
305
+ c'est le différenciateur Nodefony : une classe, deux transports, le même moteur de vues.
306
+
307
+ ## 📜 Normes appliquées
308
+
309
+ | Domaine | Norme / référence | Comment le code s'y conforme |
310
+ | ------------------ | ----------------------- | --------------------------------------------------------------------- |
311
+ | Neutralisation XSS | OWASP — Output Encoding | échappement HTML par défaut (`autoEscape`, `Eta.ts:16`) |
312
+ | Type de média HTML | `text/html` | posé par `setContextHtml()` dans `renderView()` (`Controller.ts:331`) |
313
+ | I/O non bloquante | Node.js async fs | lecture async du fichier (`readFile`, `Eta.ts:71`) |
314
+
315
+ ## ⚡ Performance et mémoire
316
+
317
+ Le rendu de vue est la partie **réellement coûteuse** d'une réponse (lecture fichier + exécution du
318
+ template), et le framework l'isole pour ça :
319
+
320
+ - **Phase dédiée** : `renderView()` chronomètre le rendu sous la phase `render`
321
+ (`phaseStart("render")`, `Controller.ts:322`) — distincte de `action` et de `send`. On voit ainsi si
322
+ le temps part dans le moteur ou dans l'écriture réseau.
323
+ - **Cache en prod** : les templates sont compilés une fois (`cache` vrai en production,
324
+ `Template.ts:20`) ; le coût de parsing n'est payé qu'au premier rendu.
325
+ - **Lecture non bloquante** : le fichier est lu en async (`FileClass.readAsync()` côté `renderView`,
326
+ `readFile` côté `renderFile`, `Eta.ts:71`) — l'event loop n'est jamais gelé par un `readFileSync`.
327
+ - **Aides frontend paresseuses** : `withFrontendLocals()` (`Controller.ts:345`) ne construit les
328
+ fonctions `frontendTags`/`asset` que si le service `frontend` répond — sinon il rend les locals tels
329
+ quels, zéro allocation superflue.
330
+
331
+ ## 📡 Observabilité — Studio
332
+
333
+ - **Debug bar** : la phase `render` d'une requête y apparaît aux côtés de `resolve`, `parse`, `action`
334
+ et `send` — c'est là qu'on repère un template lent.
335
+ - **Playground** (`/nodefony/playground`, développement) : permet de jouer une route qui rend une vue
336
+ et d'observer le HTML produit sans écrire de client.
337
+
338
+ ## ⚠️ Pièges (symptôme → cause → correction)
339
+
340
+ | Symptôme | Cause (dans le code) | Correction |
341
+ | -------------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
342
+ | `<%~ include("partial") %>` ne trouve pas la vue | Aucune racine `views/` n'est configurée (`defaultOption`, `Eta.ts:15`) | Compose côté contrôleur : rends chaque fragment, ou passe le HTML en local |
343
+ | Un `<b>` d'utilisateur s'exécute dans la page | Sortie brute `<%~ %>` sur une donnée non fiable | Utiliser `<%= %>` (échappé par défaut) |
344
+ | `<%= it.name %>` requis alors qu'on attend `<%= name %>` | `useWith` mal compris — Nodefony l'active (`Eta.ts:17`), les locals sont nus | Écrire `<%= name %>` directement |
345
+ | Modif de template ignorée en prod | `cache: true` en production (`Template.ts:20`) | Redémarrer le pod ; en dev le cache est off, recompile à chaud |
346
+ | La réponse d'erreur n'est pas ma vue Eta | Les erreurs rendent du JSON, pas un template (`error-renderer.ts:109`) | Pour une page d'erreur HTML, rendre explicitement une vue dans un handler |
347
+ | `renderView()` rejette et logge une ERROR | Fichier introuvable ou template invalide (le `catch` re-lève, `Controller.ts:333`) | Vérifier le chemin résolu (`resolve(module.path, …)`) |
348
+
349
+ ## 🧪 Tests et couverture
350
+
351
+ L'honnêteté d'abord : le moteur de vues a **peu de tests dédiés**, et surtout **aucun** test n'exerce
352
+ le vrai moteur Eta de bout en bout. Ce qui existe :
353
+
354
+ - **unit** — `Controller.test.ts` couvre le **câblage** de `renderView()` : deux cas vérifient que la
355
+ vue est rendue puis envoyée en HTML, et que les aides frontend sont injectées dans les locals. Mais
356
+ ces tests emploient un **template factice** (`{ render: async () => … }`) : ils prouvent le contrat
357
+ du contrôleur, **pas** le rendu réel, ni l'échappement.
358
+ - **intégration** — `ws-bridge-rendered-action.test.ts` (`@nodefony/http`) exerce une action **rendue**
359
+ côté pont WS, mais via `renderJson`, **pas** un template Eta.
360
+
361
+ Ce qui **manque** (à créer) :
362
+
363
+ - aucun test du **service `Eta`** lui-même — ni `render()`, ni `renderFile()` ;
364
+ - aucun test de l'**échappement HTML / XSS** (`<%= %>` échappe, `<%~ %>` non) — pourtant c'est la
365
+ défense de sécurité centrale de la brique ;
366
+ - aucun banc de **charge/mémoire** dédié au rendu de vue (le coût est mesuré au niveau du pipeline
367
+ complet via `memory.test.ts` de `@nodefony/http`).
368
+
369
+ Un banc réel devrait rendre une vraie vue `.eta` sur un serveur vivant et asserter à la fois le
370
+ `Content-Type: text/html` **et** la neutralisation d'une charge XSS. Pour les axes charge/mémoire, voir
371
+ les skills `nodefony-load-test` et `nodefony-check-memory-health`.
372
+
373
+ Couverture : `npm run coverage` dans `@nodefony/framework`.
374
+
375
+ ## 🔗 Pour aller plus loin
376
+
377
+ - ⬆️ **Retour au hub** : [Framework — vue d'ensemble](index.md) · [Toute la documentation](../../../../../docs/index.md)
378
+ - 🧭 **Pages sœurs** : [Contrôleurs](controller.md) (d'où l'on appelle `renderView`) · [Décorateurs](decorateurs.md) (`@Get`, `@Param`) · [Routage](routing.md)
379
+ - Le pare-feu et la CSP au-dessus du HTML rendu → [Firewall](../../security/docs/firewall.md)
380
+ - Signatures exactes des membres publics → graphe symbolique `.ai/symbols.json`
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "@nodefony/framework",
3
+ "version": "10.0.0-alpha.1",
4
+ "description": "Le modèle de programmation Nodefony : routeur, contrôleurs, décorateurs et vues — HTTP et WebSocket dans le même contexte",
5
+ "author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
6
+ "main": "./dist/index.js",
7
+ "type": "module",
8
+ "types": "./dist/types/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/types/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "build": "rimraf dist && rolldown -c rolldown.config.ts && tsgo -p tsconfig.declarations.json",
18
+ "dev": "rolldown -c rolldown.config.ts --watch",
19
+ "clean": "rimraf dist",
20
+ "test": "vitest run",
21
+ "test:integration": "vitest run --config vitest.integration.config.ts",
22
+ "coverage": "vitest run --coverage",
23
+ "typecheck": "tsgo --noEmit -p tsconfig.json && tsgo --noEmit -p tsconfig.tests.json"
24
+ },
25
+ "private": false,
26
+ "engines": {
27
+ "node": ">=24.0.0"
28
+ },
29
+ "keywords": [
30
+ "nodefony",
31
+ "framework",
32
+ "router",
33
+ "controller",
34
+ "decorators",
35
+ "mvc",
36
+ "typescript",
37
+ "esm",
38
+ "nodejs"
39
+ ],
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/nodefony/nodefony-core.git",
43
+ "directory": "src/packages/@nodefony/framework"
44
+ },
45
+ "dependencies": {
46
+ "@graphql-tools/merge": "9.2.3",
47
+ "@graphql-tools/schema": "10.1.0",
48
+ "eta": "4.6.0",
49
+ "graphql": "17.0.2",
50
+ "reflect-metadata": "0.2.2",
51
+ "tslib": "2.8.1"
52
+ },
53
+ "devDependencies": {
54
+ "@nodefony/http": "*",
55
+ "@types/chai": "5.2.3",
56
+ "@types/node": "26.4.1",
57
+ "@vitest/coverage-v8": "5.0.0",
58
+ "chai": "6.2.2",
59
+ "nodefony": "*",
60
+ "rimraf": "6.1.3",
61
+ "tsx": "4.23.13",
62
+ "vitest": "5.0.0"
63
+ },
64
+ "license": "CECILL-B",
65
+ "readmeFilename": "README.md",
66
+ "contributors": [],
67
+ "peerDependencies": {
68
+ "@nodefony/http": "*",
69
+ "nodefony": "*",
70
+ "zod": "^4.4.3"
71
+ },
72
+ "files": [
73
+ "dist",
74
+ "docs"
75
+ ],
76
+ "publishConfig": {
77
+ "access": "public"
78
+ },
79
+ "homepage": "https://nodefony.github.io/nodefony-core/",
80
+ "bugs": {
81
+ "url": "https://github.com/nodefony/nodefony-core/issues"
82
+ }
83
+ }