@fumadocs/cli 1.7.0 → 1.7.1

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.
@@ -2,7 +2,7 @@ import { t as docs } from "./docs--WqEHTyl.js";
2
2
  import { t as ai } from "./ai-C19ZOP53.js";
3
3
  import { r as llms } from "./llms-Br0RJXXg.js";
4
4
  import { t as og } from "./og-LFlX1Bl4.js";
5
- import { t as search } from "./search-WfSdiGx-.js";
5
+ import { t as search } from "./search-BbzvfJal.js";
6
6
  import { t as mcp } from "./mcp-Ce_W93we.js";
7
7
  import { n as webmcp } from "./webmcp-DyTLrsXL.js";
8
8
  import { t as feedback } from "./feedback-Brj092vC.js";
@@ -24,4 +24,4 @@ const features = [
24
24
  //#endregion
25
25
  export { features as t };
26
26
 
27
- //# sourceMappingURL=all-BBf7I8hG.js.map
27
+ //# sourceMappingURL=all-BVSylqu2.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"all-BBf7I8hG.js","names":[],"sources":["../src/features/all.ts"],"sourcesContent":["import type { AnyFeature } from '.';\nimport { docs } from './docs';\nimport { ai } from './ai';\nimport { llms } from './llms';\nimport { og } from './og';\nimport { search } from './search';\nimport { mcp } from './mcp';\nimport { webmcp } from './webmcp';\nimport { feedback } from './feedback';\nimport { epub } from './epub';\nimport { lint } from './lint';\n\nexport const features: AnyFeature[] = [\n docs,\n ai,\n llms,\n mcp,\n webmcp,\n og,\n search,\n feedback,\n epub,\n lint,\n];\n"],"mappings":";;;;;;;;;;;AAYA,MAAa,WAAyB;CACpC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF"}
1
+ {"version":3,"file":"all-BVSylqu2.js","names":[],"sources":["../src/features/all.ts"],"sourcesContent":["import type { AnyFeature } from '.';\nimport { docs } from './docs';\nimport { ai } from './ai';\nimport { llms } from './llms';\nimport { og } from './og';\nimport { search } from './search';\nimport { mcp } from './mcp';\nimport { webmcp } from './webmcp';\nimport { feedback } from './feedback';\nimport { epub } from './epub';\nimport { lint } from './lint';\n\nexport const features: AnyFeature[] = [\n docs,\n ai,\n llms,\n mcp,\n webmcp,\n og,\n search,\n feedback,\n epub,\n lint,\n];\n"],"mappings":";;;;;;;;;;;AAYA,MAAa,WAAyB;CACpC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF"}
@@ -1,2 +1,2 @@
1
- import { t as features } from "../all-BBf7I8hG.js";
1
+ import { t as features } from "../all-BVSylqu2.js";
2
2
  export { features };
@@ -1,11 +1,14 @@
1
+ import { o as ReactFramework, s as SourceInfo } from "../index-DI66DoV8.js";
1
2
  import { r as Feature } from "../index-Cs7hw2un.js";
2
- import { t as SourceRef } from "../utils-CxJ7kw39.js";
3
+ import { p as FormattedRoute, t as SourceRef } from "../utils-CxJ7kw39.js";
3
4
  //#region src/features/search.d.ts
4
5
  /** how client code reads public env variables */
5
6
  interface Env {
6
7
  prefix: string;
7
8
  read: string;
8
9
  }
10
+ /** `scripts/sync-content.ts`, syncing the search indexes pre-rendered to `output` */
11
+ export declare const syncScript: (output: string, imports: string, body: string) => string;
9
12
  declare const providers: {
10
13
  'orama-cloud': {
11
14
  label: string;
@@ -18,10 +21,13 @@ declare const providers: {
18
21
  static: true;
19
22
  dialog: (env: Env) => string;
20
23
  exportIndexes: ({ ref }: SourceRef) => {
21
- imports: string;
24
+ head: string;
22
25
  documents: string;
23
26
  };
24
- sync: (env: Env) => string;
27
+ sync: (env: Env, { output }: {
28
+ dir: string;
29
+ output: string;
30
+ }) => string;
25
31
  };
26
32
  algolia: {
27
33
  label: string;
@@ -34,10 +40,32 @@ declare const providers: {
34
40
  static: true;
35
41
  dialog: (env: Env) => string;
36
42
  exportIndexes: ({ ref }: SourceRef) => {
37
- imports: string;
43
+ head: string;
38
44
  documents: string;
39
45
  };
40
- sync: (env: Env) => string;
46
+ sync: (env: Env, { output }: {
47
+ dir: string;
48
+ output: string;
49
+ }) => string;
50
+ };
51
+ meilisearch: {
52
+ label: string;
53
+ hint: string;
54
+ dependencies: {
55
+ meilisearch: null;
56
+ };
57
+ publicEnv: string[];
58
+ privateEnv: string[];
59
+ static: true;
60
+ dialog: (env: Env) => string;
61
+ exportIndexes: ({ ref }: SourceRef) => {
62
+ head: string;
63
+ documents: string;
64
+ };
65
+ sync: (env: Env, { output }: {
66
+ dir: string;
67
+ output: string;
68
+ }) => string;
41
69
  };
42
70
  typesense: {
43
71
  label: string;
@@ -50,12 +78,14 @@ declare const providers: {
50
78
  privateEnv: string[];
51
79
  static: true;
52
80
  dialog: (env: Env) => string;
53
- exportIndexes: (src: SourceRef) => {
54
- imports: string;
81
+ exportIndexes: (src: SourceRef, async: boolean) => {
82
+ head: string;
55
83
  documents: string;
56
- file: (async: boolean) => string;
57
84
  };
58
- sync: (env: Env) => string;
85
+ sync: (env: Env, { output }: {
86
+ dir: string;
87
+ output: string;
88
+ }) => string;
59
89
  };
60
90
  mixedbread: {
61
91
  label: string;
@@ -67,7 +97,10 @@ declare const providers: {
67
97
  privateEnv: string[];
68
98
  static: false;
69
99
  dialog: () => string;
70
- sync: (_env: Env, dir: string) => string;
100
+ sync: (_env: Env, { dir }: {
101
+ dir: string;
102
+ output: string;
103
+ }) => string;
71
104
  searchRoute: {
72
105
  next: string;
73
106
  'react-router': string;
@@ -77,6 +110,18 @@ declare const providers: {
77
110
  };
78
111
  };
79
112
  export type SearchProvider = keyof typeof providers;
113
+ /** pre-rendered on build to export the search indexes */
114
+ export declare const staticJson: (framework: ReactFramework) => FormattedRoute;
115
+ export declare const staticRoute: (framework: ReactFramework, head: string, documents: string) => string;
116
+ /** where `static.json` is pre-rendered on build, `config` is the Vite config of TanStack Start */
117
+ export declare function staticOutput(framework: ReactFramework, config?: string): string;
118
+ /** runs the sync script after build */
119
+ export declare const syncCommand = "node --env-file-if-exists=.env.local scripts/sync-content.ts";
120
+ /** the files written by a provider, relative to the project, `output` is where `static.json` is pre-rendered */
121
+ export declare function templates(name: SearchProvider, framework: ReactFramework, { baseDir, source }: {
122
+ baseDir: string;
123
+ source: Pick<SourceInfo, 'dynamic' | 'async' | 'dir'>;
124
+ }, output: string): [file: string, content: string][];
80
125
  export declare const search: Feature<{
81
126
  provider: SearchProvider;
82
127
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","names":[],"sources":["../../src/features/search.ts"],"mappings":";;;;UAwBU;EACR;EACA;;cA6GI;;;;;MAIc;;;;;;;;;;;;;;;;MA+CA;;;;;;;;;;;;;;;;MA+CA;MAAiB;;;;;;;;;;;;;;;;;MAgDjB;;;;;;;;;;;;;;;YAqER,8BAA8B;qBAe7B,QAAQ;EAAU,UAAU"}
1
+ {"version":3,"file":"search.d.ts","names":[],"sources":["../../src/features/search.ts"],"mappings":";;;;;UAyBU;EACR;EACA;;;qBAsEW,aAAU,gBACP,iBACC;cAqCX;;;;;MAIc;;;;;;;;;;;MATc;MAAgB;;;;;;;MAyD9B;;;;;;;;;;;MAzDc;MAAgB;;;;;;;MAyG9B;;;;;;;;;;;MAzGc;MAAgB;;;;;;;MA4J9B;MAAiB;;;;;;;;;;;MA5JH;MAAgB;;;;;;;MA4M9B;;;;;;;MA5Mc;MAAgB;;;;;;;;;;YAiRtC,8BAA8B;;qBAG7B,aAAU,WAAe,mBAAA;qBAGzB,cAAW,WAAe,gBAAc,cAAc;;wBASnD,aAAa,WAAW,gBAAgB;;qBAgB3C;;wBAGG,UACd,MAAM,gBACN,WAAW,kBACT,SAAS;EAAY;EAAiB,QAAQ,KAAK;GACrD,kBACE,cAAc;qBAmBL,QAAQ;EAAU,UAAU"}
@@ -1,2 +1,2 @@
1
- import { t as search } from "../search-WfSdiGx-.js";
2
- export { search };
1
+ import { a as syncCommand, i as staticRoute, n as staticJson, o as syncScript, r as staticOutput, s as templates, t as search } from "../search-BbzvfJal.js";
2
+ export { search, staticJson, staticOutput, staticRoute, syncCommand, syncScript, templates };
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { t as exists } from "./fs-D6J9dOEx.js";
5
5
  import { i as loadProject, n as frameworks } from "./project-DwRVTm4p.js";
6
6
  import { n as UIRegistries, t as FumadocsComponentInstaller } from "./installer-C4662Woc.js";
7
7
  import { t as runFeature } from "./features-Do6x9Tsz.js";
8
- import { t as features } from "./all-BBf7I8hG.js";
8
+ import { t as features } from "./all-BVSylqu2.js";
9
9
  import fs from "node:fs/promises";
10
10
  import path from "node:path";
11
11
  import { cac } from "cac";
@@ -66,7 +66,7 @@ async function runTree(args) {
66
66
  }
67
67
  //#endregion
68
68
  //#region package.json
69
- var version = "1.7.0";
69
+ var version = "1.7.1";
70
70
  //#endregion
71
71
  //#region src/commands/customise.ts
72
72
  async function customise(config, connector) {
@@ -1,6 +1,7 @@
1
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
2
  import { a as routeModule, r as formatRoute, t as docs } from "./docs--WqEHTyl.js";
3
3
  import { a as reactFramework, d as sourceRef, i as posix, l as scripts, r as findSource, s as registerReactRouterRoutes } from "./utils-DbMe7h6c.js";
4
+ import fs from "node:fs/promises";
4
5
  import path from "node:path";
5
6
  //#region src/features/search.ts
6
7
  const publicEnv = (next) => next ? {
@@ -48,13 +49,13 @@ const dialogBody = (footer, url) => ` return (
48
49
  }
49
50
  `;
50
51
  /** `structuredData` is a function on async collections (React Router & TanStack Start) */
51
- const typesenseIndexes = (async, { ref, resolved }) => `import { ${ref} } from '@/lib/source';
52
+ const typesenseDocuments = ({ ref, resolved }, async) => `import { ${ref} } from '@/lib/source';
52
53
  import type { DocumentRecord } from 'typesense-fumadocs-adapter';
53
54
 
54
- export async function exportSearchIndexes() {
55
- const results: DocumentRecord[] = [];
55
+ async function getDocuments() {
56
+ const documents: DocumentRecord[] = [];
56
57
  for (const page of ${resolved}.getPages()) {
57
- results.push({
58
+ documents.push({
58
59
  _id: page.url,
59
60
  structured: ${async ? "await page.data.structuredData()" : "page.data.structuredData"},
60
61
  url: page.url,
@@ -63,23 +64,16 @@ export async function exportSearchIndexes() {
63
64
  locale: page.locale,
64
65
  });
65
66
  }
66
- return results;
67
- }
68
- `;
69
- const syncScript = (imports, body) => `import * as fs from 'node:fs/promises';
67
+ return documents;
68
+ }`;
69
+ /** `scripts/sync-content.ts`, syncing the search indexes pre-rendered to `output` */
70
+ const syncScript = (output, imports, body) => `import * as fs from 'node:fs/promises';
70
71
  ${imports}
71
72
 
72
- async function main(filePath: string) {
73
- const content = await fs.readFile(filePath);
74
- const records = JSON.parse(content.toString()) as DocumentRecord[];
75
- ${body}
76
- console.log(\`search updated: \${records.length} records\`);
77
- }
73
+ const records = JSON.parse(await fs.readFile('${output}', 'utf-8')) as DocumentRecord[];
78
74
 
79
- // the path of pre-rendered \`static.json\`
80
- const filePath = process.argv[2];
81
- if (!filePath) throw new Error('missing the path of static.json');
82
- void main(filePath);
75
+ ${body}
76
+ console.log(\`search updated: \${records.length} records\`);
83
77
  `;
84
78
  const providers = {
85
79
  "orama-cloud": {
@@ -113,20 +107,20 @@ export default function CustomSearchDialog(props: SharedProps) {
113
107
 
114
108
  ${dialogBody("Orama", "https://orama.com")}`,
115
109
  exportIndexes: ({ ref }) => ({
116
- imports: `import { ${ref} } from '@/lib/source';
110
+ head: `import { ${ref} } from '@/lib/source';
117
111
  import { toDocuments } from 'fumadocs-core/search/orama-cloud';`,
118
112
  documents: `toDocuments(${ref})`
119
113
  }),
120
- sync: (env) => syncScript(`import { type OramaDocument as DocumentRecord, sync } from 'fumadocs-core/search/orama-cloud';
121
- import { OramaCloud } from '@orama/core';`, ` const orama = new OramaCloud({
122
- projectId: process.env.${env.prefix}ORAMA_PROJECT_ID!,
123
- apiKey: process.env.ORAMA_PRIVATE_API_KEY!,
124
- });
114
+ sync: (env, { output }) => syncScript(output, `import { type OramaDocument as DocumentRecord, sync } from 'fumadocs-core/search/orama-cloud';
115
+ import { OramaCloud } from '@orama/core';`, `const orama = new OramaCloud({
116
+ projectId: process.env.${env.prefix}ORAMA_PROJECT_ID!,
117
+ apiKey: process.env.ORAMA_PRIVATE_API_KEY!,
118
+ });
125
119
 
126
- await sync(orama, {
127
- index: process.env.${env.prefix}ORAMA_DATASOURCE_ID!,
128
- documents: records,
129
- });
120
+ await sync(orama, {
121
+ index: process.env.${env.prefix}ORAMA_DATASOURCE_ID!,
122
+ documents: records,
123
+ });
130
124
  `)
131
125
  },
132
126
  algolia: {
@@ -156,20 +150,66 @@ export default function CustomSearchDialog(props: SharedProps) {
156
150
 
157
151
  ${dialogBody("Algolia", "https://algolia.com")}`,
158
152
  exportIndexes: ({ ref }) => ({
159
- imports: `import { ${ref} } from '@/lib/source';
153
+ head: `import { ${ref} } from '@/lib/source';
160
154
  import { toDocuments } from 'fumadocs-core/search/algolia';`,
161
155
  documents: `toDocuments(${ref})`
162
156
  }),
163
- sync: (env) => syncScript(`import { type DocumentRecord, sync } from 'fumadocs-core/search/algolia';
164
- import { algoliasearch } from 'algoliasearch';`, ` const client = algoliasearch(
165
- process.env.${env.prefix}ALGOLIA_APP_ID!,
166
- process.env.ALGOLIA_WRITE_KEY!,
167
- );
157
+ sync: (env, { output }) => syncScript(output, `import { type DocumentRecord, sync } from 'fumadocs-core/search/algolia';
158
+ import { algoliasearch } from 'algoliasearch';`, `const client = algoliasearch(
159
+ process.env.${env.prefix}ALGOLIA_APP_ID!,
160
+ process.env.ALGOLIA_WRITE_KEY!,
161
+ );
168
162
 
169
- await sync(client, {
170
- indexName: 'document',
171
- documents: records,
163
+ await sync(client, {
164
+ indexName: 'document',
165
+ documents: records,
166
+ });
167
+ `)
168
+ },
169
+ meilisearch: {
170
+ label: "Meilisearch",
171
+ hint: "self-hosted or cloud",
172
+ dependencies: { meilisearch: null },
173
+ publicEnv: ["MEILISEARCH_HOST", "MEILISEARCH_SEARCH_KEY"],
174
+ privateEnv: ["MEILISEARCH_ADMIN_KEY"],
175
+ static: true,
176
+ dialog: (env) => `'use client';
177
+ ${dialogImports}
178
+ import { useDocsSearch } from 'fumadocs-core/search/client';
179
+ import { meilisearchClient } from 'fumadocs-core/search/client/meilisearch';
180
+ import { Meilisearch } from 'meilisearch';
181
+
182
+ const client = new Meilisearch({
183
+ host: ${env.read}MEILISEARCH_HOST!,
184
+ apiKey: ${env.read}MEILISEARCH_SEARCH_KEY!,
185
+ });
186
+
187
+ export default function CustomSearchDialog(props: SharedProps) {
188
+ const { locale } = useI18n();
189
+ const { search, setSearch, query } = useDocsSearch({
190
+ client: meilisearchClient({
191
+ client,
192
+ indexName: 'docs',
193
+ locale,
194
+ }),
172
195
  });
196
+
197
+ ${dialogBody("Meilisearch", "https://www.meilisearch.com")}`,
198
+ exportIndexes: ({ ref }) => ({
199
+ head: `import { ${ref} } from '@/lib/source';
200
+ import { toDocuments } from 'fumadocs-core/search/meilisearch';`,
201
+ documents: `toDocuments(${ref})`
202
+ }),
203
+ sync: (env, { output }) => syncScript(output, `import { type DocumentRecord, sync } from 'fumadocs-core/search/meilisearch';
204
+ import { Meilisearch } from 'meilisearch';`, `const client = new Meilisearch({
205
+ host: process.env.${env.prefix}MEILISEARCH_HOST!,
206
+ apiKey: process.env.MEILISEARCH_ADMIN_KEY!,
207
+ });
208
+
209
+ await sync(client, {
210
+ indexName: 'docs',
211
+ documents: records,
212
+ });
173
213
  `)
174
214
  },
175
215
  typesense: {
@@ -201,22 +241,21 @@ export default function CustomSearchDialog(props: SharedProps) {
201
241
  });
202
242
 
203
243
  ${dialogBody("Typesense", "https://typesense.org")}`,
204
- exportIndexes: (src) => ({
205
- imports: "import { exportSearchIndexes } from '@/lib/export-search-indexes';",
206
- documents: "exportSearchIndexes()",
207
- file: (async) => typesenseIndexes(async, src)
244
+ exportIndexes: (src, async) => ({
245
+ head: typesenseDocuments(src, async),
246
+ documents: "getDocuments()"
208
247
  }),
209
- sync: (env) => syncScript(`import { type DocumentRecord, sync } from 'typesense-fumadocs-adapter';
210
- import { Client } from 'typesense';`, ` const client = new Client({
211
- nodes: [{ url: process.env.${env.prefix}TYPESENSE_URL! }],
212
- apiKey: process.env.TYPESENSE_API_KEY!,
213
- connectionTimeoutSeconds: 60 * 15,
214
- });
248
+ sync: (env, { output }) => syncScript(output, `import { type DocumentRecord, sync } from 'typesense-fumadocs-adapter';
249
+ import { Client } from 'typesense';`, `const client = new Client({
250
+ nodes: [{ url: process.env.${env.prefix}TYPESENSE_URL! }],
251
+ apiKey: process.env.TYPESENSE_API_KEY!,
252
+ connectionTimeoutSeconds: 60 * 15,
253
+ });
215
254
 
216
- await sync(client, {
217
- typesenseCollectionName: 'docs',
218
- documents: records,
219
- });
255
+ await sync(client, {
256
+ typesenseCollectionName: 'docs',
257
+ documents: records,
258
+ });
220
259
  `)
221
260
  },
222
261
  mixedbread: {
@@ -241,7 +280,7 @@ export default function CustomSearchDialog(props: SharedProps) {
241
280
  });
242
281
 
243
282
  ${dialogBody("Mixedbread", "https://mixedbread.com")}`,
244
- sync: (_env, dir) => `import { spawnSync } from 'node:child_process';
283
+ sync: (_env, { dir }) => `import { spawnSync } from 'node:child_process';
245
284
 
246
285
  // sync the content with Mixedbread CLI
247
286
  const result = spawnSync(
@@ -290,12 +329,42 @@ export const { GET } = server;
290
329
  })()
291
330
  }
292
331
  };
293
- const staticRoute = (framework, route, imports, documents) => routeModule(framework, route, {
294
- imports,
332
+ /** pre-rendered on build to export the search indexes */
333
+ const staticJson = (framework) => formatRoute({ segments: ["static.json"] }, framework, null);
334
+ const staticRoute = (framework, head, documents) => routeModule(framework, staticJson(framework), {
335
+ imports: head,
295
336
  body: `Response.json(await ${documents})`,
296
337
  revalidate: true,
297
338
  static: true
298
339
  });
340
+ /** where `static.json` is pre-rendered on build, `config` is the Vite config of TanStack Start */
341
+ function staticOutput(framework, config = "") {
342
+ switch (framework) {
343
+ case "next": return ".next/server/app/static.json.body";
344
+ case "react-router": return "build/client/static.json";
345
+ case "waku": return "dist/public/static.json";
346
+ case "tanstack-start":
347
+ if (/preset:\s*['"]vercel['"]/.test(config)) return ".vercel/output/static/static.json";
348
+ return /\bnitro\(/.test(config) ? ".output/public/static.json" : "dist/client/static.json";
349
+ }
350
+ }
351
+ /** runs the sync script after build */
352
+ const syncCommand = "node --env-file-if-exists=.env.local scripts/sync-content.ts";
353
+ /** the files written by a provider, relative to the project, `output` is where `static.json` is pre-rendered */
354
+ function templates(name, framework, { baseDir, source }, output) {
355
+ const provider = providers[name];
356
+ const env = publicEnv(framework === "next");
357
+ const files = [[path.posix.join(baseDir, "components/search.tsx"), provider.dialog(env)]];
358
+ if (provider.exportIndexes) {
359
+ const { head, documents } = provider.exportIndexes(sourceRef(source.dynamic), source.async);
360
+ files.push([path.posix.join(baseDir, staticJson(framework).file), staticRoute(framework, head, documents)]);
361
+ }
362
+ if (provider.sync) files.push(["scripts/sync-content.ts", provider.sync(env, {
363
+ dir: source.dir,
364
+ output
365
+ })]);
366
+ return files;
367
+ }
299
368
  const search = {
300
369
  id: "search",
301
370
  title: "Search",
@@ -316,19 +385,16 @@ const search = {
316
385
  const framework = reactFramework(project);
317
386
  const provider = providers[name];
318
387
  if (project.static && !provider.static) throw new Error(`${provider.label} requires a server at runtime`);
319
- const env = publicEnv(framework === "next");
388
+ const output = staticOutput(framework, framework === "tanstack-start" && project.configFile ? await fs.readFile(path.join(project.cwd, project.configFile), "utf-8") : "");
320
389
  ctx.addDependencies(provider.dependencies);
321
- await ctx.write(path.join(baseDir, "components/search.tsx"), provider.dialog(env));
390
+ for (const [file, content] of templates(name, framework, project, output)) await ctx.write(file, content);
322
391
  const searchRoute = await findSource(project.cwd, path.join(baseDir, project.info.routesDir), "createFromSource(", (file) => /(^|[\\/])search(\.ts|[\\/]route\.ts)$/.test(file));
323
392
  if (provider.searchRoute) {
324
393
  if (searchRoute) await ctx.write(searchRoute, provider.searchRoute[framework]);
325
394
  else ctx.note("Create the search route with `createMixedbreadSearchAPI`, see https://fumadocs.dev/docs/headless/search/mixedbread.");
326
395
  } else if (searchRoute) await removeDefaultSearch(ctx, searchRoute);
327
396
  if (provider.exportIndexes) {
328
- const { imports, documents, file } = provider.exportIndexes(sourceRef(project.source.dynamic));
329
- if (file) await ctx.write(path.join(baseDir, "lib/export-search-indexes.ts"), file(project.source.async));
330
- const route = formatRoute({ segments: ["static.json"] }, framework, null);
331
- await ctx.write(path.join(baseDir, route.file), staticRoute(framework, route, imports, documents));
397
+ const route = staticJson(framework);
332
398
  if (framework === "react-router") await registerReactRouterRoutes(ctx, [route]);
333
399
  else if (framework === "tanstack-start" && project.configFile) await ctx.source(project.configFile, (file) => addTanstackPrerender(file, [route.path]));
334
400
  }
@@ -342,21 +408,12 @@ const search = {
342
408
  default: "SearchDialog"
343
409
  });
344
410
  }))) ctx.note("Pass `search={{ SearchDialog }}` from `@/components/search` to `<RootProvider />`.");
345
- if (provider.sync) {
346
- const output = {
347
- next: ".next/server/app/static.json.body",
348
- "react-router": "build/client/static.json",
349
- "tanstack-start": project.static ? "dist/client/static.json" : ".output/public/static.json",
350
- waku: "dist/public/static.json"
351
- }[framework];
352
- await ctx.write("scripts/sync-content.ts", provider.sync(env, project.source.dir));
353
- await ctx.packageJson((data) => {
354
- const build = data.scripts?.build ?? "";
355
- if (build.includes("sync-content")) return;
356
- const sync = `node --env-file-if-exists=.env.local scripts/sync-content.ts${provider.exportIndexes ? ` ${output}` : ""}`;
357
- scripts(data, { build: build ? `${build} && ${sync}` : sync });
358
- });
359
- }
411
+ if (provider.sync) await ctx.packageJson((data) => {
412
+ const build = data.scripts?.build ?? "";
413
+ if (build.includes("sync-content")) return;
414
+ scripts(data, { build: build ? `${build} && ${syncCommand}` : syncCommand });
415
+ });
416
+ const env = publicEnv(framework === "next");
360
417
  for (const key of provider.publicEnv) ctx.env(env.prefix + key, "");
361
418
  for (const key of provider.privateEnv) ctx.env(key, "");
362
419
  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}.`);
@@ -376,6 +433,6 @@ async function removeDefaultSearch(ctx, file) {
376
433
  });
377
434
  }
378
435
  //#endregion
379
- export { search as t };
436
+ export { syncCommand as a, staticRoute as i, staticJson as n, syncScript as o, staticOutput as r, templates as s, search as t };
380
437
 
381
- //# sourceMappingURL=search-WfSdiGx-.js.map
438
+ //# sourceMappingURL=search-BbzvfJal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search-BbzvfJal.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 { ReactFramework, 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 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 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` */\nexport const syncScript = (\n output: string,\n imports: string,\n body: string,\n) => `import * as fs from 'node:fs/promises';\n${imports}\n\nconst records = JSON.parse(await fs.readFile('${output}', 'utf-8')) as DocumentRecord[];\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: { dir: string; output: 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 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 { 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 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 { useDocsSearch } from 'fumadocs-core/search/client';\nimport { meilisearchClient } from 'fumadocs-core/search/client/meilisearch';\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, setSearch, query } = useDocsSearch({\n client: meilisearchClient({\n client,\n indexName: 'docs',\n locale,\n }),\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')}`,\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: ['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\n/** pre-rendered on build to export the search indexes */\nexport const staticJson = (framework: ReactFramework) =>\n formatRoute({ segments: ['static.json'] }, framework, null);\n\nexport const staticRoute = (framework: ReactFramework, 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: ReactFramework, config = ''): string {\n switch (framework) {\n case 'next':\n return '.next/server/app/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 }\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: ReactFramework,\n { baseDir, source }: { baseDir: string; source: Pick<SourceInfo, 'dynamic' | 'async' | 'dir'> },\n output: string,\n): [file: string, content: string][] {\n const provider: Provider = providers[name];\n const env = publicEnv(framework === 'next');\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)\n files.push(['scripts/sync-content.ts', provider.sync(env, { dir: source.dir, 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 } = 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\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);\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 === 'next');\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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fumadocs/cli",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "The CLI tool for Fumadocs",
5
5
  "keywords": [
6
6
  "Docs",
@@ -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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"}