@nitronjs/framework 0.2.26 → 0.3.0

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.
Files changed (59) hide show
  1. package/README.md +260 -170
  2. package/lib/Auth/Auth.js +2 -2
  3. package/lib/Build/CssBuilder.js +5 -7
  4. package/lib/Build/EffectivePropUsage.js +174 -0
  5. package/lib/Build/FactoryTransform.js +1 -21
  6. package/lib/Build/FileAnalyzer.js +2 -33
  7. package/lib/Build/Manager.js +390 -58
  8. package/lib/Build/PropUsageAnalyzer.js +1189 -0
  9. package/lib/Build/jsxRuntime.js +25 -155
  10. package/lib/Build/plugins.js +212 -146
  11. package/lib/Build/propUtils.js +70 -0
  12. package/lib/Console/Commands/DevCommand.js +30 -10
  13. package/lib/Console/Commands/MakeCommand.js +8 -1
  14. package/lib/Console/Output.js +0 -2
  15. package/lib/Console/Stubs/rsc-consumer.tsx +74 -0
  16. package/lib/Console/Stubs/vendor-dev.tsx +30 -41
  17. package/lib/Console/Stubs/vendor.tsx +25 -1
  18. package/lib/Core/Config.js +0 -6
  19. package/lib/Core/Paths.js +0 -19
  20. package/lib/Database/Migration/Checksum.js +0 -3
  21. package/lib/Database/Migration/MigrationRepository.js +0 -8
  22. package/lib/Database/Migration/MigrationRunner.js +1 -2
  23. package/lib/Database/Model.js +19 -11
  24. package/lib/Database/QueryBuilder.js +25 -4
  25. package/lib/Database/Schema/Blueprint.js +10 -0
  26. package/lib/Database/Schema/Manager.js +2 -0
  27. package/lib/Date/DateTime.js +1 -1
  28. package/lib/Dev/DevContext.js +44 -0
  29. package/lib/Dev/DevErrorPage.js +990 -0
  30. package/lib/Dev/DevIndicator.js +836 -0
  31. package/lib/HMR/Server.js +16 -37
  32. package/lib/Http/Server.js +177 -24
  33. package/lib/Logging/Log.js +34 -2
  34. package/lib/Mail/Mail.js +41 -10
  35. package/lib/Route/Router.js +43 -19
  36. package/lib/Runtime/Entry.js +10 -6
  37. package/lib/Session/Manager.js +144 -1
  38. package/lib/Session/Redis.js +117 -0
  39. package/lib/Session/Session.js +0 -4
  40. package/lib/Support/Str.js +6 -4
  41. package/lib/Translation/Lang.js +376 -32
  42. package/lib/Translation/pluralize.js +81 -0
  43. package/lib/Validation/MagicBytes.js +120 -0
  44. package/lib/Validation/Validator.js +46 -29
  45. package/lib/View/Client/hmr-client.js +100 -90
  46. package/lib/View/Client/spa.js +121 -50
  47. package/lib/View/ClientManifest.js +60 -0
  48. package/lib/View/FlightRenderer.js +100 -0
  49. package/lib/View/Layout.js +0 -3
  50. package/lib/View/PropFilter.js +81 -0
  51. package/lib/View/View.js +230 -495
  52. package/lib/index.d.ts +22 -1
  53. package/package.json +3 -2
  54. package/skeleton/config/app.js +1 -0
  55. package/skeleton/config/server.js +13 -0
  56. package/skeleton/config/session.js +4 -0
  57. package/lib/Build/HydrationBuilder.js +0 -190
  58. package/lib/Console/Stubs/page-hydration-dev.tsx +0 -72
  59. 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.emitViewUpdate("resources/views/Site/Home.tsx");
