@nodefony/frontend 10.0.0-alpha.2 → 10.0.0-alpha.4

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.
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorate.js
1
+ //#region \0@oxc-project+runtime@0.149.0/helpers/esm/decorate.js
2
2
  function __decorate(decorators, target, key, desc) {
3
3
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
4
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1,4 +1,4 @@
1
- //#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorateMetadata.js
1
+ //#region \0@oxc-project+runtime@0.149.0/helpers/esm/decorateMetadata.js
2
2
  function __decorateMetadata(k, v) {
3
3
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
4
4
  }
package/dist/index.js CHANGED
@@ -10,8 +10,8 @@ import { ViteBuilder } from "./nodefony/src/builders/ViteBuilder.js";
10
10
  import { ViteConfigGenerator } from "./nodefony/service/ViteConfigGenerator.js";
11
11
  import { ViteProcessSupervisor } from "./nodefony/service/ViteProcessSupervisor.js";
12
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";
13
+ import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorateMetadata.js";
14
+ import __decorate from "./_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorate.js";
15
15
  import FrontendService_default from "./nodefony/service/FrontendService.js";
16
16
  import { buildFrontendStatus, createFrontendAdminApi } from "./nodefony/src/FrontendAdminApi.js";
17
17
  import FrontendBuild from "./nodefony/command/frontend-build.js";
@@ -42,7 +42,7 @@ const resilienceSchema = z.strictObject({
42
42
  const frontendConfigSchema = z.strictObject({
43
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
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)."),
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. Codespaces/Gitpod sont détectés automatiquement et fournissent alors l'origine par défaut — MAIS un client arrivé par la boucle locale (tunnel de port : VS Code Desktop le fait par défaut) reste servi en local, parce que l'origine publique d'une plateforme exige sa session, qu'une intégration continue ou une sonde n'a pas. Une origine écrite ICI, en revanche, gagne sur tout — c'est un réglage, pas une déduction. 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
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
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
48
  defaultRoot: z.string().default("./frontend").describe("Racine front par défaut (contient `index.html`) côté module."),
@@ -1,12 +1,12 @@
1
1
  import config from "../config/config.js";
2
2
  import { FrontendNoEntriesError, FrontendSupervisorStartError } from "../src/errors/FrontendError.js";
3
3
  import { ViteBuilder } from "../src/builders/ViteBuilder.js";
4
- import { allowedHostPatternForTemplate, detectRemoteDev, isValidOriginTemplate, viteAllowedHostFromPattern } from "../src/remoteDev.js";
4
+ import { allowedHostPatternForTemplate, detectRemoteDev, isLoopbackHostname, isValidOriginTemplate, viteAllowedHostFromPattern } from "../src/remoteDev.js";
5
5
  import { ViteProcessSupervisor } from "./ViteProcessSupervisor.js";
6
6
  import { TemplateHelper } from "../src/template/TemplateHelper.js";
7
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";
8
+ import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorateMetadata.js";
9
+ import __decorate from "../../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorate.js";
10
10
  import { Module, Service, extend, injectable, stripTrailingSlashes } from "nodefony";
11
11
  import path from "node:path";
12
12
  import fs from "node:fs";
@@ -47,12 +47,23 @@ let FrontendService = class FrontendService extends Service {
47
47
  /** Helper prod unique (lit les manifests) — `null` tant qu'on n'est pas en prod. */
48
48
  prodHelper = null;
49
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).
50
+ * QUI a épinglé l'origine publique la question n'est pas « est-elle
51
+ * épinglée » mais « par quoi », car les deux sources n'ont pas la même
52
+ * autorité :
53
+ *
54
+ * - `"config"` — `frontend.publicOrigin` : une décision ÉCRITE par l'auteur.
55
+ * Elle gagne sur tout, y compris sur le `Host` reçu : c'est le sens même
56
+ * d'un réglage explicite, et le seul moyen de servir derrière un frontal
57
+ * qui réécrit l'origine.
58
+ * - `"platform"` — Codespaces/Gitpod déduits de l'environnement : une
59
+ * DÉDUCTION, faite une fois au démarrage, sur une machine qui reçoit
60
+ * simultanément des clients arrivés par des chemins différents. Elle ne
61
+ * peut donc pas prévaloir sur un fait constaté à la requête — un client
62
+ * venu de la boucle locale se sert en local (cf `derivableHost`).
63
+ * - `null` — rien d'épinglé : chaque page annonce l'origine par laquelle son
64
+ * client est arrivé.
54
65
  */
55
- originPinned = false;
66
+ originPinnedBy = null;
56
67
  /**
57
68
  * Ports que l'instance Vite de chaque famille PEUT prendre pour ce démarrage
58
69
  * (bloc de la famille, port-retry compris) — `null` tant que `startDev` n'a
@@ -195,8 +206,9 @@ let FrontendService = class FrontendService extends Service {
195
206
  if (this.supervisors.size > 0 && [...this.supervisors.values()].some((s) => s.status().state === "ready")) return;
196
207
  const backendOrigin = `${this.cfg.backendProtocol}://${this.cfg.backendHost}:${this.resolveBackendPort()}`;
197
208
  const https = this.resolveHttps();
198
- const publicOriginTemplate = this.resolvePublicOriginTemplate();
199
- this.originPinned = publicOriginTemplate !== void 0;
209
+ const pinned = this.resolvePublicOrigin();
210
+ const publicOriginTemplate = pinned?.template;
211
+ this.originPinnedBy = pinned?.source ?? null;
200
212
  const allowedHosts = this.viteAllowedHosts(publicOriginTemplate);
201
213
  const nodeEnv = this.kernel?.environment;
202
214
  const extraEnv = this.cfg.viteEnv ?? {};
@@ -279,28 +291,43 @@ let FrontendService = class FrontendService extends Service {
279
291
  };
280
292
  }
281
293
  /**
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.
294
+ * Template d'origine publique Vite (P14.17), AVEC sa provenance. Priorité :
295
+ * `frontend.publicOrigin` (config, validée — invalide = ERROR + ignorée,
296
+ * jamais un boot cassé) puis détection de plateforme (Codespaces/Gitpod —
297
+ * variables documentées de la plateforme, qu'on lit sans les posséder).
298
+ * `null` = dérivation locale.
299
+ *
300
+ * La provenance est rendue avec le template parce qu'elle CHANGE la suite :
301
+ * une config écrite est un ordre, une plateforme déduite est une supposition
302
+ * qui cède devant le `Host` reçu quand celui-ci désigne la boucle locale
303
+ * (cf `originPinnedBy`). Les confondre servait l'origine publique à un client
304
+ * venu d'un tunnel local, qui n'a pas la session de la plateforme.
305
+ *
286
306
  * Chaque adaptation est JOURNALISÉE : on doit pouvoir lire dans le boot
287
307
  * pourquoi les `<script>` pointent où ils pointent.
288
308
  */
289
- resolvePublicOriginTemplate() {
309
+ resolvePublicOrigin() {
290
310
  const cfgOrigin = this.cfg.publicOrigin;
291
311
  if (cfgOrigin) {
292
312
  if (!isValidOriginTemplate(cfgOrigin)) {
293
313
  this.log(`frontend.publicOrigin invalide (« ${cfgOrigin} ») — attendu scheme://host[:port|:{port}] sans chemin ; origine locale utilisée`, "ERROR");
294
- return;
314
+ return null;
295
315
  }
296
316
  this.log(`origine publique Vite (config) : ${cfgOrigin}`, "INFO");
297
- return cfgOrigin;
317
+ return {
318
+ template: cfgOrigin,
319
+ source: "config"
320
+ };
298
321
  }
299
322
  const detected = detectRemoteDev(process.env);
300
323
  if (detected) {
301
- this.log(`dev déporté détecté (${detected.provider}) — origine publique Vite : ` + detected.originTemplate, "INFO");
302
- return detected.originTemplate;
324
+ this.log(`dev déporté détecté (${detected.provider}) — origine publique Vite : ${detected.originTemplate} (un client arrivé par la boucle locale reste servi en local)`, "INFO");
325
+ return {
326
+ template: detected.originTemplate,
327
+ source: "platform"
328
+ };
303
329
  }
330
+ return null;
304
331
  }
305
332
  /**
306
333
  * `server.allowedHosts` pour Vite. Vite accepte d'office IP et `localhost` ;
@@ -581,7 +608,9 @@ let FrontendService = class FrontendService extends Service {
581
608
  * résolue au démarrage (comportement d'avant la dérivation).
582
609
  */
583
610
  derivableHost(requestHost) {
584
- if (!requestHost || this.originPinned) return void 0;
611
+ if (!requestHost) return void 0;
612
+ if (this.originPinnedBy === "config") return void 0;
613
+ if (this.originPinnedBy === "platform" && !isLoopbackHostname(requestHost)) return;
585
614
  const httpKernel = this.container?.get?.("HttpKernel");
586
615
  if (!httpKernel || httpKernel.trustedHosts === true) return void 0;
587
616
  return httpKernel.isTrustedHostname?.(requestHost) === true ? requestHost : void 0;
@@ -89,17 +89,16 @@ ${fsAllowLines}
89
89
  },
90
90
  `;
91
91
  const allowedHostsLine = opts.allowedHosts === true ? ` allowedHosts: true,\n` : opts.allowedHosts && opts.allowedHosts.length > 0 ? ` allowedHosts: ${JSON.stringify(opts.allowedHosts)},\n` : "";
92
- const hmrLine = opts.hmr ? ` hmr: { host: ${JSON.stringify(opts.hmr.host)}, clientPort: ${opts.hmr.clientPort}, protocol: ${JSON.stringify(opts.hmr.protocol)} },\n` : "";
93
92
  const serverBlock = proxyPaths.size > 0 ? ` server: {
94
93
  strictPort: ${strictPort},
95
94
  cors: true,
96
- ${allowedHostsLine}${hmrLine}${httpsLines}${fsBlock} proxy: {
95
+ ${allowedHostsLine}${httpsLines}${fsBlock} proxy: {
97
96
  ${proxyLines}
98
97
  },
99
98
  },` : ` server: {
100
99
  strictPort: ${strictPort},
101
100
  cors: true,
102
- ${allowedHostsLine}${hmrLine}${httpsLines}${fsBlock} },`;
101
+ ${allowedHostsLine}${httpsLines}${fsBlock} },`;
103
102
  const baseLine = useViteOrigin ? ` base: ${JSON.stringify(opts.viteOrigin + "/")},\n` : "";
104
103
  const dedupe = [];
105
104
  if (usedTypes.has("react19")) dedupe.push("react", "react-dom");
@@ -240,8 +240,7 @@ var ViteProcessSupervisor = class {
240
240
  backendOrigin: this.opts.backendOrigin,
241
241
  viteOrigin,
242
242
  https: this.opts.https,
243
- allowedHosts: this.opts.allowedHosts,
244
- hmr: resolved?.hmr
243
+ allowedHosts: this.opts.allowedHosts
245
244
  });
246
245
  writeFileSync(this.configFilePath, content, "utf8");
247
246
  this.opts.logger.debug?.(`vite config written: ${this.configFilePath}`);
@@ -6,7 +6,7 @@
6
6
  * NAVIGATEUR utilise peut être toute autre chose — un forwarder TLS (Codespaces,
7
7
  * Gitpod), une passerelle de conteneur (`host.docker.internal`), un port remappé.
8
8
  * Ce module dissocie les deux : il produit l'origine publique (assets, `base`
9
- * Vite, WebSocket HMR) à partir d'un TEMPLATE (`{port}` substitué au port réel
9
+ * Vite) à partir d'un TEMPLATE (`{port}` substitué au port réel
10
10
  * du spawn) — explicite (`frontend.publicOrigin`) ou détecté depuis
11
11
  * l'environnement de la plateforme.
12
12
  *
@@ -15,7 +15,7 @@
15
15
  *
16
16
  * Formats VÉRIFIÉS (docs officielles + source Vite 8) :
17
17
  * - Codespaces : `https://${CODESPACE_NAME}-${port}.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}`
18
- * (TLS terminé par le forwarder WS HMR en `wss` sur 443).
18
+ * (TLS terminé par le forwarder ; le socket HMR s'en déduit côté client).
19
19
  * - Gitpod classic : `https://${port}-<hôte de GITPOD_WORKSPACE_URL>`.
20
20
  * - Vite `server.allowedHosts` : IP et `localhost`/`*.localhost` TOUJOURS
21
21
  * acceptés ; un préfixe `.` = le domaine ET tous ses sous-domaines.
@@ -25,6 +25,12 @@
25
25
  /** Placeholder substitué par le port réel du spawn dans un template d'origine. */
26
26
  const PORT_PLACEHOLDER = "{port}";
27
27
  /**
28
+ * `127.0.0.0/8` avec des octets BORNÉS (0-255). Pas de quantificateur imbriqué
29
+ * ni d'alternance qui se chevauche : linéaire sur toute entrée, y compris
30
+ * forgée (le `Host` est une donnée cliente).
31
+ */
32
+ const LOOPBACK_V4_RE = /^127\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
33
+ /**
28
34
  * Template d'origine publique : `scheme://hostTemplate[:portTemplate]`, sans
29
35
  * chemin. `{port}` peut apparaître dans l'hôte (Codespaces/Gitpod : port encodé
30
36
  * dans le sous-domaine) OU en position de port (`host:{port}`).
@@ -41,6 +47,34 @@ const ORIGIN_TEMPLATE_RE = /^(https?):\/\/([^/:\s]+)(?::(\d+|\{port\}))?$/;
41
47
  function browserReachableHost(listenHost) {
42
48
  return listenHost === "0.0.0.0" || listenHost === "::" || listenHost === "[::]" || listenHost === "" ? "127.0.0.1" : listenHost;
43
49
  }
50
+ /**
51
+ * Le client est-il arrivé par la BOUCLE LOCALE de la machine qui sert ?
52
+ *
53
+ * Ce n'est pas une commodité : c'est un fait vérifiable qui prime sur toute
54
+ * déduction faite au démarrage. Une plateforme de dev déporté se détecte par
55
+ * une variable d'environnement — donc UNE FOIS, au lancement du serveur — alors
56
+ * qu'un même serveur reçoit simultanément des clients arrivés par des chemins
57
+ * différents : l'origine publique de la plateforme, et un tunnel local (VS Code
58
+ * Desktop redirige les ports d'un Codespace sur `localhost`, c'est sa
59
+ * configuration par défaut).
60
+ *
61
+ * Servir l'origine publique à un client venu du tunnel a un coût réel : cette
62
+ * origine exige la session de la plateforme. Un navigateur humain la porte et
63
+ * ne voit rien ; une intégration continue, une sonde ou un agent ne l'ont pas,
64
+ * se font refuser, et obtiennent une page blanche que rien n'explique.
65
+ *
66
+ * La liste est FERMÉE, et c'est ce qui la rend sûre : le `Host` est une donnée
67
+ * cliente, et seuls ces noms désignent la machine locale de façon non
68
+ * ambiguë. `0.0.0.0`/`::` en sont exclus — ce sont des adresses d'écoute, pas
69
+ * des destinations (cf `browserReachableHost`).
70
+ *
71
+ * @param hostname - nom d'hôte NU, sans port (`[::1]` pour l'IPv6 canonique).
72
+ * @returns `true` si ce nom désigne la boucle locale.
73
+ */
74
+ function isLoopbackHostname(hostname) {
75
+ if (hostname === "localhost" || hostname === "::1" || hostname === "[::1]") return true;
76
+ return LOOPBACK_V4_RE.test(hostname);
77
+ }
44
78
  /** Un template d'origine est-il syntaxiquement valide ? (autorité unique) */
45
79
  function isValidOriginTemplate(template) {
46
80
  return ORIGIN_TEMPLATE_RE.test(template);
@@ -79,9 +113,10 @@ function originWithHostname(origin, hostname) {
79
113
  /**
80
114
  * Résout un template d'origine contre le port RÉEL du spawn. Pure.
81
115
  *
82
- * @returns origine + config HMR cliente, ou `null` si le template est invalide
116
+ * @returns l'origine publique, ou `null` si le template est invalide
83
117
  * (l'appelant retombe sur la dérivation locale en l'ANNONÇANT — jamais en
84
- * silence).
118
+ * silence). Aucune config HMR n'est rendue : le socket suit l'origine par
119
+ * laquelle le client Vite a été chargé, il n'a rien à recevoir.
85
120
  */
86
121
  function resolveOriginTemplate(template, port) {
87
122
  const m = ORIGIN_TEMPLATE_RE.exec(template);
@@ -89,15 +124,7 @@ function resolveOriginTemplate(template, port) {
89
124
  const [, scheme, hostTemplate, portTemplate] = m;
90
125
  const host = hostTemplate.replaceAll(PORT_PLACEHOLDER, String(port));
91
126
  const explicitPort = portTemplate ? parseInt(portTemplate.replaceAll(PORT_PLACEHOLDER, String(port)), 10) : void 0;
92
- const secure = scheme === "https";
93
- return {
94
- origin: `${scheme}://${host}${explicitPort !== void 0 ? `:${explicitPort}` : ""}`,
95
- hmr: {
96
- host,
97
- clientPort: explicitPort ?? (secure ? 443 : 80),
98
- protocol: secure ? "wss" : "ws"
99
- }
100
- };
127
+ return { origin: `${scheme}://${host}${explicitPort !== void 0 ? `:${explicitPort}` : ""}` };
101
128
  }
102
129
  /**
103
130
  * Motif `server.allowedHosts` couvrant TOUTES les origines qu'un template peut
@@ -154,4 +181,4 @@ function detectRemoteDev(env) {
154
181
  return null;
155
182
  }
156
183
  //#endregion
157
- export { PORT_PLACEHOLDER, allowedHostPatternForTemplate, browserReachableHost, detectRemoteDev, isValidOriginTemplate, originWithHostname, resolveOriginTemplate, viteAllowedHostFromPattern };
184
+ export { PORT_PLACEHOLDER, allowedHostPatternForTemplate, browserReachableHost, detectRemoteDev, isLoopbackHostname, isValidOriginTemplate, originWithHostname, resolveOriginTemplate, viteAllowedHostFromPattern };
@@ -1,4 +1,4 @@
1
- import { originWithHostname } from "../remoteDev.js";
1
+ import { isLoopbackHostname, originWithHostname } from "../remoteDev.js";
2
2
  import { createRequire } from "node:module";
3
3
  import { PLATFORM_EVENTS, escapeRegExp } from "nodefony";
4
4
  import path from "node:path";
@@ -120,7 +120,7 @@ ${tags}
120
120
  const entry = status.entries.find((e) => e.entryName === entryName);
121
121
  if (!entry) return `<!-- @nodefony/frontend: unknown entry "${entryName}" -->`;
122
122
  const resolvedOrigin = status.origin ?? `${status.https ? "https" : "http"}://${status.host}:${status.port}`;
123
- const baseUrl = requestHost ? originWithHostname(resolvedOrigin, requestHost) ?? resolvedOrigin : resolvedOrigin;
123
+ const baseUrl = !requestHost ? resolvedOrigin : isLoopbackHostname(requestHost) ? `${status.https ? "https" : "http"}://${requestHost}:${status.port}` : originWithHostname(resolvedOrigin, requestHost) ?? resolvedOrigin;
124
124
  const absEntryPath = path.resolve(entry.root, entry.entryFile).replace(/\\/g, "/");
125
125
  const entryUrl = `${baseUrl}${absEntryPath.startsWith("/") ? `/@fs${absEntryPath}` : `/@fs/${absEntryPath}`}`;
126
126
  const tags = [];
@@ -1,13 +1,13 @@
1
1
  import { Kernel, Module } from "nodefony";
2
2
  import { type IFrontendConfigInput } from "./nodefony/config/defineModuleConfig.js";
3
- import type { FrontendConfig } from "./nodefony/config/config.js";
3
+ import type { IFrontendConfig } from "./nodefony/config/config.js";
4
4
  import FrontendService from "./nodefony/service/FrontendService.js";
5
5
  declare module "nodefony" {
6
6
  interface NodefonyModuleConfig {
7
7
  "@nodefony/frontend": IFrontendConfigInput;
8
8
  }
9
9
  }
10
- declare class Frontend extends Module<FrontendConfig> {
10
+ declare class Frontend extends Module<IFrontendConfig> {
11
11
  constructor(kernel: Kernel);
12
12
  /** JSON Schema de la config frontend → data plane admin (config riche Studio). */
13
13
  configSchema(): unknown;
@@ -48,4 +48,4 @@ export type { IFrontBuilder, IFrontendModuleDeclaration, IResolvedFrontendEntry,
48
48
  export type { IViteSupervisor, IViteSupervisorStatus, ViteSupervisorState, } from "./nodefony/interfaces/IViteSupervisor.js";
49
49
  export type { IFrontendService } from "./nodefony/interfaces/IFrontendService.js";
50
50
  export { defineFrontendConfig, frontendConfigJsonSchema, type IFrontendConfigInput, } from "./nodefony/config/defineModuleConfig.js";
51
- export { frontendConfigSchema, type FrontendConfig, } from "./nodefony/config/config.js";
51
+ export { frontendConfigSchema, type IFrontendConfig, } from "./nodefony/config/config.js";
@@ -29,10 +29,10 @@ export declare const frontendConfigSchema: z.ZodObject<{
29
29
  }, z.core.$strict>>;
30
30
  }, z.core.$strict>;
31
31
  /** Type de sortie (config normalisée + défauts appliqués). */
32
- export type FrontendConfig = z.infer<typeof frontendConfigSchema>;
32
+ export type IFrontendConfig = z.infer<typeof frontendConfigSchema>;
33
33
  /**
34
34
  * Défauts du module, matérialisés depuis le schéma (source unique). Toujours
35
35
  * valides par construction ; passés au `super(..., config)` du Module class.
36
36
  */
37
- declare const config: FrontendConfig;
37
+ declare const config: IFrontendConfig;
38
38
  export default config;
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { frontendConfigSchema, type FrontendConfig } from "./config.js";
2
+ import { frontendConfigSchema, type IFrontendConfig } from "./config.js";
3
3
  /**
4
4
  * Builder type-safe de la configuration de `@nodefony/frontend`.
5
5
  *
@@ -18,7 +18,7 @@ import { frontendConfigSchema, type FrontendConfig } from "./config.js";
18
18
  * @returns config gelée prête pour `FrontendService`.
19
19
  * @throws ZodError si invalide.
20
20
  */
21
- export declare function defineFrontendConfig(config?: IFrontendConfigInput): FrontendConfig;
21
+ export declare function defineFrontendConfig(config?: IFrontendConfigInput): IFrontendConfig;
22
22
  /**
23
23
  * JSON Schema introspectable de la config frontend — destiné au panneau de config
24
24
  * Studio (`/nodefony/config`).
@@ -26,4 +26,3 @@ export declare function defineFrontendConfig(config?: IFrontendConfigInput): Fro
26
26
  export declare function frontendConfigJsonSchema(): unknown;
27
27
  /** Entrée du builder (champs avec défaut optionnels). */
28
28
  export type IFrontendConfigInput = z.input<typeof frontendConfigSchema>;
29
- export type { FrontendConfig };
@@ -29,12 +29,23 @@ declare class FrontendService extends Service implements IFrontendService {
29
29
  /** Helper prod unique (lit les manifests) — `null` tant qu'on n'est pas en prod. */
30
30
  private prodHelper;
31
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;
32
+ * QUI a épinglé l'origine publique la question n'est pas « est-elle
33
+ * épinglée » mais « par quoi », car les deux sources n'ont pas la même
34
+ * autorité :
35
+ *
36
+ * - `"config"` — `frontend.publicOrigin` : une décision ÉCRITE par l'auteur.
37
+ * Elle gagne sur tout, y compris sur le `Host` reçu : c'est le sens même
38
+ * d'un réglage explicite, et le seul moyen de servir derrière un frontal
39
+ * qui réécrit l'origine.
40
+ * - `"platform"` — Codespaces/Gitpod déduits de l'environnement : une
41
+ * DÉDUCTION, faite une fois au démarrage, sur une machine qui reçoit
42
+ * simultanément des clients arrivés par des chemins différents. Elle ne
43
+ * peut donc pas prévaloir sur un fait constaté à la requête — un client
44
+ * venu de la boucle locale se sert en local (cf `derivableHost`).
45
+ * - `null` — rien d'épinglé : chaque page annonce l'origine par laquelle son
46
+ * client est arrivé.
47
+ */
48
+ private originPinnedBy;
38
49
  /**
39
50
  * Ports que l'instance Vite de chaque famille PEUT prendre pour ce démarrage
40
51
  * (bloc de la famille, port-retry compris) — `null` tant que `startDev` n'a
@@ -100,14 +111,22 @@ declare class FrontendService extends Service implements IFrontendService {
100
111
  */
101
112
  private resolveHttps;
102
113
  /**
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.
114
+ * Template d'origine publique Vite (P14.17), AVEC sa provenance. Priorité :
115
+ * `frontend.publicOrigin` (config, validée — invalide = ERROR + ignorée,
116
+ * jamais un boot cassé) puis détection de plateforme (Codespaces/Gitpod —
117
+ * variables documentées de la plateforme, qu'on lit sans les posséder).
118
+ * `null` = dérivation locale.
119
+ *
120
+ * La provenance est rendue avec le template parce qu'elle CHANGE la suite :
121
+ * une config écrite est un ordre, une plateforme déduite est une supposition
122
+ * qui cède devant le `Host` reçu quand celui-ci désigne la boucle locale
123
+ * (cf `originPinnedBy`). Les confondre servait l'origine publique à un client
124
+ * venu d'un tunnel local, qui n'a pas la session de la plateforme.
125
+ *
107
126
  * Chaque adaptation est JOURNALISÉE : on doit pouvoir lire dans le boot
108
127
  * pourquoi les `<script>` pointent où ils pointent.
109
128
  */
110
- private resolvePublicOriginTemplate;
129
+ private resolvePublicOrigin;
111
130
  /**
112
131
  * `server.allowedHosts` pour Vite. Vite accepte d'office IP et `localhost` ;
113
132
  * cette liste ne porte que les NOMS. Source des noms légitimes = la MÊME que
@@ -39,17 +39,6 @@ export interface ViteConfigGeneratorOptions {
39
39
  * sert que les NOMS (vhosts, `host.docker.internal`, forwarders).
40
40
  */
41
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
42
  }
54
43
  export declare class ViteConfigGenerator {
55
44
  /**
@@ -5,7 +5,7 @@
5
5
  * NAVIGATEUR utilise peut être toute autre chose — un forwarder TLS (Codespaces,
6
6
  * Gitpod), une passerelle de conteneur (`host.docker.internal`), un port remappé.
7
7
  * Ce module dissocie les deux : il produit l'origine publique (assets, `base`
8
- * Vite, WebSocket HMR) à partir d'un TEMPLATE (`{port}` substitué au port réel
8
+ * Vite) à partir d'un TEMPLATE (`{port}` substitué au port réel
9
9
  * du spawn) — explicite (`frontend.publicOrigin`) ou détecté depuis
10
10
  * l'environnement de la plateforme.
11
11
  *
@@ -14,7 +14,7 @@
14
14
  *
15
15
  * Formats VÉRIFIÉS (docs officielles + source Vite 8) :
16
16
  * - Codespaces : `https://${CODESPACE_NAME}-${port}.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}`
17
- * (TLS terminé par le forwarder WS HMR en `wss` sur 443).
17
+ * (TLS terminé par le forwarder ; le socket HMR s'en déduit côté client).
18
18
  * - Gitpod classic : `https://${port}-<hôte de GITPOD_WORKSPACE_URL>`.
19
19
  * - Vite `server.allowedHosts` : IP et `localhost`/`*.localhost` TOUJOURS
20
20
  * acceptés ; un préfixe `.` = le domaine ET tous ses sous-domaines.
@@ -27,15 +27,6 @@ export declare const PORT_PLACEHOLDER = "{port}";
27
27
  export interface IResolvedPublicOrigin {
28
28
  /** Origine que le navigateur utilise — verbatim dans les `<script>` et le `base` Vite. */
29
29
  readonly origin: string;
30
- /**
31
- * Config `server.hmr` cliente : le WS HMR doit suivre le MÊME chemin que les
32
- * assets. Port implicite → 443/80 selon le scheme (cas forwarder TLS).
33
- */
34
- readonly hmr: {
35
- readonly host: string;
36
- readonly clientPort: number;
37
- readonly protocol: "ws" | "wss";
38
- };
39
30
  }
40
31
  /** Environnement de dev déporté détecté depuis les variables de la plateforme. */
41
32
  export interface IRemoteDevDetection {
@@ -51,6 +42,31 @@ export interface IRemoteDevDetection {
51
42
  * sous Windows, une CONNEXION vers `0.0.0.0` échoue aussi (health check).
52
43
  */
53
44
  export declare function browserReachableHost(listenHost: string): string;
45
+ /**
46
+ * Le client est-il arrivé par la BOUCLE LOCALE de la machine qui sert ?
47
+ *
48
+ * Ce n'est pas une commodité : c'est un fait vérifiable qui prime sur toute
49
+ * déduction faite au démarrage. Une plateforme de dev déporté se détecte par
50
+ * une variable d'environnement — donc UNE FOIS, au lancement du serveur — alors
51
+ * qu'un même serveur reçoit simultanément des clients arrivés par des chemins
52
+ * différents : l'origine publique de la plateforme, et un tunnel local (VS Code
53
+ * Desktop redirige les ports d'un Codespace sur `localhost`, c'est sa
54
+ * configuration par défaut).
55
+ *
56
+ * Servir l'origine publique à un client venu du tunnel a un coût réel : cette
57
+ * origine exige la session de la plateforme. Un navigateur humain la porte et
58
+ * ne voit rien ; une intégration continue, une sonde ou un agent ne l'ont pas,
59
+ * se font refuser, et obtiennent une page blanche que rien n'explique.
60
+ *
61
+ * La liste est FERMÉE, et c'est ce qui la rend sûre : le `Host` est une donnée
62
+ * cliente, et seuls ces noms désignent la machine locale de façon non
63
+ * ambiguë. `0.0.0.0`/`::` en sont exclus — ce sont des adresses d'écoute, pas
64
+ * des destinations (cf `browserReachableHost`).
65
+ *
66
+ * @param hostname - nom d'hôte NU, sans port (`[::1]` pour l'IPv6 canonique).
67
+ * @returns `true` si ce nom désigne la boucle locale.
68
+ */
69
+ export declare function isLoopbackHostname(hostname: string): boolean;
54
70
  /** Un template d'origine est-il syntaxiquement valide ? (autorité unique) */
55
71
  export declare function isValidOriginTemplate(template: string): boolean;
56
72
  /**
@@ -76,9 +92,10 @@ export declare function originWithHostname(origin: string, hostname: string): st
76
92
  /**
77
93
  * Résout un template d'origine contre le port RÉEL du spawn. Pure.
78
94
  *
79
- * @returns origine + config HMR cliente, ou `null` si le template est invalide
95
+ * @returns l'origine publique, ou `null` si le template est invalide
80
96
  * (l'appelant retombe sur la dérivation locale en l'ANNONÇANT — jamais en
81
- * silence).
97
+ * silence). Aucune config HMR n'est rendue : le socket suit l'origine par
98
+ * laquelle le client Vite a été chargé, il n'a rien à recevoir.
82
99
  */
83
100
  export declare function resolveOriginTemplate(template: string, port: number): IResolvedPublicOrigin | null;
84
101
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodefony/frontend",
3
- "version": "10.0.0-alpha.2",
3
+ "version": "10.0.0-alpha.4",
4
4
  "description": "Construction et rechargement à chaud des frontends de chaque module Nodefony — Vite intégré, multi-framework (React, Vue, Angular, Svelte)",
5
5
  "author": "Christophe CAMENSULI <ccamensuli@gmail.com>",
6
6
  "type": "module",
@@ -36,19 +36,19 @@
36
36
  "esm"
37
37
  ],
38
38
  "peerDependencies": {
39
- "@nodefony/framework": "^10.0.0-alpha.2",
40
- "@nodefony/http": "^10.0.0-alpha.2",
41
- "nodefony": "^10.0.0-alpha.2",
39
+ "@nodefony/framework": "^10.0.0-alpha.4",
40
+ "@nodefony/http": "^10.0.0-alpha.4",
41
+ "nodefony": "^10.0.0-alpha.4",
42
42
  "zod": "^4.4.3"
43
43
  },
44
44
  "devDependencies": {
45
- "@nodefony/framework": "^10.0.0-alpha.2",
46
- "@nodefony/http": "^10.0.0-alpha.2",
45
+ "@nodefony/framework": "^10.0.0-alpha.4",
46
+ "@nodefony/http": "^10.0.0-alpha.4",
47
47
  "@types/chai": "5.2.3",
48
48
  "@types/node": "26.4.1",
49
49
  "@vitest/coverage-v8": "5.0.0",
50
50
  "chai": "6.2.2",
51
- "nodefony": "^10.0.0-alpha.2",
51
+ "nodefony": "^10.0.0-alpha.4",
52
52
  "rimraf": "6.1.3",
53
53
  "vite": "8.2.2",
54
54
  "vitest": "5.0.0"