@nitronjs/framework 0.2.20 → 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/lib/Build/jsxRuntime.js +23 -3
- package/lib/Build/plugins.js +103 -1
- package/lib/Database/Model.js +10 -0
- package/lib/Route/Router.js +4 -4
- package/lib/View/View.js +28 -6
- package/package.json +1 -1
package/lib/Build/jsxRuntime.js
CHANGED
|
@@ -113,10 +113,12 @@ function trackProps(obj) {
|
|
|
113
113
|
if (typeof prop === 'symbol') return Reflect.get(t, prop, receiver);
|
|
114
114
|
|
|
115
115
|
const val = Reflect.get(t, prop, receiver);
|
|
116
|
-
if (typeof val === 'function') return val
|
|
116
|
+
if (typeof val === 'function') return val;
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
if (val !== undefined) {
|
|
119
|
+
const currentPath = prefix ? prefix + '.' + String(prop) : String(prop);
|
|
120
|
+
accessed.add(currentPath);
|
|
121
|
+
}
|
|
120
122
|
|
|
121
123
|
return val;
|
|
122
124
|
}
|
|
@@ -129,6 +131,16 @@ function trackProps(obj) {
|
|
|
129
131
|
return { proxy: wrap(obj, ''), accessed };
|
|
130
132
|
}
|
|
131
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
|
+
|
|
132
144
|
// Creates an island wrapper for client components (hydrated independently)
|
|
133
145
|
function createIsland(Component, name) {
|
|
134
146
|
function IslandBoundary(props) {
|
|
@@ -139,6 +151,14 @@ function createIsland(Component, name) {
|
|
|
139
151
|
const id = React.useId();
|
|
140
152
|
const tracker = trackProps(props);
|
|
141
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
|
+
|
|
142
162
|
const ctx = getContext();
|
|
143
163
|
if (ctx) {
|
|
144
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 };
|
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
|
@@ -65,10 +65,12 @@ function trackProps(obj) {
|
|
|
65
65
|
if (typeof prop === "symbol") return Reflect.get(t, prop, receiver);
|
|
66
66
|
|
|
67
67
|
const val = Reflect.get(t, prop, receiver);
|
|
68
|
-
if (typeof val === "function") return val
|
|
68
|
+
if (typeof val === "function") return val;
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
if (val !== undefined) {
|
|
71
|
+
const currentPath = prefix ? prefix + "." + String(prop) : String(prop);
|
|
72
|
+
accessed.add(currentPath);
|
|
73
|
+
}
|
|
72
74
|
|
|
73
75
|
return val;
|
|
74
76
|
}
|
|
@@ -81,6 +83,18 @@ function trackProps(obj) {
|
|
|
81
83
|
return { proxy: wrap(obj, ""), accessed };
|
|
82
84
|
}
|
|
83
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
|
+
|
|
84
98
|
/**
|
|
85
99
|
* React SSR view renderer with streaming support.
|
|
86
100
|
* Handles component rendering, asset injection, and client hydration.
|
|
@@ -198,7 +212,7 @@ class View {
|
|
|
198
212
|
return { status: 404, error: "Route not found" };
|
|
199
213
|
}
|
|
200
214
|
|
|
201
|
-
const {
|
|
215
|
+
const { params, route } = routeMatch;
|
|
202
216
|
|
|
203
217
|
let viewName = null;
|
|
204
218
|
let viewParams = {};
|
|
@@ -222,7 +236,7 @@ class View {
|
|
|
222
236
|
session: originalReq.session
|
|
223
237
|
};
|
|
224
238
|
|
|
225
|
-
for (const mw of route.
|
|
239
|
+
for (const mw of route.resolvedMiddlewares || []) {
|
|
226
240
|
if (redirectTo || handled) break;
|
|
227
241
|
try {
|
|
228
242
|
await mw(mockReq, mockRes);
|
|
@@ -237,7 +251,7 @@ class View {
|
|
|
237
251
|
if (handled) return { handled: true };
|
|
238
252
|
if (redirectTo) return this.#validateRedirect(redirectTo, originalReq.headers.host);
|
|
239
253
|
|
|
240
|
-
await handler(mockReq, mockRes);
|
|
254
|
+
await (route.resolvedHandler || route.handler)(mockReq, mockRes);
|
|
241
255
|
|
|
242
256
|
if (handled) return { handled: true };
|
|
243
257
|
if (redirectTo) return this.#validateRedirect(redirectTo, originalReq.headers.host);
|
|
@@ -383,6 +397,14 @@ class View {
|
|
|
383
397
|
if (Component[MARK]) {
|
|
384
398
|
const tracker = trackProps(params);
|
|
385
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
|
+
|
|
386
408
|
ctx.props[":R0:"] = params;
|
|
387
409
|
ctx.trackers = ctx.trackers || {};
|
|
388
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"
|