nitro5 1.0.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/src/static.js ADDED
@@ -0,0 +1,201 @@
1
+ import fs from "node:fs";
2
+ import fsp from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { getMimeType } from "./mime.js";
5
+ import { matchesCacheRule } from "./cache.js";
6
+ import { bundleTS } from "./tsc.js";
7
+
8
+ /**
9
+ * Safe path resolver (anti directory traversal)
10
+ */
11
+ function safeResolve(root, requestPath) {
12
+ const clean = requestPath.startsWith("/")
13
+ ? requestPath
14
+ : `/${requestPath}`;
15
+
16
+ const normalized = path.normalize(clean).replace(/^(\.\.[/\\])+/, "");
17
+ const abs = path.resolve(root, `.${normalized}`);
18
+ const rootAbs = path.resolve(root);
19
+
20
+ if (!abs.startsWith(rootAbs)) return null;
21
+ return abs;
22
+ }
23
+
24
+ function getMTimeKey(absPath) {
25
+ const stat = fs.statSync(absPath);
26
+ return `${absPath}:${stat.mtimeMs}`;
27
+ }
28
+
29
+ function injectHMRClient(html, hmrPort = 3001) {
30
+ const client = `
31
+ <script>
32
+ (() => {
33
+ try {
34
+ const ws = new WebSocket("ws://" + location.hostname + ":" + ${hmrPort});
35
+
36
+ ws.onmessage = async (event) => {
37
+ let msg;
38
+ try {
39
+ msg = JSON.parse(event.data);
40
+ } catch {
41
+ return;
42
+ }
43
+
44
+ if (msg.type === "hmr:update" && typeof msg.code === "string") {
45
+ const blob = new Blob([msg.code], { type: "text/javascript" });
46
+ const url = URL.createObjectURL(blob) + "?t=" + Date.now();
47
+
48
+ try {
49
+ await import(url);
50
+ } catch (err) {
51
+ console.error("HMR import failed:", err);
52
+ location.reload();
53
+ }
54
+ }
55
+ };
56
+
57
+ ws.onclose = () => {
58
+ console.warn("HMR socket closed");
59
+ };
60
+ } catch (err) {
61
+ console.error("HMR client error:", err);
62
+ }
63
+ })();
64
+ </script>
65
+ `;
66
+
67
+ if (html.includes("</body>")) {
68
+ return html.replace("</body>", `${client}</body>`);
69
+ }
70
+
71
+ return html + client;
72
+ }
73
+
74
+ /**
75
+ * Static server factory
76
+ */
77
+ export function createStaticServer({
78
+ publicDir,
79
+ cache,
80
+ cacheConfig,
81
+ filePool,
82
+ vite,
83
+ hmrPort = 3001
84
+ }) {
85
+ return async function serveStatic(req, res) {
86
+ let requestPath = req.pathname || "/";
87
+ requestPath = requestPath.split("?")[0];
88
+
89
+ if (requestPath === "/") {
90
+ requestPath = "/index.html";
91
+ }
92
+
93
+ const absPath = safeResolve(publicDir, requestPath);
94
+ if (!absPath) return false;
95
+
96
+ if (!fs.existsSync(absPath)) return false;
97
+ if (!fs.statSync(absPath).isFile()) return false;
98
+
99
+ const ext = path.extname(absPath).toLowerCase();
100
+ const cacheable =
101
+ cacheConfig?.enabled &&
102
+ matchesCacheRule(absPath, cacheConfig.files);
103
+
104
+ const cacheKey = cacheable ? getMTimeKey(absPath) : null;
105
+
106
+ /**
107
+ * =========================
108
+ * CACHE HIT
109
+ * =========================
110
+ */
111
+ if (cacheable && cacheKey) {
112
+ const cached = cache.get(cacheKey);
113
+ if (cached) {
114
+ res.binary(cached.buffer, 200, cached.contentType);
115
+ return true;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * =========================
121
+ * HTML (VITE STYLE TRANSFORM)
122
+ * =========================
123
+ */
124
+ if (ext === ".html") {
125
+ const html = await fsp.readFile(absPath, "utf8");
126
+
127
+ let transformed = html;
128
+
129
+ if (vite?.transformIndexHtml) {
130
+ transformed = await vite.transformIndexHtml(
131
+ req.fullPath || req.pathname || "/",
132
+ html
133
+ );
134
+ }
135
+
136
+ transformed = injectHMRClient(transformed, hmrPort);
137
+
138
+ const buffer = Buffer.from(transformed);
139
+ const contentType = "text/html; charset=utf-8";
140
+
141
+ if (cacheable && cacheKey) {
142
+ cache.set(cacheKey, { buffer, contentType }, cacheConfig.ttl);
143
+ }
144
+
145
+ res.binary(buffer, 200, contentType);
146
+ return true;
147
+ }
148
+
149
+ /**
150
+ * =========================
151
+ * TYPESCRIPT SUPPORT
152
+ * =========================
153
+ */
154
+ if (ext === ".ts" || ext === ".tsx") {
155
+ try {
156
+ const code = await bundleTS(absPath);
157
+
158
+ if (!code) {
159
+ throw new Error("TS bundle returned empty output");
160
+ }
161
+
162
+ const buffer = Buffer.from(code);
163
+ const contentType = "application/javascript; charset=utf-8";
164
+
165
+ if (cacheable && cacheKey) {
166
+ cache.set(cacheKey, { buffer, contentType }, cacheConfig.ttl);
167
+ }
168
+
169
+ res.binary(buffer, 200, contentType);
170
+ return true;
171
+ } catch (err) {
172
+ console.error("TS BUNDLE ERROR:", err);
173
+
174
+ res.send(
175
+ `console.error("TS build failed: ${err.message}")`,
176
+ 500,
177
+ "application/javascript"
178
+ );
179
+ return true;
180
+ }
181
+ }
182
+
183
+ /**
184
+ * =========================
185
+ * NORMAL FILES
186
+ * =========================
187
+ */
188
+ const contentType = getMimeType(absPath);
189
+
190
+ const buffer = filePool
191
+ ? await filePool.run(absPath)
192
+ : await fsp.readFile(absPath);
193
+
194
+ if (cacheable && cacheKey) {
195
+ cache.set(cacheKey, { buffer, contentType }, cacheConfig.ttl);
196
+ }
197
+
198
+ res.binary(buffer, 200, contentType);
199
+ return true;
200
+ };
201
+ }
package/src/stats.js ADDED
@@ -0,0 +1,25 @@
1
+ const stats = {
2
+ totalRequests: 0,
3
+ totalErrors: 0,
4
+ activeConnections: 0
5
+ };
6
+
7
+ export function incRequest() {
8
+ stats.totalRequests++;
9
+ }
10
+
11
+ export function incError() {
12
+ stats.totalErrors++;
13
+ }
14
+
15
+ export function incConn() {
16
+ stats.activeConnections++;
17
+ }
18
+
19
+ export function decConn() {
20
+ stats.activeConnections--;
21
+ }
22
+
23
+ export function getStats() {
24
+ return stats;
25
+ }
@@ -0,0 +1,249 @@
1
+ import cluster from "node:cluster";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import chalk from "chalk";
5
+ import { createApp } from "./app.js";
6
+ import { watchProjectFiles } from "./watcher.js";
7
+
8
+ function resolveWorkers(value) {
9
+ if (value === "auto") return os.cpus().length;
10
+
11
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
12
+ return Math.floor(value);
13
+ }
14
+
15
+ if (typeof value === "string" && value.trim() !== "") {
16
+ const n = Number(value);
17
+ if (Number.isFinite(n) && n > 0) return Math.floor(n);
18
+ }
19
+
20
+ return 1;
21
+ }
22
+
23
+ function serializeConfig(config) {
24
+ try {
25
+ return JSON.stringify(config ?? {});
26
+ } catch {
27
+ return "{}";
28
+ }
29
+ }
30
+
31
+ function readConfigFromEnv() {
32
+ try {
33
+ const raw = process.env.NITRO5_CONFIG;
34
+ if (!raw) return {};
35
+ return JSON.parse(raw);
36
+ } catch {
37
+ return {};
38
+ }
39
+ }
40
+
41
+ function formatChangedFile(changedFile) {
42
+ if (!changedFile || changedFile === "manual-restart") return "manual-restart";
43
+ try {
44
+ return path.relative(process.cwd(), changedFile) || changedFile;
45
+ } catch {
46
+ return String(changedFile);
47
+ }
48
+ }
49
+
50
+ async function startWorker(inputConfig = {}) {
51
+ const config =
52
+ inputConfig && Object.keys(inputConfig).length > 0
53
+ ? inputConfig
54
+ : readConfigFromEnv();
55
+
56
+ const app = await createApp(config);
57
+ const port = Number(config.server?.port) || 3000;
58
+
59
+ let server;
60
+ try {
61
+ server = await app.listen(port, () => {
62
+ console.log(
63
+ chalk.green(`[worker ${process.pid}] Nitro5 listening on ${port}`)
64
+ );
65
+ });
66
+ } catch (error) {
67
+ console.error(
68
+ chalk.red(`[worker ${process.pid}] failed to listen on ${port}:`),
69
+ error
70
+ );
71
+ process.exit(1);
72
+ return;
73
+ }
74
+
75
+ let shuttingDown = false;
76
+
77
+ const shutdown = (signal) => {
78
+ if (shuttingDown) return;
79
+ shuttingDown = true;
80
+
81
+ console.log(
82
+ chalk.yellow(`[worker ${process.pid}] received ${signal}, shutting down...`)
83
+ );
84
+
85
+ const exitNow = () => process.exit(0);
86
+
87
+ try {
88
+ if (server && typeof server.close === "function") {
89
+ server.close(exitNow);
90
+ } else {
91
+ exitNow();
92
+ }
93
+ } catch {
94
+ exitNow();
95
+ }
96
+
97
+ setTimeout(() => process.exit(0), 5000).unref?.();
98
+ };
99
+
100
+ process.once("SIGTERM", () => shutdown("SIGTERM"));
101
+ process.once("SIGINT", () => shutdown("SIGINT"));
102
+
103
+ process.on("uncaughtException", (error) => {
104
+ console.error(chalk.red(`[worker ${process.pid}] uncaughtException:`), error);
105
+ process.exit(1);
106
+ });
107
+
108
+ process.on("unhandledRejection", (error) => {
109
+ console.error(
110
+ chalk.red(`[worker ${process.pid}] unhandledRejection:`),
111
+ error
112
+ );
113
+ process.exit(1);
114
+ });
115
+
116
+ return server;
117
+ }
118
+
119
+ export async function startSupervisor(config = {}) {
120
+ const isPrimary = cluster.isPrimary ?? cluster.isMaster;
121
+
122
+ if (!isPrimary) {
123
+ await startWorker(config);
124
+ return;
125
+ }
126
+
127
+ const workerCount = resolveWorkers(config.server?.workers ?? 1);
128
+
129
+ console.log(
130
+ chalk.cyan(`Nitro5 supervisor started with ${workerCount} worker(s)`)
131
+ );
132
+
133
+ const workers = new Set();
134
+ const baseEnv = {
135
+ ...process.env,
136
+ NITRO5_CONFIG: serializeConfig(config),
137
+ };
138
+
139
+ let restarting = false;
140
+ let restartQueued = false;
141
+
142
+ const spawnWorker = () => {
143
+ const worker = cluster.fork(baseEnv);
144
+ workers.add(worker);
145
+
146
+ worker.on("online", () => {
147
+ console.log(chalk.gray(`[supervisor] worker ${worker.process.pid} online`));
148
+ });
149
+
150
+ worker.on("message", (msg) => {
151
+ if (msg?.type === "restart") {
152
+ void restartAll("manual-restart");
153
+ }
154
+ });
155
+
156
+ worker.on("exit", (code, signal) => {
157
+ workers.delete(worker);
158
+
159
+ console.log(
160
+ chalk.gray(
161
+ `[supervisor] worker ${worker.process.pid} exited (code=${code}, signal=${signal ?? "none"})`
162
+ )
163
+ );
164
+
165
+ if (!restarting && config.server?.autoStart) {
166
+ setTimeout(() => {
167
+ if (!restarting) spawnWorker();
168
+ }, 300);
169
+ }
170
+ });
171
+
172
+ return worker;
173
+ };
174
+
175
+ const spawnWorkerBatch = () => {
176
+ for (let i = 0; i < workerCount; i++) {
177
+ spawnWorker();
178
+ }
179
+ };
180
+
181
+ const restartAll = async (changedFile = "manual-restart") => {
182
+ if (restarting) {
183
+ restartQueued = true;
184
+ return;
185
+ }
186
+
187
+ restarting = true;
188
+
189
+ console.log(
190
+ chalk.magenta(
191
+ `[hot reload] ${formatChangedFile(changedFile)} changed`
192
+ )
193
+ );
194
+
195
+ const currentWorkers = Array.from(workers);
196
+
197
+ for (const worker of currentWorkers) {
198
+ try {
199
+ worker.kill("SIGTERM");
200
+ } catch {}
201
+ }
202
+
203
+ await new Promise((resolve) => setTimeout(resolve, 500));
204
+
205
+ workers.clear();
206
+ spawnWorkerBatch();
207
+
208
+ restarting = false;
209
+
210
+ if (restartQueued) {
211
+ restartQueued = false;
212
+ void restartAll("queued-restart");
213
+ }
214
+ };
215
+
216
+ const shutdownPrimary = () => {
217
+ if (restarting) return;
218
+ restarting = true;
219
+
220
+ console.log(chalk.yellow("[supervisor] shutting down workers..."));
221
+
222
+ for (const worker of workers) {
223
+ try {
224
+ worker.kill("SIGTERM");
225
+ } catch {}
226
+ }
227
+
228
+ setTimeout(() => process.exit(0), 5000).unref?.();
229
+ };
230
+
231
+ process.once("SIGTERM", shutdownPrimary);
232
+ process.once("SIGINT", shutdownPrimary);
233
+
234
+ spawnWorkerBatch();
235
+
236
+ if (config.dev?.hotReload) {
237
+ watchProjectFiles({
238
+ roots: [
239
+ path.join(process.cwd(), "src"),
240
+ path.join(process.cwd(), "public"),
241
+ path.join(process.cwd(), "native"),
242
+ process.cwd(),
243
+ ],
244
+ onChange: (changedFile) => {
245
+ void restartAll(changedFile);
246
+ },
247
+ });
248
+ }
249
+ }
@@ -0,0 +1,107 @@
1
+ import os from "node:os";
2
+ import { Worker } from "node:worker_threads";
3
+
4
+ class PoolWorker {
5
+ constructor(worker) {
6
+ this.worker = worker;
7
+ this.busy = false;
8
+ this.currentJob = null;
9
+ }
10
+ }
11
+
12
+ export class ThreadPool {
13
+ constructor(size = 1) {
14
+ this.size = Math.max(1, size);
15
+ this.workers = [];
16
+ this.queue = [];
17
+ this.jobId = 0;
18
+
19
+ for (let i = 0; i < this.size; i++) {
20
+ this.workers.push(this.createWorker());
21
+ }
22
+ }
23
+
24
+ createWorker() {
25
+ const worker = new Worker(new URL("./file-worker.js", import.meta.url), {
26
+ type: "module"
27
+ });
28
+
29
+ const wrapped = new PoolWorker(worker);
30
+
31
+ worker.on("message", (msg) => {
32
+ const job = wrapped.currentJob;
33
+ if (!job) return;
34
+
35
+ wrapped.currentJob = null;
36
+ wrapped.busy = false;
37
+
38
+ if (msg.ok) {
39
+ job.resolve(Buffer.from(msg.base64, "base64"));
40
+ } else {
41
+ job.reject(new Error(msg.error || "Worker thread failed"));
42
+ }
43
+
44
+ this.dispatch();
45
+ });
46
+
47
+ worker.on("error", (error) => {
48
+ const job = wrapped.currentJob;
49
+ wrapped.currentJob = null;
50
+ wrapped.busy = false;
51
+
52
+ if (job) job.reject(error);
53
+ });
54
+
55
+ worker.on("exit", (code) => {
56
+ if (code !== 0) {
57
+ const job = wrapped.currentJob;
58
+ wrapped.currentJob = null;
59
+ wrapped.busy = false;
60
+
61
+ if (job) {
62
+ job.reject(new Error(`Worker exited with code ${code}`));
63
+ }
64
+
65
+ const index = this.workers.indexOf(wrapped);
66
+ if (index >= 0) {
67
+ this.workers[index] = this.createWorker();
68
+ }
69
+ }
70
+ });
71
+
72
+ return wrapped;
73
+ }
74
+
75
+ run(filePath) {
76
+ return new Promise((resolve, reject) => {
77
+ const job = {
78
+ id: ++this.jobId,
79
+ filePath,
80
+ resolve,
81
+ reject
82
+ };
83
+
84
+ this.queue.push(job);
85
+ this.dispatch();
86
+ });
87
+ }
88
+
89
+ dispatch() {
90
+ for (const wrapped of this.workers) {
91
+ if (wrapped.busy) continue;
92
+ const job = this.queue.shift();
93
+ if (!job) return;
94
+
95
+ wrapped.busy = true;
96
+ wrapped.currentJob = job;
97
+ wrapped.worker.postMessage({
98
+ id: job.id,
99
+ filePath: job.filePath
100
+ });
101
+ }
102
+ }
103
+
104
+ async close() {
105
+ await Promise.allSettled(this.workers.map((w) => w.worker.terminate()));
106
+ }
107
+ }