@schmock/cli 2.2.3 → 2.3.1

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/package.json CHANGED
@@ -1,8 +1,17 @@
1
1
  {
2
2
  "name": "@schmock/cli",
3
3
  "description": "CLI for running Schmock mock servers from OpenAPI specs",
4
- "version": "2.2.3",
4
+ "version": "2.3.1",
5
5
  "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/khalic-lab/schmock.git",
9
+ "directory": "packages/cli"
10
+ },
11
+ "homepage": "https://github.com/khalic-lab/schmock/tree/main/packages/cli#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/khalic-lab/schmock/issues"
14
+ },
6
15
  "main": "./dist/index.js",
7
16
  "types": "./dist/index.d.ts",
8
17
  "bin": {
@@ -20,10 +29,11 @@
20
29
  "./package.json": "./package.json"
21
30
  },
22
31
  "scripts": {
23
- "build": "bun build:lib && bun build:types",
24
- "build:lib": "bun build --target node --outdir=dist src/index.ts src/bin.ts",
32
+ "clean": "rm -rf dist && rm -f tsconfig.tsbuildinfo",
33
+ "build": "bun run clean && bun run build:lib && bun run build:types",
34
+ "build:lib": "bun build --target node --format esm --outdir=dist src/index.ts src/bin.ts --external @schmock/core --external @schmock/openapi",
25
35
  "build:types": "tsc -p tsconfig.json",
26
- "test": "vitest",
36
+ "test": "vitest run",
27
37
  "test:watch": "vitest --watch",
28
38
  "test:bdd": "vitest run --config vitest.config.bdd.ts",
29
39
  "lint": "biome check src/*.ts",
@@ -31,9 +41,12 @@
31
41
  "check:publish": "publint && attw --pack --ignore-rules cjs-resolves-to-esm"
32
42
  },
33
43
  "license": "MIT",
44
+ "engines": {
45
+ "node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0"
46
+ },
34
47
  "dependencies": {
35
- "@schmock/core": "^2.2.3",
36
- "@schmock/openapi": "^2.2.3"
48
+ "@schmock/core": "^2.3.1",
49
+ "@schmock/openapi": "^2.3.1"
37
50
  },
