@nitronjs/framework 0.2.13 → 0.2.15

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/cli/njs.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  const COLORS = {
4
4
  reset: "\x1b[0m",
@@ -95,8 +95,8 @@ async function run() {
95
95
  }
96
96
 
97
97
  case "start": {
98
- const { start } = await import("../lib/Runtime/Entry.js");
99
- await start();
98
+ const { default: Start } = await import("../lib/Console/Commands/StartCommand.js");
99
+ await Start();
100
100
  break;
101
101
  }
102
102
 
package/lib/Auth/Auth.js CHANGED
@@ -82,6 +82,7 @@ class Auth {
82
82
  const guard = guardName || authConfig.defaults.guard;
83
83
 
84
84
  req.session.set(`auth_${guard}`, null);
85
+ req.session.regenerate();
85
86
  }
86
87
 
87
88
  /**
@@ -116,8 +116,11 @@ class Builder {
116
116
  };
117
117
  } catch (error) {
118
118
  this.#cleanupTemp();
119
- if (!silent) console.log(`\n${COLORS.red}✖ Build failed: ${error.message}${COLORS.reset}\n`);
120
- if (this.#isDev && !silent) console.error(error);
119
+
120
+ if (!silent) {
121
+ console.log(this.#formatBuildError(error));
122
+ }
123
+
121
124
  return { success: false, error: error.message };
122
125
  }
123
126
  }
@@ -580,6 +583,63 @@ class Builder {
580
583
  }
581
584
  }
582
585
 
586
+ #formatBuildError(error) {
587
+ const R = COLORS.red, Y = COLORS.yellow, D = COLORS.dim, C = COLORS.reset;
588
+
589
+ // FileAnalyzer errors already have formatted messages
590
+ if (error.message?.includes("✖")) {
591
+ return `\n${error.message}\n`;
592
+ }
593
+
594
+ // esbuild errors — extract detailed info from .errors array
595
+ if (error.errors?.length > 0) {
596
+ let output = `\n${R}✖ Build failed${C}\n`;
597
+
598
+ for (const err of error.errors.slice(0, 5)) {
599
+ const loc = err.location;
600
+ output += `\n ${R}${err.text}${C}\n`;
601
+
602
+ if (loc) {
603
+ const file = loc.file ? path.relative(Paths.project, loc.file) : "unknown";
604
+ output += ` ${D}at${C} ${Y}${file}:${loc.line}:${loc.column}${C}\n`;
605
+
606
+ if (loc.lineText) {
607
+ output += ` ${D}${loc.lineText.trim()}${C}\n`;
608
+ }
609
+ }
610
+
611
+ if (err.notes?.length > 0) {
612
+ for (const note of err.notes) {
613
+ output += ` ${D}→ ${note.text}${C}\n`;
614
+ }
615
+ }
616
+ }
617
+
618
+ if (error.errors.length > 5) {
619
+ output += `\n ${D}... and ${error.errors.length - 5} more errors${C}\n`;
620
+ }
621
+
622
+ return output;
623
+ }
624
+
625
+ // Generic errors — provide hint based on common patterns
626
+ const msg = error.message || "Unknown error";
627
+ let output = `\n${R}✖ Build failed${C}\n\n ${msg}\n`;
628
+
629
+ if (msg.includes("ENOENT")) {
630
+ output += `\n ${Y}Hint:${C} A file or directory was not found. Check your import paths.\n`;
631
+ }
632
+ else if (msg.includes("Duplicate")) {
633
+ output += `\n ${Y}Hint:${C} Two views resolve to the same route. Rename one of them.\n`;
634
+ }
635
+ else if (msg.includes("Cannot find module")) {
636
+ const match = msg.match(/Cannot find module ['"]([^'"]+)['"]/);
637
+ if (match) output += `\n ${Y}Hint:${C} Module "${match[1]}" is not installed. Run npm install.\n`;
638
+ }
639
+
640
+ return output;
641
+ }
642
+
583
643
  #cleanupTemp() {
584
644
  const projectDir = path.normalize(Paths.project) + path.sep;
585
645
  const normalizedTemp = path.normalize(this.#paths.nitronTemp);
@@ -35,7 +35,7 @@ globalThis.csrf = () => getContext()?.csrf || '';
35
35
 
36
36
  globalThis.request = () => {
37
37
  const ctx = getContext();
38
- return ctx?.request || { params: {}, query: {}, url: '', method: 'GET', headers: {} };
38
+ return ctx?.request || { path: '', method: 'GET', query: {}, params: {}, headers: {}, cookies: {}, ip: '', isAjax: false, session: null, auth: null };
39
39
  };
40
40
 
41
41
  const DepthContext = React.createContext(false);
@@ -0,0 +1,24 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import Paths from "../../Core/Paths.js";
4
+ import Output from "../Output.js";
5
+
6
+ export default async function Start() {
7
+ const manifestPath = path.join(Paths.build, "manifest.json");
8
+
9
+ if (!fs.existsSync(manifestPath)) {
10
+ Output.warn("Build artifacts not found. Running production build...");
11
+ Output.newline();
12
+
13
+ const { default: Build } = await import("./BuildCommand.js");
14
+ const success = await Build();
15
+
16
+ if (!success) {
17
+ Output.error("Build failed. Cannot start server.");
18
+ process.exit(1);
19
+ }
20
+ }
21
+
22
+ const { start } = await import("../../Runtime/Entry.js");
23
+ await start();
24
+ }
@@ -21,7 +21,14 @@ class DateTime {
21
21
  static #format(date, formatString) {
22
22
  const lang = locale[Config.get('app.locale', 'en')] || locale.en;
23
23
  const pad = (n) => String(n).padStart(2, '0');
24
-
24
+
25
+ // Replace multi-character tokens first to avoid per-character replacement
26
+ let result = formatString;
27
+ result = result.replace(/MMMM/g, '\x01MONTH\x01');
28
+ result = result.replace(/MMM/g, '\x01MONTHSHORT\x01');
29
+ result = result.replace(/DDDD/g, '\x01DAY\x01');
30
+ result = result.replace(/DDD/g, '\x01DAYSHORT\x01');
31
+
25
32
  const map = {
26
33
  'Y': date.getFullYear(),
27
34
  'y': String(date.getFullYear()).slice(-2),
@@ -37,8 +44,16 @@ class DateTime {
37
44
  'M': lang.months[date.getMonth()],
38
45
  'F': lang.monthsShort[date.getMonth()]
39
46
  };
40
-
41
- return formatString.replace(/[YynmdjHislDMF]/g, match => map[match]);
47
+
48
+ result = result.replace(/[YynmdjHislDMF]/g, match => map[match]);
49
+
50
+ // Restore multi-character tokens
51
+ result = result.replace(/\x01MONTH\x01/g, lang.months[date.getMonth()]);
52
+ result = result.replace(/\x01MONTHSHORT\x01/g, lang.monthsShort[date.getMonth()]);
53
+ result = result.replace(/\x01DAY\x01/g, lang.days[date.getDay()]);
54
+ result = result.replace(/\x01DAYSHORT\x01/g, lang.daysShort[date.getDay()]);
55
+
56
+ return result;
42
57
  }
43
58
 
44
59
  /**
@@ -18,7 +18,6 @@ import Auth from "../Auth/Auth.js";
18
18
  import SessionManager from "../Session/Manager.js";
19
19
  import DB from "../Database/DB.js";
20
20
  import Log from "../Logging/Log.js";
21
- import HMRServer from "../HMR/Server.js";
22
21
 
23
22
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
23
  const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, "../../package.json"), "utf8"));
@@ -107,10 +106,47 @@ class Server {
107
106
 
108
107
  this.#server.register(fastifyStatic, {
109
108
  root: Paths.public,
110
- decorateReply: false,
111
- index: false
109
+ decorateReply: true,
110
+ index: false,
111
+ wildcard: false
112
112
  });
113
113
 
114
+ // Register explicit routes for each public subdirectory.
115
+ // Routes like /storage/* have a static prefix and take priority over
116
+ // parametric routes (/:slug), preventing catch-all routes from
117
+ // intercepting static file requests.
118
+ if (fs.existsSync(Paths.public)) {
119
+ for (const entry of fs.readdirSync(Paths.public)) {
120
+ const fullPath = path.resolve(Paths.public, entry);
121
+
122
+ try {
123
+ if (!fs.statSync(fullPath).isDirectory()) continue;
124
+ }
125
+ catch {
126
+ continue;
127
+ }
128
+
129
+ this.#server.route({
130
+ method: ["GET", "HEAD"],
131
+ url: `/${entry}/*`,
132
+ handler(req, reply) {
133
+ return reply.sendFile(req.params["*"], fullPath);
134
+ }
135
+ });
136
+ }
137
+
138
+ // Fallback wildcard for root-level files (favicon.ico, robots.txt, etc.)
139
+ // Less specific than /:slug so it loses when parametric routes exist,
140
+ // but keeps backward compatibility for projects without catch-all routes.
141
+ this.#server.route({
142
+ method: ["GET", "HEAD"],
143
+ url: "/*",
144
+ handler(req, reply) {
145
+ return reply.sendFile(req.params["*"]);
146
+ }
147
+ });
148
+ }
149
+
114
150
  this.#server.register(fastifyCookie, {
115
151
  secret: process.env.APP_KEY,
116
152
  hook: "onRequest"
@@ -267,6 +303,7 @@ class Server {
267
303
 
268
304
  // Register HMR routes before Router (dev only)
269
305
  if (Environment.isDev) {
306
+ const { default: HMRServer } = await import("../HMR/Server.js");
270
307
  HMRServer.registerRoutes(this.#server);
271
308
  }
272
309
 
@@ -282,6 +319,7 @@ class Server {
282
319
  const address = await this.#server.listen({ host, port });
283
320
 
284
321
  if (Environment.isDev) {
322
+ const { default: HMRServer } = await import("../HMR/Server.js");
285
323
  HMRServer.setup(this.#server.server);
286
324
  }
287
325
 
@@ -1,5 +1,4 @@
1
1
  import Server from "../Http/Server.js";
2
- import HMRServer from "../HMR/Server.js";
3
2
  import Environment from "../Core/Environment.js";
4
3
 
5
4
  // Set development mode based on __NITRON_DEV__ env (set by DevCommand)
@@ -8,7 +7,9 @@ Environment.setDev(process.env.__NITRON_DEV__ === "true");
8
7
  export async function start() {
9
8
  await Server.start();
10
9
 
11
- if (process.send) {
10
+ if (Environment.isDev && process.send) {
11
+ const { default: HMRServer } = await import("../HMR/Server.js");
12
+
12
13
  process.on("message", (msg) => {
13
14
  if (!msg?.type || !HMRServer.isReady) return;
14
15
 
@@ -27,7 +27,7 @@ class FileStore {
27
27
  }
28
28
 
29
29
  try {
30
- const content = await fs.readFile(this.#filePath(sessionId), "utf8");
30
+ const content = await this.#retryOnLock(() => fs.readFile(this.#filePath(sessionId), "utf8"));
31
31
 
32
32
  if (!content?.trim()) {
33
33
  await this.delete(sessionId);
@@ -48,6 +48,8 @@ class FileStore {
48
48
  return null;
49
49
  }
50
50
 
51
+ console.error("[Session File] Read error:", err.message);
52
+
51
53
  return null;
52
54
  }
53
55
  }
@@ -67,10 +69,19 @@ class FileStore {
67
69
  const json = Environment.isDev ? JSON.stringify(data, null, 2) : JSON.stringify(data);
68
70
 
69
71
  try {
70
- await fs.writeFile(tempPath, json, "utf8");
72
+ await this.#retryOnLock(() => fs.writeFile(tempPath, json, "utf8"));
71
73
  await this.#atomicRename(tempPath, filePath);
72
74
  }
73
- catch {
75
+ catch (err) {
76
+ // Atomic rename failed — write directly as fallback to prevent session loss
77
+ try {
78
+ await this.#retryOnLock(() => fs.writeFile(filePath, json, "utf8"));
79
+ }
80
+ catch (writeErr) {
81
+ console.error("[Session File] Write fallback failed:", writeErr.message);
82
+ throw writeErr;
83
+ }
84
+
74
85
  await fs.unlink(tempPath).catch(() => {});
75
86
  }
76
87
  }
@@ -85,12 +96,12 @@ class FileStore {
85
96
  }
86
97
 
87
98
  try {
88
- await fs.unlink(this.#filePath(sessionId));
99
+ await this.#retryOnLock(() => fs.unlink(this.#filePath(sessionId)));
89
100
  }
90
101
  catch (err) {
91
- if (err.code !== "ENOENT") {
92
- console.error("[Session File] Delete error:", err.message);
93
- }
102
+ if (err.code === "ENOENT") return;
103
+
104
+ console.error("[Session File] Delete error:", err.message);
94
105
  }
95
106
  }
96
107
 
@@ -118,16 +129,24 @@ class FileStore {
118
129
  const lastActivity = data.lastActivity || data.createdAt;
119
130
 
120
131
  if (now - lastActivity > lifetime) {
121
- await fs.unlink(filePath);
132
+ await fs.unlink(filePath).catch(() => {});
122
133
  deleted++;
123
134
  }
124
135
  }
125
136
  catch {
137
+ // Corrupted or unreadable — try to clean up, skip if locked
126
138
  await fs.unlink(filePath).catch(() => {});
127
139
  deleted++;
128
140
  }
129
141
  }
130
142
 
143
+ // Clean leftover temp files
144
+ for (const file of files) {
145
+ if (file.endsWith(".tmp")) {
146
+ await fs.unlink(path.join(this.#storagePath, file)).catch(() => {});
147
+ }
148
+ }
149
+
131
150
  return deleted;
132
151
  }
133
152
  catch (err) {
@@ -198,15 +217,41 @@ class FileStore {
198
217
  await fs.rename(from, to);
199
218
  }
200
219
  catch (err) {
201
- if (err.code === "EPERM" || err.code === "EEXIST") {
202
- await fs.unlink(to).catch(() => {});
203
- await fs.rename(from, to);
220
+ if (err.code === "EPERM" || err.code === "EBUSY" || err.code === "EEXIST") {
221
+ await this.#retryOnLock(() => fs.copyFile(from, to));
222
+ await fs.unlink(from).catch(() => {});
204
223
  }
205
224
  else {
206
225
  throw err;
207
226
  }
208
227
  }
209
228
  }
229
+
230
+ /**
231
+ * Retries a file operation with exponential backoff on Windows lock errors.
232
+ * @private
233
+ * @param {Function} fn - Async function to retry
234
+ * @param {number} maxRetries - Maximum retry attempts (default: 4)
235
+ * @returns {Promise<*>}
236
+ */
237
+ async #retryOnLock(fn, maxRetries = 4) {
238
+ let delay = 50;
239
+
240
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
241
+ try {
242
+ return await fn();
243
+ }
244
+ catch (err) {
245
+ if ((err.code === "EBUSY" || err.code === "EPERM") && attempt < maxRetries) {
246
+ await new Promise(r => setTimeout(r, delay));
247
+ delay *= 2;
248
+ continue;
249
+ }
250
+
251
+ throw err;
252
+ }
253
+ }
254
+ }
210
255
  }
