@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/LICENSE +21 -0
- package/README.md +36 -0
- package/dist/bin.js +533 -31834
- package/dist/cli.d.ts +50 -12
- package/dist/cli.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +529 -31834
- package/package.json +19 -6
- package/dist/cli.js +0 -263
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.
|
|
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
|
-
"
|
|
24
|
-
"build
|
|
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.
|
|
36
|
-
"@schmock/openapi": "^2.
|
|
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
|
-
}
|