@nitronjs/framework 0.2.2 → 0.2.4

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 (71) hide show
  1. package/README.md +3 -1
  2. package/cli/create.js +88 -72
  3. package/cli/njs.js +17 -19
  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 +4 -3
  14. package/lib/Console/Commands/MigrateFreshCommand.js +22 -27
  15. package/lib/Console/Commands/MigrateRollbackCommand.js +8 -4
  16. package/lib/Console/Commands/MigrateStatusCommand.js +8 -4
  17. package/lib/Console/Commands/SeedCommand.js +8 -28
  18. package/lib/Console/Commands/StorageLinkCommand.js +20 -5
  19. package/lib/Console/Output.js +143 -0
  20. package/lib/Core/Config.js +2 -1
  21. package/lib/Core/Paths.js +8 -8
  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 +59 -67
  27. package/lib/Database/Model.js +165 -75
  28. package/lib/Database/QueryBuilder.js +43 -0
  29. package/lib/Database/QueryValidation.js +51 -30
  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 +24 -145
  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 +79 -9
  38. package/lib/Hashing/Hash.js +41 -0
  39. package/lib/Http/Server.js +179 -151
  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 +87 -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 +49 -27
  53. package/lib/index.js +19 -13
  54. package/package.json +1 -1
  55. package/skeleton/app/Controllers/HomeController.js +7 -1
  56. package/skeleton/package.json +2 -0
  57. package/skeleton/resources/css/global.css +1 -0
  58. package/skeleton/resources/views/Site/Home.tsx +456 -79
  59. package/skeleton/tsconfig.json +6 -1
  60. package/lib/Auth/Manager.js +0 -111
  61. package/lib/Database/Connection.js +0 -61
  62. package/lib/Database/Manager.js +0 -162
  63. package/lib/Database/Migration/migrations/0000_00_00_00_01_create_seeders_table.js +0 -20
  64. package/lib/Database/Seeder/SeederRepository.js +0 -45
  65. package/lib/Encryption/Manager.js +0 -47
  66. package/lib/Filesystem/Manager.js +0 -74
  67. package/lib/Hashing/Manager.js +0 -25
  68. package/lib/Mail/Manager.js +0 -120
  69. package/lib/Route/Loader.js +0 -80
  70. package/lib/Route/Manager.js +0 -286
  71. package/lib/Translation/Manager.js +0 -49
@@ -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,11 +1,52 @@
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
 
@@ -15,31 +56,41 @@ class HMRServer {
15
56
  cors: { origin: "*" },
16
57
  pingTimeout: 60000,
17
58
  pingInterval: 25000,
18
- serveClient: true
59
+ serveClient: false
19
60
  });
20
61
 
21
62
  this.#io.on("connection", (socket) => {
22
63
  this.#connections++;
23
- socket.on("disconnect", () => { this.#connections--; });
64
+ socket.on("disconnect", () => this.#connections--);
24
65
  });
25
-
26
- this.#ready = true;
27
66
  }
28
67
 
68
+ /**
69
+ * Whether the HMR server is ready and accepting connections.
70
+ * @returns {boolean}
71
+ */
29
72
  get isReady() {
30
- return this.#ready && this.#io !== null;
73
+ return this.#io !== null;
31
74
  }
32
75
 
76
+ /**
77
+ * Number of currently connected clients.
78
+ * @returns {number}
79
+ */
33
80
  get connectionCount() {
34
81
  return this.#connections;
35
82
  }
36
83
 
84
+ /**
85
+ * Emits a view update event to trigger hot reload of a React component.
86
+ * @param {string} filePath - Absolute path to the changed view file.
87
+ */
37
88
  emitViewUpdate(filePath) {
38
89
  if (!this.#io) return;
39
90
 
40
91
  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();
92
+ const match = normalized.match(/resources\/views\/(.+)\.tsx$/);
93
+ const viewPath = match ? match[1].toLowerCase() : path.basename(filePath, ".tsx").toLowerCase();
43
94
 
44
95
  this.#io.emit("hmr:update", {
45
96
  type: "view",
@@ -49,24 +100,40 @@ class HMRServer {
49
100
  });
50
101
  }
51
102
 
103
+ /**
104
+ * Emits a CSS update event to refresh stylesheets without page reload.
105
+ * @param {string} [filePath] - Path to the changed CSS file. If null, refreshes all CSS.
106
+ */
52
107
  emitCss(filePath) {
53
108
  if (!this.#io) return;
109
+
54
110
  this.#io.emit("hmr:css", {
55
111
  file: filePath ? path.basename(filePath) : null,
56
112
  timestamp: Date.now()
57
113
  });
58
114
  }
59
115
 
116
+ /**
117
+ * Emits a full page reload event.
118
+ * @param {string} reason - Reason for the reload (shown in dev tools).
119
+ */
60
120
  emitReload(reason) {
61
121
  if (!this.#io) return;
122
+
62
123
  this.#io.emit("hmr:reload", {
63
124
  reason,
64
125
  timestamp: Date.now()
65
126
  });
66
127
  }
67
128
 
129
+ /**
130
+ * Emits a build error event to show error overlay in browser.
131
+ * @param {Error|string} error - The error that occurred.
132
+ * @param {string} [filePath] - Path to the file that caused the error.
133
+ */
68
134
  emitError(error, filePath) {
69
135
  if (!this.#io) return;
136
+
70
137
  this.#io.emit("hmr:error", {
71
138
  file: filePath,
72
139
  message: String(error?.message || error),
@@ -74,12 +141,15 @@ class HMRServer {
74
141
  });
75
142
  }
76
143
 
144
+ /**
145
+ * Closes the WebSocket server and cleans up resources.
146
+ */
77
147
  close() {
78
148
  if (this.#io) {
79
149
  this.#io.close();
80
150
  this.#io = null;
81
151
  }
82
- this.#ready = false;
152
+
83
153
  this.#connections = 0;
84
154
  }
85
155
  }
@@ -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;