@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 +3 -3
- package/lib/Auth/Auth.js +1 -0
- package/lib/Build/Manager.js +62 -2
- package/lib/Build/jsxRuntime.js +1 -1
- package/lib/Console/Commands/StartCommand.js +24 -0
- package/lib/Date/DateTime.js +18 -3
- package/lib/Http/Server.js +41 -3
- package/lib/Runtime/Entry.js +3 -2
- package/lib/Session/File.js +56 -11
- package/lib/Session/Manager.js +10 -12
- package/lib/Session/Session.js +12 -16
- package/lib/View/View.js +103 -71
- package/lib/index.d.ts +9 -2
- package/package.json +1 -1
- package/skeleton/tsconfig.json +1 -0
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 {
|
|
99
|
-
await
|
|
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
package/lib/Build/Manager.js
CHANGED
|
@@ -116,8 +116,11 @@ class Builder {
|
|
|
116
116
|
};
|
|
117
117
|
} catch (error) {
|
|
118
118
|
this.#cleanupTemp();
|
|
119
|
-
|
|
120
|
-
if (
|
|
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);
|
package/lib/Build/jsxRuntime.js
CHANGED
|
@@ -35,7 +35,7 @@ globalThis.csrf = () => getContext()?.csrf || '';
|
|
|
35
35
|
|
|
36
36
|
globalThis.request = () => {
|
|
37
37
|
const ctx = getContext();
|
|
38
|
-
return ctx?.request || { params: {},
|
|
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
|
+
}
|
package/lib/Date/DateTime.js
CHANGED
|
@@ -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
|
-
|
|
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
|
/**
|
package/lib/Http/Server.js
CHANGED
|
@@ -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:
|
|
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
|
|
package/lib/Runtime/Entry.js
CHANGED
|
@@ -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
|
|
package/lib/Session/File.js
CHANGED
|
@@ -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
|
|
92
|
-
|
|
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
|
|
203
|
-
await fs.
|
|
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;
|
package/lib/Session/Manager.js
CHANGED
|
@@ -179,23 +179,21 @@ class SessionManager {
|
|
|
179
179
|
});
|
|
180
180
|
|
|
181
181
|
server.addHook("onSend", async (request, response, payload) => {
|
|
182
|
-
if (request.session
|
|
183
|
-
response.setCookie(manager.cookieName, request.session.id, manager.cookieConfig);
|
|
184
|
-
}
|
|
182
|
+
if (!request.session) return payload;
|
|
185
183
|
|
|
186
|
-
|
|
187
|
-
|
|
184
|
+
try {
|
|
185
|
+
await manager.persist(request.session);
|
|
188
186
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
195
|
-
await manager.deleteOld(request.session.getOldId());
|
|
192
|
+
catch (err) {
|
|
193
|
+
console.error("[Session] Persist error:", err.message);
|
|
196
194
|
}
|
|
197
195
|
|
|
198
|
-
|
|
196
|
+
return payload;
|
|
199
197
|
});
|
|
200
198
|
|
|
201
199
|
manager.#startGC();
|
package/lib/Session/Session.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
119
|
+
* Gets a flash message from the previous request.
|
|
117
120
|
* @param {string} key
|
|
118
121
|
* @returns {*}
|
|
119
122
|
*/
|
|
120
123
|
getFlash(key) {
|
|
121
|
-
|
|
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
|
-
|
|
319
|
-
query: fastifyRequest.query || {},
|
|
320
|
-
url: fastifyRequest.url || '',
|
|
301
|
+
path: (fastifyRequest.url || '').split('?')[0],
|
|
321
302
|
method: fastifyRequest.method || 'GET',
|
|
322
|
-
|
|
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
|
-
|
|
329
|
-
const
|
|
330
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
320
|
+
const layoutChain = entry?.layouts || [];
|
|
321
|
+
const layoutModules = [];
|
|
349
322
|
|
|
350
|
-
for (
|
|
351
|
-
const
|
|
352
|
-
|
|
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 =
|
|
356
|
-
collectedProps =
|
|
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
|
-
|
|
371
|
-
|
|
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
|
-
|
|
354
|
+
if (layoutModules.length > 0) {
|
|
355
|
+
element = React.createElement("div", { "data-nitron-slot": "page" }, element);
|
|
356
|
+
}
|
|
375
357
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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.
|
|
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"
|