@nitronjs/framework 0.2.3 → 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 (68) hide show
  1. package/README.md +3 -1
  2. package/cli/create.js +88 -72
  3. package/cli/njs.js +13 -6
  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 +18 -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 +143 -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 +79 -9
  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 +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 +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,6 +1,4 @@
1
- import { pathToFileURL } from "node:url";
2
1
  import dotenv from "dotenv";
3
- import path from "node:path";
4
2
  import fastify from "fastify";
5
3
  import fastifyCors from "@fastify/cors";
6
4
  import fastifyStatic from "@fastify/static";
@@ -8,69 +6,103 @@ import fastifyCookie from "@fastify/cookie";
8
6
  import fastifyHelmet from "@fastify/helmet";
9
7
  import fastifyMultipart from "@fastify/multipart";
10
8
  import fastifyFormbody from "@fastify/formbody";
9
+ import fs from "node:fs";
10
+ import { fileURLToPath } from "node:url";
11
+ import path from "node:path";
11
12
  import Paths from "../Core/Paths.js";
12
13
  import Config from "../Core/Config.js";
13
14
  import Environment from "../Core/Environment.js";
14
- import Route from "../Route/Manager.js";
15
- import View from "../View/Manager.js";
16
- import Auth from "../Auth/Manager.js";
15
+ import Router from "../Route/Router.js";
16
+ import View from "../View/View.js";
17
+ import Auth from "../Auth/Auth.js";
17
18
  import SessionManager from "../Session/Manager.js";
18
19
  import DB from "../Database/DB.js";
19
- import Log from "../Logging/Manager.js";
20
- import Loader from "../Route/Loader.js";
20
+ import Log from "../Logging/Log.js";
21
21
  import HMRServer from "../HMR/Server.js";
22
22
 
23
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
+ const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, "../../package.json"), "utf8"));
25
+
26
+ /**
27
+ * HTTP server manager built on Fastify.
28
+ * Handles server configuration, plugin registration, and lifecycle management.
29
+ *
30
+ * @example
31
+ * import { Server } from "@nitronjs/framework";
32
+ * await Server.start();
33
+ */
23
34
  class Server {
24
35
  static #server;
25
- static #serverConfigs;
36
+ static #config;
26
37
 
38
+ /**
39
+ * Starts the HTTP server with all configured plugins and routes.
40
+ * @returns {Promise<void>}
41
+ */
27
42
  static async start() {
28
43
  dotenv.config({ quiet: true });
29
-
30
- // Initialize config system first (loads all configs once)
31
44
  await Config.initialize();
32
-
33
- // Load project configs dynamically
34
- this.#serverConfigs = Config.all("server");
35
45
 
36
- await Loader.initialize();
46
+ this.#config = Config.all("server");
47
+ const { web_server = {}, cors = {} } = this.#config;
37
48
 
38
- // Load project routes dynamically
39
- await import(Paths.routeUrl("web"));
40
-
41
- await this.#createServer();
42
- await DB.setup();
43
- await SessionManager.setup(this.#server);
44
- Auth.setup(this.#server);
45
- await Route.setup(this.#server);
46
- View.setup(this.#server);
47
- await this.#listen();
48
- }
49
+ const serverDefaults = {
50
+ bodyLimit: 50 * 1024 * 1024,
51
+ trustProxy: true,
52
+ maxParamLength: 150
53
+ };
54
+
55
+ const webServerConfig = { ...serverDefaults, ...web_server };
49
56
 
50
- static async #createServer() {
57
+ // Create Fastify instance
51
58
  this.#server = fastify({
52
59
  logger: false,
53
- bodyLimit: this.#serverConfigs.web_server.bodyLimit,
54
- trustProxy: this.#serverConfigs.web_server.trustProxy,
60
+ bodyLimit: webServerConfig.bodyLimit,
61
+ trustProxy: webServerConfig.trustProxy,
55
62
  requestIdHeader: "x-request-id",
56
63
  exposeHeadRoutes: false,
57
64
  requestTimeout: 60000,
58
65
  connectionTimeout: 120000,
59
66
  routerOptions: {
60
67
  ignoreTrailingSlash: true,
61
- maxParamLength: this.#serverConfigs.web_server.maxParamLength
68
+ maxParamLength: webServerConfig.maxParamLength
62
69
  }
63
70
  });
64
71
 
65
- this.#registerPlugins();
66
- this.#registerHooks();
67
- this.#registerShutdownHandlers();
68
- }
69
-
70
- static #registerPlugins() {
72
+ // Register plugins
71
73
  this.#server.register(fastifyCors, {
72
- origin: this.#getCorsOrigin.bind(this),
73
- credentials: this.#serverConfigs.cors.origin === "*" ? false : this.#serverConfigs.cors.credentials === true
74
+ origin: (requestOrigin, cb) => {
75
+ if (!requestOrigin) {
76
+ return cb(null, true);
77
+ }
78
+
79
+ const origin = cors.origin;
80
+
81
+ if (origin === "*") {
82
+ return cb(null, true);
83
+ }
84
+ else if (origin === "auto") {
85
+ const url = process.env.APP_URL;
86
+ const port = process.env.APP_PORT;
87
+
88
+ if (!url) {
89
+ return cb(null, false);
90
+ }
91
+
92
+ const appOrigin = !port || port === "80" || port === "443" ? url : `${url}:${port}`;
93
+
94
+ return cb(null, requestOrigin === appOrigin);
95
+ }
96
+ else if (typeof origin === "string") {
97
+ return cb(null, requestOrigin === origin);
98
+ }
99
+ else if (Array.isArray(origin)) {
100
+ return cb(null, origin.includes(requestOrigin));
101
+ }
102
+
103
+ return cb(null, false);
104
+ },
105
+ credentials: cors.origin === "*" ? false : cors.credentials === true
74
106
  });
