@farming-labs/farmjs 0.2.98 → 0.2.101

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