@fumadocs/cli 1.7.4 → 1.7.6

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 (114) hide show
  1. package/dist/{ai-BoUGzPvv.js → ai-CvrtLYh1.js} +8 -6
  2. package/dist/ai-CvrtLYh1.js.map +1 -0
  3. package/dist/all-bAINnjNQ.js +26 -0
  4. package/dist/{all-BmTB7dab.js.map → all-bAINnjNQ.js.map} +1 -1
  5. package/dist/codemod/index.d.ts +1 -1
  6. package/dist/codemod/index.js +1 -2
  7. package/dist/{codemod-auol-p4R.js → codemod-Wc9nRqrw.js} +60 -39
  8. package/dist/codemod-Wc9nRqrw.js.map +1 -0
  9. package/dist/{config-Bw0kmF01.js → config-BpiQyqBs.js} +13 -8
  10. package/dist/config-BpiQyqBs.js.map +1 -0
  11. package/dist/{config-Bfg4yG8S.d.ts → config-Z-4b_Y_C.d.ts} +1 -3
  12. package/dist/config-Z-4b_Y_C.d.ts.map +1 -0
  13. package/dist/config.d.ts +1 -1
  14. package/dist/config.js +1 -2
  15. package/dist/{docs-qRGwd9Yp.js → docs---dy-BSm.js} +286 -32
  16. package/dist/docs---dy-BSm.js.map +1 -0
  17. package/dist/{epub-DbOh5Q2D.js → epub-BKvZ4ppz.js} +12 -13
  18. package/dist/epub-BKvZ4ppz.js.map +1 -0
  19. package/dist/features/ai.d.ts +2 -3
  20. package/dist/features/ai.d.ts.map +1 -1
  21. package/dist/features/ai.js +1 -2
  22. package/dist/features/all.d.ts +1 -3
  23. package/dist/features/all.d.ts.map +1 -1
  24. package/dist/features/all.js +1 -2
  25. package/dist/features/docs/index.d.ts +2 -4
  26. package/dist/features/docs/index.d.ts.map +1 -1
  27. package/dist/features/docs/index.js +1 -2
  28. package/dist/features/epub.d.ts +1 -3
  29. package/dist/features/epub.d.ts.map +1 -1
  30. package/dist/features/epub.js +1 -2
  31. package/dist/features/feedback.d.ts +1 -3
  32. package/dist/features/feedback.d.ts.map +1 -1
  33. package/dist/features/feedback.js +1 -2
  34. package/dist/features/index.d.ts +1 -1
  35. package/dist/features/index.js +1 -2
  36. package/dist/features/lint.d.ts +1 -3
  37. package/dist/features/lint.d.ts.map +1 -1
  38. package/dist/features/lint.js +1 -2
  39. package/dist/features/llms.d.ts +6 -7
  40. package/dist/features/llms.d.ts.map +1 -1
  41. package/dist/features/llms.js +1 -2
  42. package/dist/features/mcp.d.ts +5 -7
  43. package/dist/features/mcp.d.ts.map +1 -1
  44. package/dist/features/mcp.js +1 -2
  45. package/dist/features/og.d.ts +2 -3
  46. package/dist/features/og.d.ts.map +1 -1
  47. package/dist/features/og.js +1 -2
  48. package/dist/features/search.d.ts +10 -10
  49. package/dist/features/search.d.ts.map +1 -1
  50. package/dist/features/search.js +1 -2
  51. package/dist/features/utils.d.ts +2 -2
  52. package/dist/features/utils.js +1 -2
  53. package/dist/features/webmcp.d.ts +2 -3
  54. package/dist/features/webmcp.d.ts.map +1 -1
  55. package/dist/features/webmcp.js +1 -2
  56. package/dist/{features-Do6x9Tsz.js → features-C_FG2WD0.js} +5 -6
  57. package/dist/{features-Do6x9Tsz.js.map → features-C_FG2WD0.js.map} +1 -1
  58. package/dist/{feedback-DpXYMKtX.js → feedback-7u1-eklc.js} +6 -7
  59. package/dist/feedback-7u1-eklc.js.map +1 -0
  60. package/dist/{fs-D6J9dOEx.js → fs-Dqr1VJSD.js} +2 -3
  61. package/dist/{fs-D6J9dOEx.js.map → fs-Dqr1VJSD.js.map} +1 -1
  62. package/dist/{index-Cs7hw2un.d.ts → index-CAIqrY1S.d.ts} +3 -5
  63. package/dist/index-CAIqrY1S.d.ts.map +1 -0
  64. package/dist/{index-DI66DoV8.d.ts → index-DCJbgkgf.d.ts} +4 -8
  65. package/dist/index-DCJbgkgf.d.ts.map +1 -0
  66. package/dist/{index-Ck0EPdgu.d.ts → index-w9FHKPPs.d.ts} +6 -16
  67. package/dist/index-w9FHKPPs.d.ts.map +1 -0
  68. package/dist/index.js +22 -31
  69. package/dist/index.js.map +1 -1
  70. package/dist/{installer-C4662Woc.js → installer-CEOV0fBv.js} +3 -5
  71. package/dist/{installer-C4662Woc.js.map → installer-CEOV0fBv.js.map} +1 -1
  72. package/dist/{lint-zkXUULgO.js → lint-AC2ClOfa.js} +3 -4
  73. package/dist/{lint-zkXUULgO.js.map → lint-AC2ClOfa.js.map} +1 -1
  74. package/dist/{llms-DQzAst6R.js → llms-Cge1uefC.js} +11 -12
  75. package/dist/llms-Cge1uefC.js.map +1 -0
  76. package/dist/{mcp-BpFZ_fdV.js → mcp-BVpVFl6G.js} +7 -9
  77. package/dist/mcp-BVpVFl6G.js.map +1 -0
  78. package/dist/{og-m0CwRlRz.js → og-DuPd9kNq.js} +12 -11
  79. package/dist/og-DuPd9kNq.js.map +1 -0
  80. package/dist/project/index.d.ts +2 -2
  81. package/dist/project/index.js +1 -2
  82. package/dist/{project-DwRVTm4p.js → project-D655VGTX.js} +19 -10
  83. package/dist/project-D655VGTX.js.map +1 -0
  84. package/dist/registry/installer.d.ts +1 -3
  85. package/dist/registry/installer.d.ts.map +1 -1
  86. package/dist/registry/installer.js +1 -2
  87. package/dist/{search-BCHEW0-K.js → search-B44II4bM.js} +39 -30
  88. package/dist/search-B44II4bM.js.map +1 -0
  89. package/dist/{utils-DbMe7h6c.js → utils-Btt-BJ53.js} +6 -12
  90. package/dist/utils-Btt-BJ53.js.map +1 -0
  91. package/dist/{utils-CxJ7kw39.d.ts → utils-DlGz_4va.d.ts} +6 -12
  92. package/dist/utils-DlGz_4va.d.ts.map +1 -0
  93. package/dist/{webmcp-I9L7xzmw.js → webmcp-lY3_RsR3.js} +6 -8
  94. package/dist/webmcp-lY3_RsR3.js.map +1 -0
  95. package/package.json +7 -7
  96. package/dist/ai-BoUGzPvv.js.map +0 -1
  97. package/dist/all-BmTB7dab.js +0 -27
  98. package/dist/codemod-auol-p4R.js.map +0 -1
  99. package/dist/config-Bfg4yG8S.d.ts.map +0 -1
  100. package/dist/config-Bw0kmF01.js.map +0 -1
  101. package/dist/docs-qRGwd9Yp.js.map +0 -1
  102. package/dist/epub-DbOh5Q2D.js.map +0 -1
  103. package/dist/feedback-DpXYMKtX.js.map +0 -1
  104. package/dist/index-Ck0EPdgu.d.ts.map +0 -1
  105. package/dist/index-Cs7hw2un.d.ts.map +0 -1
  106. package/dist/index-DI66DoV8.d.ts.map +0 -1
  107. package/dist/llms-DQzAst6R.js.map +0 -1
  108. package/dist/mcp-BpFZ_fdV.js.map +0 -1
  109. package/dist/og-m0CwRlRz.js.map +0 -1
  110. package/dist/project-DwRVTm4p.js.map +0 -1
  111. package/dist/search-BCHEW0-K.js.map +0 -1
  112. package/dist/utils-CxJ7kw39.d.ts.map +0 -1
  113. package/dist/utils-DbMe7h6c.js.map +0 -1
  114. package/dist/webmcp-I9L7xzmw.js.map +0 -1
@@ -1,8 +1,8 @@
1
- import { c as addNextRewrites, n as enableProcessedMarkdown, p as addImport, r as addTanstackPrerender, t as addProxyMatcher } from "./codemod-auol-p4R.js";
2
- import { a as routeModule, i as reactRouterTypes, n as findNextProxy, r as formatRoute, t as docs } from "./docs-qRGwd9Yp.js";
3
- import { a as reactFramework, c as requiresMarkdown, d as sourceRef, n as addPageUrl, s as registerReactRouterRoutes, t as addExport, u as sharedRoute } from "./utils-DbMe7h6c.js";
1
+ import { c as addNextRewrites, n as enableProcessedMarkdown, p as addImport, r as addTanstackPrerender, t as addProxyMatcher } from "./codemod-Wc9nRqrw.js";
2
+ import { a as reactRouterTypes, i as formatRoute, n as findNextProxy, o as routeModule, r as astroPageRoute, t as docs } from "./docs---dy-BSm.js";
3
+ import { a as registerReactRouterRoutes, c as sharedRoute, l as sourceRef, n as addPageUrl, o as requiresMarkdown, t as addExport } from "./utils-Btt-BJ53.js";
4
4
  import path from "node:path";
5
- //#region src/features/llms.ts
5
+ // src/features/llms.ts
6
6
  /** runtime content sources expose raw Markdown as `page.data.content`, Fumadocs MDX processes it */
7
7
  const docsLlms = ({ dynamic, ref }) => `
8
8
  export const docsLlms = llms(${ref}, {
@@ -123,7 +123,8 @@ export async function getConfig() {
123
123
  staticPaths: ${src.resolved}.generateParams().map((item) => ${i18n ? "[item.lang, ...item.slug]" : "item.slug"}),
124
124
  } as const;
125
125
  }
126
- `
126
+ `,
127
+ astro: (_route, i18n, src) => astroPageRoute(i18n, pageImports(src), markdown)
127
128
  };
128
129
  function templates(framework, routes, i18n, src) {
129
130
  const index = (body) => ({
@@ -164,12 +165,11 @@ const llms = {
164
165
  requires: [docs],
165
166
  supports: requiresMarkdown,
166
167
  async apply(ctx) {
167
- const { baseDir, i18n, source, configFile } = ctx.project;
168
- const framework = reactFramework(ctx.project);
168
+ const { baseDir, i18n, source, configFile, framework } = ctx.project;
169
169
  const sourceFile = path.join(baseDir, "lib/source.ts");
170
170
  const src = sourceRef(ctx.project.source.dynamic);
171
171
  if (source.collections) await ctx.source(source.collections, (file) => {
172
- if (!enableProcessedMarkdown(file)) throw new Error(`cannot find \`defineDocs()\` in ${source.collections}`);
172
+ if (!enableProcessedMarkdown(file)) throw new Error(`cannot enable \`includeProcessedMarkdown\` in ${source.collections}`);
173
173
  });
