@nitronjs/framework 0.2.27 → 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 (58) 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 +1 -32
  7. package/lib/Build/Manager.js +354 -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 +171 -23
  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 +103 -1
  38. package/lib/Session/Session.js +0 -4
  39. package/lib/Support/Str.js +6 -4
  40. package/lib/Translation/Lang.js +376 -32
  41. package/lib/Translation/pluralize.js +81 -0
  42. package/lib/Validation/MagicBytes.js +120 -0
  43. package/lib/Validation/Validator.js +46 -29
  44. package/lib/View/Client/hmr-client.js +100 -90
  45. package/lib/View/Client/spa.js +121 -50
  46. package/lib/View/ClientManifest.js +60 -0
  47. package/lib/View/FlightRenderer.js +100 -0
  48. package/lib/View/Layout.js +0 -3
  49. package/lib/View/PropFilter.js +81 -0
  50. package/lib/View/View.js +230 -495
  51. package/lib/index.d.ts +22 -1
  52. package/package.json +2 -2
  53. package/skeleton/config/app.js +1 -0
  54. package/skeleton/config/server.js +13 -0
  55. package/skeleton/config/session.js +3 -0
  56. package/lib/Build/HydrationBuilder.js +0 -190
  57. package/lib/Console/Stubs/page-hydration-dev.tsx +0 -72
  58. 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 = "";
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
- 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) {
@@ -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 HMR routes before Router (dev only)
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.#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
+
337
481
  Log.info("Server started successfully!", { address, host, port, environment: Environment.isDev ? "development" : "production" });
338
482
  }
339
483
  catch (err) {
340
- this.#printBanner({ success: false, error: err });
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 #printBanner({ success, address, host, port, error }) {
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
- console.log(logo);
388
- console.log(`${color}┌${"─".repeat(boxWidth - 2)}┐${reset}`);
532
+ const parts = [logo];
533
+
534
+ parts.push(`${color}┌${"─".repeat(boxWidth - 2)}┐${reset}`);
389
535
 
390
536
  for (const line of lines) {
391
- console.log(`${color}│${reset} ${pad(line, boxWidth - 4)} ${color}│${reset}`);
537
+ parts.push(`${color}│${reset} ${pad(line, boxWidth - 4)} ${color}│${reset}`);
392
538
  }
393
539
 
394
- console.log(`${color}│${reset} ${" ".repeat(boxWidth - 4)} ${color}│${reset}`);
395
- 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");
396
544
  }
397
545
  }
398
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
  }