@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.
- package/dist/{all-BBf7I8hG.js → all-BVSylqu2.js} +2 -2
- package/dist/{all-BBf7I8hG.js.map → all-BVSylqu2.js.map} +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 +2 -2
- package/dist/{search-WfSdiGx-.js → search-BbzvfJal.js} +134 -77
- package/dist/search-BbzvfJal.js.map +1 -0
- package/package.json +1 -1
- package/dist/search-WfSdiGx-.js.map +0 -1
|
@@ -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-
|
|
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-
|
|
27
|
+
//# sourceMappingURL=all-BVSylqu2.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"all-
|
|
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"}
|
package/dist/features/all.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as features } from "../all-
|
|
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
|
-
|
|
24
|
+
head: string;
|
|
22
25
|
documents: string;
|
|
23
26
|
};
|
|
24
|
-
sync: (env: Env
|
|
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
|
-
|
|
43
|
+
head: string;
|
|
38
44
|
documents: string;
|
|
39
45
|
};
|
|
40
|
-
sync: (env: Env
|
|
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
|
-
|
|
81
|
+
exportIndexes: (src: SourceRef, async: boolean) => {
|
|
82
|
+
head: string;
|
|
55
83
|
documents: string;
|
|
56
|
-
file: (async: boolean) => string;
|
|
57
84
|
};
|
|
58
|
-
sync: (env: Env
|
|
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:
|
|
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":"
|
|
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"}
|
package/dist/features/search.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as search } from "../search-
|
|
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-
|
|
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.
|
|
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
|
|
52
|
+
const typesenseDocuments = ({ ref, resolved }, async) => `import { ${ref} } from '@/lib/source';
|
|
52
53
|
import type { DocumentRecord } from 'typesense-fumadocs-adapter';
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
const
|
|
55
|
+
async function getDocuments() {
|
|
56
|
+
const documents: DocumentRecord[] = [];
|
|
56
57
|
for (const page of ${resolved}.getPages()) {
|
|
57
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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';`, `
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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';`, `
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
206
|
-
documents: "
|
|
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';`, `
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
294
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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-
|
|
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 +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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"}
|