rapidoc 12.0.0 → 12.1.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 (77) hide show
  1. package/README.md +127 -25
  2. package/dist/rapidoc-flow-min.js +580 -0
  3. package/dist/rapidoc-flow-min.js.LICENSE.txt +171 -0
  4. package/dist/rapidoc-min.js +541 -1309
  5. package/dist/rapidoc-min.js.LICENSE.txt +381 -0
  6. package/package.json +21 -18
  7. package/src/README.md +150 -0
  8. package/src/components/README.md +91 -0
  9. package/src/components/api-request.ts +43 -0
  10. package/src/components/api-response.ts +9 -4
  11. package/src/components/schema-base-element.ts +18 -4
  12. package/src/components/schema-table.ts +58 -13
  13. package/src/components/schema-tree.ts +7 -0
  14. package/src/components/tag-input.ts +5 -9
  15. package/src/flow.js +3 -0
  16. package/src/rapi-flow.ts +1178 -0
  17. package/src/rapidoc-mini.ts +27 -6
  18. package/src/rapidoc.ts +108 -62
  19. package/src/styles/README.md +96 -0
  20. package/src/styles/css-bundle.js +2 -2
  21. package/src/styles/endpoint-styles.js +6 -1
  22. package/src/styles/flow-styles.js +248 -0
  23. package/src/styles/nav-styles.js +39 -15
  24. package/src/styles/schema-styles.js +13 -5
  25. package/src/styles/theme-tokens.js +2 -2
  26. package/src/templates/README.md +89 -0
  27. package/src/templates/api-response-template.js +14 -10
  28. package/src/templates/callback-template.js +1 -1
  29. package/src/templates/downloads-template.js +12 -27
  30. package/src/templates/endpoint-template.js +15 -24
  31. package/src/templates/expanded-endpoint-template.js +8 -4
  32. package/src/templates/focused-endpoint-template.js +4 -3
  33. package/src/templates/navbar-template.js +20 -11
  34. package/src/templates/request-body-template.js +29 -7
  35. package/src/templates/request-params-template.js +15 -4
  36. package/src/templates/security-scheme-template.js +11 -2
  37. package/src/templates/server-template.js +3 -2
  38. package/src/utils/README.md +146 -0
  39. package/src/utils/arazzo/README.md +119 -0
  40. package/src/utils/arazzo/components.js +15 -0
  41. package/src/utils/arazzo/graph.js +269 -0
  42. package/src/utils/arazzo/index.js +52 -0
  43. package/src/utils/arazzo/operations.js +106 -0
  44. package/src/utils/arazzo/parse.js +196 -0
  45. package/src/utils/arazzo/refs.js +40 -0
  46. package/src/utils/arazzo/svg.js +707 -0
  47. package/src/utils/curl-utils.js +5 -2
  48. package/src/utils/highlighter.js +3 -0
  49. package/src/utils/mcp/README.md +155 -0
  50. package/src/utils/mcp/common.js +45 -346
  51. package/src/utils/mcp/manifest.js +382 -0
  52. package/src/utils/mcp/project/server.ts +53 -0
  53. package/src/utils/mcp/project.js +88 -0
  54. package/src/utils/mcp/runtime.js +289 -0
  55. package/src/utils/mcp/schema.js +98 -0
  56. package/src/utils/mcp/workflow-runtime.js +374 -0
  57. package/src/utils/mcp/workflows.js +240 -0
  58. package/src/utils/mcp/zip.js +102 -0
  59. package/src/utils/mcp-generator.js +29 -125
  60. package/src/utils/mock-interceptor.js +134 -85
  61. package/src/utils/request-utils.js +61 -0
  62. package/src/utils/schema-utils.js +33 -6
  63. package/src/utils/slugger.js +5 -7
  64. package/src/utils/spec-parser.js +190 -164
  65. package/src/utils/url-utils.js +36 -1
  66. package/dist/rapidoc-static-min.js +0 -13542
  67. package/src/static/index.d.ts +0 -149
  68. package/src/static/index.js +0 -62
  69. package/src/static/navigation.js +0 -497
  70. package/src/static/render.js +0 -490
  71. package/src/static/serializer.js +0 -216
  72. package/src/utils/mcp/generators/csharp.js +0 -242
  73. package/src/utils/mcp/generators/go.js +0 -298
  74. package/src/utils/mcp/generators/java.js +0 -330
  75. package/src/utils/mcp/generators/python.js +0 -248
  76. package/src/utils/mcp/generators/rust.js +0 -281
  77. package/src/utils/mcp/generators/typescript.js +0 -290
