@farming-labs/farmjs 0.2.97 → 0.2.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -1,10 +1,46 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { createRequire } from "node:module";
4
+ import { PassThrough } from "node:stream";
3
5
  import matter from "gray-matter";
6
+ import { createElement } from "react";
7
+ import * as ReactDOMServer from "react-dom/server";
4
8
  import { applySidebarFolderIndexBehavior, buildDocsAskAIContext, buildDocsSearchFacets, buildDocsAgentDiscoverySpec, buildDocsConfigMap, buildDocsDiagnostics, createDocsContentChangeFeed, createDocsContentChangesHttpResponse, createDocsCacheableResponse, createDocsStandardsDiscoveryResponse, createDocsMarkdownResponse, createDocsRobotsResponse, createDocsSitemapResponse, createDocsAgentTraceContext, createDocsAgentTraceId, emitDocsAgentTraceEvent, emitDocsAnalyticsEvent, emitDocsTelemetryAgentSurfaceEvent, emitDocsTelemetryProjectEvent, formatDocsAskAIPackageHints, findDocsMarkdownPage, getDocsLlmsTxtMaxCharsIssue, getDocsAgentManifestLinkHeader, getDocsDiscoveryLinkHeader, isDocsAgentDiscoveryRequest, isDocsContentChangesRequest, isDocsAgentsRequest, isDocsConfigRequest, isDocsDiagnosticsRequest, isDocsSkillRequest, normalizeDocsRelated, normalizePageAgentFrontmatter, parseDocsAgentFeedbackData, performDocsSearch, performDocsSearchWithMetadata, renderDocsMarkdownDocument, renderDocsLlmsTxt, renderDocsAgentsDocument, renderDocsSkillDocument, readDocsSitemapManifestFromContentMap, stripGeneratedAgentProvenance, resolveDocsAudienceMdxContent, resolveDocsAgentFeedbackConfig, resolveDocsAgentFeedbackRequest, resolvePageSidebarFolderIndexBehavior, resolveAskAISearchRequestConfig, resolveDocsSearchAudience, resolveDocsSearchError, resolveDocsSearchRequest, resolveSearchRequestConfig, resolveDocsI18n, resolveDocsLlmsTxtRequest, resolveDocsLocale, resolveDocsMarkdownRequest, resolveDocsMetadataBaseUrl, resolveDocsContentChangesConfig, resolveDocsRequestApiRoute, resolveDocsRetrievalLastModified, resolveDocsPublishedAgentSkill, resolveDocsStandardsDiscoveryRequest, resolveDocsPath, resolvePageReadingTime, resolveReadingTimeOptions, resolveDocsSitemapPageLastmod, resolveDocsAgentsFormat, resolveDocsSkillFormat, inferDocsTelemetryAgentSurface, renderDocsPageStructuredDataJson, selectDocsLlmsTxtContent, validateDocsAgentFeedbackPayload, } from "@farming-labs/docs";
5
9
  import { buildApiReferenceOpenApiDocumentAsync, createDocsMcpHttpHandler, readDocsSitemapManifest, resolveApiReferenceOpenApiDiscovery, resolveDocsMcpConfig, resolveConfiguredAgentSkills, } from "@farming-labs/docs/server";
6
10
  import { loadDocsNavTree, loadDocsContent, flattenNavTree, normalizeDocsFrontmatterLastmod, } from "./content.js";
7
11
  export { createFarmjsApiReference } from "./api-reference.js";
