@schmock/cli 2.2.2 → 2.3.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/dist/bin.js +148 -31813
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +139 -51
- package/dist/index.js +143 -31813
- package/package.json +5 -5
package/dist/cli.d.ts
CHANGED
|
@@ -25,7 +25,8 @@ export interface WatchHandle {
|
|
|
25
25
|
close(): void;
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
28
|
+
* Prepare a replacement before closing the live server, then bind it to the
|
|
29
|
+
* same port. Invalid spec changes therefore leave the current server online.
|
|
29
30
|
*/
|
|
30
31
|
export declare function reloadServer(current: CliServer, options: CliOptions): Promise<CliServer>;
|
|
31
32
|
/**
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAkB,MAAM,WAAW,CAAC;AAcxD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,IAAI,IAAI,CAAC;CACf;AA6QD,wBAAsB,eAAe,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAG7E;AAYD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAqC3E;AAqBD,MAAM,WAAW,WAAW;IAC1B,KAAK,IAAI,IAAI,CAAC;CACf;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,SAAS,EAClB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,SAAS,CAAC,CAWpB;AAED;;GAEG;AACH,wBAAgB,UAAU,CACxB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,UAAU,EACnB,gBAAgB,EAAE,MAAM,SAAS,EACjC,QAAQ,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,GACpC,WAAW,CAqCb;AAED,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAiDvD"}
|
package/dist/cli.js
CHANGED
|
@@ -2,13 +2,27 @@
|
|
|
2
2
|
import { readFileSync, watch } from "node:fs";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
|
-
import { collectBody, parseNodeHeaders, parseNodeQuery, schmock,
|
|
5
|
+
import { collectBody, HTTP_METHODS, isHttpMethod, parseNodeHeaders, parseNodeQuery, schmock, writeSchmockResponse, } from "@schmock/core";
|
|
6
6
|
import { openapi } from "@schmock/openapi";
|
|
7
|
+
const MAX_REQUEST_BODY_SIZE = 10 * 1024 * 1024;
|
|
8
|
+
const ALLOWED_METHODS = HTTP_METHODS.join(", ");
|
|
7
9
|
const CORS_HEADERS = {
|
|
8
10
|
"access-control-allow-origin": "*",
|
|
9
|
-
"access-control-allow-methods":
|
|
11
|
+
"access-control-allow-methods": ALLOWED_METHODS,
|
|
10
12
|
"access-control-allow-headers": "Content-Type, Authorization",
|
|
11
13
|
};
|
|
14
|
+
class CliHttpError extends Error {
|
|
15
|
+
status;
|
|
16
|
+
code;
|
|
17
|
+
headers;
|
|
18
|
+
constructor(status, code, message, headers = {}) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.status = status;
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.headers = headers;
|
|
23
|
+
this.name = "CliHttpError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
12
26
|
function isSeedSource(value) {
|
|
13
27
|
return (Array.isArray(value) ||
|
|
14
28
|
typeof value === "string" ||
|
|
@@ -55,7 +69,95 @@ function handleAdminRequest(method, path, mock, res, headers) {
|
|
|
55
69
|
res.writeHead(404, headers);
|
|
56
70
|
res.end(JSON.stringify({ error: "Unknown admin endpoint", code: "NOT_FOUND" }));
|
|
57
71
|
}
|
|
58
|
-
|
|
72
|
+
function parseRequestUrl(req) {
|
|
73
|
+
const host = req.headers.host;
|
|
74
|
+
if (!host) {
|
|
75
|
+
throw new CliHttpError(400, "BAD_REQUEST", "Missing Host header");
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
return new URL(req.url ?? "/", `http://${host}`);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
throw new CliHttpError(400, "BAD_REQUEST", "Malformed request target");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function parseRequestMethod(req) {
|
|
85
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
86
|
+
if (!isHttpMethod(method)) {
|
|
87
|
+
throw new CliHttpError(405, "METHOD_NOT_ALLOWED", `Unsupported HTTP method: ${method}`, { allow: ALLOWED_METHODS });
|
|
88
|
+
}
|
|
89
|
+
return method;
|
|
90
|
+
}
|
|
91
|
+
function assertBodySize(headers) {
|
|
92
|
+
const contentLength = headers["content-length"];
|
|
93
|
+
if (contentLength === undefined)
|
|
94
|
+
return;
|
|
95
|
+
const declaredSize = Number(contentLength);
|
|
96
|
+
if (Number.isFinite(declaredSize) && declaredSize > MAX_REQUEST_BODY_SIZE) {
|
|
97
|
+
throw new CliHttpError(413, "PAYLOAD_TOO_LARGE", "Request body too large");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function toCliHttpError(error) {
|
|
101
|
+
if (error instanceof CliHttpError)
|
|
102
|
+
return error;
|
|
103
|
+
if (error !== null &&
|
|
104
|
+
typeof error === "object" &&
|
|
105
|
+
"status" in error &&
|
|
106
|
+
error.status === 413) {
|
|
107
|
+
return new CliHttpError(413, "PAYLOAD_TOO_LARGE", error instanceof Error ? error.message : "Request body too large");
|
|
108
|
+
}
|
|
109
|
+
return new CliHttpError(500, "SERVER_ERROR", error instanceof Error ? error.message : "Internal Server Error");
|
|
110
|
+
}
|
|
111
|
+
async function handleCliRequest(req, res, mock, options) {
|
|
112
|
+
try {
|
|
113
|
+
const url = parseRequestUrl(req);
|
|
114
|
+
const method = parseRequestMethod(req);
|
|
115
|
+
const path = url.pathname;
|
|
116
|
+
if (options.cors && method === "OPTIONS") {
|
|
117
|
+
res.writeHead(204, CORS_HEADERS);
|
|
118
|
+
res.end();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (options.admin && path.startsWith("/schmock-admin/")) {
|
|
122
|
+
const adminHeaders = {
|
|
123
|
+
"content-type": "application/json",
|
|
124
|
+
...(options.cors ? CORS_HEADERS : {}),
|
|
125
|
+
};
|
|
126
|
+
handleAdminRequest(method, path, mock, res, adminHeaders);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const headers = parseNodeHeaders(req);
|
|
130
|
+
assertBodySize(headers);
|
|
131
|
+
const query = parseNodeQuery(url);
|
|
132
|
+
const body = await collectBody(req, headers, MAX_REQUEST_BODY_SIZE);
|
|
133
|
+
const schmockResponse = await mock.handle(method, path, {
|
|
134
|
+
headers,
|
|
135
|
+
body,
|
|
136
|
+
query,
|
|
137
|
+
});
|
|
138
|
+
const extraHeaders = options.cors ? CORS_HEADERS : undefined;
|
|
139
|
+
writeSchmockResponse(res, schmockResponse, extraHeaders);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
const cliError = toCliHttpError(error);
|
|
143
|
+
try {
|
|
144
|
+
if (!res.headersSent) {
|
|
145
|
+
res.writeHead(cliError.status, {
|
|
146
|
+
"content-type": "application/json",
|
|
147
|
+
...(options.cors ? CORS_HEADERS : {}),
|
|
148
|
+
...cliError.headers,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (!res.writableEnded) {
|
|
152
|
+
res.end(JSON.stringify({ error: cliError.message, code: cliError.code }));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
res.destroy();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async function createCliMock(options) {
|
|
59
161
|
const mock = schmock({ debug: options.debug, state: {} });
|
|
60
162
|
const openapiOptions = {
|
|
61
163
|
spec: options.spec,
|
|
@@ -67,47 +169,15 @@ export async function createCliServer(options) {
|
|
|
67
169
|
}
|
|
68
170
|
const plugin = await openapi(openapiOptions);
|
|
69
171
|
mock.pipe(plugin);
|
|
172
|
+
return mock;
|
|
173
|
+
}
|
|
174
|
+
function listenWithMock(options, mock) {
|
|
70
175
|
const hostname = options.hostname ?? "127.0.0.1";
|
|
71
176
|
const port = options.port ?? 3000;
|
|
72
177
|
const cors = options.cors ?? false;
|
|
73
178
|
const admin = options.admin ?? false;
|
|
74
179
|
const httpServer = createServer((req, res) => {
|
|
75
|
-
|
|
76
|
-
if (cors && req.method === "OPTIONS") {
|
|
77
|
-
res.writeHead(204, CORS_HEADERS);
|
|
78
|
-
res.end();
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
82
|
-
const path = url.pathname;
|
|
83
|
-
// Admin API intercept
|
|
84
|
-
if (admin && path.startsWith("/schmock-admin/")) {
|
|
85
|
-
const adminHeaders = {
|
|
86
|
-
"content-type": "application/json",
|
|
87
|
-
...(cors ? CORS_HEADERS : {}),
|
|
88
|
-
};
|
|
89
|
-
handleAdminRequest(req.method ?? "GET", path, mock, res, adminHeaders);
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
const method = toHttpMethod(req.method ?? "GET");
|
|
93
|
-
const headers = parseNodeHeaders(req);
|
|
94
|
-
const query = parseNodeQuery(url);
|
|
95
|
-
void collectBody(req, headers)
|
|
96
|
-
.then((body) => mock
|
|
97
|
-
.handle(method, path, { headers, body, query })
|
|
98
|
-
.then((schmockResponse) => {
|
|
99
|
-
const extra = cors ? CORS_HEADERS : undefined;
|
|
100
|
-
writeSchmockResponse(res, schmockResponse, extra);
|
|
101
|
-
}))
|
|
102
|
-
.catch((error) => {
|
|
103
|
-
if (!res.headersSent) {
|
|
104
|
-
res.writeHead(500, { "content-type": "application/json" });
|
|
105
|
-
}
|
|
106
|
-
res.end(JSON.stringify({
|
|
107
|
-
error: error instanceof Error ? error.message : "Internal Server Error",
|
|
108
|
-
code: "SERVER_ERROR",
|
|
109
|
-
}));
|
|
110
|
-
});
|
|
180
|
+
void handleCliRequest(req, res, mock, { admin, cors });
|
|
111
181
|
});
|
|
112
182
|
return new Promise((resolve, reject) => {
|
|
113
183
|
httpServer.on("error", reject);
|
|
@@ -125,6 +195,10 @@ export async function createCliServer(options) {
|
|
|
125
195
|
});
|
|
126
196
|
});
|
|
127
197
|
}
|
|
198
|
+
export async function createCliServer(options) {
|
|
199
|
+
const mock = await createCliMock(options);
|
|
200
|
+
return listenWithMock(options, mock);
|
|
201
|
+
}
|
|
128
202
|
function validatePort(value) {
|
|
129
203
|
const port = Number(value);
|
|
130
204
|
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
@@ -186,39 +260,53 @@ Options:
|
|
|
186
260
|
`;
|
|
187
261
|
const WATCH_DEBOUNCE_MS = 500;
|
|
188
262
|
/**
|
|
189
|
-
*
|
|
263
|
+
* Prepare a replacement before closing the live server, then bind it to the
|
|
264
|
+
* same port. Invalid spec changes therefore leave the current server online.
|
|
190
265
|
*/
|
|
191
266
|
export async function reloadServer(current, options) {
|
|
192
267
|
const port = current.port;
|
|
268
|
+
const nextOptions = { ...options, port };
|
|
269
|
+
const replacementMock = await createCliMock(nextOptions);
|
|
193
270
|
// Close existing connections and wait for the server to fully close
|
|
194
271
|
current.server.closeAllConnections();
|
|
195
272
|
await new Promise((resolve) => {
|
|
196
273
|
current.server.close(() => resolve());
|
|
197
274
|
});
|
|
198
|
-
return
|
|
275
|
+
return listenWithMock(nextOptions, replacementMock);
|
|
199
276
|
}
|
|
200
277
|
/**
|
|
201
278
|
* Watch a spec file and hot-reload the server on changes.
|
|
202
279
|
*/
|
|
203
280
|
export function startWatch(specPath, options, getCurrentServer, onReload) {
|
|
204
281
|
let debounceTimer;
|
|
282
|
+
let closed = false;
|
|
283
|
+
let reloadQueue = Promise.resolve();
|
|
205
284
|
const watcher = watch(specPath, () => {
|
|
206
285
|
if (debounceTimer)
|
|
207
286
|
clearTimeout(debounceTimer);
|
|
208
|
-
debounceTimer = setTimeout(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
287
|
+
debounceTimer = setTimeout(() => {
|
|
288
|
+
reloadQueue = reloadQueue.then(async () => {
|
|
289
|
+
if (closed)
|
|
290
|
+
return;
|
|
291
|
+
process.stderr.write("\nSpec changed, reloading...\n");
|
|
292
|
+
try {
|
|
293
|
+
const newServer = await reloadServer(getCurrentServer(), options);
|
|
294
|
+
if (closed) {
|
|
295
|
+
newServer.close();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
onReload(newServer);
|
|
299
|
+
process.stderr.write(`Schmock server reloaded on http://${newServer.hostname}:${newServer.port}\n`);
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
process.stderr.write(`Reload failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
303
|
+
}
|
|
304
|
+
});
|
|
218
305
|
}, WATCH_DEBOUNCE_MS);
|
|
219
306
|
});
|
|
220
307
|
return {
|
|
221
308
|
close() {
|
|
309
|
+
closed = true;
|
|
222
310
|
watcher.close();
|
|
223
311
|
if (debounceTimer)
|
|
224
312
|
clearTimeout(debounceTimer);
|