@@ -0,0 +1,102 @@
1
+ /**
2
+ * A minimal ZIP writer (stored entries, no compression) for the exported MCP project, so the
3
+ * download needs no library. Entries are UTF-8 text files.
4
+ */
5
+
6
+ const CRC_TABLE = (() => {
7
+ const table = new Uint32Array(256);
8
+ for (let n = 0; n < 256; n++) {
9
+ let c = n;
10
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
11
+ table[n] = c >>> 0;
12
+ }
13
+ return table;
14
+ })();
15
+
16
+ export function crc32(bytes) {
17
+ let crc = 0xffffffff;
18
+ for (let i = 0; i < bytes.length; i++) crc = CRC_TABLE[(crc ^ bytes[i]) & 0xff] ^ (crc >>> 8);
19
+ return (crc ^ 0xffffffff) >>> 0;
20
+ }
21
+
22
+ /**
23
+ * @param {Record<string, string>} files path → text content, in the order they should be stored
24
+ * @returns {Uint8Array} the archive
25
+ */
26
+ export function createZip(files) {
27
+ const encoder = new TextEncoder();
28
+ const entries = Object.entries(files).map(([path, text]) => ({
29
+ name: encoder.encode(path),
30
+ data: encoder.encode(String(text ?? "")),
31
+ }));
32
+ for (const e of entries) e.crc = crc32(e.data);
33
+
34
+ const localSize = entries.reduce((n, e) => n + 30 + e.name.length + e.data.length, 0);
35
+ const centralSize = entries.reduce((n, e) => n + 46 + e.name.length, 0);
36
+ const out = new Uint8Array(localSize + centralSize + 22);
37
+ const view = new DataView(out.buffer);
38
+ let pos = 0;
39
+ const u16 = (v) => {
40
+ view.setUint16(pos, v, true);
41
+ pos += 2;
42
+ };
43
+ const u32 = (v) => {
44
+ view.setUint32(pos, v >>> 0, true);
45
+ pos += 4;
46
+ };
47
+ const bytes = (b) => {
48
+ out.set(b, pos);
49
+ pos += b.length;
50
+ };
51
+ // Fixed timestamp (1980-01-01 00:00), flag bit 11: names are UTF-8
52
+ const DOS_TIME = 0;
53
+ const DOS_DATE = 0x21;
54
+ const FLAGS = 0x0800;
55
+
56
+ for (const e of entries) {
57
+ e.offset = pos;
58
+ u32(0x04034b50);
59
+ u16(20);
60
+ u16(FLAGS);
61
+ u16(0); // stored
62
+ u16(DOS_TIME);
63
+ u16(DOS_DATE);
64
+ u32(e.crc);
65
+ u32(e.data.length);
66
+ u32(e.data.length);
67
+ u16(e.name.length);
68
+ u16(0);
69
+ bytes(e.name);
70
+ bytes(e.data);
71
+ }
72
+ const centralOffset = pos;
73
+ for (const e of entries) {
74
+ u32(0x02014b50);
75
+ u16(20);
76
+ u16(20);
77
+ u16(FLAGS);
78
+ u16(0);
79
+ u16(DOS_TIME);
80
+ u16(DOS_DATE);
81
+ u32(e.crc);
82
+ u32(e.data.length);
83
+ u32(e.data.length);
84
+ u16(e.name.length);
85
+ u16(0);
86
+ u16(0);
87
+ u16(0);
88
+ u16(0);
89
+ u32(0);
90
+ u32(e.offset);
91
+ bytes(e.name);
92
+ }
93
+ u32(0x06054b50);
94
+ u16(0);
95
+ u16(0);
96
+ u16(entries.length);
97
+ u16(entries.length);
98
+ u32(centralSize);
99
+ u32(centralOffset);
100
+ u16(0);
101
+ return out;
102
+ }
@@ -1,143 +1,47 @@
1
1
  /**
2
- * MCP (Model Context Protocol) Server Generator for RapiDoc
2
+ * MCP (Model Context Protocol) server export for RapiDoc.
3
3
  *
4
- * Implements client-side, zero-runtime-dependency translation from OpenAPI specs
5
- * into curated, token-efficient MCP servers across multiple language ecosystems:
6
- * - TypeScript (@modelcontextprotocol/sdk + zod)
7
- * - Python (fastmcp + standard urllib)
8
- * - Go (mark3labs/mcp-go)
9
- * - C# (.NET / ModelContextProtocol.NET)
10
- * - Rust (mcp-sdk / rmcp)
11
- * - Java (Spring AI MCP)
12
- *
13
- * Honors primary `x-mcp` extensions and compatibility aliases from Redocly,
14
- * Scalar, OpenAI (x-openai-isConsequential), and Speakeasy.
4
+ * An OpenAPI document becomes an MCP manifest (plain JSON: one tool per operation, with JSON
5
+ * Schema arguments and MCP safety annotations), and the exported project is that manifest plus
6
+ * two fixed files: a runtime that executes it with fetch, and a stdio server built on
7
+ * @modelcontextprotocol/sdk.
15
8
  */
