@nitronjs/framework 0.1.0

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 (87) hide show
  1. package/README.md +429 -0
  2. package/cli/create.js +260 -0
  3. package/cli/njs.js +164 -0
  4. package/lib/Auth/Manager.js +111 -0
  5. package/lib/Build/Manager.js +1232 -0
  6. package/lib/Console/Commands/BuildCommand.js +25 -0
  7. package/lib/Console/Commands/DevCommand.js +385 -0
  8. package/lib/Console/Commands/MakeCommand.js +110 -0
  9. package/lib/Console/Commands/MigrateCommand.js +98 -0
  10. package/lib/Console/Commands/MigrateFreshCommand.js +97 -0
  11. package/lib/Console/Commands/SeedCommand.js +92 -0
  12. package/lib/Console/Commands/StorageLinkCommand.js +31 -0
  13. package/lib/Console/Stubs/controller.js +19 -0
  14. package/lib/Console/Stubs/middleware.js +9 -0
  15. package/lib/Console/Stubs/migration.js +23 -0
  16. package/lib/Console/Stubs/model.js +7 -0
  17. package/lib/Console/Stubs/page-hydration.tsx +54 -0
  18. package/lib/Console/Stubs/seeder.js +9 -0
  19. package/lib/Console/Stubs/vendor.tsx +11 -0
  20. package/lib/Core/Config.js +86 -0
  21. package/lib/Core/Environment.js +21 -0
  22. package/lib/Core/Paths.js +188 -0
  23. package/lib/Database/Connection.js +61 -0
  24. package/lib/Database/DB.js +84 -0
  25. package/lib/Database/Drivers/MySQLDriver.js +234 -0
  26. package/lib/Database/Manager.js +162 -0
  27. package/lib/Database/Model.js +161 -0
  28. package/lib/Database/QueryBuilder.js +714 -0
  29. package/lib/Database/QueryValidation.js +62 -0
  30. package/lib/Database/Schema/Blueprint.js +126 -0
  31. package/lib/Database/Schema/Manager.js +116 -0
  32. package/lib/Date/DateTime.js +108 -0
  33. package/lib/Date/Locale.js +68 -0
  34. package/lib/Encryption/Manager.js +47 -0
  35. package/lib/Filesystem/Manager.js +49 -0
  36. package/lib/Hashing/Manager.js +25 -0
  37. package/lib/Http/Server.js +317 -0
  38. package/lib/Logging/Manager.js +153 -0
  39. package/lib/Mail/Manager.js +120 -0
  40. package/lib/Route/Loader.js +81 -0
  41. package/lib/Route/Manager.js +265 -0
  42. package/lib/Runtime/Entry.js +11 -0
  43. package/lib/Session/File.js +299 -0
  44. package/lib/Session/Manager.js +259 -0
  45. package/lib/Session/Memory.js +67 -0
  46. package/lib/Session/Session.js +196 -0
  47. package/lib/Support/Str.js +100 -0
  48. package/lib/Translation/Manager.js +49 -0
  49. package/lib/Validation/MimeTypes.js +39 -0
  50. package/lib/Validation/Validator.js +691 -0
  51. package/lib/View/Manager.js +544 -0
  52. package/lib/View/Templates/default/Home.tsx +262 -0
  53. package/lib/View/Templates/default/MainLayout.tsx +44 -0
  54. package/lib/View/Templates/errors/404.tsx +13 -0
  55. package/lib/View/Templates/errors/500.tsx +13 -0
  56. package/lib/View/Templates/errors/ErrorLayout.tsx +112 -0
  57. package/lib/View/Templates/messages/Maintenance.tsx +17 -0
  58. package/lib/View/Templates/messages/MessageLayout.tsx +136 -0
  59. package/lib/index.js +57 -0
  60. package/package.json +47 -0
  61. package/skeleton/.env.example +26 -0
  62. package/skeleton/app/Controllers/HomeController.js +9 -0
  63. package/skeleton/app/Kernel.js +11 -0
  64. package/skeleton/app/Middlewares/Authentication.js +9 -0
  65. package/skeleton/app/Middlewares/Guest.js +9 -0
  66. package/skeleton/app/Middlewares/VerifyCsrf.js +24 -0
  67. package/skeleton/app/Models/User.js +7 -0
  68. package/skeleton/config/app.js +4 -0
  69. package/skeleton/config/auth.js +16 -0
  70. package/skeleton/config/database.js +27 -0
  71. package/skeleton/config/hash.js +3 -0
  72. package/skeleton/config/server.js +28 -0
  73. package/skeleton/config/session.js +21 -0
  74. package/skeleton/database/migrations/2025_01_01_00_00_users.js +20 -0
  75. package/skeleton/database/seeders/UserSeeder.js +15 -0
  76. package/skeleton/globals.d.ts +1 -0
  77. package/skeleton/package.json +24 -0
  78. package/skeleton/public/.gitkeep +0 -0
  79. package/skeleton/resources/css/.gitkeep +0 -0
  80. package/skeleton/resources/langs/.gitkeep +0 -0
  81. package/skeleton/resources/views/Site/Home.tsx +66 -0
  82. package/skeleton/routes/web.js +4 -0
  83. package/skeleton/storage/app/private/.gitkeep +0 -0
  84. package/skeleton/storage/app/public/.gitkeep +0 -0
  85. package/skeleton/storage/framework/sessions/.gitkeep +0 -0
  86. package/skeleton/storage/logs/.gitkeep +0 -0
  87. package/skeleton/tsconfig.json +33 -0