12
+ const require = createRequire(import.meta.url);
13
+ const FARM_DOCS_BROWSER_CSS_PATH = "/__farm_docs/browser.css";
14
+ function loadFarmDocsBrowserCss() {
15
+ return fs.readFileSync(require.resolve("@farming-labs/theme/browser/css"), "utf8");
16
+ }
17
+ async function renderFarmDocsMarkup(element) {
18
+ const server = ReactDOMServer;
19
+ if (server.renderToReadableStream) {
20
+ const stream = await server.renderToReadableStream(element);
21
+ await stream.allReady;
22
+ return new Response(stream).text();
23
+ }
24
+ return new Promise((resolve, reject) => {
25
+ const destination = new PassThrough();
26
+ const chunks = [];
27
+ destination.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
28
+ destination.on("error", reject);
29
+ destination.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
30
+ const renderer = server.renderToPipeableStream(element, {
31
+ onAllReady() {
32
+ renderer.pipe(destination);
33
+ },
34
+ onShellError(error) {
35
+ reject(error);
36
+ },
37
+ onError(error) {
38
+ if (!destination.readableEnded)
39
+ destination.destroy(error);
40
+ },
41
+ });
42
+ });
43
+ }
8
44
  function isApiReferenceOpenApiRequest(url) {
9
45
  return url.searchParams.get("format")?.trim() === "openapi";
10
46
  }
@@ -37,6 +73,64 @@ function safeUrlOrigin(value) {
37
73
  return value;
38
74
  }
39
75
  }
