@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.
- package/dist/_virtual/{_@oxc-project_runtime@0.148.0 → _@oxc-project_runtime@0.149.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.148.0 → _@oxc-project_runtime@0.149.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +2 -2
- package/dist/nodefony/config/config.js +1 -1
- package/dist/nodefony/service/FrontendService.js +49 -20
- package/dist/nodefony/service/ViteConfigGenerator.js +2 -3
- package/dist/nodefony/service/ViteProcessSupervisor.js +1 -2
- package/dist/nodefony/src/remoteDev.js +41 -14
- package/dist/nodefony/src/template/TemplateHelper.js +2 -2
- package/dist/types/index.d.ts +3 -3
- package/dist/types/nodefony/config/config.d.ts +2 -2
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +2 -3
- package/dist/types/nodefony/service/FrontendService.d.ts +30 -11
- package/dist/types/nodefony/service/ViteConfigGenerator.d.ts +0 -11
- package/dist/types/nodefony/src/remoteDev.d.ts +30 -13
- package/package.json +7 -7
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region \0@oxc-project+runtime@0.
|
|
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.
|
|
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.
|
|
14
|
-
import __decorate from "./_virtual/_@oxc-project_runtime@0.
|
|
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
|
|
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.
|
|
9
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.
|
|
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
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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
|
-
|
|
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
|
|
199
|
-
|
|
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é :
|
|
283
|
-
* (config, validée — invalide = ERROR + ignorée,
|
|
284
|
-
* détection de plateforme (Codespaces/Gitpod —
|
|
285
|
-
* plateforme, qu'on lit sans les posséder).
|
|
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
|
-
|
|
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
|
|
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 :
|
|
302
|
-
return
|
|
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
|
|
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}${
|
|
95
|
+
${allowedHostsLine}${httpsLines}${fsBlock} proxy: {
|
|
97
96
|
${proxyLines}
|
|
98
97
|
},
|
|
99
98
|
},` : ` server: {
|
|
100
99
|
strictPort: ${strictPort},
|
|
101
100
|
cors: true,
|
|
102
|
-
${allowedHostsLine}${
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 = [];
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Kernel, Module } from "nodefony";
|
|
2
2
|
import { type IFrontendConfigInput } from "./nodefony/config/defineModuleConfig.js";
|
|
3
|
-
import type {
|
|
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<
|
|
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
|
|
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
|
|
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:
|
|
37
|
+
declare const config: IFrontendConfig;
|
|
38
38
|
export default config;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { frontendConfigSchema, type
|
|
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):
|
|
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
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
|
|
37
|
-
|
|
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é :
|
|
104
|
-
* (config, validée — invalide = ERROR + ignorée,
|
|
105
|
-
* détection de plateforme (Codespaces/Gitpod —
|
|
106
|
-
* plateforme, qu'on lit sans les posséder).
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
40
|
-
"@nodefony/http": "^10.0.0-alpha.
|
|
41
|
-
"nodefony": "^10.0.0-alpha.
|
|
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.
|
|
46
|
-
"@nodefony/http": "^10.0.0-alpha.
|
|
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.
|
|
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"
|