@fumadocs/cli 1.6.2 → 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/index.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/{features-CXFCgOWD.js → features-Do6x9Tsz.js} +6 -12
- package/dist/features-Do6x9Tsz.js.map +1 -0
- package/dist/index.js +65 -66
- package/dist/index.js.map +1 -1
- package/dist/installer-C4662Woc.js +33 -0
- package/dist/installer-C4662Woc.js.map +1 -0
- package/dist/registry/installer.d.ts +3 -4
- package/dist/registry/installer.d.ts.map +1 -1
- package/dist/registry/installer.js +1 -1
- package/dist/{search-WfSdiGx-.js → search-BbzvfJal.js} +134 -77
- package/dist/search-BbzvfJal.js.map +1 -0
- package/package.json +3 -2
- package/dist/features-CXFCgOWD.js.map +0 -1
- package/dist/installer-CzhVfs7i.js +0 -204
- package/dist/installer-CzhVfs7i.js.map +0 -1
- package/dist/search-WfSdiGx-.js.map +0 -1
|
@@ -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.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "The CLI tool for Fumadocs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Docs",
|
|
@@ -46,8 +46,9 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@clack/prompts": "^1.8.1",
|
|
49
|
+
"@fuma-cli/interactive": "^0.1.2",
|
|
49
50
|
"cac": "^7.0.0",
|
|
50
|
-
"fuma-cli": "^0.
|
|
51
|
+
"fuma-cli": "^0.3.1",
|
|
51
52
|
"magic-string": "^1.4.1",
|
|
52
53
|
"oxc-parser": "^0.150.0",
|
|
53
54
|
"picocolors": "^1.1.1",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"features-CXFCgOWD.js","names":[],"sources":["../src/features/index.ts"],"sourcesContent":["import fs from 'node:fs/promises';\nimport path from 'node:path';\nimport { x } from 'tinyexec';\nimport type { RegistryConnector } from 'fuma-cli/registry/connector';\nimport type { Project } from '@/project';\nimport { createSourceFile, type SourceFile } from '@/codemod';\nimport { FumadocsComponentInstaller } from '@/registry/installer';\nimport { exists } from '@/utils/fs';\n\nexport interface SelectOption<T> {\n message: string;\n choices: { value: T; label: string; hint?: string }[];\n}\n\nexport interface ConfirmOption {\n message: string;\n initialValue?: boolean;\n}\n\nexport type FeatureOption<T> = [T] extends [boolean] ? ConfirmOption : SelectOption<T>;\n\nexport interface Feature<Options extends object = Record<never, never>> {\n id: string;\n title: string;\n description: string;\n options?: { [K in keyof Options]: FeatureOption<Options[K]> };\n /** features that must be applied first */\n requires?: AnyFeature[];\n /** whether the feature is already applied to the project */\n detect?: (project: Project) => Promise<boolean>;\n /** @returns `true` if supported, otherwise the reason */\n supports?: (project: Project) => true | string;\n apply(ctx: FeatureContext, options: Options): Promise<void>;\n}\n\nexport type AnyFeature = Omit<Feature<Record<string, unknown>>, 'options'> & {\n options?: Record<string, SelectOption<unknown> | ConfirmOption>;\n};\n\nexport interface PackageJson {\n scripts?: Record<string, string>;\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n}\n\nexport interface FeatureContext {\n project: Project;\n /** write a file (relative to project root), asks before overwriting */\n write: (file: string, content: string) => Promise<void>;\n /** edit a file with codemods, `false` if the file doesn't exist */\n source: (file: string, edit: (file: SourceFile) => void) => Promise<boolean>;\n /** append lines to a text file, skip lines already present */\n append: (file: string, lines: string[]) => Promise<void>;\n remove: (file: string) => Promise<void>;\n packageJson: (edit: (data: PackageJson) => void) => Promise<void>;\n /** install a registry component */\n install: (name: string, subRegistry?: string) => Promise<void>;\n /** `null` to install the latest version */\n addDependencies: (deps: Record<string, string | null>, dev?: boolean) => void;\n env: (key: string, value: string) => void;\n /** instructions to show after the feature is applied */\n note: (text: string) => void;\n log: (message: string) => void;\n}\n\nexport interface FeatureIO {\n log: (message: string) => void;\n confirmOverwrite: (file: string) => Promise<boolean>;\n /** run package manager to install dependencies, otherwise only write them into `package.json` */\n installDependencies: boolean;\n}\n\nexport interface RunOptions {\n project: Project;\n connector: RegistryConnector;\n io: FeatureIO;\n /** preferred versions of dependencies, when a feature doesn't specify one */\n versions?: Record<string, string>;\n}\n\nexport async function runFeature<O extends object>(\n feature: Omit<Feature<O>, 'options'>,\n options: O,\n { project, connector, io, versions }: RunOptions,\n): Promise<{ notes: string[] }> {\n for (const dep of feature.requires ?? []) {\n if (dep.detect && !(await dep.detect(project)))\n throw new Error(\n `\"${feature.id}\" requires \"${dep.id}\", run \\`fumadocs feature ${dep.id}\\` first.`,\n );\n }\n const supported = feature.supports?.(project) ?? true;\n if (supported !== true) throw new Error(`\"${feature.id}\" is unavailable: ${supported}`);\n\n const { cwd } = project;\n const deps: Record<string, string | null> = {};\n const devDeps: Record<string, string | null> = {};\n const env: string[] = [];\n const notes: string[] = [];\n const installer = new FumadocsComponentInstaller(connector, project.config, cwd, {\n onWarn: io.log,\n confirmFileOverride: ({ path: file }) => io.confirmOverwrite(path.relative(cwd, file)),\n onFileDownloaded: ({ path: file }) => io.log(`installed ${path.relative(cwd, file)}`),\n });\n\n const ctx: FeatureContext = {\n project,\n log: io.log,\n note: (text) => notes.push(text),\n env: (key, value) => env.push(`${key}=${value}`),\n addDependencies(input, dev) {\n Object.assign(dev ? devDeps : deps, input);\n },\n async write(file, content) {\n const target = path.join(cwd, file);\n const current = await fs.readFile(target, 'utf-8').catch(() => null);\n if (current !== null) {\n if (current.trim() === content.trim()) return;\n if (!(await io.confirmOverwrite(file))) return;\n }\n await fs.mkdir(path.dirname(target), { recursive: true });\n await fs.writeFile(target, content);\n io.log(`${current === null ? 'created' : 'updated'} ${file}`);\n },\n async source(file, edit) {\n const target = path.join(cwd, file);\n if (!(await exists(target))) return false;\n const source = await createSourceFile(target);\n edit(source);\n if (source.s.hasChanged()) {\n await source.save();\n io.log(`updated ${file}`);\n }\n return true;\n },\n async append(file, lines) {\n const target = path.join(cwd, file);\n const current = await fs.readFile(target, 'utf-8').catch(() => '');\n const missing = lines.filter((line) => !current.includes(line));\n if (missing.length === 0) return;\n const separator = current.length === 0 || current.endsWith('\\n') ? '' : '\\n';\n await fs.mkdir(path.dirname(target), { recursive: true });\n await fs.writeFile(target, `${current}${separator}${missing.join('\\n')}\\n`);\n io.log(`updated ${file}`);\n },\n async remove(file) {\n await fs.rm(path.join(cwd, file), { force: true });\n io.log(`removed ${file}`);\n },\n async packageJson(edit) {\n const target = path.join(cwd, 'package.json');\n const data = JSON.parse(await fs.readFile(target, 'utf-8'));\n edit(data);\n await fs.writeFile(target, `${JSON.stringify(data, null, 2)}\\n`);\n io.log('updated package.json');\n },\n async install(name, subRegistry) {\n const result = await installer.install(name, subRegistry);\n const manager = await result.deps();\n // encoded as `name` or `name@version`\n const decode = (target: Record<string, string | null>, dep: string) => {\n const at = dep.indexOf('@', 1);\n if (at === -1) target[dep] = null;\n else target[dep.slice(0, at)] = dep.slice(at + 1);\n };\n for (const dep of manager.dependencies) decode(deps, dep);\n for (const dep of manager.devDependencies) decode(devDeps, dep);\n },\n };\n\n await feature.apply(ctx, options);\n\n if (env.length > 0) await ctx.append('.env.local', env);\n await installDependencies(project, deps, devDeps, io, versions);\n return { notes };\n}\n\nasync function installDependencies(\n project: Project,\n deps: Record<string, string | null>,\n devDeps: Record<string, string | null>,\n io: FeatureIO,\n versions: Record<string, string> = {},\n) {\n const { dependencies = {}, devDependencies = {} } = project.packageJson;\n const filter = (input: Record<string, string | null>) =>\n Object.entries(input).filter(([name]) => !(name in dependencies) && !(name in devDependencies));\n const missing = filter(deps);\n const missingDev = filter(devDeps);\n if (missing.length === 0 && missingDev.length === 0) return;\n\n if (io.installDependencies) {\n const pm = project.packageManager;\n const run = async (flags: string[], entries: [string, string | null][]) => {\n if (entries.length === 0) return;\n const args = [\n 'add',\n ...flags,\n ...entries.map(([name, version]) => (version ? `${name}@${version}` : name)),\n ];\n io.log(`${pm} ${args.join(' ')}`);\n await x(pm, args, {\n nodeOptions: { cwd: project.cwd, stdio: 'inherit' },\n throwOnError: true,\n });\n };\n await run([], missing);\n await run(['-D'], missingDev);\n return;\n }\n\n const file = path.join(project.cwd, 'package.json');\n const json = JSON.parse(await fs.readFile(file, 'utf-8'));\n const write = (field: string, entries: [string, string | null][]) => {\n if (entries.length === 0) return;\n const target = (json[field] ??= {});\n for (const [name, version] of entries) target[name] = version ?? versions[name] ?? 'latest';\n };\n write('dependencies', missing);\n write('devDependencies', missingDev);\n await fs.writeFile(file, `${JSON.stringify(json, null, 2)}\\n`);\n io.log('updated package.json');\n}\n"],"mappings":"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"}
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { detectPackageManager } from "fuma-cli/detect";
|
|
4
|
-
import picocolors from "picocolors";
|
|
5
|
-
import { box, confirm, isCancel, log, outro, spinner } from "@clack/prompts";
|
|
6
|
-
import { ComponentInstaller } from "fuma-cli/registry/installer";
|
|
7
|
-
//#region src/commands/shared.ts
|
|
8
|
-
const UIRegistries = {
|
|
9
|
-
"base-ui": "fumadocs/base-ui",
|
|
10
|
-
"radix-ui": "fumadocs/radix-ui"
|
|
11
|
-
};
|
|
12
|
-
//#endregion
|
|
13
|
-
//#region src/registry/plugins/preserve.ts
|
|
14
|
-
/**
|
|
15
|
-
* keep references to `fumadocs-ui/layouts/*` components as original, unless the user is installing them directly.
|
|
16
|
-
*/
|
|
17
|
-
function pluginPreserveLayouts() {
|
|
18
|
-
const layoutNames = [
|
|
19
|
-
"layouts/home",
|
|
20
|
-
"layouts/flux",
|
|
21
|
-
"layouts/glass",
|
|
22
|
-
"layouts/notebook",
|
|
23
|
-
"layouts/docs",
|
|
24
|
-
"layouts/shared"
|
|
25
|
-
];
|
|
26
|
-
const layoutComps = {
|
|
27
|
-
"local:<dir>/home/index.tsx": "layouts/home",
|
|
28
|
-
"local:<dir>/shared/index.tsx": "layouts/shared",
|
|
29
|
-
"local:<dir>/shared/client.tsx": "layouts/shared",
|
|
30
|
-
"local:<dir>/notebook/index.tsx": "layouts/notebook",
|
|
31
|
-
"local:<dir>/notebook/client.tsx": "layouts/notebook",
|
|
32
|
-
"local:<dir>/notebook/page/index.tsx": "layouts/notebook/page",
|
|
33
|
-
"local:<dir>/notebook/page/client.tsx": "layouts/notebook/page",
|
|
34
|
-
"local:<dir>/docs/index.tsx": "layouts/docs",
|
|
35
|
-
"local:<dir>/docs/client.tsx": "layouts/docs",
|
|
36
|
-
"local:<dir>/docs/page/index.tsx": "layouts/docs/page",
|
|
37
|
-
"local:<dir>/docs/page/client.tsx": "layouts/docs/page",
|
|
38
|
-
"local:<dir>/flux/index.tsx": "layouts/flux",
|
|
39
|
-
"local:<dir>/flux/page/index.tsx": "layouts/flux/page",
|
|
40
|
-
"local:<dir>/flux/page/client.tsx": "layouts/flux/page",
|
|
41
|
-
"local:<dir>/glass/index.tsx": "layouts/glass",
|
|
42
|
-
"local:<dir>/glass/page/index.tsx": "layouts/glass/page"
|
|
43
|
-
};
|
|
44
|
-
const layoutNameSet = new Set(layoutNames);
|
|
45
|
-
return {
|
|
46
|
-
beforeInstall(comp, { stack }) {
|
|
47
|
-
if (layoutNameSet.has(stack[0].name)) return;
|
|
48
|
-
return {
|
|
49
|
-
...comp,
|
|
50
|
-
$subComponents: comp.$subComponents.filter((child) => !layoutNameSet.has(child.name))
|
|
51
|
-
};
|
|
52
|
-
},
|
|
53
|
-
transformImport(specifier, { stack }) {
|
|
54
|
-
if (layoutNameSet.has(stack[0].name) || !(specifier in layoutComps)) return specifier;
|
|
55
|
-
return `fumadocs-ui/${layoutComps[specifier]}`;
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
//#endregion
|
|
60
|
-
//#region src/registry/plugins/shadcn.ts
|
|
61
|
-
/**
|
|
62
|
-
* `components/ui/*` follow the API of Shadcn UI, reuse the ones the project already has along with its `cn`.
|
|
63
|
-
*/
|
|
64
|
-
function pluginReuseUI(config, cwd = process.cwd()) {
|
|
65
|
-
const { baseDir, aliases } = config;
|
|
66
|
-
const utils = aliases.utils && path.resolve(cwd, baseDir, aliases.utils);
|
|
67
|
-
return {
|
|
68
|
-
beforeInstall(comp) {
|
|
69
|
-
const files = comp.files.filter((file) => {
|
|
70
|
-
if (file.type === "lib") return !(utils && file.path === "utils/cn.ts");
|
|
71
|
-
if (file.type !== "ui") return true;
|
|
72
|
-
return !existsSync(path.resolve(cwd, baseDir, aliases.uiDir, path.basename(file.path)));
|
|
73
|
-
});
|
|
74
|
-
if (files.length === comp.files.length) return;
|
|
75
|
-
if (files.length === 0) return {
|
|
76
|
-
...comp,
|
|
77
|
-
files,
|
|
78
|
-
dependencies: {},
|
|
79
|
-
devDependencies: {}
|
|
80
|
-
};
|
|
81
|
-
return {
|
|
82
|
-
...comp,
|
|
83
|
-
files
|
|
84
|
-
};
|
|
85
|
-
},
|
|
86
|
-
transformImport(specifier, { filePath }) {
|
|
87
|
-
if (!utils || specifier !== "local:utils/cn.ts") return specifier;
|
|
88
|
-
const relative = path.relative(path.dirname(filePath), utils).replaceAll(path.sep, "/");
|
|
89
|
-
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
//#endregion
|
|
94
|
-
//#region src/utils/prompt.ts
|
|
95
|
-
/**
|
|
96
|
-
* `@clack/core` narrows to its own `unique symbol`, which can't exclude the wider `symbol` that
|
|
97
|
-
* `@clack/prompts` returns, so a prompt result never narrows without this.
|
|
98
|
-
*/
|
|
99
|
-
function isCancel$1(value) {
|
|
100
|
-
return isCancel(value);
|
|
101
|
-
}
|
|
102
|
-
//#endregion
|
|
103
|
-
//#region src/registry/installer.ts
|
|
104
|
-
const uiRegistries = new Set(Object.values(UIRegistries));
|
|
105
|
-
/** components are compiled against one UI library, fetch them from the sub-registry of the configured one */
|
|
106
|
-
var UILibraryConnector = class {
|
|
107
|
-
constructor(base, ui) {
|
|
108
|
-
this.base = base;
|
|
109
|
-
this.ui = ui;
|
|
110
|
-
this.id = base.id;
|
|
111
|
-
}
|
|
112
|
-
resolve(subRegistry) {
|
|
113
|
-
return subRegistry && uiRegistries.has(subRegistry) ? this.ui : subRegistry;
|
|
114
|
-
}
|
|
115
|
-
fetchRegistryInfo(subRegistry) {
|
|
116
|
-
return this.base.fetchRegistryInfo(this.resolve(subRegistry));
|
|
117
|
-
}
|
|
118
|
-
fetchComponent(name, subRegistry) {
|
|
119
|
-
return this.base.fetchComponent(name, this.resolve(subRegistry));
|
|
120
|
-
}
|
|
121
|
-
hasComponent(name, subRegistry) {
|
|
122
|
-
return this.base.hasComponent(name, this.resolve(subRegistry));
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
var FumadocsComponentInstaller = class extends ComponentInstaller {
|
|
126
|
-
constructor(connector, config, cwd, io) {
|
|
127
|
-
super(new UILibraryConnector(connector, UIRegistries[config.uiLibrary]), {
|
|
128
|
-
cwd,
|
|
129
|
-
framework: config.framework === "astro" ? "none" : config.framework,
|
|
130
|
-
outDir: {
|
|
131
|
-
base: config.baseDir,
|
|
132
|
-
components: config.aliases.componentsDir,
|
|
133
|
-
css: config.aliases.cssDir,
|
|
134
|
-
layout: config.aliases.layoutDir,
|
|
135
|
-
lib: config.aliases.libDir,
|
|
136
|
-
ui: config.aliases.uiDir
|
|
137
|
-
},
|
|
138
|
-
io: {
|
|
139
|
-
onWarn: (message) => {
|
|
140
|
-
this.interactive?.spin.message(message);
|
|
141
|
-
},
|
|
142
|
-
confirmFileOverride: async (options) => {
|
|
143
|
-
if (!this.interactive) return true;
|
|
144
|
-
const { name, spin } = this.interactive;
|
|
145
|
-
spin.clear();
|
|
146
|
-
const value = await confirm({
|
|
147
|
-
message: `Do you want to override ${options.path}?`,
|
|
148
|
-
initialValue: false
|
|
149
|
-
});
|
|
150
|
-
if (isCancel$1(value)) {
|
|
151
|
-
outro("Installation terminated");
|
|
152
|
-
process.exit(0);
|
|
153
|
-
}
|
|
154
|
-
spin.start(picocolors.bold(picocolors.cyanBright(`Installing ${name}`)));
|
|
155
|
-
return value;
|
|
156
|
-
},
|
|
157
|
-
onFileDownloaded: (options) => {
|
|
158
|
-
this.interactive?.spin.message(options.path);
|
|
159
|
-
},
|
|
160
|
-
...io
|
|
161
|
-
},
|
|
162
|
-
plugins: [pluginPreserveLayouts(), pluginReuseUI(config, cwd)]
|
|
163
|
-
});
|
|
164
|
-
this.interactive = null;
|
|
165
|
-
}
|
|
166
|
-
async installInteractive(name, subRegistry) {
|
|
167
|
-
if (this.interactive) throw new Error(`cannot install while installing another component`);
|
|
168
|
-
const spin = spinner();
|
|
169
|
-
spin.start(picocolors.bold(picocolors.cyanBright(`Installing ${name}`)));
|
|
170
|
-
try {
|
|
171
|
-
this.interactive = {
|
|
172
|
-
name,
|
|
173
|
-
spin
|
|
174
|
-
};
|
|
175
|
-
const deps = await super.install(name, subRegistry).then((res) => res.deps());
|
|
176
|
-
spin.stop(picocolors.bold(picocolors.greenBright(`${name} installed`)));
|
|
177
|
-
if (deps.hasRequired()) {
|
|
178
|
-
log.message();
|
|
179
|
-
box([...deps.dependencies, ...deps.devDependencies].join("\n"), "New Dependencies");
|
|
180
|
-
const pm = (await detectPackageManager())?.name ?? "npm";
|
|
181
|
-
const value = await confirm({ message: `Do you want to install with ${pm}?` });
|
|
182
|
-
if (isCancel$1(value)) {
|
|
183
|
-
outro("Installation terminated");
|
|
184
|
-
process.exit(0);
|
|
185
|
-
}
|
|
186
|
-
if (value) {
|
|
187
|
-
const spin = spinner({ errorMessage: "Failed to install dependencies" });
|
|
188
|
-
spin.start("Installing dependencies");
|
|
189
|
-
await deps.installRequired(pm);
|
|
190
|
-
spin.stop("Dependencies installed");
|
|
191
|
-
} else await deps.writeRequired();
|
|
192
|
-
}
|
|
193
|
-
} catch (e) {
|
|
194
|
-
spin.error(e instanceof Error ? e.message : String(e));
|
|
195
|
-
process.exit(-1);
|
|
196
|
-
} finally {
|
|
197
|
-
this.interactive = null;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
//#endregion
|
|
202
|
-
export { isCancel$1 as n, UIRegistries as r, FumadocsComponentInstaller as t };
|
|
203
|
-
|
|
204
|
-
//# sourceMappingURL=installer-CzhVfs7i.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"installer-CzhVfs7i.js","names":["isCancel","isCancelSymbol","isCancel"],"sources":["../src/commands/shared.ts","../src/registry/plugins/preserve.ts","../src/registry/plugins/shadcn.ts","../src/utils/prompt.ts","../src/registry/installer.ts"],"sourcesContent":["export const UIRegistries = {\n 'base-ui': 'fumadocs/base-ui',\n 'radix-ui': 'fumadocs/radix-ui',\n};\n","import type { InstallerPlugin } from 'fuma-cli/registry/installer';\n\n/**\n * keep references to `fumadocs-ui/layouts/*` components as original, unless the user is installing them directly.\n */\nexport function pluginPreserveLayouts(): InstallerPlugin {\n const layoutNames = [\n 'layouts/home',\n 'layouts/flux',\n 'layouts/glass',\n 'layouts/notebook',\n 'layouts/docs',\n 'layouts/shared',\n ];\n // original specifier -> new specifier\n const layoutComps: Record<string, string> = {\n 'local:<dir>/home/index.tsx': 'layouts/home',\n 'local:<dir>/shared/index.tsx': 'layouts/shared',\n 'local:<dir>/shared/client.tsx': 'layouts/shared',\n 'local:<dir>/notebook/index.tsx': 'layouts/notebook',\n 'local:<dir>/notebook/client.tsx': 'layouts/notebook',\n 'local:<dir>/notebook/page/index.tsx': 'layouts/notebook/page',\n 'local:<dir>/notebook/page/client.tsx': 'layouts/notebook/page',\n 'local:<dir>/docs/index.tsx': 'layouts/docs',\n 'local:<dir>/docs/client.tsx': 'layouts/docs',\n 'local:<dir>/docs/page/index.tsx': 'layouts/docs/page',\n 'local:<dir>/docs/page/client.tsx': 'layouts/docs/page',\n 'local:<dir>/flux/index.tsx': 'layouts/flux',\n 'local:<dir>/flux/page/index.tsx': 'layouts/flux/page',\n 'local:<dir>/flux/page/client.tsx': 'layouts/flux/page',\n 'local:<dir>/glass/index.tsx': 'layouts/glass',\n 'local:<dir>/glass/page/index.tsx': 'layouts/glass/page',\n };\n const layoutNameSet = new Set(layoutNames);\n\n return {\n beforeInstall(comp, { stack }) {\n const isDirectInstall = layoutNameSet.has(stack[0].name);\n if (isDirectInstall) return;\n\n return {\n ...comp,\n $subComponents: comp.$subComponents.filter((child) => !layoutNameSet.has(child.name)),\n };\n },\n transformImport(specifier, { stack }) {\n const isDirectInstall = layoutNameSet.has(stack[0].name);\n // skip if direct install or unrelated to layout component\n if (isDirectInstall || !(specifier in layoutComps)) return specifier;\n\n return `fumadocs-ui/${layoutComps[specifier]}`;\n },\n };\n}\n","import { existsSync } from 'node:fs';\nimport path from 'node:path';\nimport type { InstallerPlugin } from 'fuma-cli/registry/installer';\nimport type { LoadedConfig } from '@/config';\n\n/**\n * `components/ui/*` follow the API of Shadcn UI, reuse the ones the project already has along with its `cn`.\n */\nexport function pluginReuseUI(config: LoadedConfig, cwd = process.cwd()): InstallerPlugin {\n const { baseDir, aliases } = config;\n const utils = aliases.utils && path.resolve(cwd, baseDir, aliases.utils);\n\n return {\n beforeInstall(comp) {\n const files = comp.files.filter((file) => {\n if (file.type === 'lib') return !(utils && file.path === 'utils/cn.ts');\n if (file.type !== 'ui') return true;\n return !existsSync(path.resolve(cwd, baseDir, aliases.uiDir, path.basename(file.path)));\n });\n\n if (files.length === comp.files.length) return;\n // a fully reused component brings no dependencies\n if (files.length === 0) return { ...comp, files, dependencies: {}, devDependencies: {} };\n return { ...comp, files };\n },\n transformImport(specifier, { filePath }) {\n if (!utils || specifier !== 'local:utils/cn.ts') return specifier;\n const relative = path.relative(path.dirname(filePath), utils).replaceAll(path.sep, '/');\n return relative.startsWith('.') ? relative : `./${relative}`;\n },\n };\n}\n","import { isCancel as isCancelSymbol } from '@clack/prompts';\n\n/**\n * `@clack/core` narrows to its own `unique symbol`, which can't exclude the wider `symbol` that\n * `@clack/prompts` returns, so a prompt result never narrows without this.\n */\nexport function isCancel<T>(value: T | symbol): value is symbol {\n return isCancelSymbol(value);\n}\n","import { ComponentInstaller, type IOInterface } from 'fuma-cli/registry/installer';\nimport { pluginPreserveLayouts } from './plugins/preserve';\nimport { pluginReuseUI } from './plugins/shadcn';\nimport type { RegistryConnector } from 'fuma-cli/registry/connector';\nimport { UIRegistries } from '@/commands/shared';\nimport type { LoadedConfig } from '@/config';\nimport { box, confirm, log, outro, spinner, SpinnerResult } from '@clack/prompts';\nimport { isCancel } from '@/utils/prompt';\nimport picocolors from 'picocolors';\nimport { detectPackageManager } from 'fuma-cli/detect';\n\nconst uiRegistries = new Set(Object.values(UIRegistries));\n\n/** components are compiled against one UI library, fetch them from the sub-registry of the configured one */\nclass UILibraryConnector implements RegistryConnector {\n readonly id: string;\n\n constructor(\n private readonly base: RegistryConnector,\n private readonly ui: string,\n ) {\n this.id = base.id;\n }\n\n private resolve(subRegistry?: string) {\n return subRegistry && uiRegistries.has(subRegistry) ? this.ui : subRegistry;\n }\n\n fetchRegistryInfo(subRegistry?: string) {\n return this.base.fetchRegistryInfo(this.resolve(subRegistry));\n }\n\n fetchComponent(name: string, subRegistry?: string) {\n return this.base.fetchComponent(name, this.resolve(subRegistry));\n }\n\n hasComponent(name: string, subRegistry?: string) {\n return this.base.hasComponent(name, this.resolve(subRegistry));\n }\n}\n\nexport class FumadocsComponentInstaller extends ComponentInstaller {\n private interactive: {\n name: string;\n spin: SpinnerResult;\n } | null = null;\n\n constructor(\n connector: RegistryConnector,\n config: LoadedConfig,\n cwd?: string,\n io?: Partial<IOInterface>,\n ) {\n super(new UILibraryConnector(connector, UIRegistries[config.uiLibrary]), {\n cwd,\n framework: config.framework === 'astro' ? 'none' : config.framework,\n outDir: {\n base: config.baseDir,\n components: config.aliases.componentsDir,\n css: config.aliases.cssDir,\n layout: config.aliases.layoutDir,\n lib: config.aliases.libDir,\n ui: config.aliases.uiDir,\n },\n io: {\n onWarn: (message) => {\n this.interactive?.spin.message(message);\n },\n confirmFileOverride: async (options) => {\n if (!this.interactive) return true;\n const { name, spin } = this.interactive;\n spin.clear();\n const value = await confirm({\n message: `Do you want to override ${options.path}?`,\n initialValue: false,\n });\n if (isCancel(value)) {\n outro('Installation terminated');\n process.exit(0);\n }\n spin.start(picocolors.bold(picocolors.cyanBright(`Installing ${name}`)));\n return value;\n },\n onFileDownloaded: (options) => {\n this.interactive?.spin.message(options.path);\n },\n ...io,\n },\n plugins: [pluginPreserveLayouts(), pluginReuseUI(config, cwd)],\n });\n }\n\n async installInteractive(name: string, subRegistry?: string): Promise<void> {\n if (this.interactive) {\n throw new Error(`cannot install while installing another component`);\n }\n\n const spin = spinner();\n spin.start(picocolors.bold(picocolors.cyanBright(`Installing ${name}`)));\n\n try {\n this.interactive = { name, spin };\n const deps = await super.install(name, subRegistry).then((res) => res.deps());\n spin.stop(picocolors.bold(picocolors.greenBright(`${name} installed`)));\n\n if (deps.hasRequired()) {\n log.message();\n box([...deps.dependencies, ...deps.devDependencies].join('\\n'), 'New Dependencies');\n const pm = (await detectPackageManager())?.name ?? 'npm';\n const value = await confirm({\n message: `Do you want to install with ${pm}?`,\n });\n\n if (isCancel(value)) {\n outro('Installation terminated');\n process.exit(0);\n }\n\n if (value) {\n const spin = spinner({\n errorMessage: 'Failed to install dependencies',\n });\n spin.start('Installing dependencies');\n await deps.installRequired(pm);\n spin.stop('Dependencies installed');\n } else {\n await deps.writeRequired();\n }\n }\n } catch (e) {\n spin.error(e instanceof Error ? e.message : String(e));\n process.exit(-1);\n } finally {\n this.interactive = null;\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -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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"}
|