76
+ function navigationItemName(item) {
77
+ return item.label?.trim() || item.title?.trim() || item.slug?.trim() || "Documentation";
78
+ }
79
+ function navigationItemUrl(item, entry) {
80
+ if (item.href?.trim())
81
+ return item.href.trim();
82
+ if (typeof item.slug !== "string")
83
+ return undefined;
84
+ const slug = item.slug.replace(/^\/+|\/+$/g, "");
85
+ return slug ? `/${entry}/${slug}` : `/${entry}`;
86
+ }
87
+ function navTreeFromNavigation(config, entry) {
88
+ const configured = config.navigation?.sidebar;
89
+ if (!Array.isArray(configured) || configured.length === 0)
90
+ return null;
91
+ const defaultOpen = config.theme?.name === "fumadocs-pixel-border";
92
+ function toNode(item) {
93
+ const children = item.children ?? item.items ?? [];
94
+ const name = navigationItemName(item);
95
+ const url = navigationItemUrl(item, entry);
96
+ if (children.length > 0) {
97
+ const mappedChildren = children.map(toNode).filter((node) => node !== null);
98
+ return {
99
+ type: "folder",
100
+ name,
101
+ ...(defaultOpen ? { defaultOpen: true } : {}),
102
+ ...(item.icon ? { icon: item.icon } : {}),
103
+ ...(url
104
+ ? {
105
+ index: {
106
+ type: "page",
107
+ name,
108
+ url,
109
+ ...(item.icon ? { icon: item.icon } : {}),
110
+ },
111
+ }
112
+ : {}),
113
+ children: mappedChildren,
114
+ };
115
+ }
116
+ if (!url)
117
+ return null;
118
+ return {
119
+ type: "page",
120
+ name,
121
+ url,
122
+ ...(item.icon ? { icon: item.icon } : {}),
123
+ };
124
+ }
125
+ return {
126
+ name: typeof config.nav?.title === "string" && config.nav.title.trim()
127
+ ? config.nav.title.trim()
128
+ : "Docs",
129
+ children: configured
130
+ .map((item) => toNode(item))
131
+ .filter((node) => node !== null),
132
+ };
133
+ }
40
134
  function resolvePreloadedContent(value) {
41
135
  if (!value || typeof value !== "object")
42
136
  return null;
@@ -74,6 +168,17 @@ function stripMarkdownText(content) {
74
168
  .replace(/\n{3,}/g, "\n\n")
75
169
  .trim();
76
170
  }
171
+ function markdownStartsWithDescription(content, description) {
172
+ if (!description?.trim())
173
+ return false;
174
+ const withoutLeadingTitle = content
175
+ .trimStart()
176
+ .replace(/^#\s+[^\r\n]+(?:\r?\n|$)/, "")
177
+ .trimStart();
178
+ const firstBlock = withoutLeadingTitle.split(/\r?\n\s*\r?\n/, 1)[0] ?? "";
179
+ const normalize = (value) => stripMarkdownText(value).replace(/\s+/g, " ").trim();
180
+ return normalize(firstBlock) === normalize(description);
181
+ }
77
182
  function normalizePathSegment(value) {
78
183
  return value.replace(/^\/+|\/+$/g, "");
79
184
  }
@@ -400,7 +505,7 @@ export function createDocsServer(config) {
400
505
  ? githubRaw.replace(/\/$/, "")
401
506
  : githubRaw?.url.replace(/\/$/, "");
402
507
  const githubBranch = typeof githubRaw === "object" ? (githubRaw.branch ?? "main") : "main";
403
- const githubContentPath = typeof githubRaw === "object" ? githubRaw.directory?.replace(/^\/|\/$/g, "") : undefined;
508
+ const githubDirectory = typeof githubRaw === "object" ? githubRaw.directory?.replace(/^\/|\/$/g, "") : undefined;
404
509
  const readingTimeOptions = resolveReadingTimeOptions(config.readingTime);
405
510
  const aiConfig = { enabled: false, ...config.ai };
406
511
  if (config.apiKey && !aiConfig.apiKey) {
@@ -471,9 +576,11 @@ export function createDocsServer(config) {
471
576
  async function load({ pathname, locale }) {
472
577
  const resolvedLocale = (i18n && locale && i18n.locales.includes(locale) ? locale : undefined) ?? i18n?.defaultLocale;
473
578
  const ctx = resolveContextFromPath(pathname, resolvedLocale);
474
- const tree = applySidebarFolderIndexBehavior(preloaded
475
- ? navTreeFromMap(preloaded, ctx.dirPrefix, entry, ordering)
476
- : loadDocsNavTree(ctx.contentDirAbs, entry, ordering), {
579
+ const configuredNavigation = navTreeFromNavigation(config, entry);
580
+ const tree = applySidebarFolderIndexBehavior(configuredNavigation ??
581
+ (preloaded
582
+ ? navTreeFromMap(preloaded, ctx.dirPrefix, entry, ordering)
583
+ : loadDocsNavTree(ctx.contentDirAbs, entry, ordering)), {
477
584
  sidebar: config.sidebar,
478
585
  defaultBehavior: "link",
479
586
  });
@@ -560,16 +667,24 @@ export function createDocsServer(config) {
560
667
  const previousPage = currentIndex > 0 ? flatPages[currentIndex - 1] : null;
561
668
  const nextPage = currentIndex < flatPages.length - 1 ? flatPages[currentIndex + 1] : null;
562
669
  let editOnGithub;
563
- if (githubRepo && githubContentPath) {
564
- const trimmed = githubContentPath.replace(/\/+$/, "");
565
- const localePrefix = ctx.locale ? `${ctx.locale}/` : "";
566
- editOnGithub = `${githubRepo}/blob/${githubBranch}/${trimmed}/${localePrefix}${toPosixPath(relPath)}`;
670
+ if (githubRepo) {
671
+ const contentPath = normalizePathSegment(path.isAbsolute(ctx.contentDirRel)
672
+ ? toPosixPath(path.relative(rootDir, ctx.contentDirRel))
673
+ : toPosixPath(ctx.contentDirRel));
674
+ const configuredDirectory = normalizePathSegment(githubDirectory ?? "");
675
+ const sourceDirectory = configuredDirectory &&
676
+ (contentPath === configuredDirectory || contentPath.startsWith(`${configuredDirectory}/`))
677
+ ? contentPath
678
+ : joinPathParts(configuredDirectory, contentPath);
679
+ const sourcePath = joinPathParts(sourceDirectory, toPosixPath(relPath));
680
+ editOnGithub = `${githubRepo}/edit/${githubBranch}/${sourcePath}`;
567
681
  }
568
682
  const fallbackTitle = isIndex
569
683
  ? "Documentation"
570
684
  : (slug.split("/").pop()?.replace(/-/g, " ") ?? "Documentation");
571
685
  const title = data.title ?? fallbackTitle;
572
686
  const description = data.description;
687
+ const descriptionInBody = markdownStartsWithDescription(humanRawContent, description);
573
688
  const structuredData = renderDocsPageStructuredDataJson({
574
689
  title,
575
690
  description,
@@ -585,6 +700,7 @@ export function createDocsServer(config) {
585
700
  url: currentUrl,
586
701
  title,
587
702
  description,
703
+ descriptionInBody,
588
704
  rawContent: humanRawContent,
589
705
  readingTime,
590
706
  readingTimeFormat: readingTimeOptions.format,
@@ -1598,9 +1714,10 @@ export function createDocsServer(config) {
1598
1714
  },
1599
1715
  getNavigation(locale) {
1600
1716
  const ctx = resolveContextFromPath(`/${entry}`, resolveLocaleForMcp(locale));
1601
- return applySidebarFolderIndexBehavior(preloaded
1602
- ? navTreeFromMap(preloaded, ctx.dirPrefix, entry, ordering)
1603
- : loadDocsNavTree(ctx.contentDirAbs, entry, ordering), {
1717
+ return applySidebarFolderIndexBehavior(navTreeFromNavigation(config, entry) ??
1718
+ (preloaded
1719
+ ? navTreeFromMap(preloaded, ctx.dirPrefix, entry, ordering)
1720
+ : loadDocsNavTree(ctx.contentDirAbs, entry, ordering)), {
1604
1721
  sidebar: config.sidebar,
1605
1722
  defaultBehavior: "link",
1606
1723
  });
@@ -1722,4 +1839,193 @@ export function createDocsServer(config) {
1722
1839
  }
1723
1840
  return { load, GET, HEAD, POST, MCP, handle };
1724
1841
  }
1842
+ function escapeDocumentText(value) {
1843
+ return String(value ?? "")
1844
+ .replace(/&/g, "&amp;")
1845
+ .replace(/</g, "&lt;")
1846
+ .replace(/>/g, "&gt;")
1847
+ .replace(/"/g, "&quot;");
1848
+ }
1849
+ function serializeInlineJson(value) {
1850
+ return JSON.stringify(value)
1851
+ .replace(/</g, "\\u003c")
1852
+ .replace(/>/g, "\\u003e")
1853
+ .replace(/\u2028/g, "\\u2028")
1854
+ .replace(/\u2029/g, "\\u2029");
1855
+ }
1856
+ function createClientDocsConfig(config) {
1857
+ const { apiKey: _apiKey, rootDir: _rootDir, contentDir: _contentDir, _preloadedContent, _preloadedAgentSkills, socialImage, ...clientConfig } = config;
1858
+ const ai = clientConfig.ai && typeof clientConfig.ai === "object"
1859
+ ? {
1860
+ ...clientConfig.ai,
1861
+ apiKey: undefined,
1862
+ providers: clientConfig.ai.providers && typeof clientConfig.ai.providers === "object"
1863
+ ? Object.fromEntries(Object.entries(clientConfig.ai.providers).map(([name, provider]) => [
1864
+ name,
1865
+ provider && typeof provider === "object"
1866
+ ? { ...provider, apiKey: undefined }
1867
+ : provider,
1868
+ ]))
1869
+ : clientConfig.ai.providers,
1870
+ }
1871
+ : clientConfig.ai;
1872
+ return {
1873
+ ...clientConfig,
1874
+ ...(ai ? { ai } : {}),
1875
+ ...(socialImage === false ? { socialImage: false } : {}),
1876
+ };
1877
+ }
1878
+ function normalizedPublicRoute(value, fallback) {
1879
+ const normalized = typeof value === "string" && value.trim() ? value.trim() : fallback;
1880
+ if (normalized === "/")
1881
+ return "/";
1882
+ return `/${normalized.replace(/^\/+|\/+$/g, "")}`;
1883
+ }
1884
+ function isHtmlDocsPageRequest(config, request) {
1885
+ const method = request.method.toUpperCase();
1886
+ if (method !== "GET" && method !== "HEAD")
1887
+ return false;
1888
+ const url = new URL(request.url);
1889
+ const pathname = url.pathname === "/" ? "/" : url.pathname.replace(/\/+$/, "");
1890
+ const docsPath = normalizedPublicRoute(config.docsPath, `/${config.entry ?? "docs"}`);
1891
+ const isPagePath = docsPath === "/"
1892
+ ? !pathname.endsWith(".md")
1893
+ : pathname === docsPath || pathname.startsWith(`${docsPath}/`);
1894
+ if (!isPagePath || pathname.endsWith(".md"))
1895
+ return false;
1896
+ const accept = request.headers.get("accept")?.toLowerCase() ?? "";
1897
+ return !accept.includes("text/markdown");
1898
+ }
1899
+ function runtimeFontClasses(fonts) {
1900
+ return Array.from(new Set([fonts?.body?.className, fonts?.body?.variable, fonts?.code?.variable].filter((value) => typeof value === "string" && value.length > 0))).join(" ");
1901
+ }
1902
+ function runtimeFontStyle(fonts) {
1903
+ const declarations = [];
1904
+ const bodyFontFamily = fonts?.body?.style?.fontFamily?.trim();
1905
+ const codeFontFamily = fonts?.code?.style?.fontFamily?.trim();
1906
+ if (bodyFontFamily) {
1907
+ declarations.push(`--font-sans:${bodyFontFamily}`, `--fd-font-sans:${bodyFontFamily}`);
1908
+ }
1909
+ if (codeFontFamily) {
1910
+ declarations.push(`--font-mono:${codeFontFamily}`, `--fd-font-mono:${codeFontFamily}`);
1911
+ }
1912
+ return declarations.join(";");
1913
+ }
1914
+ function runtimeFontPreloads(fonts) {
1915
+ const seen = new Set();
1916
+ const values = [fonts?.body, fonts?.code].flatMap((font) => font?.preloads ?? []);
1917
+ const unique = values.filter(({ href }) => {
1918
+ if (seen.has(href))
1919
+ return false;
1920
+ seen.add(href);
1921
+ return true;
1922
+ });
1923
+ return {
1924
+ markup: unique
1925
+ .map(({ href, type }) => `<link rel="preload" href="${escapeDocumentText(href)}" as="font" type="${escapeDocumentText(type)}" crossorigin>`)
1926
+ .join("\n "),
1927
+ header: unique
1928
+ .map(({ href, type }) => `<${href}>; rel=preload; as=font; type=${type}; crossorigin`)
1929
+ .join(", "),
1930
+ };
1931
+ }
1932
+ function renderFarmDocsDocument(input) {
1933
+ const { config, data } = input;
1934
+ const description = data.description ?? config.metadata?.description;
1935
+ const themeName = config.theme?.name ?? "default";
1936
+ const favicon = typeof config.favicon === "string" ? config.favicon : undefined;
1937
+ const rootClasses = runtimeFontClasses(input.fonts);
1938
+ const rootStyle = runtimeFontStyle(input.fonts);
1939
+ const fontPreloads = runtimeFontPreloads(input.fonts).markup;
1940
+ return `<!doctype html>
1941
+ <html class="dark" lang="${escapeDocumentText(data.locale ?? "en")}" data-docs-theme="${escapeDocumentText(themeName)}" data-farm-docs-runtime="true">
1942
+ <head>
1943
+ <meta charset="utf-8">
1944
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1945
+ <meta name="generator" content="@farming-labs/farmjs">
1946
+ <title>${escapeDocumentText(data.title)}</title>
1947
+ ${description ? `<meta name="description" content="${escapeDocumentText(description)}">` : ""}
1948
+ ${favicon ? `<link rel="icon" href="${escapeDocumentText(favicon)}">` : ""}
1949
+ ${fontPreloads}
1950
+ <link rel="stylesheet" href="${FARM_DOCS_BROWSER_CSS_PATH}">
1951
+ ${input.stylesheets
1952
+ .map((href) => `<link rel="stylesheet" href="${escapeDocumentText(href)}">`)
1953
+ .join("\n ")}
1954
+ </head>
1955
+ <body>
1956
+ <div id="root"><div id="farm-docs-root"${rootClasses ? ` class="${escapeDocumentText(rootClasses)}"` : ""}${rootStyle ? ` style="${escapeDocumentText(rootStyle)}"` : ""}>${input.markup}</div></div>
1957
+ <script>window.__FARM_DOCS_ADAPTER__={config:${serializeInlineJson(createClientDocsConfig(config))},data:${serializeInlineJson(data)}};</script>
1958
+ <script type="module" src="${escapeDocumentText(input.clientEntry)}"></script>
1959
+ </body>
1960
+ </html>`;
1961
+ }
1962
+ /**
1963
+ * Create the complete Farm docs runtime owned by this adapter.
1964
+ *
1965
+ * Farm core supplies host assets and module loading; this adapter owns route
1966
+ * classification, docs APIs, content loading, React rendering, and hydration
1967
+ * state for every documentation request.
1968
+ */
1969
+ export function createFarmDocsRuntimeHandler(config, options = {}) {
1970
+ const runtimeConfig = {
1971
+ ...config,
1972
+ rootDir: options.rootDir ?? config.rootDir ?? process.cwd(),
1973
+ };
1974
+ const docsServer = createDocsServer(runtimeConfig);
1975
+ return async function handleFarmDocsRuntimeRequest(request) {
1976
+ const url = new URL(request.url);
1977
+ if (url.pathname === FARM_DOCS_BROWSER_CSS_PATH) {
1978
+ return new Response(request.method.toUpperCase() === "HEAD" ? null : loadFarmDocsBrowserCss(), {
1979
+ status: 200,
1980
+ headers: {
1981
+ "Content-Type": "text/css; charset=utf-8",
1982
+ "Cache-Control": "public, max-age=31536000, immutable",
1983
+ },
1984
+ });
1985
+ }
1986
+ if (!isHtmlDocsPageRequest(runtimeConfig, request)) {
1987
+ return docsServer.handle(request);
1988
+ }
1989
+ let data;
1990
+ try {
1991
+ data = await docsServer.load({ pathname: url.pathname });
1992
+ }
1993
+ catch (error) {
1994
+ const status = error &&
1995
+ typeof error === "object" &&
1996
+ typeof error.status === "number"
1997
+ ? error.status
1998
+ : 500;
1999
+ if (status === 404)
2000
+ return null;
2001
+ throw error;
2002
+ }
2003
+ const reactModule = options.loadReactModule
2004
+ ? await options.loadReactModule()
2005
+ : (await import("./react.js"));
2006
+ const markup = await renderFarmDocsMarkup(createElement(reactModule.FarmDocsPage, {
2007
+ config: runtimeConfig,
2008
+ data,
2009
+ }));
2010
+ const fonts = await options.resolveLayoutFonts?.(url.pathname);
2011
+ const fontPreloadHeader = runtimeFontPreloads(fonts).header;
2012
+ const html = renderFarmDocsDocument({
2013
+ config: runtimeConfig,
2014
+ data,
2015
+ markup,
2016
+ clientEntry: options.clientEntry ?? "/farm-client.js",
2017
+ stylesheets: options.stylesheets ?? [],
2018
+ fonts,
2019
+ });
2020
+ return new Response(request.method.toUpperCase() === "HEAD" ? null : html, {
2021
+ status: 200,
2022
+ headers: {
2023
+ "Content-Type": "text/html; charset=utf-8",
2024
+ "Cache-Control": "public, s-maxage=60, stale-while-revalidate=300",
2025
+ "X-Farm-Docs-Adapter": "@farming-labs/farmjs",
2026
+ ...(fontPreloadHeader ? { Link: fontPreloadHeader } : {}),
2027
+ },
2028
+ });
2029
+ };
2030
+ }
1725
2031
  //# sourceMappingURL=server.js.map