@farming-labs/next 0.0.38 → 0.0.43
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/dist/api-reference.d.mts +9 -0
- package/dist/api-reference.mjs +611 -0
- package/dist/config.mjs +76 -1
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +2 -1
- package/package.json +13 -2
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ApiReferenceConfig, DocsConfig } from "@farming-labs/docs";
|
|
2
|
+
|
|
3
|
+
//#region src/api-reference.d.ts
|
|
4
|
+
declare function resolveApiReferenceConfig(value: DocsConfig["apiReference"]): Required<ApiReferenceConfig>;
|
|
5
|
+
declare function buildNextOpenApiDocument(config: DocsConfig): Record<string, unknown>;
|
|
6
|
+
declare function withNextApiReferenceBanner(config: DocsConfig): DocsConfig;
|
|
7
|
+
declare function createNextApiReference(config: DocsConfig): () => Response;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { buildNextOpenApiDocument, createNextApiReference, resolveApiReferenceConfig, withNextApiReferenceBanner };
|
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { ApiReference } from "@scalar/nextjs-api-reference";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
|
|
6
|
+
//#region src/api-reference.tsx
|
|
7
|
+
const ROUTE_FILE_RE = /^route\.(ts|tsx|js|jsx)$/;
|
|
8
|
+
const METHOD_RE = /export\s+(?:async\s+function|function|const)\s+(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\b/g;
|
|
9
|
+
function resolveTheme(config) {
|
|
10
|
+
return config.theme;
|
|
11
|
+
}
|
|
12
|
+
function buildScalarCustomCss(config) {
|
|
13
|
+
const theme = resolveTheme(config);
|
|
14
|
+
const colors = theme?.ui?.colors;
|
|
15
|
+
const typography = theme?.ui?.typography?.font?.style;
|
|
16
|
+
const layout = theme?.ui?.layout;
|
|
17
|
+
const primary = colors?.primary ?? "#6366f1";
|
|
18
|
+
const border = colors?.border ?? "#2a2a2a";
|
|
19
|
+
const muted = colors?.muted ?? "#64748b";
|
|
20
|
+
const background = colors?.background ?? "#ffffff";
|
|
21
|
+
const card = colors?.card ?? background;
|
|
22
|
+
const foreground = colors?.foreground ?? "#1b1b1b";
|
|
23
|
+
const sidebarWidth = layout?.sidebarWidth ?? 280;
|
|
24
|
+
return `
|
|
25
|
+
:root {
|
|
26
|
+
--scalar-font: ${typography?.sans ?? "\"Geist\", \"Inter\", \"Segoe UI\", sans-serif"};
|
|
27
|
+
--scalar-font-code: ${typography?.mono ?? "\"Geist Mono\", \"SFMono-Regular\", \"Menlo\", monospace"};
|
|
28
|
+
--scalar-theme-primary: ${primary};
|
|
29
|
+
--scalar-theme-border: ${border};
|
|
30
|
+
--scalar-theme-muted: ${muted};
|
|
31
|
+
--scalar-theme-background: ${background};
|
|
32
|
+
--scalar-theme-card: ${card};
|
|
33
|
+
--scalar-theme-foreground: ${foreground};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.dark-mode {
|
|
37
|
+
--scalar-background-1: color-mix(in srgb, #0b0c0b 98%, var(--scalar-theme-primary) 2%);
|
|
38
|
+
--scalar-background-2: color-mix(in srgb, #111311 96%, var(--scalar-theme-primary) 4%);
|
|
39
|
+
--scalar-background-3: color-mix(in srgb, #171917 95%, var(--scalar-theme-primary) 5%);
|
|
40
|
+
--scalar-color-1: rgba(255, 255, 255, 0.96);
|
|
41
|
+
--scalar-color-2: rgba(255, 255, 255, 0.72);
|
|
42
|
+
--scalar-color-3: rgba(255, 255, 255, 0.5);
|
|
43
|
+
--scalar-color-accent: var(--scalar-theme-primary);
|
|
44
|
+
--scalar-sidebar-color-active: var(--scalar-theme-primary);
|
|
45
|
+
--scalar-sidebar-item-active-background: color-mix(
|
|
46
|
+
in srgb,
|
|
47
|
+
var(--scalar-theme-primary) 7%,
|
|
48
|
+
transparent
|
|
49
|
+
);
|
|
50
|
+
--scalar-border-color: color-mix(
|
|
51
|
+
in srgb,
|
|
52
|
+
var(--scalar-theme-border) 22%,
|
|
53
|
+
rgba(255, 255, 255, 0.032)
|
|
54
|
+
);
|
|
55
|
+
--scalar-button-1: var(--scalar-theme-primary);
|
|
56
|
+
--scalar-button-1-color: #ffffff;
|
|
57
|
+
--scalar-button-1-hover: color-mix(in srgb, var(--scalar-theme-primary) 88%, white 12%);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.light-mode {
|
|
61
|
+
--scalar-background-1: var(--scalar-theme-background);
|
|
62
|
+
--scalar-background-2: color-mix(in srgb, var(--scalar-theme-card) 92%, white 8%);
|
|
63
|
+
--scalar-background-3: color-mix(in srgb, var(--scalar-theme-card) 84%, black 4%);
|
|
64
|
+
--scalar-color-1: var(--scalar-theme-foreground);
|
|
65
|
+
--scalar-color-2: var(--scalar-theme-muted);
|
|
66
|
+
--scalar-color-3: color-mix(in srgb, var(--scalar-theme-muted) 78%, white 22%);
|
|
67
|
+
--scalar-color-accent: var(--scalar-theme-primary);
|
|
68
|
+
--scalar-sidebar-color-active: var(--scalar-theme-primary);
|
|
69
|
+
--scalar-sidebar-item-active-background: color-mix(
|
|
70
|
+
in srgb,
|
|
71
|
+
var(--scalar-theme-primary) 5%,
|
|
72
|
+
transparent
|
|
73
|
+
);
|
|
74
|
+
--scalar-border-color: color-mix(in srgb, var(--scalar-theme-border) 42%, white 58%);
|
|
75
|
+
--scalar-button-1: var(--scalar-theme-primary);
|
|
76
|
+
--scalar-button-1-color: #ffffff;
|
|
77
|
+
--scalar-button-1-hover: color-mix(in srgb, var(--scalar-theme-primary) 88%, black 12%);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
body {
|
|
81
|
+
background: var(--scalar-background-1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.t-doc__sidebar {
|
|
85
|
+
width: min(${sidebarWidth}px, 100vw);
|
|
86
|
+
border-right: 1px solid var(--scalar-border-color);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.scalar-card,
|
|
90
|
+
.t-doc__sidebar,
|
|
91
|
+
.references-layout .reference-layout__content .request-card,
|
|
92
|
+
.references-layout .reference-layout__content .response-card,
|
|
93
|
+
.references-layout .reference-layout__content .scalar-card-header,
|
|
94
|
+
.references-layout .reference-layout__content .scalar-card-footer,
|
|
95
|
+
.references-layout .reference-layout__content .section,
|
|
96
|
+
.references-layout .reference-layout__content .section-container {
|
|
97
|
+
border-color: var(--scalar-border-color) !important;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.t-doc__sidebar,
|
|
101
|
+
.t-doc__sidebar * {
|
|
102
|
+
font-family: var(--scalar-font);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.t-doc__sidebar .sidebar-search {
|
|
106
|
+
margin: 0.5rem 0 1rem;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.t-doc__sidebar .sidebar-search input {
|
|
110
|
+
border-radius: 14px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.t-doc__sidebar .sidebar-item,
|
|
114
|
+
.t-doc__sidebar .sidebar-heading {
|
|
115
|
+
border-radius: 14px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.t-doc__sidebar .sidebar-group-label {
|
|
119
|
+
font-size: 0.72rem;
|
|
120
|
+
letter-spacing: 0.08em;
|
|
121
|
+
text-transform: uppercase;
|
|
122
|
+
color: var(--scalar-color-3);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.t-doc__sidebar .sidebar-item--active {
|
|
126
|
+
font-weight: 600;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.scalar-card,
|
|
130
|
+
.references-layout .reference-layout__content .request-card,
|
|
131
|
+
.references-layout .reference-layout__content .response-card {
|
|
132
|
+
border-radius: 18px;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.references-layout .reference-layout__content {
|
|
136
|
+
padding-top: 1.5rem;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.references-layout .section-content,
|
|
140
|
+
.references-layout .section-flare {
|
|
141
|
+
background: transparent;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.references-layout .reference-layout__content,
|
|
145
|
+
.references-layout .reference-layout__content * {
|
|
146
|
+
font-family: var(--scalar-font);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.references-layout code,
|
|
150
|
+
.references-layout pre,
|
|
151
|
+
.references-layout .scalar-codeblock {
|
|
152
|
+
font-family: var(--scalar-font-code);
|
|
153
|
+
}
|
|
154
|
+
`;
|
|
155
|
+
}
|
|
156
|
+
function getNextAppDir(root) {
|
|
157
|
+
if (existsSync(join(root, "src", "app"))) return "src/app";
|
|
158
|
+
return "app";
|
|
159
|
+
}
|
|
160
|
+
function resolveApiReferenceConfig(value) {
|
|
161
|
+
if (value === true) return {
|
|
162
|
+
enabled: true,
|
|
163
|
+
path: "api-reference",
|
|
164
|
+
routeRoot: "api",
|
|
165
|
+
exclude: []
|
|
166
|
+
};
|
|
167
|
+
if (!value) return {
|
|
168
|
+
enabled: false,
|
|
169
|
+
path: "api-reference",
|
|
170
|
+
routeRoot: "api",
|
|
171
|
+
exclude: []
|
|
172
|
+
};
|
|
173
|
+
return {
|
|
174
|
+
enabled: value.enabled !== false,
|
|
175
|
+
path: normalizePathSegment(value.path ?? "api-reference"),
|
|
176
|
+
routeRoot: normalizeRouteRoot(value.routeRoot ?? "api"),
|
|
177
|
+
exclude: normalizeApiReferenceExcludes(value.exclude)
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function normalizePathSegment(value) {
|
|
181
|
+
return value.replace(/^\/+|\/+$/g, "") || "api-reference";
|
|
182
|
+
}
|
|
183
|
+
function normalizeRouteRoot(value) {
|
|
184
|
+
return value.replace(/^\/+|\/+$/g, "") || "api";
|
|
185
|
+
}
|
|
186
|
+
function normalizeExcludeMatcher(value) {
|
|
187
|
+
return value.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "").replace(/\.(ts|tsx|js|jsx)$/i, "").replace(/\/route$/i, "");
|
|
188
|
+
}
|
|
189
|
+
function normalizeApiReferenceExcludes(values) {
|
|
190
|
+
return (values ?? []).map(normalizeExcludeMatcher).filter(Boolean);
|
|
191
|
+
}
|
|
192
|
+
function getRoutePathBase(config) {
|
|
193
|
+
const routeRoot = normalizeRouteRoot(config.routeRoot);
|
|
194
|
+
if (routeRoot === "app" || routeRoot === "src/app") return "";
|
|
195
|
+
if (routeRoot.startsWith("app/")) return `/${routeRoot.slice(4)}`;
|
|
196
|
+
if (routeRoot.startsWith("src/app/")) return `/${routeRoot.slice(8)}`;
|
|
197
|
+
return `/${routeRoot}`;
|
|
198
|
+
}
|
|
199
|
+
function resolveNextApiRouteRoot(root, config) {
|
|
200
|
+
const routeRoot = normalizeRouteRoot(config.routeRoot);
|
|
201
|
+
if (routeRoot === "app" || routeRoot.startsWith("app/") || routeRoot === "src/app" || routeRoot.startsWith("src/app/")) return join(root, ...routeRoot.split("/"));
|
|
202
|
+
return join(root, getNextAppDir(root), ...routeRoot.split("/"));
|
|
203
|
+
}
|
|
204
|
+
function shouldExcludeRoute(excludes, routePath, relativeFile, relativeDir) {
|
|
205
|
+
if (excludes.length === 0) return false;
|
|
206
|
+
const normalizedRoutePath = normalizeExcludeMatcher(routePath);
|
|
207
|
+
const candidates = new Set([
|
|
208
|
+
normalizedRoutePath,
|
|
209
|
+
normalizeExcludeMatcher(routePath.replace(/^\/+/, "")),
|
|
210
|
+
normalizeExcludeMatcher(relativeFile),
|
|
211
|
+
normalizeExcludeMatcher(relativeDir)
|
|
212
|
+
]);
|
|
213
|
+
return excludes.some((entry) => candidates.has(entry));
|
|
214
|
+
}
|
|
215
|
+
function humanizeSegment(value) {
|
|
216
|
+
return value.replace(/^\[\[?\.{3}/, "").replace(/^\[/, "").replace(/\]\]?$/, "").replace(/^\$/, "").replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
217
|
+
}
|
|
218
|
+
function endpointSegmentFromFsSegment(value) {
|
|
219
|
+
if (value.startsWith("[[...") && value.endsWith("]]")) return `{${value.slice(5, -2)}}`;
|
|
220
|
+
if (value.startsWith("[...") && value.endsWith("]")) return `{${value.slice(4, -1)}}`;
|
|
221
|
+
if (value.startsWith("[") && value.endsWith("]")) return `{${value.slice(1, -1)}}`;
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
function getPathParamName(value) {
|
|
225
|
+
if (value.startsWith("[[...") && value.endsWith("]]")) return value.slice(5, -2);
|
|
226
|
+
if (value.startsWith("[...") && value.endsWith("]")) return value.slice(4, -1);
|
|
227
|
+
if (value.startsWith("[") && value.endsWith("]")) return value.slice(1, -1);
|
|
228
|
+
}
|
|
229
|
+
function extractDocBlock(source) {
|
|
230
|
+
const match = source.match(/\/\*\*([\s\S]*?)\*\//);
|
|
231
|
+
if (!match) return {};
|
|
232
|
+
const lines = match[1].split("\n").map((line) => line.replace(/^\s*\*\s?/, "").trim()).filter(Boolean).filter((line) => !line.startsWith("@"));
|
|
233
|
+
if (lines.length === 0) return {};
|
|
234
|
+
return {
|
|
235
|
+
summary: lines[0],
|
|
236
|
+
description: lines.slice(1).join(" ")
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function extractMethods(source) {
|
|
240
|
+
const methods = /* @__PURE__ */ new Set();
|
|
241
|
+
for (const match of source.matchAll(METHOD_RE)) methods.add(match[1]);
|
|
242
|
+
return Array.from(methods);
|
|
243
|
+
}
|
|
244
|
+
function scanRouteFiles(dir) {
|
|
245
|
+
if (!existsSync(dir)) return [];
|
|
246
|
+
const results = [];
|
|
247
|
+
for (const name of readdirSync(dir)) {
|
|
248
|
+
const full = join(dir, name);
|
|
249
|
+
if (statSync(full).isDirectory()) {
|
|
250
|
+
results.push(...scanRouteFiles(full));
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (ROUTE_FILE_RE.test(name)) results.push(full);
|
|
254
|
+
}
|
|
255
|
+
return results;
|
|
256
|
+
}
|
|
257
|
+
function getDocsUrl(config) {
|
|
258
|
+
if (typeof config.nav?.url === "string") return config.nav.url;
|
|
259
|
+
return `/${config.entry ?? "docs"}`;
|
|
260
|
+
}
|
|
261
|
+
function applyTitleTemplate(config, title) {
|
|
262
|
+
const template = config.metadata?.titleTemplate;
|
|
263
|
+
if (!template) return title;
|
|
264
|
+
return template.replace("%s", title);
|
|
265
|
+
}
|
|
266
|
+
function getForcedMode(config) {
|
|
267
|
+
const toggle = config.themeToggle;
|
|
268
|
+
if (!toggle || typeof toggle !== "object") return void 0;
|
|
269
|
+
if (toggle.default === "dark") return "dark";
|
|
270
|
+
if (toggle.default === "light") return "light";
|
|
271
|
+
}
|
|
272
|
+
function isThemeToggleHidden(config) {
|
|
273
|
+
if (config.themeToggle === false) return true;
|
|
274
|
+
if (config.themeToggle && typeof config.themeToggle === "object") return config.themeToggle.enabled === false;
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
function buildPathParameters(fsSegments) {
|
|
278
|
+
const parameters = [];
|
|
279
|
+
for (const segment of fsSegments) {
|
|
280
|
+
const name = getPathParamName(segment);
|
|
281
|
+
if (!name) continue;
|
|
282
|
+
const optional = segment.startsWith("[[...");
|
|
283
|
+
parameters.push({
|
|
284
|
+
name,
|
|
285
|
+
in: "path",
|
|
286
|
+
required: !optional,
|
|
287
|
+
description: optional ? `${humanizeSegment(name)} catch-all parameter.` : `${humanizeSegment(name)} path parameter.`,
|
|
288
|
+
schema: { type: "string" }
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return parameters;
|
|
292
|
+
}
|
|
293
|
+
function buildApiReferenceRoutes(config) {
|
|
294
|
+
const apiReference = resolveApiReferenceConfig(config.apiReference);
|
|
295
|
+
if (!apiReference.enabled) return [];
|
|
296
|
+
const root = process.cwd();
|
|
297
|
+
const apiDir = resolveNextApiRouteRoot(root, apiReference);
|
|
298
|
+
const routePathBase = getRoutePathBase(apiReference);
|
|
299
|
+
const files = scanRouteFiles(apiDir);
|
|
300
|
+
const excludes = apiReference.exclude;
|
|
301
|
+
const routes = [];
|
|
302
|
+
for (const file of files) {
|
|
303
|
+
const source = readFileSync(file, "utf-8");
|
|
304
|
+
const methods = extractMethods(source);
|
|
305
|
+
if (methods.length === 0) continue;
|
|
306
|
+
const relativeFile = relative(apiDir, file).replace(/\\/g, "/");
|
|
307
|
+
const fsSegments = relativeFile.split("/").slice(0, -1).filter(Boolean);
|
|
308
|
+
const relativeDir = fsSegments.join("/");
|
|
309
|
+
const routeSegments = fsSegments.map(endpointSegmentFromFsSegment);
|
|
310
|
+
const routePath = `${routePathBase}${routeSegments.length > 0 ? `/${routeSegments.join("/")}` : ""}` || "/";
|
|
311
|
+
if (shouldExcludeRoute(excludes, routePath, relativeFile, relativeDir)) continue;
|
|
312
|
+
const docBlock = extractDocBlock(source);
|
|
313
|
+
const title = fsSegments.length > 0 ? humanizeSegment(fsSegments[fsSegments.length - 1]) : "Overview";
|
|
314
|
+
routes.push({
|
|
315
|
+
title,
|
|
316
|
+
summary: docBlock.summary ?? `${title} endpoint`,
|
|
317
|
+
description: docBlock.description,
|
|
318
|
+
routePath,
|
|
319
|
+
sourceFile: relative(root, file).replace(/\\/g, "/"),
|
|
320
|
+
methods,
|
|
321
|
+
segments: fsSegments,
|
|
322
|
+
tag: fsSegments.length > 0 ? humanizeSegment(fsSegments[0]) : "General",
|
|
323
|
+
parameters: buildPathParameters(fsSegments)
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return routes.sort((a, b) => a.routePath.localeCompare(b.routePath));
|
|
327
|
+
}
|
|
328
|
+
function createOperationId(route, method) {
|
|
329
|
+
return `${method.toLowerCase()}_${route.routePath.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "")}`;
|
|
330
|
+
}
|
|
331
|
+
function buildRequestBody(method) {
|
|
332
|
+
if (![
|
|
333
|
+
"POST",
|
|
334
|
+
"PUT",
|
|
335
|
+
"PATCH"
|
|
336
|
+
].includes(method)) return void 0;
|
|
337
|
+
return {
|
|
338
|
+
required: method === "POST",
|
|
339
|
+
content: { "application/json": {
|
|
340
|
+
schema: {
|
|
341
|
+
type: "object",
|
|
342
|
+
additionalProperties: true
|
|
343
|
+
},
|
|
344
|
+
example: { example: true }
|
|
345
|
+
} }
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function buildResponses(method) {
|
|
349
|
+
return { "200": {
|
|
350
|
+
description: method === "DELETE" ? "Resource removed successfully." : "Request completed successfully.",
|
|
351
|
+
content: { "application/json": {
|
|
352
|
+
schema: {
|
|
353
|
+
type: "object",
|
|
354
|
+
additionalProperties: true
|
|
355
|
+
},
|
|
356
|
+
example: { ok: true }
|
|
357
|
+
} }
|
|
358
|
+
} };
|
|
359
|
+
}
|
|
360
|
+
function buildOpenApiPaths(routes) {
|
|
361
|
+
const paths = {};
|
|
362
|
+
for (const route of routes) {
|
|
363
|
+
const pathItem = {};
|
|
364
|
+
for (const method of route.methods) pathItem[method.toLowerCase()] = {
|
|
365
|
+
tags: [route.tag],
|
|
366
|
+
summary: route.summary,
|
|
367
|
+
description: route.description ?? route.summary,
|
|
368
|
+
operationId: createOperationId(route, method),
|
|
369
|
+
...route.parameters.length > 0 ? { parameters: route.parameters } : {},
|
|
370
|
+
...buildRequestBody(method) ? { requestBody: buildRequestBody(method) } : {},
|
|
371
|
+
responses: buildResponses(method),
|
|
372
|
+
"x-farming-labs-source": route.sourceFile
|
|
373
|
+
};
|
|
374
|
+
paths[route.routePath] = pathItem;
|
|
375
|
+
}
|
|
376
|
+
return paths;
|
|
377
|
+
}
|
|
378
|
+
function buildNextOpenApiDocument(config) {
|
|
379
|
+
const routes = buildApiReferenceRoutes(config);
|
|
380
|
+
const tags = Array.from(new Set(routes.map((route) => route.tag))).map((name) => ({
|
|
381
|
+
name,
|
|
382
|
+
description: `${name} endpoints`
|
|
383
|
+
}));
|
|
384
|
+
return {
|
|
385
|
+
openapi: "3.1.0",
|
|
386
|
+
info: {
|
|
387
|
+
title: "API Reference",
|
|
388
|
+
description: config.metadata?.description ?? "Generated API reference from Next.js route handlers.",
|
|
389
|
+
version: "0.0.0"
|
|
390
|
+
},
|
|
391
|
+
servers: [{ url: "/" }],
|
|
392
|
+
tags,
|
|
393
|
+
paths: buildOpenApiPaths(routes)
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function DropdownIcon({ current }) {
|
|
397
|
+
const label = current === "api" ? "</>" : "▣";
|
|
398
|
+
return /* @__PURE__ */ jsx("span", {
|
|
399
|
+
"aria-hidden": "true",
|
|
400
|
+
style: {
|
|
401
|
+
display: "inline-flex",
|
|
402
|
+
width: 20,
|
|
403
|
+
height: 20,
|
|
404
|
+
alignItems: "center",
|
|
405
|
+
justifyContent: "center",
|
|
406
|
+
borderRadius: 6,
|
|
407
|
+
border: "1px solid color-mix(in srgb, var(--color-fd-border, #2a2a2a) 100%, transparent)",
|
|
408
|
+
background: "color-mix(in srgb, var(--color-fd-card, #161616) 92%, transparent)",
|
|
409
|
+
color: "var(--color-fd-primary, currentColor)",
|
|
410
|
+
boxShadow: "0 0 0 1px color-mix(in srgb, var(--color-fd-border, #2a2a2a) 32%, transparent)",
|
|
411
|
+
fontSize: 9,
|
|
412
|
+
fontWeight: 700
|
|
413
|
+
},
|
|
414
|
+
children: label
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
function SwitcherOption({ href, title, description, current }) {
|
|
418
|
+
return /* @__PURE__ */ jsxs("a", {
|
|
419
|
+
href,
|
|
420
|
+
style: {
|
|
421
|
+
display: "grid",
|
|
422
|
+
gridTemplateColumns: "20px 1fr 14px",
|
|
423
|
+
gap: 12,
|
|
424
|
+
alignItems: "start",
|
|
425
|
+
padding: "11px 12px",
|
|
426
|
+
borderRadius: 12,
|
|
427
|
+
textDecoration: "none",
|
|
428
|
+
color: "inherit",
|
|
429
|
+
background: current ? "color-mix(in srgb, var(--color-fd-primary, #3a7) 10%, transparent)" : "transparent"
|
|
430
|
+
},
|
|
431
|
+
children: [
|
|
432
|
+
/* @__PURE__ */ jsx("span", {
|
|
433
|
+
"aria-hidden": "true",
|
|
434
|
+
style: {
|
|
435
|
+
display: "inline-flex",
|
|
436
|
+
width: 20,
|
|
437
|
+
height: 20,
|
|
438
|
+
alignItems: "center",
|
|
439
|
+
justifyContent: "center",
|
|
440
|
+
borderRadius: 6,
|
|
441
|
+
border: "1px solid color-mix(in srgb, var(--color-fd-border, #2a2a2a) 100%, transparent)",
|
|
442
|
+
color: current ? "var(--color-fd-primary, currentColor)" : "var(--color-fd-muted-foreground, rgba(255,255,255,0.62))",
|
|
443
|
+
background: "color-mix(in srgb, var(--color-fd-card, #161616) 92%, transparent)",
|
|
444
|
+
boxShadow: "0 0 0 1px color-mix(in srgb, var(--color-fd-border, #2a2a2a) 32%, transparent)",
|
|
445
|
+
fontSize: 9,
|
|
446
|
+
fontWeight: 700
|
|
447
|
+
},
|
|
448
|
+
children: title === "API Reference" ? "</>" : "▣"
|
|
449
|
+
}),
|
|
450
|
+
/* @__PURE__ */ jsxs("span", {
|
|
451
|
+
style: {
|
|
452
|
+
display: "flex",
|
|
453
|
+
flexDirection: "column",
|
|
454
|
+
gap: 4
|
|
455
|
+
},
|
|
456
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
457
|
+
style: {
|
|
458
|
+
fontSize: 13,
|
|
459
|
+
fontWeight: 600,
|
|
460
|
+
lineHeight: 1.25
|
|
461
|
+
},
|
|
462
|
+
children: title
|
|
463
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
464
|
+
style: {
|
|
465
|
+
fontSize: 12,
|
|
466
|
+
opacity: .62,
|
|
467
|
+
lineHeight: 1.4
|
|
468
|
+
},
|
|
469
|
+
children: description
|
|
470
|
+
})]
|
|
471
|
+
}),
|
|
472
|
+
/* @__PURE__ */ jsx("span", {
|
|
473
|
+
"aria-hidden": "true",
|
|
474
|
+
style: {
|
|
475
|
+
fontSize: 12,
|
|
476
|
+
opacity: current ? 1 : 0,
|
|
477
|
+
color: "var(--color-fd-primary, currentColor)",
|
|
478
|
+
paddingTop: 2
|
|
479
|
+
},
|
|
480
|
+
children: "✓"
|
|
481
|
+
})
|
|
482
|
+
]
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
function ApiReferenceSwitcher({ docsUrl, apiUrl, current }) {
|
|
486
|
+
return /* @__PURE__ */ jsxs("details", {
|
|
487
|
+
style: {
|
|
488
|
+
position: "relative",
|
|
489
|
+
marginBottom: 16,
|
|
490
|
+
borderRadius: 14,
|
|
491
|
+
border: "1px solid color-mix(in srgb, var(--color-fd-border, #2a2a2a) 100%, transparent)",
|
|
492
|
+
background: "color-mix(in srgb, var(--color-fd-card, #141414) 94%, transparent)",
|
|
493
|
+
boxShadow: "0 0 0 1px color-mix(in srgb, var(--color-fd-border, #2a2a2a) 32%, transparent)",
|
|
494
|
+
overflow: "hidden"
|
|
495
|
+
},
|
|
496
|
+
children: [/* @__PURE__ */ jsxs("summary", {
|
|
497
|
+
style: {
|
|
498
|
+
listStyle: "none",
|
|
499
|
+
display: "flex",
|
|
500
|
+
alignItems: "center",
|
|
501
|
+
justifyContent: "space-between",
|
|
502
|
+
gap: 10,
|
|
503
|
+
cursor: "pointer",
|
|
504
|
+
padding: "11px 13px",
|
|
505
|
+
background: "color-mix(in srgb, var(--color-fd-card, #202020) 96%, transparent)",
|
|
506
|
+
borderBottom: "1px solid color-mix(in srgb, var(--color-fd-border, #2a2a2a) 100%, transparent)"
|
|
507
|
+
},
|
|
508
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
509
|
+
style: {
|
|
510
|
+
display: "flex",
|
|
511
|
+
alignItems: "center",
|
|
512
|
+
gap: 10
|
|
513
|
+
},
|
|
514
|
+
children: [/* @__PURE__ */ jsx(DropdownIcon, { current }), /* @__PURE__ */ jsx("span", {
|
|
515
|
+
style: {
|
|
516
|
+
fontSize: 14,
|
|
517
|
+
fontWeight: 600
|
|
518
|
+
},
|
|
519
|
+
children: current === "api" ? "API Reference" : "Documentation"
|
|
520
|
+
})]
|
|
521
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
522
|
+
"aria-hidden": "true",
|
|
523
|
+
style: {
|
|
524
|
+
fontSize: 11,
|
|
525
|
+
opacity: .56,
|
|
526
|
+
transform: "translateY(1px)"
|
|
527
|
+
},
|
|
528
|
+
children: "▿"
|
|
529
|
+
})]
|
|
530
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
531
|
+
style: {
|
|
532
|
+
display: "flex",
|
|
533
|
+
flexDirection: "column",
|
|
534
|
+
gap: 2,
|
|
535
|
+
padding: 8,
|
|
536
|
+
background: "color-mix(in srgb, var(--color-fd-card, #151515) 96%, transparent)"
|
|
537
|
+
},
|
|
538
|
+
children: [/* @__PURE__ */ jsx(SwitcherOption, {
|
|
539
|
+
href: docsUrl,
|
|
540
|
+
title: "Documentation",
|
|
541
|
+
description: "Markdown pages, guides, and concepts",
|
|
542
|
+
current: current === "docs"
|
|
543
|
+
}), /* @__PURE__ */ jsx(SwitcherOption, {
|
|
544
|
+
href: apiUrl,
|
|
545
|
+
title: "API Reference",
|
|
546
|
+
description: "Scalar-powered route handler reference",
|
|
547
|
+
current: current === "api"
|
|
548
|
+
})]
|
|
549
|
+
})]
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
function mergeBanner(existing, next) {
|
|
553
|
+
if (!existing) return next;
|
|
554
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
555
|
+
style: {
|
|
556
|
+
display: "flex",
|
|
557
|
+
flexDirection: "column",
|
|
558
|
+
gap: 12
|
|
559
|
+
},
|
|
560
|
+
children: [existing, next]
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
function withNextApiReferenceBanner(config) {
|
|
564
|
+
const apiReference = resolveApiReferenceConfig(config.apiReference);
|
|
565
|
+
if (!apiReference.enabled) return config;
|
|
566
|
+
if (config.sidebar === false) return config;
|
|
567
|
+
const switcher = /* @__PURE__ */ jsx(ApiReferenceSwitcher, {
|
|
568
|
+
docsUrl: getDocsUrl(config),
|
|
569
|
+
apiUrl: `/${apiReference.path}`,
|
|
570
|
+
current: "docs"
|
|
571
|
+
});
|
|
572
|
+
if (!config.sidebar || config.sidebar === true) return {
|
|
573
|
+
...config,
|
|
574
|
+
sidebar: { banner: switcher }
|
|
575
|
+
};
|
|
576
|
+
return {
|
|
577
|
+
...config,
|
|
578
|
+
sidebar: {
|
|
579
|
+
...config.sidebar,
|
|
580
|
+
banner: mergeBanner(config.sidebar.banner, switcher)
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function createNextApiReference(config) {
|
|
585
|
+
const apiReference = resolveApiReferenceConfig(config.apiReference);
|
|
586
|
+
return ApiReference({
|
|
587
|
+
pageTitle: applyTitleTemplate(config, "API Reference"),
|
|
588
|
+
title: "API Reference",
|
|
589
|
+
content: () => buildNextOpenApiDocument(config),
|
|
590
|
+
theme: "deepSpace",
|
|
591
|
+
layout: "modern",
|
|
592
|
+
darkMode: getForcedMode(config) === "dark" ? true : void 0,
|
|
593
|
+
forceDarkModeState: getForcedMode(config),
|
|
594
|
+
hideDarkModeToggle: isThemeToggleHidden(config),
|
|
595
|
+
customCss: buildScalarCustomCss(config),
|
|
596
|
+
pathRouting: { basePath: `/${apiReference.path}` },
|
|
597
|
+
showSidebar: true,
|
|
598
|
+
defaultOpenFirstTag: true,
|
|
599
|
+
tagsSorter: "alpha",
|
|
600
|
+
operationsSorter: "alpha",
|
|
601
|
+
operationTitleSource: "summary",
|
|
602
|
+
defaultHttpClient: {
|
|
603
|
+
targetKey: "shell",
|
|
604
|
+
clientKey: "curl"
|
|
605
|
+
},
|
|
606
|
+
documentDownloadType: "json"
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
//#endregion
|
|
611
|
+
export { buildNextOpenApiDocument, createNextApiReference, resolveApiReferenceConfig, withNextApiReferenceBanner };
|
package/dist/config.mjs
CHANGED
|
@@ -51,10 +51,11 @@ const DOCS_LAYOUT_TEMPLATE = `\
|
|
|
51
51
|
${GENERATED_BANNER}
|
|
52
52
|
import docsConfig from "@/docs.config";
|
|
53
53
|
import { createDocsLayout, createDocsMetadata } from "@farming-labs/theme";
|
|
54
|
+
import { withNextApiReferenceBanner } from "@farming-labs/next/api-reference";
|
|
54
55
|
|
|
55
56
|
export const metadata = createDocsMetadata(docsConfig);
|
|
56
57
|
|
|
57
|
-
const DocsLayout = createDocsLayout(docsConfig);
|
|
58
|
+
const DocsLayout = createDocsLayout(withNextApiReferenceBanner(docsConfig));
|
|
58
59
|
|
|
59
60
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
|
60
61
|
return (
|
|
@@ -74,6 +75,15 @@ export const { GET, POST } = createDocsAPI({
|
|
|
74
75
|
i18n: docsConfig.i18n,
|
|
75
76
|
});
|
|
76
77
|
|
|
78
|
+
export const revalidate = false;
|
|
79
|
+
`;
|
|
80
|
+
const API_REFERENCE_ROUTE_TEMPLATE = `\
|
|
81
|
+
${GENERATED_BANNER}
|
|
82
|
+
import docsConfig from "@/docs.config";
|
|
83
|
+
import { createNextApiReference } from "@farming-labs/next/api-reference";
|
|
84
|
+
|
|
85
|
+
export const GET = createNextApiReference(docsConfig);
|
|
86
|
+
|
|
77
87
|
export const revalidate = false;
|
|
78
88
|
`;
|
|
79
89
|
const FILE_EXTS = [
|
|
@@ -106,6 +116,63 @@ function readOgEndpoint(root) {
|
|
|
106
116
|
} catch {}
|
|
107
117
|
}
|
|
108
118
|
}
|
|
119
|
+
function readApiReferenceConfig(root) {
|
|
120
|
+
for (const ext of FILE_EXTS) {
|
|
121
|
+
const configPath = join(root, `docs.config.${ext}`);
|
|
122
|
+
if (!existsSync(configPath)) continue;
|
|
123
|
+
try {
|
|
124
|
+
const content = readFileSync(configPath, "utf-8");
|
|
125
|
+
if (content.match(/apiReference\s*:\s*false/)) return {
|
|
126
|
+
enabled: false,
|
|
127
|
+
path: "api-reference",
|
|
128
|
+
routeRoot: "api"
|
|
129
|
+
};
|
|
130
|
+
if (content.match(/apiReference\s*:\s*true/)) return {
|
|
131
|
+
enabled: true,
|
|
132
|
+
path: "api-reference",
|
|
133
|
+
routeRoot: "api"
|
|
134
|
+
};
|
|
135
|
+
const block = extractObjectLiteral(content, "apiReference");
|
|
136
|
+
if (!block) continue;
|
|
137
|
+
const enabledMatch = block.match(/enabled\s*:\s*(true|false)/);
|
|
138
|
+
const pathMatch = block.match(/path\s*:\s*["']([^"']+)["']/);
|
|
139
|
+
const routeRootMatch = block.match(/routeRoot\s*:\s*["']([^"']+)["']/);
|
|
140
|
+
return {
|
|
141
|
+
enabled: enabledMatch ? enabledMatch[1] !== "false" : true,
|
|
142
|
+
path: pathMatch?.[1]?.replace(/^\/+|\/+$/g, "") || "api-reference",
|
|
143
|
+
routeRoot: routeRootMatch?.[1]?.replace(/^\/+|\/+$/g, "") || "api"
|
|
144
|
+
};
|
|
145
|
+
} catch {
|
|
146
|
+
return {
|
|
147
|
+
enabled: false,
|
|
148
|
+
path: "api-reference",
|
|
149
|
+
routeRoot: "api"
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
enabled: false,
|
|
155
|
+
path: "api-reference",
|
|
156
|
+
routeRoot: "api"
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function extractObjectLiteral(content, key) {
|
|
160
|
+
const keyIndex = content.search(new RegExp(`${key}\\s*:\\s*\\{`));
|
|
161
|
+
if (keyIndex === -1) return void 0;
|
|
162
|
+
const braceStart = content.indexOf("{", keyIndex);
|
|
163
|
+
if (braceStart === -1) return void 0;
|
|
164
|
+
let depth = 0;
|
|
165
|
+
for (let index = braceStart; index < content.length; index += 1) {
|
|
166
|
+
const char = content[index];
|
|
167
|
+
if (char === "{") {
|
|
168
|
+
depth += 1;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (char !== "}") continue;
|
|
172
|
+
depth -= 1;
|
|
173
|
+
if (depth === 0) return content.slice(braceStart + 1, index);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
109
176
|
function withDocs(nextConfig = {}) {
|
|
110
177
|
const root = process.cwd();
|
|
111
178
|
if (!hasFile(root, "mdx-components")) writeFileSync(join(root, "mdx-components.tsx"), MDX_COMPONENTS_TEMPLATE);
|
|
@@ -123,6 +190,14 @@ function withDocs(nextConfig = {}) {
|
|
|
123
190
|
mkdirSync(docsApiRouteDir, { recursive: true });
|
|
124
191
|
writeFileSync(join(docsApiRouteDir, "route.ts"), DOCS_API_ROUTE_TEMPLATE);
|
|
125
192
|
}
|
|
193
|
+
const apiReference = readApiReferenceConfig(root);
|
|
194
|
+
if (apiReference.enabled && !isStaticExport) {
|
|
195
|
+
const apiReferenceRouteDir = join(root, appDir, ...apiReference.path.split("/"), "[[...slug]]");
|
|
196
|
+
if (!hasFile(apiReferenceRouteDir, "route")) {
|
|
197
|
+
mkdirSync(apiReferenceRouteDir, { recursive: true });
|
|
198
|
+
writeFileSync(join(apiReferenceRouteDir, "route.ts"), API_REFERENCE_ROUTE_TEMPLATE);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
126
201
|
const ogEndpoint = readOgEndpoint(root);
|
|
127
202
|
const remarkPlugins = ["remark-gfm", "remark-frontmatter"];
|
|
128
203
|
if (ogEndpoint) remarkPlugins.push(["@farming-labs/next/mdx-plugins/remark-og", { endpoint: ogEndpoint }]);
|
package/dist/index.d.mts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
+
import { buildNextOpenApiDocument, createNextApiReference, resolveApiReferenceConfig, withNextApiReferenceBanner } from "./api-reference.mjs";
|
|
1
2
|
import { withDocs } from "./config.mjs";
|
|
2
|
-
export { withDocs };
|
|
3
|
+
export { buildNextOpenApiDocument, createNextApiReference, resolveApiReferenceConfig, withDocs, withNextApiReferenceBanner };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { withDocs } from "./config.mjs";
|
|
2
|
+
import { buildNextOpenApiDocument, createNextApiReference, resolveApiReferenceConfig, withNextApiReferenceBanner } from "./api-reference.mjs";
|
|
2
3
|
|
|
3
|
-
export { withDocs };
|
|
4
|
+
export { buildNextOpenApiDocument, createNextApiReference, resolveApiReferenceConfig, withDocs, withNextApiReferenceBanner };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farming-labs/next",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.43",
|
|
4
4
|
"description": "Next.js adapter for @farming-labs/docs — MDX config wrapper",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"docs",
|
|
@@ -22,6 +22,11 @@
|
|
|
22
22
|
"import": "./dist/index.mjs",
|
|
23
23
|
"default": "./dist/index.mjs"
|
|
24
24
|
},
|
|
25
|
+
"./api-reference": {
|
|
26
|
+
"types": "./dist/api-reference.d.mts",
|
|
27
|
+
"import": "./dist/api-reference.mjs",
|
|
28
|
+
"default": "./dist/api-reference.mjs"
|
|
29
|
+
},
|
|
25
30
|
"./config": {
|
|
26
31
|
"types": "./dist/config.d.mts",
|
|
27
32
|
"import": "./dist/config.mjs",
|
|
@@ -52,6 +57,7 @@
|
|
|
52
57
|
"@mdx-js/loader": "^3.1.0",
|
|
53
58
|
"@mdx-js/react": "^3.1.0",
|
|
54
59
|
"@next/mdx": "^16.1.6",
|
|
60
|
+
"@scalar/nextjs-api-reference": "0.10.3",
|
|
55
61
|
"fumadocs-core": "^16.6.1",
|
|
56
62
|
"remark-frontmatter": "^5.0.0",
|
|
57
63
|
"remark-gfm": "^4.0.1",
|
|
@@ -59,11 +65,16 @@
|
|
|
59
65
|
},
|
|
60
66
|
"devDependencies": {
|
|
61
67
|
"@types/node": "^22.10.0",
|
|
68
|
+
"@types/react": "^19.2.2",
|
|
62
69
|
"tsdown": "^0.20.3",
|
|
63
70
|
"typescript": "^5.9.3",
|
|
64
|
-
"vitest": "^3.2.4"
|
|
71
|
+
"vitest": "^3.2.4",
|
|
72
|
+
"@farming-labs/docs": "0.0.43",
|
|
73
|
+
"@farming-labs/theme": "0.0.43"
|
|
65
74
|
},
|
|
66
75
|
"peerDependencies": {
|
|
76
|
+
"@farming-labs/docs": ">=0.0.1",
|
|
77
|
+
"@farming-labs/theme": ">=0.0.1",
|
|
67
78
|
"next": ">=16.0.0",
|
|
68
79
|
"react": ">=19.2.0",
|
|
69
80
|
"react-dom": ">=19.2.0"
|