@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.
@@ -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.bind(receiver);
116
+ if (typeof val === 'function') return val;
117
117
 
118
- const currentPath = prefix ? prefix + '.' + String(prop) : String(prop);
119
- accessed.add(currentPath);
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 || {};
@@ -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
- export { findExports };
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 };
@@ -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
  }
@@ -151,7 +151,7 @@ class Router {
151
151
  }
152
152
 
153
153
  // Resolve middleware names to handlers
154
- const middlewareHandlers = route.middlewares.map(middleware => {
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
- const handler = hotReload.wrap(route.handler);
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: middlewareHandlers,
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.bind(receiver);
68
+ if (typeof val === "function") return val;
69
69
 
70
- const currentPath = prefix ? prefix + "." + String(prop) : String(prop);
71
- accessed.add(currentPath);
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 { handler, params, route } = routeMatch;
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.middlewares || []) {
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.20",
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"