@fumadocs/cli 1.7.0 → 1.7.2
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/ai-wXWLPFI5.js +112 -0
- package/dist/ai-wXWLPFI5.js.map +1 -0
- package/dist/{all-BBf7I8hG.js → all-BmDsMSFN.js} +3 -3
- package/dist/{all-BBf7I8hG.js.map → all-BmDsMSFN.js.map} +1 -1
- package/dist/features/ai.js +1 -1
- package/dist/features/all.js +1 -1
- package/dist/features/search.d.ts +55 -10
- package/dist/features/search.d.ts.map +1 -1
- package/dist/features/search.js +2 -2
- package/dist/index.js +68 -169
- package/dist/index.js.map +1 -1
- package/dist/{search-WfSdiGx-.js → search-BbzvfJal.js} +134 -77
- package/dist/search-BbzvfJal.js.map +1 -0
- package/package.json +4 -4
- package/dist/ai-C19ZOP53.js +0 -100
- package/dist/ai-C19ZOP53.js.map +0 -1
- package/dist/search-WfSdiGx-.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fumadocs/cli",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.2",
|
|
4
4
|
"description": "The CLI tool for Fumadocs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Docs",
|
|
@@ -49,14 +49,14 @@
|
|
|
49
49
|
"@fuma-cli/interactive": "^0.1.2",
|
|
50
50
|
"cac": "^7.0.0",
|
|
51
51
|
"fuma-cli": "^0.3.1",
|
|
52
|
-
"magic-string": "^1.4.
|
|
53
|
-
"oxc-parser": "^0.
|
|
52
|
+
"magic-string": "^1.4.2",
|
|
53
|
+
"oxc-parser": "^0.152.0",
|
|
54
54
|
"picocolors": "^1.1.1",
|
|
55
55
|
"tinyexec": "^1.3.1",
|
|
56
56
|
"zod": "^4.6.5"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@types/node": "26.6.
|
|
59
|
+
"@types/node": "26.6.4",
|
|
60
60
|
"tsconfig": "0.0.0",
|
|
61
61
|
"tsdown": "0.23.0"
|
|
62
62
|
},
|
package/dist/ai-C19ZOP53.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { E as prependJsxChildren, b as findJsxElement, p as addImport } from "./codemod-auol-p4R.js";
|
|
2
|
-
import { t as docs } from "./docs--WqEHTyl.js";
|
|
3
|
-
import { r as findSource } from "./utils-DbMe7h6c.js";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
//#region src/features/ai.ts
|
|
6
|
-
const providers = {
|
|
7
|
-
openrouter: {
|
|
8
|
-
label: "AI SDK",
|
|
9
|
-
hint: "default to OpenRouter",
|
|
10
|
-
env: "OPENROUTER_API_KEY"
|
|
11
|
-
},
|
|
12
|
-
llmgateway: {
|
|
13
|
-
label: "LLMGateway",
|
|
14
|
-
hint: "open-source LLM gateway, API key required",
|
|
15
|
-
env: "LLM_GATEWAY_API_KEY"
|
|
16
|
-
},
|
|
17
|
-
inkeep: {
|
|
18
|
-
label: "Inkeep AI",
|
|
19
|
-
hint: "API key required",
|
|
20
|
-
env: "INKEEP_API_KEY"
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
const ai = {
|
|
24
|
-
id: "ai",
|
|
25
|
-
title: "AI Chat",
|
|
26
|
-
description: "Ask AI dialog for your docs",
|
|
27
|
-
requires: [docs],
|
|
28
|
-
options: { provider: {
|
|
29
|
-
message: "Choose an AI provider",
|
|
30
|
-
choices: Object.entries(providers).map(([value, { label, hint }]) => ({
|
|
31
|
-
value,
|
|
32
|
-
label,
|
|
33
|
-
hint
|
|
34
|
-
}))
|
|
35
|
-
} },
|
|
36
|
-
supports: (project) => project.static ? "AI chat requires a server to run the chat route handler." : true,
|
|
37
|
-
async apply(ctx, { provider }) {
|
|
38
|
-
await ctx.install(`ai/${provider}`);
|
|
39
|
-
ctx.env(providers[provider].env, "");
|
|
40
|
-
if (ctx.project.source.dynamic) await readDynamicSource(ctx);
|
|
41
|
-
const { cwd, baseDir, info } = ctx.project;
|
|
42
|
-
const layout = await findSource(cwd, path.join(baseDir, info.routesDir), "<DocsLayout");
|
|
43
|
-
if (!(layout !== void 0 && await ctx.source(layout, (file) => {
|
|
44
|
-
if (file.code.includes("<AISearch")) return;
|
|
45
|
-
const element = findJsxElement(file, "DocsLayout");
|
|
46
|
-
if (!element) return;
|
|
47
|
-
prependJsxChildren(file, element, `<AISearch>
|
|
48
|
-
<AISearchPanel />
|
|
49
|
-
<AISearchTrigger
|
|
50
|
-
position="float"
|
|
51
|
-
className={cn(
|
|
52
|
-
buttonVariants({
|
|
53
|
-
variant: 'secondary',
|
|
54
|
-
className: 'text-fd-muted-foreground rounded-2xl',
|
|
55
|
-
}),
|
|
56
|
-
)}
|
|
57
|
-
>
|
|
58
|
-
<MessageCircleIcon className="size-4.5" />
|
|
59
|
-
Ask AI
|
|
60
|
-
</AISearchTrigger>
|
|
61
|
-
</AISearch>`);
|
|
62
|
-
addImport(file, {
|
|
63
|
-
from: "@/components/ai/search",
|
|
64
|
-
named: [
|
|
65
|
-
"AISearch",
|
|
66
|
-
"AISearchPanel",
|
|
67
|
-
"AISearchTrigger"
|
|
68
|
-
]
|
|
69
|
-
});
|
|
70
|
-
addImport(file, {
|
|
71
|
-
from: "lucide-react",
|
|
72
|
-
named: ["MessageCircleIcon"]
|
|
73
|
-
});
|
|
74
|
-
addImport(file, {
|
|
75
|
-
from: "@/lib/cn",
|
|
76
|
-
named: ["cn"]
|
|
77
|
-
});
|
|
78
|
-
addImport(file, {
|
|
79
|
-
from: "fumadocs-ui/components/ui/button",
|
|
80
|
-
named: ["buttonVariants"]
|
|
81
|
-
});
|
|
82
|
-
}))) ctx.note("Add `<AISearch>` from `@/components/ai/search` to your docs layout, see https://fumadocs.dev/docs/integrations/llms#ask-ai.");
|
|
83
|
-
ctx.note(`Set ${providers[provider].env} in \`.env.local\`.`);
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
/** the installed chat route indexes pages of a static `source`, runtime sources resolve on demand */
|
|
87
|
-
async function readDynamicSource(ctx) {
|
|
88
|
-
const { cwd, baseDir } = ctx.project;
|
|
89
|
-
const route = await findSource(cwd, baseDir, "createSearchServer");
|
|
90
|
-
if (!(route !== void 0 && await ctx.source(route, (file) => {
|
|
91
|
-
file.s.replaceAll("import { source } from '@/lib/source';", "import { getSource } from '@/lib/source';");
|
|
92
|
-
file.s.replaceAll("source.getPages()", "(await getSource()).getPages()");
|
|
93
|
-
file.s.replaceAll(" if (!('getText' in page.data)) return null;\n\n", "");
|
|
94
|
-
file.s.replaceAll("await page.data.getText('processed')", "page.data.content");
|
|
95
|
-
}))) ctx.note("Read pages from `getSource()` in the chat route, the search index is built from `page.data.content`.");
|
|
96
|
-
}
|
|
97
|
-
//#endregion
|
|
98
|
-
export { ai as t };
|
|
99
|
-
|
|
100
|
-
//# sourceMappingURL=ai-C19ZOP53.js.map
|
package/dist/ai-C19ZOP53.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ai-C19ZOP53.js","names":[],"sources":["../src/features/ai.ts"],"sourcesContent":["import path from 'node:path';\nimport type { Feature, FeatureContext } from '@/features';\nimport { findSource } from './utils';\nimport { addImport, findJsxElement, prependJsxChildren } from '@/codemod';\nimport { docs } from './docs';\n\nconst providers = {\n openrouter: { label: 'AI SDK', hint: 'default to OpenRouter', env: 'OPENROUTER_API_KEY' },\n llmgateway: {\n label: 'LLMGateway',\n hint: 'open-source LLM gateway, API key required',\n env: 'LLM_GATEWAY_API_KEY',\n },\n inkeep: { label: 'Inkeep AI', hint: 'API key required', env: 'INKEEP_API_KEY' },\n};\n\nexport type AIProvider = keyof typeof providers;\n\nexport const ai: Feature<{ provider: AIProvider }> = {\n id: 'ai',\n title: 'AI Chat',\n description: 'Ask AI dialog for your docs',\n requires: [docs],\n options: {\n provider: {\n message: 'Choose an AI provider',\n choices: Object.entries(providers).map(([value, { label, hint }]) => ({\n value: value as AIProvider,\n label,\n hint,\n })),\n },\n },\n supports: (project) =>\n project.static ? 'AI chat requires a server to run the chat route handler.' : true,\n async apply(ctx, { provider }) {\n await ctx.install(`ai/${provider}`);\n ctx.env(providers[provider].env, '');\n if (ctx.project.source.dynamic) await readDynamicSource(ctx);\n\n const { cwd, baseDir, info } = ctx.project;\n const layout = await findSource(cwd, path.join(baseDir, info.routesDir), '<DocsLayout');\n const edited =\n layout !== undefined &&\n (await ctx.source(layout, (file) => {\n if (file.code.includes('<AISearch')) return;\n const element = findJsxElement(file, 'DocsLayout');\n if (!element) return;\n\n prependJsxChildren(\n file,\n element,\n `<AISearch>\n <AISearchPanel />\n <AISearchTrigger\n position=\"float\"\n className={cn(\n buttonVariants({\n variant: 'secondary',\n className: 'text-fd-muted-foreground rounded-2xl',\n }),\n )}\n >\n <MessageCircleIcon className=\"size-4.5\" />\n Ask AI\n </AISearchTrigger>\n</AISearch>`,\n );\n addImport(file, {\n from: '@/components/ai/search',\n named: ['AISearch', 'AISearchPanel', 'AISearchTrigger'],\n });\n addImport(file, { from: 'lucide-react', named: ['MessageCircleIcon'] });\n addImport(file, { from: '@/lib/cn', named: ['cn'] });\n addImport(file, { from: 'fumadocs-ui/components/ui/button', named: ['buttonVariants'] });\n }));\n\n if (!edited) {\n ctx.note(\n 'Add `<AISearch>` from `@/components/ai/search` to your docs layout, see https://fumadocs.dev/docs/integrations/llms#ask-ai.',\n );\n }\n ctx.note(`Set ${providers[provider].env} in \\`.env.local\\`.`);\n },\n};\n\n/** the installed chat route indexes pages of a static `source`, runtime sources resolve on demand */\nasync function readDynamicSource(ctx: FeatureContext) {\n const { cwd, baseDir } = ctx.project;\n const route = await findSource(cwd, baseDir, 'createSearchServer');\n const edited =\n route !== undefined &&\n (await ctx.source(route, (file) => {\n file.s.replaceAll(\n \"import { source } from '@/lib/source';\",\n \"import { getSource } from '@/lib/source';\",\n );\n file.s.replaceAll('source.getPages()', '(await getSource()).getPages()');\n // every page of a runtime source carries its Markdown\n file.s.replaceAll(\" if (!('getText' in page.data)) return null;\\n\\n\", '');\n file.s.replaceAll(\"await page.data.getText('processed')\", 'page.data.content');\n }));\n\n if (!edited)\n ctx.note(\n 'Read pages from `getSource()` in the chat route, the search index is built from `page.data.content`.',\n );\n}\n"],"mappings":";;;;;AAMA,MAAM,YAAY;CAChB,YAAY;EAAE,OAAO;EAAU,MAAM;EAAyB,KAAK;CAAqB;CACxF,YAAY;EACV,OAAO;EACP,MAAM;EACN,KAAK;CACP;CACA,QAAQ;EAAE,OAAO;EAAa,MAAM;EAAoB,KAAK;CAAiB;AAChF;AAIA,MAAa,KAAwC;CACnD,IAAI;CACJ,OAAO;CACP,aAAa;CACb,UAAU,CAAC,IAAI;CACf,SAAS,EACP,UAAU;EACR,SAAS;EACT,SAAS,OAAO,QAAQ,SAAS,CAAC,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,aAAa;GAC7D;GACP;GACA;EACF,EAAE;CACJ,EACF;CACA,WAAW,YACT,QAAQ,SAAS,6DAA6D;CAChF,MAAM,MAAM,KAAK,EAAE,YAAY;EAC7B,MAAM,IAAI,QAAQ,MAAM,UAAU;EAClC,IAAI,IAAI,UAAU,SAAS,CAAC,KAAK,EAAE;EACnC,IAAI,IAAI,QAAQ,OAAO,SAAS,MAAM,kBAAkB,GAAG;EAE3D,MAAM,EAAE,KAAK,SAAS,SAAS,IAAI;EACnC,MAAM,SAAS,MAAM,WAAW,KAAK,KAAK,KAAK,SAAS,KAAK,SAAS,GAAG,aAAa;EAoCtF,IAAI,EAlCF,WAAW,KAAA,KACV,MAAM,IAAI,OAAO,SAAS,SAAS;GAClC,IAAI,KAAK,KAAK,SAAS,WAAW,GAAG;GACrC,MAAM,UAAU,eAAe,MAAM,YAAY;GACjD,IAAI,CAAC,SAAS;GAEd,mBACE,MACA,SACA;;;;;;;;;;;;;;YAeF;GACA,UAAU,MAAM;IACd,MAAM;IACN,OAAO;KAAC;KAAY;KAAiB;IAAiB;GACxD,CAAC;GACD,UAAU,MAAM;IAAE,MAAM;IAAgB,OAAO,CAAC,mBAAmB;GAAE,CAAC;GACtE,UAAU,MAAM;IAAE,MAAM;IAAY,OAAO,CAAC,IAAI;GAAE,CAAC;GACnD,UAAU,MAAM;IAAE,MAAM;IAAoC,OAAO,CAAC,gBAAgB;GAAE,CAAC;EACzF,CAAC,IAGD,IAAI,KACF,6HACF;EAEF,IAAI,KAAK,OAAO,UAAU,SAAS,CAAC,IAAI,oBAAoB;CAC9D;AACF;;AAGA,eAAe,kBAAkB,KAAqB;CACpD,MAAM,EAAE,KAAK,YAAY,IAAI;CAC7B,MAAM,QAAQ,MAAM,WAAW,KAAK,SAAS,oBAAoB;CAcjE,IAAI,EAZF,UAAU,KAAA,KACT,MAAM,IAAI,OAAO,QAAQ,SAAS;EACjC,KAAK,EAAE,WACL,0CACA,2CACF;EACA,KAAK,EAAE,WAAW,qBAAqB,gCAAgC;EAEvE,KAAK,EAAE,WAAW,yDAAyD,EAAE;EAC7E,KAAK,EAAE,WAAW,wCAAwC,mBAAmB;CAC/E,CAAC,IAGD,IAAI,KACF,sGACF;AACJ"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"search-WfSdiGx-.js","names":[],"sources":["../src/features/search.ts"],"sourcesContent":["import path from 'node:path';\nimport type { Feature, FeatureContext } from '@/features';\nimport type { ReactFramework } from '@/project';\nimport { type FormattedRoute, 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 reactFramework,\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\nconst publicEnv = (next: boolean): Env =>\n next\n ? { prefix: 'NEXT_PUBLIC_', read: 'process.env.NEXT_PUBLIC_' }\n : { prefix: 'VITE_', read: 'import.meta.env.VITE_' };\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\nconst dialogBody = (footer: string, url: string) => ` return (\n <SearchDialog search={search} onSearchChange={setSearch} isLoading={query.isLoading} {...props}>\n <SearchDialogOverlay />\n <SearchDialogContent>\n <SearchDialogHeader>\n <SearchDialogIcon />\n <SearchDialogInput />\n <SearchDialogClose />\n </SearchDialogHeader>\n <SearchDialogList 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 typesenseIndexes = (\n async: boolean,\n { ref, resolved }: SourceRef,\n) => `import { ${ref} } from '@/lib/source';\nimport type { DocumentRecord } from 'typesense-fumadocs-adapter';\n\nexport async function exportSearchIndexes() {\n const results: DocumentRecord[] = [];\n for (const page of ${resolved}.getPages()) {\n results.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 results;\n}\n`;\n\nconst syncScript = (imports: string, body: string) => `import * as fs from 'node:fs/promises';\n${imports}\n\nasync function main(filePath: string) {\n const content = await fs.readFile(filePath);\n const records = JSON.parse(content.toString()) as DocumentRecord[];\n${body}\n console.log(\\`search updated: \\${records.length} records\\`);\n}\n\n// the path of pre-rendered \\`static.json\\`\nconst filePath = process.argv[2];\nif (!filePath) throw new Error('missing the path of static.json');\nvoid main(filePath);\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?: (src: SourceRef) => {\n imports: string;\n /** expression producing the documents */\n documents: string;\n /** generated `lib/export-search-indexes.ts`, for providers without a `toDocuments()` */\n file?: (async: boolean) => string;\n };\n sync?: (env: Env, dir: string) => string;\n /** server-side search, replaces the default search route */\n searchRoute?: Record<ReactFramework, 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 { useDocsSearch } 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, setSearch, query } = useDocsSearch({\n type: 'orama-cloud',\n client,\n locale,\n });\n\n${dialogBody('Orama', 'https://orama.com')}`,\n exportIndexes: ({ ref }) => ({\n imports: `import { ${ref} } from '@/lib/source';\nimport { toDocuments } from 'fumadocs-core/search/orama-cloud';`,\n documents: `toDocuments(${ref})`,\n }),\n sync: (env) =>\n syncScript(\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\n await 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 { useDocsSearch } from 'fumadocs-core/search/client';\nimport { algoliaClient } from 'fumadocs-core/search/client/algolia';\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, setSearch, query } = useDocsSearch({\n client: algoliaClient({\n client: algolia,\n indexName: 'document',\n locale,\n }),\n });\n\n${dialogBody('Algolia', 'https://algolia.com')}`,\n exportIndexes: ({ ref }) => ({\n imports: `import { ${ref} } from '@/lib/source';\nimport { toDocuments } from 'fumadocs-core/search/algolia';`,\n documents: `toDocuments(${ref})`,\n }),\n sync: (env) =>\n syncScript(\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\n await sync(client, {\n indexName: 'document',\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')}`,\n exportIndexes: (src) => ({\n imports: \"import { exportSearchIndexes } from '@/lib/export-search-indexes';\",\n documents: 'exportSearchIndexes()',\n file: (async) => typesenseIndexes(async, src),\n }),\n sync: (env) =>\n syncScript(\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\n await 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: ['MIXEDBREAD_API_KEY', 'MIXEDBREAD_STORE_ID'],\n static: false,\n dialog: () => `'use client';\n${dialogImports}\nimport { useDocsSearch } from 'fumadocs-core/search/client';\nimport { fetchClient } from 'fumadocs-core/search/client/fetch';\n\nexport default function CustomSearchDialog(props: SharedProps) {\n const { locale } = useI18n();\n const { search, setSearch, query } = useDocsSearch({\n client: fetchClient({\n api: '/api/search',\n locale,\n }),\n });\n\n${dialogBody('Mixedbread', 'https://mixedbread.com')}`,\n sync: (_env, dir) => `import { spawnSync } from 'node:child_process';\n\n// sync the content with Mixedbread CLI\nconst result = spawnSync(\n 'npx',\n ['--yes', '@mixedbread/cli', 'vs', 'sync', process.env.MIXEDBREAD_STORE_ID!, '${dir}', '--ci'],\n { stdio: 'inherit' },\n);\n\nprocess.exit(result.status ?? 1);\n`,\n searchRoute: (() => {\n const server = `import { createMixedbreadSearchAPI } from 'fumadocs-core/search/mixedbread';\nimport Mixedbread from '@mixedbread/sdk';\n\nconst server = createMixedbreadSearchAPI({\n client: new Mixedbread({\n apiKey: process.env.MIXEDBREAD_API_KEY,\n }),\n storeIdentifier: 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 };\n })(),\n },\n} satisfies Record<string, Provider>;\n\nexport type SearchProvider = keyof typeof providers;\n\nconst staticRoute = (\n framework: ReactFramework,\n route: FormattedRoute,\n imports: string,\n documents: string,\n) =>\n routeModule(framework, route, {\n imports,\n body: `Response.json(await ${documents})`,\n revalidate: true,\n static: true,\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 } = project;\n const framework = reactFramework(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 const env = publicEnv(framework === 'next');\n\n ctx.addDependencies(provider.dependencies);\n await ctx.write(path.join(baseDir, 'components/search.tsx'), provider.dialog(env));\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 { imports, documents, file } = provider.exportIndexes(\n sourceRef(project.source.dynamic),\n );\n if (file)\n await ctx.write(\n path.join(baseDir, 'lib/export-search-indexes.ts'),\n file(project.source.async),\n );\n\n const route = formatRoute({ segments: ['static.json'] }, framework, null);\n await ctx.write(\n path.join(baseDir, route.file),\n staticRoute(framework, route, imports, documents),\n );\n\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 // path of the pre-rendered `static.json`, passed to the sync script\n const output = {\n next: '.next/server/app/static.json.body',\n 'react-router': 'build/client/static.json',\n 'tanstack-start': project.static ? 'dist/client/static.json' : '.output/public/static.json',\n waku: 'dist/public/static.json',\n }[framework];\n await ctx.write('scripts/sync-content.ts', provider.sync(env, project.source.dir));\n await ctx.packageJson((data) => {\n const build = data.scripts?.build ?? '';\n if (build.includes('sync-content')) return;\n const sync = `node --env-file-if-exists=.env.local scripts/sync-content.ts${provider.exportIndexes ? ` ${output}` : ''}`;\n scripts(data, { build: build ? `${build} && ${sync}` : sync });\n });\n }\n\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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"}
|