@nitronjs/framework 0.2.12 → 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/FileAnalyzer.js +1 -1
- package/lib/Build/Manager.js +62 -2
- package/lib/Build/jsxRuntime.js +1 -1
- package/lib/Build/plugins.js +3 -7
- package/lib/Console/Commands/MigrateCommand.js +2 -0
- package/lib/Console/Commands/MigrateFreshCommand.js +2 -0
- package/lib/Console/Commands/MigrateRollbackCommand.js +2 -0
- package/lib/Console/Commands/MigrateStatusCommand.js +2 -0
- package/lib/Console/Commands/SeedCommand.js +2 -0
- package/lib/Database/Drivers/MySQLDriver.js +5 -1
- package/lib/Database/Migration/MigrationRunner.js +2 -0
- package/lib/Database/Model.js +20 -0
- package/lib/Database/QueryBuilder.js +12 -74
- package/lib/Date/DateTime.js +18 -3
- package/lib/Filesystem/Storage.js +19 -0
- package/lib/Http/Server.js +41 -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/Client/spa.js +22 -0
- package/lib/View/View.js +111 -81
- package/lib/index.d.ts +57 -15
- package/package.json +1 -1
- package/skeleton/tsconfig.json +1 -0
package/lib/Auth/Auth.js
CHANGED
|
@@ -239,7 +239,7 @@ class FileAnalyzer {
|
|
|
239
239
|
if (declaration?.type === "VariableDeclaration") {
|
|
240
240
|
for (const decl of declaration.declarations) {
|
|
241
241
|
if (decl.id.type === "Identifier") {
|
|
242
|
-
if (decl.id.name === "
|
|
242
|
+
if (decl.id.name === "Layout" &&
|
|
243
243
|
decl.init?.type === "BooleanLiteral" &&
|
|
244
244
|
decl.init.value === false) {
|
|
245
245
|
meta.layoutDisabled = true;
|
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/Build/plugins.js
CHANGED
|
@@ -162,7 +162,8 @@ export function createServerModuleBlockerPlugin() {
|
|
|
162
162
|
|
|
163
163
|
/**
|
|
164
164
|
* Creates an esbuild plugin that transforms server functions for client use.
|
|
165
|
-
* Replaces csrf()
|
|
165
|
+
* Replaces csrf() calls with runtime lookups.
|
|
166
|
+
* Note: route() is now a global function defined in spa.js
|
|
166
167
|
* @returns {import("esbuild").Plugin}
|
|
167
168
|
*/
|
|
168
169
|
export function createServerFunctionsPlugin() {
|
|
@@ -176,7 +177,7 @@ export function createServerFunctionsPlugin() {
|
|
|
176
177
|
|
|
177
178
|
let source = await fs.promises.readFile(args.path, "utf8");
|
|
178
179
|
|
|
179
|
-
if (!source.includes("csrf(")
|
|
180
|
+
if (!source.includes("csrf(")) {
|
|
180
181
|
return null;
|
|
181
182
|
}
|
|
182
183
|
|
|
@@ -185,11 +186,6 @@ export function createServerFunctionsPlugin() {
|
|
|
185
186
|
"window.__NITRON_RUNTIME__.csrf"
|
|
186
187
|
);
|
|
187
188
|
|
|
188
|
-
source = source.replace(
|
|
189
|
-
/\broute\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
190
|
-
(_, routeName) => `window.__NITRON_RUNTIME__.routes["${routeName}"]`
|
|
191
|
-
);
|
|
192
|
-
|
|
193
189
|
const ext = args.path.split(".").pop();
|
|
194
190
|
const loader = ext === "tsx" ? "tsx" : ext === "ts" ? "ts" : ext === "jsx" ? "jsx" : "js";
|
|
195
191
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import dotenv from 'dotenv';
|
|
2
2
|
import DB from '../../Database/DB.js';
|
|
3
3
|
import Config from '../../Core/Config.js';
|
|
4
|
+
import Environment from '../../Core/Environment.js';
|
|
4
5
|
import MigrationRunner from '../../Database/Migration/MigrationRunner.js';
|
|
5
6
|
import seed from './SeedCommand.js';
|
|
6
7
|
|
|
@@ -8,6 +9,7 @@ export default async function migrate(options = {}) {
|
|
|
8
9
|
const { seed: shouldSeed = false } = options;
|
|
9
10
|
|
|
10
11
|
dotenv.config({ quiet: true });
|
|
12
|
+
Environment.setDev(true);
|
|
11
13
|
await Config.initialize();
|
|
12
14
|
|
|
13
15
|
try {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import dotenv from 'dotenv';
|
|
2
2
|
import DB from '../../Database/DB.js';
|
|
3
3
|
import Config from '../../Core/Config.js';
|
|
4
|
+
import Environment from '../../Core/Environment.js';
|
|
4
5
|
import MigrationRunner from '../../Database/Migration/MigrationRunner.js';
|
|
5
6
|
import Output from '../../Console/Output.js';
|
|
6
7
|
import seed from './SeedCommand.js';
|
|
@@ -9,6 +10,7 @@ export default async function migrateFresh(options = {}) {
|
|
|
9
10
|
const { seed: shouldSeed = false } = options;
|
|
10
11
|
|
|
11
12
|
dotenv.config({ quiet: true });
|
|
13
|
+
Environment.setDev(true);
|
|
12
14
|
await Config.initialize();
|
|
13
15
|
|
|
14
16
|
try {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import dotenv from 'dotenv';
|
|
2
2
|
import DB from '../../Database/DB.js';
|
|
3
3
|
import Config from '../../Core/Config.js';
|
|
4
|
+
import Environment from '../../Core/Environment.js';
|
|
4
5
|
import MigrationRunner from '../../Database/Migration/MigrationRunner.js';
|
|
5
6
|
import Output from '../../Console/Output.js';
|
|
6
7
|
|
|
@@ -8,6 +9,7 @@ export default async function rollback(options = {}) {
|
|
|
8
9
|
const { step = 1, all = false } = options;
|
|
9
10
|
|
|
10
11
|
dotenv.config({ quiet: true });
|
|
12
|
+
Environment.setDev(true);
|
|
11
13
|
await Config.initialize();
|
|
12
14
|
|
|
13
15
|
try {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import dotenv from 'dotenv';
|
|
2
2
|
import DB from '../../Database/DB.js';
|
|
3
3
|
import Config from '../../Core/Config.js';
|
|
4
|
+
import Environment from '../../Core/Environment.js';
|
|
4
5
|
import MigrationRunner from '../../Database/Migration/MigrationRunner.js';
|
|
5
6
|
import Output from '../../Console/Output.js';
|
|
6
7
|
|
|
7
8
|
export default async function status() {
|
|
8
9
|
dotenv.config({ quiet: true });
|
|
10
|
+
Environment.setDev(true);
|
|
9
11
|
await Config.initialize();
|
|
10
12
|
|
|
11
13
|
try {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import dotenv from 'dotenv';
|
|
2
2
|
import DB from '../../Database/DB.js';
|
|
3
3
|
import Config from '../../Core/Config.js';
|
|
4
|
+
import Environment from '../../Core/Environment.js';
|
|
4
5
|
import SeederRunner from '../../Database/Seeder/SeederRunner.js';
|
|
5
6
|
|
|
6
7
|
export default async function seed(seederName = null) {
|
|
7
8
|
dotenv.config({ quiet: true });
|
|
9
|
+
Environment.setDev(true);
|
|
8
10
|
await Config.initialize();
|
|
9
11
|
|
|
10
12
|
try {
|
|
@@ -154,13 +154,17 @@ class MySQLDriver {
|
|
|
154
154
|
#handleError(error, sql, bindings = null) {
|
|
155
155
|
if (Environment.isProd) {
|
|
156
156
|
console.error("[MySQLDriver] Query failed", {
|
|
157
|
+
message: error.message,
|
|
157
158
|
code: error.code,
|
|
158
159
|
errno: error.errno,
|
|
159
|
-
sqlState: error.sqlState
|
|
160
|
+
sqlState: error.sqlState,
|
|
161
|
+
sql: sql?.substring(0, 200)
|
|
160
162
|
});
|
|
161
163
|
|
|
162
164
|
const sanitized = new Error("Database query failed");
|
|
163
165
|
sanitized.code = error.code;
|
|
166
|
+
sanitized.errno = error.errno;
|
|
167
|
+
sanitized.sqlState = error.sqlState;
|
|
164
168
|
sanitized.driver = "mysql";
|
|
165
169
|
|
|
166
170
|
return sanitized;
|
package/lib/Database/Model.js
CHANGED
|
@@ -247,4 +247,24 @@ function hydrate(modelClass, row) {
|
|
|
247
247
|
return instance;
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
251
|
+
// QueryBuilder Method Forwarding
|
|
252
|
+
// Automatically forward all QueryBuilder methods to Model for fluent chaining
|
|
253
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
const QUERY_METHODS = [
|
|
256
|
+
// Chain methods (return QueryBuilder)
|
|
257
|
+
'distinct', 'orWhere', 'whereIn', 'whereNotIn',
|
|
258
|
+
'whereBetween', 'whereNot', 'join', 'groupBy', 'offset',
|
|
259
|
+
// Terminal methods (return results)
|
|
260
|
+
'count', 'max', 'min', 'sum', 'avg', 'delete'
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
for (const method of QUERY_METHODS) {
|
|
264
|
+
Model[method] = function(...args) {
|
|
265
|
+
ensureTable(this);
|
|
266
|
+
return DB.table(this.table, null, this)[method](...args);
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
250
270
|
export default Model;
|
|
@@ -135,8 +135,10 @@ class QueryBuilder {
|
|
|
135
135
|
message: error.message,
|
|
136
136
|
code: error.code,
|
|
137
137
|
errno: error.errno,
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
sqlState: error.sqlState,
|
|
139
|
+
sql: error.sql?.substring(0, 200),
|
|
140
|
+
bindings: error.bindings,
|
|
141
|
+
driver: error.driver
|
|
140
142
|
});
|
|
141
143
|
|
|
142
144
|
const sanitized = new Error('Database query failed. Please contact support if the problem persists.');
|
|
@@ -297,24 +299,14 @@ class QueryBuilder {
|
|
|
297
299
|
return this;
|
|
298
300
|
}
|
|
299
301
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
return this;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
whereNotNull(column) {
|
|
311
|
-
this.#wheres.push({
|
|
312
|
-
type: 'notNull',
|
|
313
|
-
column: this.#validateIdentifier(column),
|
|
314
|
-
boolean: 'AND'
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
return this;
|
|
302
|
+
/**
|
|
303
|
+
* Add a WHERE NOT (!=) clause.
|
|
304
|
+
* @param {string} column - Column name
|
|
305
|
+
* @param {*} value - Value to compare
|
|
306
|
+
* @returns {QueryBuilder}
|
|
307
|
+
*/
|
|
308
|
+
whereNot(column, value) {
|
|
309
|
+
return this.where(column, '!=', value);
|
|
318
310
|
}
|
|
319
311
|
|
|
320
312
|
whereBetween(column, values) {
|
|
@@ -334,22 +326,6 @@ class QueryBuilder {
|
|
|
334
326
|
return this;
|
|
335
327
|
}
|
|
336
328
|
|
|
337
|
-
whereRaw(sql, bindings = []) {
|
|
338
|
-
if (typeof sql !== 'string' || sql.length === 0) {
|
|
339
|
-
throw new Error('whereRaw requires non-empty SQL string');
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
this.#wheres.push({
|
|
343
|
-
type: 'raw',
|
|
344
|
-
sql,
|
|
345
|
-
boolean: 'AND'
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
this.#bindings.push(...bindings);
|
|
349
|
-
|
|
350
|
-
return this;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
329
|
join(table, first, operator, second, type = 'inner') {
|
|
354
330
|
if (arguments.length === 3) {
|
|
355
331
|
second = operator;
|
|
@@ -370,14 +346,6 @@ class QueryBuilder {
|
|
|
370
346
|
return this;
|
|
371
347
|
}
|
|
372
348
|
|
|
373
|
-
leftJoin(table, first, operator, second) {
|
|
374
|
-
return this.join(table, first, operator, second, 'left');
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
rightJoin(table, first, operator, second) {
|
|
378
|
-
return this.join(table, first, operator, second, 'right');
|
|
379
|
-
}
|
|
380
|
-
|
|
381
349
|
orderBy(column, direction = 'ASC') {
|
|
382
350
|
this.#orders.push({
|
|
383
351
|
column: this.#validateIdentifier(column),
|
|
@@ -394,25 +362,6 @@ class QueryBuilder {
|
|
|
394
362
|
return this;
|
|
395
363
|
}
|
|
396
364
|
|
|
397
|
-
having(column, operator, value) {
|
|
398
|
-
if (arguments.length === 2) {
|
|
399
|
-
value = operator;
|
|
400
|
-
operator = '=';
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
operator = this.#validateWhereOperator(operator);
|
|
404
|
-
|
|
405
|
-
this.#havings.push({
|
|
406
|
-
column: this.#validateIdentifier(column),
|
|
407
|
-
operator,
|
|
408
|
-
value
|
|
409
|
-
});
|
|
410
|
-
this.#bindings.push(value);
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
return this;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
365
|
limit(value) {
|
|
417
366
|
this.#limitValue = this.#validateInteger(value);
|
|
418
367
|
|
|
@@ -425,10 +374,6 @@ class QueryBuilder {
|
|
|
425
374
|
return this;
|
|
426
375
|
}
|
|
427
376
|
|
|
428
|
-
forPage(page, perPage = 15) {
|
|
429
|
-
return this.offset((page - 1) * perPage).limit(perPage);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
377
|
async get() {
|
|
433
378
|
const connection = await this.#getConnection();
|
|
434
379
|
|
|
@@ -740,13 +685,6 @@ class QueryBuilder {
|
|
|
740
685
|
return sql;
|
|
741
686
|
}
|
|
742
687
|
|
|
743
|
-
toSql() {
|
|
744
|
-
return this.#toSql();
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
getBindings() {
|
|
748
|
-
return [...this.#bindings];
|
|
749
|
-
}
|
|
750
688
|
}
|
|
751
689
|
|
|
752
690
|
export class RawExpression {
|
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
|
/**
|
|
@@ -65,6 +65,25 @@ class Storage {
|
|
|
65
65
|
await fs.promises.unlink(fullPath);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Moves or renames a file in storage.
|
|
70
|
+
* @param {string} fromPath - Current relative path to the file
|
|
71
|
+
* @param {string} toPath - New relative path for the file
|
|
72
|
+
* @param {boolean} isPrivate - Whether to operate on private storage
|
|
73
|
+
* @returns {Promise<void>}
|
|
74
|
+
*/
|
|
75
|
+
static async move(fromPath, toPath, isPrivate = false) {
|
|
76
|
+
const base = isPrivate ? this.#privateRoot : this.#publicRoot;
|
|
77
|
+
const fullFromPath = this.#validatePath(base, fromPath);
|
|
78
|
+
const fullToPath = this.#validatePath(base, toPath);
|
|
79
|
+
|
|
80
|
+
// Ensure target directory exists
|
|
81
|
+
const targetDir = path.dirname(fullToPath);
|
|
82
|
+
await fs.promises.mkdir(targetDir, { recursive: true });
|
|
83
|
+
|
|
84
|
+
await fs.promises.rename(fullFromPath, fullToPath);
|
|
85
|
+
}
|
|
86
|
+
|
|
68
87
|
/**
|
|
69
88
|
* Checks if a file exists in storage.
|
|
70
89
|
* @param {string} filePath - Relative path to the file
|
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"
|
|
@@ -171,6 +208,8 @@ class Server {
|
|
|
171
208
|
for (const [key, field] of Object.entries(req.body)) {
|
|
172
209
|
if (field?.toBuffer) {
|
|
173
210
|
if (field.filename) {
|
|
211
|
+
const lastDot = field.filename.lastIndexOf(".");
|
|
212
|
+
field.extension = lastDot > 0 ? field.filename.slice(lastDot + 1).toLowerCase() : "";
|
|
174
213
|
files[key] = field;
|
|
175
214
|
}
|
|
176
215
|
}
|
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/Client/spa.js
CHANGED
|
@@ -6,6 +6,28 @@
|
|
|
6
6
|
var layouts = [];
|
|
7
7
|
var navigating = false;
|
|
8
8
|
|
|
9
|
+
// Route helper function for client-side
|
|
10
|
+
globalThis.route = function(name, params) {
|
|
11
|
+
var runtime = window.__NITRON_RUNTIME__;
|
|
12
|
+
if (!runtime || !runtime.routes) {
|
|
13
|
+
console.error("Route runtime not initialized");
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
var pattern = runtime.routes[name];
|
|
18
|
+
if (!pattern) {
|
|
19
|
+
console.error('Route "' + name + '" not found');
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!params) return pattern;
|
|
24
|
+
|
|
25
|
+
// Replace :param tokens with actual values
|
|
26
|
+
return pattern.replace(/:(\w+)/g, function(match, key) {
|
|
27
|
+
return params[key] !== undefined ? params[key] : match;
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
|
|
9
31
|
function init() {
|
|
10
32
|
var rt = window.__NITRON_RUNTIME__;
|
|
11
33
|
if (rt && rt.layouts) layouts = rt.layouts;
|
package/lib/View/View.js
CHANGED
|
@@ -291,120 +291,134 @@ 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
|
-
static #mergeMeta(layoutModules, viewMeta) {
|
|
392
|
+
static #mergeMeta(layoutModules, viewMeta, params = {}) {
|
|
385
393
|
const result = {};
|
|
386
394
|
|
|
387
395
|
for (const layoutMod of layoutModules) {
|
|
388
396
|
if (layoutMod.Meta) {
|
|
389
|
-
|
|
397
|
+
const resolvedLayoutMeta = typeof layoutMod.Meta === "function"
|
|
398
|
+
? layoutMod.Meta(params)
|
|
399
|
+
: layoutMod.Meta;
|
|
400
|
+
for (const key of Object.keys(resolvedLayoutMeta)) {
|
|
390
401
|
if (!(key in result)) {
|
|
391
|
-
result[key] =
|
|
402
|
+
result[key] = resolvedLayoutMeta[key];
|
|
392
403
|
}
|
|
393
404
|
}
|
|
394
405
|
}
|
|
395
406
|
}
|
|
396
407
|
|
|
397
408
|
if (viewMeta) {
|
|
398
|
-
|
|
409
|
+
const resolvedViewMeta = typeof viewMeta === "function"
|
|
410
|
+
? viewMeta(params)
|
|
411
|
+
: viewMeta;
|
|
412
|
+
for (const key of Object.keys(resolvedViewMeta)) {
|
|
399
413
|
if (key === "title" && result.title?.template) {
|
|
400
|
-
const viewTitle = typeof
|
|
401
|
-
?
|
|
402
|
-
:
|
|
414
|
+
const viewTitle = typeof resolvedViewMeta.title === "object"
|
|
415
|
+
? resolvedViewMeta.title.default
|
|
416
|
+
: resolvedViewMeta.title;
|
|
403
417
|
if (viewTitle) {
|
|
404
418
|
result.title = result.title.template.replace("%s", viewTitle);
|
|
405
419
|
}
|
|
406
420
|
} else {
|
|
407
|
-
result[key] =
|
|
421
|
+
result[key] = resolvedViewMeta[key];
|
|
408
422
|
}
|
|
409
423
|
}
|
|
410
424
|
}
|
|
@@ -648,8 +662,16 @@ class View {
|
|
|
648
662
|
}
|
|
649
663
|
|
|
650
664
|
const content = readFileSync(fullPath, "utf8");
|
|
651
|
-
|
|
652
|
-
|
|
665
|
+
|
|
666
|
+
// Match both old format (routes["name"]) and new format (route("name"))
|
|
667
|
+
const oldMatches = [...content.matchAll(/routes\["([^"]+)"\]/g)];
|
|
668
|
+
const newMatches = [...content.matchAll(/route\s*\(\s*['"]([^'"]+)['"]/g)];
|
|
669
|
+
|
|
670
|
+
const routes = [
|
|
671
|
+
...oldMatches.map(match => match[1]),
|
|
672
|
+
...newMatches.map(match => match[1])
|
|
673
|
+
];
|
|
674
|
+
|
|
653
675
|
const relativePath = "/js/" + path.relative(jsDir, fullPath).replace(/\\/g, "/");
|
|
654
676
|
|
|
655
677
|
this.#routesCache.set(relativePath, [...new Set(routes)]);
|
|
@@ -701,7 +723,7 @@ class View {
|
|
|
701
723
|
const devIndicator = this.#isDev ? this.#generateDevIndicator(nonceAttr) : "";
|
|
702
724
|
|
|
703
725
|
return `<!DOCTYPE html>
|
|
704
|
-
<html lang="en">
|
|
726
|
+
<html lang="${meta?.lang || "en"}">
|
|
705
727
|
<head>
|
|
706
728
|
${this.#generateHead(meta, css)}
|
|
707
729
|
</head>
|
|
@@ -724,6 +746,14 @@ ${vendorScript}${hmrScript}${hydrateScript}${spaScript}${devIndicator}
|
|
|
724
746
|
parts.push(`<meta name="description" content="${escapeHtml(meta.description)}">`);
|
|
725
747
|
}
|
|
726
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
|
+
|
|
727
757
|
for (const href of css) {
|
|
728
758
|
parts.push(`<link rel="stylesheet" href="${escapeHtml(href)}">`);
|
|
729
759
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,15 +1,51 @@
|
|
|
1
1
|
export class Storage {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
static
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Reads a file from storage.
|
|
4
|
+
* @param filePath - Relative path to the file
|
|
5
|
+
* @param isPrivate - Whether to read from private storage (default: false)
|
|
6
|
+
* @returns File contents or null if not found
|
|
7
|
+
*/
|
|
8
|
+
static get(filePath: string, isPrivate?: boolean): Promise<Buffer | null>;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Saves a file to storage.
|
|
12
|
+
* @param file - File object from multipart upload
|
|
13
|
+
* @param dir - Directory path within storage
|
|
14
|
+
* @param fileName - Name for the saved file
|
|
15
|
+
* @param isPrivate - Whether to save to private storage (default: false)
|
|
16
|
+
* @returns True if save successful
|
|
17
|
+
*/
|
|
18
|
+
static put(file: { _buf: Buffer }, dir: string, fileName: string, isPrivate?: boolean): Promise<boolean>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Deletes a file from storage.
|
|
22
|
+
* @param filePath - Relative path to the file
|
|
23
|
+
* @param isPrivate - Whether to delete from private storage (default: false)
|
|
24
|
+
*/
|
|
25
|
+
static delete(filePath: string, isPrivate?: boolean): Promise<void>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Moves or renames a file in storage.
|
|
29
|
+
* @param fromPath - Current relative path to the file
|
|
30
|
+
* @param toPath - New relative path for the file
|
|
31
|
+
* @param isPrivate - Whether to operate on private storage (default: false)
|
|
32
|
+
*/
|
|
33
|
+
static move(fromPath: string, toPath: string, isPrivate?: boolean): Promise<void>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Checks if a file exists in storage.
|
|
37
|
+
* @param filePath - Relative path to the file
|
|
38
|
+
* @param isPrivate - Whether to check private storage (default: false)
|
|
39
|
+
* @returns True if file exists
|
|
40
|
+
*/
|
|
41
|
+
static exists(filePath: string, isPrivate?: boolean): boolean;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Gets the public URL for a file in public storage.
|
|
45
|
+
* @param filePath - Relative path to the file
|
|
46
|
+
* @returns Public URL path
|
|
47
|
+
*/
|
|
48
|
+
static url(filePath: string): string;
|
|
13
49
|
}
|
|
14
50
|
|
|
15
51
|
export class Model {
|
|
@@ -20,9 +56,8 @@ export class Model {
|
|
|
20
56
|
static hidden: string[];
|
|
21
57
|
static casts: Record<string, string>;
|
|
22
58
|
|
|
23
|
-
static
|
|
59
|
+
static get<T extends Model>(this: new () => T): Promise<T[]>;
|
|
24
60
|
static find<T extends Model>(this: new () => T, id: number | string): Promise<T | null>;
|
|
25
|
-
static findOrFail<T extends Model>(this: new () => T, id: number | string): Promise<T>;
|
|
26
61
|
static first<T extends Model>(this: new () => T): Promise<T | null>;
|
|
27
62
|
static where<T extends Model>(this: new () => T, column: string, operator?: any, value?: any): QueryBuilder<T>;
|
|
28
63
|
static select<T extends Model>(this: new () => T, ...columns: string[]): QueryBuilder<T>;
|
|
@@ -189,7 +224,7 @@ export class Lang {
|
|
|
189
224
|
export class DateTime {
|
|
190
225
|
static toSQL(timestamp?: number | null): string;
|
|
191
226
|
static getTime(sqlDateTime?: string | null): number;
|
|
192
|
-
static getDate(timestamp?: number | null, format?: string): string;
|
|
227
|
+
static getDate(timestamp?: string | number | null, format?: string): string;
|
|
193
228
|
static addDays(days: number): string;
|
|
194
229
|
static addHours(hours: number): string;
|
|
195
230
|
static subDays(days: number): string;
|
|
@@ -468,7 +503,6 @@ declare global {
|
|
|
468
503
|
method: string;
|
|
469
504
|
query: Record<string, any>;
|
|
470
505
|
params: Record<string, any>;
|
|
471
|
-
body: Record<string, any>;
|
|
472
506
|
headers: Record<string, string>;
|
|
473
507
|
cookies: Record<string, string>;
|
|
474
508
|
ip: string;
|
|
@@ -480,5 +514,13 @@ declare global {
|
|
|
480
514
|
forget(key: string): void;
|
|
481
515
|
flash(key: string, value: any): void;
|
|
482
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
|
+
};
|
|
483
525
|
};
|
|
484
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"
|