16
9
 
17
10
  export {
18
11
  AUTH_TRANSPORT_HEADERS,
19
- VALID_MCP_TARGETS,
20
- MCP_TARGET_LABELS,
21
- parseMcpTargets,
12
+ parseMcpEnabled,
22
13
  toSnakeCase,
23
- toPascalCase,
24
- toCamelCase,
25
14
  sanitizeDescription,
26
15
  deriveToolName,
27
- extractParameters,
28
- extractMcpOperations,
29
16
  estimateTokenCost,
17
+ envNameFor,
30
18
  generateHostConfigs,
31
19
  } from "./mcp/common.js";
20
+ export { buildMcpManifest, toJsonSchema, MANIFEST_VERSION } from "./mcp/manifest.js";
21
+ export { compileWorkflows } from "./mcp/workflows.js";
22
+ export { createMcpRuntime, buildRequest, credentialsFromEnv } from "./mcp/runtime.js";
23
+ export { writeMcpProject } from "./mcp/project.js";
24
+ export { createZip } from "./mcp/zip.js";
32
25
 
33
- import { extractMcpOperations, estimateTokenCost, generateHostConfigs } from "./mcp/common.js";
34
- import {
35
- generateTypeScriptMcpServer,
36
- generateTypeScriptFiles,
37
- } from "./mcp/generators/typescript.js";
38
- import { generatePythonFastMcpServer, generatePythonFiles } from "./mcp/generators/python.js";
39
- import { generateGoMcpServer, generateGoFiles } from "./mcp/generators/go.js";
40
- import { generateCSharpMcpServer, generateCSharpFiles } from "./mcp/generators/csharp.js";
41
- import { generateRustMcpServer, generateRustFiles } from "./mcp/generators/rust.js";
42
- import { generateJavaMcpServer, generateJavaFiles } from "./mcp/generators/java.js";
43
-
44
- export {
45
- generateTypeScriptMcpServer,
46
- generatePythonFastMcpServer,
47
- generateGoMcpServer,
48
- generateCSharpMcpServer,
49
- generateRustMcpServer,
50
- generateJavaMcpServer,
51
- };
26
+ import { estimateTokenCost, generateHostConfigs } from "./mcp/common.js";
27
+ import { buildMcpManifest } from "./mcp/manifest.js";
28
+ import { writeMcpProject } from "./mcp/project.js";
52
29
 
