@nitronjs/framework 0.1.24 → 0.2.0

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.
Files changed (37) hide show
  1. package/lib/Build/CssBuilder.js +129 -0
  2. package/lib/Build/FileAnalyzer.js +395 -0
  3. package/lib/Build/HydrationBuilder.js +173 -0
  4. package/lib/Build/Manager.js +290 -943
  5. package/lib/Build/colors.js +10 -0
  6. package/lib/Build/jsxRuntime.js +116 -0
  7. package/lib/Build/plugins.js +264 -0
  8. package/lib/Console/Commands/BuildCommand.js +6 -5
  9. package/lib/Console/Commands/DevCommand.js +151 -311
  10. package/lib/Console/Stubs/page-hydration-dev.tsx +72 -0
  11. package/lib/Console/Stubs/page-hydration.tsx +9 -10
  12. package/lib/Console/Stubs/vendor-dev.tsx +50 -0
  13. package/lib/Core/Environment.js +29 -2
  14. package/lib/Core/Paths.js +12 -4
  15. package/lib/Database/Drivers/MySQLDriver.js +5 -4
  16. package/lib/Database/QueryBuilder.js +2 -3
  17. package/lib/Filesystem/Manager.js +32 -7
  18. package/lib/HMR/Server.js +87 -0
  19. package/lib/Http/Server.js +9 -5
  20. package/lib/Logging/Manager.js +68 -18
  21. package/lib/Route/Loader.js +3 -4
  22. package/lib/Route/Manager.js +24 -3
  23. package/lib/Runtime/Entry.js +26 -1
  24. package/lib/Session/File.js +18 -7
  25. package/lib/View/Client/hmr-client.js +166 -0
  26. package/lib/View/Client/spa.js +142 -0
  27. package/lib/View/Layout.js +94 -0
  28. package/lib/View/Manager.js +390 -46
  29. package/lib/index.d.ts +55 -0
  30. package/package.json +2 -1
  31. package/skeleton/.env.example +0 -2
  32. package/skeleton/app/Controllers/HomeController.js +27 -3
  33. package/skeleton/config/app.js +15 -14
  34. package/skeleton/config/session.js +1 -1
  35. package/skeleton/globals.d.ts +3 -63
  36. package/skeleton/resources/views/Site/Home.tsx +274 -50
  37. package/skeleton/tsconfig.json +5 -1
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync } from "fs";
2
2
  import path from "path";
3
- import { pathToFileURL } from "url";
3
+ import { pathToFileURL, fileURLToPath } from "url";
4
4
  import { randomBytes } from "crypto";
5
5
  import { AsyncLocalStorage } from "node:async_hooks";
6
6
  import { PassThrough } from "stream";
@@ -10,6 +10,12 @@ import Log from "../Logging/Manager.js";
10
10
  import Route from "../Route/Manager.js";
11
11
  import Paths from "../Core/Paths.js";
12
12
  import Config from "../Core/Config.js";
13
+ import Environment from "../Core/Environment.js";
14
+
15
+ // Get framework version from package.json
16
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
+ const packageJsonPath = path.join(__dirname, "../../package.json");
18
+ const FRAMEWORK_VERSION = JSON.parse(readFileSync(packageJsonPath, "utf-8")).version;
13
19
 
14
20
  const CTX = Symbol.for("__nitron_view_context__");
15
21
  const MARK = Symbol.for("__nitron_client_component__");
@@ -36,11 +42,14 @@ class View {
36
42
  static #root = Paths.project;
37
43
  static #userViews = Paths.buildViews;
38
44
  static #frameworkViews = Paths.buildFrameworkViews;
39
- static #manifestPath = path.join(Paths.project, "build/manifest.json");
45
+ static #manifestPath = path.join(Paths.build, "manifest.json");
40
46
  static #manifest = null;
41
47
  static #routesCache = null;
42
48
  static #manifestMtime = null;
43
- static #isDev = process.env.APP_DEV === "true";
49
+
50
+ static get #isDev() {
51
+ return Environment.isDev;
52
+ }
44
53
 
