@farming-labs/astro 0.0.38 → 0.0.44

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 (65) hide show
  1. package/dist/api-reference.d.ts +3 -0
  2. package/dist/api-reference.d.ts.map +1 -0
  3. package/dist/api-reference.js +20 -0
  4. package/dist/api-reference.js.map +1 -0
  5. package/dist/astro/src/api-reference.d.ts +3 -0
  6. package/dist/astro/src/api-reference.d.ts.map +1 -0
  7. package/dist/astro/src/api-reference.js +20 -0
  8. package/dist/astro/src/api-reference.js.map +1 -0
  9. package/dist/astro/src/content.d.ts +54 -0
  10. package/dist/astro/src/content.d.ts.map +1 -0
  11. package/dist/astro/src/content.js +202 -0
  12. package/dist/astro/src/content.js.map +1 -0
  13. package/dist/astro/src/index.d.ts +11 -0
  14. package/dist/astro/src/index.d.ts.map +1 -0
  15. package/dist/astro/src/index.js +11 -0
  16. package/dist/astro/src/index.js.map +1 -0
  17. package/dist/astro/src/markdown.d.ts +18 -0
  18. package/dist/astro/src/markdown.d.ts.map +1 -0
  19. package/dist/astro/src/markdown.js +279 -0
  20. package/dist/astro/src/markdown.js.map +1 -0
  21. package/dist/astro/src/server.d.ts +66 -0
  22. package/dist/astro/src/server.d.ts.map +1 -0
  23. package/dist/astro/src/server.js +658 -0
  24. package/dist/astro/src/server.js.map +1 -0
  25. package/dist/docs/src/api-reference.d.ts +27 -0
  26. package/dist/docs/src/api-reference.d.ts.map +1 -0
  27. package/dist/docs/src/api-reference.js +594 -0
  28. package/dist/docs/src/api-reference.js.map +1 -0
  29. package/dist/docs/src/create-theme.d.ts +74 -0
  30. package/dist/docs/src/create-theme.d.ts.map +1 -0
  31. package/dist/docs/src/create-theme.js +86 -0
  32. package/dist/docs/src/create-theme.js.map +1 -0
  33. package/dist/docs/src/define-docs.d.ts +6 -0
  34. package/dist/docs/src/define-docs.d.ts.map +1 -0
  35. package/dist/docs/src/define-docs.js +27 -0
  36. package/dist/docs/src/define-docs.js.map +1 -0
  37. package/dist/docs/src/i18n.d.ts +15 -0
  38. package/dist/docs/src/i18n.d.ts.map +1 -0
  39. package/dist/docs/src/i18n.js +48 -0
  40. package/dist/docs/src/i18n.js.map +1 -0
  41. package/dist/docs/src/index.d.ts +16 -0
  42. package/dist/docs/src/index.d.ts.map +1 -0
  43. package/dist/docs/src/index.js +14 -0
  44. package/dist/docs/src/index.js.map +1 -0
  45. package/dist/docs/src/metadata.d.ts +24 -0
  46. package/dist/docs/src/metadata.d.ts.map +1 -0
  47. package/dist/docs/src/metadata.js +90 -0
  48. package/dist/docs/src/metadata.js.map +1 -0
  49. package/dist/docs/src/server.d.ts +3 -0
  50. package/dist/docs/src/server.d.ts.map +1 -0
  51. package/dist/docs/src/server.js +2 -0
  52. package/dist/docs/src/server.js.map +1 -0
  53. package/dist/docs/src/types.d.ts +1344 -0
  54. package/dist/docs/src/types.d.ts.map +1 -0
  55. package/dist/docs/src/types.js +6 -0
  56. package/dist/docs/src/types.js.map +1 -0
  57. package/dist/docs/src/utils.d.ts +6 -0
  58. package/dist/docs/src/utils.d.ts.map +1 -0
  59. package/dist/docs/src/utils.js +32 -0
  60. package/dist/docs/src/utils.js.map +1 -0
  61. package/dist/server.d.ts +1 -0
  62. package/dist/server.d.ts.map +1 -1
  63. package/dist/server.js +1 -0
  64. package/dist/server.js.map +1 -1
  65. package/package.json +8 -2