75
107
 
76
108
  this.#server.register(fastifyStatic, {
@@ -101,110 +133,90 @@ class Server {
101
133
  xssFilter: true
102
134
  });
103
135
 
136
+ const multipartDefaults = {
137
+ fieldNameSize: 100,
138
+ fieldSize: 2 * 1024 * 1024,
139
+ maxFields: 50,
140
+ maxFileSize: 20 * 1024 * 1024,
141
+ maxFiles: 10,
142
+ maxParts: 60,
143
+ attachFieldsToBody: true
144
+ };
145
+
146
+ const multipartConfig = { ...multipartDefaults, ...web_server.multipart };
147
+
104
148
  this.#server.register(fastifyMultipart, {
105
149
  limits: {
106
- fieldNameSize: this.#serverConfigs.web_server.multipart.fieldNameSize,
107
- fieldSize: this.#serverConfigs.web_server.multipart.fieldSize,
108
- fields: this.#serverConfigs.web_server.multipart.maxFields,
109
- fileSize: this.#serverConfigs.web_server.multipart.maxFileSize,
110
- files: this.#serverConfigs.web_server.multipart.maxFiles,
111
- parts: this.#serverConfigs.web_server.multipart.maxParts
150
+ fieldNameSize: multipartConfig.fieldNameSize,
151
+ fieldSize: multipartConfig.fieldSize,
152
+ fields: multipartConfig.maxFields,
153
+ fileSize: multipartConfig.maxFileSize,
154
+ files: multipartConfig.maxFiles,
155
+ parts: multipartConfig.maxParts
112
156
  },
113
- attachFieldsToBody: this.#serverConfigs.web_server.multipart.attachFieldsToBody
157
+ attachFieldsToBody: multipartConfig.attachFieldsToBody
114
158
  });
115
159
 
116
160
  this.#server.register(fastifyFormbody);
117
- }
118
-
119
- static #getCorsOrigin(requestOrigin, cb) {
120
- if (!requestOrigin) return cb(null, true);
121
-
122
- const { origin } = this.#serverConfigs.cors;
123
-
124
- if (origin === "*") return cb(null, true);
125
-
126
- if (origin === "auto") {
127
- const url = process.env.APP_URL;
128
- const port = process.env.APP_PORT;
129
- if (!url) return cb(null, false);
130
161
 
131
- const appOrigin = !port || port === "80" || port === "443" ? url : `${url}:${port}`;
132
- return cb(null, requestOrigin === appOrigin);
133
- }
134
-
135
- if (typeof origin === "string") return cb(null, requestOrigin === origin);
136
- if (Array.isArray(origin)) return cb(null, origin.includes(requestOrigin));
162
+ // Register hooks
163
+ this.#server.addHook("preHandler", async (req) => {
164
+ if (!req.isMultipart() || !req.body) {
165
+ return;
166
+ }
137
167
 
138
- return cb(null, false);
139
- }
168
+ const normalized = {};
169
+ const files = {};
140
170
 
