@virentia/inspector 0.1.2

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.
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Virentia Inspector</title>
7
+ <script type="module" crossorigin src="/assets/index-DDQrGtgW.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-BLmv4jmH.css">
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ </body>
13
+ </html>
package/dist/cli.cjs ADDED
@@ -0,0 +1,240 @@
1
+ #!/usr/bin/env node
2
+ let node_fs = require("node:fs");
3
+ let node_http = require("node:http");
4
+ let node_path = require("node:path");
5
+ let node_url = require("node:url");
6
+ let node_util = require("node:util");
7
+ let node_crypto = require("node:crypto");
8
+ //#region lib/server/relay.ts
9
+ const relayPathname = "/__virentia_devtools";
10
+ const websocketKeySuffix = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
11
+ function installVirentiaInspectorRelay(server) {
12
+ const clients = /* @__PURE__ */ new Set();
13
+ const onUpgrade = (request, socket, head) => {
14
+ const host = request.headers.host ?? "127.0.0.1";
15
+ if (new URL(request.url ?? "/", `http://${host}`).pathname !== relayPathname) return;
16
+ const key = request.headers["sec-websocket-key"];
17
+ if (typeof key !== "string") {
18
+ socket.destroy();
19
+ return;
20
+ }
21
+ const client = {
22
+ buffer: Buffer.alloc(0),
23
+ socket
24
+ };
25
+ clients.add(client);
26
+ socket.write([
27
+ "HTTP/1.1 101 Switching Protocols",
28
+ "Upgrade: websocket",
29
+ "Connection: Upgrade",
30
+ `Sec-WebSocket-Accept: ${createAcceptKey(key)}`,
31
+ "",
32
+ ""
33
+ ].join("\r\n"));
34
+ socket.on("data", (chunk) => {
35
+ try {
36
+ handleData(client, chunk, clients);
37
+ } catch {
38
+ socket.destroy();
39
+ }
40
+ });
41
+ socket.on("close", () => clients.delete(client));
42
+ socket.on("error", () => clients.delete(client));
43
+ if (head.length) handleData(client, head, clients);
44
+ };
45
+ server.on("upgrade", onUpgrade);
46
+ return () => {
47
+ server.off("upgrade", onUpgrade);
48
+ for (const client of clients) client.socket.destroy();
49
+ clients.clear();
50
+ };
51
+ }
52
+ function handleData(client, chunk, clients) {
53
+ client.buffer = Buffer.concat([client.buffer, chunk]);
54
+ while (client.buffer.length) {
55
+ const frame = readFrame(client.buffer);
56
+ if (!frame) return;
57
+ client.buffer = frame.rest;
58
+ if (frame.opcode === 8) {
59
+ client.socket.end();
60
+ return;
61
+ }
62
+ if (frame.opcode === 9) {
63
+ writeFrame(client.socket, frame.payload, 10);
64
+ continue;
65
+ }
66
+ if (frame.opcode !== 1) continue;
67
+ broadcast(client, clients, frame.payload.toString("utf8"));
68
+ }
69
+ }
70
+ function broadcast(sender, clients, message) {
71
+ for (const client of clients) {
72
+ if (client === sender) continue;
73
+ writeFrame(client.socket, Buffer.from(message));
74
+ }
75
+ }
76
+ function readFrame(buffer) {
77
+ if (buffer.length < 2) return null;
78
+ const opcode = buffer[0] & 15;
79
+ const masked = (buffer[1] & 128) === 128;
80
+ let length = buffer[1] & 127;
81
+ let offset = 2;
82
+ if (length === 126) {
83
+ if (buffer.length < offset + 2) return null;
84
+ length = buffer.readUInt16BE(offset);
85
+ offset += 2;
86
+ } else if (length === 127) {
87
+ if (buffer.length < offset + 8) return null;
88
+ const longLength = buffer.readBigUInt64BE(offset);
89
+ if (longLength > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("WebSocket frame is too large");
90
+ length = Number(longLength);
91
+ offset += 8;
92
+ }
93
+ const maskOffset = offset;
94
+ if (masked) offset += 4;
95
+ if (buffer.length < offset + length) return null;
96
+ const rawPayload = buffer.subarray(offset, offset + length);
97
+ return {
98
+ opcode,
99
+ payload: masked ? unmask(rawPayload, buffer.subarray(maskOffset, maskOffset + 4)) : rawPayload,
100
+ rest: buffer.subarray(offset + length)
101
+ };
102
+ }
103
+ function writeFrame(socket, payload, opcode = 1) {
104
+ let header;
105
+ if (payload.length < 126) {
106
+ header = Buffer.alloc(2);
107
+ header[1] = payload.length;
108
+ } else if (payload.length <= 65535) {
109
+ header = Buffer.alloc(4);
110
+ header[1] = 126;
111
+ header.writeUInt16BE(payload.length, 2);
112
+ } else {
113
+ header = Buffer.alloc(10);
114
+ header[1] = 127;
115
+ header.writeBigUInt64BE(BigInt(payload.length), 2);
116
+ }
117
+ header[0] = 128 | opcode;
118
+ socket.write(Buffer.concat([header, payload]));
119
+ }
120
+ function unmask(payload, mask) {
121
+ const output = Buffer.allocUnsafe(payload.length);
122
+ for (let index = 0; index < payload.length; index += 1) output[index] = payload[index] ^ mask[index % 4];
123
+ return output;
124
+ }
125
+ function createAcceptKey(key) {
126
+ return (0, node_crypto.createHash)("sha1").update(`${key}${websocketKeySuffix}`).digest("base64");
127
+ }
128
+ //#endregion
129
+ //#region lib/server/cli.ts
130
+ const args = (0, node_util.parseArgs)({
131
+ allowPositionals: false,
132
+ options: {
133
+ help: {
134
+ short: "h",
135
+ type: "boolean"
136
+ },
137
+ host: {
138
+ default: "127.0.0.1",
139
+ type: "string"
140
+ },
141
+ open: {
142
+ short: "o",
143
+ type: "boolean"
144
+ },
145
+ port: {
146
+ default: "5174",
147
+ short: "p",
148
+ type: "string"
149
+ }
150
+ }
151
+ });
152
+ if (args.values.help) {
153
+ printHelp();
154
+ process.exit(0);
155
+ }
156
+ const host = args.values.host ?? "127.0.0.1";
157
+ const port = Number(args.values.port ?? 5174);
158
+ const appDir = (0, node_url.fileURLToPath)(new URL("./app/", require("url").pathToFileURL(__filename).href));
159
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
160
+ console.error(`[virentia-inspector] Invalid port: ${args.values.port}`);
161
+ process.exit(1);
162
+ }
163
+ const server = (0, node_http.createServer)((request, response) => {
164
+ const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
165
+ const filePath = resolveFile(decodeURIComponent(url.pathname));
166
+ if (!filePath) {
167
+ response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
168
+ response.end("Not found");
169
+ return;
170
+ }
171
+ response.writeHead(200, { "content-type": contentType(filePath) });
172
+ (0, node_fs.createReadStream)(filePath).pipe(response);
173
+ });
174
+ const closeRelay = installVirentiaInspectorRelay(server);
175
+ server.on("error", (error) => {
176
+ closeRelay();
177
+ if (error.code === "EADDRINUSE") console.error(`[virentia-inspector] ${host}:${port} is already in use.`);
178
+ else console.error(`[virentia-inspector] ${error.message}`);
179
+ process.exit(1);
180
+ });
181
+ server.listen(port, host, () => {
182
+ const url = `http://${host}:${port}/`;
183
+ console.log(`Virentia inspector is running at ${url}`);
184
+ console.log("Use installVirentiaDevtools({ autoOpen: true }) in your app.");
185
+ if (args.values.open) openBrowser(url);
186
+ });
187
+ function resolveFile(pathname) {
188
+ const root = (0, node_path.resolve)(appDir);
189
+ const target = (0, node_path.resolve)(root, `.${pathname === "/" ? "/index.html" : pathname}`);
190
+ if (!target.startsWith(root)) return null;
191
+ if ((0, node_fs.existsSync)(target)) {
192
+ const stat = (0, node_fs.statSync)(target);
193
+ if (stat.isFile()) return target;
194
+ if (stat.isDirectory()) {
195
+ const index = (0, node_path.resolve)(target, "index.html");
196
+ return (0, node_fs.existsSync)(index) ? index : null;
197
+ }
198
+ }
199
+ const fallback = (0, node_path.resolve)(root, "index.html");
200
+ return (0, node_fs.existsSync)(fallback) ? fallback : null;
201
+ }
202
+ function contentType(filePath) {
203
+ switch ((0, node_path.extname)(filePath)) {
204
+ case ".css": return "text/css; charset=utf-8";
205
+ case ".html": return "text/html; charset=utf-8";
206
+ case ".js": return "text/javascript; charset=utf-8";
207
+ case ".json": return "application/json; charset=utf-8";
208
+ case ".map": return "application/json; charset=utf-8";
209
+ case ".svg": return "image/svg+xml";
210
+ case ".woff2": return "font/woff2";
211
+ default: return "application/octet-stream";
212
+ }
213
+ }
214
+ function openBrowser(url) {
215
+ import("node:child_process").then(({ spawn }) => {
216
+ spawn(process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open", process.platform === "win32" ? [
217
+ "/c",
218
+ "start",
219
+ "",
220
+ url
221
+ ] : [url], {
222
+ detached: true,
223
+ stdio: "ignore"
224
+ }).unref();
225
+ });
226
+ }
227
+ function printHelp() {
228
+ console.log(`Virentia inspector
229
+
230
+ Usage:
231
+ virentia-inspector [--host 127.0.0.1] [--port 5174] [--open]
232
+
233
+ Options:
234
+ -h, --help Show help
235
+ --host Host to listen on, default 127.0.0.1
236
+ -p, --port Port to listen on, default 5174
237
+ -o, --open Open the inspector URL in the system browser
238
+ `);
239
+ }
240
+ //#endregion
package/dist/cli.d.cts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.mjs ADDED
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+ import { createReadStream, existsSync, statSync } from "node:fs";
3
+ import { createServer } from "node:http";
4
+ import { extname, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { parseArgs } from "node:util";
7
+ import { createHash } from "node:crypto";
8
+ //#region lib/server/relay.ts
9
+ const relayPathname = "/__virentia_devtools";
10
+ const websocketKeySuffix = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
11
+ function installVirentiaInspectorRelay(server) {
12
+ const clients = /* @__PURE__ */ new Set();
13
+ const onUpgrade = (request, socket, head) => {
14
+ const host = request.headers.host ?? "127.0.0.1";
15
+ if (new URL(request.url ?? "/", `http://${host}`).pathname !== relayPathname) return;
16
+ const key = request.headers["sec-websocket-key"];
17
+ if (typeof key !== "string") {
18
+ socket.destroy();
19
+ return;
20
+ }
21
+ const client = {
22
+ buffer: Buffer.alloc(0),
23
+ socket
24
+ };
25
+ clients.add(client);
26
+ socket.write([
27
+ "HTTP/1.1 101 Switching Protocols",
28
+ "Upgrade: websocket",
29
+ "Connection: Upgrade",
30
+ `Sec-WebSocket-Accept: ${createAcceptKey(key)}`,
31
+ "",
32
+ ""
33
+ ].join("\r\n"));
34
+ socket.on("data", (chunk) => {
35
+ try {
36
+ handleData(client, chunk, clients);
37
+ } catch {
38
+ socket.destroy();
39
+ }
40
+ });
41
+ socket.on("close", () => clients.delete(client));
42
+ socket.on("error", () => clients.delete(client));
43
+ if (head.length) handleData(client, head, clients);
44
+ };
45
+ server.on("upgrade", onUpgrade);
46
+ return () => {
47
+ server.off("upgrade", onUpgrade);
48
+ for (const client of clients) client.socket.destroy();
49
+ clients.clear();
50
+ };
51
+ }
52
+ function handleData(client, chunk, clients) {
53
+ client.buffer = Buffer.concat([client.buffer, chunk]);
54
+ while (client.buffer.length) {
55
+ const frame = readFrame(client.buffer);
56
+ if (!frame) return;
57
+ client.buffer = frame.rest;
58
+ if (frame.opcode === 8) {
59
+ client.socket.end();
60
+ return;
61
+ }
62
+ if (frame.opcode === 9) {
63
+ writeFrame(client.socket, frame.payload, 10);
64
+ continue;
65
+ }
66
+ if (frame.opcode !== 1) continue;
67
+ broadcast(client, clients, frame.payload.toString("utf8"));
68
+ }
69
+ }
70
+ function broadcast(sender, clients, message) {
71
+ for (const client of clients) {
72
+ if (client === sender) continue;
73
+ writeFrame(client.socket, Buffer.from(message));
74
+ }
75
+ }
76
+ function readFrame(buffer) {
77
+ if (buffer.length < 2) return null;
78
+ const opcode = buffer[0] & 15;
79
+ const masked = (buffer[1] & 128) === 128;
80
+ let length = buffer[1] & 127;
81
+ let offset = 2;
82
+ if (length === 126) {
83
+ if (buffer.length < offset + 2) return null;
84
+ length = buffer.readUInt16BE(offset);
85
+ offset += 2;
86
+ } else if (length === 127) {
87
+ if (buffer.length < offset + 8) return null;
88
+ const longLength = buffer.readBigUInt64BE(offset);
89
+ if (longLength > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("WebSocket frame is too large");
90
+ length = Number(longLength);
91
+ offset += 8;
92
+ }
93
+ const maskOffset = offset;
94
+ if (masked) offset += 4;
95
+ if (buffer.length < offset + length) return null;
96
+ const rawPayload = buffer.subarray(offset, offset + length);
97
+ return {
98
+ opcode,
99
+ payload: masked ? unmask(rawPayload, buffer.subarray(maskOffset, maskOffset + 4)) : rawPayload,
100
+ rest: buffer.subarray(offset + length)
101
+ };
102
+ }
103
+ function writeFrame(socket, payload, opcode = 1) {
104
+ let header;
105
+ if (payload.length < 126) {
106
+ header = Buffer.alloc(2);
107
+ header[1] = payload.length;
108
+ } else if (payload.length <= 65535) {
109
+ header = Buffer.alloc(4);
110
+ header[1] = 126;
111
+ header.writeUInt16BE(payload.length, 2);
112
+ } else {
113
+ header = Buffer.alloc(10);
114
+ header[1] = 127;
115
+ header.writeBigUInt64BE(BigInt(payload.length), 2);
116
+ }
117
+ header[0] = 128 | opcode;
118
+ socket.write(Buffer.concat([header, payload]));
119
+ }
120
+ function unmask(payload, mask) {
121
+ const output = Buffer.allocUnsafe(payload.length);
122
+ for (let index = 0; index < payload.length; index += 1) output[index] = payload[index] ^ mask[index % 4];
123
+ return output;
124
+ }
125
+ function createAcceptKey(key) {
126
+ return createHash("sha1").update(`${key}${websocketKeySuffix}`).digest("base64");
127
+ }
128
+ //#endregion
129
+ //#region lib/server/cli.ts
130
+ const args = parseArgs({
131
+ allowPositionals: false,
132
+ options: {
133
+ help: {
134
+ short: "h",
135
+ type: "boolean"
136
+ },
137
+ host: {
138
+ default: "127.0.0.1",
139
+ type: "string"
140
+ },
141
+ open: {
142
+ short: "o",
143
+ type: "boolean"
144
+ },
145
+ port: {
146
+ default: "5174",
147
+ short: "p",
148
+ type: "string"
149
+ }
150
+ }
151
+ });
152
+ if (args.values.help) {
153
+ printHelp();
154
+ process.exit(0);
155
+ }
156
+ const host = args.values.host ?? "127.0.0.1";
157
+ const port = Number(args.values.port ?? 5174);
158
+ const appDir = fileURLToPath(new URL("./app/", import.meta.url));
159
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
160
+ console.error(`[virentia-inspector] Invalid port: ${args.values.port}`);
161
+ process.exit(1);
162
+ }
163
+ const server = createServer((request, response) => {
164
+ const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
165
+ const filePath = resolveFile(decodeURIComponent(url.pathname));
166
+ if (!filePath) {
167
+ response.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
168
+ response.end("Not found");
169
+ return;
170
+ }
171
+ response.writeHead(200, { "content-type": contentType(filePath) });
172
+ createReadStream(filePath).pipe(response);
173
+ });
174
+ const closeRelay = installVirentiaInspectorRelay(server);
175
+ server.on("error", (error) => {
176
+ closeRelay();
177
+ if (error.code === "EADDRINUSE") console.error(`[virentia-inspector] ${host}:${port} is already in use.`);
178
+ else console.error(`[virentia-inspector] ${error.message}`);
179
+ process.exit(1);
180
+ });
181
+ server.listen(port, host, () => {
182
+ const url = `http://${host}:${port}/`;
183
+ console.log(`Virentia inspector is running at ${url}`);
184
+ console.log("Use installVirentiaDevtools({ autoOpen: true }) in your app.");
185
+ if (args.values.open) openBrowser(url);
186
+ });
187
+ function resolveFile(pathname) {
188
+ const root = resolve(appDir);
189
+ const target = resolve(root, `.${pathname === "/" ? "/index.html" : pathname}`);
190
+ if (!target.startsWith(root)) return null;
191
+ if (existsSync(target)) {
192
+ const stat = statSync(target);
193
+ if (stat.isFile()) return target;
194
+ if (stat.isDirectory()) {
195
+ const index = resolve(target, "index.html");
196
+ return existsSync(index) ? index : null;
197
+ }
198
+ }
199
+ const fallback = resolve(root, "index.html");
200
+ return existsSync(fallback) ? fallback : null;
201
+ }
202
+ function contentType(filePath) {
203
+ switch (extname(filePath)) {
204
+ case ".css": return "text/css; charset=utf-8";
205
+ case ".html": return "text/html; charset=utf-8";
206
+ case ".js": return "text/javascript; charset=utf-8";
207
+ case ".json": return "application/json; charset=utf-8";
208
+ case ".map": return "application/json; charset=utf-8";
209
+ case ".svg": return "image/svg+xml";
210
+ case ".woff2": return "font/woff2";
211
+ default: return "application/octet-stream";
212
+ }
213
+ }
214
+ function openBrowser(url) {
215
+ import("node:child_process").then(({ spawn }) => {
216
+ spawn(process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open", process.platform === "win32" ? [
217
+ "/c",
218
+ "start",
219
+ "",
220
+ url
221
+ ] : [url], {
222
+ detached: true,
223
+ stdio: "ignore"
224
+ }).unref();
225
+ });
226
+ }
227
+ function printHelp() {
228
+ console.log(`Virentia inspector
229
+
230
+ Usage:
231
+ virentia-inspector [--host 127.0.0.1] [--port 5174] [--open]
232
+
233
+ Options:
234
+ -h, --help Show help
235
+ --host Host to listen on, default 127.0.0.1
236
+ -p, --port Port to listen on, default 5174
237
+ -o, --open Open the inspector URL in the system browser
238
+ `);
239
+ }
240
+ //#endregion
241
+ export {};