45
54
  static setup(server) {
46
55
  server.decorateReply("view", async function(name, params = {}) {
@@ -60,7 +69,8 @@ class View {
60
69
  name,
61
70
  params,
62
71
  csrf,
63
- fastifyRequest
72
+ fastifyRequest,
73
+ entry
64
74
  );
65
75
 
66
76
  View.#setSecurityHeaders(this, nonce);
@@ -99,6 +109,124 @@ class View {
99
109
  message: View.#isDev ? err.message : "An unexpected error occurred"
100
110
  });
101
111
  });
112
+
113
+ server.get("/__nitron/navigate", async (req, res) => {
114
+ const url = req.query.url;
115
+ if (!url) {
116
+ return res.code(400).send({ error: "Missing url" });
117
+ }
118
+
119
+ const result = await View.#renderPartial(url, req, res);
120
+
121
+ if (result.handled) return;
122
+
123
+ return res
124
+ .code(result.status || 200)
125
+ .header("X-Content-Type-Options", "nosniff")
126
+ .send(result);
127
+ });
128
+ }
129
+
130
+ static async #renderPartial(url, originalReq, originalRes) {
131
+ try {
132
+ const parsedUrl = new URL(url, `http://${originalReq.headers.host}`);
133
+ const routeMatch = Route.match(parsedUrl.pathname, "GET");
134
+
135
+ if (!routeMatch) {
136
+ return { status: 404, error: "Route not found" };
137
+ }
138
+
139
+ const { handler, params, route } = routeMatch;
140
+
141
+ let viewName = null;
142
+ let viewParams = {};
143
+ let redirectTo = null;
144
+ let handled = false;
145
+
146
+ const mockRes = {
147
+ view: (name, p = {}) => { viewName = name; viewParams = p; return mockRes; },
148
+ redirect: (loc) => { redirectTo = loc; return mockRes; },
149
+ code: (c) => { originalRes.code(c); return mockRes; },
150
+ type: (t) => { originalRes.type(t); return mockRes; },
151
+ send: (d) => { handled = true; originalRes.send(d); return mockRes; },
152
+ header: (k, v) => { originalRes.header(k, v); return mockRes; }
153
+ };
154
+
155
+ const mockReq = {
156
+ ...originalReq,
157
+ url: parsedUrl.pathname,
158
+ query: Object.fromEntries(parsedUrl.searchParams),
159
+ params,
160
+ session: originalReq.session
161
+ };
162
+
163
+ for (const mw of route.middlewares || []) {
164
+ if (redirectTo || handled) break;
165
+ try {
166
+ await mw(mockReq, mockRes);
167
+ } catch (err) {
168
+ if (err.statusCode === 401 || err.statusCode === 403) {
169
+ return { status: err.statusCode, error: err.message };
170
+ }
171
+ throw err;
172
+ }
173
+ }
174
+
175
+ if (handled) return { handled: true };
176
+ if (redirectTo) return this.#validateRedirect(redirectTo, originalReq.headers.host);
177
+
178
+ await handler(mockReq, mockRes);
179
+
180
+ if (handled) return { handled: true };
181
+ if (redirectTo) return this.#validateRedirect(redirectTo, originalReq.headers.host);
182
+ if (!viewName) return { status: 500, error: "No view rendered" };
183
+
184
+ const entry = this.#getEntry("user", viewName);
185
+ if (!entry) return { status: 404, error: "View not found" };
186
+
187
+ const csrf = originalReq.session?.getCsrfToken?.();
188
+ const { html, meta, props } = await this.#render(
189
+ this.#userViews, viewName, viewParams, csrf, originalReq, entry
190
+ );
191
+
192
+ const response = { html, layouts: entry.layouts || [], meta };
193
+
194
+ if (entry.hydrationScript) {
195
+ response.hydrationScript = entry.hydrationScript;
196
+ response.props = props;
197
+ response.runtime = {
198
+ csrf: csrf || "",
199
+ routes: this.#getRoutesForScript(entry.hydrationScript)
200
+ };
201
+ }
202
+
203
+ return response;
204
+ } catch (err) {
205
+ Log.error("SPA Navigate Error", { error: err.message, url });
206
+ return { status: 500, error: this.#isDev ? err.message : "Server error" };
207
+ }
208
+ }
209
+
210
+ static #validateRedirect(location, host) {
211
+ if (location.startsWith("/")) {
212
+ return { redirect: location };
213
+ }
214
+ try {
215
+ if (new URL(location).host === host) {
216
+ return { redirect: location };
217
+ }
218
+ } catch {}
219
+ return { status: 400, error: "Invalid redirect" };
220
+ }
221
+
222
+ static #getRoutesForScript(script) {
223
+ const allRoutes = Route.getClientManifest();
224
+ const usedNames = this.#getUsedRoutes(script);
225
+ const routes = {};
226
+ for (const name of usedNames) {
227
+ if (allRoutes[name]) routes[name] = allRoutes[name];
228
+ }
229
+ return routes;
102
230
  }
