@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
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { Service, Module } from "nodefony";
|
|
2
|
+
import type { IFrontendService, IFrontendBuildResult } from "../interfaces/IFrontendService.js";
|
|
3
|
+
import type { IFrontendModuleDeclaration, IResolvedFrontendEntry } from "../interfaces/IFrontBuilder.js";
|
|
4
|
+
import type { IViteSupervisorStatus } from "../interfaces/IViteSupervisor.js";
|
|
5
|
+
/**
|
|
6
|
+
* Service injectable du module `@nodefony/frontend`.
|
|
7
|
+
*
|
|
8
|
+
* Cycle de vie :
|
|
9
|
+
* 1. construction : merge options par défaut + surcharge app (`module-frontend`).
|
|
10
|
+
* 2. `onKernelReady` : si dev + `autoStartInDevelopment` → start superviseur.
|
|
11
|
+
* 3. modules consommateurs appellent `registerEntry(...)` dans leur init.
|
|
12
|
+
* 4. terminate kernel : `stop()` superviseur.
|
|
13
|
+
*
|
|
14
|
+
* Branche POC `poc/frontend-child` : utilise `ViteProcessSupervisor` (spawn).
|
|
15
|
+
* Branche POC `poc/frontend-single` : remplacera par `ViteInProcSupervisor`.
|
|
16
|
+
*/
|
|
17
|
+
declare class FrontendService extends Service implements IFrontendService {
|
|
18
|
+
#private;
|
|
19
|
+
module: Module;
|
|
20
|
+
private readonly cfg;
|
|
21
|
+
private readonly builder;
|
|
22
|
+
private readonly entries;
|
|
23
|
+
/** Une instance Vite par famille d'isolation (`default`, `angular`, …). */
|
|
24
|
+
private readonly supervisors;
|
|
25
|
+
/** Template helper par famille (route les `<script>` vers le bon port Vite). */
|
|
26
|
+
private readonly templateHelpers;
|
|
27
|
+
/** Index inverse `entryName → famille`, pour router `renderTags`. */
|
|
28
|
+
private readonly entryFamily;
|
|
29
|
+
/** Helper prod unique (lit les manifests) — `null` tant qu'on n'est pas en prod. */
|
|
30
|
+
private prodHelper;
|
|
31
|
+
/**
|
|
32
|
+
* L'origine publique est-elle ÉPINGLÉE par une décision explicite
|
|
33
|
+
* (`frontend.publicOrigin` en config, ou plateforme de dev déporté détectée) ?
|
|
34
|
+
* `true` → la dérivation par `Host` est désactivée : un réglage voulu gagne
|
|
35
|
+
* toujours sur une déduction (cf ordre de priorité, README du module).
|
|
36
|
+
*/
|
|
37
|
+
private originPinned;
|
|
38
|
+
/**
|
|
39
|
+
* Ports que l'instance Vite de chaque famille PEUT prendre pour ce démarrage
|
|
40
|
+
* (bloc de la famille, port-retry compris) — `null` tant que `startDev` n'a
|
|
41
|
+
* pas établi le plan, et remis à `null` par `stopDev`.
|
|
42
|
+
*
|
|
43
|
+
* Alloué une fois par démarrage de développement, jamais en production
|
|
44
|
+
* (`startDev` n'y tourne pas) : aucun coût par requête.
|
|
45
|
+
*/
|
|
46
|
+
private plannedPortBlocks;
|
|
47
|
+
constructor(module: Module);
|
|
48
|
+
/** Base CDN normalisée (sans slash final). `""` = origine Nodefony. */
|
|
49
|
+
private get assetBase();
|
|
50
|
+
/**
|
|
51
|
+
* Résout l'URL publique d'un asset. Préfixe `p` par `assetBaseUrl` (CDN) si
|
|
52
|
+
* renseigné, sinon le renvoie tel quel (origine, chemin relatif). Les URLs
|
|
53
|
+
* absolues (`http(s)://…`) sont renvoyées inchangées. Helper template :
|
|
54
|
+
* `asset('/test/logo.png')` → `https://cdn.example.com/test/logo.png` ou
|
|
55
|
+
* `/test/logo.png` (assetBaseUrl vide).
|
|
56
|
+
*/
|
|
57
|
+
assetUrl(p: string): string;
|
|
58
|
+
init(): Promise<this>;
|
|
59
|
+
/**
|
|
60
|
+
* Enregistre une déclaration frontend d'un module consommateur.
|
|
61
|
+
*
|
|
62
|
+
* À appeler dans le `initialize()` ou `onKernelReady()` du module
|
|
63
|
+
* consommateur — toujours AVANT `onReady` du kernel (sinon le supervisor
|
|
64
|
+
* démarre sans cette entrée).
|
|
65
|
+
*/
|
|
66
|
+
registerEntry(consumerModule: Module, declaration: IFrontendModuleDeclaration): IResolvedFrontendEntry;
|
|
67
|
+
listEntries(): ReadonlyArray<IResolvedFrontendEntry>;
|
|
68
|
+
status(): IViteSupervisorStatus;
|
|
69
|
+
statusAll(): ReadonlyArray<{
|
|
70
|
+
family: string;
|
|
71
|
+
status: IViteSupervisorStatus;
|
|
72
|
+
}>;
|
|
73
|
+
/**
|
|
74
|
+
* Démarre une instance Vite **par famille d'isolation** (multi-supervisor).
|
|
75
|
+
*
|
|
76
|
+
* Résilience : chaque famille démarre indépendamment (`Promise.allSettled`).
|
|
77
|
+
* Une famille qui échoue (ex. Angular) est isolée — elle ne fait jamais
|
|
78
|
+
* échouer les autres ni le backend. `startDev` ne rejette que si **aucune**
|
|
79
|
+
* famille n'a pu démarrer.
|
|
80
|
+
*/
|
|
81
|
+
startDev(): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Port backend que Vite doit proxifier — le port RÉELLEMENT écouté, pas celui
|
|
84
|
+
* qu'on espérait.
|
|
85
|
+
*
|
|
86
|
+
* `config.backendPort` (5151) n'est qu'une intention : avec
|
|
87
|
+
* `servers.portPolicy: "auto"`, un port occupé fait glisser l'écoute du backend
|
|
88
|
+
* (5151 → 5153). Un proxy figé sur 5151 enverrait alors les appels API du front
|
|
89
|
+
* vers le serveur d'une AUTRE app — au mieux des 404, au pire les données du
|
|
90
|
+
* voisin. On lit donc le port sur le serveur lui-même.
|
|
91
|
+
*
|
|
92
|
+
* Résolution par NOM (`server-http`), jamais par import : `@nodefony/frontend`
|
|
93
|
+
* ne dépend pas de `@nodefony/http` (cycle via la config d'app).
|
|
94
|
+
*/
|
|
95
|
+
private resolveBackendPort;
|
|
96
|
+
/**
|
|
97
|
+
* Résout les certificats HTTPS partagés (service `certificates` de
|
|
98
|
+
* @nodefony/http) si `https: true`. Pas de duplication — mêmes PEM que
|
|
99
|
+
* `server-https` (5152). Retombe sur HTTP avec un warning si indisponible.
|
|
100
|
+
*/
|
|
101
|
+
private resolveHttps;
|
|
102
|
+
/**
|
|
103
|
+
* Template d'origine publique Vite (P14.17). Priorité : `frontend.publicOrigin`
|
|
104
|
+
* (config, validée — invalide = ERROR + ignorée, jamais un boot cassé) puis
|
|
105
|
+
* détection de plateforme (Codespaces/Gitpod — variables documentées de la
|
|
106
|
+
* plateforme, qu'on lit sans les posséder). `undefined` = dérivation locale.
|
|
107
|
+
* Chaque adaptation est JOURNALISÉE : on doit pouvoir lire dans le boot
|
|
108
|
+
* pourquoi les `<script>` pointent où ils pointent.
|
|
109
|
+
*/
|
|
110
|
+
private resolvePublicOriginTemplate;
|
|
111
|
+
/**
|
|
112
|
+
* `server.allowedHosts` pour Vite. Vite accepte d'office IP et `localhost` ;
|
|
113
|
+
* cette liste ne porte que les NOMS. Source des noms légitimes = la MÊME que
|
|
114
|
+
* la barrière Host de Nodefony (`kernel.domain` + `trustedHosts` http) — une
|
|
115
|
+
* seule liste à maintenir : autoriser un hôte dans `trustedHosts` ouvre à la
|
|
116
|
+
* fois la barrière 421 ET Vite. S'y ajoute l'hôte du template d'origine
|
|
117
|
+
* publique. `trustedHosts: true` (barrière déléguée au reverse-proxy) →
|
|
118
|
+
* `true` (même délégation). Un pattern non exprimable chez Vite est ANNONCÉ.
|
|
119
|
+
*/
|
|
120
|
+
private viteAllowedHosts;
|
|
121
|
+
/** Regroupe les entries par famille d'isolation + remplit l'index inverse. */
|
|
122
|
+
private groupEntriesByFamily;
|
|
123
|
+
/**
|
|
124
|
+
* Démarre l'instance Vite d'une famille sur un port dédié. Enregistre le
|
|
125
|
+
* supervisor + son template helper AVANT le `start()` (l'état dégradé reste
|
|
126
|
+
* observable même si le démarrage échoue → rendu propre, pas d'exception).
|
|
127
|
+
*/
|
|
128
|
+
private startFamily;
|
|
129
|
+
/**
|
|
130
|
+
* Câblage prod (idempotent) : monte chaque `outDir` sur son `publicPath`
|
|
131
|
+
* auprès du serveur statique `server-static` (résolu par nom — pas d'import
|
|
132
|
+
* http) et crée le helper prod (lecture manifests). No-op si pas d'entrée.
|
|
133
|
+
*
|
|
134
|
+
* Une entrée SANS build servirait une page blanche : jamais en silence.
|
|
135
|
+
* Cas nominal cloud-native : le build est fait à l'image (`npm run build`,
|
|
136
|
+
* qui chaîne `frontend:build` dans les apps générées) → manifest présent,
|
|
137
|
+
* zéro travail ici. Cas « prod essayée sur le poste » (devDeps installées →
|
|
138
|
+
* vite résolvable) : build one-shot au boot — idempotent, et il supprime
|
|
139
|
+
* l'écran blanc qui perd l'utilisateur, surtout en `--detach`. Sans vite
|
|
140
|
+
* (image runtime sans devDependencies) : impossible de réparer ici → on
|
|
141
|
+
* NOMME l'entrée, le manifest attendu et le geste, en ERROR.
|
|
142
|
+
* Cf project_resilience_no_silent_degradation (fail-soft dispo, fail-loud
|
|
143
|
+
* dégradation — tout fallback annoncé).
|
|
144
|
+
*/
|
|
145
|
+
private setupProd;
|
|
146
|
+
stopDev(): Promise<void>;
|
|
147
|
+
/**
|
|
148
|
+
* Build production — `vite.build()` **par entry** (chaque bundle a son propre
|
|
149
|
+
* `root`/`outDir`/`base`/`manifest` : multi-module + isolation Angular).
|
|
150
|
+
*
|
|
151
|
+
* Idempotent : une entrée dont le `manifest.json` est plus récent que ses
|
|
152
|
+
* sources est **ignorée** (`skipped`) — relance prod console rapide. `force`
|
|
153
|
+
* rebuild tout. Les échecs sont **collectés** (un bundle KO n'arrête pas les
|
|
154
|
+
* autres) et remontés dans `failures` → la commande CLI casse l'exit code.
|
|
155
|
+
*
|
|
156
|
+
* ⚠️ **`NODE_ENV` est posé le temps du build, et restauré.** Vite dérive son
|
|
157
|
+
* `isProduction` de `process.env.NODE_ENV`, qui **prime sur le `mode`** de la
|
|
158
|
+
* configuration : une commande CLI, dont le kernel démarre en développement,
|
|
159
|
+
* produisait donc un bundle de DÉVELOPPEMENT malgré `mode: "production"` —
|
|
160
|
+
* `import.meta.env.DEV` vrai chez l'utilisateur final (tout code gardé par ce
|
|
161
|
+
* drapeau s'exécutait en production), et les messages d'aide de Vue publiés.
|
|
162
|
+
* La restauration n'est pas une précaution de style : `build()` est aussi
|
|
163
|
+
* appelé par `setupProd()`, et laisser la variable retournée marquerait un
|
|
164
|
+
* process de développement comme production pour le reste de sa vie.
|
|
165
|
+
*
|
|
166
|
+
* @param opts.force ignore le cache de fraîcheur (rebuild systématique).
|
|
167
|
+
*/
|
|
168
|
+
build(opts?: {
|
|
169
|
+
force?: boolean;
|
|
170
|
+
}): Promise<IFrontendBuildResult>;
|
|
171
|
+
/**
|
|
172
|
+
* Une entrée est « fraîche » si son `manifest.json` existe ET qu'aucun fichier
|
|
173
|
+
* source (sous `root`, hors `node_modules`/`outDir`/`.vite`) n'est plus récent.
|
|
174
|
+
* Scan disque borné (dossier front petit) — évite un rebuild Vite inutile.
|
|
175
|
+
*/
|
|
176
|
+
private isBuildFresh;
|
|
177
|
+
/** Mtime du fichier le plus récent sous `dir` (récursif borné). */
|
|
178
|
+
private newestSourceMtime;
|
|
179
|
+
/**
|
|
180
|
+
* Document HTML complet pour une entrée — lit l'`index.html` du module
|
|
181
|
+
* (le dev y met meta/polices/scripts externes) + injecte les tags Nodefony.
|
|
182
|
+
* Le controller renvoie : `this.render(svc.renderDocument("x", this.context.cspNonce))`.
|
|
183
|
+
* @param nonce nonce CSP de la requête (`Context.cspNonce`) — propagé aux `<script>`.
|
|
184
|
+
*/
|
|
185
|
+
renderDocument(entryName: string, nonce?: string, requestHost?: string): string;
|
|
186
|
+
renderTags(entryName: string, nonce?: string, requestHost?: string): string;
|
|
187
|
+
/**
|
|
188
|
+
* Le `Host` de la requête peut-il servir à dériver l'origine des assets ?
|
|
189
|
+
*
|
|
190
|
+
* Trois conditions, dans cet ordre — chacune protège un cas RÉEL :
|
|
191
|
+
* 1. **origine non épinglée** : un `frontend.publicOrigin` explicite (ou une
|
|
192
|
+
* plateforme de dev déporté détectée) est une décision de l'auteur, elle
|
|
193
|
+
* gagne toujours sur une déduction ;
|
|
194
|
+
* 2. **barrière `trustedHosts` franchie** : le `Host` est une donnée
|
|
195
|
+
* CLIENTE. Sans ce filtre, un `Host` forgé ferait émettre des
|
|
196
|
+
* `<script src="https://attaquant:5173/…">` dans une page de dev ;
|
|
197
|
+
* 3. **barrière non déléguée** (`trustedHosts !== true`) : le bypass total
|
|
198
|
+
* ne dit plus rien de la légitimité d'un nom, et surtout le CSP émis par
|
|
199
|
+
* le firewall (`#viteCspFragment`) ne couvre alors QUE loopback +
|
|
200
|
+
* domaine canonique — dériver ailleurs produirait une page dont les
|
|
201
|
+
* scripts sont bloqués. Les deux listes doivent rester la même liste.
|
|
202
|
+
*
|
|
203
|
+
* @returns le nom d'hôte à employer, ou `undefined` pour garder l'origine
|
|
204
|
+
* résolue au démarrage (comportement d'avant la dérivation).
|
|
205
|
+
*/
|
|
206
|
+
private derivableHost;
|
|
207
|
+
/**
|
|
208
|
+
* Ports Vite à déclarer au CSP, famille par famille : le BLOC entier tant que
|
|
209
|
+
* l'instance ne sert pas, son port RÉEL dès qu'elle sert.
|
|
210
|
+
*
|
|
211
|
+
* Pourquoi le bloc avant : le CSP part AVEC la page et ne se renégocie jamais.
|
|
212
|
+
* Une page servie avant que Vite ait résolu son port doit déjà porter le port
|
|
213
|
+
* qu'il prendra, sinon son socket de rechargement à chaud est refusé pour
|
|
214
|
+
* toute la durée de la page.
|
|
215
|
+
*
|
|
216
|
+
* Pourquoi le port seul après : la plage est le prix d'une incertitude, elle
|
|
217
|
+
* ne doit pas lui survivre. Mesuré sur ce dépôt (3 familles × 4 hôtes de
|
|
218
|
+
* confiance), garder les blocs porte l'en-tête CSP à ~7,9 Ko sur CHAQUE
|
|
219
|
+
* réponse, contre ~2,3 Ko une fois les ports connus — au bord des 8 Ko que
|
|
220
|
+
* refusent beaucoup de relais.
|
|
221
|
+
*
|
|
222
|
+
* Développement seulement : `startDev` ne tourne pas en production. La
|
|
223
|
+
* garantie reste une liste d'origines nommées — jamais un `ws:` sans hôte,
|
|
224
|
+
* qui la supprimerait au lieu de corriger le symptôme.
|
|
225
|
+
*
|
|
226
|
+
* @returns ports en chaîne, dédupliqués ; repli `devPort` si rien n'est connu.
|
|
227
|
+
*/
|
|
228
|
+
private cspPorts;
|
|
229
|
+
}
|
|
230
|
+
export default FrontendService;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { IResolvedFrontendEntry } from "../interfaces/IFrontBuilder.js";
|
|
2
|
+
/**
|
|
3
|
+
* Génère le contenu d'un `vite.config.generated.mjs` pour le superviseur
|
|
4
|
+
* child_process. Le fichier généré est autosuffisant : il importe Vite +
|
|
5
|
+
* les plugins de chaque preset hardcodés selon les types détectés.
|
|
6
|
+
*
|
|
7
|
+
* Pourquoi pas appeler `IFrontBuilder.buildViteConfig()` directement et
|
|
8
|
+
* passer la config en stdin ? Parce que Vite ne lit pas la config depuis
|
|
9
|
+
* stdin, et que les plugins (instances JS) ne sont pas sérialisables JSON.
|
|
10
|
+
*/
|
|
11
|
+
export interface ViteConfigGeneratorOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Origine du serveur Nodefony pour le proxy Vite (`server.proxy`).
|
|
14
|
+
* Exemple : `"http://127.0.0.1:5151"`. En dev uniquement — ignoré en prod.
|
|
15
|
+
*/
|
|
16
|
+
readonly backendOrigin?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Origine publique du dev server Vite — ex `"http://127.0.0.1:5173"`.
|
|
19
|
+
* Définie comme `base` dans Vite, ce qui force les imports internes du
|
|
20
|
+
* source transformé (ex `/src/App.tsx`) à devenir absolus
|
|
21
|
+
* (`http://host:port/src/App.tsx`). Sans ça, une page rendue par
|
|
22
|
+
* Nodefony (5151) qui charge `/src/main.tsx` voit ses imports résolus
|
|
23
|
+
* contre 5151 → 404. Active aussi `strictPort` pour garantir l'origine.
|
|
24
|
+
*/
|
|
25
|
+
readonly viteOrigin?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Certificats HTTPS — paths absolus vers les fichiers PEM. Quand fourni,
|
|
28
|
+
* la config Vite générée inclut `server.https: { key, cert }` (lus via
|
|
29
|
+
* `fs.readFileSync` au démarrage Vite).
|
|
30
|
+
*/
|
|
31
|
+
readonly https?: {
|
|
32
|
+
readonly keyPath: string;
|
|
33
|
+
readonly certPath: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Hôtes acceptés dans le header `Host` (`server.allowedHosts`, Vite ≥6).
|
|
37
|
+
* `true` = tous. Un motif `.suffixe` couvre le domaine et ses sous-domaines.
|
|
38
|
+
* Les IP et `localhost` sont toujours acceptés par Vite — cette liste ne
|
|
39
|
+
* sert que les NOMS (vhosts, `host.docker.internal`, forwarders).
|
|
40
|
+
*/
|
|
41
|
+
readonly allowedHosts?: true | ReadonlyArray<string>;
|
|
42
|
+
/**
|
|
43
|
+
* Config `server.hmr` CLIENTE — où le navigateur ouvre le WebSocket HMR
|
|
44
|
+
* quand un intermédiaire (forwarder TLS, passerelle conteneur) sépare
|
|
45
|
+
* l'origine publique de l'adresse d'écoute. Absent = fallback client Vite
|
|
46
|
+
* (`location.hostname` + port d'écoute), correct en local.
|
|
47
|
+
*/
|
|
48
|
+
readonly hmr?: {
|
|
49
|
+
readonly host: string;
|
|
50
|
+
readonly clientPort: number;
|
|
51
|
+
readonly protocol: "ws" | "wss";
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export declare class ViteConfigGenerator {
|
|
55
|
+
/**
|
|
56
|
+
* Construit le content `.mjs` à écrire à côté de `index.html` du module.
|
|
57
|
+
*
|
|
58
|
+
* Si `opts.backendOrigin` est fourni en mode `development`, agrège les
|
|
59
|
+
* `apiProxyPaths` de toutes les entries et génère un `server.proxy` qui
|
|
60
|
+
* forward chaque préfixe vers le backend Nodefony. Sans ça, les fetch
|
|
61
|
+
* relatifs depuis l'app servie par Vite atterrissent sur Vite et reçoivent
|
|
62
|
+
* un SPA-fallback HTML (cause classique de `Unexpected token '<'`).
|
|
63
|
+
*/
|
|
64
|
+
toMjs(entries: ReadonlyArray<IResolvedFrontendEntry>, mode: "development" | "production", opts?: ViteConfigGeneratorOptions): string;
|
|
65
|
+
}
|
|
66
|
+
export default ViteConfigGenerator;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { IViteSupervisor, IViteSupervisorStatus } from "../interfaces/IViteSupervisor.js";
|
|
2
|
+
import type { IResolvedFrontendEntry } from "../interfaces/IFrontBuilder.js";
|
|
3
|
+
/**
|
|
4
|
+
* Logger minimal — injecté par FrontendService pour piper les logs Vite
|
|
5
|
+
* dans le syslog Nodefony sans dépendance dure sur Service.
|
|
6
|
+
*/
|
|
7
|
+
export interface IViteSupervisorLogger {
|
|
8
|
+
info(msg: string): void;
|
|
9
|
+
error(msg: string): void;
|
|
10
|
+
debug?(msg: string): void;
|
|
11
|
+
}
|
|
12
|
+
export interface ViteSupervisorOptions {
|
|
13
|
+
readonly devHost: string;
|
|
14
|
+
readonly devPort: number;
|
|
15
|
+
/**
|
|
16
|
+
* Template d'origine PUBLIQUE du dev server (P14.17) — `{port}` substitué au
|
|
17
|
+
* port RÉEL de chaque spawn (suit les retries de port). Vide/absent = dérivé
|
|
18
|
+
* de `devHost:port`. Dissocie ce que Vite ÉCOUTE de ce que le navigateur
|
|
19
|
+
* APPELLE (forwarder Codespaces/Gitpod, `host.docker.internal`, remap).
|
|
20
|
+
*/
|
|
21
|
+
readonly publicOriginTemplate?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Hôtes que Vite doit accepter dans le header `Host` (`server.allowedHosts`).
|
|
24
|
+
* `true` = tous. Dérivé par FrontendService de la liste `trustedHosts` http —
|
|
25
|
+
* jamais maintenu ici (1 règle = 1 implémentation).
|
|
26
|
+
*/
|
|
27
|
+
readonly allowedHosts?: true | ReadonlyArray<string>;
|
|
28
|
+
readonly startupTimeoutMs: number;
|
|
29
|
+
readonly pipeLogs: boolean;
|
|
30
|
+
readonly cwd: string;
|
|
31
|
+
readonly logger: IViteSupervisorLogger;
|
|
32
|
+
/**
|
|
33
|
+
* Origine du serveur Nodefony pour `server.proxy` côté Vite — ex `"http://127.0.0.1:5151"`.
|
|
34
|
+
* Quand fourni, les paths `apiProxyPaths` des entries sont proxifiés vers ce backend.
|
|
35
|
+
*/
|
|
36
|
+
readonly backendOrigin?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Certificats à utiliser si Vite doit servir en HTTPS. Paths absolus vers les
|
|
39
|
+
* fichiers PEM — les mêmes que Nodefony utilise pour son `server-https` (5152).
|
|
40
|
+
*/
|
|
41
|
+
readonly https?: {
|
|
42
|
+
readonly keyPath: string;
|
|
43
|
+
readonly certPath: string;
|
|
44
|
+
};
|
|
45
|
+
/** Valeur de `NODE_ENV` à propager au child Vite — généralement `kernel.environment`. */
|
|
46
|
+
readonly nodeEnv?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Variables d'env additionnelles passées au child Vite. Les clés `VITE_*`
|
|
49
|
+
* sont automatiquement exposées au browser via `import.meta.env`.
|
|
50
|
+
*/
|
|
51
|
+
readonly extraEnv?: Record<string, string>;
|
|
52
|
+
/** Auto-restart sur crash inattendu de Vite (default `true`). */
|
|
53
|
+
readonly autoRestart?: boolean;
|
|
54
|
+
/** Max tentatives de restart avant d'abandonner (default `5`). */
|
|
55
|
+
readonly maxRestarts?: number;
|
|
56
|
+
/** Délai initial du backoff exponentiel — doublé à chaque tentative (default `500ms`). */
|
|
57
|
+
readonly restartBackoffBaseMs?: number;
|
|
58
|
+
/** Plafond du backoff (default `8000ms` = 8s). */
|
|
59
|
+
readonly restartBackoffMaxMs?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Intervalle health check (default `30000` = 30s). `0` désactive.
|
|
62
|
+
* Le supervisor fait un GET HTTP(S) sur `viteOrigin/` et compte les échecs
|
|
63
|
+
* consécutifs ; au-delà du seuil, kill le child → trigger auto-restart.
|
|
64
|
+
*/
|
|
65
|
+
readonly healthCheckIntervalMs?: number;
|
|
66
|
+
/** Échecs health check consécutifs avant restart (default `3`). */
|
|
67
|
+
readonly healthCheckFailureThreshold?: number;
|
|
68
|
+
/** Timeout d'un health check individuel (default `5000ms`). */
|
|
69
|
+
readonly healthCheckTimeoutMs?: number;
|
|
70
|
+
/**
|
|
71
|
+
* Tentatives de port à essayer si EADDRINUSE (default `3`).
|
|
72
|
+
* Le supervisor essaie devPort, devPort+1, devPort+2.
|
|
73
|
+
*/
|
|
74
|
+
readonly portRetryAttempts?: number;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Un texte (message d'erreur OU sortie brute de Vite) dénonce-t-il un port occupé ?
|
|
78
|
+
*
|
|
79
|
+
* **Source UNIQUE** de cette décision. Elle était dupliquée en deux regex qui ont
|
|
80
|
+
* divergé : l'une cherchait `port X is in use`, alors que Vite écrit
|
|
81
|
+
* `Port 5173 is ALREADY in use`. Résultat, le retry de port ne se déclenchait
|
|
82
|
+
* jamais et la seconde app perdait tout son frontend — un conflit de port pourtant
|
|
83
|
+
* parfaitement rattrapable. On tolère donc les deux formulations, et on ne
|
|
84
|
+
* l'écrit qu'ici (deux implémentations d'une même règle = dérive garantie).
|
|
85
|
+
*/
|
|
86
|
+
export declare function isPortInUseMessage(text: string): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Message d'échec d'un boot qui n'a jamais rendu la main dans le temps imparti —
|
|
89
|
+
* en y REPORTANT ce que vite avait déjà dit.
|
|
90
|
+
*
|
|
91
|
+
* Le conflit de port ne se voit que dans la sortie du child. Le rejet par
|
|
92
|
+
* échéance ne portait que la durée : un boot qui annonçait « Port 5173 is
|
|
93
|
+
* already in use » puis restait pendu (vite ne meurt pas toujours, et sur une
|
|
94
|
+
* application réelle le pré-bundling tient l'échéance) sortait sous le libellé
|
|
95
|
+
* `timeout after …`, que le détecteur de port occupé ne reconnaît pas. Le repli
|
|
96
|
+
* sur `port+1` existait et n'était jamais atteint — la sortie d'échec choisie
|
|
97
|
+
* décidait à elle seule si la 2ᵉ application aurait un frontend.
|
|
98
|
+
*
|
|
99
|
+
* Fonction PURE : la règle « ce qu'on a OBSERVÉ prime sur la façon dont on a
|
|
100
|
+
* échoué » se teste sans spawn, donc sans dépendre d'une course entre l'écriture
|
|
101
|
+
* de l'erreur et la mort du process.
|
|
102
|
+
*
|
|
103
|
+
* @param timeoutMs - échéance dépassée.
|
|
104
|
+
* @param observed - sortie brute accumulée du child (stdout + stderr).
|
|
105
|
+
* @returns message d'erreur, préfixé `EADDRINUSE:` si un port occupé est dénoncé.
|
|
106
|
+
*/
|
|
107
|
+
export declare function startupTimeoutMessage(timeoutMs: number, observed: string): string;
|
|
108
|
+
/**
|
|
109
|
+
* Superviseur Vite résilient — branche POC `poc/frontend-child`.
|
|
110
|
+
*
|
|
111
|
+
* Garanties :
|
|
112
|
+
* - Idempotent : appels concurrents à `start()` partagent la même promesse.
|
|
113
|
+
* - Auto-restart : crash inattendu → restart avec backoff exponentiel borné.
|
|
114
|
+
* - Port conflict : retry sur port+1 jusqu'au plafond `portRetryAttempts`.
|
|
115
|
+
* - Health check : ping périodique, kill+restart sur N échecs consécutifs.
|
|
116
|
+
* - Cleanup strict : listeners + timers tracés et libérés au `stop()`.
|
|
117
|
+
*
|
|
118
|
+
* Tous les listeners sur `child` sont supprimés explicitement avant qu'on
|
|
119
|
+
* laisse le child mourir (évite memory leak entre restarts).
|
|
120
|
+
*/
|
|
121
|
+
export declare class ViteProcessSupervisor implements IViteSupervisor {
|
|
122
|
+
private readonly opts;
|
|
123
|
+
private readonly cfg;
|
|
124
|
+
private readonly generator;
|
|
125
|
+
private child;
|
|
126
|
+
private state;
|
|
127
|
+
private resolvedPort;
|
|
128
|
+
/** Origine publique du spawn courant (source unique des URLs — cf status). */
|
|
129
|
+
private resolvedOrigin;
|
|
130
|
+
private lastError;
|
|
131
|
+
private entries;
|
|
132
|
+
private configFilePath;
|
|
133
|
+
private restartCount;
|
|
134
|
+
private healthFailures;
|
|
135
|
+
private healthCheckTimer;
|
|
136
|
+
private restartTimer;
|
|
137
|
+
private startPromise;
|
|
138
|
+
private stopPromise;
|
|
139
|
+
private willingShutdown;
|
|
140
|
+
/** Kill VOULU par le health check (recovery) : le prochain exit doit relancer. */
|
|
141
|
+
private expectRestartKill;
|
|
142
|
+
/**
|
|
143
|
+
* Signal d'arrêt reçu par NOTRE process (Ctrl+C au groupe foreground, SIGTERM
|
|
144
|
+
* d'un orchestrateur) → tout exit de Vite est un ARRÊT, jamais un crash.
|
|
145
|
+
* Indispensable : Vite intercepte SIGINT et sort `code=130, signal=null` —
|
|
146
|
+
* indiscernable d'un crash côté exit event, et `willingShutdown` (posé par le
|
|
147
|
+
* stop du kernel) arrive APRÈS la mort de Vite (race sans IPC, vécu Ctrl+C :
|
|
148
|
+
* « vite restart #1 failed » en ERROR sur un arrêt normal).
|
|
149
|
+
*/
|
|
150
|
+
private readonly markShutdown;
|
|
151
|
+
/**
|
|
152
|
+
* Listeners attachés au child courant — drainés à chaque mort du child.
|
|
153
|
+
* Sans ça, le child gardé en référence (avant GC) accumule des handlers
|
|
154
|
+
* entre restarts → MaxListenersExceededWarning + leak.
|
|
155
|
+
*/
|
|
156
|
+
private childListeners;
|
|
157
|
+
constructor(opts: ViteSupervisorOptions);
|
|
158
|
+
start(entries: ReadonlyArray<IResolvedFrontendEntry>, _viteConfigUnused: Record<string, unknown>): Promise<void>;
|
|
159
|
+
stop(): Promise<void>;
|
|
160
|
+
status(): IViteSupervisorStatus;
|
|
161
|
+
/**
|
|
162
|
+
* Essaie de spawn Vite en variant le port si EADDRINUSE. Le port résolu
|
|
163
|
+
* est stocké dans `resolvedPort` (utilisé par status() + TemplateHelper).
|
|
164
|
+
*/
|
|
165
|
+
private spawnWithPortRetry;
|
|
166
|
+
/** Replis de port du DERNIER démarrage — cf `IViteSupervisorStatus.portRetries`. */
|
|
167
|
+
private portRetries;
|
|
168
|
+
private isPortInUseError;
|
|
169
|
+
/** Spawn Vite sur un port donné et attend le ready. */
|
|
170
|
+
private attemptSpawn;
|
|
171
|
+
private waitReady;
|
|
172
|
+
/**
|
|
173
|
+
* Après que Vite est ready, on attache un handler qui distingue :
|
|
174
|
+
* - shutdown volontaire (willingShutdown=true → state=stopped, no restart)
|
|
175
|
+
* - crash inattendu (state=ready au moment du exit → scheduleRestart)
|
|
176
|
+
*/
|
|
177
|
+
private attachRuntimeExitHandler;
|
|
178
|
+
private scheduleRestart;
|
|
179
|
+
private startHealthCheck;
|
|
180
|
+
private stopHealthCheck;
|
|
181
|
+
/**
|
|
182
|
+
* Ping HTTP(S) GET sur la racine de Vite. Considère réussi si réception de
|
|
183
|
+
* headers (même 4xx — Vite ne renvoie pas forcément 200 à `/`).
|
|
184
|
+
*/
|
|
185
|
+
private pingVite;
|
|
186
|
+
/** Stop volontaire — n'enclenche PAS l'auto-restart. */
|
|
187
|
+
private doStop;
|
|
188
|
+
/** Force kill du child (pour les health checks failing). Le exit handler gère le restart. */
|
|
189
|
+
private killChild;
|
|
190
|
+
/**
|
|
191
|
+
* Envoie un signal à l'ARBRE de Vite — lui et ses propres enfants, à commencer par
|
|
192
|
+
* le service esbuild que Vite lance pour la pré-optimisation des dépendances.
|
|
193
|
+
*
|
|
194
|
+
* `child.kill()` n'atteint que Vite. Sous POSIX cela suffit en pratique : Vite n'est
|
|
195
|
+
* pas leader de son groupe (`detached: false`, voulu — un Ctrl+C terminal doit
|
|
196
|
+
* l'atteindre), donc le comportement ici est inchangé, et l'arbre est de toute façon
|
|
197
|
+
* emporté par le groupe du superviseur de développement. Sous Windows il n'y a pas de
|
|
198
|
+
* groupe du tout : les descendants survivaient à chaque arrêt, et `stopDev()` laissait
|
|
199
|
+
* un service esbuild derrière lui.
|
|
200
|
+
*
|
|
201
|
+
* Une seule implémentation dans le dépôt ({@link signalProcessGroup}, cœur) : groupe
|
|
202
|
+
* POSIX ou `taskkill /T` selon la plateforme. La recopier ici aurait fait réapparaître
|
|
203
|
+
* l'angle mort qu'on venait de fermer dans le superviseur de développement.
|
|
204
|
+
*
|
|
205
|
+
* @returns ce qui a pu être atteint. `gone` = plus rien à tuer, donc plus aucun `exit`
|
|
206
|
+
* à attendre : c'est un VERDICT, là où l'arrêt reposait jusqu'ici sur une exception
|
|
207
|
+
* remontée par `child.kill()`. Une exception dit qu'un appel a échoué, pas ce qu'il
|
|
208
|
+
* est advenu du process — et elle disparaît dès qu'un intermédiaire la rattrape.
|
|
209
|
+
*/
|
|
210
|
+
private signalTree;
|
|
211
|
+
private trackListener;
|
|
212
|
+
private cleanupChildListeners;
|
|
213
|
+
}
|
|
214
|
+
export default ViteProcessSupervisor;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { IAdminApi } from "nodefony";
|
|
2
|
+
import type FrontendService from "../service/FrontendService.js";
|
|
3
|
+
/**
|
|
4
|
+
* Producteur `IAdminApi` du builder frontend — exposé sous `/nodefony/frontend/api/*`.
|
|
5
|
+
* Surface l'état du **superviseur Vite** (dev) pour la visibilité Studio : process Vite
|
|
6
|
+
* (pid), port réel résolu, état (donc HMR actif), bundles servis.
|
|
7
|
+
*
|
|
8
|
+
* Vite est un outil **DEV-only** : en production le superviseur ne tourne pas (état
|
|
9
|
+
* `idle`, pid `null`) — l'UI vient alors du bundle compilé (`manifest.json`). L'endpoint
|
|
10
|
+
* répond dans les deux cas (best-effort, jamais throw) : le front en déduit l'affichage.
|
|
11
|
+
*/
|
|
12
|
+
/** Vue SÛRE d'une instance Vite — sans chemins FS absolus (anti info-leak). */
|
|
13
|
+
export interface IViteInstanceView {
|
|
14
|
+
/** Famille d'isolation (`default`, `angular`, …). */
|
|
15
|
+
family: string;
|
|
16
|
+
/** État du superviseur (`ready` = HMR actif). */
|
|
17
|
+
state: string;
|
|
18
|
+
/** Hôte d'ÉCOUTE du serveur Vite (dev). */
|
|
19
|
+
host: string;
|
|
20
|
+
/** Origine PUBLIQUE effective (celle que le navigateur utilise) — `null` si non démarré. */
|
|
21
|
+
origin: string | null;
|
|
22
|
+
/** Port RÉEL résolu (Vite incrémente si occupé) — `null` si non démarré. */
|
|
23
|
+
port: number | null;
|
|
24
|
+
/** PID du process enfant Vite — `null` hors dev. */
|
|
25
|
+
pid: number | null;
|
|
26
|
+
/** Vite servi en HTTPS (suit la config du serveur). */
|
|
27
|
+
https: boolean;
|
|
28
|
+
/** Redémarrages du superviseur depuis le boot. */
|
|
29
|
+
restartCount: number;
|
|
30
|
+
/** Échecs de health-check du superviseur. */
|
|
31
|
+
healthFailures: number;
|
|
32
|
+
/** Entrées servies — nom logique + type + version du framework UI. */
|
|
33
|
+
entries: {
|
|
34
|
+
entryName: string;
|
|
35
|
+
type: string;
|
|
36
|
+
version?: string;
|
|
37
|
+
}[];
|
|
38
|
+
}
|
|
39
|
+
/** Snapshot frontend servi par l'endpoint `vite`. */
|
|
40
|
+
export interface IFrontendStatusView {
|
|
41
|
+
/** Au moins une instance Vite `ready` (HMR actif). */
|
|
42
|
+
available: boolean;
|
|
43
|
+
/** Version de Vite (le builder), si résolue. */
|
|
44
|
+
vite?: string;
|
|
45
|
+
/** Instance principale (famille `default` ou la première). */
|
|
46
|
+
primary: IViteInstanceView;
|
|
47
|
+
/** Toutes les instances (multi-bundle). */
|
|
48
|
+
bundles: IViteInstanceView[];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Construit le snapshot frontend (état Vite) — lecture pure, jamais throw.
|
|
52
|
+
*
|
|
53
|
+
* @param service - le {@link FrontendService} (résolu du container).
|
|
54
|
+
* @returns la vue sûre prête à `renderJson` / push realtime.
|
|
55
|
+
*/
|
|
56
|
+
export declare function buildFrontendStatus(service: FrontendService): IFrontendStatusView;
|
|
57
|
+
/**
|
|
58
|
+
* Construit le producteur `IAdminApi` du frontend (namespace `"frontend"`).
|
|
59
|
+
*
|
|
60
|
+
* @param service - le {@link FrontendService} à introspecter.
|
|
61
|
+
* @returns le contrat admin, prêt à `broker.register()`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function createFrontendAdminApi(service: FrontendService): IAdminApi;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { IFrontBuilder, IResolvedFrontendEntry } from "../../interfaces/IFrontBuilder.js";
|
|
2
|
+
import type { IFrontPreset } from "../../interfaces/IFrontPreset.js";
|
|
3
|
+
/**
|
|
4
|
+
* Construit la config Vite finale à partir des entrées résolues et des presets.
|
|
5
|
+
*
|
|
6
|
+
* Le builder ne lance JAMAIS Vite — il fournit uniquement la config. Le
|
|
7
|
+
* démarrage est délégué au superviseur (child_process ou in-proc).
|
|
8
|
+
*/
|
|
9
|
+
export declare class ViteBuilder implements IFrontBuilder {
|
|
10
|
+
private readonly presets;
|
|
11
|
+
constructor();
|
|
12
|
+
listPresets(): ReadonlyArray<IFrontPreset>;
|
|
13
|
+
getPreset(type: IFrontPreset["type"]): IFrontPreset | undefined;
|
|
14
|
+
registerPreset(preset: IFrontPreset): void;
|
|
15
|
+
buildViteConfig(entries: ReadonlyArray<IResolvedFrontendEntry>, mode: "development" | "production", assetBaseUrl?: string): Promise<Record<string, unknown>>;
|
|
16
|
+
}
|
|
17
|
+
export default ViteBuilder;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Erreur de base pour @nodefony/frontend.
|
|
3
|
+
*
|
|
4
|
+
* - `code` : identifiant machine, consommé par Vision et l'audit-logger.
|
|
5
|
+
* - `context` : payload structuré pour le PDU syslog.
|
|
6
|
+
*/
|
|
7
|
+
export declare class FrontendError extends Error {
|
|
8
|
+
readonly code: string;
|
|
9
|
+
readonly context?: Record<string, unknown> | undefined;
|
|
10
|
+
constructor(message: string, code: string, context?: Record<string, unknown> | undefined);
|
|
11
|
+
}
|
|
12
|
+
/** Preset inconnu — module a déclaré `type: "foo"` non enregistré. */
|
|
13
|
+
export declare class FrontendPresetUnknownError extends FrontendError {
|
|
14
|
+
constructor(type: string);
|
|
15
|
+
}
|
|
16
|
+
/** Echec de démarrage de Vite (spawn raté, port occupé, config invalide). */
|
|
17
|
+
export declare class FrontendSupervisorStartError extends FrontendError {
|
|
18
|
+
constructor(reason: string, cause?: unknown);
|
|
19
|
+
}
|
|
20
|
+
/** Au moins un bundle a échoué au build production (Vite). */
|
|
21
|
+
export declare class FrontendBuildError extends FrontendError {
|
|
22
|
+
readonly failures: ReadonlyArray<{
|
|
23
|
+
entryName: string;
|
|
24
|
+
message: string;
|
|
25
|
+
}>;
|
|
26
|
+
constructor(failures: ReadonlyArray<{
|
|
27
|
+
entryName: string;
|
|
28
|
+
message: string;
|
|
29
|
+
}>);
|
|
30
|
+
}
|
|
31
|
+
/** Aucune entrée front trouvée alors qu'on tente de démarrer le dev server. */
|
|
32
|
+
export declare class FrontendNoEntriesError extends FrontendError {
|
|
33
|
+
constructor();
|
|
34
|
+
}
|