@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,708 @@
|
|
|
1
|
+
import config from "../config/config.js";
|
|
2
|
+
import { FrontendNoEntriesError, FrontendSupervisorStartError } from "../src/errors/FrontendError.js";
|
|
3
|
+
import { ViteBuilder } from "../src/builders/ViteBuilder.js";
|
|
4
|
+
import { allowedHostPatternForTemplate, detectRemoteDev, isValidOriginTemplate, viteAllowedHostFromPattern } from "../src/remoteDev.js";
|
|
5
|
+
import { ViteProcessSupervisor } from "./ViteProcessSupervisor.js";
|
|
6
|
+
import { TemplateHelper } from "../src/template/TemplateHelper.js";
|
|
7
|
+
import { familyPortBlocks, familyPortPlan, isolationGroup } from "../src/isolationGroups.js";
|
|
8
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
|
|
9
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
|
|
10
|
+
import { Module, Service, extend, injectable, stripTrailingSlashes } from "nodefony";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
//#region nodefony/service/FrontendService.ts
|
|
14
|
+
/**
|
|
15
|
+
* Normalise un préfixe public : garantit un `/` en tête et en queue.
|
|
16
|
+
* `"_assets/x"` → `"/_assets/x/"`, `"/"` → `"/"`.
|
|
17
|
+
*/
|
|
18
|
+
const normalizePublicPath = (p) => {
|
|
19
|
+
let s = p.trim();
|
|
20
|
+
if (!s.startsWith("/")) s = `/${s}`;
|
|
21
|
+
if (!s.endsWith("/")) s = `${s}/`;
|
|
22
|
+
return s.replace(/\/{2,}/g, "/");
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Service injectable du module `@nodefony/frontend`.
|
|
26
|
+
*
|
|
27
|
+
* Cycle de vie :
|
|
28
|
+
* 1. construction : merge options par défaut + surcharge app (`module-frontend`).
|
|
29
|
+
* 2. `onKernelReady` : si dev + `autoStartInDevelopment` → start superviseur.
|
|
30
|
+
* 3. modules consommateurs appellent `registerEntry(...)` dans leur init.
|
|
31
|
+
* 4. terminate kernel : `stop()` superviseur.
|
|
32
|
+
*
|
|
33
|
+
* Branche POC `poc/frontend-child` : utilise `ViteProcessSupervisor` (spawn).
|
|
34
|
+
* Branche POC `poc/frontend-single` : remplacera par `ViteInProcSupervisor`.
|
|
35
|
+
*/
|
|
36
|
+
let FrontendService = class FrontendService extends Service {
|
|
37
|
+
module;
|
|
38
|
+
cfg;
|
|
39
|
+
builder = new ViteBuilder();
|
|
40
|
+
entries = [];
|
|
41
|
+
/** Une instance Vite par famille d'isolation (`default`, `angular`, …). */
|
|
42
|
+
supervisors = /* @__PURE__ */ new Map();
|
|
43
|
+
/** Template helper par famille (route les `<script>` vers le bon port Vite). */
|
|
44
|
+
templateHelpers = /* @__PURE__ */ new Map();
|
|
45
|
+
/** Index inverse `entryName → famille`, pour router `renderTags`. */
|
|
46
|
+
entryFamily = /* @__PURE__ */ new Map();
|
|
47
|
+
/** Helper prod unique (lit les manifests) — `null` tant qu'on n'est pas en prod. */
|
|
48
|
+
prodHelper = null;
|
|
49
|
+
/**
|
|
50
|
+
* L'origine publique est-elle ÉPINGLÉE par une décision explicite
|
|
51
|
+
* (`frontend.publicOrigin` en config, ou plateforme de dev déporté détectée) ?
|
|
52
|
+
* `true` → la dérivation par `Host` est désactivée : un réglage voulu gagne
|
|
53
|
+
* toujours sur une déduction (cf ordre de priorité, README du module).
|
|
54
|
+
*/
|
|
55
|
+
originPinned = false;
|
|
56
|
+
/**
|
|
57
|
+
* Ports que l'instance Vite de chaque famille PEUT prendre pour ce démarrage
|
|
58
|
+
* (bloc de la famille, port-retry compris) — `null` tant que `startDev` n'a
|
|
59
|
+
* pas établi le plan, et remis à `null` par `stopDev`.
|
|
60
|
+
*
|
|
61
|
+
* Alloué une fois par démarrage de développement, jamais en production
|
|
62
|
+
* (`startDev` n'y tourne pas) : aucun coût par requête.
|
|
63
|
+
*/
|
|
64
|
+
plannedPortBlocks = null;
|
|
65
|
+
constructor(module) {
|
|
66
|
+
const merged = extend(true, {}, config, module.options ?? {});
|
|
67
|
+
super("frontend", module.container, module.notificationsCenter, merged);
|
|
68
|
+
this.module = module;
|
|
69
|
+
this.cfg = merged;
|
|
70
|
+
}
|
|
71
|
+
/** Base CDN normalisée (sans slash final). `""` = origine Nodefony. */
|
|
72
|
+
get assetBase() {
|
|
73
|
+
return stripTrailingSlashes(this.cfg.assetBaseUrl ?? "");
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Résout l'URL publique d'un asset. Préfixe `p` par `assetBaseUrl` (CDN) si
|
|
77
|
+
* renseigné, sinon le renvoie tel quel (origine, chemin relatif). Les URLs
|
|
78
|
+
* absolues (`http(s)://…`) sont renvoyées inchangées. Helper template :
|
|
79
|
+
* `asset('/test/logo.png')` → `https://cdn.example.com/test/logo.png` ou
|
|
80
|
+
* `/test/logo.png` (assetBaseUrl vide).
|
|
81
|
+
*/
|
|
82
|
+
assetUrl(p) {
|
|
83
|
+
if (/^https?:\/\//i.test(p)) return p;
|
|
84
|
+
const base = this.assetBase;
|
|
85
|
+
if (!base) return p;
|
|
86
|
+
return base + (p.startsWith("/") ? p : `/${p}`);
|
|
87
|
+
}
|
|
88
|
+
async init() {
|
|
89
|
+
this.log(`MODULE frontend service init`, "DEBUG");
|
|
90
|
+
this.kernel?.once("onServersReady", async () => {
|
|
91
|
+
const env = this.kernel?.environment;
|
|
92
|
+
if (env === "development" && this.cfg.autoStartInDevelopment) {
|
|
93
|
+
if (this.entries.length === 0) {
|
|
94
|
+
this.log("no frontend entries declared — Vite supervisor not started", "INFO");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const names = this.entries.map((e) => e.entryName);
|
|
98
|
+
this.kernel?.fire("onFrontendStart", { bundles: names.length });
|
|
99
|
+
try {
|
|
100
|
+
await this.startDev();
|
|
101
|
+
} catch (e) {
|
|
102
|
+
this.log(e, "ERROR");
|
|
103
|
+
} finally {
|
|
104
|
+
let ready = 0;
|
|
105
|
+
for (const [, sup] of this.supervisors) {
|
|
106
|
+
const st = sup.status();
|
|
107
|
+
if (st.state !== "ready") continue;
|
|
108
|
+
ready++;
|
|
109
|
+
const scheme = st.https ? "https" : "http";
|
|
110
|
+
const url = `${st.origin ?? `${scheme}://${st.host}:${st.port}`}/`;
|
|
111
|
+
for (const e of st.entries) {
|
|
112
|
+
const fw = e.type.replace(/[0-9]+$/, "");
|
|
113
|
+
this.kernel?.reportBootLine("Frontend (Vite)", `${e.entryName.padEnd(24)}${url}${fw ? ` ${fw}` : ""}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
this.kernel?.fire("onFrontendReady", {
|
|
117
|
+
bundles: names.length,
|
|
118
|
+
names,
|
|
119
|
+
ready
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
} else if (env !== "development") await this.setupProd();
|
|
123
|
+
});
|
|
124
|
+
this.kernel?.once("onTerminate", async () => {
|
|
125
|
+
try {
|
|
126
|
+
await this.stopDev();
|
|
127
|
+
} catch {}
|
|
128
|
+
});
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Enregistre une déclaration frontend d'un module consommateur.
|
|
133
|
+
*
|
|
134
|
+
* À appeler dans le `initialize()` ou `onKernelReady()` du module
|
|
135
|
+
* consommateur — toujours AVANT `onReady` du kernel (sinon le supervisor
|
|
136
|
+
* démarre sans cette entrée).
|
|
137
|
+
*/
|
|
138
|
+
registerEntry(consumerModule, declaration) {
|
|
139
|
+
const moduleRoot = consumerModule.path ?? process.cwd();
|
|
140
|
+
const root = path.resolve(moduleRoot, declaration.root ?? this.cfg.defaultRoot);
|
|
141
|
+
const outDir = path.resolve(moduleRoot, declaration.outDir ?? this.cfg.defaultOutDir);
|
|
142
|
+
const absEntry = path.resolve(moduleRoot, declaration.entry);
|
|
143
|
+
const relEntry = path.relative(root, absEntry);
|
|
144
|
+
const entryName = declaration.name ?? consumerModule.name;
|
|
145
|
+
const entry = {
|
|
146
|
+
moduleName: consumerModule.name,
|
|
147
|
+
entryName,
|
|
148
|
+
type: declaration.type,
|
|
149
|
+
root,
|
|
150
|
+
entryFile: relEntry,
|
|
151
|
+
outDir,
|
|
152
|
+
publicPath: normalizePublicPath(declaration.publicPath ?? `/_assets/${entryName}`),
|
|
153
|
+
apiProxyPaths: declaration.apiProxyPaths ?? []
|
|
154
|
+
};
|
|
155
|
+
this.entries.push(entry);
|
|
156
|
+
this.log(`registered entry: ${entry.entryName} (${entry.type}) from "${entry.moduleName}"`, "INFO");
|
|
157
|
+
return entry;
|
|
158
|
+
}
|
|
159
|
+
listEntries() {
|
|
160
|
+
return this.entries;
|
|
161
|
+
}
|
|
162
|
+
status() {
|
|
163
|
+
const primary = this.supervisors.get("default") ?? [...this.supervisors.values()][0];
|
|
164
|
+
if (!primary) return {
|
|
165
|
+
state: "idle",
|
|
166
|
+
host: this.cfg.devHost,
|
|
167
|
+
origin: null,
|
|
168
|
+
port: null,
|
|
169
|
+
pid: null,
|
|
170
|
+
https: !!this.cfg.https,
|
|
171
|
+
restartCount: 0,
|
|
172
|
+
healthFailures: 0,
|
|
173
|
+
portRetries: 0,
|
|
174
|
+
lastError: null,
|
|
175
|
+
entries: this.entries
|
|
176
|
+
};
|
|
177
|
+
return primary.status();
|
|
178
|
+
}
|
|
179
|
+
statusAll() {
|
|
180
|
+
return [...this.supervisors.entries()].map(([family, s]) => ({
|
|
181
|
+
family,
|
|
182
|
+
status: s.status()
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Démarre une instance Vite **par famille d'isolation** (multi-supervisor).
|
|
187
|
+
*
|
|
188
|
+
* Résilience : chaque famille démarre indépendamment (`Promise.allSettled`).
|
|
189
|
+
* Une famille qui échoue (ex. Angular) est isolée — elle ne fait jamais
|
|
190
|
+
* échouer les autres ni le backend. `startDev` ne rejette que si **aucune**
|
|
191
|
+
* famille n'a pu démarrer.
|
|
192
|
+
*/
|
|
193
|
+
async startDev() {
|
|
194
|
+
if (this.entries.length === 0) throw new FrontendNoEntriesError();
|
|
195
|
+
if (this.supervisors.size > 0 && [...this.supervisors.values()].some((s) => s.status().state === "ready")) return;
|
|
196
|
+
const backendOrigin = `${this.cfg.backendProtocol}://${this.cfg.backendHost}:${this.resolveBackendPort()}`;
|
|
197
|
+
const https = this.resolveHttps();
|
|
198
|
+
const publicOriginTemplate = this.resolvePublicOriginTemplate();
|
|
199
|
+
this.originPinned = publicOriginTemplate !== void 0;
|
|
200
|
+
const allowedHosts = this.viteAllowedHosts(publicOriginTemplate);
|
|
201
|
+
const nodeEnv = this.kernel?.environment;
|
|
202
|
+
const extraEnv = this.cfg.viteEnv ?? {};
|
|
203
|
+
const groups = this.groupEntriesByFamily();
|
|
204
|
+
const portPlan = familyPortPlan(this.cfg.devPort, [...groups.keys()], this.cfg.resilience?.portRetryAttempts ?? 3);
|
|
205
|
+
const families = [...portPlan.keys()];
|
|
206
|
+
if (publicOriginTemplate && !publicOriginTemplate.includes("{port}") && families.length > 1) this.log(`publicOrigin figée (« ${publicOriginTemplate} ») avec ${families.length} familles Vite (${families.join(", ")}) — une origine ne peut en servir qu'UNE : utiliser {port} (ex. https://host:{port}) pour suivre chaque famille`, "WARNING");
|
|
207
|
+
this.plannedPortBlocks = familyPortBlocks(portPlan, this.cfg.resilience?.portRetryAttempts ?? 3);
|
|
208
|
+
this.#registerCsp();
|
|
209
|
+
this.fire("frontend:starting", {
|
|
210
|
+
backendOrigin,
|
|
211
|
+
entries: this.entries
|
|
212
|
+
});
|
|
213
|
+
const total = this.entries.length;
|
|
214
|
+
let done = 0;
|
|
215
|
+
(await Promise.allSettled(families.map((family) => {
|
|
216
|
+
const famSize = groups.get(family).length;
|
|
217
|
+
return this.startFamily(family, groups.get(family), portPlan.get(family), {
|
|
218
|
+
backendOrigin,
|
|
219
|
+
https,
|
|
220
|
+
nodeEnv,
|
|
221
|
+
extraEnv,
|
|
222
|
+
publicOriginTemplate,
|
|
223
|
+
allowedHosts
|
|
224
|
+
}).finally(() => {
|
|
225
|
+
done += famSize;
|
|
226
|
+
this.kernel?.fire("onFrontendProgress", {
|
|
227
|
+
ready: done,
|
|
228
|
+
total
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}))).forEach((res, i) => {
|
|
232
|
+
if (res.status === "rejected") {
|
|
233
|
+
const reason = res.reason;
|
|
234
|
+
this.log(`frontend family "${families[i]}" failed to start (isolated): ${reason?.message ?? reason}`, "ERROR");
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
if ([...this.supervisors.values()].filter((s) => s.status().state === "ready").length === 0) {
|
|
238
|
+
const err = new FrontendSupervisorStartError("no frontend family could start");
|
|
239
|
+
this.fire("frontend:error", err);
|
|
240
|
+
throw err;
|
|
241
|
+
}
|
|
242
|
+
this.#registerCsp();
|
|
243
|
+
this.fire("frontend:ready", this.status());
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Port backend que Vite doit proxifier — le port RÉELLEMENT écouté, pas celui
|
|
247
|
+
* qu'on espérait.
|
|
248
|
+
*
|
|
249
|
+
* `config.backendPort` (5151) n'est qu'une intention : avec
|
|
250
|
+
* `servers.portPolicy: "auto"`, un port occupé fait glisser l'écoute du backend
|
|
251
|
+
* (5151 → 5153). Un proxy figé sur 5151 enverrait alors les appels API du front
|
|
252
|
+
* vers le serveur d'une AUTRE app — au mieux des 404, au pire les données du
|
|
253
|
+
* voisin. On lit donc le port sur le serveur lui-même.
|
|
254
|
+
*
|
|
255
|
+
* Résolution par NOM (`server-http`), jamais par import : `@nodefony/frontend`
|
|
256
|
+
* ne dépend pas de `@nodefony/http` (cycle via la config d'app).
|
|
257
|
+
*/
|
|
258
|
+
resolveBackendPort() {
|
|
259
|
+
const server = this.container?.get?.("server-http");
|
|
260
|
+
const real = server?.active && server.port ? server.port : 0;
|
|
261
|
+
if (real > 0 && real !== this.cfg.backendPort) this.log(`backend à l'écoute sur ${real} (et non ${this.cfg.backendPort}) — le proxy Vite suit le port réel`, "INFO");
|
|
262
|
+
return real > 0 ? real : this.cfg.backendPort;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Résout les certificats HTTPS partagés (service `certificates` de
|
|
266
|
+
* @nodefony/http) si `https: true`. Pas de duplication — mêmes PEM que
|
|
267
|
+
* `server-https` (5152). Retombe sur HTTP avec un warning si indisponible.
|
|
268
|
+
*/
|
|
269
|
+
resolveHttps() {
|
|
270
|
+
if (!this.cfg.https) return void 0;
|
|
271
|
+
const certs = this.container?.get?.("certificates");
|
|
272
|
+
if (!certs?.privateKeyPath || !certs?.certPath) {
|
|
273
|
+
this.log("https: true requested but `certificates` service unavailable — falling back to HTTP", "WARNING");
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
keyPath: certs.privateKeyPath,
|
|
278
|
+
certPath: certs.certPath
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Template d'origine publique Vite (P14.17). Priorité : `frontend.publicOrigin`
|
|
283
|
+
* (config, validée — invalide = ERROR + ignorée, jamais un boot cassé) puis
|
|
284
|
+
* détection de plateforme (Codespaces/Gitpod — variables documentées de la
|
|
285
|
+
* plateforme, qu'on lit sans les posséder). `undefined` = dérivation locale.
|
|
286
|
+
* Chaque adaptation est JOURNALISÉE : on doit pouvoir lire dans le boot
|
|
287
|
+
* pourquoi les `<script>` pointent où ils pointent.
|
|
288
|
+
*/
|
|
289
|
+
resolvePublicOriginTemplate() {
|
|
290
|
+
const cfgOrigin = this.cfg.publicOrigin;
|
|
291
|
+
if (cfgOrigin) {
|
|
292
|
+
if (!isValidOriginTemplate(cfgOrigin)) {
|
|
293
|
+
this.log(`frontend.publicOrigin invalide (« ${cfgOrigin} ») — attendu scheme://host[:port|:{port}] sans chemin ; origine locale utilisée`, "ERROR");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
this.log(`origine publique Vite (config) : ${cfgOrigin}`, "INFO");
|
|
297
|
+
return cfgOrigin;
|
|
298
|
+
}
|
|
299
|
+
const detected = detectRemoteDev(process.env);
|
|
300
|
+
if (detected) {
|
|
301
|
+
this.log(`dev déporté détecté (${detected.provider}) — origine publique Vite : ` + detected.originTemplate, "INFO");
|
|
302
|
+
return detected.originTemplate;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* `server.allowedHosts` pour Vite. Vite accepte d'office IP et `localhost` ;
|
|
307
|
+
* cette liste ne porte que les NOMS. Source des noms légitimes = la MÊME que
|
|
308
|
+
* la barrière Host de Nodefony (`kernel.domain` + `trustedHosts` http) — une
|
|
309
|
+
* seule liste à maintenir : autoriser un hôte dans `trustedHosts` ouvre à la
|
|
310
|
+
* fois la barrière 421 ET Vite. S'y ajoute l'hôte du template d'origine
|
|
311
|
+
* publique. `trustedHosts: true` (barrière déléguée au reverse-proxy) →
|
|
312
|
+
* `true` (même délégation). Un pattern non exprimable chez Vite est ANNONCÉ.
|
|
313
|
+
*/
|
|
314
|
+
viteAllowedHosts(publicOriginTemplate) {
|
|
315
|
+
const th = (this.container?.get?.("HttpKernel"))?.trustedHosts;
|
|
316
|
+
if (th === true) return true;
|
|
317
|
+
const hosts = /* @__PURE__ */ new Set();
|
|
318
|
+
if (this.kernel?.domain) {
|
|
319
|
+
const d = viteAllowedHostFromPattern(this.kernel.domain);
|
|
320
|
+
if (d) hosts.add(d);
|
|
321
|
+
}
|
|
322
|
+
const patterns = Array.isArray(th) ? th : th ? [th] : [];
|
|
323
|
+
for (const p of patterns) {
|
|
324
|
+
if (typeof p !== "string") continue;
|
|
325
|
+
const v = viteAllowedHostFromPattern(p);
|
|
326
|
+
if (v) hosts.add(v);
|
|
327
|
+
else this.log(`trustedHosts « ${p} » non exprimable en allowedHosts Vite — cet hôte passera la barrière Nodefony mais Vite le refusera`, "WARNING");
|
|
328
|
+
}
|
|
329
|
+
if (publicOriginTemplate) {
|
|
330
|
+
const v = allowedHostPatternForTemplate(publicOriginTemplate);
|
|
331
|
+
if (v) hosts.add(v);
|
|
332
|
+
}
|
|
333
|
+
return hosts.size > 0 ? [...hosts] : void 0;
|
|
334
|
+
}
|
|
335
|
+
/** Regroupe les entries par famille d'isolation + remplit l'index inverse. */
|
|
336
|
+
groupEntriesByFamily() {
|
|
337
|
+
const groups = /* @__PURE__ */ new Map();
|
|
338
|
+
this.entryFamily.clear();
|
|
339
|
+
for (const entry of this.entries) {
|
|
340
|
+
const family = isolationGroup(entry.type);
|
|
341
|
+
this.entryFamily.set(entry.entryName, family);
|
|
342
|
+
const arr = groups.get(family);
|
|
343
|
+
if (arr) arr.push(entry);
|
|
344
|
+
else groups.set(family, [entry]);
|
|
345
|
+
}
|
|
346
|
+
return groups;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Démarre l'instance Vite d'une famille sur un port dédié. Enregistre le
|
|
350
|
+
* supervisor + son template helper AVANT le `start()` (l'état dégradé reste
|
|
351
|
+
* observable même si le démarrage échoue → rendu propre, pas d'exception).
|
|
352
|
+
*/
|
|
353
|
+
async startFamily(family, entries, port, ctx) {
|
|
354
|
+
const r = this.cfg.resilience ?? {};
|
|
355
|
+
const supervisor = new ViteProcessSupervisor({
|
|
356
|
+
devHost: this.cfg.devHost,
|
|
357
|
+
devPort: port,
|
|
358
|
+
publicOriginTemplate: ctx.publicOriginTemplate,
|
|
359
|
+
allowedHosts: ctx.allowedHosts,
|
|
360
|
+
startupTimeoutMs: this.cfg.startupTimeoutMs,
|
|
361
|
+
pipeLogs: this.cfg.pipeViteLogs,
|
|
362
|
+
cwd: entries[0].root,
|
|
363
|
+
backendOrigin: ctx.backendOrigin,
|
|
364
|
+
https: ctx.https,
|
|
365
|
+
nodeEnv: ctx.nodeEnv,
|
|
366
|
+
extraEnv: ctx.extraEnv,
|
|
367
|
+
autoRestart: r.autoRestart,
|
|
368
|
+
maxRestarts: r.maxRestarts,
|
|
369
|
+
restartBackoffBaseMs: r.restartBackoffBaseMs,
|
|
370
|
+
restartBackoffMaxMs: r.restartBackoffMaxMs,
|
|
371
|
+
healthCheckIntervalMs: r.healthCheckIntervalMs,
|
|
372
|
+
healthCheckFailureThreshold: r.healthCheckFailureThreshold,
|
|
373
|
+
healthCheckTimeoutMs: r.healthCheckTimeoutMs,
|
|
374
|
+
portRetryAttempts: r.portRetryAttempts,
|
|
375
|
+
logger: {
|
|
376
|
+
info: (m) => this.log(`[${family}] ${m}`, "INFO"),
|
|
377
|
+
error: (m) => this.log(`[${family}] ${m}`, "ERROR"),
|
|
378
|
+
debug: (m) => this.log(`[${family}] ${m}`, "DEBUG")
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
this.supervisors.set(family, supervisor);
|
|
382
|
+
this.templateHelpers.set(family, new TemplateHelper(supervisor, "development"));
|
|
383
|
+
const cfg = await this.builder.buildViteConfig([...entries], "development");
|
|
384
|
+
await supervisor.start(entries, cfg);
|
|
385
|
+
this.log(`vite [${family}] ready on ${supervisor.status().host}:${supervisor.status().port}`, "INFO");
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Câblage prod (idempotent) : monte chaque `outDir` sur son `publicPath`
|
|
389
|
+
* auprès du serveur statique `server-static` (résolu par nom — pas d'import
|
|
390
|
+
* http) et crée le helper prod (lecture manifests). No-op si pas d'entrée.
|
|
391
|
+
*
|
|
392
|
+
* Une entrée SANS build servirait une page blanche : jamais en silence.
|
|
393
|
+
* Cas nominal cloud-native : le build est fait à l'image (`npm run build`,
|
|
394
|
+
* qui chaîne `frontend:build` dans les apps générées) → manifest présent,
|
|
395
|
+
* zéro travail ici. Cas « prod essayée sur le poste » (devDeps installées →
|
|
396
|
+
* vite résolvable) : build one-shot au boot — idempotent, et il supprime
|
|
397
|
+
* l'écran blanc qui perd l'utilisateur, surtout en `--detach`. Sans vite
|
|
398
|
+
* (image runtime sans devDependencies) : impossible de réparer ici → on
|
|
399
|
+
* NOMME l'entrée, le manifest attendu et le geste, en ERROR.
|
|
400
|
+
* Cf project_resilience_no_silent_degradation (fail-soft dispo, fail-loud
|
|
401
|
+
* dégradation — tout fallback annoncé).
|
|
402
|
+
*/
|
|
403
|
+
async setupProd() {
|
|
404
|
+
if (this.prodHelper) return;
|
|
405
|
+
if (this.entries.length === 0) {
|
|
406
|
+
this.log("no frontend entries declared — prod static not mounted", "INFO");
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const unbuilt = this.entries.filter((e) => !fs.existsSync(path.join(e.outDir, ".vite", "manifest.json")));
|
|
410
|
+
if (unbuilt.length > 0) {
|
|
411
|
+
const names = unbuilt.map((e) => e.entryName).join(", ");
|
|
412
|
+
try {
|
|
413
|
+
this.log(`entrée(s) frontend sans build (${names}) — construction au boot (one-shot). En production réelle, builde à l'image : npm run build.`, "WARNING");
|
|
414
|
+
const r = await this.build();
|
|
415
|
+
if (r.failures.length > 0) this.log(`build front en ÉCHEC au boot (${r.failures.map((f) => f.entryName).join(", ")}) — ces pages seront servies SANS interface`, "ERROR");
|
|
416
|
+
} catch (e) {
|
|
417
|
+
this.log(`entrée(s) frontend sans build (${names}) et vite indisponible ici — les pages seront servies SANS interface (blanches). Builde AVANT de lancer : npm run build (ou nodefony frontend:build), puis redémarre. Si vite manque aussi en développement, il n'est pas une dépendance de ce module : installe-le côté application (npm i -D vite, plus le plugin de ton framework). Détail : ${e instanceof Error ? e.message : String(e)}`, "ERROR");
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const stat = this.container?.get?.("server-static");
|
|
421
|
+
if (stat?.addMount) for (const e of this.entries) {
|
|
422
|
+
stat.addMount(e.publicPath, e.outDir);
|
|
423
|
+
this.log(`prod static mount ${e.publicPath} → ${e.outDir}`, "INFO");
|
|
424
|
+
}
|
|
425
|
+
else this.log("server-static service unavailable — assets won't be served by Nodefony (expecting a frontal proxy)", "WARNING");
|
|
426
|
+
this.prodHelper = new TemplateHelper(null, "production", this.entries, this.assetBase);
|
|
427
|
+
this.fire("frontend:ready", this.status());
|
|
428
|
+
}
|
|
429
|
+
async stopDev() {
|
|
430
|
+
if (this.supervisors.size === 0) return;
|
|
431
|
+
await Promise.allSettled([...this.supervisors.values()].map((s) => s.stop()));
|
|
432
|
+
this.supervisors.clear();
|
|
433
|
+
this.templateHelpers.clear();
|
|
434
|
+
this.entryFamily.clear();
|
|
435
|
+
this.plannedPortBlocks = null;
|
|
436
|
+
(this.container?.get?.("firewall"))?.unregisterCspOrigins?.("frontend");
|
|
437
|
+
this.fire("frontend:stopped");
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Build production — `vite.build()` **par entry** (chaque bundle a son propre
|
|
441
|
+
* `root`/`outDir`/`base`/`manifest` : multi-module + isolation Angular).
|
|
442
|
+
*
|
|
443
|
+
* Idempotent : une entrée dont le `manifest.json` est plus récent que ses
|
|
444
|
+
* sources est **ignorée** (`skipped`) — relance prod console rapide. `force`
|
|
445
|
+
* rebuild tout. Les échecs sont **collectés** (un bundle KO n'arrête pas les
|
|
446
|
+
* autres) et remontés dans `failures` → la commande CLI casse l'exit code.
|
|
447
|
+
*
|
|
448
|
+
* ⚠️ **`NODE_ENV` est posé le temps du build, et restauré.** Vite dérive son
|
|
449
|
+
* `isProduction` de `process.env.NODE_ENV`, qui **prime sur le `mode`** de la
|
|
450
|
+
* configuration : une commande CLI, dont le kernel démarre en développement,
|
|
451
|
+
* produisait donc un bundle de DÉVELOPPEMENT malgré `mode: "production"` —
|
|
452
|
+
* `import.meta.env.DEV` vrai chez l'utilisateur final (tout code gardé par ce
|
|
453
|
+
* drapeau s'exécutait en production), et les messages d'aide de Vue publiés.
|
|
454
|
+
* La restauration n'est pas une précaution de style : `build()` est aussi
|
|
455
|
+
* appelé par `setupProd()`, et laisser la variable retournée marquerait un
|
|
456
|
+
* process de développement comme production pour le reste de sa vie.
|
|
457
|
+
*
|
|
458
|
+
* @param opts.force ignore le cache de fraîcheur (rebuild systématique).
|
|
459
|
+
*/
|
|
460
|
+
async build(opts) {
|
|
461
|
+
if (this.entries.length === 0) throw new FrontendNoEntriesError();
|
|
462
|
+
const vite = await import("vite");
|
|
463
|
+
const result = {
|
|
464
|
+
built: [],
|
|
465
|
+
skipped: [],
|
|
466
|
+
failures: []
|
|
467
|
+
};
|
|
468
|
+
const nodeEnvBefore = process.env.NODE_ENV;
|
|
469
|
+
process.env.NODE_ENV = "production";
|
|
470
|
+
try {
|
|
471
|
+
return await this.#buildEntries(vite, result, opts);
|
|
472
|
+
} finally {
|
|
473
|
+
if (nodeEnvBefore === void 0) delete process.env.NODE_ENV;
|
|
474
|
+
else process.env.NODE_ENV = nodeEnvBefore;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* La boucle de build proprement dite — extraite pour que `build()` ne porte
|
|
479
|
+
* que la garde d'environnement, et que le `finally` de restauration couvre
|
|
480
|
+
* tous les chemins de sortie sans indenter cent lignes.
|
|
481
|
+
*/
|
|
482
|
+
async #buildEntries(vite, result, opts) {
|
|
483
|
+
for (const entry of this.entries) {
|
|
484
|
+
if (!opts?.force && this.isBuildFresh(entry)) {
|
|
485
|
+
result.skipped.push(entry.entryName);
|
|
486
|
+
this.log(`build skip "${entry.entryName}" (à jour — --force pour forcer)`, "INFO");
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
try {
|
|
490
|
+
const cfg = await this.builder.buildViteConfig([entry], "production", this.assetBase);
|
|
491
|
+
await vite.build(cfg);
|
|
492
|
+
result.built.push(entry.entryName);
|
|
493
|
+
this.log(`build ok "${entry.entryName}" → ${entry.outDir}`, "INFO");
|
|
494
|
+
} catch (e) {
|
|
495
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
496
|
+
result.failures.push({
|
|
497
|
+
entryName: entry.entryName,
|
|
498
|
+
message
|
|
499
|
+
});
|
|
500
|
+
this.log(`build FAILED "${entry.entryName}": ${message}`, "ERROR");
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
this.log(`frontend build: ${result.built.length} built, ${result.skipped.length} skipped, ${result.failures.length} failed`, result.failures.length ? "WARNING" : "INFO");
|
|
504
|
+
return result;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Une entrée est « fraîche » si son `manifest.json` existe ET qu'aucun fichier
|
|
508
|
+
* source (sous `root`, hors `node_modules`/`outDir`/`.vite`) n'est plus récent.
|
|
509
|
+
* Scan disque borné (dossier front petit) — évite un rebuild Vite inutile.
|
|
510
|
+
*/
|
|
511
|
+
isBuildFresh(entry) {
|
|
512
|
+
let manifestMtime;
|
|
513
|
+
try {
|
|
514
|
+
manifestMtime = fs.statSync(path.join(entry.outDir, ".vite", "manifest.json")).mtimeMs;
|
|
515
|
+
} catch {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
return this.newestSourceMtime(entry.root, entry.outDir) <= manifestMtime;
|
|
519
|
+
}
|
|
520
|
+
/** Mtime du fichier le plus récent sous `dir` (récursif borné). */
|
|
521
|
+
newestSourceMtime(dir, outDir) {
|
|
522
|
+
let newest = 0;
|
|
523
|
+
const walk = (d) => {
|
|
524
|
+
let items;
|
|
525
|
+
try {
|
|
526
|
+
items = fs.readdirSync(d, { withFileTypes: true });
|
|
527
|
+
} catch {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
for (const it of items) {
|
|
531
|
+
if (it.name === "node_modules" || it.name === ".vite") continue;
|
|
532
|
+
const full = path.join(d, it.name);
|
|
533
|
+
if (full === outDir) continue;
|
|
534
|
+
if (it.isDirectory()) walk(full);
|
|
535
|
+
else try {
|
|
536
|
+
const m = fs.statSync(full).mtimeMs;
|
|
537
|
+
if (m > newest) newest = m;
|
|
538
|
+
} catch {}
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
walk(dir);
|
|
542
|
+
return newest;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Document HTML complet pour une entrée — lit l'`index.html` du module
|
|
546
|
+
* (le dev y met meta/polices/scripts externes) + injecte les tags Nodefony.
|
|
547
|
+
* Le controller renvoie : `this.render(svc.renderDocument("x", this.context.cspNonce))`.
|
|
548
|
+
* @param nonce nonce CSP de la requête (`Context.cspNonce`) — propagé aux `<script>`.
|
|
549
|
+
*/
|
|
550
|
+
renderDocument(entryName, nonce, requestHost) {
|
|
551
|
+
if (this.prodHelper) return this.prodHelper.renderDocument(entryName, nonce);
|
|
552
|
+
const family = this.entryFamily.get(entryName);
|
|
553
|
+
const helper = family ? this.templateHelpers.get(family) : void 0;
|
|
554
|
+
if (!helper) return `<!-- @nodefony/frontend: helper not initialized for "${entryName}" -->`;
|
|
555
|
+
return helper.renderDocument(entryName, nonce, this.derivableHost(requestHost));
|
|
556
|
+
}
|
|
557
|
+
renderTags(entryName, nonce, requestHost) {
|
|
558
|
+
if (this.prodHelper) return this.prodHelper.renderTags(entryName, nonce);
|
|
559
|
+
const family = this.entryFamily.get(entryName);
|
|
560
|
+
const helper = family ? this.templateHelpers.get(family) : void 0;
|
|
561
|
+
if (!helper) return `<!-- @nodefony/frontend: helper not initialized for "${entryName}" -->`;
|
|
562
|
+
return helper.renderTags(entryName, nonce, this.derivableHost(requestHost));
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Le `Host` de la requête peut-il servir à dériver l'origine des assets ?
|
|
566
|
+
*
|
|
567
|
+
* Trois conditions, dans cet ordre — chacune protège un cas RÉEL :
|
|
568
|
+
* 1. **origine non épinglée** : un `frontend.publicOrigin` explicite (ou une
|
|
569
|
+
* plateforme de dev déporté détectée) est une décision de l'auteur, elle
|
|
570
|
+
* gagne toujours sur une déduction ;
|
|
571
|
+
* 2. **barrière `trustedHosts` franchie** : le `Host` est une donnée
|
|
572
|
+
* CLIENTE. Sans ce filtre, un `Host` forgé ferait émettre des
|
|
573
|
+
* `<script src="https://attaquant:5173/…">` dans une page de dev ;
|
|
574
|
+
* 3. **barrière non déléguée** (`trustedHosts !== true`) : le bypass total
|
|
575
|
+
* ne dit plus rien de la légitimité d'un nom, et surtout le CSP émis par
|
|
576
|
+
* le firewall (`#viteCspFragment`) ne couvre alors QUE loopback +
|
|
577
|
+
* domaine canonique — dériver ailleurs produirait une page dont les
|
|
578
|
+
* scripts sont bloqués. Les deux listes doivent rester la même liste.
|
|
579
|
+
*
|
|
580
|
+
* @returns le nom d'hôte à employer, ou `undefined` pour garder l'origine
|
|
581
|
+
* résolue au démarrage (comportement d'avant la dérivation).
|
|
582
|
+
*/
|
|
583
|
+
derivableHost(requestHost) {
|
|
584
|
+
if (!requestHost || this.originPinned) return void 0;
|
|
585
|
+
const httpKernel = this.container?.get?.("HttpKernel");
|
|
586
|
+
if (!httpKernel || httpKernel.trustedHosts === true) return void 0;
|
|
587
|
+
return httpKernel.isTrustedHostname?.(requestHost) === true ? requestHost : void 0;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Déclare les origines Vite dev au firewall `@nodefony/security` (résolution PAR
|
|
591
|
+
* NOM = anti-cycle, comme `setupProd`/`server-static`). Le firewall émet alors UN
|
|
592
|
+
* seul CSP (nonce + origines mergées) → remplace le hack `setHeader` des controllers.
|
|
593
|
+
* No-op si security absent (app sans firewall).
|
|
594
|
+
*/
|
|
595
|
+
#registerCsp() {
|
|
596
|
+
(this.container?.get?.("firewall"))?.registerCspOrigins?.("frontend", this.#viteCspFragment());
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Fragment CSP des besoins Vite DEV. Deux exigences :
|
|
600
|
+
* 1. **`'self'` dans CHAQUE directive** : `connect-src`/`style-src`/`img-src`/
|
|
601
|
+
* `font-src` n'héritent PAS de `default-src` → sans `'self'`, tout le
|
|
602
|
+
* same-origin (fetch API, styles, images, fonts) serait bloqué.
|
|
603
|
+
* 2. **Origines Vite sur tous les hosts de dev** : loopback + `kernel.domain`
|
|
604
|
+
* + la liste `trustedHosts` du module http (vhosts comme `nodefony.com`) ×
|
|
605
|
+
* ports Vite. Sans ça, accéder via un vhost bloque les ressources Vite.
|
|
606
|
+
* Tokens : `'unsafe-eval'` (React Fast Refresh — le nonce ne couvre PAS l'eval)
|
|
607
|
+
* et `'unsafe-inline'` style (styles injectés par Vite). PAS de `'unsafe-inline'`
|
|
608
|
+
* script : le preamble inline est NONCÉ. Mergé par le firewall (jamais en prod :
|
|
609
|
+
* `startDev` ne tourne pas en production → CSP strict same-origin).
|
|
610
|
+
*/
|
|
611
|
+
#viteCspFragment() {
|
|
612
|
+
const scheme = this.cfg.https ? "https" : "http";
|
|
613
|
+
const wsScheme = this.cfg.https ? "wss" : "ws";
|
|
614
|
+
const ports = this.cspPorts();
|
|
615
|
+
const hosts = /* @__PURE__ */ new Set(["127.0.0.1", "localhost"]);
|
|
616
|
+
if (this.kernel?.domain) hosts.add(this.kernel.domain);
|
|
617
|
+
const th = (this.container?.get?.("HttpKernel"))?.trustedHosts;
|
|
618
|
+
if (typeof th === "string") hosts.add(th);
|
|
619
|
+
else if (Array.isArray(th)) for (const h of th) hosts.add(h);
|
|
620
|
+
const httpSrc = [];
|
|
621
|
+
const wsSrc = [];
|
|
622
|
+
for (const h of hosts) for (const p of ports) {
|
|
623
|
+
httpSrc.push(`${scheme}://${h}:${p}`);
|
|
624
|
+
wsSrc.push(`${wsScheme}://${h}:${p}`);
|
|
625
|
+
}
|
|
626
|
+
for (const s of this.supervisors.values()) {
|
|
627
|
+
const o = s.status().origin;
|
|
628
|
+
if (!o) continue;
|
|
629
|
+
if (!httpSrc.includes(o)) httpSrc.push(o);
|
|
630
|
+
const w = o.replace(/^http/, "ws");
|
|
631
|
+
if (!wsSrc.includes(w)) wsSrc.push(w);
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
"script-src": [
|
|
635
|
+
"'self'",
|
|
636
|
+
"'unsafe-eval'",
|
|
637
|
+
...httpSrc
|
|
638
|
+
],
|
|
639
|
+
"style-src": [
|
|
640
|
+
"'self'",
|
|
641
|
+
"'unsafe-inline'",
|
|
642
|
+
...httpSrc
|
|
643
|
+
],
|
|
644
|
+
"worker-src": ["'self'", "blob:"],
|
|
645
|
+
"img-src": [
|
|
646
|
+
"'self'",
|
|
647
|
+
"data:",
|
|
648
|
+
"blob:",
|
|
649
|
+
"https://www.gravatar.com",
|
|
650
|
+
"https://*.googleusercontent.com",
|
|
651
|
+
"https://avatars.githubusercontent.com",
|
|
652
|
+
...httpSrc
|
|
653
|
+
],
|
|
654
|
+
"font-src": [
|
|
655
|
+
"'self'",
|
|
656
|
+
"data:",
|
|
657
|
+
...httpSrc
|
|
658
|
+
],
|
|
659
|
+
"connect-src": [
|
|
660
|
+
"'self'",
|
|
661
|
+
"blob:",
|
|
662
|
+
"data:",
|
|
663
|
+
...httpSrc,
|
|
664
|
+
...wsSrc
|
|
665
|
+
]
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Ports Vite à déclarer au CSP, famille par famille : le BLOC entier tant que
|
|
670
|
+
* l'instance ne sert pas, son port RÉEL dès qu'elle sert.
|
|
671
|
+
*
|
|
672
|
+
* Pourquoi le bloc avant : le CSP part AVEC la page et ne se renégocie jamais.
|
|
673
|
+
* Une page servie avant que Vite ait résolu son port doit déjà porter le port
|
|
674
|
+
* qu'il prendra, sinon son socket de rechargement à chaud est refusé pour
|
|
675
|
+
* toute la durée de la page.
|
|
676
|
+
*
|
|
677
|
+
* Pourquoi le port seul après : la plage est le prix d'une incertitude, elle
|
|
678
|
+
* ne doit pas lui survivre. Mesuré sur ce dépôt (3 familles × 4 hôtes de
|
|
679
|
+
* confiance), garder les blocs porte l'en-tête CSP à ~7,9 Ko sur CHAQUE
|
|
680
|
+
* réponse, contre ~2,3 Ko une fois les ports connus — au bord des 8 Ko que
|
|
681
|
+
* refusent beaucoup de relais.
|
|
682
|
+
*
|
|
683
|
+
* Développement seulement : `startDev` ne tourne pas en production. La
|
|
684
|
+
* garantie reste une liste d'origines nommées — jamais un `ws:` sans hôte,
|
|
685
|
+
* qui la supprimerait au lieu de corriger le symptôme.
|
|
686
|
+
*
|
|
687
|
+
* @returns ports en chaîne, dédupliqués ; repli `devPort` si rien n'est connu.
|
|
688
|
+
*/
|
|
689
|
+
cspPorts() {
|
|
690
|
+
const ports = /* @__PURE__ */ new Set();
|
|
691
|
+
for (const [family, block] of this.plannedPortBlocks ?? []) {
|
|
692
|
+
const st = this.supervisors.get(family)?.status();
|
|
693
|
+
if ((st?.state === "ready" || st?.state === "compiling") && st?.port) ports.add(String(st.port));
|
|
694
|
+
else for (const p of block) ports.add(String(p));
|
|
695
|
+
}
|
|
696
|
+
for (const [family, sup] of this.supervisors) {
|
|
697
|
+
if (this.plannedPortBlocks?.has(family)) continue;
|
|
698
|
+
const st = sup.status();
|
|
699
|
+
if (st.port) ports.add(String(st.port));
|
|
700
|
+
}
|
|
701
|
+
if (ports.size === 0) ports.add(String(this.cfg.devPort));
|
|
702
|
+
return ports;
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
FrontendService = __decorate([injectable(), __decorateMetadata("design:paramtypes", [typeof Module === "undefined" ? Object : Module])], FrontendService);
|
|
706
|
+
var FrontendService_default = FrontendService;
|
|
707
|
+
//#endregion
|
|
708
|
+
export { FrontendService_default as default };
|