@nitronjs/framework 0.2.3 → 0.2.5

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 +14 -7
  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 +17 -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 +142 -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 +81 -10
  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 +94 -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
@@ -0,0 +1,416 @@
1
+ /**
2
+ * NitronJS Router
3
+ *
4
+ * Handles route definitions, middleware chains, and Fastify integration.
5
+ * Includes built-in Hot Module Replacement (HMR) for development.
6
+ *
7
+ * @example
8
+ * // routes/web.js
9
+ * import { Route } from "@nitronjs/framework";
10
+ * import HomeController from "../app/Controllers/HomeController.js";
11
+ *
12
+ * Route.get("/", HomeController.index).name("home");
13
+ *
14
+ * Route.prefix("/admin").middleware("auth").group(() => {
15
+ * Route.get("/dashboard", AdminController.dashboard).name("admin.dashboard");
16
+ * });
17
+ */
18
+
19
+ import fs from "fs";
20
+ import path from "path";
21
+ import Paths from "../Core/Paths.js";
22
+ import Config from "../Core/Config.js";
23
+ import Environment from "../Core/Environment.js";
24
+
25
+ // ─────────────────────────────────────────────────────────────────────────────
26
+ // HMR Handler Registry (Development Only)
27
+ // ─────────────────────────────────────────────────────────────────────────────
28
+
29
+ class HotReloadRegistry {
30
+ #handlers = new Map();
31
+ #initialized = false;
32
+
33
+ async initialize() {
34
+ if (this.#initialized || !Environment.isDev) return;
35
+
36
+ const directories = [Paths.controllers, Paths.middlewares];
37
+
38
+ for (const dir of directories) {
39
+ await this.#scanDirectory(dir);
40
+ }
41
+
42
+ this.#initialized = true;
43
+ }
44
+
45
+ /**
46
+ * Wraps a handler for hot reload support.
47
+ * In production, returns the handler unchanged.
48
+ * In development, returns a wrapper that re-imports the file on each request.
49
+ */
50
+ wrap(handler) {
51
+ if (!Environment.isDev) return handler;
52
+
53
+ const info = this.#handlers.get(handler);
54
+ if (!info) return handler;
55
+
56
+ const { filePath, methodName } = info;
57
+
58
+ return async (request, response, param) => {
59
+ const module = await import(`file://${filePath}?t=${Date.now()}`);
60
+
61
+ return module.default[methodName](request, response, param);
62
+ };
63
+ }
64
+
65
+ async #scanDirectory(dir) {
66
+ if (!fs.existsSync(dir)) return;
67
+
68
+ for (const filePath of this.#getJsFiles(dir)) {
69
+ try {
70
+ const module = await import(`file://${filePath}`);
71
+ const Class = module.default;
72
+
73
+ if (!Class || typeof Class !== "function") continue;
74
+
75
+ for (const name of Object.getOwnPropertyNames(Class)) {
76
+ if (typeof Class[name] === "function" && !["length", "name", "prototype"].includes(name)) {
77
+ this.#handlers.set(Class[name], { filePath, methodName: name });
78
+ }
79
+ }
80
+ }
81
+ catch {
82
+ // Ignore import errors during scanning
83
+ }
84
+ }
85
+ }
86
+
87
+ #getJsFiles(dir) {
88
+ const files = [];
89
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
90
+
91
+ for (const entry of entries) {
92
+ const fullPath = path.join(dir, entry.name);
93
+
94
+ if (entry.isDirectory()) {
95
+ files.push(...this.#getJsFiles(fullPath));
96
+ }
97
+ else if (entry.name.endsWith(".js")) {
98
+ files.push(fullPath);
99
+ }
100
+ }
101
+
102
+ return files;
103
+ }
104
+ }
105
+
106
+ const hotReload = new HotReloadRegistry();
107
+
108
+ // ─────────────────────────────────────────────────────────────────────────────
109
+ // Route Manager
110
+ // ─────────────────────────────────────────────────────────────────────────────
111
+
112
+ class Router {
113
+ static #routes = [];
114
+ static #kernel = null;
115
+
116
+ // ─────────────────────────────────────────────────────────────────────────
117
+ // Setup & Initialization
118
+ // ─────────────────────────────────────────────────────────────────────────
119
+
120
+ /**
121
+ * Initialize router, load routes, and register with Fastify.
122
+ * @param {FastifyInstance} server - Fastify server instance
123
+ */
124
+ static async setup(server) {
125
+ // Initialize HMR registry (development only)
126
+ await hotReload.initialize();
127
+
128
+ // Load route definitions from routes/web.js
129
+ await import(Paths.routeUrl("web"));
130
+
131
+ // Setup global route() helper
132
+ globalThis.route = (name, params, query) => this.url(name, params, query);
133
+
134
+ const Kernel = await this.#getKernel();
135
+ const csrf = Config.all("session").csrf;
136
+
137
+ for (const route of this.#routes) {
138
+ // Auto-add CSRF middleware for applicable routes
139
+ if (csrf.enabled && csrf.methods.includes(route.method)) {
140
+ const isExcluded = csrf.except.some(pattern => {
141
+ if (route.url === pattern) return true;
142
+ if (pattern.includes("*")) {
143
+ return new RegExp("^" + pattern.replace(/\*/g, ".*") + "$").test(route.url);
144
+ }
145
+ return false;
146
+ });
147
+
148
+ if (!isExcluded) {
149
+ route.middlewares.unshift("verify-csrf");
150
+ }
151
+ }
152
+
153
+ // Resolve middleware names to handlers
154
+ const middlewareHandlers = route.middlewares.map(middleware => {
155
+ const [name, param] = middleware.split(":");
156
+
157
+ if (!Kernel.routeMiddlewares[name]) {
158
+ throw new Error(`Middleware '${name}' is not defined in Kernel.js`);
159
+ }
160
+
161
+ const handler = hotReload.wrap(Kernel.routeMiddlewares[name].handler);
162
+
163
+ return (request, response) => handler(request, response, param);
164
+ });
165
+
166
+ // Wrap controller handler for HMR
167
+ const handler = hotReload.wrap(route.handler);
168
+
169
+ server.route({
170
+ method: route.method,
171
+ url: route.url,
172
+ preHandler: middlewareHandlers,
173
+ handler
174
+ });
175
+ }
176
+ }
177
+
178
+ // ─────────────────────────────────────────────────────────────────────────
179
+ // Route Definition Methods
180
+ // ─────────────────────────────────────────────────────────────────────────
181
+
182
+ static get(url, handler) {
183
+ return this.#addRoute("GET", url, handler);
184
+ }
185
+
186
+ static post(url, handler) {
187
+ return this.#addRoute("POST", url, handler);
188
+ }
189
+
190
+ static put(url, handler) {
191
+ return this.#addRoute("PUT", url, handler);
192
+ }
193
+
194
+ static delete(url, handler) {
195
+ return this.#addRoute("DELETE", url, handler);
196
+ }
197
+
198
+ static patch(url, handler) {
199
+ return this.#addRoute("PATCH", url, handler);
200
+ }
201
+
202
+ // ─────────────────────────────────────────────────────────────────────────
203
+ // Route Grouping
204
+ // ─────────────────────────────────────────────────────────────────────────
205
+
206
+ static prefix(url) {
207
+ return new RouteGroup({ prefix: url }, this.#routes);
208
+ }
209
+
210
+ static middleware(handlers) {
211
+ const middlewareArray = typeof handlers === "string" ? [handlers] : handlers;
212
+
213
+ return new RouteGroup({ middlewares: middlewareArray }, this.#routes);
214
+ }
215
+
216
+ static name(name) {
217
+ return new RouteGroup({ name }, this.#routes);
218
+ }
219
+
220
+ // ─────────────────────────────────────────────────────────────────────────
221
+ // URL Generation
222
+ // ─────────────────────────────────────────────────────────────────────────
223
+
224
+ /**
225
+ * Generate URL for a named route.
226
+ *
227
+ * @param {string} name - Route name
228
+ * @param {Object} params - Route parameters
229
+ * @param {Object} query - Query string parameters
230
+ * @returns {string} Generated URL
231
+ */
232
+ static url(name, params = {}, query = {}) {
233
+ const route = this.#routes.find(r => r.name === name);
234
+
235
+ if (!route) {
236
+ throw new Error(`Route '${name}' is not defined`);
237
+ }
238
+
239
+ let url = route.url;
240
+
241
+ for (const key in params) {
242
+ url = url.replace(`:${key}`, params[key]);
243
+ }
244
+
245
+ if (query && Object.keys(query).length > 0) {
246
+ url += "?" + new URLSearchParams(query).toString();
247
+ }
248
+
249
+ return url;
250
+ }
251
+
252
+ /**
253
+ * Get client-side route manifest for frontend navigation.
254
+ */
255
+ static getClientManifest() {
256
+ const manifest = {};
257
+ for (const route of this.#routes) {
258
+ if (route.name) {
259
+ manifest[route.name] = route.url;
260
+ }
261
+ }
262
+ return manifest;
263
+ }
264
+
265
+ /**
266
+ * Match a URL path to a route definition.
267
+ */
268
+ static match(pathname, method = "GET") {
269
+ for (const route of this.#routes) {
270
+ if (route.method !== method) continue;
271
+
272
+ const pattern = route.url
273
+ .replace(/:[^/]+/g, "([^/]+)")
274
+ .replace(/\//g, "\\/");
275
+ const regex = new RegExp(`^${pattern}$`);
276
+ const match = pathname.match(regex);
277
+
278
+ if (match) {
279
+ const paramNames = (route.url.match(/:[^/]+/g) || []).map(p => p.slice(1));
280
+ const params = {};
281
+ paramNames.forEach((name, i) => {
282
+ params[name] = match[i + 1];
283
+ });
284
+ return { handler: route.handler, params, route };
285
+ }
286
+ }
287
+ return null;
288
+ }
289
+
290
+ // ─────────────────────────────────────────────────────────────────────────
291
+ // Internal Methods
292
+ // ─────────────────────────────────────────────────────────────────────────
293
+
294
+ static #addRoute(method, url, handler) {
295
+ const route = {
296
+ method,
297
+ url,
298
+ handler,
299
+ middlewares: [],
300
+ name: null
301
+ };
302
+
303
+ this.#routes.push(route);
304
+
305
+ return new RouteItem(route);
306
+ }
307
+
308
+ static async #getKernel() {
309
+ if (!this.#kernel) {
310
+ this.#kernel = (await import(Paths.kernelUrl())).default;
311
+ }
312
+ return this.#kernel;
313
+ }
314
+
315
+ // Legacy compatibility (used by View/Manager.js)
316
+ static getSessionConfig() {
317
+ return Config.all("session");
318
+ }
319
+
320
+ static async getKernel() {
321
+ return this.#getKernel();
322
+ }
323
+
324
+ // Alias for backwards compatibility
325
+ static route(name, params, query) {
326
+ return this.url(name, params, query);
327
+ }
328
+ }
329
+
330
+ // ─────────────────────────────────────────────────────────────────────────────
331
+ // Route Item (Fluent API for single route)
332
+ // ─────────────────────────────────────────────────────────────────────────────
333
+
334
+ class RouteItem {
335
+ constructor(route) {
336
+ this.route = route;
337
+ }
338
+
339
+ middleware(handlers) {
340
+ if (typeof handlers === "string") {
341
+ this.route.middlewares.push(handlers);
342
+ }
343
+ else if (Array.isArray(handlers)) {
344
+ this.route.middlewares.push(...handlers);
345
+ }
346
+
347
+ return this;
348
+ }
349
+
350
+ name(name) {
351
+ this.route.name = name;
352
+
353
+ return this;
354
+ }
355
+ }
356
+
357
+ // ─────────────────────────────────────────────────────────────────────────────
358
+ // Route Group (Fluent API for grouped routes)
359
+ // ─────────────────────────────────────────────────────────────────────────────
360
+
361
+ class RouteGroup {
362
+ constructor(options, routes) {
363
+ this.options = {
364
+ prefix: options.prefix || "",
365
+ middlewares: options.middlewares || [],
366
+ name: options.name || ""
367
+ };
368
+ this.routes = routes;
369
+ }
370
+
371
+ prefix(url) {
372
+ this.options.prefix = url;
373
+ return this;
374
+ }
375
+
376
+ middleware(handlers) {
377
+ if (typeof handlers === "string") {
378
+ this.options.middlewares.push(handlers);
379
+ }
380
+ else if (Array.isArray(handlers)) {
381
+ this.options.middlewares.push(...handlers);
382
+ }
383
+
384
+ return this;
385
+ }
386
+
387
+ name(name) {
388
+ this.options.name = name;
389
+ return this;
390
+ }
391
+
392
+ group(callback) {
393
+ const beforeCount = this.routes.length;
394
+ callback();
395
+ const newRoutes = this.routes.slice(beforeCount);
396
+
397
+ for (const route of newRoutes) {
398
+ // Apply prefix
399
+ if (this.options.prefix) {
400
+ route.url = route.url === "/"
401
+ ? this.options.prefix
402
+ : this.options.prefix + route.url;
403
+ }
404
+
405
+ // Apply name prefix
406
+ if (route.name && this.options.name) {
407
+ route.name = this.options.name + route.name;
408
+ }
409
+
410
+ // Prepend middlewares
411
+ route.middlewares.unshift(...this.options.middlewares);
412
+ }
413
+ }
414
+ }
415
+
416
+ export default Router;