@farming-labs/docs 0.2.120 → 0.2.122
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 +8 -0
- package/dist/access.d.mts +13 -0
- package/dist/access.mjs +56 -0
- package/dist/agent-conformance.d.mts +60 -0
- package/dist/agent-conformance.mjs +538 -0
- package/dist/agent-contract.d.mts +47 -0
- package/dist/agent-contract.mjs +645 -0
- package/dist/agent-detection.d.mts +6 -0
- package/dist/agent-detection.mjs +83 -0
- package/dist/agent-evals.d.mts +224 -0
- package/dist/{agent-evals-BLNLvXbG.mjs → agent-evals.mjs} +7 -565
- package/dist/agent-feedback-evaluations-node.mjs +38 -0
- package/dist/agent-feedback-evaluations.d.mts +91 -0
- package/dist/{agent-feedback-evaluations-CcMGlgsO.mjs → agent-feedback-evaluations.mjs} +2 -2
- package/dist/agent-feedback-loop.d.mts +45 -0
- package/dist/{agent-feedback-loop-CcZL_sb1.mjs → agent-feedback-loop.mjs} +1 -1
- package/dist/agent-maintenance.d.mts +71 -0
- package/dist/{agent-maintenance-CuZ7iAKq.mjs → agent-maintenance.mjs} +1 -1
- package/dist/agent-provenance.d.mts +23 -0
- package/dist/agent-provenance.mjs +77 -0
- package/dist/agent-scope.mjs +321 -0
- package/dist/agent-skills-archive.mjs +86 -0
- package/dist/agent-skills-bundle.d.mts +1 -1
- package/dist/{agent-skills-progressive-disclosure-PjPtTYL-.mjs → agent-skills-progressive-disclosure.mjs} +2 -2
- package/dist/{agent-skills-server-D5IuvwM7.d.mts → agent-skills-server.d.mts} +3 -3
- package/dist/{agent-skills-server-BC7md56k.mjs → agent-skills-server.mjs} +2 -2
- package/dist/agent-skills-vite.d.mts +3 -3
- package/dist/agent-skills-vite.mjs +1 -2
- package/dist/agent-surface-drift.mjs +236 -0
- package/dist/{golden-evaluations-CLUViycJ.mjs → agent-usefulness.mjs} +5 -290
- package/dist/{content-change-hydration-wqXIZHp6.d.mts → agent.d.mts} +10 -411
- package/dist/agent.mjs +2729 -0
- package/dist/{analytics-w3FDOksr.d.mts → analytics.d.mts} +2 -2
- package/dist/{analytics-p8Vhhu2P.mjs → analytics.mjs} +2 -2
- package/dist/{sitemap-server-BUdPBcGs.d.mts → api-reference.d.mts} +2 -6
- package/dist/api-reference.mjs +1481 -0
- package/dist/{audience-exposure-DBrFWULA.d.mts → audience-exposure.d.mts} +1 -1
- package/dist/audience-exposure.mjs +8 -0
- package/dist/audience.d.mts +44 -0
- package/dist/audience.mjs +1654 -0
- package/dist/authoring-mcp.d.mts +44 -0
- package/dist/{authoring-mcp-CiCUQe3o.mjs → authoring-mcp.mjs} +3 -3
- package/dist/browser.d.mts +6 -4
- package/dist/browser.mjs +6 -7
- package/dist/build.d.mts +8 -0
- package/dist/build.mjs +8 -0
- package/dist/changelog.d.mts +20 -0
- package/dist/changelog.mjs +39 -0
- package/dist/{agent-export-CA6IUQ7T.mjs → cli/agent-export.mjs} +19 -45
- package/dist/{agent-CYk2DxY7.mjs → cli/agent.mjs} +6 -35
- package/dist/{agents-BUiVmj8i.mjs → cli/agents.mjs} +5 -18
- package/dist/cli/api-doctor.mjs +206 -0
- package/dist/{authoring-C2ymvYWl.mjs → cli/authoring.mjs} +2 -8
- package/dist/{cloud-6eKrlmKd.mjs → cli/cloud.mjs} +3 -3
- package/dist/{codeblocks-CHpgAwY-.mjs → cli/codeblocks.mjs} +2 -2
- package/dist/cli/config.d.mts +11 -0
- package/dist/{config-BZnKAj_Y.mjs → cli/config.mjs} +1 -1
- package/dist/cli/dev-banner.mjs +58 -0
- package/dist/{dev-Bnn-NUNa.mjs → cli/dev.mjs} +3 -57
- package/dist/{doctor-Bfkrkzad.mjs → cli/doctor.mjs} +41 -226
- package/dist/{downgrade-CHMta85e.mjs → cli/downgrade.mjs} +2 -2
- package/dist/cli/errors.d.mts +4 -0
- package/dist/{errors-BtnVLhNF.mjs → cli/errors.mjs} +1 -1
- package/dist/{feedback-evals-DMilg9pY.mjs → cli/feedback-evals.mjs} +3 -36
- package/dist/{feedback-DL4T5oIO.mjs → cli/feedback.mjs} +1 -1
- package/dist/cli/git-dates.mjs +35 -0
- package/dist/cli/golden-evaluations.mjs +52 -0
- package/dist/cli/index.d.mts +2 -3
- package/dist/cli/index.mjs +38 -38
- package/dist/{init-K0LmXDZ5.mjs → cli/init.mjs} +3 -3
- package/dist/{mcp-iLCmnPJI.mjs → cli/mcp.mjs} +7 -19
- package/dist/{openapi-diff-HcpUNjA-.mjs → cli/openapi-diff.mjs} +2 -7
- package/dist/{openapi-sdk-BQKC6H-_.mjs → cli/openapi-sdk.mjs} +2 -7
- package/dist/{openapi-source-DK6ts_Bk.mjs → cli/openapi-source.mjs} +2 -2
- package/dist/{package-version-HJhYeq4M.mjs → cli/package-version.mjs} +2 -2
- package/dist/{propose-JmUv_BTt.mjs → cli/propose.mjs} +1 -1
- package/dist/{review-C6EmbV0l.mjs → cli/review.mjs} +16 -15
- package/dist/{robots-DteZB8vl.mjs → cli/robots.mjs} +3 -7
- package/dist/{search-Bhuv_YsL.mjs → cli/search.mjs} +7 -20
- package/dist/{sitemap-DW5LDKDa.mjs → cli/sitemap.mjs} +7 -42
- package/dist/{skills-BAPQLSCm.mjs → cli/skills.mjs} +5 -19
- package/dist/{templates-BuQ8YsGv.mjs → cli/templates.mjs} +1 -1
- package/dist/{upgrade-DlQMOFV1.mjs → cli/upgrade.mjs} +2 -2
- package/dist/{utils-C7KDrPpA.mjs → cli/utils.mjs} +1 -1
- package/dist/client/react.d.mts +2 -18
- package/dist/client/react.mjs +1 -157
- package/dist/{cloud-analytics-D14HplI7.mjs → cloud-analytics.mjs} +1 -1
- package/dist/{cloud-ask-ai-BrsTXQf6.d.mts → cloud-ask-ai.d.mts} +2 -2
- package/dist/{cloud-ask-ai-BqKhjpS5.mjs → cloud-ask-ai.mjs} +3 -3
- package/dist/code-blocks.d.mts +51 -0
- package/dist/{code-blocks-GYdvS3BH.mjs → code-blocks.mjs} +2 -2
- package/dist/code-group-mdx.d.mts +11 -0
- package/dist/code-group-mdx.mjs +57 -0
- package/dist/content-change-hydration.d.mts +87 -0
- package/dist/{openapi-mcp-BIOTLiA7.mjs → content-change-hydration.mjs} +5 -210
- package/dist/content-changes.d.mts +47 -0
- package/dist/content-changes.mjs +216 -0
- package/dist/create-theme.d.mts +48 -0
- package/dist/create-theme.mjs +57 -0
- package/dist/date.d.mts +5 -0
- package/dist/date.mjs +17 -0
- package/dist/define-docs.d.mts +9 -0
- package/dist/define-docs.mjs +46 -0
- package/dist/docs-cloud-client.d.mts +18 -0
- package/dist/docs-cloud-client.mjs +161 -0
- package/dist/docs-cloud-server.d.mts +2 -2
- package/dist/docs-cloud-server.mjs +2 -3
- package/dist/heading-toc.d.mts +10 -0
- package/dist/heading-toc.mjs +23 -0
- package/dist/http-body.d.mts +14 -0
- package/dist/{http-body-DkAPk6D5.mjs → http-body.mjs} +1 -1
- package/dist/http-cache.d.mts +25 -0
- package/dist/http-cache.mjs +140 -0
- package/dist/http-link.mjs +81 -0
- package/dist/i18n.d.mts +18 -0
- package/dist/{i18n-B1gJ9EW0.mjs → i18n.mjs} +1 -1
- package/dist/index.d.mts +42 -674
- package/dist/index.mjs +40 -14
- package/dist/markdown-rendering.mjs +1 -1
- package/dist/markdown-section-cache.mjs +49 -0
- package/dist/markdown-sections.d.mts +47 -0
- package/dist/{markdown-sections-9Oxr33b-.mjs → markdown-sections.mjs} +29 -703
- package/dist/mcp-auth.d.mts +14 -0
- package/dist/mcp-auth.mjs +99 -0
- package/dist/mcp.d.mts +5 -3
- package/dist/mcp.mjs +21 -9
- package/dist/mdx.d.mts +6 -0
- package/dist/mdx.mjs +7 -0
- package/dist/metadata.d.mts +60 -0
- package/dist/{metadata-BOHn0Y5K.mjs → metadata.mjs} +2 -2
- package/dist/native-api-reference.mjs +968 -0
- package/dist/okf.d.mts +42 -0
- package/dist/okf.mjs +145 -0
- package/dist/openapi-contract.mjs +154 -0
- package/dist/{openapi-mcp-node-C4jUpZG6.mjs → openapi-mcp-node.mjs} +1 -1
- package/dist/openapi-mcp.d.mts +26 -0
- package/dist/openapi-mcp.mjs +132 -0
- package/dist/openapi-operations.d.mts +71 -0
- package/dist/{openapi-operations-DFpaqOqA.mjs → openapi-operations.mjs} +1 -1
- package/dist/openapi-overlays.mjs +221 -0
- package/dist/openapi-references.mjs +233 -0
- package/dist/pagination.d.mts +7 -0
- package/dist/pagination.mjs +100 -0
- package/dist/prompt-references.d.mts +12 -0
- package/dist/prompt-references.mjs +132 -0
- package/dist/prompt-utils.d.mts +36 -0
- package/dist/{prompt-utils-B5g-bv7o.mjs → prompt-utils.mjs} +1 -57
- package/dist/reading-time.d.mts +23 -0
- package/dist/reading-time.mjs +58 -0
- package/dist/related.d.mts +7 -0
- package/dist/related.mjs +50 -0
- package/dist/retrieval-digest.d.mts +16 -0
- package/dist/{retrieval-digest-CT-eIEcp.mjs → retrieval-digest.mjs} +1 -1
- package/dist/review.d.mts +69 -0
- package/dist/review.mjs +562 -0
- package/dist/robots.d.mts +54 -0
- package/dist/{robots-BYJAVeI6.mjs → robots.mjs} +5 -3
- package/dist/runtime.d.mts +3 -3
- package/dist/runtime.mjs +3 -9
- package/dist/search.d.mts +210 -0
- package/dist/search.mjs +3401 -0
- package/dist/server.d.mts +22 -198
- package/dist/server.mjs +21 -19
- package/dist/sidebar.d.mts +15 -0
- package/dist/sidebar.mjs +73 -0
- package/dist/sitemap-server.d.mts +7 -0
- package/dist/{sitemap-server-Lg3YhUx4.mjs → sitemap-server.mjs} +2 -2
- package/dist/{sitemap-Cl8vrixd.d.mts → sitemap.d.mts} +3 -3
- package/dist/sitemap.mjs +254 -0
- package/dist/{standards-discovery-BHPc7YIX.d.mts → standards-discovery.d.mts} +2 -2
- package/dist/{standards-discovery-sSEojU80.mjs → standards-discovery.mjs} +3 -139
- package/dist/telemetry-runtime.d.mts +42 -0
- package/dist/telemetry-runtime.mjs +238 -0
- package/dist/telemetry.d.mts +15 -0
- package/dist/telemetry.mjs +117 -0
- package/dist/{types-BJFeMXWZ.d.mts → types.d.mts} +3 -2
- package/dist/urls.d.mts +7 -0
- package/dist/urls.mjs +21 -0
- package/dist/utils.d.mts +8 -0
- package/dist/utils.mjs +22 -0
- package/package.json +21 -3
- package/dist/agent-DkAidqtV.mjs +0 -9026
- package/dist/api-reference-BYee4zMo.mjs +0 -3046
- package/dist/prompt-references-DMgbu_dz.mjs +0 -1082
- package/dist/retrieval-digest-AOx2-LbC.d.mts +0 -367
- package/dist/sidebar-CFHBj29f.mjs +0 -421
- package/dist/sidebar-CMPbVfGP.d.mts +0 -63
- /package/dist/{openapi-CfCzX_a1.mjs → cli/openapi.mjs} +0 -0
|
@@ -0,0 +1,1481 @@
|
|
|
1
|
+
import { toDocsMarkdownUrl } from "./urls.mjs";
|
|
2
|
+
import { acceptsDocsMarkdown, createDocsMarkdownResponse, detectDocsMarkdownAgentRequest, hasDocsMarkdownSignatureAgent, renderDocsMarkdownDocument } from "./agent.mjs";
|
|
3
|
+
import { buildNormalizedOpenApiModel } from "./openapi-operations.mjs";
|
|
4
|
+
import { assertValidOpenApiContract } from "./openapi-contract.mjs";
|
|
5
|
+
import { applyOpenApiOverlayDocuments } from "./openapi-overlays.mjs";
|
|
6
|
+
import { renderNativeApiReferenceHtml } from "./native-api-reference.mjs";
|
|
7
|
+
import { resolveOpenApiReferences, resolveOpenApiReferencesSync } from "./openapi-references.mjs";
|
|
8
|
+
import { parse } from "yaml";
|
|
9
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
10
|
+
import { readFile } from "node:fs/promises";
|
|
11
|
+
import { basename, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
12
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
13
|
+
import { getHtmlDocument } from "@scalar/core/libs/html-rendering";
|
|
14
|
+
|
|
15
|
+
//#region src/api-reference.ts
|
|
16
|
+
const NEXT_ROUTE_FILE_RE = /^route\.(ts|tsx|js|jsx)$/;
|
|
17
|
+
const SVELTE_ROUTE_FILE_RE = /^\+server\.(ts|js)$/;
|
|
18
|
+
const ASTRO_ROUTE_FILE_RE = /^[^.].*\.(ts|js|mts|mjs)$/;
|
|
19
|
+
const NUXT_ROUTE_FILE_RE = /^[^.].*\.(ts|js|mts|mjs)$/;
|
|
20
|
+
const TANSTACK_ROUTE_FILE_RE = /\.(ts|tsx|js|jsx)$/;
|
|
21
|
+
const METHOD_RE = /export\s+(?:async\s+function|function|const)\s+(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD|ALL)\b/g;
|
|
22
|
+
const METHOD_NAMES = [
|
|
23
|
+
"GET",
|
|
24
|
+
"POST",
|
|
25
|
+
"PUT",
|
|
26
|
+
"PATCH",
|
|
27
|
+
"DELETE",
|
|
28
|
+
"OPTIONS",
|
|
29
|
+
"HEAD"
|
|
30
|
+
];
|
|
31
|
+
const DEFAULT_API_REFERENCE_OPENAPI_ROUTE = "/api/docs?format=openapi";
|
|
32
|
+
const OPENAPI_SPEC_FETCH_TIMEOUT_MS = 1e4;
|
|
33
|
+
const OPENAPI_SPEC_MAX_BYTES = 5 * 1024 * 1024;
|
|
34
|
+
function normalizePathSegment(value) {
|
|
35
|
+
return value.replace(/^\/+|\/+$/g, "");
|
|
36
|
+
}
|
|
37
|
+
function resolveApiReferenceConfig(value) {
|
|
38
|
+
if (value === true) return {
|
|
39
|
+
enabled: true,
|
|
40
|
+
path: "api-reference",
|
|
41
|
+
specUrl: void 0,
|
|
42
|
+
overlays: [],
|
|
43
|
+
versions: [],
|
|
44
|
+
defaultVersion: void 0,
|
|
45
|
+
catalogTargets: void 0,
|
|
46
|
+
renderer: void 0,
|
|
47
|
+
rendererOptions: void 0,
|
|
48
|
+
mcp: void 0,
|
|
49
|
+
routeRoot: "api",
|
|
50
|
+
exclude: []
|
|
51
|
+
};
|
|
52
|
+
if (!value) return {
|
|
53
|
+
enabled: false,
|
|
54
|
+
path: "api-reference",
|
|
55
|
+
specUrl: void 0,
|
|
56
|
+
overlays: [],
|
|
57
|
+
versions: [],
|
|
58
|
+
defaultVersion: void 0,
|
|
59
|
+
catalogTargets: void 0,
|
|
60
|
+
renderer: void 0,
|
|
61
|
+
rendererOptions: void 0,
|
|
62
|
+
mcp: void 0,
|
|
63
|
+
routeRoot: "api",
|
|
64
|
+
exclude: []
|
|
65
|
+
};
|
|
66
|
+
const specUrl = normalizeRemoteSpecUrl(value.specUrl);
|
|
67
|
+
const overlays = normalizeOpenApiOverlaySources(value.overlays, "apiReference.overlays");
|
|
68
|
+
const { versions, defaultVersion } = resolveApiReferenceVersions(value);
|
|
69
|
+
if (versions.length > 0 && specUrl) throw new Error("`apiReference.versions` cannot be combined with `apiReference.specUrl`.");
|
|
70
|
+
if (versions.length > 0 && overlays.length > 0) throw new Error("`apiReference.versions` cannot be combined with top-level `apiReference.overlays`; configure overlays on each version instead.");
|
|
71
|
+
const renderer = normalizeApiReferenceRenderer(value.renderer);
|
|
72
|
+
return {
|
|
73
|
+
enabled: value.enabled !== false,
|
|
74
|
+
path: normalizePathSegment(value.path ?? "api-reference"),
|
|
75
|
+
specUrl,
|
|
76
|
+
overlays,
|
|
77
|
+
versions,
|
|
78
|
+
defaultVersion,
|
|
79
|
+
catalogTargets: normalizeApiReferenceCatalogTargets(value.catalogTargets),
|
|
80
|
+
renderer: renderer?.name,
|
|
81
|
+
rendererOptions: renderer?.options,
|
|
82
|
+
mcp: resolveOpenApiMcpConfig(value.mcp),
|
|
83
|
+
routeRoot: normalizePathSegment(value.routeRoot ?? "api") || "api",
|
|
84
|
+
exclude: normalizeApiReferenceExcludes(value.exclude)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function normalizeOpenApiOverlaySources(value, location) {
|
|
88
|
+
if (value === void 0) return [];
|
|
89
|
+
if (!Array.isArray(value)) throw new Error(`\`${location}\` must be an array of sources.`);
|
|
90
|
+
return value.map((source, index) => {
|
|
91
|
+
if (typeof source !== "string" || !source.trim()) throw new Error(`\`${location}[${index}]\` must be a non-empty string.`);
|
|
92
|
+
return source.trim();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function resolveApiReferenceVersions(value) {
|
|
96
|
+
if (value.versions === void 0) {
|
|
97
|
+
if (value.defaultVersion !== void 0) throw new Error("`apiReference.defaultVersion` requires `apiReference.versions`.");
|
|
98
|
+
return { versions: [] };
|
|
99
|
+
}
|
|
100
|
+
if (!value.versions || typeof value.versions !== "object" || Array.isArray(value.versions)) throw new Error("`apiReference.versions` must be an object keyed by version identifier.");
|
|
101
|
+
const entries = Object.entries(value.versions);
|
|
102
|
+
if (entries.length === 0) throw new Error("`apiReference.versions` must not be empty.");
|
|
103
|
+
const defaultVersion = value.defaultVersion?.trim();
|
|
104
|
+
if (!defaultVersion) throw new Error("`apiReference.defaultVersion` is required when versions are configured.");
|
|
105
|
+
if (!Object.prototype.hasOwnProperty.call(value.versions, defaultVersion)) throw new Error(`\`apiReference.defaultVersion\` references unknown version \`${defaultVersion}\`.`);
|
|
106
|
+
return {
|
|
107
|
+
versions: entries.map(([id, rawVersion]) => resolveApiReferenceVersionConfig(id, rawVersion, defaultVersion)),
|
|
108
|
+
defaultVersion
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function resolveApiReferenceVersionConfig(rawId, value, defaultVersion) {
|
|
112
|
+
const id = rawId.trim();
|
|
113
|
+
if (!id || !/^[a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?$/.test(id)) throw new Error(`OpenAPI version identifier \`${rawId}\` must be URL-safe and contain only letters, numbers, dots, underscores, or hyphens.`);
|
|
114
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`\`apiReference.versions.${id}\` must be an object.`);
|
|
115
|
+
const specUrl = normalizeRemoteSpecUrl(value.specUrl);
|
|
116
|
+
if (!specUrl) throw new Error(`\`apiReference.versions.${id}.specUrl\` is required.`);
|
|
117
|
+
return {
|
|
118
|
+
id,
|
|
119
|
+
label: value.label?.trim() || id,
|
|
120
|
+
specUrl,
|
|
121
|
+
overlays: normalizeOpenApiOverlaySources(value.overlays, `apiReference.versions.${id}.overlays`),
|
|
122
|
+
default: id === defaultVersion
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function resolveOpenApiMcpConfig(value) {
|
|
126
|
+
if (!value) return void 0;
|
|
127
|
+
return value === true ? { enabled: true } : {
|
|
128
|
+
...value,
|
|
129
|
+
enabled: value.enabled !== false
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function isApiReferenceRenderer(value) {
|
|
133
|
+
return value === "farming-labs" || value === "fumadocs" || value === "scalar";
|
|
134
|
+
}
|
|
135
|
+
function normalizeApiReferenceRenderer(value) {
|
|
136
|
+
if (isApiReferenceRenderer(value)) return { name: value };
|
|
137
|
+
if (!value || typeof value !== "object" || !isApiReferenceRenderer(value.name)) return;
|
|
138
|
+
if (value.options !== void 0 && (!value.options || typeof value.options !== "object" || Array.isArray(value.options))) return;
|
|
139
|
+
return {
|
|
140
|
+
name: value.name,
|
|
141
|
+
options: value.options
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function resolveApiReferenceRenderer(value, _framework) {
|
|
145
|
+
const config = resolveApiReferenceConfig(value);
|
|
146
|
+
if (config.renderer) return config.renderer;
|
|
147
|
+
return "farming-labs";
|
|
148
|
+
}
|
|
149
|
+
function resolveApiReferenceOpenApiDiscovery(value, options = {}) {
|
|
150
|
+
const config = resolveApiReferenceConfig(value);
|
|
151
|
+
if (!config.enabled) return { enabled: false };
|
|
152
|
+
const selectedSpecUrl = resolveApiReferenceVersion(config)?.specUrl ?? config.specUrl;
|
|
153
|
+
const catalogTargets = config.catalogTargets ?? (!selectedSpecUrl || isRequestRelativeSpecUrl(selectedSpecUrl) ? ["/"] : void 0);
|
|
154
|
+
const route = options.route ?? DEFAULT_API_REFERENCE_OPENAPI_ROUTE;
|
|
155
|
+
const versions = config.versions.length > 0 ? config.versions.map((version) => ({
|
|
156
|
+
id: version.id,
|
|
157
|
+
label: version.label,
|
|
158
|
+
default: version.default,
|
|
159
|
+
url: appendUrlSearchParam(route, "version", version.id),
|
|
160
|
+
apiReferencePath: `/${config.path}/${encodeURIComponent(version.id)}`,
|
|
161
|
+
specUrl: isLocalSpecSource(version.specUrl) ? void 0 : version.specUrl
|
|
162
|
+
})) : void 0;
|
|
163
|
+
return {
|
|
164
|
+
enabled: true,
|
|
165
|
+
url: route,
|
|
166
|
+
urlSource: options.route === void 0 ? "default" : "configured",
|
|
167
|
+
source: selectedSpecUrl ? "configured" : "generated",
|
|
168
|
+
specUrl: isLocalSpecSource(selectedSpecUrl) ? void 0 : selectedSpecUrl,
|
|
169
|
+
apiReferencePath: `/${config.path}`,
|
|
170
|
+
catalogTargets,
|
|
171
|
+
versions
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function appendUrlSearchParam(url, name, value) {
|
|
175
|
+
const [withoutHash, hash] = url.split("#", 2);
|
|
176
|
+
return `${withoutHash}${withoutHash.includes("?") ? "&" : "?"}${encodeURIComponent(name)}=${encodeURIComponent(value)}${hash === void 0 ? "" : `#${hash}`}`;
|
|
177
|
+
}
|
|
178
|
+
function resolveApiReferenceVersion(config, requestedVersion) {
|
|
179
|
+
if (config.versions.length === 0) return void 0;
|
|
180
|
+
const versionId = requestedVersion?.trim() || config.defaultVersion;
|
|
181
|
+
return config.versions.find((version) => version.id === versionId);
|
|
182
|
+
}
|
|
183
|
+
function resolveApiReferenceVersionFromPathname(value, pathname) {
|
|
184
|
+
const config = resolveApiReferenceConfig(value);
|
|
185
|
+
if (config.versions.length === 0) return void 0;
|
|
186
|
+
const pathSegments = normalizePathSegment(config.path).split("/").filter(Boolean);
|
|
187
|
+
const pathnameSegments = pathname.split("/").filter(Boolean);
|
|
188
|
+
if (!pathSegments.every((segment, index) => pathnameSegments[index] === segment)) return;
|
|
189
|
+
const rawVersion = pathnameSegments[pathSegments.length];
|
|
190
|
+
if (!rawVersion) return void 0;
|
|
191
|
+
try {
|
|
192
|
+
return resolveApiReferenceVersion(config, decodeURIComponent(rawVersion));
|
|
193
|
+
} catch {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function isApiReferenceOpenApiRequest(url) {
|
|
198
|
+
return url.searchParams.get("format")?.trim() === "openapi";
|
|
199
|
+
}
|
|
200
|
+
function normalizeRemoteSpecUrl(value) {
|
|
201
|
+
const trimmed = value?.trim();
|
|
202
|
+
if (!trimmed) return void 0;
|
|
203
|
+
return trimmed;
|
|
204
|
+
}
|
|
205
|
+
function normalizeApiReferenceCatalogTargets(value) {
|
|
206
|
+
if (value === void 0) return void 0;
|
|
207
|
+
return Array.from(new Set(value.filter((target) => typeof target === "string").map((target) => target.trim()).filter(Boolean)));
|
|
208
|
+
}
|
|
209
|
+
function isRequestRelativeSpecUrl(value) {
|
|
210
|
+
return typeof value === "string" && value.startsWith("/") && !value.startsWith("//");
|
|
211
|
+
}
|
|
212
|
+
function isRemoteSpecUrl(value) {
|
|
213
|
+
return /^(?:https?:)?\/\//i.test(value);
|
|
214
|
+
}
|
|
215
|
+
function isLocalSpecSource(value) {
|
|
216
|
+
if (!value) return false;
|
|
217
|
+
if (value.startsWith("file:")) return true;
|
|
218
|
+
return !isRemoteSpecUrl(value) && !isRequestRelativeSpecUrl(value) && !/^[a-z][a-z\d+.-]*:/i.test(value);
|
|
219
|
+
}
|
|
220
|
+
function buildApiReferencePageTitle(config, title = "API Reference") {
|
|
221
|
+
const template = config.metadata?.titleTemplate;
|
|
222
|
+
if (!template) return title;
|
|
223
|
+
return template.replace("%s", title);
|
|
224
|
+
}
|
|
225
|
+
function buildApiReferenceScalarCss(config) {
|
|
226
|
+
const theme = resolveTheme(config);
|
|
227
|
+
const colors = theme?.ui?.colors;
|
|
228
|
+
const typography = theme?.ui?.typography?.font?.style;
|
|
229
|
+
const layout = theme?.ui?.layout;
|
|
230
|
+
const radius = resolveApiReferenceRadius(theme);
|
|
231
|
+
const primary = colors?.primary ?? "#6366f1";
|
|
232
|
+
const border = colors?.border ?? "#2a2a2a";
|
|
233
|
+
const muted = colors?.muted ?? "#64748b";
|
|
234
|
+
const background = colors?.background ?? "#ffffff";
|
|
235
|
+
const card = colors?.card ?? background;
|
|
236
|
+
const foreground = colors?.foreground ?? inferApiReferenceForeground(theme, background);
|
|
237
|
+
const primaryForeground = colors?.primaryForeground ?? inferContrastingForeground(primary, theme, background);
|
|
238
|
+
const sidebarWidth = layout?.sidebarWidth ?? 280;
|
|
239
|
+
const sans = typography?.sans ?? "\"Geist\", \"Inter\", \"Segoe UI\", sans-serif";
|
|
240
|
+
const mono = typography?.mono ?? "\"Geist Mono\", \"SFMono-Regular\", \"Menlo\", monospace";
|
|
241
|
+
const isPixelBorder = theme?.name?.includes("pixel-border");
|
|
242
|
+
return `
|
|
243
|
+
:root {
|
|
244
|
+
--scalar-font: ${sans};
|
|
245
|
+
--scalar-font-code: ${mono};
|
|
246
|
+
--scalar-radius: ${radius};
|
|
247
|
+
--scalar-radius-sm: calc(${radius} + 2px);
|
|
248
|
+
--scalar-theme-primary: ${primary};
|
|
249
|
+
--scalar-theme-border: ${border};
|
|
250
|
+
--scalar-theme-muted: ${muted};
|
|
251
|
+
--scalar-theme-background: ${background};
|
|
252
|
+
--scalar-theme-card: ${card};
|
|
253
|
+
--scalar-theme-foreground: ${foreground};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.dark-mode {
|
|
257
|
+
--scalar-background-1: color-mix(
|
|
258
|
+
in srgb,
|
|
259
|
+
var(--scalar-theme-background) 96%,
|
|
260
|
+
var(--scalar-theme-primary) 4%
|
|
261
|
+
);
|
|
262
|
+
--scalar-background-2: color-mix(in srgb, var(--scalar-theme-card) 94%, var(--scalar-theme-primary) 6%);
|
|
263
|
+
--scalar-background-3: color-mix(
|
|
264
|
+
in srgb,
|
|
265
|
+
var(--scalar-theme-card) 90%,
|
|
266
|
+
var(--scalar-theme-foreground) 10%
|
|
267
|
+
);
|
|
268
|
+
--scalar-color-1: var(--scalar-theme-foreground);
|
|
269
|
+
--scalar-color-2: color-mix(in srgb, var(--scalar-theme-foreground) 72%, transparent);
|
|
270
|
+
--scalar-color-3: color-mix(in srgb, var(--scalar-theme-foreground) 52%, transparent);
|
|
271
|
+
--scalar-color-accent: var(--scalar-theme-primary);
|
|
272
|
+
--scalar-sidebar-background-1: var(--scalar-background-1);
|
|
273
|
+
--scalar-sidebar-background-2: var(--scalar-background-2);
|
|
274
|
+
--scalar-sidebar-color-1: var(--scalar-color-1);
|
|
275
|
+
--scalar-sidebar-color-2: var(--scalar-color-2);
|
|
276
|
+
--scalar-sidebar-search-background: var(--scalar-background-2);
|
|
277
|
+
--scalar-sidebar-search-border-color: var(--scalar-border-color);
|
|
278
|
+
--scalar-sidebar-search-color: var(--scalar-color-2);
|
|
279
|
+
--scalar-sidebar-color-active: var(--scalar-theme-primary);
|
|
280
|
+
--scalar-sidebar-item-active-background: color-mix(
|
|
281
|
+
in srgb,
|
|
282
|
+
var(--scalar-theme-primary) 7%,
|
|
283
|
+
transparent
|
|
284
|
+
);
|
|
285
|
+
--scalar-border-color: ${isPixelBorder ? "color-mix(in srgb, var(--scalar-theme-foreground) 10%, transparent)" : "color-mix(in srgb, var(--scalar-theme-border) 14%, rgba(255, 255, 255, 0.02))"};
|
|
286
|
+
--scalar-button-1: var(--scalar-theme-primary);
|
|
287
|
+
--scalar-button-1-color: ${primaryForeground};
|
|
288
|
+
--scalar-button-1-hover: color-mix(in srgb, var(--scalar-theme-primary) 88%, white 12%);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.light-mode {
|
|
292
|
+
--scalar-background-1: var(--scalar-theme-background);
|
|
293
|
+
--scalar-background-2: color-mix(in srgb, var(--scalar-theme-card) 92%, white 8%);
|
|
294
|
+
--scalar-background-3: color-mix(in srgb, var(--scalar-theme-card) 84%, black 4%);
|
|
295
|
+
--scalar-color-1: var(--scalar-theme-foreground);
|
|
296
|
+
--scalar-color-2: var(--scalar-theme-muted);
|
|
297
|
+
--scalar-color-3: color-mix(in srgb, var(--scalar-theme-muted) 78%, white 22%);
|
|
298
|
+
--scalar-color-accent: var(--scalar-theme-primary);
|
|
299
|
+
--scalar-sidebar-background-1: var(--scalar-background-1);
|
|
300
|
+
--scalar-sidebar-background-2: var(--scalar-background-2);
|
|
301
|
+
--scalar-sidebar-color-1: var(--scalar-color-1);
|
|
302
|
+
--scalar-sidebar-color-2: var(--scalar-color-2);
|
|
303
|
+
--scalar-sidebar-search-background: var(--scalar-background-2);
|
|
304
|
+
--scalar-sidebar-search-border-color: var(--scalar-border-color);
|
|
305
|
+
--scalar-sidebar-search-color: var(--scalar-color-2);
|
|
306
|
+
--scalar-sidebar-color-active: var(--scalar-theme-primary);
|
|
307
|
+
--scalar-sidebar-item-active-background: color-mix(
|
|
308
|
+
in srgb,
|
|
309
|
+
var(--scalar-theme-primary) 5%,
|
|
310
|
+
transparent
|
|
311
|
+
);
|
|
312
|
+
--scalar-border-color: ${isPixelBorder ? "color-mix(in srgb, var(--scalar-theme-foreground) 14%, transparent)" : "color-mix(in srgb, var(--scalar-theme-border) 30%, white 70%)"};
|
|
313
|
+
--scalar-button-1: var(--scalar-theme-primary);
|
|
314
|
+
--scalar-button-1-color: ${primaryForeground};
|
|
315
|
+
--scalar-button-1-hover: color-mix(in srgb, var(--scalar-theme-primary) 88%, black 12%);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
body {
|
|
319
|
+
background: var(--scalar-background-1);
|
|
320
|
+
color: var(--scalar-color-1);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.t-doc__sidebar {
|
|
324
|
+
width: min(${sidebarWidth}px, 100vw);
|
|
325
|
+
border-right: 1px solid var(--scalar-border-color);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.scalar-card,
|
|
329
|
+
.t-doc__sidebar,
|
|
330
|
+
.references-layout .reference-layout__content .request-card,
|
|
331
|
+
.references-layout .reference-layout__content .response-card,
|
|
332
|
+
.references-layout .reference-layout__content .scalar-card-header,
|
|
333
|
+
.references-layout .reference-layout__content .scalar-card-footer,
|
|
334
|
+
.references-layout .reference-layout__content .section,
|
|
335
|
+
.references-layout .reference-layout__content .section-container {
|
|
336
|
+
border-color: var(--scalar-border-color) !important;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.t-doc__sidebar,
|
|
340
|
+
.t-doc__sidebar * {
|
|
341
|
+
font-family: var(--scalar-font);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.t-doc__sidebar .sidebar-search {
|
|
345
|
+
margin: 0.5rem 0 1rem;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.t-doc__sidebar .sidebar-search input {
|
|
349
|
+
border-radius: var(--scalar-radius-sm);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.t-doc__sidebar .sidebar-item,
|
|
353
|
+
.t-doc__sidebar .sidebar-heading {
|
|
354
|
+
border-radius: var(--scalar-radius-sm);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.t-doc__sidebar .sidebar-group-label {
|
|
358
|
+
font-size: 0.72rem;
|
|
359
|
+
letter-spacing: 0.08em;
|
|
360
|
+
text-transform: uppercase;
|
|
361
|
+
color: var(--scalar-color-3);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.t-doc__sidebar .sidebar-item--active {
|
|
365
|
+
font-weight: 600;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.scalar-card,
|
|
369
|
+
.references-layout .reference-layout__content .request-card,
|
|
370
|
+
.references-layout .reference-layout__content .response-card {
|
|
371
|
+
border-radius: var(--scalar-radius);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.references-layout .reference-layout__content {
|
|
375
|
+
padding-top: 1.5rem;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.references-layout .section-content,
|
|
379
|
+
.references-layout .section-flare {
|
|
380
|
+
background: transparent;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.references-layout .reference-layout__content,
|
|
384
|
+
.references-layout .reference-layout__content * {
|
|
385
|
+
font-family: var(--scalar-font);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.references-layout code,
|
|
389
|
+
.references-layout pre,
|
|
390
|
+
.references-layout .scalar-codeblock {
|
|
391
|
+
font-family: var(--scalar-font-code);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.references-layout,
|
|
395
|
+
.references-layout * {
|
|
396
|
+
color: inherit;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.references-layout .reference-layout__content .introduction,
|
|
400
|
+
.references-layout .reference-layout__content .section,
|
|
401
|
+
.references-layout .reference-layout__content .section-container,
|
|
402
|
+
.references-layout .reference-layout__content .operation-details,
|
|
403
|
+
.references-layout .reference-layout__content .markdown,
|
|
404
|
+
.references-layout .reference-layout__content .markdown *,
|
|
405
|
+
.references-layout .reference-layout__content .property,
|
|
406
|
+
.references-layout .reference-layout__content .property *,
|
|
407
|
+
.references-layout .reference-layout__content .parameter-item,
|
|
408
|
+
.references-layout .reference-layout__content .parameter-item *,
|
|
409
|
+
.references-layout .reference-layout__content .response-card,
|
|
410
|
+
.references-layout .reference-layout__content .response-card *,
|
|
411
|
+
.references-layout .reference-layout__content .request-card,
|
|
412
|
+
.references-layout .reference-layout__content .request-card * {
|
|
413
|
+
color: var(--scalar-color-1);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.references-layout a,
|
|
417
|
+
.references-layout button {
|
|
418
|
+
color: var(--scalar-color-1);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
${isPixelBorder ? buildPixelBorderScalarCss() : ""}
|
|
422
|
+
`;
|
|
423
|
+
}
|
|
424
|
+
function buildPixelBorderScalarCss() {
|
|
425
|
+
return `
|
|
426
|
+
.t-doc__sidebar,
|
|
427
|
+
.references-layout .reference-layout__content {
|
|
428
|
+
background-image:
|
|
429
|
+
repeating-linear-gradient(
|
|
430
|
+
-45deg,
|
|
431
|
+
color-mix(in srgb, var(--scalar-theme-border) 12%, transparent),
|
|
432
|
+
color-mix(in srgb, var(--scalar-theme-border) 12%, transparent) 1px,
|
|
433
|
+
transparent 1px,
|
|
434
|
+
transparent 8px
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.t-doc__sidebar .sidebar-group-label,
|
|
439
|
+
.t-doc__sidebar .sidebar-item,
|
|
440
|
+
.references-layout .reference-layout__content .section-header {
|
|
441
|
+
font-family: var(--scalar-font-code);
|
|
442
|
+
text-transform: uppercase;
|
|
443
|
+
letter-spacing: 0.06em;
|
|
444
|
+
}
|
|
445
|
+
`;
|
|
446
|
+
}
|
|
447
|
+
function inferApiReferenceForeground(theme, background) {
|
|
448
|
+
if (looksDarkTheme(theme, background)) return "#f5f5f4";
|
|
449
|
+
return "#1b1b1b";
|
|
450
|
+
}
|
|
451
|
+
function resolveApiReferenceRadius(theme) {
|
|
452
|
+
if (theme?.ui?.radius) return theme.ui.radius;
|
|
453
|
+
const name = theme?.name?.toLowerCase() ?? "";
|
|
454
|
+
if (name.includes("pixel-border") || name.includes("darksharp")) return "0px";
|
|
455
|
+
return "var(--radius, 0.75rem)";
|
|
456
|
+
}
|
|
457
|
+
function inferContrastingForeground(value, theme, background) {
|
|
458
|
+
const rgb = parseCssColor(value);
|
|
459
|
+
if (rgb) return relativeLuminance(rgb) > .58 ? "#0b0b0b" : "#ffffff";
|
|
460
|
+
if (theme?.name?.includes("pixel-border")) return "#0b0b0b";
|
|
461
|
+
if (looksDarkTheme(theme, background)) return "#0b0b0b";
|
|
462
|
+
return "#ffffff";
|
|
463
|
+
}
|
|
464
|
+
function looksDarkTheme(theme, background) {
|
|
465
|
+
const name = theme?.name?.toLowerCase() ?? "";
|
|
466
|
+
if (name.includes("pixel-border") || name.includes("darksharp")) return true;
|
|
467
|
+
const rgb = parseCssColor(background);
|
|
468
|
+
if (!rgb) return false;
|
|
469
|
+
return relativeLuminance(rgb) < .45;
|
|
470
|
+
}
|
|
471
|
+
function parseCssColor(value) {
|
|
472
|
+
const normalized = value.trim().toLowerCase();
|
|
473
|
+
if (normalized.startsWith("#")) return parseHexColor(normalized);
|
|
474
|
+
const rgbMatch = normalized.match(/^rgba?\((.+)\)$/);
|
|
475
|
+
if (rgbMatch) {
|
|
476
|
+
const [r, g, b] = rgbMatch[1].split(",").slice(0, 3).map((part) => Number.parseFloat(part.trim()));
|
|
477
|
+
if ([
|
|
478
|
+
r,
|
|
479
|
+
g,
|
|
480
|
+
b
|
|
481
|
+
].every((channel) => Number.isFinite(channel))) return [
|
|
482
|
+
r,
|
|
483
|
+
g,
|
|
484
|
+
b
|
|
485
|
+
];
|
|
486
|
+
}
|
|
487
|
+
const hslMatch = normalized.match(/^hsla?\((.+)\)$/);
|
|
488
|
+
if (hslMatch) {
|
|
489
|
+
const [h, s, l] = hslMatch[1].split(/[\s,/]+/).filter(Boolean).slice(0, 3);
|
|
490
|
+
const hue = Number.parseFloat(h);
|
|
491
|
+
const saturation = Number.parseFloat(s.replace("%", ""));
|
|
492
|
+
const lightness = Number.parseFloat(l.replace("%", ""));
|
|
493
|
+
if ([
|
|
494
|
+
hue,
|
|
495
|
+
saturation,
|
|
496
|
+
lightness
|
|
497
|
+
].every((channel) => Number.isFinite(channel))) return hslToRgb(hue, saturation / 100, lightness / 100);
|
|
498
|
+
}
|
|
499
|
+
const oklchMatch = normalized.match(/^oklch\((.+)\)$/);
|
|
500
|
+
if (oklchMatch) {
|
|
501
|
+
const [l, c, h] = oklchMatch[1].split(/[\s/]+/).filter(Boolean);
|
|
502
|
+
const lightness = Number.parseFloat(l);
|
|
503
|
+
const chroma = Number.parseFloat(c);
|
|
504
|
+
const hue = Number.parseFloat(h);
|
|
505
|
+
if ([
|
|
506
|
+
lightness,
|
|
507
|
+
chroma,
|
|
508
|
+
hue
|
|
509
|
+
].every((channel) => Number.isFinite(channel))) return oklchToRgb(lightness, chroma, hue);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function oklchToRgb(l, c, h) {
|
|
513
|
+
const hue = h * Math.PI / 180;
|
|
514
|
+
const a = Math.cos(hue) * c;
|
|
515
|
+
const b = Math.sin(hue) * c;
|
|
516
|
+
const l_ = l + .3963377774 * a + .2158037573 * b;
|
|
517
|
+
const m_ = l - .1055613458 * a - .0638541728 * b;
|
|
518
|
+
const s_ = l - .0894841775 * a - 1.291485548 * b;
|
|
519
|
+
const l3 = l_ ** 3;
|
|
520
|
+
const m3 = m_ ** 3;
|
|
521
|
+
const s3 = s_ ** 3;
|
|
522
|
+
const linearR = 4.0767416621 * l3 - 3.3077115913 * m3 + .2309699292 * s3;
|
|
523
|
+
const linearG = -1.2684380046 * l3 + 2.6097574011 * m3 - .3413193965 * s3;
|
|
524
|
+
const linearB = -.0041960863 * l3 - .7034186147 * m3 + 1.707614701 * s3;
|
|
525
|
+
return [
|
|
526
|
+
srgbFromLinear(linearR),
|
|
527
|
+
srgbFromLinear(linearG),
|
|
528
|
+
srgbFromLinear(linearB)
|
|
529
|
+
];
|
|
530
|
+
}
|
|
531
|
+
function srgbFromLinear(value) {
|
|
532
|
+
const normalized = value <= .0031308 ? value * 12.92 : 1.055 * value ** (1 / 2.4) - .055;
|
|
533
|
+
return Math.max(0, Math.min(255, Math.round(normalized * 255)));
|
|
534
|
+
}
|
|
535
|
+
function parseHexColor(value) {
|
|
536
|
+
const raw = value.slice(1);
|
|
537
|
+
if (raw.length === 3) return [
|
|
538
|
+
Number.parseInt(raw[0] + raw[0], 16),
|
|
539
|
+
Number.parseInt(raw[1] + raw[1], 16),
|
|
540
|
+
Number.parseInt(raw[2] + raw[2], 16)
|
|
541
|
+
];
|
|
542
|
+
if (raw.length === 6 || raw.length === 8) return [
|
|
543
|
+
Number.parseInt(raw.slice(0, 2), 16),
|
|
544
|
+
Number.parseInt(raw.slice(2, 4), 16),
|
|
545
|
+
Number.parseInt(raw.slice(4, 6), 16)
|
|
546
|
+
];
|
|
547
|
+
}
|
|
548
|
+
function hslToRgb(h, s, l) {
|
|
549
|
+
const hue = (h % 360 + 360) % 360 / 360;
|
|
550
|
+
if (s === 0) {
|
|
551
|
+
const gray = Math.round(l * 255);
|
|
552
|
+
return [
|
|
553
|
+
gray,
|
|
554
|
+
gray,
|
|
555
|
+
gray
|
|
556
|
+
];
|
|
557
|
+
}
|
|
558
|
+
const q = l < .5 ? l * (1 + s) : l + s - l * s;
|
|
559
|
+
const p = 2 * l - q;
|
|
560
|
+
const convert = (channel) => {
|
|
561
|
+
let t = channel;
|
|
562
|
+
if (t < 0) t += 1;
|
|
563
|
+
if (t > 1) t -= 1;
|
|
564
|
+
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
565
|
+
if (t < 1 / 2) return q;
|
|
566
|
+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
567
|
+
return p;
|
|
568
|
+
};
|
|
569
|
+
return [
|
|
570
|
+
Math.round(convert(hue + 1 / 3) * 255),
|
|
571
|
+
Math.round(convert(hue) * 255),
|
|
572
|
+
Math.round(convert(hue - 1 / 3) * 255)
|
|
573
|
+
];
|
|
574
|
+
}
|
|
575
|
+
function relativeLuminance([r, g, b]) {
|
|
576
|
+
const normalize = (channel) => {
|
|
577
|
+
const value = channel / 255;
|
|
578
|
+
return value <= .03928 ? value / 12.92 : ((value + .055) / 1.055) ** 2.4;
|
|
579
|
+
};
|
|
580
|
+
return .2126 * normalize(r) + .7152 * normalize(g) + .0722 * normalize(b);
|
|
581
|
+
}
|
|
582
|
+
function buildApiReferenceOpenApiDocument(config, options) {
|
|
583
|
+
const apiReference = resolveApiReferenceConfig(config.apiReference);
|
|
584
|
+
const selectedVersion = resolveApiReferenceVersion(apiReference, options.version);
|
|
585
|
+
if (apiReference.versions.length > 0 && !selectedVersion) return buildUnavailableOpenApiDocument(config, `Unknown OpenAPI version \`${options.version}\`. Available versions: ${apiReference.versions.map((version) => version.id).join(", ")}.`);
|
|
586
|
+
const specUrl = selectedVersion?.specUrl ?? apiReference.specUrl;
|
|
587
|
+
const overlays = selectedVersion?.overlays ?? apiReference.overlays;
|
|
588
|
+
if (specUrl) {
|
|
589
|
+
if (isLocalSpecSource(specUrl)) try {
|
|
590
|
+
if (overlays.some((overlay) => !isLocalSpecSource(overlay))) throw new Error("Remote OpenAPI overlays require the async API reference builder.");
|
|
591
|
+
return normalizeConfiguredOpenApiDocument(readLocalOpenApiDocument(specUrl, options.rootDir, overlays), config, options.baseUrl, specUrl, selectedVersion);
|
|
592
|
+
} catch (error) {
|
|
593
|
+
return buildUnavailableOpenApiDocument(config, `Unable to load the configured OpenAPI document. ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
594
|
+
}
|
|
595
|
+
return buildUnavailableOpenApiDocument(config, `Remote OpenAPI specs require the async API reference builder. Use the framework route helper or buildApiReferenceOpenApiDocumentAsync().`);
|
|
596
|
+
}
|
|
597
|
+
const routes = buildApiReferenceRoutes(config, options);
|
|
598
|
+
return buildOpenApiDocumentFromRoutes(config, options.framework, routes, options.baseUrl);
|
|
599
|
+
}
|
|
600
|
+
async function buildApiReferenceOpenApiDocumentAsync(config, options) {
|
|
601
|
+
try {
|
|
602
|
+
return await loadApiReferenceOpenApiDocumentAsync(config, options);
|
|
603
|
+
} catch (error) {
|
|
604
|
+
return buildUnavailableOpenApiDocument(config, `Unable to load the configured OpenAPI document. ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Load the resolved OpenAPI document without replacing diagnostics with an unavailable schema.
|
|
609
|
+
* This is used by CLI health checks that need the original parsing, reference, and overlay error.
|
|
610
|
+
*/
|
|
611
|
+
async function loadApiReferenceOpenApiDocumentAsync(config, options) {
|
|
612
|
+
const apiReference = resolveApiReferenceConfig(config.apiReference);
|
|
613
|
+
const selectedVersion = resolveApiReferenceVersion(apiReference, options.version);
|
|
614
|
+
if (apiReference.versions.length > 0 && !selectedVersion) throw new Error(`Unknown OpenAPI version \`${options.version}\`. Available versions: ${apiReference.versions.map((version) => version.id).join(", ")}.`);
|
|
615
|
+
const specUrl = selectedVersion?.specUrl ?? apiReference.specUrl;
|
|
616
|
+
const overlays = selectedVersion?.overlays ?? apiReference.overlays;
|
|
617
|
+
if (!specUrl) return buildApiReferenceOpenApiDocument(config, options);
|
|
618
|
+
return normalizeConfiguredOpenApiDocument(isLocalSpecSource(specUrl) ? await readLocalOpenApiDocumentAsync(specUrl, options.rootDir, overlays, options.baseUrl) : await fetchRemoteOpenApiDocument(specUrl, options.baseUrl, overlays, options.rootDir), config, options.baseUrl, specUrl, selectedVersion);
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Build deterministic, operation-level source pages from every configured OpenAPI version.
|
|
622
|
+
*
|
|
623
|
+
* These pages are framework-neutral projections used by documentation search and other
|
|
624
|
+
* read-only knowledge surfaces. They never inherit MCP execution headers or credentials.
|
|
625
|
+
*/
|
|
626
|
+
async function buildApiReferenceOperationPagesAsync(config, options) {
|
|
627
|
+
const apiReference = resolveApiReferenceConfig(config.apiReference);
|
|
628
|
+
if (!apiReference.enabled) return [];
|
|
629
|
+
const versions = apiReference.versions.length > 0 ? apiReference.versions : [void 0];
|
|
630
|
+
return (await Promise.all(versions.map(async (version) => {
|
|
631
|
+
const model = buildNormalizedOpenApiModel(await buildApiReferenceOpenApiDocumentAsync(config, {
|
|
632
|
+
...options,
|
|
633
|
+
version: version?.id
|
|
634
|
+
}));
|
|
635
|
+
const basePath = `/${apiReference.path}${version ? `/${encodeURIComponent(version.id)}` : ""}`;
|
|
636
|
+
return model.operations.map((operation) => {
|
|
637
|
+
const url = `${basePath}/operations/${encodeURIComponent(operation.slug)}`;
|
|
638
|
+
const content = renderApiReferenceOperationMarkdown(operation, {
|
|
639
|
+
apiTitle: model.title,
|
|
640
|
+
apiVersion: version?.label ?? model.version,
|
|
641
|
+
specificationVersion: model.specificationVersion
|
|
642
|
+
});
|
|
643
|
+
const description = operation.summary ?? operation.description ?? operation.selector;
|
|
644
|
+
return {
|
|
645
|
+
title: operation.title,
|
|
646
|
+
url,
|
|
647
|
+
canonicalUrl: url,
|
|
648
|
+
content,
|
|
649
|
+
description,
|
|
650
|
+
rawContent: content,
|
|
651
|
+
agentContent: content,
|
|
652
|
+
agentRawContent: content,
|
|
653
|
+
type: "api",
|
|
654
|
+
...options.locale ? { locale: options.locale } : {},
|
|
655
|
+
...version?.id || model.version ? { version: version?.id ?? model.version } : {},
|
|
656
|
+
tags: Array.from(new Set([
|
|
657
|
+
"openapi",
|
|
658
|
+
operation.method.toLowerCase(),
|
|
659
|
+
...operation.tags
|
|
660
|
+
]))
|
|
661
|
+
};
|
|
662
|
+
});
|
|
663
|
+
}))).flat();
|
|
664
|
+
}
|
|
665
|
+
function normalizeApiReferenceMarkdownPath(value) {
|
|
666
|
+
const trimmed = value.trim().replace(/\.md$/i, "");
|
|
667
|
+
if (!trimmed) return "/";
|
|
668
|
+
return `/${trimmed.replace(/^\/+|\/+$/g, "")}`.replace(/\/{2,}/g, "/");
|
|
669
|
+
}
|
|
670
|
+
function isApiReferenceOperationPath(pathname, apiReferencePath) {
|
|
671
|
+
const basePath = `/${normalizePathSegment(apiReferencePath)}`;
|
|
672
|
+
if (!pathname.startsWith(`${basePath}/`)) return false;
|
|
673
|
+
const segments = pathname.slice(basePath.length + 1).split("/");
|
|
674
|
+
return segments.length === 2 && segments[0] === "operations" && Boolean(segments[1]) || segments.length === 3 && segments[1] === "operations" && Boolean(segments[0] && segments[2]);
|
|
675
|
+
}
|
|
676
|
+
function resolveApiReferenceOperationMarkdownPath(request, apiReferencePath, apiRoute = "/api/docs") {
|
|
677
|
+
if (request.method !== "GET" && request.method !== "HEAD") return null;
|
|
678
|
+
const url = new URL(request.url);
|
|
679
|
+
const pathname = normalizeApiReferenceMarkdownPath(url.pathname);
|
|
680
|
+
if (pathname === normalizeApiReferenceMarkdownPath(apiRoute) && url.searchParams.get("format")?.trim() === "markdown") {
|
|
681
|
+
const requestedPath = normalizeApiReferenceMarkdownPath(url.searchParams.get("path")?.trim() ?? "");
|
|
682
|
+
return isApiReferenceOperationPath(requestedPath, apiReferencePath) ? requestedPath : null;
|
|
683
|
+
}
|
|
684
|
+
if (url.pathname.toLowerCase().endsWith(".md")) return isApiReferenceOperationPath(pathname, apiReferencePath) ? pathname : null;
|
|
685
|
+
return (acceptsDocsMarkdown(request) || hasDocsMarkdownSignatureAgent(request) || detectDocsMarkdownAgentRequest(request).detected) && isApiReferenceOperationPath(pathname, apiReferencePath) ? pathname : null;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Resolve a public per-operation Markdown request using the same generated projection as search,
|
|
689
|
+
* Ask AI, and MCP. Returns `null` when the request is not an API operation Markdown route.
|
|
690
|
+
*/
|
|
691
|
+
async function createApiReferenceOperationMarkdownResponse(config, options) {
|
|
692
|
+
const apiReference = resolveApiReferenceConfig(config.apiReference);
|
|
693
|
+
if (!apiReference.enabled) return null;
|
|
694
|
+
const requestedPath = resolveApiReferenceOperationMarkdownPath(options.request, apiReference.path, options.apiRoute);
|
|
695
|
+
if (!requestedPath) return null;
|
|
696
|
+
const requestUrl = new URL(options.request.url);
|
|
697
|
+
const origin = options.origin ?? options.baseUrl ?? requestUrl.origin;
|
|
698
|
+
const operationPages = options.operationPages ? await (typeof options.operationPages === "function" ? options.operationPages() : options.operationPages) : await buildApiReferenceOperationPagesAsync(config, {
|
|
699
|
+
framework: options.framework,
|
|
700
|
+
rootDir: options.rootDir,
|
|
701
|
+
baseUrl: options.baseUrl ?? requestUrl.origin,
|
|
702
|
+
locale: options.locale
|
|
703
|
+
});
|
|
704
|
+
const page = operationPages.find((candidate) => normalizeApiReferenceMarkdownPath(candidate.url) === requestedPath);
|
|
705
|
+
const canonicalPath = page?.canonicalUrl ?? page?.url ?? requestedPath;
|
|
706
|
+
const canonicalUrl = new URL(canonicalPath, origin).toString();
|
|
707
|
+
const contentLocation = new URL(toDocsMarkdownUrl(page?.url ?? requestedPath), origin).toString();
|
|
708
|
+
const relativeRequestedPath = requestedPath.slice(`/${apiReference.path}`.length).replace(/^\/+/, "");
|
|
709
|
+
return createDocsMarkdownResponse({
|
|
710
|
+
request: options.request,
|
|
711
|
+
apiRoute: options.apiRoute,
|
|
712
|
+
document: page ? renderDocsMarkdownDocument(page, {
|
|
713
|
+
origin,
|
|
714
|
+
sitemap: options.sitemap,
|
|
715
|
+
okf: options.okf
|
|
716
|
+
}) : null,
|
|
717
|
+
entry: apiReference.path,
|
|
718
|
+
requestedPath: relativeRequestedPath,
|
|
719
|
+
origin,
|
|
720
|
+
locale: options.locale,
|
|
721
|
+
canonicalUrl,
|
|
722
|
+
contentLocation,
|
|
723
|
+
pages: [...options.pages ?? [], ...operationPages],
|
|
724
|
+
sitemap: options.sitemap
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
function renderApiReferenceOperationMarkdown(operation, metadata) {
|
|
728
|
+
const lines = [
|
|
729
|
+
`# ${operation.title}`,
|
|
730
|
+
"",
|
|
731
|
+
`\`${operation.method} ${operation.path}\``,
|
|
732
|
+
"",
|
|
733
|
+
`- Operation ID: \`${operation.operationId}\``,
|
|
734
|
+
`- API: ${metadata.apiTitle}`,
|
|
735
|
+
`- API version: ${metadata.apiVersion}`,
|
|
736
|
+
`- OpenAPI: ${metadata.specificationVersion}`
|
|
737
|
+
];
|
|
738
|
+
if (operation.tags.length > 0) lines.push(`- Tags: ${operation.tags.join(", ")}`);
|
|
739
|
+
if (operation.deprecated) lines.push("- Deprecated: yes");
|
|
740
|
+
if (operation.description && operation.description !== operation.title) lines.push("", operation.description);
|
|
741
|
+
if (operation.servers.length > 0) lines.push("", "## Servers", "", ...operation.servers.map((server) => `- ${server}`));
|
|
742
|
+
if (operation.parameters.length > 0) {
|
|
743
|
+
lines.push("", "## Parameters", "");
|
|
744
|
+
for (const parameter of operation.parameters) lines.push(renderApiReferenceParameter(parameter));
|
|
745
|
+
}
|
|
746
|
+
if (operation.requestBody) {
|
|
747
|
+
lines.push("", "## Request body", "");
|
|
748
|
+
if (operation.requestBody.description) lines.push(operation.requestBody.description, "");
|
|
749
|
+
lines.push(`Required: ${operation.requestBody.required ? "yes" : "no"}`);
|
|
750
|
+
appendApiReferenceMediaTypes(lines, operation.requestBody.content);
|
|
751
|
+
}
|
|
752
|
+
if (operation.responses.length > 0) {
|
|
753
|
+
lines.push("", "## Responses", "");
|
|
754
|
+
for (const response of operation.responses) {
|
|
755
|
+
lines.push(`### ${response.status}`, "", response.description ?? "No response description.");
|
|
756
|
+
appendApiReferenceMediaTypes(lines, response.content);
|
|
757
|
+
lines.push("");
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (operation.security.length > 0) {
|
|
761
|
+
lines.push("", "## Security", "");
|
|
762
|
+
for (const requirement of operation.security) {
|
|
763
|
+
const schemes = Object.entries(requirement).map(([name, scopes]) => scopes.length > 0 ? `${name} (${scopes.join(", ")})` : name);
|
|
764
|
+
lines.push(`- ${schemes.join(" and ")}`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return `${lines.join("\n").trim()}\n`;
|
|
768
|
+
}
|
|
769
|
+
function renderApiReferenceParameter(parameter) {
|
|
770
|
+
const details = [parameter.in, parameter.required ? "required" : "optional"];
|
|
771
|
+
const schema = renderApiReferenceSchema(parameter.schema);
|
|
772
|
+
if (schema) details.push(schema);
|
|
773
|
+
return `- \`${parameter.name}\` (${details.join(", ")})${parameter.description ? ` — ${parameter.description}` : ""}`;
|
|
774
|
+
}
|
|
775
|
+
function appendApiReferenceMediaTypes(lines, content) {
|
|
776
|
+
for (const media of content) {
|
|
777
|
+
lines.push("", `- Content type: \`${media.mediaType}\``);
|
|
778
|
+
const schema = renderApiReferenceSchema(media.schema);
|
|
779
|
+
if (schema) lines.push(` Schema: \`${schema}\``);
|
|
780
|
+
for (const example of media.examples) {
|
|
781
|
+
const value = renderApiReferenceSchema(example.value);
|
|
782
|
+
lines.push(` Example ${example.name}: ${value ? `\`${value}\`` : "available"}`);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function renderApiReferenceSchema(value) {
|
|
787
|
+
if (value === void 0) return void 0;
|
|
788
|
+
if (typeof value === "string") return value;
|
|
789
|
+
if (typeof value === "number" || typeof value === "boolean" || value === null) return String(value);
|
|
790
|
+
try {
|
|
791
|
+
const serialized = JSON.stringify(value);
|
|
792
|
+
return serialized.length > 500 ? `${serialized.slice(0, 497)}...` : serialized;
|
|
793
|
+
} catch {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
function buildOpenApiDocumentFromRoutes(config, framework, routes, baseUrl) {
|
|
798
|
+
const tags = Array.from(new Set(routes.map((route) => route.tag))).map((name) => ({
|
|
799
|
+
name,
|
|
800
|
+
description: `${name} endpoints`
|
|
801
|
+
}));
|
|
802
|
+
return {
|
|
803
|
+
openapi: "3.1.0",
|
|
804
|
+
info: {
|
|
805
|
+
title: "API Reference",
|
|
806
|
+
description: config.metadata?.description ?? `Generated API reference for ${framework}.`,
|
|
807
|
+
version: "0.0.0"
|
|
808
|
+
},
|
|
809
|
+
servers: [{ url: baseUrl ?? "/" }],
|
|
810
|
+
tags,
|
|
811
|
+
paths: buildOpenApiPaths(routes)
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
function buildApiReferenceHtmlDocument(config, options) {
|
|
815
|
+
return buildApiReferenceHtmlDocumentFromDocument(config, options, buildApiReferenceOpenApiDocument(config, options));
|
|
816
|
+
}
|
|
817
|
+
async function buildApiReferenceHtmlDocumentAsync(config, options) {
|
|
818
|
+
return buildApiReferenceHtmlDocumentFromDocument(config, options, await buildApiReferenceOpenApiDocumentAsync(config, options));
|
|
819
|
+
}
|
|
820
|
+
function buildApiReferenceHtmlDocumentFromDocument(config, options, document) {
|
|
821
|
+
const apiReference = resolveApiReferenceConfig(config.apiReference);
|
|
822
|
+
const version = resolveApiReferenceVersion(apiReference, options.version);
|
|
823
|
+
const basePath = `/${apiReference.path}${version ? `/${encodeURIComponent(version.id)}` : ""}`;
|
|
824
|
+
if (resolveApiReferenceRenderer(config.apiReference, options.framework) === "farming-labs") {
|
|
825
|
+
const model = buildNormalizedOpenApiModel(document);
|
|
826
|
+
const info = document.info && typeof document.info === "object" && !Array.isArray(document.info) ? document.info : void 0;
|
|
827
|
+
const title = options.title ?? model.title;
|
|
828
|
+
const openApiUrl = version ? appendUrlSearchParam(DEFAULT_API_REFERENCE_OPENAPI_ROUTE, "version", version.id) : DEFAULT_API_REFERENCE_OPENAPI_ROUTE;
|
|
829
|
+
return renderNativeApiReferenceHtml({
|
|
830
|
+
model,
|
|
831
|
+
pageTitle: buildApiReferencePageTitle(config, version ? `${title} – ${version.label}` : title),
|
|
832
|
+
title: version ? `${title} – ${version.label}` : title,
|
|
833
|
+
description: typeof info?.description === "string" && info.description.trim() ? info.description : config.metadata?.description,
|
|
834
|
+
basePath,
|
|
835
|
+
openApiUrl,
|
|
836
|
+
activeVersion: version?.id,
|
|
837
|
+
versions: apiReference.versions.map((entry) => ({
|
|
838
|
+
id: entry.id,
|
|
839
|
+
label: entry.label,
|
|
840
|
+
href: `/${apiReference.path}/${encodeURIComponent(entry.id)}`,
|
|
841
|
+
active: entry.id === version?.id
|
|
842
|
+
})),
|
|
843
|
+
theme: resolveTheme(config)
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
const title = options.title ?? "API Reference";
|
|
847
|
+
return getHtmlDocument({
|
|
848
|
+
pageTitle: buildApiReferencePageTitle(config, title),
|
|
849
|
+
title,
|
|
850
|
+
content: () => document,
|
|
851
|
+
theme: "deepSpace",
|
|
852
|
+
layout: "modern",
|
|
853
|
+
customCss: buildApiReferenceScalarCss(config),
|
|
854
|
+
pathRouting: { basePath },
|
|
855
|
+
showSidebar: true,
|
|
856
|
+
defaultOpenFirstTag: true,
|
|
857
|
+
tagsSorter: "alpha",
|
|
858
|
+
operationsSorter: "alpha",
|
|
859
|
+
operationTitleSource: "summary",
|
|
860
|
+
defaultHttpClient: {
|
|
861
|
+
targetKey: "shell",
|
|
862
|
+
clientKey: "curl"
|
|
863
|
+
},
|
|
864
|
+
documentDownloadType: "json"
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
async function fetchRemoteOpenApiDocument(specUrl, baseUrl, overlays = [], rootDir = process.cwd()) {
|
|
868
|
+
let url;
|
|
869
|
+
try {
|
|
870
|
+
url = baseUrl ? new URL(specUrl, baseUrl) : new URL(specUrl);
|
|
871
|
+
} catch {
|
|
872
|
+
throw new Error(baseUrl ? "`apiReference.specUrl` must be an absolute URL or a request-relative path." : "`apiReference.specUrl` must be an absolute URL.");
|
|
873
|
+
}
|
|
874
|
+
const entry = await fetchOpenApiReferenceDocument(url, true);
|
|
875
|
+
return resolveOpenApiReferences(await applyConfiguredOpenApiOverlaysAsync(entry.document, overlays, rootDir, baseUrl), {
|
|
876
|
+
sourceUri: entry.uri,
|
|
877
|
+
loadDocument: async (referenceUrl) => {
|
|
878
|
+
if (referenceUrl.protocol !== "http:" && referenceUrl.protocol !== "https:") throw new Error(`Remote OpenAPI documents cannot load non-HTTP references: ${referenceUrl.href}`);
|
|
879
|
+
return fetchOpenApiReferenceDocument(referenceUrl, false);
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
async function fetchOpenApiReferenceDocument(url, validateContract) {
|
|
884
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("`apiReference.specUrl` must use HTTP or HTTPS for remote documents.");
|
|
885
|
+
const abortController = new AbortController();
|
|
886
|
+
const timeout = setTimeout(() => abortController.abort(), OPENAPI_SPEC_FETCH_TIMEOUT_MS);
|
|
887
|
+
let response;
|
|
888
|
+
let body;
|
|
889
|
+
try {
|
|
890
|
+
response = await fetch(url, {
|
|
891
|
+
headers: { accept: "application/json, application/yaml, application/x-yaml, text/yaml, text/x-yaml" },
|
|
892
|
+
redirect: "follow",
|
|
893
|
+
signal: abortController.signal
|
|
894
|
+
});
|
|
895
|
+
if (!response.ok) throw new Error(`Received ${response.status} ${response.statusText}`.trim());
|
|
896
|
+
if (response.url) {
|
|
897
|
+
const responseUrl = new URL(response.url);
|
|
898
|
+
if (responseUrl.protocol !== "http:" && responseUrl.protocol !== "https:") throw new Error("The remote OpenAPI endpoint redirected to a non-HTTP URL.");
|
|
899
|
+
}
|
|
900
|
+
body = await readOpenApiResponseBody(response);
|
|
901
|
+
} catch (error) {
|
|
902
|
+
if (abortController.signal.aborted) throw new Error(`The remote OpenAPI endpoint did not respond within ${OPENAPI_SPEC_FETCH_TIMEOUT_MS / 1e3} seconds.`);
|
|
903
|
+
throw error;
|
|
904
|
+
} finally {
|
|
905
|
+
clearTimeout(timeout);
|
|
906
|
+
}
|
|
907
|
+
if (!body.trim()) throw new Error("The remote endpoint returned an empty response.");
|
|
908
|
+
const sourceUri = response.url || url.href;
|
|
909
|
+
return {
|
|
910
|
+
uri: sourceUri,
|
|
911
|
+
document: parseOpenApiDocument(body, sourceUri, response.headers.get("content-type"), "remote", validateContract)
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
async function readOpenApiResponseBody(response) {
|
|
915
|
+
const contentLength = response.headers.get("content-length");
|
|
916
|
+
if (contentLength) {
|
|
917
|
+
const declaredBytes = Number(contentLength);
|
|
918
|
+
if (Number.isFinite(declaredBytes) && declaredBytes > OPENAPI_SPEC_MAX_BYTES) throw new Error(`The remote OpenAPI document exceeds the ${formatByteLimit(OPENAPI_SPEC_MAX_BYTES)} size limit.`);
|
|
919
|
+
}
|
|
920
|
+
if (!response.body) return "";
|
|
921
|
+
const reader = response.body.getReader();
|
|
922
|
+
const decoder = new TextDecoder();
|
|
923
|
+
let receivedBytes = 0;
|
|
924
|
+
let body = "";
|
|
925
|
+
try {
|
|
926
|
+
while (true) {
|
|
927
|
+
const { done, value } = await reader.read();
|
|
928
|
+
if (done) break;
|
|
929
|
+
receivedBytes += value.byteLength;
|
|
930
|
+
if (receivedBytes > OPENAPI_SPEC_MAX_BYTES) {
|
|
931
|
+
await reader.cancel();
|
|
932
|
+
throw new Error(`The remote OpenAPI document exceeds the ${formatByteLimit(OPENAPI_SPEC_MAX_BYTES)} size limit.`);
|
|
933
|
+
}
|
|
934
|
+
body += decoder.decode(value, { stream: true });
|
|
935
|
+
}
|
|
936
|
+
body += decoder.decode();
|
|
937
|
+
return body;
|
|
938
|
+
} finally {
|
|
939
|
+
reader.releaseLock();
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
function formatByteLimit(bytes) {
|
|
943
|
+
return `${bytes / (1024 * 1024)} MiB`;
|
|
944
|
+
}
|
|
945
|
+
function readLocalOpenApiDocument(specUrl, rootDir = process.cwd(), overlays = []) {
|
|
946
|
+
const filePath = resolveLocalOpenApiFilePath(specUrl, rootDir);
|
|
947
|
+
const document = readOpenApiFileDocument(filePath, true);
|
|
948
|
+
const trustedRoot = specUrl.startsWith("file:") ? void 0 : realpathSync(resolve(rootDir));
|
|
949
|
+
return resolveOpenApiReferencesSync(applyConfiguredOpenApiOverlaysSync(document, overlays, rootDir), {
|
|
950
|
+
sourceUri: pathToFileURL(filePath).href,
|
|
951
|
+
loadDocument: (referenceUrl) => {
|
|
952
|
+
if (referenceUrl.protocol === "http:" || referenceUrl.protocol === "https:") throw new Error("Remote OpenAPI references require the async API reference builder.");
|
|
953
|
+
return readOpenApiFileReference(referenceUrl, trustedRoot);
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
async function readLocalOpenApiDocumentAsync(specUrl, rootDir = process.cwd(), overlays = [], baseUrl) {
|
|
958
|
+
const filePath = resolveLocalOpenApiFilePath(specUrl, rootDir);
|
|
959
|
+
const document = await readOpenApiFileDocumentAsync(filePath, true);
|
|
960
|
+
const trustedRoot = specUrl.startsWith("file:") ? void 0 : realpathSync(resolve(rootDir));
|
|
961
|
+
return resolveOpenApiReferences(await applyConfiguredOpenApiOverlaysAsync(document, overlays, rootDir, baseUrl), {
|
|
962
|
+
sourceUri: pathToFileURL(filePath).href,
|
|
963
|
+
loadDocument: async (referenceUrl, fromUrl) => {
|
|
964
|
+
if (referenceUrl.protocol === "http:" || referenceUrl.protocol === "https:") return fetchOpenApiReferenceDocument(referenceUrl, false);
|
|
965
|
+
if (fromUrl.protocol === "http:" || fromUrl.protocol === "https:") throw new Error(`Remote OpenAPI documents cannot load local file references: ${referenceUrl.href}`);
|
|
966
|
+
return readOpenApiFileReferenceAsync(referenceUrl, trustedRoot);
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
function applyConfiguredOpenApiOverlaysSync(document, overlays, rootDir) {
|
|
971
|
+
if (overlays.length === 0) return document;
|
|
972
|
+
const result = applyOpenApiOverlayDocuments(document, overlays.map((source) => {
|
|
973
|
+
if (!isLocalSpecSource(source)) throw new Error("Remote OpenAPI overlays require the async API reference builder.");
|
|
974
|
+
return {
|
|
975
|
+
source,
|
|
976
|
+
document: readOpenApiFileDocument(resolveLocalOpenApiFilePath(source, rootDir), false)
|
|
977
|
+
};
|
|
978
|
+
}));
|
|
979
|
+
assertValidOpenApiContract(result);
|
|
980
|
+
return result;
|
|
981
|
+
}
|
|
982
|
+
async function applyConfiguredOpenApiOverlaysAsync(document, overlays, rootDir, baseUrl) {
|
|
983
|
+
if (overlays.length === 0) return document;
|
|
984
|
+
const result = applyOpenApiOverlayDocuments(document, await Promise.all(overlays.map(async (source) => {
|
|
985
|
+
if (isLocalSpecSource(source)) return {
|
|
986
|
+
source,
|
|
987
|
+
document: await readOpenApiFileDocumentAsync(resolveLocalOpenApiFilePath(source, rootDir), false)
|
|
988
|
+
};
|
|
989
|
+
let url;
|
|
990
|
+
try {
|
|
991
|
+
url = baseUrl ? new URL(source, baseUrl) : new URL(source);
|
|
992
|
+
} catch {
|
|
993
|
+
throw new Error(baseUrl ? `OpenAPI overlay \`${source}\` must be an absolute URL or a request-relative path.` : `OpenAPI overlay \`${source}\` must be an absolute URL.`);
|
|
994
|
+
}
|
|
995
|
+
return {
|
|
996
|
+
source,
|
|
997
|
+
document: (await fetchOpenApiReferenceDocument(url, false)).document
|
|
998
|
+
};
|
|
999
|
+
})));
|
|
1000
|
+
assertValidOpenApiContract(result);
|
|
1001
|
+
return result;
|
|
1002
|
+
}
|
|
1003
|
+
function readOpenApiFileReference(url, trustedRoot) {
|
|
1004
|
+
if (url.protocol !== "file:") throw new Error(`Local OpenAPI documents cannot load this reference: ${url.href}`);
|
|
1005
|
+
const filePath = resolveReferencedOpenApiFilePath(url, trustedRoot);
|
|
1006
|
+
return {
|
|
1007
|
+
uri: pathToFileURL(filePath).href,
|
|
1008
|
+
document: readOpenApiFileDocument(filePath, false)
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
async function readOpenApiFileReferenceAsync(url, trustedRoot) {
|
|
1012
|
+
if (url.protocol !== "file:") throw new Error(`Local OpenAPI documents cannot load this reference: ${url.href}`);
|
|
1013
|
+
const filePath = resolveReferencedOpenApiFilePath(url, trustedRoot);
|
|
1014
|
+
return {
|
|
1015
|
+
uri: pathToFileURL(filePath).href,
|
|
1016
|
+
document: await readOpenApiFileDocumentAsync(filePath, false)
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
function readOpenApiFileDocument(filePath, validateContract) {
|
|
1020
|
+
assertOpenApiFileSize(filePath);
|
|
1021
|
+
const body = readFileSync(filePath, "utf-8");
|
|
1022
|
+
if (!body.trim()) throw new Error(`The local OpenAPI file is empty: ${filePath}`);
|
|
1023
|
+
return parseOpenApiDocument(body, filePath, void 0, "local", validateContract);
|
|
1024
|
+
}
|
|
1025
|
+
async function readOpenApiFileDocumentAsync(filePath, validateContract) {
|
|
1026
|
+
assertOpenApiFileSize(filePath);
|
|
1027
|
+
const body = await readFile(filePath, "utf-8");
|
|
1028
|
+
if (!body.trim()) throw new Error(`The local OpenAPI file is empty: ${filePath}`);
|
|
1029
|
+
return parseOpenApiDocument(body, filePath, void 0, "local", validateContract);
|
|
1030
|
+
}
|
|
1031
|
+
function assertOpenApiFileSize(filePath) {
|
|
1032
|
+
if (statSync(filePath).size > OPENAPI_SPEC_MAX_BYTES) throw new Error(`The local OpenAPI document exceeds the ${formatByteLimit(OPENAPI_SPEC_MAX_BYTES)} size limit: ${filePath}`);
|
|
1033
|
+
}
|
|
1034
|
+
function resolveLocalOpenApiFilePath(specUrl, rootDir) {
|
|
1035
|
+
if (specUrl.startsWith("file:")) return fileURLToPath(new URL(specUrl));
|
|
1036
|
+
const resolvedRoot = resolve(rootDir);
|
|
1037
|
+
return resolveContainedOpenApiFilePath(resolve(resolvedRoot, specUrl), realpathSync(resolvedRoot));
|
|
1038
|
+
}
|
|
1039
|
+
function resolveReferencedOpenApiFilePath(url, trustedRoot) {
|
|
1040
|
+
const filePath = fileURLToPath(url);
|
|
1041
|
+
return trustedRoot ? resolveContainedOpenApiFilePath(filePath, trustedRoot) : realpathSync(filePath);
|
|
1042
|
+
}
|
|
1043
|
+
function resolveContainedOpenApiFilePath(filePath, realRoot) {
|
|
1044
|
+
const realFilePath = realpathSync(filePath);
|
|
1045
|
+
const relativePath = relative(realRoot, realFilePath);
|
|
1046
|
+
if (relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) throw new Error("Project-relative OpenAPI files must stay inside the project root. Use an explicit `file:` URL for an external local file.");
|
|
1047
|
+
return realFilePath;
|
|
1048
|
+
}
|
|
1049
|
+
function parseOpenApiDocument(body, source, contentType, sourceKind, validateContract = true) {
|
|
1050
|
+
const extension = getOpenApiSourceExtension(source);
|
|
1051
|
+
const normalizedContentType = contentType?.split(";", 1)[0]?.trim().toLowerCase();
|
|
1052
|
+
const format = normalizedContentType?.includes("yaml") || extension === ".yaml" || extension === ".yml" ? "yaml" : normalizedContentType?.includes("json") || extension === ".json" ? "json" : "auto";
|
|
1053
|
+
let parsed;
|
|
1054
|
+
try {
|
|
1055
|
+
if (format === "yaml") parsed = parse(body);
|
|
1056
|
+
else if (format === "json") parsed = JSON.parse(body);
|
|
1057
|
+
else try {
|
|
1058
|
+
parsed = JSON.parse(body);
|
|
1059
|
+
} catch {
|
|
1060
|
+
parsed = parse(body);
|
|
1061
|
+
}
|
|
1062
|
+
} catch {
|
|
1063
|
+
const location = sourceKind === "remote" ? "remote endpoint" : "local file";
|
|
1064
|
+
const verb = sourceKind === "remote" ? "return" : "contain";
|
|
1065
|
+
const expectedFormat = format === "auto" ? "JSON or YAML" : format.toUpperCase();
|
|
1066
|
+
throw new Error(`The ${location} did not ${verb} valid ${expectedFormat}.`);
|
|
1067
|
+
}
|
|
1068
|
+
if (validateContract && (!parsed || typeof parsed !== "object" || Array.isArray(parsed))) throw new Error("The configured source contains a value instead of an OpenAPI object.");
|
|
1069
|
+
if (validateContract) assertValidOpenApiContract(parsed);
|
|
1070
|
+
return parsed;
|
|
1071
|
+
}
|
|
1072
|
+
function getOpenApiSourceExtension(source) {
|
|
1073
|
+
try {
|
|
1074
|
+
return extname(new URL(source, "https://farming-labs.invalid").pathname).toLowerCase();
|
|
1075
|
+
} catch {
|
|
1076
|
+
return extname(source).toLowerCase();
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
function normalizeConfiguredOpenApiDocument(document, config, baseUrl, specUrl, version) {
|
|
1080
|
+
const info = document.info && typeof document.info === "object" && !Array.isArray(document.info) ? document.info : {};
|
|
1081
|
+
const model = buildNormalizedOpenApiModel(document);
|
|
1082
|
+
const normalizedPaths = normalizeRemoteOpenApiPaths(document.paths);
|
|
1083
|
+
const normalizedTags = normalizeRemoteOpenApiTags(document.tags, normalizedPaths, model.tags);
|
|
1084
|
+
const normalizedServers = isRequestRelativeSpecUrl(specUrl) ? [{ url: baseUrl ?? "/" }] : document.servers;
|
|
1085
|
+
return {
|
|
1086
|
+
...document,
|
|
1087
|
+
info: {
|
|
1088
|
+
title: "API Reference",
|
|
1089
|
+
version: "0.0.0",
|
|
1090
|
+
...info,
|
|
1091
|
+
description: typeof info.description === "string" && info.description.trim() ? info.description : config.metadata?.description
|
|
1092
|
+
},
|
|
1093
|
+
servers: normalizedServers,
|
|
1094
|
+
paths: normalizedPaths,
|
|
1095
|
+
tags: normalizedTags,
|
|
1096
|
+
...version ? { "x-farming-labs-api-version": {
|
|
1097
|
+
id: version.id,
|
|
1098
|
+
label: version.label,
|
|
1099
|
+
default: version.default
|
|
1100
|
+
} } : {}
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
function normalizeRemoteOpenApiPaths(value) {
|
|
1104
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
1105
|
+
const paths = value;
|
|
1106
|
+
const normalized = {};
|
|
1107
|
+
for (const [routePath, pathItem] of Object.entries(paths)) {
|
|
1108
|
+
if (!pathItem || typeof pathItem !== "object" || Array.isArray(pathItem)) continue;
|
|
1109
|
+
const normalizedPathItem = { ...pathItem };
|
|
1110
|
+
for (const method of [
|
|
1111
|
+
"get",
|
|
1112
|
+
"post",
|
|
1113
|
+
"put",
|
|
1114
|
+
"patch",
|
|
1115
|
+
"delete",
|
|
1116
|
+
"options",
|
|
1117
|
+
"head",
|
|
1118
|
+
"trace"
|
|
1119
|
+
]) {
|
|
1120
|
+
const operation = normalizedPathItem[method];
|
|
1121
|
+
if (!operation || typeof operation !== "object" || Array.isArray(operation)) continue;
|
|
1122
|
+
const operationRecord = operation;
|
|
1123
|
+
const tags = Array.isArray(operationRecord.tags) ? operationRecord.tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0) : [];
|
|
1124
|
+
normalizedPathItem[method] = {
|
|
1125
|
+
...operationRecord,
|
|
1126
|
+
tags: tags.length > 0 ? tags : [inferRemoteOpenApiTag(routePath)]
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
normalized[routePath] = normalizedPathItem;
|
|
1130
|
+
}
|
|
1131
|
+
return normalized;
|
|
1132
|
+
}
|
|
1133
|
+
function inferRemoteOpenApiTag(routePath) {
|
|
1134
|
+
return humanizeSegment(routePath.split("/").filter(Boolean).find((value) => !value.startsWith("{")) ?? "general");
|
|
1135
|
+
}
|
|
1136
|
+
function normalizeRemoteOpenApiTags(value, paths, modelTags = []) {
|
|
1137
|
+
const existingTags = Array.isArray(value) ? value.filter((tag) => !!tag && typeof tag === "object") : [];
|
|
1138
|
+
const tagsByName = /* @__PURE__ */ new Map();
|
|
1139
|
+
for (const tag of existingTags) {
|
|
1140
|
+
const name = typeof tag.name === "string" && tag.name.trim() ? tag.name.trim() : void 0;
|
|
1141
|
+
if (!name) continue;
|
|
1142
|
+
tagsByName.set(name, tag);
|
|
1143
|
+
}
|
|
1144
|
+
for (const name of modelTags) if (!tagsByName.has(name)) tagsByName.set(name, {
|
|
1145
|
+
name,
|
|
1146
|
+
description: `${name} endpoints`
|
|
1147
|
+
});
|
|
1148
|
+
if (paths) for (const pathItem of Object.values(paths)) for (const method of [
|
|
1149
|
+
"get",
|
|
1150
|
+
"post",
|
|
1151
|
+
"put",
|
|
1152
|
+
"patch",
|
|
1153
|
+
"delete",
|
|
1154
|
+
"options",
|
|
1155
|
+
"head",
|
|
1156
|
+
"trace"
|
|
1157
|
+
]) {
|
|
1158
|
+
const operation = pathItem[method];
|
|
1159
|
+
if (!operation || typeof operation !== "object" || Array.isArray(operation)) continue;
|
|
1160
|
+
const tags = Array.isArray(operation.tags) ? operation.tags : [];
|
|
1161
|
+
for (const tag of tags) {
|
|
1162
|
+
if (typeof tag !== "string" || !tag.trim()) continue;
|
|
1163
|
+
if (!tagsByName.has(tag)) tagsByName.set(tag, {
|
|
1164
|
+
name: tag,
|
|
1165
|
+
description: `${tag} endpoints`
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
if (tagsByName.size === 0) return [{
|
|
1170
|
+
name: "General",
|
|
1171
|
+
description: "General endpoints"
|
|
1172
|
+
}];
|
|
1173
|
+
return Array.from(tagsByName.values());
|
|
1174
|
+
}
|
|
1175
|
+
function buildUnavailableOpenApiDocument(config, description) {
|
|
1176
|
+
return {
|
|
1177
|
+
openapi: "3.1.0",
|
|
1178
|
+
info: {
|
|
1179
|
+
title: "API Reference",
|
|
1180
|
+
description,
|
|
1181
|
+
version: "0.0.0"
|
|
1182
|
+
},
|
|
1183
|
+
servers: [{ url: "/" }],
|
|
1184
|
+
tags: [{
|
|
1185
|
+
name: "Unavailable",
|
|
1186
|
+
description: config.metadata?.description ?? "OpenAPI spec could not be loaded."
|
|
1187
|
+
}],
|
|
1188
|
+
paths: {}
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
function buildApiReferenceRoutes(config, options) {
|
|
1192
|
+
const apiReference = resolveApiReferenceConfig(config.apiReference);
|
|
1193
|
+
if (!apiReference.enabled) return [];
|
|
1194
|
+
const rootDir = options.rootDir ?? process.cwd();
|
|
1195
|
+
switch (options.framework) {
|
|
1196
|
+
case "next": return buildFileConventionRoutes({
|
|
1197
|
+
rootDir,
|
|
1198
|
+
sourceDir: resolveRootedDir(rootDir, apiReference.routeRoot, getNextAppDir(rootDir)),
|
|
1199
|
+
routePathBase: toRouteBase(apiReference.routeRoot, getNextAppDir(rootDir)),
|
|
1200
|
+
isRouteFile: (name) => NEXT_ROUTE_FILE_RE.test(name),
|
|
1201
|
+
toRouteSegments: (relativeFile) => relativeFile.split("/").slice(0, -1).filter(Boolean),
|
|
1202
|
+
exclude: apiReference.exclude
|
|
1203
|
+
});
|
|
1204
|
+
case "farmjs": return buildFileConventionRoutes({
|
|
1205
|
+
rootDir,
|
|
1206
|
+
sourceDir: resolveRootedDir(rootDir, apiReference.routeRoot, getFarmAppDir(rootDir)),
|
|
1207
|
+
routePathBase: toRouteBase(apiReference.routeRoot, getFarmAppDir(rootDir)),
|
|
1208
|
+
isRouteFile: (name) => NEXT_ROUTE_FILE_RE.test(name),
|
|
1209
|
+
toRouteSegments: (relativeFile) => relativeFile.split("/").slice(0, -1).filter(Boolean),
|
|
1210
|
+
exclude: apiReference.exclude
|
|
1211
|
+
});
|
|
1212
|
+
case "sveltekit": return buildFileConventionRoutes({
|
|
1213
|
+
rootDir,
|
|
1214
|
+
sourceDir: resolveRootedDir(rootDir, apiReference.routeRoot, "src/routes"),
|
|
1215
|
+
routePathBase: toRouteBase(apiReference.routeRoot, "src/routes"),
|
|
1216
|
+
isRouteFile: (name) => SVELTE_ROUTE_FILE_RE.test(name),
|
|
1217
|
+
toRouteSegments: (relativeFile) => relativeFile.split("/").slice(0, -1).filter(Boolean),
|
|
1218
|
+
exclude: apiReference.exclude
|
|
1219
|
+
});
|
|
1220
|
+
case "astro": return buildFileConventionRoutes({
|
|
1221
|
+
rootDir,
|
|
1222
|
+
sourceDir: resolveRootedDir(rootDir, apiReference.routeRoot, "src/pages"),
|
|
1223
|
+
routePathBase: toRouteBase(apiReference.routeRoot, "src/pages"),
|
|
1224
|
+
isRouteFile: (name) => ASTRO_ROUTE_FILE_RE.test(name),
|
|
1225
|
+
toRouteSegments: (relativeFile) => routeSegmentsFromEndpointFile(relativeFile),
|
|
1226
|
+
exclude: apiReference.exclude
|
|
1227
|
+
});
|
|
1228
|
+
case "nuxt": return buildFileConventionRoutes({
|
|
1229
|
+
rootDir,
|
|
1230
|
+
sourceDir: resolveRootedDir(rootDir, apiReference.routeRoot, "server"),
|
|
1231
|
+
routePathBase: toRouteBase(apiReference.routeRoot, "server"),
|
|
1232
|
+
isRouteFile: (name) => NUXT_ROUTE_FILE_RE.test(name),
|
|
1233
|
+
toRouteSegments: (relativeFile) => routeSegmentsFromEndpointFile(stripNuxtMethodSuffix(relativeFile)),
|
|
1234
|
+
exclude: apiReference.exclude,
|
|
1235
|
+
getMethods: (source, file) => extractNuxtMethods(source, file)
|
|
1236
|
+
});
|
|
1237
|
+
case "tanstack-start": return buildTanstackRoutes(rootDir, apiReference);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
function getFarmAppDir(rootDir) {
|
|
1241
|
+
return existsSync(join(rootDir, "src", "app")) ? "src/app" : "app";
|
|
1242
|
+
}
|
|
1243
|
+
function buildFileConventionRoutes({ rootDir, sourceDir, routePathBase, isRouteFile, toRouteSegments, exclude, getMethods = extractMethods }) {
|
|
1244
|
+
const files = scanRouteFiles(sourceDir, isRouteFile);
|
|
1245
|
+
const routes = [];
|
|
1246
|
+
for (const file of files) {
|
|
1247
|
+
const source = readFileSync(file, "utf-8");
|
|
1248
|
+
const methods = getMethods(source, file);
|
|
1249
|
+
if (methods.length === 0) continue;
|
|
1250
|
+
const relativeFile = relative(sourceDir, file).replace(/\\/g, "/");
|
|
1251
|
+
const routeSegments = toRouteSegments(relativeFile);
|
|
1252
|
+
const routePath = buildRoutePath(routePathBase, routeSegments);
|
|
1253
|
+
if (shouldExcludeRoute(exclude, routePath, relativeFile, routeSegments.join("/"))) continue;
|
|
1254
|
+
routes.push(createApiReferenceRoute({
|
|
1255
|
+
rootDir,
|
|
1256
|
+
file,
|
|
1257
|
+
source,
|
|
1258
|
+
methods,
|
|
1259
|
+
routePath
|
|
1260
|
+
}));
|
|
1261
|
+
}
|
|
1262
|
+
return routes.sort((a, b) => a.routePath.localeCompare(b.routePath));
|
|
1263
|
+
}
|
|
1264
|
+
function buildTanstackRoutes(rootDir, apiReference) {
|
|
1265
|
+
const routesDir = join(rootDir, "src", "routes");
|
|
1266
|
+
const files = scanRouteFiles(routesDir, (name) => TANSTACK_ROUTE_FILE_RE.test(name));
|
|
1267
|
+
const routeBase = `/${normalizePathSegment(apiReference.routeRoot)}`;
|
|
1268
|
+
const routes = [];
|
|
1269
|
+
for (const file of files) {
|
|
1270
|
+
const source = readFileSync(file, "utf-8");
|
|
1271
|
+
if (!source.includes("createFileRoute(") || !source.includes("handlers")) continue;
|
|
1272
|
+
const pathMatch = source.match(/createFileRoute\(\s*["'`]([^"'`]+)["'`]\s*\)/);
|
|
1273
|
+
if (!pathMatch) continue;
|
|
1274
|
+
const routePath = normalizeTanstackRoutePath(pathMatch[1]);
|
|
1275
|
+
if (!routePath.startsWith(routeBase)) continue;
|
|
1276
|
+
const methods = extractTanstackMethods(source);
|
|
1277
|
+
if (methods.length === 0) continue;
|
|
1278
|
+
const relativeFile = relative(routesDir, file).replace(/\\/g, "/");
|
|
1279
|
+
if (shouldExcludeRoute(apiReference.exclude, routePath, relativeFile, relativeFile)) continue;
|
|
1280
|
+
routes.push(createApiReferenceRoute({
|
|
1281
|
+
rootDir,
|
|
1282
|
+
file,
|
|
1283
|
+
source,
|
|
1284
|
+
methods,
|
|
1285
|
+
routePath
|
|
1286
|
+
}));
|
|
1287
|
+
}
|
|
1288
|
+
return routes.sort((a, b) => a.routePath.localeCompare(b.routePath));
|
|
1289
|
+
}
|
|
1290
|
+
function createApiReferenceRoute({ rootDir, file, source, methods, routePath }) {
|
|
1291
|
+
const docBlock = extractDocBlock(source);
|
|
1292
|
+
const pathSegments = routePath.split("/").filter(Boolean);
|
|
1293
|
+
const titleSegment = pathSegments[pathSegments.length - 1] ?? "overview";
|
|
1294
|
+
const tagSegment = pathSegments[0] ?? "general";
|
|
1295
|
+
const title = humanizeSegment(titleSegment);
|
|
1296
|
+
return {
|
|
1297
|
+
title,
|
|
1298
|
+
summary: docBlock.summary ?? `${title} endpoint`,
|
|
1299
|
+
description: docBlock.description,
|
|
1300
|
+
routePath,
|
|
1301
|
+
sourceFile: relative(rootDir, file).replace(/\\/g, "/"),
|
|
1302
|
+
methods,
|
|
1303
|
+
tag: humanizeSegment(tagSegment),
|
|
1304
|
+
parameters: buildPathParameters(routePath)
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
function buildPathParameters(routePath) {
|
|
1308
|
+
const parameters = [];
|
|
1309
|
+
for (const segment of routePath.split("/")) {
|
|
1310
|
+
const match = segment.match(/^\{(.+)\}$/);
|
|
1311
|
+
if (!match) continue;
|
|
1312
|
+
parameters.push({
|
|
1313
|
+
name: match[1],
|
|
1314
|
+
in: "path",
|
|
1315
|
+
required: true,
|
|
1316
|
+
description: `${humanizeSegment(match[1])} path parameter.`,
|
|
1317
|
+
schema: { type: "string" }
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
return parameters;
|
|
1321
|
+
}
|
|
1322
|
+
function buildOpenApiPaths(routes) {
|
|
1323
|
+
const paths = {};
|
|
1324
|
+
for (const route of routes) {
|
|
1325
|
+
const pathItem = {};
|
|
1326
|
+
for (const method of route.methods) pathItem[method.toLowerCase()] = {
|
|
1327
|
+
tags: [route.tag],
|
|
1328
|
+
summary: route.summary,
|
|
1329
|
+
description: route.description ?? route.summary,
|
|
1330
|
+
operationId: createOperationId(route, method),
|
|
1331
|
+
...route.parameters.length > 0 ? { parameters: route.parameters } : {},
|
|
1332
|
+
...buildRequestBody(method) ? { requestBody: buildRequestBody(method) } : {},
|
|
1333
|
+
responses: buildResponses(method),
|
|
1334
|
+
"x-farming-labs-source": route.sourceFile
|
|
1335
|
+
};
|
|
1336
|
+
paths[route.routePath] = pathItem;
|
|
1337
|
+
}
|
|
1338
|
+
return paths;
|
|
1339
|
+
}
|
|
1340
|
+
function buildRequestBody(method) {
|
|
1341
|
+
if (![
|
|
1342
|
+
"POST",
|
|
1343
|
+
"PUT",
|
|
1344
|
+
"PATCH"
|
|
1345
|
+
].includes(method)) return void 0;
|
|
1346
|
+
return {
|
|
1347
|
+
required: method === "POST",
|
|
1348
|
+
content: { "application/json": {
|
|
1349
|
+
schema: {
|
|
1350
|
+
type: "object",
|
|
1351
|
+
additionalProperties: true
|
|
1352
|
+
},
|
|
1353
|
+
example: { example: true }
|
|
1354
|
+
} }
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
function buildResponses(method) {
|
|
1358
|
+
return { "200": {
|
|
1359
|
+
description: method === "DELETE" ? "Resource removed successfully." : "Request completed successfully.",
|
|
1360
|
+
content: { "application/json": {
|
|
1361
|
+
schema: {
|
|
1362
|
+
type: "object",
|
|
1363
|
+
additionalProperties: true
|
|
1364
|
+
},
|
|
1365
|
+
example: { ok: true }
|
|
1366
|
+
} }
|
|
1367
|
+
} };
|
|
1368
|
+
}
|
|
1369
|
+
function createOperationId(route, method) {
|
|
1370
|
+
return `${method.toLowerCase()}_${route.routePath.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "")}`;
|
|
1371
|
+
}
|
|
1372
|
+
function resolveTheme(config) {
|
|
1373
|
+
return config.theme;
|
|
1374
|
+
}
|
|
1375
|
+
function normalizeApiReferenceExcludes(values) {
|
|
1376
|
+
return (values ?? []).map(normalizeExcludeMatcher).filter(Boolean);
|
|
1377
|
+
}
|
|
1378
|
+
function normalizeExcludeMatcher(value) {
|
|
1379
|
+
return value.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "").replace(/\.(ts|tsx|js|jsx|mjs|mts)$/i, "").replace(/\/route$/i, "").replace(/\/\+server$/i, "").replace(/\/index$/i, "").replace(/\.(get|post|put|patch|delete|options|head)$/i, "");
|
|
1380
|
+
}
|
|
1381
|
+
function shouldExcludeRoute(excludes, routePath, relativeFile, relativeDir) {
|
|
1382
|
+
if (excludes.length === 0) return false;
|
|
1383
|
+
const candidates = new Set([
|
|
1384
|
+
normalizeExcludeMatcher(routePath),
|
|
1385
|
+
normalizeExcludeMatcher(routePath.replace(/^\/+/, "")),
|
|
1386
|
+
normalizeExcludeMatcher(relativeFile),
|
|
1387
|
+
normalizeExcludeMatcher(relativeDir)
|
|
1388
|
+
]);
|
|
1389
|
+
return excludes.some((entry) => candidates.has(entry));
|
|
1390
|
+
}
|
|
1391
|
+
function scanRouteFiles(dir, isRouteFile) {
|
|
1392
|
+
if (!existsSync(dir)) return [];
|
|
1393
|
+
const results = [];
|
|
1394
|
+
for (const name of readdirSync(dir)) {
|
|
1395
|
+
const full = join(dir, name);
|
|
1396
|
+
if (statSync(full).isDirectory()) {
|
|
1397
|
+
results.push(...scanRouteFiles(full, isRouteFile));
|
|
1398
|
+
continue;
|
|
1399
|
+
}
|
|
1400
|
+
if (isRouteFile(name)) results.push(full);
|
|
1401
|
+
}
|
|
1402
|
+
return results;
|
|
1403
|
+
}
|
|
1404
|
+
function resolveRootedDir(rootDir, routeRoot, defaultRoot) {
|
|
1405
|
+
const normalized = normalizePathSegment(routeRoot) || "api";
|
|
1406
|
+
if (normalized === defaultRoot || normalized.startsWith(`${defaultRoot}/`)) return join(rootDir, ...normalized.split("/"));
|
|
1407
|
+
return join(rootDir, ...defaultRoot.split("/"), ...normalized.split("/"));
|
|
1408
|
+
}
|
|
1409
|
+
function toRouteBase(routeRoot, defaultRoot) {
|
|
1410
|
+
const normalized = normalizePathSegment(routeRoot) || "api";
|
|
1411
|
+
return `/${normalizePathSegment(normalized === defaultRoot || normalized.startsWith(`${defaultRoot}/`) ? normalized.slice(defaultRoot.length).replace(/^\/+/, "") : normalized)}`;
|
|
1412
|
+
}
|
|
1413
|
+
function getNextAppDir(rootDir) {
|
|
1414
|
+
if (existsSync(join(rootDir, "src", "app"))) return "src/app";
|
|
1415
|
+
return "app";
|
|
1416
|
+
}
|
|
1417
|
+
function routeSegmentsFromEndpointFile(relativeFile) {
|
|
1418
|
+
const segments = relativeFile.split("/");
|
|
1419
|
+
const name = (segments.pop() ?? "").replace(/\.(ts|js|mts|mjs)$/i, "");
|
|
1420
|
+
if (name !== "index") segments.push(name);
|
|
1421
|
+
return segments.filter(Boolean);
|
|
1422
|
+
}
|
|
1423
|
+
function stripNuxtMethodSuffix(relativeFile) {
|
|
1424
|
+
return relativeFile.replace(/\.(get|post|put|patch|delete|options|head)(?=\.(ts|js|mts|mjs)$)/i, "");
|
|
1425
|
+
}
|
|
1426
|
+
function buildRoutePath(basePath, rawSegments) {
|
|
1427
|
+
const segments = rawSegments.filter(Boolean).map((segment) => endpointSegmentFromConvention(segment)).join("/");
|
|
1428
|
+
const path = [normalizePathSegment(basePath), segments].filter(Boolean).join("/");
|
|
1429
|
+
return path ? `/${path}` : "/";
|
|
1430
|
+
}
|
|
1431
|
+
function endpointSegmentFromConvention(value) {
|
|
1432
|
+
if (value.startsWith("[[...") && value.endsWith("]]")) return `{${value.slice(5, -2)}}`;
|
|
1433
|
+
if (value.startsWith("[...") && value.endsWith("]")) return `{${value.slice(4, -1)}}`;
|
|
1434
|
+
if (value.startsWith("[") && value.endsWith("]")) return `{${value.slice(1, -1)}}`;
|
|
1435
|
+
return value;
|
|
1436
|
+
}
|
|
1437
|
+
function normalizeTanstackRoutePath(value) {
|
|
1438
|
+
return `/${value.replace(/^\/+|\/+$/g, "").split("/").map((segment) => segment.startsWith("$") ? `{${segment.slice(1)}}` : segment).filter(Boolean).join("/")}`;
|
|
1439
|
+
}
|
|
1440
|
+
function extractDocBlock(source) {
|
|
1441
|
+
const match = source.match(/\/\*\*([\s\S]*?)\*\//);
|
|
1442
|
+
if (!match) return {};
|
|
1443
|
+
const lines = match[1].split("\n").map((line) => line.replace(/^\s*\*\s?/, "").trim()).filter(Boolean).filter((line) => !line.startsWith("@"));
|
|
1444
|
+
if (lines.length === 0) return {};
|
|
1445
|
+
return {
|
|
1446
|
+
summary: lines[0],
|
|
1447
|
+
description: lines.slice(1).join(" ")
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
function extractMethods(source) {
|
|
1451
|
+
const methods = /* @__PURE__ */ new Set();
|
|
1452
|
+
for (const match of source.matchAll(METHOD_RE)) {
|
|
1453
|
+
if (match[1] === "ALL") {
|
|
1454
|
+
METHOD_NAMES.forEach((method) => methods.add(method));
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
methods.add(match[1]);
|
|
1458
|
+
}
|
|
1459
|
+
return Array.from(methods);
|
|
1460
|
+
}
|
|
1461
|
+
function extractNuxtMethods(source, file) {
|
|
1462
|
+
const methods = extractMethods(source);
|
|
1463
|
+
if (methods.length > 0) return methods;
|
|
1464
|
+
const suffix = basename(file).match(/\.(get|post|put|patch|delete|options|head)\.(ts|js|mts|mjs)$/i);
|
|
1465
|
+
if (suffix) return [suffix[1].toUpperCase()];
|
|
1466
|
+
if (/defineEventHandler|eventHandler/.test(source)) return ["GET"];
|
|
1467
|
+
return [];
|
|
1468
|
+
}
|
|
1469
|
+
function extractTanstackMethods(source) {
|
|
1470
|
+
const methods = /* @__PURE__ */ new Set();
|
|
1471
|
+
const handlersMatch = source.match(/handlers\s*:\s*\{([\s\S]*?)\}/m);
|
|
1472
|
+
if (!handlersMatch) return [];
|
|
1473
|
+
for (const match of handlersMatch[1].matchAll(/\b(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s*:/g)) methods.add(match[1]);
|
|
1474
|
+
return Array.from(methods);
|
|
1475
|
+
}
|
|
1476
|
+
function humanizeSegment(value) {
|
|
1477
|
+
return value.replace(/^\{/, "").replace(/\}$/, "").replace(/^\[\[?\.{3}/, "").replace(/^\[/, "").replace(/\]\]?$/, "").replace(/^\$/, "").replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
//#endregion
|
|
1481
|
+
export { DEFAULT_API_REFERENCE_OPENAPI_ROUTE, buildApiReferenceHtmlDocument, buildApiReferenceHtmlDocumentAsync, buildApiReferenceOpenApiDocument, buildApiReferenceOpenApiDocumentAsync, buildApiReferenceOperationPagesAsync, buildApiReferencePageTitle, buildApiReferenceScalarCss, createApiReferenceOperationMarkdownResponse, isApiReferenceOpenApiRequest, loadApiReferenceOpenApiDocumentAsync, resolveApiReferenceConfig, resolveApiReferenceOpenApiDiscovery, resolveApiReferenceRenderer, resolveApiReferenceVersion, resolveApiReferenceVersionFromPathname };
|