mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-07-28 00:15:01 +00:00
Core: Support context-aware type-safe slugs function in loader()
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"fumadocs-core": patch
|
||||
---
|
||||
|
||||
Support context-aware type-safe `slugs` function in `loader()`
|
||||
Vendored
+1
@@ -12,5 +12,6 @@
|
||||
"**/packages/create-app/template/**/*": "plaintext"
|
||||
},
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
}
|
||||
|
||||
@@ -6,4 +6,3 @@ export * as PathUtils from './path';
|
||||
|
||||
export type * from './page-tree/builder';
|
||||
export type * from './storage/content';
|
||||
export type * from './plugins';
|
||||
|
||||
@@ -4,12 +4,12 @@ import { buildContentStorage, type ContentStorage } from './storage/content';
|
||||
import { createPageTreeBuilder, type PageTreeOptions } from '@/source/page-tree/builder';
|
||||
import { joinPath } from './path';
|
||||
import { normalizeUrl } from '@/utils/normalize-url';
|
||||
import { buildPlugins, type LoaderPlugin, type LoaderPluginOption } from '@/source/plugins';
|
||||
import { slugsPlugin } from '@/source/plugins/slugs';
|
||||
import { SlugFn, slugsPlugin } from '@/source/plugins/slugs';
|
||||
import { iconPlugin, type IconResolver } from '@/source/plugins/icon';
|
||||
import type { MetaData, PageData, Source, SourceConfig } from './source';
|
||||
import { visit } from '@/page-tree/utils';
|
||||
import path from 'node:path';
|
||||
import type { PageTreeTransformer } from '@/source/page-tree/builder';
|
||||
|
||||
export interface LoaderConfig {
|
||||
source: SourceConfig;
|
||||
@@ -32,7 +32,7 @@ export interface LoaderOptions<C extends LoaderConfig = LoaderConfig> {
|
||||
typedPlugin: (plugin: LoaderPlugin<C>) => LoaderPlugin;
|
||||
}) => LoaderPluginOption[]);
|
||||
icon?: IconResolver;
|
||||
slugs?: (info: { path: string }) => string[];
|
||||
slugs?: SlugFn<C>;
|
||||
}
|
||||
|
||||
export interface ResolvedLoaderConfig {
|
||||
@@ -418,13 +418,13 @@ function resolveConfig(
|
||||
url: url ? (...args) => normalizeUrl(url(...args)) : createGetUrl(baseUrl, base.i18n),
|
||||
source,
|
||||
plugins: buildPlugins([
|
||||
slugsPlugin(slugs),
|
||||
icon && iconPlugin(icon),
|
||||
...(typeof plugins === 'function'
|
||||
? plugins({
|
||||
typedPlugin: (plugin) => plugin as unknown as LoaderPlugin,
|
||||
})
|
||||
: plugins),
|
||||
slugsPlugin(slugs),
|
||||
]),
|
||||
};
|
||||
|
||||
@@ -436,6 +436,58 @@ function resolveConfig(
|
||||
return config;
|
||||
}
|
||||
|
||||
export interface LoaderPlugin<Config extends LoaderConfig = LoaderConfig> {
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* Change the order of plugin:
|
||||
* - `pre`: before normal plugins
|
||||
* - `post`: after normal plugins
|
||||
*/
|
||||
enforce?: 'pre' | 'post';
|
||||
|
||||
/**
|
||||
* receive & replace loader options
|
||||
*/
|
||||
config?: (config: ResolvedLoaderConfig) => ResolvedLoaderConfig | void | undefined;
|
||||
|
||||
/**
|
||||
* transform the storage after loading
|
||||
*/
|
||||
transformStorage?: (context: { storage: ContentStorage<Config['source']> }) => void;
|
||||
|
||||
/**
|
||||
* transform the generated page tree
|
||||
*/
|
||||
transformPageTree?: PageTreeTransformer<Config['source']>;
|
||||
}
|
||||
|
||||
export type LoaderPluginOption<Config extends LoaderConfig = LoaderConfig> =
|
||||
| LoaderPlugin<Config>
|
||||
| LoaderPluginOption<Config>[]
|
||||
| undefined;
|
||||
|
||||
const priorityMap = {
|
||||
pre: 1,
|
||||
default: 0,
|
||||
post: -1,
|
||||
};
|
||||
|
||||
function buildPlugins(plugins: LoaderPluginOption[], sort = true): LoaderPlugin[] {
|
||||
const flatten: LoaderPlugin[] = [];
|
||||
|
||||
for (const plugin of plugins) {
|
||||
if (Array.isArray(plugin)) flatten.push(...buildPlugins(plugin, false));
|
||||
else if (plugin) flatten.push(plugin);
|
||||
}
|
||||
|
||||
if (sort)
|
||||
return flatten.sort(
|
||||
(a, b) => priorityMap[b.enforce ?? 'default'] - priorityMap[a.enforce ?? 'default'],
|
||||
);
|
||||
return flatten;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- infer types
|
||||
export type InferPageType<Utils extends LoaderOutput<any>> =
|
||||
Utils extends LoaderOutput<infer Config> ? Page<Config['source']['pageData']> : never;
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import type { ContentStorage } from '@/source/storage/content';
|
||||
import type { PageTreeTransformer } from '@/source/page-tree/builder';
|
||||
import type { LoaderConfig, ResolvedLoaderConfig } from '@/source/loader';
|
||||
|
||||
export interface LoaderPlugin<Config extends LoaderConfig = LoaderConfig> {
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* Change the order of plugin:
|
||||
* - `pre`: before normal plugins
|
||||
* - `post`: after normal plugins
|
||||
*/
|
||||
enforce?: 'pre' | 'post';
|
||||
|
||||
/**
|
||||
* receive & replace loader options
|
||||
*/
|
||||
config?: (config: ResolvedLoaderConfig) => ResolvedLoaderConfig | void | undefined;
|
||||
|
||||
/**
|
||||
* transform the storage after loading
|
||||
*/
|
||||
transformStorage?: (context: { storage: ContentStorage<Config['source']> }) => void;
|
||||
|
||||
/**
|
||||
* transform the generated page tree
|
||||
*/
|
||||
transformPageTree?: PageTreeTransformer<Config['source']>;
|
||||
}
|
||||
|
||||
export type LoaderPluginOption<Config extends LoaderConfig = LoaderConfig> =
|
||||
| LoaderPlugin<Config>
|
||||
| LoaderPluginOption<Config>[]
|
||||
| undefined;
|
||||
|
||||
const priorityMap = {
|
||||
pre: 1,
|
||||
default: 0,
|
||||
post: -1,
|
||||
};
|
||||
|
||||
export function buildPlugins(plugins: LoaderPluginOption[], sort = true): LoaderPlugin[] {
|
||||
const flatten: LoaderPlugin[] = [];
|
||||
|
||||
for (const plugin of plugins) {
|
||||
if (Array.isArray(plugin)) flatten.push(...buildPlugins(plugin, false));
|
||||
else if (plugin) flatten.push(plugin);
|
||||
}
|
||||
|
||||
if (sort)
|
||||
return flatten.sort(
|
||||
(a, b) => priorityMap[b.enforce ?? 'default'] - priorityMap[a.enforce ?? 'default'],
|
||||
);
|
||||
return flatten;
|
||||
}
|
||||
@@ -1,10 +1,20 @@
|
||||
import type { LoaderPlugin } from '@/source/plugins';
|
||||
import { basename, dirname, extname } from '@/source/path';
|
||||
import type { ContentStoragePageFile } from '../storage/content';
|
||||
import type { LoaderConfig, LoaderPlugin } from '../loader';
|
||||
|
||||
/**
|
||||
* a function to generate slugs, return `undefined` to fallback to default generation.
|
||||
*/
|
||||
export type SlugFn<Config extends LoaderConfig = LoaderConfig> = (
|
||||
file: ContentStoragePageFile<Config['source']>,
|
||||
) => string[] | undefined;
|
||||
|
||||
/**
|
||||
* Generate slugs for pages if missing
|
||||
*/
|
||||
export function slugsPlugin(slugsFn?: (info: { path: string }) => string[]): LoaderPlugin {
|
||||
export function slugsPlugin<Config extends LoaderConfig = LoaderConfig>(
|
||||
slugFn?: SlugFn<Config>,
|
||||
): LoaderPlugin<Config> {
|
||||
function isIndex(file: string) {
|
||||
return basename(file, extname(file)) === 'index';
|
||||
}
|
||||
@@ -12,24 +22,23 @@ export function slugsPlugin(slugsFn?: (info: { path: string }) => string[]): Loa
|
||||
return {
|
||||
name: 'fumadocs:slugs',
|
||||
transformStorage({ storage }) {
|
||||
const indexFiles = new Set<string>();
|
||||
const indexFiles: 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);
|
||||
const customSlugs = slugFn?.(file);
|
||||
// for custom slugs function, don't handle conflicting cases like `dir/index.mdx` vs `dir.mdx`
|
||||
if (customSlugs === undefined && isIndex(path)) {
|
||||
indexFiles.push(path);
|
||||
continue;
|
||||
}
|
||||
|
||||
file.slugs = slugsFn ? slugsFn({ path }) : getSlugs(path);
|
||||
|
||||
file.slugs = customSlugs ?? getSlugs(path);
|
||||
const key = file.slugs.join('/');
|
||||
if (taken.has(key)) throw new Error('Duplicated slugs');
|
||||
if (taken.has(key)) throw new Error(`Duplicated slugs: ${key}`);
|
||||
taken.add(key);
|
||||
}
|
||||
|
||||
@@ -44,6 +53,23 @@ export function slugsPlugin(slugsFn?: (info: { path: string }) => string[]): Loa
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate slugs from file data (e.g. frontmatter).
|
||||
*
|
||||
* @param key - the property name in file data to generate slugs, default to `slug`.
|
||||
*/
|
||||
export function slugsFromData<Config extends LoaderConfig = LoaderConfig>(
|
||||
key = 'slug',
|
||||
): SlugFn<Config> {
|
||||
return (file) => {
|
||||
const k = key as keyof typeof file.data;
|
||||
|
||||
if (k in file.data && typeof file.data[k] === 'string') {
|
||||
return file.data[k].split('/').filter((v) => v.length > 0);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const GroupRegex = /^\(.+\)$/;
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,21 +8,25 @@ export type ContentStorage<Config extends SourceConfig = SourceConfig> = FileSys
|
||||
>;
|
||||
|
||||
export type ContentStorageFile<Config extends SourceConfig = SourceConfig> =
|
||||
| {
|
||||
path: string;
|
||||
absolutePath?: string;
|
||||
| ContentStorageMetaFile<Config>
|
||||
| ContentStoragePageFile<Config>;
|
||||
|
||||
format: 'meta';
|
||||
data: Config['metaData'];
|
||||
}
|
||||
| {
|
||||
path: string;
|
||||
absolutePath?: string;
|
||||
export interface ContentStorageMetaFile<Config extends SourceConfig = SourceConfig> {
|
||||
path: string;
|
||||
absolutePath?: string;
|
||||
|
||||
format: 'page';
|
||||
slugs: string[];
|
||||
data: Config['pageData'];
|
||||
};
|
||||
format: 'meta';
|
||||
data: Config['metaData'];
|
||||
}
|
||||
|
||||
export interface ContentStoragePageFile<Config extends SourceConfig = SourceConfig> {
|
||||
path: string;
|
||||
absolutePath?: string;
|
||||
|
||||
format: 'page';
|
||||
slugs: string[];
|
||||
data: Config['pageData'];
|
||||
}
|
||||
|
||||
function isLocaleValid(locale: string) {
|
||||
return locale.length > 0 && !/\d+/.test(locale);
|
||||
|
||||
@@ -12,7 +12,7 @@ export default defineConfig({
|
||||
'src/content/mdx/preset-*.ts',
|
||||
'src/source/{index,schema}.ts',
|
||||
'src/source/client/*.{ts,tsx}',
|
||||
'src/source/plugins/lucide-icons.ts',
|
||||
'src/source/plugins/{lucide-icons,slugs}.ts',
|
||||
'src/search/{index,client,server,algolia,orama-cloud}.ts',
|
||||
'src/utils/use-on-change.ts',
|
||||
'src/utils/use-effect-event.ts',
|
||||
|
||||
Reference in New Issue
Block a user