@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 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
- const success = await Migrate({ seed: additionalArgs.includes("--seed") });
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
- const success = await Rollback({ step, all });
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
- process.exit(0);
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
- const success = await MigrateFresh({ seed: additionalArgs.includes("--seed") });
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
- const success = await Seed(seederName);
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
- const success = await StorageLink();
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
- const success = await Make(type, name);
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) {
@@ -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(target, path) {
92
- if (target === null || typeof target !== 'object') return target;
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
- return new Proxy(target, {
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
- 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);
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 || {};
@@ -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 };
@@ -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
+ }
@@ -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
@@ -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(target, prefix) {
45
- if (target === null || typeof target !== "object") return target;
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
- return new Proxy(target, {
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
- 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);
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 { handler, params, route } = routeMatch;
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.middlewares || []) {
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.19",
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"