@nitronjs/framework 0.2.3 → 0.2.5

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 (68) hide show
  1. package/README.md +3 -1
  2. package/cli/create.js +88 -72
  3. package/cli/njs.js +14 -7
  4. package/lib/Auth/Auth.js +167 -0
  5. package/lib/Build/CssBuilder.js +9 -0
  6. package/lib/Build/FileAnalyzer.js +16 -0
  7. package/lib/Build/HydrationBuilder.js +17 -0
  8. package/lib/Build/Manager.js +15 -0
  9. package/lib/Build/colors.js +4 -0
  10. package/lib/Build/plugins.js +84 -20
  11. package/lib/Console/Commands/DevCommand.js +13 -9
  12. package/lib/Console/Commands/MakeCommand.js +24 -10
  13. package/lib/Console/Commands/MigrateCommand.js +0 -1
  14. package/lib/Console/Commands/MigrateFreshCommand.js +17 -25
  15. package/lib/Console/Commands/MigrateRollbackCommand.js +6 -3
  16. package/lib/Console/Commands/MigrateStatusCommand.js +6 -3
  17. package/lib/Console/Commands/SeedCommand.js +4 -2
  18. package/lib/Console/Commands/StorageLinkCommand.js +20 -5
  19. package/lib/Console/Output.js +142 -0
  20. package/lib/Core/Config.js +2 -1
  21. package/lib/Core/Paths.js +8 -0
  22. package/lib/Database/DB.js +141 -51
  23. package/lib/Database/Drivers/MySQLDriver.js +102 -157
  24. package/lib/Database/Migration/Checksum.js +3 -8
  25. package/lib/Database/Migration/MigrationRepository.js +25 -35
  26. package/lib/Database/Migration/MigrationRunner.js +56 -61
  27. package/lib/Database/Model.js +157 -83
  28. package/lib/Database/QueryBuilder.js +31 -0
  29. package/lib/Database/QueryValidation.js +36 -44
  30. package/lib/Database/Schema/Blueprint.js +25 -36
  31. package/lib/Database/Schema/Manager.js +31 -68
  32. package/lib/Database/Seeder/SeederRunner.js +12 -31
  33. package/lib/Date/DateTime.js +9 -0
  34. package/lib/Encryption/Encryption.js +52 -0
  35. package/lib/Faker/Faker.js +11 -0
  36. package/lib/Filesystem/Storage.js +120 -0
  37. package/lib/HMR/Server.js +81 -10
  38. package/lib/Hashing/Hash.js +41 -0
  39. package/lib/Http/Server.js +177 -152
  40. package/lib/Logging/{Manager.js → Log.js} +68 -80
  41. package/lib/Mail/Mail.js +187 -0
  42. package/lib/Route/Router.js +416 -0
  43. package/lib/Session/File.js +135 -233
  44. package/lib/Session/Manager.js +117 -171
  45. package/lib/Session/Memory.js +28 -38
  46. package/lib/Session/Session.js +71 -107
  47. package/lib/Support/Str.js +103 -0
  48. package/lib/Translation/Lang.js +54 -0
  49. package/lib/View/Client/hmr-client.js +94 -51
  50. package/lib/View/Client/nitronjs-icon.png +0 -0
  51. package/lib/View/{Manager.js → View.js} +44 -29
  52. package/lib/index.d.ts +42 -8
  53. package/lib/index.js +19 -12
  54. package/package.json +1 -1
  55. package/skeleton/app/Controllers/HomeController.js +7 -1
  56. package/skeleton/resources/css/global.css +1 -0
  57. package/skeleton/resources/views/Site/Home.tsx +456 -79
  58. package/skeleton/tsconfig.json +6 -1
  59. package/lib/Auth/Manager.js +0 -111
  60. package/lib/Database/Connection.js +0 -61
  61. package/lib/Database/Manager.js +0 -162
  62. package/lib/Encryption/Manager.js +0 -47
  63. package/lib/Filesystem/Manager.js +0 -74
  64. package/lib/Hashing/Manager.js +0 -25
  65. package/lib/Mail/Manager.js +0 -120
  66. package/lib/Route/Loader.js +0 -80
  67. package/lib/Route/Manager.js +0 -286
  68. package/lib/Translation/Manager.js +0 -49
