@nitronjs/framework 0.2.18 → 0.2.20
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 +18 -13
- package/lib/Build/FactoryTransform.js +132 -0
- package/lib/Build/Manager.js +6 -0
- package/lib/Build/jsxRuntime.js +50 -3
- package/lib/Console/UpdateChecker.js +118 -0
- package/lib/View/View.js +117 -8
- package/package.json +2 -1
package/cli/njs.js
CHANGED
|
@@ -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) {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { init, parse } from "es-module-lexer";
|
|
4
|
+
|
|
5
|
+
await init;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Transforms a built view module into an async factory function.
|
|
9
|
+
* Keeps imports at module level, wraps body code in a factory
|
|
10
|
+
* that re-executes per request. Skips "use client" modules.
|
|
11
|
+
*/
|
|
12
|
+
function transformFile(filePath) {
|
|
13
|
+
const code = fs.readFileSync(filePath, "utf8");
|
|
14
|
+
const trimmed = code.trimStart();
|
|
15
|
+
|
|
16
|
+
if (trimmed.startsWith('"use client"') || trimmed.startsWith("'use client'")) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let imports;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
[imports] = parse(code);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Collect static import statement ranges
|
|
30
|
+
const importRanges = [];
|
|
31
|
+
|
|
32
|
+
for (const imp of imports) {
|
|
33
|
+
if (imp.d === -1) {
|
|
34
|
+
importRanges.push({ start: imp.ss, end: imp.se });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
importRanges.sort((a, b) => a.start - b.start);
|
|
39
|
+
|
|
40
|
+
// Find last `export { ... };` block
|
|
41
|
+
let exportStart = code.length;
|
|
42
|
+
|
|
43
|
+
while (true) {
|
|
44
|
+
exportStart = code.lastIndexOf("export", exportStart - 1);
|
|
45
|
+
if (exportStart === -1) return false;
|
|
46
|
+
if (code.slice(exportStart + 6).trimStart().startsWith("{")) break;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const exportClose = code.indexOf("};", exportStart);
|
|
50
|
+
|
|
51
|
+
if (exportClose === -1) return false;
|
|
52
|
+
|
|
53
|
+
const exportEnd = exportClose + 2;
|
|
54
|
+
const exportInner = code.slice(exportStart, exportEnd).match(/\{([\s\S]*)\}/)?.[1];
|
|
55
|
+
|
|
56
|
+
if (!exportInner) return false;
|
|
57
|
+
|
|
58
|
+
// Build return statement from export specifiers
|
|
59
|
+
const returnParts = [];
|
|
60
|
+
|
|
61
|
+
for (const spec of exportInner.split(",").map(s => s.trim()).filter(Boolean)) {
|
|
62
|
+
const m = spec.match(/^(.+?)\s+as\s+(.+)$/);
|
|
63
|
+
|
|
64
|
+
if (m) {
|
|
65
|
+
const local = m[1].trim();
|
|
66
|
+
const exported = m[2].trim();
|
|
67
|
+
|
|
68
|
+
returnParts.push(exported === "default" ? `"default": ${local}` : `${exported}: ${local}`);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
returnParts.push(spec);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Separate imports from body
|
|
76
|
+
const excludes = [...importRanges, { start: exportStart, end: exportEnd }];
|
|
77
|
+
|
|
78
|
+
excludes.sort((a, b) => a.start - b.start);
|
|
79
|
+
|
|
80
|
+
const importLines = importRanges.map(r => code.slice(r.start, r.end).trim());
|
|
81
|
+
const bodyParts = [];
|
|
82
|
+
let cursor = 0;
|
|
83
|
+
|
|
84
|
+
for (const range of excludes) {
|
|
85
|
+
if (cursor < range.start) {
|
|
86
|
+
bodyParts.push(code.slice(cursor, range.start));
|
|
87
|
+
}
|
|
88
|
+
cursor = range.end;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const remaining = code.slice(cursor).trim();
|
|
92
|
+
|
|
93
|
+
if (remaining && !remaining.startsWith("//# sourceMappingURL")) {
|
|
94
|
+
bodyParts.push(remaining);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const body = bodyParts.join("").trim();
|
|
98
|
+
|
|
99
|
+
// Write transformed file
|
|
100
|
+
const output =
|
|
101
|
+
importLines.join("\n") + "\n\n" +
|
|
102
|
+
"export const __factory = true;\n\n" +
|
|
103
|
+
"export default async function __viewFactory() {\n" +
|
|
104
|
+
body + "\n" +
|
|
105
|
+
"return { " + returnParts.join(", ") + " };\n" +
|
|
106
|
+
"}\n";
|
|
107
|
+
|
|
108
|
+
fs.writeFileSync(filePath, output);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Transforms all built view files in a directory tree.
|
|
114
|
+
*/
|
|
115
|
+
function transformDirectory(dir) {
|
|
116
|
+
let count = 0;
|
|
117
|
+
|
|
118
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
119
|
+
const full = path.join(dir, entry.name);
|
|
120
|
+
|
|
121
|
+
if (entry.isDirectory()) {
|
|
122
|
+
count += transformDirectory(full);
|
|
123
|
+
}
|
|
124
|
+
else if (entry.name.endsWith(".js") && !entry.name.endsWith(".map")) {
|
|
125
|
+
if (transformFile(full)) count++;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return count;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export { transformFile, transformDirectory };
|
package/lib/Build/Manager.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
createMarkerPlugin,
|
|
21
21
|
createServerModuleBlockerPlugin
|
|
22
22
|
} from "./plugins.js";
|
|
23
|
+
import { transformFile as factoryTransform } from "./FactoryTransform.js";
|
|
23
24
|
import COLORS from "./colors.js";
|
|
24
25
|
|
|
25
26
|
dotenv.config({ quiet: true });
|
|
@@ -334,6 +335,11 @@ class Builder {
|
|
|
334
335
|
this.#cache.viewsChanged = true;
|
|
335
336
|
await this.#runEsbuild(changedFiles, outDir, { meta, outbase: srcDir });
|
|
336
337
|
await this.#postProcessMeta(changedFiles, srcDir, outDir);
|
|
338
|
+
|
|
339
|
+
for (const entry of changedFiles) {
|
|
340
|
+
const rel = path.relative(srcDir, entry).replace(/\.tsx$/, ".js");
|
|
341
|
+
factoryTransform(path.join(outDir, rel));
|
|
342
|
+
}
|
|
337
343
|
}
|
|
338
344
|
|
|
339
345
|
this.#stats[namespace === "user" ? "user" : "framework"] = entries.length;
|
package/lib/Build/jsxRuntime.js
CHANGED
|
@@ -84,6 +84,51 @@ function wrapWithDepth(children) {
|
|
|
84
84
|
return OriginalJsx.jsx(DepthContext.Provider, { value: true, children });
|
|
85
85
|
}
|
|
86
86
|
|
|
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
|
|
89
|
+
function trackProps(obj) {
|
|
90
|
+
const accessed = new Set();
|
|
91
|
+
const cache = new WeakMap();
|
|
92
|
+
|
|
93
|
+
function wrap(source, prefix) {
|
|
94
|
+
if (source === null || typeof source !== 'object') return source;
|
|
95
|
+
if (cache.has(source)) return cache.get(source);
|
|
96
|
+
|
|
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, {
|
|
112
|
+
get(t, prop, receiver) {
|
|
113
|
+
if (typeof prop === 'symbol') return Reflect.get(t, prop, receiver);
|
|
114
|
+
|
|
115
|
+
const val = Reflect.get(t, prop, receiver);
|
|
116
|
+
if (typeof val === 'function') return val.bind(receiver);
|
|
117
|
+
|
|
118
|
+
const currentPath = prefix ? prefix + '.' + String(prop) : String(prop);
|
|
119
|
+
accessed.add(currentPath);
|
|
120
|
+
|
|
121
|
+
return val;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
cache.set(source, proxy);
|
|
126
|
+
return proxy;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { proxy: wrap(obj, ''), accessed };
|
|
130
|
+
}
|
|
131
|
+
|
|
87
132
|
// Creates an island wrapper for client components (hydrated independently)
|
|
88
133
|
function createIsland(Component, name) {
|
|
89
134
|
function IslandBoundary(props) {
|
|
@@ -92,18 +137,20 @@ function createIsland(Component, name) {
|
|
|
92
137
|
}
|
|
93
138
|
|
|
94
139
|
const id = React.useId();
|
|
95
|
-
const
|
|
140
|
+
const tracker = trackProps(props);
|
|
96
141
|
|
|
97
142
|
const ctx = getContext();
|
|
98
143
|
if (ctx) {
|
|
99
144
|
ctx.props = ctx.props || {};
|
|
100
|
-
ctx.
|
|
145
|
+
ctx.trackers = ctx.trackers || {};
|
|
146
|
+
ctx.props[id] = props;
|
|
147
|
+
ctx.trackers[id] = tracker.accessed;
|
|
101
148
|
}
|
|
102
149
|
|
|
103
150
|
return OriginalJsx.jsx('div', {
|
|
104
151
|
'data-cid': id,
|
|
105
152
|
'data-island': name,
|
|
106
|
-
children: wrapWithDepth(OriginalJsx.jsx(Component,
|
|
153
|
+
children: wrapWithDepth(OriginalJsx.jsx(Component, tracker.proxy))
|
|
107
154
|
});
|
|
108
155
|
}
|
|
109
156
|
|
|
@@ -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/View/View.js
CHANGED
|
@@ -38,6 +38,49 @@ function escapeHtml(str) {
|
|
|
38
38
|
return String(str).replace(/[&<>"'`]/g, char => ESC_MAP[char]);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function trackProps(obj) {
|
|
42
|
+
const accessed = new Set();
|
|
43
|
+
const cache = new WeakMap();
|
|
44
|
+
|
|
45
|
+
function wrap(source, prefix) {
|
|
46
|
+
if (source === null || typeof source !== "object") return source;
|
|
47
|
+
if (cache.has(source)) return cache.get(source);
|
|
48
|
+
|
|
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, {
|
|
64
|
+
get(t, prop, receiver) {
|
|
65
|
+
if (typeof prop === "symbol") return Reflect.get(t, prop, receiver);
|
|
66
|
+
|
|
67
|
+
const val = Reflect.get(t, prop, receiver);
|
|
68
|
+
if (typeof val === "function") return val.bind(receiver);
|
|
69
|
+
|
|
70
|
+
const currentPath = prefix ? prefix + "." + String(prop) : String(prop);
|
|
71
|
+
accessed.add(currentPath);
|
|
72
|
+
|
|
73
|
+
return val;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
cache.set(source, proxy);
|
|
78
|
+
return proxy;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { proxy: wrap(obj, ""), accessed };
|
|
82
|
+
}
|
|
83
|
+
|
|
41
84
|
/**
|
|
42
85
|
* React SSR view renderer with streaming support.
|
|
43
86
|
* Handles component rendering, asset injection, and client hydration.
|
|
@@ -338,14 +381,19 @@ class View {
|
|
|
338
381
|
let element;
|
|
339
382
|
|
|
340
383
|
if (Component[MARK]) {
|
|
341
|
-
|
|
384
|
+
const tracker = trackProps(params);
|
|
385
|
+
|
|
386
|
+
ctx.props[":R0:"] = params;
|
|
387
|
+
ctx.trackers = ctx.trackers || {};
|
|
388
|
+
ctx.trackers[":R0:"] = tracker.accessed;
|
|
389
|
+
|
|
342
390
|
element = React.createElement(
|
|
343
391
|
"div",
|
|
344
392
|
{
|
|
345
393
|
"data-cid": ":R0:",
|
|
346
394
|
"data-island": Component.displayName || Component.name || "Anonymous"
|
|
347
395
|
},
|
|
348
|
-
React.createElement(Component,
|
|
396
|
+
React.createElement(Component, tracker.proxy)
|
|
349
397
|
);
|
|
350
398
|
} else {
|
|
351
399
|
element = Component(params);
|
|
@@ -368,6 +416,15 @@ class View {
|
|
|
368
416
|
}
|
|
369
417
|
|
|
370
418
|
html = await this.#renderToHtml(element);
|
|
419
|
+
|
|
420
|
+
if (ctx.trackers) {
|
|
421
|
+
for (const [id, accessed] of Object.entries(ctx.trackers)) {
|
|
422
|
+
if (ctx.props[id]) {
|
|
423
|
+
ctx.props[id] = this.#pickProps(ctx.props[id], accessed);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
371
428
|
collectedProps = ctx.props;
|
|
372
429
|
} catch (error) {
|
|
373
430
|
const componentName = mod.default?.displayName || mod.default?.name || "Unknown";
|
|
@@ -478,17 +535,27 @@ class View {
|
|
|
478
535
|
|
|
479
536
|
static async #importModule(filePath) {
|
|
480
537
|
const url = pathToFileURL(filePath).href;
|
|
538
|
+
let cached = this.#moduleCache.get(filePath);
|
|
481
539
|
|
|
482
540
|
if (this.#isDev) {
|
|
483
|
-
|
|
541
|
+
const mtime = statSync(filePath).mtimeMs;
|
|
542
|
+
|
|
543
|
+
if (!cached || cached.mtime !== mtime) {
|
|
544
|
+
const mod = await import(url + `?v=${mtime}`);
|
|
545
|
+
cached = { mod, mtime };
|
|
546
|
+
this.#moduleCache.set(filePath, cached);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return cached.mod.__factory ? await cached.mod.default() : cached.mod;
|
|
484
550
|
}
|
|
485
551
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
this.#moduleCache.set(filePath,
|
|
552
|
+
if (!cached) {
|
|
553
|
+
const mod = await import(url);
|
|
554
|
+
cached = { mod };
|
|
555
|
+
this.#moduleCache.set(filePath, cached);
|
|
490
556
|
}
|
|
491
|
-
|
|
557
|
+
|
|
558
|
+
return cached.mod.__factory ? await cached.mod.default() : cached.mod;
|
|
492
559
|
}
|
|
493
560
|
|
|
494
561
|
static #sanitizeProps(obj, seen = new WeakSet()) {
|
|
@@ -549,6 +616,48 @@ class View {
|
|
|
549
616
|
return result;
|
|
550
617
|
}
|
|
551
618
|
|
|
619
|
+
static #pickProps(props, accessed) {
|
|
620
|
+
const paths = [...accessed].sort();
|
|
621
|
+
const leaves = [];
|
|
622
|
+
|
|
623
|
+
for (let i = 0; i < paths.length; i++) {
|
|
624
|
+
const isLeaf = i === paths.length - 1 || !paths[i + 1].startsWith(paths[i] + ".");
|
|
625
|
+
|
|
626
|
+
if (isLeaf) {
|
|
627
|
+
leaves.push(paths[i]);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const result = {};
|
|
632
|
+
|
|
633
|
+
for (const leaf of leaves) {
|
|
634
|
+
const parts = leaf.split(".");
|
|
635
|
+
let src = props;
|
|
636
|
+
let dst = result;
|
|
637
|
+
|
|
638
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
639
|
+
if (src == null) break;
|
|
640
|
+
|
|
641
|
+
const nextSrc = src[parts[i]];
|
|
642
|
+
|
|
643
|
+
if (dst[parts[i]] == null) {
|
|
644
|
+
dst[parts[i]] = Array.isArray(nextSrc) ? [] : {};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
src = nextSrc;
|
|
648
|
+
dst = dst[parts[i]];
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const last = parts[parts.length - 1];
|
|
652
|
+
|
|
653
|
+
if (src != null && !(Array.isArray(dst) && last === "length")) {
|
|
654
|
+
dst[last] = src[last];
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return this.#sanitizeProps(result);
|
|
659
|
+
}
|
|
660
|
+
|
|
552
661
|
static #renderToHtml(element) {
|
|
553
662
|
return new Promise((resolve, reject) => {
|
|
554
663
|
const chunks = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitronjs/framework",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.20",
|
|
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"
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"bcrypt": "^5.1.1",
|
|
28
28
|
"chokidar": "^5.0.0",
|
|
29
29
|
"dotenv": "^17.2.3",
|
|
30
|
+
"es-module-lexer": "^2.0.0",
|
|
30
31
|
"esbuild": "^0.27.2",
|
|
31
32
|
"fastify": "^5.6.2",
|
|
32
33
|
"mysql2": "^3.16.0",
|