174
174
  if (await addExport(ctx, sourceFile, "docsLlms", docsLlms(src))) await ctx.source(sourceFile, (file) => addImport(file, {
175
175
  from: "fumadocs-core/source",
@@ -185,7 +185,7 @@ const llms = {
185
185
  await addPageUrl(ctx, "getPageMarkdownUrl", markdownUrl(i18n !== null));
186
186
  }
187
187
  const routes = llmsRoutes(framework, i18n, docsRoute, contentRoute);
188
- for (const [route, content] of templates(framework, routes, i18n !== null, src)) await ctx.write(path.join(baseDir, route.file), content);
188
+ for (const [route, content] of templates(framework, routes, i18n, src)) await ctx.write(path.join(baseDir, route.file), content);
189
189
  if (framework === "react-router") await registerReactRouterRoutes(ctx, [
190
190
  routes.index,
191
191
  routes.full,
@@ -196,7 +196,7 @@ const llms = {
196
196
  if (i18n) await extendNextProxy(ctx, routes.markdown);
197
197
  await configureMarkdownUrl(ctx, docsRoute, routes.markdown);
198
198
  }
199
- if (ctx.project.static && framework !== "next") ctx.note("Per-page Markdown routes are dynamic, add the pages to your prerender config to serve them statically.");
199
+ if (ctx.project.static && (framework === "react-router" || framework === "tanstack-start")) ctx.note("Per-page Markdown routes are dynamic, add the pages to your prerender config to serve them statically.");
200
200
  if (framework === "react-router" || framework === "waku") ctx.note("To serve pages with a `.md` suffix, rewrite `/docs/*.md` to the `llms.mdx` route in a middleware, see https://fumadocs.dev/docs/integrations/llms#md-extension.");
201
201
  ctx.note("Add page actions to your docs page with `MarkdownCopyButton` and `ViewOptionsPopover` from 'fumadocs-ui/layouts/docs/page', the Markdown URL of a page is `getPageMarkdownUrl(page).url` from '@/lib/shared'.");
202
202
  }
@@ -222,7 +222,6 @@ async function extendNextProxy(ctx, route) {
222
222
  if (!addProxyMatcher(file, patterns)) throw new Error("cannot find the matcher");
223
223
  }).catch(() => false))) ctx.note(`Add ${patterns.map((p) => `\`${p}\``).join(", ")} to the matcher of your i18n middleware.`);
224
224
  }
225
- //#endregion
226
225
  export { markdownRewrite as a, templates as c, llmsRoutes as i, extendNextProxy as n, markdownUrl as o, llms as r, tanstackMarkdownUrl as s, docsLlms as t };
227
226
 
228
- //# sourceMappingURL=llms-DQzAst6R.js.map
227
+ //# sourceMappingURL=llms-Cge1uefC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llms-Cge1uefC.js","names":[],"sources":["../src/features/llms.ts"],"sourcesContent":["import path from 'node:path';\nimport type { Feature, FeatureContext } from '@/features';\nimport type { Framework, I18nInfo } from '@/project';\nimport {\n astroPageRoute,\n type FormattedRoute,\n formatRoute,\n reactRouterTypes,\n routeModule,\n type RouteDescriptor,\n} from '@/project/route';\nimport {\n addImport,\n addNextRewrites,\n addProxyMatcher,\n addTanstackPrerender,\n enableProcessedMarkdown,\n} from '@/codemod';\nimport { docs, findNextProxy } from './docs';\nimport {\n addExport,\n addPageUrl,\n type SourceRef,\n sourceRef,\n registerReactRouterRoutes,\n requiresMarkdown,\n sharedRoute,\n} from './utils';\n\n/** runtime content sources expose raw Markdown as `page.data.content`, Fumadocs MDX processes it */\nexport const docsLlms = ({ dynamic, ref }: SourceRef) => `\nexport const docsLlms = llms(${ref}, {\n renderPage: ${dynamic ? '(page)' : 'async (page)'} => \\`# \\${page.data.title} (\\${page.url})\n\n\\${${dynamic ? 'page.data.content' : \"await page.data.getText('processed')\"}}\\`,\n});`;\n\n/** the Markdown URL of a page, `/llms.mdx/docs/<slugs>/content.md` */\nexport const markdownUrl = (i18n: boolean) => `\nconst getContentUrl = createGetUrl(docsContentRoute${i18n ? ', i18n' : ''});\n\nexport function getPageMarkdownUrl(page: { slugs: string[]; locale?: string }) {\n const segments = [...page.slugs, 'content.md'];\n\n return { segments, url: getContentUrl(segments, page.locale) };\n}`;\n\n/** TanStack Start serves Markdown with a `.md` suffix under the docs route */\nexport const tanstackMarkdownUrl = (i18n: boolean) => `\nconst getDocsUrl = createGetUrl(docsRoute${i18n ? ', i18n' : ''});\n\nexport function getPageMarkdownUrl(page: { slugs: string[]; locale?: string }) {\n const segments = [...page.slugs];\n if (segments.length === 0) {\n segments.push('index.md');\n } else {\n segments[segments.length - 1] += '.md';\n }\n\n return { segments, url: getDocsUrl(segments, page.locale) };\n}\n\n/** @returns page slugs */\nexport function decodeMarkdownUrl(segments: string[]) {\n if (segments.length === 0) return [];\n\n const out = [...segments];\n out[out.length - 1] = out[out.length - 1].replace(/\\\\.md$/, '');\n if (out.length === 1 && out[0] === 'index') out.pop();\n return out;\n}`;\n\nconst imports = `import { docsLlms } from '@/lib/source';`;\nconst pageImports = ({ ref }: SourceRef) => `import { docsLlms, ${ref} } from '@/lib/source';`;\nconst markdown = `new Response(await docsLlms.page(page), {\n headers: {\n 'Content-Type': 'text/markdown',\n },\n })`;\n\ninterface Routes {\n index: FormattedRoute;\n full: FormattedRoute;\n markdown: FormattedRoute;\n}\n\ntype Template = (route: FormattedRoute, i18n: I18nInfo | null, src: SourceRef) => string;\n\n/** the per-page Markdown route, the only one reading route params */\nconst markdownTemplates: Record<Framework, Template> = {\n next: (route, i18n, src) => `${pageImports(src)}\nimport { notFound } from 'next/navigation';\n\nexport const revalidate = false;\n\nexport async function GET(_req: Request, { params }: RouteContext<'${route.path}'>) {\n const { slug${i18n ? ', lang' : ''} } = await params;\n // remove the appended \"content.md\", \\`/docs/index.md\\` is rewritten to the root page\n const slugs = slug?.slice(0, -1) ?? [];\n if (slugs.at(-1) === 'index') slugs.pop();\n const page = ${src.resolved}.getPage(slugs${i18n ? ', lang' : ''});\n if (!page) notFound();\n\n return ${markdown};\n}\n\nexport ${src.dynamic ? 'async ' : ''}function generateStaticParams() {\n return ${src.resolved}.generateParams().map((item) => ({\n ...item,\n slug: [...item.slug, 'content.md'],\n }));\n}\n`,\n 'react-router': (route, i18n, src) => `${reactRouterTypes(route)}\n${pageImports(src)}\n\nexport async function loader({ params }: Route.LoaderArgs) {\n const slugs = params['*'].split('/').filter((v) => v.length > 0);\n // remove the appended \"content.md\"\n slugs.pop();\n const page = ${src.resolved}.getPage(slugs${i18n ? ', params.lang' : ''});\n if (!page) return new Response('not found', { status: 404 });\n\n return ${markdown};\n}\n`,\n 'tanstack-start': (\n route,\n i18n,\n src,\n ) => `import { createFileRoute, notFound } from '@tanstack/react-router';\nimport { decodeMarkdownUrl } from '@/lib/shared';\n${pageImports(src)}\n\nexport const Route = createFileRoute('${route.path}')({\n server: {\n handlers: {\n GET: async ({ params }) => {\n const slugs = decodeMarkdownUrl(params._splat?.split('/') ?? []);\n const page = ${src.resolved}.getPage(slugs${i18n ? ', params.lang' : ''});\n if (!page) throw notFound();\n\n return ${markdown.replaceAll('\\n ', '\\n ')};\n },\n },\n },\n});\n`,\n waku: (route, i18n, src) => `${pageImports(src)}\nimport type { ApiContext } from 'waku/router';\nimport { unstable_notFound } from 'waku/router/server';\n\nexport async function GET(_: Request, { params }: ApiContext<'${route.path}'>) {\n const page = ${src.resolved}.getPage(params.slug${i18n ? ', params.lang' : ''});\n if (!page) unstable_notFound();\n\n return ${markdown};\n}\n\nexport async function getConfig() {\n return {\n render: 'static' as const,\n staticPaths: ${src.resolved}.generateParams().map((item) => ${i18n ? '[item.lang, ...item.slug]' : 'item.slug'}),\n } as const;\n}\n`,\n astro: (_route, i18n, src) => astroPageRoute(i18n, pageImports(src), markdown),\n};\n\nexport function templates(\n framework: Framework,\n routes: Routes,\n i18n: I18nInfo | null,\n src: SourceRef,\n): [route: FormattedRoute, content: string][] {\n const index = (body: string) => ({ imports, body, revalidate: true, static: true });\n\n return [\n [\n routes.index,\n routeModule(framework, routes.index, index('new Response(await docsLlms.index())')),\n ],\n [\n routes.full,\n routeModule(framework, routes.full, index('new Response(await docsLlms.full())')),\n ],\n [routes.markdown, markdownTemplates[framework](routes.markdown, i18n, src)],\n ];\n}\n\nexport function llmsRoutes(\n framework: Framework,\n i18n: I18nInfo | null,\n docsRoute: string,\n contentRoute: string,\n): Routes {\n const format = (route: RouteDescriptor) => formatRoute(route, framework, i18n);\n return {\n index: format({ segments: ['llms.txt'] }),\n full: format({ segments: ['llms-full.txt'] }),\n markdown: format(\n framework === 'tanstack-start'\n ? { segments: [docsRoute, '{$}.md'], locale: true }\n : {\n segments: [\n contentRoute,\n { param: 'slug', catchAll: true, optional: true, suffix: 'content.md' },\n ],\n locale: true,\n },\n ),\n };\n}\n\nexport const llms: Feature = {\n id: 'llms',\n title: 'LLM Routes',\n description: 'serve docs as Markdown for LLMs: llms.txt, llms-full.txt and per-page Markdown',\n requires: [docs],\n supports: requiresMarkdown,\n async apply(ctx) {\n const { baseDir, i18n, source, configFile, framework } = ctx.project;\n const sourceFile = path.join(baseDir, 'lib/source.ts');\n const src = sourceRef(ctx.project.source.dynamic);\n // runtime content sources already give raw Markdown\n if (source.collections) {\n await ctx.source(source.collections, (file) => {\n if (!enableProcessedMarkdown(file))\n throw new Error(`cannot enable \\`includeProcessedMarkdown\\` in ${source.collections}`);\n });\n }\n if (await addExport(ctx, sourceFile, 'docsLlms', docsLlms(src))) {\n await ctx.source(sourceFile, (file) =>\n addImport(file, { from: 'fumadocs-core/source', named: ['llms'] }),\n );\n }\n\n const docsRoute = source.baseUrl.replace(/\\/$/, '');\n let contentRoute = `/llms.mdx${docsRoute}`;\n if (framework === 'tanstack-start') {\n await sharedRoute(ctx, 'docsRoute', source.baseUrl);\n await addPageUrl(ctx, 'getPageMarkdownUrl', tanstackMarkdownUrl(i18n !== null));\n } else {\n contentRoute = await sharedRoute(ctx, 'docsContentRoute', contentRoute);\n await addPageUrl(ctx, 'getPageMarkdownUrl', markdownUrl(i18n !== null));\n }\n const routes = llmsRoutes(framework, i18n, docsRoute, contentRoute);\n for (const [route, content] of templates(framework, routes, i18n, src))\n await ctx.write(path.join(baseDir, route.file), content);\n\n if (framework === 'react-router') {\n await registerReactRouterRoutes(ctx, [routes.index, routes.full, routes.markdown]);\n } else if (framework === 'tanstack-start' && ctx.project.static && configFile) {\n // SPA mode only serves prerendered routes\n await ctx.source(configFile, (file) =>\n addTanstackPrerender(file, [routes.index.path, routes.full.path]),\n );\n } else if (framework === 'next') {\n if (i18n) await extendNextProxy(ctx, routes.markdown);\n await configureMarkdownUrl(ctx, docsRoute, routes.markdown);\n }\n if (ctx.project.static && (framework === 'react-router' || framework === 'tanstack-start')) {\n ctx.note(\n 'Per-page Markdown routes are dynamic, add the pages to your prerender config to serve them statically.',\n );\n }\n if (framework === 'react-router' || framework === 'waku') {\n ctx.note(\n 'To serve pages with a `.md` suffix, rewrite `/docs/*.md` to the `llms.mdx` route in a middleware, see https://fumadocs.dev/docs/integrations/llms#md-extension.',\n );\n }\n ctx.note(\n \"Add page actions to your docs page with `MarkdownCopyButton` and `ViewOptionsPopover` from 'fumadocs-ui/layouts/docs/page', the Markdown URL of a page is `getPageMarkdownUrl(page).url` from '@/lib/shared'.\",\n );\n },\n};\n\n/** `/docs/*.md` serves the Markdown of a page, like TanStack Start's `.md` route */\nexport const markdownRewrite = (docsRoute: string, markdown: FormattedRoute, i18n: boolean) => ({\n source: `${i18n ? '/:lang' : ''}${docsRoute}/:slug*.md`,\n destination: `${markdown.pattern}/content.md`,\n});\n\nasync function configureMarkdownUrl(\n ctx: FeatureContext,\n docsRoute: string,\n markdown: FormattedRoute,\n) {\n const { configFile, i18n } = ctx.project;\n const rewrite = markdownRewrite(docsRoute, markdown, i18n !== null);\n const edited =\n configFile !== undefined &&\n (await ctx\n .source(configFile, (file) => {\n if (file.code.includes('llms.mdx')) return;\n if (!addNextRewrites(file, [rewrite])) throw new Error('cannot add rewrites');\n })\n .catch(() => false));\n\n if (!edited) {\n ctx.note(\n `Rewrite \\`${rewrite.source}\\` to \\`${rewrite.destination}\\` in your Next.js config to serve pages with a \\`.md\\` suffix.`,\n );\n }\n}\n\n/** routes under `[lang]` need the i18n middleware to resolve the locale */\nexport async function extendNextProxy(ctx: FeatureContext, route: FormattedRoute) {\n const patterns = [route.pattern.replace('/:lang', ''), route.pattern];\n const proxy = await findNextProxy(ctx.project);\n const edited =\n proxy !== undefined &&\n (await ctx\n .source(proxy, (file) => {\n if (!addProxyMatcher(file, patterns)) throw new Error('cannot find the matcher');\n })\n .catch(() => false));\n if (!edited)\n ctx.note(\n `Add ${patterns.map((p) => `\\`${p}\\``).join(', ')} to the matcher of your i18n middleware.`,\n );\n}\n"],"mappings":"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"}
@@ -1,9 +1,9 @@
1
- import { i as addReactRouterPrerenderArray } from "./codemod-auol-p4R.js";
2
- import { a as routeModule, r as formatRoute } from "./docs-qRGwd9Yp.js";
3
- import { a as reactFramework, c as requiresMarkdown, d as sourceRef, s as registerReactRouterRoutes } from "./utils-DbMe7h6c.js";
4
- import { r as llms } from "./llms-DQzAst6R.js";
1
+ import { i as addReactRouterPrerenderArray } from "./codemod-Wc9nRqrw.js";
2
+ import { i as formatRoute, o as routeModule } from "./docs---dy-BSm.js";
3
+ import { a as registerReactRouterRoutes, l as sourceRef, o as requiresMarkdown } from "./utils-Btt-BJ53.js";
4
+ import { r as llms } from "./llms-Cge1uefC.js";
5
5
  import path from "node:path";
6
- //#region src/features/mcp.ts
6
+ // src/features/mcp.ts
7
7
  const server = ({ ref }) => `import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
8
8
  import { registerSearchTool, registerSourceTools } from 'fumadocs-core/mcp';
9
9
  import { createFromSource } from 'fumadocs-core/search/server';
@@ -39,8 +39,7 @@ const mcp = {
39
39
  requires: [llms],
40
40
  supports: (project) => project.static ? "MCP requires a server at runtime" : requiresMarkdown(project),
41
41
  async apply(ctx) {
42
- const { baseDir } = ctx.project;
43
- const framework = reactFramework(ctx.project);
42
+ const { baseDir, framework } = ctx.project;
44
43
  ctx.addDependencies({
45
44
  "@modelcontextprotocol/server": null,
46
45
  zod: null
@@ -61,7 +60,6 @@ const mcp = {
61
60
  }`);
