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.
- package/README.md +171 -54
- package/dist/rapidoc-flow-min.js +580 -0
- package/dist/rapidoc-flow-min.js.LICENSE.txt +171 -0
- package/dist/rapidoc-min.js +645 -202
- package/dist/rapidoc-min.js.LICENSE.txt +381 -0
- package/package.json +57 -42
- package/src/README.md +150 -0
- package/src/components/README.md +91 -0
- package/src/components/api-request.ts +912 -0
- package/src/components/api-response.ts +514 -0
- package/src/components/json-tree.ts +171 -0
- package/src/components/schema-base-element.ts +329 -0
- package/src/components/schema-table.ts +475 -0
- package/src/components/schema-tree.ts +393 -0
- package/src/components/tag-input.ts +180 -0
- package/src/flow.js +3 -0
- package/src/index.js +6 -0
- package/src/oauth-receiver.ts +29 -0
- package/src/rapi-flow.ts +1178 -0
- package/src/rapidoc-mini.ts +603 -0
- package/src/rapidoc.ts +1854 -0
- package/src/styles/README.md +96 -0
- package/src/styles/auth-styles.js +318 -0
- package/src/styles/border-styles.js +24 -0
- package/src/styles/css-bundle.js +129 -0
- package/src/styles/custom-styles.js +8 -0
- package/src/styles/dialog-box-styles.js +102 -0
- package/src/styles/endpoint-styles.js +564 -0
- package/src/styles/flex-styles.js +16 -0
- package/src/styles/flow-styles.js +248 -0
- package/src/styles/font-styles.js +432 -0
- package/src/styles/info-styles.js +36 -0
- package/src/styles/input-styles.js +555 -0
- package/src/styles/layout-styles.js +396 -0
- package/src/styles/microlighter-styles.js +99 -0
- package/src/styles/nav-styles.js +633 -0
- package/src/styles/param-list-styles.js +261 -0
- package/src/styles/schema-styles.js +296 -0
- package/src/styles/tab-styles.js +106 -0
- package/src/styles/theme-tokens.js +582 -0
- package/src/templates/README.md +89 -0
- package/src/templates/advanced-search-dialog-template.js +94 -0
- package/src/templates/api-response-template.js +223 -0
- package/src/templates/callback-template.js +86 -0
- package/src/templates/code-samples-template.js +52 -0
- package/src/templates/components-template.js +86 -0
- package/src/templates/downloads-template.js +70 -0
- package/src/templates/endpoint-template.js +377 -0
- package/src/templates/expanded-endpoint-template.js +270 -0
- package/src/templates/focused-endpoint-template.js +146 -0
- package/src/templates/header-template.js +92 -0
- package/src/templates/logo-template.js +21 -0
- package/src/templates/main-body-template.js +102 -0
- package/src/templates/navbar-template.js +498 -0
- package/src/templates/overview-template.js +104 -0
- package/src/templates/request-body-template.js +673 -0
- package/src/templates/request-params-template.js +485 -0
- package/src/templates/security-scheme-template.js +1272 -0
- package/src/templates/server-template.js +153 -0
- package/src/utils/README.md +146 -0
- package/src/utils/arazzo/README.md +119 -0
- package/src/utils/arazzo/components.js +15 -0
- package/src/utils/arazzo/graph.js +269 -0
- package/src/utils/arazzo/index.js +52 -0
- package/src/utils/arazzo/operations.js +106 -0
- package/src/utils/arazzo/parse.js +196 -0
- package/src/utils/arazzo/refs.js +40 -0
- package/src/utils/arazzo/svg.js +707 -0
- package/src/utils/auto-color-scheme.js +99 -0
- package/src/utils/bind-navbar.js +175 -0
- package/src/utils/color-utils.js +59 -0
- package/src/utils/common-utils.js +191 -0
- package/src/utils/curl-utils.js +126 -0
- package/src/utils/highlighter.js +419 -0
- package/src/utils/markdown-utils.js +110 -0
- package/src/utils/mcp/README.md +155 -0
- package/src/utils/mcp/common.js +166 -0
- package/src/utils/mcp/manifest.js +382 -0
- package/src/utils/mcp/project/server.ts +53 -0
- package/src/utils/mcp/project.js +88 -0
- package/src/utils/mcp/runtime.js +289 -0
- package/src/utils/mcp/schema.js +98 -0
- package/src/utils/mcp/workflow-runtime.js +374 -0
- package/src/utils/mcp/workflows.js +240 -0
- package/src/utils/mcp/zip.js +102 -0
- package/src/utils/mcp-generator.js +47 -0
- package/src/utils/mock-interceptor.js +557 -0
- package/src/utils/oauth-utils.js +87 -0
- package/src/utils/request-utils.js +264 -0
- package/src/utils/response-utils.js +203 -0
- package/src/utils/sanitize.js +57 -0
- package/src/utils/schema-utils.js +1954 -0
- package/src/utils/slugger.js +187 -0
- package/src/utils/spec-parser.js +941 -0
- package/src/utils/url-utils.js +246 -0
- 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
|
+
}
|