@nitronjs/framework 0.1.23 → 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 -936
- 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 +15 -16
- 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/Runtime/Entry.js
CHANGED
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
import Server from "../Http/Server.js";
|
|
2
|
+
import HMRServer from "../HMR/Server.js";
|
|
3
|
+
import Environment from "../Core/Environment.js";
|
|
4
|
+
|
|
5
|
+
// Set development mode based on __NITRON_DEV__ env (set by DevCommand)
|
|
6
|
+
Environment.setDev(process.env.__NITRON_DEV__ === "true");
|
|
2
7
|
|
|
3
8
|
export async function start() {
|
|
4
9
|
await Server.start();
|
|
10
|
+
|
|
11
|
+
if (process.send) {
|
|
12
|
+
process.on("message", (msg) => {
|
|
13
|
+
if (!msg?.type || !HMRServer.isReady) return;
|
|
14
|
+
|
|
15
|
+
switch (msg.type) {
|
|
16
|
+
case "view":
|
|
17
|
+
HMRServer.emitViewUpdate(msg.filePath);
|
|
18
|
+
break;
|
|
19
|
+
case "css":
|
|
20
|
+
HMRServer.emitCss(msg.filePath);
|
|
21
|
+
break;
|
|
22
|
+
case "reload":
|
|
23
|
+
HMRServer.emitReload(msg.reason);
|
|
24
|
+
break;
|
|
25
|
+
case "error":
|
|
26
|
+
HMRServer.emitError(msg.message);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
5
31
|
}
|
|
6
32
|
|
|
7
|
-
// Auto-start when run directly
|
|
8
33
|
const isMain = process.argv[1]?.endsWith("Entry.js");
|
|
9
34
|
if (isMain) {
|
|
10
35
|
start();
|
package/lib/Session/File.js
CHANGED
|
@@ -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
|
|
109
|
-
if (!sessionId
|
|
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
|
|
157
|
-
if (!sessionId
|
|
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 =
|
|
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
|
|
233
|
-
if (!sessionId
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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;
|