yxorp 0.2.0 → 0.2.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.
- package/README.md +23 -41
- package/dist/index.js +62 -20
- package/dist/middleware/mock.middleware.js +3 -2
- package/dist/middleware/proxyRes.middleware.d.ts +1 -1
- package/dist/middleware/proxyRes.middleware.js +7 -1
- package/dist/middleware/rewrite.middleware.js +3 -2
- package/dist/middleware/static.middleware.js +2 -5
- package/dist/services/cli-overrides.d.ts +7 -0
- package/dist/services/cli-overrides.js +25 -0
- package/dist/services/config-resolver.d.ts +1 -0
- package/dist/services/config-resolver.js +3 -0
- package/dist/services/config-validator.d.ts +9 -0
- package/dist/services/config-validator.js +38 -0
- package/dist/services/config-watcher.d.ts +4 -0
- package/dist/services/config-watcher.js +27 -0
- package/dist/services/http-server.service.js +1 -0
- package/dist/types/yxorp-config.d.ts +0 -1
- package/dist/utils/request-timing.d.ts +6 -0
- package/dist/utils/request-timing.js +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ Yxorp looks for its config in this order:
|
|
|
47
47
|
| 2 | `./yxorp.json` | In your project root |
|
|
48
48
|
| 3 | `./.yxorp/settings.json` | Inside a hidden `.yxorp` directory |
|
|
49
49
|
|
|
50
|
-
When config lives inside `.yxorp/`, all relative paths (
|
|
50
|
+
When config lives inside `.yxorp/`, all relative paths (mock files, rewrite files, static directories) are resolved relative to that directory. This keeps things tidy:
|
|
51
51
|
|
|
52
52
|
```
|
|
53
53
|
my-project/
|
|
@@ -59,6 +59,14 @@ my-project/
|
|
|
59
59
|
logo.svg
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
### CLI overrides
|
|
63
|
+
|
|
64
|
+
`--port` and `--target` override the corresponding config values without touching the file — handy for one-off runs or scripting:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
yxorp --port 4000 --target http://localhost:8080
|
|
68
|
+
```
|
|
69
|
+
|
|
62
70
|
---
|
|
63
71
|
|
|
64
72
|
## Config Reference
|
|
@@ -96,40 +104,6 @@ Extra HTTP headers to send with every proxied request to the target server. Usef
|
|
|
96
104
|
|
|
97
105
|
---
|
|
98
106
|
|
|
99
|
-
### `scripts`
|
|
100
|
-
|
|
101
|
-
Additional JavaScript files loaded **once at startup**. Use them to set up shared data or helpers for your mock/rewrite scripts.
|
|
102
|
-
|
|
103
|
-
Why `scripts` and not just `require()` inside each mock file? Because mock scripts are hot-reloaded on every request (their module cache is cleared). Any data they `require()` would also need to be re-imported. Scripts loaded via the `scripts` array sidestep this — they run once at startup and can stash data on `globalThis` for all mock/rewrite scripts to use.
|
|
104
|
-
|
|
105
|
-
```json
|
|
106
|
-
"scripts": [
|
|
107
|
-
"./scripts/seed-data.js"
|
|
108
|
-
]
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
```javascript
|
|
112
|
-
// scripts/seed-data.js — runs once, survives hot-reload
|
|
113
|
-
globalThis.users = [
|
|
114
|
-
{ id: 1, name: 'Alice' },
|
|
115
|
-
{ id: 2, name: 'Bob' },
|
|
116
|
-
];
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
```javascript
|
|
120
|
-
// mock/user.js — hot-reloaded on every request, but seed-data is untouched
|
|
121
|
-
module.exports = (req, res) => {
|
|
122
|
-
const user = globalThis.users.find(u => u.id === Number(req.params.id));
|
|
123
|
-
res.end(JSON.stringify(user));
|
|
124
|
-
};
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
As a rule of thumb:
|
|
128
|
-
- **`scripts`** — for data you initialize once (lookup tables, config, seed data)
|
|
129
|
-
- **`require()` inside mock/rewrite files** — for utility helpers you want imported fresh each time
|
|
130
|
-
|
|
131
|
-
---
|
|
132
|
-
|
|
133
107
|
### Mock Rules (`mockRules`)
|
|
134
108
|
|
|
135
109
|
Intercept a matching request **before** it reaches the target and respond immediately. The target server never sees it.
|
|
@@ -155,7 +129,7 @@ Intercept a matching request **before** it reaches the target and respond immedi
|
|
|
155
129
|
}
|
|
156
130
|
```
|
|
157
131
|
|
|
158
|
-
The script receives `req` and `res` and
|
|
132
|
+
The script receives the raw Node.js `req` (`IncomingMessage`) and `res` (`ServerResponse`) — **just like writing a tiny backend**. There's no magic layered on top: you're fully responsible for setting the status code, headers, and body yourself, exactly as the real target server would.
|
|
159
133
|
|
|
160
134
|
```javascript
|
|
161
135
|
// ./mock/create-user.js
|
|
@@ -166,6 +140,10 @@ module.exports = (req, res) => {
|
|
|
166
140
|
};
|
|
167
141
|
```
|
|
168
142
|
|
|
143
|
+
If you don't set `res.statusCode`, it defaults to Node's `200`. If you don't call `res.end()`, the response hangs — Yxorp won't do it for you. The only assist it provides: if the script finishes without sending headers, Yxorp sets `content-type: application/json` for you (handy for quick `res.end(JSON.stringify(...))` one-liners).
|
|
144
|
+
|
|
145
|
+
This mirrors how a real backend works on purpose — mock scripts are meant to **stand in for the target server**, so the same rules apply: you decide what the client receives, down to the last header.
|
|
146
|
+
|
|
169
147
|
**Hot-reload is built-in** — edit the script file and the next request picks up changes automatically. No restart needed.
|
|
170
148
|
|
|
171
149
|
#### Disable a rule:
|
|
@@ -295,10 +273,6 @@ Here's a complete config showing all features in action:
|
|
|
295
273
|
"target": "https://api.example.com",
|
|
296
274
|
"proxyPort": 3000,
|
|
297
275
|
|
|
298
|
-
"scripts": [
|
|
299
|
-
"./scripts/globals.js"
|
|
300
|
-
],
|
|
301
|
-
|
|
302
276
|
"staticRules": [
|
|
303
277
|
{
|
|
304
278
|
"path": "/assets",
|
|
@@ -341,7 +315,15 @@ Here's a complete config showing all features in action:
|
|
|
341
315
|
|
|
342
316
|
---
|
|
343
317
|
|
|
344
|
-
##
|
|
318
|
+
## Hot-Reload
|
|
319
|
+
|
|
320
|
+
### Config
|
|
321
|
+
|
|
322
|
+
Yxorp watches its config file at runtime. **Save the file — changes take effect on the next request.** No restart needed.
|
|
323
|
+
|
|
324
|
+
This works for all config fields: mock rules, rewrite rules, remote rules, static rules, and headers. The proxy server keeps running and the new config is applied immediately.
|
|
325
|
+
|
|
326
|
+
### Scripts
|
|
345
327
|
|
|
346
328
|
Mock and rewrite scripts are reloaded on every request — **just save the file and the next request uses the new code**. No need to restart Yxorp.
|
|
347
329
|
|
package/dist/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
|
-
var _a;
|
|
6
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const config_service_1 = require("./services/config.service");
|
|
9
9
|
const logger_service_1 = require("./services/logger.service");
|
|
@@ -12,48 +12,90 @@ const rewrite_rules_matcher_service_1 = require("./services/rules-matchers/rewri
|
|
|
12
12
|
const mock_rules_matcher_service_1 = require("./services/rules-matchers/mock-rules-matcher.service");
|
|
13
13
|
const yxorp_server_service_1 = require("./services/yxorp-server.service");
|
|
14
14
|
const config_resolver_1 = require("./services/config-resolver");
|
|
15
|
+
const config_watcher_1 = require("./services/config-watcher");
|
|
16
|
+
const config_validator_1 = require("./services/config-validator");
|
|
17
|
+
const cli_overrides_1 = require("./services/cli-overrides");
|
|
18
|
+
// --- Version flag ---
|
|
19
|
+
if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
20
|
+
const pkgPath = path_1.default.join(__dirname, '../package.json');
|
|
21
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf-8'));
|
|
22
|
+
console.log(pkg.version);
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
15
25
|
// --- Config resolution ---
|
|
16
26
|
let configDir;
|
|
27
|
+
let configPath;
|
|
17
28
|
let config;
|
|
18
29
|
try {
|
|
19
30
|
const resolved = (0, config_resolver_1.resolveConfig)(process.cwd(), process.argv);
|
|
20
31
|
configDir = resolved.configDir;
|
|
32
|
+
configPath = resolved.configPath;
|
|
21
33
|
config = resolved.config;
|
|
22
34
|
}
|
|
23
35
|
catch (e) {
|
|
24
36
|
console.error(e.message || e);
|
|
25
37
|
process.exit(1);
|
|
26
38
|
}
|
|
39
|
+
// --- CLI overrides ---
|
|
40
|
+
// e.g. `yxorp --port 4000 --target http://localhost:8080` — flags win over config values.
|
|
41
|
+
config = (0, cli_overrides_1.applyCliOverrides)(config, process.argv);
|
|
42
|
+
const configErrors = (0, config_validator_1.validateConfig)(config);
|
|
43
|
+
if (configErrors.length) {
|
|
44
|
+
console.error('Invalid config:');
|
|
45
|
+
configErrors.forEach(err => console.error(` - ${err}`));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
27
48
|
// Change CWD to config directory so relative paths in config work correctly
|
|
28
49
|
if (configDir !== process.cwd()) {
|
|
29
50
|
process.chdir(configDir);
|
|
30
51
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
52
|
+
function buildProxyConfig(cfg) {
|
|
53
|
+
const proxyOptions = {
|
|
54
|
+
target: cfg.target,
|
|
55
|
+
changeOrigin: true,
|
|
56
|
+
followRedirects: true,
|
|
57
|
+
secure: false,
|
|
58
|
+
ws: true,
|
|
59
|
+
selfHandleResponse: true,
|
|
60
|
+
};
|
|
61
|
+
if (cfg.proxyHeaders) {
|
|
62
|
+
proxyOptions.headers = cfg.proxyHeaders;
|
|
63
|
+
}
|
|
64
|
+
return { ...cfg, proxyOptions };
|
|
41
65
|
}
|
|
42
|
-
const proxyConfig = {
|
|
43
|
-
...config,
|
|
44
|
-
proxyOptions,
|
|
45
|
-
};
|
|
46
|
-
(_a = config === null || config === void 0 ? void 0 : config.scripts) === null || _a === void 0 ? void 0 : _a.forEach(script => {
|
|
47
|
-
require(path_1.default.resolve(script));
|
|
48
|
-
});
|
|
49
66
|
// Manual composition — no DI container
|
|
50
67
|
const logger = new logger_service_1.LoggerService();
|
|
51
68
|
const appConfig = new config_service_1.Config();
|
|
52
|
-
appConfig.set(
|
|
69
|
+
appConfig.set(buildProxyConfig(config));
|
|
53
70
|
const mockRulesMatcher = new mock_rules_matcher_service_1.MockRulesMatcher(appConfig);
|
|
54
71
|
const rewriteRulesMatcher = new rewrite_rules_matcher_service_1.RewriteRulesMatcher(appConfig);
|
|
55
72
|
const remoteRulesMatcher = new remote_rules_matcher_service_1.RemoteRulesMatcher(appConfig);
|
|
56
|
-
const { listen } = (0, yxorp_server_service_1.createServer)(appConfig, logger, rewriteRulesMatcher, mockRulesMatcher, remoteRulesMatcher);
|
|
73
|
+
const { server, listen } = (0, yxorp_server_service_1.createServer)(appConfig, logger, rewriteRulesMatcher, mockRulesMatcher, remoteRulesMatcher);
|
|
57
74
|
listen(config.proxyPort, () => {
|
|
58
75
|
console.log(`Yxorp server started successfully on http://localhost:${config.proxyPort}`);
|
|
59
76
|
});
|
|
77
|
+
// --- Config hot-reload ---
|
|
78
|
+
const configWatcher = (0, config_watcher_1.watchConfig)(configPath, (newConfig) => {
|
|
79
|
+
const errors = (0, config_validator_1.validateConfig)(newConfig);
|
|
80
|
+
if (errors.length) {
|
|
81
|
+
logger.error('Config reload skipped — invalid config:');
|
|
82
|
+
errors.forEach(err => logger.error(` - ${err}`));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
appConfig.set(buildProxyConfig(newConfig));
|
|
86
|
+
logger.info('Config reloaded');
|
|
87
|
+
}, (e) => logger.error(`Config reload failed: ${e.message || e}`));
|
|
88
|
+
// --- Graceful shutdown ---
|
|
89
|
+
let shuttingDown = false;
|
|
90
|
+
function shutdown(signal) {
|
|
91
|
+
if (shuttingDown)
|
|
92
|
+
return;
|
|
93
|
+
shuttingDown = true;
|
|
94
|
+
logger.info(`Received ${signal}, shutting down...`);
|
|
95
|
+
configWatcher.close();
|
|
96
|
+
server.close(() => {
|
|
97
|
+
process.exit(0);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
101
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
@@ -7,6 +7,7 @@ exports.MockMiddleware = void 0;
|
|
|
7
7
|
const mime_1 = __importDefault(require("mime"));
|
|
8
8
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const request_timing_1 = require("../utils/request-timing");
|
|
10
11
|
class MockMiddleware {
|
|
11
12
|
constructor(logger) {
|
|
12
13
|
this.logger = logger;
|
|
@@ -28,7 +29,7 @@ class MockMiddleware {
|
|
|
28
29
|
res.setHeader('content-type', 'application/json');
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
|
-
this.logger.info(`mock ${res.statusCode || 200} ${req.method} ${req.url}`);
|
|
32
|
+
this.logger.info(`mock ${res.statusCode || 200} ${req.method} ${req.url} ${(0, request_timing_1.elapsedMs)(req)}ms`);
|
|
32
33
|
return;
|
|
33
34
|
}
|
|
34
35
|
if ('file' in mockRule) {
|
|
@@ -40,7 +41,7 @@ class MockMiddleware {
|
|
|
40
41
|
}
|
|
41
42
|
res.setHeader('content-length', file.length);
|
|
42
43
|
res.end(file);
|
|
43
|
-
this.logger.info(`mock ${res.statusCode} ${req.method} ${req.url}`);
|
|
44
|
+
this.logger.info(`mock ${res.statusCode} ${req.method} ${req.url} ${(0, request_timing_1.elapsedMs)(req)}ms`);
|
|
44
45
|
return;
|
|
45
46
|
}
|
|
46
47
|
next();
|
|
@@ -5,5 +5,5 @@ import { LoggerService } from '../services/logger.service';
|
|
|
5
5
|
export declare class ProxyResMiddleware implements Middleware<[proxyRes: IncomingMessage, req: IncomingMessage, res: ServerResponse]> {
|
|
6
6
|
private logger;
|
|
7
7
|
constructor(logger: LoggerService);
|
|
8
|
-
use(proxyRes: IncomingMessage,
|
|
8
|
+
use(proxyRes: IncomingMessage, req: IncomingMessage, res: ServerResponse): void;
|
|
9
9
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ProxyResMiddleware = void 0;
|
|
4
|
+
const request_timing_1 = require("../utils/request-timing");
|
|
4
5
|
class ProxyResMiddleware {
|
|
5
6
|
constructor(logger) {
|
|
6
7
|
this.logger = logger;
|
|
7
8
|
}
|
|
8
|
-
use(proxyRes,
|
|
9
|
+
use(proxyRes, req, res) {
|
|
9
10
|
try {
|
|
10
11
|
// Skip transfer-encoding since we reconstruct the body with known length
|
|
11
12
|
for (let key in proxyRes.headers) {
|
|
@@ -19,6 +20,11 @@ class ProxyResMiddleware {
|
|
|
19
20
|
res.removeHeader('content-length');
|
|
20
21
|
res.setHeader('content-length', response.length);
|
|
21
22
|
res.end(response);
|
|
23
|
+
// RewriteMiddleware already logs rewritten responses — log here only for plain pass-through,
|
|
24
|
+
// so every proxied request gets exactly one log line.
|
|
25
|
+
if (!req.rewriteRule) {
|
|
26
|
+
this.logger.info(`proxy ${res.statusCode} ${req.method} ${req.url} ${(0, request_timing_1.elapsedMs)(req)}ms`);
|
|
27
|
+
}
|
|
22
28
|
}
|
|
23
29
|
catch (e) {
|
|
24
30
|
this.logger.error(e);
|
|
@@ -7,6 +7,7 @@ exports.RewriteMiddleware = void 0;
|
|
|
7
7
|
const http_encoding_1 = require("http-encoding");
|
|
8
8
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const request_timing_1 = require("../utils/request-timing");
|
|
10
11
|
class RewriteMiddleware {
|
|
11
12
|
constructor(logger) {
|
|
12
13
|
this.logger = logger;
|
|
@@ -29,7 +30,7 @@ class RewriteMiddleware {
|
|
|
29
30
|
const rewritedResponse = await handler(encodedResponse, proxyRes, req, res);
|
|
30
31
|
proxyRes.rawBody = await (0, http_encoding_1.encodeBuffer)(rewritedResponse, encoding);
|
|
31
32
|
}
|
|
32
|
-
this.logger.info(`rewrite ${proxyRes.statusCode} ${req.method} ${req.url}`);
|
|
33
|
+
this.logger.info(`rewrite ${proxyRes.statusCode} ${req.method} ${req.url} ${(0, request_timing_1.elapsedMs)(req)}ms`);
|
|
33
34
|
next();
|
|
34
35
|
return;
|
|
35
36
|
}
|
|
@@ -39,7 +40,7 @@ class RewriteMiddleware {
|
|
|
39
40
|
if (rewriteRule.statusCode) {
|
|
40
41
|
proxyRes.statusCode = rewriteRule.statusCode;
|
|
41
42
|
}
|
|
42
|
-
this.logger.info(`rewrite ${proxyRes.statusCode} ${req.method} ${req.url}`);
|
|
43
|
+
this.logger.info(`rewrite ${proxyRes.statusCode} ${req.method} ${req.url} ${(0, request_timing_1.elapsedMs)(req)}ms`);
|
|
43
44
|
next();
|
|
44
45
|
return;
|
|
45
46
|
}
|
|
@@ -7,6 +7,7 @@ exports.StaticMiddleware = void 0;
|
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
9
|
const mime_1 = __importDefault(require("mime"));
|
|
10
|
+
const request_timing_1 = require("../utils/request-timing");
|
|
10
11
|
class StaticMiddleware {
|
|
11
12
|
constructor(config, logger) {
|
|
12
13
|
this.config = config;
|
|
@@ -17,10 +18,6 @@ class StaticMiddleware {
|
|
|
17
18
|
const staticRules = this.config.get().staticRules || [];
|
|
18
19
|
const url = new URL(req.url || '', 'http://fake.com');
|
|
19
20
|
const urlPath = url.pathname;
|
|
20
|
-
if (!staticRules) {
|
|
21
|
-
next();
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
21
|
const currentStaticRule = staticRules.filter((staticRule) => {
|
|
25
22
|
return urlPath.startsWith(staticRule.path);
|
|
26
23
|
})[0];
|
|
@@ -59,7 +56,7 @@ class StaticMiddleware {
|
|
|
59
56
|
}
|
|
60
57
|
res.setHeader('content-length', file.length);
|
|
61
58
|
res.end(file);
|
|
62
|
-
this.logger.info(`static ${res.statusCode} ${req.method} ${req.url}`);
|
|
59
|
+
this.logger.info(`static ${res.statusCode} ${req.method} ${req.url} ${(0, request_timing_1.elapsedMs)(req)}ms`);
|
|
63
60
|
}
|
|
64
61
|
catch (e) {
|
|
65
62
|
this.logger.error(e);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ConfigFile } from '../types/yxorp-config';
|
|
2
|
+
/**
|
|
3
|
+
* Applies `--port`/`--target` CLI flags on top of a resolved config, without
|
|
4
|
+
* touching the config file. CLI flags win over config values. Returns a new
|
|
5
|
+
* object — the input config is left untouched.
|
|
6
|
+
*/
|
|
7
|
+
export declare function applyCliOverrides(config: ConfigFile, argv: string[]): ConfigFile;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyCliOverrides = void 0;
|
|
4
|
+
function getArgValue(argv, flag) {
|
|
5
|
+
const index = argv.indexOf(flag);
|
|
6
|
+
return index !== -1 && index + 1 < argv.length ? argv[index + 1] : undefined;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Applies `--port`/`--target` CLI flags on top of a resolved config, without
|
|
10
|
+
* touching the config file. CLI flags win over config values. Returns a new
|
|
11
|
+
* object — the input config is left untouched.
|
|
12
|
+
*/
|
|
13
|
+
function applyCliOverrides(config, argv) {
|
|
14
|
+
const portOverride = getArgValue(argv, '--port');
|
|
15
|
+
const targetOverride = getArgValue(argv, '--target');
|
|
16
|
+
if (portOverride === undefined && targetOverride === undefined) {
|
|
17
|
+
return config;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
...config,
|
|
21
|
+
...(portOverride !== undefined ? { proxyPort: portOverride } : {}),
|
|
22
|
+
...(targetOverride !== undefined ? { target: targetOverride } : {}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
exports.applyCliOverrides = applyCliOverrides;
|
|
@@ -20,6 +20,7 @@ function resolveConfig(cwd, argv) {
|
|
|
20
20
|
}
|
|
21
21
|
return {
|
|
22
22
|
configDir: path_1.default.dirname(resolved),
|
|
23
|
+
configPath: resolved,
|
|
23
24
|
config: JSON.parse(fs_1.default.readFileSync(resolved, 'utf-8')),
|
|
24
25
|
};
|
|
25
26
|
}
|
|
@@ -28,6 +29,7 @@ function resolveConfig(cwd, argv) {
|
|
|
28
29
|
if (fs_1.default.existsSync(rootConfig)) {
|
|
29
30
|
return {
|
|
30
31
|
configDir: cwd,
|
|
32
|
+
configPath: rootConfig,
|
|
31
33
|
config: JSON.parse(fs_1.default.readFileSync(rootConfig, 'utf-8')),
|
|
32
34
|
};
|
|
33
35
|
}
|
|
@@ -36,6 +38,7 @@ function resolveConfig(cwd, argv) {
|
|
|
36
38
|
if (fs_1.default.existsSync(dotConfig)) {
|
|
37
39
|
return {
|
|
38
40
|
configDir: dotDir,
|
|
41
|
+
configPath: dotConfig,
|
|
39
42
|
config: JSON.parse(fs_1.default.readFileSync(dotConfig, 'utf-8')),
|
|
40
43
|
};
|
|
41
44
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates a parsed config object and returns a list of human-readable
|
|
3
|
+
* error messages. An empty array means the config is valid.
|
|
4
|
+
*
|
|
5
|
+
* Intentionally permissive — it only checks the shape of fields that would
|
|
6
|
+
* otherwise cause confusing crashes deeper in the pipeline (e.g. a missing
|
|
7
|
+
* `target` blowing up inside httpxy with an opaque stack trace).
|
|
8
|
+
*/
|
|
9
|
+
export declare function validateConfig(config: any): string[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateConfig = void 0;
|
|
4
|
+
const RULE_ARRAYS = ['remoteRules', 'staticRules', 'mockRules', 'rewriteRules'];
|
|
5
|
+
/**
|
|
6
|
+
* Validates a parsed config object and returns a list of human-readable
|
|
7
|
+
* error messages. An empty array means the config is valid.
|
|
8
|
+
*
|
|
9
|
+
* Intentionally permissive — it only checks the shape of fields that would
|
|
10
|
+
* otherwise cause confusing crashes deeper in the pipeline (e.g. a missing
|
|
11
|
+
* `target` blowing up inside httpxy with an opaque stack trace).
|
|
12
|
+
*/
|
|
13
|
+
function validateConfig(config) {
|
|
14
|
+
const errors = [];
|
|
15
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
16
|
+
return ['Config must be a JSON object'];
|
|
17
|
+
}
|
|
18
|
+
if (typeof config.target !== 'string' || !config.target.trim()) {
|
|
19
|
+
errors.push('"target" is required and must be a non-empty string (e.g. "https://api.example.com")');
|
|
20
|
+
}
|
|
21
|
+
const { proxyPort } = config;
|
|
22
|
+
const isValidPort = (typeof proxyPort === 'number' && Number.isInteger(proxyPort) && proxyPort >= 0) ||
|
|
23
|
+
(typeof proxyPort === 'string' && proxyPort.trim() !== '' && Number.isInteger(Number(proxyPort)));
|
|
24
|
+
if (!isValidPort) {
|
|
25
|
+
errors.push('"proxyPort" is required and must be a number or numeric string (e.g. 3000)');
|
|
26
|
+
}
|
|
27
|
+
if (config.proxyHeaders !== undefined &&
|
|
28
|
+
(typeof config.proxyHeaders !== 'object' || config.proxyHeaders === null || Array.isArray(config.proxyHeaders))) {
|
|
29
|
+
errors.push('"proxyHeaders" must be an object of string key-value pairs');
|
|
30
|
+
}
|
|
31
|
+
for (const key of RULE_ARRAYS) {
|
|
32
|
+
if (config[key] !== undefined && !Array.isArray(config[key])) {
|
|
33
|
+
errors.push(`"${key}" must be an array`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return errors;
|
|
37
|
+
}
|
|
38
|
+
exports.validateConfig = validateConfig;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.watchConfig = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
function watchConfig(configPath, onReload, onError) {
|
|
9
|
+
let debounceTimer = null;
|
|
10
|
+
return fs_1.default.watch(configPath, () => {
|
|
11
|
+
if (debounceTimer)
|
|
12
|
+
return;
|
|
13
|
+
debounceTimer = setTimeout(() => {
|
|
14
|
+
debounceTimer = null;
|
|
15
|
+
try {
|
|
16
|
+
const raw = fs_1.default.readFileSync(configPath, 'utf-8');
|
|
17
|
+
if (!raw.trim())
|
|
18
|
+
return;
|
|
19
|
+
onReload(JSON.parse(raw));
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
onError(e);
|
|
23
|
+
}
|
|
24
|
+
}, 100);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
exports.watchConfig = watchConfig;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.elapsedMs = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Milliseconds elapsed since the request started (set by HttpServer on arrival).
|
|
6
|
+
* Falls back to 0 if startTime wasn't recorded for some reason.
|
|
7
|
+
*/
|
|
8
|
+
function elapsedMs(req) {
|
|
9
|
+
return req.startTime !== undefined ? Date.now() - req.startTime : 0;
|
|
10
|
+
}
|
|
11
|
+
exports.elapsedMs = elapsedMs;
|