@nodefony/frontend 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 +338 -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 +63 -0
- package/dist/nodefony/command/frontend-build.js +68 -0
- package/dist/nodefony/command/frontend-dev.js +31 -0
- package/dist/nodefony/command/frontend-status.js +40 -0
- package/dist/nodefony/config/config.js +65 -0
- package/dist/nodefony/config/defineModuleConfig.js +34 -0
- package/dist/nodefony/interfaces/IFrontBuilder.js +1 -0
- package/dist/nodefony/interfaces/IFrontPreset.js +1 -0
- package/dist/nodefony/interfaces/IFrontendService.js +1 -0
- package/dist/nodefony/interfaces/IViteSupervisor.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/service/FrontendService.js +708 -0
- package/dist/nodefony/service/ViteConfigGenerator.js +139 -0
- package/dist/nodefony/service/ViteProcessSupervisor.js +589 -0
- package/dist/nodefony/src/FrontendAdminApi.js +113 -0
- package/dist/nodefony/src/builders/ViteBuilder.js +75 -0
- package/dist/nodefony/src/errors/FrontendError.js +50 -0
- package/dist/nodefony/src/isolationGroups.js +116 -0
- package/dist/nodefony/src/presets/angular-vite.js +27 -0
- package/dist/nodefony/src/presets/react19-vite.js +26 -0
- package/dist/nodefony/src/presets/svelte5-vite.js +37 -0
- package/dist/nodefony/src/presets/vanilla-vite.js +17 -0
- package/dist/nodefony/src/presets/vue3-vite.js +23 -0
- package/dist/nodefony/src/remoteDev.js +157 -0
- package/dist/nodefony/src/template/TemplateHelper.js +255 -0
- package/dist/types/index.d.ts +51 -0
- package/dist/types/nodefony/command/frontend-build.d.ts +17 -0
- package/dist/types/nodefony/command/frontend-dev.d.ts +12 -0
- package/dist/types/nodefony/command/frontend-status.d.ts +14 -0
- package/dist/types/nodefony/config/config.d.ts +38 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +29 -0
- package/dist/types/nodefony/interfaces/IFrontBuilder.d.ts +69 -0
- package/dist/types/nodefony/interfaces/IFrontPreset.d.ts +26 -0
- package/dist/types/nodefony/interfaces/IFrontendService.d.ts +77 -0
- package/dist/types/nodefony/interfaces/IViteSupervisor.d.ts +56 -0
- package/dist/types/nodefony/interfaces/index.d.ts +4 -0
- package/dist/types/nodefony/service/FrontendService.d.ts +230 -0
- package/dist/types/nodefony/service/ViteConfigGenerator.d.ts +66 -0
- package/dist/types/nodefony/service/ViteProcessSupervisor.d.ts +214 -0
- package/dist/types/nodefony/src/FrontendAdminApi.d.ts +63 -0
- package/dist/types/nodefony/src/builders/ViteBuilder.d.ts +17 -0
- package/dist/types/nodefony/src/errors/FrontendError.d.ts +34 -0
- package/dist/types/nodefony/src/isolationGroups.d.ts +89 -0
- package/dist/types/nodefony/src/presets/angular-vite.d.ts +15 -0
- package/dist/types/nodefony/src/presets/react19-vite.d.ts +9 -0
- package/dist/types/nodefony/src/presets/svelte5-vite.d.ts +13 -0
- package/dist/types/nodefony/src/presets/vanilla-vite.d.ts +9 -0
- package/dist/types/nodefony/src/presets/vue3-vite.d.ts +11 -0
- package/dist/types/nodefony/src/remoteDev.d.ts +107 -0
- package/dist/types/nodefony/src/template/TemplateHelper.d.ts +99 -0
- package/docs/index.md +925 -0
- package/package.json +80 -0
package/docs/index.md
ADDED
|
@@ -0,0 +1,925 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "@nodefony/frontend — le builder d'interfaces"
|
|
3
|
+
navTitle: "@nodefony/frontend"
|
|
4
|
+
lang: fr
|
|
5
|
+
module: "@nodefony/frontend"
|
|
6
|
+
topic: frontend
|
|
7
|
+
section: "Interface"
|
|
8
|
+
audience: [developer, devops]
|
|
9
|
+
tags:
|
|
10
|
+
[frontend, vite, hmr, react, vue, angular, build, bundle, manifest, csp, cdn]
|
|
11
|
+
version: "doc"
|
|
12
|
+
status: stable
|
|
13
|
+
updated: 2026-07-19
|
|
14
|
+
source: "src/packages/@nodefony/frontend/docs/index.md"
|
|
15
|
+
coverageModule: frontend
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# @nodefony/frontend — le builder d'interfaces
|
|
19
|
+
|
|
20
|
+
> Le module qui donne une **interface** à ton application. Il pilote [Vite](https://vite.dev) pour
|
|
21
|
+
> transformer ton code React, Vue ou Angular en quelque chose que le navigateur comprend, avec
|
|
22
|
+
> rechargement à chaud pendant que tu développes et bundles optimisés en production. Sa particularité
|
|
23
|
+
> tient en deux décisions : **Vite tourne dans un processus séparé** (compiler ne ralentit jamais ton
|
|
24
|
+
> serveur) et **c'est Nodefony qui rend la page HTML**, pas Vite — ta page reste une page du framework,
|
|
25
|
+
> avec sa session, son pare-feu et son nonce de sécurité.
|
|
26
|
+
|
|
27
|
+
📍 [Documentation](../../../../../docs/index.md) › **@nodefony/frontend**
|
|
28
|
+
|
|
29
|
+
## 🧠 Le modèle mental — deux serveurs, un seul site
|
|
30
|
+
|
|
31
|
+
Le réflexe habituel est de croire qu'un projet front et un projet back sont deux applications. Ici,
|
|
32
|
+
il n'y en a qu'une : ton module Nodefony **déclare** son interface, et le module frontend s'occupe du
|
|
33
|
+
reste. Concrètement, deux serveurs tournent en développement et se partagent le travail.
|
|
34
|
+
|
|
35
|
+
```mermaid
|
|
36
|
+
flowchart TD
|
|
37
|
+
BR["Navigateur"] -->|1 · GET /shop| NF["Nodefony · 5151<br/>route → contrôleur → HTML"]
|
|
38
|
+
NF -->|2 · HTML + balises script| BR
|
|
39
|
+
BR -->|3 · assets, modules, HMR| VITE["Vite · 5173<br/>processus séparé"]
|
|
40
|
+
BR -->|4 · fetch /shop/api| VITE
|
|
41
|
+
VITE -->|proxy| NF
|
|
42
|
+
NF -.->|spawn au démarrage<br/>arrêt au terminate| VITE
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Lis le schéma comme une visite : la **page** vient toujours de Nodefony (1-2) ; les **modules
|
|
46
|
+
JavaScript** viennent de Vite en direct (3), donc ton serveur n'est jamais sur le chemin critique des
|
|
47
|
+
assets ; et les **appels d'API** repartent vers Nodefony par le proxy de Vite (4). En production, Vite
|
|
48
|
+
disparaît : les assets sont pré-construits et servis en fichiers statiques.
|
|
49
|
+
|
|
50
|
+
## 📖 Lexique
|
|
51
|
+
|
|
52
|
+
| Terme | Sens |
|
|
53
|
+
| ------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
54
|
+
| Vite | L'outil qui transpile et sert le code front. Serveur de développement en dev, compilateur en production. |
|
|
55
|
+
| HMR | _Hot Module Replacement_ : ta modification apparaît dans le navigateur sans recharger la page. |
|
|
56
|
+
| Entrée (_entry_) | Le point de départ d'une interface (`main.tsx`). Un module = une entrée = un bundle. |
|
|
57
|
+
| Bundle | Le résultat compilé d'une entrée : un fichier JS (plus ses morceaux) que le navigateur charge. |
|
|
58
|
+
| Preset | La recette d'un framework UI (React, Vue, Angular) : quel greffon Vite, quelles extensions. |
|
|
59
|
+
| Famille d'isolation | Groupe d'entrées qui partagent **un** processus Vite. Angular a la sienne. |
|
|
60
|
+
| Superviseur | L'objet qui lance, surveille, relance et arrête le processus Vite. |
|
|
61
|
+
| Manifeste | `manifest.json` produit par le build : la carte « fichier source → fichier compilé empreinté ». |
|
|
62
|
+
| Empreinte | Le hachage dans le nom d'un fichier compilé (`main-a1b2c3.js`) — permet un cache navigateur permanent. |
|
|
63
|
+
| `publicPath` | Le préfixe d'URL sous lequel les assets d'un bundle sont servis (`/_assets/shop/`). |
|
|
64
|
+
| Repli SPA | Le comportement d'un serveur front : toute URL inconnue rend `index.html`. Source du piège n°1. |
|
|
65
|
+
| CSP | _Content Security Policy_ : l'en-tête qui liste les origines de scripts autorisées par le navigateur. |
|
|
66
|
+
| Nonce | Jeton à usage unique posé sur un `<script>` pour l'autoriser malgré une CSP stricte. |
|
|
67
|
+
| CDN | _Content Delivery Network_ : un réseau de serveurs de proximité qui sert les assets à la place du tien. |
|
|
68
|
+
| Préambule React | Petit script que React Fast Refresh exige dans le `<head>` avant tout module React. |
|
|
69
|
+
| `/@fs/` | Le préfixe par lequel Vite sert un fichier par son **chemin absolu** sur le disque. |
|
|
70
|
+
| Molette `ui` | Le réglage d'un module distribué : servir son interface via Vite ou via des assets pré-construits. |
|
|
71
|
+
|
|
72
|
+
## Qu'est-ce que c'est ?
|
|
73
|
+
|
|
74
|
+
Un navigateur ne sait lire ni du TSX, ni un composant Vue, ni un décorateur Angular. Il faut un
|
|
75
|
+
**atelier de transformation** entre ton code et lui : c'est ce qu'on appelle un builder front.
|
|
76
|
+
Historiquement cet atelier était lent — chaque sauvegarde reconstruisait tout le projet. Vite a
|
|
77
|
+
renversé le modèle : il ne compile **que le fichier demandé**, à la demande, et pousse les
|
|
78
|
+
modifications à chaud dans la page ouverte.
|
|
79
|
+
|
|
80
|
+
`@nodefony/frontend` n'est pas une réimplémentation de cet atelier : c'est **le chef d'orchestre** qui
|
|
81
|
+
le branche sur ton application — qui compile, qui rend la page, comment le front parle au back, et ce
|
|
82
|
+
qui remplace Vite une fois en production.
|
|
83
|
+
|
|
84
|
+
### La vision Nodefony — ce que ce module fait différemment
|
|
85
|
+
|
|
86
|
+
**Vite est un processus système, pas une bibliothèque.** Le superviseur lance le binaire Vite avec
|
|
87
|
+
`child_process.spawn` (`ViteProcessSupervisor.attemptSpawn()`, `ViteProcessSupervisor.ts:372`). La
|
|
88
|
+
conséquence est concrète : compiler dix mille modules ne coûte **rien** à la latence de tes requêtes,
|
|
89
|
+
et un plantage de Vite ne tue pas ton serveur — le superviseur le relance tout seul.
|
|
90
|
+
|
|
91
|
+
**C'est Nodefony qui sert le HTML.** Beaucoup de piles séparent un serveur front (qui rend la page) et
|
|
92
|
+
un serveur d'API (qui rend le JSON). Ici, la page d'entrée reste une route de ton contrôleur :
|
|
93
|
+
elle traverse le pare-feu, connaît la session, reçoit son nonce CSP. Le module se contente d'y
|
|
94
|
+
**injecter les bonnes balises** (`TemplateHelper.renderDevTags()`, `TemplateHelper.ts:153`).
|
|
95
|
+
|
|
96
|
+
**Un seul Vite pour N modules.** Trois modules à interface ne lancent pas trois serveurs Vite : leurs
|
|
97
|
+
entrées sont agrégées dans une seule instance multi-entrées. La seule exception est documentée et
|
|
98
|
+
justifiée — Angular est isolé, parce que son greffon transforme **tous** les `.ts` du serveur de
|
|
99
|
+
développement (`isolationGroup()`, `isolationGroups.ts:39`).
|
|
100
|
+
|
|
101
|
+
**Le module ne dépend ni de `@nodefony/http` ni de `@nodefony/framework`.** Tout ce dont il a besoin
|
|
102
|
+
d'eux (le serveur statique, le pare-feu, les certificats, le port réellement écouté) est résolu **par
|
|
103
|
+
nom** dans le conteneur. C'est ce qui le garde en bout de chaîne, sans cycle de dépendances.
|
|
104
|
+
|
|
105
|
+
## 🧭 Par où commencer
|
|
106
|
+
|
|
107
|
+
Quatre parcours selon ce que tu viens faire. L'ordre à l'intérieur de chacun n'est pas décoratif :
|
|
108
|
+
chaque étape suppose la précédente.
|
|
109
|
+
|
|
110
|
+
**Je branche une interface sur mon module** — le chemin le plus court vers une page qui vit.
|
|
111
|
+
|
|
112
|
+
1. [Démarrage rapide](#-démarrage-rapide) — un module, une entrée, une page. Copie-colle, ça marche.
|
|
113
|
+
2. [`registerEntry`](#registerentry--la-déclaration-dune-interface) — les sept champs de la
|
|
114
|
+
déclaration, et lesquels comptent vraiment.
|
|
115
|
+
3. [`apiProxyPaths`](#apiproxypaths--que-le-fetch-atteigne-le-serveur) — **à ne pas sauter** : c'est
|
|
116
|
+
l'oubli qui produit le bug n°1 du module.
|
|
117
|
+
4. [Pièges](#-pièges) — les symptômes qu'on rencontre dans l'ordre où on les rencontre.
|
|
118
|
+
|
|
119
|
+
**Je pars en production** — ce qui change quand Vite n'est plus là.
|
|
120
|
+
|
|
121
|
+
1. [Les deux modes de livraison](#-les-deux-modes-de-livraison-de-linterface) — comprendre ce qui
|
|
122
|
+
remplace Vite, et qui décide.
|
|
123
|
+
2. [Construire pour la production](#construire-pour-la-production--frontendbuild) — la commande, le
|
|
124
|
+
cache de fraîcheur, le code de sortie.
|
|
125
|
+
3. [`publicPath` et `assetBaseUrl`](#publicpath-et-assetbaseurl--où-vivent-les-assets) — où atterrissent
|
|
126
|
+
les fichiers, et comment basculer vers un CDN sans toucher au code.
|
|
127
|
+
4. [Configuration](#-configuration) — ce qui n'a plus d'effet une fois hors développement.
|
|
128
|
+
|
|
129
|
+
**Je supervise ou je débugge.**
|
|
130
|
+
|
|
131
|
+
1. [Observabilité](#-observabilité--studio-et-cli) — l'état réel du superviseur, en ligne de commande
|
|
132
|
+
et dans Studio.
|
|
133
|
+
2. [Architecture interne](#-architecture-interne) — ce qui se passe entre le démarrage du
|
|
134
|
+
kernel et le premier `<script>`.
|
|
135
|
+
3. [Résilience](#résilience--ce-qui-se-passe-quand-vite-tombe) — relance automatique, ports occupés,
|
|
136
|
+
sonde de vie.
|
|
137
|
+
|
|
138
|
+
## 🗂️ Ce que le module apporte
|
|
139
|
+
|
|
140
|
+
Le tableau pour situer en cinq secondes ; les fiches en dessous pour savoir quoi lire.
|
|
141
|
+
|
|
142
|
+
| Brique | Ce qu'elle résout | Tu en as besoin quand… |
|
|
143
|
+
| ------------------------------------------------------------------------- | --------------------------------------------------- | ----------------------------------------- |
|
|
144
|
+
| [`registerEntry`](#registerentry--la-déclaration-dune-interface) | déclarer qu'un module a une interface | toujours — c'est le point de contact |
|
|
145
|
+
| [`apiProxyPaths`](#apiproxypaths--que-le-fetch-atteigne-le-serveur) | que les appels d'API atteignent ton serveur | ton interface parle à ton back (donc oui) |
|
|
146
|
+
| [Presets](#-extension) | brancher React, Vue, Angular ou du TypeScript nu | tu choisis ton framework UI |
|
|
147
|
+
| [Familles d'isolation](#familles-disolation--pourquoi-angular-a-son-vite) | faire cohabiter plusieurs frameworks | tu mélanges Angular avec autre chose |
|
|
148
|
+
| [Rendu des balises](#rendu--des-balises-ou-un-document-complet) | injecter le front dans une page servie par Nodefony | tu écris le contrôleur de la page |
|
|
149
|
+
| [Modes de livraison](#-les-deux-modes-de-livraison-de-linterface) | Vite en dev, assets pré-construits ailleurs | tu déploies, ou tu publies un module |
|
|
150
|
+
| [Build de production](#construire-pour-la-production--frontendbuild) | compiler, empreinter, produire le manifeste | tu prépares une image ou un paquet |
|
|
151
|
+
| [Résilience](#résilience--ce-qui-se-passe-quand-vite-tombe) | survivre à un crash, un port occupé, un gel | ton poste n'est pas un labo aseptisé |
|
|
152
|
+
|
|
153
|
+
```nodefony-cards
|
|
154
|
+
[
|
|
155
|
+
{ "icon": "📝", "title": "registerEntry", "href": "#registerentry--la-déclaration-dune-interface",
|
|
156
|
+
"desc": "Le point de contact unique du module. Un module l'appelle dans son onKernelBoot et dit trois choses : quel framework, quel fichier d'entrée, quels chemins d'API proxifier. Tout le reste a un défaut sensé.",
|
|
157
|
+
"meta": "la seule API que la plupart des applications toucheront jamais" },
|
|
158
|
+
{ "icon": "🔀", "title": "apiProxyPaths", "href": "#apiproxypaths--que-le-fetch-atteigne-le-serveur",
|
|
159
|
+
"desc": "En développement ton interface vient de Vite : un fetch part donc vers Vite, qui ne connaît pas la route et répond son index.html. Le symptôme (Unexpected token '<') ne parle jamais de proxy — et le data plane d'administration, lui, est proxifié d'office.",
|
|
160
|
+
"meta": "à ne pas sauter : c'est l'oubli qui produit le bug n°1" },
|
|
161
|
+
{ "icon": "🎨", "title": "Presets", "href": "#-extension",
|
|
162
|
+
"desc": "Quatre recettes prêtes — React, Vue, Angular, vanilla : quel greffon Vite charger, quelles dépendances pré-empaqueter, quelles extensions reconnaître. Les greffons sont chargés paresseusement : tu ne paies pas React si tu fais du Vue.",
|
|
163
|
+
"meta": "tu choisis ton framework UI, ou tu en ajoutes un" },
|
|
164
|
+
{ "icon": "🧱", "title": "Familles d'isolation", "href": "#familles-disolation--pourquoi-angular-a-son-vite",
|
|
165
|
+
"desc": "React, Vue et vanilla partagent une instance Vite sans se gêner. Angular non : son greffon transforme tout fichier .ts du serveur, y compris ceux des autres bundles — d'où une instance dédiée, sur son propre bloc de ports.",
|
|
166
|
+
"meta": "tu mélanges Angular avec autre chose" },
|
|
167
|
+
{ "icon": "🖼️", "title": "Rendu des balises", "href": "#rendu--des-balises-ou-un-document-complet",
|
|
168
|
+
"desc": "Deux portes d'entrée pour la même source : renderTags (tu écris ta page, on injecte les balises) et renderDocument (tu écris ton index.html, on l'injecte dedans). Plus les helpers de vue disponibles dans tes templates Eta.",
|
|
169
|
+
"meta": "tu écris le contrôleur de la page" },
|
|
170
|
+
{ "icon": "🚚", "title": "Modes de livraison", "href": "#-les-deux-modes-de-livraison-de-linterface",
|
|
171
|
+
"desc": "D'où viennent les fichiers JavaScript que charge le navigateur : Vite pendant que tu développes, assets pré-construits en production — et dans tout module installé depuis npm, qui ne doit exiger ni Vite ni compilation.",
|
|
172
|
+
"meta": "à lire avant tout déploiement, et avant de publier un module" },
|
|
173
|
+
{ "icon": "📦", "title": "Build de production", "href": "#construire-pour-la-production--frontendbuild",
|
|
174
|
+
"desc": "La commande qui compile, empreinte et produit le manifeste — entrée par entrée. Idempotente (une entrée plus fraîche que ses sources est ignorée) et tolérante : un bundle en échec n'arrête pas les autres, mais fait sortir en erreur.",
|
|
175
|
+
"meta": "tu prépares une image ou un paquet" },
|
|
176
|
+
{ "icon": "🛟", "title": "Résilience", "href": "#résilience--ce-qui-se-passe-quand-vite-tombe",
|
|
177
|
+
"desc": "Port occupé, plantage, gel, Ctrl+C, arrêt du kernel : ce que fait le superviseur dans chaque cas, et pourquoi un Ctrl+C ne doit surtout pas compter comme un plantage.",
|
|
178
|
+
"meta": "ton poste n'est pas un labo aseptisé" }
|
|
179
|
+
]
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## 🚀 Démarrage rapide
|
|
183
|
+
|
|
184
|
+
Vu depuis une application créée par `nodefony create app`. Trois fichiers, et une interface React qui
|
|
185
|
+
se recharge à chaud.
|
|
186
|
+
|
|
187
|
+
### 1. Charger le module — l'ordre compte
|
|
188
|
+
|
|
189
|
+
`@nodefony/frontend` doit être chargé **avant** les modules qui déclarent une interface : leur
|
|
190
|
+
`onKernelBoot()` résout le service `frontend` dans le conteneur, il doit donc déjà exister.
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
// nodefony.config.ts — l'orchestrateur de l'application
|
|
194
|
+
export default defineConfig(() => ({
|
|
195
|
+
modules: [
|
|
196
|
+
"@nodefony/http",
|
|
197
|
+
"@nodefony/framework",
|
|
198
|
+
// Le builder AVANT ses consommateurs : les modules à interface résolvent le
|
|
199
|
+
// service `frontend` dans leur onKernelBoot() — il doit déjà être enregistré.
|
|
200
|
+
use("@nodefony/frontend", {
|
|
201
|
+
// Tout est optionnel. `https: true` réutilise les certificats de Nodefony :
|
|
202
|
+
// à activer si tu ouvres ta page en https (sinon le navigateur bloque le
|
|
203
|
+
// contenu mixte page sécurisée ↔ modules en clair).
|
|
204
|
+
https: false,
|
|
205
|
+
viteEnv: { VITE_API_BASE: "/shop/api" },
|
|
206
|
+
}),
|
|
207
|
+
"shop",
|
|
208
|
+
],
|
|
209
|
+
}));
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### 2. Déclarer l'interface du module
|
|
213
|
+
|
|
214
|
+
Un module devient « à interface » en appelant `registerEntry` au démarrage. Le contrôleur, lui, rend
|
|
215
|
+
la page : il demande au service le **document complet**, construit à partir de l'`index.html` que tu
|
|
216
|
+
as écrit dans `frontend/`.
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
// src/modules/shop/index.ts — le module et son contrôleur, réunis pour l'exemple
|
|
220
|
+
import { Kernel, Module } from "nodefony";
|
|
221
|
+
import { Controller, Get, controller, controllers } from "@nodefony/framework";
|
|
222
|
+
import type { ContextType } from "@nodefony/http";
|
|
223
|
+
import type { FrontendService } from "@nodefony/frontend";
|
|
224
|
+
|
|
225
|
+
@controller("/shop")
|
|
226
|
+
class ShopController extends Controller {
|
|
227
|
+
constructor(context: ContextType) {
|
|
228
|
+
super("shop", context);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** La page d'entrée : rendue par Nodefony, ses modules servis par Vite. */
|
|
232
|
+
@Get("/")
|
|
233
|
+
page() {
|
|
234
|
+
this.setContextHtml();
|
|
235
|
+
const frontend = this.get<FrontendService>("frontend");
|
|
236
|
+
// `renderDocument` lit frontend/index.html et y injecte les balises. Le nonce
|
|
237
|
+
// de la requête est propagé aux <script> → la CSP stricte reste satisfaite.
|
|
238
|
+
const html =
|
|
239
|
+
frontend?.renderDocument("shop", this.context?.cspNonce) ??
|
|
240
|
+
"<!-- @nodefony/frontend indisponible -->";
|
|
241
|
+
return this.render(html);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** L'API que l'interface appellera — d'où la déclaration `apiProxyPaths`. */
|
|
245
|
+
@Get("/api/products")
|
|
246
|
+
products() {
|
|
247
|
+
return this.renderJson([{ id: "1", label: "Cordage 12mm" }]);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
@controllers([ShopController])
|
|
252
|
+
class Shop extends Module {
|
|
253
|
+
constructor(kernel: Kernel) {
|
|
254
|
+
super("shop", kernel, import.meta.url, {});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Déclare l'interface AVANT `onKernelReady` : le superviseur Vite démarre avec
|
|
259
|
+
* les entrées connues à ce moment-là. Enregistrer plus tard = entrée ignorée.
|
|
260
|
+
*/
|
|
261
|
+
override async onKernelBoot(): Promise<this> {
|
|
262
|
+
const frontend = this.kernel?.container?.get("frontend") as
|
|
263
|
+
FrontendService | undefined;
|
|
264
|
+
if (!frontend) {
|
|
265
|
+
this.log("@nodefony/frontend absent — chargé après ce module ?", "ERROR");
|
|
266
|
+
return this;
|
|
267
|
+
}
|
|
268
|
+
frontend.registerEntry(this, {
|
|
269
|
+
type: "react19",
|
|
270
|
+
entry: "./frontend/src/main.tsx",
|
|
271
|
+
// SANS cette ligne, fetch("/shop/api/products") depuis la page servie par
|
|
272
|
+
// Vite reçoit le repli SPA (du HTML) → « Unexpected token '<' ».
|
|
273
|
+
apiProxyPaths: ["/shop/api"],
|
|
274
|
+
});
|
|
275
|
+
return this;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export default Shop;
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### 3. Poser les fichiers front
|
|
283
|
+
|
|
284
|
+
Le module attend une racine front (défaut `./frontend`) contenant un `index.html` et ton point
|
|
285
|
+
d'entrée. Ton `index.html` est **le tien** : mets-y tes polices, tes méta, tes scripts externes.
|
|
286
|
+
|
|
287
|
+
```html
|
|
288
|
+
<!-- src/modules/shop/frontend/index.html -->
|
|
289
|
+
<!doctype html>
|
|
290
|
+
<html lang="fr">
|
|
291
|
+
<head>
|
|
292
|
+
<meta charset="utf-8" />
|
|
293
|
+
<title>Boutique</title>
|
|
294
|
+
<!--nodefony:frontend-->
|
|
295
|
+
</head>
|
|
296
|
+
<body>
|
|
297
|
+
<div id="root"></div>
|
|
298
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
299
|
+
</body>
|
|
300
|
+
</html>
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Le marqueur `<!--nodefony:frontend-->` indique **où** injecter les balises ; sans lui, elles sont
|
|
304
|
+
posées avant `</head>`. Le `<script>` d'entrée que tu vois en bas est retiré automatiquement au rendu
|
|
305
|
+
(`TemplateHelper.injectIntoHtml()`, `TemplateHelper.ts:105`) : il n'est résolvable que par Vite quand
|
|
306
|
+
Vite sert lui-même la page, ce qui n'est pas le cas ici.
|
|
307
|
+
|
|
308
|
+
### Ce qu'on observe
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
# Au démarrage : l'entrée est enregistrée, puis Vite annonce son port réel
|
|
312
|
+
# INFO registered entry: shop (react19) from "shop"
|
|
313
|
+
# INFO vite [default] ready on 127.0.0.1:5173
|
|
314
|
+
|
|
315
|
+
# La page vient de Nodefony et porte déjà les balises Vite
|
|
316
|
+
curl -s http://localhost:5151/shop | grep -o 'src="http[^"]*"'
|
|
317
|
+
# src="http://127.0.0.1:5173/@vite/client"
|
|
318
|
+
# src="http://127.0.0.1:5173/@fs/…/shop/frontend/src/main.tsx"
|
|
319
|
+
|
|
320
|
+
# L'API répond en JSON — et le même appel depuis le navigateur passe par le proxy Vite
|
|
321
|
+
curl -s http://localhost:5151/shop/api/products
|
|
322
|
+
# [{"id":"1","label":"Cordage 12mm"}]
|
|
323
|
+
|
|
324
|
+
# L'état du superviseur, en une commande
|
|
325
|
+
npx nodefony frontend:status
|
|
326
|
+
# state : ready
|
|
327
|
+
# endpoint : 127.0.0.1:5173
|
|
328
|
+
# entries : 1
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
> [!TIP]
|
|
332
|
+
> Modifie un composant et sauvegarde : la page se met à jour **sans rechargement**. Si tu vois un
|
|
333
|
+
> rechargement complet à chaque fois, c'est normal en Angular — son greffon ne fait pas de
|
|
334
|
+
> remplacement à chaud, il recharge.
|
|
335
|
+
|
|
336
|
+
## ⚙️ Configuration
|
|
337
|
+
|
|
338
|
+
Tout se déclare dans `nodefony.config.ts` via `use("@nodefony/frontend", { … })`. Le schéma Zod
|
|
339
|
+
(`frontendConfigSchema`, `config.ts:108`) est la **source unique** des défauts : chaque `.default()`
|
|
340
|
+
y vit, et nulle part ailleurs. Le builder `defineFrontendConfig()` (`defineModuleConfig.ts:22`) valide
|
|
341
|
+
et gèle au démarrage ; `frontendConfigJsonSchema()` (`defineModuleConfig.ts:31`) expose le tout en
|
|
342
|
+
JSON Schema pour l'écran de configuration de Studio.
|
|
343
|
+
|
|
344
|
+
> [!NOTE]
|
|
345
|
+
> Cette configuration concerne le **module** (le serveur Vite, le build). Ce qui décrit **une
|
|
346
|
+
> interface** (entrée, racine, préfixe public) n'est pas de la configuration : c'est une déclaration
|
|
347
|
+
> faite au démarrage par le module consommateur, via `registerEntry`.
|
|
348
|
+
|
|
349
|
+
### Le serveur de développement
|
|
350
|
+
|
|
351
|
+
| Option | Type | Défaut | Effet |
|
|
352
|
+
| ------------------------ | ------------------ | ------------- | --------------------------------------------------------------------------------- |
|
|
353
|
+
| `devHost` | `string` | `"127.0.0.1"` | Hôte de Vite, tel quel dans les `<script>` — doit être joignable du navigateur. |
|
|
354
|
+
| `devPort` | `number` | `5173` | Port de base. Occupé ⇒ le superviseur essaie les suivants. |
|
|
355
|
+
| `autoStartInDevelopment` | `boolean` | `true` | Démarrer Vite au boot en `development`. Ignoré ailleurs. |
|
|
356
|
+
| `startupTimeoutMs` | `number` | `30000` | Attente du `Local: …` de Vite avant de déclarer l'échec. |
|
|
357
|
+
| `pipeViteLogs` | `boolean` | `true` | Reverser la sortie de Vite dans le journal Nodefony. |
|
|
358
|
+
| `https` | `boolean` | `false` | Servir Vite en HTTPS avec **les certificats de Nodefony** (pas de doublon). |
|
|
359
|
+
| `viteEnv` | `Record<string,…>` | `{}` | Variables passées au processus Vite ; les clés `VITE_*` atteignent le navigateur. |
|
|
360
|
+
|
|
361
|
+
### Le proxy vers ton serveur
|
|
362
|
+
|
|
363
|
+
| Option | Type | Défaut | Effet |
|
|
364
|
+
| ----------------- | ------------------- | ------------- | --------------------------------------------------------- |
|
|
365
|
+
| `backendHost` | `string` | `"127.0.0.1"` | Hôte visé par le proxy de Vite. |
|
|
366
|
+
| `backendPort` | `number` | `5151` | Port visé — **une intention**, voir l'encadré ci-dessous. |
|
|
367
|
+
| `backendProtocol` | `"http" \| "https"` | `"http"` | Protocole du proxy. `https` pour viser le serveur TLS. |
|
|
368
|
+
|
|
369
|
+
> [!IMPORTANT]
|
|
370
|
+
> **`backendPort` n'est pas forcément le port écouté.** Avec une politique de port automatique, un
|
|
371
|
+
> 5151 occupé fait glisser l'écoute sur 5153. Un proxy figé enverrait alors les appels de ton
|
|
372
|
+
> interface vers le serveur d'une **autre** application. Le module lit donc le port réel sur le
|
|
373
|
+
> serveur lui-même (`FrontendService.resolveBackendPort()`, `FrontendService.ts:455`) et journalise
|
|
374
|
+
> l'écart.
|
|
375
|
+
|
|
376
|
+
### Le build de production
|
|
377
|
+
|
|
378
|
+
| Option | Type | Défaut | Effet |
|
|
379
|
+
| --------------- | -------- | ----------------- | -------------------------------------------------------------------------- |
|
|
380
|
+
| `defaultRoot` | `string` | `"./frontend"` | Racine front d'un module (contient `index.html`), si l'entrée ne dit rien. |
|
|
381
|
+
| `defaultOutDir` | `string` | `"./public/dist"` | Dossier de sortie du build, si l'entrée ne dit rien. |
|
|
382
|
+
| `assetBaseUrl` | `string` | `""` | Base CDN des assets en production. Vide = servis depuis ton origine. |
|
|
383
|
+
|
|
384
|
+
### La résilience du superviseur
|
|
385
|
+
|
|
386
|
+
Sous-section `resilience` (`resilienceSchema`, `config.ts:36`). Tout est optionnel ; les défauts
|
|
387
|
+
s'appliquent même si tu omets la section entière.
|
|
388
|
+
|
|
389
|
+
| Option | Défaut | Effet |
|
|
390
|
+
| ----------------------------- | ------- | ---------------------------------------------------------- |
|
|
391
|
+
| `autoRestart` | `true` | Relancer Vite après un plantage inattendu. |
|
|
392
|
+
| `maxRestarts` | `5` | Au-delà, le superviseur passe en `errored` et abandonne. |
|
|
393
|
+
| `restartBackoffBaseMs` | `500` | Base du délai exponentiel entre deux relances. |
|
|
394
|
+
| `restartBackoffMaxMs` | `8000` | Plafond de ce délai. |
|
|
395
|
+
| `healthCheckIntervalMs` | `30000` | Période de la sonde de vie. `0` la désactive. |
|
|
396
|
+
| `healthCheckFailureThreshold` | `3` | Échecs consécutifs avant de tuer Vite pour le relancer. |
|
|
397
|
+
| `healthCheckTimeoutMs` | `5000` | Délai d'une sonde individuelle. |
|
|
398
|
+
| `portRetryAttempts` | `3` | Ports essayés en plus du port de base quand il est occupé. |
|
|
399
|
+
|
|
400
|
+
> [!TIP]
|
|
401
|
+
> **En intégration continue, mets `autoRestart: false`.** Un Vite qui plante puis se relance en
|
|
402
|
+
> boucle fait passer ton pipeline au vert avec une interface morte. Sans relance, l'échec est visible.
|
|
403
|
+
|
|
404
|
+
## 🔌 Les deux modes de livraison de l'interface
|
|
405
|
+
|
|
406
|
+
C'est la section à lire avant tout déploiement, et **avant de publier un module** sur npm. La
|
|
407
|
+
question qu'elle tranche : d'où viennent les fichiers JavaScript que charge le navigateur ?
|
|
408
|
+
|
|
409
|
+
**Situation 1 — je développe l'application, les sources sont là.** Vite tourne, chaque sauvegarde se
|
|
410
|
+
voit immédiatement. C'est le mode `vite`.
|
|
411
|
+
|
|
412
|
+
**Situation 2 — je déploie en production.** Les sources sont peut-être là, mais compiler à chaud dans
|
|
413
|
+
un conteneur n'a aucun sens : les bundles sont construits une fois, empreintés, servis en fichiers
|
|
414
|
+
statiques. C'est le mode `static`.
|
|
415
|
+
|
|
416
|
+
**Situation 3 — j'installe le module d'un tiers qui embarque une interface d'administration.** Je ne
|
|
417
|
+
veux **ni** installer Vite, **ni** compiler l'interface de quelqu'un d'autre. Le paquet npm doit
|
|
418
|
+
contenir ses assets déjà construits. C'est encore le mode `static` — et c'est la raison principale de
|
|
419
|
+
son existence.
|
|
420
|
+
|
|
421
|
+
### Qui décide, et comment
|
|
422
|
+
|
|
423
|
+
Le module qui embarque une interface expose une molette `ui` avec trois positions, résolue au
|
|
424
|
+
démarrage par `resolveUiDelivery()` (`prebuiltUi.ts:48`, dans `@nodefony/http`) :
|
|
425
|
+
|
|
426
|
+
| Molette | Comportement |
|
|
427
|
+
| -------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
428
|
+
| `auto` | Vite **si** `development` **et** service frontend présent **et** sources présentes ; sinon assets pré-construits. |
|
|
429
|
+
| `static` | Force les assets pré-construits. Absents ⇒ mode `none` et raison journalisée. |
|
|
430
|
+
| `vite` | Force Vite. Service ou sources absents ⇒ mode `none` et raison journalisée. |
|
|
431
|
+
|
|
432
|
+
Le mode résolu et **sa raison** sont toujours journalisés — jamais de dégradation silencieuse. Un
|
|
433
|
+
mode `none` n'arrête pas le démarrage : le module se signale indisponible, avec une raison
|
|
434
|
+
actionnable (« le paquet a-t-il bien construit son interface à la publication ? »).
|
|
435
|
+
|
|
436
|
+
### Ce que fait chaque mode
|
|
437
|
+
|
|
438
|
+
| Aspect | Mode `vite` | Mode `static` |
|
|
439
|
+
| -------------------- | --------------------------------------------- | ---------------------------------------------------------- |
|
|
440
|
+
| D'où viennent les JS | serveur Vite, port dédié | dossier `dist/` servi par le serveur statique |
|
|
441
|
+
| Rechargement à chaud | oui | non |
|
|
442
|
+
| `registerEntry` | appelé — le module est visible du superviseur | **jamais appelé** — le module est invisible du superviseur |
|
|
443
|
+
| Dépendance à Vite | oui (`peerDependency`) | **aucune** |
|
|
444
|
+
| Rendu de la page | `renderDocument` / `renderTags` | `PrebuiltUi.renderIndex()` (`prebuiltUi.ts:196`) |
|
|
445
|
+
| Nonce CSP | posé sur chaque balise injectée | posé par remplacement sur chaque `<script>` de l'index |
|
|
446
|
+
|
|
447
|
+
> [!IMPORTANT]
|
|
448
|
+
> **Un module en mode `static` n'apparaît pas dans `listEntries()`.** C'est logique une fois le
|
|
449
|
+
> mécanisme compris — il n'a jamais appelé `registerEntry` — mais déroutant sur le moment : l'interface
|
|
450
|
+
> fonctionne parfaitement alors que le superviseur affirme ne rien connaître d'elle.
|
|
451
|
+
|
|
452
|
+
### Le cas d'un module distribué
|
|
453
|
+
|
|
454
|
+
Si tu publies un module avec une interface d'administration, la règle est simple : **construis ton
|
|
455
|
+
interface à la publication**, expédie `dist/frontend/` dans le paquet, et laisse la molette sur
|
|
456
|
+
`auto`. Chez toi (dépôt, lien local), tu gardes le rechargement à chaud ; chez ton utilisateur,
|
|
457
|
+
l'interface fonctionne sans qu'il installe quoi que ce soit. C'est exactement ce que fait
|
|
458
|
+
[Studio](../../studio/docs/index.md), premier consommateur du module.
|
|
459
|
+
|
|
460
|
+
## 🏗️ Architecture interne
|
|
461
|
+
|
|
462
|
+
### Le trajet du démarrage
|
|
463
|
+
|
|
464
|
+
```mermaid
|
|
465
|
+
sequenceDiagram
|
|
466
|
+
participant K as Kernel
|
|
467
|
+
participant M as Module à interface
|
|
468
|
+
participant S as FrontendService
|
|
469
|
+
participant V as Processus Vite
|
|
470
|
+
M->>S: onKernelBoot — registerEntry(module, déclaration)
|
|
471
|
+
Note over S: résout root/entryFile/outDir/publicPath<br/>et empile l'entrée
|
|
472
|
+
K->>S: onServersReady (les serveurs écoutent DÉJÀ)
|
|
473
|
+
S->>S: regroupe les entrées par famille + plan de ports
|
|
474
|
+
S->>V: écrit vite.config.generated.mjs, puis spawn
|
|
475
|
+
V-->>S: « Local: http://host:port » ⇒ état ready
|
|
476
|
+
S->>S: déclare les origines Vite au pare-feu (CSP)
|
|
477
|
+
Note over S,V: sonde de vie périodique · relance sur plantage
|
|
478
|
+
K->>S: onTerminate
|
|
479
|
+
S->>V: SIGINT, puis SIGKILL au bout de 3 s
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
**Pourquoi `onServersReady` et pas `onReady`.** Vite ne doit démarrer qu'une fois les serveurs
|
|
483
|
+
Nodefony en écoute (`FrontendService.init()`, `FrontendService.ts:146`). Dans l'autre ordre, le proxy
|
|
484
|
+
de Vite viserait un serveur inexistant et les premiers appels d'API échoueraient — un défaut
|
|
485
|
+
intermittent, apparaissant seulement quand le navigateur est plus rapide que le démarrage.
|
|
486
|
+
|
|
487
|
+
### Les pièces
|
|
488
|
+
|
|
489
|
+
| Pièce | Rôle | Ancre |
|
|
490
|
+
| ----------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------- |
|
|
491
|
+
| `FrontendService` | l'orchestrateur : entrées, familles, cycle de vie, rendu | `FrontendService.ts:70` |
|
|
492
|
+
| `ViteProcessSupervisor` | lance, surveille, relance et arrête **un** processus Vite | `ViteProcessSupervisor.ts:215` |
|
|
493
|
+
| `ViteConfigGenerator` | écrit la configuration Vite (fonction pure, testée seule) | `ViteConfigGenerator.toMjs()` (`ViteConfigGenerator.ts:80`) |
|
|
494
|
+
| `ViteBuilder` | construit l'objet de configuration Vite pour le build en processus | `ViteBuilder.buildViteConfig()` (`ViteBuilder.ts:41`) |
|
|
495
|
+
| `TemplateHelper` | produit les balises (dev) ou lit le manifeste (prod) | `TemplateHelper.ts:36` |
|
|
496
|
+
| `isolationGroups` | à quelle famille appartient un preset, et sur quel bloc de ports | `isolationGroup()` (`isolationGroups.ts:39`) |
|
|
497
|
+
| `FrontendAdminApi` | la vue sûre de l'état, pour Studio | `buildFrontendStatus()` (`FrontendAdminApi.ts:139`) |
|
|
498
|
+
|
|
499
|
+
### Une configuration Vite écrite, pas passée
|
|
500
|
+
|
|
501
|
+
Le superviseur **écrit un fichier** `vite.config.generated.mjs` à la racine front, puis lance Vite
|
|
502
|
+
dessus. Ce détour a une raison : Vite ne lit pas sa configuration sur l'entrée standard, et les
|
|
503
|
+
greffons sont des objets JavaScript — non sérialisables en JSON. Le fichier généré est donc autonome :
|
|
504
|
+
il importe lui-même les greffons dont les presets détectés ont besoin.
|
|
505
|
+
|
|
506
|
+
**Ne l'édite jamais** : il est réécrit à chaque démarrage. Ce qu'il contient de notable :
|
|
507
|
+
|
|
508
|
+
- une **entrée par bundle** (`input`), d'où le multi-modules dans une seule instance ;
|
|
509
|
+
- la `base` en **URL absolue** vers Vite — sans quoi un import transformé en `/src/App.tsx` serait
|
|
510
|
+
résolu contre l'origine de Nodefony, donc en 404 ;
|
|
511
|
+
- `strictPort` activé dès que cette base est posée : si Vite glissait de port, la base mentirait en
|
|
512
|
+
silence ;
|
|
513
|
+
- un `server.fs.allow` élargi aux racines de **chaque** entrée — sans quoi deux modules ayant tous
|
|
514
|
+
deux `frontend/src/main.tsx` verraient le second recevoir le fichier du premier ;
|
|
515
|
+
- un `resolve.dedupe` sur les paquets du framework UI — deux copies de React dans la même page
|
|
516
|
+
produisent l'énigmatique « Invalid hook call » et une page blanche.
|
|
517
|
+
|
|
518
|
+
### `apiProxyPaths` — que le `fetch` atteigne le serveur
|
|
519
|
+
|
|
520
|
+
C'est le mécanisme le plus important à comprendre du module, parce que son absence produit une erreur
|
|
521
|
+
qui ne parle pas de proxy.
|
|
522
|
+
|
|
523
|
+
Ta page est chargée depuis Vite. Un `fetch("/shop/api/products")` part donc vers **Vite** (port 5173),
|
|
524
|
+
pas vers Nodefony. Vite ne connaît pas cette route ; comme tout serveur de développement d'application
|
|
525
|
+
monopage, il répond alors son `index.html`. Ton code reçoit du HTML là où il attendait du JSON :
|
|
526
|
+
|
|
527
|
+
```
|
|
528
|
+
SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
Déclarer `apiProxyPaths: ["/shop/api"]` inscrit ce préfixe dans le proxy de la configuration générée :
|
|
532
|
+
Vite transmet alors ces requêtes à Nodefony, sur le port **réellement** écouté. Trois points à
|
|
533
|
+
connaître :
|
|
534
|
+
|
|
535
|
+
1. **Les préfixes de tous les modules sont agrégés et dédupliqués** — un seul Vite, un seul proxy.
|
|
536
|
+
2. **Une clé commençant par `^` est traitée comme une expression régulière** par Vite. C'est ainsi que
|
|
537
|
+
le data plane d'administration est couvert d'un coup.
|
|
538
|
+
3. **`/nodefony/<module>/api` est ajouté d'office**, sans que tu le déclares (`ViteConfigGenerator.ts:172`).
|
|
539
|
+
Sans cela, la barre de débogage injectée en développement appellerait `/nodefony/profiler/api` et
|
|
540
|
+
recevrait le repli SPA — le clic serait mort.
|
|
541
|
+
|
|
542
|
+
> [!WARNING]
|
|
543
|
+
> Ne proxifie **que** tes chemins d'API. Proxifier `/` renverrait aussi les modules et le rechargement
|
|
544
|
+
> à chaud vers Nodefony, qui n'en sait rien : plus rien ne se charge.
|
|
545
|
+
|
|
546
|
+
### Familles d'isolation — pourquoi Angular a son Vite
|
|
547
|
+
|
|
548
|
+
React, Vue et vanilla ciblent des extensions disjointes (`.tsx`, `.vue`) et cohabitent sans conflit
|
|
549
|
+
dans une seule instance. Angular, lui, transforme **tout** fichier `.ts` du serveur de développement,
|
|
550
|
+
y compris ceux des autres bundles — il échoue alors sur des fichiers hors de son `tsconfig`, ce qui
|
|
551
|
+
déclenche une boucle de rechargement.
|
|
552
|
+
|
|
553
|
+
D'où le regroupement par **famille** (`isolationGroup()`, `isolationGroups.ts:20`) : `angular` a la
|
|
554
|
+
sienne, tout le reste partage `default`. Chaque famille obtient un **bloc de ports disjoint**
|
|
555
|
+
(`familyPortPlan()`, `isolationGroups.ts:63`) de taille `portRetryAttempts + 1` : ainsi, une instance
|
|
556
|
+
qui glisse de port sur conflit ne peut jamais empiéter sur le bloc d'une autre. La famille principale
|
|
557
|
+
garde le port habituel (`PRIMARY_FAMILY`, `isolationGroups.ts:56`).
|
|
558
|
+
|
|
559
|
+
**Les familles démarrent indépendamment.** Si Angular échoue, React continue de fonctionner : le
|
|
560
|
+
démarrage n'échoue que si **aucune** famille n'a pu démarrer (`FrontendService.startDev()`,
|
|
561
|
+
`FrontendService.ts:316`).
|
|
562
|
+
|
|
563
|
+
### Résilience — ce qui se passe quand Vite tombe
|
|
564
|
+
|
|
565
|
+
Le superviseur est écrit pour survivre à un poste de développement réel, où les ports sont occupés et
|
|
566
|
+
les processus meurent.
|
|
567
|
+
|
|
568
|
+
| Situation | Réponse |
|
|
569
|
+
| -------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
570
|
+
| Port occupé au lancement | essai sur le port suivant, jusqu'à `portRetryAttempts` (`ViteProcessSupervisor.ts:276`) |
|
|
571
|
+
| Vite plante | relance avec délai exponentiel plafonné (`scheduleRestart()`, `ViteProcessSupervisor.ts:674`) |
|
|
572
|
+
| Vite ne répond plus (gelé) | sonde périodique ; après N échecs, Vite est tué pour être relancé (`ViteProcessSupervisor.ts:599`) |
|
|
573
|
+
| Deux `start()` concurrents | la promesse en cours est partagée — jamais deux processus |
|
|
574
|
+
| Ctrl+C au terminal | le signal marque un arrêt **voulu** : pas de relance (`markShutdown`, `ViteProcessSupervisor.ts:245`) |
|
|
575
|
+
| Arrêt du kernel | `SIGINT`, puis `SIGKILL` après 3 s — aucun zombie ne bloque le port (`ViteProcessSupervisor.ts:26`) |
|
|
576
|
+
|
|
577
|
+
Deux subtilités valent d'être connues, parce qu'elles expliquent des comportements sinon
|
|
578
|
+
incompréhensibles :
|
|
579
|
+
|
|
580
|
+
- **Vite intercepte `SIGINT` et sort proprement**, avec un code indiscernable d'un plantage. Sans le
|
|
581
|
+
marquage du signal reçu par le processus serveur, un simple Ctrl+C ferait apparaître un
|
|
582
|
+
« redémarrage échoué » en erreur, sur un arrêt parfaitement normal.
|
|
583
|
+
- **La détection d'un port occupé est écrite à un seul endroit** (`isPortInUseMessage()`,
|
|
584
|
+
`ViteProcessSupervisor.ts:164`), et tolère les deux formulations de Vite (« is in use » comme « is
|
|
585
|
+
**already** in use »). Deux implémentations de la même règle avaient divergé : la reprise sur port
|
|
586
|
+
ne se déclenchait jamais, et la seconde application perdait toute son interface.
|
|
587
|
+
|
|
588
|
+
Les écouteurs attachés au processus enfant sont suivis puis retirés à chaque mort
|
|
589
|
+
(`cleanupChildListeners()`, `ViteProcessSupervisor.ts:922`) : sans cela, les relances successives les
|
|
590
|
+
accumuleraient jusqu'à l'avertissement de fuite.
|
|
591
|
+
|
|
592
|
+
## 🧰 API publique
|
|
593
|
+
|
|
594
|
+
Les signatures exactes vivent dans le graphe généré (`jq '.symbols.FrontendService' .ai/symbols.json`)
|
|
595
|
+
et dans les types du paquet — jamais recopiées ici, où elles se périmeraient. Ce qui suit montre
|
|
596
|
+
**l'usage**.
|
|
597
|
+
|
|
598
|
+
### `registerEntry` — la déclaration d'une interface
|
|
599
|
+
|
|
600
|
+
`FrontendService.registerEntry()` (`FrontendService.ts:221`) est appelée par le module consommateur,
|
|
601
|
+
dans son `onKernelBoot()`. Elle résout les chemins relatifs, calcule le préfixe public et renvoie
|
|
602
|
+
l'entrée résolue (`IResolvedFrontendEntry`, `IFrontBuilder.ts:40`).
|
|
603
|
+
|
|
604
|
+
| Champ | Requis | Défaut | Rôle |
|
|
605
|
+
| --------------- | ------ | ------------------ | ---------------------------------------------------------- |
|
|
606
|
+
| `type` | oui | — | Le preset : `react19`, `vue3`, `angular`, `vanilla`. |
|
|
607
|
+
| `entry` | oui | — | Le fichier d'entrée, relatif à la racine du module. |
|
|
608
|
+
| `root` | non | `./frontend` | La racine front (celle qui contient `index.html`). |
|
|
609
|
+
| `outDir` | non | `./public/dist` | Où le build écrit ce bundle. |
|
|
610
|
+
| `name` | non | nom du module | Nom logique du bundle — c'est la clé de `renderTags(...)`. |
|
|
611
|
+
| `publicPath` | non | `/_assets/<name>/` | Préfixe d'URL des assets en production. |
|
|
612
|
+
| `apiProxyPaths` | non | `[]` | Les préfixes que Vite doit transmettre à Nodefony. |
|
|
613
|
+
|
|
614
|
+
```ts ignore
|
|
615
|
+
frontend.registerEntry(this, {
|
|
616
|
+
type: "vue3",
|
|
617
|
+
entry: "./frontend/src/main.ts",
|
|
618
|
+
name: "admin", // → renderTags("admin"), assets sous /_assets/admin/
|
|
619
|
+
apiProxyPaths: ["/admin/api"],
|
|
620
|
+
});
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
> [!NOTE]
|
|
624
|
+
> **`entry` est relatif au module, `root` à la racine front.** Le service stocke l'entrée relative à
|
|
625
|
+
> `root` pour que l'URL servie par Vite et la clé du manifeste soient cohérentes par construction.
|
|
626
|
+
> C'est la source d'une confusion fréquente quand on lit les chemins dans les journaux.
|
|
627
|
+
|
|
628
|
+
### Rendu — des balises ou un document complet
|
|
629
|
+
|
|
630
|
+
Deux portes, une seule source. La différence tient à qui écrit la coquille HTML.
|
|
631
|
+
|
|
632
|
+
```ts ignore
|
|
633
|
+
// Porte 1 — tu écris la page, on injecte les balises
|
|
634
|
+
const tags = frontend.renderTags("shop", context.cspNonce);
|
|
635
|
+
|
|
636
|
+
// Porte 2 — tu écris frontend/index.html, on injecte dedans (recommandé)
|
|
637
|
+
const html = frontend.renderDocument("shop", context.cspNonce);
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
`renderDocument` (`FrontendService.ts:876`) lit l'`index.html` **de ton module**, retire le `<script>`
|
|
641
|
+
d'entrée source, injecte les balises au marqueur (ou avant `</head>`), et renvoie le document.
|
|
642
|
+
Pas d'`index.html` ? Une coquille minimale est générée. En production, l'index est mis en cache ; en
|
|
643
|
+
développement il est relu à chaque appel, pour que tes modifications de la coquille apparaissent.
|
|
644
|
+
|
|
645
|
+
Ce qui est injecté en développement (`TemplateHelper.renderDevTags()`, `TemplateHelper.ts:153`) :
|
|
646
|
+
|
|
647
|
+
1. le **préambule React Fast Refresh** pour les entrées `react19` — sans lui, `@vitejs/plugin-react`
|
|
648
|
+
refuse de démarrer ;
|
|
649
|
+
2. le client Vite (`@vite/client`) qui ouvre la connexion de rechargement à chaud ;
|
|
650
|
+
3. ton entrée, servie par son **chemin absolu** (`/@fs/…`) plutôt que relatif — c'est ce qui permet à
|
|
651
|
+
deux modules d'avoir chacun leur `frontend/src/main.tsx` sans collision ;
|
|
652
|
+
4. un pont qui relaie les événements de rechargement vers la barre de débogage, **sans ouvrir de
|
|
653
|
+
seconde connexion** (`hmrBridgeTag()`, `TemplateHelper.ts:226`) ;
|
|
654
|
+
5. la barre de débogage elle-même, résolue une fois et servie via Vite (`debugBarTag()`,
|
|
655
|
+
`TemplateHelper.ts:252`).
|
|
656
|
+
|
|
657
|
+
Quand Vite n'est pas prêt, le rendu ne lève **jamais** : il renvoie un commentaire HTML disant
|
|
658
|
+
l'état. Une page dégradée reste une page.
|
|
659
|
+
|
|
660
|
+
### Les helpers de vue
|
|
661
|
+
|
|
662
|
+
Si tu rends une vue Eta plutôt qu'une chaîne, trois helpers sont déjà dans tes variables locales
|
|
663
|
+
(`Controller.withFrontendLocals()`, `Controller.ts:345`) — inspirés des helpers d'assets de Symfony :
|
|
664
|
+
|
|
665
|
+
```html
|
|
666
|
+
<%~ frontendDocument("shop") %>
|
|
667
|
+
<!-- le document complet -->
|
|
668
|
+
<%~ frontendTags("shop") %>
|
|
669
|
+
<!-- juste les balises -->
|
|
670
|
+
<img src="<%= asset('/img/logo.png') %>" />
|
|
671
|
+
<!-- URL CDN si configurée -->
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
Le service `frontend` y est résolu **par nom** : le module `framework` ne dépend pas de
|
|
675
|
+
`@nodefony/frontend`, et une application sans interface n'a simplement pas ces helpers.
|
|
676
|
+
|
|
677
|
+
### Construire pour la production — `frontend:build`
|
|
678
|
+
|
|
679
|
+
```bash
|
|
680
|
+
npx nodefony frontend:build # construit ce qui a changé
|
|
681
|
+
npx nodefony frontend:build --force # reconstruit tout
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
Dans une application générée par `nodefony create app`, tu n'as pas à y penser :
|
|
685
|
+
**`npm run build` construit l'application entière** — le backend (rolldown) puis le front (il
|
|
686
|
+
chaîne `nodefony frontend:build`). Un seul geste avant `npm start` ou dans un pipeline.
|
|
687
|
+
|
|
688
|
+
`FrontendService.build()` (`FrontendService.ts:761`) appelle Vite **entrée par entrée**, et non une
|
|
689
|
+
fois pour toutes. Ce n'est pas un détail : chaque bundle a sa racine, son dossier de sortie, sa base
|
|
690
|
+
et son manifeste — c'est ce qui rend le multi-modules possible et ce qui isole Angular.
|
|
691
|
+
|
|
692
|
+
Quatre comportements à connaître :
|
|
693
|
+
|
|
694
|
+
- **Idempotent.** Une entrée dont le manifeste est plus récent que ses sources est ignorée
|
|
695
|
+
(`isBuildFresh()`, `FrontendService.ts:829`) — le scan est borné au dossier front et saute
|
|
696
|
+
`node_modules`. Relancer un déploiement ne recompile pas tout.
|
|
697
|
+
- **Les échecs sont collectés, pas propagés.** Un bundle en échec n'arrête pas les autres ; la
|
|
698
|
+
commande passe le code de sortie à `1` s'il en reste un — de quoi casser un pipeline sans masquer
|
|
699
|
+
les autres résultats.
|
|
700
|
+
- **Le résultat est un bilan** : construits / ignorés / en échec, journalisé et renvoyé.
|
|
701
|
+
- **Un démarrage en production sans build se répare — ou se dénonce.** `setupProd()`
|
|
702
|
+
(`FrontendService.ts:655`) vérifie le manifeste de chaque entrée AVANT de monter les statics.
|
|
703
|
+
Manifeste absent et Vite installé (poste de développement, devDependencies présentes) : le build
|
|
704
|
+
tourne **une fois au démarrage**, annoncé en WARNING — fini l'écran blanc après un
|
|
705
|
+
`nodefony production --detach` lancé trop tôt. Manifeste absent et Vite introuvable (image de
|
|
706
|
+
production sans devDependencies) : impossible de compiler ici — le démarrage continue (l'API
|
|
707
|
+
sert) mais une ERROR nomme l'entrée, le manifeste attendu et le geste (`npm run build` à
|
|
708
|
+
l'image). Jamais de page blanche muette.
|
|
709
|
+
|
|
710
|
+
### Les commandes
|
|
711
|
+
|
|
712
|
+
| Commande | Rôle |
|
|
713
|
+
| ------------------------------- | ----------------------------------------------------------------------- |
|
|
714
|
+
| `nodefony frontend:build [-f]` | Construit les bundles de production. `-f` ignore le cache de fraîcheur. |
|
|
715
|
+
| `nodefony frontend:dev` | Démarre le serveur Vite manuellement (si le démarrage auto est coupé). |
|
|
716
|
+
| `nodefony frontend:status [-j]` | État du superviseur : état, point d'écoute, pid, entrées. `-j` en JSON. |
|
|
717
|
+
|
|
718
|
+
### `publicPath` et `assetBaseUrl` — où vivent les assets
|
|
719
|
+
|
|
720
|
+
`publicPath` est le **concept pivot** de la production : la même valeur sert simultanément de `base`
|
|
721
|
+
Vite au build, de préfixe de montage pour le serveur statique, et de préfixe des URLs émises dans la
|
|
722
|
+
page. Les trois restent alignés par construction — impossible d'en changer un seul et de casser les
|
|
723
|
+
deux autres.
|
|
724
|
+
|
|
725
|
+
Défaut : `/_assets/<name>/`, normalisé avec ses barres obliques
|
|
726
|
+
(`normalizePublicPath()`, `FrontendService.ts:50`). Chaque bundle a donc son espace, sans collision
|
|
727
|
+
entre modules.
|
|
728
|
+
|
|
729
|
+
`assetBaseUrl` ajoute une couche : la base d'un CDN. Renseignée, elle préfixe la `base` du build et
|
|
730
|
+
les URLs de la page, **sans toucher au montage statique** (qui reste relatif à ton origine). Basculer
|
|
731
|
+
vers un CDN est donc un changement de configuration, pas de code :
|
|
732
|
+
|
|
733
|
+
```ts ignore
|
|
734
|
+
use("@nodefony/frontend", { assetBaseUrl: "https://cdn.example.com" });
|
|
735
|
+
// → <script src="https://cdn.example.com/_assets/shop/main-a1b2c3.js">
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
En production, `setupProd()` (`FrontendService.ts:655`) monte chaque dossier de sortie sur son
|
|
739
|
+
`publicPath` via le serveur statique — résolu **par nom**, jamais par import, pour ne pas créer de
|
|
740
|
+
cycle. Si ce service est absent (proxy frontal, CDN devant), un avertissement le dit et rien n'est
|
|
741
|
+
monté : c'est un déploiement valide, pas une panne.
|
|
742
|
+
|
|
743
|
+
### Ce qui est servi en production
|
|
744
|
+
|
|
745
|
+
`renderProdTags()` (`TemplateHelper.ts:315`) lit `manifest.json` — la carte produite par Vite — et
|
|
746
|
+
émet, dans cet ordre : les feuilles de style d'abord (pour éviter le flash de contenu non stylé), les
|
|
747
|
+
préchargements des morceaux partagés, puis le script d'entrée. Le manifeste est lu **une fois par
|
|
748
|
+
dossier de sortie** et mis en cache : aucune lecture disque par requête. Le CSS est collecté
|
|
749
|
+
récursivement à travers les imports (`collectCss()`, `TemplateHelper.ts:389`), sans quoi le style
|
|
750
|
+
d'un morceau partagé manquerait sur certaines pages.
|
|
751
|
+
|
|
752
|
+
Manifeste absent ? Un commentaire HTML le dit, avec la commande à lancer. Pas d'exception, pas de
|
|
753
|
+
page blanche muette.
|
|
754
|
+
|
|
755
|
+
## 🧩 Extension
|
|
756
|
+
|
|
757
|
+
**Ajouter un framework UI** revient à écrire un preset (`IFrontPreset`, `IFrontPreset.ts:16`) : son
|
|
758
|
+
identifiant, ses extensions, ses dépendances à pré-empaqueter, et une fonction qui construit ses
|
|
759
|
+
greffons Vite. Les quatre presets fournis sont les modèles à copier — React (`react19-vite.ts:9`),
|
|
760
|
+
Vue (`vue3-vite.ts:11`), Angular (`angular-vite.ts:15`), vanilla (`vanilla-vite.ts:9`). Tous
|
|
761
|
+
importent leur greffon **paresseusement** : un preset non utilisé ne coûte ni installation, ni
|
|
762
|
+
chargement.
|
|
763
|
+
|
|
764
|
+
Deux points d'attention avant de se lancer :
|
|
765
|
+
|
|
766
|
+
- le preset alimente le build en processus (`ViteBuilder`), mais la configuration du **serveur de
|
|
767
|
+
développement** est écrite par le générateur, qui possède sa propre correspondance type → greffon
|
|
768
|
+
(`ViteConfigGenerator.toMjs()`, `ViteConfigGenerator.ts:80`). Un nouveau type doit être ajouté aux
|
|
769
|
+
**deux** endroits, sinon il lève `FrontendPresetUnknownError` (`FrontendError.ts:19`) ;
|
|
770
|
+
- si le nouveau framework transforme des fichiers qui ne lui appartiennent pas, il lui faut sa propre
|
|
771
|
+
famille d'isolation — c'est la leçon d'Angular.
|
|
772
|
+
|
|
773
|
+
**Remplacer le superviseur** est prévu par le contrat `IViteSupervisor` (`IViteSupervisor.ts:41`) :
|
|
774
|
+
`start`, `stop`, `status`. C'est le seul point d'isolement entre « Vite dans un processus séparé » et
|
|
775
|
+
toute autre stratégie.
|
|
776
|
+
|
|
777
|
+
## 🔐 Sécurité — la CSP, sans trou et sans bricolage
|
|
778
|
+
|
|
779
|
+
Une politique de sécurité du contenu stricte bloque, par construction, les scripts venus d'une autre
|
|
780
|
+
origine. Or en développement, tes modules viennent du port 5173 alors que ta page vient du 5151 :
|
|
781
|
+
**tout** serait bloqué.
|
|
782
|
+
|
|
783
|
+
La solution retenue n'est pas d'affaiblir la politique, mais de la **composer**. Une fois Vite prêt
|
|
784
|
+
(donc ses ports réellement connus), le service déclare ses origines au pare-feu
|
|
785
|
+
(`#registerCsp()`, `FrontendService.ts:948`), qui émet **un seul** en-tête, origines fusionnées et
|
|
786
|
+
nonce par requête. À l'arrêt, les origines sont retirées et la politique redevient stricte.
|
|
787
|
+
|
|
788
|
+
Le fragment déclaré (`#viteCspFragment()`, `FrontendService.ts:909`) mérite deux explications, parce
|
|
789
|
+
qu'elles piègent tout le monde :
|
|
790
|
+
|
|
791
|
+
- **`'self'` est répété dans chaque directive.** `connect-src`, `style-src`, `img-src` et `font-src`
|
|
792
|
+
n'héritent **pas** de `default-src` : les omettre bloquerait tes propres appels, styles, images et
|
|
793
|
+
polices.
|
|
794
|
+
- **`'unsafe-eval'` est nécessaire en développement** pour React Fast Refresh, que le nonce ne couvre
|
|
795
|
+
pas. En revanche `'unsafe-inline'` n'est **pas** accordé aux scripts : le préambule injecté porte un
|
|
796
|
+
nonce.
|
|
797
|
+
|
|
798
|
+
Les origines sont générées pour tous les hôtes légitimes de développement — boucle locale, domaine du
|
|
799
|
+
kernel, hôtes de confiance déclarés au module HTTP — croisés avec les ports Vite réels. Sans cela,
|
|
800
|
+
accéder à ton application par un hôte virtuel bloquerait tout.
|
|
801
|
+
|
|
802
|
+
> [!IMPORTANT]
|
|
803
|
+
> **Ce fragment n'existe qu'en développement.** En production le superviseur ne démarre pas, donc rien
|
|
804
|
+
> n'est déclaré : la politique reste stricte et même origine. Il n'y a pas de mode où `'unsafe-eval'`
|
|
805
|
+
> fuirait jusqu'à un déploiement.
|
|
806
|
+
|
|
807
|
+
Deux garde-fous complètent le tableau : le producteur de données pour Studio expose une vue **sans
|
|
808
|
+
chemins de fichiers absolus** (`IViteInstanceView`, `FrontendAdminApi.ts:78`), et le serveur de
|
|
809
|
+
développement n'autorise l'accès disque qu'aux racines explicitement listées.
|
|
810
|
+
|
|
811
|
+
## ⚡ Performance et mémoire
|
|
812
|
+
|
|
813
|
+
**Le coût par requête est nul, par construction.** Compiler se passe dans un autre processus : ni la
|
|
814
|
+
boucle d'événements, ni la mémoire de ton serveur ne voient passer une transformation de module.
|
|
815
|
+
C'est la raison d'être du choix `child_process` — mesurée au moment du choix, et la raison pour
|
|
816
|
+
laquelle les fils d'exécution (`worker_threads`) ont été essayés puis écartés : la sérialisation des
|
|
817
|
+
journaux annulait le bénéfice.
|
|
818
|
+
|
|
819
|
+
Sur le chemin chaud du rendu, trois précautions :
|
|
820
|
+
|
|
821
|
+
- le **manifeste** est lu une fois par dossier de sortie, jamais par requête ;
|
|
822
|
+
- l'**`index.html`** est mis en cache en production (relu en développement, où la fraîcheur prime) ;
|
|
823
|
+
- les **écouteurs** du processus enfant sont suivis et retirés à chaque mort
|
|
824
|
+
(`trackListener()`, `ViteProcessSupervisor.ts:912`) — sans quoi les relances les accumuleraient.
|
|
825
|
+
|
|
826
|
+
La sonde de vie coûte une requête HTTP toutes les trente secondes par famille. Elle est désactivable
|
|
827
|
+
(`healthCheckIntervalMs: 0`) si ce budget te gêne, au prix de la détection d'un Vite gelé.
|
|
828
|
+
|
|
829
|
+
## 📡 Observabilité — Studio et CLI
|
|
830
|
+
|
|
831
|
+
En ligne de commande, `nodefony frontend:status` donne l'état, le point d'écoute réel, le pid et les
|
|
832
|
+
entrées servies ; `-j` produit le même contenu en JSON, exploitable par un script.
|
|
833
|
+
|
|
834
|
+
Côté data plane, le module enregistre son producteur au démarrage
|
|
835
|
+
(`createFrontendAdminApi()`, `FrontendAdminApi.ts:165`) :
|
|
836
|
+
|
|
837
|
+
| Route | Contenu |
|
|
838
|
+
| --------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
839
|
+
| `GET /nodefony/frontend/api/vite` | État du superviseur : instance principale, toutes les familles, versions résolues du framework UI et de Vite. |
|
|
840
|
+
|
|
841
|
+
La réponse dit `available: true` dès qu'une instance est prête — c'est le signal « rechargement à
|
|
842
|
+
chaud actif ». Hors développement elle répond quand même, avec un état `idle` et aucun pid :
|
|
843
|
+
l'interface en déduit que l'UI vient des bundles compilés. La lecture ne lève **jamais**.
|
|
844
|
+
|
|
845
|
+
En développement, deux surfaces de plus : la **barre de débogage** injectée automatiquement dans
|
|
846
|
+
toute page front affiche le framework, l'origine Vite et un compteur de rechargements à chaud ; et la
|
|
847
|
+
**checklist de démarrage** affiche une ligne par bundle servi, avec l'URL à ouvrir — Vite terminant
|
|
848
|
+
sa compilation après le reste du kernel, le service émet deux événements dédiés pour que cette ligne
|
|
849
|
+
apparaisse avant le « prêt ».
|
|
850
|
+
|
|
851
|
+
## ⚠️ Pièges
|
|
852
|
+
|
|
853
|
+
<!-- prettier-ignore -->
|
|
854
|
+
| Symptôme | Cause | Correction |
|
|
855
|
+
| --- | --- | --- |
|
|
856
|
+
| `Unexpected token '<'` sur un `fetch` | Vite répond son repli SPA : le préfixe d'API n'est pas proxifié | déclarer `apiProxyPaths: ["/mon/api"]` dans `registerEntry` |
|
|
857
|
+
| Le service `frontend` est introuvable au `onKernelBoot` | ordre de chargement des modules | placer `@nodefony/frontend` **avant** ses consommateurs dans `modules` |
|
|
858
|
+
| L'entrée n'apparaît pas dans le superviseur | `registerEntry` appelé après `onKernelReady` | enregistrer dans `onKernelBoot`, jamais plus tard |
|
|
859
|
+
| `@vitejs/plugin-react can't detect preamble` | le préambule React n'est pas dans la page | rendre via `renderTags`/`renderDocument`, qui l'injectent |
|
|
860
|
+
| Page blanche + « Invalid hook call » | deux copies de React dans la page (deux `node_modules`) | comportement couvert par `resolve.dedupe` ; purger le pré-empaquetage de Vite |
|
|
861
|
+
| Deux modules affichent la **même** interface | racines identiques et URL relatives | comportement couvert : l'entrée est servie par chemin absolu `/@fs/…` |
|
|
862
|
+
| Vite écoute sur un autre port que `devPort` | port occupé ⇒ reprise sur le suivant | lire le port **réel** dans `frontend:status`, jamais la configuration |
|
|
863
|
+
| Les appels d'API partent vers une autre application | port du serveur glissé, proxy figé sur `backendPort` | comportement couvert : le port réel est lu sur le serveur ; vérifier le journal |
|
|
864
|
+
| `no frontend entries declared` au démarrage | aucun module n'a appelé `registerEntry` | normal si tu n'as pas d'interface ; sinon voir les deux lignes ci-dessus |
|
|
865
|
+
| `max restarts reached` | Vite plante en boucle | lire les lignes `[vite]` du journal — l'erreur est dans ton code front |
|
|
866
|
+
| Le navigateur refuse le certificat de Vite | certificat auto-signé sur une origine distincte | l'accepter sur l'origine Vite, ou installer l'autorité racine de développement |
|
|
867
|
+
| `Refused to load the script` (politique de sécurité) | le pare-feu n'a pas les origines Vite (Vite pas encore prêt au rendu) | recharger une fois Vite prêt ; vérifier que le nonce est bien propagé |
|
|
868
|
+
| Commentaire `prod manifest missing` dans la page | les bundles n'ont pas été construits, et Vite n'était pas là pour le faire au démarrage | `npm run build` (ou `npx nodefony frontend:build`) puis **recharge la page** — l'absence de manifeste n'est jamais mise en cache (`loadManifest()`, `TemplateHelper.ts:336`), le serveur voit le build sans redémarrer |
|
|
869
|
+
| Les assets répondent 404 en production | le serveur statique est absent ou le préfixe ne correspond pas | vérifier le montage journalisé au démarrage, et `publicPath` |
|
|
870
|
+
| Un module à interface est invisible de `listEntries()` | il est en mode `static` — il n'appelle jamais `registerEntry` | attendu ; regarder la molette `ui` et le mode journalisé au démarrage |
|
|
871
|
+
| Modifications du front sans effet | `vite.config.generated.mjs` édité à la main | ne jamais l'éditer : il est réécrit à chaque démarrage |
|
|
872
|
+
| Angular recharge la page entière au lieu du composant | son greffon ne fait pas de remplacement à chaud | attendu — c'est le comportement du greffon Angular |
|
|
873
|
+
|
|
874
|
+
## 🧪 Tests et couverture
|
|
875
|
+
|
|
876
|
+
Les compteurs exacts sont régénérés depuis vitest et vivent dans la carte de l'aperçu — jamais figés
|
|
877
|
+
dans ce texte.
|
|
878
|
+
|
|
879
|
+
| Type | Où | Ce qui est prouvé |
|
|
880
|
+
| ------------------------- | ------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
881
|
+
| Unitaire — génération | `tests/unit/ViteConfigGenerator.test.ts` | la configuration écrite : proxy, base absolue, HTTPS, greffons, entrées |
|
|
882
|
+
| Unitaire — build | `tests/unit/ViteBuilder.test.ts` | la base de production dérivée de `publicPath` et `assetBaseUrl` |
|
|
883
|
+
| Unitaire — isolation | `tests/unit/isolationGroups.test.ts` | familles, ordre déterministe, blocs de ports disjoints |
|
|
884
|
+
| Unitaire — ports | `tests/unit/vitePortInUse.test.ts` | la détection d'un port occupé, dans **toutes** les formulations de Vite |
|
|
885
|
+
| Intégration — superviseur | `tests/integration/ViteProcessSupervisor.test.ts` | vrai `spawn` : démarrage, arrêt, idempotence, relance après plantage |
|
|
886
|
+
| Intégration — build | `tests/integration/frontend-build.test.ts` | vrai `vite.build`, manifeste lu, document rendu |
|
|
887
|
+
|
|
888
|
+
```bash
|
|
889
|
+
cd src/packages/@nodefony/frontend
|
|
890
|
+
npm test # unitaires — rapides, sans processus externe
|
|
891
|
+
npm run test:integration # lance de vrais processus Vite (quelques secondes)
|
|
892
|
+
npm run coverage # couverture (vitest)
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
**Ce qui n'est volontairement pas mesuré.** L'intégration lance Vite dans un **processus séparé** :
|
|
896
|
+
ce code n'est jamais instrumenté par la couverture. Un pourcentage global bas sur ce module ne dit
|
|
897
|
+
donc rien de sa fiabilité — c'est un artefact de mesure, pas une dette. Ce qui est mesurable est le
|
|
898
|
+
générateur de configuration, fonction pure, couvert intégralement.
|
|
899
|
+
|
|
900
|
+
**Ce qui manque.** Pas de banc de charge dédié : le module n'est pas sur le chemin d'une requête (son
|
|
901
|
+
coût par requête est structurellement nul), et le budget mémoire du pipeline est gardé ailleurs.
|
|
902
|
+
Le rendu du navigateur n'est pas testé automatiquement — la vérification passe par la transformation
|
|
903
|
+
Vite en ligne de commande, jamais par un navigateur sans affichage.
|
|
904
|
+
|
|
905
|
+
## 🔗 Pour aller plus loin
|
|
906
|
+
|
|
907
|
+
- ⬆️ **Retour** : [Toute la documentation](../../../../../docs/index.md) ·
|
|
908
|
+
[Démarrer avec Nodefony](../../../../../docs/demarrer.md)
|
|
909
|
+
- 📗 **Guide pas à pas** :
|
|
910
|
+
[créer un module avec une interface React](../../../../../docs/guides/frontend-react.md)
|
|
911
|
+
- 🖥️ **Le premier consommateur** : [`@nodefony/studio`](../../studio/docs/index.md) — l'administration
|
|
912
|
+
du framework, servie par ce module en développement et par ses assets pré-construits ailleurs.
|
|
913
|
+
- 🔌 **La couche en dessous** : [`@nodefony/http`](../../http/docs/index.md) — serveur statique,
|
|
914
|
+
certificats partagés, molette de livraison de l'interface.
|
|
915
|
+
- 🧭 **Le rendu des pages** : [`@nodefony/framework`](../../framework/docs/index.md) — contrôleurs,
|
|
916
|
+
vues Eta et les helpers `frontendTags`/`frontendDocument`.
|
|
917
|
+
- 🔐 **La politique de sécurité** : [`@nodefony/security`](../../security/docs/index.md) — le pare-feu
|
|
918
|
+
qui compose l'en-tête CSP à partir des origines déclarées ici.
|
|
919
|
+
- 🏗️ **Comment tout est construit** :
|
|
920
|
+
[build et empaquetage](../../../../../docs/architecture/build-bundling.md) ·
|
|
921
|
+
[vue d'ensemble du framework](../../../../../docs/architecture/vue-ensemble.md)
|
|
922
|
+
- 📖 [Lexique général](../../../../../docs/lexique.md) du framework.
|
|
923
|
+
</content>
|
|
924
|
+
|
|
925
|
+
</invoke>
|