103
231
 
104
232
  static #getEntry(namespace, name) {
@@ -128,7 +256,7 @@ class View {
128
256
  return this.#manifest;
129
257
  }
130
258
 
131
- static async #render(baseDir, name, params, csrf = "", fastifyRequest = null) {
259
+ static async #render(baseDir, name, params, csrf = "", fastifyRequest = null, entry = null) {
132
260
  if (!name || typeof name !== "string") {
133
261
  throw new Error("Invalid view name");
134
262
  }
@@ -150,6 +278,19 @@ class View {
150
278
  throw new Error("View must have a default export");
151
279
  }
152
280
 
281
+ const layoutChain = entry?.layouts || [];
282
+ const layoutModules = [];
283
+
284
+ for (const layoutName of layoutChain) {
285
+ const layoutPath = path.join(baseDir, layoutName + ".js");
286
+ if (existsSync(layoutPath)) {
287
+ const layoutMod = await import(pathToFileURL(layoutPath).href + `?t=${Date.now()}`);
288
+ if (layoutMod.default) {
289
+ layoutModules.push(layoutMod);
290
+ }
291
+ }
292
+ }
293
+
153
294
  const nonce = randomBytes(16).toString("hex");
154
295
  const ctx = {
155
296
  nonce,
@@ -184,6 +325,15 @@ class View {
184
325
  element = React.createElement(Component, params);
185
326
  }
186
327
 
328
+ if (layoutModules.length > 0) {
329
+ element = React.createElement("div", { "data-nitron-slot": "page" }, element);
330
+ }
331
+
332
+ for (let i = layoutModules.length - 1; i >= 0; i--) {
333
+ const LayoutComponent = layoutModules[i].default;
334
+ element = React.createElement(LayoutComponent, { children: element });
335
+ }
336
+
187
337
  html = await Context.run(ctx, () => this.#renderToHtml(element));
188
338
  collectedProps = ctx.props;
189
339
  } catch (error) {
@@ -203,14 +353,51 @@ class View {
203
353
  throw error;
204
354
  }
205
355
 
356
+ const mergedMeta = this.#mergeMeta(layoutModules, mod.Meta);
357
+
206
358
  return {
207
359
  html,
208
360
  nonce,
209
- meta: mod.Meta ?? null,
361
+ meta: mergedMeta,
210
362
  props: collectedProps
211
363
  };
212
364
  }
213
365
 
366
+ static #mergeMeta(layoutModules, viewMeta) {
367
+ const result = {};
368
+
369
+ for (const layoutMod of layoutModules) {
370
+ if (layoutMod.Meta) {
371
+ for (const key of Object.keys(layoutMod.Meta)) {
372
+ if (!(key in result)) {
373
+ result[key] = layoutMod.Meta[key];
374
+ }
375
+ }
376
+ }
377
+ }
378
+
379
+ if (viewMeta) {
380
+ for (const key of Object.keys(viewMeta)) {
381
+ if (key === "title" && result.title?.template) {
382
+ const viewTitle = typeof viewMeta.title === "object"
383
+ ? viewMeta.title.default
384
+ : viewMeta.title;
385
+ if (viewTitle) {
386
+ result.title = result.title.template.replace("%s", viewTitle);
387
+ }
388
+ } else {
389
+ result[key] = viewMeta[key];
390
+ }
391
+ }
392
+ }
393
+
394
+ if (result.title && typeof result.title === "object") {
395
+ result.title = result.title.default || "NitronJS";
396
+ }
397
+
398
+ return Object.keys(result).length ? result : null;
399
+ }
400
+
214
401
  static #parseReactError(error) {