141
- static #parseBracketNotation(data) {
142
- if (!data || typeof data !== "object") {
143
- return data;
144
- }
171
+ for (const [key, field] of Object.entries(req.body)) {
172
+ if (field?.toBuffer) {
173
+ if (field.filename) {
174
+ files[key] = field;
175
+ }
176
+ }
177
+ else if (field && typeof field === "object" && "value" in field) {
178
+ normalized[key] = field.value;
179
+ }
180
+ else {
181
+ normalized[key] = field;
182
+ }
183
+ }
145
184
 
146
- const hasBrackets = Object.keys(data).some(key => key.includes("["));
147
- if (!hasBrackets) {
148
- return data;
149
- }
185
+ const data = { ...normalized, ...files };
186
+ const hasBrackets = Object.keys(data).some(key => key.includes("["));
150
187
 
151
- const result = {};
188
+ if (!hasBrackets) {
189
+ req.body = data;
152
190
 
153
- for (const [key, value] of Object.entries(data)) {
154
- if (!key.includes("[")) {
155
- result[key] = value;
156
- continue;
191
+ return;
157
192
  }
158
193
 
159
- const parts = key.replace(/\]/g, "").split("[");
160
- let current = result;
161
-
162
- for (let i = 0; i < parts.length - 1; i++) {
163
- const part = parts[i];
164
- const nextPart = parts[i + 1];
165
- const isNextIndex = /^\d+$/.test(nextPart);
194
+ const result = {};
166
195
 
167
- if (current[part] === undefined) {
168
- current[part] = isNextIndex ? [] : {};
196
+ for (const [key, value] of Object.entries(data)) {
197
+ if (!key.includes("[")) {
198
+ result[key] = value;
199
+ continue;
169
200
  }
170
201
 
171
- current = current[part];
172
- }
173
-
174
- const lastPart = parts[parts.length - 1];
175
- current[lastPart] = value;
176
- }
202
+ const parts = key.replace(/\]/g, "").split("[");
203
+ let current = result;
177
204
 
178
- return result;
179
- }
205
+ for (let i = 0; i < parts.length - 1; i++) {
206
+ const part = parts[i];
207
+ const isNextIndex = /^\d+$/.test(parts[i + 1]);
180
208
 
181
- static #registerHooks() {
182
- this.#server.addHook("preHandler", async (req) => {
183
- if (req.isMultipart() && req.body) {
184
- const normalized = {};
185
- const files = {};
186
-
187
- for (const [key, field] of Object.entries(req.body)) {
188
- if (field && typeof field === 'object') {
189
- if (field.toBuffer) {
190
- if (!field.filename) continue;
191
-
192
- files[key] = field;
193
- }
194
- else if ('value' in field) {
195
- normalized[key] = field.value;
196
- }
197
- else {
198
- normalized[key] = field;
199
- }
200
- }
201
- else {
202
- normalized[key] = field;
209
+ if (current[part] === undefined) {
210
+ current[part] = isNextIndex ? [] : {};
203
211
  }
212
+
213
+ current = current[part];
204
214
  }
205
215
 
206
- req.body = this.#parseBracketNotation({ ...normalized, ...files });
216
+ current[parts[parts.length - 1]] = value;
207
217
  }
218
+
219
+ req.body = result;
208
220
  });
209
221
 
210
222
  this.#server.addHook("onResponse", async (req, res) => {
@@ -220,26 +232,51 @@ class Server {
220
232
  requestId: req.id
221
233
  });
222
234
  });
223
- }
224
235
 
225
- static #registerShutdownHandlers() {
236
+ // Register shutdown handlers
226
237
  const shutdown = async () => {
227
- const sessionManager = await SessionManager.getInstance();
228
- sessionManager.stopGarbageCollection();
229
- await DB.close();
230
- await this.#server.close();
231
- process.exit(0);
238
+ console.log();
239
+ console.log("\x1b[33m⏻ Server shutting down...\x1b[0m");
240
+ console.log("\x1b[32m✓ Server stopped gracefully\x1b[0m");
241
+
242
+ try {
243
+ (await SessionManager.getInstance()).stopGC();
244
+ await Promise.race([
245
+ DB.close(),
246
+ new Promise(resolve => setTimeout(resolve, 1000))
247
+ ]);
248
+ await Promise.race([
249
+ this.#server.close(),
250
+ new Promise(resolve => setTimeout(resolve, 1000))
251
+ ]);
252
+ }
253
+ catch {
254
+ // Ignore errors during shutdown
255
+ }
232
256
  };
233
257
 
234
258
  process.on("SIGTERM", shutdown);
235
259
  process.on("SIGINT", shutdown);
236
- }
237
260
 
