mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 10:55:57 +00:00
MDX: Support Tanstack Router/Start via createClientLoader
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-mdx': patch
|
||||
---
|
||||
|
||||
Support Tanstack Router/Start via `createClientLoader`
|
||||
@@ -19,4 +19,5 @@ yarn.lock
|
||||
/playwright/.cache/
|
||||
.tanstack
|
||||
|
||||
src/routeTree.gen.ts
|
||||
src/routeTree.gen.ts
|
||||
source.generated.ts
|
||||
@@ -5,6 +5,8 @@ description: This is another page
|
||||
|
||||
Hello World again!
|
||||
|
||||
## Installation
|
||||
|
||||
```npm
|
||||
npm i fumadocs-core fumadocs-ui
|
||||
```
|
||||
|
||||
@@ -9,22 +9,21 @@
|
||||
"start": "node .output/server/index.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fumadocs/mdx-remote": "workspace:*",
|
||||
"@tanstack/react-router": "^1.128.8",
|
||||
"@tanstack/react-router-devtools": "^1.128.8",
|
||||
"@tanstack/react-start": "^1.128.8",
|
||||
"fumadocs-core": "workspace:*",
|
||||
"fumadocs-mdx": "workspace:*",
|
||||
"fumadocs-ui": "workspace:*",
|
||||
"gray-matter": "^4.0.3",
|
||||
"lucide-static": "^0.525.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"vite": "^7.0.5",
|
||||
"zod": "^4.0.5"
|
||||
"vite": "^7.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@types/node": "^24.0.15",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig, defineDocs } from 'fumadocs-mdx/config';
|
||||
|
||||
export const docs = defineDocs({
|
||||
dir: 'content',
|
||||
});
|
||||
|
||||
export default defineConfig();
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
||||
|
||||
export function NotFound() {
|
||||
return (
|
||||
<HomeLayout
|
||||
nav={{
|
||||
title: 'Tanstack Start',
|
||||
}}
|
||||
className="text-center py-32 justify-center"
|
||||
>
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<h1 className="text-6xl font-bold text-fd-muted-foreground">404</h1>
|
||||
<h2 className="text-2xl font-semibold">Page Not Found</h2>
|
||||
<p className="text-fd-muted-foreground max-w-md">
|
||||
The page you are looking for might have been removed, had its name
|
||||
changed, or is temporarily unavailable.
|
||||
</p>
|
||||
<Link
|
||||
to="/"
|
||||
className="mt-4 px-4 py-2 rounded-lg bg-fd-primary text-fd-primary-foreground font-medium text-sm hover:opacity-90 transition-opacity"
|
||||
>
|
||||
Back to Home
|
||||
</Link>
|
||||
</div>
|
||||
</HomeLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,59 +1,9 @@
|
||||
import {
|
||||
loader,
|
||||
type MetaData,
|
||||
type PageData,
|
||||
type Source,
|
||||
type VirtualFile,
|
||||
} from 'fumadocs-core/source';
|
||||
import matter from 'gray-matter';
|
||||
import * as path from 'node:path';
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
import * as icons from 'lucide-static';
|
||||
|
||||
const files = Object.entries(
|
||||
import.meta.glob<true, 'raw'>('/content/**/*', {
|
||||
eager: true,
|
||||
query: '?raw',
|
||||
import: 'default',
|
||||
}),
|
||||
);
|
||||
|
||||
const virtualFiles: VirtualFile[] = files.flatMap(([file, content]) => {
|
||||
const ext = path.extname(file);
|
||||
const virtualPath = path.relative('content', path.join(process.cwd(), file));
|
||||
|
||||
if (ext === '.mdx' || ext === '.md') {
|
||||
const parsed = matter(content);
|
||||
|
||||
return {
|
||||
type: 'page',
|
||||
path: virtualPath,
|
||||
data: {
|
||||
...parsed.data,
|
||||
content: parsed.content,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (ext === '.json') {
|
||||
return {
|
||||
type: 'meta',
|
||||
path: virtualPath,
|
||||
data: JSON.parse(content),
|
||||
};
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
import { create, docs } from '../../source.generated';
|
||||
|
||||
export const source = loader({
|
||||
source: {
|
||||
files: virtualFiles,
|
||||
} as Source<{
|
||||
pageData: PageData & {
|
||||
content: string;
|
||||
};
|
||||
metaData: MetaData;
|
||||
}>,
|
||||
source: await create.sourceAsync(docs.doc, docs.meta),
|
||||
baseUrl: '/docs',
|
||||
// @ts-expect-error -- string
|
||||
icon(icon) {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { createRouter as createTanStackRouter } from '@tanstack/react-router';
|
||||
import { routeTree } from './routeTree.gen';
|
||||
import { NotFound } from '~/components/not-found';
|
||||
|
||||
export function createRouter() {
|
||||
return createTanStackRouter({
|
||||
routeTree,
|
||||
defaultPreload: 'intent',
|
||||
scrollRestoration: true,
|
||||
defaultNotFoundComponent: NotFound,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import { createServerFileRoute } from '@tanstack/react-start/server';
|
||||
import { createSearchAPI } from 'fumadocs-core/search/server';
|
||||
import { source } from '~/lib/source';
|
||||
import { structure } from 'fumadocs-core/mdx-plugins';
|
||||
import { createFromSource } from 'fumadocs-core/search/server';
|
||||
|
||||
const server = createSearchAPI('advanced', {
|
||||
indexes: source.getPages().map((page) => ({
|
||||
id: page.url,
|
||||
url: page.url,
|
||||
title: page.data.title!,
|
||||
structuredData: structure(page.data.content),
|
||||
})),
|
||||
});
|
||||
const server = createFromSource(source);
|
||||
|
||||
export const ServerRoute = createServerFileRoute('/api/search').methods({
|
||||
GET: async ({ request }) => server.GET(request),
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
import { createFileRoute, notFound } from '@tanstack/react-router';
|
||||
import { executeMdxSync } from '@fumadocs/mdx-remote/client';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import { createServerFn } from '@tanstack/react-start';
|
||||
import { source } from '~/lib/source';
|
||||
import type { PageTree } from 'fumadocs-core/server';
|
||||
import { useMemo } from 'react';
|
||||
import { docs } from '../../../source.generated';
|
||||
import {
|
||||
DocsBody,
|
||||
DocsDescription,
|
||||
DocsPage,
|
||||
DocsTitle,
|
||||
} from 'fumadocs-ui/page';
|
||||
import { createServerFn } from '@tanstack/react-start';
|
||||
import { createCompiler } from '@fumadocs/mdx-remote';
|
||||
import { source } from '~/lib/source';
|
||||
import path from 'node:path';
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
import type { PageTree } from 'fumadocs-core/server';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const compiler = createCompiler();
|
||||
import { createClientLoader } from 'fumadocs-mdx/runtime/vite';
|
||||
|
||||
export const Route = createFileRoute('/docs/$')({
|
||||
component: Page,
|
||||
loader: async ({ params }) => {
|
||||
const slugs = (params._splat ?? '').split('/');
|
||||
return loader({ data: slugs });
|
||||
const data = await loader({ data: params._splat?.split('/') ?? [] });
|
||||
await clientLoader.preload(data.path);
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -34,59 +32,19 @@ const loader = createServerFn({
|
||||
const page = source.getPage(slugs);
|
||||
if (!page) throw notFound();
|
||||
|
||||
const { content, ...rest } = page.data;
|
||||
const compiled = await compiler.compileFile({
|
||||
path: path.resolve('content', page.path),
|
||||
value: content,
|
||||
});
|
||||
|
||||
return {
|
||||
tree: source.pageTree as object,
|
||||
...rest,
|
||||
compiled: compiled.toString(),
|
||||
path: page.path,
|
||||
};
|
||||
});
|
||||
|
||||
function Page() {
|
||||
const data = Route.useLoaderData();
|
||||
const {
|
||||
default: MDX,
|
||||
toc,
|
||||
tree,
|
||||
} = useMemo(() => {
|
||||
const result = executeMdxSync(data.compiled);
|
||||
|
||||
function traverse(folder: PageTree.Folder | PageTree.Root) {
|
||||
for (const node of folder.children) {
|
||||
if (node.type === 'page' && typeof node.icon === 'string') {
|
||||
node.icon = (
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: node.icon,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (node.type === 'folder') traverse(node);
|
||||
}
|
||||
}
|
||||
|
||||
const tree = structuredClone(data.tree) as PageTree.Root;
|
||||
traverse(tree);
|
||||
return { ...result, tree };
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<DocsLayout
|
||||
tree={tree}
|
||||
nav={{
|
||||
title: 'Fumadocs Tanstack',
|
||||
}}
|
||||
>
|
||||
const clientLoader = createClientLoader(docs.doc, {
|
||||
id: 'docs',
|
||||
component({ toc, frontmatter, default: MDX }) {
|
||||
return (
|
||||
<DocsPage toc={toc}>
|
||||
<DocsTitle>{data.title}</DocsTitle>
|
||||
<DocsDescription>{data.description}</DocsDescription>
|
||||
<DocsTitle>{frontmatter.title}</DocsTitle>
|
||||
<DocsDescription>{frontmatter.description}</DocsDescription>
|
||||
<DocsBody>
|
||||
<MDX
|
||||
components={{
|
||||
@@ -95,6 +53,53 @@ function Page() {
|
||||
/>
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
function Page() {
|
||||
const data = Route.useLoaderData();
|
||||
const Content = clientLoader.getComponent(data.path);
|
||||
const tree = useMemo(
|
||||
() => transformPageTree(data.tree as PageTree.Folder),
|
||||
[data.tree],
|
||||
);
|
||||
|
||||
return (
|
||||
<DocsLayout
|
||||
tree={tree}
|
||||
nav={{
|
||||
title: 'Fumadocs Tanstack',
|
||||
}}
|
||||
>
|
||||
<Content />
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
function transformPageTree(tree: PageTree.Folder): PageTree.Folder {
|
||||
function page(item: PageTree.Item) {
|
||||
if (typeof item.icon !== 'string') return item;
|
||||
|
||||
return {
|
||||
...item,
|
||||
icon: (
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: item.icon,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...tree,
|
||||
index: tree.index ? page(tree.index) : undefined,
|
||||
children: tree.children.map((item) => {
|
||||
if (item.type === 'page') return page(item);
|
||||
if (item.type === 'folder') return transformPageTree(item);
|
||||
return item;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
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(),
|
||||
tanstackStart({
|
||||
customViteReactPlugin: true,
|
||||
}),
|
||||
react(),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -119,13 +119,11 @@ export function defineDocs<
|
||||
docs: defineCollections({
|
||||
type: 'doc',
|
||||
dir,
|
||||
files: ['**/*.{md,mdx}'],
|
||||
schema: frontmatterSchema as any,
|
||||
...options?.docs,
|
||||
}),
|
||||
meta: defineCollections({
|
||||
type: 'meta',
|
||||
files: ['**/*.{json,yaml}'],
|
||||
dir,
|
||||
schema: metaSchema as any,
|
||||
...options?.meta,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as path from 'node:path';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import { glob } from 'tinyglobby';
|
||||
import { getTypeFromPath } from '@/utils/get-type-from-path';
|
||||
import type { LoadedConfig } from '@/utils/config';
|
||||
import type { DocCollection, MetaCollection } from '@/config';
|
||||
import { validate } from '@/utils/schema';
|
||||
@@ -15,6 +14,7 @@ import {
|
||||
type ImportPathConfig,
|
||||
toImportPath,
|
||||
} from '@/utils/import-formatter';
|
||||
import { getGlobPatterns, isFileSupported } from '@/utils/collections';
|
||||
|
||||
async function readFileWithCache(file: string): Promise<string> {
|
||||
const cached = fileCache.read<string>('read-file', file);
|
||||
@@ -186,16 +186,17 @@ async function getCollectionFiles(
|
||||
const dirs = Array.isArray(collection.dir)
|
||||
? collection.dir
|
||||
: [collection.dir];
|
||||
const patterns = getGlobPatterns(collection);
|
||||
|
||||
await Promise.all(
|
||||
dirs.map(async (dir) => {
|
||||
const result = await glob(collection.files ?? '**/*', {
|
||||
const result = await glob(patterns, {
|
||||
cwd: path.resolve(dir),
|
||||
absolute: true,
|
||||
});
|
||||
|
||||
for (const item of result) {
|
||||
if (getTypeFromPath(item) !== collection.type) continue;
|
||||
if (!isFileSupported(item, collection)) continue;
|
||||
|
||||
files.set(item, {
|
||||
path: path.relative(dir, item),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TableOfContents } from 'fumadocs-core/server';
|
||||
import { type FC, lazy, type LazyExoticComponent, type ReactNode } from 'react';
|
||||
import { createElement, type FC, lazy, type ReactNode } from 'react';
|
||||
import type { MDXProps } from 'mdx/types';
|
||||
import type { StructuredData } from 'fumadocs-core/mdx-plugins';
|
||||
import type { DocCollection, DocsCollection, MetaCollection } from '@/config';
|
||||
@@ -175,24 +175,93 @@ export function fromConfig<Config>(): {
|
||||
};
|
||||
}
|
||||
|
||||
type ClientLoader<Props> = Record<
|
||||
string,
|
||||
LazyExoticComponent<(props: Props) => ReactNode>
|
||||
>;
|
||||
export interface ClientLoaderOptions<Frontmatter, Props> {
|
||||
/**
|
||||
* Loader ID (usually your collection name)
|
||||
*
|
||||
* The code splitting strategy of frameworks like Tanstack Start may duplicate `createClientLoader()` into different chunks.
|
||||
*
|
||||
* We use loader ID to share cache between multiple instances of client loader.
|
||||
*
|
||||
* @defaultValue ''
|
||||
*/
|
||||
id?: string;
|
||||
|
||||
export function toClientRenderer<Frontmatter, Props extends object = object>(
|
||||
component: (loaded: CompiledMDXFile<Frontmatter>, props: Props) => ReactNode;
|
||||
}
|
||||
|
||||
export interface ClientLoader<Frontmatter, Props> {
|
||||
preload: (path: string) => Promise<CompiledMDXFile<Frontmatter>>;
|
||||
/**
|
||||
* Get a component that renders content with `React.lazy`
|
||||
*/
|
||||
getComponent: (path: string) => FC<Props>;
|
||||
}
|
||||
|
||||
const loaderStore = new Map<
|
||||
string,
|
||||
{
|
||||
preloaded: Map<string, CompiledMDXFile<any>>;
|
||||
}
|
||||
>();
|
||||
|
||||
export function createClientLoader<Frontmatter, Props = object>(
|
||||
files: Record<string, () => Promise<CompiledMDXFile<Frontmatter>>>,
|
||||
renderer: (loaded: CompiledMDXFile<Frontmatter>, props: Props) => ReactNode,
|
||||
): ClientLoader<Props> {
|
||||
const loader: ClientLoader<Props> = {};
|
||||
options: ClientLoaderOptions<Frontmatter, Props>,
|
||||
): ClientLoader<Frontmatter, Props> {
|
||||
const { id = '', component } = options;
|
||||
const store = loaderStore.get(id) ?? {
|
||||
preloaded: new Map(),
|
||||
};
|
||||
loaderStore.set(id, store);
|
||||
|
||||
let renderer;
|
||||
|
||||
return {
|
||||
async preload(path) {
|
||||
const loaded = await files[path]();
|
||||
store.preloaded.set(path, loaded);
|
||||
return loaded;
|
||||
},
|
||||
getComponent(path) {
|
||||
renderer ??= toClientRenderer(files, component, {
|
||||
cache: store.preloaded,
|
||||
});
|
||||
return renderer[path];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface ClientRendererOptions<Frontmatter> {
|
||||
cache?: Map<string, CompiledMDXFile<Frontmatter>>;
|
||||
}
|
||||
|
||||
type ClientRenderer<Props> = Record<string, FC<Props>>;
|
||||
|
||||
export function toClientRenderer<Frontmatter, Props = object>(
|
||||
files: Record<string, () => Promise<CompiledMDXFile<Frontmatter>>>,
|
||||
component: (loaded: CompiledMDXFile<Frontmatter>, props: Props) => ReactNode,
|
||||
options: ClientRendererOptions<Frontmatter> = {},
|
||||
): ClientRenderer<Props> {
|
||||
const { cache } = options;
|
||||
const renderer: ClientRenderer<Props> = {};
|
||||
|
||||
for (const k in files) {
|
||||
loader[k] = lazy(async () => {
|
||||
const OnDemand = lazy(async () => {
|
||||
const loaded = await files[k]();
|
||||
|
||||
return { default: (props) => renderer(loaded, props) };
|
||||
return { default: (props) => component(loaded, props) };
|
||||
});
|
||||
|
||||
if (cache) {
|
||||
renderer[k] = (props: Props) => {
|
||||
const cached = cache.get(k);
|
||||
if (!cached) return createElement(OnDemand, props);
|
||||
return component(cached, props);
|
||||
};
|
||||
} else {
|
||||
renderer[k] = OnDemand;
|
||||
}
|
||||
}
|
||||
|
||||
return loader;
|
||||
return renderer;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { DocCollection, MetaCollection } from '@/config/define';
|
||||
|
||||
export function getSupportedFormats(
|
||||
collection: DocCollection | MetaCollection,
|
||||
) {
|
||||
return {
|
||||
doc: ['mdx', 'md'],
|
||||
meta: ['json', 'yaml'],
|
||||
}[collection.type];
|
||||
}
|
||||
|
||||
export function getGlobPatterns(
|
||||
collection: MetaCollection | DocCollection,
|
||||
): string[] {
|
||||
if (collection.files) return collection.files;
|
||||
return [`**/*.{${getSupportedFormats(collection).join(',')}}`];
|
||||
}
|
||||
|
||||
export function isFileSupported(
|
||||
filePath: string,
|
||||
collection: MetaCollection | DocCollection,
|
||||
) {
|
||||
for (const format of getSupportedFormats(collection)) {
|
||||
if (filePath.endsWith(`.${format}`)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { extname } from 'node:path';
|
||||
|
||||
const docTypes = ['.mdx', '.md'];
|
||||
const metaTypes = ['.json', '.yaml'];
|
||||
|
||||
export function getTypeFromPath(path: string): 'doc' | 'meta' | undefined {
|
||||
const ext = extname(path);
|
||||
|
||||
if (docTypes.includes(ext)) return 'doc';
|
||||
if (metaTypes.includes(ext)) return 'meta';
|
||||
}
|
||||
@@ -54,3 +54,10 @@ export function toImportPath(file: string, config: ImportPathConfig): string {
|
||||
|
||||
return importPath.replaceAll(path.sep, '/');
|
||||
}
|
||||
|
||||
export function ident(code: string, tab: number = 1) {
|
||||
return code
|
||||
.split('\n')
|
||||
.map((v) => ' '.repeat(tab) + v)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
@@ -6,11 +6,17 @@ import { countLines } from '@/utils/count-lines';
|
||||
import { fumaMatter } from '@/utils/fuma-matter';
|
||||
import { validate, ValidationError } from '@/utils/schema';
|
||||
import { z } from 'zod';
|
||||
import { toImportPath } from '@/utils/import-formatter';
|
||||
import type { DocCollection, DocsCollection, MetaCollection } from '@/config';
|
||||
import { ident, toImportPath } from '@/utils/import-formatter';
|
||||
import type {
|
||||
AnyCollection,
|
||||
DocCollection,
|
||||
DocsCollection,
|
||||
MetaCollection,
|
||||
} from '@/config';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { load } from 'js-yaml';
|
||||
import { getGlobPatterns } from '@/utils/collections';
|
||||
|
||||
const fileRegex = /\.(md|mdx)$/;
|
||||
const onlySchema = z.literal(['frontmatter', 'all']);
|
||||
@@ -58,94 +64,31 @@ export default function mdx(
|
||||
`export const create = fromConfig<typeof Config>();`,
|
||||
];
|
||||
|
||||
function filesToGlob(files: string[]) {
|
||||
return files.map((file) => {
|
||||
if (file.startsWith('./')) return file;
|
||||
if (file.startsWith('/')) return `.${file}`;
|
||||
function docs(name: string, collection: DocsCollection) {
|
||||
const args = [
|
||||
ident(`doc: ${generateGlob(name, collection.docs)}`),
|
||||
ident(`meta: ${generateGlob(name, collection.meta)}`),
|
||||
].join(',\n');
|
||||
|
||||
return `./${file}`;
|
||||
});
|
||||
return `export const ${name} = create.docs("${name}", {\n${args}\n});`;
|
||||
}
|
||||
|
||||
function docs(name: string, dir: string, collection: DocsCollection) {
|
||||
const docFiles = collection.docs.files
|
||||
? filesToGlob(collection.docs.files)
|
||||
: ['./**/*.{mdx,md}'];
|
||||
const metaFiles = collection.meta.files
|
||||
? filesToGlob(collection.meta.files)
|
||||
: ['./**/*.{yaml,json}'];
|
||||
|
||||
return `export const ${name} = create.docs('${name}', {
|
||||
doc: import.meta.glob(${JSON.stringify(docFiles)}, {
|
||||
query: {
|
||||
collection: '${name}',
|
||||
},
|
||||
base: '${dir}',
|
||||
}),
|
||||
meta: import.meta.glob(${JSON.stringify(metaFiles)}, {
|
||||
query: {
|
||||
collection: '${name}',
|
||||
},
|
||||
base: '${dir}',
|
||||
import: 'default',
|
||||
}),
|
||||
});`;
|
||||
function doc(name: string, collection: DocCollection) {
|
||||
return `export const ${name} = create.doc("${name}", ${generateGlob(name, collection)});`;
|
||||
}
|
||||
|
||||
function doc(name: string, dir: string, collection: DocCollection) {
|
||||
const files = collection.files
|
||||
? filesToGlob(collection.files)
|
||||
: ['./**/*.{mdx,md}'];
|
||||
|
||||
return `export const ${name} = create.doc(
|
||||
'${name}',
|
||||
import.meta.glob(${JSON.stringify(files)}, {
|
||||
query: {
|
||||
collection: '${name}',
|
||||
},
|
||||
base: '${dir}',
|
||||
}),
|
||||
);`;
|
||||
}
|
||||
|
||||
function meta(name: string, dir: string, collection: MetaCollection) {
|
||||
const files = collection.files
|
||||
? filesToGlob(collection.files)
|
||||
: ['./**/*.{yaml,json}'];
|
||||
|
||||
return `export const ${name} = create.meta(
|
||||
'${name}',
|
||||
import.meta.glob(${JSON.stringify(files)}, {
|
||||
query: {
|
||||
collection: '${name}',
|
||||
},
|
||||
base: '${dir}',
|
||||
import: 'default',
|
||||
}),
|
||||
);`;
|
||||
function meta(name: string, collection: MetaCollection) {
|
||||
return `export const ${name} = create.meta("${name}", ${generateGlob(name, collection)});`;
|
||||
}
|
||||
|
||||
for (const [name, collection] of loaded.collections.entries()) {
|
||||
let dir = collection.dir;
|
||||
|
||||
if (Array.isArray(dir) && dir.length === 1) {
|
||||
dir = dir[0];
|
||||
} else if (Array.isArray(dir)) {
|
||||
throw new Error(
|
||||
`[Fumadocs MDX] Vite Plugin doesn't support multiple \`dir\` for a collection at the moment.`,
|
||||
);
|
||||
}
|
||||
if (!dir.startsWith('./') && !dir.startsWith('/')) {
|
||||
dir = '/' + dir;
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
if (collection.type === 'docs') {
|
||||
lines.push(docs(name, dir, collection));
|
||||
lines.push(docs(name, collection));
|
||||
} else if (collection.type === 'meta') {
|
||||
lines.push(meta(name, dir, collection));
|
||||
lines.push(meta(name, collection));
|
||||
} else {
|
||||
lines.push(doc(name, dir, collection));
|
||||
lines.push(doc(name, collection));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,3 +223,49 @@ export default function mdx(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function generateGlob(
|
||||
name: string,
|
||||
collection: MetaCollection | DocCollection,
|
||||
) {
|
||||
const patterns = mapGlobPatterns(getGlobPatterns(collection));
|
||||
const options: Record<string, unknown> = {
|
||||
query: {
|
||||
collection: name,
|
||||
},
|
||||
base: getGlobBase(collection),
|
||||
};
|
||||
|
||||
if (collection.type === 'meta') {
|
||||
options.import = 'default';
|
||||
}
|
||||
|
||||
return `import.meta.glob(${JSON.stringify(patterns)}, ${JSON.stringify(options, null, 2)})`;
|
||||
}
|
||||
|
||||
function mapGlobPatterns(patterns: string[]) {
|
||||
return patterns.map((file) => {
|
||||
if (file.startsWith('./')) return file;
|
||||
if (file.startsWith('/')) return `.${file}`;
|
||||
|
||||
return `./${file}`;
|
||||
});
|
||||
}
|
||||
|
||||
function getGlobBase(collection: AnyCollection) {
|
||||
let dir = collection.dir;
|
||||
|
||||
if (Array.isArray(dir)) {
|
||||
if (dir.length !== 1)
|
||||
throw new Error(
|
||||
`[Fumadocs MDX] Vite Plugin doesn't support multiple \`dir\` for a collection at the moment.`,
|
||||
);
|
||||
|
||||
dir = dir[0];
|
||||
}
|
||||
|
||||
if (!dir.startsWith('./') && !dir.startsWith('/')) {
|
||||
return '/' + dir;
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
Generated
+9
-12
@@ -279,7 +279,7 @@ importers:
|
||||
version: 0.2.2(@content-collections/[email protected]([email protected]))([email protected])([email protected]([email protected]))([email protected])
|
||||
'@content-collections/next':
|
||||
specifier: ^0.2.6
|
||||
version: 0.2.6(@content-collections/[email protected]([email protected]))([email protected](@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))
|
||||
version: 0.2.6(@content-collections/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]))
|
||||
'@types/mdx':
|
||||
specifier: ^2.0.13
|
||||
version: 2.0.13
|
||||
@@ -638,9 +638,6 @@ importers:
|
||||
|
||||
examples/tanstack-start:
|
||||
dependencies:
|
||||
'@fumadocs/mdx-remote':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/mdx-remote
|
||||
'@tanstack/react-router':
|
||||
specifier: ^1.128.8
|
||||
version: 1.128.8([email protected]([email protected]))([email protected])
|
||||
@@ -653,12 +650,12 @@ importers:
|
||||
fumadocs-core:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/core
|
||||
fumadocs-mdx:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/mdx
|
||||
fumadocs-ui:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/ui
|
||||
gray-matter:
|
||||
specifier: ^4.0.3
|
||||
version: 4.0.3
|
||||
lucide-static:
|
||||
specifier: ^0.525.0
|
||||
version: 0.525.0
|
||||
@@ -674,9 +671,6 @@ importers:
|
||||
vite:
|
||||
specifier: ^7.0.5
|
||||
version: 7.0.5(@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected])
|
||||
zod:
|
||||
specifier: ^4.0.5
|
||||
version: 4.0.5
|
||||
devDependencies:
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.1.11
|
||||
@@ -690,6 +684,9 @@ importers:
|
||||
'@types/react-dom':
|
||||
specifier: ^19.0.3
|
||||
version: 19.1.6(@types/[email protected])
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^4.7.0
|
||||
version: 4.7.0([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))
|
||||
tailwindcss:
|
||||
specifier: ^4.1.11
|
||||
version: 4.1.11
|
||||
@@ -926,7 +923,7 @@ importers:
|
||||
version: 0.2.2(@content-collections/[email protected]([email protected]))([email protected])([email protected]([email protected]))([email protected])
|
||||
'@content-collections/next':
|
||||
specifier: ^0.2.6
|
||||
version: 0.2.6(@content-collections/[email protected]([email protected]))([email protected](@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))
|
||||
version: 0.2.6(@content-collections/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]))
|
||||
'@react-router/dev':
|
||||
specifier: ^7.7.0
|
||||
version: 7.7.0(@react-router/[email protected]([email protected]([email protected]([email protected]))([email protected]))([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected])([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))([email protected])
|
||||
@@ -10573,7 +10570,7 @@ snapshots:
|
||||
- acorn
|
||||
- supports-color
|
||||
|
||||
'@content-collections/[email protected](@content-collections/[email protected]([email protected]))([email protected](@opentelemetry/[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]))([email protected]))':
|
||||
dependencies:
|
||||
'@content-collections/core': 0.10.0([email protected])
|
||||
'@content-collections/integrations': 0.2.1(@content-collections/[email protected]([email protected]))
|
||||
|
||||
Reference in New Issue
Block a user