@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.
- package/README.md +3 -1
- package/cli/create.js +88 -72
- package/cli/njs.js +13 -6
- 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 +0 -1
- package/lib/Console/Commands/MigrateFreshCommand.js +18 -25
- package/lib/Console/Commands/MigrateRollbackCommand.js +6 -3
- package/lib/Console/Commands/MigrateStatusCommand.js +6 -3
- package/lib/Console/Commands/SeedCommand.js +4 -2
- 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 -0
- 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 +56 -61
- package/lib/Database/Model.js +157 -83
- package/lib/Database/QueryBuilder.js +31 -0
- package/lib/Database/QueryValidation.js +36 -44
- package/lib/Database/Schema/Blueprint.js +25 -36
- package/lib/Database/Schema/Manager.js +31 -68
- package/lib/Database/Seeder/SeederRunner.js +12 -31
- 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 +177 -152
- 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 +42 -8
- package/lib/index.js +19 -12
- package/package.json +1 -1
- package/skeleton/app/Controllers/HomeController.js +7 -1
- 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/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
|
@@ -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;
|