api-json-server 1.0.1 → 1.2.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.
Files changed (51) hide show
  1. package/README.md +636 -0
  2. package/dist/behavior.js +44 -0
  3. package/dist/history/historyRecorder.js +66 -0
  4. package/dist/history/types.js +2 -0
  5. package/dist/index.js +126 -5
  6. package/dist/loadSpec.js +32 -0
  7. package/dist/logger/customLogger.js +75 -0
  8. package/dist/logger/formatters.js +82 -0
  9. package/dist/logger/types.js +2 -0
  10. package/dist/openapi.js +152 -0
  11. package/dist/registerEndpoints.js +97 -0
  12. package/dist/requestMatch.js +99 -0
  13. package/dist/responseRenderer.js +98 -0
  14. package/dist/server.js +210 -0
  15. package/dist/spec.js +146 -0
  16. package/dist/stringTemplate.js +55 -0
  17. package/examples/auth-variants.json +31 -0
  18. package/examples/basic-crud.json +46 -0
  19. package/examples/companies-nested.json +47 -0
  20. package/examples/orders-and-matches.json +49 -0
  21. package/examples/users-faker.json +35 -0
  22. package/mock.spec.json +1 -0
  23. package/mockserve.spec.schema.json +7 -0
  24. package/package.json +20 -3
  25. package/scripts/build-schema.ts +21 -0
  26. package/src/behavior.ts +56 -0
  27. package/src/history/historyRecorder.ts +77 -0
  28. package/src/history/types.ts +25 -0
  29. package/src/index.ts +124 -85
  30. package/src/loadSpec.ts +5 -2
  31. package/src/logger/customLogger.ts +85 -0
  32. package/src/logger/formatters.ts +74 -0
  33. package/src/logger/types.ts +30 -0
  34. package/src/openapi.ts +203 -0
  35. package/src/registerEndpoints.ts +94 -162
  36. package/src/requestMatch.ts +104 -0
  37. package/src/responseRenderer.ts +112 -0
  38. package/src/server.ts +236 -14
  39. package/src/spec.ts +108 -8
  40. package/src/stringTemplate.ts +55 -0
  41. package/tests/behavior.test.ts +88 -0
  42. package/tests/cors.test.ts +128 -0
  43. package/tests/faker.test.ts +175 -0
  44. package/tests/fixtures/spec.basic.json +39 -0
  45. package/tests/headers.test.ts +124 -0
  46. package/tests/helpers.ts +28 -0
  47. package/tests/history.test.ts +188 -0
  48. package/tests/matching.test.ts +245 -0
  49. package/tests/server.test.ts +73 -0
  50. package/tests/template.test.ts +90 -0
  51. package/src/template.ts +0 -61
