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.
- package/README.md +127 -25
- package/dist/rapidoc-flow-min.js +580 -0
- package/dist/rapidoc-flow-min.js.LICENSE.txt +171 -0
- package/dist/rapidoc-min.js +541 -1309
- package/dist/rapidoc-min.js.LICENSE.txt +381 -0
- package/package.json +21 -18
- package/src/README.md +150 -0
- package/src/components/README.md +91 -0
- package/src/components/api-request.ts +43 -0
- package/src/components/api-response.ts +9 -4
- package/src/components/schema-base-element.ts +18 -4
- package/src/components/schema-table.ts +58 -13
- package/src/components/schema-tree.ts +7 -0
- package/src/components/tag-input.ts +5 -9
- package/src/flow.js +3 -0
- package/src/rapi-flow.ts +1178 -0
- package/src/rapidoc-mini.ts +27 -6
- package/src/rapidoc.ts +108 -62
- package/src/styles/README.md +96 -0
- package/src/styles/css-bundle.js +2 -2
- package/src/styles/endpoint-styles.js +6 -1
- package/src/styles/flow-styles.js +248 -0
- package/src/styles/nav-styles.js +39 -15
- package/src/styles/schema-styles.js +13 -5
- package/src/styles/theme-tokens.js +2 -2
- package/src/templates/README.md +89 -0
- package/src/templates/api-response-template.js +14 -10
- package/src/templates/callback-template.js +1 -1
- package/src/templates/downloads-template.js +12 -27
- package/src/templates/endpoint-template.js +15 -24
- package/src/templates/expanded-endpoint-template.js +8 -4
- package/src/templates/focused-endpoint-template.js +4 -3
- package/src/templates/navbar-template.js +20 -11
- package/src/templates/request-body-template.js +29 -7
- package/src/templates/request-params-template.js +15 -4
- package/src/templates/security-scheme-template.js +11 -2
- package/src/templates/server-template.js +3 -2
- 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/curl-utils.js +5 -2
- package/src/utils/highlighter.js +3 -0
- package/src/utils/mcp/README.md +155 -0
- package/src/utils/mcp/common.js +45 -346
- 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 +29 -125
- package/src/utils/mock-interceptor.js +134 -85
- package/src/utils/request-utils.js +61 -0
- package/src/utils/schema-utils.js +33 -6
- package/src/utils/slugger.js +5 -7
- package/src/utils/spec-parser.js +190 -164
- package/src/utils/url-utils.js +36 -1
- package/dist/rapidoc-static-min.js +0 -13542
- package/src/static/index.d.ts +0 -149
- package/src/static/index.js +0 -62
- package/src/static/navigation.js +0 -497
- package/src/static/render.js +0 -490
- package/src/static/serializer.js +0 -216
- package/src/utils/mcp/generators/csharp.js +0 -242
- package/src/utils/mcp/generators/go.js +0 -298
- package/src/utils/mcp/generators/java.js +0 -330
- package/src/utils/mcp/generators/python.js +0 -248
- package/src/utils/mcp/generators/rust.js +0 -281
- 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)
|
|
2
|
+
* MCP (Model Context Protocol) server export for RapiDoc.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
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 {
|
|
34
|
-
import {
|
|
35
|
-
|
|
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
|
-
*
|
|
55
|
-
* language-specific server implementation and packaging manifests.
|
|
31
|
+
* Builds the manifest and the files of the exported project.
|
|
56
32
|
*
|
|
57
|
-
* @param {object}
|
|
58
|
-
* @param {object} options
|
|
59
|
-
*
|
|
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(
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const budget = estimateTokenCost(
|
|
66
|
-
const
|
|
67
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
47
|
-
|
|
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
|
-
|
|
104
|
+
bases.set(`${u.origin}|${p}`, { origin: u.origin, base: p });
|
|
59
105
|
} else {
|
|
60
106
|
const p = rawUrl.replace(/\/+$/, "");
|
|
61
|
-
if (p) bases.
|
|
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) =>
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
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 =
|
|
251
|
+
if (strategy === "cycle" && cycle) {
|
|
252
|
+
const currentIndex = cycle.get(endpointKey) || 0;
|
|
199
253
|
const selected = codes[currentIndex % codes.length];
|
|
200
|
-
|
|
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
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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 (!
|
|
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
|
|
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 (
|
|
371
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
427
|
+
if (config.delay > 0) {
|
|
428
|
+
await new Promise((resolve) => setTimeout(resolve, config.delay));
|
|
372
429
|
}
|
|
373
430
|
|
|
374
|
-
if (
|
|
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 (!
|
|
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
|
|
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 =
|
|
479
|
+
const delayMs = config.delay > 0 ? config.delay : 10;
|
|
429
480
|
|
|
430
481
|
setTimeout(() => {
|
|
431
|
-
if (
|
|
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
|
|
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
|
-
|
|
477
|
-
|
|
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
|
|
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
|
-
|
|
501
|
-
if (
|
|
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
|
-
|
|
507
|
-
|
|
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
|
}
|