@farming-labs/docs 0.0.37 → 0.0.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  //#region src/cli/index.d.ts
2
2
  /** Parse flags like --template next, --name my-docs, --theme darksharp, --entry docs, --framework astro (exported for tests). */
3
- declare function parseFlags(argv: string[]): Record<string, string | undefined>;
3
+ declare function parseFlags(argv: string[]): Record<string, string | boolean | undefined>;
4
4
  //#endregion
5
5
  export { parseFlags };
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import pc from "picocolors";
2
+ import fs from "node:fs";
3
3
  import path from "node:path";
4
+ import pc from "picocolors";
4
5
  import * as p from "@clack/prompts";
5
- import fs from "node:fs";
6
6
  import { execSync, spawn } from "node:child_process";
7
7
 
8
8
  //#region src/cli/utils.ts
@@ -273,6 +273,11 @@ function renderI18nConfig(cfg, indent = " ") {
273
273
  if (!i18n || i18n.locales.length === 0) return "";
274
274
  return `${indent}i18n: {\n${indent} locales: [${i18n.locales.map((locale) => `"${locale}"`).join(", ")}],\n${indent} defaultLocale: "${i18n.defaultLocale}",\n${indent}},\n`;
275
275
  }
276
+ function renderApiReferenceConfig(cfg, indent = " ") {
277
+ const apiReference = cfg.apiReference;
278
+ if (!apiReference) return "";
279
+ return `${indent}apiReference: {\n${indent} enabled: true,\n${indent} path: "${apiReference.path}",\n${indent} routeRoot: "${apiReference.routeRoot}",\n${indent}},\n`;
280
+ }
276
281
  function toLocaleImportName(locale) {
277
282
  return `LocalePage_${locale.replace(/[^a-zA-Z0-9_$]/g, "_")}`;
278
283
  }
@@ -329,6 +334,10 @@ function nextDocsLayoutConfigImport(useAlias, nextAppDir = "app") {
329
334
  if (useAlias) return "@/docs.config";
330
335
  return nextAppDir === "src/app" ? "../../../docs.config" : "../../docs.config";
331
336
  }
337
+ function nextApiReferenceConfigImport(useAlias, nextAppDir = "app", filePath) {
338
+ if (useAlias) return "@/docs.config";
339
+ return relativeImport(filePath, "docs.config.ts");
340
+ }
332
341
  /** Config import for SvelteKit src/lib/docs.server.ts → src/lib/docs.config */