215
402
  const result = {
216
403
  cause: null,
@@ -399,6 +586,7 @@ class View {
399
586
  meta: processedMeta,
400
587
  css: cssFiles,
401
588
  hydrationScript: entry?.hydrationScript,
589
+ layouts: entry?.layouts || [],
402
590
  nonce,
403
591
  csrf,
404
592
  props
@@ -453,41 +641,47 @@ class View {
453
641
  scan(jsDir);
454
642
  }
455
643
 
456
- static #generateHtml({ html, meta, css, hydrationScript, nonce, csrf, props }) {
644
+ static #generateHtml({ html, meta, css, hydrationScript, layouts, nonce, csrf, props }) {
457
645
  const nonceAttr = nonce ? ` nonce="${escapeHtml(nonce)}"` : "";
646
+ const hasHydration = !!hydrationScript;
458
647
 
459
- let runtimeScript = "";
460
- if (hydrationScript) {
461
- const allRoutes = Route.getClientManifest();
462
- const usedRouteNames = this.#getUsedRoutes(hydrationScript);
463
- const routes = {};
464
-
465
- for (const routeName of usedRouteNames) {
466
- if (allRoutes[routeName]) {
467
- routes[routeName] = allRoutes[routeName];
468
- }
469
- }
470
-
471
- const propsJson = JSON.stringify(props || {})
472
- .replace(/</g, "\\u003c")
473
- .replace(/>/g, "\\u003e");
648
+ const allRoutes = Route.getClientManifest();
649
+ const usedRouteNames = hasHydration ? this.#getUsedRoutes(hydrationScript) : [];
650
+ const routes = {};
651
+ for (const name of usedRouteNames) {
652
+ if (allRoutes[name]) routes[name] = allRoutes[name];
653
+ }
474
654
 
475
- const runtimeData = JSON.stringify({
476
- csrf: csrf || "",
477
- routes
478
- });
655
+ const runtimeData = {
656
+ csrf: csrf || "",
657
+ routes,
658
+ layouts: layouts || []
659
+ };
479
660
 
480
- runtimeScript = `<script${nonceAttr}>window.__NITRON_RUNTIME__=${runtimeData};window.__NITRON_PROPS__=${propsJson};</script>`;
661
+ let runtimeScript;
662
+ if (hasHydration) {
663
+ const propsJson = JSON.stringify(props || {}).replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
664
+ runtimeScript = `<script${nonceAttr}>window.__NITRON_RUNTIME__=${JSON.stringify(runtimeData)};window.__NITRON_PROPS__=${propsJson};</script>`;
665
+ } else {
666
+ runtimeScript = `<script${nonceAttr}>window.__NITRON_RUNTIME__=${JSON.stringify(runtimeData)};</script>`;
481
667
  }
482
668
 
483
- const vendorScript = hydrationScript
669
+ const vendorScript = hasHydration || this.#isDev
484
670
  ? `<script src="/storage/js/vendor.js"${nonceAttr}></script>`
485
671
  : "";
486
672
 
487
- const hydrateScript = hydrationScript
673
+ const hmrScript = this.#isDev
674
+ ? `<script src="/__nitron_hmr/socket.io.js"${nonceAttr}></script><script src="/storage/js/hmr.js"${nonceAttr}></script>`
675
+ : "";
676
+
677
+ const hydrateScript = hasHydration
488
678
  ? `<script type="module" src="/storage${hydrationScript}"${nonceAttr}></script>`
489
679
  : "";
490
680
 
681
+ const spaScript = `<script${nonceAttr} src="/storage/js/spa.js"></script>`;
682
+
683
+ const devIndicator = this.#isDev ? this.#generateDevIndicator(nonceAttr) : "";
684
+
491
685
  return `<!DOCTYPE html>
492
686
  <html lang="en">
493
687
  <head>
@@ -495,7 +689,8 @@ ${this.#generateHead(meta, css)}
495
689
  </head>
496
690
  <body>
497
691
  <div id="app">${html}</div>
498
- ${runtimeScript}${vendorScript}${hydrateScript}
692
+ ${runtimeScript}
693
+ ${vendorScript}${hmrScript}${hydrateScript}${spaScript}${devIndicator}
499
694
  </body>
500
695
  </html>`;
501
696
  }
@@ -518,27 +713,176 @@ ${runtimeScript}${vendorScript}${hydrateScript}
518
713
  return parts.join("\n");
519
714
  }
520
715
 
716
+ static #generateDevIndicator(nonceAttr) {
717
+ const nodeVersion = process.version;
718
+ const port = Config.get("server.port", 3000);
719
+
720
+ return `
721
+ <div id="__nitron_dev__" style="position:fixed;bottom:16px;left:16px;z-index:999999;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;">
722
+ <style${nonceAttr}>
723
+ #__nitron_dev__ * { box-sizing: border-box; }
724
+ #__nitron_dev_btn__ {
725
+ display:flex;align-items:center;gap:8px;padding:8px 14px;
726
+ background:#0a0a0a;
727
+ color:#fff;border:1px solid #1f1f1f;border-radius:10px;
728
+ cursor:pointer;box-shadow:0 4px 16px rgba(0,0,0,0.5);
729
+ transition:all 0.2s ease;font-size:13px;font-weight:500;
730
+ }
731
+ #__nitron_dev_btn__:hover {
732
+ background:#111;border-color:#2a2a2a;
733
+ box-shadow:0 6px 24px rgba(0,0,0,0.6);
734
+ }
735
+ #__nitron_dev_btn__ .logo {
736
+ width:20px;height:20px;background:#fff;
737
+ border-radius:5px;display:flex;align-items:center;justify-content:center;
738
+ }
739
+ #__nitron_dev_btn__ .logo svg { width:12px;height:12px; }
740
+ #__nitron_dev_btn__ .dot { width:6px;height:6px;border-radius:50%;animation:pulse 2s infinite; }
741
+ @keyframes pulse { 0%,100%{opacity:1;} 50%{opacity:0.4;} }
742
+ #__nitron_dev_panel__ {
743
+ display:none;position:absolute;bottom:48px;left:0;width:260px;
744
+ background:#0a0a0a;
745
+ border:1px solid #1f1f1f;border-radius:12px;
746
+ box-shadow:0 16px 48px rgba(0,0,0,0.6);
747
+ overflow:hidden;animation:slideUp 0.15s ease-out;
748
+ }
749
+ @keyframes slideUp { from{opacity:0;transform:translateY(8px);} to{opacity:1;transform:translateY(0);} }
750
+ #__nitron_dev_panel__ .header {
751
+ padding:14px 16px;background:#111;
752
+ border-bottom:1px solid #1f1f1f;display:flex;align-items:center;gap:10px;
753
+ }
754
+ #__nitron_dev_panel__ .header .icon {
755
+ width:28px;height:28px;background:#fff;
756
+ border-radius:6px;display:flex;align-items:center;justify-content:center;
757
+ }
758
+ #__nitron_dev_panel__ .header .icon svg { width:16px;height:16px; }
759
+ #__nitron_dev_panel__ .header .info { display:flex;flex-direction:column; }
760
+ #__nitron_dev_panel__ .header .title { font-size:14px;font-weight:600;color:#fff;line-height:1.2; }
761
+ #__nitron_dev_panel__ .header .version { font-size:11px;color:#666;line-height:1.2; }
762
+ #__nitron_dev_panel__ .header .badge {
763
+ margin-left:auto;padding:3px 8px;background:#1a1a1a;
764
+ color:#4ade80;font-size:10px;font-weight:600;border-radius:4px;text-transform:uppercase;
765
+ border:1px solid #2a2a2a;
766
+ }
767
+ #__nitron_dev_panel__ .body { padding:12px 16px; }
768
+ #__nitron_dev_panel__ .row {
769
+ display:flex;justify-content:space-between;align-items:center;
770
+ padding:7px 0;border-bottom:1px solid #1a1a1a;
771
+ }
772
+ #__nitron_dev_panel__ .row:last-child { border-bottom:none; }
773
+ #__nitron_dev_panel__ .row .label { color:#666;font-size:12px; }
774
+ #__nitron_dev_panel__ .row .value { color:#999;font-size:12px;font-weight:500;display:flex;align-items:center;gap:6px; }
775
+ #__nitron_dev_panel__ .row .status { width:6px;height:6px;border-radius:50%; }
776
+ #__nitron_dev_panel__ .row .status.ok { background:#4ade80; }
777
+ #__nitron_dev_panel__ .row .status.warn { background:#fbbf24; }
778
+ #__nitron_dev_panel__ .row .status.err { background:#ef4444; }
779
+ #__nitron_dev_panel__ .footer {
780
+ padding:10px 16px;background:#080808;border-top:1px solid #1a1a1a;
781
+ display:flex;gap:6px;
782
+ }
783
+ #__nitron_dev_panel__ .footer a {
784
+ flex:1;padding:6px;background:#111;border:1px solid #1f1f1f;
785
+ border-radius:6px;color:#666;font-size:10px;text-decoration:none;text-align:center;
786
+ transition:all 0.15s;
787
+ }
788
+ #__nitron_dev_panel__ .footer a:hover { background:#1a1a1a;color:#999;border-color:#2a2a2a; }
789
+ </style>
790
+ <button id="__nitron_dev_btn__">
791
+ <span class="logo"><svg viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2.5"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg></span>
792
+ <span style="color:#888;">NitronJS</span>
793
+ <span class="dot" id="__nitron_status_dot__" style="background:#4ade80;"></span>
794
+ </button>
795
+ <div id="__nitron_dev_panel__">
796
+ <div class="header">
797
+ <div class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg></div>
798
+ <div class="info">
799
+ <span class="title">NitronJS</span>
800
+ <span class="version">v${FRAMEWORK_VERSION}</span>
801
+ </div>
802
+ <span class="badge">dev</span>
803
+ </div>
804
+ <div class="body">
805
+ <div class="row"><span class="label">Environment</span><span class="value"><span class="status ok"></span>Development</span></div>
806
+ <div class="row"><span class="label">HMR</span><span class="value"><span class="status" id="__nitron_hmr_ind__"></span><span id="__nitron_hmr_txt__">Checking...</span></span></div>
807
+ <div class="row"><span class="label">Server</span><span class="value">localhost:${port}</span></div>
808
+ <div class="row"><span class="label">Node.js</span><span class="value">${nodeVersion}</span></div>
809
+ <div class="row"><span class="label">React</span><span class="value"><span class="status ok"></span>Ready</span></div>
810
+ </div>
811
+ <div class="footer">
812
+ <a href="/__nitron/routes" target="_blank">Routes</a>
813
+ <a href="/__nitron/info" target="_blank">Info</a>
814
+ <a href="https://nitronjs.dev/docs" target="_blank">Docs</a>
815
+ </div>
816
+ </div>
817
+ </div>
818
+ <script${nonceAttr}>
819
+ (function(){
820
+ var btn=document.getElementById('__nitron_dev_btn__');
821
+ var panel=document.getElementById('__nitron_dev_panel__');
822
+ var dot=document.getElementById('__nitron_status_dot__');
823
+ var hmrInd=document.getElementById('__nitron_hmr_ind__');
824
+ var hmrTxt=document.getElementById('__nitron_hmr_txt__');
825
+ btn.onclick=function(e){e.stopPropagation();panel.style.display=panel.style.display==='none'?'block':'none';};
826
+ document.addEventListener('click',function(e){if(!e.target.closest('#__nitron_dev__'))panel.style.display='none';});
827
+ function updateHmr(){
828
+ var connected=window.__nitron_hmr_connected__;
829
+ hmrInd.className='status '+(connected?'ok':'err');
830
+ hmrTxt.textContent=connected?'Connected':'Disconnected';
831
+ dot.style.background=connected?'#4ade80':'#ef4444';
832
+ }
833
+ if(typeof io!=='undefined'){setInterval(updateHmr,1000);updateHmr();}
834
+ else{hmrInd.className='status warn';hmrTxt.textContent='Not Available';}
835
+ })();
836
+ </script>`;
837
+ }
838
+
521
839
  static #setSecurityHeaders(res, nonce) {