53
30
  /**
54
- * Main orchestrator: extracts MCP operations, computes budget, and generates
55
- * language-specific server implementation and packaging manifests.
31
+ * Builds the manifest and the files of the exported project.
56
32
  *
57
- * @param {object} parsedSpec Raw or parsed OpenAPI specification object
58
- * @param {object} options Target language, server name, and filtering options
59
- * @returns {object} { target, serverCode, hostConfig, budget, files, extracted }
33
+ * @param {object} spec Raw OpenAPI object or `ProcessSpec`'s result
34
+ * @param {object} [options] `serverName`, `includeInternal`, `includeDeprecated`, `arazzo`,
35
+ * `workflowRuntime` (the text of workflow-runtime.js for the export; see project.js)
36
+ * @returns {{ manifest: object, files: Record<string, string>, budget: object, hostConfig: string,
37
+ * problems: object[] }} `problems` lists the workflows left out and why
60
38
  */
61
- export function generateMcpServer(parsedSpec, options = {}) {
62
- const rawTarget = (options.target || "typescript").toLowerCase();
63
- const target = rawTarget === "c#" ? "csharp" : rawTarget;
64
- const extracted = extractMcpOperations(parsedSpec, options);
65
- const budget = estimateTokenCost(extracted.tools, extracted.resources, extracted.workflows);
66
- const hostConfigs = generateHostConfigs(options.serverName || extracted.metadata.title, {
67
- target,
68
- });
69
-
70
- let serverCode;
71
- let targetFiles;
72
-
73
- if (target === "python") {
74
- serverCode = generatePythonFastMcpServer(
75
- extracted.metadata,
76
- extracted.tools,
77
- extracted.resources,
78
- extracted.workflows,
79
- options,
80
- );
81
- targetFiles = generatePythonFiles(serverCode, extracted.metadata, options);
82
- } else if (target === "go") {
83
- serverCode = generateGoMcpServer(
84
- extracted.metadata,
85
- extracted.tools,
86
- extracted.resources,
87
- extracted.workflows,
88
- options,
89
- );
90
- targetFiles = generateGoFiles(serverCode, extracted.metadata, options);
91
- } else if (target === "csharp") {
92
- serverCode = generateCSharpMcpServer(
93
- extracted.metadata,
94
- extracted.tools,
95
- extracted.resources,
96
- extracted.workflows,
97
- options,
98
- );
99
- targetFiles = generateCSharpFiles(serverCode, extracted.metadata, options);
100
- } else if (target === "rust") {
101
- serverCode = generateRustMcpServer(
102
- extracted.metadata,
103
- extracted.tools,
104
- extracted.resources,
105
- extracted.workflows,
106
- options,
107
- );
108
- targetFiles = generateRustFiles(serverCode, extracted.metadata, options);
109
- } else if (target === "java") {
110
- serverCode = generateJavaMcpServer(
111
- extracted.metadata,
112
- extracted.tools,
113
- extracted.resources,
114
- extracted.workflows,
115
- options,
116
- );
117
- targetFiles = generateJavaFiles(serverCode, extracted.metadata, options);
118
- } else {
119
- // Default: TypeScript
120
- serverCode = generateTypeScriptMcpServer(
121
- extracted.metadata,
122
- extracted.tools,
123
- extracted.resources,
124
- extracted.workflows,
125
- options,
126
- );
127
- targetFiles = generateTypeScriptFiles(serverCode, extracted.metadata, options);
128
- }
129
-
130
- const files = {
131
- "claude_desktop_config.json": hostConfigs.claudeDesktopConfig,
132
- ...targetFiles,
133
- };
134
-
135
- return {
136
- target,
137
- serverCode,
138
- hostConfig: hostConfigs.claudeDesktopConfig,
139
- budget,
140
- files,
141
- extracted,
142
- };
39
+ export function generateMcpServer(spec, options = {}) {
40
+ const problems = [];
41
+ const manifest = buildMcpManifest(spec, { ...options, onProblem: (p) => problems.push(p) });
42
+ const files = writeMcpProject(manifest, options);
43
+ const budget = estimateTokenCost(manifest);
44
+ const schemes = Object.values(manifest.sources).flatMap((s) => Object.keys(s.securitySchemes));
45
+ const hostConfig = generateHostConfigs(manifest.server.name, { schemes }).claudeDesktopConfig;
46
+ return { manifest, files, budget, hostConfig, problems };
143
47
  }
@@ -23,29 +23,75 @@ const STATUS_TEXTS = {
23
23
  504: "Gateway Timeout",
24
24
  };
25
25
 
26
- let isMockActive = false;
27
26
  let originalFetch = null;
28
27
  let originalXhrOpen = null;
29
28
  let originalXhrSend = null;
30
29
  let originalXhrSetRequestHeader = null;
31
30
 
32
- let compiledRoutes = [];
33
- let mockConfig = {
34
- statusCode: "",
35
- statusStrategy: "first",
36
- delay: 0,
37
- log: "true",
38
- };
31
+ /**
32
+ * One mock per owner (the element that turned it on), so several <rapi-doc> and <rapi-doc-mini>
33
+ * on a page can each mock their own API: `{ routes, config, cycle }`. Calls without an owner use
34
+ * DEFAULT_OWNER, so a page with one mocked element works as before.
35
+ */
36
+ const mocks = new Map();
37
+ const DEFAULT_OWNER = Symbol("rapidoc-mock");
39
38
 
40
- const routeCycleIndices = new Map();
39
+ /**
40
+ * The status setting: a string of codes (`"200, 404"`) for every operation, or a map by
41
+ * operation (`{ "POST /store/order": "503, 200", "*": "200" }`, `*` the default), also as JSON
42
+ * text. Map keys are the spec's method and path template, method case-insensitive.
43
+ */
44
+ function normalizeStatus(value) {
45
+ if (value && typeof value === "object") {
46
+ const map = {};
47
+ for (const [key, codes] of Object.entries(value)) {
48
+ const k = key.trim();
49
+ if (!k || !codes) continue;
50
+ map[k === "*" ? "*" : k.replace(/^(\w+)\s+/, (m, method) => `${method.toUpperCase()} `)] =
51
+ String(codes);
52
+ }
53
+ return map;
54
+ }
55
+ const text = typeof value === "string" ? value.trim() : "";
56
+ if (text.startsWith("{")) {
57
+ try {
58
+ return normalizeStatus(JSON.parse(text));
59
+ } catch {
60
+ return "";
61
+ }
62
+ }
63
+ return text;
64
+ }
65
+
66
+ /** The codes for a route: its own entry of a status map, else the map's `*`, else the string. */
67
+ function codesFor(status, routeKey) {
68
+ if (!status || typeof status === "string") return status || "";
69
+ return status[routeKey] ?? status["*"] ?? "";
70
+ }
71
+
72
+ function normalizeConfig(options = {}, current = {}) {
73
+ return {
74
+ statusCode:
75
+ options.statusCode !== undefined
76
+ ? normalizeStatus(options.statusCode)
77
+ : current.statusCode || "",
78
+ statusStrategy:
79
+ options.statusStrategy !== undefined
80
+ ? options.statusStrategy || "first"
81
+ : current.statusStrategy || "first",
82
+ delay: options.delay !== undefined ? Number(options.delay) || 0 : current.delay || 0,
83
+ log: options.log !== undefined ? options.log !== "false" : current.log !== false,
84
+ };
85
+ }
41
86
 
42
87
  /**
43
- * Normalizes and extracts base paths and hostnames from OpenAPI servers array.
88
+ * The bases a route can be requested under, from the OpenAPI servers: `{ origin, base }` per
89
+ * server (origin null for a relative server URL), plus the bare path (`{ origin: null, base: "" }`)
90
+ * so a request without the server's base path still matches.
44
91
  */
45
92
  function extractServerBases(servers = []) {
46
- const bases = new Set();
47
- // Always include empty root base path
48
- bases.add("");
93
+ const bases = new Map();
94
+ bases.set("|", { origin: null, base: "" });
49
95
 
50
96
  servers.forEach((s) => {
51
97
  const rawUrl = s.computedUrl || s.url || "";
@@ -55,17 +101,17 @@ function extractServerBases(servers = []) {
55
101
  if (rawUrl.startsWith("http://") || rawUrl.startsWith("https://")) {
56
102
  const u = new URL(rawUrl);
57
103
  const p = u.pathname.replace(/\/+$/, "");
58
- if (p) bases.add(p);
104
+ bases.set(`${u.origin}|${p}`, { origin: u.origin, base: p });
59
105
  } else {
60
106
  const p = rawUrl.replace(/\/+$/, "");
61
- if (p) bases.add(p.startsWith("/") ? p : `/${p}`);
107
+ if (p) bases.set(`|${p}`, { origin: null, base: p.startsWith("/") ? p : `/${p}` });
62
108
  }
63
109
  } catch {
64
110
  // Ignore invalid URL formats
65
111
  }
66
112
  });
67
113
 
68
- return Array.from(bases);
114
+ return Array.from(bases.values());
69
115
  }
70
116
 
71
117
  /**
@@ -114,7 +160,11 @@ export function compileRoutes(spec) {
114
160
  const endpointBases =
115
161
  pathItem.servers?.length > 0 ? extractServerBases(pathItem.servers) : rootBases;
116
162
 
117
- const patterns = endpointBases.map((base) => compileRoutePattern(pathTemplate, base));
163
+ const patterns = endpointBases.map(({ origin, base }) => ({
164
+ regex: compileRoutePattern(pathTemplate, base),
165
+ origin,
166
+ base,
167
+ }));
118
168
 
119
169
  routes.push({
120
170
  method,
@@ -131,10 +181,12 @@ export function compileRoutes(spec) {
131
181
  }
132
182
 
133
183
  /**
134
- * Matches a request (method + URL) against the compiled routes.
184
+ * Matches a request (method + URL) against every mock's routes. With several mocks, the most
185
+ * specific match wins: a route under the server's origin and base path beats one under the base
186
+ * path alone, which beats a bare path; ties go to the mock enabled first.
135
187
  */
136
188
  function matchRoute(method, urlStr) {
137
- if (!compiledRoutes.length || !urlStr) {
189
+ if (!mocks.size || !urlStr) {
138
190
  return null;
139
191
  }
140
192
 
@@ -152,30 +204,32 @@ function matchRoute(method, urlStr) {
152
204
  const upperMethod = (method || "GET").toUpperCase();
153
205
  const pathname = decodeURI(reqUrl.pathname);
154
206
 
155
- for (const route of compiledRoutes) {
156
- if (route.method !== upperMethod) {
157
- continue;
158
- }
207
+ let best = null;
208
+ for (const mock of mocks.values()) {
209
+ for (const route of mock.routes) {
210
+ if (route.method !== upperMethod) {
211
+ continue;
212
+ }
159
213
 
160
- for (const pattern of route.patterns) {
161
- const match = pathname.match(pattern);
162
- if (match) {
163
- return {
164
- route,
165
- pathParams: match.groups || {},
166
- url: reqUrl,
167
- };
214
+ for (const pattern of route.patterns) {
215
+ const match = pathname.match(pattern.regex);
216
+ if (!match) continue;
217
+ const rank =
218
+ (pattern.base ? 1 : 0) + (pattern.origin && pattern.origin === reqUrl.origin ? 1 : 0);
219
+ if (!best || rank > best.rank) {
220
+ best = { mock, route, rank, pathParams: match.groups || {}, url: reqUrl };
221
+ }
168
222
  }
169
223
  }
170
224
  }
171
225
 
172
- return null;
226
+ return best;
173
227
  }
174
228
 
175
229
  /**
176
230
  * Chooses the status code based on config and strategy (first, cycle, random).
177
231
  */
178
- function selectStatusCode(configuredCodesStr, strategy, endpointKey, responses = {}) {
232
+ function selectStatusCode(configuredCodesStr, strategy, endpointKey, responses = {}, cycle) {
179
233
  const codes = (configuredCodesStr || "")
180
234
  .split(",")
181
235
  .map((s) => s.trim())
@@ -194,10 +248,10 @@ function selectStatusCode(configuredCodesStr, strategy, endpointKey, responses =
194
248
  return codes[Math.floor(Math.random() * codes.length)];
195
249
  }
196
250
 
197
- if (strategy === "cycle") {
198
- const currentIndex = routeCycleIndices.get(endpointKey) || 0;
251
+ if (strategy === "cycle" && cycle) {
252
+ const currentIndex = cycle.get(endpointKey) || 0;
199
253
  const selected = codes[currentIndex % codes.length];
200
- routeCycleIndices.set(endpointKey, (currentIndex + 1) % codes.length);
254
+ cycle.set(endpointKey, (currentIndex + 1) % codes.length);
201
255
  return selected;
202
256
  }
203
257
 
@@ -309,23 +363,32 @@ function buildMockResponsePayload(route, statusCode) {
309
363
  };
310
364
  }
311
365
 
366
+ /** The status and payload of a matched request, by the mock that owns the route. */
367
+ function answer(match) {
368
+ const { config, cycle } = match.mock;
369
+ const statusCode = selectStatusCode(
370
+ codesFor(config.statusCode, match.route.routeKey),
371
+ config.statusStrategy,
372
+ match.route.routeKey,
373
+ match.route.responses,
374
+ cycle,
375
+ );
376
+ return { config, statusCode, payload: buildMockResponsePayload(match.route, statusCode) };
377
+ }
378
+
312
379
  /**
313
- * Enables the in-browser mock server by patching window.fetch and XMLHttpRequest.
380
+ * Enables the in-browser mock for `options.owner` (the element; omitted: a page-wide default),
381
+ * replacing that owner's routes and config. Other owners' mocks stay. window.fetch and
382
+ * XMLHttpRequest are patched once, while at least one mock is enabled.
314
383
  */
315
384
  export function enableMockServer(spec, options = {}) {
316
- mockConfig = {
317
- statusCode: options.statusCode || "",
318
- statusStrategy: options.statusStrategy || "first",
319
- delay: Number(options.delay) || 0,
320
- log: options.log !== "false",
321
- };
322
-
323
- compiledRoutes = compileRoutes(spec);
324
- routeCycleIndices.clear();
325
-
326
- if (isMockActive) {
327
- return;
328
- }
385
+ const owner = options.owner || DEFAULT_OWNER;
386
+ const current = mocks.get(owner);
387
+ mocks.set(owner, {
388
+ routes: compileRoutes(spec),
389
+ config: normalizeConfig(options, current?.config),
390
+ cycle: new Map(),
391
+ });
329
392
 
330
393
  if (typeof window === "undefined") {
331
394
  return;
@@ -336,7 +399,7 @@ export function enableMockServer(spec, options = {}) {
336
399
  originalFetch = window.fetch;
337
400
 
338
401
  window.fetch = async function mockFetch(input, init = {}) {
339
- if (!isMockActive) {
402
+ if (!mocks.size) {
340
403
  return originalFetch.apply(this, arguments);
341
404
  }
342
405
 
@@ -359,19 +422,13 @@ export function enableMockServer(spec, options = {}) {
359
422
  return originalFetch.apply(this, arguments);
360
423
  }
361
424
 
362
- const statusCode = selectStatusCode(
363
- mockConfig.statusCode,
364
- mockConfig.statusStrategy,
365
- match.route.routeKey,
366
- match.route.responses,
367
- );
368
- const payload = buildMockResponsePayload(match.route, statusCode);
425
+ const { config, payload } = answer(match);
369
426
 
370
- if (mockConfig.delay > 0) {
371
- await new Promise((resolve) => setTimeout(resolve, mockConfig.delay));
427
+ if (config.delay > 0) {
428
+ await new Promise((resolve) => setTimeout(resolve, config.delay));
372
429
  }
373
430
 
374
- if (mockConfig.log) {
431
+ if (config.log) {
375
432
  console.info(
376
433
  `%c[RapiDoc Mock]%c ${method.toUpperCase()} ${match.url.pathname} ➔ ${payload.statusCode} ${payload.statusText}`,
377
434
  "color:#10b981; font-weight:bold;",
@@ -408,7 +465,7 @@ export function enableMockServer(spec, options = {}) {
408
465
  };
409
466
 
410
467
  window.XMLHttpRequest.prototype.send = function mockXhrSend() {
411
- if (!isMockActive) {
468
+ if (!mocks.size) {
412
469
  return originalXhrSend.apply(this, arguments);
413
470
  }
414
471
 
@@ -417,18 +474,12 @@ export function enableMockServer(spec, options = {}) {
417
474
  return originalXhrSend.apply(this, arguments);
418
475
  }
419
476
 
420
- const statusCode = selectStatusCode(
421
- mockConfig.statusCode,
422
- mockConfig.statusStrategy,
423
- match.route.routeKey,
424
- match.route.responses,
425
- );
426
- const payload = buildMockResponsePayload(match.route, statusCode);
477
+ const { config, payload } = answer(match);
427
478
 
428
- const delayMs = mockConfig.delay > 0 ? mockConfig.delay : 10;
479
+ const delayMs = config.delay > 0 ? config.delay : 10;
429
480
 
430
481
  setTimeout(() => {
431
- if (mockConfig.log) {
482
+ if (config.log) {
432
483
  console.info(
433
484
  `%c[RapiDoc Mock (XHR)]%c ${this._mockMethod.toUpperCase()} ${match.url.pathname} ➔ ${payload.statusCode} ${payload.statusText}`,
434
485
  "color:#10b981; font-weight:bold;",
@@ -465,17 +516,15 @@ export function enableMockServer(spec, options = {}) {
465
516
  }, delayMs);
466
517
  };
467
518
  }
468
-
469
- isMockActive = true;
470
519
  }
471
520
 
472
521
  /**
473
- * Disables the mock server and restores original network primitives.
522
+ * Disables the mock of `owner` (omitted: the page-wide default). The original fetch and
523
+ * XMLHttpRequest come back once no mock is enabled.
474
524
  */
475
- export function disableMockServer() {
476
- isMockActive = false;
477
- compiledRoutes = [];
478
- routeCycleIndices.clear();
525
+ export function disableMockServer(owner = DEFAULT_OWNER) {
526
+ mocks.delete(owner);
527
+ if (mocks.size) return;
479
528
 
480
529
  if (typeof window !== "undefined") {
481
530
  if (originalFetch) {
@@ -494,15 +543,15 @@ export function disableMockServer() {
494
543
  }
495
544
 
496
545
  /**
497
- * Updates mock options without re-registering or recompiling routes.
546
+ * Updates the options of `options.owner`'s mock (omitted: the page-wide default) without
547
+ * recompiling its routes. Does nothing for an owner without a mock.
498
548
  */
499
549
  export function updateMockConfig(options = {}) {
500
- if (options.statusCode !== undefined) mockConfig.statusCode = options.statusCode;
501
- if (options.statusStrategy !== undefined) mockConfig.statusStrategy = options.statusStrategy;
502
- if (options.delay !== undefined) mockConfig.delay = Number(options.delay) || 0;
503
- if (options.log !== undefined) mockConfig.log = options.log !== "false";
550
+ const mock = mocks.get(options.owner || DEFAULT_OWNER);
551
+ if (mock) mock.config = normalizeConfig(options, mock.config);
504
552
  }
505
553
 
506
- export function isMockServerActive() {
507
- return isMockActive;
554
+ /** Whether `owner`'s mock is enabled; without an owner, whether any mock is. */
555
+ export function isMockServerActive(owner) {
556
+ return owner ? mocks.has(owner) : mocks.size > 0;
508
557
  }