@@ -1,103 +1,66 @@
1
- import DatabaseManager from "../Manager.js";
1
+ import DB from "../DB.js";
2
2
  import Config from "../../Core/Config.js";
3
3
 
4
4
  export default class Schema {
5
5
 
6
+ // Public Methods
6
7
  static async create(tableName, callback) {
7
8
  const blueprint = new (await import('./Blueprint.js')).default(tableName);
8
9
  callback(blueprint);
9
-
10
- const sql = this.#buildCreateTableSQL(blueprint, false);
11
-
12
- const connection = DatabaseManager.getInstance().connection();
13
- await connection.raw(sql);
10
+ await DB.rawQuery(this.#buildCreateSQL(blueprint, false));
14
11
  }
15
12
 
16
13
  static async createIfNotExists(tableName, callback) {
17
14
  const blueprint = new (await import('./Blueprint.js')).default(tableName);
18
15
  callback(blueprint);
19
-
20
- const sql = this.#buildCreateTableSQL(blueprint, true);
21
-
22
- const connection = DatabaseManager.getInstance().connection();
23
- await connection.raw(sql);
16
+ await DB.rawQuery(this.#buildCreateSQL(blueprint, true));
24
17
  }
25
18
 
26
19
  static async dropIfExists(tableName) {
27
- const connection = DatabaseManager.getInstance().connection();
28
- await connection.raw(`DROP TABLE IF EXISTS \`${tableName}\``);
20
+ await DB.rawQuery(`DROP TABLE IF EXISTS \`${tableName}\``);
29
21
  }
30
22
 
31
- static #buildCreateTableSQL(blueprint, ifNotExists = false) {
32
- const columns = blueprint.getColumns();
33
- const columnsSql = columns.map(col => this.#buildColumnSQL(col));
34
-
35
- const manager = DatabaseManager.getInstance();
36
- const connection = manager.connection();
37
- const connectionName = connection.getName();
38
- const databaseConfig = Config.all('database');
39
- const dbConfig = databaseConfig.connections[connectionName];
23
+ // Private Methods
24
+ static #buildCreateSQL(blueprint, ifNotExists = false) {
25
+ const columns = blueprint.getColumns().map(col => this.#buildColumnSQL(col));
26
+ const dbConfig = Config.all('database').connections.mysql;
40
27
 
41
28
  const charset = dbConfig.charset || 'utf8mb4';
42
29
  const collation = dbConfig.collation || 'utf8mb4_unicode_ci';
43
-
44
30
  const ifNotExistsClause = ifNotExists ? 'IF NOT EXISTS ' : '';
45
- let sql = `CREATE TABLE ${ifNotExistsClause}\`${blueprint.getTableName()}\` (\n`;
46
- sql += ' ' + columnsSql.join(',\n ');
47
- sql += `\n) ENGINE=InnoDB DEFAULT CHARSET=${charset} COLLATE=${collation}`;
48
31
 
49
- return sql;
32
+ return `CREATE TABLE ${ifNotExistsClause}\`${blueprint.getTableName()}\` (\n ${columns.join(',\n ')}\n) ENGINE=InnoDB DEFAULT CHARSET=${charset} COLLATE=${collation}`;
50
33
  }
51
34
 
52
35
  static #buildColumnSQL(column) {
53
- let sql = `\`${column.name}\` `;
54
-
55
- switch (column.type) {
56
- case 'id':
57
- sql += 'BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY';
58
- return sql;
59
- case 'string':
60
- sql += `VARCHAR(${column.length || 255})`;
61
- break;
62
- case 'text':
63
- sql += 'TEXT';
64
- break;
65
- case 'integer':
66
- sql += 'INT';
67
- break;
68
- case 'bigInteger':
69
- sql += 'BIGINT';
70
- break;
71
- case 'boolean':
72
- sql += 'TINYINT(1)';
73
- break;
74
- case 'timestamp':
75
- sql += 'TIMESTAMP';
76
- break;
77
- case 'json':
78
- sql += 'JSON';
79
- break;
80
- default:
81
- throw new Error(`Unknown column type: ${column.type}`);
36
+ if (column.type === 'id') {
37
+ return `\`${column.name}\` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY`;
82
38
  }
83
39
 
84
- if (column.modifiers) {
85
- if (column.modifiers.nullable) {
86
- sql += ' NULL';
87
- } else {
88
- sql += ' NOT NULL';
89
- }
40
+ const typeMap = {
41
+ string: `VARCHAR(${column.length || 255})`,
42
+ text: 'TEXT',
43
+ integer: 'INT',
44
+ bigInteger: 'BIGINT',
45
+ boolean: 'TINYINT(1)',
46
+ timestamp: 'TIMESTAMP',
47
+ json: 'JSON'
48
+ };
49
+
50
+ const type = typeMap[column.type];
51
+ if (!type) throw new Error(`Unknown column type: ${column.type}`);
52
+
53
+ let sql = `\`${column.name}\` ${type}`;
90
54
 
55
+ if (column.modifiers) {
56
+ sql += column.modifiers.nullable ? ' NULL' : ' NOT NULL';
91
57
  if (column.modifiers.default !== null) {
92
- const defaultValue = typeof column.modifiers.default === 'string'
58
+ const val = typeof column.modifiers.default === 'string'
93
59
  ? `'${column.modifiers.default}'`
94
60
  : column.modifiers.default;
95
- sql += ` DEFAULT ${defaultValue}`;
96
- }
97
-
98
- if (column.modifiers.unique) {
99
- sql += ' UNIQUE';
61
+ sql += ` DEFAULT ${val}`;
100
62
  }
63
+ if (column.modifiers.unique) sql += ' UNIQUE';
101
64
  }
102
65
 
103
66
  return sql;
@@ -2,80 +2,61 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { pathToFileURL } from 'url';
4
4
  import Paths from '../../Core/Paths.js';
5
-
6
- const COLORS = {
7
- reset: '\x1b[0m',
8
- red: '\x1b[31m',
9
- green: '\x1b[32m',
10
- yellow: '\x1b[33m',
11
- cyan: '\x1b[36m',
12
- dim: '\x1b[2m',
13
- bold: '\x1b[1m'
14
- };
5
+ import Output from '../../Console/Output.js';
15
6
 
16
7
  class SeederRunner {
17
-
18
8
  static async run(seederName = null) {
19
9
  const seedersDir = Paths.seeders;
20
10
 
21
11
  if (!fs.existsSync(seedersDir)) {
22
- console.log(`${COLORS.yellow}⚠️ No seeders directory found${COLORS.reset}`);
12
+ Output.warn("No seeders directory found");
23
13
  return { success: true, ran: [] };
24
14
  }
25
15
 
26
- let files = fs.readdirSync(seedersDir)
27
- .filter(f => f.endsWith('.js'))
28
- .sort();
16
+ let files = fs.readdirSync(seedersDir).filter(f => f.endsWith('.js')).sort();
29
17
 
30
18
  if (files.length === 0) {
31
- console.log(`${COLORS.yellow}⚠️ No seeder files found${COLORS.reset}`);
19
+ Output.warn("No seeder files found");
32
20
  return { success: true, ran: [] };
33
21
  }
34
22
 
35
- // If specific seeder name provided, filter to only that one
36
23
  if (seederName) {
37
24
  const targetFile = seederName.endsWith('.js') ? seederName : `${seederName}.js`;
38
25
  files = files.filter(f => f === targetFile);
39
-
40
26
  if (files.length === 0) {
41
- console.log(`${COLORS.red}❌ Seeder not found: ${seederName}${COLORS.reset}`);
27
+ Output.error(`Seeder not found: ${seederName}`);
42
28
  return { success: false, ran: [], error: new Error(`Seeder not found: ${seederName}`) };
43
29
  }
44
30
  }
45
31
 
46
- console.log(`${COLORS.cyan}🌱 Running seeders${COLORS.reset}\n`);
32
+ Output.seederHeader();
47
33
 
48
34
  const executed = [];
49
35
 
50
36
  try {
51
37
  for (const file of files) {
52
- const filePath = path.join(seedersDir, file);
53
- const fileUrl = pathToFileURL(filePath).href;
54
-
55
- console.log(`${COLORS.dim}Seeding:${COLORS.reset} ${COLORS.cyan}${file}${COLORS.reset}`);
38
+ const fileUrl = pathToFileURL(path.join(seedersDir, file)).href;
39
+ Output.pending("Seeding", file);
56
40
 
57
41
  const { default: seeder } = await import(`${fileUrl}?t=${Date.now()}`);
58
-
59
42
  if (typeof seeder.run !== 'function') {
60
43
  throw new Error(`Seeder ${file} does not have a run() method`);
61
44
  }
62
45
 
63
46
  await seeder.run();
64
47
  executed.push(file);
65
-
66
- console.log(`${COLORS.green}✅ Seeded:${COLORS.reset} ${COLORS.cyan}${file}${COLORS.reset}\n`);
48
+ Output.done("Seeded", file);
67
49
  }
68
50
 
69
- console.log(`${COLORS.green}${COLORS.bold}✅ All seeders completed successfully.${COLORS.reset}`);
51
+ Output.seederSuccess();
70
52
  return { success: true, ran: executed };
71
-
72
53
  }
73
54
  catch (error) {
74
- console.error(`\n${COLORS.red}❌ Seeding failed: ${error.message}${COLORS.reset}`);
55
+ Output.newline();
56
+ Output.error(`Seeding failed: ${error.message}`);
75
57
  return { success: false, ran: executed, error };
76
58
  }
77
59
  }
78
-
79
60
  }
80
61
 
81
62
  export default SeederRunner;
@@ -1,6 +1,15 @@
1
1
  import locale from './Locale.js';
2
2
  import Config from '../Core/Config.js';
3
3
 
4
+ /**
5
+ * Date and time utility with timezone and localization support.
6
+ * Uses config/app.js timezone and locale settings.
7
+ *
8
+ * @example
9
+ * DateTime.toSQL(); // "2025-01-15 10:30:00"
10
+ * DateTime.addDays(7); // 7 days from now
11
+ * DateTime.diffForHumans(ts); // "2 hours ago"
12
+ */
4
13
  class DateTime {
5
14
  static #getDate(date = null) {
6
15
  const timezone = Config.get('app.timezone', 'UTC');
@@ -0,0 +1,52 @@
1
+ import crypto from "crypto";
2
+
3
+ /**
4
+ * AES-256-CBC encryption and decryption utility.
5
+ * Uses APP_KEY from environment for secure encryption.
6
+ */
7
+ class Encryption {
8
+ /**
9
+ * Encrypts a value using AES-256-CBC.
10
+ * @param {string|Object} value - Value to encrypt (objects are JSON stringified)
11
+ * @returns {string} Encrypted string in format "iv:encryptedData"
12
+ */
13
+ static encrypt(value) {
14
+ if (typeof value === "object") {
15
+ value = JSON.stringify(value);
16
+ }
17
+
18
+ const secretKey = crypto.createHash("sha256").update(process.env.APP_KEY).digest();
19
+ const iv = crypto.randomBytes(16);
20
+ const cipher = crypto.createCipheriv("aes-256-cbc", secretKey, iv);
21
+
22
+ let encrypted = cipher.update(value, "utf8", "hex");
23
+ encrypted += cipher.final("hex");
24
+
25
+ return iv.toString("hex") + ":" + encrypted;
26
+ }
27
+
28
+ /**
29
+ * Decrypts an AES-256-CBC encrypted value.
30
+ * @param {string} encryptedValue - Encrypted string in format "iv:encryptedData"
31
+ * @returns {string|false} Decrypted string or false if decryption fails
32
+ */
33
+ static decrypt(encryptedValue) {
34
+ try {
35
+ const parts = encryptedValue.split(":");
36
+ const iv = Buffer.from(parts.shift(), "hex");
37
+ const encryptedText = Buffer.from(parts.join(":"), "hex");
38
+ const secretKey = crypto.createHash("sha256").update(process.env.APP_KEY).digest();
39
+ const decipher = crypto.createDecipheriv("aes-256-cbc", secretKey, iv);
40
+
41
+ let decrypted = decipher.update(encryptedText, "hex", "utf8");
42
+ decrypted += decipher.final("utf8");
43
+
44
+ return decrypted;
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ }
50
+ }
51
+
52
+ export default Encryption;
@@ -8,6 +8,17 @@ import * as ColorData from './Data/Color.js';
8
8
  import * as DateData from './Data/Date.js';
9
9
  import * as PhoneData from './Data/Phone.js';
10
10
 
11
+ /**
12
+ * Fake data generator for testing and seeding.
13
+ * Generates unique values by default to prevent duplicates.
14
+ *
15
+ * @example
16
+ * import { Faker } from "@nitronjs/framework";
17
+ *
18
+ * const name = Faker.fullName();
19
+ * const email = Faker.email();
20
+ * const phone = Faker.phoneNumber();
21
+ */
11
22
  class FakerClass {
12
23
 
13
24
  #usedValues = new Map();
@@ -0,0 +1,120 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import Paths from "../Core/Paths.js";
4
+
5
+ /**
6
+ * File storage manager for public and private file operations.
7
+ * Includes security measures against directory traversal attacks.
8
+ */
9
+ class Storage {
10
+ static #publicRoot = Paths.storagePublic;
11
+ static #privateRoot = Paths.storagePrivate;
12
+
13
+ /**
14
+ * Reads a file from storage.
15
+ * @param {string} filePath - Relative path to the file
16
+ * @param {boolean} isPrivate - Whether to read from private storage
17
+ * @returns {Promise<Buffer|null>} File contents or null if not found
18
+ */
19
+ static async get(filePath, isPrivate = false) {
20
+ const base = isPrivate ? this.#privateRoot : this.#publicRoot;
21
+
22
+ try {
23
+ const fullPath = this.#validatePath(base, filePath);
24
+
25
+ return await fs.promises.readFile(fullPath);
26
+ }
27
+ catch (err) {
28
+ if (err.message.includes("directory traversal")) {
29
+ throw err;
30
+ }
31
+
32
+ return null;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Saves a file to storage.
38
+ * @param {Object} file - File object with _buf property containing file data
39
+ * @param {string} dir - Directory path within storage
40
+ * @param {string} fileName - Name for the saved file
41
+ * @param {boolean} isPrivate - Whether to save to private storage
42
+ * @returns {Promise<boolean>} True if save successful
43
+ */
44
+ static async put(file, dir, fileName, isPrivate = false) {
45
+ const base = isPrivate ? this.#privateRoot : this.#publicRoot;
46
+ const folderPath = this.#validatePath(base, dir);
47
+ const fullPath = this.#validatePath(base, path.join(dir, fileName));
48
+
49
+ await fs.promises.mkdir(folderPath, { recursive: true });
50
+ await fs.promises.writeFile(fullPath, file._buf);
51
+
52
+ return true;
53
+ }
54
+
55
+ /**
56
+ * Deletes a file from storage.
57
+ * @param {string} filePath - Relative path to the file
58
+ * @param {boolean} isPrivate - Whether to delete from private storage
59
+ * @returns {Promise<void>}
60
+ */
61
+ static async delete(filePath, isPrivate = false) {
62
+ const base = isPrivate ? this.#privateRoot : this.#publicRoot;
63
+ const fullPath = this.#validatePath(base, filePath);
64
+
65
+ await fs.promises.unlink(fullPath);
66
+ }
67
+
68
+ /**
69
+ * Checks if a file exists in storage.
70
+ * @param {string} filePath - Relative path to the file
71
+ * @param {boolean} isPrivate - Whether to check private storage
72
+ * @returns {boolean} True if file exists
73
+ */
74
+ static exists(filePath, isPrivate = false) {
75
+ const base = isPrivate ? this.#privateRoot : this.#publicRoot;
76
+
77
+ try {
78
+ const fullPath = this.#validatePath(base, filePath);
79
+
80
+ return fs.existsSync(fullPath);
81
+ }
82
+ catch {
83
+ return false;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Gets the public URL for a file in public storage.
89
+ * @param {string} filePath - Relative path to the file
90
+ * @returns {string} Public URL path
91
+ */
92
+ static url(filePath) {
93
+ if (filePath.startsWith("/")) {
94
+ filePath = filePath.substring(1);
95
+ }
96
+
97
+ return `/storage/${filePath}`;
98
+ }
99
+
100
+ /**
101
+ * Validates a file path to prevent directory traversal attacks.
102
+ * @param {string} base - Base directory path
103
+ * @param {string} filePath - Relative file path to validate
104
+ * @returns {string} Full validated path
105
+ * @throws {Error} If path escapes base directory
106
+ * @private
107
+ */
108
+ static #validatePath(base, filePath) {
109
+ const normalizedBase = path.normalize(base) + path.sep;
110
+ const fullPath = path.normalize(path.join(base, filePath));
111
+
112
+ if (!fullPath.startsWith(normalizedBase)) {
113
+ throw new Error("Invalid file path: directory traversal detected");
114
+ }
115
+
116
+ return fullPath;
117
+ }
118
+ }
119
+
120
+ export default Storage;
package/lib/HMR/Server.js CHANGED
@@ -1,45 +1,97 @@
1
1
  import { Server as SocketServer } from "socket.io";
2
+ import { createRequire } from "module";
2
3
  import path from "path";
4
+ import fs from "fs";
3
5
 
6
+ /**
7
+ * HMR (Hot Module Replacement) server for development mode.
8
+ * Manages WebSocket connections and broadcasts file change events to connected clients.
9
+ *
10
+ * @example
11
+ * // In HTTP Server
12
+ * HMR.registerRoutes(fastify);
13
+ * HMR.setup(httpServer);
14
+ *
15
+ * // Emit updates
16
+ * HMR.emitViewUpdate("resources/views/Site/Home.tsx");
17
+ * HMR.emitCss("site_style.css");
18
+ */
4
19
  class HMRServer {
5
20
  #io = null;
6
- #ready = false;
7
21
  #connections = 0;
22
+ #clientScript = null;
8
23
 
24
+ // Public Methods
25
+
26
+ /**
27
+ * Registers socket.io client script route in Fastify.
28
+ * Must be called before Router.setup() to ensure the route is registered.
29
+ * @param {import("fastify").FastifyInstance} fastify
30
+ */
31
+ registerRoutes(fastify) {
32
+ const require = createRequire(import.meta.url);
33
+ const socketIoDir = path.dirname(require.resolve("socket.io/package.json"));
34
+
35
+ this.#clientScript = path.join(socketIoDir, "client-dist", "socket.io.min.js");
36
+
37
+ fastify.get("/__nitron_hmr/socket.io.js", (req, reply) => {
38
+ if (!this.#clientScript || !fs.existsSync(this.#clientScript)) {
39
+ return reply.code(404).send("Socket.io client not found");
40
+ }
41
+
42
+ reply.type("application/javascript").send(fs.readFileSync(this.#clientScript, "utf-8"));
43
+ });
44
+ }
45
+
46
+ /**
47
+ * Initializes the WebSocket server.
48
+ * @param {import("http").Server} httpServer - Node.js HTTP server instance.
49
+ */
9
50
  setup(httpServer) {
10
51
  if (this.#io) return;
11
52
 
12
53
  this.#io = new SocketServer(httpServer, {
13
54
  path: "/__nitron_hmr",
14
- transports: ["websocket", "polling"],
55
+ transports: ["websocket"],
15
56
  cors: { origin: "*" },
16
57
  pingTimeout: 60000,
17
58
  pingInterval: 25000,
18
- serveClient: true
59
+ serveClient: false,
60
+ allowEIO3: true
19
61
  });
20
62
 
21
63
  this.#io.on("connection", (socket) => {
22
64
  this.#connections++;
23
- socket.on("disconnect", () => { this.#connections--; });
65
+ socket.on("disconnect", () => this.#connections--);
24
66
  });
25
-
26
- this.#ready = true;
27
67
  }
28
68
 
69
+ /**
70
+ * Whether the HMR server is ready and accepting connections.
71
+ * @returns {boolean}
72
+ */
29
73
  get isReady() {
30
- return this.#ready && this.#io !== null;
74
+ return this.#io !== null;
31
75
  }
32
76
 
77
+ /**
78
+ * Number of currently connected clients.
79
+ * @returns {number}
80
+ */
33
81
  get connectionCount() {
34
82
  return this.#connections;
35
83
  }
36
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
+ */
37
89
  emitViewUpdate(filePath) {
38
90
  if (!this.#io) return;
39
91
 
40
92
  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();
93
+ const match = normalized.match(/resources\/views\/(.+)\.tsx$/);
94
+ const viewPath = match ? match[1].toLowerCase() : path.basename(filePath, ".tsx").toLowerCase();
43
95
 
44
96
  this.#io.emit("hmr:update", {
45
97
  type: "view",
@@ -49,24 +101,40 @@ class HMRServer {
49
101
  });
50
102
  }
51
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.
107
+ */
52
108
  emitCss(filePath) {
53
109
  if (!this.#io) return;
110
+
54
111
  this.#io.emit("hmr:css", {
55
112
  file: filePath ? path.basename(filePath) : null,
56
113
  timestamp: Date.now()
57
114
  });
58
115
  }
59
116
 
117
+ /**
118
+ * Emits a full page reload event.
119
+ * @param {string} reason - Reason for the reload (shown in dev tools).
120
+ */
60
121
  emitReload(reason) {
61
122
  if (!this.#io) return;
123
+
62
124
  this.#io.emit("hmr:reload", {
63
125
  reason,
64
126
  timestamp: Date.now()
65
127
  });
66
128
  }
67
129
 
130
+ /**
131
+ * Emits a build error event to show error overlay in browser.
132
+ * @param {Error|string} error - The error that occurred.
133
+ * @param {string} [filePath] - Path to the file that caused the error.
134
+ */
68
135
  emitError(error, filePath) {
69
136
  if (!this.#io) return;
137
+
70
138
  this.#io.emit("hmr:error", {
71
139
  file: filePath,
72
140
  message: String(error?.message || error),
@@ -74,12 +142,15 @@ class HMRServer {
74
142
  });
75
143
  }
76
144
 
145
+ /**
146
+ * Closes the WebSocket server and cleans up resources.
147
+ */
77
148
  close() {
78
149
  if (this.#io) {
79
150
  this.#io.close();
80
151
  this.#io = null;
81
152
  }
82
- this.#ready = false;
153
+
83
154
  this.#connections = 0;
84
155
  }
85
156
  }
@@ -0,0 +1,41 @@
1
+ import bcrypt from "bcrypt";
2
+ import Config from "../Core/Config.js";
3
+
4
+ /**
5
+ * Secure password hashing using bcrypt with APP_KEY pepper.
6
+ */
7
+ class Hash {
8
+ /**
9
+ * Hashes a plain text password.
10
+ * @param {string} textField - Plain text password to hash
11
+ * @returns {Promise<string>} Bcrypt hashed password
12
+ * @throws {Error} If APP_KEY is not set
13
+ */
14
+ static async make(textField) {
15
+ if (!process.env.APP_KEY) {
16
+ throw new Error("APP_KEY is required for hashing");
17
+ }
18
+
19
+ const saltRounds = Config.get("hash.salt_rounds", 10);
20
+ const salt = await bcrypt.genSalt(saltRounds);
21
+
22
+ return await bcrypt.hash(textField + process.env.APP_KEY, salt);
23
+ }
24
+
25
+ /**
26
+ * Verifies a plain text password against a hash.
27
+ * @param {string} textField - Plain text password to verify
28
+ * @param {string} hashedText - Bcrypt hash to compare against
29
+ * @returns {Promise<boolean>} True if password matches
30
+ * @throws {Error} If APP_KEY is not set
31
+ */
32
+ static async check(textField, hashedText) {
33
+ if (!process.env.APP_KEY) {
34
+ throw new Error("APP_KEY is required for hashing");
35
+ }
36
+
37
+ return await bcrypt.compare(textField + process.env.APP_KEY, hashedText);
38
+ }
39
+ }
40
+
41
+ export default Hash;