38
51
  "devDependencies": {
39
52
  "@amiceli/vitest-cucumber": "^7.0.0",
package/dist/cli.js DELETED
@@ -1,263 +0,0 @@
1
- /// <reference path="../../core/schmock.d.ts" />
2
- import { readFileSync, watch } from "node:fs";
3
- import { createServer } from "node:http";
4
- import { parseArgs } from "node:util";
5
- import { collectBody, parseNodeHeaders, parseNodeQuery, schmock, toHttpMethod, writeSchmockResponse, } from "@schmock/core";
6
- import { openapi } from "@schmock/openapi";
7
- const CORS_HEADERS = {
8
- "access-control-allow-origin": "*",
9
- "access-control-allow-methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
10
- "access-control-allow-headers": "Content-Type, Authorization",
11
- };
12
- function isSeedSource(value) {
13
- return (Array.isArray(value) ||
14
- typeof value === "string" ||
15
- (typeof value === "object" && value !== null && "count" in value));
16
- }
17
- function loadSeedFile(seedPath) {
18
- const raw = readFileSync(seedPath, "utf-8");
19
- const parsed = JSON.parse(raw);
20
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
21
- throw new Error(`Seed file must contain a JSON object, got: ${typeof parsed}`);
22
- }
23
- const result = {};
24
- for (const [key, value] of Object.entries(parsed)) {
25
- if (isSeedSource(value)) {
26
- result[key] = value;
27
- }
28
- }
29
- return result;
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
- }
58
- export async function createCliServer(options) {
59
- const mock = schmock({ debug: options.debug, state: {} });
60
- const openapiOptions = {
61
- spec: options.spec,
62
- fakerSeed: options.fakerSeed,
63
- validateRequests: options.errors,
64
- };
65
- if (options.seed) {
66
- openapiOptions.seed = loadSeedFile(options.seed);
67
- }
68
- const plugin = await openapi(openapiOptions);
69
- mock.pipe(plugin);
70
- const hostname = options.hostname ?? "127.0.0.1";
71
- const port = options.port ?? 3000;
72
- const cors = options.cors ?? false;
73
- const admin = options.admin ?? false;
74
- const httpServer = createServer((req, res) => {
75
- // Handle CORS preflight
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
- });
111
- });
112
- return new Promise((resolve, reject) => {
113
- httpServer.on("error", reject);
114
- httpServer.listen(port, hostname, () => {
115
- const addr = httpServer.address();
116
- const actualPort = addr !== null && typeof addr === "object" ? addr.port : port;
117
- resolve({
118
- server: httpServer,
119
- port: actualPort,
120
- hostname,
121
- close() {
122
- httpServer.close();
123
- },
124
- });
125
- });
126
- });
127
- }
128
- function validatePort(value) {
129
- const port = Number(value);
130
- if (!Number.isInteger(port) || port < 0 || port > 65535) {
131
- throw new Error(`Invalid port "${value}". Port must be an integer between 0 and 65535.`);
132
- }
133
- return port;
134
- }
135
- export function parseCliArgs(args) {
136
- const { values, positionals } = parseArgs({
137
- args,
138
- options: {
139
- spec: { type: "string" },
140
- port: { type: "string" },
141
- hostname: { type: "string" },
142
- seed: { type: "string" },
143
- cors: { type: "boolean", default: false },
144
- debug: { type: "boolean", default: false },
145
- errors: { type: "boolean", default: false },
146
- watch: { type: "boolean", default: false },
147
- admin: { type: "boolean", default: false },
148
- "seed-random": { type: "string" },
149
- help: { type: "boolean", short: "h", default: false },
150
- },
151
- strict: true,
152
- allowPositionals: true,
153
- });
154
- const spec = values.spec ?? positionals[0] ?? "";
155
- return {
156
- spec,
157
- port: values.port ? validatePort(values.port) : undefined,
158
- hostname: values.hostname,
159
- seed: values.seed,
160
- cors: values.cors,
161
- debug: values.debug,
162
- errors: values.errors,
163
- watch: values.watch,
164
- admin: values.admin,
165
- fakerSeed: values["seed-random"]
166
- ? Number(values["seed-random"])
167
- : undefined,
168
- help: values.help ?? false,
169
- };
170
- }
171
- const USAGE = `Usage: schmock <spec> [options]
172
- schmock --spec <path> [options]
173
-
174
- Options:
175
- --spec <path> OpenAPI/Swagger spec file (or pass as first argument)
176
- --port <number> Port to listen on (default: 3000)
177
- --hostname <host> Hostname to bind to (default: 127.0.0.1)
178
- --seed <path> JSON file with seed data
179
- --cors Enable CORS for all responses
180
- --debug Enable debug logging
181
- --errors Enable request body validation against spec
182
- --watch Watch spec file and hot-reload on changes
183
- --admin Enable /schmock-admin/* introspection endpoints
184
- --seed-random <n> Seed for deterministic random generation
185
- -h, --help Show this help message
186
- `;
187
- const WATCH_DEBOUNCE_MS = 500;
188
- /**
189
- * Reload a CLI server: close the old one, create a new one on the same port.
190
- */
191
- export async function reloadServer(current, options) {
192
- const port = current.port;
193
- // Close existing connections and wait for the server to fully close
194
- current.server.closeAllConnections();
195
- await new Promise((resolve) => {
196
- current.server.close(() => resolve());
197
- });
198
- return createCliServer({ ...options, port });
199
- }
200
- /**
201
- * Watch a spec file and hot-reload the server on changes.
202
- */
203
- export function startWatch(specPath, options, getCurrentServer, onReload) {
204
- let debounceTimer;
205
- const watcher = watch(specPath, () => {
206
- if (debounceTimer)
207
- clearTimeout(debounceTimer);
208
- debounceTimer = setTimeout(async () => {
209
- process.stderr.write("\nSpec changed, reloading...\n");
210
- try {
211
- const newServer = await reloadServer(getCurrentServer(), options);
212
- onReload(newServer);
213
- process.stderr.write(`Schmock server reloaded on http://${newServer.hostname}:${newServer.port}\n`);
214
- }
215
- catch (err) {
216
- process.stderr.write(`Reload failed: ${err instanceof Error ? err.message : String(err)}\n`);
217
- }
218
- }, WATCH_DEBOUNCE_MS);
219
- });
220
- return {
221
- close() {
222
- watcher.close();
223
- if (debounceTimer)
224
- clearTimeout(debounceTimer);
225
- },
226
- };
227
- }
228
- export async function run(args) {
229
- const options = parseCliArgs(args);
230
- if (options.help) {
231
- process.stderr.write(USAGE);
232
- return;
233
- }
234
- if (!options.spec) {
235
- process.stderr.write("Error: --spec is required\n\n");
236
- process.stderr.write(USAGE);
237
- process.exitCode = 1;
238
- return;
239
- }
240
- let cliServer = await createCliServer(options);
241
- process.stderr.write(`Schmock server running on http://${cliServer.hostname}:${cliServer.port}\n`);
242
- process.stderr.write(`Spec: ${options.spec}\n`);
243
- if (options.cors) {
244
- process.stderr.write("CORS: enabled\n");
245
- }
246
- if (options.admin) {
247
- process.stderr.write("Admin: enabled (/schmock-admin/*)\n");
248
- }
249
- let watchHandle;
250
- if (options.watch) {
251
- watchHandle = startWatch(options.spec, options, () => cliServer, (newServer) => {
252
- cliServer = newServer;
253
- });
254
- process.stderr.write("Watch: enabled\n");
255
- }
256
- const shutdown = () => {
257
- process.stderr.write("\nShutting down...\n");
258
- watchHandle?.close();
259
- cliServer.close();
260
- };
261
- process.on("SIGINT", shutdown);
262
- process.on("SIGTERM", shutdown);
263
- }