mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
Core & OpenAPI: Publish plugins API on loader(), deprecate storage & page tree transformers
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'fumadocs-openapi': patch
|
||||
'fumadocs-core': patch
|
||||
---
|
||||
|
||||
Publish `plugins` API on `loader()`, deprecate storage & page tree transformers
|
||||
@@ -46,14 +46,12 @@ export const openapi = createOpenAPI({
|
||||
```
|
||||
|
||||
```ts title="lib/source.ts"
|
||||
import { transformerOpenAPI } from 'fumadocs-openapi/server';
|
||||
import { openapiPlugin } from 'fumadocs-openapi/server';
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
|
||||
export const source = loader({
|
||||
// [!code ++:3] adds a badge to each page item in page tree
|
||||
pageTree: {
|
||||
transformers: [transformerOpenAPI()],
|
||||
},
|
||||
// [!code ++] adds a badge to each page item in page tree
|
||||
plugins: [openapiPlugin()],
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createMDXSource } from 'fumadocs-mdx';
|
||||
import type { InferMetaType, InferPageType } from 'fumadocs-core/source';
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
import { icons } from 'lucide-react';
|
||||
import { transformerOpenAPI } from 'fumadocs-openapi/server';
|
||||
import { openapiPlugin } from 'fumadocs-openapi/server';
|
||||
import { createElement } from 'react';
|
||||
import { blog as blogPosts, docs } from '@/.source';
|
||||
|
||||
@@ -13,9 +13,7 @@ export const source = loader({
|
||||
return createElement(icons[icon as keyof typeof icons]);
|
||||
},
|
||||
source: docs.toFumadocsSource(),
|
||||
pageTree: {
|
||||
transformers: [transformerOpenAPI()],
|
||||
},
|
||||
plugins: [openapiPlugin()],
|
||||
});
|
||||
|
||||
export const blog = loader({
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { createMDXSource } from 'fumadocs-mdx';
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
import { transformerOpenAPI } from 'fumadocs-openapi/server';
|
||||
import { openapiPlugin } from 'fumadocs-openapi/server';
|
||||
import { docs, meta } from '@/.source';
|
||||
|
||||
export const source = loader({
|
||||
baseUrl: '/docs',
|
||||
source: createMDXSource(docs, meta),
|
||||
pageTree: {
|
||||
transformers: [transformerOpenAPI()],
|
||||
},
|
||||
plugins: [openapiPlugin()],
|
||||
});
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
import { create, docs } from '../../source.generated.js';
|
||||
import { transformerOpenAPI } from 'fumadocs-openapi/server';
|
||||
import { openapiPlugin } from 'fumadocs-openapi/server';
|
||||
|
||||
export const source = loader({
|
||||
source: await create.sourceAsync(docs.doc, docs.meta),
|
||||
baseUrl: '/docs',
|
||||
pageTree: {
|
||||
transformers: [transformerOpenAPI()],
|
||||
},
|
||||
plugins: [openapiPlugin()],
|
||||
});
|
||||
|
||||
@@ -23,3 +23,4 @@ export {
|
||||
parseFilePath,
|
||||
parseFolderPath,
|
||||
} from './path';
|
||||
export type * from './plugins';
|
||||
|
||||
@@ -3,10 +3,11 @@ import { FileSystem } from './file-system';
|
||||
import { type VirtualFile } from '@/source/loader';
|
||||
import { basename, dirname, joinPath, slash, splitPath } from '@/source/path';
|
||||
import type { I18nConfig } from '@/i18n';
|
||||
import type { LoaderPlugin } from '@/source/plugins';
|
||||
|
||||
export interface LoadOptions {
|
||||
transformers?: Transformer[];
|
||||
buildFile: (file: VirtualFile) => MetaFile | PageFile;
|
||||
plugins?: LoaderPlugin[];
|
||||
}
|
||||
|
||||
export type ContentStorage<
|
||||
@@ -31,10 +32,7 @@ export interface PageFile<Data extends PageData = PageData> {
|
||||
data: Data;
|
||||
}
|
||||
|
||||
export type Transformer = (context: {
|
||||
storage: ContentStorage;
|
||||
options: LoadOptions;
|
||||
}) => void;
|
||||
export type Transformer = (context: { storage: ContentStorage }) => void;
|
||||
|
||||
function isLocaleValid(locale: string) {
|
||||
return locale.length > 0 && !/\d+/.test(locale);
|
||||
@@ -76,7 +74,7 @@ export function loadFiles(
|
||||
options: LoadOptions,
|
||||
i18n: I18nConfig,
|
||||
): Record<string, ContentStorage> {
|
||||
const { buildFile, transformers = [] } = options;
|
||||
const { buildFile, plugins = [] } = options;
|
||||
const parser = parsers[i18n.parser ?? 'dot'];
|
||||
const storages: Record<string, ContentStorage> = {};
|
||||
const normalized = files.map((file) =>
|
||||
@@ -107,11 +105,12 @@ export function loadFiles(
|
||||
if (locale === lang) storage.write(path, item);
|
||||
}
|
||||
|
||||
for (const transformer of transformers) {
|
||||
transformer({
|
||||
storage,
|
||||
options,
|
||||
});
|
||||
const context = {
|
||||
storage,
|
||||
};
|
||||
|
||||
for (const plugin of plugins) {
|
||||
plugin.transformStorage?.(context);
|
||||
}
|
||||
|
||||
storages[lang] = storage;
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
loadFiles,
|
||||
type MetaFile,
|
||||
type PageFile,
|
||||
type Transformer,
|
||||
} from './load-files';
|
||||
import type { MetaData, PageData, UrlFn } from './types';
|
||||
import { type BaseOptions, createPageTreeBuilder } from './page-tree/builder';
|
||||
@@ -18,6 +17,13 @@ import {
|
||||
parseFilePath,
|
||||
} from './path';
|
||||
import { normalizeUrl } from '@/utils/normalize-url';
|
||||
import { buildPlugins, type LoaderPlugins } from '@/source/plugins';
|
||||
import { slugsPlugin } from '@/source/plugins/slugs';
|
||||
import {
|
||||
compatPlugin,
|
||||
type LegacyLoaderOptions,
|
||||
type LegacyPageTreeOptions,
|
||||
} from '@/source/plugins/compat';
|
||||
|
||||
export interface LoaderConfig {
|
||||
source: SourceConfig;
|
||||
@@ -29,29 +35,29 @@ export interface SourceConfig {
|
||||
metaData: MetaData;
|
||||
}
|
||||
|
||||
export interface LoaderOptions<
|
||||
T extends SourceConfig = SourceConfig,
|
||||
export type LoaderOptions<
|
||||
Config extends SourceConfig = SourceConfig,
|
||||
I18n extends I18nConfig | undefined = I18nConfig | undefined,
|
||||
> {
|
||||
baseUrl: string;
|
||||
|
||||
icon?: NonNullable<BaseOptions['resolveIcon']>;
|
||||
slugs?: (info: FileInfo) => string[];
|
||||
url?: UrlFn;
|
||||
|
||||
source: Source<T> | Source<T>[];
|
||||
// TODO: solve the overlaps between page tree transformers & storage transformers, like a plugin API?
|
||||
transformers?: Transformer[];
|
||||
|
||||
/**
|
||||
* Additional options for page tree builder
|
||||
*/
|
||||
pageTree?: Partial<BaseOptions<T['pageData'], T['metaData']>>;
|
||||
|
||||
> = NoInfer<BaseLoaderOptions<Config>> & {
|
||||
source: Source<Config> | Source<Config>[];
|
||||
/**
|
||||
* Configure i18n
|
||||
*/
|
||||
i18n?: I18n;
|
||||
};
|
||||
|
||||
interface BaseLoaderOptions<Config extends SourceConfig>
|
||||
extends LegacyLoaderOptions {
|
||||
baseUrl: string;
|
||||
icon?: NonNullable<BaseOptions['resolveIcon']>;
|
||||
slugs?: (info: FileInfo) => string[];
|
||||
url?: UrlFn;
|
||||
/**
|
||||
* Additional options for page tree builder
|
||||
*/
|
||||
pageTree?: Partial<BaseOptions> &
|
||||
LegacyPageTreeOptions<Config['pageData'], Config['metaData']>;
|
||||
plugins?: LoaderPlugins<Config['pageData'], Config['metaData']>;
|
||||
}
|
||||
|
||||
export interface Source<Config extends SourceConfig> {
|
||||
@@ -281,50 +287,19 @@ function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
|
||||
console.warn('`loader()` now requires a `baseUrl` option to be defined.');
|
||||
}
|
||||
|
||||
const {
|
||||
source,
|
||||
baseUrl = '/',
|
||||
i18n,
|
||||
slugs: slugsFn,
|
||||
url: urlFn,
|
||||
transformers = [],
|
||||
} = options;
|
||||
const { source, baseUrl = '/', i18n, slugs: slugsFn, url: urlFn } = options;
|
||||
const getUrl: UrlFn = urlFn
|
||||
? (...args) => normalizeUrl(urlFn(...args))
|
||||
: createGetUrl(baseUrl, i18n);
|
||||
const defaultLanguage = i18n?.defaultLanguage ?? '';
|
||||
const files = loadSource(source);
|
||||
|
||||
const transformerSlugs: Transformer = ({ storage }) => {
|
||||
const indexFiles = new Set<string>();
|
||||
const taken = new Set<string>();
|
||||
// for custom slugs function, don't handle conflicting cases like `dir/index.mdx` vs `dir.mdx`
|
||||
const autoIndex = slugsFn === undefined;
|
||||
|
||||
for (const path of storage.getFiles()) {
|
||||
const file = storage.read(path);
|
||||
if (!file || file.format !== 'page' || file.slugs) continue;
|
||||
|
||||
if (isIndex(path) && autoIndex) {
|
||||
indexFiles.add(path);
|
||||
continue;
|
||||
}
|
||||
|
||||
file.slugs = slugsFn ? slugsFn(parseFilePath(path)) : getSlugs(path);
|
||||
|
||||
const key = file.slugs.join('/');
|
||||
if (taken.has(key)) throw new Error('Duplicated slugs');
|
||||
taken.add(key);
|
||||
}
|
||||
|
||||
for (const path of indexFiles) {
|
||||
const file = storage.read(path);
|
||||
if (file?.format !== 'page') continue;
|
||||
|
||||
file.slugs = getSlugs(path);
|
||||
if (taken.has(file.slugs.join('/'))) file.slugs.push('index');
|
||||
}
|
||||
};
|
||||
const plugins = [slugsPlugin(slugsFn)];
|
||||
if (options.plugins) {
|
||||
plugins.push(...buildPlugins(options.plugins));
|
||||
}
|
||||
if (options.pageTree) {
|
||||
plugins.push(...compatPlugin(options, options.pageTree));
|
||||
}
|
||||
|
||||
const storages = loadFiles(
|
||||
files,
|
||||
@@ -347,7 +322,7 @@ function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
|
||||
data: file.data,
|
||||
} as MetaFile;
|
||||
},
|
||||
transformers: [transformerSlugs, ...transformers],
|
||||
plugins,
|
||||
},
|
||||
i18n ?? {
|
||||
defaultLanguage,
|
||||
@@ -366,6 +341,7 @@ function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
|
||||
pageTree ??= builder.buildI18n({
|
||||
storages,
|
||||
resolveIcon: options.icon,
|
||||
plugins,
|
||||
...options.pageTree,
|
||||
});
|
||||
|
||||
@@ -500,10 +476,6 @@ function fileToPage<Data = PageData>(
|
||||
|
||||
const GroupRegex = /^\(.+\)$/;
|
||||
|
||||
function isIndex(file: string) {
|
||||
return basename(file, extname(file)) === 'index';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert file path into slugs, also encode non-ASCII characters, so they can work in pathname
|
||||
*/
|
||||
|
||||
@@ -3,11 +3,8 @@ import type * as PageTree from './definitions';
|
||||
import type { MetaData, PageData, UrlFn } from '../types';
|
||||
import type { ContentStorage } from '@/source/load-files';
|
||||
import { basename, extname, joinPath } from '@/source/path';
|
||||
import {
|
||||
legacyTransformer,
|
||||
type LegacyTransformerOptions,
|
||||
} from '@/source/page-tree/legacy';
|
||||
import { transformerFallback } from '@/source/page-tree/transformer-fallback';
|
||||
import type { LoaderPlugin } from '@/source/plugins';
|
||||
|
||||
export interface PageTreeBuilderContext<
|
||||
Page extends PageData = PageData,
|
||||
@@ -17,10 +14,10 @@ export interface PageTreeBuilderContext<
|
||||
* @internal resolve paths without extensions
|
||||
*/
|
||||
resolveName: (name: string, format: 'meta' | 'page') => string;
|
||||
options: BaseOptions<Page, Meta>;
|
||||
options: BaseOptions;
|
||||
transformers: PageTreeTransformer<Page, Meta>[];
|
||||
|
||||
builder: PageTreeBuilder<Page, Meta>;
|
||||
builder: PageTreeBuilder;
|
||||
storage: ContentStorage<Page, Meta>;
|
||||
getUrl: UrlFn;
|
||||
|
||||
@@ -30,10 +27,8 @@ export interface PageTreeBuilderContext<
|
||||
}
|
||||
|
||||
export interface PageTreeTransformer<
|
||||
// eslint-disable-next-line
|
||||
Page extends PageData = any,
|
||||
// eslint-disable-next-line
|
||||
Meta extends MetaData = any,
|
||||
Page extends PageData = PageData,
|
||||
Meta extends MetaData = MetaData,
|
||||
> {
|
||||
name?: string;
|
||||
|
||||
@@ -58,17 +53,15 @@ export interface PageTreeTransformer<
|
||||
) => PageTree.Root;
|
||||
}
|
||||
|
||||
export interface BaseOptions<
|
||||
Page extends PageData = PageData,
|
||||
Meta extends MetaData = MetaData,
|
||||
> extends LegacyTransformerOptions<Page, Meta> {
|
||||
export interface BaseOptions {
|
||||
id?: string;
|
||||
/**
|
||||
* Remove references to the file path of original nodes (`$ref`)
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
noRef?: boolean;
|
||||
transformers?: PageTreeTransformer<Page, Meta>[];
|
||||
plugins?: LoaderPlugin[];
|
||||
resolveIcon?: (icon: string | undefined) => ReactNode | undefined;
|
||||
/**
|
||||
* generate fallback page tree
|
||||
@@ -78,14 +71,10 @@ export interface BaseOptions<
|
||||
generateFallback?: boolean;
|
||||
}
|
||||
|
||||
export interface PageTreeBuilder<
|
||||
Page extends PageData = PageData,
|
||||
Meta extends MetaData = MetaData,
|
||||
> {
|
||||
export interface PageTreeBuilder {
|
||||
build: (
|
||||
options: BaseOptions<Page, Meta> & {
|
||||
id?: string;
|
||||
storage: ContentStorage<Page, Meta>;
|
||||
options: BaseOptions & {
|
||||
storage: ContentStorage;
|
||||
},
|
||||
) => PageTree.Root;
|
||||
|
||||
@@ -93,9 +82,8 @@ export interface PageTreeBuilder<
|
||||
* Build page tree and fallback to the default language if the localized page doesn't exist
|
||||
*/
|
||||
buildI18n: (
|
||||
options: BaseOptions<Page, Meta> & {
|
||||
id?: string;
|
||||
storages: Record<string, ContentStorage<Page, Meta>>;
|
||||
options: BaseOptions & {
|
||||
storages: Record<string, ContentStorage>;
|
||||
},
|
||||
) => Record<string, PageTree.Root>;
|
||||
}
|
||||
@@ -347,10 +335,10 @@ function build(id: string, ctx: PageTreeBuilderContext): PageTree.Root {
|
||||
|
||||
export function createPageTreeBuilder(getUrl: UrlFn): PageTreeBuilder {
|
||||
function getTransformers(options: BaseOptions, generateFallback: boolean) {
|
||||
const transformers: PageTreeTransformer[] = [legacyTransformer(options)];
|
||||
const transformers: PageTreeTransformer[] = [];
|
||||
|
||||
if (options.transformers) {
|
||||
transformers.push(...options.transformers);
|
||||
for (const plugin of options.plugins ?? []) {
|
||||
if (plugin.transformPageTree) transformers.push(plugin.transformPageTree);
|
||||
}
|
||||
|
||||
if (generateFallback) {
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import type {
|
||||
MetaData,
|
||||
MetaFile,
|
||||
PageData,
|
||||
PageFile,
|
||||
PageTreeTransformer,
|
||||
} from '@/source';
|
||||
import type * as PageTree from './definitions';
|
||||
|
||||
export interface LegacyTransformerOptions<
|
||||
Page extends PageData,
|
||||
Meta extends MetaData,
|
||||
> {
|
||||
/**
|
||||
* @deprecated use `transformers` instead
|
||||
*/
|
||||
attachFile?: (node: PageTree.Item, file?: PageFile<Page>) => PageTree.Item;
|
||||
/**
|
||||
* @deprecated use `transformers` instead
|
||||
*/
|
||||
attachFolder?: (
|
||||
node: PageTree.Folder,
|
||||
folder: {
|
||||
children: (PageFile<Page> | MetaFile<Meta>)[];
|
||||
},
|
||||
meta?: MetaFile<Meta>,
|
||||
) => PageTree.Folder;
|
||||
/**
|
||||
* @deprecated use `transformers` instead
|
||||
*/
|
||||
attachSeparator?: (node: PageTree.Separator) => PageTree.Separator;
|
||||
}
|
||||
|
||||
export function legacyTransformer<Page extends PageData, Meta extends MetaData>(
|
||||
transformer: LegacyTransformerOptions<Page, Meta>,
|
||||
): PageTreeTransformer<Page, Meta> {
|
||||
return {
|
||||
file(node, file) {
|
||||
if (!transformer.attachFile) return node;
|
||||
const content = file ? this.storage.read(file) : undefined;
|
||||
|
||||
return transformer.attachFile(
|
||||
node,
|
||||
content?.format === 'page' ? content : undefined,
|
||||
);
|
||||
},
|
||||
folder(node, folderPath, metaPath) {
|
||||
if (!transformer.attachFolder) return node;
|
||||
|
||||
const files = this.storage.readDir(folderPath) ?? [];
|
||||
const meta = metaPath ? this.storage.read(metaPath) : undefined;
|
||||
|
||||
return transformer.attachFolder(
|
||||
node,
|
||||
{
|
||||
children: files.flatMap((file) => this.storage.read(file) ?? []),
|
||||
},
|
||||
meta?.format === 'meta' ? meta : undefined,
|
||||
);
|
||||
},
|
||||
separator(node) {
|
||||
if (!transformer.attachSeparator) return node;
|
||||
|
||||
return transformer.attachSeparator(node);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { ContentStorage } from '@/source/load-files';
|
||||
import type { PageTreeTransformer } from '@/source/page-tree/builder';
|
||||
import type { MetaData, PageData } from '@/source/types';
|
||||
|
||||
export type LoaderPlugins<
|
||||
Page extends PageData,
|
||||
Meta extends MetaData,
|
||||
> = LoaderPlugin<Page, Meta>[];
|
||||
|
||||
export interface LoaderPlugin<
|
||||
Page extends PageData = PageData,
|
||||
Meta extends MetaData = MetaData,
|
||||
> {
|
||||
/**
|
||||
* transform the storage after loading
|
||||
*/
|
||||
transformStorage?: (context: { storage: ContentStorage<Page, Meta> }) => void;
|
||||
|
||||
/**
|
||||
* transform the generated page tree
|
||||
*/
|
||||
transformPageTree?: PageTreeTransformer<Page, Meta>;
|
||||
}
|
||||
|
||||
export function buildPlugins<Page extends PageData, Meta extends MetaData>(
|
||||
plugins: LoaderPlugins<Page, Meta>,
|
||||
): LoaderPlugin<Page, Meta>[] {
|
||||
return plugins;
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import type {
|
||||
MetaData,
|
||||
MetaFile,
|
||||
PageData,
|
||||
PageFile,
|
||||
PageTreeTransformer,
|
||||
Transformer,
|
||||
} from '@/source';
|
||||
import type * as PageTree from '../page-tree/definitions';
|
||||
import type { LoaderPlugin } from '@/source/plugins';
|
||||
|
||||
export interface LegacyLoaderOptions {
|
||||
/**
|
||||
* @deprecated use `plugins` instead
|
||||
*/
|
||||
transformers?: Transformer[];
|
||||
}
|
||||
|
||||
export interface LegacyPageTreeOptions<
|
||||
Page extends PageData,
|
||||
Meta extends MetaData,
|
||||
> {
|
||||
/**
|
||||
* @deprecated use `plugins` instead
|
||||
*/
|
||||
attachFile?: (node: PageTree.Item, file?: PageFile<Page>) => PageTree.Item;
|
||||
/**
|
||||
* @deprecated use `plugins` instead
|
||||
*/
|
||||
attachFolder?: (
|
||||
node: PageTree.Folder,
|
||||
folder: {
|
||||
children: (PageFile<Page> | MetaFile<Meta>)[];
|
||||
},
|
||||
meta?: MetaFile<Meta>,
|
||||
) => PageTree.Folder;
|
||||
/**
|
||||
* @deprecated use `plugins` instead
|
||||
*/
|
||||
attachSeparator?: (node: PageTree.Separator) => PageTree.Separator;
|
||||
/**
|
||||
* @deprecated use `plugins` instead.
|
||||
*/
|
||||
transformers?: PageTreeTransformer<Page, Meta>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* legacy features compatibility layer
|
||||
*/
|
||||
export function compatPlugin<Page extends PageData, Meta extends MetaData>(
|
||||
loader: LegacyLoaderOptions,
|
||||
{
|
||||
attachFile,
|
||||
attachSeparator,
|
||||
attachFolder,
|
||||
transformers,
|
||||
}: LegacyPageTreeOptions<Page, Meta>,
|
||||
) {
|
||||
const chunk: LoaderPlugin<Page, Meta>[] = [];
|
||||
chunk.push({
|
||||
transformPageTree: {
|
||||
file(node, file) {
|
||||
if (!attachFile) return node;
|
||||
const content = file ? this.storage.read(file) : undefined;
|
||||
|
||||
return attachFile(
|
||||
node,
|
||||
content?.format === 'page' ? content : undefined,
|
||||
);
|
||||
},
|
||||
folder(node, folderPath, metaPath) {
|
||||
if (!attachFolder) return node;
|
||||
|
||||
const files = this.storage.readDir(folderPath) ?? [];
|
||||
const meta = metaPath ? this.storage.read(metaPath) : undefined;
|
||||
|
||||
return attachFolder(
|
||||
node,
|
||||
{
|
||||
children: files.flatMap((file) => this.storage.read(file) ?? []),
|
||||
},
|
||||
meta?.format === 'meta' ? meta : undefined,
|
||||
);
|
||||
},
|
||||
separator(node) {
|
||||
if (!attachSeparator) return node;
|
||||
|
||||
return attachSeparator(node);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
for (const transformer of loader.transformers ?? []) {
|
||||
chunk.push(fromStorageTransformer(transformer));
|
||||
}
|
||||
|
||||
for (const transformer of transformers ?? []) {
|
||||
chunk.push(fromPageTreeTransformer(transformer));
|
||||
}
|
||||
|
||||
return chunk;
|
||||
}
|
||||
|
||||
function fromPageTreeTransformer<Page extends PageData, Meta extends MetaData>(
|
||||
transformer: PageTreeTransformer<Page, Meta>,
|
||||
): LoaderPlugin<Page, Meta> {
|
||||
return {
|
||||
transformPageTree: transformer,
|
||||
};
|
||||
}
|
||||
|
||||
function fromStorageTransformer<Page extends PageData, Meta extends MetaData>(
|
||||
transformer: Transformer,
|
||||
): LoaderPlugin<Page, Meta> {
|
||||
return {
|
||||
transformStorage: transformer,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { type FileInfo, getSlugs, parseFilePath } from '@/source';
|
||||
import type { LoaderPlugin } from '@/source/plugins';
|
||||
import { basename, extname } from '@/source/path';
|
||||
|
||||
/**
|
||||
* Generate slugs for pages if missing
|
||||
*/
|
||||
export function slugsPlugin(
|
||||
slugsFn?: (info: FileInfo) => string[],
|
||||
): LoaderPlugin {
|
||||
function isIndex(file: string) {
|
||||
return basename(file, extname(file)) === 'index';
|
||||
}
|
||||
|
||||
return {
|
||||
transformStorage({ storage }) {
|
||||
const indexFiles = new Set<string>();
|
||||
const taken = new Set<string>();
|
||||
// for custom slugs function, don't handle conflicting cases like `dir/index.mdx` vs `dir.mdx`
|
||||
const autoIndex = slugsFn === undefined;
|
||||
|
||||
for (const path of storage.getFiles()) {
|
||||
const file = storage.read(path);
|
||||
if (!file || file.format !== 'page' || file.slugs) continue;
|
||||
|
||||
if (isIndex(path) && autoIndex) {
|
||||
indexFiles.add(path);
|
||||
continue;
|
||||
}
|
||||
|
||||
file.slugs = slugsFn ? slugsFn(parseFilePath(path)) : getSlugs(path);
|
||||
|
||||
const key = file.slugs.join('/');
|
||||
if (taken.has(key)) throw new Error('Duplicated slugs');
|
||||
taken.add(key);
|
||||
}
|
||||
|
||||
for (const path of indexFiles) {
|
||||
const file = storage.read(path);
|
||||
if (file?.format !== 'page') continue;
|
||||
|
||||
file.slugs = getSlugs(path);
|
||||
if (taken.has(file.slugs.join('/'))) file.slugs.push('index');
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,54 @@
|
||||
import { MethodLabel } from '@/ui/components/method-label';
|
||||
import type { PageTree } from 'fumadocs-core/server';
|
||||
import type { PageFile, PageTreeTransformer } from 'fumadocs-core/source';
|
||||
import type {
|
||||
LoaderPlugin,
|
||||
PageFile,
|
||||
PageTreeTransformer,
|
||||
} from 'fumadocs-core/source';
|
||||
|
||||
/**
|
||||
* Source API Integration
|
||||
* Fumadocs Source API integration, pass this to `plugins` array in `loader()`.
|
||||
*/
|
||||
export function openapiPlugin(): LoaderPlugin {
|
||||
return {
|
||||
transformPageTree: {
|
||||
file(node, filePath) {
|
||||
if (!filePath) return node;
|
||||
const file = this.storage.read(filePath);
|
||||
if (!file || file.format !== 'page') return node;
|
||||
|
||||
const data = file.data;
|
||||
let method: string | undefined;
|
||||
|
||||
if ('_openapi' in data && typeof data._openapi === 'object') {
|
||||
const meta = data._openapi as {
|
||||
method?: string;
|
||||
};
|
||||
|
||||
method = meta.method;
|
||||
}
|
||||
|
||||
if (method) {
|
||||
node.name = (
|
||||
<>
|
||||
{node.name}{' '}
|
||||
<MethodLabel className="ms-auto text-xs text-nowrap">
|
||||
{method}
|
||||
</MethodLabel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return node;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Source API Integration, add this to page tree builder options.
|
||||
*
|
||||
* Add this to page tree builder options
|
||||
* @deprecated use `openapiPlugin()`
|
||||
*/
|
||||
export const attachFile = (
|
||||
node: PageTree.Item,
|
||||
@@ -40,13 +83,9 @@ export const attachFile = (
|
||||
return node;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated use `openapiPlugin()`
|
||||
*/
|
||||
export function transformerOpenAPI(): PageTreeTransformer {
|
||||
return {
|
||||
file(node, file) {
|
||||
if (!file) return node;
|
||||
const content = this.storage.read(file);
|
||||
|
||||
return attachFile(node, content?.format === 'page' ? content : undefined);
|
||||
},
|
||||
};
|
||||
return openapiPlugin().transformPageTree!;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ const mapToPackage = {
|
||||
'utils/use-copy-button.ts': 'fumadocs-ui/utils/use-copy-button',
|
||||
};
|
||||
|
||||
// TODO: in shadcn cli, the order of files matters when writing import paths on consumer's codebase
|
||||
// in shadcn cli, the order of files matters when writing import paths on consumer's codebase
|
||||
export const registry: Registry = {
|
||||
name: 'fumadocs-ui',
|
||||
dir: srcDir,
|
||||
|
||||
Reference in New Issue
Block a user