@nitronjs/framework 0.2.17 → 0.2.19
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 +1 -1
- package/lib/Build/FactoryTransform.js +132 -0
- package/lib/Build/Manager.js +6 -0
- package/lib/Build/jsxRuntime.js +34 -3
- package/lib/View/View.js +111 -10
- package/package.json +2 -1
package/cli/njs.js
CHANGED
|
@@ -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,35 @@ 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
|
+
function trackProps(obj) {
|
|
89
|
+
const accessed = new Set();
|
|
90
|
+
|
|
91
|
+
function wrap(target, path) {
|
|
92
|
+
if (target === null || typeof target !== 'object') return target;
|
|
93
|
+
|
|
94
|
+
return new Proxy(target, {
|
|
95
|
+
get(t, prop, receiver) {
|
|
96
|
+
if (typeof prop === 'symbol') return Reflect.get(t, prop, receiver);
|
|
97
|
+
|
|
98
|
+
const val = Reflect.get(t, prop, receiver);
|
|
99
|
+
if (typeof val === 'function') return val;
|
|
100
|
+
|
|
101
|
+
const currentPath = path ? path + '.' + String(prop) : String(prop);
|
|
102
|
+
accessed.add(currentPath);
|
|
103
|
+
|
|
104
|
+
if (val !== null && typeof val === 'object') {
|
|
105
|
+
return wrap(val, currentPath);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return val;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { proxy: wrap(obj, ''), accessed };
|
|
114
|
+
}
|
|
115
|
+
|
|
87
116
|
// Creates an island wrapper for client components (hydrated independently)
|
|
88
117
|
function createIsland(Component, name) {
|
|
89
118
|
function IslandBoundary(props) {
|
|
@@ -92,18 +121,20 @@ function createIsland(Component, name) {
|
|
|
92
121
|
}
|
|
93
122
|
|
|
94
123
|
const id = React.useId();
|
|
95
|
-
const
|
|
124
|
+
const tracker = trackProps(props);
|
|
96
125
|
|
|
97
126
|
const ctx = getContext();
|
|
98
127
|
if (ctx) {
|
|
99
128
|
ctx.props = ctx.props || {};
|
|
100
|
-
ctx.
|
|
129
|
+
ctx.trackers = ctx.trackers || {};
|
|
130
|
+
ctx.props[id] = props;
|
|
131
|
+
ctx.trackers[id] = tracker.accessed;
|
|
101
132
|
}
|
|
102
133
|
|
|
103
134
|
return OriginalJsx.jsx('div', {
|
|
104
135
|
'data-cid': id,
|
|
105
136
|
'data-island': name,
|
|
106
|
-
children: wrapWithDepth(OriginalJsx.jsx(Component,
|
|
137
|
+
children: wrapWithDepth(OriginalJsx.jsx(Component, tracker.proxy))
|
|
107
138
|
});
|
|
108
139
|
}
|
|
109
140
|
|
package/lib/View/View.js
CHANGED
|
@@ -38,6 +38,34 @@ 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
|
+
|
|
44
|
+
function wrap(target, prefix) {
|
|
45
|
+
if (target === null || typeof target !== "object") return target;
|
|
46
|
+
|
|
47
|
+
return new Proxy(target, {
|
|
48
|
+
get(t, prop, receiver) {
|
|
49
|
+
if (typeof prop === "symbol") return Reflect.get(t, prop, receiver);
|
|
50
|
+
|
|
51
|
+
const val = Reflect.get(t, prop, receiver);
|
|
52
|
+
if (typeof val === "function") return val;
|
|
53
|
+
|
|
54
|
+
const currentPath = prefix ? prefix + "." + String(prop) : String(prop);
|
|
55
|
+
accessed.add(currentPath);
|
|
56
|
+
|
|
57
|
+
if (val !== null && typeof val === "object") {
|
|
58
|
+
return wrap(val, currentPath);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return val;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { proxy: wrap(obj, ""), accessed };
|
|
67
|
+
}
|
|
68
|
+
|
|
41
69
|
/**
|
|
42
70
|
* React SSR view renderer with streaming support.
|
|
43
71
|
* Handles component rendering, asset injection, and client hydration.
|
|
@@ -338,17 +366,25 @@ class View {
|
|
|
338
366
|
let element;
|
|
339
367
|
|
|
340
368
|
if (Component[MARK]) {
|
|
341
|
-
|
|
369
|
+
const tracker = trackProps(params);
|
|
370
|
+
|
|
371
|
+
ctx.props[":R0:"] = params;
|
|
372
|
+
ctx.trackers = ctx.trackers || {};
|
|
373
|
+
ctx.trackers[":R0:"] = tracker.accessed;
|
|
374
|
+
|
|
342
375
|
element = React.createElement(
|
|
343
376
|
"div",
|
|
344
377
|
{
|
|
345
378
|
"data-cid": ":R0:",
|
|
346
379
|
"data-island": Component.displayName || Component.name || "Anonymous"
|
|
347
380
|
},
|
|
348
|
-
React.createElement(Component,
|
|
381
|
+
React.createElement(Component, tracker.proxy)
|
|
349
382
|
);
|
|
350
383
|
} else {
|
|
351
|
-
element =
|
|
384
|
+
element = Component(params);
|
|
385
|
+
if (element && typeof element.then === "function") {
|
|
386
|
+
element = await element;
|
|
387
|
+
}
|
|
352
388
|
}
|
|
353
389
|
|
|
354
390
|
if (layoutModules.length > 0) {
|
|
@@ -357,10 +393,23 @@ class View {
|
|
|
357
393
|
|
|
358
394
|
for (let i = layoutModules.length - 1; i >= 0; i--) {
|
|
359
395
|
const LayoutComponent = layoutModules[i].default;
|
|
360
|
-
element =
|
|
396
|
+
element = LayoutComponent({ children: element });
|
|
397
|
+
|
|
398
|
+
if (element && typeof element.then === "function") {
|
|
399
|
+
element = await element;
|
|
400
|
+
}
|
|
361
401
|
}
|
|
362
402
|
|
|
363
403
|
html = await this.#renderToHtml(element);
|
|
404
|
+
|
|
405
|
+
if (ctx.trackers) {
|
|
406
|
+
for (const [id, accessed] of Object.entries(ctx.trackers)) {
|
|
407
|
+
if (ctx.props[id]) {
|
|
408
|
+
ctx.props[id] = this.#pickProps(ctx.props[id], accessed);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
364
413
|
collectedProps = ctx.props;
|
|
365
414
|
} catch (error) {
|
|
366
415
|
const componentName = mod.default?.displayName || mod.default?.name || "Unknown";
|
|
@@ -471,17 +520,27 @@ class View {
|
|
|
471
520
|
|
|
472
521
|
static async #importModule(filePath) {
|
|
473
522
|
const url = pathToFileURL(filePath).href;
|
|
523
|
+
let cached = this.#moduleCache.get(filePath);
|
|
474
524
|
|
|
475
525
|
if (this.#isDev) {
|
|
476
|
-
|
|
526
|
+
const mtime = statSync(filePath).mtimeMs;
|
|
527
|
+
|
|
528
|
+
if (!cached || cached.mtime !== mtime) {
|
|
529
|
+
const mod = await import(url + `?v=${mtime}`);
|
|
530
|
+
cached = { mod, mtime };
|
|
531
|
+
this.#moduleCache.set(filePath, cached);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return cached.mod.__factory ? await cached.mod.default() : cached.mod;
|
|
477
535
|
}
|
|
478
536
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
this.#moduleCache.set(filePath,
|
|
537
|
+
if (!cached) {
|
|
538
|
+
const mod = await import(url);
|
|
539
|
+
cached = { mod };
|
|
540
|
+
this.#moduleCache.set(filePath, cached);
|
|
483
541
|
}
|
|
484
|
-
|
|
542
|
+
|
|
543
|
+
return cached.mod.__factory ? await cached.mod.default() : cached.mod;
|
|
485
544
|
}
|
|
486
545
|
|
|
487
546
|
static #sanitizeProps(obj, seen = new WeakSet()) {
|
|
@@ -542,6 +601,48 @@ class View {
|
|
|
542
601
|
return result;
|
|
543
602
|
}
|
|
544
603
|
|
|
604
|
+
static #pickProps(props, accessed) {
|
|
605
|
+
const paths = [...accessed].sort();
|
|
606
|
+
const leaves = [];
|
|
607
|
+
|
|
608
|
+
for (let i = 0; i < paths.length; i++) {
|
|
609
|
+
const isLeaf = i === paths.length - 1 || !paths[i + 1].startsWith(paths[i] + ".");
|
|
610
|
+
|
|
611
|
+
if (isLeaf) {
|
|
612
|
+
leaves.push(paths[i]);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const result = {};
|
|
617
|
+
|
|
618
|
+
for (const leaf of leaves) {
|
|
619
|
+
const parts = leaf.split(".");
|
|
620
|
+
let src = props;
|
|
621
|
+
let dst = result;
|
|
622
|
+
|
|
623
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
624
|
+
if (src == null) break;
|
|
625
|
+
|
|
626
|
+
const nextSrc = src[parts[i]];
|
|
627
|
+
|
|
628
|
+
if (dst[parts[i]] == null) {
|
|
629
|
+
dst[parts[i]] = Array.isArray(nextSrc) ? [] : {};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
src = nextSrc;
|
|
633
|
+
dst = dst[parts[i]];
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const last = parts[parts.length - 1];
|
|
637
|
+
|
|
638
|
+
if (src != null && !(Array.isArray(dst) && last === "length")) {
|
|
639
|
+
dst[last] = src[last];
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return this.#sanitizeProps(result);
|
|
644
|
+
}
|
|
645
|
+
|
|
545
646
|
static #renderToHtml(element) {
|
|
546
647
|
return new Promise((resolve, reject) => {
|
|
547
648
|
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.19",
|
|
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",
|