@farming-labs/docs 0.0.2-beta.1
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/cli/index.d.mts +1 -0
- package/dist/cli/index.mjs +610 -0
- package/dist/index.d.mts +597 -0
- package/dist/index.mjs +116 -0
- package/package.json +44 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import { execSync, spawn } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
//#region src/cli/utils.ts
|
|
9
|
+
function detectFramework(cwd) {
|
|
10
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
11
|
+
if (!fs.existsSync(pkgPath)) return null;
|
|
12
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
13
|
+
if ({
|
|
14
|
+
...pkg.dependencies,
|
|
15
|
+
...pkg.devDependencies
|
|
16
|
+
}["next"]) return "nextjs";
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
function detectPackageManager(cwd) {
|
|
20
|
+
if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
21
|
+
if (fs.existsSync(path.join(cwd, "bun.lockb")) || fs.existsSync(path.join(cwd, "bun.lock"))) return "bun";
|
|
22
|
+
if (fs.existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
|
|
23
|
+
return "npm";
|
|
24
|
+
}
|
|
25
|
+
function installCommand(pm) {
|
|
26
|
+
return pm === "yarn" ? "yarn add" : `${pm} add`;
|
|
27
|
+
}
|
|
28
|
+
function devInstallCommand(pm) {
|
|
29
|
+
if (pm === "yarn") return "yarn add -D";
|
|
30
|
+
if (pm === "npm") return "npm install -D";
|
|
31
|
+
return `${pm} add -D`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Write a file, creating parent directories as needed.
|
|
35
|
+
* Returns true if the file was written, false if it already existed and was skipped.
|
|
36
|
+
*/
|
|
37
|
+
function writeFileSafe(filePath, content, overwrite = false) {
|
|
38
|
+
if (fs.existsSync(filePath) && !overwrite) return false;
|
|
39
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
40
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Check if a file exists.
|
|
45
|
+
*/
|
|
46
|
+
function fileExists(filePath) {
|
|
47
|
+
return fs.existsSync(filePath);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Read a file, returning null if it does not exist.
|
|
51
|
+
*/
|
|
52
|
+
function readFileSafe(filePath) {
|
|
53
|
+
if (!fs.existsSync(filePath)) return null;
|
|
54
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Run a shell command synchronously, inheriting stdio.
|
|
58
|
+
*/
|
|
59
|
+
function exec(command, cwd) {
|
|
60
|
+
execSync(command, {
|
|
61
|
+
cwd,
|
|
62
|
+
stdio: "inherit"
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Spawn a process and wait for a specific string in stdout,
|
|
67
|
+
* then resolve with the child process (still running).
|
|
68
|
+
*/
|
|
69
|
+
function spawnAndWaitFor(command, args, cwd, waitFor, timeoutMs = 6e4) {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const child = spawn(command, args, {
|
|
72
|
+
cwd,
|
|
73
|
+
stdio: [
|
|
74
|
+
"ignore",
|
|
75
|
+
"pipe",
|
|
76
|
+
"pipe"
|
|
77
|
+
],
|
|
78
|
+
shell: true
|
|
79
|
+
});
|
|
80
|
+
let output = "";
|
|
81
|
+
const timer = setTimeout(() => {
|
|
82
|
+
child.kill();
|
|
83
|
+
reject(/* @__PURE__ */ new Error(`Timed out waiting for "${waitFor}" after ${timeoutMs}ms`));
|
|
84
|
+
}, timeoutMs);
|
|
85
|
+
child.stdout?.on("data", (data) => {
|
|
86
|
+
const text = data.toString();
|
|
87
|
+
output += text;
|
|
88
|
+
process.stdout.write(text);
|
|
89
|
+
if (output.includes(waitFor)) {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
resolve(child);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
child.stderr?.on("data", (data) => {
|
|
95
|
+
process.stderr.write(data.toString());
|
|
96
|
+
});
|
|
97
|
+
child.on("error", (err) => {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
reject(err);
|
|
100
|
+
});
|
|
101
|
+
child.on("close", (code) => {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
if (!output.includes(waitFor)) reject(/* @__PURE__ */ new Error(`Process exited with code ${code} before "${waitFor}" appeared`));
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/cli/templates.ts
|
|
110
|
+
function docsConfigTemplate(cfg) {
|
|
111
|
+
return `\
|
|
112
|
+
import { defineDocs } from "@farming-labs/docs";
|
|
113
|
+
import { fumadocs } from "@farming-labs/fumadocs";
|
|
114
|
+
|
|
115
|
+
export default defineDocs({
|
|
116
|
+
entry: "${cfg.entry}",
|
|
117
|
+
theme: fumadocs({
|
|
118
|
+
ui: {
|
|
119
|
+
colors: { primary: "#6366f1" },
|
|
120
|
+
},
|
|
121
|
+
}),
|
|
122
|
+
|
|
123
|
+
metadata: {
|
|
124
|
+
titleTemplate: "%s – ${cfg.projectName}",
|
|
125
|
+
description: "Documentation for ${cfg.projectName}",
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
130
|
+
function nextConfigTemplate() {
|
|
131
|
+
return `\
|
|
132
|
+
import { withDocs } from "@farming-labs/next/config";
|
|
133
|
+
|
|
134
|
+
export default withDocs();
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
function nextConfigMergedTemplate(existingContent) {
|
|
138
|
+
if (existingContent.includes("withDocs")) return existingContent;
|
|
139
|
+
const lines = existingContent.split("\n");
|
|
140
|
+
const importLine = "import { withDocs } from \"@farming-labs/next/config\";";
|
|
141
|
+
const exportIdx = lines.findIndex((l) => l.match(/export\s+default/));
|
|
142
|
+
if (exportIdx === -1) return `${importLine}\n\n${existingContent}\n\nexport default withDocs();\n`;
|
|
143
|
+
const lastImportIdx = lines.reduce((acc, l, i) => l.trimStart().startsWith("import ") ? i : acc, -1);
|
|
144
|
+
if (lastImportIdx >= 0) lines.splice(lastImportIdx + 1, 0, importLine);
|
|
145
|
+
else lines.unshift(importLine, "");
|
|
146
|
+
const adjustedExportIdx = exportIdx + (lastImportIdx >= 0 && exportIdx > lastImportIdx ? 1 : 0);
|
|
147
|
+
const simpleMatch = lines[adjustedExportIdx].match(/^(\s*export\s+default\s+)(.*?)(;?\s*)$/);
|
|
148
|
+
if (simpleMatch) {
|
|
149
|
+
const [, prefix, value, suffix] = simpleMatch;
|
|
150
|
+
lines[adjustedExportIdx] = `${prefix}withDocs(${value})${suffix}`;
|
|
151
|
+
}
|
|
152
|
+
return lines.join("\n");
|
|
153
|
+
}
|
|
154
|
+
function rootLayoutTemplate() {
|
|
155
|
+
return `\
|
|
156
|
+
import type { Metadata } from "next";
|
|
157
|
+
import { RootProvider } from "@farming-labs/fumadocs";
|
|
158
|
+
import docsConfig from "@/docs.config";
|
|
159
|
+
import "./global.css";
|
|
160
|
+
|
|
161
|
+
export const metadata: Metadata = {
|
|
162
|
+
title: {
|
|
163
|
+
default: "Docs",
|
|
164
|
+
template: docsConfig.metadata?.titleTemplate ?? "%s",
|
|
165
|
+
},
|
|
166
|
+
description: docsConfig.metadata?.description,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export default function RootLayout({
|
|
170
|
+
children,
|
|
171
|
+
}: {
|
|
172
|
+
children: React.ReactNode;
|
|
173
|
+
}) {
|
|
174
|
+
return (
|
|
175
|
+
<html lang="en" suppressHydrationWarning>
|
|
176
|
+
<body>
|
|
177
|
+
<RootProvider>{children}</RootProvider>
|
|
178
|
+
</body>
|
|
179
|
+
</html>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
`;
|
|
183
|
+
}
|
|
184
|
+
function globalCssTemplate(theme) {
|
|
185
|
+
return `\
|
|
186
|
+
@import "tailwindcss";
|
|
187
|
+
@import "@farming-labs/${theme}/css";
|
|
188
|
+
`;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Inject the fumadocs CSS import into an existing global.css.
|
|
192
|
+
* Returns the modified content, or null if already present.
|
|
193
|
+
*/
|
|
194
|
+
function injectCssImport(existingContent, theme) {
|
|
195
|
+
const importLine = `@import "@farming-labs/${theme}/css";`;
|
|
196
|
+
if (existingContent.includes(importLine)) return null;
|
|
197
|
+
const lines = existingContent.split("\n");
|
|
198
|
+
const lastImportIdx = lines.reduce((acc, l, i) => l.trimStart().startsWith("@import") ? i : acc, -1);
|
|
199
|
+
if (lastImportIdx >= 0) lines.splice(lastImportIdx + 1, 0, importLine);
|
|
200
|
+
else lines.unshift(importLine);
|
|
201
|
+
return lines.join("\n");
|
|
202
|
+
}
|
|
203
|
+
function docsLayoutTemplate() {
|
|
204
|
+
return `\
|
|
205
|
+
import docsConfig from "@/docs.config";
|
|
206
|
+
import { createDocsLayout } from "@farming-labs/fumadocs";
|
|
207
|
+
|
|
208
|
+
export default createDocsLayout(docsConfig);
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
function postcssConfigTemplate() {
|
|
212
|
+
return `\
|
|
213
|
+
const config = {
|
|
214
|
+
plugins: {
|
|
215
|
+
"@tailwindcss/postcss": {},
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export default config;
|
|
220
|
+
`;
|
|
221
|
+
}
|
|
222
|
+
function tsconfigTemplate() {
|
|
223
|
+
return `\
|
|
224
|
+
{
|
|
225
|
+
"compilerOptions": {
|
|
226
|
+
"target": "ES2017",
|
|
227
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
228
|
+
"allowJs": true,
|
|
229
|
+
"skipLibCheck": true,
|
|
230
|
+
"strict": true,
|
|
231
|
+
"noEmit": true,
|
|
232
|
+
"esModuleInterop": true,
|
|
233
|
+
"module": "esnext",
|
|
234
|
+
"moduleResolution": "bundler",
|
|
235
|
+
"resolveJsonModule": true,
|
|
236
|
+
"isolatedModules": true,
|
|
237
|
+
"jsx": "react-jsx",
|
|
238
|
+
"incremental": true,
|
|
239
|
+
"plugins": [{ "name": "next" }],
|
|
240
|
+
"paths": { "@/*": ["./*"] }
|
|
241
|
+
},
|
|
242
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
243
|
+
"exclude": ["node_modules"]
|
|
244
|
+
}
|
|
245
|
+
`;
|
|
246
|
+
}
|
|
247
|
+
function welcomePageTemplate(cfg) {
|
|
248
|
+
return `\
|
|
249
|
+
---
|
|
250
|
+
title: "Documentation"
|
|
251
|
+
description: "Welcome to ${cfg.projectName} documentation"
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
# Welcome to ${cfg.projectName}
|
|
255
|
+
|
|
256
|
+
Get started with our documentation. Browse the pages on the left to learn more.
|
|
257
|
+
|
|
258
|
+
<Callout type="info">
|
|
259
|
+
This documentation was generated by \`@farming-labs/docs\`. Edit the MDX files in \`app/${cfg.entry}/\` to customize.
|
|
260
|
+
</Callout>
|
|
261
|
+
|
|
262
|
+
## Overview
|
|
263
|
+
|
|
264
|
+
This is your documentation home page. From here you can navigate to:
|
|
265
|
+
|
|
266
|
+
- [Installation](/${cfg.entry}/installation) — How to install and set up the project
|
|
267
|
+
- [Quickstart](/${cfg.entry}/quickstart) — Get up and running in minutes
|
|
268
|
+
|
|
269
|
+
## Features
|
|
270
|
+
|
|
271
|
+
- **MDX Support** — Write docs with Markdown and React components
|
|
272
|
+
- **Syntax Highlighting** — Code blocks with automatic highlighting
|
|
273
|
+
- **Dark Mode** — Built-in theme switching
|
|
274
|
+
- **Search** — Full-text search across all pages
|
|
275
|
+
- **Responsive** — Works on any screen size
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Next Steps
|
|
280
|
+
|
|
281
|
+
Start by reading the [Installation](/${cfg.entry}/installation) guide, then follow the [Quickstart](/${cfg.entry}/quickstart) to build something.
|
|
282
|
+
`;
|
|
283
|
+
}
|
|
284
|
+
function installationPageTemplate(cfg) {
|
|
285
|
+
return `\
|
|
286
|
+
---
|
|
287
|
+
title: "Installation"
|
|
288
|
+
description: "How to install and set up ${cfg.projectName}"
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
# Installation
|
|
292
|
+
|
|
293
|
+
Follow these steps to install and configure ${cfg.projectName}.
|
|
294
|
+
|
|
295
|
+
<Callout type="info">
|
|
296
|
+
Prerequisites: Node.js 18+ and a package manager (pnpm, npm, or yarn).
|
|
297
|
+
</Callout>
|
|
298
|
+
|
|
299
|
+
## Install Dependencies
|
|
300
|
+
|
|
301
|
+
\`\`\`bash
|
|
302
|
+
pnpm add @farming-labs/docs
|
|
303
|
+
\`\`\`
|
|
304
|
+
|
|
305
|
+
## Configuration
|
|
306
|
+
|
|
307
|
+
Your project includes a \`docs.config.ts\` at the root:
|
|
308
|
+
|
|
309
|
+
\`\`\`ts
|
|
310
|
+
import { defineDocs } from "@farming-labs/docs";
|
|
311
|
+
import { fumadocs } from "@farming-labs/fumadocs";
|
|
312
|
+
|
|
313
|
+
export default defineDocs({
|
|
314
|
+
entry: "${cfg.entry}",
|
|
315
|
+
theme: fumadocs({
|
|
316
|
+
ui: { colors: { primary: "#6366f1" } },
|
|
317
|
+
}),
|
|
318
|
+
});
|
|
319
|
+
\`\`\`
|
|
320
|
+
|
|
321
|
+
## Project Structure
|
|
322
|
+
|
|
323
|
+
\`\`\`
|
|
324
|
+
app/
|
|
325
|
+
${cfg.entry}/
|
|
326
|
+
layout.tsx # Docs layout
|
|
327
|
+
page.mdx # /${cfg.entry}
|
|
328
|
+
installation/
|
|
329
|
+
page.mdx # /${cfg.entry}/installation
|
|
330
|
+
quickstart/
|
|
331
|
+
page.mdx # /${cfg.entry}/quickstart
|
|
332
|
+
docs.config.ts # Docs configuration
|
|
333
|
+
next.config.ts # Next.js config with withDocs()
|
|
334
|
+
\`\`\`
|
|
335
|
+
|
|
336
|
+
## What's Next?
|
|
337
|
+
|
|
338
|
+
Head to the [Quickstart](/${cfg.entry}/quickstart) guide to start writing your first page.
|
|
339
|
+
`;
|
|
340
|
+
}
|
|
341
|
+
function quickstartPageTemplate(cfg) {
|
|
342
|
+
return `\
|
|
343
|
+
---
|
|
344
|
+
title: "Quickstart"
|
|
345
|
+
description: "Get up and running in minutes"
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
# Quickstart
|
|
349
|
+
|
|
350
|
+
This guide walks you through creating your first documentation page.
|
|
351
|
+
|
|
352
|
+
## Creating a Page
|
|
353
|
+
|
|
354
|
+
Create a new folder under \`app/${cfg.entry}/\` with a \`page.mdx\` file:
|
|
355
|
+
|
|
356
|
+
\`\`\`bash
|
|
357
|
+
mkdir -p app/${cfg.entry}/my-page
|
|
358
|
+
\`\`\`
|
|
359
|
+
|
|
360
|
+
Then create \`app/${cfg.entry}/my-page/page.mdx\`:
|
|
361
|
+
|
|
362
|
+
\`\`\`mdx
|
|
363
|
+
---
|
|
364
|
+
title: "My Page"
|
|
365
|
+
description: "A custom documentation page"
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
# My Page
|
|
369
|
+
|
|
370
|
+
Write your content here using **Markdown** and JSX components.
|
|
371
|
+
\`\`\`
|
|
372
|
+
|
|
373
|
+
Your page is now available at \`/${cfg.entry}/my-page\`.
|
|
374
|
+
|
|
375
|
+
## Using Components
|
|
376
|
+
|
|
377
|
+
### Callouts
|
|
378
|
+
|
|
379
|
+
<Callout type="info">
|
|
380
|
+
This is an informational callout. Use it for tips and notes.
|
|
381
|
+
</Callout>
|
|
382
|
+
|
|
383
|
+
<Callout type="warn">
|
|
384
|
+
This is a warning callout. Use it for important caveats.
|
|
385
|
+
</Callout>
|
|
386
|
+
|
|
387
|
+
### Code Blocks
|
|
388
|
+
|
|
389
|
+
Code blocks are automatically syntax-highlighted:
|
|
390
|
+
|
|
391
|
+
\`\`\`typescript
|
|
392
|
+
function greet(name: string): string {
|
|
393
|
+
return \\\`Hello, \\\${name}!\\\`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
console.log(greet("World"));
|
|
397
|
+
\`\`\`
|
|
398
|
+
|
|
399
|
+
## Customizing the Theme
|
|
400
|
+
|
|
401
|
+
Edit \`docs.config.ts\` to change colors, typography, and component defaults:
|
|
402
|
+
|
|
403
|
+
\`\`\`ts
|
|
404
|
+
theme: fumadocs({
|
|
405
|
+
ui: {
|
|
406
|
+
colors: { primary: "#22c55e" },
|
|
407
|
+
},
|
|
408
|
+
}),
|
|
409
|
+
\`\`\`
|
|
410
|
+
|
|
411
|
+
## Deploying
|
|
412
|
+
|
|
413
|
+
Build your docs for production:
|
|
414
|
+
|
|
415
|
+
\`\`\`bash
|
|
416
|
+
pnpm build
|
|
417
|
+
\`\`\`
|
|
418
|
+
|
|
419
|
+
Deploy to Vercel, Netlify, or any Node.js hosting platform.
|
|
420
|
+
`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
//#endregion
|
|
424
|
+
//#region src/cli/init.ts
|
|
425
|
+
async function init() {
|
|
426
|
+
const cwd = process.cwd();
|
|
427
|
+
p.intro(pc.bgCyan(pc.black(" @farming-labs/docs ")));
|
|
428
|
+
if (!detectFramework(cwd)) {
|
|
429
|
+
p.log.error("Could not detect a supported framework.\n Make sure you have a " + pc.cyan("package.json") + " with " + pc.cyan("next") + " installed.\n Supported frameworks: Next.js");
|
|
430
|
+
p.outro(pc.red("Init cancelled."));
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
p.log.success(`Detected framework: ${pc.cyan("Next.js")}`);
|
|
434
|
+
const theme = await p.select({
|
|
435
|
+
message: "Which theme would you like to use?",
|
|
436
|
+
options: [{
|
|
437
|
+
value: "fumadocs",
|
|
438
|
+
label: "Fumadocs",
|
|
439
|
+
hint: "Clean, modern docs theme with sidebar, search, and dark mode"
|
|
440
|
+
}]
|
|
441
|
+
});
|
|
442
|
+
if (p.isCancel(theme)) {
|
|
443
|
+
p.outro(pc.red("Init cancelled."));
|
|
444
|
+
process.exit(0);
|
|
445
|
+
}
|
|
446
|
+
const entry = await p.text({
|
|
447
|
+
message: "Where should your docs live?",
|
|
448
|
+
placeholder: "docs",
|
|
449
|
+
defaultValue: "docs",
|
|
450
|
+
validate: (value) => {
|
|
451
|
+
if (!value) return "Entry path is required";
|
|
452
|
+
if (value.startsWith("/")) return "Use a relative path (no leading /)";
|
|
453
|
+
if (value.includes(" ")) return "Path cannot contain spaces";
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
if (p.isCancel(entry)) {
|
|
457
|
+
p.outro(pc.red("Init cancelled."));
|
|
458
|
+
process.exit(0);
|
|
459
|
+
}
|
|
460
|
+
const entryPath = entry;
|
|
461
|
+
const pkgJson = JSON.parse(readFileSafe(path.join(cwd, "package.json")));
|
|
462
|
+
const cfg = {
|
|
463
|
+
entry: entryPath,
|
|
464
|
+
theme,
|
|
465
|
+
projectName: pkgJson.name || "My Project"
|
|
466
|
+
};
|
|
467
|
+
const s = p.spinner();
|
|
468
|
+
s.start("Scaffolding docs files");
|
|
469
|
+
const written = [];
|
|
470
|
+
const skipped = [];
|
|
471
|
+
function write(rel, content, overwrite = false) {
|
|
472
|
+
if (writeFileSafe(path.join(cwd, rel), content, overwrite)) written.push(rel);
|
|
473
|
+
else skipped.push(rel);
|
|
474
|
+
}
|
|
475
|
+
write("docs.config.ts", docsConfigTemplate(cfg));
|
|
476
|
+
const existingNextConfig = readFileSafe(path.join(cwd, "next.config.ts")) ?? readFileSafe(path.join(cwd, "next.config.mjs")) ?? readFileSafe(path.join(cwd, "next.config.js"));
|
|
477
|
+
if (existingNextConfig) {
|
|
478
|
+
const configFile = fileExists(path.join(cwd, "next.config.ts")) ? "next.config.ts" : fileExists(path.join(cwd, "next.config.mjs")) ? "next.config.mjs" : "next.config.js";
|
|
479
|
+
const merged = nextConfigMergedTemplate(existingNextConfig);
|
|
480
|
+
if (merged !== existingNextConfig) {
|
|
481
|
+
writeFileSafe(path.join(cwd, configFile), merged, true);
|
|
482
|
+
written.push(configFile + " (updated)");
|
|
483
|
+
} else skipped.push(configFile + " (already configured)");
|
|
484
|
+
} else write("next.config.ts", nextConfigTemplate());
|
|
485
|
+
write("app/layout.tsx", rootLayoutTemplate());
|
|
486
|
+
const globalCssPath = path.join(cwd, "app/global.css");
|
|
487
|
+
const existingGlobalCss = readFileSafe(globalCssPath);
|
|
488
|
+
if (existingGlobalCss) {
|
|
489
|
+
const injected = injectCssImport(existingGlobalCss, theme);
|
|
490
|
+
if (injected) {
|
|
491
|
+
writeFileSafe(globalCssPath, injected, true);
|
|
492
|
+
written.push("app/global.css (updated)");
|
|
493
|
+
} else skipped.push("app/global.css (already configured)");
|
|
494
|
+
} else write("app/global.css", globalCssTemplate(theme));
|
|
495
|
+
write(`app/${entryPath}/layout.tsx`, docsLayoutTemplate());
|
|
496
|
+
write("postcss.config.mjs", postcssConfigTemplate());
|
|
497
|
+
if (!fileExists(path.join(cwd, "tsconfig.json"))) write("tsconfig.json", tsconfigTemplate());
|
|
498
|
+
write(`app/${entryPath}/page.mdx`, welcomePageTemplate(cfg));
|
|
499
|
+
write(`app/${entryPath}/installation/page.mdx`, installationPageTemplate(cfg));
|
|
500
|
+
write(`app/${entryPath}/quickstart/page.mdx`, quickstartPageTemplate(cfg));
|
|
501
|
+
s.stop("Files scaffolded");
|
|
502
|
+
if (written.length > 0) p.log.success(`Created ${written.length} file${written.length > 1 ? "s" : ""}:\n` + written.map((f) => ` ${pc.green("+")} ${f}`).join("\n"));
|
|
503
|
+
if (skipped.length > 0) p.log.info(`Skipped ${skipped.length} existing file${skipped.length > 1 ? "s" : ""}:\n` + skipped.map((f) => ` ${pc.dim("-")} ${f}`).join("\n"));
|
|
504
|
+
const pm = detectPackageManager(cwd);
|
|
505
|
+
p.log.info(`Using ${pc.cyan(pm)} as package manager`);
|
|
506
|
+
const s2 = p.spinner();
|
|
507
|
+
s2.start("Installing dependencies");
|
|
508
|
+
try {
|
|
509
|
+
exec(`${installCommand(pm)} @farming-labs/docs @farming-labs/next @farming-labs/fumadocs`, cwd);
|
|
510
|
+
const devDeps = [
|
|
511
|
+
"@tailwindcss/postcss",
|
|
512
|
+
"postcss",
|
|
513
|
+
"tailwindcss",
|
|
514
|
+
"@types/mdx",
|
|
515
|
+
"@types/node"
|
|
516
|
+
];
|
|
517
|
+
const allDeps = {
|
|
518
|
+
...pkgJson.dependencies,
|
|
519
|
+
...pkgJson.devDependencies
|
|
520
|
+
};
|
|
521
|
+
const missingDevDeps = devDeps.filter((d) => !allDeps[d]);
|
|
522
|
+
if (missingDevDeps.length > 0) exec(`${devInstallCommand(pm)} ${missingDevDeps.join(" ")}`, cwd);
|
|
523
|
+
} catch {
|
|
524
|
+
s2.stop("Failed to install dependencies");
|
|
525
|
+
p.log.error(`Dependency installation failed. Run the install command manually:
|
|
526
|
+
${pc.cyan(`${installCommand(pm)} @farming-labs/docs`)}`);
|
|
527
|
+
p.outro(pc.yellow("Setup partially complete. Install deps and run dev server manually."));
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
s2.stop("Dependencies installed");
|
|
531
|
+
const startDev = await p.confirm({
|
|
532
|
+
message: "Start the dev server now?",
|
|
533
|
+
initialValue: true
|
|
534
|
+
});
|
|
535
|
+
if (p.isCancel(startDev) || !startDev) {
|
|
536
|
+
p.log.info(`You can start the dev server later with:
|
|
537
|
+
${pc.cyan(`${pm === "yarn" ? "yarn" : pm + " run"} dev`)}`);
|
|
538
|
+
p.outro(pc.green("Done! Happy documenting."));
|
|
539
|
+
process.exit(0);
|
|
540
|
+
}
|
|
541
|
+
p.log.step("Starting dev server...");
|
|
542
|
+
try {
|
|
543
|
+
const child = await spawnAndWaitFor("npx", [
|
|
544
|
+
"next",
|
|
545
|
+
"dev",
|
|
546
|
+
"--webpack"
|
|
547
|
+
], cwd, "Ready", 6e4);
|
|
548
|
+
const url = `http://localhost:3000/${entryPath}`;
|
|
549
|
+
console.log();
|
|
550
|
+
p.log.success(`Dev server is running! Your docs are live at:\n\n ${pc.cyan(pc.underline(url))}\n\n Press ${pc.dim("Ctrl+C")} to stop the server.`);
|
|
551
|
+
p.outro(pc.green("Happy documenting!"));
|
|
552
|
+
await new Promise((resolve) => {
|
|
553
|
+
child.on("close", () => resolve());
|
|
554
|
+
process.on("SIGINT", () => {
|
|
555
|
+
child.kill("SIGINT");
|
|
556
|
+
resolve();
|
|
557
|
+
});
|
|
558
|
+
process.on("SIGTERM", () => {
|
|
559
|
+
child.kill("SIGTERM");
|
|
560
|
+
resolve();
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
} catch (err) {
|
|
564
|
+
p.log.error(`Could not start dev server. Try running manually:
|
|
565
|
+
${pc.cyan("npx next dev --webpack")}`);
|
|
566
|
+
p.outro(pc.yellow("Setup complete. Start the server manually."));
|
|
567
|
+
process.exit(1);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
//#endregion
|
|
572
|
+
//#region src/cli/index.ts
|
|
573
|
+
const command = process.argv.slice(2)[0];
|
|
574
|
+
async function main() {
|
|
575
|
+
if (!command || command === "init") await init();
|
|
576
|
+
else if (command === "--help" || command === "-h") printHelp();
|
|
577
|
+
else if (command === "--version" || command === "-v") printVersion();
|
|
578
|
+
else {
|
|
579
|
+
console.error(pc.red(`Unknown command: ${command}`));
|
|
580
|
+
console.error();
|
|
581
|
+
printHelp();
|
|
582
|
+
process.exit(1);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
function printHelp() {
|
|
586
|
+
console.log(`
|
|
587
|
+
${pc.bold("@farming-labs/docs")} — Documentation framework CLI
|
|
588
|
+
|
|
589
|
+
${pc.dim("Usage:")}
|
|
590
|
+
farming-docs ${pc.cyan("<command>")}
|
|
591
|
+
|
|
592
|
+
${pc.dim("Commands:")}
|
|
593
|
+
${pc.cyan("init")} Scaffold docs in your project (default)
|
|
594
|
+
|
|
595
|
+
${pc.dim("Options:")}
|
|
596
|
+
${pc.cyan("-h, --help")} Show this help message
|
|
597
|
+
${pc.cyan("-v, --version")} Show version
|
|
598
|
+
`);
|
|
599
|
+
}
|
|
600
|
+
function printVersion() {
|
|
601
|
+
console.log("0.1.0");
|
|
602
|
+
}
|
|
603
|
+
main().catch((err) => {
|
|
604
|
+
console.error(pc.red("An unexpected error occurred:"));
|
|
605
|
+
console.error(err);
|
|
606
|
+
process.exit(1);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
//#endregion
|
|
610
|
+
export { };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Theme / UI configuration types for the docs framework.
|
|
4
|
+
* Inspired by Fumadocs: https://github.com/fuma-nama/fumadocs
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Fine-grained UI configuration for docs themes.
|
|
8
|
+
*
|
|
9
|
+
* Theme authors define defaults for these values in their `createTheme` call.
|
|
10
|
+
* End users can override any value when calling the theme factory.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const myTheme = createTheme({
|
|
15
|
+
* name: "my-theme",
|
|
16
|
+
* ui: {
|
|
17
|
+
* colors: { primary: "#6366f1", background: "#0a0a0a" },
|
|
18
|
+
* radius: "0px",
|
|
19
|
+
* codeBlock: { showLineNumbers: true, theme: "github-dark" },
|
|
20
|
+
* sidebar: { width: 280, style: "bordered" },
|
|
21
|
+
* },
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Font style configuration for a single text element (heading, body, etc.).
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* h1: { size: "2.25rem", weight: 700, lineHeight: "1.2", letterSpacing: "-0.02em" }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
interface FontStyle {
|
|
34
|
+
/** CSS `font-size` value (e.g. "2.25rem", "36px", "clamp(1.8rem, 3vw, 2.5rem)") */
|
|
35
|
+
size?: string;
|
|
36
|
+
/** CSS `font-weight` value (e.g. 700, "bold", "600") */
|
|
37
|
+
weight?: string | number;
|
|
38
|
+
/** CSS `line-height` value (e.g. "1.2", "1.5", "28px") */
|
|
39
|
+
lineHeight?: string;
|
|
40
|
+
/** CSS `letter-spacing` value (e.g. "-0.02em", "0.05em") */
|
|
41
|
+
letterSpacing?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Typography configuration for the docs.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* typography: {
|
|
49
|
+
* font: {
|
|
50
|
+
* style: { sans: "Inter, sans-serif", mono: "JetBrains Mono, monospace" },
|
|
51
|
+
* h1: { size: "2.25rem", weight: 700, letterSpacing: "-0.02em" },
|
|
52
|
+
* h2: { size: "1.75rem", weight: 600 },
|
|
53
|
+
* body: { size: "1rem", lineHeight: "1.75" },
|
|
54
|
+
* },
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
interface TypographyConfig {
|
|
59
|
+
/**
|
|
60
|
+
* Font configuration.
|
|
61
|
+
*/
|
|
62
|
+
font?: {
|
|
63
|
+
/**
|
|
64
|
+
* Font family definitions.
|
|
65
|
+
*/
|
|
66
|
+
style?: {
|
|
67
|
+
/** Sans-serif font family — used for body text, headings, and UI elements. */sans?: string; /** Monospace font family — used for code blocks, inline code, and terminal output. */
|
|
68
|
+
mono?: string;
|
|
69
|
+
}; /** Heading 1 (`<h1>`) style overrides */
|
|
70
|
+
h1?: FontStyle; /** Heading 2 (`<h2>`) style overrides */
|
|
71
|
+
h2?: FontStyle; /** Heading 3 (`<h3>`) style overrides */
|
|
72
|
+
h3?: FontStyle; /** Heading 4 (`<h4>`) style overrides */
|
|
73
|
+
h4?: FontStyle; /** Body text style */
|
|
74
|
+
body?: FontStyle; /** Small text style (captions, meta text) */
|
|
75
|
+
small?: FontStyle;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
interface UIConfig {
|
|
79
|
+
/**
|
|
80
|
+
* Theme color tokens.
|
|
81
|
+
*
|
|
82
|
+
* These are mapped to `--color-fd-*` CSS variables at runtime.
|
|
83
|
+
* Accepts any valid CSS color value (hex, rgb, oklch, hsl, etc.).
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* colors: {
|
|
88
|
+
* primary: "oklch(0.72 0.19 149)", // green primary
|
|
89
|
+
* primaryForeground: "#ffffff", // white text on primary
|
|
90
|
+
* accent: "hsl(220 80% 60%)", // blue accent
|
|
91
|
+
* }
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
colors?: {
|
|
95
|
+
primary?: string;
|
|
96
|
+
primaryForeground?: string;
|
|
97
|
+
background?: string;
|
|
98
|
+
foreground?: string;
|
|
99
|
+
muted?: string;
|
|
100
|
+
mutedForeground?: string;
|
|
101
|
+
border?: string;
|
|
102
|
+
card?: string;
|
|
103
|
+
cardForeground?: string;
|
|
104
|
+
accent?: string;
|
|
105
|
+
accentForeground?: string;
|
|
106
|
+
secondary?: string;
|
|
107
|
+
secondaryForeground?: string;
|
|
108
|
+
popover?: string;
|
|
109
|
+
popoverForeground?: string;
|
|
110
|
+
ring?: string;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Typography settings — font families, heading sizes, weights, etc.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* typography: {
|
|
118
|
+
* font: {
|
|
119
|
+
* style: { sans: "Inter, sans-serif", mono: "JetBrains Mono, monospace" },
|
|
120
|
+
* h1: { size: "2.25rem", weight: 700, letterSpacing: "-0.02em" },
|
|
121
|
+
* body: { size: "1rem", lineHeight: "1.75" },
|
|
122
|
+
* },
|
|
123
|
+
* }
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
typography?: TypographyConfig;
|
|
127
|
+
/**
|
|
128
|
+
* Global border-radius. Maps to CSS `--radius`.
|
|
129
|
+
* Use "0px" for sharp corners, "0.5rem" for rounded, etc.
|
|
130
|
+
*/
|
|
131
|
+
radius?: string;
|
|
132
|
+
/** Layout dimensions */
|
|
133
|
+
layout?: {
|
|
134
|
+
contentWidth?: number;
|
|
135
|
+
sidebarWidth?: number;
|
|
136
|
+
tocWidth?: number;
|
|
137
|
+
toc?: {
|
|
138
|
+
enabled?: boolean;
|
|
139
|
+
depth?: number;
|
|
140
|
+
};
|
|
141
|
+
header?: {
|
|
142
|
+
height?: number;
|
|
143
|
+
sticky?: boolean;
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
/** Code block rendering config */
|
|
147
|
+
codeBlock?: {
|
|
148
|
+
/** Show line numbers in code blocks @default false */showLineNumbers?: boolean; /** Show copy button @default true */
|
|
149
|
+
showCopyButton?: boolean; /** Shiki theme name for syntax highlighting */
|
|
150
|
+
theme?: string; /** Dark mode shiki theme (for dual-theme setups) */
|
|
151
|
+
darkTheme?: string;
|
|
152
|
+
};
|
|
153
|
+
/** Sidebar styling hints (consumed by theme CSS) */
|
|
154
|
+
sidebar?: {
|
|
155
|
+
/**
|
|
156
|
+
* Visual style of the sidebar.
|
|
157
|
+
* - "default" — standard fumadocs sidebar
|
|
158
|
+
* - "bordered" — visible bordered sections (like better-auth)
|
|
159
|
+
* - "floating" — floating card sidebar
|
|
160
|
+
*/
|
|
161
|
+
style?: "default" | "bordered" | "floating"; /** Background color override */
|
|
162
|
+
background?: string; /** Border color override */
|
|
163
|
+
borderColor?: string;
|
|
164
|
+
};
|
|
165
|
+
/** Card styling */
|
|
166
|
+
card?: {
|
|
167
|
+
/** Whether cards have visible borders @default true */bordered?: boolean; /** Card background color override */
|
|
168
|
+
background?: string;
|
|
169
|
+
};
|
|
170
|
+
/** Default props/variants for MDX components (Callout, CodeBlock, Tabs, etc.) */
|
|
171
|
+
components?: {
|
|
172
|
+
[key: string]: Record<string, unknown> | ((defaults: unknown) => unknown);
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* A docs theme configuration.
|
|
177
|
+
*
|
|
178
|
+
* Theme authors create these with `createTheme()`. The `name` identifies the
|
|
179
|
+
* theme (useful for CSS scoping, debugging, analytics). The `ui` object holds
|
|
180
|
+
* all visual configuration.
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```ts
|
|
184
|
+
* import { createTheme } from "@farming-labs/docs";
|
|
185
|
+
*
|
|
186
|
+
* export const myTheme = createTheme({
|
|
187
|
+
* name: "my-theme",
|
|
188
|
+
* ui: {
|
|
189
|
+
* colors: { primary: "#ff4d8d" },
|
|
190
|
+
* radius: "0px",
|
|
191
|
+
* },
|
|
192
|
+
* });
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
interface DocsTheme {
|
|
196
|
+
/** Unique name for this theme (used for CSS scoping and debugging) */
|
|
197
|
+
name?: string;
|
|
198
|
+
/** UI configuration — colors, typography, layout, components */
|
|
199
|
+
ui?: UIConfig;
|
|
200
|
+
/**
|
|
201
|
+
* @internal
|
|
202
|
+
* User-provided color overrides tracked by `createTheme`.
|
|
203
|
+
* Only these colors are emitted as inline CSS variables at runtime.
|
|
204
|
+
* Preset defaults stay in the theme's CSS file.
|
|
205
|
+
*/
|
|
206
|
+
_userColorOverrides?: Record<string, string>;
|
|
207
|
+
}
|
|
208
|
+
interface DocsMetadata {
|
|
209
|
+
titleTemplate?: string;
|
|
210
|
+
description?: string;
|
|
211
|
+
twitterCard?: "summary" | "summary_large_image";
|
|
212
|
+
}
|
|
213
|
+
interface OGConfig {
|
|
214
|
+
enabled?: boolean;
|
|
215
|
+
type?: "static" | "dynamic";
|
|
216
|
+
endpoint?: string;
|
|
217
|
+
defaultImage?: string;
|
|
218
|
+
}
|
|
219
|
+
interface PageFrontmatter {
|
|
220
|
+
title: string;
|
|
221
|
+
description?: string;
|
|
222
|
+
tags?: string[];
|
|
223
|
+
icon?: string;
|
|
224
|
+
/** Path to custom OG image for this page */
|
|
225
|
+
ogImage?: string;
|
|
226
|
+
}
|
|
227
|
+
interface DocsNav {
|
|
228
|
+
/**
|
|
229
|
+
* Sidebar title — a plain string or a React element (e.g. a div with an icon).
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* ```tsx
|
|
233
|
+
* // Simple string
|
|
234
|
+
* nav: { title: "My Docs" }
|
|
235
|
+
*
|
|
236
|
+
* // React element with icon
|
|
237
|
+
* nav: {
|
|
238
|
+
* title: <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
239
|
+
* <Rocket size={18} /> Example Docs
|
|
240
|
+
* </div>
|
|
241
|
+
* }
|
|
242
|
+
* ```
|
|
243
|
+
*/
|
|
244
|
+
title?: unknown;
|
|
245
|
+
/** URL the title links to. Defaults to `/{entry}`. */
|
|
246
|
+
url?: string;
|
|
247
|
+
}
|
|
248
|
+
interface ThemeToggleConfig {
|
|
249
|
+
/**
|
|
250
|
+
* Whether to show the light/dark theme toggle in the sidebar.
|
|
251
|
+
* @default true
|
|
252
|
+
*/
|
|
253
|
+
enabled?: boolean;
|
|
254
|
+
/**
|
|
255
|
+
* The default / forced theme when the toggle is hidden.
|
|
256
|
+
* Only applies when `enabled` is `false`.
|
|
257
|
+
* @default "system"
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* ```ts
|
|
261
|
+
* // Hide toggle, force dark mode
|
|
262
|
+
* themeToggle: { enabled: false, default: "dark" }
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
default?: "light" | "dark" | "system";
|
|
266
|
+
/**
|
|
267
|
+
* Toggle mode — show only light/dark, or include a system option.
|
|
268
|
+
* @default "light-dark"
|
|
269
|
+
*/
|
|
270
|
+
mode?: "light-dark" | "light-dark-system";
|
|
271
|
+
}
|
|
272
|
+
interface BreadcrumbConfig {
|
|
273
|
+
/**
|
|
274
|
+
* Whether to show the breadcrumb navigation above page content.
|
|
275
|
+
* @default true
|
|
276
|
+
*/
|
|
277
|
+
enabled?: boolean;
|
|
278
|
+
/**
|
|
279
|
+
* Custom breadcrumb component. Receives the default breadcrumb as children
|
|
280
|
+
* so you can wrap/modify it.
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* ```tsx
|
|
284
|
+
* breadcrumb: {
|
|
285
|
+
* component: ({ items }) => <MyBreadcrumb items={items} />,
|
|
286
|
+
* }
|
|
287
|
+
* ```
|
|
288
|
+
*/
|
|
289
|
+
component?: unknown;
|
|
290
|
+
}
|
|
291
|
+
interface SidebarConfig {
|
|
292
|
+
/**
|
|
293
|
+
* Whether to show the sidebar.
|
|
294
|
+
* @default true
|
|
295
|
+
*/
|
|
296
|
+
enabled?: boolean;
|
|
297
|
+
/**
|
|
298
|
+
* Custom sidebar component to completely replace the default sidebar.
|
|
299
|
+
* Receives the page tree and config as context.
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* ```tsx
|
|
303
|
+
* sidebar: {
|
|
304
|
+
* component: MySidebar,
|
|
305
|
+
* }
|
|
306
|
+
* ```
|
|
307
|
+
*/
|
|
308
|
+
component?: unknown;
|
|
309
|
+
/**
|
|
310
|
+
* Sidebar footer content (rendered below navigation items).
|
|
311
|
+
*/
|
|
312
|
+
footer?: unknown;
|
|
313
|
+
/**
|
|
314
|
+
* Sidebar banner content (rendered above navigation items).
|
|
315
|
+
*/
|
|
316
|
+
banner?: unknown;
|
|
317
|
+
/**
|
|
318
|
+
* Whether the sidebar is collapsible on desktop.
|
|
319
|
+
* @default true
|
|
320
|
+
*/
|
|
321
|
+
collapsible?: boolean;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* A single "Open in …" provider shown in the Open dropdown.
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* ```ts
|
|
328
|
+
* { name: "Claude", icon: <ClaudeIcon />, urlTemplate: "https://claude.ai?url={url}" }
|
|
329
|
+
* ```
|
|
330
|
+
*/
|
|
331
|
+
interface OpenDocsProvider {
|
|
332
|
+
/** Display name (e.g. "ChatGPT", "Claude", "Cursor") */
|
|
333
|
+
name: string;
|
|
334
|
+
/** Icon element rendered next to the name */
|
|
335
|
+
icon?: unknown;
|
|
336
|
+
/**
|
|
337
|
+
* URL template. `{url}` is replaced with the current page URL.
|
|
338
|
+
* `{mdxUrl}` is replaced with the `.mdx` variant of the page URL.
|
|
339
|
+
*
|
|
340
|
+
* @example "https://claude.ai/new?q=Read+this+doc:+{url}"
|
|
341
|
+
*/
|
|
342
|
+
urlTemplate: string;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Configuration for the "Open in …" dropdown that lets users
|
|
346
|
+
* send the current page to an LLM or external tool.
|
|
347
|
+
*
|
|
348
|
+
* @example
|
|
349
|
+
* ```ts
|
|
350
|
+
* openDocs: {
|
|
351
|
+
* enabled: true,
|
|
352
|
+
* providers: [
|
|
353
|
+
* { name: "ChatGPT", icon: <ChatGPTIcon />, urlTemplate: "https://chatgpt.com/?q={url}" },
|
|
354
|
+
* { name: "Claude", icon: <ClaudeIcon />, urlTemplate: "https://claude.ai/new?q={url}" },
|
|
355
|
+
* ],
|
|
356
|
+
* }
|
|
357
|
+
* ```
|
|
358
|
+
*/
|
|
359
|
+
interface OpenDocsConfig {
|
|
360
|
+
/** Whether to show the "Open" dropdown. @default false */
|
|
361
|
+
enabled?: boolean;
|
|
362
|
+
/**
|
|
363
|
+
* List of LLM / tool providers to show in the dropdown.
|
|
364
|
+
* If not provided, a sensible default list is used.
|
|
365
|
+
*/
|
|
366
|
+
providers?: OpenDocsProvider[];
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Configuration for the "Copy Markdown" button that copies
|
|
370
|
+
* the current page's content as Markdown to the clipboard.
|
|
371
|
+
*/
|
|
372
|
+
interface CopyMarkdownConfig {
|
|
373
|
+
/** Whether to show the "Copy Markdown" button. @default false */
|
|
374
|
+
enabled?: boolean;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Page-level action buttons shown above the page content
|
|
378
|
+
* (e.g. "Copy Markdown", "Open in …" dropdown).
|
|
379
|
+
*
|
|
380
|
+
* @example
|
|
381
|
+
* ```ts
|
|
382
|
+
* pageActions: {
|
|
383
|
+
* copyMarkdown: { enabled: true },
|
|
384
|
+
* openDocs: {
|
|
385
|
+
* enabled: true,
|
|
386
|
+
* providers: [
|
|
387
|
+
* { name: "Claude", urlTemplate: "https://claude.ai/new?q={url}" },
|
|
388
|
+
* ],
|
|
389
|
+
* },
|
|
390
|
+
* }
|
|
391
|
+
* ```
|
|
392
|
+
*/
|
|
393
|
+
interface PageActionsConfig {
|
|
394
|
+
/** "Copy Markdown" button */
|
|
395
|
+
copyMarkdown?: boolean | CopyMarkdownConfig;
|
|
396
|
+
/** "Open in …" dropdown with LLM / tool providers */
|
|
397
|
+
openDocs?: boolean | OpenDocsConfig;
|
|
398
|
+
/**
|
|
399
|
+
* Where to render the page action buttons relative to the page title.
|
|
400
|
+
*
|
|
401
|
+
* - `"below-title"` — render below the first `<h1>` heading (default)
|
|
402
|
+
* - `"above-title"` — render above the page title / content
|
|
403
|
+
*
|
|
404
|
+
* @default "below-title"
|
|
405
|
+
*/
|
|
406
|
+
position?: "above-title" | "below-title";
|
|
407
|
+
}
|
|
408
|
+
interface DocsConfig {
|
|
409
|
+
/** Entry folder for docs (e.g. "docs" → /docs) */
|
|
410
|
+
entry: string;
|
|
411
|
+
/** Theme configuration - single source of truth for UI */
|
|
412
|
+
theme?: DocsTheme;
|
|
413
|
+
/**
|
|
414
|
+
* Sidebar navigation header.
|
|
415
|
+
* Customise the title shown at the top of the sidebar.
|
|
416
|
+
*/
|
|
417
|
+
nav?: DocsNav;
|
|
418
|
+
/**
|
|
419
|
+
* Theme toggle (light/dark mode switcher) in the sidebar.
|
|
420
|
+
*
|
|
421
|
+
* - `true` or `undefined` → toggle is shown (default)
|
|
422
|
+
* - `false` → toggle is hidden, defaults to system theme
|
|
423
|
+
* - `{ enabled: false, default: "dark" }` → toggle hidden, force dark
|
|
424
|
+
*
|
|
425
|
+
* @example
|
|
426
|
+
* ```ts
|
|
427
|
+
* // Hide toggle, force dark mode
|
|
428
|
+
* themeToggle: { enabled: false, default: "dark" }
|
|
429
|
+
*
|
|
430
|
+
* // Show toggle with system option
|
|
431
|
+
* themeToggle: { mode: "light-dark-system" }
|
|
432
|
+
* ```
|
|
433
|
+
*/
|
|
434
|
+
themeToggle?: boolean | ThemeToggleConfig;
|
|
435
|
+
/**
|
|
436
|
+
* Breadcrumb navigation above page content.
|
|
437
|
+
*
|
|
438
|
+
* - `true` or `undefined` → breadcrumb is shown (default)
|
|
439
|
+
* - `false` → breadcrumb is hidden
|
|
440
|
+
* - `{ enabled: false }` → breadcrumb is hidden
|
|
441
|
+
* - `{ component: MyBreadcrumb }` → custom breadcrumb component
|
|
442
|
+
*/
|
|
443
|
+
breadcrumb?: boolean | BreadcrumbConfig;
|
|
444
|
+
/**
|
|
445
|
+
* Sidebar customisation.
|
|
446
|
+
*
|
|
447
|
+
* - `true` or `undefined` → default sidebar
|
|
448
|
+
* - `false` → sidebar is hidden
|
|
449
|
+
* - `{ component: MySidebar }` → custom sidebar component
|
|
450
|
+
* - `{ footer: <MyFooter />, banner: <MyBanner /> }` → add footer/banner
|
|
451
|
+
*/
|
|
452
|
+
sidebar?: boolean | SidebarConfig;
|
|
453
|
+
/**
|
|
454
|
+
* Custom MDX component overrides.
|
|
455
|
+
*
|
|
456
|
+
* Pass your own React components to replace defaults (e.g. Callout, CodeBlock).
|
|
457
|
+
* Components must match the expected props interface.
|
|
458
|
+
*
|
|
459
|
+
* @example
|
|
460
|
+
* ```ts
|
|
461
|
+
* import { MyCallout } from "./components/my-callout";
|
|
462
|
+
*
|
|
463
|
+
* export default defineDocs({
|
|
464
|
+
* entry: "docs",
|
|
465
|
+
* theme: fumadocs(),
|
|
466
|
+
* components: {
|
|
467
|
+
* Callout: MyCallout,
|
|
468
|
+
* },
|
|
469
|
+
* });
|
|
470
|
+
* ```
|
|
471
|
+
*/
|
|
472
|
+
components?: Record<string, unknown>;
|
|
473
|
+
/**
|
|
474
|
+
* Icon registry for sidebar items.
|
|
475
|
+
*
|
|
476
|
+
* Map string labels to React elements. Reference them in page frontmatter
|
|
477
|
+
* with `icon: "label"` and the matching icon renders in the sidebar.
|
|
478
|
+
*
|
|
479
|
+
* @example
|
|
480
|
+
* ```tsx
|
|
481
|
+
* import { Book, Terminal, Rocket } from "lucide-react";
|
|
482
|
+
*
|
|
483
|
+
* export default defineDocs({
|
|
484
|
+
* entry: "docs",
|
|
485
|
+
* theme: fumadocs(),
|
|
486
|
+
* icons: {
|
|
487
|
+
* book: <Book />,
|
|
488
|
+
* terminal: <Terminal />,
|
|
489
|
+
* rocket: <Rocket />,
|
|
490
|
+
* },
|
|
491
|
+
* });
|
|
492
|
+
* ```
|
|
493
|
+
*
|
|
494
|
+
* Then in `page.mdx` frontmatter:
|
|
495
|
+
* ```yaml
|
|
496
|
+
* ---
|
|
497
|
+
* title: "CLI Reference"
|
|
498
|
+
* icon: "terminal"
|
|
499
|
+
* ---
|
|
500
|
+
* ```
|
|
501
|
+
*/
|
|
502
|
+
icons?: Record<string, unknown>;
|
|
503
|
+
/**
|
|
504
|
+
* Page action buttons shown above the content area.
|
|
505
|
+
* Includes "Copy Markdown" and "Open in …" (LLM dropdown).
|
|
506
|
+
*
|
|
507
|
+
* @example
|
|
508
|
+
* ```ts
|
|
509
|
+
* pageActions: {
|
|
510
|
+
* copyMarkdown: { enabled: true },
|
|
511
|
+
* openDocs: {
|
|
512
|
+
* enabled: true,
|
|
513
|
+
* providers: [
|
|
514
|
+
* { name: "ChatGPT", urlTemplate: "https://chatgpt.com/?q={url}" },
|
|
515
|
+
* { name: "Claude", urlTemplate: "https://claude.ai/new?q={url}" },
|
|
516
|
+
* ],
|
|
517
|
+
* },
|
|
518
|
+
* }
|
|
519
|
+
* ```
|
|
520
|
+
*/
|
|
521
|
+
pageActions?: PageActionsConfig;
|
|
522
|
+
/** SEO metadata - separate from theme */
|
|
523
|
+
metadata?: DocsMetadata;
|
|
524
|
+
/** Open Graph image handling */
|
|
525
|
+
og?: OGConfig;
|
|
526
|
+
}
|
|
527
|
+
//#endregion
|
|
528
|
+
//#region src/define-docs.d.ts
|
|
529
|
+
/**
|
|
530
|
+
* Define docs configuration. Validates and returns the config.
|
|
531
|
+
*/
|
|
532
|
+
declare function defineDocs(config: DocsConfig): DocsConfig;
|
|
533
|
+
//#endregion
|
|
534
|
+
//#region src/utils.d.ts
|
|
535
|
+
/**
|
|
536
|
+
* Deep merge utility for theme overrides.
|
|
537
|
+
* Merges objects recursively; later values override earlier ones.
|
|
538
|
+
*/
|
|
539
|
+
declare function deepMerge<T extends Record<string, unknown>>(target: T, ...sources: Partial<T>[]): T;
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region src/create-theme.d.ts
|
|
542
|
+
/**
|
|
543
|
+
* Create a theme preset factory.
|
|
544
|
+
*
|
|
545
|
+
* Returns a function that accepts optional overrides and deep-merges them
|
|
546
|
+
* with the base theme defaults. This is the same pattern used by the
|
|
547
|
+
* built-in `fumadocs()`, `darksharp()`, and `pixelBorder()` presets.
|
|
548
|
+
*
|
|
549
|
+
* @param baseTheme - The default theme configuration
|
|
550
|
+
* @returns A factory function `(overrides?) => DocsTheme`
|
|
551
|
+
*
|
|
552
|
+
* @example
|
|
553
|
+
* ```ts
|
|
554
|
+
* import { createTheme } from "@farming-labs/docs";
|
|
555
|
+
*
|
|
556
|
+
* export const myTheme = createTheme({
|
|
557
|
+
* name: "my-theme",
|
|
558
|
+
* ui: {
|
|
559
|
+
* colors: { primary: "#6366f1" },
|
|
560
|
+
* layout: { contentWidth: 800 },
|
|
561
|
+
* },
|
|
562
|
+
* });
|
|
563
|
+
* ```
|
|
564
|
+
*/
|
|
565
|
+
declare function createTheme(baseTheme: DocsTheme): (overrides?: Partial<DocsTheme>) => DocsTheme;
|
|
566
|
+
/**
|
|
567
|
+
* Extend an existing theme preset with additional defaults.
|
|
568
|
+
*
|
|
569
|
+
* Useful when you want to build on top of an existing theme (e.g. fumadocs)
|
|
570
|
+
* rather than starting from scratch.
|
|
571
|
+
*
|
|
572
|
+
* @example
|
|
573
|
+
* ```ts
|
|
574
|
+
* import { extendTheme } from "@farming-labs/docs";
|
|
575
|
+
* import { fumadocs } from "@farming-labs/fumadocs/default";
|
|
576
|
+
*
|
|
577
|
+
* // Start with fumadocs defaults, override some values
|
|
578
|
+
* export const myTheme = extendTheme(fumadocs(), {
|
|
579
|
+
* name: "my-custom-fumadocs",
|
|
580
|
+
* ui: { colors: { primary: "#22c55e" } },
|
|
581
|
+
* });
|
|
582
|
+
* ```
|
|
583
|
+
*/
|
|
584
|
+
declare function extendTheme(baseTheme: DocsTheme, extensions: Partial<DocsTheme>): DocsTheme;
|
|
585
|
+
//#endregion
|
|
586
|
+
//#region src/metadata.d.ts
|
|
587
|
+
/**
|
|
588
|
+
* Resolve page title using metadata titleTemplate.
|
|
589
|
+
* %s is replaced with page title.
|
|
590
|
+
*/
|
|
591
|
+
declare function resolveTitle(pageTitle: string, metadata?: DocsMetadata): string;
|
|
592
|
+
/**
|
|
593
|
+
* Resolve OG image URL for a page.
|
|
594
|
+
*/
|
|
595
|
+
declare function resolveOGImage(page: PageFrontmatter, ogConfig?: OGConfig, baseUrl?: string): string | undefined;
|
|
596
|
+
//#endregion
|
|
597
|
+
export { type BreadcrumbConfig, type CopyMarkdownConfig, type DocsConfig, type DocsMetadata, type DocsNav, type DocsTheme, type FontStyle, type OGConfig, type OpenDocsConfig, type OpenDocsProvider, type PageActionsConfig, type PageFrontmatter, type SidebarConfig, type ThemeToggleConfig, type TypographyConfig, type UIConfig, createTheme, deepMerge, defineDocs, extendTheme, resolveOGImage, resolveTitle };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
//#region src/define-docs.ts
|
|
2
|
+
/**
|
|
3
|
+
* Define docs configuration. Validates and returns the config.
|
|
4
|
+
*/
|
|
5
|
+
function defineDocs(config) {
|
|
6
|
+
return {
|
|
7
|
+
entry: config.entry ?? "docs",
|
|
8
|
+
theme: config.theme,
|
|
9
|
+
nav: config.nav,
|
|
10
|
+
themeToggle: config.themeToggle,
|
|
11
|
+
breadcrumb: config.breadcrumb,
|
|
12
|
+
sidebar: config.sidebar,
|
|
13
|
+
components: config.components,
|
|
14
|
+
icons: config.icons,
|
|
15
|
+
pageActions: config.pageActions,
|
|
16
|
+
metadata: config.metadata,
|
|
17
|
+
og: config.og
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/utils.ts
|
|
23
|
+
/**
|
|
24
|
+
* Deep merge utility for theme overrides.
|
|
25
|
+
* Merges objects recursively; later values override earlier ones.
|
|
26
|
+
*/
|
|
27
|
+
function deepMerge(target, ...sources) {
|
|
28
|
+
if (!sources.length) return target;
|
|
29
|
+
const source = sources.shift();
|
|
30
|
+
if (!source) return target;
|
|
31
|
+
const result = { ...target };
|
|
32
|
+
for (const key of Object.keys(source)) {
|
|
33
|
+
const sourceVal = source[key];
|
|
34
|
+
const targetVal = result[key];
|
|
35
|
+
if (sourceVal && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal && typeof targetVal === "object" && !Array.isArray(targetVal)) result[key] = deepMerge(targetVal, sourceVal);
|
|
36
|
+
else if (sourceVal !== void 0) result[key] = sourceVal;
|
|
37
|
+
}
|
|
38
|
+
if (sources.length) return deepMerge(result, ...sources);
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/create-theme.ts
|
|
44
|
+
/**
|
|
45
|
+
* Create a theme preset factory.
|
|
46
|
+
*
|
|
47
|
+
* Returns a function that accepts optional overrides and deep-merges them
|
|
48
|
+
* with the base theme defaults. This is the same pattern used by the
|
|
49
|
+
* built-in `fumadocs()`, `darksharp()`, and `pixelBorder()` presets.
|
|
50
|
+
*
|
|
51
|
+
* @param baseTheme - The default theme configuration
|
|
52
|
+
* @returns A factory function `(overrides?) => DocsTheme`
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* import { createTheme } from "@farming-labs/docs";
|
|
57
|
+
*
|
|
58
|
+
* export const myTheme = createTheme({
|
|
59
|
+
* name: "my-theme",
|
|
60
|
+
* ui: {
|
|
61
|
+
* colors: { primary: "#6366f1" },
|
|
62
|
+
* layout: { contentWidth: 800 },
|
|
63
|
+
* },
|
|
64
|
+
* });
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
function createTheme(baseTheme) {
|
|
68
|
+
return function themeFactory(overrides = {}) {
|
|
69
|
+
const merged = deepMerge(baseTheme, overrides);
|
|
70
|
+
if (overrides.ui?.colors) merged._userColorOverrides = { ...overrides.ui.colors };
|
|
71
|
+
return merged;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Extend an existing theme preset with additional defaults.
|
|
76
|
+
*
|
|
77
|
+
* Useful when you want to build on top of an existing theme (e.g. fumadocs)
|
|
78
|
+
* rather than starting from scratch.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* import { extendTheme } from "@farming-labs/docs";
|
|
83
|
+
* import { fumadocs } from "@farming-labs/fumadocs/default";
|
|
84
|
+
*
|
|
85
|
+
* // Start with fumadocs defaults, override some values
|
|
86
|
+
* export const myTheme = extendTheme(fumadocs(), {
|
|
87
|
+
* name: "my-custom-fumadocs",
|
|
88
|
+
* ui: { colors: { primary: "#22c55e" } },
|
|
89
|
+
* });
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
function extendTheme(baseTheme, extensions) {
|
|
93
|
+
return deepMerge(baseTheme, extensions);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/metadata.ts
|
|
98
|
+
/**
|
|
99
|
+
* Resolve page title using metadata titleTemplate.
|
|
100
|
+
* %s is replaced with page title.
|
|
101
|
+
*/
|
|
102
|
+
function resolveTitle(pageTitle, metadata) {
|
|
103
|
+
return (metadata?.titleTemplate ?? "%s").replace("%s", pageTitle);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Resolve OG image URL for a page.
|
|
107
|
+
*/
|
|
108
|
+
function resolveOGImage(page, ogConfig, baseUrl) {
|
|
109
|
+
if (!ogConfig?.enabled) return void 0;
|
|
110
|
+
if (page.ogImage) return page.ogImage.startsWith("/") || page.ogImage.startsWith("http") ? page.ogImage : `${baseUrl ?? ""}${page.ogImage}`;
|
|
111
|
+
if (ogConfig.type === "dynamic" && ogConfig.endpoint) return `${baseUrl ?? ""}${ogConfig.endpoint}`;
|
|
112
|
+
return ogConfig.defaultImage;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
//#endregion
|
|
116
|
+
export { createTheme, deepMerge, defineDocs, extendTheme, resolveOGImage, resolveTitle };
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@farming-labs/docs",
|
|
3
|
+
"version": "0.0.2-beta.1",
|
|
4
|
+
"description": "Modern, flexible MDX-based docs framework — core types, config, and CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.mjs",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.mts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.mts",
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"default": "./dist/index.mjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"farming-docs": "./dist/cli/index.mjs"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"docs",
|
|
24
|
+
"mdx",
|
|
25
|
+
"documentation"
|
|
26
|
+
],
|
|
27
|
+
"author": "Farming Labs",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@clack/prompts": "^0.9.1",
|
|
31
|
+
"gray-matter": "^4.0.3",
|
|
32
|
+
"picocolors": "^1.1.1"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^22.10.0",
|
|
36
|
+
"tsdown": "^0.20.3",
|
|
37
|
+
"typescript": "^5.9.3"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsdown",
|
|
41
|
+
"dev": "tsdown --watch",
|
|
42
|
+
"typecheck": "tsc --noEmit"
|
|
43
|
+
}
|
|
44
|
+
}
|