rapidoc 11.0.1 → 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 (96) hide show
  1. package/README.md +171 -54
  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 +645 -202
  5. package/dist/rapidoc-min.js.LICENSE.txt +381 -0
  6. package/package.json +57 -42
  7. package/src/README.md +150 -0
  8. package/src/components/README.md +91 -0
  9. package/src/components/api-request.ts +912 -0
  10. package/src/components/api-response.ts +514 -0
  11. package/src/components/json-tree.ts +171 -0
  12. package/src/components/schema-base-element.ts +329 -0
  13. package/src/components/schema-table.ts +475 -0
  14. package/src/components/schema-tree.ts +393 -0
  15. package/src/components/tag-input.ts +180 -0
  16. package/src/flow.js +3 -0
  17. package/src/index.js +6 -0
  18. package/src/oauth-receiver.ts +29 -0
  19. package/src/rapi-flow.ts +1178 -0
  20. package/src/rapidoc-mini.ts +603 -0
  21. package/src/rapidoc.ts +1854 -0
  22. package/src/styles/README.md +96 -0
  23. package/src/styles/auth-styles.js +318 -0
  24. package/src/styles/border-styles.js +24 -0
  25. package/src/styles/css-bundle.js +129 -0
  26. package/src/styles/custom-styles.js +8 -0
  27. package/src/styles/dialog-box-styles.js +102 -0
  28. package/src/styles/endpoint-styles.js +564 -0
  29. package/src/styles/flex-styles.js +16 -0
  30. package/src/styles/flow-styles.js +248 -0
  31. package/src/styles/font-styles.js +432 -0
  32. package/src/styles/info-styles.js +36 -0
  33. package/src/styles/input-styles.js +555 -0
  34. package/src/styles/layout-styles.js +396 -0
  35. package/src/styles/microlighter-styles.js +99 -0
  36. package/src/styles/nav-styles.js +633 -0
  37. package/src/styles/param-list-styles.js +261 -0
  38. package/src/styles/schema-styles.js +296 -0
  39. package/src/styles/tab-styles.js +106 -0
  40. package/src/styles/theme-tokens.js +582 -0
  41. package/src/templates/README.md +89 -0
  42. package/src/templates/advanced-search-dialog-template.js +94 -0
  43. package/src/templates/api-response-template.js +223 -0
  44. package/src/templates/callback-template.js +86 -0
  45. package/src/templates/code-samples-template.js +52 -0
  46. package/src/templates/components-template.js +86 -0
  47. package/src/templates/downloads-template.js +70 -0
  48. package/src/templates/endpoint-template.js +377 -0
  49. package/src/templates/expanded-endpoint-template.js +270 -0
  50. package/src/templates/focused-endpoint-template.js +146 -0
  51. package/src/templates/header-template.js +92 -0
  52. package/src/templates/logo-template.js +21 -0
  53. package/src/templates/main-body-template.js +102 -0
  54. package/src/templates/navbar-template.js +498 -0
  55. package/src/templates/overview-template.js +104 -0
  56. package/src/templates/request-body-template.js +673 -0
  57. package/src/templates/request-params-template.js +485 -0
  58. package/src/templates/security-scheme-template.js +1272 -0
  59. package/src/templates/server-template.js +153 -0
  60. package/src/utils/README.md +146 -0
  61. package/src/utils/arazzo/README.md +119 -0
  62. package/src/utils/arazzo/components.js +15 -0
  63. package/src/utils/arazzo/graph.js +269 -0
  64. package/src/utils/arazzo/index.js +52 -0
  65. package/src/utils/arazzo/operations.js +106 -0
  66. package/src/utils/arazzo/parse.js +196 -0
  67. package/src/utils/arazzo/refs.js +40 -0
  68. package/src/utils/arazzo/svg.js +707 -0
  69. package/src/utils/auto-color-scheme.js +99 -0
  70. package/src/utils/bind-navbar.js +175 -0
  71. package/src/utils/color-utils.js +59 -0
  72. package/src/utils/common-utils.js +191 -0
  73. package/src/utils/curl-utils.js +126 -0
  74. package/src/utils/highlighter.js +419 -0
  75. package/src/utils/markdown-utils.js +110 -0
  76. package/src/utils/mcp/README.md +155 -0
  77. package/src/utils/mcp/common.js +166 -0
  78. package/src/utils/mcp/manifest.js +382 -0
  79. package/src/utils/mcp/project/server.ts +53 -0
  80. package/src/utils/mcp/project.js +88 -0
  81. package/src/utils/mcp/runtime.js +289 -0
  82. package/src/utils/mcp/schema.js +98 -0
  83. package/src/utils/mcp/workflow-runtime.js +374 -0
  84. package/src/utils/mcp/workflows.js +240 -0
  85. package/src/utils/mcp/zip.js +102 -0
  86. package/src/utils/mcp-generator.js +47 -0
  87. package/src/utils/mock-interceptor.js +557 -0
  88. package/src/utils/oauth-utils.js +87 -0
  89. package/src/utils/request-utils.js +264 -0
  90. package/src/utils/response-utils.js +203 -0
  91. package/src/utils/sanitize.js +57 -0
  92. package/src/utils/schema-utils.js +1954 -0
  93. package/src/utils/slugger.js +187 -0
  94. package/src/utils/spec-parser.js +941 -0
  95. package/src/utils/url-utils.js +246 -0
  96. package/src/utils/xml-utils.js +61 -0