20
- * HMR.emitCss("site_style.css");
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 (!this.#clientScript) {
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(fs.readFileSync(this.#clientScript, "utf-8"));
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
- * Number of currently connected clients.
79
- * @returns {number}
80
- */
81
- get connectionCount() {
82
- return this.#connections;
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
- emitCss(filePath) {
85
+ emitChange(data) {
109
86
  if (!this.#io) return;
110
87
 
111
- this.#io.emit("hmr:css", {
112
- file: filePath ? path.basename(filePath) : null,
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
  }
@@ -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: true,
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
- this.#server.addHook("preHandler", async (req) => {
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
- if (field?.toBuffer) {
214
- if (field.filename) {
215
- const lastDot = field.filename.lastIndexOf(".");
216
- field.extension = lastDot > 0 ? field.filename.slice(lastDot + 1).toLowerCase() : "";
217
- files[key] = field;
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 = "";
218
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
+ }
341
+ }
342
+
343
+ hasFile = true;
344
+ fileItems.push(item);
219
345
  }
220
- else if (field && typeof field === "object" && "value" in field) {
221
- normalized[key] = field.value;
346
+
347
+ if (fileItems.length === 1) {
348
+ files[key] = fileItems[0];
222
349
  }
223
- else {
224
- normalized[key] = field;
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) {
@@ -283,7 +413,12 @@ class Server {
283
413
  console.log("\x1b[32m✓ Server stopped gracefully\x1b[0m");
284
414
 
285
415
  try {
286
- (await SessionManager.getInstance()).stopGC();
416
+ const sessionManager = await SessionManager.getInstance();
417
+
418
+ await Promise.race([
419
+ sessionManager.close(),
420
+ new Promise(resolve => setTimeout(resolve, 1000))
421
+ ]);
287
422
  await Promise.race([
288
423
  DB.close(),
289
424
  new Promise(resolve => setTimeout(resolve, 1000))
@@ -305,15 +440,21 @@ class Server {
305
440
  await DB.setup();
306
441
  await SessionManager.setup(this.#server);
307
442
  Auth.setup(this.#server);
443
+ Lang.setup(this.#server);
308
444
 
309
- // Register HMR routes before Router (dev only)
445
+ // Register dev-only hooks and routes
310
446
  if (Environment.isDev) {
311
447
  const { default: HMRServer } = await import("../HMR/Server.js");
312
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
+ });
313
454
  }
314
455
 
315
456
  await Router.setup(this.#server);
316
- View.setup(this.#server);
457
+ await View.setup(this.#server);
317
458
 
318
459
  // Listen
319
460
  try {
@@ -328,20 +469,29 @@ class Server {
328
469
  HMRServer.setup(this.#server.server);
329
470
  }
330
471
 
331
- this.#printBanner({ success: true, address, host, port });
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
+
332
481
  Log.info("Server started successfully!", { address, host, port, environment: Environment.isDev ? "development" : "production" });
333
482
  }
334
483
  catch (err) {
335
- this.#printBanner({ success: false, error: err });
484
+ const banner = this.#renderBanner({ success: false, error: err });
485
+ if (banner) console.log(banner);
336
486
  Log.fatal("Server startup failed", { error: err.message, code: err.code, stack: err.stack });
337
487
  process.exit(1);
338
488
  }
339
489
  }
340
490
 
341
491
  // Private Methods
342
- static #printBanner({ success, address, host, port, error }) {
492
+ static #renderBanner({ success, address, host, port, error }) {
343
493
  if (this.#config.log.channel === "console") {
344
- return;
494
+ return null;
345
495
  }
346
496
 
347
497
  const color = success ? "\x1b[32m" : "\x1b[31m";
@@ -352,7 +502,7 @@ class Server {
352
502
  const pad = (content, length) => content + " ".repeat(Math.max(0, length - stripAnsi(content).length));
353
503
 
354
504
  const version = `v${packageJson.version}`;
355
-
505
+
356
506
  const logo = `
357
507
  ${color}███╗ ██╗██╗████████╗██████╗ ██████╗ ███╗ ██╗ ██╗███████╗
358
508
  ████╗ ██║██║╚══██╔══╝██╔══██╗██╔═══██╗████╗ ██║ ██║██╔════╝
@@ -379,15 +529,18 @@ ${color}███╗ ██╗██╗████████╗████
379
529
  ...(error?.code ? [`${bold}Code:${reset} ${error.code}`] : [])
380
530
  ];
381
531
 
382
- console.log(logo);
383
- console.log(`${color}┌${"─".repeat(boxWidth - 2)}┐${reset}`);
532
+ const parts = [logo];
533
+
534
+ parts.push(`${color}┌${"─".repeat(boxWidth - 2)}┐${reset}`);
384
535
 
385
536
  for (const line of lines) {
386
- console.log(`${color}│${reset} ${pad(line, boxWidth - 4)} ${color}│${reset}`);
537
+ parts.push(`${color}│${reset} ${pad(line, boxWidth - 4)} ${color}│${reset}`);
387
538
  }
388
539
 
389
- console.log(`${color}│${reset} ${" ".repeat(boxWidth - 4)} ${color}│${reset}`);
390
- console.log(`${color}└${"─".repeat(boxWidth - 2)}┘${reset}`);
540
+ parts.push(`${color}│${reset} ${" ".repeat(boxWidth - 4)} ${color}│${reset}`);
541
+ parts.push(`${color}└${"─".repeat(boxWidth - 2)}┘${reset}`);
542
+
543
+ return parts.join("\n");
391
544
  }
392
545
  }
393
546
 
@@ -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, message, context, timestamp);
78
+ this.#logToConsole(level, sanitizedMessage, sanitizedContext, timestamp);
77
79
  }
78
80
  else if (config.channel === "file") {
79
- this.#logToFile(level, message, context, timestamp, config);
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 View from "../View/View.js";
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 views, attachments, and calendar invites.
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
- * .view(res, "emails/welcome", { name: "John" })
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 from a view template.
116
- * @param {import("fastify").FastifyReply} res - Fastify response object
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
- view(res, viewName, data = null) {
122
- this.#htmlContent = View.renderFile(viewName, data);
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, "&amp;")
212
+ .replace(/</g, "&lt;")
213
+ .replace(/>/g, "&gt;")
214
+ .replace(/"/g, "&quot;")
215
+ .replace(/'/g, "&#39;");
216
+ }
217
+
187
218
  export default Mail;
@@ -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
- return new RegExp("^" + pattern.replace(/\*/g, ".*") + "$").test(route.url);
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: route.resolvedMiddlewares,
187
- handler: route.resolvedHandler
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
- .replace(/:[^/]+/g, "([^/]+)")
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
  // ─────────────────────────────────────────────────────────────────────────────
@@ -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 "view":
18
- HMRServer.emitViewUpdate(msg.filePath);
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
  }