@@ -0,0 +1,47 @@
1
+ {
2
+ "$schema": "../mockserve.spec.schema.json",
3
+ "version": 1,
4
+ "settings": {
5
+ "delayMs": 0,
6
+ "errorRate": 0,
7
+ "errorStatus": 500,
8
+ "errorResponse": { "error": "Mock error" },
9
+ "fakerSeed": 42
10
+ },
11
+ "endpoints": [
12
+ {
13
+ "method": "GET",
14
+ "path": "/companies",
15
+ "response": {
16
+ "companies": {
17
+ "__repeat": {
18
+ "min": 3,
19
+ "max": 5,
20
+ "template": {
21
+ "id": { "__faker": "string.uuid" },
22
+ "name": { "__faker": "company.name" },
23
+ "domain": { "__faker": "internet.domainName" },
24
+ "address": {
25
+ "street": { "__faker": "location.streetAddress" },
26
+ "city": { "__faker": "location.city" },
27
+ "zip": { "__faker": "location.zipCode" },
28
+ "country": { "__faker": "location.country" }
29
+ },
30
+ "contacts": {
31
+ "__repeat": {
32
+ "count": 2,
33
+ "min": 0,
34
+ "template": {
35
+ "name": { "__faker": "person.fullName" },
36
+ "email": { "__faker": "internet.email" },
37
+ "phone": { "__faker": "phone.number" }
38
+ }
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ ]
47
+ }
@@ -0,0 +1,49 @@
1
+ {
2
+ "$schema": "../mockserve.spec.schema.json",
3
+ "version": 1,
4
+ "settings": {
5
+ "delayMs": 0,
6
+ "errorRate": 0,
7
+ "errorStatus": 500,
8
+ "errorResponse": { "error": "Mock error" }
9
+ },
10
+ "endpoints": [
11
+ {
12
+ "method": "GET",
13
+ "path": "/orders",
14
+ "match": { "query": { "status": "pending" } },
15
+ "response": {
16
+ "status": "{{query.status}}",
17
+ "orders": {
18
+ "__repeat": {
19
+ "min": 5,
20
+ "max": 8,
21
+ "template": {
22
+ "id": { "__faker": "string.uuid" },
23
+ "total": { "__faker": { "method": "number.int", "args": [ { "min": 20, "max": 200 } ] } },
24
+ "placedAt": { "__faker": { "method": "date.recent", "args": [7] } }
25
+ }
26
+ }
27
+ }
28
+ }
29
+ },
30
+ {
31
+ "method": "POST",
32
+ "path": "/orders",
33
+ "status": 201,
34
+ "response": {
35
+ "id": { "__faker": "string.uuid" },
36
+ "priority": "{{body.priority}}",
37
+ "message": "Order created"
38
+ },
39
+ "variants": [
40
+ {
41
+ "name": "high priority",
42
+ "match": { "body": { "priority": "high" } },
43
+ "status": 202,
44
+ "response": { "queued": true, "message": "Order queued for review" }
45
+ }
46
+ ]
47
+ }
48
+ ]
49
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "../mockserve.spec.schema.json",
3
+ "version": 1,
4
+ "settings": {
5
+ "delayMs": 0,
6
+ "errorRate": 0,
7
+ "errorStatus": 500,
8
+ "errorResponse": { "error": "Mock error" },
9
+ "fakerSeed": 123
10
+ },
11
+ "endpoints": [
12
+ {
13
+ "method": "GET",
14
+ "path": "/users",
15
+ "response": {
16
+ "users": {
17
+ "__repeat": {
18
+ "min": 10,
19
+ "max": 15,
20
+ "template": {
21
+ "id": { "__faker": "string.uuid" },
22
+ "firstName": { "__faker": "person.firstName" },
23
+ "lastName": { "__faker": "person.lastName" },
24
+ "avatarUrl": { "__faker": "image.avatar" },
25
+ "phone": { "__faker": "phone.number" },
26
+ "email": { "__faker": "internet.email" },
27
+ "company": { "__faker": "company.name" },
28
+ "joinedAt": { "__faker": { "method": "date.recent", "args": [30] } }
29
+ }
30
+ }
31
+ }
32
+ }
33
+ }
34
+ ]
35
+ }
package/mock.spec.json CHANGED
@@ -1,4 +1,5 @@
1
1
  {
2
+ "$schema": "./mockserve.spec.schema.json",
2
3
  "version": 1,
3
4
  "settings": {
4
5
  "delayMs": 0,
@@ -0,0 +1,7 @@
1
+ {
2
+ "$ref": "#/definitions/MockServeSpec",
3
+ "definitions": {
4
+ "MockServeSpec": {}
5
+ },
6
+ "$schema": "http://json-schema.org/draft-07/schema#"
7
+ }
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "api-json-server",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "dev": "tsx src/index.ts",
8
8
  "build": "tsc",
9
- "start": "node dist/index.js"
9
+ "start": "node dist/index.js",
10
+ "test": "vitest run",
11
+ "test:watch": "vitest",
12
+ "schema:build": "tsx scripts/build-schema.ts"
10
13
  },
11
14
  "bin": {
12
15
  "mockserve": "dist/index.js"
@@ -17,12 +20,26 @@
17
20
  "type": "commonjs",
18
21
  "devDependencies": {
19
22
  "@types/node": "^25.0.9",
23
+ "@types/supertest": "^6.0.3",
24
+ "@types/swagger-ui-dist": "^3.30.6",
25
+ "supertest": "^7.2.2",
20
26
  "tsx": "^4.21.0",
21
- "typescript": "^5.9.3"
27
+ "typescript": "^5.9.3",
28
+ "vitest": "^4.0.17",
29
+ "zod-to-json-schema": "^3.25.1"
22
30
  },
23
31
  "dependencies": {
32
+ "@faker-js/faker": "^10.2.0",
33
+ "@fastify/cookie": "^11.0.2",
34
+ "@fastify/cors": "^11.2.0",
35
+ "@fastify/http-proxy": "^11.4.1",
36
+ "@fastify/static": "^9.0.0",
37
+ "chalk": "^5.6.2",
24
38
  "commander": "^14.0.2",
25
39
  "fastify": "^5.7.1",
40
+ "pino-pretty": "^13.1.3",
41
+ "swagger-ui-dist": "^5.31.0",
42
+ "yaml": "^2.8.2",
26
43
  "zod": "^4.3.5"
27
44
  }
28
45
  }
@@ -0,0 +1,21 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { zodToJsonSchema } from "zod-to-json-schema";
3
+ import { MockSpecSchema } from "../src/spec.js";
4
+
5
+ /**
6
+ * Build the JSON schema file from the Zod spec.
7
+ */
8
+ async function main() {
9
+ const schema = MockSpecSchema as unknown as Parameters<typeof zodToJsonSchema>[0];
10
+ const jsonSchema = zodToJsonSchema(schema, {
11
+ name: "MockServeSpec"
12
+ });
13
+
14
+ await writeFile("mockserve.spec.schema.json", JSON.stringify(jsonSchema, null, 2), "utf-8");
15
+ console.log("Wrote mockserve.spec.schema.json");
16
+ }
17
+
18
+ main().catch((err) => {
19
+ console.error(err);
20
+ process.exit(1);
21
+ });
@@ -0,0 +1,56 @@
1
+ import type { TemplateValue } from "./spec.js";
2
+ import { faker } from "@faker-js/faker";
3
+
4
+ export type DelayConfig = number | { min: number; max: number };
5
+
6
+ export type BehaviorSettings = {
7
+ delayMs: number;
8
+ errorRate: number;
9
+ errorStatus: number;
10
+ errorResponse: TemplateValue;
11
+ };
12
+
13
+ export type BehaviorOverrides = Partial<BehaviorSettings> & {
14
+ delay?: DelayConfig;
15
+ };
16
+
17
+ /**
18
+ * Resolve a delay value from either a number or a range configuration.
19
+ */
20
+ export function resolveDelay(delay?: DelayConfig): number {
21
+ if (!delay) return 0;
22
+ if (typeof delay === "number") return delay;
23
+ return faker.number.int({ min: delay.min, max: delay.max });
24
+ }
25
+
26
+ /**
27
+ * Pause for the given number of milliseconds.
28
+ */
29
+ export function sleep(ms: number): Promise<void> {
30
+ return new Promise((resolve) => setTimeout(resolve, ms));
31
+ }
32
+
33
+ /**
34
+ * Decide whether a request should fail based on an error rate.
35
+ */
36
+ export function shouldFail(errorRate: number): boolean {
37
+ if (errorRate <= 0) return false;
38
+ if (errorRate >= 1) return true;
39
+ return Math.random() < errorRate;
40
+ }
41
+
42
+ /**
43
+ * Resolve behavior settings with precedence: chosen overrides -> endpoint overrides -> global settings.
44
+ */
45
+ export function resolveBehavior(
46
+ settings: BehaviorSettings,
47
+ endpointOverrides?: BehaviorOverrides,
48
+ chosenOverrides?: BehaviorOverrides
49
+ ): BehaviorSettings {
50
+ return {
51
+ delayMs: chosenOverrides?.delayMs ?? endpointOverrides?.delayMs ?? settings.delayMs,
52
+ errorRate: chosenOverrides?.errorRate ?? endpointOverrides?.errorRate ?? settings.errorRate,
53
+ errorStatus: chosenOverrides?.errorStatus ?? endpointOverrides?.errorStatus ?? settings.errorStatus,
54
+ errorResponse: chosenOverrides?.errorResponse ?? endpointOverrides?.errorResponse ?? settings.errorResponse
55
+ };
56
+ }
@@ -0,0 +1,77 @@
1
+ import type { HistoryEntry, HistoryFilter } from "./types.js";
2
+ import { randomUUID } from "node:crypto";
3
+
4
+ /**
5
+ * In-memory request history recorder.
6
+ */
7
+ export class HistoryRecorder {
8
+ private entries: HistoryEntry[] = [];
9
+ private readonly maxEntries: number;
10
+
11
+ /**
12
+ * Create a new history recorder.
13
+ * @param maxEntries Maximum number of entries to keep (default 1000).
14
+ */
15
+ constructor(maxEntries = 1000) {
16
+ this.maxEntries = maxEntries;
17
+ }
18
+
19
+ /**
20
+ * Record a new request.
21
+ */
22
+ record(entry: Omit<HistoryEntry, "id" | "timestamp">): HistoryEntry {
23
+ const fullEntry: HistoryEntry = {
24
+ id: randomUUID(),
25
+ timestamp: new Date().toISOString(),
26
+ ...entry
27
+ };
28
+
29
+ this.entries.push(fullEntry);
30
+
31
+ // Keep only the most recent entries
32
+ if (this.entries.length > this.maxEntries) {
33
+ this.entries.shift();
34
+ }
35
+
36
+ return fullEntry;
37
+ }
38
+
39
+ /**
40
+ * Get all history entries, optionally filtered.
41
+ */
42
+ query(filter?: HistoryFilter): HistoryEntry[] {
43
+ let results = this.entries;
44
+
45
+ if (filter?.endpoint) {
46
+ results = results.filter((e) => e.path === filter.endpoint);
47
+ }
48
+
49
+ if (filter?.method) {
50
+ results = results.filter((e) => e.method.toUpperCase() === filter.method?.toUpperCase());
51
+ }
52
+
53
+ if (filter?.statusCode !== undefined) {
54
+ results = results.filter((e) => e.statusCode === filter.statusCode);
55
+ }
56
+
57
+ if (filter?.limit && filter.limit > 0) {
58
+ results = results.slice(-filter.limit);
59
+ }
60
+
61
+ return results;
62
+ }
63
+
64
+ /**
65
+ * Clear all history entries.
66
+ */
67
+ clear(): void {
68
+ this.entries = [];
69
+ }
70
+
71
+ /**
72
+ * Get the total number of recorded entries.
73
+ */
74
+ count(): number {
75
+ return this.entries.length;
76
+ }
77
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Request history entry structure.
3
+ */
4
+ export interface HistoryEntry {
5
+ id: string;
6
+ timestamp: string;
7
+ method: string;
8
+ url: string;
9
+ path: string;
10
+ query: Record<string, unknown>;
11
+ headers: Record<string, string | string[] | undefined>;
12
+ body: unknown;
13
+ statusCode?: number;
14
+ responseTime?: number;
15
+ }
16
+
17
+ /**
18
+ * Filter options for querying history.
19
+ */
20
+ export interface HistoryFilter {
21
+ endpoint?: string;
22
+ method?: string;
23
+ statusCode?: number;
24
+ limit?: number;
25
+ }
package/src/index.ts CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  import { Command } from "commander";
4
4
  import { watch } from "node:fs";
5
+ import { createLogger, logServerStart, logServerReload } from "./logger/customLogger.js";
6
+ import type { LoggerOptions } from "./logger/types.js";
5
7
 
6
8
  const program = new Command();
7
9
 
@@ -17,106 +19,143 @@ program
17
19
  .option("-s, --spec <path>", "Path to the spec JSON file", "mock.spec.json")
18
20
  .option("--watch", "Reload when spec file changes", true)
19
21
  .option("--no-watch", "Disable reload when spec file changes")
20
- .action(async (opts: { port: string; spec: string, watch: boolean }) => {
21
- const port = Number(opts.port);
22
-
23
- if (!Number.isFinite(port) || port <= 0) {
24
- console.error(`Invalid port: ${opts.port}`);
25
- process.exit(1);
26
- }
27
-
28
- const specPath = opts.spec;
29
-
30
- const { loadSpecFromFile } = await import("./loadSpec.js");
31
- const { buildServer } = await import("./server.js");
22
+ .option("--base-url <url>", "Public base URL used in OpenAPI servers[] (e.g. https://example.com)")
23
+ .option("--log-format <format>", "Log format: pretty or json", "pretty")
24
+ .option("--log-level <level>", "Log level: trace, debug, info, warn, error, fatal", "info")
25
+ .action(async (opts: { port: string; spec: string; watch: boolean; baseUrl?: string; logFormat: string; logLevel: string }) => {
26
+ await startCommand(opts);
27
+ });
32
28
 
33
- let app = null as null | import("fastify").FastifyInstance;
34
- let isReloading = false;
35
- let debounceTimer: NodeJS.Timeout | null = null;
29
+ /**
30
+ * Run the mock server CLI command.
31
+ */
32
+ async function startCommand(opts: { port: string; spec: string; watch: boolean; baseUrl?: string; logFormat: string; logLevel: string }): Promise<void> {
33
+ const port = Number(opts.port);
34
+
35
+ if (!Number.isFinite(port) || port <= 0) {
36
+ console.error(`Invalid port: ${opts.port}`);
37
+ process.exit(1);
38
+ }
39
+
40
+ const specPath = opts.spec;
41
+
42
+ // Create logger based on CLI options
43
+ const logFormat = opts.logFormat === "json" ? "json" : "pretty";
44
+ const logLevel = ["trace", "debug", "info", "warn", "error", "fatal"].includes(opts.logLevel)
45
+ ? opts.logLevel as LoggerOptions["level"]
46
+ : "info";
47
+
48
+ const logger = createLogger({
49
+ enabled: true,
50
+ format: logFormat,
51
+ level: logLevel
52
+ });
36
53
 
37
- async function startWithSpec() {
38
- const loadedAt = new Date().toISOString();
54
+ const { loadSpecFromFile } = await import("./loadSpec.js");
55
+ const { buildServer } = await import("./server.js");
39
56
 
40
- const spec = await loadSpecFromFile(specPath);
41
- console.log(`Loaded spec v${spec.version} with ${spec.endpoints.length} endpoint(s).`);
57
+ let app = null as null | import("fastify").FastifyInstance;
58
+ let isReloading = false;
59
+ let debounceTimer: NodeJS.Timeout | null = null;
42
60
 
43
- const nextApp = buildServer(spec, { specPath, loadedAt });
44
- try {
45
- await nextApp.listen({ port, host: "0.0.0.0" });
46
- } catch (err) {
47
- nextApp.log.error(err);
48
- throw err;
49
- }
61
+ /**
62
+ * Build and start a server using the current spec file.
63
+ */
64
+ async function startWithSpec() {
65
+ const loadedAt = new Date().toISOString();
50
66
 
51
- nextApp.log.info(`Mock server running on http://localhost:${port}`);
52
- nextApp.log.info(`Spec: ${specPath} (loadedAt=${loadedAt})`);
67
+ const spec = await loadSpecFromFile(specPath);
68
+ logger.info(`Loaded spec v${spec.version} with ${spec.endpoints.length} endpoint(s).`);
53
69
 
54
- return nextApp;
70
+ const nextApp = buildServer(spec, { specPath, loadedAt, baseUrl: opts.baseUrl, logger: true });
71
+ try {
72
+ await nextApp.listen({ port, host: "0.0.0.0" });
73
+ } catch (err) {
74
+ nextApp.log.error(err);
75
+ throw err;
55
76
  }
56
77
 
57
- async function reload() {
58
- if (isReloading) return;
59
- isReloading = true;
78
+ logServerStart(nextApp.log, port, specPath);
60
79
 
61
- try {
62
- console.log("Reloading spec...");
63
-
64
- // 1) Stop accepting requests on the old server FIRST
65
- if (app) {
66
- console.log("Closing current server...");
67
- await app.close();
68
- console.log("Current server closed.");
69
- app = null;
70
- }
80
+ return nextApp;
81
+ }
71
82
 
72
- // 2) Start a new server on the same port with the updated spec
73
- app = await startWithSpec();
74
-
75
- console.log("Reload complete.");
76
- } catch (err) {
77
- console.error("Reload failed.");
78
-
79
- // At this point the old server may already be closed. We want visibility.
80
- console.error(String(err));
81
-
82
- // Optional: try to start again to avoid being down
83
- try {
84
- if (!app) {
85
- console.log("Attempting to start server again after reload failure...");
86
- app = await startWithSpec();
87
- console.log("Recovery start succeeded.");
88
- }
89
- } catch (err2) {
90
- console.error("Recovery start failed. Server is down until next successful reload.");
91
- console.error(String(err2));
92
- }
93
- } finally {
94
- isReloading = false;
95
- }
96
- }
83
+ /**
84
+ * Reload the server when the spec changes.
85
+ */
86
+ async function reload() {
87
+ if (isReloading) return;
88
+ isReloading = true;
97
89
 
98
- // Initial start
99
90
  try {
91
+ logger.info("Reloading spec...");
92
+
93
+ // 1) Stop accepting requests on the old server FIRST
94
+ if (app) {
95
+ logger.debug("Closing current server...");
96
+ await app.close();
97
+ logger.debug("Current server closed.");
98
+ app = null;
99
+ }
100
+
101
+ // 2) Start a new server on the same port with the updated spec
100
102
  app = await startWithSpec();
103
+
104
+ logServerReload(logger, true);
101
105
  } catch (err) {
102
- console.error(String(err));
103
- process.exit(1);
104
- }
106
+ const errorMsg = err instanceof Error ? err.message : String(err);
107
+ logServerReload(logger, false, errorMsg);
105
108
 
106
- // Watch spec for changes
107
- if (opts.watch) {
108
- console.log(`Watching spec file for changes: ${specPath}`);
109
-
110
- // fs.watch emits multiple events; debounce to avoid rapid reload loops
111
- watch(specPath, () => {
112
- if (debounceTimer) clearTimeout(debounceTimer);
113
- debounceTimer = setTimeout(() => {
114
- void reload();
115
- }, 200);
116
- });
117
- } else {
118
- console.log("Watch disabled (--no-watch).");
109
+ // Optional: try to start again to avoid being down
110
+ try {
111
+ if (!app) {
112
+ logger.info("Attempting to start server again after reload failure...");
113
+ app = await startWithSpec();
114
+ logger.info("Recovery start succeeded.");
115
+ }
116
+ } catch (err2) {
117
+ logger.error("Recovery start failed. Server is down until next successful reload.");
118
+ logger.error(err2);
119
+ }
120
+ } finally {
121
+ isReloading = false;
119
122
  }
120
- });
123
+ }
124
+
125
+ // Initial start
126
+ try {
127
+ app = await startWithSpec();
128
+ } catch (err) {
129
+ logger.error(err);
130
+ process.exit(1);
131
+ }
132
+
133
+ // Watch spec for changes
134
+ /**
135
+ * Handle file changes with a debounced reload.
136
+ */
137
+ function onSpecChange(): void {
138
+ debounceTimer = scheduleReload(reload, debounceTimer);
139
+ }
140
+
141
+ if (opts.watch) {
142
+ logger.info(`Watching spec file for changes: ${specPath}`);
143
+
144
+ // fs.watch emits multiple events; debounce to avoid rapid reload loops
145
+ watch(specPath, onSpecChange);
146
+ } else {
147
+ logger.info("Watch disabled (--no-watch).");
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Schedule a debounced reload when the spec changes.
153
+ */
154
+ function scheduleReload(reload: () => Promise<void>, debounceTimer: NodeJS.Timeout | null): NodeJS.Timeout {
155
+ if (debounceTimer) clearTimeout(debounceTimer);
156
+ return setTimeout(() => {
157
+ void reload();
158
+ }, 200);
159
+ }
121
160
 
122
161
  program.parse(process.argv);
package/src/loadSpec.ts CHANGED
@@ -1,6 +1,9 @@
1
- import { readFile } from 'node:fs/promises'
2
- import { MockSpecSchema, type MockSpecInferSchema } from './spec.js'
1
+ import { readFile } from "node:fs/promises";
2
+ import { MockSpecSchema, type MockSpecInferSchema } from "./spec.js";
3
3
 
4
+ /**
5
+ * Load and validate a mock spec from disk.
6
+ */
4
7
  export async function loadSpecFromFile(specPath: string): Promise<MockSpecInferSchema> {
5
8
  let raw: string
6
9
  try {