@@ -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
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * MCP (Model Context Protocol) server export for RapiDoc.
3
+ *
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.
8
+ */
9
+
10
+ export {
11
+ AUTH_TRANSPORT_HEADERS,
12
+ parseMcpEnabled,
13
+ toSnakeCase,
14
+ sanitizeDescription,
15
+ deriveToolName,
16
+ estimateTokenCost,
17
+ envNameFor,
18
+ generateHostConfigs,
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";
25
+
26
+ import { estimateTokenCost, generateHostConfigs } from "./mcp/common.js";
27
+ import { buildMcpManifest } from "./mcp/manifest.js";
28
+ import { writeMcpProject } from "./mcp/project.js";
29
+
30
+ /**
31
+ * Builds the manifest and the files of the exported project.
32
+ *
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
38
+ */
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 };
47
+ }
@@ -0,0 +1,557 @@
1
+ import { generateExample } from "./schema-utils.js";
2
+
3
+ const STATUS_TEXTS = {
4
+ 200: "OK",
5
+ 201: "Created",
6
+ 202: "Accepted",
7
+ 204: "No Content",
8
+ 301: "Moved Permanently",
9
+ 302: "Found",
10
+ 304: "Not Modified",
11
+ 400: "Bad Request",
12
+ 401: "Unauthorized",
13
+ 403: "Forbidden",
14
+ 404: "Not Found",
15
+ 405: "Method Not Allowed",
16
+ 409: "Conflict",
17
+ 422: "Unprocessable Entity",
18
+ 429: "Too Many Requests",
19
+ 500: "Internal Server Error",
20
+ 501: "Not Implemented",
21
+ 502: "Bad Gateway",
22
+ 503: "Service Unavailable",
23
+ 504: "Gateway Timeout",
24
+ };
25
+
26
+ let originalFetch = null;
27
+ let originalXhrOpen = null;
28
+ let originalXhrSend = null;
29
+ let originalXhrSetRequestHeader = null;
30
+
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");
38
+
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
+ }
86
+
87
+ /**
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.
91
+ */
92
+ function extractServerBases(servers = []) {
93
+ const bases = new Map();
94
+ bases.set("|", { origin: null, base: "" });
95
+
96
+ servers.forEach((s) => {
97
+ const rawUrl = s.computedUrl || s.url || "";
98
+ if (!rawUrl) return;
99
+
100
+ try {
101
+ if (rawUrl.startsWith("http://") || rawUrl.startsWith("https://")) {
102
+ const u = new URL(rawUrl);
103
+ const p = u.pathname.replace(/\/+$/, "");
104
+ bases.set(`${u.origin}|${p}`, { origin: u.origin, base: p });
105
+ } else {
106
+ const p = rawUrl.replace(/\/+$/, "");
107
+ if (p) bases.set(`|${p}`, { origin: null, base: p.startsWith("/") ? p : `/${p}` });
108
+ }
109
+ } catch {
110
+ // Ignore invalid URL formats
111
+ }
112
+ });
113
+
114
+ return Array.from(bases.values());
115
+ }
116
+
117
+ /**
118
+ * Converts an OpenAPI path template (e.g. /pets/{id}) into a matching RegExp.
119
+ */
120
+ function compileRoutePattern(pathTemplate, basePath = "") {
121
+ const cleanBase = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
122
+ const fullPath = cleanBase + (pathTemplate.startsWith("/") ? pathTemplate : `/${pathTemplate}`);
123
+
124
+ // Replace {param} with a placeholder token to avoid escaping issues
125
+ const tokenized = fullPath.replace(/\{([a-zA-Z0-9_-]+)\}/g, "__PARAM_$1__");
126
+
127
+ // Escape special regex characters
128
+ const escaped = tokenized.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
129
+
130
+ // Replace placeholder token with regex capture group
131
+ const pattern = escaped.replace(/__PARAM_([a-zA-Z0-9_-]+)__/g, "(?<$1>[^/?#]+)");
132
+
133
+ return new RegExp(`^${pattern}(?:\\/)?$`);
134
+ }
135
+
136
+ /**
137
+ * Builds the route table from the resolved OpenAPI spec.
138
+ */
139
+ export function compileRoutes(spec) {
140
+ const routes = [];
141
+ const rootBases = extractServerBases(spec?.servers);
142
+
143
+ if (!spec?.tags || !Array.isArray(spec.tags)) {
144
+ return routes;
145
+ }
146
+
147
+ const seenRoutes = new Set();
148
+
149
+ spec.tags.forEach((tag) => {
150
+ (tag.paths || []).forEach((pathItem) => {
151
+ const method = (pathItem.method || "GET").toUpperCase();
152
+ const pathTemplate = pathItem.path || "";
153
+ const routeKey = `${method} ${pathTemplate}`;
154
+
155
+ if (seenRoutes.has(routeKey)) {
156
+ return;
157
+ }
158
+ seenRoutes.add(routeKey);
159
+
160
+ const endpointBases =
161
+ pathItem.servers?.length > 0 ? extractServerBases(pathItem.servers) : rootBases;
162
+
163
+ const patterns = endpointBases.map(({ origin, base }) => ({
164
+ regex: compileRoutePattern(pathTemplate, base),
165
+ origin,
166
+ base,
167
+ }));
168
+
169
+ routes.push({
170
+ method,
171
+ pathTemplate,
172
+ routeKey,
173
+ patterns,
174
+ responses: pathItem.responses || {},
175
+ parameters: pathItem.parameters || [],
176
+ });
177
+ });
178
+ });
179
+
180
+ return routes;
181
+ }
182
+
183
+ /**
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.
187
+ */
188
+ function matchRoute(method, urlStr) {
189
+ if (!mocks.size || !urlStr) {
190
+ return null;
191
+ }
192
+
193
+ let reqUrl;
194
+ try {
195
+ const origin =
196
+ typeof window !== "undefined" && window.location?.href
197
+ ? window.location.href
198
+ : "http://localhost";
199
+ reqUrl = new URL(urlStr, origin);
200
+ } catch {
201
+ return null;
202
+ }
203
+
204
+ const upperMethod = (method || "GET").toUpperCase();
205
+ const pathname = decodeURI(reqUrl.pathname);
206
+
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
+ }
213
+
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
+ }
222
+ }
223
+ }
224
+ }
225
+
226
+ return best;
227
+ }
228
+
229
+ /**
230
+ * Chooses the status code based on config and strategy (first, cycle, random).
231
+ */
232
+ function selectStatusCode(configuredCodesStr, strategy, endpointKey, responses = {}, cycle) {
233
+ const codes = (configuredCodesStr || "")
234
+ .split(",")
235
+ .map((s) => s.trim())
236
+ .filter(Boolean);
237
+
238
+ if (codes.length === 0) {
239
+ // Look for 2xx response defined in spec
240
+ const twoXx = Object.keys(responses).find((k) => k.startsWith("2"));
241
+ if (twoXx) return twoXx;
242
+ if ("default" in responses) return "default";
243
+ const firstCode = Object.keys(responses)[0];
244
+ return firstCode || "200";
245
+ }
246
+
247
+ if (strategy === "random") {
248
+ return codes[Math.floor(Math.random() * codes.length)];
249
+ }
250
+
251
+ if (strategy === "cycle" && cycle) {
252
+ const currentIndex = cycle.get(endpointKey) || 0;
253
+ const selected = codes[currentIndex % codes.length];
254
+ cycle.set(endpointKey, (currentIndex + 1) % codes.length);
255
+ return selected;
256
+ }
257
+
258
+ // 'first'
259
+ return codes[0];
260
+ }
261
+
262
+ /**
263
+ * Generates the mock response body and headers for a matched route.
264
+ */
265
+ function buildMockResponsePayload(route, statusCode) {
266
+ const responseDef =
267
+ route.responses?.[statusCode] ||
268
+ route.responses?.[String(statusCode)] ||
269
+ route.responses?.["default"];
270
+
271
+ const headers = {
272
+ "X-Mock-Server": "RapiDoc",
273
+ "Access-Control-Allow-Origin": "*",
274
+ };
275
+
276
+ let body = "";
277
+ let contentType = "application/json";
278
+
279
+ if (responseDef?.content) {
280
+ const availableTypes = Object.keys(responseDef.content);
281
+ // Prefer json, then xml, then text, then first available
282
+ contentType =
283
+ availableTypes.find((t) => t.includes("json")) ||
284
+ availableTypes.find((t) => t.includes("xml")) ||
285
+ availableTypes.find((t) => t.includes("text")) ||
286
+ availableTypes[0] ||
287
+ "application/json";
288
+
289
+ const mediaObj = responseDef.content[contentType];
290
+ if (mediaObj) {
291
+ if (mediaObj.example !== undefined) {
292
+ body =
293
+ typeof mediaObj.example === "object"
294
+ ? JSON.stringify(mediaObj.example, null, 2)
295
+ : String(mediaObj.example);
296
+ } else if (mediaObj.examples && Object.keys(mediaObj.examples).length > 0) {
297
+ const firstEg = Object.values(mediaObj.examples)[0]?.value;
298
+ if (firstEg !== undefined) {
299
+ body = typeof firstEg === "object" ? JSON.stringify(firstEg, null, 2) : String(firstEg);
300
+ }
301
+ }
302
+
303
+ if (!body && mediaObj.schema) {
304
+ const generated = generateExample(
305
+ mediaObj.schema,
306
+ contentType,
307
+ null,
308
+ null,
309
+ true,
310
+ false,
311
+ "text",
312
+ true,
313
+ );
314
+
315
+ if (generated && generated.length > 0) {
316
+ const val = generated[0].exampleValue;
317
+ body = typeof val === "object" ? JSON.stringify(val, null, 2) : String(val);
318
+ }
319
+ }
320
+ }
321
+ }
322
+
323
+ // Handle empty or default body when spec has no content defined
324
+ if (!body) {
325
+ if (String(statusCode) === "204") {
326
+ body = "";
327
+ } else if (contentType.includes("json")) {
328
+ const statusNum = Number(statusCode) || 200;
329
+ body = JSON.stringify(
330
+ {
331
+ status: statusNum,
332
+ message: STATUS_TEXTS[statusNum] || "Mock Response",
333
+ },
334
+ null,
335
+ 2,
336
+ );
337
+ } else {
338
+ body = STATUS_TEXTS[Number(statusCode)] || "OK";
339
+ }
340
+ }
341
+
342
+ // Copy defined headers from spec if any
343
+ if (responseDef?.headers) {
344
+ Object.entries(responseDef.headers).forEach(([hdrKey, hdrDef]) => {
345
+ if (hdrDef.schema?.default !== undefined) {
346
+ headers[hdrKey] = String(hdrDef.schema.default);
347
+ } else if (hdrDef.example !== undefined) {
348
+ headers[hdrKey] = String(hdrDef.example);
349
+ }
350
+ });
351
+ }
352
+
353
+ if (String(statusCode) !== "204") {
354
+ headers["Content-Type"] = contentType;
355
+ }
356
+
357
+ return {
358
+ statusCode: Number(statusCode) || 200,
359
+ statusText:
360
+ STATUS_TEXTS[Number(statusCode)] || (statusCode === "default" ? "OK" : "Mock Response"),
361
+ headers,
362
+ body,
363
+ };
364
+ }
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
+
379
+ /**
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.
383
+ */
384
+ export function enableMockServer(spec, options = {}) {
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
+ });
392
+
393
+ if (typeof window === "undefined") {
394
+ return;
395
+ }
396
+
397
+ // --- Patch window.fetch ---
398
+ if (!originalFetch && window.fetch) {
399
+ originalFetch = window.fetch;
400
+
401
+ window.fetch = async function mockFetch(input, init = {}) {
402
+ if (!mocks.size) {
403
+ return originalFetch.apply(this, arguments);
404
+ }
405
+
406
+ let url = "";
407
+ let method = "GET";
408
+
409
+ if (typeof input === "string") {
410
+ url = input;
411
+ method = init?.method || "GET";
412
+ } else if (input instanceof URL) {
413
+ url = input.href;
414
+ method = init?.method || "GET";
415
+ } else if (input && typeof input === "object" && "url" in input) {
416
+ url = input.url;
417
+ method = init?.method || input.method || "GET";
418
+ }
419
+
420
+ const match = matchRoute(method, url);
421
+ if (!match) {
422
+ return originalFetch.apply(this, arguments);
423
+ }
424
+
425
+ const { config, payload } = answer(match);
426
+
427
+ if (config.delay > 0) {
428
+ await new Promise((resolve) => setTimeout(resolve, config.delay));
429
+ }
430
+
431
+ if (config.log) {
432
+ console.info(
433
+ `%c[RapiDoc Mock]%c ${method.toUpperCase()} ${match.url.pathname} ➔ ${payload.statusCode} ${payload.statusText}`,
434
+ "color:#10b981; font-weight:bold;",
435
+ "color:inherit;",
436
+ );
437
+ }
438
+
439
+ return new Response(payload.body, {
440
+ status: payload.statusCode,
441
+ statusText: payload.statusText,
442
+ headers: payload.headers,
443
+ });
444
+ };
445
+ }
446
+
447
+ // --- Patch XMLHttpRequest ---
448
+ if (!originalXhrOpen && window.XMLHttpRequest) {
449
+ originalXhrOpen = window.XMLHttpRequest.prototype.open;
450
+ originalXhrSend = window.XMLHttpRequest.prototype.send;
451
+ originalXhrSetRequestHeader = window.XMLHttpRequest.prototype.setRequestHeader;
452
+
453
+ window.XMLHttpRequest.prototype.open = function mockXhrOpen(method, url, ...rest) {
454
+ this._mockMethod = method;
455
+ this._mockUrl = url;
456
+ this._mockHeaders = {};
457
+ return originalXhrOpen.apply(this, [method, url, ...rest]);
458
+ };
459
+
460
+ window.XMLHttpRequest.prototype.setRequestHeader = function mockXhrSetHeader(name, value) {
461
+ if (this._mockHeaders) {
462
+ this._mockHeaders[name] = value;
463
+ }
464
+ return originalXhrSetRequestHeader.apply(this, arguments);
465
+ };
466
+
467
+ window.XMLHttpRequest.prototype.send = function mockXhrSend() {
468
+ if (!mocks.size) {
469
+ return originalXhrSend.apply(this, arguments);
470
+ }
471
+
472
+ const match = matchRoute(this._mockMethod, this._mockUrl);
473
+ if (!match) {
474
+ return originalXhrSend.apply(this, arguments);
475
+ }
476
+
477
+ const { config, payload } = answer(match);
478
+
479
+ const delayMs = config.delay > 0 ? config.delay : 10;
480
+
481
+ setTimeout(() => {
482
+ if (config.log) {
483
+ console.info(
484
+ `%c[RapiDoc Mock (XHR)]%c ${this._mockMethod.toUpperCase()} ${match.url.pathname} ➔ ${payload.statusCode} ${payload.statusText}`,
485
+ "color:#10b981; font-weight:bold;",
486
+ "color:inherit;",
487
+ );
488
+ }
489
+
490
+ Object.defineProperty(this, "readyState", { value: 4, writable: true });
491
+ Object.defineProperty(this, "status", { value: payload.statusCode, writable: true });
492
+ Object.defineProperty(this, "statusText", { value: payload.statusText, writable: true });
493
+ Object.defineProperty(this, "responseText", { value: payload.body, writable: true });
494
+ Object.defineProperty(this, "response", { value: payload.body, writable: true });
495
+
496
+ const headerStr = Object.entries(payload.headers)
497
+ .map(([k, v]) => `${k}: ${v}\r\n`)
498
+ .join("");
499
+
500
+ this.getAllResponseHeaders = () => headerStr;
501
+ this.getResponseHeader = (name) => {
502
+ const lower = name.toLowerCase();
503
+ const found = Object.keys(payload.headers).find((k) => k.toLowerCase() === lower);
504
+ return found ? payload.headers[found] : null;
505
+ };
506
+
507
+ this.dispatchEvent(new Event("readystatechange"));
508
+ this.dispatchEvent(new Event("load"));
509
+ this.dispatchEvent(new Event("loadend"));
510
+ if (typeof this.onreadystatechange === "function") {
511
+ this.onreadystatechange();
512
+ }
513
+ if (typeof this.onload === "function") {
514
+ this.onload();
515
+ }
516
+ }, delayMs);
517
+ };
518
+ }
519
+ }
520
+
521
+ /**
522
+ * Disables the mock of `owner` (omitted: the page-wide default). The original fetch and
523
+ * XMLHttpRequest come back once no mock is enabled.
524
+ */
525
+ export function disableMockServer(owner = DEFAULT_OWNER) {
526
+ mocks.delete(owner);
527
+ if (mocks.size) return;
528
+
529
+ if (typeof window !== "undefined") {
530
+ if (originalFetch) {
531
+ window.fetch = originalFetch;
532
+ originalFetch = null;
533
+ }
534
+ if (originalXhrOpen) {
535
+ window.XMLHttpRequest.prototype.open = originalXhrOpen;
536
+ window.XMLHttpRequest.prototype.send = originalXhrSend;
537
+ window.XMLHttpRequest.prototype.setRequestHeader = originalXhrSetRequestHeader;
538
+ originalXhrOpen = null;
539
+ originalXhrSend = null;
540
+ originalXhrSetRequestHeader = null;
541
+ }
542
+ }
543
+ }
544
+
545
+ /**
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.
548
+ */
549
+ export function updateMockConfig(options = {}) {
550
+ const mock = mocks.get(options.owner || DEFAULT_OWNER);
551
+ if (mock) mock.config = normalizeConfig(options, mock.config);
552
+ }
553
+
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;
557
+ }