@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
@@ -4,307 +4,209 @@ import Paths from "../Core/Paths.js";
4
4
  import Environment from "../Core/Environment.js";
5
5
 
6
6
  /**
7
- * File Session Store
8
- *
9
- * Stores sessions as individual JSON files on disk.
10
- * Uses atomic writes to prevent corruption.
11
- *
12
- * Features:
13
- * - One file per session (session_<id>.json)
14
- * - Atomic write operations (temp file + rename)
15
- * - Automatic directory creation
16
- * - Garbage collection for expired sessions
7
+ * File-based session storage.
8
+ * Stores each session as a JSON file with atomic writes.
17
9
  */
18
- class File {
10
+ class FileStore {
19
11
  #storagePath;
20
- ready; // Promise for Manager to wait on initialization
12
+ ready;
21
13
 
22
14
  constructor() {
23
- // Store sessions in storage/framework/sessions/
24
15
  this.#storagePath = Paths.storageSessions;
25
16
  this.ready = this.#initialize();
26
17
  }
27
18
 
28
19
  /**
29
- * Initialize storage: create directory and cleanup corrupted files
20
+ * Gets session data by ID.
21
+ * @param {string} sessionId
22
+ * @returns {Promise<Object|null>}
30
23
  */
31
- async #initialize() {
32
- try {
33
- await fs.mkdir(this.#storagePath, { recursive: true });
34
-
35
- // Cleanup empty/corrupted files from previous crash (silent)
36
- await this.#cleanupCorruptedFiles();
37
- }
38
- catch (error) {
39
- console.error('[File Store] Initialization failed:', error.message);
40
- throw error;
24
+ async get(sessionId) {
25
+ if (!this.#isValidId(sessionId)) {
26
+ return null;
41
27
  }
42
- }
43
28
 
44
- /**
45
- * Cleanup empty or corrupted session files (startup only)
46
- */
47
- async #cleanupCorruptedFiles() {
48
29
  try {
49
- const files = await fs.readdir(this.#storagePath);
50
- let cleaned = 0;
51
-
52
- for (const file of files) {
53
- if (!file.startsWith('session_') || file.endsWith('.tmp')) {
54
- continue;
55
- }
56
-
57
- const filePath = path.join(this.#storagePath, file);
58
-
59
- try {
60
- const content = await fs.readFile(filePath, 'utf8');
61
-
62
- // Check if empty or invalid JSON
63
- if (!content || content.trim() === '') {
64
- await fs.unlink(filePath);
65
- cleaned++;
66
- }
67
- else {
68
- JSON.parse(content); // Validate JSON
69
- }
70
- }
71
- catch {
72
- // Corrupted or unreadable - delete
73
- await fs.unlink(filePath).catch(() => {});
74
- cleaned++;
75
- }
76
- }
77
-
78
- if (cleaned > 0) {
79
- console.log(`[File Store] Cleaned ${cleaned} corrupted session files`);
80
- }
81
- }
82
- catch (error) {
83
- // Non-critical, ignore
84
- }
85
- }
86
-
87
- /**
88
- * Validate session ID format (hex only, max 128 chars)
89
- * Prevents path traversal attacks
90
- */
91
- #isValidSessionId(sessionId) {
92
- if (!sessionId || typeof sessionId !== 'string') return false;
93
- if (sessionId.length > 128) return false;
94
- return /^[a-f0-9]+$/i.test(sessionId);
95
- }
96
-
97
- /**
98
- * Get session file path
99
- */
100
- #getFilePath (sessionId) {
101
- return path.join(this.#storagePath, `session_${sessionId}.json`);
102
- }
30
+ const content = await fs.readFile(this.#filePath(sessionId), "utf8");
103
31
 
104
- /**
105
- * Get temporary file path for atomic writes
106
- */
107
- #getTempFilePath (sessionId) {
108
- return path.join(this.#storagePath, `session_${sessionId}.json.tmp`);
109
- }
110
-
111
- /**
112
- * Retrieve session data by ID
113
- * @param {string} sessionId - Session identifier
114
- * @returns {object|null} Session data or null if not found
115
- */
116
- async get (sessionId) {
117
- await this.ready;
118
-
119
- // Guard: Validate session ID format (hex only)
120
- if (!this.#isValidSessionId(sessionId)) {
121
- return null;
122
- }
123
-
124
- try {
125
- const filePath = this.#getFilePath(sessionId);
126
- const content = await fs.readFile(filePath, 'utf8');
127
-
128
- // Validate JSON before parsing
129
- if (!content || content.trim() === '') {
130
- // Silent cleanup - empty files are normal during hot-reload
131
- await fs.unlink(filePath).catch(() => {});
32
+ if (!content?.trim()) {
33
+ await this.delete(sessionId);
132
34
 
133
35
  return null;
134
36
  }
135
-
37
+
136
38
  return JSON.parse(content);
137
39
  }
138
- catch (error) {
139
- // File not found
140
- if (error.code === 'ENOENT') {
40
+ catch (err) {
41
+ if (err.code === "ENOENT") {
141
42
  return null;
142
43
  }
143
-
144
- // Corrupted file - silent cleanup
145
- if (error instanceof SyntaxError) {
146
- const filePath = this.#getFilePath(sessionId);
147
- await fs.unlink(filePath).catch(() => {});
44
+
45
+ if (err instanceof SyntaxError) {
46
+ await this.delete(sessionId);
47
+
148
48
  return null;
149
49
  }
150
-
151
- // Only log unexpected errors
152
- if (error.code !== 'EBUSY' && error.code !== 'EPERM') {
153
- console.error('[File Store] Read error:', error.message);
154
- }
50
+
155
51
  return null;
156
52
  }
157
53
  }
158
54
 
159
55
  /**
160
- * Store session data with retry and atomic write
161
- * @param {string} sessionId - Session identifier
162
- * @param {object} sessionData - Session data to store
56
+ * Stores session data.
57
+ * @param {string} sessionId
58
+ * @param {Object} data
163
59
  */
164
- async set (sessionId, sessionData) {
165
- await this.ready;
166
-
167
- // Guard: Validate session ID format (hex only)
168
- if (!this.#isValidSessionId(sessionId)) {
60
+ async set(sessionId, data) {
61
+ if (!this.#isValidId(sessionId)) {
169
62
  return;
170
63
  }
171
-
172
- const filePath = this.#getFilePath(sessionId);
173
- const tempPath = this.#getTempFilePath(sessionId);
174
- const jsonContent = Environment.isDev
175
- ? JSON.stringify(sessionData, null, 2)
176
- : JSON.stringify(sessionData);
177
-
178
- // Retry mechanism for Windows file locking
179
- const maxRetries = 3;
180
- const retryDelay = 50; // ms
181
-
182
- for (let attempt = 0; attempt < maxRetries; attempt++) {
183
- try {
184
- // Write to temp file first (with restricted permissions on Unix)
185
- const writeOptions = process.platform === 'win32'
186
- ? { encoding: 'utf8' }
187
- : { encoding: 'utf8', mode: 0o600 };
188
-
189
- await fs.writeFile(tempPath, jsonContent, writeOptions);
190
-
191
- // Atomic rename (cross-platform)
192
- try {
193
- await fs.rename(tempPath, filePath);
194
- }
195
- catch (renameError) {
196
- // Windows: EPERM if target exists and is locked
197
- if (renameError.code === 'EPERM' || renameError.code === 'EEXIST') {
198
- try {
199
- await fs.unlink(filePath);
200
- }
201
- catch {}
202
- await fs.rename(tempPath, filePath);
203
- }
204
- else {
205
- throw renameError;
206
- }
207
- }
208
-
209
- return; // Success
210
- }
211
- catch (error) {
212
- const isLastAttempt = attempt === maxRetries - 1;
213
- const isLockError = ['EBUSY', 'EPERM', 'ENOENT'].includes(error.code);
214
-
215
- // Clean up temp file on error
216
- try {
217
- await fs.unlink(tempPath);
218
- }
219
- catch {}
220
-
221
- // If file locked and not last attempt, retry
222
- if (isLockError && !isLastAttempt) {
223
- await new Promise(resolve => setTimeout(resolve, retryDelay * (attempt + 1)));
224
- continue;
225
- }
226
-
227
- // Last attempt or non-lock error - log it
228
- if (isLastAttempt || !isLockError) {
229
- console.error('[File Store] Write failed after retries:', error.message);
230
- }
231
- return;
232
- }
64
+
65
+ const filePath = this.#filePath(sessionId);
66
+ const tempPath = filePath + ".tmp";
67
+ const json = Environment.isDev ? JSON.stringify(data, null, 2) : JSON.stringify(data);
68
+
69
+ try {
70
+ await fs.writeFile(tempPath, json, "utf8");
71
+ await this.#atomicRename(tempPath, filePath);
72
+ }
73
+ catch {
74
+ await fs.unlink(tempPath).catch(() => {});
233
75
  }
234
76
  }
235
77
 
236
78
  /**
237
- * Delete session file
238
- * @param {string} sessionId - Session identifier
79
+ * Deletes a session.
80
+ * @param {string} sessionId
239
81
  */
240
82
  async delete(sessionId) {
241
- await this.ready;
242
-
243
- // Guard: Validate session ID format (hex only)
244
- if (!this.#isValidSessionId(sessionId)) {
83
+ if (!this.#isValidId(sessionId)) {
245
84
  return;
246
85
  }
247
-
86
+
248
87
  try {
249
- const filePath = this.#getFilePath(sessionId);
250
- await fs.unlink(filePath);
88
+ await fs.unlink(this.#filePath(sessionId));
251
89
  }
252
- catch (error) {
253
- // Ignore file-not-found, log other errors
254
- if (error.code !== 'ENOENT') {
255
- console.error('[File Store] Delete error:', error.message);
90
+ catch (err) {
91
+ if (err.code !== "ENOENT") {
92
+ console.error("[Session File] Delete error:", err.message);
256
93
  }
257
94
  }
258
95
  }
259
96
 
260
97
  /**
261
- * Garbage collection - remove expired session files
262
- * @param {number} lifetime - Max session lifetime in milliseconds
263
- * @returns {number} Number of deleted sessions
98
+ * Garbage collection - removes expired sessions.
99
+ * @param {number} lifetime - Max lifetime in milliseconds
100
+ * @returns {Promise<number>} Number of deleted sessions
264
101
  */
265
- async gc (lifetime) {
266
- await this.ready;
267
-
102
+ async gc(lifetime) {
268
103
  try {
269
104
  const files = await fs.readdir(this.#storagePath);
270
- let deletedCount = 0;
271
105
  const now = Date.now();
106
+ let deleted = 0;
272
107
 
273
108
  for (const file of files) {
274
- // Only process session files, skip temp files
275
- if (!file.startsWith('session_') || file.endsWith('.tmp')) {
109
+ if (!file.startsWith("session_") || file.endsWith(".tmp")) {
276
110
  continue;
277
111
  }
278
112
 
279
113
  const filePath = path.join(this.#storagePath, file);
280
114
 
281
115
  try {
282
- const content = await fs.readFile(filePath, 'utf8');
283
- const sessionData = JSON.parse(content);
284
-
285
- // Delete if expired
286
- const lastActivity = sessionData.lastActivity || sessionData.createdAt;
116
+ const content = await fs.readFile(filePath, "utf8");
117
+ const data = JSON.parse(content);
118
+ const lastActivity = data.lastActivity || data.createdAt;
119
+
287
120
  if (now - lastActivity > lifetime) {
288
121
  await fs.unlink(filePath);
289
- deletedCount++;
122
+ deleted++;
290
123
  }
291
124
  }
292
- catch (error) {
293
- // Corrupted or unreadable file - delete it
294
- if (error.code !== 'ENOENT') {
295
- await fs.unlink(filePath).catch(() => {});
296
- deletedCount++;
297
- }
125
+ catch {
126
+ await fs.unlink(filePath).catch(() => {});
127
+ deleted++;
298
128
  }
299
129
  }
300
130
 
301
- return deletedCount;
131
+ return deleted;
302
132
  }
303
- catch (error) {
304
- console.error('[File Store] GC error:', error.message);
133
+ catch (err) {
134
+ console.error("[Session File] GC error:", err.message);
135
+
305
136
  return 0;
306
137
  }
307
138
  }
139
+
140
+ /** @private */
141
+ async #initialize() {
142
+ await fs.mkdir(this.#storagePath, { recursive: true });
143
+ await this.#cleanupCorrupted();
144
+ }
145
+
146
+ /** @private */
147
+ async #cleanupCorrupted() {
148
+ try {
149
+ const files = await fs.readdir(this.#storagePath);
150
+ let cleaned = 0;
151
+
152
+ for (const file of files) {
153
+ if (!file.startsWith("session_") || file.endsWith(".tmp")) {
154
+ continue;
155
+ }
156
+
157
+ const filePath = path.join(this.#storagePath, file);
158
+
159
+ try {
160
+ const content = await fs.readFile(filePath, "utf8");
161
+
162
+ if (!content?.trim()) {
163
+ await fs.unlink(filePath);
164
+ cleaned++;
165
+ }
166
+ else {
167
+ JSON.parse(content);
168
+ }
169
+ }
170
+ catch {
171
+ await fs.unlink(filePath).catch(() => {});
172
+ cleaned++;
173
+ }
174
+ }
175
+
176
+ if (cleaned > 0) {
177
+ console.log(`[Session File] Cleaned ${cleaned} corrupted files`);
178
+ }
179
+ }
180
+ catch {
181
+ // Non-critical
182
+ }
183
+ }
184
+
185
+ /** @private */
186
+ #isValidId(id) {
187
+ return id && typeof id === "string" && id.length <= 128 && /^[a-f0-9]+$/i.test(id);
188
+ }
189
+
190
+ /** @private */
191
+ #filePath(sessionId) {
192
+ return path.join(this.#storagePath, `session_${sessionId}.json`);
193
+ }
194
+
195
+ /** @private */
196
+ async #atomicRename(from, to) {
197
+ try {
198
+ await fs.rename(from, to);
199
+ }
200
+ catch (err) {
201
+ if (err.code === "EPERM" || err.code === "EEXIST") {
202
+ await fs.unlink(to).catch(() => {});
203
+ await fs.rename(from, to);
204
+ }
205
+ else {
206
+ throw err;
207
+ }
208
+ }
209
+ }
308
210
  }
309
211
 
310
- export default File;
212
+ export default FileStore;