@nitronjs/framework 0.2.11 → 0.2.13
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/Build/FileAnalyzer.js +1 -1
- package/lib/Build/jsxRuntime.js +39 -5
- 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/Console/Stubs/vendor-dev.tsx +29 -4
- package/lib/Console/Stubs/vendor.tsx +15 -4
- 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/Filesystem/Storage.js +19 -0
- package/lib/Http/Server.js +2 -0
- package/lib/View/Client/spa.js +22 -0
- package/lib/View/View.js +25 -11
- package/lib/index.d.ts +48 -13
- package/package.json +1 -1
|
@@ -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/jsxRuntime.js
CHANGED
|
@@ -1,10 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom JSX Runtime for NitronJS
|
|
3
|
+
*
|
|
4
|
+
* Handles React 19's strict key prop requirements and provides
|
|
5
|
+
* island architecture support for client components.
|
|
6
|
+
*/
|
|
1
7
|
const JSX_RUNTIME = `
|
|
2
8
|
import * as React from 'react';
|
|
3
9
|
import * as OriginalJsx from '__react_jsx_original__';
|
|
4
10
|
|
|
5
11
|
const CTX = Symbol.for('__nitron_view_context__');
|
|
6
12
|
const MARK = Symbol.for('__nitron_client_component__');
|
|
7
|
-
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
13
|
+
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype', 'key', 'ref']);
|
|
14
|
+
|
|
15
|
+
// Filter React 19 key warnings from third-party libraries
|
|
16
|
+
if (typeof console !== 'undefined' && console.error) {
|
|
17
|
+
const originalError = console.error;
|
|
18
|
+
console.error = function(...args) {
|
|
19
|
+
const msg = args[0];
|
|
20
|
+
if (typeof msg === 'string' && (
|
|
21
|
+
msg.includes('A props object containing a "key" prop is being spread into JSX') ||
|
|
22
|
+
msg.includes('\`key\` is not a prop')
|
|
23
|
+
)) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
return originalError.apply(console, args);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
8
29
|
|
|
9
30
|
function getContext() {
|
|
10
31
|
return globalThis[CTX]?.getStore?.();
|
|
@@ -20,6 +41,7 @@ globalThis.request = () => {
|
|
|
20
41
|
const DepthContext = React.createContext(false);
|
|
21
42
|
const componentCache = new WeakMap();
|
|
22
43
|
|
|
44
|
+
// Sanitizes props for client-side hydration (removes functions, symbols, circular refs)
|
|
23
45
|
function sanitizeProps(obj, seen = new WeakSet()) {
|
|
24
46
|
if (obj == null) return obj;
|
|
25
47
|
|
|
@@ -62,6 +84,7 @@ function wrapWithDepth(children) {
|
|
|
62
84
|
return OriginalJsx.jsx(DepthContext.Provider, { value: true, children });
|
|
63
85
|
}
|
|
64
86
|
|
|
87
|
+
// Creates an island wrapper for client components (hydrated independently)
|
|
65
88
|
function createIsland(Component, name) {
|
|
66
89
|
function IslandBoundary(props) {
|
|
67
90
|
if (React.useContext(DepthContext)) {
|
|
@@ -96,12 +119,23 @@ function getWrappedComponent(Component) {
|
|
|
96
119
|
return componentCache.get(Component);
|
|
97
120
|
}
|
|
98
121
|
|
|
122
|
+
// Extracts key from props without triggering React 19's getter warning
|
|
99
123
|
function extractKey(props, key) {
|
|
100
|
-
if (props == null
|
|
101
|
-
|
|
124
|
+
if (props == null) return [props, key];
|
|
125
|
+
if (!Object.prototype.hasOwnProperty.call(props, 'key')) return [props, key];
|
|
126
|
+
|
|
127
|
+
const cleanProps = {};
|
|
128
|
+
let extractedKey = key;
|
|
129
|
+
|
|
130
|
+
for (const k of Object.keys(props)) {
|
|
131
|
+
if (k === 'key') {
|
|
132
|
+
if (extractedKey == null) extractedKey = props[k];
|
|
133
|
+
} else {
|
|
134
|
+
cleanProps[k] = props[k];
|
|
135
|
+
}
|
|
102
136
|
}
|
|
103
|
-
|
|
104
|
-
return [
|
|
137
|
+
|
|
138
|
+
return [cleanProps, extractedKey];
|
|
105
139
|
}
|
|
106
140
|
|
|
107
141
|
export function jsx(type, props, key) {
|
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 {
|
|
@@ -4,12 +4,37 @@ import * as ReactDOMClient from 'react-dom/client';
|
|
|
4
4
|
import * as OriginalJsx from 'react/jsx-runtime';
|
|
5
5
|
import RefreshRuntime from 'react-refresh/runtime';
|
|
6
6
|
|
|
7
|
+
// Filter React 19 key warnings from third-party libraries
|
|
8
|
+
const originalError = console.error;
|
|
9
|
+
console.error = function(...args: any[]) {
|
|
10
|
+
const msg = args[0];
|
|
11
|
+
if (typeof msg === 'string' && (
|
|
12
|
+
msg.includes('A props object containing a "key" prop is being spread into JSX') ||
|
|
13
|
+
msg.includes('`key` is not a prop')
|
|
14
|
+
)) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
return originalError.apply(console, args);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Extracts key from props without triggering React 19's getter warning
|
|
7
21
|
function extractKey(props: any, key: any): [any, any] {
|
|
8
|
-
if (props == null
|
|
9
|
-
|
|
22
|
+
if (props == null) return [props, key];
|
|
23
|
+
if (!Object.prototype.hasOwnProperty.call(props, 'key')) return [props, key];
|
|
24
|
+
|
|
25
|
+
const cleanProps: any = {};
|
|
26
|
+
let extractedKey = key;
|
|
27
|
+
|
|
28
|
+
for (const k of Object.keys(props)) {
|
|
29
|
+
if (k === 'key') {
|
|
30
|
+
if (extractedKey == null) extractedKey = props[k];
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
cleanProps[k] = props[k];
|
|
34
|
+
}
|
|
10
35
|
}
|
|
11
|
-
|
|
12
|
-
return [
|
|
36
|
+
|
|
37
|
+
return [cleanProps, extractedKey];
|
|
13
38
|
}
|
|
14
39
|
|
|
15
40
|
function jsx(type: any, props: any, key?: any) {
|
|
@@ -3,12 +3,23 @@ import * as ReactDOM from 'react-dom';
|
|
|
3
3
|
import * as ReactDOMClient from 'react-dom/client';
|
|
4
4
|
import * as OriginalJsx from 'react/jsx-runtime';
|
|
5
5
|
|
|
6
|
+
// Extracts key from props without triggering React 19's getter warning
|
|
6
7
|
function extractKey(props: any, key: any): [any, any] {
|
|
7
|
-
if (props == null
|
|
8
|
-
|
|
8
|
+
if (props == null) return [props, key];
|
|
9
|
+
if (!Object.prototype.hasOwnProperty.call(props, 'key')) return [props, key];
|
|
10
|
+
|
|
11
|
+
const cleanProps: any = {};
|
|
12
|
+
let extractedKey = key;
|
|
13
|
+
|
|
14
|
+
for (const k of Object.keys(props)) {
|
|
15
|
+
if (k === 'key') {
|
|
16
|
+
if (extractedKey == null) extractedKey = props[k];
|
|
17
|
+
} else {
|
|
18
|
+
cleanProps[k] = props[k];
|
|
19
|
+
}
|
|
9
20
|
}
|
|
10
|
-
|
|
11
|
-
return [
|
|
21
|
+
|
|
22
|
+
return [cleanProps, extractedKey];
|
|
12
23
|
}
|
|
13
24
|
|
|
14
25
|
function jsx(type: any, props: any, key?: any) {
|
|
@@ -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 {
|
|
@@ -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
|
@@ -171,6 +171,8 @@ class Server {
|
|
|
171
171
|
for (const [key, field] of Object.entries(req.body)) {
|
|
172
172
|
if (field?.toBuffer) {
|
|
173
173
|
if (field.filename) {
|
|
174
|
+
const lastDot = field.filename.lastIndexOf(".");
|
|
175
|
+
field.extension = lastDot > 0 ? field.filename.slice(lastDot + 1).toLowerCase() : "";
|
|
174
176
|
files[key] = field;
|
|
175
177
|
}
|
|
176
178
|
}
|
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
|
@@ -371,7 +371,7 @@ class View {
|
|
|
371
371
|
throw error;
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
-
const mergedMeta = this.#mergeMeta(layoutModules, mod.Meta);
|
|
374
|
+
const mergedMeta = this.#mergeMeta(layoutModules, mod.Meta, params);
|
|
375
375
|
|
|
376
376
|
return {
|
|
377
377
|
html,
|
|
@@ -381,30 +381,36 @@ class View {
|
|
|
381
381
|
};
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
-
static #mergeMeta(layoutModules, viewMeta) {
|
|
384
|
+
static #mergeMeta(layoutModules, viewMeta, params = {}) {
|
|
385
385
|
const result = {};
|
|
386
386
|
|
|
387
387
|
for (const layoutMod of layoutModules) {
|
|
388
388
|
if (layoutMod.Meta) {
|
|
389
|
-
|
|
389
|
+
const resolvedLayoutMeta = typeof layoutMod.Meta === "function"
|
|
390
|
+
? layoutMod.Meta(params)
|
|
391
|
+
: layoutMod.Meta;
|
|
392
|
+
for (const key of Object.keys(resolvedLayoutMeta)) {
|
|
390
393
|
if (!(key in result)) {
|
|
391
|
-
result[key] =
|
|
394
|
+
result[key] = resolvedLayoutMeta[key];
|
|
392
395
|
}
|
|
393
396
|
}
|
|
394
397
|
}
|
|
395
398
|
}
|
|
396
399
|
|
|
397
400
|
if (viewMeta) {
|
|
398
|
-
|
|
401
|
+
const resolvedViewMeta = typeof viewMeta === "function"
|
|
402
|
+
? viewMeta(params)
|
|
403
|
+
: viewMeta;
|
|
404
|
+
for (const key of Object.keys(resolvedViewMeta)) {
|
|
399
405
|
if (key === "title" && result.title?.template) {
|
|
400
|
-
const viewTitle = typeof
|
|
401
|
-
?
|
|
402
|
-
:
|
|
406
|
+
const viewTitle = typeof resolvedViewMeta.title === "object"
|
|
407
|
+
? resolvedViewMeta.title.default
|
|
408
|
+
: resolvedViewMeta.title;
|
|
403
409
|
if (viewTitle) {
|
|
404
410
|
result.title = result.title.template.replace("%s", viewTitle);
|
|
405
411
|
}
|
|
406
412
|
} else {
|
|
407
|
-
result[key] =
|
|
413
|
+
result[key] = resolvedViewMeta[key];
|
|
408
414
|
}
|
|
409
415
|
}
|
|
410
416
|
}
|
|
@@ -648,8 +654,16 @@ class View {
|
|
|
648
654
|
}
|
|
649
655
|
|
|
650
656
|
const content = readFileSync(fullPath, "utf8");
|
|
651
|
-
|
|
652
|
-
|
|
657
|
+
|
|
658
|
+
// Match both old format (routes["name"]) and new format (route("name"))
|
|
659
|
+
const oldMatches = [...content.matchAll(/routes\["([^"]+)"\]/g)];
|
|
660
|
+
const newMatches = [...content.matchAll(/route\s*\(\s*['"]([^'"]+)['"]/g)];
|
|
661
|
+
|
|
662
|
+
const routes = [
|
|
663
|
+
...oldMatches.map(match => match[1]),
|
|
664
|
+
...newMatches.map(match => match[1])
|
|
665
|
+
];
|
|
666
|
+
|
|
653
667
|
const relativePath = "/js/" + path.relative(jsDir, fullPath).replace(/\\/g, "/");
|
|
654
668
|
|
|
655
669
|
this.#routesCache.set(relativePath, [...new Set(routes)]);
|
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>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitronjs/framework",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13",
|
|
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"
|