@nitronjs/framework 0.1.24 → 0.2.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.
Files changed (41) hide show
  1. package/README.md +331 -253
  2. package/lib/Build/CssBuilder.js +129 -0
  3. package/lib/Build/FileAnalyzer.js +395 -0
  4. package/lib/Build/HydrationBuilder.js +173 -0
  5. package/lib/Build/Manager.js +290 -943
  6. package/lib/Build/colors.js +10 -0
  7. package/lib/Build/jsxRuntime.js +116 -0
  8. package/lib/Build/plugins.js +264 -0
  9. package/lib/Console/Commands/BuildCommand.js +6 -5
  10. package/lib/Console/Commands/DevCommand.js +151 -311
  11. package/lib/Console/Stubs/page-hydration-dev.tsx +72 -0
  12. package/lib/Console/Stubs/page-hydration.tsx +9 -10
  13. package/lib/Console/Stubs/vendor-dev.tsx +50 -0
  14. package/lib/Core/Environment.js +29 -2
  15. package/lib/Core/Paths.js +12 -4
  16. package/lib/Database/Drivers/MySQLDriver.js +5 -4
  17. package/lib/Database/Migration/MigrationRepository.js +2 -6
  18. package/lib/Database/Model.js +7 -8
  19. package/lib/Database/QueryBuilder.js +2 -3
  20. package/lib/Database/Seeder/SeederRepository.js +2 -6
  21. package/lib/Filesystem/Manager.js +32 -7
  22. package/lib/HMR/Server.js +87 -0
  23. package/lib/Http/Server.js +9 -5
  24. package/lib/Logging/Manager.js +68 -18
  25. package/lib/Route/Loader.js +3 -4
  26. package/lib/Route/Manager.js +24 -3
  27. package/lib/Runtime/Entry.js +26 -1
  28. package/lib/Session/File.js +18 -7
  29. package/lib/View/Client/hmr-client.js +166 -0
  30. package/lib/View/Client/spa.js +142 -0
  31. package/lib/View/Layout.js +94 -0
  32. package/lib/View/Manager.js +390 -46
  33. package/lib/index.d.ts +55 -0
  34. package/package.json +2 -1
  35. package/skeleton/.env.example +0 -2
  36. package/skeleton/app/Controllers/HomeController.js +4 -4
  37. package/skeleton/config/app.js +15 -14
  38. package/skeleton/config/session.js +1 -1
  39. package/skeleton/globals.d.ts +3 -63
  40. package/skeleton/resources/views/Site/Home.tsx +62 -42
  41. package/skeleton/tsconfig.json +5 -1
@@ -0,0 +1,50 @@
1
+ import * as React from 'react';
2
+ import * as ReactDOM from 'react-dom';
3
+ import * as ReactDOMClient from 'react-dom/client';
4
+ import * as ReactJSXRuntime from 'react/jsx-runtime';
5
+ import RefreshRuntime from 'react-refresh/runtime';
6
+
7
+ Object.assign(window, {
8
+ __NITRON_REACT__: React,
9
+ __NITRON_REACT_DOM__: ReactDOM,
10
+ __NITRON_REACT_DOM_CLIENT__: ReactDOMClient,
11
+ __NITRON_JSX_RUNTIME__: ReactJSXRuntime
12
+ });
13
+
14
+ RefreshRuntime.injectIntoGlobalHook(window);
15
+
16
+ interface RefreshModule {
17
+ performReactRefresh: () => void;
18
+ register: (type: any, id: string) => void;
19
+ createSignatureFunctionForTransform: () => (type: any, key: string, forceReset?: boolean, getCustomHooks?: () => any[]) => any;
20
+ isLikelyComponentType: (type: any) => boolean;
21
+ }
22
+
23
+ const NitronRefresh: RefreshModule = {
24
+ performReactRefresh: () => {
25
+ if ((RefreshRuntime as any).hasUnrecoverableErrors?.()) {
26
+ window.location.reload();
27
+ return;
28
+ }
29
+ RefreshRuntime.performReactRefresh();
30
+ },
31
+ register: (type: any, id: string) => {
32
+ RefreshRuntime.register(type, id);
33
+ },
34
+ createSignatureFunctionForTransform: () => {
35
+ return RefreshRuntime.createSignatureFunctionForTransform();
36
+ },
37
+ isLikelyComponentType: (type: any) => {
38
+ return RefreshRuntime.isLikelyComponentType(type);
39
+ }
40
+ };
41
+
42
+ (window as any).__NITRON_REFRESH__ = NitronRefresh;
43
+
44
+ (window as any).$RefreshReg$ = (type: any, id: string) => {
45
+ RefreshRuntime.register(type, id);
46
+ };
47
+
48
+ (window as any).$RefreshSig$ = () => {
49
+ return RefreshRuntime.createSignatureFunctionForTransform();
50
+ };
@@ -1,8 +1,36 @@
1
1
  import fs from "node:fs";