@@ -0,0 +1,658 @@
1
+ /**
2
+ * Server-side helpers for Astro docs routes.
3
+ *
4
+ * `createDocsServer(config)` returns all the functions needed for
5
+ * a complete docs site: `load` for getting page data, `GET` for search,
6
+ * and `POST` for AI chat.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * // src/lib/docs.server.ts
11
+ * import { createDocsServer } from "@farming-labs/astro/server";
12
+ * import config from "./docs.config";
13
+ *
14
+ * const contentFiles = import.meta.glob("/docs/**\/*.{md,mdx}", {
15
+ * query: "?raw", import: "default", eager: true,
16
+ * }) as Record<string, string>;
17
+ *
18
+ * export const { load, GET, POST } = createDocsServer({
19
+ * ...config,
20
+ * _preloadedContent: contentFiles,
21
+ * });
22
+ * ```
23
+ *
24
+ * ```ts
25
+ * // src/pages/docs/[...slug].astro
26
+ * import { load } from "../../lib/docs.server";
27
+ * const data = await load(Astro.url.pathname);
28
+ * ```
29
+ */
30
+ import fs from "node:fs";
31
+ import path from "node:path";
32
+ import matter from "gray-matter";
33
+ import { resolveDocsI18n, resolveDocsLocale, resolveDocsPath } from "@farming-labs/docs";
34
+ import { loadDocsNavTree, loadDocsContent, flattenNavTree } from "./content.js";
35
+ import { renderMarkdown } from "./markdown.js";
36
+ export { createAstroApiReference } from "./api-reference.js";
37
+ function resolveAIModelAndProvider(aiConfig, requestedModelId) {
38
+ const raw = aiConfig.model;
39
+ const modelList = (typeof raw === "object" && raw?.models) || [];
40
+ let modelId = requestedModelId;
41
+ if (!modelId) {
42
+ if (typeof raw === "string")
43
+ modelId = raw;
44
+ else if (typeof raw === "object")
45
+ modelId = raw.defaultModel ?? raw.models?.[0]?.id;
46
+ if (!modelId)
47
+ modelId = "gpt-4o-mini";
48
+ }
49
+ const entry = modelList.find((m) => m.id === modelId);
50
+ const providerKey = entry?.provider;
51
+ const providerConfig = providerKey && aiConfig.providers?.[providerKey];
52
+ const baseUrl = ((providerConfig && providerConfig.baseUrl) ||
53
+ aiConfig.baseUrl ||
54
+ "https://api.openai.com/v1").replace(/\/$/, "");
55
+ const apiKey = (providerConfig && providerConfig.apiKey) ||
56
+ aiConfig.apiKey ||
57
+ (typeof import.meta !== "undefined" ? import.meta.env?.OPENAI_API_KEY : undefined) ||
58
+ (typeof process !== "undefined" ? process.env?.OPENAI_API_KEY : undefined);
59
+ return { model: modelId, baseUrl, apiKey };
60
+ }
61
+ function stripMarkdownText(content) {
62
+ return content
63
+ .replace(/^(import|export)\s.*$/gm, "")
64
+ .replace(/<[^>]+\/>/g, "")
65
+ .replace(/<\/?[A-Z][^>]*>/g, "")
66
+ .replace(/<\/?[a-z][^>]*>/g, "")
67
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
68
+ .replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1")
69
+ .replace(/^#{1,6}\s+/gm, "")
70
+ .replace(/(\*{1,3}|_{1,3})(.*?)\1/g, "$2")
71
+ .replace(/```[\s\S]*?```/g, "")
72
+ .replace(/`([^`]+)`/g, "$1")
73
+ .replace(/^>\s+/gm, "")
74
+ .replace(/^[-*_]{3,}\s*$/gm, "")
75
+ .replace(/\n{3,}/g, "\n\n")
76
+ .trim();
77
+ }
78
+ function normalizePathSegment(value) {
79
+ return value.replace(/^\/+|\/+$/g, "");
80
+ }
81
+ function joinPathParts(...parts) {
82
+ return parts
83
+ .map((part) => normalizePathSegment(part))
84
+ .filter(Boolean)
85
+ .join("/");
86
+ }
87
+ function toPosixPath(value) {
88
+ return value.replace(/\\\\/g, "/");
89
+ }
90
+ function buildDirPrefix(contentDir) {
91
+ const rel = path.isAbsolute(contentDir)
92
+ ? toPosixPath(path.relative(process.cwd(), contentDir))
93
+ : toPosixPath(contentDir);
94
+ const normalized = normalizePathSegment(rel);
95
+ return normalized ? `/${normalized}/` : "/";
96
+ }
97
+ function navTreeFromMap(contentMap, dirPrefix, entry, ordering) {
98
+ const dirs = [];
99
+ for (const key of Object.keys(contentMap)) {
100
+ if (!key.startsWith(dirPrefix))
101
+ continue;
102
+ const rel = key.slice(dirPrefix.length);
103
+ const segments = rel.split("/");
104
+ const fileName = segments.pop();
105
+ const base = fileName.replace(/\.(md|mdx|svx)$/, "");
106
+ if (base !== "page" && base !== "index" && base !== "+page")
107
+ continue;
108
+ const { data } = matter(contentMap[key]);
109
+ const dirParts = segments;
110
+ const slug = dirParts.join("/");
111
+ const url = slug ? `/${entry}/${slug}` : `/${entry}`;
112
+ const fallbackTitle = dirParts.length > 0
113
+ ? dirParts[dirParts.length - 1].replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())
114
+ : "Documentation";
115
+ dirs.push({
116
+ parts: dirParts,
117
+ title: data.title ?? fallbackTitle,
118
+ url,
119
+ icon: data.icon,
120
+ order: typeof data.order === "number" ? data.order : Infinity,
121
+ });
122
+ }
123
+ dirs.sort((a, b) => {
124
+ if (a.parts.length !== b.parts.length)
125
+ return a.parts.length - b.parts.length;
126
+ return a.parts.join("/").localeCompare(b.parts.join("/"));
127
+ });
128
+ const children = [];
129
+ const rootInfo = dirs.find((d) => d.parts.length === 0);
130
+ if (rootInfo) {
131
+ children.push({
132
+ type: "page",
133
+ name: rootInfo.title,
134
+ url: rootInfo.url,
135
+ icon: rootInfo.icon,
136
+ });
137
+ }
138
+ function findSlugOrder(parentParts) {
139
+ if (!Array.isArray(ordering))
140
+ return undefined;
141
+ let items = ordering;
142
+ for (const part of parentParts) {
143
+ const found = items.find((i) => i.slug === part);
144
+ if (!found?.children)
145
+ return undefined;
146
+ items = found.children;
147
+ }
148
+ return items;
149
+ }
150
+ function buildLevel(parentParts) {
151
+ const depth = parentParts.length;
152
+ const directChildren = dirs.filter((d) => {
153
+ if (d.parts.length !== depth + 1)
154
+ return false;
155
+ for (let i = 0; i < depth; i++) {
156
+ if (d.parts[i] !== parentParts[i])
157
+ return false;
158
+ }
159
+ return true;
160
+ });
161
+ const slugOrder = findSlugOrder(parentParts);
162
+ if (slugOrder) {
163
+ const slugMap = new Set(slugOrder.map((i) => i.slug));
164
+ const ordered = [];
165
+ for (const item of slugOrder) {
166
+ const match = directChildren.find((d) => d.parts[depth] === item.slug);
167
+ if (match)
168
+ ordered.push(match);
169
+ }
170
+ for (const child of directChildren) {
171
+ if (!slugMap.has(child.parts[depth]))
172
+ ordered.push(child);
173
+ }
174
+ const nodes = [];
175
+ for (const child of ordered) {
176
+ const hasGrandChildren = dirs.some((d) => {
177
+ if (d.parts.length <= child.parts.length)
178
+ return false;
179
+ return child.parts.every((p, i) => d.parts[i] === p);
180
+ });
181
+ if (hasGrandChildren) {
182
+ nodes.push({
183
+ type: "folder",
184
+ name: child.title,
185
+ icon: child.icon,
186
+ index: { type: "page", name: child.title, url: child.url, icon: child.icon },
187
+ children: buildLevel(child.parts),
188
+ });
189
+ }
190
+ else {
191
+ nodes.push({ type: "page", name: child.title, url: child.url, icon: child.icon });
192
+ }
193
+ }
194
+ return nodes;
195
+ }
196
+ if (ordering === "numeric") {
197
+ directChildren.sort((a, b) => {
198
+ if (a.order === b.order)
199
+ return 0;
200
+ return a.order - b.order;
201
+ });
202
+ }
203
+ const nodes = [];
204
+ for (const child of directChildren) {
205
+ const hasGrandChildren = dirs.some((d) => {
206
+ if (d.parts.length <= child.parts.length)
207
+ return false;
208
+ return child.parts.every((p, i) => d.parts[i] === p);
209
+ });
210
+ if (hasGrandChildren) {
211
+ nodes.push({
212
+ type: "folder",
213
+ name: child.title,
214
+ icon: child.icon,
215
+ index: {
216
+ type: "page",
217
+ name: child.title,
218
+ url: child.url,
219
+ icon: child.icon,
220
+ },
221
+ children: buildLevel(child.parts),
222
+ });
223
+ }
224
+ else {
225
+ nodes.push({
226
+ type: "page",
227
+ name: child.title,
228
+ url: child.url,
229
+ icon: child.icon,
230
+ });
231
+ }
232
+ }
233
+ return nodes;
234
+ }
235
+ children.push(...buildLevel([]));
236
+ return { name: "Docs", children };
237
+ }
238
+ function searchIndexFromMap(contentMap, dirPrefix, entry) {
239
+ const pages = [];
240
+ for (const [key, raw] of Object.entries(contentMap)) {
241
+ if (!key.startsWith(dirPrefix))
242
+ continue;
243
+ const rel = key.slice(dirPrefix.length);
244
+ const segments = rel.split("/");
245
+ const fileName = segments.pop();
246
+ const base = fileName.replace(/\.(md|mdx|svx)$/, "");
247
+ const isIdx = base === "page" || base === "index" || base === "+page";
248
+ const slug = isIdx ? segments.join("/") : [...segments, base].join("/");
249
+ const url = slug ? `/${entry}/${slug}` : `/${entry}`;
250
+ const { data, content } = matter(raw);
251
+ const title = data.title ?? base.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
252
+ pages.push({
253
+ slug,
254
+ url,
255
+ title,
256
+ description: data.description,
257
+ icon: data.icon,
258
+ content: stripMarkdownText(content),
259
+ rawContent: content,
260
+ });
261
+ }
262
+ return pages;
263
+ }
264
+ function findPageInMap(contentMap, dirPrefix, slug) {
265
+ const isIndex = slug === "";
266
+ const candidates = isIndex
267
+ ? ["page.md", "page.mdx", "index.md"]
268
+ : [
269
+ `${slug}/page.md`,
270
+ `${slug}/page.mdx`,
271
+ `${slug}/index.md`,
272
+ `${slug}/index.svx`,
273
+ `${slug}.md`,
274
+ `${slug}.svx`,
275
+ ];
276
+ for (const candidate of candidates) {
277
+ const key = `${dirPrefix}${candidate}`;
278
+ if (key in contentMap) {
279
+ return { raw: contentMap[key], relPath: candidate };
280
+ }
281
+ }
282
+ return null;
283
+ }
284
+ /**
285
+ * Create all server-side functions needed for an Astro docs site.
286
+ *
287
+ * @param config - The `DocsConfig` object (from `defineDocs()` in `docs.config.ts`).
288
+ *
289
+ * Pass `_preloadedContent` (from `import.meta.glob`) to bundle markdown files
290
+ * at build time — required for serverless deployments (Vercel, Netlify, etc.)
291
+ * where the filesystem is not available at runtime.
292
+ */
293
+ export function createDocsServer(config = {}) {
294
+ const entry = config.entry ?? "docs";
295
+ const contentDirBase = config.contentDir ?? entry;
296
+ const i18n = resolveDocsI18n(config.i18n);
297
+ const githubRaw = config.github;
298
+ const github = typeof githubRaw === "string" ? { url: githubRaw } : (githubRaw ?? null);
299
+ const githubRepo = github?.url;
300
+ const githubBranch = github?.branch ?? "main";
301
+ const githubContentPath = github?.directory;
302
+ const preloaded = config._preloadedContent;
303
+ const ordering = config.ordering;
304
+ const aiConfig = config.ai ?? {};
305
+ if (config.apiKey && !aiConfig.apiKey) {
306
+ aiConfig.apiKey = config.apiKey;
307
+ }
308
+ function resolveContentDirRel(locale) {
309
+ if (!locale)
310
+ return contentDirBase;
311
+ if (path.isAbsolute(contentDirBase))
312
+ return path.join(contentDirBase, locale);
313
+ return joinPathParts(contentDirBase, locale);
314
+ }
315
+ function resolveContextFromPath(pathname, locale) {
316
+ const match = resolveDocsPath(pathname, entry);
317
+ const contentDirRel = resolveContentDirRel(locale);
318
+ return {
319
+ ...match,
320
+ locale,
321
+ contentDirRel,
322
+ contentDirAbs: path.resolve(contentDirRel),
323
+ dirPrefix: buildDirPrefix(contentDirRel),
324
+ };
325
+ }
326
+ function resolveLocaleFromRequest(request) {
327
+ if (!i18n)
328
+ return undefined;
329
+ const url = new URL(request.url);
330
+ const direct = resolveDocsLocale(url.searchParams, i18n);
331
+ if (direct)
332
+ return direct;
333
+ const referrer = request.headers.get("referer") ?? request.headers.get("referrer");
334
+ if (referrer) {
335
+ try {
336
+ const refUrl = new URL(referrer);
337
+ const fromRef = resolveDocsLocale(refUrl.searchParams, i18n);
338
+ if (fromRef)
339
+ return fromRef;
340
+ }
341
+ catch {
342
+ // ignore
343
+ }
344
+ }
345
+ return i18n.defaultLocale;
346
+ }
347
+ function resolveContextFromRequest(request) {
348
+ const locale = resolveLocaleFromRequest(request);
349
+ const url = new URL(request.url);
350
+ const pathnameParam = url.searchParams.get("pathname");
351
+ const referrer = request.headers.get("referer") ?? request.headers.get("referrer");
352
+ const refPath = referrer ? new URL(referrer).pathname : undefined;
353
+ const pathname = pathnameParam ?? refPath ?? `/${entry}`;
354
+ return resolveContextFromPath(pathname, locale);
355
+ }
356
+ // ─── Unified load (tree + page content in one call) ────────
357
+ async function load(pathname) {
358
+ let url;
359
+ try {
360
+ url = new URL(pathname);
361
+ }
362
+ catch {
363
+ url = new URL(pathname, "http://localhost");
364
+ }
365
+ const locale = resolveDocsLocale(url.searchParams, i18n) ?? i18n?.defaultLocale;
366
+ const ctx = resolveContextFromPath(url.pathname, locale);
367
+ const tree = preloaded
368
+ ? navTreeFromMap(preloaded, ctx.dirPrefix, entry, ordering)
369
+ : loadDocsNavTree(ctx.contentDirAbs, entry, ordering);
370
+ const flatPages = flattenNavTree(tree);
371
+ const slug = ctx.slug;
372
+ const isIndex = slug === "";
373
+ let raw;
374
+ let relPath;
375
+ let lastModified;
376
+ if (preloaded) {
377
+ const result = findPageInMap(preloaded, ctx.dirPrefix, slug);
378
+ if (!result) {
379
+ const err = new Error(`Page not found: /${entry}/${slug}`);
380
+ err.status = 404;
381
+ throw err;
382
+ }
383
+ raw = result.raw;
384
+ relPath = result.relPath;
385
+ lastModified = new Date().toLocaleDateString("en-US", {
386
+ year: "numeric",
387
+ month: "long",
388
+ day: "numeric",
389
+ });
390
+ }
391
+ else {
392
+ let filePath = null;
393
+ relPath = "";
394
+ if (isIndex) {
395
+ for (const name of ["page.md", "page.mdx", "index.md"]) {
396
+ const candidate = path.join(ctx.contentDirAbs, name);
397
+ if (fs.existsSync(candidate)) {
398
+ filePath = candidate;
399
+ relPath = name;
400
+ break;
401
+ }
402
+ }
403
+ }
404
+ else {
405
+ const candidates = [
406
+ path.join(ctx.contentDirAbs, slug, "page.md"),
407
+ path.join(ctx.contentDirAbs, slug, "page.mdx"),
408
+ path.join(ctx.contentDirAbs, slug, "index.md"),
409
+ path.join(ctx.contentDirAbs, slug, "index.svx"),
410
+ path.join(ctx.contentDirAbs, `${slug}.md`),
411
+ path.join(ctx.contentDirAbs, `${slug}.svx`),
412
+ ];
413
+ for (const candidate of candidates) {
414
+ if (fs.existsSync(candidate)) {
415
+ filePath = candidate;
416
+ relPath = path.relative(ctx.contentDirAbs, candidate);
417
+ break;
418
+ }
419
+ }
420
+ }
421
+ if (!filePath) {
422
+ const err = new Error(`Page not found: /${entry}/${slug}`);
423
+ err.status = 404;
424
+ throw err;
425
+ }
426
+ raw = fs.readFileSync(filePath, "utf-8");
427
+ const stat = fs.statSync(filePath);
428
+ lastModified = stat.mtime.toLocaleDateString("en-US", {
429
+ year: "numeric",
430
+ month: "long",
431
+ day: "numeric",
432
+ });
433
+ }
434
+ const { data, content } = matter(raw);
435
+ const html = await renderMarkdown(content);
436
+ const currentUrl = isIndex ? `/${entry}` : `/${entry}/${slug}`;
437
+ const currentIndex = flatPages.findIndex((p) => p.url === currentUrl);
438
+ const previousPage = currentIndex > 0 ? flatPages[currentIndex - 1] : null;
439
+ const nextPage = currentIndex < flatPages.length - 1 ? flatPages[currentIndex + 1] : null;
440
+ let editOnGithub;
441
+ if (githubRepo && githubContentPath) {
442
+ const trimmed = githubContentPath.replace(/\/+$/, "");
443
+ const localePrefix = ctx.locale ? `${ctx.locale}/` : "";
444
+ editOnGithub = `${githubRepo}/blob/${githubBranch}/${trimmed}/${localePrefix}${relPath}`;
445
+ }
446
+ const fallbackTitle = isIndex
447
+ ? "Documentation"
448
+ : (slug.split("/").pop()?.replace(/-/g, " ") ?? "Documentation");
449
+ return {
450
+ tree,
451
+ flatPages,
452
+ title: data.title ?? fallbackTitle,
453
+ description: data.description,
454
+ html,
455
+ rawMarkdown: content,
456
+ entry,
457
+ locale: ctx.locale,
458
+ ...(isIndex ? {} : { slug }),
459
+ previousPage,
460
+ nextPage,
461
+ editOnGithub,
462
+ lastModified,
463
+ };
464
+ }
465
+ // ─── Search index ──────────────────────────────────────────
466
+ const searchIndexByEntry = new Map();
467
+ function getSearchIndex(ctx) {
468
+ const key = ctx.locale ?? "__default__";
469
+ const cached = searchIndexByEntry.get(key);
470
+ if (cached)
471
+ return cached;
472
+ const index = preloaded
473
+ ? searchIndexFromMap(preloaded, ctx.dirPrefix, entry)
474
+ : loadDocsContent(ctx.contentDirAbs, entry);
475
+ searchIndexByEntry.set(key, index);
476
+ return index;
477
+ }
478
+ function searchByQuery(query, ctx) {
479
+ const index = getSearchIndex(ctx);
480
+ return index
481
+ .map((page) => {
482
+ const titleMatch = page.title.toLowerCase().includes(query) ? 10 : 0;
483
+ const words = query.split(/\s+/);
484
+ const contentMatch = words.reduce((score, word) => {
485
+ return score + (page.content.toLowerCase().includes(word) ? 1 : 0);
486
+ }, 0);
487
+ return { ...page, score: titleMatch + contentMatch };
488
+ })
489
+ .filter((r) => r.score > 0)
490
+ .sort((a, b) => b.score - a.score);
491
+ }
492
+ // ─── llms.txt content builder ────────────────────────────────
493
+ const llmsSiteTitle = typeof config.nav === "object" &&
494
+ typeof config.nav?.title === "string"
495
+ ? config.nav.title
496
+ : "Documentation";
497
+ const llmsTxtConfig = config.llmsTxt;
498
+ const llmsBaseUrl = typeof llmsTxtConfig === "object" ? (llmsTxtConfig.baseUrl ?? "") : "";
499
+ const llmsTitle = typeof llmsTxtConfig === "object" ? (llmsTxtConfig.siteTitle ?? llmsSiteTitle) : llmsSiteTitle;
500
+ const llmsDesc = typeof llmsTxtConfig === "object" ? llmsTxtConfig.siteDescription : undefined;
501
+ const llmsCache = new Map();
502
+ function getLlmsContent(ctx) {
503
+ const key = ctx.locale ?? "__default__";
504
+ const cached = llmsCache.get(key);
505
+ if (cached)
506
+ return cached;
507
+ const pages = getSearchIndex(ctx);
508
+ let llmsTxt = `# ${llmsTitle}\n\n`;
509
+ let llmsFullTxt = `# ${llmsTitle}\n\n`;
510
+ if (llmsDesc) {
511
+ llmsTxt += `> ${llmsDesc}\n\n`;
512
+ llmsFullTxt += `> ${llmsDesc}\n\n`;
513
+ }
514
+ llmsTxt += `## Pages\n\n`;
515
+ for (const page of pages) {
516
+ llmsTxt += `- [${page.title}](${llmsBaseUrl}${page.url})`;
517
+ if (page.description)
518
+ llmsTxt += `: ${page.description}`;
519
+ llmsTxt += `\n`;
520
+ llmsFullTxt += `## ${page.title}\n\n`;
521
+ llmsFullTxt += `URL: ${llmsBaseUrl}${page.url}\n\n`;
522
+ if (page.description)
523
+ llmsFullTxt += `${page.description}\n\n`;
524
+ llmsFullTxt += `${page.content}\n\n---\n\n`;
525
+ }
526
+ const next = { llmsTxt, llmsFullTxt };
527
+ llmsCache.set(key, next);
528
+ return next;
529
+ }
530
+ // ─── GET /api/docs?query=… | ?format=llms | ?format=llms-full ──
531
+ function GET(context) {
532
+ const ctx = resolveContextFromRequest(context.request);
533
+ const url = new URL(context.request.url);
534
+ const format = url.searchParams.get("format");
535
+ if (format === "llms" || format === "llms-full") {
536
+ const llmsContent = getLlmsContent(ctx);
537
+ return new Response(format === "llms-full" ? llmsContent.llmsFullTxt : llmsContent.llmsTxt, {
538
+ headers: {
539
+ "Content-Type": "text/plain; charset=utf-8",
540
+ "Cache-Control": "public, max-age=3600",
541
+ },
542
+ });
543
+ }
544
+ const query = url.searchParams.get("query")?.toLowerCase().trim();
545
+ if (!query) {
546
+ return new Response(JSON.stringify([]), {
547
+ headers: { "Content-Type": "application/json" },
548
+ });
549
+ }
550
+ const results = searchByQuery(query, ctx)
551
+ .slice(0, 10)
552
+ .map(({ title, url, description }) => ({
553
+ content: title,
554
+ url,
555
+ description,
556
+ }));
557
+ return new Response(JSON.stringify(results), {
558
+ headers: { "Content-Type": "application/json" },
559
+ });
560
+ }
561
+ // ─── POST /api/docs — AI chat with RAG ────────────────────
562
+ const projectName = (typeof config.nav?.title === "string"
563
+ ? config.nav.title
564
+ : null);
565
+ const packageName = aiConfig.packageName;
566
+ const docsUrl = aiConfig.docsUrl;
567
+ function buildDefaultSystemPrompt() {
568
+ const lines = [
569
+ `You are a helpful documentation assistant${projectName ? ` for ${projectName}` : ""}.`,
570
+ "Answer questions based on the provided documentation context.",
571
+ "Be concise and accurate. If the answer is not in the context, say so honestly.",
572
+ "Use markdown formatting for code examples and links.",
573
+ ];
574
+ if (packageName) {
575
+ lines.push(`When showing import examples, always use "${packageName}" as the package name.`);
576
+ }
577
+ if (docsUrl) {
578
+ lines.push(`When linking to documentation pages, use "${docsUrl}" as the base URL (e.g. ${docsUrl}/docs/get-started).`);
579
+ }
580
+ return lines.join(" ");
581
+ }
582
+ const DEFAULT_SYSTEM_PROMPT = buildDefaultSystemPrompt();
583
+ async function POST(context) {
584
+ if (!aiConfig.enabled) {
585
+ return new Response(JSON.stringify({
586
+ error: "AI is not enabled. Set `ai: { enabled: true }` in your docs config to enable it.",
587
+ }), { status: 404, headers: { "Content-Type": "application/json" } });
588
+ }
589
+ const resolvedKey = aiConfig.apiKey ??
590
+ (typeof import.meta !== "undefined" ? import.meta.env?.OPENAI_API_KEY : undefined) ??
591
+ (typeof process !== "undefined" ? process.env?.OPENAI_API_KEY : undefined);
592
+ if (!resolvedKey) {
593
+ return new Response(JSON.stringify({
594
+ error: "AI is enabled but no API key was found. Set `apiKey` in your docs config `ai` section or add OPENAI_API_KEY to your environment.",
595
+ }), { status: 500, headers: { "Content-Type": "application/json" } });
596
+ }
597
+ const ctx = resolveContextFromRequest(context.request);
598
+ let body;
599
+ try {
600
+ body = await context.request.json();
601
+ }
602
+ catch {
603
+ return new Response(JSON.stringify({ error: "Invalid JSON body. Expected { messages: [...] }" }), { status: 400, headers: { "Content-Type": "application/json" } });
604
+ }
605
+ const messages = body.messages;
606
+ if (!Array.isArray(messages) || messages.length === 0) {
607
+ return new Response(JSON.stringify({ error: "messages array is required and must not be empty." }), { status: 400, headers: { "Content-Type": "application/json" } });
608
+ }
609
+ const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
610
+ if (!lastUserMessage) {
611
+ return new Response(JSON.stringify({ error: "At least one user message is required." }), {
612
+ status: 400,
613
+ headers: { "Content-Type": "application/json" },
614
+ });
615
+ }
616
+ const maxResults = aiConfig.maxResults ?? 5;
617
+ const scored = searchByQuery(lastUserMessage.content.toLowerCase(), ctx).slice(0, maxResults);
618
+ const contextParts = scored.map((doc) => `## ${doc.title}\nURL: ${doc.url}\n${doc.description ? `Description: ${doc.description}\n` : ""}\n${doc.content}`);
619
+ const ragContext = contextParts.join("\n\n---\n\n");
620
+ const systemPrompt = aiConfig.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
621
+ const systemMessage = {
622
+ role: "system",
623
+ content: ragContext
624
+ ? `${systemPrompt}\n\n---\n\nDocumentation context:\n\n${ragContext}`
625
+ : systemPrompt,
626
+ };
627
+ const llmMessages = [
628
+ systemMessage,
629
+ ...messages.filter((m) => m.role !== "system"),
630
+ ];
631
+ const requestedModel = typeof body.model === "string" && body.model.trim().length > 0
632
+ ? body.model.trim()
633
+ : undefined;
634
+ const resolved = resolveAIModelAndProvider(aiConfig, requestedModel);
635
+ const finalKey = resolved.apiKey ?? resolvedKey;
636
+ const llmResponse = await fetch(`${resolved.baseUrl}/chat/completions`, {
637
+ method: "POST",
638
+ headers: {
639
+ "Content-Type": "application/json",
640
+ Authorization: `Bearer ${finalKey}`,
641
+ },
642
+ body: JSON.stringify({ model: resolved.model, stream: true, messages: llmMessages }),
643
+ });
644
+ if (!llmResponse.ok) {
645
+ const errText = await llmResponse.text().catch(() => "Unknown error");
646
+ return new Response(JSON.stringify({ error: `LLM API error (${llmResponse.status}): ${errText}` }), { status: 502, headers: { "Content-Type": "application/json" } });
647
+ }
648
+ return new Response(llmResponse.body, {
649
+ headers: {
650
+ "Content-Type": "text/event-stream",
651
+ "Cache-Control": "no-cache",
652
+ Connection: "keep-alive",
653
+ },
654
+ });
655
+ }
656
+ return { load, GET, POST };
657
+ }
658
+ //# sourceMappingURL=server.js.map