mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 23:49:51 +00:00
CLI: add assets to Orama Cloud template
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'create-fumadocs-app': patch
|
||||
---
|
||||
|
||||
Support Orama Cloud template
|
||||
@@ -8,6 +8,7 @@
|
||||
"@content-collections/core": "^0.11.1",
|
||||
"@content-collections/mdx": "^0.2.2",
|
||||
"@content-collections/next": "^0.2.8",
|
||||
"@orama/core": "^1.2.13",
|
||||
"@react-router/dev": "^7.9.4",
|
||||
"@react-router/node": "^7.9.4",
|
||||
"@react-router/serve": "^7.9.4",
|
||||
|
||||
@@ -8,6 +8,7 @@ export default [
|
||||
{
|
||||
rules: {
|
||||
'import/no-relative-packages': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { versions as localVersions } from '@/versions';
|
||||
import versionPkg from '../../create-app-versions/package.json';
|
||||
|
||||
export const sourceDir = fileURLToPath(new URL(`../`, import.meta.url).href);
|
||||
export const cwd = process.cwd();
|
||||
@@ -40,3 +42,8 @@ export const templates = [
|
||||
componentsDir: 'src/components',
|
||||
},
|
||||
] satisfies TemplateInfo[];
|
||||
|
||||
export const depVersions = {
|
||||
...localVersions,
|
||||
...versionPkg.dependencies,
|
||||
};
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs/promises';
|
||||
import { copy, tryGitInit } from '@/utils';
|
||||
import { versions as localVersions } from '@/versions';
|
||||
import versionPkg from '../../create-app-versions/package.json';
|
||||
import { copy, pick, tryGitInit } from '@/utils';
|
||||
import type { PackageManager } from './auto-install';
|
||||
import { autoInstall } from './auto-install';
|
||||
import { cwd, sourceDir, TemplateInfo, templates } from './constants';
|
||||
import {
|
||||
cwd,
|
||||
depVersions,
|
||||
sourceDir,
|
||||
type TemplateInfo,
|
||||
templates,
|
||||
} from './constants';
|
||||
|
||||
export type Template = (typeof templates)[number]['value'];
|
||||
export interface Options {
|
||||
@@ -45,15 +49,26 @@ export interface TemplatePluginContext {
|
||||
dest: string;
|
||||
}
|
||||
|
||||
type Awaitable<T> = T | Promise<T>;
|
||||
|
||||
export interface TemplatePlugin {
|
||||
afterWrite?: (context: TemplatePluginContext) => Promise<void>;
|
||||
packageJson?: (
|
||||
this: TemplatePluginContext,
|
||||
packageJson: any,
|
||||
) => Awaitable<void | any>;
|
||||
afterWrite?: (this: TemplatePluginContext) => Awaitable<void>;
|
||||
readme?: (
|
||||
this: TemplatePluginContext,
|
||||
content: string,
|
||||
) => Awaitable<void | string>;
|
||||
}
|
||||
|
||||
function defaults(options: Options): Required<Options> {
|
||||
return {
|
||||
...options,
|
||||
plugins: options.plugins ?? [],
|
||||
useSrcDir: options.useSrcDir ?? false,
|
||||
useSrcDir:
|
||||
options.template.startsWith('+next') && options.useSrcDir === true,
|
||||
lint: options.lint ?? false,
|
||||
initializeGit: options.initializeGit ?? false,
|
||||
installDeps: options.installDeps ?? false,
|
||||
@@ -61,6 +76,10 @@ function defaults(options: Options): Required<Options> {
|
||||
};
|
||||
}
|
||||
|
||||
function isRelative(dir: string, file: string) {
|
||||
return !path.relative(dir, file).startsWith(`..${path.sep}`);
|
||||
}
|
||||
|
||||
export async function create(createOptions: Options): Promise<void> {
|
||||
const options = defaults(createOptions);
|
||||
const {
|
||||
@@ -84,71 +103,63 @@ export async function create(createOptions: Options): Promise<void> {
|
||||
options,
|
||||
};
|
||||
|
||||
function isRelative(dir: string, file: string) {
|
||||
return !path
|
||||
.relative(path.join(dest, dir), file)
|
||||
.startsWith(`..${path.sep}`);
|
||||
}
|
||||
|
||||
function defaultRename(file: string): string {
|
||||
file = file.replace('example.gitignore', '.gitignore');
|
||||
|
||||
if (!useSrcDir || !isNext) {
|
||||
return file;
|
||||
}
|
||||
|
||||
if (
|
||||
path.basename(file) === 'mdx-components.tsx' ||
|
||||
isRelative('app', file) ||
|
||||
isRelative('lib', file)
|
||||
) {
|
||||
return path.join(dest, 'src', path.relative(dest, file));
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
await copy(path.join(sourceDir, `template/${template}`), dest, {
|
||||
rename: defaultRename,
|
||||
});
|
||||
rename(file) {
|
||||
file = file.replace('example.gitignore', '.gitignore');
|
||||
|
||||
if (isNext) {
|
||||
// optional ESLint configuration
|
||||
if (lint) {
|
||||
await copy(path.join(sourceDir, `template/+next+${lint}`), dest, {
|
||||
rename: defaultRename,
|
||||
});
|
||||
log('Configured Linter');
|
||||
}
|
||||
|
||||
// update tsconfig.json for src dir
|
||||
if (useSrcDir) {
|
||||
const tsconfigPath = path.join(dest, 'tsconfig.json');
|
||||
const content = (await fs.readFile(tsconfigPath)).toString();
|
||||
|
||||
const config = JSON.parse(content);
|
||||
|
||||
if (config.compilerOptions?.paths) {
|
||||
Object.assign(config.compilerOptions.paths, {
|
||||
'@/*': ['./src/*'],
|
||||
});
|
||||
if (
|
||||
useSrcDir &&
|
||||
(path.basename(file) === 'mdx-components.tsx' ||
|
||||
isRelative(path.join(dest, 'app'), file) ||
|
||||
isRelative(path.join(dest, 'lib'), file))
|
||||
) {
|
||||
return path.join(dest, 'src', path.relative(dest, file));
|
||||
}
|
||||
|
||||
await fs.writeFile(tsconfigPath, JSON.stringify(config, null, 2));
|
||||
}
|
||||
return file;
|
||||
},
|
||||
});
|
||||
|
||||
// optional ESLint configuration
|
||||
if (isNext && lint) {
|
||||
await copy(path.join(sourceDir, `template/+next+${lint}`), dest);
|
||||
log('Configured Linter');
|
||||
}
|
||||
|
||||
const packageJson = await createPackageJson(projectName, dest, options);
|
||||
// update tsconfig.json for src dir
|
||||
if (isNext && useSrcDir) {
|
||||
const tsconfigPath = path.join(dest, 'tsconfig.json');
|
||||
const content = (await fs.readFile(tsconfigPath)).toString();
|
||||
const config = JSON.parse(content);
|
||||
|
||||
if (config.compilerOptions?.paths) {
|
||||
Object.assign(config.compilerOptions.paths, {
|
||||
'@/*': ['./src/*'],
|
||||
});
|
||||
}
|
||||
|
||||
await fs.writeFile(tsconfigPath, JSON.stringify(config, null, 2));
|
||||
}
|
||||
|
||||
let packageJson = await createPackageJson(projectName, dest, options);
|
||||
for (const plugin of plugins) {
|
||||
const result = await plugin.packageJson?.call(pluginContext, packageJson);
|
||||
|
||||
if (result) packageJson = result;
|
||||
}
|
||||
await fs.writeFile(
|
||||
path.join(dest, 'package.json'),
|
||||
JSON.stringify(packageJson, null, 2),
|
||||
);
|
||||
|
||||
const readMe = await getReadme(dest, projectName);
|
||||
await fs.writeFile(path.join(dest, 'README.md'), readMe);
|
||||
let readme = await getReadme(dest, projectName);
|
||||
for (const plugin of plugins) {
|
||||
readme = (await plugin.readme?.call(pluginContext, readme)) ?? readme;
|
||||
}
|
||||
await fs.writeFile(path.join(dest, 'README.md'), readme);
|
||||
|
||||
for (const plugin of plugins) {
|
||||
await plugin.afterWrite?.(pluginContext);
|
||||
await plugin.afterWrite?.call(pluginContext);
|
||||
}
|
||||
|
||||
if (installDeps) {
|
||||
@@ -181,8 +192,8 @@ async function createPackageJson(
|
||||
const isNext = template.startsWith('+next');
|
||||
function replaceWorkspaceDeps(deps: Record<string, string>) {
|
||||
for (const k in deps) {
|
||||
if (deps[k].startsWith('workspace:') && k in localVersions) {
|
||||
deps[k] = localVersions[k as keyof typeof localVersions];
|
||||
if (deps[k].startsWith('workspace:') && k in depVersions) {
|
||||
deps[k] = depVersions[k as keyof typeof depVersions];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +233,7 @@ async function createPackageJson(
|
||||
},
|
||||
devDependencies: {
|
||||
...packageJson.devDependencies,
|
||||
...pick(versionPkg.dependencies, ['@biomejs/biome']),
|
||||
...pick(depVersions, ['@biomejs/biome']),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -237,7 +248,7 @@ async function createPackageJson(
|
||||
devDependencies: {
|
||||
...packageJson.devDependencies,
|
||||
eslint: '^9',
|
||||
'eslint-config-next': versionPkg.dependencies.next,
|
||||
'eslint-config-next': depVersions.next,
|
||||
'@eslint/eslintrc': '^3',
|
||||
},
|
||||
};
|
||||
@@ -245,18 +256,3 @@ async function createPackageJson(
|
||||
|
||||
return packageJson;
|
||||
}
|
||||
|
||||
function pick<T extends object, K extends keyof T>(
|
||||
obj: T,
|
||||
keys: K[],
|
||||
): Pick<T, K> {
|
||||
const result: Partial<T> = {};
|
||||
|
||||
for (const key of keys) {
|
||||
if (key in obj) {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
}
|
||||
|
||||
return result as Pick<T, K>;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,79 @@
|
||||
import { TemplatePlugin } from '@/create-app';
|
||||
import { copy } from '@/utils';
|
||||
import { copy, pick } from '@/utils';
|
||||
import path from 'node:path';
|
||||
import { sourceDir } from '@/constants';
|
||||
import { depVersions, sourceDir } from '@/constants';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
const oramaCloud: TemplatePlugin = {
|
||||
async afterWrite({ dest, template, options }) {
|
||||
const baseDir = path.join(dest, options.useSrcDir ? 'src' : '.');
|
||||
await copy(
|
||||
path.join(sourceDir, 'template/+orama-cloud/search.tsx'),
|
||||
path.join(baseDir, template.componentsDir, 'search.tsx'),
|
||||
packageJson(packageJson) {
|
||||
return {
|
||||
...packageJson,
|
||||
scripts: {
|
||||
...packageJson.scripts,
|
||||
build: `${packageJson.build} && bun scripts/sync-content.ts`,
|
||||
},
|
||||
dependencies: {
|
||||
...packageJson.dependencies,
|
||||
...pick(depVersions, ['@orama/core']),
|
||||
},
|
||||
};
|
||||
},
|
||||
readme(content) {
|
||||
return `${content}\n\n## Orama Cloud
|
||||
|
||||
This project uses Orama Cloud for 3rd party search solution.
|
||||
|
||||
See https://fumadocs.dev/docs/headless/search/orama-cloud for integrating Orama Cloud to Fumadocs.`;
|
||||
},
|
||||
async afterWrite() {
|
||||
const { dest, template, options } = this;
|
||||
const appDir = path.join(dest, options.useSrcDir ? 'src' : '.');
|
||||
await copy(path.join(sourceDir, 'template/+orama-cloud/@root'), dest);
|
||||
await copy(path.join(sourceDir, 'template/+orama-cloud/@app'), appDir);
|
||||
|
||||
const filePath = {
|
||||
'+next+fuma-docs-mdx': '.next/server/app/static.json.body',
|
||||
'tanstack-start': '.output/public/static.json',
|
||||
'react-router': 'build/client/static.json',
|
||||
'react-router-spa': 'build/client/static.json',
|
||||
waku: 'dist/public/static.json',
|
||||
}[template.value];
|
||||
|
||||
const SyncContentScript = `import { type OramaDocument, sync } from 'fumadocs-core/search/orama-cloud';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import { OramaCloud } from '@orama/core';
|
||||
|
||||
// the path of pre-rendered \`static.json\`
|
||||
const filePath = ${JSON.stringify(filePath)};
|
||||
|
||||
async function main() {
|
||||
const orama = new OramaCloud({
|
||||
projectId: process.env.NEXT_PUBLIC_ORAMA_PROJECT_ID,
|
||||
apiKey: process.env.ORAMA_PRIVATE_API_KEY,
|
||||
});
|
||||
|
||||
const content = await fs.readFile(filePath);
|
||||
const records = JSON.parse(content.toString()) as OramaDocument[];
|
||||
|
||||
await sync(orama, {
|
||||
index: process.env.NEXT_PUBLIC_ORAMA_DATASOURCE_ID,
|
||||
documents: records,
|
||||
});
|
||||
|
||||
console.log(\`search updated: \${records.length} records\`);
|
||||
}
|
||||
|
||||
void main();
|
||||
`;
|
||||
await fs.mkdir(path.join(dest, 'scripts'), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(dest, 'scripts/sync-content.ts'),
|
||||
SyncContentScript,
|
||||
);
|
||||
|
||||
await copy(
|
||||
path.join(sourceDir, `template/+orama-cloud/${template.value}`),
|
||||
baseDir,
|
||||
appDir,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -101,3 +101,18 @@ export async function tryGitInit(cwd: string): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function pick<T extends object, K extends keyof T>(
|
||||
obj: T,
|
||||
keys: K[],
|
||||
): Pick<T, K> {
|
||||
const result: Partial<T> = {};
|
||||
|
||||
for (const key of keys) {
|
||||
if (key in obj) {
|
||||
result[key] = obj[key];
|
||||
}
|
||||
}
|
||||
|
||||
return result as Pick<T, K>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { exportSearchIndexes } from '@/lib/export-search-indexes';
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
export async function GET() {
|
||||
return Response.json(await exportSearchIndexes());
|
||||
}
|
||||
+2
-2
@@ -17,8 +17,8 @@ import { OramaCloud } from '@orama/core';
|
||||
import { useI18n } from 'fumadocs-ui/contexts/i18n';
|
||||
|
||||
const client = new OramaCloud({
|
||||
projectId: '',
|
||||
apiKey: '',
|
||||
projectId: process.env.NEXT_PUBLIC_ORAMA_DATASOURCE_ID,
|
||||
apiKey: process.env.NEXT_PUBLIC_ORAMA_API_KEY,
|
||||
});
|
||||
|
||||
export default function CustomSearchDialog(props: SharedProps) {
|
||||
@@ -0,0 +1,14 @@
|
||||
import { source } from '@/lib/source';
|
||||
import type { OramaDocument } from 'fumadocs-core/search/orama-cloud';
|
||||
|
||||
export async function exportSearchIndexes() {
|
||||
return source.getPages().map((page) => {
|
||||
return {
|
||||
id: page.url,
|
||||
structured: page.data.structuredData,
|
||||
url: page.url,
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
} satisfies OramaDocument;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
NEXT_PUBLIC_ORAMA_DATASOURCE_ID="Rest API data source ID"
|
||||
NEXT_PUBLIC_ORAMA_PROJECT_ID="project ID"
|
||||
NEXT_PUBLIC_ORAMA_API_KEY="public API key"
|
||||
|
||||
# keep it secretly!
|
||||
ORAMA_PRIVATE_API_KEY="private API key"
|
||||
@@ -0,0 +1,8 @@
|
||||
import { index, route, type RouteConfig } from '@react-router/dev/routes';
|
||||
|
||||
export default [
|
||||
index('routes/home.tsx'),
|
||||
route('docs/*', 'docs/page.tsx'),
|
||||
route('api/search', 'docs/search.ts'),
|
||||
route('static.json', 'routes/static.ts'),
|
||||
] satisfies RouteConfig;
|
||||
@@ -0,0 +1,5 @@
|
||||
import { exportSearchIndexes } from '@/lib/export-search-indexes';
|
||||
|
||||
export async function loader() {
|
||||
return Response.json(await exportSearchIndexes());
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { index, route, type RouteConfig } from '@react-router/dev/routes';
|
||||
|
||||
export default [
|
||||
index('routes/home.tsx'),
|
||||
route('docs/*', 'docs/page.tsx'),
|
||||
route('api/search', 'docs/search.ts'),
|
||||
route('static.json', 'routes/static.ts'),
|
||||
] satisfies RouteConfig;
|
||||
@@ -0,0 +1,5 @@
|
||||
import { exportSearchIndexes } from '@/lib/export-search-indexes';
|
||||
|
||||
export async function loader() {
|
||||
return Response.json(await exportSearchIndexes());
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { exportSearchIndexes } from '@/lib/export-search-indexes';
|
||||
|
||||
export const Route = createFileRoute('/static.json')({
|
||||
server: {
|
||||
handlers: {
|
||||
GET: async () => Response.json(await exportSearchIndexes()),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import tsConfigPaths from 'vite-tsconfig-paths';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import mdx from 'fumadocs-mdx/vite';
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
plugins: [
|
||||
mdx(await import('./source.config')),
|
||||
tailwindcss(),
|
||||
tsConfigPaths({
|
||||
projects: ['./tsconfig.json'],
|
||||
}),
|
||||
tanstackStart({
|
||||
prerender: {
|
||||
enabled: true,
|
||||
// pre-render the static file
|
||||
pages: [{ path: '/static.json' }],
|
||||
},
|
||||
}),
|
||||
react(),
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { exportSearchIndexes } from '@/lib/export-search-indexes';
|
||||
|
||||
export async function GET() {
|
||||
return Response.json(await exportSearchIndexes());
|
||||
}
|
||||
|
||||
export const getConfig = () => ({
|
||||
render: 'static',
|
||||
});
|
||||
Generated
+55
-55
Reference in New Issue
Block a user