238
- static async #listen() {
261
+ // Setup managers
262
+ await DB.setup();
263
+ await SessionManager.setup(this.#server);
264
+ Auth.setup(this.#server);
265
+
266
+ // Register HMR routes before Router (dev only)
267
+ if (Environment.isDev) {
268
+ HMRServer.registerRoutes(this.#server);
269
+ }
270
+
271
+ await Router.setup(this.#server);
272
+ View.setup(this.#server);
273
+
274
+ // Listen
239
275
  try {
240
276
  const url = process.env.APP_URL ? new URL(process.env.APP_URL) : null;
241
- const host = url ? url.hostname : "127.0.0.1";
277
+ const host = url?.hostname || "127.0.0.1";
242
278
  const port = Number(process.env.APP_PORT) || 3000;
279
+
243
280
  const address = await this.#server.listen({ host, port });
244
281
 
245
282
  if (Environment.isDev) {
@@ -247,36 +284,30 @@ class Server {
247
284
  }
248
285
 
249
286
  this.#printBanner({ success: true, address, host, port });
250
-
251
- Log.info("Server started successfully!", {
252
- address,
253
- host,
254
- port,
255
- environment: Environment.isDev ? "development" : "production"
256
- });
287
+ Log.info("Server started successfully!", { address, host, port, environment: Environment.isDev ? "development" : "production" });
257
288
  }
258
289
  catch (err) {
259
290
  this.#printBanner({ success: false, error: err });
260
-
261
- Log.fatal("Server startup failed", {
262
- error: err.message,
263
- code: err.code,
264
- stack: err.stack
265
- });
266
-
291
+ Log.fatal("Server startup failed", { error: err.message, code: err.code, stack: err.stack });
267
292
  process.exit(1);
268
293
  }
269
294
  }
270
295
 
296
+ // Private Methods
271
297
  static #printBanner({ success, address, host, port, error }) {
272
- const { log } = this.#serverConfigs;
273
- if (log.channel !== "none" && log.channel !== "file") return;
298
+ if (this.#config.log.channel !== "none" && this.#config.log.channel !== "file") {
299
+ return;
300
+ }
274
301
 
275
302
  const color = success ? "\x1b[32m" : "\x1b[31m";
276
303
  const reset = "\x1b[0m";
277
304
  const bold = "\x1b[1m";
278
305
  const boxWidth = 70;
306
+ const stripAnsi = str => str.replace(/\x1b\[[0-9;]*m/g, "");
307
+ const pad = (content, length) => content + " ".repeat(Math.max(0, length - stripAnsi(content).length));
279
308
 
309
+ const version = `v${packageJson.version}`;
310
+
280
311
  const logo = `
281
312
  ${color}███╗ ██╗██╗████████╗██████╗ ██████╗ ███╗ ██╗ ██╗███████╗
282
313
  ████╗ ██║██║╚══██╔══╝██╔══██╗██╔═══██╗████╗ ██║ ██║██╔════╝
@@ -286,12 +317,6 @@ ${color}███╗ ██╗██╗████████╗████
286
317
  ╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚════╝ ╚══════╝${reset}
287
318
  `;
288
319
 
289
- const stripAnsi = str => str.replace(/\x1b\[[0-9;]*m/g, "");
290
- const pad = (content, length) => {
291
- const visible = stripAnsi(content);
292
- return content + " ".repeat(Math.max(0, length - visible.length));
293
- };
294
-
295
320
  const lines = success
296
321
  ? [
297
322
  `${color}${bold}✓${reset} ${bold}Server started successfully!${reset}`,
@@ -299,7 +324,8 @@ ${color}███╗ ██╗██╗████████╗████
299
324
  `${bold}Address:${reset} ${address}`,
300
325
  `${bold}Host:${reset} ${host}`,
301
326
  `${bold}Port:${reset} ${port}`,
302
- `${bold}Mode:${reset} ${Environment.isDev ? "development" : "production"}`
327
+ `${bold}Mode:${reset} ${Environment.isDev ? "development" : "production"}`,
328
+ `${bold}Version:${reset} ${version}`
303
329
  ]
304
330
  : [
305
331
  `${color}${bold}✕${reset} ${bold}Server failed to start${reset}`,
@@ -317,7 +343,6 @@ ${color}███╗ ██╗██╗████████╗████
317
343
 
318
344
  console.log(`${color}│${reset} ${" ".repeat(boxWidth - 4)} ${color}│${reset}`);
319
345
  console.log(`${color}└${"─".repeat(boxWidth - 2)}┘${reset}`);
320
- console.log();
321
346
  }
322
347
  }
323
348