@farming-labs/next 0.1.1-beta.4 → 0.1.2
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 +488 -0
- package/dist/client-callbacks.d.mts +6 -0
- package/dist/client-callbacks.mjs +16 -0
- package/dist/config.d.mts +28 -0
- package/dist/config.mjs +322 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +4 -0
- package/dist/layout.d.mts +12 -0
- package/dist/layout.mjs +18 -0
- package/dist/mdx-plugins/rehype-code.d.mts +2 -0
- package/dist/mdx-plugins/rehype-code.mjs +3 -0
- package/dist/mdx-plugins/rehype-toc.d.mts +2 -0
- package/dist/mdx-plugins/rehype-toc.mjs +3 -0
- package/dist/mdx-plugins/remark-heading.d.mts +2 -0
- package/dist/mdx-plugins/remark-heading.mjs +3 -0
- package/dist/mdx-plugins/remark-og.d.mts +22 -0
- package/dist/mdx-plugins/remark-og.mjs +30 -0
- package/package.json +3 -3
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { resolveApiReferenceConfig } from "@farming-labs/docs/server";
|
|
2
|
+
import { DocsConfig } from "@farming-labs/docs";
|
|
3
|
+
|
|
4
|
+
//#region src/api-reference.d.ts
|
|
5
|
+
declare function buildNextOpenApiDocument(config: DocsConfig): Record<string, unknown>;
|
|
6
|
+
declare function withNextApiReferenceBanner(config: DocsConfig): DocsConfig;
|
|
7
|
+
declare function createNextApiReference(config: DocsConfig): () => Promise<Response>;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { buildNextOpenApiDocument, createNextApiReference, resolveApiReferenceConfig, withNextApiReferenceBanner };
|
|
@@ -0,0 +1,488 @@
|
|
|
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 { buildApiReferenceOpenApiDocumentAsync, buildApiReferencePageTitle, buildApiReferenceScalarCss, resolveApiReferenceConfig } from "@farming-labs/docs/server";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
|
|
7
|
+
//#region src/api-reference.tsx
|
|
8
|
+
const ROUTE_FILE_RE = /^route\.(ts|tsx|js|jsx)$/;
|
|
9
|
+
const METHOD_RE = /export\s+(?:async\s+function|function|const)\s+(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\b/g;
|
|
10
|
+
function getNextAppDir(root) {
|
|
11
|
+
if (existsSync(join(root, "src", "app"))) return "src/app";
|
|
12
|
+
return "app";
|
|
13
|
+
}
|
|
14
|
+
function normalizeRouteRoot(value) {
|
|
15
|
+
return value.replace(/^\/+|\/+$/g, "") || "api";
|
|
16
|
+
}
|
|
17
|
+
function normalizeExcludeMatcher(value) {
|
|
18
|
+
return value.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "").replace(/\.(ts|tsx|js|jsx)$/i, "").replace(/\/route$/i, "");
|
|
19
|
+
}
|
|
20
|
+
function getRoutePathBase(config) {
|
|
21
|
+
const routeRoot = normalizeRouteRoot(config.routeRoot);
|
|
22
|
+
if (routeRoot === "app" || routeRoot === "src/app") return "";
|
|
23
|
+
if (routeRoot.startsWith("app/")) return `/${routeRoot.slice(4)}`;
|
|
24
|
+
if (routeRoot.startsWith("src/app/")) return `/${routeRoot.slice(8)}`;
|
|
25
|
+
return `/${routeRoot}`;
|
|
26
|
+
}
|
|
27
|
+
function resolveNextApiRouteRoot(root, config) {
|
|
28
|
+
const routeRoot = normalizeRouteRoot(config.routeRoot);
|
|
29
|
+
if (routeRoot === "app" || routeRoot.startsWith("app/") || routeRoot === "src/app" || routeRoot.startsWith("src/app/")) return join(root, ...routeRoot.split("/"));
|
|
30
|
+
return join(root, getNextAppDir(root), ...routeRoot.split("/"));
|
|
31
|
+
}
|
|
32
|
+
function shouldExcludeRoute(excludes, routePath, relativeFile, relativeDir) {
|
|
33
|
+
if (excludes.length === 0) return false;
|
|
34
|
+
const normalizedRoutePath = normalizeExcludeMatcher(routePath);
|
|
35
|
+
const candidates = new Set([
|
|
36
|
+
normalizedRoutePath,
|
|
37
|
+
normalizeExcludeMatcher(routePath.replace(/^\/+/, "")),
|
|
38
|
+
normalizeExcludeMatcher(relativeFile),
|
|
39
|
+
normalizeExcludeMatcher(relativeDir)
|
|
40
|
+
]);
|
|
41
|
+
return excludes.some((entry) => candidates.has(entry));
|
|
42
|
+
}
|
|
43
|
+
function humanizeSegment(value) {
|
|
44
|
+
return value.replace(/^\[\[?\.{3}/, "").replace(/^\[/, "").replace(/\]\]?$/, "").replace(/^\$/, "").replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
|
45
|
+
}
|
|
46
|
+
function endpointSegmentFromFsSegment(value) {
|
|
47
|
+
if (value.startsWith("[[...") && value.endsWith("]]")) return `{${value.slice(5, -2)}}`;
|
|
48
|
+
if (value.startsWith("[...") && value.endsWith("]")) return `{${value.slice(4, -1)}}`;
|
|
49
|
+
if (value.startsWith("[") && value.endsWith("]")) return `{${value.slice(1, -1)}}`;
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function getPathParamName(value) {
|
|
53
|
+
if (value.startsWith("[[...") && value.endsWith("]]")) return value.slice(5, -2);
|
|
54
|
+
if (value.startsWith("[...") && value.endsWith("]")) return value.slice(4, -1);
|
|
55
|
+
if (value.startsWith("[") && value.endsWith("]")) return value.slice(1, -1);
|
|
56
|
+
}
|
|
57
|
+
function extractDocBlock(source) {
|
|
58
|
+
const match = source.match(/\/\*\*([\s\S]*?)\*\//);
|
|
59
|
+
if (!match) return {};
|
|
60
|
+
const lines = match[1].split("\n").map((line) => line.replace(/^\s*\*\s?/, "").trim()).filter(Boolean).filter((line) => !line.startsWith("@"));
|
|
61
|
+
if (lines.length === 0) return {};
|
|
62
|
+
return {
|
|
63
|
+
summary: lines[0],
|
|
64
|
+
description: lines.slice(1).join(" ")
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function extractMethods(source) {
|
|
68
|
+
const methods = /* @__PURE__ */ new Set();
|
|
69
|
+
for (const match of source.matchAll(METHOD_RE)) methods.add(match[1]);
|
|
70
|
+
return Array.from(methods);
|
|
71
|
+
}
|
|
72
|
+
function scanRouteFiles(dir) {
|
|
73
|
+
if (!existsSync(dir)) return [];
|
|
74
|
+
const results = [];
|
|
75
|
+
for (const name of readdirSync(dir)) {
|
|
76
|
+
const full = join(dir, name);
|
|
77
|
+
if (statSync(full).isDirectory()) {
|
|
78
|
+
results.push(...scanRouteFiles(full));
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (ROUTE_FILE_RE.test(name)) results.push(full);
|
|
82
|
+
}
|
|
83
|
+
return results;
|
|
84
|
+
}
|
|
85
|
+
function getDocsUrl(config) {
|
|
86
|
+
if (typeof config.nav?.url === "string") return config.nav.url;
|
|
87
|
+
return `/${config.entry ?? "docs"}`;
|
|
88
|
+
}
|
|
89
|
+
function getForcedMode(config) {
|
|
90
|
+
const toggle = config.themeToggle;
|
|
91
|
+
if (!toggle || typeof toggle !== "object") return void 0;
|
|
92
|
+
if (toggle.default === "dark") return "dark";
|
|
93
|
+
if (toggle.default === "light") return "light";
|
|
94
|
+
}
|
|
95
|
+
function isThemeToggleHidden(config) {
|
|
96
|
+
if (config.themeToggle === false) return true;
|
|
97
|
+
if (config.themeToggle && typeof config.themeToggle === "object") return config.themeToggle.enabled === false;
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
function buildPathParameters(fsSegments) {
|
|
101
|
+
const parameters = [];
|
|
102
|
+
for (const segment of fsSegments) {
|
|
103
|
+
const name = getPathParamName(segment);
|
|
104
|
+
if (!name) continue;
|
|
105
|
+
const optional = segment.startsWith("[[...");
|
|
106
|
+
parameters.push({
|
|
107
|
+
name,
|
|
108
|
+
in: "path",
|
|
109
|
+
required: !optional,
|
|
110
|
+
description: optional ? `${humanizeSegment(name)} catch-all parameter.` : `${humanizeSegment(name)} path parameter.`,
|
|
111
|
+
schema: { type: "string" }
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return parameters;
|
|
115
|
+
}
|
|
116
|
+
function buildApiReferenceRoutes(config) {
|
|
117
|
+
const apiReference = resolveApiReferenceConfig(config.apiReference);
|
|
118
|
+
if (!apiReference.enabled) return [];
|
|
119
|
+
const root = process.cwd();
|
|
120
|
+
const apiDir = resolveNextApiRouteRoot(root, apiReference);
|
|
121
|
+
const routePathBase = getRoutePathBase(apiReference);
|
|
122
|
+
const files = scanRouteFiles(apiDir);
|
|
123
|
+
const excludes = apiReference.exclude;
|
|
124
|
+
const routes = [];
|
|
125
|
+
for (const file of files) {
|
|
126
|
+
const source = readFileSync(file, "utf-8");
|
|
127
|
+
const methods = extractMethods(source);
|
|
128
|
+
if (methods.length === 0) continue;
|
|
129
|
+
const relativeFile = relative(apiDir, file).replace(/\\/g, "/");
|
|
130
|
+
const fsSegments = relativeFile.split("/").slice(0, -1).filter(Boolean);
|
|
131
|
+
const relativeDir = fsSegments.join("/");
|
|
132
|
+
const routeSegments = fsSegments.map(endpointSegmentFromFsSegment);
|
|
133
|
+
const routePath = `${routePathBase}${routeSegments.length > 0 ? `/${routeSegments.join("/")}` : ""}` || "/";
|
|
134
|
+
if (shouldExcludeRoute(excludes, routePath, relativeFile, relativeDir)) continue;
|
|
135
|
+
const docBlock = extractDocBlock(source);
|
|
136
|
+
const title = fsSegments.length > 0 ? humanizeSegment(fsSegments[fsSegments.length - 1]) : "Overview";
|
|
137
|
+
routes.push({
|
|
138
|
+
title,
|
|
139
|
+
summary: docBlock.summary ?? `${title} endpoint`,
|
|
140
|
+
description: docBlock.description,
|
|
141
|
+
routePath,
|
|
142
|
+
sourceFile: relative(root, file).replace(/\\/g, "/"),
|
|
143
|
+
methods,
|
|
144
|
+
segments: fsSegments,
|
|
145
|
+
tag: fsSegments.length > 0 ? humanizeSegment(fsSegments[0]) : "General",
|
|
146
|
+
parameters: buildPathParameters(fsSegments)
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return routes.sort((a, b) => a.routePath.localeCompare(b.routePath));
|
|
150
|
+
}
|
|
151
|
+
function createOperationId(route, method) {
|
|
152
|
+
return `${method.toLowerCase()}_${route.routePath.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, "")}`;
|
|
153
|
+
}
|
|
154
|
+
function buildRequestBody(method) {
|
|
155
|
+
if (![
|
|
156
|
+
"POST",
|
|
157
|
+
"PUT",
|
|
158
|
+
"PATCH"
|
|
159
|
+
].includes(method)) return void 0;
|
|
160
|
+
return {
|
|
161
|
+
required: method === "POST",
|
|
162
|
+
content: { "application/json": {
|
|
163
|
+
schema: {
|
|
164
|
+
type: "object",
|
|
165
|
+
additionalProperties: true
|
|
166
|
+
},
|
|
167
|
+
example: { example: true }
|
|
168
|
+
} }
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function buildResponses(method) {
|
|
172
|
+
return { "200": {
|
|
173
|
+
description: method === "DELETE" ? "Resource removed successfully." : "Request completed successfully.",
|
|
174
|
+
content: { "application/json": {
|
|
175
|
+
schema: {
|
|
176
|
+
type: "object",
|
|
177
|
+
additionalProperties: true
|
|
178
|
+
},
|
|
179
|
+
example: { ok: true }
|
|
180
|
+
} }
|
|
181
|
+
} };
|
|
182
|
+
}
|
|
183
|
+
function buildOpenApiPaths(routes) {
|
|
184
|
+
const paths = {};
|
|
185
|
+
for (const route of routes) {
|
|
186
|
+
const pathItem = {};
|
|
187
|
+
for (const method of route.methods) pathItem[method.toLowerCase()] = {
|
|
188
|
+
tags: [route.tag],
|
|
189
|
+
summary: route.summary,
|
|
190
|
+
description: route.description ?? route.summary,
|
|
191
|
+
operationId: createOperationId(route, method),
|
|
192
|
+
...route.parameters.length > 0 ? { parameters: route.parameters } : {},
|
|
193
|
+
...buildRequestBody(method) ? { requestBody: buildRequestBody(method) } : {},
|
|
194
|
+
responses: buildResponses(method),
|
|
195
|
+
"x-farming-labs-source": route.sourceFile
|
|
196
|
+
};
|
|
197
|
+
paths[route.routePath] = pathItem;
|
|
198
|
+
}
|
|
199
|
+
return paths;
|
|
200
|
+
}
|
|
201
|
+
function buildNextOpenApiDocument(config) {
|
|
202
|
+
if (resolveApiReferenceConfig(config.apiReference).specUrl) return {
|
|
203
|
+
openapi: "3.1.0",
|
|
204
|
+
info: {
|
|
205
|
+
title: "API Reference",
|
|
206
|
+
description: "Remote OpenAPI specs are resolved at request time through createNextApiReference().",
|
|
207
|
+
version: "0.0.0"
|
|
208
|
+
},
|
|
209
|
+
servers: [{ url: "/" }],
|
|
210
|
+
tags: [],
|
|
211
|
+
paths: {}
|
|
212
|
+
};
|
|
213
|
+
const routes = buildApiReferenceRoutes(config);
|
|
214
|
+
const tags = Array.from(new Set(routes.map((route) => route.tag))).map((name) => ({
|
|
215
|
+
name,
|
|
216
|
+
description: `${name} endpoints`
|
|
217
|
+
}));
|
|
218
|
+
return {
|
|
219
|
+
openapi: "3.1.0",
|
|
220
|
+
info: {
|
|
221
|
+
title: "API Reference",
|
|
222
|
+
description: config.metadata?.description ?? "Generated API reference from Next.js route handlers.",
|
|
223
|
+
version: "0.0.0"
|
|
224
|
+
},
|
|
225
|
+
servers: [{ url: "/" }],
|
|
226
|
+
tags,
|
|
227
|
+
paths: buildOpenApiPaths(routes)
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function DropdownIcon({ current, radius }) {
|
|
231
|
+
const label = current === "api" ? "</>" : "▣";
|
|
232
|
+
return /* @__PURE__ */ jsx("span", {
|
|
233
|
+
"aria-hidden": "true",
|
|
234
|
+
style: {
|
|
235
|
+
display: "inline-flex",
|
|
236
|
+
width: 20,
|
|
237
|
+
height: 20,
|
|
238
|
+
alignItems: "center",
|
|
239
|
+
justifyContent: "center",
|
|
240
|
+
borderRadius: radius,
|
|
241
|
+
border: "1px solid color-mix(in srgb, var(--color-fd-border, #2a2a2a) 100%, transparent)",
|
|
242
|
+
background: "color-mix(in srgb, var(--color-fd-card, #161616) 92%, transparent)",
|
|
243
|
+
color: "var(--color-fd-primary, currentColor)",
|
|
244
|
+
boxShadow: "0 0 0 1px color-mix(in srgb, var(--color-fd-border, #2a2a2a) 32%, transparent)",
|
|
245
|
+
fontSize: 9,
|
|
246
|
+
fontWeight: 700
|
|
247
|
+
},
|
|
248
|
+
children: label
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
function getApiReferenceSwitcherTheme(config) {
|
|
252
|
+
const themeName = config.theme?.name?.toLowerCase() ?? "";
|
|
253
|
+
const isPixelBorder = themeName.includes("pixel-border");
|
|
254
|
+
const isDarksharp = themeName.includes("darksharp");
|
|
255
|
+
const isShiny = themeName.includes("shiny");
|
|
256
|
+
const radius = config.theme?.ui?.radius ?? (isPixelBorder || isDarksharp ? "0px" : "var(--radius, 0.75rem)");
|
|
257
|
+
return {
|
|
258
|
+
cardRadius: radius,
|
|
259
|
+
iconRadius: radius,
|
|
260
|
+
backgroundImage: isPixelBorder ? "repeating-linear-gradient(-45deg, color-mix(in srgb, var(--color-fd-border) 10%, transparent), color-mix(in srgb, var(--color-fd-border) 10%, transparent) 1px, transparent 1px, transparent 6px)" : void 0,
|
|
261
|
+
boxShadow: isPixelBorder || isDarksharp ? "none" : isShiny ? "0 14px 40px color-mix(in srgb, var(--color-fd-border, #2a2a2a) 18%, transparent)" : "0 0 0 1px color-mix(in srgb, var(--color-fd-border, #2a2a2a) 32%, transparent)",
|
|
262
|
+
titleStyle: {
|
|
263
|
+
fontFamily: isPixelBorder ? "var(--fd-font-mono, var(--font-geist-mono, ui-monospace, monospace))" : void 0,
|
|
264
|
+
textTransform: isPixelBorder ? "uppercase" : void 0,
|
|
265
|
+
letterSpacing: isPixelBorder ? "0.08em" : void 0,
|
|
266
|
+
fontSize: isPixelBorder ? 12 : 14
|
|
267
|
+
},
|
|
268
|
+
descriptionStyle: {
|
|
269
|
+
fontFamily: isPixelBorder ? "var(--fd-font-mono, var(--font-geist-mono, ui-monospace, monospace))" : void 0,
|
|
270
|
+
textTransform: isPixelBorder ? "uppercase" : void 0,
|
|
271
|
+
letterSpacing: isPixelBorder ? "0.04em" : void 0,
|
|
272
|
+
fontSize: isPixelBorder ? 11 : 12,
|
|
273
|
+
opacity: isPixelBorder ? .74 : .62
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function SwitcherOption({ href, title, description, current, config }) {
|
|
278
|
+
const theme = getApiReferenceSwitcherTheme(config);
|
|
279
|
+
return /* @__PURE__ */ jsxs("a", {
|
|
280
|
+
href,
|
|
281
|
+
style: {
|
|
282
|
+
display: "grid",
|
|
283
|
+
gridTemplateColumns: "20px 1fr 14px",
|
|
284
|
+
gap: 12,
|
|
285
|
+
alignItems: "start",
|
|
286
|
+
padding: "11px 12px",
|
|
287
|
+
borderRadius: theme.cardRadius,
|
|
288
|
+
textDecoration: "none",
|
|
289
|
+
color: "inherit",
|
|
290
|
+
background: current ? "color-mix(in srgb, var(--color-fd-primary, #3a7) 10%, transparent)" : "transparent",
|
|
291
|
+
backgroundImage: !current ? theme.backgroundImage : void 0
|
|
292
|
+
},
|
|
293
|
+
children: [
|
|
294
|
+
/* @__PURE__ */ jsx("span", {
|
|
295
|
+
"aria-hidden": "true",
|
|
296
|
+
style: {
|
|
297
|
+
display: "inline-flex",
|
|
298
|
+
width: 20,
|
|
299
|
+
height: 20,
|
|
300
|
+
alignItems: "center",
|
|
301
|
+
justifyContent: "center",
|
|
302
|
+
borderRadius: theme.iconRadius,
|
|
303
|
+
border: "1px solid color-mix(in srgb, var(--color-fd-border, #2a2a2a) 100%, transparent)",
|
|
304
|
+
color: current ? "var(--color-fd-primary, currentColor)" : "var(--color-fd-muted-foreground, rgba(255,255,255,0.62))",
|
|
305
|
+
background: "color-mix(in srgb, var(--color-fd-card, #161616) 92%, transparent)",
|
|
306
|
+
boxShadow: "0 0 0 1px color-mix(in srgb, var(--color-fd-border, #2a2a2a) 32%, transparent)",
|
|
307
|
+
fontSize: 9,
|
|
308
|
+
fontWeight: 700
|
|
309
|
+
},
|
|
310
|
+
children: title === "API Reference" ? "</>" : "▣"
|
|
311
|
+
}),
|
|
312
|
+
/* @__PURE__ */ jsxs("span", {
|
|
313
|
+
style: {
|
|
314
|
+
display: "flex",
|
|
315
|
+
flexDirection: "column",
|
|
316
|
+
gap: 4
|
|
317
|
+
},
|
|
318
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
319
|
+
style: {
|
|
320
|
+
fontWeight: 600,
|
|
321
|
+
lineHeight: 1.25,
|
|
322
|
+
...theme.titleStyle
|
|
323
|
+
},
|
|
324
|
+
children: title
|
|
325
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
326
|
+
style: {
|
|
327
|
+
lineHeight: 1.4,
|
|
328
|
+
...theme.descriptionStyle
|
|
329
|
+
},
|
|
330
|
+
children: description
|
|
331
|
+
})]
|
|
332
|
+
}),
|
|
333
|
+
/* @__PURE__ */ jsx("span", {
|
|
334
|
+
"aria-hidden": "true",
|
|
335
|
+
style: {
|
|
336
|
+
fontSize: 12,
|
|
337
|
+
opacity: current ? 1 : 0,
|
|
338
|
+
color: "var(--color-fd-primary, currentColor)",
|
|
339
|
+
paddingTop: 2
|
|
340
|
+
},
|
|
341
|
+
children: "✓"
|
|
342
|
+
})
|
|
343
|
+
]
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
function ApiReferenceSwitcher({ docsUrl, apiUrl, current, config }) {
|
|
347
|
+
const currentLabel = current === "api" ? "API Reference" : "Documentation";
|
|
348
|
+
const theme = getApiReferenceSwitcherTheme(config);
|
|
349
|
+
return /* @__PURE__ */ jsxs("details", {
|
|
350
|
+
style: {
|
|
351
|
+
position: "relative",
|
|
352
|
+
marginBottom: 16,
|
|
353
|
+
borderRadius: theme.cardRadius,
|
|
354
|
+
border: "1px solid color-mix(in srgb, var(--color-fd-border, #2a2a2a) 100%, transparent)",
|
|
355
|
+
background: "color-mix(in srgb, var(--color-fd-card, #141414) 94%, transparent)",
|
|
356
|
+
boxShadow: theme.boxShadow,
|
|
357
|
+
overflow: "hidden",
|
|
358
|
+
backgroundImage: theme.backgroundImage
|
|
359
|
+
},
|
|
360
|
+
children: [/* @__PURE__ */ jsxs("summary", {
|
|
361
|
+
style: {
|
|
362
|
+
listStyle: "none",
|
|
363
|
+
display: "flex",
|
|
364
|
+
alignItems: "center",
|
|
365
|
+
justifyContent: "space-between",
|
|
366
|
+
gap: 10,
|
|
367
|
+
cursor: "pointer",
|
|
368
|
+
padding: "11px 13px",
|
|
369
|
+
background: "color-mix(in srgb, var(--color-fd-card, #202020) 96%, transparent)",
|
|
370
|
+
borderBottom: "1px solid color-mix(in srgb, var(--color-fd-border, #2a2a2a) 100%, transparent)"
|
|
371
|
+
},
|
|
372
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
373
|
+
style: {
|
|
374
|
+
display: "flex",
|
|
375
|
+
alignItems: "center",
|
|
376
|
+
gap: 10
|
|
377
|
+
},
|
|
378
|
+
children: [/* @__PURE__ */ jsx(DropdownIcon, {
|
|
379
|
+
current,
|
|
380
|
+
radius: theme.iconRadius
|
|
381
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
382
|
+
style: {
|
|
383
|
+
fontWeight: 600,
|
|
384
|
+
...theme.titleStyle
|
|
385
|
+
},
|
|
386
|
+
children: currentLabel
|
|
387
|
+
})]
|
|
388
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
389
|
+
"aria-hidden": "true",
|
|
390
|
+
style: {
|
|
391
|
+
fontSize: 11,
|
|
392
|
+
opacity: .56,
|
|
393
|
+
transform: "translateY(1px)"
|
|
394
|
+
},
|
|
395
|
+
children: "▿"
|
|
396
|
+
})]
|
|
397
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
398
|
+
style: {
|
|
399
|
+
display: "flex",
|
|
400
|
+
flexDirection: "column",
|
|
401
|
+
gap: 2,
|
|
402
|
+
padding: 8,
|
|
403
|
+
background: "color-mix(in srgb, var(--color-fd-card, #151515) 96%, transparent)"
|
|
404
|
+
},
|
|
405
|
+
children: [/* @__PURE__ */ jsx(SwitcherOption, {
|
|
406
|
+
href: docsUrl,
|
|
407
|
+
title: "Documentation",
|
|
408
|
+
description: "Markdown pages, guides, and concepts",
|
|
409
|
+
current: current === "docs",
|
|
410
|
+
config
|
|
411
|
+
}), /* @__PURE__ */ jsx(SwitcherOption, {
|
|
412
|
+
href: apiUrl,
|
|
413
|
+
title: "API Reference",
|
|
414
|
+
description: "Scalar-powered route handler reference",
|
|
415
|
+
current: current === "api",
|
|
416
|
+
config
|
|
417
|
+
})]
|
|
418
|
+
})]
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
function mergeBanner(existing, next) {
|
|
422
|
+
if (!existing) return next;
|
|
423
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
424
|
+
style: {
|
|
425
|
+
display: "flex",
|
|
426
|
+
flexDirection: "column",
|
|
427
|
+
gap: 12
|
|
428
|
+
},
|
|
429
|
+
children: [existing, next]
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
function withNextApiReferenceBanner(config) {
|
|
433
|
+
const apiReference = resolveApiReferenceConfig(config.apiReference);
|
|
434
|
+
if (!apiReference.enabled) return config;
|
|
435
|
+
if (config.sidebar === false) return config;
|
|
436
|
+
const switcher = /* @__PURE__ */ jsx(ApiReferenceSwitcher, {
|
|
437
|
+
docsUrl: getDocsUrl(config),
|
|
438
|
+
apiUrl: `/${apiReference.path}`,
|
|
439
|
+
current: "docs",
|
|
440
|
+
config
|
|
441
|
+
});
|
|
442
|
+
if (!config.sidebar || config.sidebar === true) return {
|
|
443
|
+
...config,
|
|
444
|
+
sidebar: { banner: switcher }
|
|
445
|
+
};
|
|
446
|
+
return {
|
|
447
|
+
...config,
|
|
448
|
+
sidebar: {
|
|
449
|
+
...config.sidebar,
|
|
450
|
+
banner: mergeBanner(config.sidebar.banner, switcher)
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function createNextApiReference(config) {
|
|
455
|
+
const apiReference = resolveApiReferenceConfig(config.apiReference);
|
|
456
|
+
return async () => {
|
|
457
|
+
if (!apiReference.enabled) return new Response("Not Found", { status: 404 });
|
|
458
|
+
const document = await buildApiReferenceOpenApiDocumentAsync(config, {
|
|
459
|
+
framework: "next",
|
|
460
|
+
rootDir: process.cwd()
|
|
461
|
+
});
|
|
462
|
+
return ApiReference({
|
|
463
|
+
pageTitle: buildApiReferencePageTitle(config, "API Reference"),
|
|
464
|
+
title: "API Reference",
|
|
465
|
+
content: document,
|
|
466
|
+
theme: "deepSpace",
|
|
467
|
+
layout: "modern",
|
|
468
|
+
darkMode: getForcedMode(config) === "dark" ? true : void 0,
|
|
469
|
+
forceDarkModeState: getForcedMode(config),
|
|
470
|
+
hideDarkModeToggle: isThemeToggleHidden(config),
|
|
471
|
+
customCss: buildApiReferenceScalarCss(config),
|
|
472
|
+
pathRouting: { basePath: `/${apiReference.path}` },
|
|
473
|
+
showSidebar: true,
|
|
474
|
+
defaultOpenFirstTag: true,
|
|
475
|
+
tagsSorter: "alpha",
|
|
476
|
+
operationsSorter: "alpha",
|
|
477
|
+
operationTitleSource: "summary",
|
|
478
|
+
defaultHttpClient: {
|
|
479
|
+
targetKey: "shell",
|
|
480
|
+
clientKey: "curl"
|
|
481
|
+
},
|
|
482
|
+
documentDownloadType: "json"
|
|
483
|
+
})();
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
//#endregion
|
|
488
|
+
export { buildNextOpenApiDocument, createNextApiReference, resolveApiReferenceConfig, withNextApiReferenceBanner };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
import docsConfig from "@farming-labs/next-internal-docs-config";
|
|
5
|
+
import { DocsClientHooks } from "@farming-labs/theme/client-hooks";
|
|
6
|
+
|
|
7
|
+
//#region src/client-callbacks.tsx
|
|
8
|
+
function DocsClientCallbacks() {
|
|
9
|
+
return /* @__PURE__ */ jsx(DocsClientHooks, {
|
|
10
|
+
onCopyClick: docsConfig.onCopyClick,
|
|
11
|
+
onFeedback: docsConfig.feedback && typeof docsConfig.feedback === "object" ? docsConfig.feedback.onFeedback : void 0
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
export { DocsClientCallbacks as default };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as next from "next";
|
|
2
|
+
|
|
3
|
+
//#region src/config.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Next.js config wrapper for @farming-labs/docs.
|
|
6
|
+
* Handles MDX compilation, frontmatter, syntax highlighting, TOC extraction,
|
|
7
|
+
* and auto-generates mdx-components.tsx + docs layout when missing.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // next.config.ts — this is all you need:
|
|
11
|
+
* import { withDocs } from "@farming-labs/next/config";
|
|
12
|
+
* export default withDocs();
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // With existing Next.js config
|
|
16
|
+
* import { withDocs } from "@farming-labs/next/config";
|
|
17
|
+
* export default withDocs({
|
|
18
|
+
* images: { remotePatterns: [{ hostname: "example.com" }] },
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // Full static export (Cloudflare Pages, etc.) — no server; API route is skipped.
|
|
23
|
+
* // In docs.config set ai.enabled: false and staticExport: true.
|
|
24
|
+
* export default withDocs({ output: "export" });
|
|
25
|
+
*/
|
|
26
|
+
declare function withDocs(nextConfig?: Record<string, unknown>): next.NextConfig;
|
|
27
|
+
//#endregion
|
|
28
|
+
export { withDocs };
|
package/dist/config.mjs
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import createMDX from "@next/mdx";
|
|
4
|
+
|
|
5
|
+
//#region src/config.ts
|
|
6
|
+
/**
|
|
7
|
+
* Next.js config wrapper for @farming-labs/docs.
|
|
8
|
+
* Handles MDX compilation, frontmatter, syntax highlighting, TOC extraction,
|
|
9
|
+
* and auto-generates mdx-components.tsx + docs layout when missing.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // next.config.ts — this is all you need:
|
|
13
|
+
* import { withDocs } from "@farming-labs/next/config";
|
|
14
|
+
* export default withDocs();
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // With existing Next.js config
|
|
18
|
+
* import { withDocs } from "@farming-labs/next/config";
|
|
19
|
+
* export default withDocs({
|
|
20
|
+
* images: { remotePatterns: [{ hostname: "example.com" }] },
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // Full static export (Cloudflare Pages, etc.) — no server; API route is skipped.
|
|
25
|
+
* // In docs.config set ai.enabled: false and staticExport: true.
|
|
26
|
+
* export default withDocs({ output: "export" });
|
|
27
|
+
*/
|
|
28
|
+
/** Resolve Next.js App Router directory: prefer src/app when present, else app. */
|
|
29
|
+
function getNextAppDir(root) {
|
|
30
|
+
if (existsSync(join(root, "src", "app"))) return "src/app";
|
|
31
|
+
return "app";
|
|
32
|
+
}
|
|
33
|
+
const GENERATED_BANNER = "// Auto-generated by @farming-labs/next — do not edit manually.\n";
|
|
34
|
+
const MDX_COMPONENTS_TEMPLATE = `\
|
|
35
|
+
${GENERATED_BANNER}
|
|
36
|
+
import { getMDXComponents } from "@farming-labs/theme/mdx";
|
|
37
|
+
import type { MDXComponents } from "mdx/types";
|
|
38
|
+
import docsConfig from "@/docs.config";
|
|
39
|
+
|
|
40
|
+
export function useMDXComponents(components?: MDXComponents): MDXComponents {
|
|
41
|
+
return getMDXComponents(
|
|
42
|
+
{
|
|
43
|
+
...(docsConfig.components as MDXComponents),
|
|
44
|
+
...components,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
onCopyClick: docsConfig.onCopyClick,
|
|
48
|
+
theme: docsConfig.theme,
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
`;
|
|
53
|
+
const DOCS_LAYOUT_TEMPLATE = `\
|
|
54
|
+
${GENERATED_BANNER}
|
|
55
|
+
import docsConfig from "@/docs.config";
|
|
56
|
+
import { createNextDocsLayout, createNextDocsMetadata } from "@farming-labs/next/layout";
|
|
57
|
+
|
|
58
|
+
export const metadata = createNextDocsMetadata(docsConfig);
|
|
59
|
+
|
|
60
|
+
const DocsLayout = createNextDocsLayout(docsConfig);
|
|
61
|
+
|
|
62
|
+
export default function Layout({ children }: { children: React.ReactNode }) {
|
|
63
|
+
return <DocsLayout>{children}</DocsLayout>;
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
66
|
+
const DOCS_API_ROUTE_TEMPLATE = `\
|
|
67
|
+
${GENERATED_BANNER}
|
|
68
|
+
import docsConfig from "@/docs.config";
|
|
69
|
+
import { createDocsAPI } from "@farming-labs/theme/api";
|
|
70
|
+
|
|
71
|
+
export const { GET, POST } = createDocsAPI({
|
|
72
|
+
entry: docsConfig.entry,
|
|
73
|
+
i18n: docsConfig.i18n,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export const revalidate = false;
|
|
77
|
+
`;
|
|
78
|
+
const DOCS_MCP_ROUTE_TEMPLATE = `\
|
|
79
|
+
${GENERATED_BANNER}
|
|
80
|
+
import docsConfig from "@/docs.config";
|
|
81
|
+
import { createDocsMCPAPI } from "@farming-labs/theme/api";
|
|
82
|
+
|
|
83
|
+
export const { GET, POST, DELETE } = createDocsMCPAPI({
|
|
84
|
+
entry: docsConfig.entry,
|
|
85
|
+
contentDir: docsConfig.contentDir,
|
|
86
|
+
nav: docsConfig.nav,
|
|
87
|
+
ordering: docsConfig.ordering,
|
|
88
|
+
mcp: docsConfig.mcp,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export const revalidate = false;
|
|
92
|
+
`;
|
|
93
|
+
const API_REFERENCE_ROUTE_TEMPLATE = `\
|
|
94
|
+
${GENERATED_BANNER}
|
|
95
|
+
import docsConfig from "@/docs.config";
|
|
96
|
+
import { createNextApiReference } from "@farming-labs/next/api-reference";
|
|
97
|
+
|
|
98
|
+
export const GET = createNextApiReference(docsConfig);
|
|
99
|
+
|
|
100
|
+
export const revalidate = false;
|
|
101
|
+
`;
|
|
102
|
+
const FILE_EXTS = [
|
|
103
|
+
"tsx",
|
|
104
|
+
"ts",
|
|
105
|
+
"jsx",
|
|
106
|
+
"js"
|
|
107
|
+
];
|
|
108
|
+
const INTERNAL_DOCS_CONFIG_ALIAS = "@farming-labs/next-internal-docs-config";
|
|
109
|
+
function hasFile(root, baseName) {
|
|
110
|
+
return FILE_EXTS.some((ext) => existsSync(join(root, `${baseName}.${ext}`)));
|
|
111
|
+
}
|
|
112
|
+
function isManagedGeneratedFile(filePath) {
|
|
113
|
+
if (!existsSync(filePath)) return false;
|
|
114
|
+
try {
|
|
115
|
+
return readFileSync(filePath, "utf-8").startsWith(GENERATED_BANNER);
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Read the docs entry path from docs.config.ts[x] (defaults to "docs"). */
|
|
121
|
+
function readDocsEntry(root) {
|
|
122
|
+
for (const ext of FILE_EXTS) {
|
|
123
|
+
const configPath = join(root, `docs.config.${ext}`);
|
|
124
|
+
if (existsSync(configPath)) try {
|
|
125
|
+
const match = readFileSync(configPath, "utf-8").match(/entry\s*:\s*["']([^"']+)["']/);
|
|
126
|
+
if (match) return match[1];
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
return "docs";
|
|
130
|
+
}
|
|
131
|
+
function readDocsConfigPath(root) {
|
|
132
|
+
for (const ext of FILE_EXTS) {
|
|
133
|
+
const relativePath = `docs.config.${ext}`;
|
|
134
|
+
if (existsSync(join(root, relativePath))) return relativePath;
|
|
135
|
+
}
|
|
136
|
+
return "docs.config.ts";
|
|
137
|
+
}
|
|
138
|
+
/** Read the OG endpoint from docs.config.ts[x] (returns undefined if not set). */
|
|
139
|
+
function readOgEndpoint(root) {
|
|
140
|
+
for (const ext of FILE_EXTS) {
|
|
141
|
+
const configPath = join(root, `docs.config.${ext}`);
|
|
142
|
+
if (existsSync(configPath)) try {
|
|
143
|
+
const match = readFileSync(configPath, "utf-8").match(/endpoint\s*:\s*["']([^"']+)["']/);
|
|
144
|
+
if (match && match[1]) return match[1];
|
|
145
|
+
} catch {}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function readApiReferenceConfig(root) {
|
|
149
|
+
for (const ext of FILE_EXTS) {
|
|
150
|
+
const configPath = join(root, `docs.config.${ext}`);
|
|
151
|
+
if (!existsSync(configPath)) continue;
|
|
152
|
+
try {
|
|
153
|
+
const content = readFileSync(configPath, "utf-8");
|
|
154
|
+
if (content.match(/apiReference\s*:\s*false/)) return {
|
|
155
|
+
enabled: false,
|
|
156
|
+
path: "api-reference",
|
|
157
|
+
routeRoot: "api"
|
|
158
|
+
};
|
|
159
|
+
if (content.match(/apiReference\s*:\s*true/)) return {
|
|
160
|
+
enabled: true,
|
|
161
|
+
path: "api-reference",
|
|
162
|
+
routeRoot: "api"
|
|
163
|
+
};
|
|
164
|
+
const block = extractObjectLiteral(content, "apiReference");
|
|
165
|
+
if (!block) continue;
|
|
166
|
+
const enabledMatch = block.match(/enabled\s*:\s*(true|false)/);
|
|
167
|
+
const pathMatch = block.match(/path\s*:\s*["']([^"']+)["']/);
|
|
168
|
+
const routeRootMatch = block.match(/routeRoot\s*:\s*["']([^"']+)["']/);
|
|
169
|
+
return {
|
|
170
|
+
enabled: enabledMatch ? enabledMatch[1] !== "false" : true,
|
|
171
|
+
path: pathMatch?.[1]?.replace(/^\/+|\/+$/g, "") || "api-reference",
|
|
172
|
+
routeRoot: routeRootMatch?.[1]?.replace(/^\/+|\/+$/g, "") || "api"
|
|
173
|
+
};
|
|
174
|
+
} catch {
|
|
175
|
+
return {
|
|
176
|
+
enabled: false,
|
|
177
|
+
path: "api-reference",
|
|
178
|
+
routeRoot: "api"
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
enabled: false,
|
|
184
|
+
path: "api-reference",
|
|
185
|
+
routeRoot: "api"
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function extractObjectLiteral(content, key) {
|
|
189
|
+
const keyIndex = content.search(new RegExp(`${key}\\s*:\\s*\\{`));
|
|
190
|
+
if (keyIndex === -1) return void 0;
|
|
191
|
+
const braceStart = content.indexOf("{", keyIndex);
|
|
192
|
+
if (braceStart === -1) return void 0;
|
|
193
|
+
let depth = 0;
|
|
194
|
+
for (let index = braceStart; index < content.length; index += 1) {
|
|
195
|
+
const char = content[index];
|
|
196
|
+
if (char === "{") {
|
|
197
|
+
depth += 1;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (char !== "}") continue;
|
|
201
|
+
depth -= 1;
|
|
202
|
+
if (depth === 0) return content.slice(braceStart + 1, index);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function readMcpConfig(root) {
|
|
206
|
+
for (const ext of FILE_EXTS) {
|
|
207
|
+
const configPath = join(root, `docs.config.${ext}`);
|
|
208
|
+
if (!existsSync(configPath)) continue;
|
|
209
|
+
try {
|
|
210
|
+
const content = readFileSync(configPath, "utf-8");
|
|
211
|
+
if (content.match(/mcp\s*:\s*false/)) return {
|
|
212
|
+
enabled: false,
|
|
213
|
+
route: "/api/docs/mcp"
|
|
214
|
+
};
|
|
215
|
+
if (content.match(/mcp\s*:\s*true/)) return {
|
|
216
|
+
enabled: true,
|
|
217
|
+
route: "/api/docs/mcp"
|
|
218
|
+
};
|
|
219
|
+
const block = extractObjectLiteral(content, "mcp");
|
|
220
|
+
if (!block) continue;
|
|
221
|
+
const enabledMatch = block.match(/enabled\s*:\s*(true|false)/);
|
|
222
|
+
const routeMatch = block.match(/route\s*:\s*["']([^"']+)["']/);
|
|
223
|
+
return {
|
|
224
|
+
enabled: enabledMatch ? enabledMatch[1] !== "false" : true,
|
|
225
|
+
route: normalizeRoutePath(routeMatch?.[1] ?? "/api/docs/mcp")
|
|
226
|
+
};
|
|
227
|
+
} catch {
|
|
228
|
+
return {
|
|
229
|
+
enabled: false,
|
|
230
|
+
route: "/api/docs/mcp"
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
enabled: false,
|
|
236
|
+
route: "/api/docs/mcp"
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function normalizeRoutePath(route) {
|
|
240
|
+
const normalized = `/${route}`.replace(/\/+/g, "/");
|
|
241
|
+
return normalized !== "/" ? normalized.replace(/\/+$/, "") : "/api/docs/mcp";
|
|
242
|
+
}
|
|
243
|
+
function withDocs(nextConfig = {}) {
|
|
244
|
+
const root = process.cwd();
|
|
245
|
+
const docsConfigPath = readDocsConfigPath(root);
|
|
246
|
+
const docsConfigAbsolutePath = join(root, docsConfigPath);
|
|
247
|
+
const docsConfigRelativeAlias = docsConfigPath.startsWith("./") || docsConfigPath.startsWith("../") ? docsConfigPath : `./${docsConfigPath}`;
|
|
248
|
+
if (!hasFile(root, "mdx-components")) writeFileSync(join(root, "mdx-components.tsx"), MDX_COMPONENTS_TEMPLATE);
|
|
249
|
+
const entry = readDocsEntry(root);
|
|
250
|
+
const appDir = getNextAppDir(root);
|
|
251
|
+
const layoutDir = join(root, appDir, entry);
|
|
252
|
+
if (!existsSync(layoutDir)) mkdirSync(layoutDir, { recursive: true });
|
|
253
|
+
const docsLayoutPath = join(layoutDir, "layout.tsx");
|
|
254
|
+
if (!hasFile(layoutDir, "layout") || isManagedGeneratedFile(docsLayoutPath)) writeFileSync(join(layoutDir, "layout.tsx"), DOCS_LAYOUT_TEMPLATE);
|
|
255
|
+
const isStaticExport = nextConfig.output === "export";
|
|
256
|
+
const docsApiRouteDir = join(root, appDir, "api", "docs");
|
|
257
|
+
if (!isStaticExport && !hasFile(docsApiRouteDir, "route")) {
|
|
258
|
+
mkdirSync(docsApiRouteDir, { recursive: true });
|
|
259
|
+
writeFileSync(join(docsApiRouteDir, "route.ts"), DOCS_API_ROUTE_TEMPLATE);
|
|
260
|
+
}
|
|
261
|
+
const mcp = readMcpConfig(root);
|
|
262
|
+
const docsMcpRouteDir = join(root, appDir, "api", "docs", "mcp");
|
|
263
|
+
if (mcp.enabled && mcp.route === "/api/docs/mcp" && !isStaticExport && !hasFile(docsMcpRouteDir, "route")) {
|
|
264
|
+
mkdirSync(docsMcpRouteDir, { recursive: true });
|
|
265
|
+
writeFileSync(join(docsMcpRouteDir, "route.ts"), DOCS_MCP_ROUTE_TEMPLATE);
|
|
266
|
+
}
|
|
267
|
+
const apiReference = readApiReferenceConfig(root);
|
|
268
|
+
if (apiReference.enabled && !isStaticExport) {
|
|
269
|
+
const apiReferenceRouteDir = join(root, appDir, ...apiReference.path.split("/"), "[[...slug]]");
|
|
270
|
+
if (!hasFile(apiReferenceRouteDir, "route")) {
|
|
271
|
+
mkdirSync(apiReferenceRouteDir, { recursive: true });
|
|
272
|
+
writeFileSync(join(apiReferenceRouteDir, "route.ts"), API_REFERENCE_ROUTE_TEMPLATE);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const ogEndpoint = readOgEndpoint(root);
|
|
276
|
+
const remarkPlugins = ["remark-gfm", "remark-frontmatter"];
|
|
277
|
+
if (ogEndpoint) remarkPlugins.push(["@farming-labs/next/mdx-plugins/remark-og", { endpoint: ogEndpoint }]);
|
|
278
|
+
remarkPlugins.push(["remark-mdx-frontmatter", { name: "metadata" }], "@farming-labs/next/mdx-plugins/remark-heading");
|
|
279
|
+
const withMDX = createMDX({
|
|
280
|
+
extension: /\.mdx?$/,
|
|
281
|
+
options: {
|
|
282
|
+
remarkPlugins,
|
|
283
|
+
rehypePlugins: ["@farming-labs/next/mdx-plugins/rehype-toc", ["@farming-labs/next/mdx-plugins/rehype-code", { themes: {
|
|
284
|
+
dark: "github-dark",
|
|
285
|
+
light: "github-light"
|
|
286
|
+
} }]]
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
const defaultExts = [
|
|
290
|
+
"js",
|
|
291
|
+
"jsx",
|
|
292
|
+
"md",
|
|
293
|
+
"mdx",
|
|
294
|
+
"ts",
|
|
295
|
+
"tsx"
|
|
296
|
+
];
|
|
297
|
+
const userExts = nextConfig.pageExtensions;
|
|
298
|
+
if (userExts) {
|
|
299
|
+
for (const ext of ["md", "mdx"]) if (!userExts.includes(ext)) userExts.push(ext);
|
|
300
|
+
} else nextConfig.pageExtensions = defaultExts;
|
|
301
|
+
const existingTurbopack = nextConfig.turbopack ?? {};
|
|
302
|
+
const existingResolveAlias = existingTurbopack.resolveAlias ?? {};
|
|
303
|
+
nextConfig.turbopack = {
|
|
304
|
+
...existingTurbopack,
|
|
305
|
+
resolveAlias: {
|
|
306
|
+
...existingResolveAlias,
|
|
307
|
+
[INTERNAL_DOCS_CONFIG_ALIAS]: docsConfigRelativeAlias
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
const userWebpack = nextConfig.webpack;
|
|
311
|
+
nextConfig.webpack = (config, options) => {
|
|
312
|
+
const resolvedConfig = userWebpack ? userWebpack(config, options) : config;
|
|
313
|
+
resolvedConfig.resolve ??= {};
|
|
314
|
+
resolvedConfig.resolve.alias ??= {};
|
|
315
|
+
resolvedConfig.resolve.alias[INTERNAL_DOCS_CONFIG_ALIAS] = docsConfigAbsolutePath;
|
|
316
|
+
return resolvedConfig;
|
|
317
|
+
};
|
|
318
|
+
return withMDX(nextConfig);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
//#endregion
|
|
322
|
+
export { withDocs };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { buildNextOpenApiDocument, createNextApiReference, resolveApiReferenceConfig, withNextApiReferenceBanner } from "./api-reference.mjs";
|
|
2
|
+
import { withDocs } from "./config.mjs";
|
|
3
|
+
export { buildNextOpenApiDocument, createNextApiReference, resolveApiReferenceConfig, withDocs, withNextApiReferenceBanner };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { withDocs } from "./config.mjs";
|
|
2
|
+
import { buildNextOpenApiDocument, createNextApiReference, resolveApiReferenceConfig, withNextApiReferenceBanner } from "./api-reference.mjs";
|
|
3
|
+
|
|
4
|
+
export { buildNextOpenApiDocument, createNextApiReference, resolveApiReferenceConfig, withDocs, withNextApiReferenceBanner };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
2
|
+
import { DocsConfig } from "@farming-labs/docs";
|
|
3
|
+
|
|
4
|
+
//#region src/layout.d.ts
|
|
5
|
+
declare function createNextDocsMetadata(config: DocsConfig): Record<string, unknown>;
|
|
6
|
+
declare function createNextDocsLayout(config: DocsConfig): ({
|
|
7
|
+
children
|
|
8
|
+
}: {
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
}) => react_jsx_runtime0.JSX.Element;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { createNextDocsLayout, createNextDocsMetadata };
|
package/dist/layout.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { withNextApiReferenceBanner } from "./api-reference.mjs";
|
|
2
|
+
import DocsClientCallbacks from "./client-callbacks.mjs";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { createDocsLayout, createDocsMetadata } from "@farming-labs/theme";
|
|
5
|
+
|
|
6
|
+
//#region src/layout.tsx
|
|
7
|
+
function createNextDocsMetadata(config) {
|
|
8
|
+
return createDocsMetadata(config);
|
|
9
|
+
}
|
|
10
|
+
function createNextDocsLayout(config) {
|
|
11
|
+
const DocsLayout = createDocsLayout(withNextApiReferenceBanner(config));
|
|
12
|
+
return function NextDocsLayout({ children }) {
|
|
13
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(DocsClientCallbacks, {}), /* @__PURE__ */ jsx(DocsLayout, { children })] });
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
export { createNextDocsLayout, createNextDocsMetadata };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/mdx-plugins/remark-og.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Remark plugin that augments frontmatter with Open Graph metadata.
|
|
4
|
+
*
|
|
5
|
+
* Runs between remark-frontmatter and remark-mdx-frontmatter.
|
|
6
|
+
* Reads title/description from the YAML node and appends openGraph + twitter
|
|
7
|
+
* fields so that remark-mdx-frontmatter exports them as part of `metadata`.
|
|
8
|
+
*
|
|
9
|
+
* Skips injection when frontmatter already has `openGraph:` or `ogImage:`, so
|
|
10
|
+
* pages can use static OG images from frontmatter instead of the dynamic endpoint.
|
|
11
|
+
*/
|
|
12
|
+
interface RemarkOgOptions {
|
|
13
|
+
endpoint?: string;
|
|
14
|
+
}
|
|
15
|
+
declare function remarkOg(options?: RemarkOgOptions): (tree: {
|
|
16
|
+
children: Array<{
|
|
17
|
+
type: string;
|
|
18
|
+
value: string;
|
|
19
|
+
}>;
|
|
20
|
+
}) => void;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { remarkOg as default };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/mdx-plugins/remark-og.ts
|
|
2
|
+
function extractField(yaml, key) {
|
|
3
|
+
const re = new RegExp(`^${key}:\\s*(?:"([^"]*?)"|'([^']*?)'|(.+?))\\s*$`, "m");
|
|
4
|
+
const m = yaml.match(re);
|
|
5
|
+
return m?.[1] ?? m?.[2] ?? m?.[3];
|
|
6
|
+
}
|
|
7
|
+
/** True if the YAML already defines openGraph or ogImage (static OG). */
|
|
8
|
+
function hasStaticOg(yaml) {
|
|
9
|
+
return /^\s*openGraph\s*:/m.test(yaml) || /^\s*ogImage\s*:/m.test(yaml);
|
|
10
|
+
}
|
|
11
|
+
function remarkOg(options = {}) {
|
|
12
|
+
const { endpoint = "/api/og" } = options;
|
|
13
|
+
return (tree) => {
|
|
14
|
+
const yamlNode = tree.children.find((n) => n.type === "yaml");
|
|
15
|
+
if (!yamlNode) return;
|
|
16
|
+
if (hasStaticOg(yamlNode.value)) return;
|
|
17
|
+
const title = extractField(yamlNode.value, "title");
|
|
18
|
+
if (!title) return;
|
|
19
|
+
const description = extractField(yamlNode.value, "description");
|
|
20
|
+
const params = new URLSearchParams({ title });
|
|
21
|
+
if (description) params.set("description", description);
|
|
22
|
+
const ogUrl = `${endpoint}?${params.toString()}`;
|
|
23
|
+
yamlNode.value += `
|
|
24
|
+
openGraph:
|
|
25
|
+
images:\n - url: "${ogUrl}"\n width: 1200\n height: 630\ntwitter:\n card: "summary_large_image"\n images:\n - "${ogUrl}"`;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
export { remarkOg as default };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farming-labs/next",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Next.js adapter for @farming-labs/docs — MDX config wrapper",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"docs",
|
|
@@ -79,8 +79,8 @@
|
|
|
79
79
|
"tsdown": "^0.20.3",
|
|
80
80
|
"typescript": "^5.9.3",
|
|
81
81
|
"vitest": "^3.2.4",
|
|
82
|
-
"@farming-labs/docs": "0.1.
|
|
83
|
-
"@farming-labs/theme": "0.1.
|
|
82
|
+
"@farming-labs/docs": "0.1.2",
|
|
83
|
+
"@farming-labs/theme": "0.1.2"
|
|
84
84
|
},
|
|
85
85
|
"peerDependencies": {
|
|
86
86
|
"@farming-labs/docs": ">=0.0.1",
|