@farming-labs/docs 0.0.36 → 0.0.38

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.
Files changed (2) hide show
  1. package/dist/cli/index.mjs +617 -64
  2. package/package.json +1 -1
@@ -15,6 +15,7 @@ function detectFramework(cwd) {
15
15
  ...pkg.devDependencies
16
16
  };
17
17
  if (allDeps["next"]) return "nextjs";
18
+ if (allDeps["@tanstack/react-start"]) return "tanstack-start";
18
19
  if (allDeps["@sveltejs/kit"]) return "sveltekit";
19
20
  if (allDeps["astro"]) return "astro";
20
21
  if (allDeps["nuxt"]) return "nuxt";
@@ -65,6 +66,7 @@ const GLOBAL_CSS_CANDIDATES = [
65
66
  "src/app/globals.css",
66
67
  "src/app/global.css",
67
68
  "src/app.css",
69
+ "src/styles/app.css",
68
70
  "styles/globals.css",
69
71
  "styles/global.css",
70
72
  "src/styles/globals.css",
@@ -226,6 +228,46 @@ const THEME_INFO = {
226
228
  function getThemeInfo(theme) {
227
229
  return THEME_INFO[theme] ?? THEME_INFO.fumadocs;
228
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
+ }
229
271
  function renderI18nConfig(cfg, indent = " ") {
230
272
  const i18n = cfg.i18n;
231
273
  if (!i18n || i18n.locales.length === 0) return "";
@@ -737,6 +779,414 @@ pnpm build
737
779
  Deploy to Vercel, Netlify, or any Node.js hosting platform.
738
780
  `;
739
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
+ }
740
1190
  function svelteDocsConfigTemplate(cfg) {
741
1191
  if (cfg.theme === "custom" && cfg.customThemeName) {
742
1192
  const exportName = getThemeExportName(cfg.customThemeName);
@@ -1763,7 +2213,8 @@ const VALID_TEMPLATES = [
1763
2213
  "next",
1764
2214
  "nuxt",
1765
2215
  "sveltekit",
1766
- "astro"
2216
+ "astro",
2217
+ "tanstack-start"
1767
2218
  ];
1768
2219
  const COMMON_LOCALE_OPTIONS = [
1769
2220
  {
@@ -1838,6 +2289,12 @@ function normalizeLocaleCode(value) {
1838
2289
  function parseLocaleInput(input) {
1839
2290
  return Array.from(new Set(input.split(",").map((value) => normalizeLocaleCode(value)).filter(Boolean)));
1840
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
+ }
1841
2298
  async function init(options = {}) {
1842
2299
  const cwd = process.cwd();
1843
2300
  p.intro(pc.bgCyan(pc.black(" @farming-labs/docs ")));
@@ -1852,7 +2309,7 @@ async function init(options = {}) {
1852
2309
  }, {
1853
2310
  value: "fresh",
1854
2311
  label: "Fresh project",
1855
- hint: "Bootstrap a new app from a template (Next, Nuxt, SvelteKit, Astro)"
2312
+ hint: "Bootstrap a new app from a template (Next, Nuxt, SvelteKit, Astro, TanStack Start)"
1856
2313
  }]
1857
2314
  });
1858
2315
  if (p.isCancel(projectTypeAnswer)) {
@@ -1892,6 +2349,11 @@ async function init(options = {}) {
1892
2349
  value: "astro",
1893
2350
  label: "Astro",
1894
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"
1895
2357
  }
1896
2358
  ]
1897
2359
  });
@@ -1920,7 +2382,7 @@ async function init(options = {}) {
1920
2382
  }
1921
2383
  projectName = nameAnswer.trim() || defaultProjectName;
1922
2384
  }
1923
- const templateLabel = template === "next" ? "Next.js" : template === "nuxt" ? "Nuxt" : template === "sveltekit" ? "SvelteKit" : "Astro";
2385
+ const templateLabel = template === "next" ? "Next.js" : template === "nuxt" ? "Nuxt" : template === "sveltekit" ? "SvelteKit" : template === "astro" ? "Astro" : "TanStack Start";
1924
2386
  const targetDir = path.join(cwd, projectName);
1925
2387
  const fs = await import("node:fs");
1926
2388
  if (fs.existsSync(targetDir)) {
@@ -1979,7 +2441,7 @@ async function init(options = {}) {
1979
2441
  }
1980
2442
  let framework = detectFramework(cwd);
1981
2443
  if (framework) {
1982
- const frameworkName = framework === "nextjs" ? "Next.js" : framework === "sveltekit" ? "SvelteKit" : framework === "astro" ? "Astro" : "Nuxt";
2444
+ const frameworkName = framework === "nextjs" ? "Next.js" : framework === "tanstack-start" ? "TanStack Start" : framework === "sveltekit" ? "SvelteKit" : framework === "astro" ? "Astro" : "Nuxt";
1983
2445
  p.log.success(`Detected framework: ${pc.cyan(frameworkName)}`);
1984
2446
  } else {
1985
2447
  p.log.warn("Could not auto-detect a framework from " + pc.cyan("package.json") + ".");
@@ -1991,6 +2453,11 @@ async function init(options = {}) {
1991
2453
  label: "Next.js",
1992
2454
  hint: "React framework with App Router"
1993
2455
  },
2456
+ {
2457
+ value: "tanstack-start",
2458
+ label: "TanStack Start",
2459
+ hint: "React with TanStack Router and server functions"
2460
+ },
1994
2461
  {
1995
2462
  value: "sveltekit",
1996
2463
  label: "SvelteKit",
@@ -2101,7 +2568,7 @@ async function init(options = {}) {
2101
2568
  }
2102
2569
  customThemeName = nameAnswer.trim().replace(/\.(ts|css)$/i, "") || defaultThemeName;
2103
2570
  }
2104
- const aliasHint = framework === "nextjs" ? `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)`;
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)`;
2105
2572
  const useAlias = await p.confirm({
2106
2573
  message: `Use path aliases for imports? ${pc.dim(aliasHint)}`,
2107
2574
  initialValue: false
@@ -2157,67 +2624,71 @@ async function init(options = {}) {
2157
2624
  process.exit(0);
2158
2625
  }
2159
2626
  const entryPath = entry.trim() || defaultEntry;
2160
- const enableI18n = await p.confirm({
2161
- message: "Do you want to scaffold internationalized docs ?",
2162
- initialValue: false
2163
- });
2164
- if (p.isCancel(enableI18n)) {
2165
- p.outro(pc.red("Init cancelled."));
2166
- process.exit(0);
2167
- }
2168
2627
  let docsI18n;
2169
- if (enableI18n) {
2170
- const selectedLocales = await p.multiselect({
2171
- message: "Which languages should we scaffold?",
2172
- options: COMMON_LOCALE_OPTIONS.map((option) => ({
2173
- value: option.value,
2174
- label: option.label,
2175
- hint: option.hint
2176
- }))
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
2177
2633
  });
2178
- if (p.isCancel(selectedLocales)) {
2634
+ if (p.isCancel(enableI18n)) {
2179
2635
  p.outro(pc.red("Init cancelled."));
2180
2636
  process.exit(0);
2181
2637
  }
2182
- const extraLocalesAnswer = await p.text({
2183
- message: "Any additional locale codes? (comma-separated, optional)",
2184
- placeholder: "nl, sv, pt-BR",
2185
- defaultValue: "",
2186
- validate: (value) => {
2187
- 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";
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);
2188
2651
  }
2189
- });
2190
- if (p.isCancel(extraLocalesAnswer)) {
2191
- p.outro(pc.red("Init cancelled."));
2192
- process.exit(0);
2193
- }
2194
- const locales = Array.from(new Set([...(selectedLocales ?? []).map((locale) => normalizeLocaleCode(locale)), ...parseLocaleInput(extraLocalesAnswer ?? "")])).filter(Boolean);
2195
- if (locales.length === 0) {
2196
- p.log.error("Pick at least one locale to scaffold i18n support.");
2197
- p.outro(pc.red("Init cancelled."));
2198
- process.exit(1);
2199
- }
2200
- const defaultLocaleAnswer = await p.select({
2201
- message: "Which locale should be the default?",
2202
- options: locales.map((locale) => ({
2203
- value: locale,
2204
- label: locale,
2205
- hint: locale === "en" ? "Recommended default" : void 0
2206
- })),
2207
- initialValue: locales[0]
2208
- });
2209
- if (p.isCancel(defaultLocaleAnswer)) {
2210
- p.outro(pc.red("Init cancelled."));
2211
- process.exit(0);
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
+ };
2212
2687
  }
2213
- docsI18n = {
2214
- locales,
2215
- defaultLocale: defaultLocaleAnswer
2216
- };
2217
2688
  }
2218
2689
  const detectedCssFiles = detectGlobalCssFiles(cwd);
2219
2690
  let globalCssRelPath;
2220
- const defaultCssPath = 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";
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";
2221
2692
  if (detectedCssFiles.length === 1) {
2222
2693
  globalCssRelPath = detectedCssFiles[0];
2223
2694
  p.log.info(`Found global CSS at ${pc.cyan(globalCssRelPath)}`);
@@ -2272,7 +2743,8 @@ async function init(options = {}) {
2272
2743
  if (writeFileSafe(path.join(cwd, rel), content, overwrite)) written.push(rel);
2273
2744
  else skipped.push(rel);
2274
2745
  }
2275
- if (framework === "sveltekit") scaffoldSvelteKit(cwd, cfg, globalCssRelPath, write, skipped, written);
2746
+ if (framework === "tanstack-start") scaffoldTanstackStart(cwd, cfg, globalCssRelPath, write, skipped, written);
2747
+ else if (framework === "sveltekit") scaffoldSvelteKit(cwd, cfg, globalCssRelPath, write, skipped, written);
2276
2748
  else if (framework === "astro") scaffoldAstro(cwd, cfg, globalCssRelPath, write, skipped, written);
2277
2749
  else if (framework === "nuxt") scaffoldNuxt(cwd, cfg, globalCssRelPath, write, skipped, written);
2278
2750
  else scaffoldNextJs(cwd, cfg, globalCssRelPath, write, skipped, written);
@@ -2316,7 +2788,17 @@ async function init(options = {}) {
2316
2788
  const s2 = p.spinner();
2317
2789
  s2.start("Installing dependencies");
2318
2790
  try {
2319
- if (framework === "sveltekit") exec(`${installCommand(pm)} @farming-labs/docs @farming-labs/svelte @farming-labs/svelte-theme`, cwd);
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);
2320
2802
  else if (framework === "astro") {
2321
2803
  const adapterPkg = getAstroAdapterPkg(cfg.astroAdapter ?? "vercel");
2322
2804
  exec(`${installCommand(pm)} @farming-labs/docs @farming-labs/astro @farming-labs/astro-theme ${adapterPkg}`, cwd);
@@ -2356,7 +2838,11 @@ async function init(options = {}) {
2356
2838
  process.exit(0);
2357
2839
  }
2358
2840
  p.log.step("Starting dev server...");
2359
- const devCommand = framework === "sveltekit" ? {
2841
+ const devCommand = framework === "tanstack-start" ? {
2842
+ cmd: "npx",
2843
+ args: ["vite", "dev"],
2844
+ waitFor: "ready"
2845
+ } : framework === "sveltekit" ? {
2360
2846
  cmd: "npx",
2361
2847
  args: ["vite", "dev"],
2362
2848
  waitFor: "ready"
@@ -2377,7 +2863,7 @@ async function init(options = {}) {
2377
2863
  ],
2378
2864
  waitFor: "Ready"
2379
2865
  };
2380
- const defaultPort = framework === "sveltekit" ? "5173" : framework === "astro" ? "4321" : framework === "nuxt" ? "3000" : "3000";
2866
+ const defaultPort = framework === "tanstack-start" ? "5173" : framework === "sveltekit" ? "5173" : framework === "astro" ? "4321" : framework === "nuxt" ? "3000" : "3000";
2381
2867
  try {
2382
2868
  const child = await spawnAndWaitFor(devCommand.cmd, devCommand.args, cwd, devCommand.waitFor, 6e4);
2383
2869
  const url = `http://localhost:${defaultPort}/${entryPath}`;
@@ -2396,7 +2882,7 @@ async function init(options = {}) {
2396
2882
  });
2397
2883
  });
2398
2884
  } catch (err) {
2399
- const manualCmd = framework === "sveltekit" ? "npx vite dev" : framework === "astro" ? "npx astro dev" : framework === "nuxt" ? "npx nuxt dev" : "pnpm dev";
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";
2400
2886
  p.log.error(`Could not start dev server. Try running manually:
2401
2887
  ${pc.cyan(manualCmd)}`);
2402
2888
  p.outro(pc.yellow("Setup complete. Start the server manually."));
@@ -2489,6 +2975,68 @@ function scaffoldNextJs(cwd, cfg, globalCssRelPath, write, skipped, written) {
2489
2975
  write(`${appDir}/${cfg.entry}/installation/page.mdx`, installationPageTemplate(cfg));
2490
2976
  write(`${appDir}/${cfg.entry}/quickstart/page.mdx`, quickstartPageTemplate(cfg));
2491
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
+ }
2492
3040
  function scaffoldSvelteKit(cwd, cfg, globalCssRelPath, write, skipped, written) {
2493
3041
  if (cfg.theme === "custom" && cfg.customThemeName) {
2494
3042
  const baseName = cfg.customThemeName.replace(/\.(ts|css)$/i, "");
@@ -2673,11 +3221,16 @@ async function upgrade(options = {}) {
2673
3221
  preset = normalized;
2674
3222
  framework = frameworkFromPreset(preset);
2675
3223
  } else {
2676
- framework = detectFramework(cwd);
2677
- if (!framework) {
3224
+ const detected = detectFramework(cwd);
3225
+ if (!detected) {
2678
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.");
2679
3227
  process.exit(1);
2680
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;
2681
3234
  preset = presetFromFramework(framework);
2682
3235
  }
2683
3236
  let pm = detectPackageManagerFromLockfile(cwd);
@@ -2785,7 +3338,7 @@ ${pc.dim("Supported frameworks:")}
2785
3338
  Next.js, SvelteKit, Astro, Nuxt
2786
3339
 
2787
3340
  ${pc.dim("Options for init:")}
2788
- ${pc.cyan("--template <name>")} Bootstrap a project (${pc.dim("next")}, ${pc.dim("nuxt")}, ${pc.dim("sveltekit")}, ${pc.dim("astro")}); use with ${pc.cyan("--name")}
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")}
2789
3342
  ${pc.cyan("--name <project>")} Project folder name when using ${pc.cyan("--template")}; prompt if omitted (e.g. ${pc.dim("my-docs")})
2790
3343
  ${pc.cyan("--theme <name>")} Skip theme prompt (e.g. ${pc.dim("darksharp")}, ${pc.dim("greentree")})
2791
3344
  ${pc.cyan("--entry <path>")} Skip entry path prompt (e.g. ${pc.dim("docs")})
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/docs",
3
- "version": "0.0.36",
3
+ "version": "0.0.38",
4
4
  "description": "Modern, flexible MDX-based docs framework — core types, config, and CLI",
5
5
  "keywords": [
6
6
  "docs",