62
61
  }
63
62
  };
64
- //#endregion
65
63
  export { mcpRoute as n, templates as r, mcp as t };
66
64
 
67
- //# sourceMappingURL=mcp-BpFZ_fdV.js.map
65
+ //# sourceMappingURL=mcp-BVpVFl6G.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-BVpVFl6G.js","names":[],"sources":["../src/features/mcp.ts"],"sourcesContent":["import path from 'node:path';\nimport type { Feature } from '@/features';\nimport type { Framework } from '@/project';\nimport { type FormattedRoute, formatRoute, routeModule } from '@/project/route';\nimport { addReactRouterPrerenderArray } from '@/codemod';\nimport { llms } from './llms';\nimport { registerReactRouterRoutes, requiresMarkdown, type SourceRef, sourceRef } from './utils';\n\nconst server = ({\n ref,\n}: SourceRef) => `import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';\nimport { registerSearchTool, registerSourceTools } from 'fumadocs-core/mcp';\nimport { createFromSource } from 'fumadocs-core/search/server';\nimport { docsLlms, ${ref} } from '@/lib/source';\n\nconst handler = createMcpHandler(() => {\n const mcp = new McpServer({\n name: 'docs',\n version: '1.0.0',\n });\n\n registerSourceTools(mcp, ${ref}, docsLlms);\n registerSearchTool(mcp, createFromSource(${ref}));\n\n return mcp;\n});\n`;\n\nexport const templates = (framework: Framework, route: FormattedRoute, src: SourceRef) =>\n routeModule(framework, route, {\n imports: server(src),\n body: 'handler.fetch(request)',\n request: true,\n methods: ['GET', 'POST', 'DELETE'],\n });\n\nexport const mcpRoute = (framework: Framework) =>\n formatRoute({ segments: ['api/mcp'] }, framework, null);\n\nexport const mcp: Feature = {\n id: 'mcp',\n title: 'MCP Server',\n description: 'a MCP server for AI agents to search and read your docs, at /api/mcp',\n requires: [llms],\n supports: (project) =>\n project.static ? 'MCP requires a server at runtime' : requiresMarkdown(project),\n async apply(ctx) {\n const { baseDir, framework } = ctx.project;\n ctx.addDependencies({ '@modelcontextprotocol/server': null, zod: null });\n\n const route = mcpRoute(framework);\n await ctx.write(\n path.join(baseDir, route.file),\n templates(framework, route, sourceRef(ctx.project.source.dynamic)),\n );\n\n if (framework === 'react-router') {\n await registerReactRouterRoutes(ctx, [route]);\n await ctx.source('react-router.config.ts', (file) => {\n addReactRouterPrerenderArray(file, 'excluded', [`/${route.path}`]);\n });\n }\n\n ctx.note(`Connect AI agents to your docs with the MCP server:\n{\n \"mcpServers\": {\n \"docs\": { \"url\": \"https://your-site.com/api/mcp\" }\n }\n}`);\n },\n};\n"],"mappings":";;;;;;AAQA,MAAM,UAAU,EACd,UACe;;;qBAGI,IAAI;;;;;;;;6BAQI,IAAI;6CACY,IAAI;;;;;AAMjD,MAAa,aAAa,WAAsB,OAAuB,QACrE,YAAY,WAAW,OAAO;CAC5B,SAAS,OAAO,GAAG;CACnB,MAAM;CACN,SAAS;CACT,SAAS;EAAC;EAAO;EAAQ;CAAQ;AACnC,CAAC;AAEH,MAAa,YAAY,cACvB,YAAY,EAAE,UAAU,CAAC,SAAS,EAAE,GAAG,WAAW,IAAI;AAExD,MAAa,MAAe;CAC1B,IAAI;CACJ,OAAO;CACP,aAAa;CACb,UAAU,CAAC,IAAI;CACf,WAAW,YACT,QAAQ,SAAS,qCAAqC,iBAAiB,OAAO;CAChF,MAAM,MAAM,KAAK;EACf,MAAM,EAAE,SAAS,cAAc,IAAI;EACnC,IAAI,gBAAgB;GAAE,gCAAgC;GAAM,KAAK;EAAK,CAAC;EAEvE,MAAM,QAAQ,SAAS,SAAS;EAChC,MAAM,IAAI,MACR,KAAK,KAAK,SAAS,MAAM,IAAI,GAC7B,UAAU,WAAW,OAAO,UAAU,IAAI,QAAQ,OAAO,OAAO,CAAC,CACnE;EAEA,IAAI,cAAc,gBAAgB;GAChC,MAAM,0BAA0B,KAAK,CAAC,KAAK,CAAC;GAC5C,MAAM,IAAI,OAAO,2BAA2B,SAAS;IACnD,6BAA6B,MAAM,YAAY,CAAC,IAAI,MAAM,MAAM,CAAC;GACnE,CAAC;EACH;EAEA,IAAI,KAAK;;;;;EAKX;CACA;AACF"}
@@ -1,9 +1,9 @@
1
- import { S as getProperty, f as addElements, u as getConfigObject } from "./codemod-auol-p4R.js";
2
- import { i as reactRouterTypes, r as formatRoute, t as docs } from "./docs-qRGwd9Yp.js";
3
- import { a as reactFramework, d as sourceRef, n as addPageUrl, s as registerReactRouterRoutes, u as sharedRoute } from "./utils-DbMe7h6c.js";
4
- import { n as extendNextProxy } from "./llms-DQzAst6R.js";
1
+ import { S as getProperty, f as addElements, u as getConfigObject } from "./codemod-Wc9nRqrw.js";
2
+ import { a as reactRouterTypes, i as formatRoute, r as astroPageRoute, t as docs } from "./docs---dy-BSm.js";
3
+ import { a as registerReactRouterRoutes, c as sharedRoute, l as sourceRef, n as addPageUrl } from "./utils-Btt-BJ53.js";
4
+ import { n as extendNextProxy } from "./llms-Cge1uefC.js";
5
5
  import path from "node:path";