211
256
 
212
257
  export default FileStore;
@@ -179,23 +179,21 @@ class SessionManager {
179
179
  });
180
180
 
181
181
  server.addHook("onSend", async (request, response, payload) => {
182
- if (request.session?.shouldRegenerate()) {
183
- response.setCookie(manager.cookieName, request.session.id, manager.cookieConfig);
184
- }
182
+ if (!request.session) return payload;
185
183
 
186
- return payload;
187
- });
184
+ try {
185
+ await manager.persist(request.session);
188
186
 
189
- server.addHook("onResponse", async (request, response) => {
190
- if (!request.session || response.statusCode >= 400) {
191
- return;
187
+ if (request.session.shouldRegenerate()) {
188
+ response.setCookie(manager.cookieName, request.session.id, manager.cookieConfig);
189
+ await manager.deleteOld(request.session.getOldId());
190
+ }
192
191
  }
193
-
194
- if (request.session.shouldRegenerate()) {
195
- await manager.deleteOld(request.session.getOldId());
192
+ catch (err) {
193
+ console.error("[Session] Persist error:", err.message);
196
194
  }
197
195
 
198
- await manager.persist(request.session);
196
+ return payload;
199
197
  });
200
198
 
201
199
  manager.#startGC();
@@ -17,6 +17,10 @@ class Session {
17
17
  this.#data = sessionData.data || {};
18
18
  this.#createdAt = sessionData.createdAt || Date.now();
19
19
  this.#lastActivity = sessionData.lastActivity || null;
20
+
21
+ // Age flash data: previous request's flash becomes readable, then expires.
22
+ this.#data._previousFlash = this.#data._flash || {};
23
+ this.#data._flash = {};
20
24
  }
21
25
 
22
26
  get id() {
@@ -50,11 +54,14 @@ class Session {
50
54
  }
51
55
 
52
56
  /**
53
- * Gets all session data (copy).
57
+ * Gets all session data for persistence.
58
+ * Excludes aged flash data that should not survive another request.
54
59
  * @returns {Object}
55
60
  */
56
61
  all() {
57
- return { ...this.#data };
62
+ const { _previousFlash, ...rest } = this.#data;
63
+
64
+ return rest;
58
65
  }
59
66
 
60
67
  /**
@@ -100,32 +107,21 @@ class Session {
100
107
  }
101
108
 
102
109
  /**
103
- * Stores a flash message (available only for next request).
110
+ * Stores a flash message (available only for the next request).
104
111
  * @param {string} key
105
112
  * @param {*} value
106
113
  */
107
114
  flash(key, value) {
108
- if (!this.#data._flash) {
109
- this.#data._flash = {};
110
- }
111
-
112
115
  this.#data._flash[key] = value;
113
116
  }
114
117
 
115
118
  /**
116
- * Gets and removes a flash message.
119
+ * Gets a flash message from the previous request.
117
120
  * @param {string} key
118
121
  * @returns {*}
119
122
  */
120
123
  getFlash(key) {
121
- if (!this.#data._flash?.[key]) {
122
- return undefined;
123
- }
124
-
125
- const value = this.#data._flash[key];
126
- delete this.#data._flash[key];
127
-
128
- return value;
124
+ return this.#data._previousFlash?.[key];
129
125
  }
130
126
 
131
127
  /**
package/lib/View/View.js CHANGED
@@ -53,6 +53,7 @@ class View {
53
53
  static #manifest = null;
54
54
  static #routesCache = null;
55
55
  static #manifestMtime = null;
56
+ static #moduleCache = new Map();
56
57
 
57
58
  static get #isDev() {
58
59
  return Environment.isDev;
@@ -291,94 +292,102 @@ class View {
291
292
  throw new Error(`View not found: ${name}`);
292
293
  }
293
294
 
294
- const mod = await import(pathToFileURL(viewPath).href + `?t=${Date.now()}`);
295
- if (!mod.default) {
296
- throw new Error("View must have a default export");
297
- }
298
-
299
- const layoutChain = entry?.layouts || [];
300
- const layoutModules = [];
301
-
302
- for (const layoutName of layoutChain) {
303
- const layoutPath = path.join(baseDir, layoutName + ".js");
304
- if (existsSync(layoutPath)) {
305
- const layoutMod = await import(pathToFileURL(layoutPath).href + `?t=${Date.now()}`);
306
- if (layoutMod.default) {
307
- layoutModules.push(layoutMod);
308
- }
309
- }
310
- }
311
-
312
295
  const nonce = randomBytes(16).toString("hex");
313
296
  const ctx = {
314
297
  nonce,
315
298
  csrf,
316
299
  props: {},
317
300
  request: fastifyRequest ? {
318
- params: fastifyRequest.params || {},
319
- query: fastifyRequest.query || {},
320
- url: fastifyRequest.url || '',
301
+ path: (fastifyRequest.url || '').split('?')[0],
321
302
  method: fastifyRequest.method || 'GET',
322
- headers: fastifyRequest.headers || {}
303
+ query: fastifyRequest.query || {},
304
+ params: fastifyRequest.params || {},
305
+ headers: fastifyRequest.headers || {},
306
+ cookies: fastifyRequest.cookies || {},
307
+ ip: fastifyRequest.ip || '',
308
+ isAjax: fastifyRequest.headers?.['x-requested-with'] === 'XMLHttpRequest',
309
+ session: fastifyRequest.session || null,
310
+ auth: fastifyRequest.auth || null,
323
311
  } : null
324
312
  };
325
- let html = "";
326
- let collectedProps = {};
327
313
 
328
- try {
329
- const Component = mod.default;
330
- let element;
331
-
332
- if (Component[MARK]) {
333
- ctx.props[":R0:"] = this.#sanitizeProps(params);
334
- element = React.createElement(
335
- "div",
336
- {
337
- "data-cid": ":R0:",
338
- "data-island": Component.displayName || Component.name || "Anonymous"
339
- },
340
- React.createElement(Component, params)
341
- );
342
- } else {
343
- element = React.createElement(Component, params);
314
+ return Context.run(ctx, async () => {
315
+ const mod = await this.#importModule(viewPath);
316
+ if (!mod.default) {
317
+ throw new Error("View must have a default export");
344
318
  }
345
319
 
346
- if (layoutModules.length > 0) {
347
- element = React.createElement("div", { "data-nitron-slot": "page" }, element);
348
- }
320
+ const layoutChain = entry?.layouts || [];
321
+ const layoutModules = [];
349
322
 
350
- for (let i = layoutModules.length - 1; i >= 0; i--) {
351
- const LayoutComponent = layoutModules[i].default;
352
- element = React.createElement(LayoutComponent, { children: element });
323
+ for (const layoutName of layoutChain) {
324
+ const layoutPath = path.join(baseDir, layoutName + ".js");
325
+ if (existsSync(layoutPath)) {
326
+ const layoutMod = await this.#importModule(layoutPath);
327
+ if (layoutMod.default) {
328
+ layoutModules.push(layoutMod);
329
+ }
330
+ }
353
331
  }
354
332
 
355
- html = await Context.run(ctx, () => this.#renderToHtml(element));
356
- collectedProps = ctx.props;
357
- } catch (error) {
358
- const componentName = mod.default?.displayName || mod.default?.name || "Unknown";
359
- const errorDetails = this.#parseReactError(error);
360
-
361
- Log.error("Render Error", {
362
- view: name,
363
- component: componentName,
364
- error: error.message,
365
- cause: errorDetails.cause,
366
- location: errorDetails.location,
367
- stack: error.stack
368
- });
333
+ let html = "";
334
+ let collectedProps = {};
369
335
 
370
- error.statusCode = error.statusCode || 500;
371
- throw error;
372
- }
336
+ try {
337
+ const Component = mod.default;
338
+ let element;
339
+
340
+ if (Component[MARK]) {
341
+ ctx.props[":R0:"] = this.#sanitizeProps(params);
342
+ element = React.createElement(
343
+ "div",
344
+ {
345
+ "data-cid": ":R0:",
346
+ "data-island": Component.displayName || Component.name || "Anonymous"
347
+ },
348
+ React.createElement(Component, params)
349
+ );
350
+ } else {
351
+ element = React.createElement(Component, params);
352
+ }
373
353
 
374
- const mergedMeta = this.#mergeMeta(layoutModules, mod.Meta, params);
354
+ if (layoutModules.length > 0) {
355
+ element = React.createElement("div", { "data-nitron-slot": "page" }, element);
356
+ }
375
357
 
376
- return {
377
- html,
378
- nonce,
379
- meta: mergedMeta,
380
- props: collectedProps
381
- };
358
+ for (let i = layoutModules.length - 1; i >= 0; i--) {
359
+ const LayoutComponent = layoutModules[i].default;
360
+ element = React.createElement(LayoutComponent, { children: element });
361
+ }
362
+
363
+ html = await this.#renderToHtml(element);
364
+ collectedProps = ctx.props;
365
+ } catch (error) {
366
+ const componentName = mod.default?.displayName || mod.default?.name || "Unknown";
367
+ const errorDetails = this.#parseReactError(error);
368
+
369
+ Log.error("Render Error", {
370
+ view: name,
371
+ component: componentName,
372
+ error: error.message,
373
+ cause: errorDetails.cause,
374
+ location: errorDetails.location,
375
+ stack: error.stack
376
+ });
377
+
378
+ error.statusCode = error.statusCode || 500;
379
+ throw error;
380
+ }
381
+
382
+ const mergedMeta = this.#mergeMeta(layoutModules, mod.Meta, params);
383
+
384
+ return {
385
+ html,
386
+ nonce,
387
+ meta: mergedMeta,
388
+ props: collectedProps
389
+ };
390
+ });
382
391
  }
383
392
 
384
393
  static #mergeMeta(layoutModules, viewMeta, params = {}) {
@@ -460,6 +469,21 @@ class View {
460
469
  return result;
461
470
  }
462
471
 
472
+ static async #importModule(filePath) {
473
+ const url = pathToFileURL(filePath).href;
474
+
475
+ if (this.#isDev) {
476
+ return import(url + `?t=${Date.now()}`);
477
+ }
478
+
479
+ let mod = this.#moduleCache.get(filePath);
480
+ if (!mod) {
481
+ mod = await import(url);
482
+ this.#moduleCache.set(filePath, mod);
483
+ }
484
+ return mod;
485
+ }
486
+
463
487
  static #sanitizeProps(obj, seen = new WeakSet()) {
464
488
  if (obj == null) {
465
489
  return obj;
@@ -715,7 +739,7 @@ class View {
715
739
  const devIndicator = this.#isDev ? this.#generateDevIndicator(nonceAttr) : "";
716
740
 
717
741
  return `<!DOCTYPE html>
718
- <html lang="en">
742
+ <html lang="${meta?.lang || "en"}">
719
743
  <head>
720
744
  ${this.#generateHead(meta, css)}
721
745
  </head>
@@ -738,6 +762,14 @@ ${vendorScript}${hmrScript}${hydrateScript}${spaScript}${devIndicator}
738
762
  parts.push(`<meta name="description" content="${escapeHtml(meta.description)}">`);
739
763
  }
740
764
 
765
+ if (meta.keywords) {
766
+ parts.push(`<meta name="keywords" content="${escapeHtml(meta.keywords)}">`);
767
+ }
768
+
769
+ if (meta.favicon) {
770
+ parts.push(`<link rel="icon" href="${escapeHtml(meta.favicon)}">`);
771
+ }
772
+
741
773
  for (const href of css) {
742
774
  parts.push(`<link rel="stylesheet" href="${escapeHtml(href)}">`);
743
775
  }
package/lib/index.d.ts CHANGED
@@ -224,7 +224,7 @@ export class Lang {
224
224
  export class DateTime {
225
225
  static toSQL(timestamp?: number | null): string;
226
226
  static getTime(sqlDateTime?: string | null): number;
227
- static getDate(timestamp?: number | null, format?: string): string;
227
+ static getDate(timestamp?: string | number | null, format?: string): string;
228
228
  static addDays(days: number): string;
229
229
  static addHours(hours: number): string;
230
230
  static subDays(days: number): string;
@@ -503,7 +503,6 @@ declare global {
503
503
  method: string;
504
504
  query: Record<string, any>;
505
505
  params: Record<string, any>;
506
- body: Record<string, any>;
507
506
  headers: Record<string, string>;
508
507
  cookies: Record<string, string>;
509
508
  ip: string;
@@ -515,5 +514,13 @@ declare global {
515
514
  forget(key: string): void;
516
515
  flash(key: string, value: any): void;
517
516
  };
517
+ auth: {
518
+ guard(name?: string): {
519
+ user<T = any>(): Promise<T | null>;
520
+ check(): boolean;
521
+ };
522
+ user<T = any>(): Promise<T | null>;
523
+ check(): boolean;
524
+ };
518
525
  };
519
526
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitronjs/framework",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
4
4
  "description": "NitronJS is a modern and extensible Node.js MVC framework built on Fastify. It focuses on clean architecture, modular structure, and developer productivity, offering built-in routing, middleware, configuration management, CLI tooling, and native React integration for scalable full-stack applications.",
5
5
  "bin": {
6
6
  "njs": "./cli/njs.js"
@@ -12,6 +12,7 @@
12
12
  "forceConsistentCasingInFileNames": true,
13
13
  "noEmit": true,
14
14
  "baseUrl": ".",
15
+ "allowJs": true,
15
16
  "paths": {
16
17
  "@/*": ["./*"],
17
18
  "@css/*": ["./resources/css/*"],