@yydb/yyds 0.0.14 → 0.0.15
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/bin/yyds.mjs +18 -3
- package/package.json +10 -7
- package/readme.md +5 -4
- package/src/node/webui-cmd.ts +137 -0
- package/src/node/webui-static.test.ts +12 -0
- package/src/node/webui-static.ts +137 -0
package/bin/yyds.mjs
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { loadYydsNative } from "../src/node/load.ts";
|
|
3
3
|
import { parseStartOptions, runNodeStart } from "../src/node/runtime.ts";
|
|
4
|
+
import { parseWebuiArgs, runWebui } from "../src/node/webui-cmd.ts";
|
|
4
5
|
|
|
5
6
|
function usage() {
|
|
6
|
-
return "usage: yyds <init|start|status|stop>
|
|
7
|
+
return "usage: yyds <init|start|status|stop|webui> ...";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function startUsage() {
|
|
11
|
+
return "usage: yyds start --data-dir DIR --cluster-id ID --node-id ID [--redis-port PORT] [--pgsql-port PORT] [--mysql-port PORT] [--mongo-port PORT] [--wire-port PORT]";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function webuiUsage() {
|
|
15
|
+
return "usage: yyds webui [--endpoint URL | --data-dir DIR --cluster-id ID --node-id ID [--wire-port PORT]] [--port NUMBER] [--no-open]";
|
|
7
16
|
}
|
|
8
17
|
|
|
9
18
|
function parseCommonArgs(command, args) {
|
|
@@ -70,7 +79,7 @@ function parseCommonArgs(command, args) {
|
|
|
70
79
|
}
|
|
71
80
|
if (!options.dataDir) throw new Error(usage());
|
|
72
81
|
if ((command === "init" || command === "start") && (!options.clusterId || !options.nodeId)) {
|
|
73
|
-
throw new Error(
|
|
82
|
+
throw new Error(startUsage());
|
|
74
83
|
}
|
|
75
84
|
return options;
|
|
76
85
|
}
|
|
@@ -78,6 +87,12 @@ function parseCommonArgs(command, args) {
|
|
|
78
87
|
async function main(args) {
|
|
79
88
|
const command = args[0];
|
|
80
89
|
const rest = args.slice(1);
|
|
90
|
+
|
|
91
|
+
if (command === "webui") {
|
|
92
|
+
await runWebui(parseWebuiArgs(rest));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
81
96
|
const native = loadYydsNative();
|
|
82
97
|
const options = parseCommonArgs(command, rest);
|
|
83
98
|
|
|
@@ -124,7 +139,7 @@ async function main(args) {
|
|
|
124
139
|
return;
|
|
125
140
|
}
|
|
126
141
|
default:
|
|
127
|
-
throw new Error(command ? `unknown command '${command}'` : usage());
|
|
142
|
+
throw new Error(command ? `unknown command '${command}'` : `${usage()}\n${startUsage()}\n${webuiUsage()}`);
|
|
128
143
|
}
|
|
129
144
|
}
|
|
130
145
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yydb/yyds",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"description": "Node.js facade, CLI, and native bindings for local YYDS nodes.",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -32,11 +32,13 @@
|
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
34
|
"bin",
|
|
35
|
-
"src"
|
|
35
|
+
"src",
|
|
36
|
+
"webui"
|
|
36
37
|
],
|
|
37
38
|
"scripts": {
|
|
39
|
+
"build": "node scripts/bundle-webui.mjs",
|
|
38
40
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
39
|
-
"test": "node --experimental-strip-types --test \"./tests/**/*.test.ts\""
|
|
41
|
+
"test": "node --experimental-strip-types --test \"./tests/**/*.test.ts\" \"./src/node/**/*.test.ts\""
|
|
40
42
|
},
|
|
41
43
|
"repository": {
|
|
42
44
|
"type": "git",
|
|
@@ -57,13 +59,14 @@
|
|
|
57
59
|
"access": "public"
|
|
58
60
|
},
|
|
59
61
|
"optionalDependencies": {
|
|
60
|
-
"@yydb/yyds-win32-x64": "0.0.
|
|
61
|
-
"@yydb/yyds-linux-x64": "0.0.
|
|
62
|
-
"@yydb/yyds-darwin-x64": "0.0.
|
|
63
|
-
"@yydb/yyds-darwin-arm64": "0.0.
|
|
62
|
+
"@yydb/yyds-win32-x64": "0.0.15",
|
|
63
|
+
"@yydb/yyds-linux-x64": "0.0.15",
|
|
64
|
+
"@yydb/yyds-darwin-x64": "0.0.15",
|
|
65
|
+
"@yydb/yyds-darwin-arm64": "0.0.15"
|
|
64
66
|
},
|
|
65
67
|
"devDependencies": {
|
|
66
68
|
"@types/node": "^22.15.0",
|
|
69
|
+
"@yydb/yyds-webui": "0.0.15",
|
|
67
70
|
"typescript": "^5.9.2"
|
|
68
71
|
},
|
|
69
72
|
"engines": {
|
package/readme.md
CHANGED
|
@@ -10,7 +10,7 @@ Node.js facade for running a local YYDS node: native lifecycle blocks, a small C
|
|
|
10
10
|
| `@yydb/yyds/node/cluster` | `launchYydsServer` for `redis-server`, `mysqld`, `postgres` |
|
|
11
11
|
| `@yydb/yyds/node/runtime` | `runNodeStart`, `parseStartOptions` |
|
|
12
12
|
|
|
13
|
-
The `yyds` binary provides `init`, `start`, `status`, and `
|
|
13
|
+
The `yyds` binary provides `init`, `start`, `status`, `stop`, and `webui`. Native addons for your platform are pulled in automatically as optional dependencies.
|
|
14
14
|
|
|
15
15
|
## Example
|
|
16
16
|
|
|
@@ -30,10 +30,11 @@ yyds init --data-dir ./node-data --cluster-id local --node-id node-1
|
|
|
30
30
|
yyds start --data-dir ./node-data --cluster-id local --node-id node-1
|
|
31
31
|
yyds status --data-dir ./node-data
|
|
32
32
|
yyds stop --data-dir ./node-data
|
|
33
|
+
yyds webui --data-dir ./node-data --cluster-id local --node-id node-1
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
## Related packages
|
|
36
37
|
|
|
37
|
-
- [`@yydb/redis`](https://github.com/yy-database/yyds/tree/dev/projects/yyds.ts/packages/redis) �?Redis protocol disguise
|
|
38
|
-
- [`@yydb/mysql`](https://github.com/yy-database/yyds/tree/dev/projects/yyds.ts/packages/mysql) �?MySQL protocol disguise
|
|
39
|
-
- [`@yydb/postgresql`](https://github.com/yy-database/yyds/tree/dev/projects/yyds.ts/packages/postgresql) �?PostgreSQL protocol disguise
|
|
38
|
+
- [`@yydb/redis`](https://github.com/yy-database/yyds/tree/dev/projects/yyds.ts/packages/redis) �?Redis protocol disguise
|
|
39
|
+
- [`@yydb/mysql`](https://github.com/yy-database/yyds/tree/dev/projects/yyds.ts/packages/mysql) �?MySQL protocol disguise
|
|
40
|
+
- [`@yydb/postgresql`](https://github.com/yy-database/yyds/tree/dev/projects/yyds.ts/packages/postgresql) �?PostgreSQL protocol disguise
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { ChildProcess } from "node:child_process";
|
|
4
|
+
import type { Server } from "node:http";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
openBrowser,
|
|
8
|
+
resolveWebuiDist,
|
|
9
|
+
spawnStart,
|
|
10
|
+
startStaticServer,
|
|
11
|
+
waitForWire,
|
|
12
|
+
wsEndpointFromWirePort,
|
|
13
|
+
} from "./webui-static.ts";
|
|
14
|
+
|
|
15
|
+
export interface WebuiOptions {
|
|
16
|
+
dataDir: string;
|
|
17
|
+
clusterId: string;
|
|
18
|
+
nodeId: string;
|
|
19
|
+
wirePort: number;
|
|
20
|
+
webuiPort: number;
|
|
21
|
+
endpoint?: string;
|
|
22
|
+
open: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parsePort(raw: string | undefined, fallback: number, label: string): number {
|
|
26
|
+
const value = Number(raw ?? fallback);
|
|
27
|
+
if (!Number.isInteger(value) || value < 1 || value > 65535) {
|
|
28
|
+
throw new Error(`invalid ${label}: ${raw ?? fallback}`);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function parseWebuiArgs(args: string[]): WebuiOptions {
|
|
34
|
+
const options: WebuiOptions = {
|
|
35
|
+
dataDir: "",
|
|
36
|
+
clusterId: "",
|
|
37
|
+
nodeId: "",
|
|
38
|
+
wirePort: 7700,
|
|
39
|
+
webuiPort: 8780,
|
|
40
|
+
open: true,
|
|
41
|
+
};
|
|
42
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
43
|
+
const flag = args[index];
|
|
44
|
+
const value = args[index + 1];
|
|
45
|
+
switch (flag) {
|
|
46
|
+
case "--data-dir":
|
|
47
|
+
options.dataDir = value ?? "";
|
|
48
|
+
index += 1;
|
|
49
|
+
break;
|
|
50
|
+
case "--cluster-id":
|
|
51
|
+
options.clusterId = value ?? "";
|
|
52
|
+
index += 1;
|
|
53
|
+
break;
|
|
54
|
+
case "--node-id":
|
|
55
|
+
options.nodeId = value ?? "";
|
|
56
|
+
index += 1;
|
|
57
|
+
break;
|
|
58
|
+
case "--wire-port":
|
|
59
|
+
options.wirePort = parsePort(value, options.wirePort, "wire-port");
|
|
60
|
+
index += 1;
|
|
61
|
+
break;
|
|
62
|
+
case "--port":
|
|
63
|
+
options.webuiPort = parsePort(value, options.webuiPort, "port");
|
|
64
|
+
index += 1;
|
|
65
|
+
break;
|
|
66
|
+
case "--endpoint":
|
|
67
|
+
options.endpoint = value ?? "";
|
|
68
|
+
index += 1;
|
|
69
|
+
break;
|
|
70
|
+
case "--no-open":
|
|
71
|
+
options.open = false;
|
|
72
|
+
break;
|
|
73
|
+
default:
|
|
74
|
+
throw new Error(`unknown option '${flag}'`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!options.endpoint && (!options.dataDir || !options.clusterId || !options.nodeId)) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
"webui requires --endpoint or --data-dir with --cluster-id and --node-id",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return options;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function runWebui(options: WebuiOptions): Promise<void> {
|
|
86
|
+
const endpoint = options.endpoint ?? wsEndpointFromWirePort(options.wirePort);
|
|
87
|
+
const distRoot = resolveWebuiDist();
|
|
88
|
+
if (!fs.existsSync(join(distRoot, "index.html"))) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"WebUI assets not found. Run `pnpm --filter @yydb/yyds build` to bundle webui/.",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let startChild: ChildProcess | null = null;
|
|
95
|
+
if (!options.endpoint) {
|
|
96
|
+
startChild = spawnStart({
|
|
97
|
+
dataDir: options.dataDir,
|
|
98
|
+
clusterId: options.clusterId,
|
|
99
|
+
nodeId: options.nodeId,
|
|
100
|
+
wirePort: options.wirePort,
|
|
101
|
+
});
|
|
102
|
+
startChild.stdout?.on("data", (chunk) => process.stdout.write(chunk));
|
|
103
|
+
startChild.stderr?.on("data", (chunk) => process.stderr.write(chunk));
|
|
104
|
+
startChild.on("exit", (code) => {
|
|
105
|
+
if (code && code !== 0) {
|
|
106
|
+
console.error(`yyds start exited with code ${code}`);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
await waitForWire(endpoint, 8000);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const server = await startStaticServer(distRoot, options.webuiPort);
|
|
113
|
+
const pageUrl = `http://127.0.0.1:${options.webuiPort}/?endpoint=${encodeURIComponent(endpoint)}`;
|
|
114
|
+
|
|
115
|
+
console.log(`WebUI ${pageUrl}`);
|
|
116
|
+
console.log(`Wire ${endpoint}`);
|
|
117
|
+
|
|
118
|
+
if (options.open) {
|
|
119
|
+
openBrowser(pageUrl);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await waitForShutdown(server, startChild);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function waitForShutdown(server: Server, startChild: ChildProcess | null): Promise<void> {
|
|
126
|
+
return new Promise((resolveWait) => {
|
|
127
|
+
const shutdown = () => {
|
|
128
|
+
server.close();
|
|
129
|
+
if (startChild && !startChild.killed) {
|
|
130
|
+
startChild.kill();
|
|
131
|
+
}
|
|
132
|
+
resolveWait();
|
|
133
|
+
};
|
|
134
|
+
process.once("SIGINT", shutdown);
|
|
135
|
+
process.once("SIGTERM", shutdown);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
|
|
4
|
+
import { wsEndpointFromWirePort } from "./webui-static.ts";
|
|
5
|
+
|
|
6
|
+
test("wsEndpointFromWirePort maps loopback wire port to WebSocket URL", () => {
|
|
7
|
+
assert.equal(wsEndpointFromWirePort(7700), "ws://127.0.0.1:7700/wire");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("wsEndpointFromWirePort accepts custom host", () => {
|
|
11
|
+
assert.equal(wsEndpointFromWirePort(9001, "10.0.0.5"), "ws://10.0.0.5:9001/wire");
|
|
12
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { createServer, type Server } from "node:http";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
const MIME: Record<string, string> = {
|
|
9
|
+
".html": "text/html; charset=utf-8",
|
|
10
|
+
".js": "text/javascript; charset=utf-8",
|
|
11
|
+
".css": "text/css; charset=utf-8",
|
|
12
|
+
".json": "application/json; charset=utf-8",
|
|
13
|
+
".svg": "image/svg+xml",
|
|
14
|
+
".woff2": "font/woff2",
|
|
15
|
+
".png": "image/png",
|
|
16
|
+
".ico": "image/x-icon",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function contentType(path: string): string {
|
|
20
|
+
const extension = path.slice(path.lastIndexOf("."));
|
|
21
|
+
return MIME[extension] ?? "application/octet-stream";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveWebuiDist(): string {
|
|
25
|
+
const bundled = join(dirname(fileURLToPath(import.meta.url)), "../../webui");
|
|
26
|
+
if (existsSync(join(bundled, "index.html"))) {
|
|
27
|
+
return bundled;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const require = createRequire(import.meta.url);
|
|
31
|
+
const pkgJson = require.resolve("@yydb/yyds-webui/package.json");
|
|
32
|
+
const external = join(dirname(pkgJson), "dist", "cdn");
|
|
33
|
+
if (existsSync(join(external, "index.html"))) {
|
|
34
|
+
return external;
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// @yydb/yyds-webui not installed
|
|
38
|
+
}
|
|
39
|
+
return bundled;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function wsEndpointFromWirePort(wirePort: number, host = "127.0.0.1"): string {
|
|
43
|
+
return `ws://${host}:${wirePort}/wire`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function startStaticServer(root: string, port: number): Promise<Server> {
|
|
47
|
+
const absRoot = resolve(root);
|
|
48
|
+
return new Promise((resolvePromise, reject) => {
|
|
49
|
+
const server = createServer((request, response) => {
|
|
50
|
+
const pathname = decodeURIComponent(new URL(request.url ?? "/", "http://localhost").pathname);
|
|
51
|
+
const relative = pathname === "/" ? "index.html" : pathname.replace(/^\//, "");
|
|
52
|
+
const target = resolve(absRoot, relative);
|
|
53
|
+
if (!target.startsWith(absRoot)) {
|
|
54
|
+
response.writeHead(403);
|
|
55
|
+
response.end("forbidden");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const bytes = readFileSync(target);
|
|
60
|
+
response.writeHead(200, { "Content-Type": contentType(target) });
|
|
61
|
+
response.end(bytes);
|
|
62
|
+
} catch {
|
|
63
|
+
response.writeHead(404);
|
|
64
|
+
response.end("not found");
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
server.listen(port, "127.0.0.1", () => resolvePromise(server));
|
|
68
|
+
server.on("error", reject);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface StartSpawnOptions {
|
|
73
|
+
dataDir: string;
|
|
74
|
+
clusterId: string;
|
|
75
|
+
nodeId: string;
|
|
76
|
+
wirePort: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function spawnStart(options: StartSpawnOptions): ChildProcess {
|
|
80
|
+
const cli = fileURLToPath(new URL("../../bin/yyds.mjs", import.meta.url));
|
|
81
|
+
const args = [
|
|
82
|
+
cli,
|
|
83
|
+
"start",
|
|
84
|
+
"--data-dir",
|
|
85
|
+
options.dataDir,
|
|
86
|
+
"--cluster-id",
|
|
87
|
+
options.clusterId,
|
|
88
|
+
"--node-id",
|
|
89
|
+
options.nodeId,
|
|
90
|
+
"--wire-port",
|
|
91
|
+
String(options.wirePort),
|
|
92
|
+
];
|
|
93
|
+
return spawn(process.execPath, args, {
|
|
94
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
95
|
+
env: process.env,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function openBrowser(url: string): void {
|
|
100
|
+
const platform = process.platform;
|
|
101
|
+
if (platform === "win32") {
|
|
102
|
+
spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const opener = platform === "darwin" ? "open" : "xdg-open";
|
|
106
|
+
spawn(opener, [url], { detached: true, stdio: "ignore" });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function sleep(ms: number): Promise<void> {
|
|
110
|
+
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function waitForWire(endpoint: string, timeoutMs = 8000): Promise<void> {
|
|
114
|
+
const deadline = Date.now() + timeoutMs;
|
|
115
|
+
while (Date.now() < deadline) {
|
|
116
|
+
try {
|
|
117
|
+
const socket = new WebSocket(endpoint);
|
|
118
|
+
const opened = await new Promise<boolean>((resolveOpen) => {
|
|
119
|
+
const timer = setTimeout(() => resolveOpen(false), 500);
|
|
120
|
+
socket.addEventListener("open", () => {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
resolveOpen(true);
|
|
123
|
+
});
|
|
124
|
+
socket.addEventListener("error", () => {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
resolveOpen(false);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
socket.close();
|
|
130
|
+
if (opened) return;
|
|
131
|
+
} catch {
|
|
132
|
+
await sleep(400);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
await sleep(200);
|
|
136
|
+
}
|
|
137
|
+
}
|