@farming-labs/next 0.1.1-beta.2 → 0.1.1-beta.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/next",
3
- "version": "0.1.1-beta.2",
3
+ "version": "0.1.1-beta.4",
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.1-beta.2",
83
- "@farming-labs/theme": "0.1.1-beta.2"
82
+ "@farming-labs/docs": "0.1.1-beta.4",
83
+ "@farming-labs/theme": "0.1.1-beta.4"
84
84
  },
85
85
  "peerDependencies": {
86
86
  "@farming-labs/docs": ">=0.0.1",
@@ -1,9 +0,0 @@
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 };
@@ -1,488 +0,0 @@
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 };
@@ -1,6 +0,0 @@
1
- import * as react_jsx_runtime0 from "react/jsx-runtime";
2
-
3
- //#region src/client-callbacks.d.ts
4
- declare function DocsClientCallbacks(): react_jsx_runtime0.JSX.Element;
5
- //#endregion
6
- export { DocsClientCallbacks as default };
@@ -1,16 +0,0 @@
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 };
package/dist/config.d.mts DELETED
@@ -1,28 +0,0 @@
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 DELETED
@@ -1,263 +0,0 @@
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 API_REFERENCE_ROUTE_TEMPLATE = `\
79
- ${GENERATED_BANNER}
80
- import docsConfig from "@/docs.config";
81
- import { createNextApiReference } from "@farming-labs/next/api-reference";
82
-
83
- export const GET = createNextApiReference(docsConfig);
84
-
85
- export const revalidate = false;
86
- `;
87
- const FILE_EXTS = [
88
- "tsx",
89
- "ts",
90
- "jsx",
91
- "js"
92
- ];
93
- const INTERNAL_DOCS_CONFIG_ALIAS = "@farming-labs/next-internal-docs-config";
94
- function hasFile(root, baseName) {
95
- return FILE_EXTS.some((ext) => existsSync(join(root, `${baseName}.${ext}`)));
96
- }
97
- function isManagedGeneratedFile(filePath) {
98
- if (!existsSync(filePath)) return false;
99
- try {
100
- return readFileSync(filePath, "utf-8").startsWith(GENERATED_BANNER);
101
- } catch {
102
- return false;
103
- }
104
- }
105
- /** Read the docs entry path from docs.config.ts[x] (defaults to "docs"). */
106
- function readDocsEntry(root) {
107
- for (const ext of FILE_EXTS) {
108
- const configPath = join(root, `docs.config.${ext}`);
109
- if (existsSync(configPath)) try {
110
- const match = readFileSync(configPath, "utf-8").match(/entry\s*:\s*["']([^"']+)["']/);
111
- if (match) return match[1];
112
- } catch {}
113
- }
114
- return "docs";
115
- }
116
- function readDocsConfigPath(root) {
117
- for (const ext of FILE_EXTS) {
118
- const relativePath = `docs.config.${ext}`;
119
- if (existsSync(join(root, relativePath))) return relativePath;
120
- }
121
- return "docs.config.ts";
122
- }
123
- /** Read the OG endpoint from docs.config.ts[x] (returns undefined if not set). */
124
- function readOgEndpoint(root) {
125
- for (const ext of FILE_EXTS) {
126
- const configPath = join(root, `docs.config.${ext}`);
127
- if (existsSync(configPath)) try {
128
- const match = readFileSync(configPath, "utf-8").match(/endpoint\s*:\s*["']([^"']+)["']/);
129
- if (match && match[1]) return match[1];
130
- } catch {}
131
- }
132
- }
133
- function readApiReferenceConfig(root) {
134
- for (const ext of FILE_EXTS) {
135
- const configPath = join(root, `docs.config.${ext}`);
136
- if (!existsSync(configPath)) continue;
137
- try {
138
- const content = readFileSync(configPath, "utf-8");
139
- if (content.match(/apiReference\s*:\s*false/)) return {
140
- enabled: false,
141
- path: "api-reference",
142
- routeRoot: "api"
143
- };
144
- if (content.match(/apiReference\s*:\s*true/)) return {
145
- enabled: true,
146
- path: "api-reference",
147
- routeRoot: "api"
148
- };
149
- const block = extractObjectLiteral(content, "apiReference");
150
- if (!block) continue;
151
- const enabledMatch = block.match(/enabled\s*:\s*(true|false)/);
152
- const pathMatch = block.match(/path\s*:\s*["']([^"']+)["']/);
153
- const routeRootMatch = block.match(/routeRoot\s*:\s*["']([^"']+)["']/);
154
- return {
155
- enabled: enabledMatch ? enabledMatch[1] !== "false" : true,
156
- path: pathMatch?.[1]?.replace(/^\/+|\/+$/g, "") || "api-reference",
157
- routeRoot: routeRootMatch?.[1]?.replace(/^\/+|\/+$/g, "") || "api"
158
- };
159
- } catch {
160
- return {
161
- enabled: false,
162
- path: "api-reference",
163
- routeRoot: "api"
164
- };
165
- }
166
- }
167
- return {
168
- enabled: false,
169
- path: "api-reference",
170
- routeRoot: "api"
171
- };
172
- }
173
- function extractObjectLiteral(content, key) {
174
- const keyIndex = content.search(new RegExp(`${key}\\s*:\\s*\\{`));
175
- if (keyIndex === -1) return void 0;
176
- const braceStart = content.indexOf("{", keyIndex);
177
- if (braceStart === -1) return void 0;
178
- let depth = 0;
179
- for (let index = braceStart; index < content.length; index += 1) {
180
- const char = content[index];
181
- if (char === "{") {
182
- depth += 1;
183
- continue;
184
- }
185
- if (char !== "}") continue;
186
- depth -= 1;
187
- if (depth === 0) return content.slice(braceStart + 1, index);
188
- }
189
- }
190
- function withDocs(nextConfig = {}) {
191
- const root = process.cwd();
192
- const docsConfigPath = readDocsConfigPath(root);
193
- const docsConfigAbsolutePath = join(root, docsConfigPath);
194
- const docsConfigRelativeAlias = docsConfigPath.startsWith("./") || docsConfigPath.startsWith("../") ? docsConfigPath : `./${docsConfigPath}`;
195
- if (!hasFile(root, "mdx-components")) writeFileSync(join(root, "mdx-components.tsx"), MDX_COMPONENTS_TEMPLATE);
196
- const entry = readDocsEntry(root);
197
- const appDir = getNextAppDir(root);
198
- const layoutDir = join(root, appDir, entry);
199
- if (!existsSync(layoutDir)) mkdirSync(layoutDir, { recursive: true });
200
- const docsLayoutPath = join(layoutDir, "layout.tsx");
201
- if (!hasFile(layoutDir, "layout") || isManagedGeneratedFile(docsLayoutPath)) writeFileSync(join(layoutDir, "layout.tsx"), DOCS_LAYOUT_TEMPLATE);
202
- const isStaticExport = nextConfig.output === "export";
203
- const docsApiRouteDir = join(root, appDir, "api", "docs");
204
- if (!isStaticExport && !hasFile(docsApiRouteDir, "route")) {
205
- mkdirSync(docsApiRouteDir, { recursive: true });
206
- writeFileSync(join(docsApiRouteDir, "route.ts"), DOCS_API_ROUTE_TEMPLATE);
207
- }
208
- const apiReference = readApiReferenceConfig(root);
209
- if (apiReference.enabled && !isStaticExport) {
210
- const apiReferenceRouteDir = join(root, appDir, ...apiReference.path.split("/"), "[[...slug]]");
211
- if (!hasFile(apiReferenceRouteDir, "route")) {
212
- mkdirSync(apiReferenceRouteDir, { recursive: true });
213
- writeFileSync(join(apiReferenceRouteDir, "route.ts"), API_REFERENCE_ROUTE_TEMPLATE);
214
- }
215
- }
216
- const ogEndpoint = readOgEndpoint(root);
217
- const remarkPlugins = ["remark-gfm", "remark-frontmatter"];
218
- if (ogEndpoint) remarkPlugins.push(["@farming-labs/next/mdx-plugins/remark-og", { endpoint: ogEndpoint }]);
219
- remarkPlugins.push(["remark-mdx-frontmatter", { name: "metadata" }], "@farming-labs/next/mdx-plugins/remark-heading");
220
- const withMDX = createMDX({
221
- extension: /\.mdx?$/,
222
- options: {
223
- remarkPlugins,
224
- rehypePlugins: ["@farming-labs/next/mdx-plugins/rehype-toc", ["@farming-labs/next/mdx-plugins/rehype-code", { themes: {
225
- dark: "github-dark",
226
- light: "github-light"
227
- } }]]
228
- }
229
- });
230
- const defaultExts = [
231
- "js",
232
- "jsx",
233
- "md",
234
- "mdx",
235
- "ts",
236
- "tsx"
237
- ];
238
- const userExts = nextConfig.pageExtensions;
239
- if (userExts) {
240
- for (const ext of ["md", "mdx"]) if (!userExts.includes(ext)) userExts.push(ext);
241
- } else nextConfig.pageExtensions = defaultExts;
242
- const existingTurbopack = nextConfig.turbopack ?? {};
243
- const existingResolveAlias = existingTurbopack.resolveAlias ?? {};
244
- nextConfig.turbopack = {
245
- ...existingTurbopack,
246
- resolveAlias: {
247
- ...existingResolveAlias,
248
- [INTERNAL_DOCS_CONFIG_ALIAS]: docsConfigRelativeAlias
249
- }
250
- };
251
- const userWebpack = nextConfig.webpack;
252
- nextConfig.webpack = (config, options) => {
253
- const resolvedConfig = userWebpack ? userWebpack(config, options) : config;
254
- resolvedConfig.resolve ??= {};
255
- resolvedConfig.resolve.alias ??= {};
256
- resolvedConfig.resolve.alias[INTERNAL_DOCS_CONFIG_ALIAS] = docsConfigAbsolutePath;
257
- return resolvedConfig;
258
- };
259
- return withMDX(nextConfig);
260
- }
261
-
262
- //#endregion
263
- export { withDocs };
package/dist/index.d.mts DELETED
@@ -1,3 +0,0 @@
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 DELETED
@@ -1,4 +0,0 @@
1
- import { withDocs } from "./config.mjs";
2
- import { buildNextOpenApiDocument, createNextApiReference, resolveApiReferenceConfig, withNextApiReferenceBanner } from "./api-reference.mjs";
3
-
4
- export { buildNextOpenApiDocument, createNextApiReference, resolveApiReferenceConfig, withDocs, withNextApiReferenceBanner };
package/dist/layout.d.mts DELETED
@@ -1,12 +0,0 @@
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 DELETED
@@ -1,18 +0,0 @@
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 };
@@ -1,2 +0,0 @@
1
- import { rehypeCode as rehype_code_default } from "fumadocs-core/mdx-plugins/rehype-code";
2
- export { rehype_code_default as default };
@@ -1,3 +0,0 @@
1
- import { rehypeCode as rehype_code_default } from "fumadocs-core/mdx-plugins/rehype-code";
2
-
3
- export { rehype_code_default as default };
@@ -1,2 +0,0 @@
1
- import { rehypeToc as rehype_toc_default } from "fumadocs-core/mdx-plugins/rehype-toc";
2
- export { rehype_toc_default as default };
@@ -1,3 +0,0 @@
1
- import { rehypeToc as rehype_toc_default } from "fumadocs-core/mdx-plugins/rehype-toc";
2
-
3
- export { rehype_toc_default as default };
@@ -1,2 +0,0 @@
1
- import { remarkHeading as remark_heading_default } from "fumadocs-core/mdx-plugins/remark-heading";
2
- export { remark_heading_default as default };
@@ -1,3 +0,0 @@
1
- import { remarkHeading as remark_heading_default } from "fumadocs-core/mdx-plugins/remark-heading";
2
-
3
- export { remark_heading_default as default };
@@ -1,22 +0,0 @@
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 };
@@ -1,30 +0,0 @@
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 };