6
- //#region src/features/og.ts
6
+ // src/features/og.ts
7
7
  /** the image URL of a page, `/og/docs/<slugs>/image.png` */
8
8
  const imageUrl = (ext, i18n) => `
9
9
  const getImageUrl = createGetUrl(docsImageRoute${i18n ? ", i18n" : ""});
@@ -89,7 +89,8 @@ export async function getConfig() {
89
89
  staticPaths: ${src.resolved}.generateParams().map((item) => ${i18n ? "[item.lang, ...item.slug]" : "item.slug"}),
90
90
  } as const;
91
91
  }
92
- `
92
+ `,
93
+ astro: (_route, engine, i18n, src) => astroPageRoute(i18n, `${imports(engine)}\nimport { ${src.ref} } from '@/lib/source';`, image(engine))
93
94
  };
94
95
  const og = {
95
96
  id: "og",
@@ -109,8 +110,7 @@ const og = {
109
110
  }]
110
111
  } },
111
112
  async apply(ctx, { engine }) {
112
- const { baseDir, configFile, i18n, source } = ctx.project;
113
- const framework = reactFramework(ctx.project);
113
+ const { baseDir, configFile, i18n, source, framework } = ctx.project;
114
114
  if (engine === "next-og" && framework !== "next") throw new Error("next/og is only available on Next.js");
115
115
  if (engine === "takumi") ctx.addDependencies({ "takumi-js": null });
116
116
  const shared = path.join(baseDir, "lib/shared.ts");
@@ -126,17 +126,18 @@ const og = {
126
126
  }],
127
127
  locale: true
128
128
  }, framework, i18n, "tsx");
129
- await ctx.write(path.join(baseDir, route.file), templates[framework](route, engine, i18n !== null, sourceRef(ctx.project.source.dynamic)));
129
+ await ctx.write(path.join(baseDir, route.file), templates[framework](route, engine, i18n, sourceRef(ctx.project.source.dynamic)));
130
130
  if (framework === "react-router") await registerReactRouterRoutes(ctx, [route]);
131
131
  if (framework === "next" && engine === "takumi" && configFile) await ctx.source(configFile, (file) => {
132
132
  const config = getConfigObject(file);
133
133
  if (config && !getProperty(config, "serverExternalPackages")) addElements(file, config, ["serverExternalPackages: ['@takumi-rs/core']"]);
134
134
  });
135
135
  if (framework === "next" && i18n) await extendNextProxy(ctx, route);
136
+ if (ctx.project.static && framework === "react-router") ctx.note("Image routes are dynamic, add `getPageImageUrl(page).url` of each page to your prerender config.");
137
+ else if (ctx.project.static && framework === "tanstack-start") ctx.note("TanStack Start prerenders responses as text, which breaks images. Prerender the image routes with Nitro instead, see https://fumadocs.dev/docs/integrations/og/takumi.");
136
138
  ctx.note("The image URL of a page is `getPageImageUrl(page).url` from '@/lib/shared', reference it in the page metadata (e.g. `og:image`).");
137
139
  }
138
140
  };
139
- //#endregion
140
141
  export { og as t };
141
142
 
142
- //# sourceMappingURL=og-m0CwRlRz.js.map
143
+ //# sourceMappingURL=og-DuPd9kNq.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"og-DuPd9kNq.js","names":[],"sources":["../src/features/og.ts"],"sourcesContent":["import path from 'node:path';\nimport type { Feature } from '@/features';\nimport type { Framework, I18nInfo } from '@/project';\nimport {\n astroPageRoute,\n type FormattedRoute,\n formatRoute,\n reactRouterTypes,\n} from '@/project/route';\nimport { addElements, getConfigObject, getProperty } from '@/codemod';\nimport { docs } from './docs';\nimport { extendNextProxy } from './llms';\nimport {\n addPageUrl,\n registerReactRouterRoutes,\n sharedRoute,\n type SourceRef,\n sourceRef,\n} from './utils';\n\ntype Engine = 'takumi' | 'next-og';\n\n/** the image URL of a page, `/og/docs/<slugs>/image.png` */\nconst imageUrl = (ext: string, i18n: boolean) => `\nconst getImageUrl = createGetUrl(docsImageRoute${i18n ? ', i18n' : ''});\n\nexport function getPageImageUrl(page: { slugs: string[]; locale?: string }) {\n const segments = [...page.slugs, 'image.${ext}'];\n\n return { segments, url: getImageUrl(segments, page.locale) };\n}`;\n\nconst imports = (engine: Engine) =>\n `import { generateOGImage } from 'fumadocs-ui/og${engine === 'takumi' ? '/takumi' : ''}';`;\n\nconst image = (engine: Engine) => `generateOGImage({\n title: page.data.title,\n description: page.data.description,\n site: 'My App',${engine === 'takumi' ? \"\\n format: 'webp',\" : ''}\n })`;\n\nconst ext = (engine: Engine) => (engine === 'takumi' ? 'webp' : 'png');\n\ntype Template = (\n route: FormattedRoute,\n engine: Engine,\n i18n: I18nInfo | null,\n src: SourceRef,\n) => string;\n\nconst templates: Record<Framework, Template> = {\n next: (route, engine, i18n, src) => `import { ${src.ref} } from '@/lib/source';\nimport { notFound } from 'next/navigation';\n${imports(engine)}\n\nexport const revalidate = false;\n\nexport async function GET(_req: Request, { params }: RouteContext<'${route.path}'>) {\n const { slug${i18n ? ', lang' : ''} } = await params;\n const page = ${src.resolved}.getPage(slug.slice(0, -1)${i18n ? ', lang' : ''});\n if (!page) notFound();\n\n return ${image(engine)};\n}\n\nexport ${src.dynamic ? 'async ' : ''}function generateStaticParams() {\n return ${src.resolved}.generateParams().map((item) => ({\n ...item,\n slug: [...item.slug, 'image.${ext(engine)}'],\n }));\n}\n`,\n 'react-router': (route, engine, i18n, src) => `${reactRouterTypes(route)}\nimport { ${src.ref} } from '@/lib/source';\n${imports(engine)}\n\nexport async function loader({ params }: Route.LoaderArgs) {\n const slugs = params['*'].split('/').filter((v) => v.length > 0);\n const page = ${src.resolved}.getPage(slugs.slice(0, -1)${i18n ? ', params.lang' : ''});\n if (!page) throw new Response(undefined, { status: 404 });\n\n return ${image(engine)};\n}\n`,\n 'tanstack-start': (\n route,\n engine,\n i18n,\n src,\n ) => `import { createFileRoute } from '@tanstack/react-router';\nimport { ${src.ref} } from '@/lib/source';\n${imports(engine)}\n\nexport const Route = createFileRoute('${route.path}')({\n server: {\n handlers: {\n GET: async ({ params }) => {\n const slugs = params._splat?.split('/') ?? [];\n const page = ${src.resolved}.getPage(slugs.slice(0, -1)${i18n ? ', params.lang' : ''});\n if (!page) return new Response(undefined, { status: 404 });\n\n return ${image(engine).replaceAll('\\n ', '\\n ')};\n },\n },\n },\n});\n`,\n waku: (route, engine, i18n, src) => `import { ${src.ref} } from '@/lib/source';\nimport type { ApiContext } from 'waku/router';\n${imports(engine)}\n\nexport async function GET(_: Request, { params }: ApiContext<'${route.path}'>) {\n const page = ${src.resolved}.getPage(params.slug${i18n ? ', params.lang' : ''});\n if (!page) return new Response(undefined, { status: 404 });\n\n return ${image(engine)};\n}\n\nexport async function getConfig() {\n return {\n render: 'static' as const,\n staticPaths: ${src.resolved}.generateParams().map((item) => ${i18n ? '[item.lang, ...item.slug]' : 'item.slug'}),\n } as const;\n}\n`,\n astro: (_route, engine, i18n, src) =>\n astroPageRoute(\n i18n,\n `${imports(engine)}\\nimport { ${src.ref} } from '@/lib/source';`,\n image(engine),\n ),\n};\n\nexport const og: Feature<{ engine: Engine }> = {\n id: 'og',\n title: 'OG Image',\n description: 'generate Open Graph images for docs pages',\n requires: [docs],\n options: {\n engine: {\n message: 'Choose an image engine',\n choices: [\n { value: 'takumi', label: 'Takumi', hint: 'outputs WebP, framework-agnostic' },\n { value: 'next-og', label: 'next/og', hint: 'Next.js built-in solution' },\n ],\n },\n },\n async apply(ctx, { engine }) {\n const { baseDir, configFile, i18n, source, framework } = ctx.project;\n if (engine === 'next-og' && framework !== 'next')\n throw new Error('next/og is only available on Next.js');\n if (engine === 'takumi') ctx.addDependencies({ 'takumi-js': null });\n\n const shared = path.join(baseDir, 'lib/shared.ts');\n const imageRoute = await sharedRoute(\n ctx,\n 'docsImageRoute',\n `/og${source.baseUrl.replace(/\\/$/, '')}`,\n );\n const added = await addPageUrl(ctx, 'getPageImageUrl', imageUrl(ext(engine), i18n !== null));\n if (!added && engine === 'takumi') {\n await ctx.source(shared, (file) => {\n file.s.replaceAll('image.png', 'image.webp');\n });\n }\n\n const route = formatRoute(\n {\n segments: [imageRoute, { param: 'slug', catchAll: true, suffix: `image.${ext(engine)}` }],\n locale: true,\n },\n framework,\n i18n,\n 'tsx',\n );\n await ctx.write(\n path.join(baseDir, route.file),\n templates[framework](route, engine, i18n, sourceRef(ctx.project.source.dynamic)),\n );\n\n if (framework === 'react-router') await registerReactRouterRoutes(ctx, [route]);\n if (framework === 'next' && engine === 'takumi' && configFile) {\n await ctx.source(configFile, (file) => {\n const config = getConfigObject(file);\n if (config && !getProperty(config, 'serverExternalPackages'))\n addElements(file, config, [\"serverExternalPackages: ['@takumi-rs/core']\"]);\n });\n }\n if (framework === 'next' && i18n) await extendNextProxy(ctx, route);\n if (ctx.project.static && framework === 'react-router') {\n ctx.note(\n 'Image routes are dynamic, add `getPageImageUrl(page).url` of each page to your prerender config.',\n );\n } else if (ctx.project.static && framework === 'tanstack-start') {\n ctx.note(\n 'TanStack Start prerenders responses as text, which breaks images. Prerender the image routes with Nitro instead, see https://fumadocs.dev/docs/integrations/og/takumi.',\n );\n }\n\n ctx.note(\n \"The image URL of a page is `getPageImageUrl(page).url` from '@/lib/shared', reference it in the page metadata (e.g. `og:image`).\",\n );\n },\n};\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import { a as Project, c as detectFramework, d as loadProject, i as PackageManager, l as frameworks, n as FrameworkInfo, o as ReactFramework, r as I18nInfo, s as SourceInfo, t as Framework, u as isSupportedFramework } from "../index-DI66DoV8.js";
2
- export { Framework, FrameworkInfo, I18nInfo, PackageManager, Project, ReactFramework, SourceInfo, detectFramework, frameworks, isSupportedFramework, loadProject };
1
+ import { a as Project, c as frameworks, i as PackageManager, l as isSupportedFramework, n as FrameworkInfo, o as SourceInfo, r as I18nInfo, s as detectFramework, t as Framework, u as loadProject } from "../index-DCJbgkgf.js";
2
+ export { Framework, FrameworkInfo, I18nInfo, PackageManager, Project, SourceInfo, detectFramework, frameworks, isSupportedFramework, loadProject };
@@ -1,2 +1 @@
1
- import { i as loadProject, n as frameworks, r as isSupportedFramework, t as detectFramework } from "../project-DwRVTm4p.js";
2
- export { detectFramework, frameworks, isSupportedFramework, loadProject };
1
+ export { t as detectFramework, n as frameworks, r as isSupportedFramework, i as loadProject } from "../project-D655VGTX.js";
@@ -1,8 +1,8 @@
1
- import { t as exists } from "./fs-D6J9dOEx.js";
1
+ import { t as exists } from "./fs-Dqr1VJSD.js";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { detectFramework as detectFramework$1, detectPackageManager } from "fuma-cli/detect";
5
- //#region src/project/index.ts
5
+ // src/project/index.ts
6
6
  const frameworks = {
7
7
  next: {
8
8
  provider: "fumadocs-ui/provider/next",
@@ -38,7 +38,13 @@ const frameworks = {
38
38
  provider: "fumadocs-ui/provider/astro",
39
39
  rootFile: "layouts/Layout.astro",
40
40
  routesDir: "pages",
41
- configFiles: ["astro.config.mjs", "astro.config.ts"]
41
+ configFiles: [
42
+ "astro.config.mjs",
43
+ "astro.config.ts",
44
+ "astro.config.js",
45
+ "astro.config.mts"
46
+ ],
47
+ isStatic: (config) => !/output:\s*['"]server['"]/.test(config)
42
48
  }
43
49
  };
44
50
  function isSupportedFramework(v) {
@@ -58,7 +64,7 @@ async function loadProject(config, cwd = process.cwd(), framework = assertFramew
58
64
  if (framework === "react-router") {
59
65
  const content = await fs.readFile(path.join(cwd, "react-router.config.ts"), "utf-8").catch(() => "");
60
66
  isStatic = /ssr:\s*false/.test(content);
61
- } else if (configContent && info.isStatic) isStatic = info.isStatic(configContent);
67
+ } else if (info.isStatic) isStatic = info.isStatic(configContent ?? "");
62
68
  const read = (file) => fs.readFile(path.join(cwd, file), "utf-8").catch(() => null);
63
69
  const i18nConfig = await read(path.join(config.baseDir, "lib/i18n.ts"));
64
70
  return {
@@ -72,16 +78,20 @@ async function loadProject(config, cwd = process.cwd(), framework = assertFramew
72
78
  config,
73
79
  configFile,
74
80
  packageJson,
75
- source: await detectSource(cwd, config.baseDir, read)
81
+ source: await detectSource(cwd, config.baseDir, read, configFile, configContent)
76
82
  };
77
83
  }
78
- async function detectSource(cwd, baseDir, read) {
84
+ async function detectSource(cwd, baseDir, read, configFile, configContent) {
79
85
  const sourceFile = path.join(baseDir, "lib/source.ts");
80
86
  const source = await read(sourceFile) ?? "";
81
87
  let collections = null;
82
88
  let content = source;
89
+ const astro = source.includes("@fumadocs/astro/source");
83
90
  if (source.includes("defineDocs(")) collections = sourceFile;
84
- else if (await exists(path.join(cwd, "source.config.ts"))) {
91
+ else if (astro) {
92
+ collections = configFile ?? null;
93
+ content = configContent ?? "";
94
+ } else if (await exists(path.join(cwd, "source.config.ts"))) {
85
95
  collections = "source.config.ts";
86
96
  content = await read(collections) ?? "";
87
97
  }
@@ -95,7 +105,7 @@ async function detectSource(cwd, baseDir, read) {
95
105
  loader: /export const source\b/.test(source),
96
106
  dynamic: /export (async )?function getSource\b/.test(source),
97
107
  collections,
98
- async: /async:\s*true/.test(content),
108
+ async: astro || /async:\s*true/.test(content),
99
109
  baseUrl: baseUrl ?? "/docs",
100
110
  dir: /dir:\s*['"]([^'"]*)['"]/.exec(content)?.[1] ?? "content/docs"
101
111
  };
@@ -104,7 +114,6 @@ function assertFramework(framework) {
104
114
  if (isSupportedFramework(framework)) return framework;
105
115
  throw new Error(`Unsupported framework "${framework}", supported: ${Object.keys(frameworks).join(", ")}.`);
106
116
  }
107
- //#endregion
108
117
  export { loadProject as i, frameworks as n, isSupportedFramework as r, detectFramework$1 as t };
109
118
 
110
- //# sourceMappingURL=project-DwRVTm4p.js.map
119
+ //# sourceMappingURL=project-D655VGTX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-D655VGTX.js","names":[],"sources":["../src/project/index.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { detectFramework, detectPackageManager } from 'fuma-cli/detect';\nimport type { LoadedConfig } from '@/config';\nimport { exists } from '@/utils/fs';\n\nexport type PackageManager = NonNullable<Awaited<ReturnType<typeof detectPackageManager>>>['name'];\n\nexport type Framework = 'next' | 'react-router' | 'tanstack-start' | 'waku' | 'astro';\n\nexport interface FrameworkInfo {\n /** import specifier of `RootProvider` */\n provider: string;\n /** root file that imports the global CSS, relative to base dir */\n rootFile: string;\n /** routes directory, relative to base dir */\n routesDir: string;\n /** config file candidates (relative to cwd) that register bundler plugins */\n configFiles: string[];\n /** how to detect `static` mode from the config file */\n isStatic?: (config: string) => boolean;\n}\n\nexport const frameworks: Record<Framework, FrameworkInfo> = {\n next: {\n provider: 'fumadocs-ui/provider/next',\n rootFile: 'app/layout.tsx',\n routesDir: 'app',\n configFiles: ['next.config.mjs', 'next.config.ts', 'next.config.js'],\n isStatic: (config) => /output:\\s*['\"]export['\"]/.test(config),\n },\n 'react-router': {\n provider: 'fumadocs-ui/provider/react-router',\n rootFile: 'root.tsx',\n routesDir: 'routes',\n configFiles: ['vite.config.ts'],\n },\n 'tanstack-start': {\n provider: 'fumadocs-ui/provider/tanstack',\n rootFile: 'routes/__root.tsx',\n routesDir: 'routes',\n configFiles: ['vite.config.ts'],\n isStatic: (config) => /\\bspa:\\s*\\{/.test(config),\n },\n waku: {\n provider: 'fumadocs-ui/provider/waku',\n rootFile: 'pages/_root.tsx',\n routesDir: 'pages',\n configFiles: ['waku.config.ts'],\n },\n astro: {\n provider: 'fumadocs-ui/provider/astro',\n rootFile: 'layouts/Layout.astro',\n routesDir: 'pages',\n configFiles: ['astro.config.mjs', 'astro.config.ts', 'astro.config.js', 'astro.config.mts'],\n isStatic: (config) => !/output:\\s*['\"]server['\"]/.test(config),\n },\n};\n\n/** how docs content is loaded, detected from `lib/source.ts` */\nexport interface SourceInfo {\n /** `lib/source.ts` exports a `source` loader */\n loader: boolean;\n /** `lib/source.ts` exports `getSource()`, a runtime content source resolved on demand */\n dynamic: boolean;\n /** file configuring the collections, relative to cwd: `lib/source.ts` (Fumadocs MDX macro), `source.config.ts` or the Astro config; `null` for other content sources */\n collections: string | null;\n /** collections are loaded lazily, `structuredData` is a function */\n async: boolean;\n /** base URL of docs pages */\n baseUrl: string;\n /** content directory */\n dir: string;\n}\n\nexport interface I18nInfo {\n /** the default locale is hidden from URLs (`hideLocale: 'default-locale'`) */\n optionalLocale: boolean;\n}\n\nexport interface Project {\n cwd: string;\n framework: Framework;\n info: FrameworkInfo;\n /** prerendered to static HTML, no server at runtime */\n static: boolean;\n /** `lib/i18n.ts` exists */\n i18n: I18nInfo | null;\n source: SourceInfo;\n packageManager: PackageManager;\n /** directory of app code, relative to `cwd` */\n baseDir: string;\n config: LoadedConfig;\n /** config file that registers bundler plugins, relative to `cwd` */\n configFile?: string;\n packageJson: {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n}\n\nexport function isSupportedFramework(v: string): v is Framework {\n return v in frameworks;\n}\n\nexport async function loadProject(\n config: LoadedConfig,\n cwd = process.cwd(),\n framework: Framework = assertFramework(config.framework),\n): Promise<Project> {\n const info = frameworks[framework];\n const [packageJson, pm] = await Promise.all([\n fs\n .readFile(path.join(cwd, 'package.json'), 'utf-8')\n .then((content) => JSON.parse(content))\n .catch(() => ({})),\n detectPackageManager({ cwd }),\n ]);\n let configFile: string | undefined;\n let configContent: string | undefined;\n for (const file of info.configFiles) {\n if (await exists(path.join(cwd, file))) {\n configFile = file;\n configContent = await fs.readFile(path.join(cwd, file), 'utf-8');\n break;\n }\n }\n\n let isStatic = false;\n if (framework === 'react-router') {\n const content = await fs\n .readFile(path.join(cwd, 'react-router.config.ts'), 'utf-8')\n .catch(() => '');\n isStatic = /ssr:\\s*false/.test(content);\n } else if (info.isStatic) {\n isStatic = info.isStatic(configContent ?? '');\n }\n\n const read = (file: string) => fs.readFile(path.join(cwd, file), 'utf-8').catch(() => null);\n const i18nConfig = await read(path.join(config.baseDir, 'lib/i18n.ts'));\n\n return {\n cwd,\n framework,\n info,\n static: isStatic,\n i18n:\n i18nConfig === null\n ? null\n : { optionalLocale: /hideLocale:\\s*['\"]default-locale['\"]/.test(i18nConfig) },\n packageManager: pm?.name ?? 'npm',\n baseDir: config.baseDir,\n config,\n configFile,\n packageJson,\n source: await detectSource(cwd, config.baseDir, read, configFile, configContent),\n };\n}\n\nasync function detectSource(\n cwd: string,\n baseDir: string,\n read: (file: string) => Promise<string | null>,\n configFile?: string,\n configContent?: string,\n): Promise<SourceInfo> {\n const sourceFile = path.join(baseDir, 'lib/source.ts');\n const source = (await read(sourceFile)) ?? '';\n let collections: string | null = null;\n let content = source;\n const astro = source.includes('@fumadocs/astro/source');\n if (source.includes('defineDocs(')) {\n collections = sourceFile;\n } else if (astro) {\n // the options of `fumadocs()` in Astro config\n collections = configFile ?? null;\n content = configContent ?? '';\n } else if (await exists(path.join(cwd, 'source.config.ts'))) {\n collections = 'source.config.ts';\n content = (await read(collections)) ?? '';\n }\n\n let baseUrl = /baseUrl:\\s*['\"]([^'\"]*)['\"]/.exec(source)?.[1];\n // e.g. `baseUrl: docsRoute` from `lib/shared.ts`\n const ref = /baseUrl:\\s*(\\w+)/.exec(source)?.[1];\n if (!baseUrl && ref) {\n const shared = (await read(path.join(baseDir, 'lib/shared.ts'))) ?? '';\n baseUrl = new RegExp(`${ref}\\\\s*=\\\\s*['\"]([^'\"]*)['\"]`).exec(shared)?.[1];\n }\n\n return {\n loader: /export const source\\b/.test(source),\n dynamic: /export (async )?function getSource\\b/.test(source),\n collections,\n // pages of Astro collections load their data on demand\n async: astro || /async:\\s*true/.test(content),\n baseUrl: baseUrl ?? '/docs',\n dir: /dir:\\s*['\"]([^'\"]*)['\"]/.exec(content)?.[1] ?? 'content/docs',\n };\n}\n\nfunction assertFramework(framework: string): Framework {\n if (isSupportedFramework(framework)) return framework;\n throw new Error(\n `Unsupported framework \"${framework}\", supported: ${Object.keys(frameworks).join(', ')}.`,\n );\n}\n\nexport { detectFramework };\n"],"mappings":"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"}
@@ -1,10 +1,8 @@
1
- import { r as LoadedConfig } from "../config-Bfg4yG8S.js";
1
+ import { r as LoadedConfig } from "../config-Z-4b_Y_C.js";
2
2
  import { InteractiveInstaller } from "@fuma-cli/interactive";
3
3
  import { IOInterface } from "fuma-cli/registry/installer";
4
4
  import { RegistryConnector } from "fuma-cli/registry/connector";
5
- //#region src/registry/installer.d.ts
6
5
  export declare class FumadocsComponentInstaller extends InteractiveInstaller {
7
6
  constructor(connector: RegistryConnector, config: LoadedConfig, cwd?: string, io?: Partial<IOInterface>);
8
7
  }
9
- //#endregion
10
8
  //# sourceMappingURL=installer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"installer.d.ts","names":[],"sources":["../../src/registry/installer.ts"],"mappings":";;;;;qBAMa,mCAAmC;EAC9C,YACE,WAAW,mBACX,QAAQ,cACR,cACA,KAAK,QAAQ"}
1
+ {"version":3,"file":"installer.d.ts","names":[],"sources":["../../src/registry/installer.ts"],"mappings":";;;;qBAMa,mCAAmC;EAC9C,YACE,WAAW,mBACX,QAAQ,cACR,cACA,KAAK,QAAQ"}
@@ -1,2 +1 @@
1
- import { t as FumadocsComponentInstaller } from "../installer-C4662Woc.js";
2
- export { FumadocsComponentInstaller };
1
+ export { t as FumadocsComponentInstaller } from "../installer-CEOV0fBv.js";
@@ -1,16 +1,20 @@
1
- import { b as findJsxElement, m as addJsxAttribute, o as filterReactRouterPrerenderArray, p as addImport, r as addTanstackPrerender, s as filterReactRouterRoute } from "./codemod-auol-p4R.js";
2
- import { a as routeModule, r as formatRoute, t as docs } from "./docs-qRGwd9Yp.js";
3
- import { a as reactFramework, d as sourceRef, i as posix, l as scripts, r as findSource, s as registerReactRouterRoutes } from "./utils-DbMe7h6c.js";
1
+ import { b as findJsxElement, m as addJsxAttribute, o as filterReactRouterPrerenderArray, p as addImport, r as addTanstackPrerender, s as filterReactRouterRoute } from "./codemod-Wc9nRqrw.js";
2
+ import { i as formatRoute, o as routeModule, t as docs } from "./docs---dy-BSm.js";
3
+ import { a as registerReactRouterRoutes, i as posix, l as sourceRef, r as findSource, s as scripts } from "./utils-Btt-BJ53.js";
4
4
  import fs from "node:fs/promises";
5
5
  import path from "node:path";
6
- //#region src/features/search.ts
7
- const publicEnv = (next) => next ? {
8
- prefix: "NEXT_PUBLIC_",
9
- read: "process.env.NEXT_PUBLIC_"
10
- } : {
11
- prefix: "VITE_",
12
- read: "import.meta.env.VITE_"
13
- };
6
+ // src/features/search.ts
7
+ function publicEnv(framework) {
8
+ if (framework === "next") return {
9
+ prefix: "NEXT_PUBLIC_",
10
+ read: "process.env.NEXT_PUBLIC_"
11
+ };
12
+ const prefix = framework === "astro" ? "PUBLIC_" : "VITE_";
13
+ return {
14
+ prefix,
15
+ read: `import.meta.env.${prefix}`
16
+ };
17
+ }
14
18
  const dialogImports = `import {
15
19
  SearchDialog,
16
20
  SearchDialogClose,
@@ -67,11 +71,13 @@ async function getDocuments() {
67
71
  }
68
72
  return documents;
69
73
  }`;
70
- /** `scripts/sync-content.ts`, syncing the search indexes pre-rendered to `output` */
74
+ /** `scripts/sync-content.ts`, syncing the search indexes pre-rendered to `output`, the first match of a glob pattern (Next.js) */
71
75
  const syncScript = (output, imports, body) => `import * as fs from 'node:fs/promises';
72
76
  ${imports}
73
77
 
74
- const records = JSON.parse(await fs.readFile('${output}', 'utf-8')) as DocumentRecord[];
78
+ ${output.includes("*") ? `// Next.js writes it under \`route-cache\` when built with an adapter, like on Vercel
79
+ const [file] = await Array.fromAsync(fs.glob('${output}'));
80
+ const records = JSON.parse(await fs.readFile(file, 'utf-8')) as DocumentRecord[];` : `const records = JSON.parse(await fs.readFile('${output}', 'utf-8')) as DocumentRecord[];`}
75
81
 
76
82
  ${body}
77
83
  console.log(\`search updated: \${records.length} records\`);
@@ -285,26 +291,26 @@ import Mixedbread from '@mixedbread/sdk';`, `await sync(new Mixedbread(), {
285
291
  });
286
292
  `),
287
293
  searchRoute: (() => {
288
- const server = `import { createMixedbreadSearchAPI } from 'fumadocs-core/search/mixedbread';
289
- import Mixedbread from '@mixedbread/sdk';
294
+ const server = (astro = false) => `import { createMixedbreadSearchAPI } from 'fumadocs-core/search/mixedbread';
295
+ import Mixedbread from '@mixedbread/sdk';${astro ? "\nimport { getSecret } from 'astro:env/server';" : ""}
290
296
 
291
297
  const server = createMixedbreadSearchAPI({
292
- client: new Mixedbread(),
293
- storeIdentifier: process.env.MIXEDBREAD_STORE_ID!,
298
+ client: new Mixedbread(${astro ? "{ apiKey: getSecret('MXBAI_API_KEY') }" : ""}),
299
+ storeIdentifier: ${astro ? "getSecret('MIXEDBREAD_STORE_ID')" : "process.env.MIXEDBREAD_STORE_ID"}!,
294
300
  });
295
301
  `;
296
302
  return {
297
- next: `${server}
303
+ next: `${server()}
298
304
  export const { GET } = server;
299
305
  `,
300
306
  "react-router": `import type { Route } from './+types/search';
301
- ${server}
307
+ ${server()}
302
308
  export async function loader({ request }: Route.LoaderArgs) {
303
309
  return server.GET(request);
304
310
  }
305
311
  `,
306
312
  "tanstack-start": `import { createFileRoute } from '@tanstack/react-router';
307
- ${server}
313
+ ${server()}
308
314
  export const Route = createFileRoute('/api/search')({
309
315
  server: {
310
316
  handlers: {
@@ -313,8 +319,12 @@ export const Route = createFileRoute('/api/search')({
313
319
  },
314
320
  });
315
321
  `,
316
- waku: `${server}
322
+ waku: `${server()}
317
323
  export const { GET } = server;
324
+ `,
325
+ astro: `import type { APIRoute } from 'astro';
326
+ ${server(true)}
327
+ export const GET: APIRoute = ({ request }) => server.GET(request);
318
328
  `
319
329
  };
320
330
  })()
@@ -329,14 +339,15 @@ const staticRoute = (framework, head, documents) => routeModule(framework, stati
329
339
  static: true
330
340
  });
331
341
  /** where `static.json` is pre-rendered on build, `config` is the Vite config of TanStack Start */
332
- function staticOutput(framework, config = "") {
342
+ function staticOutput(framework, config = "", isStatic = true) {
333
343
  switch (framework) {
334
- case "next": return ".next/server/app/static.json.body";
344
+ case "next": return ".next/server/{app,route-cache/*/*/$}/static.json.body";
335
345
  case "react-router": return "build/client/static.json";
336
346
  case "waku": return "dist/public/static.json";
337
347
  case "tanstack-start":
338
348
  if (/preset:\s*['"]vercel['"]/.test(config)) return ".vercel/output/static/static.json";
339
349
  return /\bnitro\(/.test(config) ? ".output/public/static.json" : "dist/client/static.json";
350
+ case "astro": return isStatic ? "dist/static.json" : "dist/client/static.json";
340
351
  }
341
352
  }
342
353
  /** runs the sync script after build */
@@ -344,7 +355,7 @@ const syncCommand = "node --env-file-if-exists=.env.local scripts/sync-content.t
344
355
  /** the files written by a provider, relative to the project, `output` is where `static.json` is pre-rendered */
345
356
  function templates(name, framework, { baseDir, source }, output) {
346
357
  const provider = providers[name];
347
- const env = publicEnv(framework === "next");
358
+ const env = publicEnv(framework);
348
359
  const files = [[path.posix.join(baseDir, "components/search.tsx"), provider.dialog(env)]];
349
360
  if (provider.exportIndexes) {
350
361
  const { head, documents } = provider.exportIndexes(sourceRef(source.dynamic), source.async);
@@ -369,11 +380,10 @@ const search = {
369
380
  } },
370
381
  async apply(ctx, { provider: name }) {
371
382
  const { project } = ctx;
372
- const { baseDir } = project;
373
- const framework = reactFramework(project);
383
+ const { baseDir, framework } = project;
374
384
  const provider = providers[name];
375
385
  if (project.static && !provider.static) throw new Error(`${provider.label} requires a server at runtime`);
376
- const output = staticOutput(framework, framework === "tanstack-start" && project.configFile ? await fs.readFile(path.join(project.cwd, project.configFile), "utf-8") : "");
386
+ const output = staticOutput(framework, framework === "tanstack-start" && project.configFile ? await fs.readFile(path.join(project.cwd, project.configFile), "utf-8") : "", project.static);
377
387
  ctx.addDependencies(provider.dependencies);
378
388
  for (const [file, content] of templates(name, framework, project, output)) await ctx.write(file, content);
379
389
  const searchRoute = await findSource(project.cwd, path.join(baseDir, project.info.routesDir), "createFromSource(", (file) => /(^|[\\/])search(\.ts|[\\/]route\.ts)$/.test(file));
@@ -401,7 +411,7 @@ const search = {
401
411
  if (build.includes("sync-content")) return;
402
412
  scripts(data, { build: build ? `${build} && ${syncCommand}` : syncCommand });
403
413
  });
404
- const env = publicEnv(framework === "next");
414
+ const env = publicEnv(framework);
405
415
  for (const key of provider.publicEnv) ctx.env(env.prefix + key, "");
406
416
  for (const key of provider.privateEnv) ctx.env(key, "");
407
417
  ctx.note(`Fill the ${provider.label} keys in \`.env.local\`, the search index is synced after each build.\nSee https://fumadocs.dev/docs/headless/search/${name}.`);
@@ -420,7 +430,6 @@ async function removeDefaultSearch(ctx, file) {
420
430
  filterReactRouterPrerenderArray(source, "excluded", (item) => item !== "/api/search");
421
431
  });
422
432
  }
423
- //#endregion
424
433
  export { syncCommand as a, staticRoute as i, staticJson as n, syncScript as o, staticOutput as r, templates as s, search as t };
425
434
 
426
- //# sourceMappingURL=search-BCHEW0-K.js.map
435
+ //# sourceMappingURL=search-B44II4bM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-B44II4bM.js","names":[],"sources":["../src/features/search.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport type { Feature, FeatureContext } from '@/features';\nimport type { Framework, SourceInfo } from '@/project';\nimport { formatRoute, routeModule } from '@/project/route';\nimport {\n addImport,\n addJsxAttribute,\n addTanstackPrerender,\n filterReactRouterPrerenderArray,\n filterReactRouterRoute,\n findJsxElement,\n} from '@/codemod';\nimport { docs } from './docs';\nimport {\n findSource,\n posix,\n registerReactRouterRoutes,\n scripts,\n type SourceRef,\n sourceRef,\n} from './utils';\n\n/** how client code reads public env variables */\ninterface Env {\n prefix: string;\n read: string;\n}\n\nfunction publicEnv(framework: Framework): Env {\n if (framework === 'next') return { prefix: 'NEXT_PUBLIC_', read: 'process.env.NEXT_PUBLIC_' };\n const prefix = framework === 'astro' ? 'PUBLIC_' : 'VITE_';\n return { prefix, read: `import.meta.env.${prefix}` };\n}\n\nconst dialogImports = `import {\n SearchDialog,\n SearchDialogClose,\n SearchDialogContent,\n SearchDialogFooter,\n SearchDialogHeader,\n SearchDialogIcon,\n SearchDialogInput,\n SearchDialogList,\n SearchDialogOverlay,\n type SharedProps,\n} from 'fumadocs-ui/components/dialog/search';\nimport { useI18n } from 'fumadocs-ui/contexts/i18n';`;\n\n/** `legacy` for hooks in the shape of the deprecated `useDocsSearch()`, like the Typesense adapter */\nconst dialogBody = (footer: string, url: string, legacy = false) => ` return (\n <SearchDialog ${legacy ? 'search={search} onSearchChange={setSearch} isLoading={query.isLoading}' : '{...search}'} {...props}>\n <SearchDialogOverlay />\n <SearchDialogContent>\n <SearchDialogHeader>\n <SearchDialogIcon />\n <SearchDialogInput />\n <SearchDialogClose />\n </SearchDialogHeader>\n <SearchDialogList ${legacy ? \"items={query.data !== 'empty' ? query.data : null} \" : ''}/>\n <SearchDialogFooter>\n <a\n href=\"${url}\"\n rel=\"noreferrer noopener\"\n className=\"ms-auto text-xs text-fd-muted-foreground\"\n >\n Search powered by ${footer}\n </a>\n </SearchDialogFooter>\n </SearchDialogContent>\n </SearchDialog>\n );\n}\n`;\n\n/** `structuredData` is a function on async collections (React Router & TanStack Start) */\nconst typesenseDocuments = (\n { ref, resolved }: SourceRef,\n async: boolean,\n) => `import { ${ref} } from '@/lib/source';\nimport type { DocumentRecord } from 'typesense-fumadocs-adapter';\n\nasync function getDocuments() {\n const documents: DocumentRecord[] = [];\n for (const page of ${resolved}.getPages()) {\n documents.push({\n _id: page.url,\n structured: ${async ? 'await page.data.structuredData()' : 'page.data.structuredData'},\n url: page.url,\n title: page.data.title,\n description: page.data.description,\n locale: page.locale,\n });\n }\n return documents;\n}`;\n\n/** `scripts/sync-content.ts`, syncing the search indexes pre-rendered to `output`, the first match of a glob pattern (Next.js) */\nexport const syncScript = (\n output: string,\n imports: string,\n body: string,\n) => `import * as fs from 'node:fs/promises';\n${imports}\n\n${\n output.includes('*')\n ? `// Next.js writes it under \\`route-cache\\` when built with an adapter, like on Vercel\nconst [file] = await Array.fromAsync(fs.glob('${output}'));\nconst records = JSON.parse(await fs.readFile(file, 'utf-8')) as DocumentRecord[];`\n : `const records = JSON.parse(await fs.readFile('${output}', 'utf-8')) as DocumentRecord[];`\n}\n\n${body}\nconsole.log(\\`search updated: \\${records.length} records\\`);\n`;\n\ninterface Provider {\n label: string;\n hint: string;\n dependencies: Record<string, string | null>;\n /** keys exposed to the client, prefixed per framework */\n publicEnv: string[];\n privateEnv: string[];\n /** works without a server at runtime */\n static: boolean;\n dialog: (env: Env) => string;\n /** exports search indexes to a pre-rendered `static.json`, synced by `sync` after build */\n exportIndexes?: (\n src: SourceRef,\n async: boolean,\n ) => {\n /** code before the route handler */\n head: string;\n /** expression producing the documents */\n documents: string;\n };\n /** `scripts/sync-content.ts`, run after build */\n sync?: (env: Env, paths: { output: string }) => string;\n /** server-side search, replaces the default search route */\n searchRoute?: Record<Framework, string>;\n}\n\nconst providers = {\n 'orama-cloud': {\n label: 'Orama Cloud',\n hint: 'signup needed',\n dependencies: { '@orama/core': null },\n publicEnv: ['ORAMA_DATASOURCE_ID', 'ORAMA_PROJECT_ID', 'ORAMA_API_KEY'],\n privateEnv: ['ORAMA_PRIVATE_API_KEY'],\n static: true,\n dialog: (env) => `'use client';\n${dialogImports}\nimport { useOramaCloudSearch } from 'fumadocs-core/search/client';\nimport { OramaCloud } from '@orama/core';\n\nconst client = new OramaCloud({\n projectId: ${env.read}ORAMA_PROJECT_ID!,\n apiKey: ${env.read}ORAMA_API_KEY!,\n});\n\nexport default function CustomSearchDialog(props: SharedProps) {\n const { locale } = useI18n();\n const search = useOramaCloudSearch({\n client,\n locale,\n });\n\n${dialogBody('Orama', 'https://orama.com')}`,\n exportIndexes: ({ ref }) => ({\n head: `import { ${ref} } from '@/lib/source';\nimport { toDocuments } from 'fumadocs-core/search/orama-cloud';`,\n documents: `toDocuments(${ref})`,\n }),\n sync: (env, { output }) =>\n syncScript(\n output,\n `import { type OramaDocument as DocumentRecord, sync } from 'fumadocs-core/search/orama-cloud';\nimport { OramaCloud } from '@orama/core';`,\n `const orama = new OramaCloud({\n projectId: process.env.${env.prefix}ORAMA_PROJECT_ID!,\n apiKey: process.env.ORAMA_PRIVATE_API_KEY!,\n});\n\nawait sync(orama, {\n index: process.env.${env.prefix}ORAMA_DATASOURCE_ID!,\n documents: records,\n});\n`,\n ),\n },\n algolia: {\n label: 'Algolia',\n hint: 'signup needed',\n dependencies: { algoliasearch: null },\n publicEnv: ['ALGOLIA_APP_ID', 'ALGOLIA_SEARCH_KEY'],\n privateEnv: ['ALGOLIA_WRITE_KEY'],\n static: true,\n dialog: (env) => `'use client';\n${dialogImports}\nimport { useAlgoliaSearch } from 'fumadocs-core/search/client';\nimport { liteClient } from 'algoliasearch/lite';\n\nconst algolia = liteClient(${env.read}ALGOLIA_APP_ID!, ${env.read}ALGOLIA_SEARCH_KEY!);\n\nexport default function CustomSearchDialog(props: SharedProps) {\n const { locale } = useI18n();\n const search = useAlgoliaSearch({\n client: algolia,\n indexName: 'document',\n locale,\n });\n\n${dialogBody('Algolia', 'https://algolia.com')}`,\n exportIndexes: ({ ref }) => ({\n head: `import { ${ref} } from '@/lib/source';\nimport { toDocuments } from 'fumadocs-core/search/algolia';`,\n documents: `toDocuments(${ref})`,\n }),\n sync: (env, { output }) =>\n syncScript(\n output,\n `import { type DocumentRecord, sync } from 'fumadocs-core/search/algolia';\nimport { algoliasearch } from 'algoliasearch';`,\n `const client = algoliasearch(\n process.env.${env.prefix}ALGOLIA_APP_ID!,\n process.env.ALGOLIA_WRITE_KEY!,\n);\n\nawait sync(client, {\n indexName: 'document',\n documents: records,\n});\n`,\n ),\n },\n meilisearch: {\n label: 'Meilisearch',\n hint: 'self-hosted or cloud',\n dependencies: { meilisearch: null },\n publicEnv: ['MEILISEARCH_HOST', 'MEILISEARCH_SEARCH_KEY'],\n privateEnv: ['MEILISEARCH_ADMIN_KEY'],\n static: true,\n dialog: (env) => `'use client';\n${dialogImports}\nimport { useMeilisearch } from 'fumadocs-core/search/client';\nimport { Meilisearch } from 'meilisearch';\n\nconst client = new Meilisearch({\n host: ${env.read}MEILISEARCH_HOST!,\n apiKey: ${env.read}MEILISEARCH_SEARCH_KEY!,\n});\n\nexport default function CustomSearchDialog(props: SharedProps) {\n const { locale } = useI18n();\n const search = useMeilisearch({\n client,\n indexName: 'docs',\n locale,\n });\n\n${dialogBody('Meilisearch', 'https://www.meilisearch.com')}`,\n exportIndexes: ({ ref }) => ({\n head: `import { ${ref} } from '@/lib/source';\nimport { toDocuments } from 'fumadocs-core/search/meilisearch';`,\n documents: `toDocuments(${ref})`,\n }),\n sync: (env, { output }) =>\n syncScript(\n output,\n `import { type DocumentRecord, sync } from 'fumadocs-core/search/meilisearch';\nimport { Meilisearch } from 'meilisearch';`,\n `const client = new Meilisearch({\n host: process.env.${env.prefix}MEILISEARCH_HOST!,\n apiKey: process.env.MEILISEARCH_ADMIN_KEY!,\n});\n\nawait sync(client, {\n indexName: 'docs',\n documents: records,\n});\n`,\n ),\n },\n typesense: {\n label: 'Typesense',\n hint: 'self-hosted or cloud, community adapter',\n dependencies: { typesense: null, 'typesense-fumadocs-adapter': null },\n publicEnv: ['TYPESENSE_URL', 'TYPESENSE_SEARCH_KEY'],\n privateEnv: ['TYPESENSE_API_KEY'],\n static: true,\n dialog: (env) => `'use client';\n${dialogImports}\nimport { Client } from 'typesense';\nimport { useTypesenseSearch } from 'typesense-fumadocs-adapter/client';\n\nconst client = new Client({\n nodes: [{ url: ${env.read}TYPESENSE_URL! }],\n apiKey: ${env.read}TYPESENSE_SEARCH_KEY!,\n});\n\nexport default function CustomSearchDialog(props: SharedProps) {\n const { locale } = useI18n();\n const { search, setSearch, query } = useTypesenseSearch({\n typesenseCollectionName: 'docs',\n locale,\n client,\n });\n\n${dialogBody('Typesense', 'https://typesense.org', true)}`,\n exportIndexes: (src, async) => ({\n head: typesenseDocuments(src, async),\n documents: 'getDocuments()',\n }),\n sync: (env, { output }) =>\n syncScript(\n output,\n `import { type DocumentRecord, sync } from 'typesense-fumadocs-adapter';\nimport { Client } from 'typesense';`,\n `const client = new Client({\n nodes: [{ url: process.env.${env.prefix}TYPESENSE_URL! }],\n apiKey: process.env.TYPESENSE_API_KEY!,\n connectionTimeoutSeconds: 60 * 15,\n});\n\nawait sync(client, {\n typesenseCollectionName: 'docs',\n documents: records,\n});\n`,\n ),\n },\n mixedbread: {\n label: 'Mixedbread',\n hint: 'AI search, signup needed',\n dependencies: { '@mixedbread/sdk': null },\n publicEnv: [],\n privateEnv: ['MXBAI_API_KEY', 'MIXEDBREAD_STORE_ID'],\n static: false,\n dialog: () => `'use client';\n${dialogImports}\nimport { useFetchSearch } from 'fumadocs-core/search/client';\n\nexport default function CustomSearchDialog(props: SharedProps) {\n const { locale } = useI18n();\n const search = useFetchSearch({\n api: '/api/search',\n locale,\n // every search is a request to Mixedbread\n delayMs: 300,\n });\n\n${dialogBody('Mixedbread', 'https://mixedbread.com')}`,\n exportIndexes: ({ ref }) => ({\n head: `import { ${ref} } from '@/lib/source';\nimport { toDocuments } from 'fumadocs-core/search/mixedbread';`,\n documents: `toDocuments(${ref})`,\n }),\n sync: (_env, { output }) =>\n syncScript(\n output,\n `import { type DocumentRecord, sync } from 'fumadocs-core/search/mixedbread';\nimport Mixedbread from '@mixedbread/sdk';`,\n `await sync(new Mixedbread(), {\n storeIdentifier: process.env.MIXEDBREAD_STORE_ID!,\n documents: records,\n});\n`,\n ),\n searchRoute: (() => {\n // Astro inlines `import.meta.env` at build time, `getSecret()` reads secrets at runtime\n const server = (\n astro = false,\n ) => `import { createMixedbreadSearchAPI } from 'fumadocs-core/search/mixedbread';\nimport Mixedbread from '@mixedbread/sdk';${astro ? \"\\nimport { getSecret } from 'astro:env/server';\" : ''}\n\nconst server = createMixedbreadSearchAPI({\n client: new Mixedbread(${astro ? \"{ apiKey: getSecret('MXBAI_API_KEY') }\" : ''}),\n storeIdentifier: ${astro ? \"getSecret('MIXEDBREAD_STORE_ID')\" : 'process.env.MIXEDBREAD_STORE_ID'}!,\n});\n`;\n return {\n next: `${server()}\nexport const { GET } = server;\n`,\n 'react-router': `import type { Route } from './+types/search';\n${server()}\nexport async function loader({ request }: Route.LoaderArgs) {\n return server.GET(request);\n}\n`,\n 'tanstack-start': `import { createFileRoute } from '@tanstack/react-router';\n${server()}\nexport const Route = createFileRoute('/api/search')({\n server: {\n handlers: {\n GET: async ({ request }) => server.GET(request),\n },\n },\n});\n`,\n waku: `${server()}\nexport const { GET } = server;\n`,\n astro: `import type { APIRoute } from 'astro';\n${server(true)}\nexport const GET: APIRoute = ({ request }) => server.GET(request);\n`,\n };\n })(),\n },\n} satisfies Record<string, Provider>;\n\nexport type SearchProvider = keyof typeof providers;\n\n/** pre-rendered on build to export the search indexes */\nexport const staticJson = (framework: Framework) =>\n formatRoute({ segments: ['static.json'] }, framework, null);\n\nexport const staticRoute = (framework: Framework, head: string, documents: string) =>\n routeModule(framework, staticJson(framework), {\n imports: head,\n body: `Response.json(await ${documents})`,\n revalidate: true,\n static: true,\n });\n\n/** where `static.json` is pre-rendered on build, `config` is the Vite config of TanStack Start */\nexport function staticOutput(framework: Framework, config = '', isStatic = true): string {\n switch (framework) {\n case 'next':\n return '.next/server/{app,route-cache/*/*/$}/static.json.body';\n case 'react-router':\n return 'build/client/static.json';\n case 'waku':\n return 'dist/public/static.json';\n case 'tanstack-start':\n // Nitro moves the client output to the directory of its preset\n if (/preset:\\s*['\"]vercel['\"]/.test(config)) return '.vercel/output/static/static.json';\n return /\\bnitro\\(/.test(config) ? '.output/public/static.json' : 'dist/client/static.json';\n case 'astro':\n // with routes rendered on demand, prerendered files are in the client output\n return isStatic ? 'dist/static.json' : 'dist/client/static.json';\n }\n}\n\n/** runs the sync script after build */\nexport const syncCommand = 'node --env-file-if-exists=.env.local scripts/sync-content.ts';\n\n/** the files written by a provider, relative to the project, `output` is where `static.json` is pre-rendered */\nexport function templates(\n name: SearchProvider,\n framework: Framework,\n { baseDir, source }: { baseDir: string; source: Pick<SourceInfo, 'dynamic' | 'async'> },\n output: string,\n): [file: string, content: string][] {\n const provider: Provider = providers[name];\n const env = publicEnv(framework);\n const files: [string, string][] = [\n [path.posix.join(baseDir, 'components/search.tsx'), provider.dialog(env)],\n ];\n\n if (provider.exportIndexes) {\n const { head, documents } = provider.exportIndexes(sourceRef(source.dynamic), source.async);\n files.push([\n path.posix.join(baseDir, staticJson(framework).file),\n staticRoute(framework, head, documents),\n ]);\n }\n if (provider.sync) files.push(['scripts/sync-content.ts', provider.sync(env, { output })]);\n return files;\n}\n\nexport const search: Feature<{ provider: SearchProvider }> = {\n id: 'search',\n title: 'Search',\n description: 'replace the default search with a 3rd party search solution',\n requires: [docs],\n supports: (project) =>\n project.source.loader ||\n project.source.dynamic ||\n '`lib/source.ts` must export a `source` loader',\n options: {\n provider: {\n message: 'Choose a search provider',\n choices: Object.entries(providers).map(([value, { label, hint }]) => ({\n value: value as SearchProvider,\n label,\n hint,\n })),\n },\n },\n async apply(ctx, { provider: name }) {\n const { project } = ctx;\n const { baseDir, framework } = project;\n const provider: Provider = providers[name];\n if (project.static && !provider.static)\n throw new Error(`${provider.label} requires a server at runtime`);\n\n const config =\n framework === 'tanstack-start' && project.configFile\n ? await fs.readFile(path.join(project.cwd, project.configFile), 'utf-8')\n : '';\n const output = staticOutput(framework, config, project.static);\n\n ctx.addDependencies(provider.dependencies);\n for (const [file, content] of templates(name, framework, project, output))\n await ctx.write(file, content);\n\n // other routes (e.g. MCP) also create a search server, only match the search route itself\n const searchRoute = await findSource(\n project.cwd,\n path.join(baseDir, project.info.routesDir),\n 'createFromSource(',\n (file) => /(^|[\\\\/])search(\\.ts|[\\\\/]route\\.ts)$/.test(file),\n );\n if (provider.searchRoute) {\n if (searchRoute) await ctx.write(searchRoute, provider.searchRoute[framework]);\n else\n ctx.note(\n 'Create the search route with `createMixedbreadSearchAPI`, see https://fumadocs.dev/docs/headless/search/mixedbread.',\n );\n } else if (searchRoute) {\n await removeDefaultSearch(ctx, searchRoute);\n }\n\n if (provider.exportIndexes) {\n const route = staticJson(framework);\n if (framework === 'react-router') {\n await registerReactRouterRoutes(ctx, [route]);\n } else if (framework === 'tanstack-start' && project.configFile) {\n await ctx.source(project.configFile, (file) => addTanstackPrerender(file, [route.path]));\n }\n }\n\n const rootProvider = await findSource(project.cwd, baseDir, '<RootProvider');\n const edited =\n rootProvider !== undefined &&\n (await ctx.source(rootProvider, (file) => {\n const element = findJsxElement(file, 'RootProvider')?.openingElement;\n if (!element || file.code.slice(element.start, element.end).includes('search=')) return;\n addJsxAttribute(file, element, 'search={{ SearchDialog }}');\n addImport(file, { from: '@/components/search', default: 'SearchDialog' });\n }));\n if (!edited)\n ctx.note(\n 'Pass `search={{ SearchDialog }}` from `@/components/search` to `<RootProvider />`.',\n );\n\n if (provider.sync) {\n await ctx.packageJson((data) => {\n const build = data.scripts?.build ?? '';\n if (build.includes('sync-content')) return;\n scripts(data, { build: build ? `${build} && ${syncCommand}` : syncCommand });\n });\n }\n\n const env = publicEnv(framework);\n for (const key of provider.publicEnv) ctx.env(env.prefix + key, '');\n for (const key of provider.privateEnv) ctx.env(key, '');\n ctx.note(\n `Fill the ${provider.label} keys in \\`.env.local\\`, the search index is synced after each build.\\nSee https://fumadocs.dev/docs/headless/search/${name}.`,\n );\n },\n};\n\n/** remove the default search route, which conflicts with the search provider */\nasync function removeDefaultSearch(ctx: FeatureContext, file: string) {\n const { baseDir, framework } = ctx.project;\n await ctx.remove(file);\n if (framework !== 'react-router') return;\n\n const entry = posix(path.relative(baseDir, file));\n await ctx.source(path.join(baseDir, 'routes.ts'), (source) => {\n filterReactRouterRoute(source, (item) => item.entry !== entry);\n });\n await ctx.source('react-router.config.ts', (source) => {\n filterReactRouterPrerenderArray(source, 'excluded', (item) => item !== '/api/search');\n });\n}\n"],"mappings":"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"}