@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.
Files changed (56) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +338 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/index.js +63 -0
  6. package/dist/nodefony/command/frontend-build.js +68 -0
  7. package/dist/nodefony/command/frontend-dev.js +31 -0
  8. package/dist/nodefony/command/frontend-status.js +40 -0
  9. package/dist/nodefony/config/config.js +65 -0
  10. package/dist/nodefony/config/defineModuleConfig.js +34 -0
  11. package/dist/nodefony/interfaces/IFrontBuilder.js +1 -0
  12. package/dist/nodefony/interfaces/IFrontPreset.js +1 -0
  13. package/dist/nodefony/interfaces/IFrontendService.js +1 -0
  14. package/dist/nodefony/interfaces/IViteSupervisor.js +1 -0
  15. package/dist/nodefony/interfaces/index.js +1 -0
  16. package/dist/nodefony/service/FrontendService.js +708 -0
  17. package/dist/nodefony/service/ViteConfigGenerator.js +139 -0
  18. package/dist/nodefony/service/ViteProcessSupervisor.js +589 -0
  19. package/dist/nodefony/src/FrontendAdminApi.js +113 -0
  20. package/dist/nodefony/src/builders/ViteBuilder.js +75 -0
  21. package/dist/nodefony/src/errors/FrontendError.js +50 -0
  22. package/dist/nodefony/src/isolationGroups.js +116 -0
  23. package/dist/nodefony/src/presets/angular-vite.js +27 -0
  24. package/dist/nodefony/src/presets/react19-vite.js +26 -0
  25. package/dist/nodefony/src/presets/svelte5-vite.js +37 -0
  26. package/dist/nodefony/src/presets/vanilla-vite.js +17 -0
  27. package/dist/nodefony/src/presets/vue3-vite.js +23 -0
  28. package/dist/nodefony/src/remoteDev.js +157 -0
  29. package/dist/nodefony/src/template/TemplateHelper.js +255 -0
  30. package/dist/types/index.d.ts +51 -0
  31. package/dist/types/nodefony/command/frontend-build.d.ts +17 -0
  32. package/dist/types/nodefony/command/frontend-dev.d.ts +12 -0
  33. package/dist/types/nodefony/command/frontend-status.d.ts +14 -0
  34. package/dist/types/nodefony/config/config.d.ts +38 -0
  35. package/dist/types/nodefony/config/defineModuleConfig.d.ts +29 -0
  36. package/dist/types/nodefony/interfaces/IFrontBuilder.d.ts +69 -0
  37. package/dist/types/nodefony/interfaces/IFrontPreset.d.ts +26 -0
  38. package/dist/types/nodefony/interfaces/IFrontendService.d.ts +77 -0
  39. package/dist/types/nodefony/interfaces/IViteSupervisor.d.ts +56 -0
  40. package/dist/types/nodefony/interfaces/index.d.ts +4 -0
  41. package/dist/types/nodefony/service/FrontendService.d.ts +230 -0
  42. package/dist/types/nodefony/service/ViteConfigGenerator.d.ts +66 -0
  43. package/dist/types/nodefony/service/ViteProcessSupervisor.d.ts +214 -0
  44. package/dist/types/nodefony/src/FrontendAdminApi.d.ts +63 -0
  45. package/dist/types/nodefony/src/builders/ViteBuilder.d.ts +17 -0
  46. package/dist/types/nodefony/src/errors/FrontendError.d.ts +34 -0
  47. package/dist/types/nodefony/src/isolationGroups.d.ts +89 -0
  48. package/dist/types/nodefony/src/presets/angular-vite.d.ts +15 -0
  49. package/dist/types/nodefony/src/presets/react19-vite.d.ts +9 -0
  50. package/dist/types/nodefony/src/presets/svelte5-vite.d.ts +13 -0
  51. package/dist/types/nodefony/src/presets/vanilla-vite.d.ts +9 -0
  52. package/dist/types/nodefony/src/presets/vue3-vite.d.ts +11 -0
  53. package/dist/types/nodefony/src/remoteDev.d.ts +107 -0
  54. package/dist/types/nodefony/src/template/TemplateHelper.d.ts +99 -0
  55. package/docs/index.md +925 -0
  56. 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
+ }