@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.
- package/README.md +3 -1
- package/cli/create.js +88 -72
- package/cli/njs.js +17 -19
- package/lib/Auth/Auth.js +167 -0
- package/lib/Build/CssBuilder.js +9 -0
- package/lib/Build/FileAnalyzer.js +16 -0
- package/lib/Build/HydrationBuilder.js +17 -0
- package/lib/Build/Manager.js +15 -0
- package/lib/Build/colors.js +4 -0
- package/lib/Build/plugins.js +84 -20
- package/lib/Console/Commands/DevCommand.js +13 -9
- package/lib/Console/Commands/MakeCommand.js +24 -10
- package/lib/Console/Commands/MigrateCommand.js +4 -3
- package/lib/Console/Commands/MigrateFreshCommand.js +22 -27
- package/lib/Console/Commands/MigrateRollbackCommand.js +8 -4
- package/lib/Console/Commands/MigrateStatusCommand.js +8 -4
- package/lib/Console/Commands/SeedCommand.js +8 -28
- package/lib/Console/Commands/StorageLinkCommand.js +20 -5
- package/lib/Console/Output.js +143 -0
- package/lib/Core/Config.js +2 -1
- package/lib/Core/Paths.js +8 -8
- package/lib/Database/DB.js +141 -51
- package/lib/Database/Drivers/MySQLDriver.js +102 -157
- package/lib/Database/Migration/Checksum.js +3 -8
- package/lib/Database/Migration/MigrationRepository.js +25 -35
- package/lib/Database/Migration/MigrationRunner.js +59 -67
- package/lib/Database/Model.js +165 -75
- package/lib/Database/QueryBuilder.js +43 -0
- package/lib/Database/QueryValidation.js +51 -30
- package/lib/Database/Schema/Blueprint.js +25 -36
- package/lib/Database/Schema/Manager.js +31 -68
- package/lib/Database/Seeder/SeederRunner.js +24 -145
- package/lib/Date/DateTime.js +9 -0
- package/lib/Encryption/Encryption.js +52 -0
- package/lib/Faker/Faker.js +11 -0
- package/lib/Filesystem/Storage.js +120 -0
- package/lib/HMR/Server.js +79 -9
- package/lib/Hashing/Hash.js +41 -0
- package/lib/Http/Server.js +179 -151
- package/lib/Logging/{Manager.js → Log.js} +68 -80
- package/lib/Mail/Mail.js +187 -0
- package/lib/Route/Router.js +416 -0
- package/lib/Session/File.js +135 -233
- package/lib/Session/Manager.js +117 -171
- package/lib/Session/Memory.js +28 -38
- package/lib/Session/Session.js +71 -107
- package/lib/Support/Str.js +103 -0
- package/lib/Translation/Lang.js +54 -0
- package/lib/View/Client/hmr-client.js +87 -51
- package/lib/View/Client/nitronjs-icon.png +0 -0
- package/lib/View/{Manager.js → View.js} +44 -29
- package/lib/index.d.ts +49 -27
- package/lib/index.js +19 -13
- package/package.json +1 -1
- package/skeleton/app/Controllers/HomeController.js +7 -1
- package/skeleton/package.json +2 -0
- package/skeleton/resources/css/global.css +1 -0
- package/skeleton/resources/views/Site/Home.tsx +456 -79
- package/skeleton/tsconfig.json +6 -1
- package/lib/Auth/Manager.js +0 -111
- package/lib/Database/Connection.js +0 -61
- package/lib/Database/Manager.js +0 -162
- package/lib/Database/Migration/migrations/0000_00_00_00_01_create_seeders_table.js +0 -20
- package/lib/Database/Seeder/SeederRepository.js +0 -45
- package/lib/Encryption/Manager.js +0 -47
- package/lib/Filesystem/Manager.js +0 -74
- package/lib/Hashing/Manager.js +0 -25
- package/lib/Mail/Manager.js +0 -120
- package/lib/Route/Loader.js +0 -80
- package/lib/Route/Manager.js +0 -286
- package/lib/Translation/Manager.js +0 -49
package/lib/Session/File.js
CHANGED
|
@@ -4,307 +4,209 @@ import Paths from "../Core/Paths.js";
|
|
|
4
4
|
import Environment from "../Core/Environment.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* File
|
|
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
|
|
10
|
+
class FileStore {
|
|
19
11
|
#storagePath;
|
|
20
|
-
ready;
|
|
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
|
-
*
|
|
20
|
+
* Gets session data by ID.
|
|
21
|
+
* @param {string} sessionId
|
|
22
|
+
* @returns {Promise<Object|null>}
|
|
30
23
|
*/
|
|
31
|
-
async
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
139
|
-
|
|
140
|
-
if (error.code === 'ENOENT') {
|
|
40
|
+
catch (err) {
|
|
41
|
+
if (err.code === "ENOENT") {
|
|
141
42
|
return null;
|
|
142
43
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
*
|
|
161
|
-
* @param {string} sessionId
|
|
162
|
-
* @param {
|
|
56
|
+
* Stores session data.
|
|
57
|
+
* @param {string} sessionId
|
|
58
|
+
* @param {Object} data
|
|
163
59
|
*/
|
|
164
|
-
async set
|
|
165
|
-
|
|
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.#
|
|
173
|
-
const tempPath =
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
*
|
|
238
|
-
* @param {string} sessionId
|
|
79
|
+
* Deletes a session.
|
|
80
|
+
* @param {string} sessionId
|
|
239
81
|
*/
|
|
240
82
|
async delete(sessionId) {
|
|
241
|
-
|
|
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
|
-
|
|
250
|
-
await fs.unlink(filePath);
|
|
88
|
+
await fs.unlink(this.#filePath(sessionId));
|
|
251
89
|
}
|
|
252
|
-
catch (
|
|
253
|
-
|
|
254
|
-
|
|
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 -
|
|
262
|
-
* @param {number} lifetime - Max
|
|
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
|
|
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
|
-
|
|
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,
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
122
|
+
deleted++;
|
|
290
123
|
}
|
|
291
124
|
}
|
|
292
|
-
catch
|
|
293
|
-
|
|
294
|
-
|
|
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
|
|
131
|
+
return deleted;
|
|
302
132
|
}
|
|
303
|
-
catch (
|
|
304
|
-
console.error(
|
|
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
|
|
212
|
+
export default FileStore;
|