@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 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;AAiCD,wBAAsB,eAAe,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CA2G7E;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,GAAG;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CA2B3E;AAeD,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAgCvD"}
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="../../../types/schmock.d.ts" />
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
- const cliServer = await createCliServer(options);
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);