333
342
  function svelteServerConfigImport(useAlias) {
334
343
  return useAlias ? "$lib/docs.config" : "./docs.config";
@@ -365,7 +374,7 @@ import { ${exportName} } from "${cfg.useAlias ? "@/themes/" + cfg.customThemeNam
365
374
 
366
375
  export default defineDocs({
367
376
  entry: "${cfg.entry}",
368
- ${renderI18nConfig(cfg)} theme: ${exportName}({
377
+ ${renderI18nConfig(cfg)}${renderApiReferenceConfig(cfg)} theme: ${exportName}({
369
378
  ui: {
370
379
  colors: { primary: "#6366f1" },
371
380
  },
@@ -385,7 +394,7 @@ import { ${t.factory} } from "${t.nextImport}";
385
394
 
386
395
  export default defineDocs({
387
396
  entry: "${cfg.entry}",
388
- ${renderI18nConfig(cfg)} theme: ${t.factory}({
397
+ ${renderI18nConfig(cfg)}${renderApiReferenceConfig(cfg)} theme: ${t.factory}({
389
398
  ui: {
390
399
  colors: { primary: "#6366f1" },
391
400
  },
@@ -523,6 +532,17 @@ export default function Layout({ children }: { children: React.ReactNode }) {
523
532
  }
524
533
  `;
525
534
  }
535
+ function nextApiReferenceRouteTemplate(cfg, filePath) {
536
+ const appDir = cfg.nextAppDir ?? "app";
537
+ return `
538
+ import docsConfig from "${nextApiReferenceConfigImport(cfg.useAlias, appDir, filePath)}";
539
+ import { createNextApiReference } from "@farming-labs/next/api-reference";
540
+
541
+ export const GET = createNextApiReference(docsConfig);
542
+
543
+ export const revalidate = false;
544
+ `;
545
+ }
526
546
  function nextLocaleDocPageTemplate(defaultLocale) {
527
547
  return `\
528
548
  import type { ComponentType } from "react";
@@ -789,7 +809,7 @@ import { ${exportName} } from "./themes/${cfg.customThemeName.replace(/\.ts$/i,
789
809
  export default defineDocs({
790
810
  entry: "${cfg.entry}",
791
811
  contentDir: "${cfg.entry}",
792
- theme: ${exportName}({
812
+ ${renderApiReferenceConfig(cfg)} theme: ${exportName}({
793
813
  ui: {
794
814
  colors: { primary: "#6366f1" },
795
815
  },
@@ -817,7 +837,7 @@ import { ${t.factory} } from "${t.nextImport}";
817
837
  export default defineDocs({
818
838
  entry: "${cfg.entry}",
819
839
  contentDir: "${cfg.entry}",
820
- theme: ${t.factory}({
840
+ ${renderApiReferenceConfig(cfg)} theme: ${t.factory}({
821
841
  ui: {
822
842
  colors: { primary: "#6366f1" },
823
843
  },
@@ -948,6 +968,24 @@ export const Route = createFileRoute("/api/docs")({
948
968
  });
949
969
  `;
950
970
  }
971
+ function tanstackApiReferenceRouteTemplate(opts) {
972
+ const routePath = `/${opts.apiReferencePath}${opts.catchAll ? "/$" : "/"}`;
973
+ return `\
974
+ import { createFileRoute } from "@tanstack/react-router";
975
+ import { createTanstackApiReference } from "@farming-labs/tanstack-start/api-reference";
976
+ import docsConfig from "${tanstackDocsConfigImport(opts.filePath)}";
977
+
978
+ const handler = createTanstackApiReference(docsConfig);
979
+
980
+ export const Route = createFileRoute("${routePath}")({
981
+ server: {
982
+ handlers: {
983
+ GET: handler,
984
+ },
985
+ },
986
+ });
987
+ `;
988
+ }
951
989
  function tanstackRootRouteTemplate(globalCssRelPath) {
952
990
  return `\
953
991
  import appCss from "${relativeAssetPath("src/routes/__root.tsx", globalCssRelPath)}?url";
@@ -1197,7 +1235,7 @@ import { ${exportName} } from "${"../../themes/" + cfg.customThemeName.replace(/
1197
1235
  export default defineDocs({
1198
1236
  entry: "${cfg.entry}",
1199
1237
  contentDir: "${cfg.entry}",
1200
- ${renderI18nConfig(cfg)} theme: ${exportName}({
1238
+ ${renderI18nConfig(cfg)}${renderApiReferenceConfig(cfg)} theme: ${exportName}({
1201
1239
  ui: {
1202
1240
  colors: { primary: "#6366f1" },
1203
1241
  },
@@ -1225,7 +1263,7 @@ import { ${t.factory} } from "${t.svelteImport}";
1225
1263
  export default defineDocs({
1226
1264
  entry: "${cfg.entry}",
1227
1265
  contentDir: "${cfg.entry}",
1228
- ${renderI18nConfig(cfg)} theme: ${t.factory}({
1266
+ ${renderI18nConfig(cfg)}${renderApiReferenceConfig(cfg)} theme: ${t.factory}({
1229
1267
  ui: {
1230
1268
  colors: { primary: "#6366f1" },
1231
1269
  },
@@ -1294,6 +1332,14 @@ function svelteDocsPageTemplate(cfg) {
1294
1332
  <DocsContent {data} {config} />
1295
1333
  `;
1296
1334
  }
1335
+ function svelteApiReferenceRouteTemplate(filePath, useAlias) {
1336
+ return `\
1337
+ import { createSvelteApiReference } from "@farming-labs/svelte/api-reference";
1338
+ import config from "${useAlias ? "$lib/docs.config" : relativeImport(filePath, "src/lib/docs.config.ts")}";
1339
+
1340
+ export const GET = createSvelteApiReference(config);
1341
+ `;
1342
+ }
1297
1343
  function svelteRootLayoutTemplate(globalCssRelPath) {
1298
1344
  let cssImport;
1299
1345
  if (globalCssRelPath.startsWith("src/")) cssImport = "./" + globalCssRelPath.slice(4);
@@ -1507,7 +1553,7 @@ import { ${exportName} } from "${"../../themes/" + cfg.customThemeName.replace(/
1507
1553
  export default defineDocs({
1508
1554
  entry: "${cfg.entry}",
1509
1555
  contentDir: "${cfg.entry}",
1510
- ${renderI18nConfig(cfg)} theme: ${exportName}({
1556
+ ${renderI18nConfig(cfg)}${renderApiReferenceConfig(cfg)} theme: ${exportName}({
1511
1557
  ui: {
1512
1558
  colors: { primary: "#6366f1" },
1513
1559
  },
@@ -1535,7 +1581,7 @@ import { ${t.factory} } from "${t.astroImport}";
1535
1581
  export default defineDocs({
1536
1582
  entry: "${cfg.entry}",
1537
1583
  contentDir: "${cfg.entry}",
1538
- ${renderI18nConfig(cfg)} theme: ${t.factory}({
1584
+ ${renderI18nConfig(cfg)}${renderApiReferenceConfig(cfg)} theme: ${t.factory}({
1539
1585
  ui: {
1540
1586
  colors: { primary: "#6366f1" },
1541
1587
  },
@@ -1676,6 +1722,14 @@ export const POST: APIRoute = async ({ request }) => {
1676
1722
  };
1677
1723
  `;
1678
1724
  }
1725
+ function astroApiReferenceRouteTemplate(filePath) {
1726
+ return `\
1727
+ import { createAstroApiReference } from "@farming-labs/astro/api-reference";
1728
+ import config from "${relativeImport(filePath, "src/lib/docs.config.ts")}";
1729
+
1730
+ export const GET = createAstroApiReference(config);
1731
+ `;
1732
+ }
1679
1733
  function astroGlobalCssTemplate(theme, customThemeName, globalCssRelPath) {
1680
1734
  if (theme === "custom" && customThemeName && globalCssRelPath) return `\
1681
1735
  @import "${getCustomThemeCssImportPath(globalCssRelPath, customThemeName.replace(/\.css$/i, ""))}";
@@ -1863,7 +1917,7 @@ import { ${exportName} } from "${cfg.useAlias ? "~/themes/" + cfg.customThemeNam
1863
1917
  export default defineDocs({
1864
1918
  entry: "${cfg.entry}",
1865
1919
  contentDir: "${cfg.entry}",
1866
- ${renderI18nConfig(cfg)} theme: ${exportName}({
1920
+ ${renderI18nConfig(cfg)}${renderApiReferenceConfig(cfg)} theme: ${exportName}({
1867
1921
  ui: {
1868
1922
  colors: { primary: "#6366f1" },
1869
1923
  },
@@ -1891,7 +1945,7 @@ import { ${t.factory} } from "${t.nuxtImport}";
1891
1945
  export default defineDocs({
1892
1946
  entry: "${cfg.entry}",
1893
1947
  contentDir: "${cfg.entry}",
1894
- ${renderI18nConfig(cfg)} theme: ${t.factory}({
1948
+ ${renderI18nConfig(cfg)}${renderApiReferenceConfig(cfg)} theme: ${t.factory}({
1895
1949
  ui: {
1896
1950
  colors: { primary: "#6366f1" },
1897
1951
  },
@@ -1973,6 +2027,14 @@ export default defineEventHandler(async (event) => {
1973
2027
  });
1974
2028
  `;
1975
2029
  }
2030
+ function nuxtServerApiReferenceRouteTemplate(filePath, useAlias) {
2031
+ return `\
2032
+ import { defineApiReferenceHandler } from "@farming-labs/nuxt/api-reference";
2033
+ import config from "${useAlias ? "~/docs.config" : relativeImport(filePath, "docs.config.ts")}";
2034
+
2035
+ export default defineApiReferenceHandler(config);
2036
+ `;
2037
+ }
1976
2038
  function nuxtDocsPageTemplate(cfg) {
1977
2039
  return `\
1978
2040
  <script setup lang="ts">
@@ -2295,6 +2357,55 @@ function normalizeEntryPath(entry) {
2295
2357
  function getTanstackDocsRouteDir(entry) {
2296
2358
  return path.posix.join("src/routes", normalizeEntryPath(entry));
2297
2359
  }
2360
+ function normalizeApiRouteRoot(routeRoot) {
2361
+ return routeRoot.replace(/^\/+|\/+$/g, "");
2362
+ }
2363
+ function detectApiRouteRoot(cwd, framework, nextAppDir = "app") {
2364
+ const defaultRoot = "api";
2365
+ const detectFromRecursiveRouteFiles = (baseDir, matcher) => {
2366
+ if (!fs.existsSync(baseDir)) return null;
2367
+ const candidates = /* @__PURE__ */ new Map();
2368
+ const walk = (dir, prefix = "") => {
2369
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
2370
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
2371
+ const fullPath = path.join(dir, entry.name);
2372
+ if (entry.isDirectory()) {
2373
+ walk(fullPath, relativePath);
2374
+ continue;
2375
+ }
2376
+ if (!matcher(entry, relativePath)) continue;
2377
+ const [topLevel] = relativePath.split("/");
2378
+ if (!topLevel) continue;
2379
+ candidates.set(topLevel, (candidates.get(topLevel) ?? 0) + 1);
2380
+ }
2381
+ };
2382
+ walk(baseDir);
2383
+ return Array.from(candidates.entries()).sort((a, b) => b[1] - a[1])[0]?.[0] ?? null;
2384
+ };
2385
+ if (framework === "nextjs") {
2386
+ const appRoot = path.join(cwd, nextAppDir);
2387
+ if (fs.existsSync(path.join(appRoot, defaultRoot))) return defaultRoot;
2388
+ return detectFromRecursiveRouteFiles(appRoot, (_entry, relativePath) => /\/?route\.(?:[cm]?[jt]sx?)$/i.test(relativePath)) ?? defaultRoot;
2389
+ }
2390
+ if (framework === "tanstack-start") {
2391
+ const routesRoot = path.join(cwd, "src/routes");
2392
+ if (fs.existsSync(path.join(routesRoot, defaultRoot))) return defaultRoot;
2393
+ return detectFromRecursiveRouteFiles(routesRoot, (_entry, relativePath) => /(?:^|\/)[^/]+\.(?:[cm]?[jt]sx?)$/i.test(relativePath)) ?? defaultRoot;
2394
+ }
2395
+ if (framework === "sveltekit") {
2396
+ const routesRoot = path.join(cwd, "src/routes");
2397
+ if (fs.existsSync(path.join(routesRoot, defaultRoot))) return defaultRoot;
2398
+ return detectFromRecursiveRouteFiles(routesRoot, (_entry, relativePath) => /\/?\+server\.(?:[cm]?[jt]s)$/i.test(relativePath)) ?? defaultRoot;
2399
+ }
2400
+ if (framework === "astro") {
2401
+ const pagesRoot = path.join(cwd, "src/pages");
2402
+ if (fs.existsSync(path.join(pagesRoot, defaultRoot))) return defaultRoot;
2403
+ return detectFromRecursiveRouteFiles(pagesRoot, (entry, relativePath) => /\.(?:[cm]?[jt]s)$/i.test(relativePath) && !relativePath.endsWith(".d.ts") && entry.isFile()) ?? defaultRoot;
2404
+ }
2405
+ const serverRoot = path.join(cwd, "server");
2406
+ if (fs.existsSync(path.join(serverRoot, defaultRoot))) return defaultRoot;
2407
+ return detectFromRecursiveRouteFiles(serverRoot, (entry, relativePath) => /\.(?:[cm]?[jt]s)$/i.test(relativePath) && !relativePath.endsWith(".d.ts") && entry.isFile()) ?? defaultRoot;
2408
+ }
2298
2409
  async function init(options = {}) {
2299
2410
  const cwd = process.cwd();
2300
2411
  p.intro(pc.bgCyan(pc.black(" @farming-labs/docs ")));
@@ -2499,54 +2610,66 @@ async function init(options = {}) {
2499
2610
  nextAppDir = useSrcApp ? "src/app" : "app";
2500
2611
  }
2501
2612
  }
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);
2613
+ const themeOptions = [
2614
+ {
2615
+ value: "fumadocs",
2616
+ label: "Fumadocs (Default)",
2617
+ hint: "Clean, modern docs theme with sidebar, search, and dark mode"
2618
+ },
2619
+ {
2620
+ value: "darksharp",
2621
+ label: "Darksharp",
2622
+ hint: "All-black, sharp edges, zero-radius look"
2623
+ },
2624
+ {
2625
+ value: "pixel-border",
2626
+ label: "Pixel Border",
2627
+ hint: "Rounded borders, pixel-perfect spacing, refined sidebar"
2628
+ },
2629
+ {
2630
+ value: "colorful",
2631
+ label: "Colorful",
2632
+ hint: "Fumadocs-style neutral theme with description support"
2633
+ },
2634
+ {
2635
+ value: "darkbold",
2636
+ label: "DarkBold",
2637
+ hint: "Pure monochrome, Geist typography, clean minimalism"
2638
+ },
2639
+ {
2640
+ value: "shiny",
2641
+ label: "Shiny",
2642
+ hint: "Glossy, modern look with subtle shimmer effects"
2643
+ },
2644
+ {
2645
+ value: "greentree",
2646
+ label: "GreenTree",
2647
+ hint: "Emerald green accent, Inter font, Mintlify-inspired"
2648
+ },
2649
+ {
2650
+ value: "custom",
2651
+ label: "Create your own theme",
2652
+ hint: "Scaffold a new theme file + CSS in themes/ (name asked next)"
2653
+ }
2654
+ ];
2655
+ const themeValues = new Set(themeOptions.map((option) => option.value));
2656
+ let theme;
2657
+ if (options.theme) {
2658
+ if (!themeValues.has(options.theme)) {
2659
+ p.log.error(`Invalid ${pc.cyan("--theme")}. Use one of: ${themeOptions.map((option) => pc.cyan(option.value)).join(", ")}`);
2660
+ process.exit(1);
2661
+ }
2662
+ theme = options.theme;
2663
+ } else {
2664
+ const themeAnswer = await p.select({
2665
+ message: "Which theme would you like to use?",
2666
+ options: themeOptions
2667
+ });
2668
+ if (p.isCancel(themeAnswer)) {
2669
+ p.outro(pc.red("Init cancelled."));
2670
+ process.exit(0);
2671
+ }
2672
+ theme = themeAnswer;
2550
2673
  }
2551
2674
  const defaultThemeName = "my-theme";
2552
2675
  let customThemeName;
@@ -2609,21 +2732,84 @@ async function init(options = {}) {
2609
2732
  astroAdapter = adapter;
2610
2733
  }
2611
2734
  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";
2735
+ let entryPath;
2736
+ if (options.entry) {
2737
+ const normalizedEntry = options.entry.trim();
2738
+ if (normalizedEntry.startsWith("/")) {
2739
+ p.log.error("Use a relative path for --entry (no leading /)");
2740
+ process.exit(1);
2620
2741
  }
2621
- });
2622
- if (p.isCancel(entry)) {
2623
- p.outro(pc.red("Init cancelled."));
2624
- process.exit(0);
2742
+ if (normalizedEntry.includes(" ")) {
2743
+ p.log.error("Path passed to --entry cannot contain spaces");
2744
+ process.exit(1);
2745
+ }
2746
+ entryPath = normalizedEntry || defaultEntry;
2747
+ } else {
2748
+ const entry = await p.text({
2749
+ message: "Where should your docs live?",
2750
+ placeholder: defaultEntry,
2751
+ defaultValue: defaultEntry,
2752
+ validate: (value) => {
2753
+ const v = (value ?? "").trim();
2754
+ if (v.startsWith("/")) return "Use a relative path (no leading /)";
2755
+ if (v.includes(" ")) return "Path cannot contain spaces";
2756
+ }
2757
+ });
2758
+ if (p.isCancel(entry)) {
2759
+ p.outro(pc.red("Init cancelled."));
2760
+ process.exit(0);
2761
+ }
2762
+ entryPath = entry.trim() || defaultEntry;
2763
+ }
2764
+ const defaultApiRouteRoot = normalizeApiRouteRoot(options.apiRouteRoot?.trim() || detectApiRouteRoot(cwd, framework, nextAppDir));
2765
+ let apiReferenceConfig;
2766
+ let enableApiReference;
2767
+ if (typeof options.apiReference === "boolean") enableApiReference = options.apiReference;
2768
+ else if (typeof options.apiRouteRoot === "string" && options.apiRouteRoot.trim()) enableApiReference = true;
2769
+ else {
2770
+ const apiReferenceAnswer = await p.confirm({
2771
+ message: "Do you want to scaffold API reference support?",
2772
+ initialValue: false
2773
+ });
2774
+ if (p.isCancel(apiReferenceAnswer)) {
2775
+ p.outro(pc.red("Init cancelled."));
2776
+ process.exit(0);
2777
+ }
2778
+ enableApiReference = apiReferenceAnswer;
2779
+ }
2780
+ if (enableApiReference) {
2781
+ let routeRoot = options.apiRouteRoot?.trim();
2782
+ if (!routeRoot) {
2783
+ const routeRootAnswer = await p.text({
2784
+ message: "API route root to scan?",
2785
+ placeholder: defaultApiRouteRoot,
2786
+ defaultValue: defaultApiRouteRoot,
2787
+ validate: (value) => {
2788
+ const normalizedValue = normalizeApiRouteRoot((value ?? "").trim());
2789
+ if (!normalizedValue) return "Route root cannot be empty";
2790
+ if (normalizedValue.includes(" ")) return "Route root cannot contain spaces";
2791
+ }
2792
+ });
2793
+ if (p.isCancel(routeRootAnswer)) {
2794
+ p.outro(pc.red("Init cancelled."));
2795
+ process.exit(0);
2796
+ }
2797
+ routeRoot = routeRootAnswer.trim() || defaultApiRouteRoot;
2798
+ }
2799
+ const normalizedRouteRoot = normalizeApiRouteRoot(routeRoot);
2800
+ if (!normalizedRouteRoot) {
2801
+ p.log.error("Route root cannot be empty");
2802
+ process.exit(1);
2803
+ }
2804
+ if (normalizedRouteRoot.includes(" ")) {
2805
+ p.log.error("Route root cannot contain spaces");
2806
+ process.exit(1);
2807
+ }
2808
+ apiReferenceConfig = {
2809
+ path: "api-reference",
2810
+ routeRoot: normalizedRouteRoot
2811
+ };
2625
2812
  }
2626
- const entryPath = entry.trim() || defaultEntry;
2627
2813
  let docsI18n;
2628
2814
  if (framework === "tanstack-start") p.log.info("Skipping i18n scaffold for TanStack Start. Configure localized routes manually if needed.");
2629
2815
  else {
@@ -2733,6 +2919,7 @@ async function init(options = {}) {
2733
2919
  useAlias,
2734
2920
  astroAdapter,
2735
2921
  i18n: docsI18n,
2922
+ apiReference: apiReferenceConfig,
2736
2923
  ...framework === "nextjs" && { nextAppDir }
2737
2924
  };
2738
2925
  const s = p.spinner();
@@ -2929,6 +3116,10 @@ function scaffoldNextJs(cwd, cfg, globalCssRelPath, write, skipped, written) {
2929
3116
  } else skipped.push(globalCssRelPath + " (already configured)");
2930
3117
  } else write(globalCssRelPath, globalCssTemplate(cfg.theme, cfg.customThemeName, globalCssRelPath));
2931
3118
  write(`${appDir}/${cfg.entry}/layout.tsx`, docsLayoutTemplate(cfg));
3119
+ if (cfg.apiReference) {
3120
+ const apiReferenceRoute = `${appDir}/${cfg.apiReference.path}/[[...slug]]/route.ts`;
3121
+ write(apiReferenceRoute, nextApiReferenceRouteTemplate(cfg, apiReferenceRoute));
3122
+ }
2932
3123
  write("postcss.config.mjs", postcssConfigTemplate());
2933
3124
  if (!fileExists(path.join(cwd, "tsconfig.json"))) write("tsconfig.json", tsconfigTemplate(cfg.useAlias));
2934
3125
  if (cfg.i18n?.locales.length) {
@@ -3001,6 +3192,22 @@ function scaffoldTanstackStart(cwd, cfg, globalCssRelPath, write, skipped, writt
3001
3192
  projectName: cfg.projectName
3002
3193
  }));
3003
3194
  write(apiRoute, tanstackApiDocsRouteTemplate(cfg.useAlias, apiRoute));
3195
+ if (cfg.apiReference) {
3196
+ const apiReferenceIndexRoute = `src/routes/${cfg.apiReference.path}.index.ts`;
3197
+ const apiReferenceCatchAllRoute = `src/routes/${cfg.apiReference.path}.$.ts`;
3198
+ write(apiReferenceIndexRoute, tanstackApiReferenceRouteTemplate({
3199
+ filePath: apiReferenceIndexRoute,
3200
+ useAlias: cfg.useAlias,
3201
+ apiReferencePath: cfg.apiReference.path,
3202
+ catchAll: false
3203
+ }));
3204
+ write(apiReferenceCatchAllRoute, tanstackApiReferenceRouteTemplate({
3205
+ filePath: apiReferenceCatchAllRoute,
3206
+ useAlias: cfg.useAlias,
3207
+ apiReferencePath: cfg.apiReference.path,
3208
+ catchAll: true
3209
+ }));
3210
+ }
3004
3211
  const rootRoutePath = path.join(cwd, "src/routes/__root.tsx");
3005
3212
  const existingRootRoute = readFileSafe(rootRoutePath);
3006
3213
  if (!existingRootRoute) write("src/routes/__root.tsx", tanstackRootRouteTemplate(globalCssRelPath), true);
@@ -3049,6 +3256,12 @@ function scaffoldSvelteKit(cwd, cfg, globalCssRelPath, write, skipped, written)
3049
3256
  write(`src/routes/${cfg.entry}/+layout.server.js`, svelteDocsLayoutServerTemplate(cfg));
3050
3257
  write(`src/routes/${cfg.entry}/[...slug]/+page.svelte`, svelteDocsPageTemplate(cfg));
3051
3258
  if (cfg.i18n?.locales.length) write(`src/routes/${cfg.entry}/+page.svelte`, svelteDocsPageTemplate(cfg));
3259
+ if (cfg.apiReference) {
3260
+ const apiReferenceIndexRoute = `src/routes/${cfg.apiReference.path}/+server.ts`;
3261
+ const apiReferenceCatchAllRoute = `src/routes/${cfg.apiReference.path}/[...slug]/+server.ts`;
3262
+ write(apiReferenceIndexRoute, svelteApiReferenceRouteTemplate(apiReferenceIndexRoute, cfg.useAlias));
3263
+ write(apiReferenceCatchAllRoute, svelteApiReferenceRouteTemplate(apiReferenceCatchAllRoute, cfg.useAlias));
3264
+ }
3052
3265
  if (!readFileSafe(path.join(cwd, "src/routes/+layout.svelte"))) write("src/routes/+layout.svelte", svelteRootLayoutTemplate(globalCssRelPath));
3053
3266
  const globalCssAbsPath = path.join(cwd, globalCssRelPath);
3054
3267
  const existingGlobalCss = readFileSafe(globalCssAbsPath);
@@ -3087,6 +3300,12 @@ function scaffoldAstro(cwd, cfg, globalCssRelPath, write, skipped, written) {
3087
3300
  write(`src/pages/${cfg.entry}/index.astro`, astroDocsIndexTemplate(cfg));
3088
3301
  write(`src/pages/${cfg.entry}/[...slug].astro`, astroDocsPageTemplate(cfg));
3089
3302
  write(`src/pages/api/${cfg.entry}.ts`, astroApiRouteTemplate(cfg));
3303
+ if (cfg.apiReference) {
3304
+ const apiReferenceIndexRoute = `src/pages/${cfg.apiReference.path}/index.ts`;
3305
+ const apiReferenceCatchAllRoute = `src/pages/${cfg.apiReference.path}/[...slug].ts`;
3306
+ write(apiReferenceIndexRoute, astroApiReferenceRouteTemplate(apiReferenceIndexRoute));
3307
+ write(apiReferenceCatchAllRoute, astroApiReferenceRouteTemplate(apiReferenceCatchAllRoute));
3308
+ }
3090
3309
  const globalCssAbsPath = path.join(cwd, globalCssRelPath);
3091
3310
  const existingGlobalCss = readFileSafe(globalCssAbsPath);
3092
3311
  const cssTheme = {
@@ -3124,6 +3343,12 @@ function scaffoldNuxt(cwd, cfg, globalCssRelPath, write, skipped, written) {
3124
3343
  write("server/api/docs.post.ts", nuxtServerApiDocsPostTemplate());
3125
3344
  write("server/api/docs/load.get.ts", nuxtServerApiDocsLoadTemplate());
3126
3345
  write(`pages/${cfg.entry}/[[...slug]].vue`, nuxtDocsPageTemplate(cfg));
3346
+ if (cfg.apiReference) {
3347
+ const apiReferenceIndexRoute = `server/routes/${cfg.apiReference.path}/index.ts`;
3348
+ const apiReferenceCatchAllRoute = `server/routes/${cfg.apiReference.path}/[...slug].ts`;
3349
+ write(apiReferenceIndexRoute, nuxtServerApiReferenceRouteTemplate(apiReferenceIndexRoute, cfg.useAlias));
3350
+ write(apiReferenceCatchAllRoute, nuxtServerApiReferenceRouteTemplate(apiReferenceCatchAllRoute, cfg.useAlias));
3351
+ }
3127
3352
  path.join(cwd, "nuxt.config.ts");
3128
3353
  if (!fileExists(path.join(cwd, "nuxt.config.ts")) && !fileExists(path.join(cwd, "nuxt.config.js"))) write("nuxt.config.ts", nuxtConfigTemplate(cfg));
3129
3354
  const cssTheme = {
@@ -3156,10 +3381,11 @@ function scaffoldNuxt(cwd, cfg, globalCssRelPath, write, skipped, written) {
3156
3381
  //#region src/cli/upgrade.ts
3157
3382
  /**
3158
3383
  * Upgrade @farming-labs/* packages to latest.
3159
- * Detects framework from package.json by default, or use --framework (next, nuxt, sveltekit, astro).
3384
+ * Detects framework from package.json by default, or use --framework (next, tanstack-start, nuxt, sveltekit, astro).
3160
3385
  */
3161
3386
  const PRESETS = [
3162
3387
  "next",
3388
+ "tanstack-start",
3163
3389
  "nuxt",
3164
3390
  "sveltekit",
3165
3391
  "astro"
@@ -3170,6 +3396,11 @@ const PACKAGES_BY_FRAMEWORK = {
3170
3396
  "@farming-labs/theme",
3171
3397
  "@farming-labs/next"
3172
3398
  ],
3399
+ "tanstack-start": [
3400
+ "@farming-labs/docs",
3401
+ "@farming-labs/theme",
3402
+ "@farming-labs/tanstack-start"
3403
+ ],
3173
3404
  nuxt: [
3174
3405
  "@farming-labs/docs",
3175
3406
  "@farming-labs/nuxt",
@@ -3223,11 +3454,7 @@ async function upgrade(options = {}) {
3223
3454
  } else {
3224
3455
  const detected = detectFramework(cwd);
3225
3456
  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.");
3457
+ p.log.error("Could not detect a supported framework (Next.js, TanStack Start, Nuxt, SvelteKit, Astro). Use " + pc.cyan("--framework <next|tanstack-start|nuxt|sveltekit|astro>") + " to specify.");
3231
3458
  process.exit(1);
3232
3459
  }
3233
3460
  framework = detected;
@@ -3289,29 +3516,36 @@ const command = args[0];
3289
3516
  /** Parse flags like --template next, --name my-docs, --theme darksharp, --entry docs, --framework astro (exported for tests). */
3290
3517
  function parseFlags(argv) {
3291
3518
  const flags = {};
3519
+ const booleanFlags = new Set(["api-reference"]);
3292
3520
  for (let i = 0; i < argv.length; i++) {
3293
3521
  const arg = argv[i];
3294
3522
  if (arg.startsWith("--") && arg.includes("=")) {
3295
3523
  const [key, value] = arg.slice(2).split("=");
3296
- flags[key] = value;
3524
+ if (key.startsWith("no-")) flags[key.slice(3)] = false;
3525
+ else if (booleanFlags.has(key) && value === "true") flags[key] = true;
3526
+ else if (booleanFlags.has(key) && value === "false") flags[key] = false;
3527
+ else flags[key] = value;
3297
3528
  } else if (arg.startsWith("--") && argv[i + 1] && !argv[i + 1].startsWith("--")) {
3298
3529
  flags[arg.slice(2)] = argv[i + 1];
3299
3530
  i++;
3300
- }
3531
+ } else if (arg.startsWith("--no-")) flags[arg.slice(5)] = false;
3532
+ else if (arg.startsWith("--") && booleanFlags.has(arg.slice(2))) flags[arg.slice(2)] = true;
3301
3533
  }
3302
3534
  return flags;
3303
3535
  }
3304
3536
  async function main() {
3305
3537
  const flags = parseFlags(args);
3306
3538
  const initOptions = {
3307
- template: flags.template,
3308
- name: flags.name,
3309
- theme: flags.theme,
3310
- entry: flags.entry
3539
+ template: typeof flags.template === "string" ? flags.template : void 0,
3540
+ name: typeof flags.name === "string" ? flags.name : void 0,
3541
+ theme: typeof flags.theme === "string" ? flags.theme : void 0,
3542
+ entry: typeof flags.entry === "string" ? flags.entry : void 0,
3543
+ apiReference: typeof flags["api-reference"] === "boolean" ? flags["api-reference"] : void 0,
3544
+ apiRouteRoot: typeof flags["api-route-root"] === "string" ? flags["api-route-root"] : void 0
3311
3545
  };
3312
3546
  if (!command || command === "init") await init(initOptions);
3313
3547
  else if (command === "upgrade") await upgrade({
3314
- framework: flags.framework ?? (args[1] && !args[1].startsWith("--") ? args[1] : void 0),
3548
+ framework: (typeof flags.framework === "string" ? flags.framework : void 0) ?? (args[1] && !args[1].startsWith("--") ? args[1] : void 0),
3315
3549
  tag: args.includes("--beta") ? "beta" : "latest"
3316
3550
  });
3317
3551
  else if (command === "--help" || command === "-h") printHelp();
@@ -3335,16 +3569,19 @@ ${pc.dim("Commands:")}
3335
3569
  ${pc.cyan("upgrade")} Upgrade @farming-labs/* packages to latest (auto-detect or use --framework)
3336
3570
 
3337
3571
  ${pc.dim("Supported frameworks:")}
3338
- Next.js, SvelteKit, Astro, Nuxt
3572
+ Next.js, TanStack Start, SvelteKit, Astro, Nuxt
3339
3573
 
3340
3574
  ${pc.dim("Options for init:")}
3341
3575
  ${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
3576
  ${pc.cyan("--name <project>")} Project folder name when using ${pc.cyan("--template")}; prompt if omitted (e.g. ${pc.dim("my-docs")})
3343
3577
  ${pc.cyan("--theme <name>")} Skip theme prompt (e.g. ${pc.dim("darksharp")}, ${pc.dim("greentree")})
3344
3578
  ${pc.cyan("--entry <path>")} Skip entry path prompt (e.g. ${pc.dim("docs")})
3579
+ ${pc.cyan("--api-reference")} Scaffold API reference support during ${pc.cyan("init")}
3580
+ ${pc.cyan("--no-api-reference")} Skip API reference scaffold during ${pc.cyan("init")}
3581
+ ${pc.cyan("--api-route-root <path>")} Override the API route root scanned by ${pc.cyan("apiReference.routeRoot")} (e.g. ${pc.dim("api")}, ${pc.dim("internal-api")})
3345
3582
 
3346
3583
  ${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
3584
+ ${pc.cyan("--framework <name>")} Explicit framework (${pc.dim("next")}, ${pc.dim("tanstack-start")}, ${pc.dim("nuxt")}, ${pc.dim("sveltekit")}, ${pc.dim("astro")}); omit to auto-detect
3348
3585
  ${pc.cyan("--latest")} Install latest stable (default)
3349
3586
  ${pc.cyan("--beta")} Install beta versions
3350
3587