@farming-labs/docs 0.1.58 → 0.1.60

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.
@@ -0,0 +1,1289 @@
1
+ import { K as rootLayoutTemplate, W as postcssConfigTemplate, g as docsLayoutTemplate, v as globalCssTemplate, yt as tsconfigTemplate } from "./templates-2M1vfnf-.mjs";
2
+ import { i as detectPackageManagerFromLockfile } from "./utils-DSMXVnEu.mjs";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import pc from "picocolors";
6
+ import { spawn } from "node:child_process";
7
+ import { fileURLToPath } from "node:url";
8
+ import { z } from "zod";
9
+ import os from "node:os";
10
+
11
+ //#region src/cli/dev-banner.ts
12
+ function createDevLogger() {
13
+ return {
14
+ info(msg) {
15
+ if (msg.includes("VITE") || msg.includes("vite") || msg.includes("ready in") || msg.includes("Local:") || msg.includes("Network:") || msg.includes("➜") || msg.includes("Port") || msg.includes("trying another")) return;
16
+ console.log(msg);
17
+ },
18
+ warn(msg) {
19
+ console.warn(pc.yellow(msg));
20
+ },
21
+ warnOnce(msg) {
22
+ console.warn(pc.yellow(msg));
23
+ },
24
+ error(msg) {
25
+ console.error(pc.red(msg));
26
+ },
27
+ clearScreen() {},
28
+ hasErrorLogged() {
29
+ return false;
30
+ },
31
+ hasWarned: false
32
+ };
33
+ }
34
+ function resolveLocalUrl(protocol, port, localUrl) {
35
+ const resolved = localUrl ?? `${protocol}://localhost:${port}/`;
36
+ return resolved.endsWith("/") ? resolved : `${resolved}/`;
37
+ }
38
+ function resolveNetworkUrl(options) {
39
+ if (!(options.host === true || options.host === "0.0.0.0" || typeof options.host === "string" && options.host !== "localhost" && options.host !== "127.0.0.1")) return null;
40
+ if (options.networkUrl) return options.networkUrl.endsWith("/") ? options.networkUrl : `${options.networkUrl}/`;
41
+ const interfaces = os.networkInterfaces();
42
+ for (const entries of Object.values(interfaces)) for (const iface of entries ?? []) if (iface.family === "IPv4" && !iface.internal) return `${options.protocol}://${iface.address}:${options.port}/`;
43
+ return "";
44
+ }
45
+ function printDevBanner({ name = "@farming-labs/docs", version = "v0.0.0", port, host, protocol = "http", startTime, localUrl, networkUrl }) {
46
+ const elapsed = Date.now() - startTime;
47
+ const resolvedLocalUrl = resolveLocalUrl(protocol, port, localUrl);
48
+ const resolvedNetworkUrl = resolveNetworkUrl({
49
+ protocol,
50
+ port,
51
+ host,
52
+ networkUrl
53
+ });
54
+ console.log("");
55
+ console.log(` ${pc.bold(pc.green(name))} ${pc.dim(version)} ${pc.dim(`ready in ${elapsed}ms`)}`);
56
+ console.log("");
57
+ console.log(` ${pc.dim("➜")} ${pc.bold("Local:")} ${pc.cyan(resolvedLocalUrl)}`);
58
+ if (resolvedNetworkUrl === null) console.log(` ${pc.dim("➜")} ${pc.bold("Network:")} ${pc.dim("use --host to expose")}`);
59
+ else if (resolvedNetworkUrl === "") console.log(` ${pc.dim("➜")} ${pc.bold("Network:")} ${pc.dim("no external interface found")}`);
60
+ else console.log(` ${pc.dim("➜")} ${pc.bold("Network:")} ${pc.cyan(resolvedNetworkUrl)}`);
61
+ console.log("");
62
+ }
63
+
64
+ //#endregion
65
+ //#region src/cli/dev.ts
66
+ const MANAGED_CONFIG_FILE = "docs.cloud.json";
67
+ const DEFAULT_RUNTIME_ROOT = ".docs/site";
68
+ const DEFAULT_DOCS_ROOT = "docs";
69
+ const DEFAULT_API_REFERENCE_ROOT = "api-reference";
70
+ const DEFAULT_MANAGED_OPENAPI_ENDPOINT = "/api/docs/openapi";
71
+ const DEFAULT_THEME_PRESET = "default";
72
+ const DEFAULT_POLL_INTERVAL_MS = 750;
73
+ const DEFAULT_NEXT_VERSION = "16.2.3";
74
+ const DEFAULT_REACT_VERSION = "^19.2.0";
75
+ const DEFAULT_TAILWIND_VERSION = "^4.1.18";
76
+ const DEFAULT_POSTCSS_VERSION = "^8.5.6";
77
+ const DEFAULT_TYPESCRIPT_VERSION = "^5.9.3";
78
+ const ANSI_ESCAPE_PATTERN = /\u001B\[[0-9;]*m/g;
79
+ const MANAGED_OPENAPI_CONVENTION_CANDIDATES = [
80
+ "api/openapi.json",
81
+ "api/openapi.yaml",
82
+ "api/openapi.yml",
83
+ "openapi.json",
84
+ "openapi.yaml",
85
+ "openapi.yml"
86
+ ];
87
+ const RUNTIME_FRAMEWORK_VALUES = [
88
+ "nextjs",
89
+ "tanstack-start",
90
+ "sveltekit",
91
+ "astro",
92
+ "nuxt"
93
+ ];
94
+ const THEME_PRESETS = {
95
+ default: {
96
+ configName: "default",
97
+ templateTheme: "fumadocs",
98
+ importPath: "@farming-labs/theme",
99
+ factory: "fumadocs"
100
+ },
101
+ fumadocs: {
102
+ configName: "fumadocs",
103
+ templateTheme: "fumadocs",
104
+ importPath: "@farming-labs/theme",
105
+ factory: "fumadocs"
106
+ },
107
+ darksharp: {
108
+ configName: "darksharp",
109
+ templateTheme: "darksharp",
110
+ importPath: "@farming-labs/theme/darksharp",
111
+ factory: "darksharp"
112
+ },
113
+ "pixel-border": {
114
+ configName: "pixel-border",
115
+ templateTheme: "pixel-border",
116
+ importPath: "@farming-labs/theme/pixel-border",
117
+ factory: "pixelBorder"
118
+ },
119
+ colorful: {
120
+ configName: "colorful",
121
+ templateTheme: "colorful",
122
+ importPath: "@farming-labs/theme/colorful",
123
+ factory: "colorful"
124
+ },
125
+ darkbold: {
126
+ configName: "darkbold",
127
+ templateTheme: "darkbold",
128
+ importPath: "@farming-labs/theme/darkbold",
129
+ factory: "darkbold"
130
+ },
131
+ shiny: {
132
+ configName: "shiny",
133
+ templateTheme: "shiny",
134
+ importPath: "@farming-labs/theme/shiny",
135
+ factory: "shiny"
136
+ },
137
+ greentree: {
138
+ configName: "greentree",
139
+ templateTheme: "greentree",
140
+ importPath: "@farming-labs/theme/greentree",
141
+ factory: "greentree"
142
+ },
143
+ concrete: {
144
+ configName: "concrete",
145
+ templateTheme: "concrete",
146
+ importPath: "@farming-labs/theme/concrete",
147
+ factory: "concrete"
148
+ },
149
+ "command-grid": {
150
+ configName: "command-grid",
151
+ templateTheme: "command-grid",
152
+ importPath: "@farming-labs/theme/command-grid",
153
+ factory: "commandGrid"
154
+ },
155
+ hardline: {
156
+ configName: "hardline",
157
+ templateTheme: "hardline",
158
+ importPath: "@farming-labs/theme/hardline",
159
+ factory: "hardline"
160
+ }
161
+ };
162
+ const managedConfigSchema = z.object({
163
+ docs: z.union([
164
+ z.object({
165
+ mode: z.literal("frameworkless"),
166
+ root: z.string().optional(),
167
+ runtime: z.enum(RUNTIME_FRAMEWORK_VALUES).optional()
168
+ }).passthrough(),
169
+ z.object({
170
+ mode: z.literal("framework"),
171
+ root: z.string().optional(),
172
+ runtime: z.enum(RUNTIME_FRAMEWORK_VALUES)
173
+ }).passthrough(),
174
+ z.object({
175
+ framework: z.literal("managed"),
176
+ root: z.string().optional(),
177
+ runtime: z.enum(RUNTIME_FRAMEWORK_VALUES).optional()
178
+ }).passthrough()
179
+ ]),
180
+ content: z.object({
181
+ docsRoot: z.string().optional(),
182
+ apiReferenceRoot: z.string().optional(),
183
+ openapi: z.array(z.object({
184
+ name: z.string().optional(),
185
+ path: z.string(),
186
+ route: z.string().optional(),
187
+ navigationLabel: z.string().optional()
188
+ }).passthrough()).optional()
189
+ }).passthrough().optional(),
190
+ site: z.object({
191
+ name: z.string().optional(),
192
+ title: z.string().optional(),
193
+ titleTemplate: z.string().optional(),
194
+ description: z.string().optional()
195
+ }).passthrough().optional(),
196
+ theme: z.object({ preset: z.string().optional() }).passthrough().optional(),
197
+ cloud: z.object({ enabled: z.boolean().optional() }).passthrough().optional()
198
+ }).passthrough();
199
+ function toPosixPath(value) {
200
+ return value.replace(/\\/g, "/");
201
+ }
202
+ function normalizePathKey(value) {
203
+ return path.resolve(value);
204
+ }
205
+ function normalizeManagedRoutePath(value) {
206
+ return value?.trim().replace(/^\/+|\/+$/g, "") || DEFAULT_API_REFERENCE_ROOT;
207
+ }
208
+ function isRemoteManagedSpecPath(value) {
209
+ return /^(?:https?:)?\/\//i.test(value);
210
+ }
211
+ function isRequestRelativeManagedSpecPath(value) {
212
+ return value.startsWith("/");
213
+ }
214
+ function resolveManagedOpenApiSpec(projectRoot, openapi) {
215
+ const configured = openapi?.find((entry) => typeof entry.path === "string" && entry.path.trim());
216
+ if (configured) {
217
+ const rawPath = configured.path.trim();
218
+ const route = normalizeManagedRoutePath(configured.route);
219
+ if (isRemoteManagedSpecPath(rawPath) || isRequestRelativeManagedSpecPath(rawPath)) return {
220
+ name: configured.name?.trim() || "API Reference",
221
+ route,
222
+ path: rawPath,
223
+ specUrl: rawPath,
224
+ navigationLabel: configured.navigationLabel?.trim() || void 0
225
+ };
226
+ return {
227
+ name: configured.name?.trim() || "API Reference",
228
+ route,
229
+ path: rawPath,
230
+ specUrl: DEFAULT_MANAGED_OPENAPI_ENDPOINT,
231
+ sourcePath: path.resolve(projectRoot, rawPath),
232
+ navigationLabel: configured.navigationLabel?.trim() || void 0
233
+ };
234
+ }
235
+ for (const candidate of MANAGED_OPENAPI_CONVENTION_CANDIDATES) {
236
+ const sourcePath = path.join(projectRoot, candidate);
237
+ if (!fs.existsSync(sourcePath) || !fs.statSync(sourcePath).isFile()) continue;
238
+ return {
239
+ name: "API Reference",
240
+ route: DEFAULT_API_REFERENCE_ROOT,
241
+ path: candidate,
242
+ specUrl: DEFAULT_MANAGED_OPENAPI_ENDPOINT,
243
+ sourcePath
244
+ };
245
+ }
246
+ }
247
+ function getDocsPackageVersion() {
248
+ const candidateUrls = [new URL("../package.json", import.meta.url), new URL("../../package.json", import.meta.url)];
249
+ for (const candidateUrl of candidateUrls) try {
250
+ const packageJsonPath = fileURLToPath(candidateUrl);
251
+ const raw = fs.readFileSync(packageJsonPath, "utf-8");
252
+ const parsed = JSON.parse(raw);
253
+ if (parsed.name === "@farming-labs/docs" && parsed.version) return parsed.version;
254
+ } catch {}
255
+ return "latest";
256
+ }
257
+ function resolveThemePreset(preset) {
258
+ if (!preset) return THEME_PRESETS[DEFAULT_THEME_PRESET];
259
+ return THEME_PRESETS[preset] ?? THEME_PRESETS[DEFAULT_THEME_PRESET];
260
+ }
261
+ function isMarkdownFile(filePath) {
262
+ return /\.(md|mdx)$/i.test(filePath);
263
+ }
264
+ function isPageSourceFile(filePath) {
265
+ if (!isMarkdownFile(filePath)) return false;
266
+ return path.basename(filePath).toLowerCase() !== "agent.md";
267
+ }
268
+ function formatZodError(error) {
269
+ return error.issues.map((issue) => {
270
+ return `${issue.path.length > 0 ? issue.path.join(".") : "root"}: ${issue.message}`;
271
+ }).join("; ");
272
+ }
273
+ function formatLogLabel(label) {
274
+ switch (label) {
275
+ case "docs": return pc.bold(pc.cyan(`[${label}]`));
276
+ case "ready":
277
+ case "local":
278
+ case "network": return pc.bold(pc.green(`[${label}]`));
279
+ case "warn": return pc.bold(pc.yellow(`[${label}]`));
280
+ case "error": return pc.bold(pc.red(`[${label}]`));
281
+ case "note":
282
+ case "next": return pc.bold(pc.dim(`[${label}]`));
283
+ case "PAGE": return pc.bold(pc.green(`[${label}]`));
284
+ default: return pc.bold(pc.blue(`[${label}]`));
285
+ }
286
+ }
287
+ function logLine(label, message) {
288
+ console.log(`${formatLogLabel(label)} ${message}`);
289
+ }
290
+ function logErrorLine(message) {
291
+ console.error(`${formatLogLabel("error")} ${message}`);
292
+ }
293
+ function stripAnsi(value) {
294
+ return value.replace(ANSI_ESCAPE_PATTERN, "");
295
+ }
296
+ function normalizeLogLine(value) {
297
+ return stripAnsi(value).replace(/\r/g, "").trim();
298
+ }
299
+ function normalizeVersionTag(value) {
300
+ return value.startsWith("v") ? value : `v${value}`;
301
+ }
302
+ function resolveRequestedHost(options) {
303
+ return options.hostname ?? options.host;
304
+ }
305
+ function resolveBannerPort(localUrl, fallbackPort) {
306
+ if (localUrl) try {
307
+ const parsed = new URL(localUrl);
308
+ if (parsed.port) return Number(parsed.port);
309
+ return parsed.protocol === "https:" ? 443 : 80;
310
+ } catch {}
311
+ if (fallbackPort) {
312
+ const numeric = Number(fallbackPort);
313
+ if (Number.isFinite(numeric) && numeric > 0) return numeric;
314
+ }
315
+ return 3e3;
316
+ }
317
+ function resolveBannerProtocol(localUrl) {
318
+ if (!localUrl) return "http";
319
+ try {
320
+ return new URL(localUrl).protocol === "https:" ? "https" : "http";
321
+ } catch {
322
+ return "http";
323
+ }
324
+ }
325
+ function createLineReader(onLine) {
326
+ let buffer = "";
327
+ return {
328
+ push(chunk) {
329
+ buffer += chunk.toString();
330
+ let newlineIndex = buffer.indexOf("\n");
331
+ while (newlineIndex !== -1) {
332
+ const line = buffer.slice(0, newlineIndex).replace(/\r$/, "");
333
+ buffer = buffer.slice(newlineIndex + 1);
334
+ onLine(line);
335
+ newlineIndex = buffer.indexOf("\n");
336
+ }
337
+ },
338
+ flush() {
339
+ const line = buffer.replace(/\r$/, "");
340
+ buffer = "";
341
+ if (line) onLine(line);
342
+ }
343
+ };
344
+ }
345
+ function parseNextDevLine(rawLine) {
346
+ const line = normalizeLogLine(rawLine);
347
+ if (!line) return null;
348
+ const routeMatch = line.match(/^\[docs-page\]\s+(\S+)$/);
349
+ if (routeMatch) return {
350
+ type: "page",
351
+ pathname: routeMatch[1]
352
+ };
353
+ const localMatch = line.match(/Local:\s*(https?:\/\/\S+)/i);
354
+ if (localMatch) return {
355
+ type: "local",
356
+ url: localMatch[1]
357
+ };
358
+ const networkMatch = line.match(/Network:\s*(https?:\/\/\S+)/i);
359
+ if (networkMatch) return {
360
+ type: "network",
361
+ url: networkMatch[1]
362
+ };
363
+ const readyMatch = line.match(/Ready in\s+(.+)$/i);
364
+ if (readyMatch) return {
365
+ type: "ready",
366
+ duration: readyMatch[1].trim()
367
+ };
368
+ if (/\bStarting\b/i.test(line)) return { type: "starting" };
369
+ const compilingMatch = line.match(/Compiling(?:\s+(.+?))?(?:\s*\.\.\.)?$/i);
370
+ if (compilingMatch) return {
371
+ type: "compiling",
372
+ target: compilingMatch[1]?.trim() || "app"
373
+ };
374
+ const compiledMatch = line.match(/Compiled(?:\s+(.+?))?\s+in\s+(.+)$/i);
375
+ if (compiledMatch) return {
376
+ type: "compiled",
377
+ target: compiledMatch[1]?.trim() || "app",
378
+ duration: compiledMatch[2].trim()
379
+ };
380
+ if (/^(?:warning\b|warn\b)/i.test(line) || /\bdeprecated\b/i.test(line)) return {
381
+ type: "warning",
382
+ message: line.replace(/^(?:warning\b|warn\b):?\s*/i, "").trim()
383
+ };
384
+ if (/\b(failed to compile|module not found|error:|type error|syntax error|uncaught)\b/i.test(line)) return {
385
+ type: "error",
386
+ message: line
387
+ };
388
+ return null;
389
+ }
390
+ function walkFiles(rootDir) {
391
+ if (!fs.existsSync(rootDir)) return [];
392
+ const files = [];
393
+ const stack = [rootDir];
394
+ while (stack.length > 0) {
395
+ const current = stack.pop();
396
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
397
+ const fullPath = path.join(current, entry.name);
398
+ if (entry.isDirectory()) stack.push(fullPath);
399
+ else if (entry.isFile()) files.push(fullPath);
400
+ }
401
+ }
402
+ return files.sort();
403
+ }
404
+ function directoryHasMarkdown(rootDir) {
405
+ return walkFiles(rootDir).some(isPageSourceFile);
406
+ }
407
+ function findLocalFrameworkRoot(startDir) {
408
+ let current = path.resolve(startDir);
409
+ while (true) {
410
+ const docsPkg = path.join(current, "packages", "docs", "package.json");
411
+ const nextPkg = path.join(current, "packages", "next", "package.json");
412
+ const themePkg = path.join(current, "packages", "fumadocs", "package.json");
413
+ if (fs.existsSync(docsPkg) && fs.existsSync(nextPkg) && fs.existsSync(themePkg)) return current;
414
+ const parent = path.dirname(current);
415
+ if (parent === current) return null;
416
+ current = parent;
417
+ }
418
+ }
419
+ function detectNearestPackageManager(startDir) {
420
+ let current = path.resolve(startDir);
421
+ while (true) {
422
+ const detected = detectPackageManagerFromLockfile(current);
423
+ if (detected) return detected;
424
+ const parent = path.dirname(current);
425
+ if (parent === current) return "npm";
426
+ current = parent;
427
+ }
428
+ }
429
+ function readManagedDocsProject(projectRoot) {
430
+ const configPath = path.join(projectRoot, MANAGED_CONFIG_FILE);
431
+ if (!fs.existsSync(configPath)) throw new Error(`Could not find ${MANAGED_CONFIG_FILE} in ${projectRoot}. Frameworkless dev expects ${MANAGED_CONFIG_FILE}, ${DEFAULT_DOCS_ROOT}/, and optionally ${DEFAULT_API_REFERENCE_ROOT}/.`);
432
+ let parsedJson;
433
+ try {
434
+ parsedJson = JSON.parse(fs.readFileSync(configPath, "utf-8"));
435
+ } catch (error) {
436
+ throw new Error(`Could not parse ${MANAGED_CONFIG_FILE}. The file must be valid JSON.${error instanceof Error ? ` ${error.message}` : ""}`);
437
+ }
438
+ const parsed = managedConfigSchema.safeParse(parsedJson);
439
+ if (!parsed.success) throw new Error(`Invalid ${MANAGED_CONFIG_FILE}: ${formatZodError(parsed.error)}`);
440
+ const docsConfig = parsed.data.docs;
441
+ if (("mode" in docsConfig ? docsConfig.mode : docsConfig.framework === "managed" ? "frameworkless" : "framework") !== "frameworkless") throw new Error(`${MANAGED_CONFIG_FILE} uses docs.mode = "framework". ${pc.cyan("docs dev")} only supports frameworkless projects right now.`);
442
+ const runtimeFramework = "runtime" in docsConfig && docsConfig.runtime ? docsConfig.runtime : "nextjs";
443
+ if (runtimeFramework !== "nextjs") throw new Error(`Frameworkless ${pc.cyan("docs dev")} currently supports only docs.runtime = "nextjs".`);
444
+ const docsRoot = parsed.data.content?.docsRoot ?? DEFAULT_DOCS_ROOT;
445
+ const apiReferenceRoot = parsed.data.content?.apiReferenceRoot ?? DEFAULT_API_REFERENCE_ROOT;
446
+ const runtimeDir = path.resolve(projectRoot, docsConfig.root ?? DEFAULT_RUNTIME_ROOT);
447
+ const docsSourceDir = path.resolve(projectRoot, docsRoot);
448
+ const apiReferenceSourceDir = path.resolve(projectRoot, apiReferenceRoot);
449
+ const apiReferenceSpec = resolveManagedOpenApiSpec(projectRoot, parsed.data.content?.openapi);
450
+ if (runtimeDir === docsSourceDir || runtimeDir.startsWith(`${docsSourceDir}${path.sep}`) || runtimeDir === apiReferenceSourceDir || runtimeDir.startsWith(`${apiReferenceSourceDir}${path.sep}`)) throw new Error(`docs.root must point outside ${docsRoot}/ and ${apiReferenceRoot}/ so the generated runtime does not overwrite authored content.`);
451
+ const siteName = parsed.data.site?.name ?? parsed.data.site?.title ?? path.basename(projectRoot) ?? "Docs";
452
+ const theme = resolveThemePreset(parsed.data.theme?.preset);
453
+ return {
454
+ configPath,
455
+ projectRoot,
456
+ runtimeDir,
457
+ runtimeFramework,
458
+ docsRoot,
459
+ apiReferenceRoot,
460
+ apiReferenceSpec,
461
+ siteName,
462
+ titleTemplate: parsed.data.site?.titleTemplate ?? `%s | ${siteName}`,
463
+ description: parsed.data.site?.description ?? `Documentation for ${siteName}.`,
464
+ theme
465
+ };
466
+ }
467
+ function resolveLocalPackageSpec(projectRoot, runtimeDir) {
468
+ const frameworkRoot = findLocalFrameworkRoot(projectRoot);
469
+ if (!frameworkRoot) return null;
470
+ const packageDirs = {
471
+ "@farming-labs/docs": path.join(frameworkRoot, "packages", "docs"),
472
+ "@farming-labs/next": path.join(frameworkRoot, "packages", "next"),
473
+ "@farming-labs/theme": path.join(frameworkRoot, "packages", "fumadocs")
474
+ };
475
+ if (!Object.values(packageDirs).every((value) => fs.existsSync(path.join(value, "package.json")))) return null;
476
+ return Object.fromEntries(Object.entries(packageDirs).map(([name, packageDir]) => {
477
+ return [name, `file:${toPosixPath(path.relative(runtimeDir, packageDir) || ".")}`];
478
+ }));
479
+ }
480
+ function renderRuntimePackageJson(project) {
481
+ const localPackageSpec = resolveLocalPackageSpec(project.projectRoot, project.runtimeDir);
482
+ const docsVersion = getDocsPackageVersion();
483
+ const frameworkSpec = localPackageSpec ?? {
484
+ "@farming-labs/docs": docsVersion,
485
+ "@farming-labs/next": docsVersion,
486
+ "@farming-labs/theme": docsVersion
487
+ };
488
+ return `${JSON.stringify({
489
+ name: `${project.siteName.toLowerCase().replace(/[^a-z0-9]+/g, "-") || "docs"}-managed-runtime`,
490
+ private: true,
491
+ scripts: {
492
+ dev: "next dev --turbopack",
493
+ build: "next build --turbopack",
494
+ start: "next start"
495
+ },
496
+ dependencies: {
497
+ ...frameworkSpec,
498
+ next: DEFAULT_NEXT_VERSION,
499
+ react: DEFAULT_REACT_VERSION,
500
+ "react-dom": DEFAULT_REACT_VERSION,
501
+ yaml: "^2.8.2"
502
+ },
503
+ devDependencies: {
504
+ "@tailwindcss/postcss": DEFAULT_TAILWIND_VERSION,
505
+ "@types/mdx": "^2.0.13",
506
+ "@types/node": "^22.10.0",
507
+ "@types/react": "^19.2.2",
508
+ "@types/react-dom": "^19.2.2",
509
+ postcss: DEFAULT_POSTCSS_VERSION,
510
+ tailwindcss: DEFAULT_TAILWIND_VERSION,
511
+ typescript: DEFAULT_TYPESCRIPT_VERSION
512
+ }
513
+ }, null, 2)}\n`;
514
+ }
515
+ function renderNextConfig(project) {
516
+ const relativeProjectRoot = toPosixPath(path.relative(project.runtimeDir, project.projectRoot) || ".");
517
+ return `import path from "node:path";
518
+ import { withDocs } from "@farming-labs/next/config";
519
+
520
+ const projectRoot = path.resolve(process.cwd(), ${JSON.stringify(relativeProjectRoot)});
521
+
522
+ export default withDocs({
523
+ turbopack: {
524
+ root: projectRoot,
525
+ },
526
+ });
527
+ `;
528
+ }
529
+ function renderNextEnvDts() {
530
+ return `/// <reference types="next" />
531
+ /// <reference types="next/image-types/global" />
532
+
533
+ // NOTE: This file should not be edited
534
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
535
+ `;
536
+ }
537
+ function renderRedirectPage(target) {
538
+ return `import { redirect } from "next/navigation";
539
+
540
+ export default function HomePage() {
541
+ redirect(${JSON.stringify(target)});
542
+ }
543
+ `;
544
+ }
545
+ function renderEmptyHomePage() {
546
+ return `export default function HomePage() {
547
+ return (
548
+ <main style={{ padding: 32, fontFamily: "system-ui, sans-serif" }}>
549
+ <h1>No docs content found</h1>
550
+ <p>Add markdown files to \`${DEFAULT_DOCS_ROOT}/\` or \`${DEFAULT_API_REFERENCE_ROOT}/\` and run the dev server again.</p>
551
+ </main>
552
+ );
553
+ }
554
+ `;
555
+ }
556
+ function renderDocsConfigFile(options) {
557
+ const apiReferenceBlock = options.apiReference ? ` apiReference: {
558
+ enabled: true,
559
+ path: ${JSON.stringify(options.apiReference.path)},
560
+ renderer: "fumadocs",
561
+ specUrl: ${JSON.stringify(options.apiReference.specUrl)},
562
+ },
563
+ ` : "";
564
+ return `import { defineDocs } from "@farming-labs/docs";
565
+ import { ${options.theme.factory} } from "${options.theme.importPath}";
566
+
567
+ export default defineDocs({
568
+ entry: "${options.entry}",
569
+ theme: ${options.theme.factory}(),
570
+ nav: {
571
+ title: ${JSON.stringify(options.navTitle)},
572
+ url: ${JSON.stringify(options.navUrl)},
573
+ },
574
+ sidebar: {
575
+ flat: true,
576
+ },
577
+ metadata: {
578
+ titleTemplate: ${JSON.stringify(options.titleTemplate)},
579
+ description: ${JSON.stringify(options.description)},
580
+ },
581
+ ${apiReferenceBlock}});
582
+ `;
583
+ }
584
+ function renderManagedApiReferenceLayout() {
585
+ return `import docsConfig from "@/docs.config";
586
+ import { createNextApiReferenceLayout } from "@farming-labs/next/api-reference";
587
+
588
+ const ApiReferenceLayout = createNextApiReferenceLayout(docsConfig);
589
+
590
+ export default function Layout({ children }: { children: React.ReactNode }) {
591
+ return <ApiReferenceLayout>{children}</ApiReferenceLayout>;
592
+ }
593
+ `;
594
+ }
595
+ function renderManagedApiReferencePage() {
596
+ return `import docsConfig from "@/docs.config";
597
+ import { createNextApiReferencePage } from "@farming-labs/next/api-reference";
598
+
599
+ const ApiReferencePage = createNextApiReferencePage(docsConfig);
600
+
601
+ export const dynamic = "force-dynamic";
602
+ export const revalidate = 0;
603
+
604
+ export default ApiReferencePage;
605
+ `;
606
+ }
607
+ function renderManagedOpenApiRoute(project, spec) {
608
+ const relativeProjectRoot = toPosixPath(path.relative(project.runtimeDir, project.projectRoot) || ".");
609
+ return `import fs from "node:fs/promises";
610
+ import path from "node:path";
611
+ import { parse } from "yaml";
612
+
613
+ const projectRoot = path.resolve(process.cwd(), ${JSON.stringify(relativeProjectRoot)});
614
+ const specPath = path.resolve(projectRoot, ${JSON.stringify(spec.path)});
615
+
616
+ function parseOpenApiDocument(source: string, filePath: string) {
617
+ const extension = path.extname(filePath).toLowerCase();
618
+ if (extension === ".yaml" || extension === ".yml") {
619
+ return parse(source);
620
+ }
621
+
622
+ return JSON.parse(source) as Record<string, unknown>;
623
+ }
624
+
625
+ export async function GET() {
626
+ try {
627
+ const raw = await fs.readFile(specPath, "utf-8");
628
+ return Response.json(parseOpenApiDocument(raw, specPath));
629
+ } catch (error) {
630
+ const message = error instanceof Error ? error.message : "Unknown error";
631
+
632
+ return Response.json(
633
+ {
634
+ error: "Unable to load OpenAPI document",
635
+ message,
636
+ specPath,
637
+ },
638
+ {
639
+ status: 500,
640
+ },
641
+ );
642
+ }
643
+ }
644
+
645
+ export const dynamic = "force-dynamic";
646
+ export const revalidate = 0;
647
+ `;
648
+ }
649
+ function resolveAppRouteDir(appDir, route) {
650
+ return path.join(appDir, ...normalizeManagedRoutePath(route).split("/"));
651
+ }
652
+ function createManagedOpenApiSyncResult(route) {
653
+ return {
654
+ pageCount: 1,
655
+ routes: [`/${normalizeManagedRoutePath(route)}`]
656
+ };
657
+ }
658
+ function getManagedOpenApiTrackedPaths(project) {
659
+ if (project.apiReferenceSpec && !project.apiReferenceSpec.sourcePath) return [];
660
+ if (project.apiReferenceSpec?.sourcePath) return [project.apiReferenceSpec.sourcePath];
661
+ return MANAGED_OPENAPI_CONVENTION_CANDIDATES.map((candidate) => path.join(project.projectRoot, candidate));
662
+ }
663
+ function validateManagedOpenApiSpec(project) {
664
+ const sourcePath = project.apiReferenceSpec?.sourcePath;
665
+ if (!sourcePath) return;
666
+ if (!fs.existsSync(sourcePath) || !fs.statSync(sourcePath).isFile()) {
667
+ const relativePath = path.relative(project.projectRoot, sourcePath) || sourcePath;
668
+ throw new Error(`OpenAPI source not found at ${relativePath}. Update ${MANAGED_CONFIG_FILE} or add the spec file and try again.`);
669
+ }
670
+ }
671
+ function renderAlternateSectionLayout(configImportPath) {
672
+ return `import docsConfig from "${configImportPath}";
673
+ import { createDocsLayout, createDocsMetadata } from "@farming-labs/theme";
674
+
675
+ export const metadata = createDocsMetadata(docsConfig);
676
+
677
+ const DocsLayout = createDocsLayout(docsConfig);
678
+
679
+ export default function Layout({ children }: { children: React.ReactNode }) {
680
+ return <DocsLayout>{children}</DocsLayout>;
681
+ }
682
+ `;
683
+ }
684
+ function renderManagedPreviewProxy() {
685
+ return `import { NextResponse } from "next/server";
686
+ import type { NextRequest } from "next/server";
687
+
688
+ const LOG_PREFIX = "[docs-page]";
689
+
690
+ export function proxy(request: NextRequest) {
691
+ const pathname = request.nextUrl.pathname;
692
+ const purpose = request.headers.get("purpose");
693
+ const prefetch = request.headers.get("next-router-prefetch");
694
+
695
+ if (purpose !== "prefetch" && prefetch === null) {
696
+ console.log(\`\${LOG_PREFIX} \${pathname}\`);
697
+ }
698
+
699
+ return NextResponse.next();
700
+ }
701
+
702
+ export const config = {
703
+ matcher: ["/docs/:path*", "/api-reference/:path*"],
704
+ };
705
+ `;
706
+ }
707
+ function writeFileIfChanged(filePath, content) {
708
+ if (fs.existsSync(filePath)) {
709
+ if (fs.readFileSync(filePath, "utf-8") === content) return;
710
+ }
711
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
712
+ fs.writeFileSync(filePath, content, "utf-8");
713
+ }
714
+ function copyFile(sourcePath, destinationPath) {
715
+ if (normalizePathKey(sourcePath) === normalizePathKey(destinationPath)) return;
716
+ fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
717
+ fs.copyFileSync(sourcePath, destinationPath);
718
+ }
719
+ function resolveGeneratedPagePath(destinationRoot, relativeSourcePath) {
720
+ const sourceDir = path.dirname(relativeSourcePath);
721
+ const extension = path.extname(relativeSourcePath);
722
+ const baseName = path.basename(relativeSourcePath, extension);
723
+ if (baseName === "page" || baseName === "index") return path.join(destinationRoot, sourceDir, "page.mdx");
724
+ return path.join(destinationRoot, sourceDir, baseName, "page.mdx");
725
+ }
726
+ function resolveGeneratedRoute(baseRoute, relativeSourcePath) {
727
+ const sourceDir = path.dirname(relativeSourcePath);
728
+ const extension = path.extname(relativeSourcePath);
729
+ const baseName = path.basename(relativeSourcePath, extension);
730
+ const sourceDirPosix = sourceDir === "." ? "" : toPosixPath(sourceDir);
731
+ if (baseName === "page" || baseName === "index") return sourceDirPosix ? `/${baseRoute}/${sourceDirPosix}` : `/${baseRoute}`;
732
+ return sourceDirPosix ? `/${baseRoute}/${sourceDirPosix}/${baseName}` : `/${baseRoute}/${baseName}`;
733
+ }
734
+ function buildPageCandidates(basePath) {
735
+ return [
736
+ basePath,
737
+ `${basePath}.mdx`,
738
+ `${basePath}.md`,
739
+ path.join(basePath, "index.mdx"),
740
+ path.join(basePath, "index.md"),
741
+ path.join(basePath, "page.mdx"),
742
+ path.join(basePath, "page.md")
743
+ ];
744
+ }
745
+ function splitReference(value) {
746
+ const hashIndex = value.indexOf("#");
747
+ const beforeHash = hashIndex === -1 ? value : value.slice(0, hashIndex);
748
+ const hash = hashIndex === -1 ? "" : value.slice(hashIndex);
749
+ const queryIndex = beforeHash.indexOf("?");
750
+ return {
751
+ pathPart: queryIndex === -1 ? beforeHash : beforeHash.slice(0, queryIndex),
752
+ suffix: `${queryIndex === -1 ? "" : beforeHash.slice(queryIndex)}${hash}`
753
+ };
754
+ }
755
+ function isExternalReference(value) {
756
+ return /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(value);
757
+ }
758
+ function stripRelativeReferencePrefix(value) {
759
+ let stripped = value;
760
+ while (stripped.startsWith("./")) stripped = stripped.slice(2);
761
+ while (stripped.startsWith("../")) stripped = stripped.slice(3);
762
+ return stripped;
763
+ }
764
+ function matchesContentRootHint(value, contentRootHints) {
765
+ const normalized = toPosixPath(value).replace(/^\/+/, "");
766
+ return contentRootHints.some((hint) => normalized === hint || normalized.startsWith(`${hint}/`));
767
+ }
768
+ function resolveLinkedSourcePage(sourcePagePath, referencePath, pageMap, projectRoot, contentRootHints) {
769
+ const basePath = path.resolve(path.dirname(sourcePagePath), referencePath);
770
+ for (const candidate of buildPageCandidates(basePath)) {
771
+ const normalized = normalizePathKey(candidate);
772
+ if (pageMap.has(normalized)) return normalized;
773
+ }
774
+ const rootRelativeReference = stripRelativeReferencePrefix(referencePath);
775
+ if (matchesContentRootHint(rootRelativeReference, contentRootHints)) {
776
+ const rootRelativeBasePath = path.resolve(projectRoot, rootRelativeReference);
777
+ for (const candidate of buildPageCandidates(rootRelativeBasePath)) {
778
+ const normalized = normalizePathKey(candidate);
779
+ if (pageMap.has(normalized)) return normalized;
780
+ }
781
+ }
782
+ return null;
783
+ }
784
+ function rewriteReference(rawReference, context) {
785
+ const trimmed = rawReference.trim();
786
+ const wrapped = trimmed.startsWith("<") && trimmed.endsWith(">");
787
+ const reference = wrapped ? trimmed.slice(1, -1) : trimmed;
788
+ if (!reference || reference.startsWith("#") || path.isAbsolute(reference) || isExternalReference(reference)) return rawReference;
789
+ const { pathPart, suffix } = splitReference(reference);
790
+ if (!pathPart) return rawReference;
791
+ const linkedSourcePage = resolveLinkedSourcePage(context.sourcePagePath, pathPart, context.pageMap, context.projectRoot, context.contentRootHints);
792
+ if (linkedSourcePage) {
793
+ const rewritten = `${context.routeMap.get(linkedSourcePage)}${suffix}`;
794
+ return wrapped ? `<${rewritten}>` : rewritten;
795
+ }
796
+ const assetSourcePath = path.resolve(path.dirname(context.sourcePagePath), pathPart);
797
+ if (fs.existsSync(assetSourcePath) && fs.statSync(assetSourcePath).isFile()) {
798
+ const assetDestinationPath = path.resolve(path.dirname(context.destinationPagePath), pathPart);
799
+ context.assetCopies.set(normalizePathKey(assetDestinationPath), assetSourcePath);
800
+ }
801
+ return rawReference;
802
+ }
803
+ function rewritePageContent(content, context) {
804
+ let output = content.replace(/(!?\[[^\]]*?\]\()([^)]+)(\))/g, (_match, prefix, url, suffix) => {
805
+ return `${prefix}${rewriteReference(url, context)}${suffix}`;
806
+ });
807
+ output = output.replace(/\b(href|src)=("([^"]+)"|'([^']+)')/g, (match, attr, quoted, dbl, sgl) => {
808
+ const raw = dbl ?? sgl;
809
+ if (!raw || raw.includes("{")) return match;
810
+ const rewritten = rewriteReference(raw, context);
811
+ const quote = quoted.startsWith("\"") ? "\"" : "'";
812
+ return `${attr}=${quote}${rewritten}${quote}`;
813
+ });
814
+ return output;
815
+ }
816
+ function buildManagedPageMap(sections) {
817
+ const pageMap = /* @__PURE__ */ new Map();
818
+ const routeMap = /* @__PURE__ */ new Map();
819
+ for (const section of sections) {
820
+ const pageFiles = walkFiles(section.sourceDir).filter(isPageSourceFile);
821
+ for (const pageFile of pageFiles) {
822
+ const relativeSourcePath = path.relative(section.sourceDir, pageFile);
823
+ const normalizedSource = normalizePathKey(pageFile);
824
+ pageMap.set(normalizedSource, resolveGeneratedPagePath(section.destinationDir, relativeSourcePath));
825
+ routeMap.set(normalizedSource, resolveGeneratedRoute(section.baseRoute, relativeSourcePath));
826
+ }
827
+ }
828
+ return {
829
+ pageMap,
830
+ routeMap
831
+ };
832
+ }
833
+ function syncManagedSection(options) {
834
+ fs.rmSync(options.destinationDir, {
835
+ recursive: true,
836
+ force: true
837
+ });
838
+ const sourceFiles = walkFiles(options.sourceDir);
839
+ const pageFiles = sourceFiles.filter(isPageSourceFile);
840
+ if (pageFiles.length === 0) return {
841
+ pageCount: 0,
842
+ routes: []
843
+ };
844
+ const assetCopies = /* @__PURE__ */ new Map();
845
+ const routes = [];
846
+ for (const pageFile of pageFiles) {
847
+ const normalizedSource = normalizePathKey(pageFile);
848
+ const destinationPagePath = options.pageMap.get(normalizedSource);
849
+ const relativeSourcePath = path.relative(options.sourceDir, pageFile);
850
+ writeFileIfChanged(destinationPagePath, rewritePageContent(fs.readFileSync(pageFile, "utf-8"), {
851
+ sourcePagePath: pageFile,
852
+ destinationPagePath,
853
+ pageMap: options.pageMap,
854
+ routeMap: options.routeMap,
855
+ assetCopies,
856
+ projectRoot: options.projectRoot,
857
+ contentRootHints: options.contentRootHints
858
+ }));
859
+ routes.push(resolveGeneratedRoute(options.baseRoute, relativeSourcePath));
860
+ }
861
+ for (const filePath of sourceFiles) {
862
+ const relativeSourcePath = path.relative(options.sourceDir, filePath);
863
+ if (pageFiles.includes(filePath)) continue;
864
+ copyFile(filePath, path.join(options.destinationDir, relativeSourcePath));
865
+ }
866
+ for (const [destinationPath, sourcePath] of assetCopies.entries()) copyFile(sourcePath, destinationPath);
867
+ return {
868
+ pageCount: pageFiles.length,
869
+ routes: routes.sort()
870
+ };
871
+ }
872
+ function pickHomeTarget(docs, apiReference) {
873
+ if (docs.routes.includes("/docs")) return "/docs";
874
+ if (docs.routes.length > 0) return docs.routes[0];
875
+ if (apiReference.routes.includes("/api-reference")) return "/api-reference";
876
+ if (apiReference.routes.length > 0) return apiReference.routes[0];
877
+ return "/docs";
878
+ }
879
+ function computeManagedSourceStamp(projectRoot) {
880
+ const project = readManagedDocsProject(projectRoot);
881
+ const trackedPaths = [project.configPath];
882
+ for (const rootName of [project.docsRoot, project.apiReferenceRoot]) trackedPaths.push(...walkFiles(path.join(project.projectRoot, rootName)));
883
+ trackedPaths.push(...getManagedOpenApiTrackedPaths(project));
884
+ return trackedPaths.map((filePath) => {
885
+ if (!fs.existsSync(filePath)) return `${filePath}:missing`;
886
+ const stat = fs.statSync(filePath);
887
+ return `${filePath}:${stat.mtimeMs}:${stat.size}`;
888
+ }).join("|");
889
+ }
890
+ function materializeManagedRuntime(projectRoot) {
891
+ const project = readManagedDocsProject(projectRoot);
892
+ const docsSourceDir = path.join(project.projectRoot, project.docsRoot);
893
+ const apiReferenceSourceDir = path.join(project.projectRoot, project.apiReferenceRoot);
894
+ const hasOpenApiSpec = Boolean(project.apiReferenceSpec);
895
+ const hasDocsMarkdown = directoryHasMarkdown(docsSourceDir);
896
+ const hasApiReferenceMarkdown = directoryHasMarkdown(apiReferenceSourceDir);
897
+ validateManagedOpenApiSpec(project);
898
+ if (!hasDocsMarkdown && !hasApiReferenceMarkdown && !hasOpenApiSpec) throw new Error(`No docs content found. Add markdown files under ${project.docsRoot}/ or ${project.apiReferenceRoot}/, or point content.openapi at an OpenAPI file in ${MANAGED_CONFIG_FILE}.`);
899
+ const templateConfig = {
900
+ entry: "docs",
901
+ theme: project.theme.templateTheme,
902
+ projectName: project.siteName,
903
+ framework: project.runtimeFramework,
904
+ useAlias: true,
905
+ nextAppDir: "app",
906
+ apiReference: {
907
+ path: normalizeManagedRoutePath(project.apiReferenceSpec?.route),
908
+ routeRoot: "api"
909
+ }
910
+ };
911
+ const appDir = path.join(project.runtimeDir, "app");
912
+ const docsDir = path.join(appDir, "docs");
913
+ const apiReferenceRoute = project.apiReferenceSpec?.route ?? DEFAULT_API_REFERENCE_ROOT;
914
+ const apiReferenceDir = resolveAppRouteDir(appDir, apiReferenceRoute);
915
+ const managedOpenApiRouteDir = path.join(appDir, "api", "docs", "openapi");
916
+ const legacyMiddlewarePath = path.join(project.runtimeDir, "middleware.ts");
917
+ fs.mkdirSync(project.runtimeDir, { recursive: true });
918
+ fs.rmSync(legacyMiddlewarePath, { force: true });
919
+ writeFileIfChanged(path.join(project.runtimeDir, "package.json"), renderRuntimePackageJson(project));
920
+ writeFileIfChanged(path.join(project.runtimeDir, "next.config.ts"), renderNextConfig(project));
921
+ writeFileIfChanged(path.join(project.runtimeDir, "next-env.d.ts"), renderNextEnvDts());
922
+ writeFileIfChanged(path.join(project.runtimeDir, "proxy.ts"), renderManagedPreviewProxy());
923
+ writeFileIfChanged(path.join(project.runtimeDir, "tsconfig.json"), tsconfigTemplate(true));
924
+ writeFileIfChanged(path.join(project.runtimeDir, "postcss.config.mjs"), postcssConfigTemplate());
925
+ writeFileIfChanged(path.join(project.runtimeDir, ".gitignore"), ".next\nnode_modules\n");
926
+ writeFileIfChanged(path.join(project.runtimeDir, "docs.config.ts"), renderDocsConfigFile({
927
+ entry: "docs",
928
+ navTitle: project.siteName,
929
+ navUrl: "/docs",
930
+ siteName: project.siteName,
931
+ titleTemplate: project.titleTemplate,
932
+ description: project.description,
933
+ theme: project.theme,
934
+ apiReference: project.apiReferenceSpec ? {
935
+ path: apiReferenceRoute,
936
+ specUrl: project.apiReferenceSpec.specUrl
937
+ } : void 0
938
+ }));
939
+ if (!project.apiReferenceSpec) writeFileIfChanged(path.join(project.runtimeDir, "api-reference.config.ts"), renderDocsConfigFile({
940
+ entry: "api-reference",
941
+ navTitle: `${project.siteName} API Reference`,
942
+ navUrl: "/api-reference",
943
+ siteName: project.siteName,
944
+ titleTemplate: project.titleTemplate,
945
+ description: project.description,
946
+ theme: project.theme
947
+ }));
948
+ else fs.rmSync(path.join(project.runtimeDir, "api-reference.config.ts"), { force: true });
949
+ writeFileIfChanged(path.join(appDir, "layout.tsx"), rootLayoutTemplate(templateConfig, "app/global.css"));
950
+ writeFileIfChanged(path.join(appDir, "global.css"), globalCssTemplate(project.theme.templateTheme));
951
+ const { pageMap, routeMap } = buildManagedPageMap([{
952
+ sourceDir: docsSourceDir,
953
+ destinationDir: docsDir,
954
+ baseRoute: "docs"
955
+ }, !project.apiReferenceSpec ? {
956
+ sourceDir: apiReferenceSourceDir,
957
+ destinationDir: apiReferenceDir,
958
+ baseRoute: "api-reference"
959
+ } : null].filter(Boolean));
960
+ const contentRootHints = [toPosixPath(project.docsRoot).replace(/\/+$/, ""), toPosixPath(project.apiReferenceRoot).replace(/\/+$/, "")];
961
+ const docs = syncManagedSection({
962
+ sourceDir: docsSourceDir,
963
+ destinationDir: docsDir,
964
+ baseRoute: "docs",
965
+ pageMap,
966
+ routeMap,
967
+ projectRoot: project.projectRoot,
968
+ contentRootHints
969
+ });
970
+ let apiReference;
971
+ if (project.apiReferenceSpec) {
972
+ fs.rmSync(apiReferenceDir, {
973
+ recursive: true,
974
+ force: true
975
+ });
976
+ writeFileIfChanged(path.join(apiReferenceDir, "layout.tsx"), renderManagedApiReferenceLayout());
977
+ writeFileIfChanged(path.join(apiReferenceDir, "[[...slug]]", "page.tsx"), renderManagedApiReferencePage());
978
+ if (project.apiReferenceSpec.sourcePath) writeFileIfChanged(path.join(managedOpenApiRouteDir, "route.ts"), renderManagedOpenApiRoute(project, project.apiReferenceSpec));
979
+ else fs.rmSync(managedOpenApiRouteDir, {
980
+ recursive: true,
981
+ force: true
982
+ });
983
+ apiReference = createManagedOpenApiSyncResult(project.apiReferenceSpec.route);
984
+ } else {
985
+ fs.rmSync(managedOpenApiRouteDir, {
986
+ recursive: true,
987
+ force: true
988
+ });
989
+ apiReference = syncManagedSection({
990
+ sourceDir: apiReferenceSourceDir,
991
+ destinationDir: apiReferenceDir,
992
+ baseRoute: "api-reference",
993
+ pageMap,
994
+ routeMap,
995
+ projectRoot: project.projectRoot,
996
+ contentRootHints
997
+ });
998
+ }
999
+ writeFileIfChanged(path.join(docsDir, "layout.tsx"), docsLayoutTemplate(templateConfig));
1000
+ if (!project.apiReferenceSpec) writeFileIfChanged(path.join(apiReferenceDir, "layout.tsx"), renderAlternateSectionLayout("@/api-reference.config"));
1001
+ const homeTarget = pickHomeTarget(docs, apiReference);
1002
+ writeFileIfChanged(path.join(appDir, "page.tsx"), docs.pageCount > 0 || apiReference.pageCount > 0 ? renderRedirectPage(homeTarget) : renderEmptyHomePage());
1003
+ return {
1004
+ runtimeDir: project.runtimeDir,
1005
+ docs,
1006
+ apiReference,
1007
+ homeTarget
1008
+ };
1009
+ }
1010
+ function getInstallCommand(packageManager) {
1011
+ if (packageManager === "pnpm") return {
1012
+ command: "pnpm",
1013
+ args: [
1014
+ "install",
1015
+ "--ignore-workspace",
1016
+ "--prefer-offline"
1017
+ ]
1018
+ };
1019
+ if (packageManager === "yarn") return {
1020
+ command: "yarn",
1021
+ args: ["install"]
1022
+ };
1023
+ if (packageManager === "bun") return {
1024
+ command: "bun",
1025
+ args: ["install"]
1026
+ };
1027
+ return {
1028
+ command: "npm",
1029
+ args: ["install", "--prefer-offline"]
1030
+ };
1031
+ }
1032
+ function getRuntimeInstallStamp(project) {
1033
+ return fs.readFileSync(path.join(project.runtimeDir, "package.json"), "utf-8");
1034
+ }
1035
+ async function runCapturedCommand(options) {
1036
+ const recentLines = [];
1037
+ const child = spawn(options.command, options.args, {
1038
+ cwd: options.cwd,
1039
+ stdio: [
1040
+ "ignore",
1041
+ "pipe",
1042
+ "pipe"
1043
+ ],
1044
+ shell: process.platform === "win32"
1045
+ });
1046
+ const rememberLine = (line) => {
1047
+ const normalized = normalizeLogLine(line);
1048
+ if (!normalized) return;
1049
+ recentLines.push(normalized);
1050
+ if (recentLines.length > 12) recentLines.shift();
1051
+ };
1052
+ const stdoutReader = createLineReader((line) => {
1053
+ rememberLine(line);
1054
+ if (options.verbose) logLine(options.label, pc.dim(normalizeLogLine(line)));
1055
+ });
1056
+ const stderrReader = createLineReader((line) => {
1057
+ rememberLine(line);
1058
+ if (options.verbose) logLine(options.label, pc.dim(normalizeLogLine(line)));
1059
+ });
1060
+ child.stdout?.on("data", (chunk) => stdoutReader.push(chunk));
1061
+ child.stderr?.on("data", (chunk) => stderrReader.push(chunk));
1062
+ await new Promise((resolve, reject) => {
1063
+ child.on("error", reject);
1064
+ child.on("close", (code) => {
1065
+ stdoutReader.flush();
1066
+ stderrReader.flush();
1067
+ if (code && code !== 0) {
1068
+ const details = recentLines.length > 0 ? `\n${recentLines.map((line) => ` ${line}`).join("\n")}` : "";
1069
+ reject(/* @__PURE__ */ new Error(`${options.failureMessage}${details}`));
1070
+ return;
1071
+ }
1072
+ resolve();
1073
+ });
1074
+ });
1075
+ }
1076
+ function terminateChildProcessTree(child) {
1077
+ if (child.exitCode !== null || child.killed) return;
1078
+ const childPid = child.pid;
1079
+ if (process.platform !== "win32" && typeof childPid === "number") {
1080
+ try {
1081
+ process.kill(-childPid, "SIGTERM");
1082
+ } catch {
1083
+ child.kill("SIGTERM");
1084
+ return;
1085
+ }
1086
+ setTimeout(() => {
1087
+ if (child.exitCode !== null || child.killed) return;
1088
+ try {
1089
+ process.kill(-childPid, "SIGKILL");
1090
+ } catch {}
1091
+ }, 1500).unref();
1092
+ return;
1093
+ }
1094
+ child.kill("SIGTERM");
1095
+ }
1096
+ function getRunScriptCommand(packageManager, scriptName, scriptArgs = []) {
1097
+ if (packageManager === "yarn") return {
1098
+ command: "yarn",
1099
+ args: [scriptName, ...scriptArgs]
1100
+ };
1101
+ if (packageManager === "npm") return {
1102
+ command: "npm",
1103
+ args: [
1104
+ "run",
1105
+ scriptName,
1106
+ ...scriptArgs.length > 0 ? ["--", ...scriptArgs] : []
1107
+ ]
1108
+ };
1109
+ return {
1110
+ command: packageManager,
1111
+ args: [
1112
+ "run",
1113
+ scriptName,
1114
+ ...scriptArgs
1115
+ ]
1116
+ };
1117
+ }
1118
+ function getNextDevArgs(options) {
1119
+ const args = [];
1120
+ if (options.port) args.push("--port", options.port);
1121
+ const requestedHost = resolveRequestedHost(options);
1122
+ if (requestedHost === true) args.push("--hostname", "0.0.0.0");
1123
+ else if (typeof requestedHost === "string") args.push("--hostname", requestedHost);
1124
+ return args;
1125
+ }
1126
+ function resolveLocalPreviewOrigin(options) {
1127
+ const port = options.port ?? "3000";
1128
+ const requestedHost = resolveRequestedHost(options);
1129
+ return `http://${requestedHost === true || requestedHost === "0.0.0.0" ? "localhost" : typeof requestedHost === "string" && requestedHost.trim() ? requestedHost.trim() : "localhost"}:${port}`;
1130
+ }
1131
+ async function dev(options = {}) {
1132
+ const projectRoot = process.cwd();
1133
+ const project = readManagedDocsProject(projectRoot);
1134
+ const packageManager = detectNearestPackageManager(projectRoot);
1135
+ const initial = materializeManagedRuntime(projectRoot);
1136
+ const runtimeNodeModules = path.join(project.runtimeDir, "node_modules");
1137
+ const runtimeInstallMarker = path.join(project.runtimeDir, ".docs-runtime-install-stamp");
1138
+ const devLogger = createDevLogger();
1139
+ const version = normalizeVersionTag(getDocsPackageVersion());
1140
+ const runtimeInstallStamp = getRuntimeInstallStamp(project);
1141
+ console.log(pc.dim("Preparing local preview..."));
1142
+ if (options.verbose) {
1143
+ logLine("source", `${pc.cyan(MANAGED_CONFIG_FILE)} drives ${pc.cyan(`${project.docsRoot}/`)} and ${pc.cyan(`${project.apiReferenceRoot}/`)}`);
1144
+ logLine("runtime", `Generated runtime at ${pc.cyan(path.relative(projectRoot, project.runtimeDir) || project.runtimeDir)}`);
1145
+ logLine("watch", `Watching ${project.docsRoot}/, ${project.apiReferenceRoot}/, and ${MANAGED_CONFIG_FILE}`);
1146
+ logLine("sync", `Loaded ${initial.docs.pageCount} docs page${initial.docs.pageCount === 1 ? "" : "s"} and ${initial.apiReference.pageCount} api-reference page${initial.apiReference.pageCount === 1 ? "" : "s"}`);
1147
+ }
1148
+ if (!fs.existsSync(runtimeNodeModules) || !fs.existsSync(runtimeInstallMarker) || fs.readFileSync(runtimeInstallMarker, "utf-8") !== runtimeInstallStamp) {
1149
+ if (options.verbose) logLine("install", `Installing runtime dependencies with ${pc.cyan(packageManager)}`);
1150
+ const install = getInstallCommand(packageManager);
1151
+ await runCapturedCommand({
1152
+ label: "install",
1153
+ command: install.command,
1154
+ args: install.args,
1155
+ cwd: project.runtimeDir,
1156
+ verbose: options.verbose,
1157
+ failureMessage: `Failed to install runtime dependencies with ${packageManager}.`
1158
+ });
1159
+ writeFileIfChanged(runtimeInstallMarker, runtimeInstallStamp);
1160
+ if (options.verbose) logLine("install", "Runtime dependencies ready");
1161
+ }
1162
+ let lastStamp = computeManagedSourceStamp(projectRoot);
1163
+ const interval = setInterval(() => {
1164
+ try {
1165
+ const nextStamp = computeManagedSourceStamp(projectRoot);
1166
+ if (nextStamp === lastStamp) return;
1167
+ lastStamp = nextStamp;
1168
+ const updated = materializeManagedRuntime(projectRoot);
1169
+ if (options.verbose) logLine("sync", `Updated preview from ${updated.docs.pageCount} docs page${updated.docs.pageCount === 1 ? "" : "s"} and ${updated.apiReference.pageCount} api-reference page${updated.apiReference.pageCount === 1 ? "" : "s"}`);
1170
+ else logLine("sync", "Preview updated");
1171
+ } catch (error) {
1172
+ logErrorLine(error instanceof Error ? `Failed to sync frameworkless content: ${error.message}` : "Failed to sync frameworkless content.");
1173
+ }
1174
+ }, DEFAULT_POLL_INTERVAL_MS);
1175
+ const serverStartTime = Date.now();
1176
+ const { command, args } = getRunScriptCommand(packageManager, "dev", getNextDevArgs(options));
1177
+ const child = spawn(command, args, {
1178
+ cwd: initial.runtimeDir,
1179
+ detached: process.platform !== "win32",
1180
+ env: {
1181
+ ...process.env,
1182
+ FARMING_LABS_DOCS_DEV_ORIGIN: resolveLocalPreviewOrigin(options)
1183
+ },
1184
+ stdio: [
1185
+ "inherit",
1186
+ "pipe",
1187
+ "pipe"
1188
+ ],
1189
+ shell: process.platform === "win32"
1190
+ });
1191
+ if (options.verbose) logLine("server", "Starting local preview runtime");
1192
+ const serverState = {
1193
+ localUrl: void 0,
1194
+ networkUrl: void 0,
1195
+ readyShown: false,
1196
+ startingShown: false
1197
+ };
1198
+ const handleNextLine = (line, stream) => {
1199
+ const normalized = normalizeLogLine(line);
1200
+ if (!normalized) return;
1201
+ const event = parseNextDevLine(normalized);
1202
+ if (event) switch (event.type) {
1203
+ case "starting":
1204
+ if (options.verbose && !serverState.startingShown) {
1205
+ serverState.startingShown = true;
1206
+ logLine("server", "Booting Next.js preview engine");
1207
+ }
1208
+ return;
1209
+ case "local":
1210
+ serverState.localUrl = event.url;
1211
+ return;
1212
+ case "network":
1213
+ serverState.networkUrl = event.url;
1214
+ return;
1215
+ case "ready":
1216
+ if (serverState.readyShown) return;
1217
+ printDevBanner({
1218
+ name: "@farming-labs/docs",
1219
+ version,
1220
+ port: resolveBannerPort(serverState.localUrl, options.port),
1221
+ host: resolveRequestedHost(options),
1222
+ protocol: resolveBannerProtocol(serverState.localUrl),
1223
+ startTime: serverStartTime,
1224
+ localUrl: serverState.localUrl,
1225
+ networkUrl: serverState.networkUrl
1226
+ });
1227
+ console.log(pc.dim(" press Ctrl+C to stop"));
1228
+ serverState.readyShown = true;
1229
+ return;
1230
+ case "page":
1231
+ logLine("PAGE", event.pathname);
1232
+ return;
1233
+ case "compiling":
1234
+ if (options.verbose) logLine("compile", `Compiling ${pc.bold(event.target)}`);
1235
+ return;
1236
+ case "compiled":
1237
+ if (options.verbose) logLine("compile", `Compiled ${pc.bold(event.target)}${event.duration ? ` in ${pc.bold(event.duration)}` : ""}`);
1238
+ return;
1239
+ case "warning":
1240
+ devLogger.warn(event.message);
1241
+ return;
1242
+ case "error":
1243
+ devLogger.error(event.message);
1244
+ return;
1245
+ }
1246
+ if (stream === "stderr") {
1247
+ if (/\bwarn(?:ing)?\b/i.test(normalized)) devLogger.warn(normalized);
1248
+ else devLogger.error(normalized);
1249
+ return;
1250
+ }
1251
+ if (options.verbose) logLine("next", pc.dim(normalized));
1252
+ };
1253
+ const stdoutReader = createLineReader((line) => handleNextLine(line, "stdout"));
1254
+ const stderrReader = createLineReader((line) => handleNextLine(line, "stderr"));
1255
+ child.stdout?.on("data", (chunk) => stdoutReader.push(chunk));
1256
+ child.stderr?.on("data", (chunk) => stderrReader.push(chunk));
1257
+ let cleanedUp = false;
1258
+ const cleanup = () => {
1259
+ if (cleanedUp) return;
1260
+ cleanedUp = true;
1261
+ clearInterval(interval);
1262
+ process.off("SIGINT", cleanup);
1263
+ process.off("SIGTERM", cleanup);
1264
+ terminateChildProcessTree(child);
1265
+ };
1266
+ process.on("SIGINT", cleanup);
1267
+ process.on("SIGTERM", cleanup);
1268
+ await new Promise((resolve, reject) => {
1269
+ child.on("error", (error) => {
1270
+ stdoutReader.flush();
1271
+ stderrReader.flush();
1272
+ cleanup();
1273
+ reject(error);
1274
+ });
1275
+ child.on("close", (code) => {
1276
+ stdoutReader.flush();
1277
+ stderrReader.flush();
1278
+ cleanup();
1279
+ if (code && code !== 0) {
1280
+ reject(/* @__PURE__ */ new Error(`The frameworkless dev server exited with code ${code}.`));
1281
+ return;
1282
+ }
1283
+ resolve();
1284
+ });
1285
+ });
1286
+ }
1287
+
1288
+ //#endregion
1289
+ export { dev };