@nodefony/http 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 +77 -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/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
- package/dist/index.js +108 -0
- package/dist/nodefony/command/assetsPublishCommand.js +102 -0
- package/dist/nodefony/command/certificatesCommand.js +47 -0
- package/dist/nodefony/command/networkCommand.js +27 -0
- package/dist/nodefony/command/proxyGenerateCommand.js +66 -0
- package/dist/nodefony/config/config.js +335 -0
- package/dist/nodefony/config/defineModuleConfig.js +93 -0
- package/dist/nodefony/interfaces/IContext.js +1 -0
- package/dist/nodefony/interfaces/ICookie.js +1 -0
- package/dist/nodefony/interfaces/IErrorRenderer.js +1 -0
- package/dist/nodefony/interfaces/IHttpConfig.js +1 -0
- package/dist/nodefony/interfaces/IHttpKernel.js +1 -0
- package/dist/nodefony/interfaces/IRequest.js +1 -0
- package/dist/nodefony/interfaces/IRequestLogger.js +1 -0
- package/dist/nodefony/interfaces/IResponse.js +1 -0
- package/dist/nodefony/interfaces/ISession.js +1 -0
- package/dist/nodefony/interfaces/IUpload.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/service/HttpAdminApi.js +376 -0
- package/dist/nodefony/service/ProfilerAdminApi.js +73 -0
- package/dist/nodefony/service/audit-logger.js +159 -0
- package/dist/nodefony/service/certificates.js +545 -0
- package/dist/nodefony/service/error-renderer.js +320 -0
- package/dist/nodefony/service/http-kernel.js +948 -0
- package/dist/nodefony/service/pretty-request-logger.js +72 -0
- package/dist/nodefony/service/request-logger.js +54 -0
- package/dist/nodefony/service/servers/clientError.js +20 -0
- package/dist/nodefony/service/servers/server-http.js +135 -0
- package/dist/nodefony/service/servers/server-https.js +204 -0
- package/dist/nodefony/service/servers/server-static.js +192 -0
- package/dist/nodefony/service/servers/server-websocket-secure.js +104 -0
- package/dist/nodefony/service/servers/server-websocket.js +104 -0
- package/dist/nodefony/service/servers/serverShutdown.js +31 -0
- package/dist/nodefony/service/servers/wsHeartbeat.js +64 -0
- package/dist/nodefony/service/sessions/sessions-service.js +580 -0
- package/dist/nodefony/service/trace.js +72 -0
- package/dist/nodefony/service/upload/upload-service.js +171 -0
- package/dist/nodefony/src/assets/collectAssets.js +34 -0
- package/dist/nodefony/src/assets/prebuiltUi.js +125 -0
- package/dist/nodefony/src/context/Context.js +415 -0
- package/dist/nodefony/src/context/domainMatcher.js +88 -0
- package/dist/nodefony/src/context/forwarded.js +185 -0
- package/dist/nodefony/src/context/http/HttpContext.js +309 -0
- package/dist/nodefony/src/context/http/Request.js +543 -0
- package/dist/nodefony/src/context/http/Response.js +368 -0
- package/dist/nodefony/src/context/http/parser.js +188 -0
- package/dist/nodefony/src/context/http/urlFastPath.js +103 -0
- package/dist/nodefony/src/context/http2/Request.js +29 -0
- package/dist/nodefony/src/context/http2/Response.js +97 -0
- package/dist/nodefony/src/context/metaData.js +47 -0
- package/dist/nodefony/src/context/requestId.js +41 -0
- package/dist/nodefony/src/context/trustProxy.js +167 -0
- package/dist/nodefony/src/context/websocket/Response.js +181 -0
- package/dist/nodefony/src/context/websocket/WebsocketContext.js +389 -0
- package/dist/nodefony/src/context/websocket/wsBackpressure.js +56 -0
- package/dist/nodefony/src/context/websocket/wsLogContent.js +68 -0
- package/dist/nodefony/src/cookies/cookie.js +258 -0
- package/dist/nodefony/src/errors/httpError.js +69 -0
- package/dist/nodefony/src/profiler/FrameProfile.js +95 -0
- package/dist/nodefony/src/profiler/Profiler.js +139 -0
- package/dist/nodefony/src/proxy/generateProxyConfig.js +157 -0
- package/dist/nodefony/src/rateLimit/IRateLimitStore.js +1 -0
- package/dist/nodefony/src/rateLimit/MemoryRateLimitStore.js +146 -0
- package/dist/nodefony/src/rateLimit/WsConnectionCounter.js +64 -0
- package/dist/nodefony/src/rateLimit/rateLimitFilters.js +20 -0
- package/dist/nodefony/src/servers/portBinder.js +114 -0
- package/dist/nodefony/src/session/session.js +390 -0
- package/dist/nodefony/src/session/storage/MemorySessionStorage.js +185 -0
- package/dist/nodefony/src/session/storage/RevocationGuardStorage.js +137 -0
- package/dist/nodefony/src/session/storage/sessionFilters.js +83 -0
- package/dist/nodefony/src/session/storage/sessionSort.js +53 -0
- package/dist/types/index.d.ts +83 -0
- package/dist/types/nodefony/command/assetsPublishCommand.d.ts +23 -0
- package/dist/types/nodefony/command/certificatesCommand.d.ts +17 -0
- package/dist/types/nodefony/command/networkCommand.d.ts +8 -0
- package/dist/types/nodefony/command/proxyGenerateCommand.d.ts +19 -0
- package/dist/types/nodefony/config/config.d.ts +197 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +39 -0
- package/dist/types/nodefony/interfaces/IContext.d.ts +138 -0
- package/dist/types/nodefony/interfaces/ICookie.d.ts +47 -0
- package/dist/types/nodefony/interfaces/IErrorRenderer.d.ts +55 -0
- package/dist/types/nodefony/interfaces/IHttpConfig.d.ts +12 -0
- package/dist/types/nodefony/interfaces/IHttpKernel.d.ts +10 -0
- package/dist/types/nodefony/interfaces/IRequest.d.ts +35 -0
- package/dist/types/nodefony/interfaces/IRequestLogger.d.ts +31 -0
- package/dist/types/nodefony/interfaces/IResponse.d.ts +39 -0
- package/dist/types/nodefony/interfaces/ISession.d.ts +283 -0
- package/dist/types/nodefony/interfaces/IUpload.d.ts +66 -0
- package/dist/types/nodefony/interfaces/index.d.ts +7 -0
- package/dist/types/nodefony/service/HttpAdminApi.d.ts +18 -0
- package/dist/types/nodefony/service/ProfilerAdminApi.d.ts +23 -0
- package/dist/types/nodefony/service/audit-logger.d.ts +143 -0
- package/dist/types/nodefony/service/certificates.d.ts +246 -0
- package/dist/types/nodefony/service/error-renderer.d.ts +74 -0
- package/dist/types/nodefony/service/http-kernel.d.ts +377 -0
- package/dist/types/nodefony/service/pretty-request-logger.d.ts +25 -0
- package/dist/types/nodefony/service/request-logger.d.ts +18 -0
- package/dist/types/nodefony/service/servers/clientError.d.ts +14 -0
- package/dist/types/nodefony/service/servers/server-http.d.ts +42 -0
- package/dist/types/nodefony/service/servers/server-https.d.ts +41 -0
- package/dist/types/nodefony/service/servers/server-static.d.ts +62 -0
- package/dist/types/nodefony/service/servers/server-websocket-secure.d.ts +29 -0
- package/dist/types/nodefony/service/servers/server-websocket.d.ts +29 -0
- package/dist/types/nodefony/service/servers/serverShutdown.d.ts +27 -0
- package/dist/types/nodefony/service/servers/wsHeartbeat.d.ts +46 -0
- package/dist/types/nodefony/service/sessions/sessions-service.d.ts +218 -0
- package/dist/types/nodefony/service/trace.d.ts +39 -0
- package/dist/types/nodefony/service/upload/upload-service.d.ts +61 -0
- package/dist/types/nodefony/src/assets/collectAssets.d.ts +35 -0
- package/dist/types/nodefony/src/assets/prebuiltUi.d.ts +99 -0
- package/dist/types/nodefony/src/context/Context.d.ts +195 -0
- package/dist/types/nodefony/src/context/domainMatcher.d.ts +67 -0
- package/dist/types/nodefony/src/context/forwarded.d.ts +95 -0
- package/dist/types/nodefony/src/context/http/HttpContext.d.ts +85 -0
- package/dist/types/nodefony/src/context/http/Request.d.ts +203 -0
- package/dist/types/nodefony/src/context/http/Response.d.ts +68 -0
- package/dist/types/nodefony/src/context/http/parser.d.ts +65 -0
- package/dist/types/nodefony/src/context/http/urlFastPath.d.ts +52 -0
- package/dist/types/nodefony/src/context/http2/Request.d.ts +14 -0
- package/dist/types/nodefony/src/context/http2/Response.d.ts +20 -0
- package/dist/types/nodefony/src/context/metaData.d.ts +58 -0
- package/dist/types/nodefony/src/context/requestId.d.ts +28 -0
- package/dist/types/nodefony/src/context/trustProxy.d.ts +77 -0
- package/dist/types/nodefony/src/context/websocket/Response.d.ts +53 -0
- package/dist/types/nodefony/src/context/websocket/WebsocketContext.d.ts +125 -0
- package/dist/types/nodefony/src/context/websocket/wsBackpressure.d.ts +73 -0
- package/dist/types/nodefony/src/context/websocket/wsLogContent.d.ts +37 -0
- package/dist/types/nodefony/src/cookies/cookie.d.ts +88 -0
- package/dist/types/nodefony/src/errors/httpError.d.ts +15 -0
- package/dist/types/nodefony/src/profiler/FrameProfile.d.ts +110 -0
- package/dist/types/nodefony/src/profiler/Profiler.d.ts +192 -0
- package/dist/types/nodefony/src/proxy/generateProxyConfig.d.ts +76 -0
- package/dist/types/nodefony/src/rateLimit/IRateLimitStore.d.ts +98 -0
- package/dist/types/nodefony/src/rateLimit/MemoryRateLimitStore.d.ts +40 -0
- package/dist/types/nodefony/src/rateLimit/WsConnectionCounter.d.ts +37 -0
- package/dist/types/nodefony/src/rateLimit/rateLimitFilters.d.ts +18 -0
- package/dist/types/nodefony/src/servers/portBinder.d.ts +102 -0
- package/dist/types/nodefony/src/session/session.d.ts +171 -0
- package/dist/types/nodefony/src/session/storage/MemorySessionStorage.d.ts +77 -0
- package/dist/types/nodefony/src/session/storage/RevocationGuardStorage.d.ts +81 -0
- package/dist/types/nodefony/src/session/storage/sessionFilters.d.ts +102 -0
- package/dist/types/nodefony/src/session/storage/sessionSort.d.ts +45 -0
- package/docs/cookies.md +365 -0
- package/docs/index.md +163 -0
- package/docs/observabilite.md +460 -0
- package/docs/rate-limit.md +372 -0
- package/docs/servers.md +935 -0
- package/docs/session.md +768 -0
- package/docs/upload.md +460 -0
- package/package.json +101 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import __decorateMetadata from "../../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
|
|
2
|
+
import __decorate from "../../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
|
|
3
|
+
import http_kernel_default from "../http-kernel.js";
|
|
4
|
+
import __decorateParam from "../../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js";
|
|
5
|
+
import { Cli, FileClass, Module, Service, inject } from "nodefony";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import fsp from "node:fs/promises";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
//#region nodefony/service/upload/upload-service.ts
|
|
10
|
+
/** Test d'existence non bloquant (`fsp.access`) — remplace `existsSync`. */
|
|
11
|
+
const existsAsync = async (p) => {
|
|
12
|
+
try {
|
|
13
|
+
await fsp.access(p);
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
let upload = class upload extends Service {
|
|
20
|
+
httpKernel;
|
|
21
|
+
path;
|
|
22
|
+
module;
|
|
23
|
+
constructor(module, httpKernel) {
|
|
24
|
+
super("upload", httpKernel?.container, httpKernel.notificationsCenter);
|
|
25
|
+
this.httpKernel = httpKernel;
|
|
26
|
+
this.module = module;
|
|
27
|
+
this.kernel?.once("onBoot", async () => {
|
|
28
|
+
this.options = this.httpKernel.options;
|
|
29
|
+
if (path.isAbsolute(this.options.upload.uploadDir)) this.path = this.options.upload.uploadDir;
|
|
30
|
+
else this.path = path.resolve(`${this.kernel?.path}/${this.options.upload.uploadDir}`);
|
|
31
|
+
try {
|
|
32
|
+
await fsp.mkdir(this.path, { recursive: true });
|
|
33
|
+
} catch (e) {
|
|
34
|
+
this.path = "/tmp";
|
|
35
|
+
this.options.upload.uploadDir = this.path;
|
|
36
|
+
this.log(e, "DEBUG");
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Construit un `UploadedFile` à partir d'un fichier multipart parsé —
|
|
42
|
+
* **async, non bloquant** (stat via `fsp.lstat`, plus de `lstatSync` par
|
|
43
|
+
* fichier uploadé).
|
|
44
|
+
*
|
|
45
|
+
* @param file - fichier parsé (forme neutre `IParsedUploadFile`, busboy).
|
|
46
|
+
* @param name - nom de champ (fallback si pas de `originalFilename`).
|
|
47
|
+
* @returns le `UploadedFile` hydraté.
|
|
48
|
+
*/
|
|
49
|
+
async createUploadFile(file, name) {
|
|
50
|
+
return UploadedFile.create(file, name);
|
|
51
|
+
}
|
|
52
|
+
log(pci, severity, msgid, msg) {
|
|
53
|
+
if (this.syslog) {
|
|
54
|
+
if (!msgid) msgid = "HTTP UPLOAD";
|
|
55
|
+
return this.syslog.log(pci, severity, msgid, msg);
|
|
56
|
+
}
|
|
57
|
+
throw new Error(`Syslog not ready`);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
upload = __decorate([__decorateParam(1, inject("HttpKernel")), __decorateMetadata("design:paramtypes", [typeof Module === "undefined" ? Object : Module, typeof http_kernel_default === "undefined" ? Object : http_kernel_default])], upload);
|
|
61
|
+
var UploadedFile = class UploadedFile extends FileClass {
|
|
62
|
+
parsedFile;
|
|
63
|
+
size;
|
|
64
|
+
prettySize;
|
|
65
|
+
filename;
|
|
66
|
+
lastModifiedDate;
|
|
67
|
+
hashAlgorithm;
|
|
68
|
+
hash;
|
|
69
|
+
constructor(parsedFile, name, options) {
|
|
70
|
+
super(parsedFile.filepath, options);
|
|
71
|
+
this.parsedFile = parsedFile;
|
|
72
|
+
this.size = this.getSize();
|
|
73
|
+
this.prettySize = this.getPrettySize();
|
|
74
|
+
this.filename = this.realName(name);
|
|
75
|
+
this.mimeType = this.getMimeType();
|
|
76
|
+
this.lastModifiedDate = this.parsedFile.mtime;
|
|
77
|
+
this.hashAlgorithm = this.parsedFile.hashAlgorithm;
|
|
78
|
+
this.hash = this.parsedFile.hash;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Construit un `UploadedFile` SANS `lstatSync` bloquant — stat résolu en async
|
|
82
|
+
* (`FileClass.stat`). À utiliser dans le pipeline d'upload (per-request).
|
|
83
|
+
*
|
|
84
|
+
* @param parsedFile - fichier multipart parsé (forme neutre busboy).
|
|
85
|
+
* @param name - nom de champ (fallback de nom).
|
|
86
|
+
* @returns le `UploadedFile` hydraté (stats async).
|
|
87
|
+
* @remarks Nommée `create` (pas `from`) pour ne pas entrer en conflit avec la
|
|
88
|
+
* signature statique de `FileClass.from(path)` (TS2417).
|
|
89
|
+
*/
|
|
90
|
+
static async create(parsedFile, name) {
|
|
91
|
+
const file = new UploadedFile(parsedFile, name, { defer: true });
|
|
92
|
+
await file.stat();
|
|
93
|
+
return file;
|
|
94
|
+
}
|
|
95
|
+
getSize() {
|
|
96
|
+
return this.parsedFile.size;
|
|
97
|
+
}
|
|
98
|
+
getPrettySize() {
|
|
99
|
+
return Cli.niceBytes(this.parsedFile.size);
|
|
100
|
+
}
|
|
101
|
+
realName(name) {
|
|
102
|
+
return this.parsedFile.originalFilename || name || this.parsedFile.newFilename;
|
|
103
|
+
}
|
|
104
|
+
getMimeType() {
|
|
105
|
+
if (this.parsedFile) return this.parsedFile.mimetype || super.getMimeType(this.filename);
|
|
106
|
+
return super.getMimeType();
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Nom de fichier SÛR à poser dans un dossier de destination.
|
|
110
|
+
*
|
|
111
|
+
* `filename` vient de `originalFilename`, c'est-à-dire de l'en-tête multipart :
|
|
112
|
+
* **une donnée d'attaquant**. Le fichier temporaire, lui, est protégé (nom
|
|
113
|
+
* généré) — mais dès que l'application écrit `file.move("/var/uploads")`, c'est
|
|
114
|
+
* ce nom client qui compose la destination, et `path.resolve` honore les `..` :
|
|
115
|
+
* `../../etc/cron.d/x` sortait du dossier visé.
|
|
116
|
+
*
|
|
117
|
+
* On ne garde donc que le dernier segment, en coupant sur les DEUX séparateurs
|
|
118
|
+
* (un client Windows envoie `..\\..\\x`, que `path.basename` POSIX ne découpe
|
|
119
|
+
* pas). Reste `.`/`..`/vide → on retombe sur le nom du fichier temporaire,
|
|
120
|
+
* jamais sur quelque chose que le client contrôle.
|
|
121
|
+
*
|
|
122
|
+
* @returns un nom de fichier sans aucune composante de chemin.
|
|
123
|
+
*/
|
|
124
|
+
#safeTargetName() {
|
|
125
|
+
const cleaned = ((this.filename || this.name || "").split(/[/\\]/).pop() ?? "").trim();
|
|
126
|
+
if (cleaned === "" || cleaned === "." || cleaned === "..") return path.basename(this.parsedFile.filepath);
|
|
127
|
+
return cleaned;
|
|
128
|
+
}
|
|
129
|
+
move(target) {
|
|
130
|
+
if (fs.existsSync(target)) {
|
|
131
|
+
const newFile = new FileClass(target);
|
|
132
|
+
if (newFile.isDirectory()) {
|
|
133
|
+
const n = path.resolve(newFile.path, this.#safeTargetName());
|
|
134
|
+
return super.move(n);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const dirname = path.dirname(target);
|
|
138
|
+
if (fs.existsSync(dirname)) {
|
|
139
|
+
if (target === dirname) {
|
|
140
|
+
const name = path.resolve(target, this.#safeTargetName());
|
|
141
|
+
return super.move(name);
|
|
142
|
+
} else return super.move(target);
|
|
143
|
+
}
|
|
144
|
+
throw fs.lstatSync(dirname);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Variante **async** de `move()` — déplace le fichier uploadé sans bloquer
|
|
148
|
+
* l'event-loop (`fsp.access`/`fsp.rename` via `FileClass.moveAsync`).
|
|
149
|
+
* À préférer dans le pipeline (controller).
|
|
150
|
+
*
|
|
151
|
+
* @param target - destination (fichier ou dossier existant).
|
|
152
|
+
* @returns nouvelle instance `FileClass` (hydratée async) sur la destination.
|
|
153
|
+
*/
|
|
154
|
+
async moveAsync(target) {
|
|
155
|
+
const dest = target;
|
|
156
|
+
if (await existsAsync(dest)) {
|
|
157
|
+
const newFile = await FileClass.from(dest);
|
|
158
|
+
if (newFile.isDirectory()) return super.moveAsync(path.resolve(newFile.path, this.#safeTargetName()));
|
|
159
|
+
}
|
|
160
|
+
const dirname = path.dirname(dest);
|
|
161
|
+
if (await existsAsync(dirname)) {
|
|
162
|
+
if (dest === dirname) return super.moveAsync(path.resolve(dest, this.#safeTargetName()));
|
|
163
|
+
return super.moveAsync(dest);
|
|
164
|
+
}
|
|
165
|
+
await fsp.lstat(dirname);
|
|
166
|
+
throw new Error(`upload moveAsync: target dir not found: ${dirname}`);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
var upload_service_default = upload;
|
|
170
|
+
//#endregion
|
|
171
|
+
export { UploadedFile, upload_service_default as default, upload };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { stripTrailingSlashes } from "nodefony";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
//#region nodefony/src/assets/collectAssets.ts
|
|
4
|
+
/** Normalise un préfixe en segment de chemin (`/_assets/x/` → `_assets/x`). */
|
|
5
|
+
const prefixToSegment = (prefix) => stripTrailingSlashes(prefix.replace(/^\/+/, ""));
|
|
6
|
+
/**
|
|
7
|
+
* Construit le plan de publication des assets : pour chaque source unique
|
|
8
|
+
* (dédupliquée par préfixe — le DERNIER gagne, comme `addMount`), calcule le
|
|
9
|
+
* dossier cible `outDir/<préfixe-en-chemin>` miroir de l'URL.
|
|
10
|
+
*
|
|
11
|
+
* PUR (0 I/O) → testable. La copie réelle + le manifeste sont faits par la
|
|
12
|
+
* commande `assets:publish`. L'upload (S3/CDN/rsync) reste à l'orchestrateur :
|
|
13
|
+
* Nodefony assemble l'arbre, le déploiement le pousse (cloud-native).
|
|
14
|
+
*
|
|
15
|
+
* @param sources dossiers + préfixes (mounts natifs + bundles frontend)
|
|
16
|
+
* @param outDir racine ABSOLUE de l'arbre de sortie (ex. `<root>/dist-assets`)
|
|
17
|
+
* @returns plan ordonné, 1 entrée par préfixe unique
|
|
18
|
+
*/
|
|
19
|
+
function planAssetPublish(sources, outDir) {
|
|
20
|
+
const byPrefix = /* @__PURE__ */ new Map();
|
|
21
|
+
for (const s of sources) byPrefix.set(s.prefix, s.dir);
|
|
22
|
+
const plan = [];
|
|
23
|
+
for (const [prefix, dir] of byPrefix) {
|
|
24
|
+
const seg = prefixToSegment(prefix);
|
|
25
|
+
plan.push({
|
|
26
|
+
prefix,
|
|
27
|
+
dir,
|
|
28
|
+
target: seg ? join(outDir, seg) : outDir
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return plan;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { planAssetPublish };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
//#region nodefony/src/assets/prebuiltUi.ts
|
|
4
|
+
/**
|
|
5
|
+
* Résout le mode de livraison de l'UI embarquée d'un module.
|
|
6
|
+
*
|
|
7
|
+
* Pattern universel des admin-UI embarquées (bull-board, GraphiQL, profiler
|
|
8
|
+
* Symfony) : le consommateur ne compile JAMAIS l'UI d'un module tiers — les
|
|
9
|
+
* assets sont pré-buildés au publish et servis statiques. Le mode `vite`
|
|
10
|
+
* (HMR) n'a de sens que là où les sources existent (repo self-hosted, `--link`).
|
|
11
|
+
*
|
|
12
|
+
* PUR hormis deux `existsSync` (boot uniquement, jamais dans le hot path).
|
|
13
|
+
*
|
|
14
|
+
* @returns le mode effectif + la raison à logger (fail-loud si `none`)
|
|
15
|
+
*/
|
|
16
|
+
function resolveUiDelivery(opts) {
|
|
17
|
+
const requested = opts.requested ?? "auto";
|
|
18
|
+
const hasSources = fs.existsSync(opts.sourcesDir);
|
|
19
|
+
const hasPrebuilt = fs.existsSync(opts.distIndex);
|
|
20
|
+
if (requested === "vite") {
|
|
21
|
+
if (!opts.hasFrontendService) return {
|
|
22
|
+
mode: "none",
|
|
23
|
+
reason: `ui: "vite" forced but @nodefony/frontend service is not registered`
|
|
24
|
+
};
|
|
25
|
+
if (!hasSources) return {
|
|
26
|
+
mode: "none",
|
|
27
|
+
reason: `ui: "vite" forced but frontend sources are missing (${opts.sourcesDir})`
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
mode: "vite",
|
|
31
|
+
reason: `ui: "vite" forced by config`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (requested === "static") {
|
|
35
|
+
if (!hasPrebuilt) return {
|
|
36
|
+
mode: "none",
|
|
37
|
+
reason: `ui: "static" forced but prebuilt index is missing (${opts.distIndex})`
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
mode: "static",
|
|
41
|
+
reason: `ui: "static" forced by config`
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (opts.environment === "development" && opts.hasFrontendService) {
|
|
45
|
+
if (hasSources) return {
|
|
46
|
+
mode: "vite",
|
|
47
|
+
reason: "auto → vite (development + frontend service + sources)"
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (hasPrebuilt) return {
|
|
51
|
+
mode: "static",
|
|
52
|
+
reason: `auto → static (prebuilt assets ${path.dirname(opts.distIndex)})`
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
mode: "none",
|
|
56
|
+
reason: `auto → none: no Vite path (dev+frontend+sources) and no prebuilt index (${opts.distIndex}) — did the package ship its "prepack" UI build?`
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Livraison STATIQUE de l'UI pré-buildée d'un module (mode `static`).
|
|
61
|
+
*
|
|
62
|
+
* Deux responsabilités, zéro dépendance à @nodefony/frontend :
|
|
63
|
+
* 1. {@link mount} — sert `distDir` sous `publicPath` via `server-static`
|
|
64
|
+
* (assets hashés immuables produits par `vite build` au publish).
|
|
65
|
+
* 2. {@link renderIndex} — document HTML d'entrée (l'`index.html` transformé
|
|
66
|
+
* par Vite, tags déjà réécrits vers `publicPath`), nonce CSP injecté par
|
|
67
|
+
* requête. Le SPA fallback reste au controller du module (routes littérales,
|
|
68
|
+
* cf. StudioController).
|
|
69
|
+
*
|
|
70
|
+
* Perf : l'index est lu UNE fois (lazy, caché) ; seule l'injection du nonce
|
|
71
|
+
* coûte un `replaceAll` par rendu — route d'entrée UI, jamais un hot path.
|
|
72
|
+
*/
|
|
73
|
+
var PrebuiltUi = class {
|
|
74
|
+
publicPath;
|
|
75
|
+
distDir;
|
|
76
|
+
indexPath;
|
|
77
|
+
/** Template HTML caché — `null` tant que non lu (lazy). */
|
|
78
|
+
template = null;
|
|
79
|
+
constructor(opts) {
|
|
80
|
+
let p = opts.publicPath.trim();
|
|
81
|
+
if (!p.startsWith("/")) p = `/${p}`;
|
|
82
|
+
if (!p.endsWith("/")) p = `${p}/`;
|
|
83
|
+
this.publicPath = p.replace(/\/{2,}/g, "/");
|
|
84
|
+
this.distDir = opts.distDir;
|
|
85
|
+
this.indexPath = path.join(this.distDir, opts.indexFile ?? "index.html");
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Monte `distDir` sous `publicPath` auprès de `server-static` (résolu par
|
|
89
|
+
* nom). Si le service n'existe pas encore (module chargé avant http), un
|
|
90
|
+
* retry unique est armé sur `onReady`.
|
|
91
|
+
*
|
|
92
|
+
* @returns `true` si monté immédiatement, `false` si différé/indisponible
|
|
93
|
+
* (l'appelant loggue — ce helper n'a pas de logger)
|
|
94
|
+
*/
|
|
95
|
+
mount(container, kernel) {
|
|
96
|
+
const stat = container?.get?.("server-static");
|
|
97
|
+
if (stat?.addMount) {
|
|
98
|
+
stat.addMount(this.publicPath, this.distDir);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
kernel?.once("onReady", () => {
|
|
102
|
+
(container?.get?.("server-static"))?.addMount?.(this.publicPath, this.distDir);
|
|
103
|
+
});
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Document HTML d'entrée de l'UI (index pré-buildé par Vite).
|
|
108
|
+
*
|
|
109
|
+
* @param nonce nonce CSP de la requête (`Context.cspNonce`) — injecté sur
|
|
110
|
+
* chaque `<script>` ; les assets `src` same-origin passent déjà
|
|
111
|
+
* par `'self'`, le nonce couvre un éventuel inline Vite.
|
|
112
|
+
* @returns le HTML, ou un commentaire HTML fail-loud si l'index est absent
|
|
113
|
+
*/
|
|
114
|
+
renderIndex(nonce) {
|
|
115
|
+
if (this.template === null) try {
|
|
116
|
+
this.template = fs.readFileSync(this.indexPath, "utf8");
|
|
117
|
+
} catch {
|
|
118
|
+
return `<!DOCTYPE html><!-- prebuilt UI index missing: ${this.indexPath} -->`;
|
|
119
|
+
}
|
|
120
|
+
if (!nonce) return this.template;
|
|
121
|
+
return this.template.replaceAll("<script", `<script nonce="${nonce}"`);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
//#endregion
|
|
125
|
+
export { PrebuiltUi, resolveUiDelivery };
|