@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,7 @@
1
1
  import fs from "fs/promises";
2
2
  import path from "path";
3
3
  import Paths from "../Core/Paths.js";
4
+ import Environment from "../Core/Environment.js";
4
5
 
5
6
  /**
6
7
  * File Session Store
@@ -83,6 +84,16 @@ class File {
83
84
  }
84
85
  }
85
86
 
87
+ /**
88
+ * Validate session ID format (hex only, max 128 chars)
89
+ * Prevents path traversal attacks
90
+ */
91
+ #isValidSessionId(sessionId) {
92
+ if (!sessionId || typeof sessionId !== 'string') return false;
93
+ if (sessionId.length > 128) return false;
94
+ return /^[a-f0-9]+$/i.test(sessionId);
95
+ }
96
+
86
97
  /**
87
98
  * Get session file path
88
99
  */
@@ -105,8 +116,8 @@ class File {
105
116
  async get (sessionId) {
106
117
  await this.ready;
107
118
 
108
- // Guard: Validate session ID length
109
- if (!sessionId || sessionId.length > 128) {
119
+ // Guard: Validate session ID format (hex only)
120
+ if (!this.#isValidSessionId(sessionId)) {
110
121
  return null;
111
122
  }
112
123
 
@@ -153,14 +164,14 @@ class File {
153
164
  async set (sessionId, sessionData) {
154
165
  await this.ready;
155
166
 
156
- // Guard: Validate session ID length
157
- if (!sessionId || sessionId.length > 128) {
167
+ // Guard: Validate session ID format (hex only)
168
+ if (!this.#isValidSessionId(sessionId)) {
158
169
  return;
159
170
  }
160
171
 
161
172
  const filePath = this.#getFilePath(sessionId);
162
173
  const tempPath = this.#getTempFilePath(sessionId);
163
- const jsonContent = process.env.APP_DEV === "true"
174
+ const jsonContent = Environment.isDev
164
175
  ? JSON.stringify(sessionData, null, 2)
165
176
  : JSON.stringify(sessionData);
166
177
 
@@ -229,8 +240,8 @@ class File {
229
240
  async delete(sessionId) {
230
241
  await this.ready;
231
242
 
232
- // Guard: Validate session ID length
233
- if (!sessionId || sessionId.length > 128) {
243
+ // Guard: Validate session ID format (hex only)
244
+ if (!this.#isValidSessionId(sessionId)) {
234
245
  return;
235
246
  }
236
247
 
@@ -0,0 +1,166 @@
1
+ (function() {
2
+ "use strict";
3
+
4
+ var socket = null;
5
+ var overlay = null;
6
+
7
+ function connect() {
8
+ if (socket) return;
9
+ if (typeof io === "undefined") {
10
+ setTimeout(connect, 100);
11
+ return;
12
+ }
13
+
14
+ try {
15
+ socket = io({ path: "/__nitron_hmr", transports: ["websocket", "polling"], reconnection: true });
16
+ }
17
+ catch (e) {
18
+ return;
19
+ }
20
+
21
+ socket.on("connect", function() {
22
+ window.__nitron_hmr_connected__ = true;
23
+ hideOverlay();
24
+ });
25
+
26
+ socket.on("disconnect", function() {
27
+ window.__nitron_hmr_connected__ = false;
28
+ });
29
+
30
+ socket.on("hmr:update", function(data) {
31
+ refetchPage();
32
+ });
33
+
34
+ socket.on("hmr:reload", function(data) {
35
+ location.reload();
36
+ });
37
+
38
+ socket.on("hmr:css", function(data) {
39
+ updateCss(data.file);
40
+ });
41
+
42
+ socket.on("hmr:error", function(data) {
43
+ showOverlay(data.message || "Unknown error", data.file);
44
+ });
45
+ }
46
+
47
+ function refetchPage() {
48
+ var url = location.pathname + location.search;
49
+
50
+ fetch("/__nitron/navigate?url=" + encodeURIComponent(url), {
51
+ headers: { "X-Nitron-SPA": "1" },
52
+ credentials: "same-origin"
53
+ })
54
+ .then(function(r) {
55
+ if (!r.ok) throw new Error("HTTP " + r.status);
56
+ return r.json();
57
+ })
58
+ .then(function(d) {
59
+ if (d.error || d.redirect) {
60
+ location.reload();
61
+ return;
62
+ }
63
+
64
+ // Find current slot
65
+ var slot = document.querySelector("[data-nitron-slot='page']");
66
+ if (!slot || !d.html) {
67
+ location.reload();
68
+ return;
69
+ }
70
+
71
+ // Parse response HTML and extract slot content
72
+ var tmp = document.createElement("div");
73
+ tmp.innerHTML = d.html;
74
+ var newSlot = tmp.querySelector("[data-nitron-slot='page']");
75
+ var newContent = newSlot ? newSlot.innerHTML : d.html;
76
+
77
+ // Update slot content
78
+ slot.innerHTML = newContent;
79
+
80
+ // Update hydration
81
+ if (d.hydrationScript) {
82
+ if (d.runtime && window.__NITRON_RUNTIME__) {
83
+ Object.assign(window.__NITRON_RUNTIME__, d.runtime);
84
+ }
85
+ if (d.props) {
86
+ window.__NITRON_PROPS__ = d.props;
87
+ }
88
+ loadHydration(d.hydrationScript);
89
+ }
90
+ })
91
+ .catch(function(e) {
92
+ location.reload();
93
+ });
94
+ }
95
+
96
+ function loadHydration(scriptPath) {
97
+ var script = document.createElement("script");
98
+ script.type = "module";
99
+ script.src = "/storage" + scriptPath + "?t=" + Date.now();
100
+ script.onload = function() {
101
+ if (window.__NITRON_REFRESH__) {
102
+ try {
103
+ window.__NITRON_REFRESH__.performReactRefresh();
104
+ } catch(e) {}
105
+ }
106
+ script.remove();
107
+ };
108
+ document.head.appendChild(script);
109
+ }
110
+
111
+ function updateCss(file) {
112
+ var links = document.querySelectorAll('link[rel="stylesheet"]');
113
+ var timestamp = Date.now();
114
+
115
+ if (!file) {
116
+ for (var i = 0; i < links.length; i++) {
117
+ var href = (links[i].href || "").split("?")[0];
118
+ links[i].href = href + "?t=" + timestamp;
119
+ }
120
+ return;
121
+ }
122
+
123
+ var found = false;
124
+ var target = file.split(/[\\/]/).pop();
125
+
126
+ for (var i = 0; i < links.length; i++) {
127
+ var href = (links[i].href || "").split("?")[0];
128
+ if (href.endsWith("/" + target)) {
129
+ links[i].href = href + "?t=" + timestamp;
130
+ found = true;
131
+ }
132
+ }
133
+
134
+ if (!found) location.reload();
135
+ }
136
+
137
+ function showOverlay(msg, file) {
138
+ hideOverlay();
139
+ overlay = document.createElement("div");
140
+ overlay.id = "__nitron_error__";
141
+ overlay.innerHTML =
142
+ '<div style="position:fixed;inset:0;background:rgba(0,0,0,.95);color:#ff4444;padding:32px;font-family:monospace;z-index:999999;overflow:auto">' +
143
+ '<div style="font-size:24px;font-weight:bold;margin-bottom:16px">Build Error</div>' +
144
+ '<div style="color:#888;margin-bottom:16px">' + escapeHtml(file || "") + '</div>' +
145
+ '<pre style="white-space:pre-wrap;background:#1a1a2e;padding:16px;border-radius:8px">' + escapeHtml(msg) + '</pre>' +
146
+ '<button onclick="this.parentNode.parentNode.remove()" style="position:absolute;top:16px;right:16px;background:#333;color:#fff;border:none;padding:8px 16px;cursor:pointer;border-radius:4px">Close</button>' +
147
+ '</div>';
148
+ document.body.appendChild(overlay);
149
+ }
150
+
151
+ function hideOverlay() {
152
+ var el = document.getElementById("__nitron_error__");
153
+ if (el) el.remove();
154
+ overlay = null;
155
+ }
156
+
157
+ function escapeHtml(str) {
158
+ return String(str || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
159
+ }
160
+
161
+ if (document.readyState === "loading") {
162
+ document.addEventListener("DOMContentLoaded", connect);
163
+ } else {
164
+ connect();
165
+ }
166
+ })();
@@ -0,0 +1,142 @@
1
+ (function() {
2
+ "use strict";
3
+
4
+ var ENDPOINT = "/__nitron/navigate";
5
+ var SKIP = /^(\/storage|\/api|\/__|#|javascript:|mailto:|tel:)/;
6
+ var layouts = [];
7
+ var navigating = false;
8
+
9
+ function init() {
10
+ var rt = window.__NITRON_RUNTIME__;
11
+ if (rt && rt.layouts) layouts = rt.layouts;
12
+ document.addEventListener("click", onClick, true);
13
+ window.addEventListener("popstate", onPopState);
14
+ }
15
+
16
+ function onClick(e) {
17
+ if (navigating) return;
18
+ var link = e.target.closest("a");
19
+ if (!link) return;
20
+ var href = link.getAttribute("href");
21
+ if (!href || link.target === "_blank" || link.download) return;
22
+ if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
23
+ if (!isLocal(href)) return;
24
+ e.preventDefault();
25
+ navigate(href, true);
26
+ }
27
+
28
+ function onPopState() {
29
+ navigate(location.pathname + location.search, false);
30
+ }
31
+
32
+ function isLocal(href) {
33
+ if (!href || SKIP.test(href)) return false;
34
+ if (/^https?:\/\/|^\/\//.test(href)) {
35
+ try { return new URL(href, location.origin).origin === location.origin; }
36
+ catch (e) { return false; }
37
+ }
38
+ return true;
39
+ }
40
+
41
+ function navigate(url, push) {
42
+ if (navigating) return;
43
+ navigating = true;
44
+
45
+ var ctrl = typeof AbortController !== "undefined" ? new AbortController() : null;
46
+ var timer = setTimeout(function() { if (ctrl) ctrl.abort(); fallback(); }, 10000);
47
+
48
+ fetch(ENDPOINT + "?url=" + encodeURIComponent(url), {
49
+ headers: { "X-Nitron-SPA": "1" },
50
+ credentials: "same-origin",
51
+ signal: ctrl ? ctrl.signal : undefined
52
+ })
53
+ .then(function(r) {
54
+ clearTimeout(timer);
55
+ if (!r.ok) throw new Error("HTTP " + r.status);
56
+ return r.json();
57
+ })
58
+ .then(function(d) {
59
+ if (d.error) { fallback(); return; }
60
+ if (d.redirect) {
61
+ navigating = false;
62
+ isLocal(d.redirect) ? navigate(d.redirect, push) : (location.href = d.redirect);
63
+ return;
64
+ }
65
+
66
+ if (!layouts.length || !arrEq(layouts, d.layouts || [])) {
67
+ fallback();
68
+ return;
69
+ }
70
+
71
+ updateSlot(d.html);
72
+ layouts = d.layouts || [];
73
+ if (window.__NITRON_RUNTIME__) window.__NITRON_RUNTIME__.layouts = layouts;
74
+
75
+ if (d.hydrationScript) {
76
+ if (d.runtime) Object.assign(window.__NITRON_RUNTIME__ || {}, d.runtime);
77
+ if (d.props) window.__NITRON_PROPS__ = d.props;
78
+ loadScript("/storage" + d.hydrationScript + "?t=" + Date.now(), true);
79
+ }
80
+
81
+ if (push) history.pushState({ nitron: 1 }, "", url);
82
+ if (d.meta) {
83
+ if (d.meta.title) document.title = d.meta.title;
84
+ setMeta("description", d.meta.description);
85
+ }
86
+
87
+ scrollTo(0, 0);
88
+ dispatchEvent(new CustomEvent("nitron:navigate", { detail: { url: url } }));
89
+ navigating = false;
90
+ })
91
+ .catch(function() {
92
+ clearTimeout(timer);
93
+ fallback();
94
+ });
95
+
96
+ function fallback() {
97
+ navigating = false;
98
+ location.href = url;
99
+ }
100
+ }
101
+
102
+ function arrEq(a, b) {
103
+ if (!a || !b || a.length !== b.length) return false;
104
+ for (var i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
105
+ return true;
106
+ }
107
+
108
+ function updateSlot(html) {
109
+ var slot = document.querySelector("[data-nitron-slot='page']");
110
+ if (!slot) {
111
+ location.reload();
112
+ return;
113
+ }
114
+ var tmp = document.createElement("div");
115
+ tmp.innerHTML = html;
116
+ var inner = tmp.querySelector("[data-nitron-slot='page']");
117
+ slot.innerHTML = inner ? inner.innerHTML : html;
118
+ }
119
+
120
+ function loadScript(src, isModule) {
121
+ var s = document.createElement("script");
122
+ if (isModule) s.type = "module";
123
+ s.src = src;
124
+ document.body.appendChild(s);
125
+ }
126
+
127
+ function setMeta(name, val) {
128
+ if (!val) return;
129
+ var m = document.querySelector('meta[name="' + name + '"]');
130
+ if (m) m.content = val;
131
+ else {
132
+ m = document.createElement("meta");
133
+ m.name = name;
134
+ m.content = val;
135
+ document.head.appendChild(m);
136
+ }
137
+ }
138
+
139
+ document.readyState === "loading"
140
+ ? document.addEventListener("DOMContentLoaded", init)
141
+ : init();
142
+ })();
@@ -0,0 +1,94 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import Environment from "../Core/Environment.js";
4
+
5
+ const LAYOUT_NAMES = new Set([
6
+ "layout.tsx",
7
+ "theme.tsx",
8
+ "shell.tsx",
9
+ "Layout.tsx",
10
+ "Theme.tsx",
11
+ "Shell.tsx"
12
+ ]);
13
+
14
+ class Layout {
15
+ static #cache = new Map();
16
+
17
+ static get #isDev() {
18
+ return Environment.isDev;
19
+ }
20
+
21
+ static resolve(viewPath, viewsRoot) {
22
+ const cacheKey = `${viewsRoot}:${viewPath}`;
23
+
24
+ if (!this.#isDev && this.#cache.has(cacheKey)) {
25
+ return this.#cache.get(cacheKey);
26
+ }
27
+
28
+ const layouts = [];
29
+ let currentDir = path.dirname(path.join(viewsRoot, viewPath));
30
+ const normalizedRoot = path.normalize(viewsRoot) + path.sep;
31
+
32
+ while (currentDir.startsWith(normalizedRoot) || currentDir === path.normalize(viewsRoot)) {
33
+ const layout = this.#findLayoutInDir(currentDir);
34
+
35
+ if (layout) {
36
+ layouts.push(layout);
37
+ }
38
+
39
+ if (currentDir === path.normalize(viewsRoot)) {
40
+ break;
41
+ }
42
+
43
+ currentDir = path.dirname(currentDir);
44
+ }
45
+
46
+ layouts.reverse();
47
+
48
+ const result = layouts.map(layoutPath => {
49
+ const relative = path.relative(viewsRoot, layoutPath)
50
+ .replace(/\.tsx$/, "")
51
+ .replace(/\\/g, "/");
52
+ return {
53
+ path: layoutPath,
54
+ name: relative
55
+ };
56
+ });
57
+
58
+ this.#cache.set(cacheKey, result);
59
+ return result;
60
+ }
61
+
62
+ static #findLayoutInDir(dir) {
63
+ if (!fs.existsSync(dir)) {
64
+ return null;
65
+ }
66
+
67
+ let found = null;
68
+ const entries = fs.readdirSync(dir);
69
+
70
+ for (const entry of entries) {
71
+ if (LAYOUT_NAMES.has(entry)) {
72
+ if (found) {
73
+ throw new Error(
74
+ `Multiple layouts in ${dir}: ${path.basename(found)} and ${entry}`
75
+ );
76
+ }
77
+ found = path.join(dir, entry);
78
+ }
79
+ }
80
+
81
+ return found;
82
+ }
83
+
84
+ static isLayout(filePath) {
85
+ const basename = path.basename(filePath);
86
+ return LAYOUT_NAMES.has(basename);
87
+ }
88
+
89
+ static clearCache() {
90
+ this.#cache.clear();
91
+ }
92
+ }
93
+
94
+ export default Layout;