@nitronjs/framework 0.2.27 → 0.3.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/README.md +260 -170
- package/lib/Auth/Auth.js +2 -2
- package/lib/Build/CssBuilder.js +5 -7
- package/lib/Build/EffectivePropUsage.js +174 -0
- package/lib/Build/FactoryTransform.js +1 -21
- package/lib/Build/FileAnalyzer.js +2 -33
- package/lib/Build/Manager.js +354 -58
- package/lib/Build/PropUsageAnalyzer.js +1189 -0
- package/lib/Build/jsxRuntime.js +25 -155
- package/lib/Build/plugins.js +212 -146
- package/lib/Build/propUtils.js +70 -0
- package/lib/Console/Commands/DevCommand.js +30 -10
- package/lib/Console/Commands/MakeCommand.js +8 -1
- package/lib/Console/Output.js +0 -2
- package/lib/Console/Stubs/rsc-consumer.tsx +74 -0
- package/lib/Console/Stubs/vendor-dev.tsx +30 -41
- package/lib/Console/Stubs/vendor.tsx +25 -1
- package/lib/Core/Config.js +0 -6
- package/lib/Core/Paths.js +0 -19
- package/lib/Database/Migration/Checksum.js +0 -3
- package/lib/Database/Migration/MigrationRepository.js +0 -8
- package/lib/Database/Migration/MigrationRunner.js +1 -2
- package/lib/Database/Model.js +19 -11
- package/lib/Database/QueryBuilder.js +25 -4
- package/lib/Database/Schema/Blueprint.js +10 -0
- package/lib/Database/Schema/Manager.js +2 -0
- package/lib/Date/DateTime.js +1 -1
- package/lib/Dev/DevContext.js +44 -0
- package/lib/Dev/DevErrorPage.js +990 -0
- package/lib/Dev/DevIndicator.js +836 -0
- package/lib/HMR/Server.js +16 -37
- package/lib/Http/Server.js +171 -23
- package/lib/Logging/Log.js +34 -2
- package/lib/Mail/Mail.js +41 -10
- package/lib/Route/Router.js +43 -19
- package/lib/Runtime/Entry.js +10 -6
- package/lib/Session/Manager.js +103 -1
- package/lib/Session/Session.js +0 -4
- package/lib/Support/Str.js +6 -4
- package/lib/Translation/Lang.js +376 -32
- package/lib/Translation/pluralize.js +81 -0
- package/lib/Validation/MagicBytes.js +120 -0
- package/lib/Validation/Validator.js +46 -29
- package/lib/View/Client/hmr-client.js +100 -90
- package/lib/View/Client/spa.js +121 -50
- package/lib/View/ClientManifest.js +60 -0
- package/lib/View/FlightRenderer.js +100 -0
- package/lib/View/Layout.js +0 -3
- package/lib/View/PropFilter.js +81 -0
- package/lib/View/View.js +230 -495
- package/lib/index.d.ts +22 -1
- package/package.json +2 -2
- package/skeleton/config/app.js +1 -0
- package/skeleton/config/server.js +13 -0
- package/skeleton/config/session.js +3 -0
- package/lib/Build/HydrationBuilder.js +0 -190
- package/lib/Console/Stubs/page-hydration-dev.tsx +0 -72
- package/lib/Console/Stubs/page-hydration.tsx +0 -53
package/lib/HMR/Server.js
CHANGED
|
@@ -14,10 +14,10 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
14
14
|
* // In HTTP Server
|
|
15
15
|
* HMR.registerRoutes(fastify);
|
|
16
16
|
* HMR.setup(httpServer);
|
|
17
|
-
*
|
|
17
|
+
*
|
|
18
18
|
* // Emit updates
|
|
19
|
-
* HMR.
|
|
20
|
-
* HMR.
|
|
19
|
+
* HMR.emitChange({ changeType: "page", file: "Site/Home.tsx" });
|
|
20
|
+
* HMR.emitChange({ changeType: "css", file: "global.css" });
|
|
21
21
|
*/
|
|
22
22
|
class HMRServer {
|
|
23
23
|
#io = null;
|
|
@@ -33,13 +33,14 @@ class HMRServer {
|
|
|
33
33
|
*/
|
|
34
34
|
registerRoutes(fastify) {
|
|
35
35
|
this.#clientScript = this.#findSocketIoClient();
|
|
36
|
+
const cachedScript = this.#clientScript ? fs.readFileSync(this.#clientScript, "utf-8") : null;
|
|
36
37
|
|
|
37
38
|
fastify.get("/__nitron_client/socket.io.js", (req, reply) => {
|
|
38
|
-
if (!
|
|
39
|
+
if (!cachedScript) {
|
|
39
40
|
return reply.code(503).send("// HMR disabled: socket.io client not found");
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
reply.type("application/javascript").send(
|
|
43
|
+
reply.type("application/javascript").send(cachedScript);
|
|
43
44
|
});
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -75,41 +76,19 @@ class HMRServer {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
/**
|
|
78
|
-
*
|
|
79
|
-
* @
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Emits a view update event to trigger hot reload of a React component.
|
|
87
|
-
* @param {string} filePath - Absolute path to the changed view file.
|
|
88
|
-
*/
|
|
89
|
-
emitViewUpdate(filePath) {
|
|
90
|
-
if (!this.#io) return;
|
|
91
|
-
|
|
92
|
-
const normalized = filePath.replace(/\\/g, "/");
|
|
93
|
-
const match = normalized.match(/resources\/views\/(.+)\.tsx$/);
|
|
94
|
-
const viewPath = match ? match[1].toLowerCase() : path.basename(filePath, ".tsx").toLowerCase();
|
|
95
|
-
|
|
96
|
-
this.#io.emit("hmr:update", {
|
|
97
|
-
type: "view",
|
|
98
|
-
file: viewPath,
|
|
99
|
-
url: `/js/${viewPath}.js`,
|
|
100
|
-
timestamp: Date.now()
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Emits a CSS update event to refresh stylesheets without page reload.
|
|
106
|
-
* @param {string} [filePath] - Path to the changed CSS file. If null, refreshes all CSS.
|
|
79
|
+
* Emits a unified change event for RSC-based hot updates.
|
|
80
|
+
* @param {object} data
|
|
81
|
+
* @param {string} data.changeType - "page" | "layout" | "css"
|
|
82
|
+
* @param {boolean} [data.cssChanged] - Whether CSS also changed (Tailwind rebuild)
|
|
83
|
+
* @param {string} [data.file] - Relative path of the changed file (for logging)
|
|
107
84
|
*/
|
|
108
|
-
|
|
85
|
+
emitChange(data) {
|
|
109
86
|
if (!this.#io) return;
|
|
110
87
|
|
|
111
|
-
this.#io.emit("hmr:
|
|
112
|
-
|
|
88
|
+
this.#io.emit("hmr:change", {
|
|
89
|
+
changeType: data.changeType,
|
|
90
|
+
cssChanged: data.cssChanged || false,
|
|
91
|
+
file: data.file || null,
|
|
113
92
|
timestamp: Date.now()
|
|
114
93
|
});
|
|
115
94
|
}
|
package/lib/Http/Server.js
CHANGED
|
@@ -17,11 +17,27 @@ import View from "../View/View.js";
|
|
|
17
17
|
import Auth from "../Auth/Auth.js";
|
|
18
18
|
import SessionManager from "../Session/Manager.js";
|
|
19
19
|
import DB from "../Database/DB.js";
|
|
20
|
+
import Lang from "../Translation/Lang.js";
|
|
20
21
|
import Log from "../Logging/Log.js";
|
|
22
|
+
import { detectMime, sanitizeFilename, isSameMimeFamily } from "../Validation/MagicBytes.js";
|
|
21
23
|
|
|
22
24
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
25
|
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, "../../package.json"), "utf8"));
|
|
24
26
|
|
|
27
|
+
const SECURITY_DEFAULTS = {
|
|
28
|
+
verifyMagicBytes: true,
|
|
29
|
+
dangerousExtensions: [
|
|
30
|
+
".exe", ".bat", ".cmd", ".com", ".scr", ".pif", ".msi", ".dll",
|
|
31
|
+
".sh", ".cgi", ".csh", ".ksh",
|
|
32
|
+
".php", ".phtml", ".php5", ".php7",
|
|
33
|
+
".jsp", ".asp", ".aspx",
|
|
34
|
+
".vbs", ".vbe", ".wsf", ".wsh", ".ps1", ".psm1",
|
|
35
|
+
".hta", ".cpl", ".inf", ".reg", ".sct"
|
|
36
|
+
],
|
|
37
|
+
compoundExtensions: [".tar.gz", ".tar.bz2", ".tar.xz", ".tar.zst"],
|
|
38
|
+
allowDoubleExtension: false
|
|
39
|
+
};
|
|
40
|
+
|
|
25
41
|
/**
|
|
26
42
|
* HTTP server manager built on Fastify.
|
|
27
43
|
* Handles server configuration, plugin registration, and lifecycle management.
|
|
@@ -52,7 +68,7 @@ class Server {
|
|
|
52
68
|
|
|
53
69
|
const serverDefaults = {
|
|
54
70
|
bodyLimit: 50 * 1024 * 1024,
|
|
55
|
-
trustProxy:
|
|
71
|
+
trustProxy: false,
|
|
56
72
|
maxParamLength: 150
|
|
57
73
|
};
|
|
58
74
|
|
|
@@ -201,7 +217,9 @@ class Server {
|
|
|
201
217
|
this.#server.register(fastifyFormbody);
|
|
202
218
|
|
|
203
219
|
// Register hooks
|
|
204
|
-
|
|
220
|
+
const securityConfig = { ...SECURITY_DEFAULTS, ...multipartConfig?.security };
|
|
221
|
+
|
|
222
|
+
this.#server.addHook("preHandler", async (req, reply) => {
|
|
205
223
|
if (!req.isMultipart() || !req.body) {
|
|
206
224
|
return;
|
|
207
225
|
}
|
|
@@ -210,18 +228,127 @@ class Server {
|
|
|
210
228
|
const files = {};
|
|
211
229
|
|
|
212
230
|
for (const [key, field] of Object.entries(req.body)) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
231
|
+
const items = Array.isArray(field) ? field : [field];
|
|
232
|
+
let hasFile = false;
|
|
233
|
+
const fileItems = [];
|
|
234
|
+
|
|
235
|
+
for (const item of items) {
|
|
236
|
+
if (!item?.toBuffer) {
|
|
237
|
+
if (!hasFile && item && typeof item === "object" && "value" in item) {
|
|
238
|
+
normalized[key] = item.value;
|
|
239
|
+
}
|
|
240
|
+
else if (!hasFile) {
|
|
241
|
+
normalized[key] = item;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Filename guard — no filename means drop the file (existing behavior)
|
|
248
|
+
if (!item.filename) continue;
|
|
249
|
+
|
|
250
|
+
// 1. Filename sanitization
|
|
251
|
+
item.filename = sanitizeFilename(item.filename);
|
|
252
|
+
|
|
253
|
+
// 2. Extension extraction
|
|
254
|
+
const lastDot = item.filename.lastIndexOf(".");
|
|
255
|
+
|
|
256
|
+
if (lastDot === 0) {
|
|
257
|
+
item.extension = item.filename.slice(1).toLowerCase();
|
|
258
|
+
}
|
|
259
|
+
else if (lastDot > 0) {
|
|
260
|
+
item.extension = item.filename.slice(lastDot + 1).toLowerCase();
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
item.extension = "";
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 3. Dangerous extension check
|
|
267
|
+
if (item.extension && securityConfig.dangerousExtensions.includes("." + item.extension)) {
|
|
268
|
+
Log.warn("File rejected", {
|
|
269
|
+
filename: item.filename,
|
|
270
|
+
reason: "Dangerous extension"
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return reply.code(422).send({
|
|
274
|
+
statusCode: 422,
|
|
275
|
+
error: "Unprocessable Entity",
|
|
276
|
+
message: "File upload rejected."
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// 4. Double extension check
|
|
281
|
+
if (!securityConfig.allowDoubleExtension) {
|
|
282
|
+
const parts = item.filename.split(".");
|
|
283
|
+
const middleExtensions = parts.slice(1, -1);
|
|
284
|
+
|
|
285
|
+
if (middleExtensions.length > 0) {
|
|
286
|
+
const filenameLower = item.filename.toLowerCase();
|
|
287
|
+
let exemptExts = [];
|
|
288
|
+
|
|
289
|
+
for (const compound of securityConfig.compoundExtensions) {
|
|
290
|
+
if (filenameLower.endsWith(compound)) {
|
|
291
|
+
exemptExts = compound.slice(1).split("."); // ".tar.gz" → ["tar", "gz"]
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const remaining = middleExtensions.map(e => e.toLowerCase()).filter(e => !exemptExts.includes(e));
|
|
297
|
+
|
|
298
|
+
for (const ext of remaining) {
|
|
299
|
+
if (securityConfig.dangerousExtensions.includes("." + ext)) {
|
|
300
|
+
Log.warn("File rejected", {
|
|
301
|
+
filename: item.filename,
|
|
302
|
+
reason: "Dangerous double extension"
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
return reply.code(422).send({
|
|
306
|
+
statusCode: 422,
|
|
307
|
+
error: "Unprocessable Entity",
|
|
308
|
+
message: "File upload rejected."
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 5. Magic byte detection
|
|
316
|
+
if (securityConfig.verifyMagicBytes) {
|
|
317
|
+
try {
|
|
318
|
+
const buf = await item.toBuffer();
|
|
319
|
+
|
|
320
|
+
item._magicMime = detectMime(buf.subarray(0, 12));
|
|
321
|
+
item._fileSize = buf.length;
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
Log.warn("toBuffer failed", {
|
|
325
|
+
filename: item.filename,
|
|
326
|
+
error: err.message
|
|
327
|
+
});
|
|
328
|
+
item._magicMime = null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 6. MIME mismatch logging
|
|
332
|
+
if (item._magicMime && item.mimetype && item._magicMime !== item.mimetype.toLowerCase()) {
|
|
333
|
+
if (!isSameMimeFamily(item._magicMime, item.mimetype)) {
|
|
334
|
+
Log.warn("MIME mismatch detected", {
|
|
335
|
+
filename: item.filename,
|
|
336
|
+
declared: item.mimetype,
|
|
337
|
+
detected: item._magicMime
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
218
341
|
}
|
|
342
|
+
|
|
343
|
+
hasFile = true;
|
|
344
|
+
fileItems.push(item);
|
|
219
345
|
}
|
|
220
|
-
|
|
221
|
-
|
|
346
|
+
|
|
347
|
+
if (fileItems.length === 1) {
|
|
348
|
+
files[key] = fileItems[0];
|
|
222
349
|
}
|
|
223
|
-
else {
|
|
224
|
-
|
|
350
|
+
else if (fileItems.length > 1) {
|
|
351
|
+
files[key] = fileItems;
|
|
225
352
|
}
|
|
226
353
|
}
|
|
227
354
|
|
|
@@ -247,6 +374,9 @@ class Server {
|
|
|
247
374
|
|
|
248
375
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
249
376
|
const part = parts[i];
|
|
377
|
+
|
|
378
|
+
if (part === "__proto__" || part === "constructor" || part === "prototype") break;
|
|
379
|
+
|
|
250
380
|
const isNextIndex = /^\d+$/.test(parts[i + 1]);
|
|
251
381
|
|
|
252
382
|
if (current[part] === undefined) {
|
|
@@ -310,15 +440,21 @@ class Server {
|
|
|
310
440
|
await DB.setup();
|
|
311
441
|
await SessionManager.setup(this.#server);
|
|
312
442
|
Auth.setup(this.#server);
|
|
443
|
+
Lang.setup(this.#server);
|
|
313
444
|
|
|
314
|
-
// Register
|
|
445
|
+
// Register dev-only hooks and routes
|
|
315
446
|
if (Environment.isDev) {
|
|
316
447
|
const { default: HMRServer } = await import("../HMR/Server.js");
|
|
317
448
|
HMRServer.registerRoutes(this.#server);
|
|
449
|
+
|
|
450
|
+
const { default: DevContext } = await import("../Dev/DevContext.js");
|
|
451
|
+
this.#server.addHook("onRequest", async (req) => {
|
|
452
|
+
DevContext.attach(req);
|
|
453
|
+
});
|
|
318
454
|
}
|
|
319
455
|
|
|
320
456
|
await Router.setup(this.#server);
|
|
321
|
-
View.setup(this.#server);
|
|
457
|
+
await View.setup(this.#server);
|
|
322
458
|
|
|
323
459
|
// Listen
|
|
324
460
|
try {
|
|
@@ -333,20 +469,29 @@ class Server {
|
|
|
333
469
|
HMRServer.setup(this.#server.server);
|
|
334
470
|
}
|
|
335
471
|
|
|
336
|
-
this.#
|
|
472
|
+
const banner = this.#renderBanner({ success: true, address, host, port });
|
|
473
|
+
|
|
474
|
+
if (Environment.isDev && process.send) {
|
|
475
|
+
process.send({ type: "banner", text: banner });
|
|
476
|
+
}
|
|
477
|
+
else if (banner) {
|
|
478
|
+
console.log(banner);
|
|
479
|
+
}
|
|
480
|
+
|
|
337
481
|
Log.info("Server started successfully!", { address, host, port, environment: Environment.isDev ? "development" : "production" });
|
|
338
482
|
}
|
|
339
483
|
catch (err) {
|
|
340
|
-
this.#
|
|
484
|
+
const banner = this.#renderBanner({ success: false, error: err });
|
|
485
|
+
if (banner) console.log(banner);
|
|
341
486
|
Log.fatal("Server startup failed", { error: err.message, code: err.code, stack: err.stack });
|
|
342
487
|
process.exit(1);
|
|
343
488
|
}
|
|
344
489
|
}
|
|
345
490
|
|
|
346
491
|
// Private Methods
|
|
347
|
-
static #
|
|
492
|
+
static #renderBanner({ success, address, host, port, error }) {
|
|
348
493
|
if (this.#config.log.channel === "console") {
|
|
349
|
-
return;
|
|
494
|
+
return null;
|
|
350
495
|
}
|
|
351
496
|
|
|
352
497
|
const color = success ? "\x1b[32m" : "\x1b[31m";
|
|
@@ -357,7 +502,7 @@ class Server {
|
|
|
357
502
|
const pad = (content, length) => content + " ".repeat(Math.max(0, length - stripAnsi(content).length));
|
|
358
503
|
|
|
359
504
|
const version = `v${packageJson.version}`;
|
|
360
|
-
|
|
505
|
+
|
|
361
506
|
const logo = `
|
|
362
507
|
${color}███╗ ██╗██╗████████╗██████╗ ██████╗ ███╗ ██╗ ██╗███████╗
|
|
363
508
|
████╗ ██║██║╚══██╔══╝██╔══██╗██╔═══██╗████╗ ██║ ██║██╔════╝
|
|
@@ -384,15 +529,18 @@ ${color}███╗ ██╗██╗████████╗████
|
|
|
384
529
|
...(error?.code ? [`${bold}Code:${reset} ${error.code}`] : [])
|
|
385
530
|
];
|
|
386
531
|
|
|
387
|
-
|
|
388
|
-
|
|
532
|
+
const parts = [logo];
|
|
533
|
+
|
|
534
|
+
parts.push(`${color}┌${"─".repeat(boxWidth - 2)}┐${reset}`);
|
|
389
535
|
|
|
390
536
|
for (const line of lines) {
|
|
391
|
-
|
|
537
|
+
parts.push(`${color}│${reset} ${pad(line, boxWidth - 4)} ${color}│${reset}`);
|
|
392
538
|
}
|
|
393
539
|
|
|
394
|
-
|
|
395
|
-
|
|
540
|
+
parts.push(`${color}│${reset} ${" ".repeat(boxWidth - 4)} ${color}│${reset}`);
|
|
541
|
+
parts.push(`${color}└${"─".repeat(boxWidth - 2)}┘${reset}`);
|
|
542
|
+
|
|
543
|
+
return parts.join("\n");
|
|
396
544
|
}
|
|
397
545
|
}
|
|
398
546
|
|
package/lib/Logging/Log.js
CHANGED
|
@@ -70,13 +70,15 @@ class Log {
|
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
const sanitizedMessage = sanitizeLogValue(message);
|
|
74
|
+
const sanitizedContext = sanitizeLogContext(context);
|
|
73
75
|
const timestamp = new Date();
|
|
74
76
|
|
|
75
77
|
if (config.channel === "console") {
|
|
76
|
-
this.#logToConsole(level,
|
|
78
|
+
this.#logToConsole(level, sanitizedMessage, sanitizedContext, timestamp);
|
|
77
79
|
}
|
|
78
80
|
else if (config.channel === "file") {
|
|
79
|
-
this.#logToFile(level,
|
|
81
|
+
this.#logToFile(level, sanitizedMessage, sanitizedContext, timestamp, config);
|
|
80
82
|
}
|
|
81
83
|
}
|
|
82
84
|
catch (e) {
|
|
@@ -194,4 +196,34 @@ class Log {
|
|
|
194
196
|
}
|
|
195
197
|
}
|
|
196
198
|
|
|
199
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
200
|
+
// Private Helper Functions
|
|
201
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
function sanitizeLogValue(value) {
|
|
204
|
+
if (typeof value !== "string") return value;
|
|
205
|
+
|
|
206
|
+
return value.replace(/[\r\n]/g, " ");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function sanitizeLogContext(context) {
|
|
210
|
+
if (!context || typeof context !== "object") return context;
|
|
211
|
+
|
|
212
|
+
const result = {};
|
|
213
|
+
|
|
214
|
+
for (const [key, value] of Object.entries(context)) {
|
|
215
|
+
if (typeof value === "string") {
|
|
216
|
+
result[key] = sanitizeLogValue(value);
|
|
217
|
+
}
|
|
218
|
+
else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
219
|
+
result[key] = sanitizeLogContext(value);
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
result[key] = value;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
|
|
197
229
|
export default Log;
|
package/lib/Mail/Mail.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import nodemailer from "nodemailer";
|
|
2
|
-
import
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import Paths from "../Core/Paths.js";
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* Email sending utility with fluent API.
|
|
6
|
-
* Supports text, HTML
|
|
7
|
-
*
|
|
8
|
+
* Supports text, HTML, attachments, and calendar invites.
|
|
9
|
+
*
|
|
8
10
|
* @example
|
|
9
11
|
* await Mail.from("noreply@example.com")
|
|
10
12
|
* .to("user@example.com")
|
|
11
13
|
* .subject("Welcome!")
|
|
12
|
-
* .
|
|
14
|
+
* .html("<h1>Hello!</h1>")
|
|
13
15
|
* .send();
|
|
14
16
|
*/
|
|
15
17
|
class Mail {
|
|
@@ -112,14 +114,34 @@ class Mail {
|
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
/**
|
|
115
|
-
* Sets HTML content
|
|
116
|
-
* @param {
|
|
117
|
-
* @param {string} viewName - View template name
|
|
118
|
-
* @param {Object|null} data - Data to pass to the view
|
|
117
|
+
* Sets raw HTML content.
|
|
118
|
+
* @param {string} htmlString - HTML string
|
|
119
119
|
* @returns {Mail} This instance for chaining
|
|
120
120
|
*/
|
|
121
|
-
|
|
122
|
-
this.#htmlContent =
|
|
121
|
+
html(htmlString) {
|
|
122
|
+
this.#htmlContent = htmlString;
|
|
123
|
+
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Sets HTML content from a template file with placeholder replacement.
|
|
129
|
+
* Template files are loaded from resources/views/ directory.
|
|
130
|
+
* Placeholders use {{ key }} syntax.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} templateName - Template path relative to views dir (e.g. "emails/welcome")
|
|
133
|
+
* @param {Object} data - Data to replace placeholders with
|
|
134
|
+
* @returns {Mail} This instance for chaining
|
|
135
|
+
*/
|
|
136
|
+
view(templateName, data = {}) {
|
|
137
|
+
const filePath = path.join(Paths.views, `${templateName}.html`);
|
|
138
|
+
let content = fs.readFileSync(filePath, "utf-8");
|
|
139
|
+
|
|
140
|
+
for (const [key, value] of Object.entries(data)) {
|
|
141
|
+
content = content.replaceAll(`{{ ${key} }}`, escapeHtml(String(value ?? "")));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.#htmlContent = content;
|
|
123
145
|
|
|
124
146
|
return this;
|
|
125
147
|
}
|
|
@@ -184,4 +206,13 @@ class Mail {
|
|
|
184
206
|
}
|
|
185
207
|
}
|
|
186
208
|
|
|
209
|
+
function escapeHtml(str) {
|
|
210
|
+
return str
|
|
211
|
+
.replace(/&/g, "&")
|
|
212
|
+
.replace(/</g, "<")
|
|
213
|
+
.replace(/>/g, ">")
|
|
214
|
+
.replace(/"/g, """)
|
|
215
|
+
.replace(/'/g, "'");
|
|
216
|
+
}
|
|
217
|
+
|
|
187
218
|
export default Mail;
|
package/lib/Route/Router.js
CHANGED
|
@@ -154,7 +154,10 @@ class Router {
|
|
|
154
154
|
const isExcluded = csrf.except.some(pattern => {
|
|
155
155
|
if (route.url === pattern) return true;
|
|
156
156
|
if (pattern.includes("*")) {
|
|
157
|
-
|
|
157
|
+
const escaped = pattern
|
|
158
|
+
.replace(/[.+?^${}()|[\]\\]/g, "\\$&")
|
|
159
|
+
.replace(/\*/g, ".*");
|
|
160
|
+
return new RegExp("^" + escaped + "$").test(route.url);
|
|
158
161
|
}
|
|
159
162
|
return false;
|
|
160
163
|
});
|
|
@@ -165,6 +168,8 @@ class Router {
|
|
|
165
168
|
}
|
|
166
169
|
|
|
167
170
|
// Resolve middleware names to handlers
|
|
171
|
+
route.middlewareNames = route.middlewares.map(m => m.split(":")[0]);
|
|
172
|
+
|
|
168
173
|
route.resolvedMiddlewares = route.middlewares.map(middleware => {
|
|
169
174
|
const [name, param] = middleware.split(":");
|
|
170
175
|
|
|
@@ -183,8 +188,39 @@ class Router {
|
|
|
183
188
|
server.route({
|
|
184
189
|
method: route.method,
|
|
185
190
|
url: route.url,
|
|
186
|
-
preHandler:
|
|
187
|
-
|
|
191
|
+
preHandler: Environment.isDev
|
|
192
|
+
? route.resolvedMiddlewares.map((mw, i) => {
|
|
193
|
+
const mwName = route.middlewareNames[i];
|
|
194
|
+
return async (request, response) => {
|
|
195
|
+
const start = performance.now();
|
|
196
|
+
await mw(request, response);
|
|
197
|
+
if (request.__devCtx) {
|
|
198
|
+
request.__devCtx.middlewareTiming.push({
|
|
199
|
+
name: mwName,
|
|
200
|
+
duration: performance.now() - start
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
})
|
|
205
|
+
: route.resolvedMiddlewares,
|
|
206
|
+
handler: Environment.isDev
|
|
207
|
+
? async (request, response) => {
|
|
208
|
+
if (request.__devCtx) {
|
|
209
|
+
request.__devCtx.route = {
|
|
210
|
+
name: route.name,
|
|
211
|
+
pattern: route.url,
|
|
212
|
+
method: route.method,
|
|
213
|
+
middlewareNames: route.middlewareNames
|
|
214
|
+
};
|
|
215
|
+
request.__devCtx.controllerStart = performance.now();
|
|
216
|
+
}
|
|
217
|
+
const result = await route.resolvedHandler(request, response);
|
|
218
|
+
if (request.__devCtx) {
|
|
219
|
+
request.__devCtx.controllerDuration = performance.now() - request.__devCtx.controllerStart;
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
: route.resolvedHandler
|
|
188
224
|
});
|
|
189
225
|
}
|
|
190
226
|
}
|
|
@@ -253,7 +289,7 @@ class Router {
|
|
|
253
289
|
let url = route.url;
|
|
254
290
|
|
|
255
291
|
for (const key in params) {
|
|
256
|
-
url = url.replace(`:${key}`, params[key]);
|
|
292
|
+
url = url.replace(`:${key}`, encodeURIComponent(params[key]));
|
|
257
293
|
}
|
|
258
294
|
|
|
259
295
|
if (query && Object.keys(query).length > 0) {
|
|
@@ -284,8 +320,9 @@ class Router {
|
|
|
284
320
|
if (route.method !== method) continue;
|
|
285
321
|
|
|
286
322
|
const pattern = route.url
|
|
287
|
-
.
|
|
288
|
-
.replace(/\//g, "\\/")
|
|
323
|
+
.split(/:[^/]+/)
|
|
324
|
+
.map(s => s.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\//g, "\\/"))
|
|
325
|
+
.join("([^/]+)");
|
|
289
326
|
const regex = new RegExp(`^${pattern}$`);
|
|
290
327
|
const match = pathname.match(regex);
|
|
291
328
|
|
|
@@ -326,19 +363,6 @@ class Router {
|
|
|
326
363
|
return this.#kernel;
|
|
327
364
|
}
|
|
328
365
|
|
|
329
|
-
// Legacy compatibility (used by View/Manager.js)
|
|
330
|
-
static getSessionConfig() {
|
|
331
|
-
return Config.all("session");
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
static async getKernel() {
|
|
335
|
-
return this.#getKernel();
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Alias for backwards compatibility
|
|
339
|
-
static route(name, params, query) {
|
|
340
|
-
return this.url(name, params, query);
|
|
341
|
-
}
|
|
342
366
|
}
|
|
343
367
|
|
|
344
368
|
// ─────────────────────────────────────────────────────────────────────────────
|
package/lib/Runtime/Entry.js
CHANGED
|
@@ -7,6 +7,10 @@ Environment.setDev(process.env.__NITRON_DEV__ === "true");
|
|
|
7
7
|
export async function start() {
|
|
8
8
|
await Server.start();
|
|
9
9
|
|
|
10
|
+
if (process.send) {
|
|
11
|
+
process.send({ type: "ready" });
|
|
12
|
+
}
|
|
13
|
+
|
|
10
14
|
if (Environment.isDev && process.send) {
|
|
11
15
|
const { default: HMRServer } = await import("../HMR/Server.js");
|
|
12
16
|
|
|
@@ -14,11 +18,8 @@ export async function start() {
|
|
|
14
18
|
if (!msg?.type || !HMRServer.isReady) return;
|
|
15
19
|
|
|
16
20
|
switch (msg.type) {
|
|
17
|
-
case "
|
|
18
|
-
HMRServer.
|
|
19
|
-
break;
|
|
20
|
-
case "css":
|
|
21
|
-
HMRServer.emitCss(msg.filePath);
|
|
21
|
+
case "change":
|
|
22
|
+
HMRServer.emitChange(msg);
|
|
22
23
|
break;
|
|
23
24
|
case "reload":
|
|
24
25
|
HMRServer.emitReload(msg.reason);
|
|
@@ -33,5 +34,8 @@ export async function start() {
|
|
|
33
34
|
|
|
34
35
|
const isMain = process.argv[1]?.endsWith("Entry.js");
|
|
35
36
|
if (isMain) {
|
|
36
|
-
start()
|
|
37
|
+
start().catch(e => {
|
|
38
|
+
console.error(e);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
});
|
|
37
41
|
}
|