@schmock/cli 1.1.1 → 1.4.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/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +109 -4
- package/dist/index.js +2745 -1499
- package/package.json +6 -6
- package/src/cli.test.ts +0 -71
- package/src/cli.ts +0 -241
- package/src/index.ts +0 -11
- package/src/steps/cli-standalone.steps.ts +0 -222
package/dist/cli.d.ts
CHANGED
|
@@ -6,6 +6,10 @@ export interface CliOptions {
|
|
|
6
6
|
seed?: string;
|
|
7
7
|
cors?: boolean;
|
|
8
8
|
debug?: boolean;
|
|
9
|
+
fakerSeed?: number;
|
|
10
|
+
errors?: boolean;
|
|
11
|
+
watch?: boolean;
|
|
12
|
+
admin?: boolean;
|
|
9
13
|
}
|
|
10
14
|
export interface CliServer {
|
|
11
15
|
server: Server;
|
|
@@ -17,5 +21,16 @@ export declare function createCliServer(options: CliOptions): Promise<CliServer>
|
|
|
17
21
|
export declare function parseCliArgs(args: string[]): CliOptions & {
|
|
18
22
|
help: boolean;
|
|
19
23
|
};
|
|
24
|
+
export interface WatchHandle {
|
|
25
|
+
close(): void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Reload a CLI server: close the old one, create a new one on the same port.
|
|
29
|
+
*/
|
|
30
|
+
export declare function reloadServer(current: CliServer, options: CliOptions): Promise<CliServer>;
|
|
31
|
+
/**
|
|
32
|
+
* Watch a spec file and hot-reload the server on changes.
|
|
33
|
+
*/
|
|
34
|
+
export declare function startWatch(specPath: string, options: CliOptions, getCurrentServer: () => CliServer, onReload: (server: CliServer) => void): WatchHandle;
|
|
20
35
|
export declare function run(args: string[]): Promise<void>;
|
|
21
36
|
//# sourceMappingURL=cli.d.ts.map
|
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,EAAE,MAAM,WAAW,CAAC;AAOxC,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;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;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAOxC,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;AA0ED,wBAAsB,eAAe,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CA0H7E;AAED,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;;GAEG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,SAAS,EAClB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,SAAS,CAAC,CAQpB;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,CA2Bb;AAED,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAiDvD"}
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/// <reference path="
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
/// <reference path="../../core/schmock.d.ts" />
|
|
2
|
+
import { readFileSync, watch } from "node:fs";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
5
|
import { schmock, toHttpMethod } from "@schmock/core";
|
|
@@ -28,10 +28,39 @@ function loadSeedFile(seedPath) {
|
|
|
28
28
|
}
|
|
29
29
|
return result;
|
|
30
30
|
}
|
|
31
|
+
function handleAdminRequest(method, path, mock, res, headers) {
|
|
32
|
+
const route = path.replace("/schmock-admin/", "");
|
|
33
|
+
if (method === "GET" && route === "routes") {
|
|
34
|
+
res.writeHead(200, headers);
|
|
35
|
+
res.end(JSON.stringify(mock.getRoutes()));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (method === "GET" && route === "state") {
|
|
39
|
+
res.writeHead(200, headers);
|
|
40
|
+
res.end(JSON.stringify(mock.getState()));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (method === "POST" && route === "reset") {
|
|
44
|
+
mock.resetHistory();
|
|
45
|
+
mock.resetState();
|
|
46
|
+
res.writeHead(204, headers);
|
|
47
|
+
res.end();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (method === "GET" && route === "history") {
|
|
51
|
+
res.writeHead(200, headers);
|
|
52
|
+
res.end(JSON.stringify(mock.history()));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
res.writeHead(404, headers);
|
|
56
|
+
res.end(JSON.stringify({ error: "Unknown admin endpoint", code: "NOT_FOUND" }));
|
|
57
|
+
}
|
|
31
58
|
export async function createCliServer(options) {
|
|
32
59
|
const mock = schmock({ debug: options.debug });
|
|
33
60
|
const openapiOptions = {
|
|
34
61
|
spec: options.spec,
|
|
62
|
+
fakerSeed: options.fakerSeed,
|
|
63
|
+
validateRequests: options.errors,
|
|
35
64
|
};
|
|
36
65
|
if (options.seed) {
|
|
37
66
|
openapiOptions.seed = loadSeedFile(options.seed);
|
|
@@ -41,6 +70,7 @@ export async function createCliServer(options) {
|
|
|
41
70
|
const hostname = options.hostname ?? "127.0.0.1";
|
|
42
71
|
const port = options.port ?? 3000;
|
|
43
72
|
const cors = options.cors ?? false;
|
|
73
|
+
const admin = options.admin ?? false;
|
|
44
74
|
const httpServer = createServer((req, res) => {
|
|
45
75
|
// Handle CORS preflight
|
|
46
76
|
if (cors && req.method === "OPTIONS") {
|
|
@@ -49,8 +79,17 @@ export async function createCliServer(options) {
|
|
|
49
79
|
return;
|
|
50
80
|
}
|
|
51
81
|
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
52
|
-
const method = toHttpMethod(req.method ?? "GET");
|
|
53
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");
|
|
54
93
|
const headers = {};
|
|
55
94
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
56
95
|
if (typeof value === "string") {
|
|
@@ -128,6 +167,10 @@ export function parseCliArgs(args) {
|
|
|
128
167
|
seed: { type: "string" },
|
|
129
168
|
cors: { type: "boolean", default: false },
|
|
130
169
|
debug: { type: "boolean", default: false },
|
|
170
|
+
errors: { type: "boolean", default: false },
|
|
171
|
+
watch: { type: "boolean", default: false },
|
|
172
|
+
admin: { type: "boolean", default: false },
|
|
173
|
+
"seed-random": { type: "string" },
|
|
131
174
|
help: { type: "boolean", short: "h", default: false },
|
|
132
175
|
},
|
|
133
176
|
strict: true,
|
|
@@ -141,6 +184,12 @@ export function parseCliArgs(args) {
|
|
|
141
184
|
seed: values.seed,
|
|
142
185
|
cors: values.cors,
|
|
143
186
|
debug: values.debug,
|
|
187
|
+
errors: values.errors,
|
|
188
|
+
watch: values.watch,
|
|
189
|
+
admin: values.admin,
|
|
190
|
+
fakerSeed: values["seed-random"]
|
|
191
|
+
? Number(values["seed-random"])
|
|
192
|
+
: undefined,
|
|
144
193
|
help: values.help ?? false,
|
|
145
194
|
};
|
|
146
195
|
}
|
|
@@ -154,8 +203,53 @@ Options:
|
|
|
154
203
|
--seed <path> JSON file with seed data
|
|
155
204
|
--cors Enable CORS for all responses
|
|
156
205
|
--debug Enable debug logging
|
|
206
|
+
--errors Enable request body validation against spec
|
|
207
|
+
--watch Watch spec file and hot-reload on changes
|
|
208
|
+
--admin Enable /schmock-admin/* introspection endpoints
|
|
209
|
+
--seed-random <n> Seed for deterministic random generation
|
|
157
210
|
-h, --help Show this help message
|
|
158
211
|
`;
|
|
212
|
+
const WATCH_DEBOUNCE_MS = 500;
|
|
213
|
+
/**
|
|
214
|
+
* Reload a CLI server: close the old one, create a new one on the same port.
|
|
215
|
+
*/
|
|
216
|
+
export async function reloadServer(current, options) {
|
|
217
|
+
const port = current.port;
|
|
218
|
+
// Close existing connections and wait for the server to fully close
|
|
219
|
+
current.server.closeAllConnections();
|
|
220
|
+
await new Promise((resolve) => {
|
|
221
|
+
current.server.close(() => resolve());
|
|
222
|
+
});
|
|
223
|
+
return createCliServer({ ...options, port });
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Watch a spec file and hot-reload the server on changes.
|
|
227
|
+
*/
|
|
228
|
+
export function startWatch(specPath, options, getCurrentServer, onReload) {
|
|
229
|
+
let debounceTimer;
|
|
230
|
+
const watcher = watch(specPath, () => {
|
|
231
|
+
if (debounceTimer)
|
|
232
|
+
clearTimeout(debounceTimer);
|
|
233
|
+
debounceTimer = setTimeout(async () => {
|
|
234
|
+
process.stderr.write("\nSpec changed, reloading...\n");
|
|
235
|
+
try {
|
|
236
|
+
const newServer = await reloadServer(getCurrentServer(), options);
|
|
237
|
+
onReload(newServer);
|
|
238
|
+
process.stderr.write(`Schmock server reloaded on http://${newServer.hostname}:${newServer.port}\n`);
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
process.stderr.write(`Reload failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
242
|
+
}
|
|
243
|
+
}, WATCH_DEBOUNCE_MS);
|
|
244
|
+
});
|
|
245
|
+
return {
|
|
246
|
+
close() {
|
|
247
|
+
watcher.close();
|
|
248
|
+
if (debounceTimer)
|
|
249
|
+
clearTimeout(debounceTimer);
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
159
253
|
export async function run(args) {
|
|
160
254
|
const options = parseCliArgs(args);
|
|
161
255
|
if (options.help) {
|
|
@@ -168,14 +262,25 @@ export async function run(args) {
|
|
|
168
262
|
process.exitCode = 1;
|
|
169
263
|
return;
|
|
170
264
|
}
|
|
171
|
-
|
|
265
|
+
let cliServer = await createCliServer(options);
|
|
172
266
|
process.stderr.write(`Schmock server running on http://${cliServer.hostname}:${cliServer.port}\n`);
|
|
173
267
|
process.stderr.write(`Spec: ${options.spec}\n`);
|
|
174
268
|
if (options.cors) {
|
|
175
269
|
process.stderr.write("CORS: enabled\n");
|
|
176
270
|
}
|
|
271
|
+
if (options.admin) {
|
|
272
|
+
process.stderr.write("Admin: enabled (/schmock-admin/*)\n");
|
|
273
|
+
}
|
|
274
|
+
let watchHandle;
|
|
275
|
+
if (options.watch) {
|
|
276
|
+
watchHandle = startWatch(options.spec, options, () => cliServer, (newServer) => {
|
|
277
|
+
cliServer = newServer;
|
|
278
|
+
});
|
|
279
|
+
process.stderr.write("Watch: enabled\n");
|
|
280
|
+
}
|
|
177
281
|
const shutdown = () => {
|
|
178
282
|
process.stderr.write("\nShutting down...\n");
|
|
283
|
+
watchHandle?.close();
|
|
179
284
|
cliServer.close();
|
|
180
285
|
};
|
|
181
286
|
process.on("SIGINT", shutdown);
|