busyserver 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/LICENSE +21 -0
- package/README.md +144 -0
- package/bin/busyserver.js +246 -0
- package/package.json +44 -0
- package/src/config.js +60 -0
- package/src/dashboard.js +32 -0
- package/src/mock.js +45 -0
- package/src/proxy.js +46 -0
- package/src/server.js +734 -0
- package/src/templates.js +330 -0
- package/src/watcher.js +106 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SATPAL
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# BusyServer
|
|
2
|
+
|
|
3
|
+
> **A lightweight, cross-platform Node.js development and static file server.**
|
|
4
|
+
|
|
5
|
+
BusyServer is a fast and production-quality static server designed for developers. It runs seamlessly anywhere Node.js is supportedโincluding Linux, macOS, Windows, and Termux/Android environments. Whether you need an instant local server, reverse proxy, API mocks, or live-reload capabilities, BusyServer has you covered.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
* **โก Live Reload**: Instant HTML, CSS, and Image hot-swapping without full page reloads via WebSockets.
|
|
10
|
+
* **๐ Directory Listing**: Modern, responsive dark/light UI with breadcrumb navigation, search/filter, and column sorting.
|
|
11
|
+
* **๐ Local HTTPS**: Built-in TLS support for secure localhost testing (`--https`).
|
|
12
|
+
* **๐ Reverse Proxy**: Forward requests to a backend server to bypass CORS (`--proxy /api=http://localhost:5000`).
|
|
13
|
+
* **๐ญ API Mocking**: Serve JSON mocks effortlessly from a directory (`--mock ./mocks`), including HTTP-method-specific mocks.
|
|
14
|
+
* **๐ SPA Fallback**: Automatically serves `index.html` for missing files (`--spa`).
|
|
15
|
+
* **๐ฑ Real QR Code**: Scan and open on your mobile device instantly (`--qr`).
|
|
16
|
+
* **๐๏ธ Performance**: Automatic Brotli and Gzip compression, HTTP Range requests (206 Partial Content), and ETag/304 caching.
|
|
17
|
+
* **๐ ๏ธ Developer Experience**: Smart port selection/fallback, host binding, custom error pages (404, 403, 405, 500), and custom headers.
|
|
18
|
+
* **๐ Web Dashboard**: Inspect active connections and server stats live at `/__busyserver`.
|
|
19
|
+
* **โ๏ธ Persistent Config**: Use `busyserver init` to generate a `.busyserverc.json`.
|
|
20
|
+
* **๐ก๏ธ Security**: Built-in protection against path traversal, null bytes, and unauthorized dotfile access.
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
Run it instantly without installing:
|
|
25
|
+
\`\`\`bash
|
|
26
|
+
npx busyserver
|
|
27
|
+
\`\`\`
|
|
28
|
+
|
|
29
|
+
Or install globally via npm:
|
|
30
|
+
\`\`\`bash
|
|
31
|
+
npm install -g busyserver
|
|
32
|
+
\`\`\`
|
|
33
|
+
|
|
34
|
+
## CLI Usage
|
|
35
|
+
|
|
36
|
+
Serve the current directory on an available port:
|
|
37
|
+
\`\`\`bash
|
|
38
|
+
busyserver
|
|
39
|
+
\`\`\`
|
|
40
|
+
|
|
41
|
+
Serve a specific directory with a custom port and auto-open browser:
|
|
42
|
+
\`\`\`bash
|
|
43
|
+
busyserver ./public -p 8080 --open
|
|
44
|
+
\`\`\`
|
|
45
|
+
|
|
46
|
+
Bind to a specific network interface (e.g., localhost only):
|
|
47
|
+
\`\`\`bash
|
|
48
|
+
busyserver --host 127.0.0.1
|
|
49
|
+
\`\`\`
|
|
50
|
+
|
|
51
|
+
Enable Single Page App (SPA) mode:
|
|
52
|
+
\`\`\`bash
|
|
53
|
+
busyserver --spa
|
|
54
|
+
\`\`\`
|
|
55
|
+
|
|
56
|
+
Show a QR code for easy mobile LAN access:
|
|
57
|
+
\`\`\`bash
|
|
58
|
+
busyserver --qr
|
|
59
|
+
\`\`\`
|
|
60
|
+
|
|
61
|
+
### API Mocking
|
|
62
|
+
|
|
63
|
+
Need a fake backend while developing? Enable mocking:
|
|
64
|
+
|
|
65
|
+
\`\`\`bash
|
|
66
|
+
busyserver --mock ./mocks
|
|
67
|
+
\`\`\`
|
|
68
|
+
|
|
69
|
+
Create files in your mock directory (`./mocks/api`):
|
|
70
|
+
- `./mocks/api/users.json` โ responds to `GET /api/users`
|
|
71
|
+
- `./mocks/api/users.POST.json` โ responds to `POST /api/users`
|
|
72
|
+
- `./mocks/api/index.json` โ responds to `GET /api/`
|
|
73
|
+
|
|
74
|
+
### Reverse Proxy
|
|
75
|
+
|
|
76
|
+
Forward API requests to a real backend to bypass CORS:
|
|
77
|
+
|
|
78
|
+
\`\`\`bash
|
|
79
|
+
busyserver --proxy /api=http://localhost:5000
|
|
80
|
+
\`\`\`
|
|
81
|
+
|
|
82
|
+
### Local HTTPS (TLS)
|
|
83
|
+
|
|
84
|
+
Test features that require secure contexts (like Service Workers or Geolocation) using local HTTPS.
|
|
85
|
+
Ensure `key.pem` and `cert.pem` are present in the directory you are running the server from.
|
|
86
|
+
|
|
87
|
+
\`\`\`bash
|
|
88
|
+
busyserver --https
|
|
89
|
+
\`\`\`
|
|
90
|
+
|
|
91
|
+
## Configuration
|
|
92
|
+
|
|
93
|
+
Generate a configuration file to save your project preferences:
|
|
94
|
+
|
|
95
|
+
\`\`\`bash
|
|
96
|
+
busyserver init
|
|
97
|
+
\`\`\`
|
|
98
|
+
|
|
99
|
+
This creates a `.busyserverc.json` in your project root:
|
|
100
|
+
|
|
101
|
+
\`\`\`json
|
|
102
|
+
{
|
|
103
|
+
"port": 3000,
|
|
104
|
+
"host": "0.0.0.0",
|
|
105
|
+
"open": false,
|
|
106
|
+
"reload": true,
|
|
107
|
+
"watch": true,
|
|
108
|
+
"spa": false,
|
|
109
|
+
"quiet": false,
|
|
110
|
+
"qr": false,
|
|
111
|
+
"https": false,
|
|
112
|
+
"compression": true,
|
|
113
|
+
"proxy": {},
|
|
114
|
+
"mock": "",
|
|
115
|
+
"ignore": [
|
|
116
|
+
"**/.git/**",
|
|
117
|
+
"**/node_modules/**"
|
|
118
|
+
],
|
|
119
|
+
"headers": {
|
|
120
|
+
"X-Custom-Header": "BusyServer"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
\`\`\`
|
|
124
|
+
|
|
125
|
+
## Dashboard
|
|
126
|
+
|
|
127
|
+
Navigate to `/__busyserver` in your browser while the server is running to view the live developer dashboard, monitor uptime, and see active WebSocket clients.
|
|
128
|
+
|
|
129
|
+
## Development & Testing
|
|
130
|
+
|
|
131
|
+
Clone the repository and install dependencies:
|
|
132
|
+
|
|
133
|
+
\`\`\`bash
|
|
134
|
+
npm install
|
|
135
|
+
\`\`\`
|
|
136
|
+
|
|
137
|
+
Run the comprehensive test suite (62 tests):
|
|
138
|
+
|
|
139
|
+
\`\`\`bash
|
|
140
|
+
npm test
|
|
141
|
+
\`\`\`
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
*Built for modern cross-platform web development.*
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
":" //# comment; exec node "$0" "$@"
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const { startBusyServer } = require("../src/server");
|
|
8
|
+
const { loadConfig } = require("../src/config");
|
|
9
|
+
|
|
10
|
+
const pkg = require("../package.json");
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
|
|
13
|
+
// โโ Init Command โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
14
|
+
|
|
15
|
+
if (args[0] === "init") {
|
|
16
|
+
const configPath = path.join(process.cwd(), ".busyserverc.json");
|
|
17
|
+
if (fs.existsSync(configPath)) {
|
|
18
|
+
console.error("โ Configuration file already exists: .busyserverc.json");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const defaultConfig = {
|
|
23
|
+
"port": 3000,
|
|
24
|
+
"host": "0.0.0.0",
|
|
25
|
+
"open": false,
|
|
26
|
+
"reload": true,
|
|
27
|
+
"watch": true,
|
|
28
|
+
"spa": false,
|
|
29
|
+
"quiet": false,
|
|
30
|
+
"qr": false,
|
|
31
|
+
"https": false,
|
|
32
|
+
"compression": true,
|
|
33
|
+
"proxy": {},
|
|
34
|
+
"mock": "",
|
|
35
|
+
"ignore": [
|
|
36
|
+
"**/.git/**",
|
|
37
|
+
"**/node_modules/**"
|
|
38
|
+
],
|
|
39
|
+
"headers": {
|
|
40
|
+
"X-Custom-Header": "BusyServer"
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2));
|
|
45
|
+
console.log("โ
Created .busyserverc.json with default settings.");
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// โโ Help โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
50
|
+
|
|
51
|
+
function showHelp() {
|
|
52
|
+
console.log(`
|
|
53
|
+
๐ BusyServer v${pkg.version} โ Live Server for Termux
|
|
54
|
+
|
|
55
|
+
Usage:
|
|
56
|
+
busyserver [directory] [options]
|
|
57
|
+
busyserver init Create .busyserverc.json
|
|
58
|
+
|
|
59
|
+
Arguments:
|
|
60
|
+
directory Directory to serve (default: current directory)
|
|
61
|
+
|
|
62
|
+
Options:
|
|
63
|
+
-p, --port <number> Server port (default: 3000)
|
|
64
|
+
--host <address> Host to bind to (default: 0.0.0.0)
|
|
65
|
+
--open Open in browser automatically
|
|
66
|
+
--no-reload Disable live reload
|
|
67
|
+
--no-watch Disable file watching
|
|
68
|
+
--spa Enable SPA mode (fallback to index.html)
|
|
69
|
+
--quiet Suppress request logging
|
|
70
|
+
--qr Show QR code for network URL
|
|
71
|
+
--https Enable local HTTPS
|
|
72
|
+
--proxy <rule> Add a proxy rule (e.g. /api=http://localhost:5000)
|
|
73
|
+
--mock <dir> Enable API mocking from directory
|
|
74
|
+
-h, --help Show this help message
|
|
75
|
+
-v, --version Show version number
|
|
76
|
+
|
|
77
|
+
Examples:
|
|
78
|
+
busyserver Serve current directory
|
|
79
|
+
busyserver ./website Serve ./website directory
|
|
80
|
+
busyserver -p 8080 --qr Serve on port 8080 with QR
|
|
81
|
+
busyserver --proxy /api=http://... Proxy /api to another server
|
|
82
|
+
|
|
83
|
+
Config:
|
|
84
|
+
Place a .busyserverc.json in your project root for persistent settings.
|
|
85
|
+
CLI arguments override config file values.
|
|
86
|
+
`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// โโ Parse Arguments โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
90
|
+
|
|
91
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
92
|
+
showHelp();
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (args.includes("-v") || args.includes("--version")) {
|
|
97
|
+
console.log(`busyserver v${pkg.version}`);
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let directory = null;
|
|
102
|
+
let cliPort = null;
|
|
103
|
+
let cliHost = null;
|
|
104
|
+
let explicitPort = false;
|
|
105
|
+
let cliOpen = null;
|
|
106
|
+
let cliReload = null;
|
|
107
|
+
let cliWatch = null;
|
|
108
|
+
let cliSpa = null;
|
|
109
|
+
let cliQuiet = null;
|
|
110
|
+
let cliQr = null;
|
|
111
|
+
let cliHttps = null;
|
|
112
|
+
let cliMock = null;
|
|
113
|
+
let cliProxy = {};
|
|
114
|
+
|
|
115
|
+
for (let i = 0; i < args.length; i++) {
|
|
116
|
+
const arg = args[i];
|
|
117
|
+
|
|
118
|
+
if (arg === "-p" || arg === "--port") {
|
|
119
|
+
const value = args[i + 1];
|
|
120
|
+
if (!value || value.startsWith("-")) {
|
|
121
|
+
console.error("โ Port number missing after " + arg);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
const num = Number(value);
|
|
125
|
+
if (!Number.isInteger(num) || num < 1 || num > 65535) {
|
|
126
|
+
console.error(`โ Invalid port number: \${value}`);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
cliPort = num;
|
|
130
|
+
explicitPort = true;
|
|
131
|
+
i++;
|
|
132
|
+
} else if (arg === "--host") {
|
|
133
|
+
const value = args[i + 1];
|
|
134
|
+
if (!value || value.startsWith("-")) {
|
|
135
|
+
console.error("โ Host address missing after --host");
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
cliHost = value;
|
|
139
|
+
i++;
|
|
140
|
+
} else if (arg === "--proxy") {
|
|
141
|
+
const value = args[i + 1];
|
|
142
|
+
if (!value || value.startsWith("-") || !value.includes("=")) {
|
|
143
|
+
console.error("โ Proxy rule must be in format /path=http://target");
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
const [pathPrefix, target] = value.split("=");
|
|
147
|
+
cliProxy[pathPrefix] = target;
|
|
148
|
+
i++;
|
|
149
|
+
} else if (arg === "--mock") {
|
|
150
|
+
const value = args[i + 1];
|
|
151
|
+
if (!value || value.startsWith("-")) {
|
|
152
|
+
console.error("โ Mock directory missing after --mock");
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
cliMock = value;
|
|
156
|
+
i++;
|
|
157
|
+
} else if (arg === "--open") {
|
|
158
|
+
cliOpen = true;
|
|
159
|
+
} else if (arg === "--no-reload") {
|
|
160
|
+
cliReload = false;
|
|
161
|
+
} else if (arg === "--no-watch") {
|
|
162
|
+
cliWatch = false;
|
|
163
|
+
} else if (arg === "--spa") {
|
|
164
|
+
cliSpa = true;
|
|
165
|
+
} else if (arg === "--quiet") {
|
|
166
|
+
cliQuiet = true;
|
|
167
|
+
} else if (arg === "--qr") {
|
|
168
|
+
cliQr = true;
|
|
169
|
+
} else if (arg === "--https") {
|
|
170
|
+
cliHttps = true;
|
|
171
|
+
} else if (arg.startsWith("-")) {
|
|
172
|
+
console.error(`โ Unknown option: \${arg}`);
|
|
173
|
+
console.log("Use 'busyserver --help' for available options.");
|
|
174
|
+
process.exit(1);
|
|
175
|
+
} else {
|
|
176
|
+
if (directory !== null) {
|
|
177
|
+
console.error(`โ Unexpected argument: \${arg}`);
|
|
178
|
+
console.log("Use 'busyserver --help' for available options.");
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
directory = arg;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// โโ Resolve Directory โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
186
|
+
|
|
187
|
+
let resolvedDir;
|
|
188
|
+
|
|
189
|
+
if (directory) {
|
|
190
|
+
if (directory.startsWith("~")) {
|
|
191
|
+
directory = path.join(os.homedir(), directory.slice(1));
|
|
192
|
+
}
|
|
193
|
+
resolvedDir = path.resolve(process.cwd(), directory);
|
|
194
|
+
} else {
|
|
195
|
+
resolvedDir = process.cwd();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const stat = fs.statSync(resolvedDir);
|
|
200
|
+
if (!stat.isDirectory()) {
|
|
201
|
+
console.error(`โ Path is not a directory: \${resolvedDir}`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error.code === "ENOENT") {
|
|
206
|
+
console.error(`โ Directory not found: \${resolvedDir}`);
|
|
207
|
+
} else {
|
|
208
|
+
console.error(`โ Cannot access directory: \${resolvedDir}`);
|
|
209
|
+
}
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// โโ Load Config โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
214
|
+
|
|
215
|
+
const config = loadConfig(resolvedDir);
|
|
216
|
+
|
|
217
|
+
// Merge proxy config
|
|
218
|
+
const finalProxy = { ...(config.proxy || {}), ...cliProxy };
|
|
219
|
+
|
|
220
|
+
// โโ Merge: defaults < config < CLI โโโโโโโโโโโโโโโโโโ
|
|
221
|
+
|
|
222
|
+
const options = {
|
|
223
|
+
root: resolvedDir,
|
|
224
|
+
port: cliPort !== null ? cliPort : config.port,
|
|
225
|
+
host: cliHost !== null ? cliHost : config.host,
|
|
226
|
+
autoPort: !explicitPort,
|
|
227
|
+
reload: cliReload !== null ? cliReload : config.reload,
|
|
228
|
+
watch: cliWatch !== null ? cliWatch : config.watch,
|
|
229
|
+
open: cliOpen !== null ? cliOpen : config.open,
|
|
230
|
+
spa: cliSpa !== null ? cliSpa : config.spa,
|
|
231
|
+
quiet: cliQuiet !== null ? cliQuiet : config.quiet,
|
|
232
|
+
qr: cliQr !== null ? cliQr : config.qr,
|
|
233
|
+
https: cliHttps !== null ? cliHttps : config.https,
|
|
234
|
+
mock: cliMock !== null ? cliMock : config.mock,
|
|
235
|
+
proxy: finalProxy,
|
|
236
|
+
compression: config.compression !== false,
|
|
237
|
+
headers: config.headers || {},
|
|
238
|
+
ignore: config.ignore || [],
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// โโ Start Server โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
242
|
+
|
|
243
|
+
startBusyServer(options).catch((error) => {
|
|
244
|
+
console.error(`โ Failed to start server: \${error.message}`);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "busyserver",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A lightweight live-reload development server for Termux โ the VS Code Live Server equivalent for your terminal.",
|
|
5
|
+
"main": "src/server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"busyserver": "bin/busyserver.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node bin/busyserver.js",
|
|
17
|
+
"test": "node --test test/server.test.js"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"termux",
|
|
21
|
+
"live-server",
|
|
22
|
+
"live-reload",
|
|
23
|
+
"dev-server",
|
|
24
|
+
"static-server",
|
|
25
|
+
"hot-reload",
|
|
26
|
+
"websocket",
|
|
27
|
+
"cli"
|
|
28
|
+
],
|
|
29
|
+
"author": "debuggersatpal",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"type": "commonjs",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18.0.0"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/debuggersatpal/busyserver.git"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"chokidar": "^5.0.0",
|
|
41
|
+
"qrcode-terminal": "^0.12.0",
|
|
42
|
+
"ws": "^8.21.3"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const DEFAULTS = {
|
|
5
|
+
port: 3000,
|
|
6
|
+
host: "0.0.0.0",
|
|
7
|
+
reload: true,
|
|
8
|
+
watch: true,
|
|
9
|
+
open: false,
|
|
10
|
+
spa: false,
|
|
11
|
+
quiet: false,
|
|
12
|
+
qr: false,
|
|
13
|
+
ignore: [],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const CONFIG_FILE = ".busyserverc.json";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Load configuration from .busyserverc.json in the given directory.
|
|
20
|
+
* Returns defaults merged with any file-based config.
|
|
21
|
+
*/
|
|
22
|
+
function loadConfig(directory) {
|
|
23
|
+
const configPath = path.join(directory, CONFIG_FILE);
|
|
24
|
+
const config = { ...DEFAULTS };
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const stat = fs.statSync(configPath);
|
|
28
|
+
if (!stat.isFile()) return config;
|
|
29
|
+
} catch {
|
|
30
|
+
return config;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
35
|
+
const parsed = JSON.parse(raw);
|
|
36
|
+
|
|
37
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
38
|
+
console.warn(`โ ๏ธ Invalid config in ${CONFIG_FILE}: expected an object`);
|
|
39
|
+
return config;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Merge only known keys
|
|
43
|
+
if (typeof parsed.port === "number") config.port = parsed.port;
|
|
44
|
+
if (typeof parsed.host === "string") config.host = parsed.host;
|
|
45
|
+
if (typeof parsed.reload === "boolean") config.reload = parsed.reload;
|
|
46
|
+
if (typeof parsed.watch === "boolean") config.watch = parsed.watch;
|
|
47
|
+
if (typeof parsed.open === "boolean") config.open = parsed.open;
|
|
48
|
+
if (typeof parsed.spa === "boolean") config.spa = parsed.spa;
|
|
49
|
+
if (typeof parsed.quiet === "boolean") config.quiet = parsed.quiet;
|
|
50
|
+
if (typeof parsed.qr === "boolean") config.qr = parsed.qr;
|
|
51
|
+
if (Array.isArray(parsed.ignore)) config.ignore = parsed.ignore;
|
|
52
|
+
|
|
53
|
+
return config;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.warn(`โ ๏ธ Failed to parse ${CONFIG_FILE}: ${error.message}`);
|
|
56
|
+
return config;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { loadConfig, DEFAULTS, CONFIG_FILE };
|
package/src/dashboard.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
function generateDashboard(options, wss, watcher) {
|
|
2
|
+
const uptime = process.uptime();
|
|
3
|
+
|
|
4
|
+
return `<!DOCTYPE html>
|
|
5
|
+
<html lang="en">
|
|
6
|
+
<head>
|
|
7
|
+
<meta charset="UTF-8">
|
|
8
|
+
<title>BusyServer Dashboard</title>
|
|
9
|
+
<style>
|
|
10
|
+
body { font-family: system-ui; background: #0f172a; color: #f8fafc; padding: 2rem; margin: 0; }
|
|
11
|
+
.card { background: #1e293b; padding: 1.5rem; border-radius: 8px; margin-bottom: 1rem; border: 1px solid #334155; }
|
|
12
|
+
h1 { margin-top: 0; color: #38bdf8; }
|
|
13
|
+
.stat { display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid #334155; }
|
|
14
|
+
.stat:last-child { border: none; }
|
|
15
|
+
</style>
|
|
16
|
+
</head>
|
|
17
|
+
<body>
|
|
18
|
+
<div class="card">
|
|
19
|
+
<h1>BusyServer Dashboard</h1>
|
|
20
|
+
<div class="stat"><span>Root</span> <span>${options.root}</span></div>
|
|
21
|
+
<div class="stat"><span>Port</span> <span>${options.port}</span></div>
|
|
22
|
+
<div class="stat"><span>Live Reload</span> <span>${options.reload ? "ON" : "OFF"}</span></div>
|
|
23
|
+
<div class="stat"><span>Watcher</span> <span>${options.watch ? "ON" : "OFF"}</span></div>
|
|
24
|
+
<div class="stat"><span>SPA Mode</span> <span>${options.spa ? "ON" : "OFF"}</span></div>
|
|
25
|
+
<div class="stat"><span>Uptime</span> <span>${Math.floor(uptime)}s</span></div>
|
|
26
|
+
<div class="stat"><span>Connected Browsers</span> <span>${wss ? wss.clients.size : 0}</span></div>
|
|
27
|
+
</div>
|
|
28
|
+
</body>
|
|
29
|
+
</html>`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { generateDashboard };
|
package/src/mock.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
|
|
4
|
+
function handleMock(req, res, options) {
|
|
5
|
+
if (!options.mock) return false;
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const rawPath = req.url.split("?")[0];
|
|
9
|
+
const mockDir = path.resolve(process.cwd(), options.mock);
|
|
10
|
+
|
|
11
|
+
// Try Method-specific mock: mock/api/users.GET.json
|
|
12
|
+
let mockFile = path.join(mockDir, rawPath + "." + req.method + ".json");
|
|
13
|
+
if (fs.existsSync(mockFile) && fs.statSync(mockFile).isFile()) {
|
|
14
|
+
const data = fs.readFileSync(mockFile, "utf-8");
|
|
15
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
16
|
+
res.end(data);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Convert e.g. /api/users to mock/api/users.json
|
|
21
|
+
mockFile = path.join(mockDir, rawPath + ".json");
|
|
22
|
+
if (fs.existsSync(mockFile) && fs.statSync(mockFile).isFile()) {
|
|
23
|
+
const data = fs.readFileSync(mockFile, "utf-8");
|
|
24
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
25
|
+
res.end(data);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Try mock/api/users/index.json
|
|
30
|
+
mockFile = path.join(mockDir, rawPath, "index.json");
|
|
31
|
+
if (fs.existsSync(mockFile) && fs.statSync(mockFile).isFile()) {
|
|
32
|
+
const data = fs.readFileSync(mockFile, "utf-8");
|
|
33
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
34
|
+
res.end(data);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
} catch (err) {
|
|
39
|
+
// ignore
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { handleMock };
|
package/src/proxy.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const http = require("http");
|
|
2
|
+
const https = require("https");
|
|
3
|
+
|
|
4
|
+
function handleProxy(req, res, options) {
|
|
5
|
+
const proxyRules = options.proxy;
|
|
6
|
+
if (!proxyRules) return false;
|
|
7
|
+
|
|
8
|
+
const urlPath = req.url.split("?")[0];
|
|
9
|
+
let targetPrefix = null;
|
|
10
|
+
let targetUrl = null;
|
|
11
|
+
|
|
12
|
+
for (const [prefix, target] of Object.entries(proxyRules)) {
|
|
13
|
+
if (urlPath.startsWith(prefix)) {
|
|
14
|
+
targetPrefix = prefix;
|
|
15
|
+
targetUrl = target;
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!targetUrl) return false;
|
|
21
|
+
|
|
22
|
+
const client = targetUrl.startsWith("https") ? https : http;
|
|
23
|
+
const proxyUrl = new URL(targetUrl);
|
|
24
|
+
// Remove prefix if you want, but standard proxy often just forwards the path.
|
|
25
|
+
// Actually, standard proxy forwards the full path unless specified. Let's forward full path.
|
|
26
|
+
const proxyReq = client.request({
|
|
27
|
+
hostname: proxyUrl.hostname,
|
|
28
|
+
port: proxyUrl.port,
|
|
29
|
+
path: req.url,
|
|
30
|
+
method: req.method,
|
|
31
|
+
headers: { ...req.headers, host: proxyUrl.hostname }
|
|
32
|
+
}, (proxyRes) => {
|
|
33
|
+
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
34
|
+
proxyRes.pipe(res);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
proxyReq.on("error", (err) => {
|
|
38
|
+
res.writeHead(502);
|
|
39
|
+
res.end("502 - Bad Gateway");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
req.pipe(proxyReq);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { handleProxy };
|