522
840
  const connectSrc = this.#isDev ? "'self' ws: wss:" : "'self'";
523
-
524
- // Get CSP whitelist from config
525
841
  const cspConfig = Config.get("app.csp", {});
526
- const whitelist = {
527
- styles: cspConfig.styles || [],
528
- fonts: cspConfig.fonts || [],
529
- images: cspConfig.images || [],
530
- scripts: cspConfig.scripts || [],
531
- connect: cspConfig.connect || [],
532
- frames: cspConfig.frames || [],
842
+
843
+ // Support for simple URL array format
844
+ // Example: csp: ["https://fonts.googleapis.com", "https://fonts.gstatic.com"]
845
+ // Or detailed format: csp: { styles: [...], fonts: [...] }
846
+ const isSimpleFormat = Array.isArray(cspConfig);
847
+
848
+ let whitelist;
849
+ if (isSimpleFormat) {
850
+ // Simple format: apply URLs to all relevant directives
851
+ const urls = cspConfig.filter(u => u !== "*");
852
+ const hasWildcard = cspConfig.includes("*");
853
+ whitelist = {
854
+ styles: hasWildcard ? ["*"] : urls,
855
+ fonts: hasWildcard ? ["*"] : urls,
856
+ images: hasWildcard ? ["*"] : urls,
857
+ scripts: hasWildcard ? ["*"] : urls,
858
+ connect: hasWildcard ? ["*"] : urls,
859
+ frames: hasWildcard ? ["*"] : [],
860
+ };
861
+ } else {
862
+ whitelist = {
863
+ styles: cspConfig.styles || [],
864
+ fonts: cspConfig.fonts || [],
865
+ images: cspConfig.images || [],
866
+ scripts: cspConfig.scripts || [],
867
+ connect: cspConfig.connect || [],
868
+ frames: cspConfig.frames || [],
869
+ };
870
+ }
871
+
872
+ // Build CSP directives with wildcard support
873
+ const buildSrc = (defaults, extra) => {
874
+ if (extra.includes("*")) return "*";
875
+ return [...defaults, ...extra].join(" ");
533
876
  };
534
877
 
535
- // Build CSP directives with whitelist
536
- const styleSrc = ["'self'", "'unsafe-inline'", ...whitelist.styles].join(" ");
537
- const fontSrc = ["'self'", ...whitelist.fonts].join(" ");
538
- const imgSrc = ["'self'", "data:", "blob:", ...whitelist.images].join(" ");
539
- const scriptSrc = ["'self'", `'nonce-${nonce}'`, ...whitelist.scripts].join(" ");
540
- const connectSrcFinal = [connectSrc, ...whitelist.connect].join(" ");
541
- const frameSrc = whitelist.frames.length ? whitelist.frames.join(" ") : "'none'";
878
+ const styleSrc = buildSrc(["'self'", "'unsafe-inline'"], whitelist.styles);
879
+ const fontSrc = buildSrc(["'self'"], whitelist.fonts);
880
+ const imgSrc = buildSrc(["'self'", "data:", "blob:"], whitelist.images);
881
+ const scriptSrc = buildSrc(["'self'", `'nonce-${nonce}'`], whitelist.scripts);
882
+ const connectSrcFinal = buildSrc([connectSrc], whitelist.connect);
883
+ const frameSrc = whitelist.frames.length
884
+ ? (whitelist.frames.includes("*") ? "*" : whitelist.frames.join(" "))
885
+ : "'none'";
542
886
 
543
887
  const csp = [
544
888
  "default-src 'self'",
package/lib/index.d.ts CHANGED
@@ -405,3 +405,58 @@ export const Paths: any;
405
405
  export const Environment: any;
406
406
 
407
407
  export function start(): Promise<void>;
408
+
409
+ // ─────────────────────────────────────────────────────────────────────────────
410
+ // Global Functions (available in all views without import)
411
+ // ─────────────────────────────────────────────────────────────────────────────
412
+
413
+ declare global {
414
+ /**
415
+ * Returns the CSRF token for the current request.
416
+ * Works on both SSR and client-side.
417
+ *
418
+ * @example
419
+ * <input type="hidden" name="_csrf" value={csrf()} />
420
+ * fetch('/api', { headers: { 'X-CSRF-Token': csrf() } })
421
+ */
422
+ function csrf(): string;
423
+
424
+ /**
425
+ * Returns the URL for a named route.
426
+ * Works on both SSR and client-side.
427
+ *
428
+ * @param name - The route name as defined in routes/web.js
429
+ * @param params - Optional route parameters
430
+ * @example
431
+ * <a href={route('home')}>Home</a>
432
+ * <a href={route('user.edit', { id: 1 })}>Edit</a>
433
+ */
434
+ function route(name: string, params?: Record<string, any>): string;
435
+
436
+ /**
437
+ * Returns the current request object.
438
+ * Only works in Server Components during SSR.
439
+ *
440
+ * @example
441
+ * const tab = request().query.tab || 'general';
442
+ * const id = request().params.id;
443
+ */
444
+ function request(): {
445
+ path: string;
446
+ method: string;
447
+ query: Record<string, any>;
448
+ params: Record<string, any>;
449
+ body: Record<string, any>;
450
+ headers: Record<string, string>;
451
+ cookies: Record<string, string>;
452
+ ip: string;
453
+ isAjax: boolean;
454
+ session: {
455
+ get<T = any>(key: string, defaultValue?: T): T;
456
+ set(key: string, value: any): void;
457
+ has(key: string): boolean;
458
+ forget(key: string): void;
459
+ flash(key: string, value: any): void;
460
+ };
461
+ };
462
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitronjs/framework",
3
- "version": "0.1.24",
3
+ "version": "0.2.0",
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"
@@ -34,6 +34,7 @@
34
34
  "postcss": "^8.5.6",
35
35
  "react": "^19.2.3",
36
36
  "react-dom": "^19.2.3",
37
+ "react-refresh": "^0.18.0",
37
38
  "socket.io": "^4.8.1",
38
39
  "tailwindcss": "^4.1.18",
39
40
  "typescript": "^5.9.3"
@@ -2,8 +2,6 @@ APP_NAME=nitronjs
2
2
  APP_KEY=
3
3
  APP_URL=http://localhost
4
4
  APP_PORT=3000
5
- APP_DEV=true
6
- APP_DEBUG=true
7
5
 
8
6
  FILESYSTEM_DRIVER=disk
9
7
 
@@ -1,9 +1,33 @@
1
1
  class HomeController {
2
- static async index(request, reply) {
3
- return reply.view("Site/Home", {
4
- name: "NitronJS"
2
+
3
+ static async index(req, res) {
4
+ return res.view("Site/Home", {
5
+ title: "Welcome to NitronJS",
6
+ features: [
7
+ {
8
+ icon: "⚡",
9
+ title: "Lightning Fast",
10
+ description: "Server-side rendering with React for blazing fast page loads"
11
+ },
12
+ {
13
+ icon: "🔥",
14
+ title: "Hot Module Reload",
15
+ description: "See your changes instantly without losing application state"
16
+ },
17
+ {
18
+ icon: "🛡️",
19
+ title: "Secure by Default",
20
+ description: "Built-in CSRF protection, secure sessions, and CSP headers"
21
+ },
22
+ {
23
+ icon: "📦",
24
+ title: "MVC Architecture",
25
+ description: "Clean separation of concerns with Models, Views, and Controllers"
26
+ }
27
+ ]
5
28
  });
6
29
  }
30
+
7
31
  }
8
32
 
9
33
  export default HomeController;