@nitronjs/framework 0.2.19 → 0.2.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/njs.js +19 -14
- package/lib/Build/jsxRuntime.js +44 -8
- package/lib/Build/plugins.js +103 -1
- package/lib/Console/UpdateChecker.js +118 -0
- package/lib/Database/Model.js +10 -0
- package/lib/Route/Router.js +4 -4
- package/lib/View/View.js +48 -11
- package/package.json +1 -1
package/cli/njs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const COLORS = {
|
|
4
4
|
reset: "\x1b[0m",
|
|
@@ -87,14 +87,18 @@ async function run() {
|
|
|
87
87
|
return;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
let exitCode = null;
|
|
91
|
+
|
|
90
92
|
switch (command) {
|
|
91
93
|
case "dev": {
|
|
94
|
+
import("../lib/Console/UpdateChecker.js").then(m => m.default());
|
|
92
95
|
const { default: Dev } = await import("../lib/Console/Commands/DevCommand.js");
|
|
93
96
|
await Dev();
|
|
94
97
|
break;
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
case "start": {
|
|
101
|
+
import("../lib/Console/UpdateChecker.js").then(m => m.default());
|
|
98
102
|
const { default: Start } = await import("../lib/Console/Commands/StartCommand.js");
|
|
99
103
|
await Start();
|
|
100
104
|
break;
|
|
@@ -103,13 +107,13 @@ async function run() {
|
|
|
103
107
|
case "build": {
|
|
104
108
|
const { default: Build } = await import("../lib/Console/Commands/BuildCommand.js");
|
|
105
109
|
await Build();
|
|
110
|
+
exitCode = 0;
|
|
106
111
|
break;
|
|
107
112
|
}
|
|
108
113
|
|
|
109
114
|
case "migrate": {
|
|
110
115
|
const { default: Migrate } = await import("../lib/Console/Commands/MigrateCommand.js");
|
|
111
|
-
|
|
112
|
-
process.exit(success ? 0 : 1);
|
|
116
|
+
exitCode = (await Migrate({ seed: additionalArgs.includes("--seed") })) ? 0 : 1;
|
|
113
117
|
break;
|
|
114
118
|
}
|
|
115
119
|
|
|
@@ -118,37 +122,33 @@ async function run() {
|
|
|
118
122
|
const stepArg = additionalArgs.find(arg => arg.startsWith('--step='));
|
|
119
123
|
const step = stepArg ? parseInt(stepArg.split('=')[1], 10) : 1;
|
|
120
124
|
const all = additionalArgs.includes('--all');
|
|
121
|
-
|
|
122
|
-
process.exit(success ? 0 : 1);
|
|
125
|
+
exitCode = (await Rollback({ step, all })) ? 0 : 1;
|
|
123
126
|
break;
|
|
124
127
|
}
|
|
125
128
|
|
|
126
129
|
case "migrate:status": {
|
|
127
130
|
const { default: Status } = await import("../lib/Console/Commands/MigrateStatusCommand.js");
|
|
128
131
|
await Status();
|
|
129
|
-
|
|
132
|
+
exitCode = 0;
|
|
130
133
|
break;
|
|
131
134
|
}
|
|
132
135
|
|
|
133
136
|
case "migrate:fresh": {
|
|
134
137
|
const { default: MigrateFresh } = await import("../lib/Console/Commands/MigrateFreshCommand.js");
|
|
135
|
-
|
|
136
|
-
process.exit(success ? 0 : 1);
|
|
138
|
+
exitCode = (await MigrateFresh({ seed: additionalArgs.includes("--seed") })) ? 0 : 1;
|
|
137
139
|
break;
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
case "seed": {
|
|
141
143
|
const { default: Seed } = await import("../lib/Console/Commands/SeedCommand.js");
|
|
142
144
|
const seederName = additionalArgs.find(a => !a.startsWith('--')) || null;
|
|
143
|
-
|
|
144
|
-
process.exit(success ? 0 : 1);
|
|
145
|
+
exitCode = (await Seed(seederName)) ? 0 : 1;
|
|
145
146
|
break;
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
case "storage:link": {
|
|
149
150
|
const { default: StorageLink } = await import("../lib/Console/Commands/StorageLinkCommand.js");
|
|
150
|
-
|
|
151
|
-
process.exit(success ? 0 : 1);
|
|
151
|
+
exitCode = (await StorageLink()) ? 0 : 1;
|
|
152
152
|
break;
|
|
153
153
|
}
|
|
154
154
|
|
|
@@ -168,8 +168,7 @@ async function run() {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
const { default: Make } = await import("../lib/Console/Commands/MakeCommand.js");
|
|
171
|
-
|
|
172
|
-
process.exit(success ? 0 : 1);
|
|
171
|
+
exitCode = (await Make(type, name)) ? 0 : 1;
|
|
173
172
|
break;
|
|
174
173
|
}
|
|
175
174
|
|
|
@@ -178,6 +177,12 @@ async function run() {
|
|
|
178
177
|
console.log(`${COLORS.dim}Run 'njs --help' for available commands${COLORS.reset}`);
|
|
179
178
|
process.exit(1);
|
|
180
179
|
}
|
|
180
|
+
|
|
181
|
+
if (exitCode !== null) {
|
|
182
|
+
const { default: checkForUpdates } = await import("../lib/Console/UpdateChecker.js");
|
|
183
|
+
await checkForUpdates();
|
|
184
|
+
process.exit(exitCode);
|
|
185
|
+
}
|
|
181
186
|
} catch (error) {
|
|
182
187
|
console.error(`${COLORS.red}Error: ${error.message}${COLORS.reset}`);
|
|
183
188
|
if (process.env.DEBUG) {
|
package/lib/Build/jsxRuntime.js
CHANGED
|
@@ -85,34 +85,62 @@ function wrapWithDepth(children) {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
// Deep Proxy that tracks which prop paths are accessed during SSR
|
|
88
|
+
// Pre-wraps all nested objects so React's Object.freeze won't break Proxy invariants
|
|
88
89
|
function trackProps(obj) {
|
|
89
90
|
const accessed = new Set();
|
|
91
|
+
const cache = new WeakMap();
|
|
90
92
|
|
|
91
|
-
function wrap(
|
|
92
|
-
if (
|
|
93
|
+
function wrap(source, prefix) {
|
|
94
|
+
if (source === null || typeof source !== 'object') return source;
|
|
95
|
+
if (cache.has(source)) return cache.get(source);
|
|
93
96
|
|
|
94
|
-
|
|
97
|
+
const isArr = Array.isArray(source);
|
|
98
|
+
const copy = isArr ? [] : {};
|
|
99
|
+
|
|
100
|
+
for (const key of Object.keys(source)) {
|
|
101
|
+
const val = source[key];
|
|
102
|
+
|
|
103
|
+
if (val !== null && typeof val === 'object' && typeof val !== 'function') {
|
|
104
|
+
copy[key] = wrap(val, prefix ? prefix + '.' + key : key);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
copy[key] = val;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const proxy = new Proxy(copy, {
|
|
95
112
|
get(t, prop, receiver) {
|
|
96
113
|
if (typeof prop === 'symbol') return Reflect.get(t, prop, receiver);
|
|
97
114
|
|
|
98
115
|
const val = Reflect.get(t, prop, receiver);
|
|
99
116
|
if (typeof val === 'function') return val;
|
|
100
117
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (val !== null && typeof val === 'object') {
|
|
105
|
-
return wrap(val, currentPath);
|
|
118
|
+
if (val !== undefined) {
|
|
119
|
+
const currentPath = prefix ? prefix + '.' + String(prop) : String(prop);
|
|
120
|
+
accessed.add(currentPath);
|
|
106
121
|
}
|
|
107
122
|
|
|
108
123
|
return val;
|
|
109
124
|
}
|
|
110
125
|
});
|
|
126
|
+
|
|
127
|
+
cache.set(source, proxy);
|
|
128
|
+
return proxy;
|
|
111
129
|
}
|
|
112
130
|
|
|
113
131
|
return { proxy: wrap(obj, ''), accessed };
|
|
114
132
|
}
|
|
115
133
|
|
|
134
|
+
function resolveExists(obj, path) {
|
|
135
|
+
const parts = path.split('.');
|
|
136
|
+
let current = obj;
|
|
137
|
+
for (let i = 0; i < parts.length; i++) {
|
|
138
|
+
if (current == null || typeof current !== 'object') return false;
|
|
139
|
+
current = current[parts[i]];
|
|
140
|
+
}
|
|
141
|
+
return current !== undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
116
144
|
// Creates an island wrapper for client components (hydrated independently)
|
|
117
145
|
function createIsland(Component, name) {
|
|
118
146
|
function IslandBoundary(props) {
|
|
@@ -123,6 +151,14 @@ function createIsland(Component, name) {
|
|
|
123
151
|
const id = React.useId();
|
|
124
152
|
const tracker = trackProps(props);
|
|
125
153
|
|
|
154
|
+
if (Component.__propHints) {
|
|
155
|
+
for (const hint of Component.__propHints) {
|
|
156
|
+
if (resolveExists(props, hint)) {
|
|
157
|
+
tracker.accessed.add(hint);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
126
162
|
const ctx = getContext();
|
|
127
163
|
if (ctx) {
|
|
128
164
|
ctx.props = ctx.props || {};
|
package/lib/Build/plugins.js
CHANGED
|
@@ -264,6 +264,13 @@ export function createMarkerPlugin(options, isDev) {
|
|
|
264
264
|
additionalCode += `try { Object.defineProperty(${exp.name}, ${symbolCode}, { value: true }); ${exp.name}.displayName = "${exp.name}"; } catch {}\n`;
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
const hints = extractPropHints(ast, exports.map(e => e.name));
|
|
268
|
+
if (hints.length > 0) {
|
|
269
|
+
for (const exp of exports) {
|
|
270
|
+
additionalCode += `try { ${exp.name}.__propHints = ${JSON.stringify(hints)}; } catch {}\n`;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
267
274
|
return { contents: source + additionalCode, loader: "tsx" };
|
|
268
275
|
});
|
|
269
276
|
}
|
|
@@ -321,4 +328,99 @@ function findExports(ast) {
|
|
|
321
328
|
return exports;
|
|
322
329
|
}
|
|
323
330
|
|
|
324
|
-
|
|
331
|
+
/**
|
|
332
|
+
* Extracts prop access hints from a "use client" component's AST.
|
|
333
|
+
* Finds all property access patterns on destructured props, including
|
|
334
|
+
* those inside event handlers and other non-SSR code paths.
|
|
335
|
+
* @param {import("@babel/parser").ParseResult} ast - Parsed AST.
|
|
336
|
+
* @param {string[]} exportNames - Names of exported components.
|
|
337
|
+
* @returns {string[]} Array of prop access paths.
|
|
338
|
+
*/
|
|
339
|
+
function extractPropHints(ast, exportNames) {
|
|
340
|
+
const paramMap = new Map();
|
|
341
|
+
|
|
342
|
+
_traverse(ast, {
|
|
343
|
+
FunctionDeclaration(p) {
|
|
344
|
+
if (exportNames.includes(p.node.id?.name)) {
|
|
345
|
+
extractObjectParams(p.node.params[0], paramMap);
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
VariableDeclarator(p) {
|
|
349
|
+
if (exportNames.includes(p.node.id?.name)) {
|
|
350
|
+
const init = p.node.init;
|
|
351
|
+
if (init?.type === "ArrowFunctionExpression" || init?.type === "FunctionExpression") {
|
|
352
|
+
extractObjectParams(init.params[0], paramMap);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
if (paramMap.size === 0) return [];
|
|
359
|
+
|
|
360
|
+
const hints = new Set();
|
|
361
|
+
|
|
362
|
+
_traverse(ast, {
|
|
363
|
+
MemberExpression(p) {
|
|
364
|
+
collectPropHint(p, paramMap, hints);
|
|
365
|
+
},
|
|
366
|
+
OptionalMemberExpression(p) {
|
|
367
|
+
collectPropHint(p, paramMap, hints);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
return [...hints].sort();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function extractObjectParams(param, map) {
|
|
375
|
+
if (!param || param.type !== "ObjectPattern") return;
|
|
376
|
+
|
|
377
|
+
for (const prop of param.properties) {
|
|
378
|
+
if (prop.type === "ObjectProperty") {
|
|
379
|
+
const propName = prop.key.name || prop.key.value;
|
|
380
|
+
const localName = prop.value.type === "Identifier"
|
|
381
|
+
? prop.value.name
|
|
382
|
+
: prop.value.type === "AssignmentPattern" && prop.value.left?.type === "Identifier"
|
|
383
|
+
? prop.value.left.name
|
|
384
|
+
: null;
|
|
385
|
+
|
|
386
|
+
if (propName && localName) {
|
|
387
|
+
map.set(localName, propName);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
else if (prop.type === "RestElement" && prop.argument?.type === "Identifier") {
|
|
391
|
+
map.set(prop.argument.name, prop.argument.name);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function collectPropHint(path, paramMap, hints) {
|
|
397
|
+
const parent = path.parent;
|
|
398
|
+
|
|
399
|
+
if ((parent.type === "MemberExpression" || parent.type === "OptionalMemberExpression") && parent.object === path.node) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const chain = [];
|
|
404
|
+
let current = path.node;
|
|
405
|
+
|
|
406
|
+
while (current.type === "MemberExpression" || current.type === "OptionalMemberExpression") {
|
|
407
|
+
if (current.computed) return;
|
|
408
|
+
if (current.property?.type !== "Identifier") return;
|
|
409
|
+
chain.unshift(current.property.name);
|
|
410
|
+
current = current.object;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (current.type !== "Identifier") return;
|
|
414
|
+
|
|
415
|
+
const rootProp = paramMap.get(current.name);
|
|
416
|
+
if (!rootProp) return;
|
|
417
|
+
|
|
418
|
+
if (parent.type === "CallExpression" && parent.callee === path.node && chain.length > 0) {
|
|
419
|
+
chain.pop();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const fullPath = chain.length > 0 ? rootProp + "." + chain.join(".") : rootProp;
|
|
423
|
+
hints.add(fullPath);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export { findExports, extractPropHints };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import Output from "./Output.js";
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const PKG_PATH = path.resolve(__dirname, "../../package.json");
|
|
8
|
+
const REGISTRY_URL = "https://registry.npmjs.org/@nitronjs/framework/latest";
|
|
9
|
+
const CHECK_INTERVAL = 24 * 60 * 60 * 1000;
|
|
10
|
+
const FETCH_TIMEOUT = 3000;
|
|
11
|
+
|
|
12
|
+
const C = Output.COLORS;
|
|
13
|
+
|
|
14
|
+
function getCachePath() {
|
|
15
|
+
const nitronDir = path.join(process.cwd(), ".nitron");
|
|
16
|
+
|
|
17
|
+
if (!fs.existsSync(nitronDir)) {
|
|
18
|
+
fs.mkdirSync(nitronDir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return path.join(nitronDir, "update-check.json");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getCurrentVersion() {
|
|
25
|
+
const pkg = JSON.parse(fs.readFileSync(PKG_PATH, "utf-8"));
|
|
26
|
+
return pkg.version;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function compareVersions(current, latest) {
|
|
30
|
+
const a = current.split(".").map(Number);
|
|
31
|
+
const b = latest.split(".").map(Number);
|
|
32
|
+
|
|
33
|
+
for (let i = 0; i < 3; i++) {
|
|
34
|
+
if ((b[i] || 0) > (a[i] || 0)) return 1;
|
|
35
|
+
if ((b[i] || 0) < (a[i] || 0)) return -1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readCache(cachePath) {
|
|
42
|
+
try {
|
|
43
|
+
if (fs.existsSync(cachePath)) {
|
|
44
|
+
return JSON.parse(fs.readFileSync(cachePath, "utf-8"));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {}
|
|
48
|
+
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function writeCache(cachePath, latestVersion) {
|
|
53
|
+
try {
|
|
54
|
+
fs.writeFileSync(cachePath, JSON.stringify({ lastCheck: Date.now(), latestVersion }));
|
|
55
|
+
}
|
|
56
|
+
catch {}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function fetchLatestVersion() {
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT);
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch(REGISTRY_URL, { signal: controller.signal });
|
|
65
|
+
const data = await res.json();
|
|
66
|
+
return data.version || null;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
clearTimeout(timeout);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function printUpdateNotice(current, latest) {
|
|
77
|
+
const msg = `Update available: ${C.dim}${current}${C.reset} ${C.cyan}→${C.reset} ${C.green}${C.bold}${latest}${C.reset}`;
|
|
78
|
+
const cmd = `Run ${C.cyan}npm update @nitronjs/framework --save${C.reset} to update`;
|
|
79
|
+
const lines = [msg, cmd];
|
|
80
|
+
const maxLen = 52;
|
|
81
|
+
|
|
82
|
+
console.log();
|
|
83
|
+
console.log(`${C.yellow}╭${"─".repeat(maxLen)}╮${C.reset}`);
|
|
84
|
+
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
const rawLen = line.replace(/\x1b\[\d+m/g, "").length;
|
|
87
|
+
const pad = maxLen - 2 - rawLen;
|
|
88
|
+
console.log(`${C.yellow}│${C.reset} ${line}${" ".repeat(Math.max(0, pad))} ${C.yellow}│${C.reset}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log(`${C.yellow}╰${"─".repeat(maxLen)}╯${C.reset}`);
|
|
92
|
+
console.log();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default async function checkForUpdates() {
|
|
96
|
+
try {
|
|
97
|
+
const cachePath = getCachePath();
|
|
98
|
+
const current = getCurrentVersion();
|
|
99
|
+
const cache = readCache(cachePath);
|
|
100
|
+
|
|
101
|
+
let latestVersion = cache?.latestVersion || null;
|
|
102
|
+
const needsCheck = !cache || (Date.now() - cache.lastCheck) > CHECK_INTERVAL;
|
|
103
|
+
|
|
104
|
+
if (needsCheck) {
|
|
105
|
+
const fetched = await fetchLatestVersion();
|
|
106
|
+
|
|
107
|
+
if (fetched) {
|
|
108
|
+
latestVersion = fetched;
|
|
109
|
+
writeCache(cachePath, fetched);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (latestVersion && compareVersions(current, latestVersion) > 0) {
|
|
114
|
+
printUpdateNotice(current, latestVersion);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {}
|
|
118
|
+
}
|
package/lib/Database/Model.js
CHANGED
|
@@ -59,6 +59,16 @@ class Model {
|
|
|
59
59
|
target._attributes[prop] = value;
|
|
60
60
|
|
|
61
61
|
return true;
|
|
62
|
+
},
|
|
63
|
+
ownKeys: (target) => {
|
|
64
|
+
return [...Object.keys(target._attributes), ...Object.getOwnPropertyNames(target)];
|
|
65
|
+
},
|
|
66
|
+
getOwnPropertyDescriptor: (target, prop) => {
|
|
67
|
+
if (prop in target._attributes) {
|
|
68
|
+
return { configurable: true, enumerable: true, value: target._attributes[prop] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return Object.getOwnPropertyDescriptor(target, prop);
|
|
62
72
|
}
|
|
63
73
|
});
|
|
64
74
|
}
|
package/lib/Route/Router.js
CHANGED
|
@@ -151,7 +151,7 @@ class Router {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
// Resolve middleware names to handlers
|
|
154
|
-
|
|
154
|
+
route.resolvedMiddlewares = route.middlewares.map(middleware => {
|
|
155
155
|
const [name, param] = middleware.split(":");
|
|
156
156
|
|
|
157
157
|
if (!Kernel.routeMiddlewares[name]) {
|
|
@@ -164,13 +164,13 @@ class Router {
|
|
|
164
164
|
});
|
|
165
165
|
|
|
166
166
|
// Wrap controller handler for HMR
|
|
167
|
-
|
|
167
|
+
route.resolvedHandler = hotReload.wrap(route.handler);
|
|
168
168
|
|
|
169
169
|
server.route({
|
|
170
170
|
method: route.method,
|
|
171
171
|
url: route.url,
|
|
172
|
-
preHandler:
|
|
173
|
-
handler
|
|
172
|
+
preHandler: route.resolvedMiddlewares,
|
|
173
|
+
handler: route.resolvedHandler
|
|
174
174
|
});
|
|
175
175
|
}
|
|
176
176
|
}
|
package/lib/View/View.js
CHANGED
|
@@ -40,32 +40,61 @@ function escapeHtml(str) {
|
|
|
40
40
|
|
|
41
41
|
function trackProps(obj) {
|
|
42
42
|
const accessed = new Set();
|
|
43
|
+
const cache = new WeakMap();
|
|
43
44
|
|
|
44
|
-
function wrap(
|
|
45
|
-
if (
|
|
45
|
+
function wrap(source, prefix) {
|
|
46
|
+
if (source === null || typeof source !== "object") return source;
|
|
47
|
+
if (cache.has(source)) return cache.get(source);
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
const isArr = Array.isArray(source);
|
|
50
|
+
const copy = isArr ? [] : {};
|
|
51
|
+
|
|
52
|
+
for (const key of Object.keys(source)) {
|
|
53
|
+
const val = source[key];
|
|
54
|
+
|
|
55
|
+
if (val !== null && typeof val === "object" && typeof val !== "function") {
|
|
56
|
+
copy[key] = wrap(val, prefix ? prefix + "." + key : key);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
copy[key] = val;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const proxy = new Proxy(copy, {
|
|
48
64
|
get(t, prop, receiver) {
|
|
49
65
|
if (typeof prop === "symbol") return Reflect.get(t, prop, receiver);
|
|
50
66
|
|
|
51
67
|
const val = Reflect.get(t, prop, receiver);
|
|
52
68
|
if (typeof val === "function") return val;
|
|
53
69
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (val !== null && typeof val === "object") {
|
|
58
|
-
return wrap(val, currentPath);
|
|
70
|
+
if (val !== undefined) {
|
|
71
|
+
const currentPath = prefix ? prefix + "." + String(prop) : String(prop);
|
|
72
|
+
accessed.add(currentPath);
|
|
59
73
|
}
|
|
60
74
|
|
|
61
75
|
return val;
|
|
62
76
|
}
|
|
63
77
|
});
|
|
78
|
+
|
|
79
|
+
cache.set(source, proxy);
|
|
80
|
+
return proxy;
|
|
64
81
|
}
|
|
65
82
|
|
|
66
83
|
return { proxy: wrap(obj, ""), accessed };
|
|
67
84
|
}
|
|
68
85
|
|
|
86
|
+
function resolveExists(obj, path) {
|
|
87
|
+
const parts = path.split(".");
|
|
88
|
+
let current = obj;
|
|
89
|
+
|
|
90
|
+
for (let i = 0; i < parts.length; i++) {
|
|
91
|
+
if (current == null || typeof current !== "object") return false;
|
|
92
|
+
current = current[parts[i]];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return current !== undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
69
98
|
/**
|
|
70
99
|
* React SSR view renderer with streaming support.
|
|
71
100
|
* Handles component rendering, asset injection, and client hydration.
|
|
@@ -183,7 +212,7 @@ class View {
|
|
|
183
212
|
return { status: 404, error: "Route not found" };
|
|
184
213
|
}
|
|
185
214
|
|
|
186
|
-
const {
|
|
215
|
+
const { params, route } = routeMatch;
|
|
187
216
|
|
|
188
217
|
let viewName = null;
|
|
189
218
|
let viewParams = {};
|
|
@@ -207,7 +236,7 @@ class View {
|
|
|
207
236
|
session: originalReq.session
|
|
208
237
|
};
|
|
209
238
|
|
|
210
|
-
for (const mw of route.
|
|
239
|
+
for (const mw of route.resolvedMiddlewares || []) {
|
|
211
240
|
if (redirectTo || handled) break;
|
|
212
241
|
try {
|
|
213
242
|
await mw(mockReq, mockRes);
|
|
@@ -222,7 +251,7 @@ class View {
|
|
|
222
251
|
if (handled) return { handled: true };
|
|
223
252
|
if (redirectTo) return this.#validateRedirect(redirectTo, originalReq.headers.host);
|
|
224
253
|
|
|
225
|
-
await handler(mockReq, mockRes);
|
|
254
|
+
await (route.resolvedHandler || route.handler)(mockReq, mockRes);
|
|
226
255
|
|
|
227
256
|
if (handled) return { handled: true };
|
|
228
257
|
if (redirectTo) return this.#validateRedirect(redirectTo, originalReq.headers.host);
|
|
@@ -368,6 +397,14 @@ class View {
|
|
|
368
397
|
if (Component[MARK]) {
|
|
369
398
|
const tracker = trackProps(params);
|
|
370
399
|
|
|
400
|
+
if (Component.__propHints) {
|
|
401
|
+
for (const hint of Component.__propHints) {
|
|
402
|
+
if (resolveExists(params, hint)) {
|
|
403
|
+
tracker.accessed.add(hint);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
371
408
|
ctx.props[":R0:"] = params;
|
|
372
409
|
ctx.trackers = ctx.trackers || {};
|
|
373
410
|
ctx.trackers[":R0:"] = tracker.accessed;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitronjs/framework",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.21",
|
|
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"
|