2
2
 
3
+ /**
4
+ * Environment Manager
5
+ *
6
+ * Handles environment detection and .env file operations.
7
+ * Development mode is detected automatically based on how the app was started.
8
+ */
3
9
  class Environment {
10
+ static #isDev = false;
4
11
 
5
- static update (key, value) {
12
+ /**
13
+ * Set development mode (called by DevCommand/BuildCommand)
14
+ */
15
+ static setDev(value) {
16
+ this.#isDev = value;
17
+ }
18
+
19
+ /**
20
+ * Check if running in development mode
21
+ */
22
+ static get isDev() {
23
+ return this.#isDev;
24
+ }
25
+
26
+ /**
27
+ * Check if running in production mode
28
+ */
29
+ static get isProd() {
30
+ return !this.#isDev;
31
+ }
32
+
33
+ static update(key, value) {
6
34
  let content = fs.readFileSync(".env", 'utf-8');
7
35
  const regex = new RegExp(`^${key}=.*`, 'm');
8
36
 
@@ -15,7 +43,6 @@ class Environment {
15
43
 
16
44
  fs.writeFileSync(".env", content.trim() + '\n');
17
45
  }
18
-
19
46
  }
20
47
 
21
48
  export default Environment;
package/lib/Core/Paths.js CHANGED
@@ -132,20 +132,28 @@ class Paths {
132
132
  return path.join(this.#project, "storage/app/public/js");
133
133
  }
134
134
 
135
+ // ─────────────────────────────────────────────────────────────────────────
136
+ // Build Output (.nitron - single hidden folder for all framework artifacts)
137
+ // ─────────────────────────────────────────────────────────────────────────
138
+
139
+ static get nitron() {
140
+ return path.join(this.#project, ".nitron");
141
+ }
142
+
135
143
  static get build() {
136
- return path.join(this.#project, "build");
144
+ return path.join(this.#project, ".nitron/build");
137
145
  }
138
146
 
139
147
  static get buildViews() {
140
- return path.join(this.#project, "build/views");
148
+ return path.join(this.#project, ".nitron/build/views");
141
149
  }
142
150
 
143
151
  static get buildFrameworkViews() {
144
- return path.join(this.#project, "build/framework/views");
152
+ return path.join(this.#project, ".nitron/build/framework/views");
145
153
  }
146
154
 
147
155
  static get nitronTemp() {
148
- return path.join(this.#project, ".nitron");
156
+ return path.join(this.#project, ".nitron/temp");
149
157
  }
150
158
 
151
159
  static get jsxRuntime() {
@@ -1,4 +1,5 @@
1
1
  import mysql from 'mysql2/promise';
2
+ import Environment from '../../Core/Environment.js';
2
3
 
3
4
  class MySQLDriver {
4
5
  #pool = null;
@@ -35,7 +36,7 @@ class MySQLDriver {
35
36
  }
36
37
 
37
38
  async query(sql, bindings = []) {
38
- const isProduction = process.env.APP_DEV === 'false';
39
+ const isProduction = Environment.isProd;
39
40
 
40
41
  try {
41
42
  const connection = await this.#pool.getConnection();
@@ -73,7 +74,7 @@ class MySQLDriver {
73
74
  }
74
75
 
75
76
  async raw(sql) {
76
- const isProduction = process.env.APP_DEV === 'false';
77
+ const isProduction = Environment.isProd;
77
78
 
78
79
  try {
79
80
  const [rows, fields] = await this.#pool.query(sql);
@@ -101,7 +102,7 @@ class MySQLDriver {
101
102
  }
102
103
 
103
104
  #maskSensitiveData(bindings) {
104
- if (process.env.APP_DEV === 'true') {
105
+ if (Environment.isDev) {
105
106
  return bindings; // Show all in development
106
107
  }
107
108
 
@@ -161,7 +162,7 @@ class MySQLDriver {
161
162
  }
162
163
 
163
164
  catch (error) {
164
- const isProduction = process.env.APP_DEV === 'false';
165
+ const isProduction = Environment.isProd;
165
166
 
166
167
  if (isProduction) {
167
168
  console.error('[MySQLDriver] Transaction query failed', {
@@ -5,12 +5,8 @@ class MigrationRepository {
5
5
  static table = 'migrations';
6
6
 
7
7
  static async tableExists() {
8
- try {
9
- await DB.table(this.table).limit(1).get();
10
- return true;
11
- } catch {
12
- return false;
13
- }
8
+ const [rows] = await DB.raw(`SHOW TABLES LIKE '${this.table}'`);
9
+ return rows.length > 0;
14
10
  }
15
11
 
16
12
  static async getExecuted() {
@@ -2,7 +2,6 @@ import DB from './DB.js';
2
2
 
3
3
  class Model {
4
4
  static table = null;
5
- static primaryKey = 'id';
6
5
 
7
6
  constructor(attrs = {}) {
8
7
  Object.defineProperty(this, '_attributes', { value: {}, writable: true });
@@ -63,7 +62,7 @@ class Model {
63
62
  throw new Error(`Model ${this.name} must define a static 'table' property`);
64
63
  }
65
64
 
66
- const row = await DB.table(this.table).where(this.primaryKey, id).first();
65
+ const row = await DB.table(this.table).where("id", id).first();
67
66
 
68
67
  if (!row) return null;
69
68
 
@@ -112,15 +111,15 @@ class Model {
112
111
  const data = {};
113
112
 
114
113
  for (const [key, value] of Object.entries(this._attributes)) {
115
- if (value !== undefined && (key !== constructor.primaryKey || !this._exists)) {
114
+ if (value !== undefined && (key !== "id" || !this._exists)) {
116
115
  data[key] = value;
117
116
  }
118
117
  }
119
118
 
120
119
  if (this._exists) {
121
- const primaryKeyValue = this._attributes[constructor.primaryKey];
120
+ const primaryKeyValue = this._attributes["id"];
122
121
  await DB.table(constructor.table)
123
- .where(constructor.primaryKey, primaryKeyValue)
122
+ .where("id", primaryKeyValue)
124
123
  .update(data);
125
124
 
126
125
  Object.assign(this._attributes, data);
@@ -128,7 +127,7 @@ class Model {
128
127
  }
129
128
  else {
130
129
  const id = await DB.table(constructor.table).insert(data);
131
- this._attributes[constructor.primaryKey] = id;
130
+ this._attributes["id"] = id;
132
131
 
133
132
  this._original = { ...this._attributes };
134
133
  this._exists = true;
@@ -139,14 +138,14 @@ class Model {
139
138
 
140
139
  async delete() {
141
140
  const constructor = this.constructor;
142
- const primaryKeyValue = this._attributes[constructor.primaryKey];
141
+ const primaryKeyValue = this._attributes["id"];
143
142
 
144
143
  if (!this._exists) {
145
144
  throw new Error('Cannot delete a model that does not exist');
146
145
  }
147
146
 
148
147
  await DB.table(constructor.table)
149
- .where(constructor.primaryKey, primaryKeyValue)
148
+ .where("id", primaryKeyValue)
150
149
  .delete();
151
150
 
152
151
  this._exists = false;
@@ -1,4 +1,5 @@
1
1
  import { validateDirection, validateIdentifier, validateWhereOperator } from "./QueryValidation.js";
2
+ import Environment from "../Core/Environment.js";
2
3
 
3
4
  class QueryBuilder {
4
5
  #table = null;
@@ -104,9 +105,7 @@ class QueryBuilder {
104
105
  }
105
106
 
106
107
  #sanitizeError(error) {
107
- const isProduction = process.env.APP_DEV === 'false';
108
-
109
- if (isProduction) {
108
+ if (Environment.isProd) {
110
109
  console.error('[QueryBuilder] Database error:', {
111
110
  message: error.message,
112
111
  code: error.code,
@@ -5,12 +5,8 @@ class SeederRepository {
5
5
  static table = 'seeders';
6
6
 
7
7
  static async tableExists() {
8
- try {
9
- await DB.table(this.table).limit(1).get();
10
- return true;
11
- } catch {
12
- return false;
13
- }
8
+ const [rows] = await DB.raw(`SHOW TABLES LIKE '${this.table}'`);
9
+ return rows.length > 0;
14
10
  }
15
11
 
16
12
  static async getExecuted() {
@@ -6,21 +6,39 @@ class FilesystemManager {
6
6
  static #publicRoot = Paths.storagePublic;
7
7
  static #privateRoot = Paths.storagePrivate;
8
8
 
9
+ /**
10
+ * Validate path to prevent directory traversal attacks
11
+ * @throws Error if path escapes base directory
12
+ */
13
+ static #validatePath(base, filePath) {
14
+ const normalizedBase = path.normalize(base) + path.sep;
15
+ const fullPath = path.normalize(path.join(base, filePath));
16
+
17
+ if (!fullPath.startsWith(normalizedBase)) {
18
+ throw new Error("Invalid file path: directory traversal detected");
19
+ }
20
+
21
+ return fullPath;
22
+ }
23
+
9
24
  static async get(filePath, isPrivate = false) {
10
25
  const base = isPrivate ? this.#privateRoot : this.#publicRoot;
11
- const fullPath = path.join(base, filePath);
12
-
26
+
13
27
  try {
28
+ const fullPath = this.#validatePath(base, filePath);
14
29
  return await fs.promises.readFile(fullPath);
15
- } catch {
30
+ } catch (err) {
31
+ if (err.message.includes("directory traversal")) throw err;
16
32
  return null;
17
33
  }
18
34
  }
19
35
 
20
36
  static async put(file, dir, fileName, isPrivate = false) {
21
37
  const base = isPrivate ? this.#privateRoot : this.#publicRoot;
22
- const folderPath = path.join(base, dir);
23
- const fullPath = path.join(folderPath, fileName);
38
+
39
+ // Validate both dir and fileName
40
+ const folderPath = this.#validatePath(base, dir);
41
+ const fullPath = this.#validatePath(base, path.join(dir, fileName));
24
42
 
25
43
  await fs.promises.mkdir(folderPath, { recursive: true });
26
44
  await fs.promises.writeFile(fullPath, file._buf);
@@ -30,12 +48,19 @@ class FilesystemManager {
30
48
 
31
49
  static async delete(filePath, isPrivate = false) {
32
50
  const base = isPrivate ? this.#privateRoot : this.#publicRoot;
33
- await fs.promises.unlink(path.join(base, filePath));
51
+ const fullPath = this.#validatePath(base, filePath);
52
+ await fs.promises.unlink(fullPath);
34
53
  }
35
54
 
36
55
  static exists(filePath, isPrivate = false) {
37
56
  const base = isPrivate ? this.#privateRoot : this.#publicRoot;
38
- return fs.existsSync(path.join(base, filePath));
57
+
58
+ try {
59
+ const fullPath = this.#validatePath(base, filePath);
60
+ return fs.existsSync(fullPath);
61
+ } catch {
62
+ return false;
63
+ }
39
64
  }
40
65
 
41
66
  static url(filePath) {
@@ -0,0 +1,87 @@
1
+ import { Server as SocketServer } from "socket.io";
2
+ import path from "path";
3
+
4
+ class HMRServer {
5
+ #io = null;
6
+ #ready = false;
7
+ #connections = 0;
8
+
9
+ setup(httpServer) {
10
+ if (this.#io) return;
11
+
12
+ this.#io = new SocketServer(httpServer, {
13
+ path: "/__nitron_hmr",
14
+ transports: ["websocket", "polling"],
15
+ cors: { origin: "*" },
16
+ pingTimeout: 60000,
17
+ pingInterval: 25000,
18
+ serveClient: true
19
+ });
20
+
21
+ this.#io.on("connection", (socket) => {
22
+ this.#connections++;
23
+ socket.on("disconnect", () => { this.#connections--; });
24
+ });
25
+
26
+ this.#ready = true;
27
+ }
28
+
29
+ get isReady() {
30
+ return this.#ready && this.#io !== null;
31
+ }
32
+
33
+ get connectionCount() {
34
+ return this.#connections;
35
+ }
36
+
37
+ emitViewUpdate(filePath) {
38
+ if (!this.#io) return;
39
+
40
+ const normalized = filePath.replace(/\\/g, "/");
41
+ const viewsMatch = normalized.match(/resources\/views\/(.+)\.tsx$/);
42
+ const viewPath = viewsMatch ? viewsMatch[1].toLowerCase() : path.basename(filePath, ".tsx").toLowerCase();
43
+
44
+ this.#io.emit("hmr:update", {
45
+ type: "view",
46
+ file: viewPath,
47
+ url: `/js/${viewPath}.js`,
48
+ timestamp: Date.now()
49
+ });
50
+ }
51
+
52
+ emitCss(filePath) {
53
+ if (!this.#io) return;
54
+ this.#io.emit("hmr:css", {
55
+ file: filePath ? path.basename(filePath) : null,
56
+ timestamp: Date.now()
57
+ });
58
+ }
59
+
60
+ emitReload(reason) {
61
+ if (!this.#io) return;
62
+ this.#io.emit("hmr:reload", {
63
+ reason,
64
+ timestamp: Date.now()
65
+ });
66
+ }
67
+
68
+ emitError(error, filePath) {
69
+ if (!this.#io) return;
70
+ this.#io.emit("hmr:error", {
71
+ file: filePath,
72
+ message: String(error?.message || error),
73
+ timestamp: Date.now()
74
+ });
75
+ }
76
+
77
+ close() {
78
+ if (this.#io) {
79
+ this.#io.close();
80
+ this.#io = null;
81
+ }
82
+ this.#ready = false;
83
+ this.#connections = 0;
84
+ }
85
+ }
86
+
87
+ export default new HMRServer();
@@ -9,6 +9,7 @@ import fastifyHelmet from "@fastify/helmet";
9
9
  import fastifyMultipart from "@fastify/multipart";
10
10
  import Paths from "../Core/Paths.js";
11
11
  import Config from "../Core/Config.js";
12
+ import Environment from "../Core/Environment.js";
12
13
  import Route from "../Route/Manager.js";
13
14
  import View from "../View/Manager.js";
14
15
  import Auth from "../Auth/Manager.js";
@@ -16,8 +17,7 @@ import SessionManager from "../Session/Manager.js";
16
17
  import DB from "../Database/DB.js";
17
18
  import Log from "../Logging/Manager.js";
18
19
  import Loader from "../Route/Loader.js";
19
-
20
- const IS_DEV = process.env.APP_DEV === "true";
20
+ import HMRServer from "../HMR/Server.js";
21
21
 
22
22
  class Server {
23
23
  static #server;
@@ -91,7 +91,7 @@ class Server {
91
91
  dnsPrefetchControl: { allow: false },
92
92
  frameguard: { action: "deny" },
93
93
  hidePoweredBy: true,
94
- hsts: !IS_DEV,
94
+ hsts: !Environment.isDev,
95
95
  ieNoOpen: true,
96
96
  noSniff: true,
97
97
  originAgentCluster: true,
@@ -239,13 +239,17 @@ class Server {
239
239
  const port = Number(process.env.APP_PORT) || 3000;
240
240
  const address = await this.#server.listen({ host, port });
241
241
 
242
+ if (Environment.isDev) {
243
+ HMRServer.setup(this.#server.server);
244
+ }
245
+
242
246
  this.#printBanner({ success: true, address, host, port });
243
247
 
244
248
  Log.info("Server started successfully!", {
245
249
  address,
246
250
  host,
247
251
  port,
248
- environment: IS_DEV ? "development" : "production"
252
+ environment: Environment.isDev ? "development" : "production"
249
253
  });
250
254
  }
251
255
  catch (err) {
@@ -292,7 +296,7 @@ ${color}███╗ ██╗██╗████████╗████
292
296
  `${bold}Address:${reset} ${address}`,
293
297
  `${bold}Host:${reset} ${host}`,
294
298
  `${bold}Port:${reset} ${port}`,
295
- `${bold}Mode:${reset} ${IS_DEV ? "development" : "production"}`
299
+ `${bold}Mode:${reset} ${Environment.isDev ? "development" : "production"}`
296
300
  ]
297
301
  : [
298
302
  `${color}${bold}✕${reset} ${bold}Server failed to start${reset}`,
@@ -11,6 +11,14 @@ class LogManager {
11
11
  fatal: 4
12
12
  };
13
13
 
14
+ static #levelLabels = {
15
+ debug: "DEBUG",
16
+ info: "INFO ",
17
+ warn: "WARN ",
18
+ error: "ERROR",
19
+ fatal: "FATAL"
20
+ };
21
+
14
22
  static debug(message, context = {}) {
15
23
  this.#log("debug", message, context);
16
24
  }
@@ -48,13 +56,7 @@ class LogManager {
48
56
  return;
49
57
  }
50
58
 
51
- const payload = {
52
- level,
53
- message,
54
- context,
55
- timestamp: new Date().toISOString(),
56
- pid: process.pid
57
- };
59
+ const timestamp = new Date();
58
60
 
59
61
  const channel = config.channel;
60
62
 
@@ -63,11 +65,11 @@ class LogManager {
63
65
  }
64
66
 
65
67
  if (channel === "console") {
66
- this.#logToConsole(payload);
68
+ this.#logToConsole(level, message, context, timestamp);
67
69
  }
68
70
 
69
71
  if (channel === "file") {
70
- this.#logToFile(payload, config);
72
+ this.#logToFile(level, message, context, timestamp, config);
71
73
  }
72
74
  } catch (e) {
73
75
  console.error("Logger failure:", e.message);
@@ -79,7 +81,18 @@ class LogManager {
79
81
  return this.#levels[level] >= this.#levels[configLevel];
80
82
  }
81
83
 
82
- static #logToConsole({ level, message, context, timestamp }) {
84
+ static #formatTimestamp(date) {
85
+ const year = date.getFullYear();
86
+ const month = String(date.getMonth() + 1).padStart(2, "0");
87
+ const day = String(date.getDate()).padStart(2, "0");
88
+ const hours = String(date.getHours()).padStart(2, "0");
89
+ const minutes = String(date.getMinutes()).padStart(2, "0");
90
+ const seconds = String(date.getSeconds()).padStart(2, "0");
91
+ const ms = String(date.getMilliseconds()).padStart(3, "0");
92
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
93
+ }
94
+
95
+ static #logToConsole(level, message, context, timestamp) {
83
96
  const colors = {
84
97
  debug: "\x1b[90m",
85
98
  info: "\x1b[36m",
@@ -102,14 +115,12 @@ class LogManager {
102
115
  const bold = "\x1b[1m";
103
116
  const dim = "\x1b[2m";
104
117
 
105
- const date = new Date(timestamp);
106
- const time = date.toLocaleTimeString("en-US", { hour12: false });
107
- const dateStr = date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
118
+ const time = this.#formatTimestamp(timestamp);
108
119
  const width = process.stdout.columns || 80;
109
120
 
110
121
  console.log(`${color}┌${"─".repeat(width - 2)}${reset}`);
111
122
  console.log(`${color}│${reset} ${color}${icon}${reset} ${bold}${level.toUpperCase()}${reset}`);
112
- console.log(`${color}│${reset} ${dim}${dateStr} at ${time}${reset}`);
123
+ console.log(`${color}│${reset} ${dim}${time}${reset}`);
113
124
  console.log(`${color}├${"─".repeat(width - 2)}${reset}`);
114
125
  console.log(`${color}│${reset}`);
115
126
  console.log(`${color}│${reset} ${bold}${message}${reset}`);
@@ -128,7 +139,32 @@ class LogManager {
128
139
  console.log();
129
140
  }
130
141
 
131
- static #logToFile(payload, config) {
142
+ static #formatContext(context, indent = "") {
143
+ const lines = [];
144
+
145
+ for (const [key, value] of Object.entries(context)) {
146
+ if (value === null || value === undefined) continue;
147
+
148
+ if (typeof value === "object" && !Array.isArray(value)) {
149
+ lines.push(`${indent}${key}:`);
150
+ lines.push(...this.#formatContext(value, indent + " "));
151
+ }
152
+ else if (key === "stack" && typeof value === "string") {
153
+ // Format stack trace nicely
154
+ lines.push(`${indent}${key}:`);
155
+ value.split("\n").forEach(line => {
156
+ lines.push(`${indent} ${line.trim()}`);
157
+ });
158
+ }
159
+ else {
160
+ lines.push(`${indent}${key}: ${value}`);
161
+ }
162
+ }
163
+
164
+ return lines;
165
+ }
166
+
167
+ static #logToFile(level, message, context, timestamp, config) {
132
168
  const filePath = path.resolve(process.cwd(), config.file);
133
169
  const dir = path.dirname(filePath);
134
170
 
@@ -136,12 +172,26 @@ class LogManager {
136
172
  fs.mkdirSync(dir, { recursive: true });
137
173
  }
138
174
 
139
- const logLine = JSON.stringify(payload) + "\n";
175
+ const time = this.#formatTimestamp(timestamp);
176
+ const label = this.#levelLabels[level];
177
+
178
+ // Build readable log entry
179
+ const lines = [];
180
+ lines.push(`[${time}] [${label}] ${message}`);
181
+
182
+ if (Object.keys(context).length > 0) {
183
+ const contextLines = this.#formatContext(context, " ");
184
+ lines.push(...contextLines);
185
+ }
186
+
187
+ lines.push(""); // Empty line between entries
188
+
189
+ const logEntry = lines.join("\n") + "\n";
140
190
 
141
191
  if (config.sync) {
142
- fs.appendFileSync(filePath, logLine);
192
+ fs.appendFileSync(filePath, logEntry);
143
193
  } else {
144
- fs.appendFile(filePath, logLine, (err) => {
194
+ fs.appendFile(filePath, logEntry, (err) => {
145
195
  if (err) {
146
196
  console.error("Failed to write log:", err.message);
147
197
  }
@@ -1,8 +1,7 @@
1
1
  import path from "path";
2
2
  import fs from "fs";
3
3
  import Paths from "../Core/Paths.js";
4
-
5
- const IS_DEV = process.env.APP_DEV === "true";
4
+ import Environment from "../Core/Environment.js";
6
5
 
7
6
  const DIRECTORIES = [
8
7
  Paths.controllers,
@@ -14,7 +13,7 @@ class Loader {
14
13
  #initialized = false;
15
14
 
16
15
  async initialize() {
17
- if (this.#initialized || !IS_DEV) return;
16
+ if (this.#initialized || !Environment.isDev) return;
18
17
 
19
18
  for (const dir of DIRECTORIES) {
20
19
  await this.#loadDirectory(dir);
@@ -24,7 +23,7 @@ class Loader {
24
23
  }
25
24
 
26
25
  wrapHandler(handler) {
27
- if (!IS_DEV) return handler;
26
+ if (!Environment.isDev) return handler;
28
27
 
29
28
  const info = this.#registry.get(handler);
30
29
  if (!info) return handler;