@wylan24/mockgen 0.0.0-stage → 0.1.0-alpha.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wylan24
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,178 @@
1
- # Temporary Holding Version
1
+ # mockgen
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![CI](https://github.com/wylan24/mockgen/actions/workflows/ci.yml/badge.svg)](https://github.com/wylan24/mockgen/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/@wylan24/mockgen?include_prereleases)](https://www.npmjs.com/package/@wylan24/mockgen)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+
7
+ Turn an OpenAPI spec — or just a folder of sample JSON responses — into a running mock server with realistic, stable fake data. One command.
8
+
9
+ ```bash
10
+ npx @wylan24/mockgen serve ./openapi.yaml
11
+ npx @wylan24/mockgen serve ./samples/
12
+ ```
13
+
14
+ You get a server on `localhost:4010` that returns schema-valid, human-looking data, returns the same data across restarts, and can be tweaked without writing code.
15
+
16
+ > **Status:** alpha (0.x). It works and is tested against 57 real-world APIs, but options may still change before 1.0. [Feedback and bug reports](https://github.com/wylan24/mockgen/issues/new/choose) are very welcome.
17
+
18
+ ## Install
19
+
20
+ Requires Node 22.19 or later. Run it without installing:
21
+
22
+ ```bash
23
+ npx @wylan24/mockgen serve ./openapi.yaml
24
+ ```
25
+
26
+ Or add it to a project, for scripts and tests:
27
+
28
+ ```bash
29
+ npm install --save-dev @wylan24/mockgen
30
+ npx mockgen serve ./openapi.yaml
31
+ ```
32
+
33
+ ## Why
34
+
35
+ - **Realistic data.** Field names drive the data: `email`, `avatarUrl`, `createdAt`, `price`, `city` all look like the real thing, and a record's name, username and email match each other.
36
+ - **Deterministic.** Same spec + seed → byte-identical responses, so screenshots and tests stay stable. `GET /users/42` always returns the same user, with `id: 42`.
37
+ - **Valid.** Responses honor `enum`, `min`/`max`, `pattern`, `minLength`, `minItems`, `uniqueItems`, `oneOf`/`anyOf`/`allOf`, nullable, and use your spec's `example` values when present.
38
+ - **Zero config.** CORS on, request log in the terminal, hot reload when the spec changes.
39
+
40
+ ## Usage
41
+
42
+ ```bash
43
+ mockgen serve <spec|dir> # Start a mock server (hot-reloads on change)
44
+ mockgen generate <spec|dir> [route] # Print generated responses, e.g. "GET /users/1"
45
+ mockgen validate <spec|dir> # Generate every response and check it against its schema
46
+ mockgen infer <dir> [-o openapi.yaml] # Write the schema inferred from samples as OpenAPI 3.1
47
+ ```
48
+
49
+ | Option | Default | |
50
+ | --- | --- | --- |
51
+ | `-p, --port` | `4010` | Port to listen on |
52
+ | `-H, --host` | `127.0.0.1` | Host to bind |
53
+ | `-s, --seed` | `1` | Seed for generated data |
54
+ | `-d, --delay` | `0` | Delay every response, e.g. `300ms` |
55
+ | `-r, --records` | `50` | Records in every list endpoint's collection |
56
+ | `--no-watch` | | Disable hot reload |
57
+ | `-o, --out` | stdout | `infer` output file (`.yaml` or `.json`) |
58
+
59
+ ### Simulating errors and latency
60
+
61
+ Per request, with a header or a query parameter:
62
+
63
+ ```bash
64
+ curl -H 'X-Mock-Status: 404' localhost:4010/users/1 # the spec's 404 response
65
+ curl 'localhost:4010/users/1?__status=503' # undocumented codes work too
66
+ curl -H 'X-Mock-Delay: 1500' localhost:4010/users # slow this one down
67
+ ```
68
+
69
+ ### Lists and pagination
70
+
71
+ Every list endpoint — a bare array of objects, or an object wrapping one (`data`, `items`, `results`, …) — is a window onto a stable collection of 50 records (`--records`):
72
+
73
+ ```bash
74
+ curl 'localhost:4010/users?page=2&limit=20' # also per_page, pageSize, offset/skip
75
+ curl 'localhost:4010/users?cursor=bzoyMA' # any cursor mockgen handed out works
76
+ ```
77
+
78
+ - Integer ids are sequential (`1…50`), so page 2 of 10 is ids 11–20, every time.
79
+ - When the spec also has `GET /users/{id}`, each list item is the same record that route returns.
80
+ - Wrapper fields are filled by name, including nested ones like `meta.pagination`: `total`, `page`, `perPage`, `totalPages`, `hasMore`, `next`/`prev` (page number, URL or cursor, and `null` on the last page).
81
+ - Responses carry `X-Total-Count` and a `Link` header with `next`/`prev`.
82
+
83
+ A page past the end is empty, even if the schema sets `minItems`.
84
+
85
+ ### In tests
86
+
87
+ ```ts
88
+ import { createMockServer } from '@wylan24/mockgen';
89
+
90
+ const mock = await createMockServer({ spec: './openapi.yaml', seed: 1 });
91
+ const url = await mock.listen(0); // random free port
92
+ // ... run tests against url ...
93
+ await mock.close();
94
+ ```
95
+
96
+ ## Supported input
97
+
98
+ ### OpenAPI specs
99
+
100
+ - OpenAPI 3.0 and 3.1, YAML or JSON, with local and multi-file `$ref`s (circular refs are fine).
101
+ - Swagger 2.0 is not supported; convert it with [swagger2openapi](https://github.com/Mermade/oas-kit) first.
102
+
103
+ ### A folder of sample responses
104
+
105
+ No spec yet? Drop real responses into a folder, named after their route. mockgen infers a schema from them and generates fresh data in the same shape.
106
+
107
+ | File | Route |
108
+ | --- | --- |
109
+ | `GET.users.json` | `GET /users` → 200 |
110
+ | `GET.users.[id].json` or `users/GET.[id].json` | `GET /users/{id}` (folders are path segments; `{id}` works too) |
111
+ | `GET.users.[id].404.json` | the 404 response, served with `X-Mock-Status: 404` |
112
+ | `GET.users.[id]~alice.json` | another sample for the same response |
113
+ | `POST.users.json` | `POST /users` → 201 |
114
+ | `DELETE.users.[id].json` (empty file) | `DELETE /users/{id}` → 204, no body |
115
+
116
+ Every sample for a route is merged: keys missing from some samples become optional, `null` values make a field nullable, ISO dates, emails, URLs and UUIDs are detected, and a few short values repeated often (`role: admin | member | viewer`) become an enum. See [examples/samples](examples/samples).
117
+
118
+ When you're ready for a real spec, `mockgen infer ./samples -o openapi.yaml` writes the inferred schema out so you can edit it.
119
+
120
+ ## Roadmap to 1.0
121
+
122
+ - [x] OpenAPI 3.0/3.1 loader with multi-file `$ref`s
123
+ - [x] Deterministic, field-name-aware data generation honoring schema constraints
124
+ - [x] Mock server: CORS, request log, status/latency simulation, hot reload
125
+ - [x] Sample-response input (folder of JSON files → inferred schema) and `mockgen infer`
126
+ - [x] List/detail consistency (`/users/{id}` returns the user from `/users`) and pagination
127
+ - [x] Spec corpus of 50+ real-world APIs in CI: every spec loads, and every generated response validates apart from documented spec bugs
128
+ - [ ] Stateful CRUD, `mock.config.ts` overrides, Docker image
129
+
130
+ ## Quality: the spec corpus
131
+
132
+ mockgen is tested against 57 real-world OpenAPI specs (Stripe, GitHub, Twilio, Box, Jira, DocuSign, Plaid, Petstore variants and more from [APIs.guru](https://apis.guru)), pinned to commits in [corpus/specs.json](corpus/specs.json). Every spec must load, and every generated response is validated against its schema with Ajv.
133
+
134
+ ```bash
135
+ npm run corpus:fetch # download the specs once (~60 MB, cached in corpus/.cache)
136
+ npm run corpus # load + generate + validate everything
137
+ npm run corpus -- stripe # just one spec
138
+ npm run corpus:explain -- linode "GET /linode/instances" 200 # why a response fails
139
+ ```
140
+
141
+ Validation follows OpenAPI semantics: 3.0 `nullable`, boolean exclusive bounds, `writeOnly` fields absent from responses, and `discriminator` choosing the `oneOf` branch.
142
+
143
+ Every spec's GET routes must also serve in under 20 ms at p95.
144
+
145
+ Some real specs contain schemas no value can satisfy (the same schema twice in a `oneOf`, `allOf: [string, null]`, contradictory enums). [corpus/baseline.json](corpus/baseline.json) lists each one with the reason. CI runs `npm run corpus -- --strict`, which fails if any spec gets worse than its baseline or any failure isn't a documented spec bug.
146
+
147
+ ## Development
148
+
149
+ Requires Node 22.19+.
150
+
151
+ ```bash
152
+ npm install
153
+ npm test
154
+ npm run serve:example # builds, then serves examples/petstore.yaml
155
+ npm run smoke # pack, install into an empty project, run the CLI and library
156
+ ```
157
+
158
+ Snapshot tests in [test/snapshot.test.ts](test/snapshot.test.ts) pin generated output byte for byte, and CI runs them on macOS, Linux and Windows. If you change generation on purpose, review the diff and update with `npx vitest run -u`.
159
+
160
+ ### Releasing
161
+
162
+ Bump `version` in `package.json`, commit, then push a matching tag:
163
+
164
+ ```bash
165
+ git tag v0.1.0-alpha.1 && git push origin v0.1.0-alpha.1
166
+ ```
167
+
168
+ The [release workflow](.github/workflows/release.yml) reruns the tests, smoke test and strict corpus gate, publishes to npm with provenance (pre-releases under the `next` dist-tag), and creates the GitHub release. It needs an `NPM_TOKEN` repository secret, or the workflow set up as a trusted publisher on npmjs.com.
169
+
170
+ ## Contributing
171
+
172
+ Bug reports, specs that mockgen gets wrong, and pull requests are all welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) to get set up, and the [Code of Conduct](CODE_OF_CONDUCT.md). Ask questions in [Discussions](https://github.com/wylan24/mockgen/discussions). Report security problems privately, as described in [SECURITY.md](SECURITY.md).
173
+
174
+ See [CHANGELOG.md](CHANGELOG.md) for what changed in each release.
175
+
176
+ ## License
177
+
178
+ [MIT](LICENSE)
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { parseArgs, styleText } from 'node:util';
5
+ import { generate } from './generate.js';
6
+ import { loadSource } from './load.js';
7
+ import { formatOpenApi, loadSamples } from './samples.js';
8
+ import { createMockServer, respond } from './server.js';
9
+ import { ResponseValidator } from './validate.js';
10
+ const HELP = `mockgen — turn an OpenAPI spec or a folder of sample responses into a running mock server
11
+
12
+ Usage
13
+ mockgen serve <spec|dir> Start a mock server (hot-reloads on change)
14
+ mockgen generate <spec|dir> [route] Print generated responses, e.g. "GET /users/1"
15
+ mockgen validate <spec|dir> Load the input and generate every response
16
+ mockgen infer <dir> [-o file] Write the schema inferred from samples as OpenAPI 3.1
17
+
18
+ Sample folders: name files after the route, e.g. GET.users.json, users/GET.[id].json,
19
+ GET.users.[id].404.json. Folders become path segments; [id] or {id} is a path param.
20
+
21
+ Options
22
+ -p, --port <n> Port to listen on (default 4010)
23
+ -H, --host <host> Host to bind (default 127.0.0.1)
24
+ -s, --seed <n> Seed for generated data (default 1)
25
+ -d, --delay <ms> Delay every response, e.g. 300 or 300ms
26
+ -r, --records <n> Records per list endpoint, paged with ?page=&limit= (default 50)
27
+ -o, --out <file> infer: write here instead of stdout (.yaml or .json)
28
+ --no-watch Disable hot reload
29
+ -h, --help Show this help
30
+ -v, --version Show the version
31
+
32
+ Per request
33
+ X-Mock-Status: 404 (or ?__status=404) Return that status instead of the success response
34
+ X-Mock-Delay: 500 (or ?__delay=500) Delay this response
35
+ ?page=2&limit=20, ?offset=40, ?cursor=… Page through list endpoints
36
+ `;
37
+ async function main(argv) {
38
+ const { values, positionals } = parseArgs({
39
+ args: argv,
40
+ allowPositionals: true,
41
+ allowNegative: true,
42
+ options: {
43
+ port: { type: 'string', short: 'p', default: '4010' },
44
+ host: { type: 'string', short: 'H', default: '127.0.0.1' },
45
+ seed: { type: 'string', short: 's', default: '1' },
46
+ delay: { type: 'string', short: 'd', default: '0' },
47
+ out: { type: 'string', short: 'o' },
48
+ records: { type: 'string', short: 'r', default: '50' },
49
+ watch: { type: 'boolean', default: true },
50
+ help: { type: 'boolean', short: 'h' },
51
+ version: { type: 'boolean', short: 'v' },
52
+ },
53
+ });
54
+ if (values.version)
55
+ return console.log(await version());
56
+ const [command, spec, ...rest] = positionals;
57
+ if (values.help || !command)
58
+ return console.log(HELP);
59
+ if (!spec)
60
+ fail(`Missing <spec>. Try: mockgen ${command} ./openapi.yaml`);
61
+ const seed = int(values.seed, '--seed');
62
+ switch (command) {
63
+ case 'serve':
64
+ return serve(spec, { port: int(values.port, '--port'), host: values.host, seed, delay: int(values.delay.replace(/ms$/, ''), '--delay'), watch: values.watch, records: int(values.records, '--records') });
65
+ case 'generate':
66
+ return generateCmd(spec, rest.join(' '), seed, int(values.records, '--records'));
67
+ case 'validate':
68
+ return validate(spec, seed);
69
+ case 'infer':
70
+ return infer(spec, values.out);
71
+ default:
72
+ fail(`Unknown command "${command}". Run mockgen --help.`);
73
+ }
74
+ }
75
+ async function serve(spec, opts) {
76
+ const started = performance.now();
77
+ const model = await loadSource(spec);
78
+ printWarnings(model);
79
+ const mock = createMockServer({ model, seed: opts.seed, delay: opts.delay, records: opts.records, log: (line) => console.log(styleText('dim', line)) });
80
+ const url = await mock.listen(opts.port, opts.host).catch((err) => {
81
+ if (err.code === 'EADDRINUSE')
82
+ fail(`Port ${opts.port} is already in use on ${opts.host}. Pick another with --port ${opts.port + 1}, or --port 0 for any free port.`);
83
+ if (err.code === 'EACCES')
84
+ fail(`No permission to use port ${opts.port} on ${opts.host}. Ports below 1024 usually need admin rights; try --port 4010.`);
85
+ throw err;
86
+ });
87
+ // Watch before announcing "ready": a script that edits the spec as soon as it sees
88
+ // that line would otherwise race the watcher and its edit could be missed.
89
+ if (opts.watch)
90
+ watchSources(model, async () => {
91
+ try {
92
+ const next = await loadSource(spec);
93
+ mock.setModel(next);
94
+ printWarnings(next);
95
+ console.log(`${styleText('green', '↻')} Reloaded ${next.routes.length} routes`);
96
+ return next;
97
+ }
98
+ catch (err) {
99
+ console.error(styleText('red', `✖ Reload failed, still serving the previous version:\n ${err.message}`));
100
+ return undefined;
101
+ }
102
+ });
103
+ console.log(`\n${styleText('bold', model.title)} ${styleText('dim', model.version)}`);
104
+ for (const r of model.routes)
105
+ console.log(` ${styleText('cyan', r.method.padEnd(7))} ${url}${model.basePath}${r.path}`);
106
+ console.log(`\n${styleText('green', '✔')} Mock server ready at ${styleText('bold', url)} in ${Math.round(performance.now() - started)}ms (seed ${opts.seed})\n`);
107
+ const shutdown = () => mock.close().then(() => process.exit(0));
108
+ process.once('SIGINT', shutdown);
109
+ process.once('SIGTERM', shutdown);
110
+ }
111
+ /**
112
+ * Watch every local file of the spec and reload on change, debounced.
113
+ *
114
+ * Watchers are re-created after every reload, failed ones included: on Linux and macOS
115
+ * a watcher follows the file, not its name, so an editor's save (write a temp file,
116
+ * rename it over the original) leaves the old watcher on a file that no longer exists.
117
+ */
118
+ function watchSources(model, reload) {
119
+ let watchers = [];
120
+ let files = model.sources;
121
+ let timer;
122
+ let retry;
123
+ const onChange = () => {
124
+ clearTimeout(timer);
125
+ timer = setTimeout(async () => {
126
+ files = (await reload())?.sources ?? files;
127
+ watch();
128
+ }, 100);
129
+ };
130
+ const watch = () => {
131
+ watchers.forEach((w) => w.close());
132
+ watchers = [];
133
+ clearTimeout(retry);
134
+ let missing = false;
135
+ for (const file of files) {
136
+ try {
137
+ watchers.push(fs.watch(file, { recursive: fs.statSync(file).isDirectory() }, onChange));
138
+ }
139
+ catch {
140
+ missing = true; // e.g. mid-save, between delete and rename
141
+ }
142
+ }
143
+ if (missing)
144
+ retry = setTimeout(watch, 200);
145
+ };
146
+ watch();
147
+ }
148
+ async function generateCmd(spec, route, seed, records) {
149
+ const model = await loadSource(spec);
150
+ if (route) {
151
+ const [method = 'GET', p = '/'] = route.includes(' ') ? route.split(/\s+/) : ['GET', route];
152
+ const [pathname = '/', query] = p.split('?');
153
+ const result = respond(model, { method: method.toUpperCase(), path: pathname, query, seed, records });
154
+ if (result.status >= 400 && !model.routes.some((r) => r.pattern.test(pathname)))
155
+ fail(result.body.message);
156
+ return console.log(JSON.stringify(result.body, null, 2));
157
+ }
158
+ const out = {};
159
+ for (const r of model.routes) {
160
+ const res = r.responses.find((x) => x.status >= 200 && x.status < 300) ?? r.responses[0];
161
+ out[`${r.method} ${r.path}`] = res?.hasExample ? res.example : generate(res?.schema, { seed, key: `${r.method} ${r.path} ${res?.status}` });
162
+ }
163
+ console.log(JSON.stringify(out, null, 2));
164
+ }
165
+ async function infer(dir, out) {
166
+ if (!(await fs.promises.stat(dir).catch(() => undefined))?.isDirectory())
167
+ fail(`infer expects a folder of sample responses, got "${dir}"`);
168
+ const model = await loadSamples(dir);
169
+ printWarnings(model);
170
+ const text = formatOpenApi(model, out);
171
+ if (!out)
172
+ return process.stdout.write(text);
173
+ await fs.promises.writeFile(out, text);
174
+ console.error(`${styleText('green', '✔')} Wrote ${model.routes.length} routes to ${out}`);
175
+ }
176
+ async function validate(spec, seed) {
177
+ const model = await loadSource(spec);
178
+ printWarnings(model);
179
+ const validator = new ResponseValidator(model.openapi);
180
+ let responses = 0;
181
+ let skipped = 0;
182
+ const invalid = [];
183
+ for (const r of model.routes) {
184
+ for (const res of r.responses) {
185
+ if (!res.schema || res.hasExample)
186
+ continue;
187
+ const label = `${r.method} ${r.path} ${res.status}`;
188
+ const value = generate(res.schema, { seed, key: label });
189
+ try {
190
+ const result = validator.validate(res.schema, value);
191
+ if (!result.valid)
192
+ invalid.push(`${label}: ${result.errors}`);
193
+ responses++;
194
+ }
195
+ catch {
196
+ skipped++; // The schema itself can't be compiled (e.g. a non-JavaScript regex).
197
+ }
198
+ }
199
+ }
200
+ for (const line of invalid)
201
+ console.error(styleText('red', `✖ ${line}`));
202
+ const summary = `${model.title}: ${model.routes.length} routes, ${responses - invalid.length}/${responses} generated responses valid${skipped ? `, ${skipped} schemas not checkable` : ''}, ${model.warnings.length} warnings`;
203
+ if (invalid.length)
204
+ fail(summary);
205
+ console.log(`${styleText('green', '✔')} ${summary}`);
206
+ }
207
+ function printWarnings(model) {
208
+ for (const w of model.warnings)
209
+ console.warn(styleText('yellow', `⚠ ${w}`));
210
+ }
211
+ function int(value, flag) {
212
+ const n = Number(value);
213
+ if (!Number.isInteger(n) || n < 0)
214
+ fail(`${flag} must be a non-negative integer, got "${value}"`);
215
+ return n;
216
+ }
217
+ async function version() {
218
+ const pkg = JSON.parse(await fs.promises.readFile(path.join(import.meta.dirname, '..', 'package.json'), 'utf8'));
219
+ return pkg.version;
220
+ }
221
+ function fail(message) {
222
+ console.error(styleText('red', `✖ ${message}`));
223
+ process.exit(1);
224
+ }
225
+ main(process.argv.slice(2)).catch((err) => fail(err.message));
226
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { parseArgs, styleText } from 'node:util';\nimport { generate } from './generate.js';\nimport type { MockModel } from './model.js';\nimport { loadSource } from './load.js';\nimport { formatOpenApi, loadSamples } from './samples.js';\nimport { createMockServer, respond } from './server.js';\nimport { ResponseValidator } from './validate.js';\n\nconst HELP = `mockgen — turn an OpenAPI spec or a folder of sample responses into a running mock server\n\nUsage\n mockgen serve <spec|dir> Start a mock server (hot-reloads on change)\n mockgen generate <spec|dir> [route] Print generated responses, e.g. \"GET /users/1\"\n mockgen validate <spec|dir> Load the input and generate every response\n mockgen infer <dir> [-o file] Write the schema inferred from samples as OpenAPI 3.1\n\nSample folders: name files after the route, e.g. GET.users.json, users/GET.[id].json,\nGET.users.[id].404.json. Folders become path segments; [id] or {id} is a path param.\n\nOptions\n -p, --port <n> Port to listen on (default 4010)\n -H, --host <host> Host to bind (default 127.0.0.1)\n -s, --seed <n> Seed for generated data (default 1)\n -d, --delay <ms> Delay every response, e.g. 300 or 300ms\n -r, --records <n> Records per list endpoint, paged with ?page=&limit= (default 50)\n -o, --out <file> infer: write here instead of stdout (.yaml or .json)\n --no-watch Disable hot reload\n -h, --help Show this help\n -v, --version Show the version\n\nPer request\n X-Mock-Status: 404 (or ?__status=404) Return that status instead of the success response\n X-Mock-Delay: 500 (or ?__delay=500) Delay this response\n ?page=2&limit=20, ?offset=40, ?cursor=… Page through list endpoints\n`;\n\nasync function main(argv: string[]) {\n const { values, positionals } = parseArgs({\n args: argv,\n allowPositionals: true,\n allowNegative: true,\n options: {\n port: { type: 'string', short: 'p', default: '4010' },\n host: { type: 'string', short: 'H', default: '127.0.0.1' },\n seed: { type: 'string', short: 's', default: '1' },\n delay: { type: 'string', short: 'd', default: '0' },\n out: { type: 'string', short: 'o' },\n records: { type: 'string', short: 'r', default: '50' },\n watch: { type: 'boolean', default: true },\n help: { type: 'boolean', short: 'h' },\n version: { type: 'boolean', short: 'v' },\n },\n });\n\n if (values.version) return console.log(await version());\n const [command, spec, ...rest] = positionals;\n if (values.help || !command) return console.log(HELP);\n if (!spec) fail(`Missing <spec>. Try: mockgen ${command} ./openapi.yaml`);\n\n const seed = int(values.seed, '--seed');\n switch (command) {\n case 'serve':\n return serve(spec, { port: int(values.port, '--port'), host: values.host, seed, delay: int(values.delay.replace(/ms$/, ''), '--delay'), watch: values.watch, records: int(values.records, '--records') });\n case 'generate':\n return generateCmd(spec, rest.join(' '), seed, int(values.records, '--records'));\n case 'validate':\n return validate(spec, seed);\n case 'infer':\n return infer(spec, values.out);\n default:\n fail(`Unknown command \"${command}\". Run mockgen --help.`);\n }\n}\n\nasync function serve(spec: string, opts: { port: number; host: string; seed: number; delay: number; watch: boolean; records: number }) {\n const started = performance.now();\n const model = await loadSource(spec);\n printWarnings(model);\n const mock = createMockServer({ model, seed: opts.seed, delay: opts.delay, records: opts.records, log: (line) => console.log(styleText('dim', line)) });\n const url = await mock.listen(opts.port, opts.host).catch((err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') fail(`Port ${opts.port} is already in use on ${opts.host}. Pick another with --port ${opts.port + 1}, or --port 0 for any free port.`);\n if (err.code === 'EACCES') fail(`No permission to use port ${opts.port} on ${opts.host}. Ports below 1024 usually need admin rights; try --port 4010.`);\n throw err;\n });\n\n // Watch before announcing \"ready\": a script that edits the spec as soon as it sees\n // that line would otherwise race the watcher and its edit could be missed.\n if (opts.watch) watchSources(model, async () => {\n try {\n const next = await loadSource(spec);\n mock.setModel(next);\n printWarnings(next);\n console.log(`${styleText('green', '↻')} Reloaded ${next.routes.length} routes`);\n return next;\n } catch (err) {\n console.error(styleText('red', `✖ Reload failed, still serving the previous version:\\n ${(err as Error).message}`));\n return undefined;\n }\n });\n\n console.log(`\\n${styleText('bold', model.title)} ${styleText('dim', model.version)}`);\n for (const r of model.routes) console.log(` ${styleText('cyan', r.method.padEnd(7))} ${url}${model.basePath}${r.path}`);\n console.log(`\\n${styleText('green', '✔')} Mock server ready at ${styleText('bold', url)} in ${Math.round(performance.now() - started)}ms (seed ${opts.seed})\\n`);\n\n const shutdown = () => mock.close().then(() => process.exit(0));\n process.once('SIGINT', shutdown);\n process.once('SIGTERM', shutdown);\n}\n\n/**\n * Watch every local file of the spec and reload on change, debounced.\n *\n * Watchers are re-created after every reload, failed ones included: on Linux and macOS\n * a watcher follows the file, not its name, so an editor's save (write a temp file,\n * rename it over the original) leaves the old watcher on a file that no longer exists.\n */\nfunction watchSources(model: MockModel, reload: () => Promise<MockModel | undefined>) {\n let watchers: fs.FSWatcher[] = [];\n let files = model.sources;\n let timer: NodeJS.Timeout | undefined;\n let retry: NodeJS.Timeout | undefined;\n const onChange = () => {\n clearTimeout(timer);\n timer = setTimeout(async () => {\n files = (await reload())?.sources ?? files;\n watch();\n }, 100);\n };\n const watch = () => {\n watchers.forEach((w) => w.close());\n watchers = [];\n clearTimeout(retry);\n let missing = false;\n for (const file of files) {\n try {\n watchers.push(fs.watch(file, { recursive: fs.statSync(file).isDirectory() }, onChange));\n } catch {\n missing = true; // e.g. mid-save, between delete and rename\n }\n }\n if (missing) retry = setTimeout(watch, 200);\n };\n watch();\n}\n\nasync function generateCmd(spec: string, route: string, seed: number, records: number) {\n const model = await loadSource(spec);\n if (route) {\n const [method = 'GET', p = '/'] = route.includes(' ') ? route.split(/\\s+/) : ['GET', route];\n const [pathname = '/', query] = p.split('?');\n const result = respond(model, { method: method.toUpperCase(), path: pathname, query, seed, records });\n if (result.status >= 400 && !model.routes.some((r) => r.pattern.test(pathname))) fail((result.body as any).message);\n return console.log(JSON.stringify(result.body, null, 2));\n }\n const out: Record<string, unknown> = {};\n for (const r of model.routes) {\n const res = r.responses.find((x) => x.status >= 200 && x.status < 300) ?? r.responses[0];\n out[`${r.method} ${r.path}`] = res?.hasExample ? res.example : generate(res?.schema, { seed, key: `${r.method} ${r.path} ${res?.status}` });\n }\n console.log(JSON.stringify(out, null, 2));\n}\n\nasync function infer(dir: string, out: string | undefined) {\n if (!(await fs.promises.stat(dir).catch(() => undefined))?.isDirectory()) fail(`infer expects a folder of sample responses, got \"${dir}\"`);\n const model = await loadSamples(dir);\n printWarnings(model);\n const text = formatOpenApi(model, out);\n if (!out) return process.stdout.write(text);\n await fs.promises.writeFile(out, text);\n console.error(`${styleText('green', '✔')} Wrote ${model.routes.length} routes to ${out}`);\n}\n\nasync function validate(spec: string, seed: number) {\n const model = await loadSource(spec);\n printWarnings(model);\n const validator = new ResponseValidator(model.openapi);\n let responses = 0;\n let skipped = 0;\n const invalid: string[] = [];\n for (const r of model.routes) {\n for (const res of r.responses) {\n if (!res.schema || res.hasExample) continue;\n const label = `${r.method} ${r.path} ${res.status}`;\n const value = generate(res.schema, { seed, key: label });\n try {\n const result = validator.validate(res.schema, value);\n if (!result.valid) invalid.push(`${label}: ${result.errors}`);\n responses++;\n } catch {\n skipped++; // The schema itself can't be compiled (e.g. a non-JavaScript regex).\n }\n }\n }\n for (const line of invalid) console.error(styleText('red', `✖ ${line}`));\n const summary = `${model.title}: ${model.routes.length} routes, ${responses - invalid.length}/${responses} generated responses valid${skipped ? `, ${skipped} schemas not checkable` : ''}, ${model.warnings.length} warnings`;\n if (invalid.length) fail(summary);\n console.log(`${styleText('green', '✔')} ${summary}`);\n}\n\nfunction printWarnings(model: MockModel) {\n for (const w of model.warnings) console.warn(styleText('yellow', `⚠ ${w}`));\n}\n\nfunction int(value: string, flag: string): number {\n const n = Number(value);\n if (!Number.isInteger(n) || n < 0) fail(`${flag} must be a non-negative integer, got \"${value}\"`);\n return n;\n}\n\nasync function version(): Promise<string> {\n const pkg = JSON.parse(await fs.promises.readFile(path.join(import.meta.dirname, '..', 'package.json'), 'utf8'));\n return pkg.version;\n}\n\nfunction fail(message: string): never {\n console.error(styleText('red', `✖ ${message}`));\n process.exit(1);\n}\n\nmain(process.argv.slice(2)).catch((err) => fail(err.message));\n"]}
@@ -0,0 +1,45 @@
1
+ import type { MockRoute, Schema } from './model.js';
2
+ export interface ListShape {
3
+ /** Property holding the records; undefined when the response is a bare array. */
4
+ key?: string;
5
+ arraySchema: Schema;
6
+ itemSchema: Schema;
7
+ }
8
+ /** Recognize a list response: a bare array of objects, or an object wrapping one. */
9
+ export declare function findListShape(schema: Schema | undefined): ListShape | undefined;
10
+ /** The `GET /users/{id}` route for list route `GET /users`, if the spec has one. */
11
+ export declare function findDetailRoute(routes: MockRoute[], list: MockRoute): MockRoute | undefined;
12
+ export interface PageWindow {
13
+ offset: number;
14
+ limit: number;
15
+ /** 1-based. */
16
+ page: number;
17
+ total: number;
18
+ }
19
+ /** Work out which slice of the collection a request asks for. */
20
+ export declare function readPage(query: URLSearchParams, arraySchema: Schema, total: number, defaultLimit?: number): PageWindow;
21
+ export interface BuildPageInput {
22
+ /** The list response's schema. */
23
+ schema: Schema;
24
+ /** Concrete request path, used to seed records and build next/prev links. */
25
+ requestPath: string;
26
+ query: URLSearchParams;
27
+ origin?: string;
28
+ seed?: number;
29
+ shape: ListShape;
30
+ /** Generated response body; for wrapped lists, its metadata is filled in place. */
31
+ body: unknown;
32
+ total: number;
33
+ /** Name of the id property that links items to the detail route. */
34
+ idKey?: string;
35
+ /** Detail response for an id, used to make list items agree with `GET /x/{id}`. */
36
+ detail?: (id: unknown) => Record<string, unknown> | undefined;
37
+ /** The detail route's schema; fields it shares with the item need no re-check. */
38
+ detailSchema?: Schema;
39
+ }
40
+ export declare function buildPage(input: BuildPageInput): {
41
+ body: unknown;
42
+ headers: Record<string, string>;
43
+ };
44
+ /** Opaque-looking cursors that are really offsets, so any cursor we hand out works. */
45
+ export declare function encodeCursor(offset: number): string;