@@ -0,0 +1,265 @@
1
+
2
+ import Loader from "./Loader.js";
3
+ import Paths from "../Core/Paths.js";
4
+ import Config from "../Core/Config.js";
5
+
6
+ class Route {
7
+ static #routes = [];
8
+ static #kernel = null;
9
+
10
+ static getSessionConfig() {
11
+ return Config.all("session");
12
+ }
13
+
14
+ static async getKernel() {
15
+ if (!this.#kernel) {
16
+ this.#kernel = (await import(Paths.kernelUrl())).default;
17
+ }
18
+ return this.#kernel;
19
+ }
20
+
21
+ static async setup(server) {
22
+ // Setup global route helper for SSR
23
+ globalThis.route = (name, params, query) => this.route(name, params, query);
24
+
25
+ const Kernel = await this.getKernel();
26
+
27
+ for (const route of this.#routes) {
28
+ if (this.#shouldApplyCsrf(route)) {
29
+ route.middlewares.unshift("verify-csrf");
30
+ }
31
+
32
+ route.middlewares = route.middlewares.map(middleware => {
33
+ const [name, param] = middleware.split(":");
34
+
35
+ if (!Kernel.routeMiddlewares[name]) {
36
+ throw new Error(`Middleware '${name}' is not defined in Kernel.js`);
37
+ }
38
+
39
+ // Wrap middleware handler for hot reload
40
+ const handler = Loader.wrapHandler(Kernel.routeMiddlewares[name].handler);
41
+
42
+ return (request, response) => {
43
+ return handler(request, response, param);
44
+ }
45
+ });
46
+
47
+ // Wrap controller handler for hot reload
48
+ const handler = Loader.wrapHandler(route.handler);
49
+
50
+ server.route({
51
+ method: route.method,
52
+ url: route.url,
53
+ preHandler: route.middlewares,
54
+ handler
55
+ });
56
+ }
57
+ }
58
+
59
+ // Get route manifest for client-side routing
60
+ static getClientManifest() {
61
+ const manifest = {};
62
+ for (const route of this.#routes) {
63
+ if (route.name) {
64
+ manifest[route.name] = route.url;
65
+ }
66
+ }
67
+ return manifest;
68
+ }
69
+
70
+ static get (url, handler) {
71
+ return this.#add("GET", url, handler);
72
+ }
73
+
74
+ static post (url, handler) {
75
+ return this.#add("POST", url, handler);
76
+ }
77
+
78
+ static put (url, handler) {
79
+ return this.#add("PUT", url, handler);
80
+ }
81
+
82
+ static delete (url, handler) {
83
+ return this.#add("DELETE", url, handler);
84
+ }
85
+
86
+ static prefix (url) {
87
+ return this.#addGroup(url, [], [], null);
88
+ }
89
+
90
+ static middleware (handlers) {
91
+ if (typeof handlers === 'string') {
92
+ return this.#addGroup("", [handlers], [], null);
93
+ }
94
+ else if (Array.isArray(handlers)) {
95
+ return this.#addGroup("", handlers, [], null);
96
+ }
97
+ }
98
+
99
+ static name (name) {
100
+ return this.#addGroup("", [], [], name);
101
+ }
102
+
103
+ static #add (method, url, handler) {
104
+ const route = {
105
+ method,
106
+ url,
107
+ handler,
108
+ middlewares: []
109
+ };
110
+
111
+ this.#routes.push(route);
112
+
113
+ return new RouteItem(route);
114
+ }
115
+
116
+ static #addGroup (prefix, middlewares, routes, name) {
117
+ const options = {
118
+ prefix,
119
+ middlewares,
120
+ routes,
121
+ name
122
+ };
123
+
124
+ return new RouteGroup(options, this.#routes);
125
+ }
126
+
127
+ static #resolveRouteName (name, params = {}) {
128
+ const route = this.#routes.find(r => r.name === name);
129
+
130
+ if (!route) {
131
+ throw new Error(`Route '${name}' is not defined`);
132
+ }
133
+
134
+ let url = route.url;
135
+
136
+ for (const key in params) {
137
+ url = url.replace(`:${key}`, params[key]);
138
+ }
139
+
140
+ return url;
141
+ }
142
+
143
+ static #shouldApplyCsrf (route) {
144
+ const sessionConfig = this.getSessionConfig();
145
+ const csrf = sessionConfig.csrf;
146
+
147
+ if (!csrf.enabled) {
148
+ return false;
149
+ }
150
+
151
+ if (!csrf.methods.includes(route.method)) {
152
+ return false;
153
+ }
154
+
155
+ for (const pattern of csrf.except) {
156
+ if (this.#matchPattern(route.url, pattern)) {
157
+ return false;
158
+ }
159
+ }
160
+
161
+ return true;
162
+ }
163
+
164
+ static #matchPattern (url, pattern) {
165
+ if (url === pattern) {
166
+ return true;
167
+ }
168
+
169
+ if (pattern.includes('*')) {
170
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
171
+
172
+ return regex.test(url);
173
+ }
174
+
175
+ return false;
176
+ }
177
+
178
+ static route (name, params = {}, query = {}) {
179
+ let url = this.#resolveRouteName(name, params);
180
+
181
+ if (query && Object.keys(query).length > 0) {
182
+ url += '?' + new URLSearchParams(query).toString();
183
+ }
184
+
185
+ return url;
186
+ }
187
+ }
188
+
189
+ class RouteItem {
190
+ constructor (route) {
191
+ this.route = route;
192
+ }
193
+
194
+ middleware (handlers) {
195
+ if (typeof handlers === 'string') {
196
+ this.route.middlewares.push(handlers);
197
+ }
198
+ else if (Array.isArray(handlers)) {
199
+ this.route.middlewares.push(...handlers);
200
+ }
201
+
202
+ return this;
203
+ }
204
+
205
+ name (name) {
206
+ this.route.name = name;
207
+
208
+ return this;
209
+ }
210
+ }
211
+
212
+ class RouteGroup {
213
+ constructor (options, routes) {
214
+ this.groupOptions = options;
215
+ this.routes = routes;
216
+ }
217
+
218
+ prefix (url) {
219
+ this.groupOptions.prefix = url;
220
+
221
+ return this;
222
+ }
223
+
224
+ middleware (handlers) {
225
+ if (typeof handlers === 'string') {
226
+ this.groupOptions.middlewares.push(handlers);
227
+ }
228
+ else if (Array.isArray(handlers)) {
229
+ this.groupOptions.middlewares.push(...handlers);
230
+ }
231
+
232
+ return this;
233
+ }
234
+
235
+ name (name) {
236
+ this.groupOptions.name = name;
237
+
238
+ return this;
239
+ }
240
+
241
+ group (callback) {
242
+ const beforeRoutes = this.routes.length;
243
+ callback();
244
+ const newRoutes = this.routes.slice(beforeRoutes);
245
+
246
+ for (const route of newRoutes) {
247
+ if (this.groupOptions.prefix) {
248
+ if (route.url === "/") {
249
+ route.url = this.groupOptions.prefix;
250
+ }
251
+ else {
252
+ route.url = this.groupOptions.prefix + route.url;
253
+ }
254
+ }
255
+
256
+ if (route.name && this.groupOptions.name) {
257
+ route.name = this.groupOptions.name + route.name;
258
+ }
259
+
260
+ route.middlewares.unshift(...this.groupOptions.middlewares);
261
+ }
262
+ }
263
+ }
264
+
265
+ export default Route;
@@ -0,0 +1,11 @@
1
+ import Server from "../Http/Server.js";
2
+
3
+ export async function start() {
4
+ await Server.start();
5
+ }
6
+
7
+ // Auto-start when run directly
8
+ const isMain = process.argv[1]?.endsWith("Entry.js");
9
+ if (isMain) {
10
+ start();
11
+ }
@@ -0,0 +1,299 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import Paths from "../Core/Paths.js";
4
+
5
+ /**
6
+ * File Session Store
7
+ *
8
+ * Stores sessions as individual JSON files on disk.
9
+ * Uses atomic writes to prevent corruption.
10
+ *
11
+ * Features:
12
+ * - One file per session (session_<id>.json)
13
+ * - Atomic write operations (temp file + rename)
14
+ * - Automatic directory creation
15
+ * - Garbage collection for expired sessions
16
+ */
17
+ class File {
18
+ #storagePath;
19
+ ready; // Promise for Manager to wait on initialization
20
+
21
+ constructor() {
22
+ // Store sessions in storage/framework/sessions/
23
+ this.#storagePath = Paths.storageSessions;
24
+ this.ready = this.#initialize();
25
+ }
26
+
27
+ /**
28
+ * Initialize storage: create directory and cleanup corrupted files
29
+ */
30
+ async #initialize() {
31
+ try {
32
+ await fs.mkdir(this.#storagePath, { recursive: true });
33
+
34
+ // Cleanup empty/corrupted files from previous crash (silent)
35
+ await this.#cleanupCorruptedFiles();
36
+ }
37
+ catch (error) {
38
+ console.error('[File Store] Initialization failed:', error.message);
39
+ throw error;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Cleanup empty or corrupted session files (startup only)
45
+ */
46
+ async #cleanupCorruptedFiles() {
47
+ try {
48
+ const files = await fs.readdir(this.#storagePath);
49
+ let cleaned = 0;
50
+
51
+ for (const file of files) {
52
+ if (!file.startsWith('session_') || file.endsWith('.tmp')) {
53
+ continue;
54
+ }
55
+
56
+ const filePath = path.join(this.#storagePath, file);
57
+
58
+ try {
59
+ const content = await fs.readFile(filePath, 'utf8');
60
+
61
+ // Check if empty or invalid JSON
62
+ if (!content || content.trim() === '') {
63
+ await fs.unlink(filePath);
64
+ cleaned++;
65
+ }
66
+ else {
67
+ JSON.parse(content); // Validate JSON
68
+ }
69
+ }
70
+ catch {
71
+ // Corrupted or unreadable - delete
72
+ await fs.unlink(filePath).catch(() => {});
73
+ cleaned++;
74
+ }
75
+ }
76
+
77
+ if (cleaned > 0) {
78
+ console.log(`[File Store] Cleaned ${cleaned} corrupted session files`);
79
+ }
80
+ }
81
+ catch (error) {
82
+ // Non-critical, ignore
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Get session file path
88
+ */
89
+ #getFilePath (sessionId) {
90
+ return path.join(this.#storagePath, `session_${sessionId}.json`);
91
+ }
92
+
93
+ /**
94
+ * Get temporary file path for atomic writes
95
+ */
96
+ #getTempFilePath (sessionId) {
97
+ return path.join(this.#storagePath, `session_${sessionId}.json.tmp`);
98
+ }
99
+
100
+ /**
101
+ * Retrieve session data by ID
102
+ * @param {string} sessionId - Session identifier
103
+ * @returns {object|null} Session data or null if not found
104
+ */
105
+ async get (sessionId) {
106
+ await this.ready;
107
+
108
+ // Guard: Validate session ID length
109
+ if (!sessionId || sessionId.length > 128) {
110
+ return null;
111
+ }
112
+
113
+ try {
114
+ const filePath = this.#getFilePath(sessionId);
115
+ const content = await fs.readFile(filePath, 'utf8');
116
+
117
+ // Validate JSON before parsing
118
+ if (!content || content.trim() === '') {
119
+ // Silent cleanup - empty files are normal during hot-reload
120
+ await fs.unlink(filePath).catch(() => {});
121
+
122
+ return null;
123
+ }
124
+
125
+ return JSON.parse(content);
126
+ }
127
+ catch (error) {
128
+ // File not found
129
+ if (error.code === 'ENOENT') {
130
+ return null;
131
+ }
132
+
133
+ // Corrupted file - silent cleanup
134
+ if (error instanceof SyntaxError) {
135
+ const filePath = this.#getFilePath(sessionId);
136
+ await fs.unlink(filePath).catch(() => {});
137
+ return null;
138
+ }
139
+
140
+ // Only log unexpected errors
141
+ if (error.code !== 'EBUSY' && error.code !== 'EPERM') {
142
+ console.error('[File Store] Read error:', error.message);
143
+ }
144
+ return null;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Store session data with retry and atomic write
150
+ * @param {string} sessionId - Session identifier
151
+ * @param {object} sessionData - Session data to store
152
+ */
153
+ async set (sessionId, sessionData) {
154
+ await this.ready;
155
+
156
+ // Guard: Validate session ID length
157
+ if (!sessionId || sessionId.length > 128) {
158
+ return;
159
+ }
160
+
161
+ const filePath = this.#getFilePath(sessionId);
162
+ const tempPath = this.#getTempFilePath(sessionId);
163
+ const jsonContent = process.env.APP_DEV === "true"
164
+ ? JSON.stringify(sessionData, null, 2)
165
+ : JSON.stringify(sessionData);
166
+
167
+ // Retry mechanism for Windows file locking
168
+ const maxRetries = 3;
169
+ const retryDelay = 50; // ms
170
+
171
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
172
+ try {
173
+ // Write to temp file first (with restricted permissions on Unix)
174
+ const writeOptions = process.platform === 'win32'
175
+ ? { encoding: 'utf8' }
176
+ : { encoding: 'utf8', mode: 0o600 };
177
+
178
+ await fs.writeFile(tempPath, jsonContent, writeOptions);
179
+
180
+ // Atomic rename (cross-platform)
181
+ try {
182
+ await fs.rename(tempPath, filePath);
183
+ }
184
+ catch (renameError) {
185
+ // Windows: EPERM if target exists and is locked
186
+ if (renameError.code === 'EPERM' || renameError.code === 'EEXIST') {
187
+ try {
188
+ await fs.unlink(filePath);
189
+ }
190
+ catch {}
191
+ await fs.rename(tempPath, filePath);
192
+ }
193
+ else {
194
+ throw renameError;
195
+ }
196
+ }
197
+
198
+ return; // Success
199
+ }
200
+ catch (error) {
201
+ const isLastAttempt = attempt === maxRetries - 1;
202
+ const isLockError = ['EBUSY', 'EPERM', 'ENOENT'].includes(error.code);
203
+
204
+ // Clean up temp file on error
205
+ try {
206
+ await fs.unlink(tempPath);
207
+ }
208
+ catch {}
209
+
210
+ // If file locked and not last attempt, retry
211
+ if (isLockError && !isLastAttempt) {
212
+ await new Promise(resolve => setTimeout(resolve, retryDelay * (attempt + 1)));
213
+ continue;
214
+ }
215
+
216
+ // Last attempt or non-lock error - log it
217
+ if (isLastAttempt || !isLockError) {
218
+ console.error('[File Store] Write failed after retries:', error.message);
219
+ }
220
+ return;
221
+ }
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Delete session file
227
+ * @param {string} sessionId - Session identifier
228
+ */
229
+ async delete(sessionId) {
230
+ await this.ready;
231
+
232
+ // Guard: Validate session ID length
233
+ if (!sessionId || sessionId.length > 128) {
234
+ return;
235
+ }
236
+
237
+ try {
238
+ const filePath = this.#getFilePath(sessionId);
239
+ await fs.unlink(filePath);
240
+ }
241
+ catch (error) {
242
+ // Ignore file-not-found, log other errors
243
+ if (error.code !== 'ENOENT') {
244
+ console.error('[File Store] Delete error:', error.message);
245
+ }
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Garbage collection - remove expired session files
251
+ * @param {number} lifetime - Max session lifetime in milliseconds
252
+ * @returns {number} Number of deleted sessions
253
+ */
254
+ async gc (lifetime) {
255
+ await this.ready;
256
+
257
+ try {
258
+ const files = await fs.readdir(this.#storagePath);
259
+ let deletedCount = 0;
260
+ const now = Date.now();
261
+
262
+ for (const file of files) {
263
+ // Only process session files, skip temp files
264
+ if (!file.startsWith('session_') || file.endsWith('.tmp')) {
265
+ continue;
266
+ }
267
+
268
+ const filePath = path.join(this.#storagePath, file);
269
+
270
+ try {
271
+ const content = await fs.readFile(filePath, 'utf8');
272
+ const sessionData = JSON.parse(content);
273
+
274
+ // Delete if expired
275
+ const lastActivity = sessionData.lastActivity || sessionData.createdAt;
276
+ if (now - lastActivity > lifetime) {
277
+ await fs.unlink(filePath);
278
+ deletedCount++;
279
+ }
280
+ }
281
+ catch (error) {
282
+ // Corrupted or unreadable file - delete it
283
+ if (error.code !== 'ENOENT') {
284
+ await fs.unlink(filePath).catch(() => {});
285
+ deletedCount++;
286
+ }
287
+ }
288
+ }
289
+
290
+ return deletedCount;
291
+ }
292
+ catch (error) {
293
+ console.error('[File Store] GC error:', error.message);
294
+ return 0;
295
+ }
296
+ }
297
+ }
298
+
299
+ export default File;