@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.
- package/lib/Build/CssBuilder.js +129 -0
- package/lib/Build/FileAnalyzer.js +395 -0
- package/lib/Build/HydrationBuilder.js +173 -0
- package/lib/Build/Manager.js +290 -943
- package/lib/Build/colors.js +10 -0
- package/lib/Build/jsxRuntime.js +116 -0
- package/lib/Build/plugins.js +264 -0
- package/lib/Console/Commands/BuildCommand.js +6 -5
- package/lib/Console/Commands/DevCommand.js +151 -311
- package/lib/Console/Stubs/page-hydration-dev.tsx +72 -0
- package/lib/Console/Stubs/page-hydration.tsx +9 -10
- package/lib/Console/Stubs/vendor-dev.tsx +50 -0
- package/lib/Core/Environment.js +29 -2
- package/lib/Core/Paths.js +12 -4
- package/lib/Database/Drivers/MySQLDriver.js +5 -4
- package/lib/Database/QueryBuilder.js +2 -3
- package/lib/Filesystem/Manager.js +32 -7
- package/lib/HMR/Server.js +87 -0
- package/lib/Http/Server.js +9 -5
- package/lib/Logging/Manager.js +68 -18
- package/lib/Route/Loader.js +3 -4
- package/lib/Route/Manager.js +24 -3
- package/lib/Runtime/Entry.js +26 -1
- package/lib/Session/File.js +18 -7
- package/lib/View/Client/hmr-client.js +166 -0
- package/lib/View/Client/spa.js +142 -0
- package/lib/View/Layout.js +94 -0
- package/lib/View/Manager.js +390 -46
- package/lib/index.d.ts +55 -0
- package/package.json +2 -1
- package/skeleton/.env.example +0 -2
- package/skeleton/app/Controllers/HomeController.js +27 -3
- package/skeleton/config/app.js +15 -14
- package/skeleton/config/session.js +1 -1
- package/skeleton/globals.d.ts +3 -63
- package/skeleton/resources/views/Site/Home.tsx +274 -50
- package/skeleton/tsconfig.json +5 -1
package/lib/View/Manager.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
655
|
+
const runtimeData = {
|
|
656
|
+
csrf: csrf || "",
|
|
657
|
+
routes,
|
|
658
|
+
layouts: layouts || []
|
|
659
|
+
};
|
|
479
660
|
|
|
480
|
-
|
|
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 =
|
|
669
|
+
const vendorScript = hasHydration || this.#isDev
|
|
484
670
|
? `<script src="/storage/js/vendor.js"${nonceAttr}></script>`
|
|
485
671
|
: "";
|
|
486
672
|
|
|
487
|
-
const
|
|
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}
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
536
|
-
const
|
|
537
|
-
const
|
|
538
|
-
const
|
|
539
|
-
const
|
|
540
|
-
const
|
|
541
|
-
|
|
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.
|
|
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"
|
package/skeleton/.env.example
CHANGED
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
class HomeController {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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;
|