@farming-labs/docs 0.0.34 → 0.0.37
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 +5 -0
- package/dist/cli/index.mjs +3365 -0
- package/dist/index.d.mts +1380 -0
- package/dist/index.mjs +223 -0
- package/package.json +1 -1
|
@@ -0,0 +1,3365 @@
|
|
|
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
|
+
const allDeps = {
|
|
14
|
+
...pkg.dependencies,
|
|
15
|
+
...pkg.devDependencies
|
|
16
|
+
};
|
|
17
|
+
if (allDeps["next"]) return "nextjs";
|
|
18
|
+
if (allDeps["@tanstack/react-start"]) return "tanstack-start";
|
|
19
|
+
if (allDeps["@sveltejs/kit"]) return "sveltekit";
|
|
20
|
+
if (allDeps["astro"]) return "astro";
|
|
21
|
+
if (allDeps["nuxt"]) return "nuxt";
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function detectPackageManagerFromLockfile(cwd) {
|
|
25
|
+
if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
26
|
+
if (fs.existsSync(path.join(cwd, "bun.lockb")) || fs.existsSync(path.join(cwd, "bun.lock"))) return "bun";
|
|
27
|
+
if (fs.existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
|
|
28
|
+
if (fs.existsSync(path.join(cwd, "package-lock.json"))) return "npm";
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
function installCommand(pm) {
|
|
32
|
+
return pm === "yarn" ? "yarn add" : `${pm} add`;
|
|
33
|
+
}
|
|
34
|
+
function devInstallCommand(pm) {
|
|
35
|
+
if (pm === "yarn") return "yarn add -D";
|
|
36
|
+
if (pm === "npm") return "npm install -D";
|
|
37
|
+
return `${pm} add -D`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Write a file, creating parent directories as needed.
|
|
41
|
+
* Returns true if the file was written, false if it already existed and was skipped.
|
|
42
|
+
*/
|
|
43
|
+
function writeFileSafe(filePath, content, overwrite = false) {
|
|
44
|
+
if (fs.existsSync(filePath) && !overwrite) return false;
|
|
45
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
46
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Check if a file exists.
|
|
51
|
+
*/
|
|
52
|
+
function fileExists(filePath) {
|
|
53
|
+
return fs.existsSync(filePath);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Read a file, returning null if it does not exist.
|
|
57
|
+
*/
|
|
58
|
+
function readFileSafe(filePath) {
|
|
59
|
+
if (!fs.existsSync(filePath)) return null;
|
|
60
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
61
|
+
}
|
|
62
|
+
/** Common locations where global CSS files live in Next.js / SvelteKit projects. */
|
|
63
|
+
const GLOBAL_CSS_CANDIDATES = [
|
|
64
|
+
"app/globals.css",
|
|
65
|
+
"app/global.css",
|
|
66
|
+
"src/app/globals.css",
|
|
67
|
+
"src/app/global.css",
|
|
68
|
+
"src/app.css",
|
|
69
|
+
"src/styles/app.css",
|
|
70
|
+
"styles/globals.css",
|
|
71
|
+
"styles/global.css",
|
|
72
|
+
"src/styles/globals.css",
|
|
73
|
+
"src/styles/global.css",
|
|
74
|
+
"assets/css/main.css",
|
|
75
|
+
"assets/main.css"
|
|
76
|
+
];
|
|
77
|
+
/**
|
|
78
|
+
* Find existing global CSS files in the project.
|
|
79
|
+
* Returns relative paths that exist.
|
|
80
|
+
*/
|
|
81
|
+
function detectGlobalCssFiles(cwd) {
|
|
82
|
+
return GLOBAL_CSS_CANDIDATES.filter((rel) => fs.existsSync(path.join(cwd, rel)));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Detect whether the Next.js project uses `app` or `src/app` for the App Router.
|
|
86
|
+
* Returns the directory that exists; if both exist, prefers src/app; if neither, returns null.
|
|
87
|
+
*/
|
|
88
|
+
function detectNextAppDir(cwd) {
|
|
89
|
+
const hasSrcApp = fs.existsSync(path.join(cwd, "src", "app"));
|
|
90
|
+
const hasApp = fs.existsSync(path.join(cwd, "app"));
|
|
91
|
+
if (hasSrcApp) return "src/app";
|
|
92
|
+
if (hasApp) return "app";
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Run a shell command synchronously, inheriting stdio.
|
|
97
|
+
*/
|
|
98
|
+
function exec(command, cwd) {
|
|
99
|
+
execSync(command, {
|
|
100
|
+
cwd,
|
|
101
|
+
stdio: "inherit"
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Spawn a process and wait for a specific string in stdout,
|
|
106
|
+
* then resolve with the child process (still running).
|
|
107
|
+
*/
|
|
108
|
+
function spawnAndWaitFor(command, args, cwd, waitFor, timeoutMs = 6e4) {
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
const child = spawn(command, args, {
|
|
111
|
+
cwd,
|
|
112
|
+
stdio: [
|
|
113
|
+
"ignore",
|
|
114
|
+
"pipe",
|
|
115
|
+
"pipe"
|
|
116
|
+
],
|
|
117
|
+
shell: true
|
|
118
|
+
});
|
|
119
|
+
let output = "";
|
|
120
|
+
const timer = setTimeout(() => {
|
|
121
|
+
child.kill();
|
|
122
|
+
reject(/* @__PURE__ */ new Error(`Timed out waiting for "${waitFor}" after ${timeoutMs}ms`));
|
|
123
|
+
}, timeoutMs);
|
|
124
|
+
child.stdout?.on("data", (data) => {
|
|
125
|
+
const text = data.toString();
|
|
126
|
+
output += text;
|
|
127
|
+
process.stdout.write(text);
|
|
128
|
+
if (output.includes(waitFor)) {
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
resolve(child);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
child.stderr?.on("data", (data) => {
|
|
134
|
+
process.stderr.write(data.toString());
|
|
135
|
+
});
|
|
136
|
+
child.on("error", (err) => {
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
reject(err);
|
|
139
|
+
});
|
|
140
|
+
child.on("close", (code) => {
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
if (!output.includes(waitFor)) reject(/* @__PURE__ */ new Error(`Process exited with code ${code} before "${waitFor}" appeared`));
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
//#endregion
|
|
148
|
+
//#region src/cli/templates.ts
|
|
149
|
+
const THEME_INFO = {
|
|
150
|
+
fumadocs: {
|
|
151
|
+
factory: "fumadocs",
|
|
152
|
+
nextImport: "@farming-labs/theme",
|
|
153
|
+
svelteImport: "@farming-labs/svelte-theme",
|
|
154
|
+
astroImport: "@farming-labs/astro-theme",
|
|
155
|
+
nuxtImport: "@farming-labs/nuxt-theme",
|
|
156
|
+
nextCssImport: "default",
|
|
157
|
+
svelteCssTheme: "fumadocs",
|
|
158
|
+
astroCssTheme: "fumadocs",
|
|
159
|
+
nuxtCssTheme: "fumadocs"
|
|
160
|
+
},
|
|
161
|
+
darksharp: {
|
|
162
|
+
factory: "darksharp",
|
|
163
|
+
nextImport: "@farming-labs/theme/darksharp",
|
|
164
|
+
svelteImport: "@farming-labs/svelte-theme/darksharp",
|
|
165
|
+
astroImport: "@farming-labs/astro-theme/darksharp",
|
|
166
|
+
nuxtImport: "@farming-labs/nuxt-theme/darksharp",
|
|
167
|
+
nextCssImport: "darksharp",
|
|
168
|
+
svelteCssTheme: "darksharp",
|
|
169
|
+
astroCssTheme: "darksharp",
|
|
170
|
+
nuxtCssTheme: "darksharp"
|
|
171
|
+
},
|
|
172
|
+
"pixel-border": {
|
|
173
|
+
factory: "pixelBorder",
|
|
174
|
+
nextImport: "@farming-labs/theme/pixel-border",
|
|
175
|
+
svelteImport: "@farming-labs/svelte-theme/pixel-border",
|
|
176
|
+
astroImport: "@farming-labs/astro-theme/pixel-border",
|
|
177
|
+
nuxtImport: "@farming-labs/nuxt-theme/pixel-border",
|
|
178
|
+
nextCssImport: "pixel-border",
|
|
179
|
+
svelteCssTheme: "pixel-border",
|
|
180
|
+
astroCssTheme: "pixel-border",
|
|
181
|
+
nuxtCssTheme: "pixel-border"
|
|
182
|
+
},
|
|
183
|
+
colorful: {
|
|
184
|
+
factory: "colorful",
|
|
185
|
+
nextImport: "@farming-labs/theme/colorful",
|
|
186
|
+
svelteImport: "@farming-labs/svelte-theme/colorful",
|
|
187
|
+
astroImport: "@farming-labs/astro-theme/colorful",
|
|
188
|
+
nuxtImport: "@farming-labs/nuxt-theme/colorful",
|
|
189
|
+
nextCssImport: "colorful",
|
|
190
|
+
svelteCssTheme: "colorful",
|
|
191
|
+
astroCssTheme: "colorful",
|
|
192
|
+
nuxtCssTheme: "colorful"
|
|
193
|
+
},
|
|
194
|
+
darkbold: {
|
|
195
|
+
factory: "darkbold",
|
|
196
|
+
nextImport: "@farming-labs/theme/darkbold",
|
|
197
|
+
svelteImport: "@farming-labs/svelte-theme/darkbold",
|
|
198
|
+
astroImport: "@farming-labs/astro-theme/darkbold",
|
|
199
|
+
nuxtImport: "@farming-labs/nuxt-theme/darkbold",
|
|
200
|
+
nextCssImport: "darkbold",
|
|
201
|
+
svelteCssTheme: "darkbold",
|
|
202
|
+
astroCssTheme: "darkbold",
|
|
203
|
+
nuxtCssTheme: "darkbold"
|
|
204
|
+
},
|
|
205
|
+
shiny: {
|
|
206
|
+
factory: "shiny",
|
|
207
|
+
nextImport: "@farming-labs/theme/shiny",
|
|
208
|
+
svelteImport: "@farming-labs/svelte-theme/shiny",
|
|
209
|
+
astroImport: "@farming-labs/astro-theme/shiny",
|
|
210
|
+
nuxtImport: "@farming-labs/nuxt-theme/shiny",
|
|
211
|
+
nextCssImport: "shiny",
|
|
212
|
+
svelteCssTheme: "shiny",
|
|
213
|
+
astroCssTheme: "shiny",
|
|
214
|
+
nuxtCssTheme: "shiny"
|
|
215
|
+
},
|
|
216
|
+
greentree: {
|
|
217
|
+
factory: "greentree",
|
|
218
|
+
nextImport: "@farming-labs/theme/greentree",
|
|
219
|
+
svelteImport: "@farming-labs/svelte-theme/greentree",
|
|
220
|
+
astroImport: "@farming-labs/astro-theme/greentree",
|
|
221
|
+
nuxtImport: "@farming-labs/nuxt-theme/greentree",
|
|
222
|
+
nextCssImport: "greentree",
|
|
223
|
+
svelteCssTheme: "greentree",
|
|
224
|
+
astroCssTheme: "greentree",
|
|
225
|
+
nuxtCssTheme: "greentree"
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
function getThemeInfo(theme) {
|
|
229
|
+
return THEME_INFO[theme] ?? THEME_INFO.fumadocs;
|
|
230
|
+
}
|
|
231
|
+
function toPosixPath(value) {
|
|
232
|
+
return value.replace(/\\/g, "/");
|
|
233
|
+
}
|
|
234
|
+
function stripScriptExtension(value) {
|
|
235
|
+
return value.replace(/\.(?:[cm]?[jt]sx?)$/i, "");
|
|
236
|
+
}
|
|
237
|
+
function relativeImport(fromFile, toFile) {
|
|
238
|
+
const fromPosix = toPosixPath(fromFile);
|
|
239
|
+
const toPosix = stripScriptExtension(toPosixPath(toFile));
|
|
240
|
+
const rel = path.posix.relative(path.posix.dirname(fromPosix), toPosix);
|
|
241
|
+
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
242
|
+
}
|
|
243
|
+
function relativeAssetPath(fromFile, toFile) {
|
|
244
|
+
const fromPosix = toPosixPath(fromFile);
|
|
245
|
+
const toPosix = toPosixPath(toFile);
|
|
246
|
+
const rel = path.posix.relative(path.posix.dirname(fromPosix), toPosix);
|
|
247
|
+
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
248
|
+
}
|
|
249
|
+
function extractImportSpecifier(importLine) {
|
|
250
|
+
const fromMatch = importLine.match(/\bfrom\s+["']([^"']+)["']/);
|
|
251
|
+
if (fromMatch) return fromMatch[1];
|
|
252
|
+
const bareImportMatch = importLine.match(/^\s*import\s+["']([^"']+)["']/);
|
|
253
|
+
if (bareImportMatch) return bareImportMatch[1];
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
function addImportLine(content, importLine) {
|
|
257
|
+
if (content.includes(importLine)) return content;
|
|
258
|
+
const specifier = extractImportSpecifier(importLine);
|
|
259
|
+
if (specifier) {
|
|
260
|
+
if (new RegExp(String.raw`\bimport(?:\s+type)?[\s\S]*?\bfrom\s+["']${specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']|^\s*import\s+["']${specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`, "m").test(content)) return content;
|
|
261
|
+
}
|
|
262
|
+
const lines = content.split("\n");
|
|
263
|
+
const lastImportIdx = lines.reduce((acc, line, index) => {
|
|
264
|
+
const trimmed = line.trimStart();
|
|
265
|
+
return trimmed.startsWith("import ") || trimmed.startsWith("import type ") ? index : acc;
|
|
266
|
+
}, -1);
|
|
267
|
+
if (lastImportIdx >= 0) lines.splice(lastImportIdx + 1, 0, importLine);
|
|
268
|
+
else lines.unshift(importLine, "");
|
|
269
|
+
return lines.join("\n");
|
|
270
|
+
}
|
|
271
|
+
function renderI18nConfig(cfg, indent = " ") {
|
|
272
|
+
const i18n = cfg.i18n;
|
|
273
|
+
if (!i18n || i18n.locales.length === 0) return "";
|
|
274
|
+
return `${indent}i18n: {\n${indent} locales: [${i18n.locales.map((locale) => `"${locale}"`).join(", ")}],\n${indent} defaultLocale: "${i18n.defaultLocale}",\n${indent}},\n`;
|
|
275
|
+
}
|
|
276
|
+
function toLocaleImportName(locale) {
|
|
277
|
+
return `LocalePage_${locale.replace(/[^a-zA-Z0-9_$]/g, "_")}`;
|
|
278
|
+
}
|
|
279
|
+
function getThemeExportName(themeName) {
|
|
280
|
+
const base = themeName.replace(/\.ts$/i, "").trim();
|
|
281
|
+
if (!base) return "customTheme";
|
|
282
|
+
return base.replace(/-([a-z])/g, (_, c) => c.toUpperCase()).replace(/^./, (c) => c.toLowerCase());
|
|
283
|
+
}
|
|
284
|
+
function getCustomThemeCssImportPath(globalCssRelPath, themeName) {
|
|
285
|
+
if (globalCssRelPath.startsWith("app/")) return `../themes/${themeName}.css`;
|
|
286
|
+
if (globalCssRelPath.startsWith("src/")) return `../../themes/${themeName}.css`;
|
|
287
|
+
return `../themes/${themeName}.css`;
|
|
288
|
+
}
|
|
289
|
+
/** Content for themes/{name}.ts - createTheme with the given name */
|
|
290
|
+
function customThemeTsTemplate(themeName) {
|
|
291
|
+
return `\
|
|
292
|
+
import { createTheme } from "@farming-labs/docs";
|
|
293
|
+
|
|
294
|
+
export const ${getThemeExportName(themeName)} = createTheme({
|
|
295
|
+
name: "${themeName.replace(/\.ts$/i, "")}",
|
|
296
|
+
ui: {
|
|
297
|
+
colors: {
|
|
298
|
+
primary: "#e11d48",
|
|
299
|
+
background: "#09090b",
|
|
300
|
+
foreground: "#fafafa",
|
|
301
|
+
muted: "#71717a",
|
|
302
|
+
border: "#27272a",
|
|
303
|
+
},
|
|
304
|
+
radius: "0.5rem",
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
`;
|
|
308
|
+
}
|
|
309
|
+
function customThemeCssTemplate(themeName) {
|
|
310
|
+
return `\
|
|
311
|
+
/* Custom theme: ${themeName} - edit variables and selectors as needed */
|
|
312
|
+
@import "@farming-labs/theme/presets/black";
|
|
313
|
+
|
|
314
|
+
.dark {
|
|
315
|
+
--color-fd-primary: #e11d48;
|
|
316
|
+
--color-fd-background: #09090b;
|
|
317
|
+
--color-fd-border: #27272a;
|
|
318
|
+
--radius: 0.5rem;
|
|
319
|
+
}
|
|
320
|
+
`;
|
|
321
|
+
}
|
|
322
|
+
/** Config import for Next.js root layout → root docs.config */
|
|
323
|
+
function nextRootLayoutConfigImport(useAlias, nextAppDir = "app") {
|
|
324
|
+
if (useAlias) return "@/docs.config";
|
|
325
|
+
return nextAppDir === "src/app" ? "../../docs.config" : "../docs.config";
|
|
326
|
+
}
|
|
327
|
+
/** Config import for Next.js app/{entry}/layout.tsx → root docs.config */
|
|
328
|
+
function nextDocsLayoutConfigImport(useAlias, nextAppDir = "app") {
|
|
329
|
+
if (useAlias) return "@/docs.config";
|
|
330
|
+
return nextAppDir === "src/app" ? "../../../docs.config" : "../../docs.config";
|
|
331
|
+
}
|
|
332
|
+
/** Config import for SvelteKit src/lib/docs.server.ts → src/lib/docs.config */
|
|
333
|
+
function svelteServerConfigImport(useAlias) {
|
|
334
|
+
return useAlias ? "$lib/docs.config" : "./docs.config";
|
|
335
|
+
}
|
|
336
|
+
/** Config import for SvelteKit src/routes/{entry}/+layout.svelte → src/lib/docs.config */
|
|
337
|
+
function svelteLayoutConfigImport(useAlias) {
|
|
338
|
+
return useAlias ? "$lib/docs.config" : "../../lib/docs.config";
|
|
339
|
+
}
|
|
340
|
+
/** Config import for SvelteKit src/routes/{entry}/[...slug]/+page.svelte → src/lib/docs.config */
|
|
341
|
+
function sveltePageConfigImport(useAlias) {
|
|
342
|
+
return useAlias ? "$lib/docs.config" : "../../../lib/docs.config";
|
|
343
|
+
}
|
|
344
|
+
/** Server import for SvelteKit +layout.server.js → src/lib/docs.server */
|
|
345
|
+
function svelteLayoutServerImport(useAlias) {
|
|
346
|
+
return useAlias ? "$lib/docs.server" : "../../lib/docs.server";
|
|
347
|
+
}
|
|
348
|
+
function astroServerConfigImport(useAlias) {
|
|
349
|
+
return useAlias ? "@/lib/docs.config" : "./docs.config";
|
|
350
|
+
}
|
|
351
|
+
function astroPageConfigImport(useAlias, depth) {
|
|
352
|
+
if (useAlias) return "@/lib/docs.config";
|
|
353
|
+
return `${"../".repeat(depth)}lib/docs.config`;
|
|
354
|
+
}
|
|
355
|
+
function astroPageServerImport(useAlias, depth) {
|
|
356
|
+
if (useAlias) return "@/lib/docs.server";
|
|
357
|
+
return `${"../".repeat(depth)}lib/docs.server`;
|
|
358
|
+
}
|
|
359
|
+
function docsConfigTemplate(cfg) {
|
|
360
|
+
if (cfg.theme === "custom" && cfg.customThemeName) {
|
|
361
|
+
const exportName = getThemeExportName(cfg.customThemeName);
|
|
362
|
+
return `\
|
|
363
|
+
import { defineDocs } from "@farming-labs/docs";
|
|
364
|
+
import { ${exportName} } from "${cfg.useAlias ? "@/themes/" + cfg.customThemeName.replace(/\.ts$/i, "") : "./themes/" + cfg.customThemeName.replace(/\.ts$/i, "")}";
|
|
365
|
+
|
|
366
|
+
export default defineDocs({
|
|
367
|
+
entry: "${cfg.entry}",
|
|
368
|
+
${renderI18nConfig(cfg)} theme: ${exportName}({
|
|
369
|
+
ui: {
|
|
370
|
+
colors: { primary: "#6366f1" },
|
|
371
|
+
},
|
|
372
|
+
}),
|
|
373
|
+
|
|
374
|
+
metadata: {
|
|
375
|
+
titleTemplate: "%s – ${cfg.projectName}",
|
|
376
|
+
description: "Documentation for ${cfg.projectName}",
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
`;
|
|
380
|
+
}
|
|
381
|
+
const t = getThemeInfo(cfg.theme);
|
|
382
|
+
return `\
|
|
383
|
+
import { defineDocs } from "@farming-labs/docs";
|
|
384
|
+
import { ${t.factory} } from "${t.nextImport}";
|
|
385
|
+
|
|
386
|
+
export default defineDocs({
|
|
387
|
+
entry: "${cfg.entry}",
|
|
388
|
+
${renderI18nConfig(cfg)} theme: ${t.factory}({
|
|
389
|
+
ui: {
|
|
390
|
+
colors: { primary: "#6366f1" },
|
|
391
|
+
},
|
|
392
|
+
}),
|
|
393
|
+
|
|
394
|
+
metadata: {
|
|
395
|
+
titleTemplate: "%s – ${cfg.projectName}",
|
|
396
|
+
description: "Documentation for ${cfg.projectName}",
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
`;
|
|
400
|
+
}
|
|
401
|
+
function nextConfigTemplate() {
|
|
402
|
+
return `\
|
|
403
|
+
import { withDocs } from "@farming-labs/next/config";
|
|
404
|
+
|
|
405
|
+
export default withDocs();
|
|
406
|
+
`;
|
|
407
|
+
}
|
|
408
|
+
function nextConfigMergedTemplate(existingContent) {
|
|
409
|
+
if (existingContent.includes("withDocs")) return existingContent;
|
|
410
|
+
const lines = existingContent.split("\n");
|
|
411
|
+
const importLine = "import { withDocs } from \"@farming-labs/next/config\";";
|
|
412
|
+
const exportIdx = lines.findIndex((l) => l.match(/export\s+default/));
|
|
413
|
+
if (exportIdx === -1) return `${importLine}\n\n${existingContent}\n\nexport default withDocs();\n`;
|
|
414
|
+
const lastImportIdx = lines.reduce((acc, l, i) => l.trimStart().startsWith("import ") ? i : acc, -1);
|
|
415
|
+
if (lastImportIdx >= 0) lines.splice(lastImportIdx + 1, 0, importLine);
|
|
416
|
+
else lines.unshift(importLine, "");
|
|
417
|
+
const adjustedExportIdx = exportIdx + (lastImportIdx >= 0 ? exportIdx > lastImportIdx ? 1 : 0 : 2);
|
|
418
|
+
const simpleMatch = lines[adjustedExportIdx].match(/^(\s*export\s+default\s+)(.*?)(;?\s*)$/);
|
|
419
|
+
if (simpleMatch) {
|
|
420
|
+
const [, prefix, value, suffix] = simpleMatch;
|
|
421
|
+
lines[adjustedExportIdx] = `${prefix}withDocs(${value})${suffix}`;
|
|
422
|
+
}
|
|
423
|
+
return lines.join("\n");
|
|
424
|
+
}
|
|
425
|
+
function rootLayoutTemplate(cfg, globalCssRelPath = "app/globals.css") {
|
|
426
|
+
let cssImport;
|
|
427
|
+
if (globalCssRelPath.startsWith("app/")) cssImport = "./" + globalCssRelPath.slice(4);
|
|
428
|
+
else if (globalCssRelPath.startsWith("src/app/")) cssImport = "./" + globalCssRelPath.slice(8);
|
|
429
|
+
else cssImport = "../" + globalCssRelPath;
|
|
430
|
+
const appDir = cfg.nextAppDir ?? "app";
|
|
431
|
+
return `\
|
|
432
|
+
import type { Metadata } from "next";
|
|
433
|
+
import { RootProvider } from "@farming-labs/theme";
|
|
434
|
+
import docsConfig from "${nextRootLayoutConfigImport(cfg.useAlias, appDir)}";
|
|
435
|
+
import "${cssImport}";
|
|
436
|
+
|
|
437
|
+
export const metadata: Metadata = {
|
|
438
|
+
title: {
|
|
439
|
+
default: "Docs",
|
|
440
|
+
template: docsConfig.metadata?.titleTemplate ?? "%s",
|
|
441
|
+
},
|
|
442
|
+
description: docsConfig.metadata?.description,
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
export default function RootLayout({
|
|
446
|
+
children,
|
|
447
|
+
}: {
|
|
448
|
+
children: React.ReactNode;
|
|
449
|
+
}) {
|
|
450
|
+
return (
|
|
451
|
+
<html lang="en" suppressHydrationWarning>
|
|
452
|
+
<body>
|
|
453
|
+
<RootProvider>{children}</RootProvider>
|
|
454
|
+
</body>
|
|
455
|
+
</html>
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
`;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Injects RootProvider (import + wrapper) into an existing root layout without overwriting.
|
|
462
|
+
* Returns the modified content, or null if RootProvider is already present or injection isn't possible.
|
|
463
|
+
*/
|
|
464
|
+
function injectRootProviderIntoLayout(content) {
|
|
465
|
+
if (!content || content.includes("RootProvider")) return null;
|
|
466
|
+
let out = content;
|
|
467
|
+
const themeImport = "import { RootProvider } from \"@farming-labs/theme\";";
|
|
468
|
+
if (!out.includes("@farming-labs/theme")) {
|
|
469
|
+
const lines = out.split("\n");
|
|
470
|
+
let lastImportIdx = -1;
|
|
471
|
+
for (let i = 0; i < lines.length; i++) {
|
|
472
|
+
const trimmed = lines[i].trimStart();
|
|
473
|
+
if (trimmed.startsWith("import ") || trimmed.startsWith("import type ")) lastImportIdx = i;
|
|
474
|
+
}
|
|
475
|
+
if (lastImportIdx >= 0) {
|
|
476
|
+
lines.splice(lastImportIdx + 1, 0, themeImport);
|
|
477
|
+
out = lines.join("\n");
|
|
478
|
+
} else out = themeImport + "\n" + out;
|
|
479
|
+
}
|
|
480
|
+
if (!out.includes("<RootProvider>")) {
|
|
481
|
+
const childrenPattern = /\{children\}/;
|
|
482
|
+
if (childrenPattern.test(out)) out = out.replace(childrenPattern, "<RootProvider>{children}</RootProvider>");
|
|
483
|
+
}
|
|
484
|
+
return out === content ? null : out;
|
|
485
|
+
}
|
|
486
|
+
function globalCssTemplate(theme, customThemeName, globalCssRelPath) {
|
|
487
|
+
if (theme === "custom" && customThemeName && globalCssRelPath) return `\
|
|
488
|
+
@import "tailwindcss";
|
|
489
|
+
@import "${getCustomThemeCssImportPath(globalCssRelPath, customThemeName.replace(/\.css$/i, ""))}";
|
|
490
|
+
`;
|
|
491
|
+
return `\
|
|
492
|
+
@import "tailwindcss";
|
|
493
|
+
@import "@farming-labs/theme/${getThemeInfo(theme).nextCssImport}/css";
|
|
494
|
+
`;
|
|
495
|
+
}
|
|
496
|
+
function injectCssImport(existingContent, theme, customThemeName, globalCssRelPath) {
|
|
497
|
+
const importLine = theme === "custom" && customThemeName && globalCssRelPath ? `@import "${getCustomThemeCssImportPath(globalCssRelPath, customThemeName.replace(/\.css$/i, ""))}";` : `@import "@farming-labs/theme/${getThemeInfo(theme).nextCssImport}/css";`;
|
|
498
|
+
if (existingContent.includes(importLine)) return null;
|
|
499
|
+
if (theme !== "custom" && existingContent.includes("@farming-labs/theme/") && existingContent.includes("/css")) return null;
|
|
500
|
+
if (theme === "custom" && existingContent.includes("themes/") && existingContent.includes(".css")) return null;
|
|
501
|
+
const lines = existingContent.split("\n");
|
|
502
|
+
const lastImportIdx = lines.reduce((acc, l, i) => l.trimStart().startsWith("@import") ? i : acc, -1);
|
|
503
|
+
if (lastImportIdx >= 0) lines.splice(lastImportIdx + 1, 0, importLine);
|
|
504
|
+
else lines.unshift(importLine);
|
|
505
|
+
return lines.join("\n");
|
|
506
|
+
}
|
|
507
|
+
function docsLayoutTemplate(cfg) {
|
|
508
|
+
const appDir = cfg.nextAppDir ?? "app";
|
|
509
|
+
return `\
|
|
510
|
+
import docsConfig from "${nextDocsLayoutConfigImport(cfg.useAlias, appDir)}";
|
|
511
|
+
import { createDocsLayout, createDocsMetadata } from "@farming-labs/theme";
|
|
512
|
+
|
|
513
|
+
export const metadata = createDocsMetadata(docsConfig);
|
|
514
|
+
|
|
515
|
+
const DocsLayout = createDocsLayout(docsConfig);
|
|
516
|
+
|
|
517
|
+
export default function Layout({ children }: { children: React.ReactNode }) {
|
|
518
|
+
return (
|
|
519
|
+
<>
|
|
520
|
+
<DocsLayout>{children}</DocsLayout>
|
|
521
|
+
</>
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
`;
|
|
525
|
+
}
|
|
526
|
+
function nextLocaleDocPageTemplate(defaultLocale) {
|
|
527
|
+
return `\
|
|
528
|
+
import type { ComponentType } from "react";
|
|
529
|
+
|
|
530
|
+
type SearchParams = Promise<{ lang?: string | string[] | undefined }> | undefined;
|
|
531
|
+
|
|
532
|
+
function normalizeLang(value: string | string[] | undefined) {
|
|
533
|
+
return Array.isArray(value) ? value[0] : value;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export async function resolveLocaleDocPage<T extends ComponentType>(
|
|
537
|
+
searchParams: SearchParams,
|
|
538
|
+
pages: Record<string, T>,
|
|
539
|
+
fallbackLocale = "${defaultLocale}",
|
|
540
|
+
) {
|
|
541
|
+
const params = (await searchParams) ?? {};
|
|
542
|
+
const locale = normalizeLang(params.lang) ?? fallbackLocale;
|
|
543
|
+
|
|
544
|
+
return pages[locale] ?? pages[fallbackLocale];
|
|
545
|
+
}
|
|
546
|
+
`;
|
|
547
|
+
}
|
|
548
|
+
function nextLocalizedPageTemplate(options) {
|
|
549
|
+
const importLines = options.pageImports.map(({ locale, importPath }) => `import ${toLocaleImportName(locale)} from "${importPath}";`).join("\n");
|
|
550
|
+
const pageMap = options.pageImports.map(({ locale }) => ` ${JSON.stringify(locale)}: ${toLocaleImportName(locale)},`).join("\n");
|
|
551
|
+
return `\
|
|
552
|
+
${importLines}
|
|
553
|
+
import { resolveLocaleDocPage } from "${options.helperImport}";
|
|
554
|
+
|
|
555
|
+
type PageProps = {
|
|
556
|
+
searchParams?: Promise<{ lang?: string | string[] | undefined }>;
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
export default async function ${options.componentName}({ searchParams }: PageProps) {
|
|
560
|
+
const Page = await resolveLocaleDocPage(searchParams, {
|
|
561
|
+
${pageMap}
|
|
562
|
+
}, "${options.defaultLocale}");
|
|
563
|
+
|
|
564
|
+
return <Page />;
|
|
565
|
+
}
|
|
566
|
+
`;
|
|
567
|
+
}
|
|
568
|
+
function postcssConfigTemplate() {
|
|
569
|
+
return `\
|
|
570
|
+
const config = {
|
|
571
|
+
plugins: {
|
|
572
|
+
"@tailwindcss/postcss": {},
|
|
573
|
+
},
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
export default config;
|
|
577
|
+
`;
|
|
578
|
+
}
|
|
579
|
+
/** @param useAlias - When false, paths (e.g. @/*) are omitted so no alias is added. */
|
|
580
|
+
function tsconfigTemplate(useAlias = false) {
|
|
581
|
+
return `\
|
|
582
|
+
{
|
|
583
|
+
"compilerOptions": {
|
|
584
|
+
"target": "ES2017",
|
|
585
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
586
|
+
"allowJs": true,
|
|
587
|
+
"skipLibCheck": true,
|
|
588
|
+
"strict": true,
|
|
589
|
+
"noEmit": true,
|
|
590
|
+
"esModuleInterop": true,
|
|
591
|
+
"module": "esnext",
|
|
592
|
+
"moduleResolution": "bundler",
|
|
593
|
+
"resolveJsonModule": true,
|
|
594
|
+
"isolatedModules": true,
|
|
595
|
+
"jsx": "react-jsx",
|
|
596
|
+
"incremental": true,
|
|
597
|
+
"plugins": [{ "name": "next" }]${useAlias ? ",\n \"paths\": { \"@/*\": [\"./*\"] }" : ""}
|
|
598
|
+
},
|
|
599
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
600
|
+
"exclude": ["node_modules"]
|
|
601
|
+
}
|
|
602
|
+
`;
|
|
603
|
+
}
|
|
604
|
+
function welcomePageTemplate(cfg) {
|
|
605
|
+
const appDir = cfg.nextAppDir ?? "app";
|
|
606
|
+
return `\
|
|
607
|
+
---
|
|
608
|
+
title: "Documentation"
|
|
609
|
+
description: "Welcome to ${cfg.projectName} documentation"
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
# Welcome to ${cfg.projectName}
|
|
613
|
+
|
|
614
|
+
Get started with our documentation. Browse the pages on the left to learn more.
|
|
615
|
+
|
|
616
|
+
<Callout type="info">
|
|
617
|
+
This documentation was generated by \`@farming-labs/docs\`. Edit the MDX files in \`${appDir}/${cfg.entry}/\` to customize.
|
|
618
|
+
</Callout>
|
|
619
|
+
|
|
620
|
+
## Overview
|
|
621
|
+
|
|
622
|
+
This is your documentation home page. From here you can navigate to:
|
|
623
|
+
|
|
624
|
+
- [Installation](/${cfg.entry}/installation) — How to install and set up the project
|
|
625
|
+
- [Quickstart](/${cfg.entry}/quickstart) — Get up and running in minutes
|
|
626
|
+
|
|
627
|
+
## Features
|
|
628
|
+
|
|
629
|
+
- **MDX Support** — Write docs with Markdown and React components
|
|
630
|
+
- **Syntax Highlighting** — Code blocks with automatic highlighting
|
|
631
|
+
- **Dark Mode** — Built-in theme switching
|
|
632
|
+
- **Search** — Full-text search across all pages
|
|
633
|
+
- **Responsive** — Works on any screen size
|
|
634
|
+
|
|
635
|
+
---
|
|
636
|
+
|
|
637
|
+
## Next Steps
|
|
638
|
+
|
|
639
|
+
Start by reading the [Installation](/${cfg.entry}/installation) guide, then follow the [Quickstart](/${cfg.entry}/quickstart) to build something.
|
|
640
|
+
`;
|
|
641
|
+
}
|
|
642
|
+
function installationPageTemplate(cfg) {
|
|
643
|
+
const t = getThemeInfo(cfg.theme);
|
|
644
|
+
return `\
|
|
645
|
+
---
|
|
646
|
+
title: "Installation"
|
|
647
|
+
description: "How to install and set up ${cfg.projectName}"
|
|
648
|
+
---
|
|
649
|
+
|
|
650
|
+
# Installation
|
|
651
|
+
|
|
652
|
+
Follow these steps to install and configure ${cfg.projectName}.
|
|
653
|
+
|
|
654
|
+
<Callout type="info">
|
|
655
|
+
Prerequisites: Node.js 18+ and a package manager (pnpm, npm, or yarn).
|
|
656
|
+
</Callout>
|
|
657
|
+
|
|
658
|
+
## Install Dependencies
|
|
659
|
+
|
|
660
|
+
\`\`\`bash
|
|
661
|
+
pnpm add @farming-labs/docs
|
|
662
|
+
\`\`\`
|
|
663
|
+
|
|
664
|
+
## Configuration
|
|
665
|
+
|
|
666
|
+
Your project includes a \`docs.config.ts\` at the root:
|
|
667
|
+
|
|
668
|
+
\`\`\`ts
|
|
669
|
+
import { defineDocs } from "@farming-labs/docs";
|
|
670
|
+
import { ${t.factory} } from "${t.nextImport}";
|
|
671
|
+
|
|
672
|
+
export default defineDocs({
|
|
673
|
+
entry: "${cfg.entry}",
|
|
674
|
+
theme: ${t.factory}({
|
|
675
|
+
ui: { colors: { primary: "#6366f1" } },
|
|
676
|
+
}),
|
|
677
|
+
});
|
|
678
|
+
\`\`\`
|
|
679
|
+
|
|
680
|
+
## Project Structure
|
|
681
|
+
|
|
682
|
+
\`\`\`
|
|
683
|
+
${cfg.nextAppDir ?? "app"}/
|
|
684
|
+
${cfg.entry}/
|
|
685
|
+
layout.tsx # Docs layout
|
|
686
|
+
page.mdx # /${cfg.entry}
|
|
687
|
+
installation/
|
|
688
|
+
page.mdx # /${cfg.entry}/installation
|
|
689
|
+
quickstart/
|
|
690
|
+
page.mdx # /${cfg.entry}/quickstart
|
|
691
|
+
docs.config.ts # Docs configuration
|
|
692
|
+
next.config.ts # Next.js config with withDocs()
|
|
693
|
+
\`\`\`
|
|
694
|
+
|
|
695
|
+
## What's Next?
|
|
696
|
+
|
|
697
|
+
Head to the [Quickstart](/${cfg.entry}/quickstart) guide to start writing your first page.
|
|
698
|
+
`;
|
|
699
|
+
}
|
|
700
|
+
function quickstartPageTemplate(cfg) {
|
|
701
|
+
const t = getThemeInfo(cfg.theme);
|
|
702
|
+
return `\
|
|
703
|
+
---
|
|
704
|
+
title: "Quickstart"
|
|
705
|
+
description: "Get up and running in minutes"
|
|
706
|
+
---
|
|
707
|
+
|
|
708
|
+
# Quickstart
|
|
709
|
+
|
|
710
|
+
This guide walks you through creating your first documentation page.
|
|
711
|
+
|
|
712
|
+
## Creating a Page
|
|
713
|
+
|
|
714
|
+
Create a new folder under \`${cfg.nextAppDir ?? "app"}/${cfg.entry}/\` with a \`page.mdx\` file:
|
|
715
|
+
|
|
716
|
+
\`\`\`bash
|
|
717
|
+
mkdir -p ${cfg.nextAppDir ?? "app"}/${cfg.entry}/my-page
|
|
718
|
+
\`\`\`
|
|
719
|
+
|
|
720
|
+
Then create \`${cfg.nextAppDir ?? "app"}/${cfg.entry}/my-page/page.mdx\`:
|
|
721
|
+
|
|
722
|
+
\`\`\`mdx
|
|
723
|
+
---
|
|
724
|
+
title: "My Page"
|
|
725
|
+
description: "A custom documentation page"
|
|
726
|
+
---
|
|
727
|
+
|
|
728
|
+
# My Page
|
|
729
|
+
|
|
730
|
+
Write your content here using **Markdown** and JSX components.
|
|
731
|
+
\`\`\`
|
|
732
|
+
|
|
733
|
+
Your page is now available at \`/${cfg.entry}/my-page\`.
|
|
734
|
+
|
|
735
|
+
## Using Components
|
|
736
|
+
|
|
737
|
+
### Callouts
|
|
738
|
+
|
|
739
|
+
<Callout type="info">
|
|
740
|
+
This is an informational callout. Use it for tips and notes.
|
|
741
|
+
</Callout>
|
|
742
|
+
|
|
743
|
+
<Callout type="warn">
|
|
744
|
+
This is a warning callout. Use it for important caveats.
|
|
745
|
+
</Callout>
|
|
746
|
+
|
|
747
|
+
### Code Blocks
|
|
748
|
+
|
|
749
|
+
Code blocks are automatically syntax-highlighted:
|
|
750
|
+
|
|
751
|
+
\`\`\`typescript
|
|
752
|
+
function greet(name: string): string {
|
|
753
|
+
return \\\`Hello, \\\${name}!\\\`;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
console.log(greet("World"));
|
|
757
|
+
\`\`\`
|
|
758
|
+
|
|
759
|
+
## Customizing the Theme
|
|
760
|
+
|
|
761
|
+
Edit \`docs.config.ts\` to change colors, typography, and component defaults:
|
|
762
|
+
|
|
763
|
+
\`\`\`ts
|
|
764
|
+
theme: ${t.factory}({
|
|
765
|
+
ui: {
|
|
766
|
+
colors: { primary: "#22c55e" },
|
|
767
|
+
},
|
|
768
|
+
}),
|
|
769
|
+
\`\`\`
|
|
770
|
+
|
|
771
|
+
## Deploying
|
|
772
|
+
|
|
773
|
+
Build your docs for production:
|
|
774
|
+
|
|
775
|
+
\`\`\`bash
|
|
776
|
+
pnpm build
|
|
777
|
+
\`\`\`
|
|
778
|
+
|
|
779
|
+
Deploy to Vercel, Netlify, or any Node.js hosting platform.
|
|
780
|
+
`;
|
|
781
|
+
}
|
|
782
|
+
function tanstackDocsConfigTemplate(cfg) {
|
|
783
|
+
if (cfg.theme === "custom" && cfg.customThemeName) {
|
|
784
|
+
const exportName = getThemeExportName(cfg.customThemeName);
|
|
785
|
+
return `\
|
|
786
|
+
import { defineDocs } from "@farming-labs/docs";
|
|
787
|
+
import { ${exportName} } from "./themes/${cfg.customThemeName.replace(/\.ts$/i, "")}";
|
|
788
|
+
|
|
789
|
+
export default defineDocs({
|
|
790
|
+
entry: "${cfg.entry}",
|
|
791
|
+
contentDir: "${cfg.entry}",
|
|
792
|
+
theme: ${exportName}({
|
|
793
|
+
ui: {
|
|
794
|
+
colors: { primary: "#6366f1" },
|
|
795
|
+
},
|
|
796
|
+
}),
|
|
797
|
+
nav: {
|
|
798
|
+
title: "${cfg.projectName} Docs",
|
|
799
|
+
url: "/${cfg.entry}",
|
|
800
|
+
},
|
|
801
|
+
themeToggle: {
|
|
802
|
+
enabled: true,
|
|
803
|
+
default: "dark",
|
|
804
|
+
},
|
|
805
|
+
metadata: {
|
|
806
|
+
titleTemplate: "%s – ${cfg.projectName}",
|
|
807
|
+
description: "Documentation for ${cfg.projectName}",
|
|
808
|
+
},
|
|
809
|
+
});
|
|
810
|
+
`;
|
|
811
|
+
}
|
|
812
|
+
const t = getThemeInfo(cfg.theme);
|
|
813
|
+
return `\
|
|
814
|
+
import { defineDocs } from "@farming-labs/docs";
|
|
815
|
+
import { ${t.factory} } from "${t.nextImport}";
|
|
816
|
+
|
|
817
|
+
export default defineDocs({
|
|
818
|
+
entry: "${cfg.entry}",
|
|
819
|
+
contentDir: "${cfg.entry}",
|
|
820
|
+
theme: ${t.factory}({
|
|
821
|
+
ui: {
|
|
822
|
+
colors: { primary: "#6366f1" },
|
|
823
|
+
},
|
|
824
|
+
}),
|
|
825
|
+
nav: {
|
|
826
|
+
title: "${cfg.projectName} Docs",
|
|
827
|
+
url: "/${cfg.entry}",
|
|
828
|
+
},
|
|
829
|
+
themeToggle: {
|
|
830
|
+
enabled: true,
|
|
831
|
+
default: "dark",
|
|
832
|
+
},
|
|
833
|
+
metadata: {
|
|
834
|
+
titleTemplate: "%s – ${cfg.projectName}",
|
|
835
|
+
description: "Documentation for ${cfg.projectName}",
|
|
836
|
+
},
|
|
837
|
+
});
|
|
838
|
+
`;
|
|
839
|
+
}
|
|
840
|
+
function tanstackDocsServerTemplate() {
|
|
841
|
+
return `\
|
|
842
|
+
import { createDocsServer } from "@farming-labs/tanstack-start/server";
|
|
843
|
+
import docsConfig from "../../docs.config";
|
|
844
|
+
|
|
845
|
+
export const docsServer = createDocsServer({
|
|
846
|
+
...docsConfig,
|
|
847
|
+
rootDir: process.cwd(),
|
|
848
|
+
});
|
|
849
|
+
`;
|
|
850
|
+
}
|
|
851
|
+
function tanstackDocsFunctionsTemplate() {
|
|
852
|
+
return `\
|
|
853
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
854
|
+
import { docsServer } from "./docs.server";
|
|
855
|
+
|
|
856
|
+
export const loadDocPage = createServerFn({ method: "GET" })
|
|
857
|
+
.inputValidator((data: { pathname: string; locale?: string }) => data)
|
|
858
|
+
.handler(async ({ data }) => docsServer.load(data));
|
|
859
|
+
`;
|
|
860
|
+
}
|
|
861
|
+
function tanstackDocsFunctionsImport(opts) {
|
|
862
|
+
if (opts.useAlias) return "@/lib/docs.functions";
|
|
863
|
+
return relativeImport(opts.filePath, "src/lib/docs.functions.ts");
|
|
864
|
+
}
|
|
865
|
+
function tanstackDocsConfigImport(filePath) {
|
|
866
|
+
return relativeImport(filePath, "docs.config.ts");
|
|
867
|
+
}
|
|
868
|
+
function tanstackDocsIndexRouteTemplate(opts) {
|
|
869
|
+
const entryUrl = `/${opts.entry.replace(/^\/+|\/+$/g, "")}`;
|
|
870
|
+
return `\
|
|
871
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
872
|
+
import { TanstackDocsPage } from "@farming-labs/tanstack-start/react";
|
|
873
|
+
import { loadDocPage } from "${tanstackDocsFunctionsImport(opts)}";
|
|
874
|
+
import docsConfig from "${tanstackDocsConfigImport(opts.filePath)}";
|
|
875
|
+
|
|
876
|
+
export const Route = createFileRoute("${entryUrl}/")({
|
|
877
|
+
loader: () => loadDocPage({ data: { pathname: "${entryUrl}" } }),
|
|
878
|
+
head: ({ loaderData }) => ({
|
|
879
|
+
meta: [
|
|
880
|
+
{ title: loaderData ? \`\${loaderData.title} – ${opts.projectName}\` : "${opts.projectName}" },
|
|
881
|
+
...(loaderData?.description
|
|
882
|
+
? [{ name: "description", content: loaderData.description }]
|
|
883
|
+
: []),
|
|
884
|
+
],
|
|
885
|
+
}),
|
|
886
|
+
component: DocsIndexPage,
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
function DocsIndexPage() {
|
|
890
|
+
const data = Route.useLoaderData();
|
|
891
|
+
return <TanstackDocsPage config={docsConfig} data={data} />;
|
|
892
|
+
}
|
|
893
|
+
`;
|
|
894
|
+
}
|
|
895
|
+
function tanstackDocsCatchAllRouteTemplate(opts) {
|
|
896
|
+
const entryUrl = `/${opts.entry.replace(/^\/+|\/+$/g, "")}`;
|
|
897
|
+
return `\
|
|
898
|
+
import { createFileRoute, notFound } from "@tanstack/react-router";
|
|
899
|
+
import { TanstackDocsPage } from "@farming-labs/tanstack-start/react";
|
|
900
|
+
import { loadDocPage } from "${tanstackDocsFunctionsImport(opts)}";
|
|
901
|
+
import docsConfig from "${tanstackDocsConfigImport(opts.filePath)}";
|
|
902
|
+
|
|
903
|
+
export const Route = createFileRoute("${entryUrl}/$")({
|
|
904
|
+
loader: async ({ location }) => {
|
|
905
|
+
try {
|
|
906
|
+
return await loadDocPage({ data: { pathname: location.pathname } });
|
|
907
|
+
} catch (error) {
|
|
908
|
+
if (
|
|
909
|
+
error &&
|
|
910
|
+
typeof error === "object" &&
|
|
911
|
+
"status" in error &&
|
|
912
|
+
(error as { status?: unknown }).status === 404
|
|
913
|
+
) {
|
|
914
|
+
throw notFound();
|
|
915
|
+
}
|
|
916
|
+
throw error;
|
|
917
|
+
}
|
|
918
|
+
},
|
|
919
|
+
head: ({ loaderData }) => ({
|
|
920
|
+
meta: [
|
|
921
|
+
{ title: loaderData ? \`\${loaderData.title} – ${opts.projectName}\` : "${opts.projectName}" },
|
|
922
|
+
...(loaderData?.description
|
|
923
|
+
? [{ name: "description", content: loaderData.description }]
|
|
924
|
+
: []),
|
|
925
|
+
],
|
|
926
|
+
}),
|
|
927
|
+
component: DocsCatchAllPage,
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
function DocsCatchAllPage() {
|
|
931
|
+
const data = Route.useLoaderData();
|
|
932
|
+
return <TanstackDocsPage config={docsConfig} data={data} />;
|
|
933
|
+
}
|
|
934
|
+
`;
|
|
935
|
+
}
|
|
936
|
+
function tanstackApiDocsRouteTemplate(useAlias, filePath) {
|
|
937
|
+
return `\
|
|
938
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
939
|
+
import { docsServer } from "${useAlias ? "@/lib/docs.server" : relativeImport(filePath, "src/lib/docs.server.ts")}";
|
|
940
|
+
|
|
941
|
+
export const Route = createFileRoute("/api/docs")({
|
|
942
|
+
server: {
|
|
943
|
+
handlers: {
|
|
944
|
+
GET: async ({ request }) => docsServer.GET({ request }),
|
|
945
|
+
POST: async ({ request }) => docsServer.POST({ request }),
|
|
946
|
+
},
|
|
947
|
+
},
|
|
948
|
+
});
|
|
949
|
+
`;
|
|
950
|
+
}
|
|
951
|
+
function tanstackRootRouteTemplate(globalCssRelPath) {
|
|
952
|
+
return `\
|
|
953
|
+
import appCss from "${relativeAssetPath("src/routes/__root.tsx", globalCssRelPath)}?url";
|
|
954
|
+
import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router";
|
|
955
|
+
import { RootProvider } from "@farming-labs/theme/tanstack";
|
|
956
|
+
|
|
957
|
+
export const Route = createRootRoute({
|
|
958
|
+
head: () => ({
|
|
959
|
+
links: [{ rel: "stylesheet", href: appCss }],
|
|
960
|
+
meta: [
|
|
961
|
+
{ charSet: "utf-8" },
|
|
962
|
+
{ name: "viewport", content: "width=device-width, initial-scale=1" },
|
|
963
|
+
{ title: "Docs" },
|
|
964
|
+
],
|
|
965
|
+
}),
|
|
966
|
+
component: RootComponent,
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
function RootComponent() {
|
|
970
|
+
return (
|
|
971
|
+
<html lang="en" suppressHydrationWarning>
|
|
972
|
+
<head>
|
|
973
|
+
<HeadContent />
|
|
974
|
+
</head>
|
|
975
|
+
<body>
|
|
976
|
+
<RootProvider>
|
|
977
|
+
<Outlet />
|
|
978
|
+
</RootProvider>
|
|
979
|
+
<Scripts />
|
|
980
|
+
</body>
|
|
981
|
+
</html>
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
`;
|
|
985
|
+
}
|
|
986
|
+
function injectTanstackRootProviderIntoRoute(content) {
|
|
987
|
+
if (!content || content.includes("RootProvider")) return null;
|
|
988
|
+
let out = addImportLine(content, "import { RootProvider } from \"@farming-labs/theme/tanstack\";");
|
|
989
|
+
if (out.includes("<Outlet />")) out = out.replace("<Outlet />", "<RootProvider><Outlet /></RootProvider>");
|
|
990
|
+
else if (out.includes("<Outlet></Outlet>")) out = out.replace("<Outlet></Outlet>", "<RootProvider><Outlet /></RootProvider>");
|
|
991
|
+
else return null;
|
|
992
|
+
return out === content ? null : out;
|
|
993
|
+
}
|
|
994
|
+
function tanstackViteConfigTemplate(useAlias) {
|
|
995
|
+
return `\
|
|
996
|
+
import { defineConfig } from "vite";
|
|
997
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
998
|
+
${useAlias ? "import tsconfigPaths from \"vite-tsconfig-paths\";\n" : ""}import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
|
999
|
+
import { docsMdx } from "@farming-labs/tanstack-start/vite";
|
|
1000
|
+
|
|
1001
|
+
export default defineConfig({
|
|
1002
|
+
plugins: [tailwindcss(), docsMdx(), ${useAlias ? "tsconfigPaths({ ignoreConfigErrors: true }), " : ""}tanstackStart()],
|
|
1003
|
+
});
|
|
1004
|
+
`;
|
|
1005
|
+
}
|
|
1006
|
+
function injectTanstackVitePlugins(content, useAlias) {
|
|
1007
|
+
if (!content) return null;
|
|
1008
|
+
let out = content;
|
|
1009
|
+
out = addImportLine(out, "import tailwindcss from \"@tailwindcss/vite\";");
|
|
1010
|
+
if (useAlias) out = addImportLine(out, "import tsconfigPaths from \"vite-tsconfig-paths\";");
|
|
1011
|
+
out = addImportLine(out, "import { docsMdx } from \"@farming-labs/tanstack-start/vite\";");
|
|
1012
|
+
const additions = [];
|
|
1013
|
+
if (!out.includes("tailwindcss()")) additions.push("tailwindcss()");
|
|
1014
|
+
if (!out.includes("docsMdx()")) additions.push("docsMdx()");
|
|
1015
|
+
if (useAlias && !out.includes("tsconfigPaths(")) additions.push("tsconfigPaths({ ignoreConfigErrors: true })");
|
|
1016
|
+
if (additions.length === 0) return out === content ? null : out;
|
|
1017
|
+
const pluginsMatch = out.match(/plugins\s*:\s*\[([\s\S]*?)\]/m);
|
|
1018
|
+
if (pluginsMatch) {
|
|
1019
|
+
const current = pluginsMatch[1].trim();
|
|
1020
|
+
const existing = current ? `${current}${current.endsWith(",") ? "" : ","}` : "";
|
|
1021
|
+
const replacement = `plugins: [\n ${existing}${existing ? "\n " : ""}${additions.join(",\n ")}\n ]`;
|
|
1022
|
+
return out.replace(pluginsMatch[0], replacement);
|
|
1023
|
+
}
|
|
1024
|
+
const configMatch = out.match(/defineConfig\(\s*\{/);
|
|
1025
|
+
if (configMatch) {
|
|
1026
|
+
const insertion = `defineConfig({\n plugins: [${additions.join(", ")}],`;
|
|
1027
|
+
return out.replace(configMatch[0], insertion);
|
|
1028
|
+
}
|
|
1029
|
+
return out === content ? null : out;
|
|
1030
|
+
}
|
|
1031
|
+
function tanstackWelcomePageTemplate(cfg) {
|
|
1032
|
+
return `\
|
|
1033
|
+
---
|
|
1034
|
+
title: "Documentation"
|
|
1035
|
+
description: "Welcome to ${cfg.projectName} documentation"
|
|
1036
|
+
---
|
|
1037
|
+
|
|
1038
|
+
# Welcome to ${cfg.projectName}
|
|
1039
|
+
|
|
1040
|
+
This docs site is powered by \`@farming-labs/docs\` and TanStack Start.
|
|
1041
|
+
|
|
1042
|
+
## Overview
|
|
1043
|
+
|
|
1044
|
+
- Content lives in \`${cfg.entry}/\`
|
|
1045
|
+
- Routes live in \`src/routes/${cfg.entry}/\`
|
|
1046
|
+
- Search is served from \`/api/docs\`
|
|
1047
|
+
|
|
1048
|
+
## Next Steps
|
|
1049
|
+
|
|
1050
|
+
Read the [Installation](/${cfg.entry}/installation) guide, then continue to [Quickstart](/${cfg.entry}/quickstart).
|
|
1051
|
+
`;
|
|
1052
|
+
}
|
|
1053
|
+
function tanstackInstallationPageTemplate(cfg) {
|
|
1054
|
+
if (cfg.theme === "custom" && cfg.customThemeName) {
|
|
1055
|
+
const baseName = cfg.customThemeName.replace(/\.(ts|css)$/i, "");
|
|
1056
|
+
const exportName = getThemeExportName(baseName);
|
|
1057
|
+
const cssImportPath = getCustomThemeCssImportPath("src/styles/app.css", baseName);
|
|
1058
|
+
return `\
|
|
1059
|
+
---
|
|
1060
|
+
title: "Installation"
|
|
1061
|
+
description: "How to install and set up ${cfg.projectName}"
|
|
1062
|
+
---
|
|
1063
|
+
|
|
1064
|
+
# Installation
|
|
1065
|
+
|
|
1066
|
+
Add the docs packages to your TanStack Start app:
|
|
1067
|
+
|
|
1068
|
+
\`\`\`bash
|
|
1069
|
+
pnpm add @farming-labs/docs @farming-labs/theme @farming-labs/tanstack-start
|
|
1070
|
+
\`\`\`
|
|
1071
|
+
|
|
1072
|
+
The scaffold also configures MDX through \`docsMdx()\` in \`vite.config.ts\`.
|
|
1073
|
+
|
|
1074
|
+
## Theme CSS
|
|
1075
|
+
|
|
1076
|
+
Keep your config theme and global CSS import aligned:
|
|
1077
|
+
|
|
1078
|
+
\`\`\`ts title="docs.config.ts"
|
|
1079
|
+
import { defineDocs } from "@farming-labs/docs";
|
|
1080
|
+
import { ${exportName} } from "./themes/${baseName}";
|
|
1081
|
+
|
|
1082
|
+
export default defineDocs({
|
|
1083
|
+
entry: "${cfg.entry}",
|
|
1084
|
+
contentDir: "${cfg.entry}",
|
|
1085
|
+
theme: ${exportName}(),
|
|
1086
|
+
});
|
|
1087
|
+
\`\`\`
|
|
1088
|
+
|
|
1089
|
+
\`\`\`css title="src/styles/app.css"
|
|
1090
|
+
@import "tailwindcss";
|
|
1091
|
+
@import "${cssImportPath}";
|
|
1092
|
+
\`\`\`
|
|
1093
|
+
|
|
1094
|
+
## Generated Files
|
|
1095
|
+
|
|
1096
|
+
\`\`\`
|
|
1097
|
+
docs.config.ts
|
|
1098
|
+
themes/${baseName}.ts
|
|
1099
|
+
themes/${baseName}.css
|
|
1100
|
+
${cfg.entry}/
|
|
1101
|
+
src/lib/docs.server.ts
|
|
1102
|
+
src/lib/docs.functions.ts
|
|
1103
|
+
src/routes/${cfg.entry}/index.tsx
|
|
1104
|
+
src/routes/${cfg.entry}/$.tsx
|
|
1105
|
+
src/routes/api/docs.ts
|
|
1106
|
+
\`\`\`
|
|
1107
|
+
`;
|
|
1108
|
+
}
|
|
1109
|
+
const t = getThemeInfo(cfg.theme);
|
|
1110
|
+
return `\
|
|
1111
|
+
---
|
|
1112
|
+
title: "Installation"
|
|
1113
|
+
description: "How to install and set up ${cfg.projectName}"
|
|
1114
|
+
---
|
|
1115
|
+
|
|
1116
|
+
# Installation
|
|
1117
|
+
|
|
1118
|
+
Add the docs packages to your TanStack Start app:
|
|
1119
|
+
|
|
1120
|
+
\`\`\`bash
|
|
1121
|
+
pnpm add @farming-labs/docs @farming-labs/theme @farming-labs/tanstack-start
|
|
1122
|
+
\`\`\`
|
|
1123
|
+
|
|
1124
|
+
The scaffold also configures MDX through \`docsMdx()\` in \`vite.config.ts\`.
|
|
1125
|
+
|
|
1126
|
+
## Theme CSS
|
|
1127
|
+
|
|
1128
|
+
Keep your config theme and global CSS import aligned:
|
|
1129
|
+
|
|
1130
|
+
\`\`\`ts title="docs.config.ts"
|
|
1131
|
+
import { defineDocs } from "@farming-labs/docs";
|
|
1132
|
+
import { ${t.factory} } from "${t.nextImport}";
|
|
1133
|
+
|
|
1134
|
+
export default defineDocs({
|
|
1135
|
+
entry: "${cfg.entry}",
|
|
1136
|
+
contentDir: "${cfg.entry}",
|
|
1137
|
+
theme: ${t.factory}(),
|
|
1138
|
+
});
|
|
1139
|
+
\`\`\`
|
|
1140
|
+
|
|
1141
|
+
\`\`\`css title="src/styles/app.css"
|
|
1142
|
+
@import "tailwindcss";
|
|
1143
|
+
@import "@farming-labs/theme/${t.nextCssImport}/css";
|
|
1144
|
+
\`\`\`
|
|
1145
|
+
|
|
1146
|
+
## Generated Files
|
|
1147
|
+
|
|
1148
|
+
\`\`\`
|
|
1149
|
+
docs.config.ts
|
|
1150
|
+
${cfg.entry}/
|
|
1151
|
+
src/lib/docs.server.ts
|
|
1152
|
+
src/lib/docs.functions.ts
|
|
1153
|
+
src/routes/${cfg.entry}/index.tsx
|
|
1154
|
+
src/routes/${cfg.entry}/$.tsx
|
|
1155
|
+
src/routes/api/docs.ts
|
|
1156
|
+
\`\`\`
|
|
1157
|
+
`;
|
|
1158
|
+
}
|
|
1159
|
+
function tanstackQuickstartPageTemplate(cfg) {
|
|
1160
|
+
return `\
|
|
1161
|
+
---
|
|
1162
|
+
title: "Quickstart"
|
|
1163
|
+
description: "Get up and running in minutes"
|
|
1164
|
+
---
|
|
1165
|
+
|
|
1166
|
+
# Quickstart
|
|
1167
|
+
|
|
1168
|
+
Create a new page under \`${cfg.entry}/\`:
|
|
1169
|
+
|
|
1170
|
+
\`\`\`bash
|
|
1171
|
+
mkdir -p ${cfg.entry}/my-page
|
|
1172
|
+
\`\`\`
|
|
1173
|
+
|
|
1174
|
+
Then add \`${cfg.entry}/my-page/page.mdx\`:
|
|
1175
|
+
|
|
1176
|
+
\`\`\`mdx
|
|
1177
|
+
---
|
|
1178
|
+
title: "My Page"
|
|
1179
|
+
description: "A custom documentation page"
|
|
1180
|
+
---
|
|
1181
|
+
|
|
1182
|
+
# My Page
|
|
1183
|
+
|
|
1184
|
+
Write your content here using **MDX**.
|
|
1185
|
+
\`\`\`
|
|
1186
|
+
|
|
1187
|
+
Visit [/${cfg.entry}/my-page](/${cfg.entry}/my-page) after starting the dev server.
|
|
1188
|
+
`;
|
|
1189
|
+
}
|
|
1190
|
+
function svelteDocsConfigTemplate(cfg) {
|
|
1191
|
+
if (cfg.theme === "custom" && cfg.customThemeName) {
|
|
1192
|
+
const exportName = getThemeExportName(cfg.customThemeName);
|
|
1193
|
+
return `\
|
|
1194
|
+
import { defineDocs } from "@farming-labs/docs";
|
|
1195
|
+
import { ${exportName} } from "${"../../themes/" + cfg.customThemeName.replace(/\.ts$/i, "")}";
|
|
1196
|
+
|
|
1197
|
+
export default defineDocs({
|
|
1198
|
+
entry: "${cfg.entry}",
|
|
1199
|
+
contentDir: "${cfg.entry}",
|
|
1200
|
+
${renderI18nConfig(cfg)} theme: ${exportName}({
|
|
1201
|
+
ui: {
|
|
1202
|
+
colors: { primary: "#6366f1" },
|
|
1203
|
+
},
|
|
1204
|
+
}),
|
|
1205
|
+
|
|
1206
|
+
nav: {
|
|
1207
|
+
title: "${cfg.projectName}",
|
|
1208
|
+
url: "/${cfg.entry}",
|
|
1209
|
+
},
|
|
1210
|
+
|
|
1211
|
+
breadcrumb: { enabled: true },
|
|
1212
|
+
|
|
1213
|
+
metadata: {
|
|
1214
|
+
titleTemplate: "%s – ${cfg.projectName}",
|
|
1215
|
+
description: "Documentation for ${cfg.projectName}",
|
|
1216
|
+
},
|
|
1217
|
+
});
|
|
1218
|
+
`;
|
|
1219
|
+
}
|
|
1220
|
+
const t = getThemeInfo(cfg.theme);
|
|
1221
|
+
return `\
|
|
1222
|
+
import { defineDocs } from "@farming-labs/docs";
|
|
1223
|
+
import { ${t.factory} } from "${t.svelteImport}";
|
|
1224
|
+
|
|
1225
|
+
export default defineDocs({
|
|
1226
|
+
entry: "${cfg.entry}",
|
|
1227
|
+
contentDir: "${cfg.entry}",
|
|
1228
|
+
${renderI18nConfig(cfg)} theme: ${t.factory}({
|
|
1229
|
+
ui: {
|
|
1230
|
+
colors: { primary: "#6366f1" },
|
|
1231
|
+
},
|
|
1232
|
+
}),
|
|
1233
|
+
|
|
1234
|
+
nav: {
|
|
1235
|
+
title: "${cfg.projectName}",
|
|
1236
|
+
url: "/${cfg.entry}",
|
|
1237
|
+
},
|
|
1238
|
+
|
|
1239
|
+
breadcrumb: { enabled: true },
|
|
1240
|
+
|
|
1241
|
+
metadata: {
|
|
1242
|
+
titleTemplate: "%s – ${cfg.projectName}",
|
|
1243
|
+
description: "Documentation for ${cfg.projectName}",
|
|
1244
|
+
},
|
|
1245
|
+
});
|
|
1246
|
+
`;
|
|
1247
|
+
}
|
|
1248
|
+
function svelteDocsServerTemplate(cfg) {
|
|
1249
|
+
return `\
|
|
1250
|
+
import { createDocsServer } from "@farming-labs/svelte/server";
|
|
1251
|
+
import config from "${svelteServerConfigImport(cfg.useAlias)}";
|
|
1252
|
+
|
|
1253
|
+
// preload for production
|
|
1254
|
+
const contentFiles = import.meta.glob("/${cfg.entry ?? "docs"}/**/*.{md,mdx,svx}", {
|
|
1255
|
+
query: "?raw",
|
|
1256
|
+
import: "default",
|
|
1257
|
+
eager: true,
|
|
1258
|
+
}) as Record<string, string>;
|
|
1259
|
+
|
|
1260
|
+
export const { load, GET, POST } = createDocsServer({
|
|
1261
|
+
...config,
|
|
1262
|
+
_preloadedContent: contentFiles,
|
|
1263
|
+
});
|
|
1264
|
+
`;
|
|
1265
|
+
}
|
|
1266
|
+
function svelteDocsLayoutTemplate(cfg) {
|
|
1267
|
+
return `\
|
|
1268
|
+
<script>
|
|
1269
|
+
import { DocsLayout } from "@farming-labs/svelte-theme";
|
|
1270
|
+
import config from "${svelteLayoutConfigImport(cfg.useAlias)}";
|
|
1271
|
+
|
|
1272
|
+
let { data, children } = $props();
|
|
1273
|
+
<\/script>
|
|
1274
|
+
|
|
1275
|
+
<DocsLayout tree={data.tree} {config}>
|
|
1276
|
+
{@render children()}
|
|
1277
|
+
</DocsLayout>
|
|
1278
|
+
`;
|
|
1279
|
+
}
|
|
1280
|
+
function svelteDocsLayoutServerTemplate(cfg) {
|
|
1281
|
+
return `\
|
|
1282
|
+
export { load } from "${svelteLayoutServerImport(cfg.useAlias)}";
|
|
1283
|
+
`;
|
|
1284
|
+
}
|
|
1285
|
+
function svelteDocsPageTemplate(cfg) {
|
|
1286
|
+
return `\
|
|
1287
|
+
<script>
|
|
1288
|
+
import { DocsContent } from "@farming-labs/svelte-theme";
|
|
1289
|
+
import config from "${sveltePageConfigImport(cfg.useAlias)}";
|
|
1290
|
+
|
|
1291
|
+
let { data } = $props();
|
|
1292
|
+
<\/script>
|
|
1293
|
+
|
|
1294
|
+
<DocsContent {data} {config} />
|
|
1295
|
+
`;
|
|
1296
|
+
}
|
|
1297
|
+
function svelteRootLayoutTemplate(globalCssRelPath) {
|
|
1298
|
+
let cssImport;
|
|
1299
|
+
if (globalCssRelPath.startsWith("src/")) cssImport = "./" + globalCssRelPath.slice(4);
|
|
1300
|
+
else cssImport = "../" + globalCssRelPath;
|
|
1301
|
+
return `\
|
|
1302
|
+
<script>
|
|
1303
|
+
import "${cssImport}";
|
|
1304
|
+
|
|
1305
|
+
let { children } = $props();
|
|
1306
|
+
<\/script>
|
|
1307
|
+
|
|
1308
|
+
{@render children()}
|
|
1309
|
+
`;
|
|
1310
|
+
}
|
|
1311
|
+
function svelteGlobalCssTemplate(theme, customThemeName, globalCssRelPath) {
|
|
1312
|
+
if (theme === "custom" && customThemeName && globalCssRelPath) return `\
|
|
1313
|
+
@import "${getCustomThemeCssImportPath(globalCssRelPath, customThemeName.replace(/\.css$/i, ""))}";
|
|
1314
|
+
`;
|
|
1315
|
+
return `\
|
|
1316
|
+
@import "@farming-labs/svelte-theme/${theme}/css";
|
|
1317
|
+
`;
|
|
1318
|
+
}
|
|
1319
|
+
function svelteCssImportLine(theme, customThemeName, globalCssRelPath) {
|
|
1320
|
+
if (theme === "custom" && customThemeName && globalCssRelPath) return `@import "${getCustomThemeCssImportPath(globalCssRelPath, customThemeName.replace(/\.css$/i, ""))}";`;
|
|
1321
|
+
return `@import "@farming-labs/svelte-theme/${theme}/css";`;
|
|
1322
|
+
}
|
|
1323
|
+
function injectSvelteCssImport(existingContent, theme, customThemeName, globalCssRelPath) {
|
|
1324
|
+
const importLine = svelteCssImportLine(theme, customThemeName, globalCssRelPath);
|
|
1325
|
+
if (existingContent.includes(importLine)) return null;
|
|
1326
|
+
if (theme !== "custom" && existingContent.includes("@farming-labs/svelte-theme/") && existingContent.includes("/css")) return null;
|
|
1327
|
+
if (theme === "custom" && existingContent.includes("themes/") && existingContent.includes(".css")) return null;
|
|
1328
|
+
const lines = existingContent.split("\n");
|
|
1329
|
+
const lastImportIdx = lines.reduce((acc, l, i) => l.trimStart().startsWith("@import") ? i : acc, -1);
|
|
1330
|
+
if (lastImportIdx >= 0) lines.splice(lastImportIdx + 1, 0, importLine);
|
|
1331
|
+
else lines.unshift(importLine);
|
|
1332
|
+
return lines.join("\n");
|
|
1333
|
+
}
|
|
1334
|
+
function svelteWelcomePageTemplate(cfg) {
|
|
1335
|
+
return `\
|
|
1336
|
+
---
|
|
1337
|
+
title: "Documentation"
|
|
1338
|
+
description: "Welcome to ${cfg.projectName} documentation"
|
|
1339
|
+
---
|
|
1340
|
+
|
|
1341
|
+
# Welcome to ${cfg.projectName}
|
|
1342
|
+
|
|
1343
|
+
Get started with our documentation. Browse the pages on the left to learn more.
|
|
1344
|
+
|
|
1345
|
+
## Overview
|
|
1346
|
+
|
|
1347
|
+
This documentation was generated by \`@farming-labs/docs\`. Edit the markdown files in \`${cfg.entry}/\` to customize.
|
|
1348
|
+
|
|
1349
|
+
## Features
|
|
1350
|
+
|
|
1351
|
+
- **Markdown Support** — Write docs with standard Markdown
|
|
1352
|
+
- **Syntax Highlighting** — Code blocks with automatic highlighting
|
|
1353
|
+
- **Dark Mode** — Built-in theme switching
|
|
1354
|
+
- **Search** — Full-text search across all pages
|
|
1355
|
+
- **Responsive** — Works on any screen size
|
|
1356
|
+
|
|
1357
|
+
---
|
|
1358
|
+
|
|
1359
|
+
## Next Steps
|
|
1360
|
+
|
|
1361
|
+
Start by reading the [Installation](/${cfg.entry}/installation) guide, then follow the [Quickstart](/${cfg.entry}/quickstart) to build something.
|
|
1362
|
+
`;
|
|
1363
|
+
}
|
|
1364
|
+
function svelteInstallationPageTemplate(cfg) {
|
|
1365
|
+
const t = getThemeInfo(cfg.theme);
|
|
1366
|
+
return `\
|
|
1367
|
+
---
|
|
1368
|
+
title: "Installation"
|
|
1369
|
+
description: "How to install and set up ${cfg.projectName}"
|
|
1370
|
+
---
|
|
1371
|
+
|
|
1372
|
+
# Installation
|
|
1373
|
+
|
|
1374
|
+
Follow these steps to install and configure ${cfg.projectName}.
|
|
1375
|
+
|
|
1376
|
+
## Prerequisites
|
|
1377
|
+
|
|
1378
|
+
- Node.js 18+
|
|
1379
|
+
- A package manager (pnpm, npm, or yarn)
|
|
1380
|
+
|
|
1381
|
+
## Install Dependencies
|
|
1382
|
+
|
|
1383
|
+
\`\`\`bash
|
|
1384
|
+
pnpm add @farming-labs/docs @farming-labs/svelte @farming-labs/svelte-theme
|
|
1385
|
+
\`\`\`
|
|
1386
|
+
|
|
1387
|
+
## Configuration
|
|
1388
|
+
|
|
1389
|
+
Your project includes a \`docs.config.ts\` in \`src/lib/\`:
|
|
1390
|
+
|
|
1391
|
+
\`\`\`ts title="src/lib/docs.config.ts"
|
|
1392
|
+
import { defineDocs } from "@farming-labs/docs";
|
|
1393
|
+
import { ${t.factory} } from "${t.svelteImport}";
|
|
1394
|
+
|
|
1395
|
+
export default defineDocs({
|
|
1396
|
+
entry: "${cfg.entry}",
|
|
1397
|
+
contentDir: "${cfg.entry}",
|
|
1398
|
+
theme: ${t.factory}({
|
|
1399
|
+
ui: { colors: { primary: "#6366f1" } },
|
|
1400
|
+
}),
|
|
1401
|
+
});
|
|
1402
|
+
\`\`\`
|
|
1403
|
+
|
|
1404
|
+
## Project Structure
|
|
1405
|
+
|
|
1406
|
+
\`\`\`
|
|
1407
|
+
${cfg.entry}/ # Markdown content
|
|
1408
|
+
page.md # /${cfg.entry}
|
|
1409
|
+
installation/
|
|
1410
|
+
page.md # /${cfg.entry}/installation
|
|
1411
|
+
quickstart/
|
|
1412
|
+
page.md # /${cfg.entry}/quickstart
|
|
1413
|
+
src/
|
|
1414
|
+
lib/
|
|
1415
|
+
docs.config.ts # Docs configuration
|
|
1416
|
+
docs.server.ts # Server-side docs loader
|
|
1417
|
+
routes/
|
|
1418
|
+
${cfg.entry}/
|
|
1419
|
+
+layout.svelte # Docs layout
|
|
1420
|
+
+layout.server.js # Layout data loader
|
|
1421
|
+
[...slug]/
|
|
1422
|
+
+page.svelte # Dynamic doc page
|
|
1423
|
+
\`\`\`
|
|
1424
|
+
|
|
1425
|
+
## What's Next?
|
|
1426
|
+
|
|
1427
|
+
Head to the [Quickstart](/${cfg.entry}/quickstart) guide to start writing your first page.
|
|
1428
|
+
`;
|
|
1429
|
+
}
|
|
1430
|
+
function svelteQuickstartPageTemplate(cfg) {
|
|
1431
|
+
const t = getThemeInfo(cfg.theme);
|
|
1432
|
+
return `\
|
|
1433
|
+
---
|
|
1434
|
+
title: "Quickstart"
|
|
1435
|
+
description: "Get up and running in minutes"
|
|
1436
|
+
---
|
|
1437
|
+
|
|
1438
|
+
# Quickstart
|
|
1439
|
+
|
|
1440
|
+
This guide walks you through creating your first documentation page.
|
|
1441
|
+
|
|
1442
|
+
## Creating a Page
|
|
1443
|
+
|
|
1444
|
+
Create a new folder under \`${cfg.entry}/\` with a \`page.md\` file:
|
|
1445
|
+
|
|
1446
|
+
\`\`\`bash
|
|
1447
|
+
mkdir -p ${cfg.entry}/my-page
|
|
1448
|
+
\`\`\`
|
|
1449
|
+
|
|
1450
|
+
Then create \`${cfg.entry}/my-page/page.md\`:
|
|
1451
|
+
|
|
1452
|
+
\`\`\`md
|
|
1453
|
+
---
|
|
1454
|
+
title: "My Page"
|
|
1455
|
+
description: "A custom documentation page"
|
|
1456
|
+
---
|
|
1457
|
+
|
|
1458
|
+
# My Page
|
|
1459
|
+
|
|
1460
|
+
Write your content here using **Markdown**.
|
|
1461
|
+
\`\`\`
|
|
1462
|
+
|
|
1463
|
+
Your page is now available at \`/${cfg.entry}/my-page\`.
|
|
1464
|
+
|
|
1465
|
+
## Code Blocks
|
|
1466
|
+
|
|
1467
|
+
Code blocks are automatically syntax-highlighted:
|
|
1468
|
+
|
|
1469
|
+
\`\`\`typescript
|
|
1470
|
+
function greet(name: string): string {
|
|
1471
|
+
return \\\`Hello, \\\${name}!\\\`;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
console.log(greet("World"));
|
|
1475
|
+
\`\`\`
|
|
1476
|
+
|
|
1477
|
+
## Customizing the Theme
|
|
1478
|
+
|
|
1479
|
+
Edit \`src/lib/docs.config.ts\` to change colors, typography, and component defaults:
|
|
1480
|
+
|
|
1481
|
+
\`\`\`ts title="src/lib/docs.config.ts"
|
|
1482
|
+
theme: ${t.factory}({
|
|
1483
|
+
ui: {
|
|
1484
|
+
colors: { primary: "#22c55e" },
|
|
1485
|
+
},
|
|
1486
|
+
}),
|
|
1487
|
+
\`\`\`
|
|
1488
|
+
|
|
1489
|
+
## Deploying
|
|
1490
|
+
|
|
1491
|
+
Build your docs for production:
|
|
1492
|
+
|
|
1493
|
+
\`\`\`bash
|
|
1494
|
+
pnpm build
|
|
1495
|
+
\`\`\`
|
|
1496
|
+
|
|
1497
|
+
Deploy to Vercel, Netlify, or any Node.js hosting platform.
|
|
1498
|
+
`;
|
|
1499
|
+
}
|
|
1500
|
+
function astroDocsConfigTemplate(cfg) {
|
|
1501
|
+
if (cfg.theme === "custom" && cfg.customThemeName) {
|
|
1502
|
+
const exportName = getThemeExportName(cfg.customThemeName);
|
|
1503
|
+
return `\
|
|
1504
|
+
import { defineDocs } from "@farming-labs/docs";
|
|
1505
|
+
import { ${exportName} } from "${"../../themes/" + cfg.customThemeName.replace(/\.ts$/i, "")}";
|
|
1506
|
+
|
|
1507
|
+
export default defineDocs({
|
|
1508
|
+
entry: "${cfg.entry}",
|
|
1509
|
+
contentDir: "${cfg.entry}",
|
|
1510
|
+
${renderI18nConfig(cfg)} theme: ${exportName}({
|
|
1511
|
+
ui: {
|
|
1512
|
+
colors: { primary: "#6366f1" },
|
|
1513
|
+
},
|
|
1514
|
+
}),
|
|
1515
|
+
|
|
1516
|
+
nav: {
|
|
1517
|
+
title: "${cfg.projectName}",
|
|
1518
|
+
url: "/${cfg.entry}",
|
|
1519
|
+
},
|
|
1520
|
+
|
|
1521
|
+
breadcrumb: { enabled: true },
|
|
1522
|
+
|
|
1523
|
+
metadata: {
|
|
1524
|
+
titleTemplate: "%s – ${cfg.projectName}",
|
|
1525
|
+
description: "Documentation for ${cfg.projectName}",
|
|
1526
|
+
},
|
|
1527
|
+
});
|
|
1528
|
+
`;
|
|
1529
|
+
}
|
|
1530
|
+
const t = getThemeInfo(cfg.theme);
|
|
1531
|
+
return `\
|
|
1532
|
+
import { defineDocs } from "@farming-labs/docs";
|
|
1533
|
+
import { ${t.factory} } from "${t.astroImport}";
|
|
1534
|
+
|
|
1535
|
+
export default defineDocs({
|
|
1536
|
+
entry: "${cfg.entry}",
|
|
1537
|
+
contentDir: "${cfg.entry}",
|
|
1538
|
+
${renderI18nConfig(cfg)} theme: ${t.factory}({
|
|
1539
|
+
ui: {
|
|
1540
|
+
colors: { primary: "#6366f1" },
|
|
1541
|
+
},
|
|
1542
|
+
}),
|
|
1543
|
+
|
|
1544
|
+
nav: {
|
|
1545
|
+
title: "${cfg.projectName}",
|
|
1546
|
+
url: "/${cfg.entry}",
|
|
1547
|
+
},
|
|
1548
|
+
|
|
1549
|
+
breadcrumb: { enabled: true },
|
|
1550
|
+
|
|
1551
|
+
metadata: {
|
|
1552
|
+
titleTemplate: "%s – ${cfg.projectName}",
|
|
1553
|
+
description: "Documentation for ${cfg.projectName}",
|
|
1554
|
+
},
|
|
1555
|
+
});
|
|
1556
|
+
`;
|
|
1557
|
+
}
|
|
1558
|
+
function astroDocsServerTemplate(cfg) {
|
|
1559
|
+
return `\
|
|
1560
|
+
import { createDocsServer } from "@farming-labs/astro/server";
|
|
1561
|
+
import config from "${astroServerConfigImport(cfg.useAlias)}";
|
|
1562
|
+
|
|
1563
|
+
const contentFiles = import.meta.glob("/${cfg.entry ?? "docs"}/**/*.{md,mdx}", {
|
|
1564
|
+
query: "?raw",
|
|
1565
|
+
import: "default",
|
|
1566
|
+
eager: true,
|
|
1567
|
+
}) as Record<string, string>;
|
|
1568
|
+
|
|
1569
|
+
export const { load, GET, POST } = createDocsServer({
|
|
1570
|
+
...config,
|
|
1571
|
+
_preloadedContent: contentFiles,
|
|
1572
|
+
});
|
|
1573
|
+
`;
|
|
1574
|
+
}
|
|
1575
|
+
const ASTRO_ADAPTER_INFO = {
|
|
1576
|
+
vercel: {
|
|
1577
|
+
pkg: "@astrojs/vercel",
|
|
1578
|
+
import: "@astrojs/vercel"
|
|
1579
|
+
},
|
|
1580
|
+
netlify: {
|
|
1581
|
+
pkg: "@astrojs/netlify",
|
|
1582
|
+
import: "@astrojs/netlify"
|
|
1583
|
+
},
|
|
1584
|
+
node: {
|
|
1585
|
+
pkg: "@astrojs/node",
|
|
1586
|
+
import: "@astrojs/node"
|
|
1587
|
+
},
|
|
1588
|
+
cloudflare: {
|
|
1589
|
+
pkg: "@astrojs/cloudflare",
|
|
1590
|
+
import: "@astrojs/cloudflare"
|
|
1591
|
+
}
|
|
1592
|
+
};
|
|
1593
|
+
function getAstroAdapterPkg(adapter) {
|
|
1594
|
+
return ASTRO_ADAPTER_INFO[adapter]?.pkg ?? ASTRO_ADAPTER_INFO.vercel.pkg;
|
|
1595
|
+
}
|
|
1596
|
+
function astroConfigTemplate(adapter = "vercel") {
|
|
1597
|
+
const info = ASTRO_ADAPTER_INFO[adapter] ?? ASTRO_ADAPTER_INFO.vercel;
|
|
1598
|
+
const adapterCall = adapter === "node" ? `${adapter}({ mode: "standalone" })` : `${adapter}()`;
|
|
1599
|
+
return `\
|
|
1600
|
+
import { defineConfig } from "astro/config";
|
|
1601
|
+
import ${adapter} from "${info.import}";
|
|
1602
|
+
|
|
1603
|
+
export default defineConfig({
|
|
1604
|
+
output: "server",
|
|
1605
|
+
adapter: ${adapterCall},
|
|
1606
|
+
});
|
|
1607
|
+
`;
|
|
1608
|
+
}
|
|
1609
|
+
function astroDocsPageTemplate(cfg) {
|
|
1610
|
+
return `\
|
|
1611
|
+
---
|
|
1612
|
+
import DocsLayout from "@farming-labs/astro-theme/src/components/DocsLayout.astro";
|
|
1613
|
+
import DocsContent from "@farming-labs/astro-theme/src/components/DocsContent.astro";
|
|
1614
|
+
import SearchDialog from "@farming-labs/astro-theme/src/components/SearchDialog.astro";
|
|
1615
|
+
import config from "${astroPageConfigImport(cfg.useAlias, 2)}";
|
|
1616
|
+
import { load } from "${astroPageServerImport(cfg.useAlias, 2)}";
|
|
1617
|
+
import "${`@farming-labs/astro-theme/${getThemeInfo(cfg.theme).astroCssTheme}/css`}";
|
|
1618
|
+
|
|
1619
|
+
const data = await load(Astro.url.pathname);
|
|
1620
|
+
---
|
|
1621
|
+
|
|
1622
|
+
<html lang="en">
|
|
1623
|
+
<head>
|
|
1624
|
+
<meta charset="utf-8" />
|
|
1625
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1626
|
+
<title>{data.title} – Docs</title>
|
|
1627
|
+
</head>
|
|
1628
|
+
<body>
|
|
1629
|
+
<DocsLayout tree={data.tree} config={config}>
|
|
1630
|
+
<DocsContent data={data} config={config} />
|
|
1631
|
+
</DocsLayout>
|
|
1632
|
+
<SearchDialog config={config} />
|
|
1633
|
+
</body>
|
|
1634
|
+
</html>
|
|
1635
|
+
`;
|
|
1636
|
+
}
|
|
1637
|
+
function astroDocsIndexTemplate(cfg) {
|
|
1638
|
+
return `\
|
|
1639
|
+
---
|
|
1640
|
+
import DocsLayout from "@farming-labs/astro-theme/src/components/DocsLayout.astro";
|
|
1641
|
+
import DocsContent from "@farming-labs/astro-theme/src/components/DocsContent.astro";
|
|
1642
|
+
import SearchDialog from "@farming-labs/astro-theme/src/components/SearchDialog.astro";
|
|
1643
|
+
import config from "${astroPageConfigImport(cfg.useAlias, 2)}";
|
|
1644
|
+
import { load } from "${astroPageServerImport(cfg.useAlias, 2)}";
|
|
1645
|
+
import "${`@farming-labs/astro-theme/${getThemeInfo(cfg.theme).astroCssTheme}/css`}";
|
|
1646
|
+
|
|
1647
|
+
const data = await load(Astro.url.pathname);
|
|
1648
|
+
---
|
|
1649
|
+
|
|
1650
|
+
<html lang="en">
|
|
1651
|
+
<head>
|
|
1652
|
+
<meta charset="utf-8" />
|
|
1653
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1654
|
+
<title>{data.title} – Docs</title>
|
|
1655
|
+
</head>
|
|
1656
|
+
<body>
|
|
1657
|
+
<DocsLayout tree={data.tree} config={config}>
|
|
1658
|
+
<DocsContent data={data} config={config} />
|
|
1659
|
+
</DocsLayout>
|
|
1660
|
+
<SearchDialog config={config} />
|
|
1661
|
+
</body>
|
|
1662
|
+
</html>
|
|
1663
|
+
`;
|
|
1664
|
+
}
|
|
1665
|
+
function astroApiRouteTemplate(cfg) {
|
|
1666
|
+
return `\
|
|
1667
|
+
import type { APIRoute } from "astro";
|
|
1668
|
+
import { GET as docsGET, POST as docsPOST } from "${astroPageServerImport(cfg.useAlias, 2)}";
|
|
1669
|
+
|
|
1670
|
+
export const GET: APIRoute = async ({ request }) => {
|
|
1671
|
+
return docsGET({ request });
|
|
1672
|
+
};
|
|
1673
|
+
|
|
1674
|
+
export const POST: APIRoute = async ({ request }) => {
|
|
1675
|
+
return docsPOST({ request });
|
|
1676
|
+
};
|
|
1677
|
+
`;
|
|
1678
|
+
}
|
|
1679
|
+
function astroGlobalCssTemplate(theme, customThemeName, globalCssRelPath) {
|
|
1680
|
+
if (theme === "custom" && customThemeName && globalCssRelPath) return `\
|
|
1681
|
+
@import "${getCustomThemeCssImportPath(globalCssRelPath, customThemeName.replace(/\.css$/i, ""))}";
|
|
1682
|
+
`;
|
|
1683
|
+
return `\
|
|
1684
|
+
@import "@farming-labs/astro-theme/${theme}/css";
|
|
1685
|
+
`;
|
|
1686
|
+
}
|
|
1687
|
+
function astroCssImportLine(theme, customThemeName, globalCssRelPath) {
|
|
1688
|
+
if (theme === "custom" && customThemeName && globalCssRelPath) return `@import "${getCustomThemeCssImportPath(globalCssRelPath, customThemeName.replace(/\.css$/i, ""))}";`;
|
|
1689
|
+
return `@import "@farming-labs/astro-theme/${theme}/css";`;
|
|
1690
|
+
}
|
|
1691
|
+
function injectAstroCssImport(existingContent, theme, customThemeName, globalCssRelPath) {
|
|
1692
|
+
const importLine = astroCssImportLine(theme, customThemeName, globalCssRelPath);
|
|
1693
|
+
if (existingContent.includes(importLine)) return null;
|
|
1694
|
+
if (theme !== "custom" && existingContent.includes("@farming-labs/astro-theme/") && existingContent.includes("/css")) return null;
|
|
1695
|
+
if (theme === "custom" && existingContent.includes("themes/") && existingContent.includes(".css")) return null;
|
|
1696
|
+
const lines = existingContent.split("\n");
|
|
1697
|
+
const lastImportIdx = lines.reduce((acc, l, i) => l.trimStart().startsWith("@import") ? i : acc, -1);
|
|
1698
|
+
if (lastImportIdx >= 0) lines.splice(lastImportIdx + 1, 0, importLine);
|
|
1699
|
+
else lines.unshift(importLine);
|
|
1700
|
+
return lines.join("\n");
|
|
1701
|
+
}
|
|
1702
|
+
function astroWelcomePageTemplate(cfg) {
|
|
1703
|
+
return `\
|
|
1704
|
+
---
|
|
1705
|
+
title: "Documentation"
|
|
1706
|
+
description: "Welcome to ${cfg.projectName} documentation"
|
|
1707
|
+
---
|
|
1708
|
+
|
|
1709
|
+
# Welcome to ${cfg.projectName}
|
|
1710
|
+
|
|
1711
|
+
Get started with our documentation. Browse the pages on the left to learn more.
|
|
1712
|
+
|
|
1713
|
+
## Overview
|
|
1714
|
+
|
|
1715
|
+
This documentation was generated by \`@farming-labs/docs\`. Edit the markdown files in \`${cfg.entry}/\` to customize.
|
|
1716
|
+
|
|
1717
|
+
## Features
|
|
1718
|
+
|
|
1719
|
+
- **Markdown Support** — Write docs with standard Markdown
|
|
1720
|
+
- **Syntax Highlighting** — Code blocks with automatic highlighting
|
|
1721
|
+
- **Dark Mode** — Built-in theme switching
|
|
1722
|
+
- **Search** — Full-text search across all pages
|
|
1723
|
+
- **Responsive** — Works on any screen size
|
|
1724
|
+
|
|
1725
|
+
---
|
|
1726
|
+
|
|
1727
|
+
## Next Steps
|
|
1728
|
+
|
|
1729
|
+
Start by reading the [Installation](/${cfg.entry}/installation) guide, then follow the [Quickstart](/${cfg.entry}/quickstart) to build something.
|
|
1730
|
+
`;
|
|
1731
|
+
}
|
|
1732
|
+
function astroInstallationPageTemplate(cfg) {
|
|
1733
|
+
const t = getThemeInfo(cfg.theme);
|
|
1734
|
+
return `\
|
|
1735
|
+
---
|
|
1736
|
+
title: "Installation"
|
|
1737
|
+
description: "How to install and set up ${cfg.projectName}"
|
|
1738
|
+
---
|
|
1739
|
+
|
|
1740
|
+
# Installation
|
|
1741
|
+
|
|
1742
|
+
Follow these steps to install and configure ${cfg.projectName}.
|
|
1743
|
+
|
|
1744
|
+
## Prerequisites
|
|
1745
|
+
|
|
1746
|
+
- Node.js 18+
|
|
1747
|
+
- A package manager (pnpm, npm, or yarn)
|
|
1748
|
+
|
|
1749
|
+
## Install Dependencies
|
|
1750
|
+
|
|
1751
|
+
\\\`\\\`\\\`bash
|
|
1752
|
+
pnpm add @farming-labs/docs @farming-labs/astro @farming-labs/astro-theme
|
|
1753
|
+
\\\`\\\`\\\`
|
|
1754
|
+
|
|
1755
|
+
## Configuration
|
|
1756
|
+
|
|
1757
|
+
Your project includes a \\\`docs.config.ts\\\` in \\\`src/lib/\\\`:
|
|
1758
|
+
|
|
1759
|
+
\\\`\\\`\\\`ts title="src/lib/docs.config.ts"
|
|
1760
|
+
import { defineDocs } from "@farming-labs/docs";
|
|
1761
|
+
import { ${t.factory} } from "${t.astroImport}";
|
|
1762
|
+
|
|
1763
|
+
export default defineDocs({
|
|
1764
|
+
entry: "${cfg.entry}",
|
|
1765
|
+
contentDir: "${cfg.entry}",
|
|
1766
|
+
theme: ${t.factory}({
|
|
1767
|
+
ui: { colors: { primary: "#6366f1" } },
|
|
1768
|
+
}),
|
|
1769
|
+
});
|
|
1770
|
+
\\\`\\\`\\\`
|
|
1771
|
+
|
|
1772
|
+
## Project Structure
|
|
1773
|
+
|
|
1774
|
+
\\\`\\\`\\\`
|
|
1775
|
+
${cfg.entry}/ # Markdown content
|
|
1776
|
+
page.md # /${cfg.entry}
|
|
1777
|
+
installation/
|
|
1778
|
+
page.md # /${cfg.entry}/installation
|
|
1779
|
+
quickstart/
|
|
1780
|
+
page.md # /${cfg.entry}/quickstart
|
|
1781
|
+
src/
|
|
1782
|
+
lib/
|
|
1783
|
+
docs.config.ts # Docs configuration
|
|
1784
|
+
docs.server.ts # Server-side docs loader
|
|
1785
|
+
pages/
|
|
1786
|
+
${cfg.entry}/
|
|
1787
|
+
index.astro # Docs index page
|
|
1788
|
+
[...slug].astro # Dynamic doc page
|
|
1789
|
+
api/
|
|
1790
|
+
${cfg.entry}.ts # Search/AI API route
|
|
1791
|
+
\\\`\\\`\\\`
|
|
1792
|
+
|
|
1793
|
+
## What's Next?
|
|
1794
|
+
|
|
1795
|
+
Head to the [Quickstart](/${cfg.entry}/quickstart) guide to start writing your first page.
|
|
1796
|
+
`;
|
|
1797
|
+
}
|
|
1798
|
+
function astroQuickstartPageTemplate(cfg) {
|
|
1799
|
+
const t = getThemeInfo(cfg.theme);
|
|
1800
|
+
return `\
|
|
1801
|
+
---
|
|
1802
|
+
title: "Quickstart"
|
|
1803
|
+
description: "Get up and running in minutes"
|
|
1804
|
+
---
|
|
1805
|
+
|
|
1806
|
+
# Quickstart
|
|
1807
|
+
|
|
1808
|
+
This guide walks you through creating your first documentation page.
|
|
1809
|
+
|
|
1810
|
+
## Creating a Page
|
|
1811
|
+
|
|
1812
|
+
Create a new folder under \\\`${cfg.entry}/\\\` with a \\\`page.md\\\` file:
|
|
1813
|
+
|
|
1814
|
+
\\\`\\\`\\\`bash
|
|
1815
|
+
mkdir -p ${cfg.entry}/my-page
|
|
1816
|
+
\\\`\\\`\\\`
|
|
1817
|
+
|
|
1818
|
+
Then create \\\`${cfg.entry}/my-page/page.md\\\`:
|
|
1819
|
+
|
|
1820
|
+
\\\`\\\`\\\`md
|
|
1821
|
+
---
|
|
1822
|
+
title: "My Page"
|
|
1823
|
+
description: "A custom documentation page"
|
|
1824
|
+
---
|
|
1825
|
+
|
|
1826
|
+
# My Page
|
|
1827
|
+
|
|
1828
|
+
Write your content here using **Markdown**.
|
|
1829
|
+
\\\`\\\`\\\`
|
|
1830
|
+
|
|
1831
|
+
Your page is now available at \\\`/${cfg.entry}/my-page\\\`.
|
|
1832
|
+
|
|
1833
|
+
## Customizing the Theme
|
|
1834
|
+
|
|
1835
|
+
Edit \\\`src/lib/docs.config.ts\\\` to change colors, typography, and component defaults:
|
|
1836
|
+
|
|
1837
|
+
\\\`\\\`\\\`ts title="src/lib/docs.config.ts"
|
|
1838
|
+
theme: ${t.factory}({
|
|
1839
|
+
ui: {
|
|
1840
|
+
colors: { primary: "#22c55e" },
|
|
1841
|
+
},
|
|
1842
|
+
}),
|
|
1843
|
+
\\\`\\\`\\\`
|
|
1844
|
+
|
|
1845
|
+
## Deploying
|
|
1846
|
+
|
|
1847
|
+
Build your docs for production:
|
|
1848
|
+
|
|
1849
|
+
\\\`\\\`\\\`bash
|
|
1850
|
+
pnpm build
|
|
1851
|
+
\\\`\\\`\\\`
|
|
1852
|
+
|
|
1853
|
+
Deploy to Vercel, Netlify, or any Node.js hosting platform.
|
|
1854
|
+
`;
|
|
1855
|
+
}
|
|
1856
|
+
function nuxtDocsConfigTemplate(cfg) {
|
|
1857
|
+
if (cfg.theme === "custom" && cfg.customThemeName) {
|
|
1858
|
+
const exportName = getThemeExportName(cfg.customThemeName);
|
|
1859
|
+
return `\
|
|
1860
|
+
import { defineDocs } from "@farming-labs/docs";
|
|
1861
|
+
import { ${exportName} } from "${cfg.useAlias ? "~/themes/" + cfg.customThemeName.replace(/\.ts$/i, "") : "./themes/" + cfg.customThemeName.replace(/\.ts$/i, "")}";
|
|
1862
|
+
|
|
1863
|
+
export default defineDocs({
|
|
1864
|
+
entry: "${cfg.entry}",
|
|
1865
|
+
contentDir: "${cfg.entry}",
|
|
1866
|
+
${renderI18nConfig(cfg)} theme: ${exportName}({
|
|
1867
|
+
ui: {
|
|
1868
|
+
colors: { primary: "#6366f1" },
|
|
1869
|
+
},
|
|
1870
|
+
}),
|
|
1871
|
+
|
|
1872
|
+
nav: {
|
|
1873
|
+
title: "${cfg.projectName}",
|
|
1874
|
+
url: "/${cfg.entry}",
|
|
1875
|
+
},
|
|
1876
|
+
|
|
1877
|
+
breadcrumb: { enabled: true },
|
|
1878
|
+
|
|
1879
|
+
metadata: {
|
|
1880
|
+
titleTemplate: "%s – ${cfg.projectName}",
|
|
1881
|
+
description: "Documentation for ${cfg.projectName}",
|
|
1882
|
+
},
|
|
1883
|
+
});
|
|
1884
|
+
`;
|
|
1885
|
+
}
|
|
1886
|
+
const t = getThemeInfo(cfg.theme);
|
|
1887
|
+
return `\
|
|
1888
|
+
import { defineDocs } from "@farming-labs/docs";
|
|
1889
|
+
import { ${t.factory} } from "${t.nuxtImport}";
|
|
1890
|
+
|
|
1891
|
+
export default defineDocs({
|
|
1892
|
+
entry: "${cfg.entry}",
|
|
1893
|
+
contentDir: "${cfg.entry}",
|
|
1894
|
+
${renderI18nConfig(cfg)} theme: ${t.factory}({
|
|
1895
|
+
ui: {
|
|
1896
|
+
colors: { primary: "#6366f1" },
|
|
1897
|
+
},
|
|
1898
|
+
}),
|
|
1899
|
+
|
|
1900
|
+
nav: {
|
|
1901
|
+
title: "${cfg.projectName}",
|
|
1902
|
+
url: "/${cfg.entry}",
|
|
1903
|
+
},
|
|
1904
|
+
|
|
1905
|
+
breadcrumb: { enabled: true },
|
|
1906
|
+
|
|
1907
|
+
metadata: {
|
|
1908
|
+
titleTemplate: "%s – ${cfg.projectName}",
|
|
1909
|
+
description: "Documentation for ${cfg.projectName}",
|
|
1910
|
+
},
|
|
1911
|
+
});
|
|
1912
|
+
`;
|
|
1913
|
+
}
|
|
1914
|
+
function nuxtDocsServerTemplate(cfg) {
|
|
1915
|
+
const contentDirName = cfg.entry ?? "docs";
|
|
1916
|
+
return `\
|
|
1917
|
+
import { createDocsServer } from "@farming-labs/nuxt/server";
|
|
1918
|
+
import config from "${cfg.useAlias ? "~/docs.config" : "../../docs.config"}";
|
|
1919
|
+
|
|
1920
|
+
const contentFiles = import.meta.glob("/${contentDirName}/**/*.{md,mdx}", {
|
|
1921
|
+
query: "?raw",
|
|
1922
|
+
import: "default",
|
|
1923
|
+
eager: true,
|
|
1924
|
+
}) as Record<string, string>;
|
|
1925
|
+
|
|
1926
|
+
export const docsServer = createDocsServer({
|
|
1927
|
+
...config,
|
|
1928
|
+
_preloadedContent: contentFiles,
|
|
1929
|
+
});
|
|
1930
|
+
`;
|
|
1931
|
+
}
|
|
1932
|
+
function nuxtServerApiDocsGetTemplate() {
|
|
1933
|
+
return `\
|
|
1934
|
+
import { getRequestURL } from "h3";
|
|
1935
|
+
import { docsServer } from "../utils/docs-server";
|
|
1936
|
+
|
|
1937
|
+
export default defineEventHandler((event) => {
|
|
1938
|
+
const url = getRequestURL(event);
|
|
1939
|
+
const request = new Request(url.href, {
|
|
1940
|
+
method: event.method,
|
|
1941
|
+
headers: event.headers,
|
|
1942
|
+
});
|
|
1943
|
+
return docsServer.GET({ request });
|
|
1944
|
+
});
|
|
1945
|
+
`;
|
|
1946
|
+
}
|
|
1947
|
+
function nuxtServerApiDocsPostTemplate() {
|
|
1948
|
+
return `\
|
|
1949
|
+
import { getRequestURL, readRawBody } from "h3";
|
|
1950
|
+
import { docsServer } from "../utils/docs-server";
|
|
1951
|
+
|
|
1952
|
+
export default defineEventHandler(async (event) => {
|
|
1953
|
+
const url = getRequestURL(event);
|
|
1954
|
+
const body = await readRawBody(event);
|
|
1955
|
+
const request = new Request(url.href, {
|
|
1956
|
+
method: "POST",
|
|
1957
|
+
headers: event.headers,
|
|
1958
|
+
body: body ?? undefined,
|
|
1959
|
+
});
|
|
1960
|
+
return docsServer.POST({ request });
|
|
1961
|
+
});
|
|
1962
|
+
`;
|
|
1963
|
+
}
|
|
1964
|
+
function nuxtServerApiDocsLoadTemplate() {
|
|
1965
|
+
return `\
|
|
1966
|
+
import { getQuery } from "h3";
|
|
1967
|
+
import { docsServer } from "../../utils/docs-server";
|
|
1968
|
+
|
|
1969
|
+
export default defineEventHandler(async (event) => {
|
|
1970
|
+
const query = getQuery(event);
|
|
1971
|
+
const pathname = (query.pathname as string) ?? "/docs";
|
|
1972
|
+
return docsServer.load(pathname);
|
|
1973
|
+
});
|
|
1974
|
+
`;
|
|
1975
|
+
}
|
|
1976
|
+
function nuxtDocsPageTemplate(cfg) {
|
|
1977
|
+
return `\
|
|
1978
|
+
<script setup lang="ts">
|
|
1979
|
+
import { DocsLayout, DocsContent } from "@farming-labs/nuxt-theme";
|
|
1980
|
+
import config from "${cfg.useAlias ? "~/docs.config" : "../../docs.config"}";
|
|
1981
|
+
|
|
1982
|
+
const route = useRoute();
|
|
1983
|
+
const pathname = computed(() => route.path);
|
|
1984
|
+
|
|
1985
|
+
const { data, error } = await useAsyncData(\`docs-\${pathname.value}\`, () =>
|
|
1986
|
+
$fetch("/api/docs/load", {
|
|
1987
|
+
query: { pathname: pathname.value },
|
|
1988
|
+
})
|
|
1989
|
+
);
|
|
1990
|
+
|
|
1991
|
+
if (error.value) {
|
|
1992
|
+
throw createError({
|
|
1993
|
+
statusCode: 404,
|
|
1994
|
+
statusMessage: "Page not found",
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
<\/script>
|
|
1998
|
+
|
|
1999
|
+
<template>
|
|
2000
|
+
<div v-if="data" class="fd-docs-wrapper">
|
|
2001
|
+
<DocsLayout :tree="data.tree" :config="config">
|
|
2002
|
+
<DocsContent :data="data" :config="config" />
|
|
2003
|
+
</DocsLayout>
|
|
2004
|
+
</div>
|
|
2005
|
+
</template>
|
|
2006
|
+
`;
|
|
2007
|
+
}
|
|
2008
|
+
function nuxtConfigTemplate(cfg) {
|
|
2009
|
+
return `\
|
|
2010
|
+
export default defineNuxtConfig({
|
|
2011
|
+
compatibilityDate: "2024-11-01",
|
|
2012
|
+
|
|
2013
|
+
css: ["@farming-labs/nuxt-theme/${getThemeInfo(cfg.theme).nuxtCssTheme}/css"],
|
|
2014
|
+
|
|
2015
|
+
vite: {
|
|
2016
|
+
optimizeDeps: {
|
|
2017
|
+
include: ["@farming-labs/docs", "@farming-labs/nuxt", "@farming-labs/nuxt-theme"],
|
|
2018
|
+
},
|
|
2019
|
+
},
|
|
2020
|
+
|
|
2021
|
+
nitro: {
|
|
2022
|
+
moduleSideEffects: ["@farming-labs/nuxt/server"],
|
|
2023
|
+
},
|
|
2024
|
+
});
|
|
2025
|
+
`;
|
|
2026
|
+
}
|
|
2027
|
+
function nuxtWelcomePageTemplate(cfg) {
|
|
2028
|
+
return `\
|
|
2029
|
+
---
|
|
2030
|
+
order: 1
|
|
2031
|
+
title: Documentation
|
|
2032
|
+
description: Welcome to ${cfg.projectName} documentation
|
|
2033
|
+
icon: book
|
|
2034
|
+
---
|
|
2035
|
+
|
|
2036
|
+
# Welcome to ${cfg.projectName}
|
|
2037
|
+
|
|
2038
|
+
Get started with our documentation. Browse the pages on the left to learn more.
|
|
2039
|
+
|
|
2040
|
+
## Overview
|
|
2041
|
+
|
|
2042
|
+
This documentation was generated by \`@farming-labs/docs\`. Edit the markdown files in \`${cfg.entry}/\` to customize.
|
|
2043
|
+
|
|
2044
|
+
## Features
|
|
2045
|
+
|
|
2046
|
+
- **Markdown Support** — Write docs with standard Markdown
|
|
2047
|
+
- **Syntax Highlighting** — Code blocks with automatic highlighting
|
|
2048
|
+
- **Dark Mode** — Built-in theme switching
|
|
2049
|
+
- **Search** — Full-text search across all pages (⌘ K)
|
|
2050
|
+
- **Responsive** — Works on any screen size
|
|
2051
|
+
|
|
2052
|
+
---
|
|
2053
|
+
|
|
2054
|
+
## Next Steps
|
|
2055
|
+
|
|
2056
|
+
Start by reading the [Installation](/${cfg.entry}/installation) guide, then follow the [Quickstart](/${cfg.entry}/quickstart) to build something.
|
|
2057
|
+
`;
|
|
2058
|
+
}
|
|
2059
|
+
function nuxtInstallationPageTemplate(cfg) {
|
|
2060
|
+
const t = getThemeInfo(cfg.theme);
|
|
2061
|
+
return `\
|
|
2062
|
+
---
|
|
2063
|
+
order: 3
|
|
2064
|
+
title: Installation
|
|
2065
|
+
description: How to install and set up ${cfg.projectName}
|
|
2066
|
+
icon: terminal
|
|
2067
|
+
---
|
|
2068
|
+
|
|
2069
|
+
# Installation
|
|
2070
|
+
|
|
2071
|
+
Follow these steps to install and configure ${cfg.projectName}.
|
|
2072
|
+
|
|
2073
|
+
## Prerequisites
|
|
2074
|
+
|
|
2075
|
+
- Node.js 18+
|
|
2076
|
+
- A package manager (pnpm, npm, or yarn)
|
|
2077
|
+
|
|
2078
|
+
## Install Dependencies
|
|
2079
|
+
|
|
2080
|
+
\`\`\`bash
|
|
2081
|
+
pnpm add @farming-labs/docs @farming-labs/nuxt @farming-labs/nuxt-theme
|
|
2082
|
+
\`\`\`
|
|
2083
|
+
|
|
2084
|
+
## Configuration
|
|
2085
|
+
|
|
2086
|
+
Your project includes a \`docs.config.ts\` at the root:
|
|
2087
|
+
|
|
2088
|
+
\`\`\`ts title="docs.config.ts"
|
|
2089
|
+
import { defineDocs } from "@farming-labs/docs";
|
|
2090
|
+
import { ${t.factory} } from "${t.nuxtImport}";
|
|
2091
|
+
|
|
2092
|
+
export default defineDocs({
|
|
2093
|
+
entry: "${cfg.entry}",
|
|
2094
|
+
contentDir: "${cfg.entry}",
|
|
2095
|
+
theme: ${t.factory}({
|
|
2096
|
+
ui: { colors: { primary: "#6366f1" } },
|
|
2097
|
+
}),
|
|
2098
|
+
});
|
|
2099
|
+
\`\`\`
|
|
2100
|
+
|
|
2101
|
+
## Project Structure
|
|
2102
|
+
|
|
2103
|
+
\`\`\`
|
|
2104
|
+
${cfg.entry}/ # Markdown content
|
|
2105
|
+
page.md
|
|
2106
|
+
installation/page.md
|
|
2107
|
+
quickstart/page.md
|
|
2108
|
+
server/
|
|
2109
|
+
utils/docs-server.ts # createDocsServer + preloaded content
|
|
2110
|
+
api/docs/
|
|
2111
|
+
load.get.ts # Page data API
|
|
2112
|
+
docs.get.ts # Search API
|
|
2113
|
+
docs.post.ts # AI chat API
|
|
2114
|
+
pages/
|
|
2115
|
+
${cfg.entry}/[[...slug]].vue # Docs catch-all page
|
|
2116
|
+
docs.config.ts
|
|
2117
|
+
nuxt.config.ts
|
|
2118
|
+
\`\`\`
|
|
2119
|
+
|
|
2120
|
+
## What's Next?
|
|
2121
|
+
|
|
2122
|
+
Head to the [Quickstart](/${cfg.entry}/quickstart) guide to start writing your first page.
|
|
2123
|
+
`;
|
|
2124
|
+
}
|
|
2125
|
+
function nuxtQuickstartPageTemplate(cfg) {
|
|
2126
|
+
const t = getThemeInfo(cfg.theme);
|
|
2127
|
+
return `\
|
|
2128
|
+
---
|
|
2129
|
+
order: 2
|
|
2130
|
+
title: Quickstart
|
|
2131
|
+
description: Get up and running in minutes
|
|
2132
|
+
icon: rocket
|
|
2133
|
+
---
|
|
2134
|
+
|
|
2135
|
+
# Quickstart
|
|
2136
|
+
|
|
2137
|
+
This guide walks you through creating your first documentation page.
|
|
2138
|
+
|
|
2139
|
+
## Creating a Page
|
|
2140
|
+
|
|
2141
|
+
Create a new folder under \`${cfg.entry}/\` with a \`page.md\` file:
|
|
2142
|
+
|
|
2143
|
+
\`\`\`bash
|
|
2144
|
+
mkdir -p ${cfg.entry}/my-page
|
|
2145
|
+
\`\`\`
|
|
2146
|
+
|
|
2147
|
+
Then create \`${cfg.entry}/my-page/page.md\`:
|
|
2148
|
+
|
|
2149
|
+
\`\`\`md
|
|
2150
|
+
---
|
|
2151
|
+
title: "My Page"
|
|
2152
|
+
description: "A custom documentation page"
|
|
2153
|
+
---
|
|
2154
|
+
|
|
2155
|
+
# My Page
|
|
2156
|
+
|
|
2157
|
+
Write your content here using **Markdown**.
|
|
2158
|
+
\`\`\`
|
|
2159
|
+
|
|
2160
|
+
Your page is now available at \`/${cfg.entry}/my-page\`.
|
|
2161
|
+
|
|
2162
|
+
## Customizing the Theme
|
|
2163
|
+
|
|
2164
|
+
Edit \`docs.config.ts\` to change colors and typography:
|
|
2165
|
+
|
|
2166
|
+
\`\`\`ts
|
|
2167
|
+
theme: ${t.factory}({
|
|
2168
|
+
ui: {
|
|
2169
|
+
colors: { primary: "#22c55e" },
|
|
2170
|
+
},
|
|
2171
|
+
}),
|
|
2172
|
+
\`\`\`
|
|
2173
|
+
|
|
2174
|
+
## Deploying
|
|
2175
|
+
|
|
2176
|
+
Build your docs for production:
|
|
2177
|
+
|
|
2178
|
+
\`\`\`bash
|
|
2179
|
+
pnpm build
|
|
2180
|
+
\`\`\`
|
|
2181
|
+
|
|
2182
|
+
Deploy to Vercel, Netlify, or any Node.js hosting platform.
|
|
2183
|
+
`;
|
|
2184
|
+
}
|
|
2185
|
+
function nuxtGlobalCssTemplate(theme, customThemeName, globalCssRelPath) {
|
|
2186
|
+
if (theme === "custom" && customThemeName && globalCssRelPath) return `\
|
|
2187
|
+
@import "${getCustomThemeCssImportPath(globalCssRelPath, customThemeName.replace(/\.css$/i, ""))}";
|
|
2188
|
+
`;
|
|
2189
|
+
return `\
|
|
2190
|
+
@import "@farming-labs/nuxt-theme/${theme}/css";
|
|
2191
|
+
`;
|
|
2192
|
+
}
|
|
2193
|
+
function nuxtCssImportLine(theme, customThemeName, globalCssRelPath) {
|
|
2194
|
+
if (theme === "custom" && customThemeName && globalCssRelPath) return `@import "${getCustomThemeCssImportPath(globalCssRelPath, customThemeName.replace(/\.css$/i, ""))}";`;
|
|
2195
|
+
return `@import "@farming-labs/nuxt-theme/${theme}/css";`;
|
|
2196
|
+
}
|
|
2197
|
+
function injectNuxtCssImport(existingContent, theme, customThemeName, globalCssRelPath) {
|
|
2198
|
+
const importLine = nuxtCssImportLine(theme, customThemeName, globalCssRelPath);
|
|
2199
|
+
if (existingContent.includes(importLine)) return null;
|
|
2200
|
+
if (theme !== "custom" && existingContent.includes("@farming-labs/nuxt-theme/") && existingContent.includes("/css")) return null;
|
|
2201
|
+
if (theme === "custom" && existingContent.includes("themes/") && existingContent.includes(".css")) return null;
|
|
2202
|
+
const lines = existingContent.split("\n");
|
|
2203
|
+
const lastImportIdx = lines.reduce((acc, l, i) => l.trimStart().startsWith("@import") ? i : acc, -1);
|
|
2204
|
+
if (lastImportIdx >= 0) lines.splice(lastImportIdx + 1, 0, importLine);
|
|
2205
|
+
else lines.unshift(importLine);
|
|
2206
|
+
return lines.join("\n");
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
//#endregion
|
|
2210
|
+
//#region src/cli/init.ts
|
|
2211
|
+
const EXAMPLES_REPO = "farming-labs/docs";
|
|
2212
|
+
const VALID_TEMPLATES = [
|
|
2213
|
+
"next",
|
|
2214
|
+
"nuxt",
|
|
2215
|
+
"sveltekit",
|
|
2216
|
+
"astro",
|
|
2217
|
+
"tanstack-start"
|
|
2218
|
+
];
|
|
2219
|
+
const COMMON_LOCALE_OPTIONS = [
|
|
2220
|
+
{
|
|
2221
|
+
value: "en",
|
|
2222
|
+
label: "English",
|
|
2223
|
+
hint: "en"
|
|
2224
|
+
},
|
|
2225
|
+
{
|
|
2226
|
+
value: "fr",
|
|
2227
|
+
label: "French",
|
|
2228
|
+
hint: "fr"
|
|
2229
|
+
},
|
|
2230
|
+
{
|
|
2231
|
+
value: "es",
|
|
2232
|
+
label: "Spanish",
|
|
2233
|
+
hint: "es"
|
|
2234
|
+
},
|
|
2235
|
+
{
|
|
2236
|
+
value: "de",
|
|
2237
|
+
label: "German",
|
|
2238
|
+
hint: "de"
|
|
2239
|
+
},
|
|
2240
|
+
{
|
|
2241
|
+
value: "pt",
|
|
2242
|
+
label: "Portuguese",
|
|
2243
|
+
hint: "pt"
|
|
2244
|
+
},
|
|
2245
|
+
{
|
|
2246
|
+
value: "it",
|
|
2247
|
+
label: "Italian",
|
|
2248
|
+
hint: "it"
|
|
2249
|
+
},
|
|
2250
|
+
{
|
|
2251
|
+
value: "ja",
|
|
2252
|
+
label: "Japanese",
|
|
2253
|
+
hint: "ja"
|
|
2254
|
+
},
|
|
2255
|
+
{
|
|
2256
|
+
value: "ko",
|
|
2257
|
+
label: "Korean",
|
|
2258
|
+
hint: "ko"
|
|
2259
|
+
},
|
|
2260
|
+
{
|
|
2261
|
+
value: "zh",
|
|
2262
|
+
label: "Chinese",
|
|
2263
|
+
hint: "zh"
|
|
2264
|
+
},
|
|
2265
|
+
{
|
|
2266
|
+
value: "ar",
|
|
2267
|
+
label: "Arabic",
|
|
2268
|
+
hint: "ar"
|
|
2269
|
+
},
|
|
2270
|
+
{
|
|
2271
|
+
value: "hi",
|
|
2272
|
+
label: "Hindi",
|
|
2273
|
+
hint: "hi"
|
|
2274
|
+
},
|
|
2275
|
+
{
|
|
2276
|
+
value: "ru",
|
|
2277
|
+
label: "Russian",
|
|
2278
|
+
hint: "ru"
|
|
2279
|
+
}
|
|
2280
|
+
];
|
|
2281
|
+
function normalizeLocaleCode(value) {
|
|
2282
|
+
const trimmed = value.trim();
|
|
2283
|
+
if (!trimmed) return "";
|
|
2284
|
+
const [language, ...rest] = trimmed.split("-");
|
|
2285
|
+
const normalizedLanguage = language.toLowerCase();
|
|
2286
|
+
if (rest.length === 0) return normalizedLanguage;
|
|
2287
|
+
return `${normalizedLanguage}-${rest.join("-").toUpperCase()}`;
|
|
2288
|
+
}
|
|
2289
|
+
function parseLocaleInput(input) {
|
|
2290
|
+
return Array.from(new Set(input.split(",").map((value) => normalizeLocaleCode(value)).filter(Boolean)));
|
|
2291
|
+
}
|
|
2292
|
+
function normalizeEntryPath(entry) {
|
|
2293
|
+
return entry.replace(/^\/+|\/+$/g, "");
|
|
2294
|
+
}
|
|
2295
|
+
function getTanstackDocsRouteDir(entry) {
|
|
2296
|
+
return path.posix.join("src/routes", normalizeEntryPath(entry));
|
|
2297
|
+
}
|
|
2298
|
+
async function init(options = {}) {
|
|
2299
|
+
const cwd = process.cwd();
|
|
2300
|
+
p.intro(pc.bgCyan(pc.black(" @farming-labs/docs ")));
|
|
2301
|
+
let projectType = "existing";
|
|
2302
|
+
if (!options.template) {
|
|
2303
|
+
const projectTypeAnswer = await p.select({
|
|
2304
|
+
message: "Are you adding docs to an existing project or starting fresh?",
|
|
2305
|
+
options: [{
|
|
2306
|
+
value: "existing",
|
|
2307
|
+
label: "Existing project",
|
|
2308
|
+
hint: "Add docs to the current app in this directory"
|
|
2309
|
+
}, {
|
|
2310
|
+
value: "fresh",
|
|
2311
|
+
label: "Fresh project",
|
|
2312
|
+
hint: "Bootstrap a new app from a template (Next, Nuxt, SvelteKit, Astro, TanStack Start)"
|
|
2313
|
+
}]
|
|
2314
|
+
});
|
|
2315
|
+
if (p.isCancel(projectTypeAnswer)) {
|
|
2316
|
+
p.outro(pc.red("Init cancelled."));
|
|
2317
|
+
process.exit(0);
|
|
2318
|
+
}
|
|
2319
|
+
projectType = projectTypeAnswer;
|
|
2320
|
+
}
|
|
2321
|
+
if (projectType === "fresh" || options.template) {
|
|
2322
|
+
let template;
|
|
2323
|
+
if (options.template) {
|
|
2324
|
+
template = options.template.toLowerCase();
|
|
2325
|
+
if (!VALID_TEMPLATES.includes(template)) {
|
|
2326
|
+
p.log.error(`Invalid ${pc.cyan("--template")}. Use one of: ${VALID_TEMPLATES.map((t) => pc.cyan(t)).join(", ")}`);
|
|
2327
|
+
process.exit(1);
|
|
2328
|
+
}
|
|
2329
|
+
} else {
|
|
2330
|
+
const templateAnswer = await p.select({
|
|
2331
|
+
message: "Which framework would you like to use?",
|
|
2332
|
+
options: [
|
|
2333
|
+
{
|
|
2334
|
+
value: "next",
|
|
2335
|
+
label: "Next.js",
|
|
2336
|
+
hint: "React with App Router"
|
|
2337
|
+
},
|
|
2338
|
+
{
|
|
2339
|
+
value: "nuxt",
|
|
2340
|
+
label: "Nuxt",
|
|
2341
|
+
hint: "Vue 3 with file-based routing"
|
|
2342
|
+
},
|
|
2343
|
+
{
|
|
2344
|
+
value: "sveltekit",
|
|
2345
|
+
label: "SvelteKit",
|
|
2346
|
+
hint: "Svelte with file-based routing"
|
|
2347
|
+
},
|
|
2348
|
+
{
|
|
2349
|
+
value: "astro",
|
|
2350
|
+
label: "Astro",
|
|
2351
|
+
hint: "Content-focused with islands"
|
|
2352
|
+
},
|
|
2353
|
+
{
|
|
2354
|
+
value: "tanstack-start",
|
|
2355
|
+
label: "TanStack Start",
|
|
2356
|
+
hint: "React with TanStack Router and server functions"
|
|
2357
|
+
}
|
|
2358
|
+
]
|
|
2359
|
+
});
|
|
2360
|
+
if (p.isCancel(templateAnswer)) {
|
|
2361
|
+
p.outro(pc.red("Init cancelled."));
|
|
2362
|
+
process.exit(0);
|
|
2363
|
+
}
|
|
2364
|
+
template = templateAnswer;
|
|
2365
|
+
}
|
|
2366
|
+
const defaultProjectName = "my-docs";
|
|
2367
|
+
let projectName = options.name?.trim();
|
|
2368
|
+
if (!projectName) {
|
|
2369
|
+
const nameAnswer = await p.text({
|
|
2370
|
+
message: "Project name? (we'll create this folder and bootstrap the app here)",
|
|
2371
|
+
placeholder: defaultProjectName,
|
|
2372
|
+
defaultValue: defaultProjectName,
|
|
2373
|
+
validate: (value) => {
|
|
2374
|
+
const v = (value ?? "").trim();
|
|
2375
|
+
if (v.includes("/") || v.includes("\\")) return "Project name cannot contain path separators";
|
|
2376
|
+
if (v.includes(" ")) return "Project name cannot contain spaces";
|
|
2377
|
+
}
|
|
2378
|
+
});
|
|
2379
|
+
if (p.isCancel(nameAnswer)) {
|
|
2380
|
+
p.outro(pc.red("Init cancelled."));
|
|
2381
|
+
process.exit(0);
|
|
2382
|
+
}
|
|
2383
|
+
projectName = nameAnswer.trim() || defaultProjectName;
|
|
2384
|
+
}
|
|
2385
|
+
const templateLabel = template === "next" ? "Next.js" : template === "nuxt" ? "Nuxt" : template === "sveltekit" ? "SvelteKit" : template === "astro" ? "Astro" : "TanStack Start";
|
|
2386
|
+
const targetDir = path.join(cwd, projectName);
|
|
2387
|
+
const fs = await import("node:fs");
|
|
2388
|
+
if (fs.existsSync(targetDir)) {
|
|
2389
|
+
p.log.error(`Directory ${pc.cyan(projectName)} already exists. Choose a different ${pc.cyan("--name")} or remove it.`);
|
|
2390
|
+
process.exit(1);
|
|
2391
|
+
}
|
|
2392
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
2393
|
+
p.log.step(`Bootstrapping project with ${pc.cyan(`'${projectName}'`)} (${templateLabel})...`);
|
|
2394
|
+
try {
|
|
2395
|
+
exec(`npx degit ${EXAMPLES_REPO}/examples/${template} . --force`, targetDir);
|
|
2396
|
+
} catch (err) {
|
|
2397
|
+
p.log.error("Failed to bootstrap. Check your connection and that the repo exists.");
|
|
2398
|
+
process.exit(1);
|
|
2399
|
+
}
|
|
2400
|
+
const pmAnswer = await p.select({
|
|
2401
|
+
message: "Which package manager do you want to use in this new project?",
|
|
2402
|
+
options: [
|
|
2403
|
+
{
|
|
2404
|
+
value: "pnpm",
|
|
2405
|
+
label: "pnpm",
|
|
2406
|
+
hint: "Fast, disk-efficient (recommended)"
|
|
2407
|
+
},
|
|
2408
|
+
{
|
|
2409
|
+
value: "npm",
|
|
2410
|
+
label: "npm",
|
|
2411
|
+
hint: "Default Node.js package manager"
|
|
2412
|
+
},
|
|
2413
|
+
{
|
|
2414
|
+
value: "yarn",
|
|
2415
|
+
label: "yarn",
|
|
2416
|
+
hint: "Classic yarn (script: yarn dev)"
|
|
2417
|
+
},
|
|
2418
|
+
{
|
|
2419
|
+
value: "bun",
|
|
2420
|
+
label: "bun",
|
|
2421
|
+
hint: "Bun runtime + bun install/dev"
|
|
2422
|
+
}
|
|
2423
|
+
]
|
|
2424
|
+
});
|
|
2425
|
+
if (p.isCancel(pmAnswer)) {
|
|
2426
|
+
p.outro(pc.red("Init cancelled."));
|
|
2427
|
+
process.exit(0);
|
|
2428
|
+
}
|
|
2429
|
+
const pmFresh = pmAnswer;
|
|
2430
|
+
p.log.success(`Bootstrapped ${pc.cyan(`'${projectName}'`)}. Installing dependencies with ${pc.cyan(pmFresh)}...`);
|
|
2431
|
+
const installCmd = pmFresh === "yarn" ? "yarn install" : pmFresh === "npm" ? "npm install" : pmFresh === "bun" ? "bun install" : "pnpm install";
|
|
2432
|
+
try {
|
|
2433
|
+
exec(installCmd, targetDir);
|
|
2434
|
+
} catch {
|
|
2435
|
+
p.log.warn(`${pmFresh} install failed. Run ${pc.cyan(installCmd)} manually inside the project.`);
|
|
2436
|
+
}
|
|
2437
|
+
const devCmd = pmFresh === "yarn" ? "yarn dev" : pmFresh === "npm" ? "npm run dev" : pmFresh === "bun" ? "bun dev" : "pnpm dev";
|
|
2438
|
+
p.outro(pc.green(`Done! Run ${pc.cyan(`cd ${projectName} && ${devCmd}`)} to start the dev server and navigate to the /docs.`));
|
|
2439
|
+
p.outro(pc.green("Happy documenting!"));
|
|
2440
|
+
process.exit(0);
|
|
2441
|
+
}
|
|
2442
|
+
let framework = detectFramework(cwd);
|
|
2443
|
+
if (framework) {
|
|
2444
|
+
const frameworkName = framework === "nextjs" ? "Next.js" : framework === "tanstack-start" ? "TanStack Start" : framework === "sveltekit" ? "SvelteKit" : framework === "astro" ? "Astro" : "Nuxt";
|
|
2445
|
+
p.log.success(`Detected framework: ${pc.cyan(frameworkName)}`);
|
|
2446
|
+
} else {
|
|
2447
|
+
p.log.warn("Could not auto-detect a framework from " + pc.cyan("package.json") + ".");
|
|
2448
|
+
const picked = await p.select({
|
|
2449
|
+
message: "Which framework are you using?",
|
|
2450
|
+
options: [
|
|
2451
|
+
{
|
|
2452
|
+
value: "nextjs",
|
|
2453
|
+
label: "Next.js",
|
|
2454
|
+
hint: "React framework with App Router"
|
|
2455
|
+
},
|
|
2456
|
+
{
|
|
2457
|
+
value: "tanstack-start",
|
|
2458
|
+
label: "TanStack Start",
|
|
2459
|
+
hint: "React with TanStack Router and server functions"
|
|
2460
|
+
},
|
|
2461
|
+
{
|
|
2462
|
+
value: "sveltekit",
|
|
2463
|
+
label: "SvelteKit",
|
|
2464
|
+
hint: "Svelte framework with file-based routing"
|
|
2465
|
+
},
|
|
2466
|
+
{
|
|
2467
|
+
value: "astro",
|
|
2468
|
+
label: "Astro",
|
|
2469
|
+
hint: "Content-focused framework with island architecture"
|
|
2470
|
+
},
|
|
2471
|
+
{
|
|
2472
|
+
value: "nuxt",
|
|
2473
|
+
label: "Nuxt",
|
|
2474
|
+
hint: "Vue 3 framework with file-based routing and Nitro server"
|
|
2475
|
+
}
|
|
2476
|
+
]
|
|
2477
|
+
});
|
|
2478
|
+
if (p.isCancel(picked)) {
|
|
2479
|
+
p.outro(pc.red("Init cancelled."));
|
|
2480
|
+
process.exit(0);
|
|
2481
|
+
}
|
|
2482
|
+
framework = picked;
|
|
2483
|
+
}
|
|
2484
|
+
let nextAppDir = "app";
|
|
2485
|
+
if (framework === "nextjs") {
|
|
2486
|
+
const detected = detectNextAppDir(cwd);
|
|
2487
|
+
if (detected) {
|
|
2488
|
+
nextAppDir = detected;
|
|
2489
|
+
p.log.info(`Using App Router at ${pc.cyan(nextAppDir)} (detected ${detected === "src/app" ? "src directory" : "root app"})`);
|
|
2490
|
+
} else {
|
|
2491
|
+
const useSrcApp = await p.confirm({
|
|
2492
|
+
message: "Do you use the src directory for the App Router? (e.g. src/app instead of app)",
|
|
2493
|
+
initialValue: false
|
|
2494
|
+
});
|
|
2495
|
+
if (p.isCancel(useSrcApp)) {
|
|
2496
|
+
p.outro(pc.red("Init cancelled."));
|
|
2497
|
+
process.exit(0);
|
|
2498
|
+
}
|
|
2499
|
+
nextAppDir = useSrcApp ? "src/app" : "app";
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
const theme = await p.select({
|
|
2503
|
+
message: "Which theme would you like to use?",
|
|
2504
|
+
options: [
|
|
2505
|
+
{
|
|
2506
|
+
value: "fumadocs",
|
|
2507
|
+
label: "Fumadocs (Default)",
|
|
2508
|
+
hint: "Clean, modern docs theme with sidebar, search, and dark mode"
|
|
2509
|
+
},
|
|
2510
|
+
{
|
|
2511
|
+
value: "darksharp",
|
|
2512
|
+
label: "Darksharp",
|
|
2513
|
+
hint: "All-black, sharp edges, zero-radius look"
|
|
2514
|
+
},
|
|
2515
|
+
{
|
|
2516
|
+
value: "pixel-border",
|
|
2517
|
+
label: "Pixel Border",
|
|
2518
|
+
hint: "Rounded borders, pixel-perfect spacing, refined sidebar"
|
|
2519
|
+
},
|
|
2520
|
+
{
|
|
2521
|
+
value: "colorful",
|
|
2522
|
+
label: "Colorful",
|
|
2523
|
+
hint: "Fumadocs-style neutral theme with description support"
|
|
2524
|
+
},
|
|
2525
|
+
{
|
|
2526
|
+
value: "darkbold",
|
|
2527
|
+
label: "DarkBold",
|
|
2528
|
+
hint: "Pure monochrome, Geist typography, clean minimalism"
|
|
2529
|
+
},
|
|
2530
|
+
{
|
|
2531
|
+
value: "shiny",
|
|
2532
|
+
label: "Shiny",
|
|
2533
|
+
hint: "Glossy, modern look with subtle shimmer effects"
|
|
2534
|
+
},
|
|
2535
|
+
{
|
|
2536
|
+
value: "greentree",
|
|
2537
|
+
label: "GreenTree",
|
|
2538
|
+
hint: "Emerald green accent, Inter font, Mintlify-inspired"
|
|
2539
|
+
},
|
|
2540
|
+
{
|
|
2541
|
+
value: "custom",
|
|
2542
|
+
label: "Create your own theme",
|
|
2543
|
+
hint: "Scaffold a new theme file + CSS in themes/ (name asked next)"
|
|
2544
|
+
}
|
|
2545
|
+
]
|
|
2546
|
+
});
|
|
2547
|
+
if (p.isCancel(theme)) {
|
|
2548
|
+
p.outro(pc.red("Init cancelled."));
|
|
2549
|
+
process.exit(0);
|
|
2550
|
+
}
|
|
2551
|
+
const defaultThemeName = "my-theme";
|
|
2552
|
+
let customThemeName;
|
|
2553
|
+
if (theme === "custom") {
|
|
2554
|
+
const nameAnswer = await p.text({
|
|
2555
|
+
message: "Theme name? (we'll create themes/<name>.ts and themes/<name>.css)",
|
|
2556
|
+
placeholder: defaultThemeName,
|
|
2557
|
+
defaultValue: defaultThemeName,
|
|
2558
|
+
validate: (value) => {
|
|
2559
|
+
const v = (value ?? "").trim().replace(/\.(ts|css)$/i, "");
|
|
2560
|
+
if (v.includes("/") || v.includes("\\")) return "Theme name cannot contain path separators";
|
|
2561
|
+
if (v.includes(" ")) return "Theme name cannot contain spaces";
|
|
2562
|
+
if (v && !/^[a-z0-9_-]+$/i.test(v)) return "Use only letters, numbers, hyphens, and underscores";
|
|
2563
|
+
}
|
|
2564
|
+
});
|
|
2565
|
+
if (p.isCancel(nameAnswer)) {
|
|
2566
|
+
p.outro(pc.red("Init cancelled."));
|
|
2567
|
+
process.exit(0);
|
|
2568
|
+
}
|
|
2569
|
+
customThemeName = nameAnswer.trim().replace(/\.(ts|css)$/i, "") || defaultThemeName;
|
|
2570
|
+
}
|
|
2571
|
+
const aliasHint = framework === "nextjs" ? `Uses ${pc.cyan("@/")} prefix (requires tsconfig paths)` : framework === "tanstack-start" ? `Uses ${pc.cyan("@/")} prefix (requires tsconfig paths)` : framework === "sveltekit" ? `Uses ${pc.cyan("$lib/")} prefix (SvelteKit built-in)` : framework === "nuxt" ? `Uses ${pc.cyan("~/")} prefix (Nuxt built-in)` : `Uses ${pc.cyan("@/")} prefix (requires tsconfig paths)`;
|
|
2572
|
+
const useAlias = await p.confirm({
|
|
2573
|
+
message: `Use path aliases for imports? ${pc.dim(aliasHint)}`,
|
|
2574
|
+
initialValue: false
|
|
2575
|
+
});
|
|
2576
|
+
if (p.isCancel(useAlias)) {
|
|
2577
|
+
p.outro(pc.red("Init cancelled."));
|
|
2578
|
+
process.exit(0);
|
|
2579
|
+
}
|
|
2580
|
+
let astroAdapter;
|
|
2581
|
+
if (framework === "astro") {
|
|
2582
|
+
const adapter = await p.select({
|
|
2583
|
+
message: "Where will you deploy?",
|
|
2584
|
+
options: [
|
|
2585
|
+
{
|
|
2586
|
+
value: "vercel",
|
|
2587
|
+
label: "Vercel",
|
|
2588
|
+
hint: "Recommended for most projects"
|
|
2589
|
+
},
|
|
2590
|
+
{
|
|
2591
|
+
value: "netlify",
|
|
2592
|
+
label: "Netlify"
|
|
2593
|
+
},
|
|
2594
|
+
{
|
|
2595
|
+
value: "cloudflare",
|
|
2596
|
+
label: "Cloudflare Pages"
|
|
2597
|
+
},
|
|
2598
|
+
{
|
|
2599
|
+
value: "node",
|
|
2600
|
+
label: "Node.js / Docker",
|
|
2601
|
+
hint: "Self-hosted standalone server"
|
|
2602
|
+
}
|
|
2603
|
+
]
|
|
2604
|
+
});
|
|
2605
|
+
if (p.isCancel(adapter)) {
|
|
2606
|
+
p.outro(pc.red("Init cancelled."));
|
|
2607
|
+
process.exit(0);
|
|
2608
|
+
}
|
|
2609
|
+
astroAdapter = adapter;
|
|
2610
|
+
}
|
|
2611
|
+
const defaultEntry = "docs";
|
|
2612
|
+
const entry = await p.text({
|
|
2613
|
+
message: "Where should your docs live?",
|
|
2614
|
+
placeholder: defaultEntry,
|
|
2615
|
+
defaultValue: defaultEntry,
|
|
2616
|
+
validate: (value) => {
|
|
2617
|
+
const v = (value ?? "").trim();
|
|
2618
|
+
if (v.startsWith("/")) return "Use a relative path (no leading /)";
|
|
2619
|
+
if (v.includes(" ")) return "Path cannot contain spaces";
|
|
2620
|
+
}
|
|
2621
|
+
});
|
|
2622
|
+
if (p.isCancel(entry)) {
|
|
2623
|
+
p.outro(pc.red("Init cancelled."));
|
|
2624
|
+
process.exit(0);
|
|
2625
|
+
}
|
|
2626
|
+
const entryPath = entry.trim() || defaultEntry;
|
|
2627
|
+
let docsI18n;
|
|
2628
|
+
if (framework === "tanstack-start") p.log.info("Skipping i18n scaffold for TanStack Start. Configure localized routes manually if needed.");
|
|
2629
|
+
else {
|
|
2630
|
+
const enableI18n = await p.confirm({
|
|
2631
|
+
message: "Do you want to scaffold internationalized docs ?",
|
|
2632
|
+
initialValue: false
|
|
2633
|
+
});
|
|
2634
|
+
if (p.isCancel(enableI18n)) {
|
|
2635
|
+
p.outro(pc.red("Init cancelled."));
|
|
2636
|
+
process.exit(0);
|
|
2637
|
+
}
|
|
2638
|
+
if (!enableI18n) docsI18n = void 0;
|
|
2639
|
+
else {
|
|
2640
|
+
const selectedLocales = await p.multiselect({
|
|
2641
|
+
message: "Which languages should we scaffold?",
|
|
2642
|
+
options: COMMON_LOCALE_OPTIONS.map((option) => ({
|
|
2643
|
+
value: option.value,
|
|
2644
|
+
label: option.label,
|
|
2645
|
+
hint: option.hint
|
|
2646
|
+
}))
|
|
2647
|
+
});
|
|
2648
|
+
if (p.isCancel(selectedLocales)) {
|
|
2649
|
+
p.outro(pc.red("Init cancelled."));
|
|
2650
|
+
process.exit(0);
|
|
2651
|
+
}
|
|
2652
|
+
const extraLocalesAnswer = await p.text({
|
|
2653
|
+
message: "Any additional locale codes? (comma-separated, optional)",
|
|
2654
|
+
placeholder: "nl, sv, pt-BR",
|
|
2655
|
+
defaultValue: "",
|
|
2656
|
+
validate: (value) => {
|
|
2657
|
+
return parseLocaleInput(value ?? "").every((locale) => /^[a-z]{2,3}(?:-[A-Z]{2})?$/.test(locale)) ? void 0 : "Use locale codes like en, fr, zh, or pt-BR";
|
|
2658
|
+
}
|
|
2659
|
+
});
|
|
2660
|
+
if (p.isCancel(extraLocalesAnswer)) {
|
|
2661
|
+
p.outro(pc.red("Init cancelled."));
|
|
2662
|
+
process.exit(0);
|
|
2663
|
+
}
|
|
2664
|
+
const locales = Array.from(new Set([...(selectedLocales ?? []).map((locale) => normalizeLocaleCode(locale)), ...parseLocaleInput(extraLocalesAnswer ?? "")])).filter(Boolean);
|
|
2665
|
+
if (locales.length === 0) {
|
|
2666
|
+
p.log.error("Pick at least one locale to scaffold i18n support.");
|
|
2667
|
+
p.outro(pc.red("Init cancelled."));
|
|
2668
|
+
process.exit(1);
|
|
2669
|
+
}
|
|
2670
|
+
const defaultLocaleAnswer = await p.select({
|
|
2671
|
+
message: "Which locale should be the default?",
|
|
2672
|
+
options: locales.map((locale) => ({
|
|
2673
|
+
value: locale,
|
|
2674
|
+
label: locale,
|
|
2675
|
+
hint: locale === "en" ? "Recommended default" : void 0
|
|
2676
|
+
})),
|
|
2677
|
+
initialValue: locales[0]
|
|
2678
|
+
});
|
|
2679
|
+
if (p.isCancel(defaultLocaleAnswer)) {
|
|
2680
|
+
p.outro(pc.red("Init cancelled."));
|
|
2681
|
+
process.exit(0);
|
|
2682
|
+
}
|
|
2683
|
+
docsI18n = {
|
|
2684
|
+
locales,
|
|
2685
|
+
defaultLocale: defaultLocaleAnswer
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
const detectedCssFiles = detectGlobalCssFiles(cwd);
|
|
2690
|
+
let globalCssRelPath;
|
|
2691
|
+
const defaultCssPath = framework === "tanstack-start" ? "src/styles/app.css" : framework === "sveltekit" ? "src/app.css" : framework === "astro" ? "src/styles/global.css" : framework === "nuxt" ? "assets/css/main.css" : framework === "nextjs" ? `${nextAppDir}/globals.css` : "app/globals.css";
|
|
2692
|
+
if (detectedCssFiles.length === 1) {
|
|
2693
|
+
globalCssRelPath = detectedCssFiles[0];
|
|
2694
|
+
p.log.info(`Found global CSS at ${pc.cyan(globalCssRelPath)}`);
|
|
2695
|
+
} else if (detectedCssFiles.length > 1) {
|
|
2696
|
+
const picked = await p.select({
|
|
2697
|
+
message: "Multiple global CSS files found. Which one should we use?",
|
|
2698
|
+
options: detectedCssFiles.map((f) => ({
|
|
2699
|
+
value: f,
|
|
2700
|
+
label: f
|
|
2701
|
+
}))
|
|
2702
|
+
});
|
|
2703
|
+
if (p.isCancel(picked)) {
|
|
2704
|
+
p.outro(pc.red("Init cancelled."));
|
|
2705
|
+
process.exit(0);
|
|
2706
|
+
}
|
|
2707
|
+
globalCssRelPath = picked;
|
|
2708
|
+
} else {
|
|
2709
|
+
const cssPathAnswer = await p.text({
|
|
2710
|
+
message: "Where is your global CSS file?",
|
|
2711
|
+
placeholder: defaultCssPath,
|
|
2712
|
+
defaultValue: defaultCssPath,
|
|
2713
|
+
validate: (value) => {
|
|
2714
|
+
const v = (value ?? "").trim();
|
|
2715
|
+
if (v && !v.endsWith(".css")) return "Path must end with .css";
|
|
2716
|
+
}
|
|
2717
|
+
});
|
|
2718
|
+
if (p.isCancel(cssPathAnswer)) {
|
|
2719
|
+
p.outro(pc.red("Init cancelled."));
|
|
2720
|
+
process.exit(0);
|
|
2721
|
+
}
|
|
2722
|
+
globalCssRelPath = cssPathAnswer.trim() || defaultCssPath;
|
|
2723
|
+
}
|
|
2724
|
+
const pkgJsonContent = readFileSafe(path.join(cwd, "package.json"));
|
|
2725
|
+
const pkgJson = pkgJsonContent ? JSON.parse(pkgJsonContent) : { name: "my-project" };
|
|
2726
|
+
const projectName = pkgJson.name || "My Project";
|
|
2727
|
+
const cfg = {
|
|
2728
|
+
entry: entryPath,
|
|
2729
|
+
theme,
|
|
2730
|
+
customThemeName,
|
|
2731
|
+
projectName,
|
|
2732
|
+
framework,
|
|
2733
|
+
useAlias,
|
|
2734
|
+
astroAdapter,
|
|
2735
|
+
i18n: docsI18n,
|
|
2736
|
+
...framework === "nextjs" && { nextAppDir }
|
|
2737
|
+
};
|
|
2738
|
+
const s = p.spinner();
|
|
2739
|
+
s.start("Scaffolding docs files");
|
|
2740
|
+
const written = [];
|
|
2741
|
+
const skipped = [];
|
|
2742
|
+
function write(rel, content, overwrite = false) {
|
|
2743
|
+
if (writeFileSafe(path.join(cwd, rel), content, overwrite)) written.push(rel);
|
|
2744
|
+
else skipped.push(rel);
|
|
2745
|
+
}
|
|
2746
|
+
if (framework === "tanstack-start") scaffoldTanstackStart(cwd, cfg, globalCssRelPath, write, skipped, written);
|
|
2747
|
+
else if (framework === "sveltekit") scaffoldSvelteKit(cwd, cfg, globalCssRelPath, write, skipped, written);
|
|
2748
|
+
else if (framework === "astro") scaffoldAstro(cwd, cfg, globalCssRelPath, write, skipped, written);
|
|
2749
|
+
else if (framework === "nuxt") scaffoldNuxt(cwd, cfg, globalCssRelPath, write, skipped, written);
|
|
2750
|
+
else scaffoldNextJs(cwd, cfg, globalCssRelPath, write, skipped, written);
|
|
2751
|
+
s.stop("Files scaffolded");
|
|
2752
|
+
if (written.length > 0) p.log.success(`Created ${written.length} file${written.length > 1 ? "s" : ""}:\n` + written.map((f) => ` ${pc.green("+")} ${f}`).join("\n"));
|
|
2753
|
+
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"));
|
|
2754
|
+
let pm = detectPackageManagerFromLockfile(cwd);
|
|
2755
|
+
if (pm) p.log.info(`Detected ${pc.cyan(pm)}`);
|
|
2756
|
+
const pmAnswerExisting = await p.select({
|
|
2757
|
+
...pm ? { initialValue: pm } : {},
|
|
2758
|
+
message: "Which package manager do you want to use in this project?",
|
|
2759
|
+
options: [
|
|
2760
|
+
{
|
|
2761
|
+
value: "pnpm",
|
|
2762
|
+
label: "pnpm",
|
|
2763
|
+
hint: "Fast, disk-efficient (recommended)"
|
|
2764
|
+
},
|
|
2765
|
+
{
|
|
2766
|
+
value: "npm",
|
|
2767
|
+
label: "npm",
|
|
2768
|
+
hint: "Default Node.js package manager"
|
|
2769
|
+
},
|
|
2770
|
+
{
|
|
2771
|
+
value: "yarn",
|
|
2772
|
+
label: "yarn",
|
|
2773
|
+
hint: "Classic yarn (script: yarn dev)"
|
|
2774
|
+
},
|
|
2775
|
+
{
|
|
2776
|
+
value: "bun",
|
|
2777
|
+
label: "bun",
|
|
2778
|
+
hint: "Bun runtime + bun install/dev"
|
|
2779
|
+
}
|
|
2780
|
+
]
|
|
2781
|
+
});
|
|
2782
|
+
if (p.isCancel(pmAnswerExisting)) {
|
|
2783
|
+
p.outro(pc.red("Init cancelled."));
|
|
2784
|
+
process.exit(0);
|
|
2785
|
+
}
|
|
2786
|
+
pm = pmAnswerExisting;
|
|
2787
|
+
p.log.info(`Using ${pc.cyan(pm)} as package manager`);
|
|
2788
|
+
const s2 = p.spinner();
|
|
2789
|
+
s2.start("Installing dependencies");
|
|
2790
|
+
try {
|
|
2791
|
+
if (framework === "tanstack-start") {
|
|
2792
|
+
exec(`${installCommand(pm)} @farming-labs/docs @farming-labs/theme @farming-labs/tanstack-start`, cwd);
|
|
2793
|
+
const devDeps = ["@tailwindcss/vite", "tailwindcss"];
|
|
2794
|
+
if (useAlias) devDeps.push("vite-tsconfig-paths");
|
|
2795
|
+
const allDeps = {
|
|
2796
|
+
...pkgJson.dependencies,
|
|
2797
|
+
...pkgJson.devDependencies
|
|
2798
|
+
};
|
|
2799
|
+
const missingDevDeps = devDeps.filter((d) => !allDeps[d]);
|
|
2800
|
+
if (missingDevDeps.length > 0) exec(`${devInstallCommand(pm)} ${missingDevDeps.join(" ")}`, cwd);
|
|
2801
|
+
} else if (framework === "sveltekit") exec(`${installCommand(pm)} @farming-labs/docs @farming-labs/svelte @farming-labs/svelte-theme`, cwd);
|
|
2802
|
+
else if (framework === "astro") {
|
|
2803
|
+
const adapterPkg = getAstroAdapterPkg(cfg.astroAdapter ?? "vercel");
|
|
2804
|
+
exec(`${installCommand(pm)} @farming-labs/docs @farming-labs/astro @farming-labs/astro-theme ${adapterPkg}`, cwd);
|
|
2805
|
+
} else if (framework === "nuxt") exec(`${installCommand(pm)} @farming-labs/docs @farming-labs/nuxt @farming-labs/nuxt-theme`, cwd);
|
|
2806
|
+
else {
|
|
2807
|
+
exec(`${installCommand(pm)} @farming-labs/docs @farming-labs/next @farming-labs/theme`, cwd);
|
|
2808
|
+
const devDeps = [
|
|
2809
|
+
"@tailwindcss/postcss",
|
|
2810
|
+
"postcss",
|
|
2811
|
+
"tailwindcss",
|
|
2812
|
+
"@types/mdx",
|
|
2813
|
+
"@types/node"
|
|
2814
|
+
];
|
|
2815
|
+
const allDeps = {
|
|
2816
|
+
...pkgJson.dependencies,
|
|
2817
|
+
...pkgJson.devDependencies
|
|
2818
|
+
};
|
|
2819
|
+
const missingDevDeps = devDeps.filter((d) => !allDeps[d]);
|
|
2820
|
+
if (missingDevDeps.length > 0) exec(`${devInstallCommand(pm)} ${missingDevDeps.join(" ")}`, cwd);
|
|
2821
|
+
}
|
|
2822
|
+
} catch {
|
|
2823
|
+
s2.stop("Failed to install dependencies");
|
|
2824
|
+
p.log.error(`Dependency installation failed. Run the install command manually:
|
|
2825
|
+
${pc.cyan(`${installCommand(pm)} @farming-labs/docs`)}`);
|
|
2826
|
+
p.outro(pc.yellow("Setup partially complete. Install deps and run dev server manually."));
|
|
2827
|
+
process.exit(1);
|
|
2828
|
+
}
|
|
2829
|
+
s2.stop("Dependencies installed");
|
|
2830
|
+
const startDev = await p.confirm({
|
|
2831
|
+
message: "Start the dev server now?",
|
|
2832
|
+
initialValue: true
|
|
2833
|
+
});
|
|
2834
|
+
if (p.isCancel(startDev) || !startDev) {
|
|
2835
|
+
p.log.info(`You can start the dev server later with:
|
|
2836
|
+
${pc.cyan(`${pm === "yarn" ? "yarn" : pm + " run"} dev`)}`);
|
|
2837
|
+
p.outro(pc.green("Done! Happy documenting."));
|
|
2838
|
+
process.exit(0);
|
|
2839
|
+
}
|
|
2840
|
+
p.log.step("Starting dev server...");
|
|
2841
|
+
const devCommand = framework === "tanstack-start" ? {
|
|
2842
|
+
cmd: "npx",
|
|
2843
|
+
args: ["vite", "dev"],
|
|
2844
|
+
waitFor: "ready"
|
|
2845
|
+
} : framework === "sveltekit" ? {
|
|
2846
|
+
cmd: "npx",
|
|
2847
|
+
args: ["vite", "dev"],
|
|
2848
|
+
waitFor: "ready"
|
|
2849
|
+
} : framework === "astro" ? {
|
|
2850
|
+
cmd: "npx",
|
|
2851
|
+
args: ["astro", "dev"],
|
|
2852
|
+
waitFor: "ready"
|
|
2853
|
+
} : framework === "nuxt" ? {
|
|
2854
|
+
cmd: "npx",
|
|
2855
|
+
args: ["nuxt", "dev"],
|
|
2856
|
+
waitFor: "Local"
|
|
2857
|
+
} : {
|
|
2858
|
+
cmd: "npx",
|
|
2859
|
+
args: [
|
|
2860
|
+
"next",
|
|
2861
|
+
"dev",
|
|
2862
|
+
"--webpack"
|
|
2863
|
+
],
|
|
2864
|
+
waitFor: "Ready"
|
|
2865
|
+
};
|
|
2866
|
+
const defaultPort = framework === "tanstack-start" ? "5173" : framework === "sveltekit" ? "5173" : framework === "astro" ? "4321" : framework === "nuxt" ? "3000" : "3000";
|
|
2867
|
+
try {
|
|
2868
|
+
const child = await spawnAndWaitFor(devCommand.cmd, devCommand.args, cwd, devCommand.waitFor, 6e4);
|
|
2869
|
+
const url = `http://localhost:${defaultPort}/${entryPath}`;
|
|
2870
|
+
console.log();
|
|
2871
|
+
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.`);
|
|
2872
|
+
p.outro(pc.green("Happy documenting!"));
|
|
2873
|
+
await new Promise((resolve) => {
|
|
2874
|
+
child.on("close", () => resolve());
|
|
2875
|
+
process.on("SIGINT", () => {
|
|
2876
|
+
child.kill("SIGINT");
|
|
2877
|
+
resolve();
|
|
2878
|
+
});
|
|
2879
|
+
process.on("SIGTERM", () => {
|
|
2880
|
+
child.kill("SIGTERM");
|
|
2881
|
+
resolve();
|
|
2882
|
+
});
|
|
2883
|
+
});
|
|
2884
|
+
} catch (err) {
|
|
2885
|
+
const manualCmd = framework === "tanstack-start" ? "npx vite dev" : framework === "sveltekit" ? "npx vite dev" : framework === "astro" ? "npx astro dev" : framework === "nuxt" ? "npx nuxt dev" : "pnpm dev";
|
|
2886
|
+
p.log.error(`Could not start dev server. Try running manually:
|
|
2887
|
+
${pc.cyan(manualCmd)}`);
|
|
2888
|
+
p.outro(pc.yellow("Setup complete. Start the server manually."));
|
|
2889
|
+
process.exit(1);
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
function getScaffoldContentRoots(cfg) {
|
|
2893
|
+
return cfg.i18n?.locales?.length ? cfg.i18n.locales.map((locale) => `${cfg.entry}/${locale}`) : [cfg.entry];
|
|
2894
|
+
}
|
|
2895
|
+
function scaffoldNextJs(cwd, cfg, globalCssRelPath, write, skipped, written) {
|
|
2896
|
+
const appDir = cfg.nextAppDir ?? "app";
|
|
2897
|
+
if (cfg.theme === "custom" && cfg.customThemeName) {
|
|
2898
|
+
const baseName = cfg.customThemeName.replace(/\.(ts|css)$/i, "");
|
|
2899
|
+
write(`themes/${baseName}.ts`, customThemeTsTemplate(baseName));
|
|
2900
|
+
write(`themes/${baseName}.css`, customThemeCssTemplate(baseName));
|
|
2901
|
+
}
|
|
2902
|
+
write("docs.config.ts", docsConfigTemplate(cfg));
|
|
2903
|
+
const existingNextConfig = readFileSafe(path.join(cwd, "next.config.ts")) ?? readFileSafe(path.join(cwd, "next.config.mjs")) ?? readFileSafe(path.join(cwd, "next.config.js"));
|
|
2904
|
+
if (existingNextConfig) {
|
|
2905
|
+
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";
|
|
2906
|
+
const merged = nextConfigMergedTemplate(existingNextConfig);
|
|
2907
|
+
if (merged !== existingNextConfig) {
|
|
2908
|
+
writeFileSafe(path.join(cwd, configFile), merged, true);
|
|
2909
|
+
written.push(configFile + " (updated)");
|
|
2910
|
+
} else skipped.push(configFile + " (already configured)");
|
|
2911
|
+
} else write("next.config.ts", nextConfigTemplate());
|
|
2912
|
+
const rootLayoutPath = path.join(cwd, `${appDir}/layout.tsx`);
|
|
2913
|
+
const existingRootLayout = readFileSafe(rootLayoutPath);
|
|
2914
|
+
if (!existingRootLayout) write(`${appDir}/layout.tsx`, rootLayoutTemplate(cfg, globalCssRelPath), true);
|
|
2915
|
+
else if (!existingRootLayout.includes("RootProvider")) {
|
|
2916
|
+
const injected = injectRootProviderIntoLayout(existingRootLayout);
|
|
2917
|
+
if (injected) {
|
|
2918
|
+
writeFileSafe(rootLayoutPath, injected, true);
|
|
2919
|
+
written.push(`${appDir}/layout.tsx (injected RootProvider)`);
|
|
2920
|
+
} else skipped.push(`${appDir}/layout.tsx (could not inject RootProvider)`);
|
|
2921
|
+
} else skipped.push(`${appDir}/layout.tsx (already has RootProvider)`);
|
|
2922
|
+
const globalCssAbsPath = path.join(cwd, globalCssRelPath);
|
|
2923
|
+
const existingGlobalCss = readFileSafe(globalCssAbsPath);
|
|
2924
|
+
if (existingGlobalCss) {
|
|
2925
|
+
const injected = injectCssImport(existingGlobalCss, cfg.theme, cfg.customThemeName, globalCssRelPath);
|
|
2926
|
+
if (injected) {
|
|
2927
|
+
writeFileSafe(globalCssAbsPath, injected, true);
|
|
2928
|
+
written.push(globalCssRelPath + " (updated)");
|
|
2929
|
+
} else skipped.push(globalCssRelPath + " (already configured)");
|
|
2930
|
+
} else write(globalCssRelPath, globalCssTemplate(cfg.theme, cfg.customThemeName, globalCssRelPath));
|
|
2931
|
+
write(`${appDir}/${cfg.entry}/layout.tsx`, docsLayoutTemplate(cfg));
|
|
2932
|
+
write("postcss.config.mjs", postcssConfigTemplate());
|
|
2933
|
+
if (!fileExists(path.join(cwd, "tsconfig.json"))) write("tsconfig.json", tsconfigTemplate(cfg.useAlias));
|
|
2934
|
+
if (cfg.i18n?.locales.length) {
|
|
2935
|
+
write(`${appDir}/components/locale-doc-page.tsx`, nextLocaleDocPageTemplate(cfg.i18n.defaultLocale));
|
|
2936
|
+
write(`${appDir}/${cfg.entry}/page.tsx`, nextLocalizedPageTemplate({
|
|
2937
|
+
locales: cfg.i18n.locales,
|
|
2938
|
+
defaultLocale: cfg.i18n.defaultLocale,
|
|
2939
|
+
componentName: "DocsIndexPage",
|
|
2940
|
+
helperImport: "../components/locale-doc-page",
|
|
2941
|
+
pageImports: cfg.i18n.locales.map((locale) => ({
|
|
2942
|
+
locale,
|
|
2943
|
+
importPath: `./${locale}/page.mdx`
|
|
2944
|
+
}))
|
|
2945
|
+
}));
|
|
2946
|
+
write(`${appDir}/${cfg.entry}/installation/page.tsx`, nextLocalizedPageTemplate({
|
|
2947
|
+
locales: cfg.i18n.locales,
|
|
2948
|
+
defaultLocale: cfg.i18n.defaultLocale,
|
|
2949
|
+
componentName: "InstallationPage",
|
|
2950
|
+
helperImport: "../../components/locale-doc-page",
|
|
2951
|
+
pageImports: cfg.i18n.locales.map((locale) => ({
|
|
2952
|
+
locale,
|
|
2953
|
+
importPath: `../${locale}/installation/page.mdx`
|
|
2954
|
+
}))
|
|
2955
|
+
}));
|
|
2956
|
+
write(`${appDir}/${cfg.entry}/quickstart/page.tsx`, nextLocalizedPageTemplate({
|
|
2957
|
+
locales: cfg.i18n.locales,
|
|
2958
|
+
defaultLocale: cfg.i18n.defaultLocale,
|
|
2959
|
+
componentName: "QuickstartPage",
|
|
2960
|
+
helperImport: "../../components/locale-doc-page",
|
|
2961
|
+
pageImports: cfg.i18n.locales.map((locale) => ({
|
|
2962
|
+
locale,
|
|
2963
|
+
importPath: `../${locale}/quickstart/page.mdx`
|
|
2964
|
+
}))
|
|
2965
|
+
}));
|
|
2966
|
+
for (const locale of cfg.i18n.locales) {
|
|
2967
|
+
const base = `${appDir}/${cfg.entry}/${locale}`;
|
|
2968
|
+
write(`${base}/page.mdx`, welcomePageTemplate(cfg));
|
|
2969
|
+
write(`${base}/installation/page.mdx`, installationPageTemplate(cfg));
|
|
2970
|
+
write(`${base}/quickstart/page.mdx`, quickstartPageTemplate(cfg));
|
|
2971
|
+
}
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2974
|
+
write(`${appDir}/${cfg.entry}/page.mdx`, welcomePageTemplate(cfg));
|
|
2975
|
+
write(`${appDir}/${cfg.entry}/installation/page.mdx`, installationPageTemplate(cfg));
|
|
2976
|
+
write(`${appDir}/${cfg.entry}/quickstart/page.mdx`, quickstartPageTemplate(cfg));
|
|
2977
|
+
}
|
|
2978
|
+
function scaffoldTanstackStart(cwd, cfg, globalCssRelPath, write, skipped, written) {
|
|
2979
|
+
if (cfg.theme === "custom" && cfg.customThemeName) {
|
|
2980
|
+
const baseName = cfg.customThemeName.replace(/\.(ts|css)$/i, "");
|
|
2981
|
+
write(`themes/${baseName}.ts`, customThemeTsTemplate(baseName));
|
|
2982
|
+
write(`themes/${baseName}.css`, customThemeCssTemplate(baseName));
|
|
2983
|
+
}
|
|
2984
|
+
write("docs.config.ts", tanstackDocsConfigTemplate(cfg));
|
|
2985
|
+
write("src/lib/docs.server.ts", tanstackDocsServerTemplate());
|
|
2986
|
+
write("src/lib/docs.functions.ts", tanstackDocsFunctionsTemplate());
|
|
2987
|
+
const routeDir = getTanstackDocsRouteDir(cfg.entry);
|
|
2988
|
+
const docsIndexRoute = `${routeDir}/index.tsx`;
|
|
2989
|
+
const docsCatchAllRoute = `${routeDir}/$.tsx`;
|
|
2990
|
+
const apiRoute = "src/routes/api/docs.ts";
|
|
2991
|
+
write(docsIndexRoute, tanstackDocsIndexRouteTemplate({
|
|
2992
|
+
entry: cfg.entry,
|
|
2993
|
+
filePath: docsIndexRoute,
|
|
2994
|
+
useAlias: cfg.useAlias,
|
|
2995
|
+
projectName: cfg.projectName
|
|
2996
|
+
}));
|
|
2997
|
+
write(docsCatchAllRoute, tanstackDocsCatchAllRouteTemplate({
|
|
2998
|
+
entry: cfg.entry,
|
|
2999
|
+
filePath: docsCatchAllRoute,
|
|
3000
|
+
useAlias: cfg.useAlias,
|
|
3001
|
+
projectName: cfg.projectName
|
|
3002
|
+
}));
|
|
3003
|
+
write(apiRoute, tanstackApiDocsRouteTemplate(cfg.useAlias, apiRoute));
|
|
3004
|
+
const rootRoutePath = path.join(cwd, "src/routes/__root.tsx");
|
|
3005
|
+
const existingRootRoute = readFileSafe(rootRoutePath);
|
|
3006
|
+
if (!existingRootRoute) write("src/routes/__root.tsx", tanstackRootRouteTemplate(globalCssRelPath), true);
|
|
3007
|
+
else if (!existingRootRoute.includes("RootProvider")) {
|
|
3008
|
+
const injected = injectTanstackRootProviderIntoRoute(existingRootRoute);
|
|
3009
|
+
if (injected) {
|
|
3010
|
+
writeFileSafe(rootRoutePath, injected, true);
|
|
3011
|
+
written.push("src/routes/__root.tsx (injected RootProvider)");
|
|
3012
|
+
} else skipped.push("src/routes/__root.tsx (could not inject RootProvider)");
|
|
3013
|
+
} else skipped.push("src/routes/__root.tsx (already has RootProvider)");
|
|
3014
|
+
const viteConfigRel = fileExists(path.join(cwd, "vite.config.ts")) ? "vite.config.ts" : fileExists(path.join(cwd, "vite.config.mts")) ? "vite.config.mts" : fileExists(path.join(cwd, "vite.config.js")) ? "vite.config.js" : "vite.config.ts";
|
|
3015
|
+
const viteConfigPath = path.join(cwd, viteConfigRel);
|
|
3016
|
+
const existingViteConfig = readFileSafe(viteConfigPath);
|
|
3017
|
+
if (!existingViteConfig) write(viteConfigRel, tanstackViteConfigTemplate(cfg.useAlias), true);
|
|
3018
|
+
else {
|
|
3019
|
+
const injected = injectTanstackVitePlugins(existingViteConfig, cfg.useAlias);
|
|
3020
|
+
if (injected) {
|
|
3021
|
+
writeFileSafe(viteConfigPath, injected, true);
|
|
3022
|
+
written.push(`${viteConfigRel} (updated)`);
|
|
3023
|
+
} else skipped.push(`${viteConfigRel} (already configured)`);
|
|
3024
|
+
}
|
|
3025
|
+
const globalCssAbsPath = path.join(cwd, globalCssRelPath);
|
|
3026
|
+
const existingGlobalCss = readFileSafe(globalCssAbsPath);
|
|
3027
|
+
if (existingGlobalCss) {
|
|
3028
|
+
const injected = injectCssImport(existingGlobalCss, cfg.theme, cfg.customThemeName, globalCssRelPath);
|
|
3029
|
+
if (injected) {
|
|
3030
|
+
writeFileSafe(globalCssAbsPath, injected, true);
|
|
3031
|
+
written.push(globalCssRelPath + " (updated)");
|
|
3032
|
+
} else skipped.push(globalCssRelPath + " (already configured)");
|
|
3033
|
+
} else write(globalCssRelPath, globalCssTemplate(cfg.theme, cfg.customThemeName, globalCssRelPath));
|
|
3034
|
+
for (const base of getScaffoldContentRoots(cfg)) {
|
|
3035
|
+
write(`${base}/page.mdx`, tanstackWelcomePageTemplate(cfg));
|
|
3036
|
+
write(`${base}/installation/page.mdx`, tanstackInstallationPageTemplate(cfg));
|
|
3037
|
+
write(`${base}/quickstart/page.mdx`, tanstackQuickstartPageTemplate(cfg));
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
function scaffoldSvelteKit(cwd, cfg, globalCssRelPath, write, skipped, written) {
|
|
3041
|
+
if (cfg.theme === "custom" && cfg.customThemeName) {
|
|
3042
|
+
const baseName = cfg.customThemeName.replace(/\.(ts|css)$/i, "");
|
|
3043
|
+
write(`themes/${baseName}.ts`, customThemeTsTemplate(baseName));
|
|
3044
|
+
write(`themes/${baseName}.css`, customThemeCssTemplate(baseName));
|
|
3045
|
+
}
|
|
3046
|
+
write("src/lib/docs.config.ts", svelteDocsConfigTemplate(cfg));
|
|
3047
|
+
write("src/lib/docs.server.ts", svelteDocsServerTemplate(cfg));
|
|
3048
|
+
write(`src/routes/${cfg.entry}/+layout.svelte`, svelteDocsLayoutTemplate(cfg));
|
|
3049
|
+
write(`src/routes/${cfg.entry}/+layout.server.js`, svelteDocsLayoutServerTemplate(cfg));
|
|
3050
|
+
write(`src/routes/${cfg.entry}/[...slug]/+page.svelte`, svelteDocsPageTemplate(cfg));
|
|
3051
|
+
if (cfg.i18n?.locales.length) write(`src/routes/${cfg.entry}/+page.svelte`, svelteDocsPageTemplate(cfg));
|
|
3052
|
+
if (!readFileSafe(path.join(cwd, "src/routes/+layout.svelte"))) write("src/routes/+layout.svelte", svelteRootLayoutTemplate(globalCssRelPath));
|
|
3053
|
+
const globalCssAbsPath = path.join(cwd, globalCssRelPath);
|
|
3054
|
+
const existingGlobalCss = readFileSafe(globalCssAbsPath);
|
|
3055
|
+
const cssTheme = {
|
|
3056
|
+
fumadocs: "fumadocs",
|
|
3057
|
+
darksharp: "darksharp",
|
|
3058
|
+
"pixel-border": "pixel-border",
|
|
3059
|
+
colorful: "colorful",
|
|
3060
|
+
darkbold: "darkbold",
|
|
3061
|
+
shiny: "shiny",
|
|
3062
|
+
greentree: "greentree",
|
|
3063
|
+
default: "fumadocs"
|
|
3064
|
+
}[cfg.theme] || "fumadocs";
|
|
3065
|
+
if (existingGlobalCss) {
|
|
3066
|
+
const injected = cfg.theme === "custom" && cfg.customThemeName ? injectSvelteCssImport(existingGlobalCss, "custom", cfg.customThemeName, globalCssRelPath) : injectSvelteCssImport(existingGlobalCss, cssTheme);
|
|
3067
|
+
if (injected) {
|
|
3068
|
+
writeFileSafe(globalCssAbsPath, injected, true);
|
|
3069
|
+
written.push(globalCssRelPath + " (updated)");
|
|
3070
|
+
} else skipped.push(globalCssRelPath + " (already configured)");
|
|
3071
|
+
} else write(globalCssRelPath, cfg.theme === "custom" && cfg.customThemeName ? svelteGlobalCssTemplate("custom", cfg.customThemeName, globalCssRelPath) : svelteGlobalCssTemplate(cssTheme));
|
|
3072
|
+
for (const base of getScaffoldContentRoots(cfg)) {
|
|
3073
|
+
write(`${base}/page.md`, svelteWelcomePageTemplate(cfg));
|
|
3074
|
+
write(`${base}/installation/page.md`, svelteInstallationPageTemplate(cfg));
|
|
3075
|
+
write(`${base}/quickstart/page.md`, svelteQuickstartPageTemplate(cfg));
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
function scaffoldAstro(cwd, cfg, globalCssRelPath, write, skipped, written) {
|
|
3079
|
+
if (cfg.theme === "custom" && cfg.customThemeName) {
|
|
3080
|
+
const baseName = cfg.customThemeName.replace(/\.(ts|css)$/i, "");
|
|
3081
|
+
write(`themes/${baseName}.ts`, customThemeTsTemplate(baseName));
|
|
3082
|
+
write(`themes/${baseName}.css`, customThemeCssTemplate(baseName));
|
|
3083
|
+
}
|
|
3084
|
+
write("src/lib/docs.config.ts", astroDocsConfigTemplate(cfg));
|
|
3085
|
+
write("src/lib/docs.server.ts", astroDocsServerTemplate(cfg));
|
|
3086
|
+
if (!fileExists(path.join(cwd, "astro.config.mjs")) && !fileExists(path.join(cwd, "astro.config.ts"))) write("astro.config.mjs", astroConfigTemplate(cfg.astroAdapter ?? "vercel"));
|
|
3087
|
+
write(`src/pages/${cfg.entry}/index.astro`, astroDocsIndexTemplate(cfg));
|
|
3088
|
+
write(`src/pages/${cfg.entry}/[...slug].astro`, astroDocsPageTemplate(cfg));
|
|
3089
|
+
write(`src/pages/api/${cfg.entry}.ts`, astroApiRouteTemplate(cfg));
|
|
3090
|
+
const globalCssAbsPath = path.join(cwd, globalCssRelPath);
|
|
3091
|
+
const existingGlobalCss = readFileSafe(globalCssAbsPath);
|
|
3092
|
+
const cssTheme = {
|
|
3093
|
+
fumadocs: "fumadocs",
|
|
3094
|
+
darksharp: "darksharp",
|
|
3095
|
+
"pixel-border": "pixel-border",
|
|
3096
|
+
colorful: "colorful",
|
|
3097
|
+
darkbold: "darkbold",
|
|
3098
|
+
shiny: "shiny",
|
|
3099
|
+
greentree: "greentree",
|
|
3100
|
+
default: "fumadocs"
|
|
3101
|
+
}[cfg.theme] || "fumadocs";
|
|
3102
|
+
if (existingGlobalCss) {
|
|
3103
|
+
const injected = cfg.theme === "custom" && cfg.customThemeName ? injectAstroCssImport(existingGlobalCss, "custom", cfg.customThemeName, globalCssRelPath) : injectAstroCssImport(existingGlobalCss, cssTheme);
|
|
3104
|
+
if (injected) {
|
|
3105
|
+
writeFileSafe(globalCssAbsPath, injected, true);
|
|
3106
|
+
written.push(globalCssRelPath + " (updated)");
|
|
3107
|
+
} else skipped.push(globalCssRelPath + " (already configured)");
|
|
3108
|
+
} else write(globalCssRelPath, cfg.theme === "custom" && cfg.customThemeName ? astroGlobalCssTemplate("custom", cfg.customThemeName, globalCssRelPath) : astroGlobalCssTemplate(cssTheme));
|
|
3109
|
+
for (const base of getScaffoldContentRoots(cfg)) {
|
|
3110
|
+
write(`${base}/page.md`, astroWelcomePageTemplate(cfg));
|
|
3111
|
+
write(`${base}/installation/page.md`, astroInstallationPageTemplate(cfg));
|
|
3112
|
+
write(`${base}/quickstart/page.md`, astroQuickstartPageTemplate(cfg));
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
function scaffoldNuxt(cwd, cfg, globalCssRelPath, write, skipped, written) {
|
|
3116
|
+
if (cfg.theme === "custom" && cfg.customThemeName) {
|
|
3117
|
+
const baseName = cfg.customThemeName.replace(/\.(ts|css)$/i, "");
|
|
3118
|
+
write(`themes/${baseName}.ts`, customThemeTsTemplate(baseName));
|
|
3119
|
+
write(`themes/${baseName}.css`, customThemeCssTemplate(baseName));
|
|
3120
|
+
}
|
|
3121
|
+
write("docs.config.ts", nuxtDocsConfigTemplate(cfg));
|
|
3122
|
+
write("server/utils/docs-server.ts", nuxtDocsServerTemplate(cfg));
|
|
3123
|
+
write("server/api/docs.get.ts", nuxtServerApiDocsGetTemplate());
|
|
3124
|
+
write("server/api/docs.post.ts", nuxtServerApiDocsPostTemplate());
|
|
3125
|
+
write("server/api/docs/load.get.ts", nuxtServerApiDocsLoadTemplate());
|
|
3126
|
+
write(`pages/${cfg.entry}/[[...slug]].vue`, nuxtDocsPageTemplate(cfg));
|
|
3127
|
+
path.join(cwd, "nuxt.config.ts");
|
|
3128
|
+
if (!fileExists(path.join(cwd, "nuxt.config.ts")) && !fileExists(path.join(cwd, "nuxt.config.js"))) write("nuxt.config.ts", nuxtConfigTemplate(cfg));
|
|
3129
|
+
const cssTheme = {
|
|
3130
|
+
fumadocs: "fumadocs",
|
|
3131
|
+
darksharp: "darksharp",
|
|
3132
|
+
"pixel-border": "pixel-border",
|
|
3133
|
+
colorful: "colorful",
|
|
3134
|
+
darkbold: "darkbold",
|
|
3135
|
+
shiny: "shiny",
|
|
3136
|
+
greentree: "greentree",
|
|
3137
|
+
default: "fumadocs"
|
|
3138
|
+
}[cfg.theme] || "fumadocs";
|
|
3139
|
+
const globalCssAbsPath = path.join(cwd, globalCssRelPath);
|
|
3140
|
+
const existingGlobalCss = readFileSafe(globalCssAbsPath);
|
|
3141
|
+
if (existingGlobalCss) {
|
|
3142
|
+
const injected = cfg.theme === "custom" && cfg.customThemeName ? injectNuxtCssImport(existingGlobalCss, "custom", cfg.customThemeName, globalCssRelPath) : injectNuxtCssImport(existingGlobalCss, cssTheme);
|
|
3143
|
+
if (injected) {
|
|
3144
|
+
writeFileSafe(globalCssAbsPath, injected, true);
|
|
3145
|
+
written.push(globalCssRelPath + " (updated)");
|
|
3146
|
+
} else skipped.push(globalCssRelPath + " (already configured)");
|
|
3147
|
+
} else write(globalCssRelPath, cfg.theme === "custom" && cfg.customThemeName ? nuxtGlobalCssTemplate("custom", cfg.customThemeName, globalCssRelPath) : nuxtGlobalCssTemplate(cssTheme));
|
|
3148
|
+
for (const base of getScaffoldContentRoots(cfg)) {
|
|
3149
|
+
write(`${base}/page.md`, nuxtWelcomePageTemplate(cfg));
|
|
3150
|
+
write(`${base}/installation/page.md`, nuxtInstallationPageTemplate(cfg));
|
|
3151
|
+
write(`${base}/quickstart/page.md`, nuxtQuickstartPageTemplate(cfg));
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
//#endregion
|
|
3156
|
+
//#region src/cli/upgrade.ts
|
|
3157
|
+
/**
|
|
3158
|
+
* Upgrade @farming-labs/* packages to latest.
|
|
3159
|
+
* Detects framework from package.json by default, or use --framework (next, nuxt, sveltekit, astro).
|
|
3160
|
+
*/
|
|
3161
|
+
const PRESETS = [
|
|
3162
|
+
"next",
|
|
3163
|
+
"nuxt",
|
|
3164
|
+
"sveltekit",
|
|
3165
|
+
"astro"
|
|
3166
|
+
];
|
|
3167
|
+
const PACKAGES_BY_FRAMEWORK = {
|
|
3168
|
+
nextjs: [
|
|
3169
|
+
"@farming-labs/docs",
|
|
3170
|
+
"@farming-labs/theme",
|
|
3171
|
+
"@farming-labs/next"
|
|
3172
|
+
],
|
|
3173
|
+
nuxt: [
|
|
3174
|
+
"@farming-labs/docs",
|
|
3175
|
+
"@farming-labs/nuxt",
|
|
3176
|
+
"@farming-labs/nuxt-theme"
|
|
3177
|
+
],
|
|
3178
|
+
sveltekit: [
|
|
3179
|
+
"@farming-labs/docs",
|
|
3180
|
+
"@farming-labs/svelte",
|
|
3181
|
+
"@farming-labs/svelte-theme"
|
|
3182
|
+
],
|
|
3183
|
+
astro: [
|
|
3184
|
+
"@farming-labs/docs",
|
|
3185
|
+
"@farming-labs/astro",
|
|
3186
|
+
"@farming-labs/astro-theme"
|
|
3187
|
+
]
|
|
3188
|
+
};
|
|
3189
|
+
function presetFromFramework(fw) {
|
|
3190
|
+
return fw === "nextjs" ? "next" : fw;
|
|
3191
|
+
}
|
|
3192
|
+
function frameworkFromPreset(preset) {
|
|
3193
|
+
return preset === "next" ? "nextjs" : preset;
|
|
3194
|
+
}
|
|
3195
|
+
/** Return package list for a framework (for testing and CLI). */
|
|
3196
|
+
function getPackagesForFramework(framework) {
|
|
3197
|
+
return PACKAGES_BY_FRAMEWORK[framework];
|
|
3198
|
+
}
|
|
3199
|
+
/** Build the install command for upgrade (for testing). */
|
|
3200
|
+
function buildUpgradeCommand(framework, tag, pm) {
|
|
3201
|
+
const packagesWithTag = PACKAGES_BY_FRAMEWORK[framework].map((name) => `${name}@${tag}`);
|
|
3202
|
+
return `${installCommand(pm)} ${packagesWithTag.join(" ")}`;
|
|
3203
|
+
}
|
|
3204
|
+
async function upgrade(options = {}) {
|
|
3205
|
+
const cwd = process.cwd();
|
|
3206
|
+
const tag = options.tag ?? "latest";
|
|
3207
|
+
p.intro(pc.bgCyan(pc.black(" @farming-labs/docs upgrade ")));
|
|
3208
|
+
if (!fileExists(path.join(cwd, "package.json"))) {
|
|
3209
|
+
p.log.error("No package.json found in the current directory. Run this from your project root.");
|
|
3210
|
+
process.exit(1);
|
|
3211
|
+
}
|
|
3212
|
+
let framework = null;
|
|
3213
|
+
let preset;
|
|
3214
|
+
if (options.framework) {
|
|
3215
|
+
const raw = options.framework.toLowerCase().trim();
|
|
3216
|
+
const normalized = raw === "nextjs" ? "next" : raw;
|
|
3217
|
+
if (!PRESETS.includes(normalized)) {
|
|
3218
|
+
p.log.error(`Invalid framework ${pc.cyan(options.framework)}. Use one of: ${PRESETS.map((t) => pc.cyan(t)).join(", ")}`);
|
|
3219
|
+
process.exit(1);
|
|
3220
|
+
}
|
|
3221
|
+
preset = normalized;
|
|
3222
|
+
framework = frameworkFromPreset(preset);
|
|
3223
|
+
} else {
|
|
3224
|
+
const detected = detectFramework(cwd);
|
|
3225
|
+
if (!detected) {
|
|
3226
|
+
p.log.error("Could not detect a supported framework (Next.js, Nuxt, SvelteKit, Astro). Use " + pc.cyan("--framework <next|nuxt|sveltekit|astro>") + " to specify.");
|
|
3227
|
+
process.exit(1);
|
|
3228
|
+
}
|
|
3229
|
+
if (detected === "tanstack-start") {
|
|
3230
|
+
p.log.error("TanStack Start is supported by " + pc.cyan("init") + " but not by " + pc.cyan("upgrade") + " yet. Upgrade the docs packages manually for now.");
|
|
3231
|
+
process.exit(1);
|
|
3232
|
+
}
|
|
3233
|
+
framework = detected;
|
|
3234
|
+
preset = presetFromFramework(framework);
|
|
3235
|
+
}
|
|
3236
|
+
let pm = detectPackageManagerFromLockfile(cwd);
|
|
3237
|
+
if (pm) p.log.info(`Detected ${pc.cyan(pm)} from lockfile`);
|
|
3238
|
+
else {
|
|
3239
|
+
const pmAnswer = await p.select({
|
|
3240
|
+
message: "Which package manager do you want to use for this upgrade?",
|
|
3241
|
+
options: [
|
|
3242
|
+
{
|
|
3243
|
+
value: "pnpm",
|
|
3244
|
+
label: "pnpm",
|
|
3245
|
+
hint: "Use pnpm add"
|
|
3246
|
+
},
|
|
3247
|
+
{
|
|
3248
|
+
value: "npm",
|
|
3249
|
+
label: "npm",
|
|
3250
|
+
hint: "Use npm add"
|
|
3251
|
+
},
|
|
3252
|
+
{
|
|
3253
|
+
value: "yarn",
|
|
3254
|
+
label: "yarn",
|
|
3255
|
+
hint: "Use yarn add"
|
|
3256
|
+
},
|
|
3257
|
+
{
|
|
3258
|
+
value: "bun",
|
|
3259
|
+
label: "bun",
|
|
3260
|
+
hint: "Use bun add"
|
|
3261
|
+
}
|
|
3262
|
+
]
|
|
3263
|
+
});
|
|
3264
|
+
if (p.isCancel(pmAnswer)) {
|
|
3265
|
+
p.outro(pc.red("Upgrade cancelled."));
|
|
3266
|
+
process.exit(0);
|
|
3267
|
+
}
|
|
3268
|
+
pm = pmAnswer;
|
|
3269
|
+
p.log.info(`Using ${pc.cyan(pm)} as package manager`);
|
|
3270
|
+
}
|
|
3271
|
+
const cmd = buildUpgradeCommand(framework, tag, pm);
|
|
3272
|
+
const packages = getPackagesForFramework(framework);
|
|
3273
|
+
p.log.step(`Upgrading ${preset} docs packages to ${tag}...`);
|
|
3274
|
+
p.log.message(pc.dim(packages.join(", ")));
|
|
3275
|
+
try {
|
|
3276
|
+
exec(cmd, cwd);
|
|
3277
|
+
p.log.success(`Packages upgraded to ${tag}.`);
|
|
3278
|
+
p.outro(pc.green("Done. Run your dev server to confirm everything works."));
|
|
3279
|
+
} catch {
|
|
3280
|
+
p.log.error("Upgrade failed. Try running manually:\n " + pc.cyan(cmd));
|
|
3281
|
+
process.exit(1);
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
//#endregion
|
|
3286
|
+
//#region src/cli/index.ts
|
|
3287
|
+
const args = process.argv.slice(2);
|
|
3288
|
+
const command = args[0];
|
|
3289
|
+
/** Parse flags like --template next, --name my-docs, --theme darksharp, --entry docs, --framework astro (exported for tests). */
|
|
3290
|
+
function parseFlags(argv) {
|
|
3291
|
+
const flags = {};
|
|
3292
|
+
for (let i = 0; i < argv.length; i++) {
|
|
3293
|
+
const arg = argv[i];
|
|
3294
|
+
if (arg.startsWith("--") && arg.includes("=")) {
|
|
3295
|
+
const [key, value] = arg.slice(2).split("=");
|
|
3296
|
+
flags[key] = value;
|
|
3297
|
+
} else if (arg.startsWith("--") && argv[i + 1] && !argv[i + 1].startsWith("--")) {
|
|
3298
|
+
flags[arg.slice(2)] = argv[i + 1];
|
|
3299
|
+
i++;
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
return flags;
|
|
3303
|
+
}
|
|
3304
|
+
async function main() {
|
|
3305
|
+
const flags = parseFlags(args);
|
|
3306
|
+
const initOptions = {
|
|
3307
|
+
template: flags.template,
|
|
3308
|
+
name: flags.name,
|
|
3309
|
+
theme: flags.theme,
|
|
3310
|
+
entry: flags.entry
|
|
3311
|
+
};
|
|
3312
|
+
if (!command || command === "init") await init(initOptions);
|
|
3313
|
+
else if (command === "upgrade") await upgrade({
|
|
3314
|
+
framework: flags.framework ?? (args[1] && !args[1].startsWith("--") ? args[1] : void 0),
|
|
3315
|
+
tag: args.includes("--beta") ? "beta" : "latest"
|
|
3316
|
+
});
|
|
3317
|
+
else if (command === "--help" || command === "-h") printHelp();
|
|
3318
|
+
else if (command === "--version" || command === "-v") printVersion();
|
|
3319
|
+
else {
|
|
3320
|
+
console.error(pc.red(`Unknown command: ${command}`));
|
|
3321
|
+
console.error();
|
|
3322
|
+
printHelp();
|
|
3323
|
+
process.exit(1);
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
function printHelp() {
|
|
3327
|
+
console.log(`
|
|
3328
|
+
${pc.bold("@farming-labs/docs")} — Documentation framework CLI
|
|
3329
|
+
|
|
3330
|
+
${pc.dim("Usage:")}
|
|
3331
|
+
npx @farming-labs/docs@latest ${pc.cyan("<command>")}
|
|
3332
|
+
|
|
3333
|
+
${pc.dim("Commands:")}
|
|
3334
|
+
${pc.cyan("init")} Scaffold docs in your project (default)
|
|
3335
|
+
${pc.cyan("upgrade")} Upgrade @farming-labs/* packages to latest (auto-detect or use --framework)
|
|
3336
|
+
|
|
3337
|
+
${pc.dim("Supported frameworks:")}
|
|
3338
|
+
Next.js, SvelteKit, Astro, Nuxt
|
|
3339
|
+
|
|
3340
|
+
${pc.dim("Options for init:")}
|
|
3341
|
+
${pc.cyan("--template <name>")} Bootstrap a project (${pc.dim("next")}, ${pc.dim("nuxt")}, ${pc.dim("sveltekit")}, ${pc.dim("astro")}, ${pc.dim("tanstack-start")}); use with ${pc.cyan("--name")}
|
|
3342
|
+
${pc.cyan("--name <project>")} Project folder name when using ${pc.cyan("--template")}; prompt if omitted (e.g. ${pc.dim("my-docs")})
|
|
3343
|
+
${pc.cyan("--theme <name>")} Skip theme prompt (e.g. ${pc.dim("darksharp")}, ${pc.dim("greentree")})
|
|
3344
|
+
${pc.cyan("--entry <path>")} Skip entry path prompt (e.g. ${pc.dim("docs")})
|
|
3345
|
+
|
|
3346
|
+
${pc.dim("Options for upgrade:")}
|
|
3347
|
+
${pc.cyan("--framework <name>")} Explicit framework (${pc.dim("next")}, ${pc.dim("nuxt")}, ${pc.dim("sveltekit")}, ${pc.dim("astro")}); omit to auto-detect
|
|
3348
|
+
${pc.cyan("--latest")} Install latest stable (default)
|
|
3349
|
+
${pc.cyan("--beta")} Install beta versions
|
|
3350
|
+
|
|
3351
|
+
${pc.cyan("-h, --help")} Show this help message
|
|
3352
|
+
${pc.cyan("-v, --version")} Show version
|
|
3353
|
+
`);
|
|
3354
|
+
}
|
|
3355
|
+
function printVersion() {
|
|
3356
|
+
console.log("0.1.0");
|
|
3357
|
+
}
|
|
3358
|
+
main().catch((err) => {
|
|
3359
|
+
console.error(pc.red("An unexpected error occurred:"));
|
|
3360
|
+
console.error(err);
|
|
3361
|
+
process.exit(1);
|
|
3362
|
+
});
|
|
3363
|
+
|
|
3364
|
+
//#endregion
|
|
3365
|
+
export { parseFlags };
|