@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.
- package/LICENSE +544 -0
- package/README.md +50 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/index.js +211 -0
- package/dist/nodefony/config/config.js +61 -0
- package/dist/nodefony/config/defineModuleConfig.js +36 -0
- package/dist/nodefony/controller/AdminApiController.js +163 -0
- package/dist/nodefony/controller/ApiKeyController.js +151 -0
- package/dist/nodefony/controller/BenchController.js +132 -0
- package/dist/nodefony/controller/IssuerMetadataController.js +148 -0
- package/dist/nodefony/controller/OAuth2Controller.js +133 -0
- package/dist/nodefony/controller/ProtectedResourceMetadataController.js +221 -0
- package/dist/nodefony/controller/SessionAuthController.js +141 -0
- package/dist/nodefony/controller/TokenAuthController.js +113 -0
- package/dist/nodefony/controller/TotpController.js +129 -0
- package/dist/nodefony/controller/WebAuthnController.js +242 -0
- package/dist/nodefony/controller/oauthAuthority.js +74 -0
- package/dist/nodefony/decorators/routerDecorators.js +967 -0
- package/dist/nodefony/interfaces/IAdminBroker.js +1 -0
- package/dist/nodefony/interfaces/IController.js +1 -0
- package/dist/nodefony/interfaces/IIdempotencyStore.js +1 -0
- package/dist/nodefony/interfaces/IResolver.js +1 -0
- package/dist/nodefony/interfaces/IRoute.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/service/AdminBroker.js +106 -0
- package/dist/nodefony/service/Eta.js +68 -0
- package/dist/nodefony/service/IdempotencyStore.js +136 -0
- package/dist/nodefony/service/router.js +243 -0
- package/dist/nodefony/src/Controller.js +515 -0
- package/dist/nodefony/src/FrameworkAdminApi.js +268 -0
- package/dist/nodefony/src/KernelAdminApi.js +1243 -0
- package/dist/nodefony/src/PlaygroundAdminApi.js +97 -0
- package/dist/nodefony/src/RedisIdempotencyStore.js +254 -0
- package/dist/nodefony/src/Resolver.js +416 -0
- package/dist/nodefony/src/ResourceController.js +148 -0
- package/dist/nodefony/src/Route.js +476 -0
- package/dist/nodefony/src/SyslogAdminApi.js +466 -0
- package/dist/nodefony/src/Template.js +15 -0
- package/dist/nodefony/src/configMutation.js +186 -0
- package/dist/nodefony/src/docsReader.js +929 -0
- package/dist/nodefony/src/idempotency.js +137 -0
- package/dist/nodefony/src/idempotencyGc.js +32 -0
- package/dist/nodefony/src/idempotencyStoreRegistry.js +36 -0
- package/dist/nodefony/src/scopeCatalog.js +40 -0
- package/dist/nodefony/src/syslogFilters.js +51 -0
- package/dist/types/index.d.ts +96 -0
- package/dist/types/nodefony/config/config.d.ts +41 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
- package/dist/types/nodefony/controller/AdminApiController.d.ts +70 -0
- package/dist/types/nodefony/controller/ApiKeyController.d.ts +49 -0
- package/dist/types/nodefony/controller/BenchController.d.ts +45 -0
- package/dist/types/nodefony/controller/IssuerMetadataController.d.ts +86 -0
- package/dist/types/nodefony/controller/OAuth2Controller.d.ts +81 -0
- package/dist/types/nodefony/controller/ProtectedResourceMetadataController.d.ts +147 -0
- package/dist/types/nodefony/controller/SessionAuthController.d.ts +74 -0
- package/dist/types/nodefony/controller/TokenAuthController.d.ts +56 -0
- package/dist/types/nodefony/controller/TotpController.d.ts +42 -0
- package/dist/types/nodefony/controller/WebAuthnController.d.ts +123 -0
- package/dist/types/nodefony/controller/oauthAuthority.d.ts +54 -0
- package/dist/types/nodefony/decorators/routerDecorators.d.ts +632 -0
- package/dist/types/nodefony/interfaces/IAdminBroker.d.ts +79 -0
- package/dist/types/nodefony/interfaces/IController.d.ts +38 -0
- package/dist/types/nodefony/interfaces/IIdempotencyStore.d.ts +1 -0
- package/dist/types/nodefony/interfaces/IResolver.d.ts +25 -0
- package/dist/types/nodefony/interfaces/IRoute.d.ts +31 -0
- package/dist/types/nodefony/interfaces/index.d.ts +5 -0
- package/dist/types/nodefony/service/AdminBroker.d.ts +37 -0
- package/dist/types/nodefony/service/Eta.d.ts +25 -0
- package/dist/types/nodefony/service/IdempotencyStore.d.ts +45 -0
- package/dist/types/nodefony/service/router.d.ts +53 -0
- package/dist/types/nodefony/src/Controller.d.ts +193 -0
- package/dist/types/nodefony/src/FrameworkAdminApi.d.ts +35 -0
- package/dist/types/nodefony/src/KernelAdminApi.d.ts +71 -0
- package/dist/types/nodefony/src/PlaygroundAdminApi.d.ts +98 -0
- package/dist/types/nodefony/src/RedisIdempotencyStore.d.ts +104 -0
- package/dist/types/nodefony/src/Resolver.d.ts +165 -0
- package/dist/types/nodefony/src/ResourceController.d.ts +171 -0
- package/dist/types/nodefony/src/Route.d.ts +192 -0
- package/dist/types/nodefony/src/SyslogAdminApi.d.ts +38 -0
- package/dist/types/nodefony/src/Template.d.ts +8 -0
- package/dist/types/nodefony/src/configMutation.d.ts +109 -0
- package/dist/types/nodefony/src/docsReader.d.ts +369 -0
- package/dist/types/nodefony/src/idempotency.d.ts +96 -0
- package/dist/types/nodefony/src/idempotencyGc.d.ts +30 -0
- package/dist/types/nodefony/src/idempotencyStoreRegistry.d.ts +59 -0
- package/dist/types/nodefony/src/scopeCatalog.d.ts +26 -0
- package/dist/types/nodefony/src/syslogFilters.d.ts +52 -0
- package/docs/admin.md +451 -0
- package/docs/controller.md +645 -0
- package/docs/decorateurs.md +845 -0
- package/docs/idempotence.md +741 -0
- package/docs/index.md +151 -0
- package/docs/routing.md +648 -0
- package/docs/templates.md +380 -0
- 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 -->|"<%= %> é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
|
+
`<`) 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 (`<`…) 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 `<`, `>` par
|
|
80
|
+
`>`, 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 <b>x</b></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 `<script>alert(1)</script>`.
|
|
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
|
+
}
|