@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
package/README.md ADDED
@@ -0,0 +1,338 @@
1
+ # @nodefony/frontend
2
+
3
+ Builder frontend Nodefony — supervise [Vite](https://vite.dev/) dans un process séparé pour transpiler les frontends de tes modules (React 19, Vue 3, Angular, vanilla TS).
4
+
5
+ > Audience : développeur Nodefony qui ajoute son **premier frontend** à un module existant. Tu connais déjà `Module`, `Service`, `Controller`. Si non, lis d'abord le [CLAUDE.md racine](https://github.com/nodefony/nodefony-core/blob/claude-ts/CLAUDE.md).
6
+
7
+ ---
8
+
9
+ ## Pourquoi un process séparé ?
10
+
11
+ Vite a besoin d'un event-loop et d'un tas V8 pour compiler/HMR. L'exécuter **in-proc** dans le serveur Nodefony bloque les requêtes pendant les rebuilds. Un process séparé (`child_process.spawn`) isole parfaitement Vite du backend :
12
+
13
+ - Crash Vite ≠ crash Nodefony
14
+ - Compilation Vite ≠ latence event-loop Nodefony
15
+ - HMR WebSocket de Vite reste autonome
16
+ - Auto-restart en cas de mort du child (résilience built-in)
17
+
18
+ ---
19
+
20
+ ## Quickstart — ajouter un frontend à ton module
21
+
22
+ ### 1. Activer `@nodefony/frontend` dans l'app
23
+
24
+ Dans `index.ts` racine :
25
+
26
+ ```ts
27
+ @modules([
28
+ "@nodefony/http",
29
+ "@nodefony/framework",
30
+ "@nodefony/frontend", // ← avant ton module consumer
31
+ "@nodefony/mon-module",
32
+ ])
33
+ class App extends Module { ... }
34
+ ```
35
+
36
+ > **Ordre important** : `@nodefony/frontend` doit être déclaré AVANT les modules qui appellent `registerEntry()` — sinon le service `frontend` n'existe pas dans le DI Container au moment du `onKernelBoot()` du consumer.
37
+
38
+ ### 2. Installer les peer deps
39
+
40
+ ```bash
41
+ npm i -D vite @vitejs/plugin-react # react19 / react-dom
42
+ # ou
43
+ npm i -D vite @vitejs/plugin-vue # vue3
44
+ ```
45
+
46
+ ### 3. Déclarer ton frontend dans le module consumer
47
+
48
+ ```ts
49
+ import { Kernel, Module } from "nodefony";
50
+ import { controllers } from "@nodefony/framework";
51
+ import type { FrontendService } from "@nodefony/frontend";
52
+ import config from "./nodefony/config/config";
53
+ import MyController from "./nodefony/controller/MyController";
54
+
55
+ @controllers([MyController])
56
+ class MyModule extends Module {
57
+ constructor(kernel: Kernel) {
58
+ super("my-module", kernel, import.meta.url, config);
59
+ }
60
+
61
+ override async onKernelBoot(): Promise<this> {
62
+ const svc = this.kernel?.container?.get("frontend") as
63
+ FrontendService | undefined;
64
+ svc?.registerEntry(this, {
65
+ type: "react19", // | "vanilla"
66
+ entry: "./frontend/src/main.tsx", // relatif au module
67
+ root: "./frontend", // contient index.html
68
+ outDir: "./public/dist", // pour la prod build
69
+ name: "my-module", // nom logique (entryName)
70
+ // Quand le browser fait fetch("/my/api/x") depuis l'app Vite,
71
+ // Vite proxifie vers Nodefony :
72
+ apiProxyPaths: ["/my/api"],
73
+ });
74
+ return this;
75
+ }
76
+ }
77
+ ```
78
+
79
+ ### 4. Créer le frontend Vite
80
+
81
+ ```
82
+ src/modules/my-module/frontend/
83
+ ├── index.html ← (peut être absent — Nodefony rend la page elle-même)
84
+ ├── src/
85
+ │ ├── main.tsx ← entry point (React 19 ici)
86
+ │ └── App.tsx
87
+ ```
88
+
89
+ `main.tsx` standard :
90
+
91
+ ```tsx
92
+ import { StrictMode } from "react";
93
+ import { createRoot } from "react-dom/client";
94
+ import { App } from "./App";
95
+
96
+ const rootEl = document.getElementById("root");
97
+ if (!rootEl) throw new Error("#root not found");
98
+ createRoot(rootEl).render(
99
+ <StrictMode>
100
+ <App />
101
+ </StrictMode>,
102
+ );
103
+ ```
104
+
105
+ ### 5. Rendre la page depuis un Controller
106
+
107
+ ```ts
108
+ import { Controller, route, controller } from "@nodefony/framework";
109
+ import { Context } from "@nodefony/http";
110
+ import type { FrontendService } from "@nodefony/frontend";
111
+
112
+ @controller("/my-route")
113
+ class MyController extends Controller {
114
+ constructor(context: Context) {
115
+ super("MyController", context);
116
+ }
117
+
118
+ @route("my-react", { path: "/" })
119
+ renderReact(): unknown {
120
+ this.setContextHtml();
121
+ const svc = this.context?.container?.get("frontend") as
122
+ FrontendService | undefined;
123
+
124
+ // Rien à faire pour la CSP : en développement, le service déclare les
125
+ // origines Vite au firewall (`@nodefony/security`), qui émet UN seul
126
+ // en-tête. Un controller qui le réécrirait écraserait le nonce.
127
+
128
+ // Deux données de la requête sont propagées au rendu :
129
+ // - le nonce CSP, sans lequel `script-src 'nonce-…'` bloque les balises ;
130
+ // - l'hôte, dont l'origine des assets Vite est dérivée en développement —
131
+ // la page annonce l'origine par laquelle le client est arrivé, si bien
132
+ // qu'un poste et un navigateur en conteneur sont servis en même temps,
133
+ // sans configuration. Scheme et port restent ceux de Vite.
134
+ const viteTags =
135
+ svc?.renderTags(
136
+ "my-module",
137
+ this.context?.cspNonce,
138
+ this.context?.domain,
139
+ ) ?? "<!-- @nodefony/frontend not started -->";
140
+
141
+ return this.render(`<!DOCTYPE html>
142
+ <html lang="en">
143
+ <head>
144
+ <meta charset="utf-8" />
145
+ <title>My App</title>
146
+ ${viteTags}
147
+ </head>
148
+ <body>
149
+ <div id="root"></div>
150
+ </body>
151
+ </html>`);
152
+ }
153
+ }
154
+ ```
155
+
156
+ ### 6. Lancer le serveur dev
157
+
158
+ ```bash
159
+ npx nodefony development
160
+ ```
161
+
162
+ Le superviseur Vite démarre automatiquement sur l'event `onServersReady` du kernel (après que les 4 serveurs Nodefony écoutent). Tu verras dans le syslog :
163
+
164
+ ```
165
+ INFO frontend : registered entry: my-module (react19) from "my-module"
166
+ INFO frontend : vite dev server ready on 127.0.0.1:5173
167
+ ```
168
+
169
+ Va sur `http://127.0.0.1:5151/my-route/` — Nodefony rend l'HTML, le browser tape Vite (5173) pour les assets, HMR fonctionne en édition de `App.tsx`.
170
+
171
+ ---
172
+
173
+ ## Config complète
174
+
175
+ Dans le `config.ts` de **ton app** ou de **ton module** :
176
+
177
+ ```ts
178
+ const config = {
179
+ "module-frontend": {
180
+ devHost: "127.0.0.1", // host d'écoute Vite
181
+ devPort: 5173, // port Vite (incrémenté si occupé)
182
+ autoStartInDevelopment: true, // démarre Vite en env=development
183
+ pipeViteLogs: true, // logs Vite dans syslog Nodefony
184
+
185
+ // HTTPS — partage les certs Nodefony (server-https 5152)
186
+ https: true, // false par défaut
187
+
188
+ // Proxy backend Vite → Nodefony
189
+ backendHost: "127.0.0.1",
190
+ backendPort: 5151,
191
+ backendProtocol: "http", // http | https
192
+
193
+ // Variables d'env passées à Vite (les VITE_* sont exposées au browser)
194
+ viteEnv: {
195
+ VITE_API_BASE: "/api/v1",
196
+ },
197
+
198
+ // Résilience supervisor
199
+ resilience: {
200
+ autoRestart: true, // restart sur crash
201
+ maxRestarts: 5, // avant abandon
202
+ restartBackoffBaseMs: 500, // backoff exponentiel
203
+ restartBackoffMaxMs: 8_000,
204
+ healthCheckIntervalMs: 30_000, // ping HTTP périodique (0 = off)
205
+ healthCheckFailureThreshold: 3, // échecs avant restart
206
+ portRetryAttempts: 3, // port+1, port+2 sur EADDRINUSE
207
+ },
208
+ },
209
+ };
210
+ ```
211
+
212
+ Tout est optionnel — les defaults fonctionnent out-of-the-box.
213
+
214
+ ---
215
+
216
+ ## Events du supervisor
217
+
218
+ `FrontendService` est un `Service` Nodefony (donc un `EventEmitter`). Tu peux écouter :
219
+
220
+ | Event | Payload | Quand |
221
+ | ------------------- | ---------------------------- | --------------------------------------- |
222
+ | `frontend:starting` | `{ backendOrigin, entries }` | Juste avant le spawn Vite |
223
+ | `frontend:ready` | `IViteSupervisorStatus` | Vite a annoncé `Local:` dans son stdout |
224
+ | `frontend:error` | `Error` | Spawn ou ready timeout échoué |
225
+ | `frontend:stopped` | (rien) | Après `stop()` propre (SIGINT envoyé) |
226
+
227
+ Exemple :
228
+
229
+ ```ts
230
+ const svc = kernel.container.get("frontend") as FrontendService;
231
+ svc.on("frontend:ready", (status) => {
232
+ console.log(`Vite up on ${status.host}:${status.port}`);
233
+ });
234
+ ```
235
+
236
+ ---
237
+
238
+ ## API publique du service
239
+
240
+ ```ts
241
+ interface IFrontendService {
242
+ registerEntry(module, declaration): IResolvedFrontendEntry;
243
+ listEntries(): ReadonlyArray<IResolvedFrontendEntry>;
244
+ status(): IViteSupervisorStatus;
245
+ startDev(): Promise<void>; // appelé auto par onServersReady
246
+ stopDev(): Promise<void>;
247
+ build(): Promise<void>; // vite.build() in-proc
248
+ // `nonce` = `Context.cspNonce` ; `requestHost` = `Context.domain` (sans port),
249
+ // dont l'origine des assets est dérivée en développement.
250
+ renderTags(entryName, nonce?, requestHost?): string;
251
+ renderDocument(entryName, nonce?, requestHost?): string;
252
+ assetUrl(path): string;
253
+ }
254
+ ```
255
+
256
+ ---
257
+
258
+ ## Troubleshooting
259
+
260
+ ### Page blanche, scripts bloqués par CSP
261
+
262
+ Le helmet de `@nodefony/security` pose `script-src 'self'` par défaut. Override dans le controller :
263
+
264
+ ```ts
265
+ this.context.response.setHeader(
266
+ "Content-Security-Policy",
267
+ svc.getCspDirectives(),
268
+ );
269
+ ```
270
+
271
+ ### `Unexpected token '<'` sur `fetch("/api/...")`
272
+
273
+ Vite sert son SPA-fallback HTML pour les routes inconnues. Déclare le préfixe dans `apiProxyPaths` :
274
+
275
+ ```ts
276
+ svc.registerEntry(this, { ..., apiProxyPaths: ["/my/api"] });
277
+ ```
278
+
279
+ ### `@vitejs/plugin-react can't detect preamble`
280
+
281
+ Le `TemplateHelper` injecte automatiquement le preamble React Fast Refresh pour les entries `type: "react19"`. Si tu vois cette erreur, vérifie que tu utilises bien `svc.renderTags("entry-name")` au lieu d'injecter les `<script>` à la main.
282
+
283
+ ### Vite démarre sur un autre port que `devPort`
284
+
285
+ Le port configuré est pris. Le supervisor retry automatiquement sur `port+1`, `port+2` (option `portRetryAttempts`). Vérifie le port résolu via `svc.status().port`.
286
+
287
+ ### Le browser refuse le cert HTTPS de Vite (`https: true`)
288
+
289
+ Va sur `https://127.0.0.1:5173/` une fois et accepte le certificat. Ou installe la CA root Nodefony : `nodefony/config/certificates/ca/nodefony-root-ca.crt.pem` dans ton trousseau.
290
+
291
+ ### Vite crash en boucle (`max restarts reached`)
292
+
293
+ Le superviseur abandonne après `maxRestarts` (default 5). Regarde les logs Vite dans le syslog (`[vite!] ...`) pour la cause. Augmente `maxRestarts` ou fix le source du crash.
294
+
295
+ ---
296
+
297
+ ## Tests
298
+
299
+ ```bash
300
+ npm test # unit — ViteConfigGenerator
301
+ npm run test:integration # intégration — supervisor, spawn réel
302
+ ```
303
+
304
+ Les tests d'intégration nécessitent `vite` installé (déjà en devDependencies du repo).
305
+
306
+ ---
307
+
308
+ ## Architecture (résumé)
309
+
310
+ ```
311
+ Module consumer
312
+ │ onKernelBoot()
313
+
314
+ FrontendService.registerEntry() ← collecte les frontends à transpiler
315
+
316
+ Kernel "onServersReady" ← 4 servers Nodefony écoutent
317
+
318
+ FrontendService.startDev()
319
+
320
+ ViteProcessSupervisor.start()
321
+ ├─ écrit vite.config.generated.mjs (proxy, https, base, env)
322
+ ├─ spawn("npx", ["vite", "--config", ...])
323
+ ├─ parse stdout "Local: https://host:port" → state = "ready"
324
+ ├─ attach exit handler (auto-restart si crash inattendu)
325
+ └─ start health check loop (ping HTTP périodique)
326
+
327
+ Browser GET /my-route/
328
+
329
+ Nodefony rend HTML + svc.renderTags() injecte:
330
+ <script type="module"> preamble React Fast Refresh
331
+ <script src="https://host:5173/@vite/client">
332
+ <script src="https://host:5173/src/main.tsx">
333
+
334
+ Browser → 5173 (Vite) pour les assets + HMR WSS
335
+ Browser → 5173 pour /api/... → Vite proxifie vers Nodefony (5151/5152)
336
+ ```
337
+
338
+ Détails internes (dans le dépôt) : voir [`CLAUDE.md`](https://github.com/nodefony/nodefony-core/blob/claude-ts/src/packages/@nodefony/frontend/CLAUDE.md) et [`MEMORY.md`](https://github.com/nodefony/nodefony-core/blob/claude-ts/src/packages/@nodefony/frontend/MEMORY.md).
@@ -0,0 +1,9 @@
1
+ //#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorate.js
2
+ function __decorate(decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ }
8
+ //#endregion
9
+ export { __decorate as default };
@@ -0,0 +1,6 @@
1
+ //#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorateMetadata.js
2
+ function __decorateMetadata(k, v) {
3
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
4
+ }
5
+ //#endregion
6
+ export { __decorateMetadata as default };
package/dist/index.js ADDED
@@ -0,0 +1,63 @@
1
+ import config, { frontendConfigSchema } from "./nodefony/config/config.js";
2
+ import { defineFrontendConfig, frontendConfigJsonSchema } from "./nodefony/config/defineModuleConfig.js";
3
+ import { FrontendError, FrontendNoEntriesError, FrontendPresetUnknownError, FrontendSupervisorStartError } from "./nodefony/src/errors/FrontendError.js";
4
+ import react19Preset from "./nodefony/src/presets/react19-vite.js";
5
+ import vue3Preset from "./nodefony/src/presets/vue3-vite.js";
6
+ import angularPreset from "./nodefony/src/presets/angular-vite.js";
7
+ import vanillaPreset from "./nodefony/src/presets/vanilla-vite.js";
8
+ import svelte5Preset from "./nodefony/src/presets/svelte5-vite.js";
9
+ import { ViteBuilder } from "./nodefony/src/builders/ViteBuilder.js";
10
+ import { ViteConfigGenerator } from "./nodefony/service/ViteConfigGenerator.js";
11
+ import { ViteProcessSupervisor } from "./nodefony/service/ViteProcessSupervisor.js";
12
+ import { TemplateHelper } from "./nodefony/src/template/TemplateHelper.js";
13
+ import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
14
+ import __decorate from "./_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
15
+ import FrontendService_default from "./nodefony/service/FrontendService.js";
16
+ import { buildFrontendStatus, createFrontendAdminApi } from "./nodefony/src/FrontendAdminApi.js";
17
+ import FrontendBuild from "./nodefony/command/frontend-build.js";
18
+ import FrontendDev from "./nodefony/command/frontend-dev.js";
19
+ import FrontendStatus from "./nodefony/command/frontend-status.js";
20
+ import { Kernel, Module, services } from "nodefony";
21
+ //#region index.ts
22
+ let Frontend = class Frontend extends Module {
23
+ constructor(kernel) {
24
+ super("frontend", kernel, import.meta.url, config);
25
+ this.addCommand(FrontendBuild);
26
+ this.addCommand(FrontendDev);
27
+ this.addCommand(FrontendStatus);
28
+ }
29
+ /** JSON Schema de la config frontend → data plane admin (config riche Studio). */
30
+ configSchema() {
31
+ return frontendConfigJsonSchema();
32
+ }
33
+ /**
34
+ * Phase `onRegister` : valide la config (défauts + override `module-frontend`)
35
+ * via `defineFrontendConfig`, puis la ré-assigne à `this.options` AVANT
36
+ * l'instanciation du `@services` (`FrontendService` lit `module.options` à sa
37
+ * construction). Plante propre avec messages clairs si la config est invalide
38
+ * (convention Zod figée 2026-05-28).
39
+ */
40
+ async onKernelRegister() {
41
+ this.options = defineFrontendConfig(this.options);
42
+ return this;
43
+ }
44
+ /**
45
+ * Phase `onBoot` : enregistre le producteur admin (`/nodefony/frontend/api/*`)
46
+ * auprès du broker, AVANT que framework ne monte le data plane à `onReady`.
47
+ * Handler lazy → le statut Vite est lu à la requête (superviseur démarré à
48
+ * `onServersReady`, bien après ce hook).
49
+ */
50
+ async onKernelBoot() {
51
+ const registry = this.kernel?.container?.get("adminBroker");
52
+ const svc = this.kernel?.container?.get("frontend");
53
+ if (registry && svc && !registry.has("frontend")) registry.register(createFrontendAdminApi(svc));
54
+ return this;
55
+ }
56
+ async onKernelReady() {
57
+ return this;
58
+ }
59
+ };
60
+ Frontend = __decorate([services([FrontendService_default]), __decorateMetadata("design:paramtypes", [typeof Kernel === "undefined" ? Object : Kernel])], Frontend);
61
+ var frontend_default = Frontend;
62
+ //#endregion
63
+ export { Frontend, FrontendError, FrontendNoEntriesError, FrontendPresetUnknownError, FrontendService_default as FrontendService, FrontendSupervisorStartError, TemplateHelper, ViteBuilder, ViteConfigGenerator, ViteProcessSupervisor, angularPreset, buildFrontendStatus, createFrontendAdminApi, frontend_default as default, defineFrontendConfig, frontendConfigJsonSchema, frontendConfigSchema, react19Preset, svelte5Preset, vanillaPreset, vue3Preset };
@@ -0,0 +1,68 @@
1
+ import { Command } from "nodefony";
2
+ import path from "node:path";
3
+ import fs from "node:fs";
4
+ //#region nodefony/command/frontend-build.ts
5
+ const options = {
6
+ helpGroup: "FRONT ET RÉSEAU",
7
+ showBanner: false,
8
+ kernelEvent: "onReady"
9
+ };
10
+ /**
11
+ * `nodefony frontend:build` — build production de tous les frontends déclarés.
12
+ *
13
+ * Écrit `public/dist/` + `manifest.json` par bundle (lu ensuite par
14
+ * `renderProdTags` + servi par `server-static`).
15
+ *
16
+ * - Idempotent : un bundle déjà à jour est **ignoré** (relance prod rapide).
17
+ * `--force` rebuild tout.
18
+ * - Erreurs : un bundle KO n'arrête pas les autres ; l'exit code passe à `1`
19
+ * s'il reste au moins un échec (cassure de pipeline CI).
20
+ */
21
+ var FrontendBuild = class extends Command {
22
+ constructor(cli) {
23
+ super("frontend:build", "construit les fronts pour la production", cli, options);
24
+ this.addOption("-f, --force", "rebuild even if the manifest is up-to-date");
25
+ }
26
+ async generate() {
27
+ const opts = this.command?.opts?.() ?? {};
28
+ const svc = this.kernel?.container?.get("frontend");
29
+ if (!svc) {
30
+ this.log("service `frontend` not registered — is @nodefony/frontend loaded?", "ERROR");
31
+ process.exitCode = 1;
32
+ return this;
33
+ }
34
+ const entries = svc.listEntries();
35
+ if (entries.length === 0) {
36
+ this.log("no frontend entries declared", "WARNING");
37
+ console.log("frontend:build — aucun frontend déclaré, rien à construire.");
38
+ return this;
39
+ }
40
+ const lastBuilt = (name) => {
41
+ const e = entries.find((x) => x.entryName === name);
42
+ if (!e) return "?";
43
+ try {
44
+ return fs.statSync(path.join(e.outDir, ".vite", "manifest.json")).mtime.toLocaleString("fr-FR");
45
+ } catch {
46
+ return "jamais";
47
+ }
48
+ };
49
+ console.log(`frontend:build — ${entries.length} frontend(s)${opts.force ? " (--force)" : ""}…`);
50
+ try {
51
+ const res = await svc.build({ force: opts.force });
52
+ if (res.built.length) console.log(` ✓ construit(s) : ${res.built.join(", ")}`);
53
+ for (const name of res.skipped) console.log(` • à jour : ${name} (dernier build ${lastBuilt(name)}) — rien à faire`);
54
+ for (const f of res.failures) console.log(` ✗ ÉCHEC : ${f.entryName} — ${f.message}`);
55
+ if (!res.built.length && !res.failures.length) console.log(" → tout est à jour. Rien à reconstruire (--force pour tout refaire).");
56
+ this.log(`built:[${res.built.join(", ") || "—"}] skipped:[${res.skipped.join(", ") || "—"}] failed:[${res.failures.map((f) => f.entryName).join(", ") || "—"}]`, res.failures.length ? "ERROR" : "INFO");
57
+ if (res.failures.length) process.exitCode = 1;
58
+ } catch (e) {
59
+ const msg = e instanceof Error ? e.message : String(e);
60
+ this.log(msg, "ERROR");
61
+ console.log(`frontend:build — ÉCHEC : ${msg}`);
62
+ process.exitCode = 1;
63
+ }
64
+ return this;
65
+ }
66
+ };
67
+ //#endregion
68
+ export { FrontendBuild as default };
@@ -0,0 +1,31 @@
1
+ import { Command } from "nodefony";
2
+ //#region nodefony/command/frontend-dev.ts
3
+ const options = {
4
+ helpGroup: "FRONT ET RÉSEAU",
5
+ showBanner: false,
6
+ kernelEvent: "onReady"
7
+ };
8
+ /**
9
+ * `nodefony frontend:dev` — démarre manuellement le superviseur Vite.
10
+ *
11
+ * Utile quand `autoStartInDevelopment: false` dans la config, ou pour
12
+ * relancer le superviseur Vite manuellement.
13
+ */
14
+ var FrontendDev = class extends Command {
15
+ constructor(cli) {
16
+ super("frontend:dev", "démarre le serveur Vite à la main", cli, options);
17
+ }
18
+ async generate() {
19
+ const svc = this.kernel?.container?.get("frontend");
20
+ if (!svc) {
21
+ this.log("service `frontend` not registered", "ERROR");
22
+ return this;
23
+ }
24
+ await svc.startDev();
25
+ const st = svc.status();
26
+ this.log(`vite dev server: ${st.host}:${st.port} [${st.state}]`, "INFO");
27
+ return this;
28
+ }
29
+ };
30
+ //#endregion
31
+ export { FrontendDev as default };
@@ -0,0 +1,40 @@
1
+ import { Command } from "nodefony";
2
+ //#region nodefony/command/frontend-status.ts
3
+ const options = {
4
+ helpGroup: "FRONT ET RÉSEAU",
5
+ showBanner: false,
6
+ kernelEvent: "onReady"
7
+ };
8
+ /**
9
+ * `nodefony frontend:status` — lit l'état du superviseur Vite.
10
+ *
11
+ * Utilisé par Vision (Phase 10) pour afficher l'état du builder dans
12
+ * son tableau de bord admin.
13
+ */
14
+ var FrontendStatus = class extends Command {
15
+ constructor(cli) {
16
+ super("frontend:status", "l'état du superviseur des fronts", cli, options);
17
+ this.addOption("-j, --json", "output as JSON");
18
+ }
19
+ async generate(_arg, opts) {
20
+ const svc = this.kernel?.container?.get("frontend");
21
+ if (!svc) {
22
+ this.log("service `frontend` not registered", "ERROR");
23
+ return this;
24
+ }
25
+ const st = svc.status();
26
+ if (opts.json) process.stdout.write(JSON.stringify(st, null, 2) + "\n");
27
+ else {
28
+ console.log(`state : ${st.state}`);
29
+ console.log(`endpoint : ${st.host}:${st.port}`);
30
+ if (st.origin && !st.origin.endsWith(`://${st.host}:${st.port}`)) console.log(`public : ${st.origin}`);
31
+ console.log(`pid : ${st.pid ?? "-"}`);
32
+ console.log(`entries : ${st.entries.length}`);
33
+ for (const e of st.entries) console.log(` · ${e.entryName} [${e.type}] ${e.entryFile}`);
34
+ if (st.lastError) console.log(`error : ${st.lastError}`);
35
+ }
36
+ return this;
37
+ }
38
+ };
39
+ //#endregion
40
+ export { FrontendStatus as default };
@@ -0,0 +1,65 @@
1
+ import { z } from "zod";
2
+ //#region nodefony/config/config.ts
3
+ /**
4
+ * @nodefony/frontend — CONFIGURATION DU MODULE (schéma Zod = source unique).
5
+ *
6
+ * ⭐ TL;DR : CE SCHÉMA EST LA CONFIG. Chaque `.default(...)` = la valeur d'usine ;
7
+ * changer un défaut du module = ÉDITER ICI (et nulle part ailleurs). L'app, elle,
8
+ * surcharge via `use("@nodefony/...", { … })` dans SON `nodefony.config.ts`.
9
+ *
10
+ * Ce module pilote Vite (builder + dev server) pour transpiler les frontends
11
+ * déclarés par chaque module Nodefony :
12
+ *
13
+ * { frontend: { type: "react19", entry: "./frontend/src/main.tsx" } }
14
+ *
15
+ * RÈGLE D'OR (ADR-0006) : ce fichier porte le **schéma Zod commenté** (type +
16
+ * validation + défaut + doc) ET matérialise les défauts via `parse({})`. Aucune
17
+ * valeur n'est re-tapée ailleurs. Le builder (`defineModuleConfig.ts` →
18
+ * `defineFrontendConfig`) importe le schéma D'ICI (nœud bas : ce fichier
19
+ * n'importe que `zod` → pas de cycle). La fusion + validation finale
20
+ * (`défauts + module.options`) est faite dans `index.ts` au hook
21
+ * `onKernelRegister` via `defineFrontendConfig` (plante propre si invalide).
22
+ *
23
+ * ⚠️ NE PAS éditer les défauts matérialisés en bas de fichier : modifier les
24
+ * `.default(...)` du schéma. La doc de chaque champ (`.describe(...)`) est
25
+ * surfacée dans le panneau de config Studio via `frontendConfigJsonSchema()`.
26
+ *
27
+ * Périmètre : config **module-level** (le dev server Vite + le build prod). La
28
+ * config **par entrée** (`registerEntry(module, { entry, root, publicPath, … })`)
29
+ * est une déclaration runtime du module consommateur, PAS de la config — donc
30
+ * hors de ce schéma.
31
+ */
32
+ const resilienceSchema = z.strictObject({
33
+ autoRestart: z.boolean().default(true).describe("Redémarre automatiquement le superviseur Vite sur crash inattendu. Défaut : true. Mettre `false` en CI pour faire échouer le pipeline sur un crash Vite au lieu de le masquer."),
34
+ maxRestarts: z.number().int().nonnegative().default(5).describe("Nombre maximal de tentatives de restart avant de passer en `state: \"errored\"`. Défaut : 5."),
35
+ restartBackoffBaseMs: z.number().int().positive().default(500).describe("Base du backoff exponentiel entre deux restarts (ms). Défaut : 500."),
36
+ restartBackoffMaxMs: z.number().int().positive().default(8e3).describe("Plafond du backoff exponentiel (ms). Défaut : 8000."),
37
+ healthCheckIntervalMs: z.number().int().nonnegative().default(3e4).describe("Intervalle entre deux health checks du dev server (ms). `0` désactive le health check. Défaut : 30000."),
38
+ healthCheckFailureThreshold: z.number().int().positive().default(3).describe("Nombre d'échecs consécutifs de health check avant de déclencher un restart. Défaut : 3."),
39
+ healthCheckTimeoutMs: z.number().int().positive().default(5e3).describe("Timeout d'un health check individuel (ms). Défaut : 5000."),
40
+ portRetryAttempts: z.number().int().positive().default(3).describe("Nombre de ports à essayer sur `EADDRINUSE` (devPort, devPort+1, …). Défaut : 3.")
41
+ }).describe("Résilience du superviseur Vite (auto-restart, backoff, health check). Toutes optionnelles — les défauts internes s'appliquent si rien n'est fourni.");
42
+ const frontendConfigSchema = z.strictObject({
43
+ devHost: z.string().default("127.0.0.1").describe("Host d'écoute du dev server Vite — utilisé tel quel dans les `<script>` injectés (doit être joignable depuis le navigateur). Prod : N/A (Vite ne tourne pas en prod, le manifest pilote)."),
44
+ devPort: z.number().int().positive().default(5173).describe("Port d'écoute du dev server Vite (5173 par défaut) — port de BASE : chaque famille de frontends prend le bloc suivant. Si occupé, c'est le SUPERVISEUR qui relance sur le port suivant (`resilience.portRetryAttempts` essais) et publie le port réel dans son `status()` — Vite, lui, ne se décale jamais seul : le fichier généré porte `strictPort` pour que l'origine annoncée au navigateur soit toujours celle qui sert."),
45
+ publicOrigin: z.string().default("").refine((v) => v === "" || /^https?:\/\/[^/\s]+$/.test(v), { message: "publicOrigin doit être une origine (`scheme://host[:port]`), sans chemin" }).describe("Origine PUBLIQUE du dev server Vite — celle que le NAVIGATEUR utilise, quand elle diffère de l'adresse d'écoute (`devHost`). ÉPINGLE le rendu sur une origine unique : à réserver aux cas où un frontal la réécrit (tunnel, proxy, port remappé). Utilisée telle quelle (port inclus SEULEMENT si écrit) dans les `<script>` injectés, le `base` Vite et le WebSocket HMR (`hmr.host`/`clientPort`, dérivés). Vide (défaut, RECOMMANDÉ) = chaque page annonce l'origine par laquelle le client est arrivé (`Host` de la requête, scheme et port de Vite) : un poste et un navigateur en conteneur sont servis EN MÊME TEMPS par la même instance, sans configuration — et Codespaces/Gitpod restent détectés automatiquement. L'hôte d'une origine épinglée est automatiquement autorisé par Vite (`server.allowedHosts`) ; les hôtes suivis par la dérivation sont ceux de `trustedHosts` de @nodefony/http (une seule liste à maintenir : elle ouvre la barrière 421, Vite, le CSP et le rendu)."),
46
+ autoStartInDevelopment: z.boolean().default(true).describe("Démarre automatiquement le superviseur Vite quand le kernel passe en `development`. Ignoré en `production`/`staging`. Reco : true en dev, sinon les helpers template injecteront une URL morte."),
47
+ defaultOutDir: z.string().default("./public/dist").describe("Dossier de sortie par défaut pour le build prod, relatif à la racine du module consommateur. Réécrit par la prop `outDir` de la déclaration d'entrée."),
48
+ defaultRoot: z.string().default("./frontend").describe("Racine front par défaut (contient `index.html`) côté module."),
49
+ assetBaseUrl: z.string().default("").describe("Base URL des assets servis en PRODUCTION (CDN / object storage / edge). Vide = assets servis depuis l'origine Nodefony en chemins relatifs (comportement historique). Renseignée (ex. `https://cdn.example.com`), elle préfixe le `base` Vite au build, les URLs de `renderProdTags` et le helper `asset('/x')`. N'affecte JAMAIS le mount `Statics`. Reco prod cloud-native : pointer le CDN devant l'object storage."),
50
+ startupTimeoutMs: z.number().int().positive().default(3e4).describe("Timeout (ms) d'attente du `Local: http://…` dans le stdout Vite avant de considérer le démarrage comme cassé. Dev : 30s suffisent pour un cold-start Vite. Prod : N/A."),
51
+ pipeViteLogs: z.boolean().default(true).describe("Propage les logs Vite vers le syslog Nodefony (sinon ils restent dans le stdout du process enfant uniquement)."),
52
+ backendHost: z.string().default("127.0.0.1").describe("Host du serveur Nodefony cible du proxy Vite (`server.proxy`). Quand le navigateur fait `fetch(\"/api/...\")` depuis la page servie par Vite, Vite proxifie vers `${backendProtocol}://${backendHost}:${backendPort}`."),
53
+ backendPort: z.number().int().positive().default(5151).describe("Port du serveur Nodefony cible du proxy Vite. HTTP par défaut (5151, config par défaut de `@nodefony/http`). Ajuster si l'app surcharge."),
54
+ backendProtocol: z.enum(["http", "https"]).default("http").describe("Protocole du proxy Vite vers Nodefony. `http` par défaut — `https` pour proxifier vers le serveur HTTPS Nodefony (5152). Avec `https` et un certificat self-signed, prévoir `secure: false` côté proxy."),
55
+ https: z.boolean().default(false).describe("Active HTTPS pour le dev server Vite (récupère les certificats du service `certificates` de `@nodefony/http`, mêmes certs que `server-https` 5152). Reco : true quand la page Nodefony est servie en HTTPS (5152) — évite le warning mixed-content. Le navigateur demandera la confiance pour 5173 si la CA root Nodefony n'est pas installée localement."),
56
+ viteEnv: z.record(z.string(), z.string()).default({}).describe("Variables d'environnement supplémentaires passées au child Vite. Les clés préfixées `VITE_` sont exposées au navigateur via `import.meta.env.VITE_*` (ex. `{ VITE_API_BASE: \"/api/v1\" }`). Reco prod : utiliser un `.env.production` dans le `root` Vite plutôt que cette option, pour ne pas leak de secrets dans le code Nodefony."),
57
+ resilience: resilienceSchema.default(() => resilienceSchema.parse({}))
58
+ }).describe("Configuration de @nodefony/frontend (dev server Vite + build prod).");
59
+ /**
60
+ * Défauts du module, matérialisés depuis le schéma (source unique). Toujours
61
+ * valides par construction ; passés au `super(..., config)` du Module class.
62
+ */
63
+ const config = frontendConfigSchema.parse({});
64
+ //#endregion
65
+ export { config as default, frontendConfigSchema };
@@ -0,0 +1,34 @@
1
+ import { frontendConfigSchema } from "./config.js";
2
+ import { parseModuleConfig } from "nodefony";
3
+ import { z } from "zod";
4
+ //#region nodefony/config/defineModuleConfig.ts
5
+ /**
6
+ * Builder type-safe de la configuration de `@nodefony/frontend`.
7
+ *
8
+ * ⭐ TL;DR : MACHINERIE DE BOOT — on n'édite (presque) jamais ce fichier. Même
9
+ * pattern que `nodefony.config.ts` ↔ `defineConfig()` du core : `config.ts` PORTE
10
+ * la config (schéma + défauts), `define<X>Config()` la VALIDE au boot (parse +
11
+ * env + freeze) et publie le JSON Schema Studio.
12
+ *
13
+ * Principes (alignés sur `defineRealtimeConfig` / famille ORM) :
14
+ * - **Source unique** : `./config.ts` (Zod). Le builder VALIDE + GÈLE, ne dévie pas.
15
+ * - **Auto-documenté + introspectable** : chaque champ Zod porte `.describe()` →
16
+ * {@link frontendConfigJsonSchema} produit un JSON Schema que le panneau de
17
+ * config Studio consomme.
18
+ *
19
+ * @param config - configuration brute (sections omises = défauts sûrs).
20
+ * @returns config gelée prête pour `FrontendService`.
21
+ * @throws ZodError si invalide.
22
+ */
23
+ function defineFrontendConfig(config = {}) {
24
+ return Object.freeze(parseModuleConfig(frontendConfigSchema, config, "@nodefony/frontend"));
25
+ }
26
+ /**
27
+ * JSON Schema introspectable de la config frontend — destiné au panneau de config
28
+ * Studio (`/nodefony/config`).
29
+ */
30
+ function frontendConfigJsonSchema() {
31
+ return z.toJSONSchema(frontendConfigSchema);
32
+ }
33
+ //#endregion
34
+ export { defineFrontendConfig, frontendConfigJsonSchema };
@@ -0,0 +1 @@
1
+ export {};