mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 10:55:57 +00:00
CLI: Use smarter AST transform for modifying app configs
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'create-fumadocs-app': patch
|
||||
---
|
||||
|
||||
Use smarter AST transform for modifying app configs
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'create-fumadocs-app': patch
|
||||
---
|
||||
|
||||
fix type info lost on `create`
|
||||
@@ -5,9 +5,14 @@ import { createGetUrl, getSlugs } from 'fumadocs-core/source';
|
||||
const getUrl = createGetUrl('/docs');
|
||||
|
||||
export default {
|
||||
ssr: false,
|
||||
ssr: true,
|
||||
async prerender({ getStaticPaths }) {
|
||||
const paths: string[] = [...getStaticPaths()];
|
||||
const paths: string[] = [];
|
||||
const excluded: string[] = [];
|
||||
|
||||
for (const path of getStaticPaths()) {
|
||||
if (!excluded.includes(path)) paths.push(path);
|
||||
}
|
||||
|
||||
for await (const entry of glob('**/*.mdx', { cwd: 'content/docs' })) {
|
||||
paths.push(getUrl(getSlugs(entry)));
|
||||
|
||||
@@ -8,10 +8,11 @@ export default {
|
||||
ssr: true,
|
||||
async prerender({ getStaticPaths }) {
|
||||
const paths: string[] = [];
|
||||
// ignore dynamic document search
|
||||
const excluded = ['/api/search'];
|
||||
|
||||
for (const path of getStaticPaths()) {
|
||||
// ignore dynamic document search
|
||||
if (path === '/api/search') continue;
|
||||
paths.push(path);
|
||||
if (!excluded.includes(path)) paths.push(path);
|
||||
}
|
||||
|
||||
for await (const entry of glob('**/*.mdx', { cwd: 'content/docs' })) {
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
"@clack/prompts": "^0.11.0",
|
||||
"commander": "^14.0.1",
|
||||
"picocolors": "^1.1.1",
|
||||
"tinyexec": "^1.0.1"
|
||||
"tinyexec": "^1.0.1",
|
||||
"ts-morph": "^27.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cross-spawn": "^6.0.6",
|
||||
|
||||
@@ -6,42 +6,40 @@ export const sourceDir = fileURLToPath(new URL(`../`, import.meta.url).href);
|
||||
export const cwd = process.cwd();
|
||||
|
||||
export interface TemplateInfo {
|
||||
value:
|
||||
| '+next+fuma-docs-mdx'
|
||||
| 'waku'
|
||||
| 'react-router'
|
||||
| 'react-router-spa'
|
||||
| 'tanstack-start';
|
||||
label: string;
|
||||
value: string;
|
||||
hint?: string;
|
||||
|
||||
componentsDir: string;
|
||||
}
|
||||
|
||||
export const templates = [
|
||||
export const templates: TemplateInfo[] = [
|
||||
{
|
||||
value: '+next+fuma-docs-mdx',
|
||||
label: 'Next.js: Fumadocs MDX',
|
||||
hint: 'recommended',
|
||||
componentsDir: 'components',
|
||||
},
|
||||
{
|
||||
value: 'waku',
|
||||
label: 'Waku: Fumadocs MDX',
|
||||
componentsDir: 'src/components',
|
||||
},
|
||||
{
|
||||
value: 'react-router',
|
||||
label: 'React Router: Fumadocs MDX (not RSC)',
|
||||
componentsDir: 'app/components',
|
||||
},
|
||||
{
|
||||
value: 'react-router-spa',
|
||||
label: 'React Router SPA: Fumadocs MDX (not RSC)',
|
||||
hint: 'SPA mode allows you to host the site statically, compatible with a CDN.',
|
||||
componentsDir: 'app/components',
|
||||
},
|
||||
{
|
||||
value: 'tanstack-start',
|
||||
label: 'Tanstack Start: Fumadocs MDX (not RSC)',
|
||||
componentsDir: 'src/components',
|
||||
},
|
||||
] as const satisfies TemplateInfo[];
|
||||
];
|
||||
|
||||
export const depVersions = {
|
||||
...localVersions,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
templates,
|
||||
} from './constants';
|
||||
|
||||
export type Template = (typeof templates)[number]['value'];
|
||||
export type Template = TemplateInfo['value'];
|
||||
export interface Options {
|
||||
outputDir: string;
|
||||
template: Template;
|
||||
|
||||
@@ -123,6 +123,10 @@ async function main(config: Options): Promise<void> {
|
||||
message: 'Configure linter?',
|
||||
initialValue: false,
|
||||
options: [
|
||||
{
|
||||
value: false,
|
||||
label: 'Disabled',
|
||||
},
|
||||
{
|
||||
value: 'eslint',
|
||||
label: 'ESLint',
|
||||
@@ -131,10 +135,6 @@ async function main(config: Options): Promise<void> {
|
||||
value: 'biome',
|
||||
label: 'Biome',
|
||||
},
|
||||
{
|
||||
value: false,
|
||||
label: 'Disabled',
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
|
||||
@@ -3,6 +3,10 @@ import { copy, pick } from '@/utils';
|
||||
import path from 'node:path';
|
||||
import { depVersions, sourceDir } from '@/constants';
|
||||
import fs from 'node:fs/promises';
|
||||
import { createSourceFile } from '@/transform/shared';
|
||||
import { addTanstackPrerender } from '@/transform/tanstack-start';
|
||||
import { addReactRouterRoute } from '@/transform/react-router';
|
||||
import { addSearchDialog } from '@/transform/provider';
|
||||
|
||||
const oramaCloud: TemplatePlugin = {
|
||||
packageJson(packageJson) {
|
||||
@@ -30,6 +34,41 @@ See https://fumadocs.dev/docs/headless/search/orama-cloud for integrating Orama
|
||||
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);
|
||||
let providerFilePath: string;
|
||||
|
||||
if (template.value === 'tanstack-start') {
|
||||
await fluent(
|
||||
createSourceFile(path.join(dest, 'vite.config.ts')),
|
||||
(file) => addTanstackPrerender(file, ['/static.json']),
|
||||
(file) => file.save(),
|
||||
);
|
||||
|
||||
providerFilePath = path.join(dest, 'src/routes/__root.tsx');
|
||||
} else if (template.value.startsWith('react-router')) {
|
||||
await fluent(
|
||||
createSourceFile(path.join(dest, 'app/routes.ts')),
|
||||
(file) =>
|
||||
addReactRouterRoute(file, [
|
||||
{
|
||||
path: 'static.json',
|
||||
entry: 'routes/static.ts',
|
||||
},
|
||||
]),
|
||||
(file) => file.save(),
|
||||
);
|
||||
|
||||
providerFilePath = path.join(dest, 'app/root.tsx');
|
||||
} else if (template.value.startsWith('+next')) {
|
||||
providerFilePath = path.join(appDir, 'app/layout.tsx');
|
||||
} else {
|
||||
providerFilePath = path.join(dest, 'src/components/provider.tsx');
|
||||
}
|
||||
|
||||
await fluent(
|
||||
createSourceFile(providerFilePath),
|
||||
(file) => addSearchDialog(file),
|
||||
(file) => file.save(),
|
||||
);
|
||||
|
||||
const filePath = {
|
||||
'+next+fuma-docs-mdx': '.next/server/app/static.json.body',
|
||||
@@ -78,4 +117,15 @@ void main();
|
||||
},
|
||||
};
|
||||
|
||||
async function fluent<T>(
|
||||
value: T | Promise<T>,
|
||||
...actions: ((value: T) => void | Promise<void>)[]
|
||||
): Promise<T> {
|
||||
for (const action of actions) {
|
||||
await action(await value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export default oramaCloud;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { SourceFile, StructureKind, SyntaxKind } from 'ts-morph';
|
||||
|
||||
/**
|
||||
* 1. find the `<RootProvider />` call.
|
||||
* 2. add `search={{ SearchDialog }}` prop.
|
||||
* 3. add import of `SearchDialog`.
|
||||
*
|
||||
*/
|
||||
export function addSearchDialog(sourceFile: SourceFile) {
|
||||
const elements = sourceFile.getDescendantsOfKind(SyntaxKind.JsxElement);
|
||||
|
||||
for (const element of elements) {
|
||||
const provider = element.getFirstChildByKind(SyntaxKind.JsxOpeningElement);
|
||||
if (provider?.getTagNameNode().getText() !== 'RootProvider') continue;
|
||||
|
||||
// Skip if search prop already exists
|
||||
if (
|
||||
provider
|
||||
.getAttributes()
|
||||
.some(
|
||||
(attr) =>
|
||||
attr.isKind(SyntaxKind.JsxAttribute) &&
|
||||
attr.getNameNode().getText() === 'search',
|
||||
)
|
||||
)
|
||||
continue;
|
||||
|
||||
provider.addAttribute({
|
||||
kind: StructureKind.JsxAttribute,
|
||||
name: 'search',
|
||||
initializer: '{{ SearchDialog }}',
|
||||
});
|
||||
}
|
||||
|
||||
sourceFile.addImportDeclaration({
|
||||
moduleSpecifier: '@/components/search',
|
||||
defaultImport: 'SearchDialog',
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { MethodDeclaration, SourceFile, SyntaxKind } from 'ts-morph';
|
||||
|
||||
/**
|
||||
* Add items to `excluded` in the prerender function
|
||||
*/
|
||||
export function removeReactRouterPrerender(
|
||||
sourceFile: SourceFile,
|
||||
paths: string[],
|
||||
) {
|
||||
const methodBody = getPrerenderMethod(sourceFile)?.getBody();
|
||||
if (!methodBody) return;
|
||||
|
||||
const excludedInitializer = methodBody
|
||||
.getDescendantsOfKind(SyntaxKind.VariableDeclaration)
|
||||
.find((item) => item.getName() === 'excluded')
|
||||
?.getInitializerIfKind(SyntaxKind.ArrayLiteralExpression);
|
||||
|
||||
if (!excludedInitializer) return;
|
||||
for (const path of paths) {
|
||||
excludedInitializer.addElement(JSON.stringify(path));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new route to route config
|
||||
*/
|
||||
export function addReactRouterRoute(
|
||||
sourceFile: SourceFile,
|
||||
routes: { path: string; entry: string }[],
|
||||
) {
|
||||
const initializer = sourceFile
|
||||
.getDefaultExportSymbol()
|
||||
?.getValueDeclaration()
|
||||
?.getFirstDescendantByKind(SyntaxKind.ArrayLiteralExpression);
|
||||
if (!initializer) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const { path, entry } of routes) {
|
||||
initializer.addElement(`route('${path}', '${entry}')`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the prerender method from the config
|
||||
*/
|
||||
function getPrerenderMethod(sourceFile: SourceFile): MethodDeclaration | null {
|
||||
return (
|
||||
sourceFile
|
||||
.getDefaultExportSymbol()
|
||||
?.getValueDeclaration()
|
||||
?.asKind(SyntaxKind.VariableDeclaration)
|
||||
?.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression)
|
||||
?.getProperty('prerender')
|
||||
?.asKind(SyntaxKind.MethodDeclaration) ?? null
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { IndentationText, Project, QuoteKind } from 'ts-morph';
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
const project = new Project({
|
||||
manipulationSettings: {
|
||||
indentationText: IndentationText.TwoSpaces,
|
||||
quoteKind: QuoteKind.Single,
|
||||
},
|
||||
});
|
||||
|
||||
export async function createSourceFile(path: string) {
|
||||
return project.createSourceFile(path, (await fs.readFile(path)).toString(), {
|
||||
overwrite: true,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { CallExpression, SourceFile, SyntaxKind } from 'ts-morph';
|
||||
|
||||
/**
|
||||
* Add path to the `pages` array in tanstack start vite config.
|
||||
*
|
||||
* If the `pages` property doesn't exist, create one.
|
||||
*/
|
||||
export function addTanstackPrerender(sourceFile: SourceFile, paths: string[]) {
|
||||
const optionsArg = getTanstackStartCall(sourceFile)
|
||||
?.getArguments()[0]
|
||||
?.asKind(SyntaxKind.ObjectLiteralExpression);
|
||||
if (!optionsArg) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pagesProperty = optionsArg
|
||||
.getProperty('pages')
|
||||
?.asKind(SyntaxKind.PropertyAssignment);
|
||||
|
||||
if (pagesProperty) {
|
||||
const initializer = pagesProperty.getInitializerIfKindOrThrow(
|
||||
SyntaxKind.ArrayLiteralExpression,
|
||||
);
|
||||
|
||||
const existingPaths = new Set<string>();
|
||||
for (const element of initializer.getElements()) {
|
||||
const value = element
|
||||
.asKind(SyntaxKind.ObjectLiteralExpression)
|
||||
?.getProperty('path')
|
||||
?.asKind(SyntaxKind.PropertyAssignment)
|
||||
?.getInitializer()
|
||||
?.getText();
|
||||
|
||||
if (value) {
|
||||
existingPaths.add(JSON.parse(value));
|
||||
}
|
||||
}
|
||||
|
||||
for (const path of paths) {
|
||||
if (existingPaths.has(path)) continue;
|
||||
initializer.addElement(toItem(path));
|
||||
}
|
||||
} else {
|
||||
optionsArg.addProperty(
|
||||
`pages: [\n${paths.map((path) => ` ${toItem(path)}`).join(',\n')}\n]`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function toItem(path: string) {
|
||||
return `{ path: '${path}' }`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the tanstackStart call expression
|
||||
*/
|
||||
function getTanstackStartCall(
|
||||
sourceFile: SourceFile,
|
||||
): CallExpression | undefined {
|
||||
const pluginsProperty = sourceFile
|
||||
.getDefaultExportSymbol()
|
||||
?.getValueDeclaration()
|
||||
?.getFirstDescendantByKind(SyntaxKind.ObjectLiteralExpression)
|
||||
?.getProperty('plugins')
|
||||
?.getFirstChildByKind(SyntaxKind.ArrayLiteralExpression);
|
||||
|
||||
if (!pluginsProperty) return;
|
||||
|
||||
for (const element of pluginsProperty.getElements()) {
|
||||
const expression = element.asKind(SyntaxKind.CallExpression);
|
||||
if (
|
||||
expression &&
|
||||
expression.getFirstChildByKind(SyntaxKind.Identifier)?.getText() ===
|
||||
'tanstackStart'
|
||||
) {
|
||||
return expression;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { RootProvider } from 'fumadocs-ui/provider/next';
|
||||
import './global.css';
|
||||
import { Inter } from 'next/font/google';
|
||||
import SearchDialog from '@/components/search';
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ['latin'],
|
||||
});
|
||||
|
||||
export default function Layout({ children }: LayoutProps<'/'>) {
|
||||
return (
|
||||
<html lang="en" className={inter.className} suppressHydrationWarning>
|
||||
<body className="flex flex-col min-h-screen">
|
||||
<RootProvider search={{ SearchDialog }}>{children}</RootProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import {
|
||||
isRouteErrorResponse,
|
||||
Links,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
} from 'react-router';
|
||||
import { RootProvider } from 'fumadocs-ui/provider/react-router';
|
||||
import type { Route } from './+types/root';
|
||||
import './app.css';
|
||||
import SearchDialog from '@/components/search';
|
||||
|
||||
export const links: Route.LinksFunction = () => [
|
||||
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
|
||||
{
|
||||
rel: 'preconnect',
|
||||
href: 'https://fonts.gstatic.com',
|
||||
crossOrigin: 'anonymous',
|
||||
},
|
||||
{
|
||||
rel: 'stylesheet',
|
||||
href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
|
||||
},
|
||||
];
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body className="flex flex-col min-h-screen">
|
||||
<RootProvider search={{ SearchDialog }}>{children}</RootProvider>
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||
let message = 'Oops!';
|
||||
let details = 'An unexpected error occurred.';
|
||||
let stack: string | undefined;
|
||||
|
||||
if (isRouteErrorResponse(error)) {
|
||||
message = error.status === 404 ? '404' : 'Error';
|
||||
details =
|
||||
error.status === 404
|
||||
? 'The requested page could not be found.'
|
||||
: error.statusText || details;
|
||||
} else if (import.meta.env.DEV && error && error instanceof Error) {
|
||||
details = error.message;
|
||||
stack = error.stack;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="pt-16 p-4 container mx-auto">
|
||||
<h1>{message}</h1>
|
||||
<p>{details}</p>
|
||||
{stack && (
|
||||
<pre className="w-full p-4 overflow-x-auto">
|
||||
<code>{stack}</code>
|
||||
</pre>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
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;
|
||||
@@ -1,76 +0,0 @@
|
||||
import {
|
||||
isRouteErrorResponse,
|
||||
Links,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
} from 'react-router';
|
||||
import { RootProvider } from 'fumadocs-ui/provider/react-router';
|
||||
import type { Route } from './+types/root';
|
||||
import './app.css';
|
||||
import SearchDialog from '@/components/search';
|
||||
|
||||
export const links: Route.LinksFunction = () => [
|
||||
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
|
||||
{
|
||||
rel: 'preconnect',
|
||||
href: 'https://fonts.gstatic.com',
|
||||
crossOrigin: 'anonymous',
|
||||
},
|
||||
{
|
||||
rel: 'stylesheet',
|
||||
href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap',
|
||||
},
|
||||
];
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body className="flex flex-col min-h-screen">
|
||||
<RootProvider search={{ SearchDialog }}>{children}</RootProvider>
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||
let message = 'Oops!';
|
||||
let details = 'An unexpected error occurred.';
|
||||
let stack: string | undefined;
|
||||
|
||||
if (isRouteErrorResponse(error)) {
|
||||
message = error.status === 404 ? '404' : 'Error';
|
||||
details =
|
||||
error.status === 404
|
||||
? 'The requested page could not be found.'
|
||||
: error.statusText || details;
|
||||
} else if (import.meta.env.DEV && error && error instanceof Error) {
|
||||
details = error.message;
|
||||
stack = error.stack;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="pt-16 p-4 container mx-auto">
|
||||
<h1>{message}</h1>
|
||||
<p>{details}</p>
|
||||
{stack && (
|
||||
<pre className="w-full p-4 overflow-x-auto">
|
||||
<code>{stack}</code>
|
||||
</pre>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
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;
|
||||
@@ -1,51 +0,0 @@
|
||||
import {
|
||||
createRootRoute,
|
||||
HeadContent,
|
||||
Outlet,
|
||||
Scripts,
|
||||
} from '@tanstack/react-router';
|
||||
import * as React from 'react';
|
||||
import appCss from '@/styles/app.css?url';
|
||||
import { RootProvider } from 'fumadocs-ui/provider/tanstack';
|
||||
import SearchDialog from '@/components/search';
|
||||
|
||||
export const Route = createRootRoute({
|
||||
head: () => ({
|
||||
meta: [
|
||||
{
|
||||
charSet: 'utf-8',
|
||||
},
|
||||
{
|
||||
name: 'viewport',
|
||||
content: 'width=device-width, initial-scale=1',
|
||||
},
|
||||
{
|
||||
title: 'Fumadocs on TanStack Start',
|
||||
},
|
||||
],
|
||||
links: [{ rel: 'stylesheet', href: appCss }],
|
||||
}),
|
||||
component: RootComponent,
|
||||
});
|
||||
|
||||
function RootComponent() {
|
||||
return (
|
||||
<RootDocument>
|
||||
<Outlet />
|
||||
</RootDocument>
|
||||
);
|
||||
}
|
||||
|
||||
function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html suppressHydrationWarning>
|
||||
<head>
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body className="flex flex-col min-h-screen">
|
||||
<RootProvider search={{ SearchDialog }}>{children}</RootProvider>
|
||||
<Scripts />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
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(),
|
||||
],
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
'use client';
|
||||
import type { ReactNode } from 'react';
|
||||
import { RootProvider } from 'fumadocs-ui/provider/waku';
|
||||
import SearchDialog from '@/components/search';
|
||||
|
||||
export function Provider({ children }: { children: ReactNode }) {
|
||||
return <RootProvider search={{ SearchDialog }}>{children}</RootProvider>;
|
||||
}
|
||||
@@ -5,9 +5,14 @@ import { createGetUrl, getSlugs } from 'fumadocs-core/source';
|
||||
const getUrl = createGetUrl('/docs');
|
||||
|
||||
export default {
|
||||
ssr: false,
|
||||
ssr: true,
|
||||
async prerender({ getStaticPaths }) {
|
||||
const paths: string[] = [...getStaticPaths()];
|
||||
const paths: string[] = [];
|
||||
const excluded: string[] = [];
|
||||
|
||||
for (const path of getStaticPaths()) {
|
||||
if (!excluded.includes(path)) paths.push(path);
|
||||
}
|
||||
|
||||
for await (const entry of glob('**/*.mdx', { cwd: 'content/docs' })) {
|
||||
paths.push(getUrl(getSlugs(entry)));
|
||||
|
||||
@@ -8,10 +8,11 @@ export default {
|
||||
ssr: true,
|
||||
async prerender({ getStaticPaths }) {
|
||||
const paths: string[] = [];
|
||||
// ignore dynamic document search
|
||||
const excluded = ['/api/search'];
|
||||
|
||||
for (const path of getStaticPaths()) {
|
||||
// ignore dynamic document search
|
||||
if (path === '/api/search') continue;
|
||||
paths.push(path);
|
||||
if (!excluded.includes(path)) paths.push(path);
|
||||
}
|
||||
|
||||
for await (const entry of glob('**/*.mdx', { cwd: 'content/docs' })) {
|
||||
|
||||
Generated
+7
-3
@@ -327,7 +327,7 @@ importers:
|
||||
version: 0.2.2(@content-collections/[email protected]([email protected]))([email protected]([email protected]))([email protected])
|
||||
'@content-collections/next':
|
||||
specifier: ^0.2.8
|
||||
version: 0.2.8(@content-collections/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]))
|
||||
version: 0.2.8(@content-collections/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))
|
||||
'@types/mdx':
|
||||
specifier: ^2.0.13
|
||||
version: 2.0.13
|
||||
@@ -1603,6 +1603,9 @@ importers:
|
||||
tinyexec:
|
||||
specifier: ^1.0.1
|
||||
version: 1.0.1
|
||||
ts-morph:
|
||||
specifier: ^27.0.2
|
||||
version: 27.0.2
|
||||
devDependencies:
|
||||
'@types/cross-spawn':
|
||||
specifier: ^6.0.6
|
||||
@@ -1636,7 +1639,7 @@ importers:
|
||||
version: 0.2.2(@content-collections/[email protected]([email protected]))([email protected]([email protected]))([email protected])
|
||||
'@content-collections/next':
|
||||
specifier: ^0.2.8
|
||||
version: 0.2.8(@content-collections/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]))
|
||||
version: 0.2.8(@content-collections/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))
|
||||
'@orama/core':
|
||||
specifier: ^1.2.13
|
||||
version: 1.2.13
|
||||
@@ -6329,6 +6332,7 @@ packages:
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-YI7mFs7iWc/VsGsh2aw6eAPD2cjzn1j+LKdYVk09x1CrdTWKYIHyd+dG5iQoN9//3hCDoZj8U6vKpZzEf5UARA==}
|
||||
cpu: [arm64, x64]
|
||||
os: [darwin, linux, win32]
|
||||
hasBin: true
|
||||
|
||||
@@ -12194,7 +12198,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@content-collections/[email protected](@content-collections/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]))':
|
||||
'@content-collections/[email protected](@content-collections/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))':
|
||||
dependencies:
|
||||
'@content-collections/core': 0.11.1([email protected])
|
||||
'@content-collections/integrations': 0.3.0(@content-collections/[email protected]([email protected]))
|
||||
|
||||
Reference in New Issue
Block a user