@nitronjs/framework 0.2.13 → 0.2.14
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/lib/Auth/Auth.js +1 -0
- package/lib/Build/Manager.js +62 -2
- package/lib/Build/jsxRuntime.js +1 -1
- package/lib/Date/DateTime.js +18 -3
- package/lib/Http/Server.js +39 -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 +87 -71
- package/lib/index.d.ts +9 -2
- package/package.json +1 -1
- package/skeleton/tsconfig.json +1 -0
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);
|
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
|
@@ -107,10 +107,47 @@ class Server {
|
|
|
107
107
|
|
|
108
108
|
this.#server.register(fastifyStatic, {
|
|
109
109
|
root: Paths.public,
|
|
110
|
-
decorateReply:
|
|
111
|
-
index: false
|
|
110
|
+
decorateReply: true,
|
|
111
|
+
index: false,
|
|
112
|
+
wildcard: false
|
|
112
113
|
});
|
|
113
114
|
|
|
115
|
+
// Register explicit routes for each public subdirectory.
|
|
116
|
+
// Routes like /storage/* have a static prefix and take priority over
|
|
117
|
+
// parametric routes (/:slug), preventing catch-all routes from
|
|
118
|
+
// intercepting static file requests.
|
|
119
|
+
if (fs.existsSync(Paths.public)) {
|
|
120
|
+
for (const entry of fs.readdirSync(Paths.public)) {
|
|
121
|
+
const fullPath = path.resolve(Paths.public, entry);
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
if (!fs.statSync(fullPath).isDirectory()) continue;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.#server.route({
|
|
131
|
+
method: ["GET", "HEAD"],
|
|
132
|
+
url: `/${entry}/*`,
|
|
133
|
+
handler(req, reply) {
|
|
134
|
+
return reply.sendFile(req.params["*"], fullPath);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Fallback wildcard for root-level files (favicon.ico, robots.txt, etc.)
|
|
140
|
+
// Less specific than /:slug so it loses when parametric routes exist,
|
|
141
|
+
// but keeps backward compatibility for projects without catch-all routes.
|
|
142
|
+
this.#server.route({
|
|
143
|
+
method: ["GET", "HEAD"],
|
|
144
|
+
url: "/*",
|
|
145
|
+
handler(req, reply) {
|
|
146
|
+
return reply.sendFile(req.params["*"]);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
114
151
|
this.#server.register(fastifyCookie, {
|
|
115
152
|
secret: process.env.APP_KEY,
|
|
116
153
|
hook: "onRequest"
|
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
|
@@ -291,94 +291,102 @@ class View {
|
|
|
291
291
|
throw new Error(`View not found: ${name}`);
|
|
292
292
|
}
|
|
293
293
|
|
|
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
294
|
const nonce = randomBytes(16).toString("hex");
|
|
313
295
|
const ctx = {
|
|
314
296
|
nonce,
|
|
315
297
|
csrf,
|
|
316
298
|
props: {},
|
|
317
299
|
request: fastifyRequest ? {
|
|
318
|
-
|
|
319
|
-
query: fastifyRequest.query || {},
|
|
320
|
-
url: fastifyRequest.url || '',
|
|
300
|
+
path: (fastifyRequest.url || '').split('?')[0],
|
|
321
301
|
method: fastifyRequest.method || 'GET',
|
|
322
|
-
|
|
302
|
+
query: fastifyRequest.query || {},
|
|
303
|
+
params: fastifyRequest.params || {},
|
|
304
|
+
headers: fastifyRequest.headers || {},
|
|
305
|
+
cookies: fastifyRequest.cookies || {},
|
|
306
|
+
ip: fastifyRequest.ip || '',
|
|
307
|
+
isAjax: fastifyRequest.headers?.['x-requested-with'] === 'XMLHttpRequest',
|
|
308
|
+
session: fastifyRequest.session || null,
|
|
309
|
+
auth: fastifyRequest.auth || null,
|
|
323
310
|
} : null
|
|
324
311
|
};
|
|
325
|
-
let html = "";
|
|
326
|
-
let collectedProps = {};
|
|
327
312
|
|
|
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);
|
|
313
|
+
return Context.run(ctx, async () => {
|
|
314
|
+
const mod = await import(pathToFileURL(viewPath).href + `?t=${Date.now()}`);
|
|
315
|
+
if (!mod.default) {
|
|
316
|
+
throw new Error("View must have a default export");
|
|
344
317
|
}
|
|
345
318
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
319
|
+
const layoutChain = entry?.layouts || [];
|
|
320
|
+
const layoutModules = [];
|
|
349
321
|
|
|
350
|
-
for (
|
|
351
|
-
const
|
|
352
|
-
|
|
322
|
+
for (const layoutName of layoutChain) {
|
|
323
|
+
const layoutPath = path.join(baseDir, layoutName + ".js");
|
|
324
|
+
if (existsSync(layoutPath)) {
|
|
325
|
+
const layoutMod = await import(pathToFileURL(layoutPath).href + `?t=${Date.now()}`);
|
|
326
|
+
if (layoutMod.default) {
|
|
327
|
+
layoutModules.push(layoutMod);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
353
330
|
}
|
|
354
331
|
|
|
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
|
-
});
|
|
332
|
+
let html = "";
|
|
333
|
+
let collectedProps = {};
|
|
369
334
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
335
|
+
try {
|
|
336
|
+
const Component = mod.default;
|
|
337
|
+
let element;
|
|
338
|
+
|
|
339
|
+
if (Component[MARK]) {
|
|
340
|
+
ctx.props[":R0:"] = this.#sanitizeProps(params);
|
|
341
|
+
element = React.createElement(
|
|
342
|
+
"div",
|
|
343
|
+
{
|
|
344
|
+
"data-cid": ":R0:",
|
|
345
|
+
"data-island": Component.displayName || Component.name || "Anonymous"
|
|
346
|
+
},
|
|
347
|
+
React.createElement(Component, params)
|
|
348
|
+
);
|
|
349
|
+
} else {
|
|
350
|
+
element = React.createElement(Component, params);
|
|
351
|
+
}
|
|
373
352
|
|
|
374
|
-
|
|
353
|
+
if (layoutModules.length > 0) {
|
|
354
|
+
element = React.createElement("div", { "data-nitron-slot": "page" }, element);
|
|
355
|
+
}
|
|
375
356
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
357
|
+
for (let i = layoutModules.length - 1; i >= 0; i--) {
|
|
358
|
+
const LayoutComponent = layoutModules[i].default;
|
|
359
|
+
element = React.createElement(LayoutComponent, { children: element });
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
html = await this.#renderToHtml(element);
|
|
363
|
+
collectedProps = ctx.props;
|
|
364
|
+
} catch (error) {
|
|
365
|
+
const componentName = mod.default?.displayName || mod.default?.name || "Unknown";
|
|
366
|
+
const errorDetails = this.#parseReactError(error);
|
|
367
|
+
|
|
368
|
+
Log.error("Render Error", {
|
|
369
|
+
view: name,
|
|
370
|
+
component: componentName,
|
|
371
|
+
error: error.message,
|
|
372
|
+
cause: errorDetails.cause,
|
|
373
|
+
location: errorDetails.location,
|
|
374
|
+
stack: error.stack
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
error.statusCode = error.statusCode || 500;
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const mergedMeta = this.#mergeMeta(layoutModules, mod.Meta, params);
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
html,
|
|
385
|
+
nonce,
|
|
386
|
+
meta: mergedMeta,
|
|
387
|
+
props: collectedProps
|
|
388
|
+
};
|
|
389
|
+
});
|
|
382
390
|
}
|
|
383
391
|
|
|
384
392
|
static #mergeMeta(layoutModules, viewMeta, params = {}) {
|
|
@@ -715,7 +723,7 @@ class View {
|
|
|
715
723
|
const devIndicator = this.#isDev ? this.#generateDevIndicator(nonceAttr) : "";
|
|
716
724
|
|
|
717
725
|
return `<!DOCTYPE html>
|
|
718
|
-
<html lang="en">
|
|
726
|
+
<html lang="${meta?.lang || "en"}">
|
|
719
727
|
<head>
|
|
720
728
|
${this.#generateHead(meta, css)}
|
|
721
729
|
</head>
|
|
@@ -738,6 +746,14 @@ ${vendorScript}${hmrScript}${hydrateScript}${spaScript}${devIndicator}
|
|
|
738
746
|
parts.push(`<meta name="description" content="${escapeHtml(meta.description)}">`);
|
|
739
747
|
}
|
|
740
748
|
|
|
749
|
+
if (meta.keywords) {
|
|
750
|
+
parts.push(`<meta name="keywords" content="${escapeHtml(meta.keywords)}">`);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (meta.favicon) {
|
|
754
|
+
parts.push(`<link rel="icon" href="${escapeHtml(meta.favicon)}">`);
|
|
755
|
+
}
|
|
756
|
+
|
|
741
757
|
for (const href of css) {
|
|
742
758
|
parts.push(`<link rel="stylesheet" href="${escapeHtml(href)}">`);
|
|
743
759
|
}
|
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.14",
|
|
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"
|