Core: Fix type inference for pageTree.attachFile in loader()

This commit is contained in:
Fuma Nama
2025-05-03 19:04:16 +08:00
parent 0bc99c56ab
commit 6ea1718167
6 changed files with 66 additions and 45 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-core': patch
---
Fix type inference for `pageTree.attachFile` in `loader()`
+7
View File
@@ -23,4 +23,11 @@ export interface I18nConfig {
* @defaultValue 'never'
*/
hideLocale?: 'always' | 'default-locale' | 'never';
/**
* Used by `loader()`, specify the way to parse i18n file structure.
*
* @defaultValue 'dot'
*/
parser?: 'dot' | 'dir';
}
+12 -9
View File
@@ -1,32 +1,35 @@
import type { MetaData, PageData } from '@/source/types';
import {
parseFilePath,
parseFolderPath,
type FileInfo,
type FolderInfo,
parseFilePath,
parseFolderPath,
} from './path';
import { joinPath, splitPath } from '@/utils/path';
export interface MetaFile {
export interface MetaFile<Data extends MetaData = MetaData> {
file: FileInfo;
format: 'meta';
data: MetaData;
data: Data;
}
export interface PageFile {
export interface PageFile<Data extends PageData = PageData> {
file: FileInfo;
format: 'page';
data: {
slugs: string[];
data: PageData;
data: Data;
};
}
export type File = MetaFile | PageFile;
type File = MetaFile | PageFile;
export interface Folder {
export interface Folder<
Page extends PageData = PageData,
Meta extends MetaData = MetaData,
> {
file: FolderInfo;
children: (File | Folder)[];
children: (MetaFile<Meta> | PageFile<Page> | Folder<Page, Meta>)[];
}
/**
+22 -21
View File
@@ -1,7 +1,6 @@
import type * as PageTree from '@/server/page-tree';
import type { I18nConfig } from '@/i18n';
import {
type I18nLoadOptions,
loadFiles,
loadFilesI18n,
type LoadOptions,
@@ -9,8 +8,10 @@ import {
type VirtualFile,
} from './load-files';
import type { MetaData, PageData, UrlFn } from './types';
import type { BuildPageTreeOptions } from './page-tree-builder';
import { createPageTreeBuilder } from './page-tree-builder';
import {
type BaseOptions as BasePageTreeBuilderOptions,
createPageTreeBuilder,
} from './page-tree-builder';
import { type FileInfo } from './path';
import type { MetaFile, PageFile, Storage } from './file-system';
import { joinPath } from '@/utils/path';
@@ -25,28 +26,28 @@ export interface SourceConfig {
metaData: MetaData;
}
export interface LoaderOptions {
export interface LoaderOptions<
T extends SourceConfig = SourceConfig,
I18n extends I18nConfig | undefined = I18nConfig | undefined,
> {
baseUrl: string;
icon?: NonNullable<BuildPageTreeOptions['resolveIcon']>;
icon?: NonNullable<BasePageTreeBuilderOptions['resolveIcon']>;
slugs?: LoadOptions['getSlugs'];
url?: UrlFn;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Inevitable
source: Source<any>;
source: Source<T>;
transformers?: Transformer[];
/**
* Additional options for page tree builder
*/
pageTree?: Partial<Omit<BuildPageTreeOptions, 'storage' | 'getUrl'>>;
pageTree?: Partial<BasePageTreeBuilderOptions<T['pageData'], T['metaData']>>;
/**
* Configure i18n
*/
i18n?: I18nConfig & {
parser?: I18nLoadOptions['i18n']['parser'];
};
i18n?: I18n;
}
export interface Source<Config extends SourceConfig> {
@@ -231,15 +232,17 @@ export function getSlugs(info: FileInfo): string[] {
);
}
type InferSourceConfig<T> = T extends Source<infer Config> ? Config : never;
export function loader<Options extends LoaderOptions>(
options: Options,
export function loader<
Config extends SourceConfig,
I18n extends I18nConfig | undefined = undefined,
>(
options: LoaderOptions<Config, I18n>,
): LoaderOutput<{
source: InferSourceConfig<Options['source']>;
i18n: Options['i18n'] extends I18nConfig ? true : false;
source: Config;
i18n: I18n extends I18nConfig ? true : false;
}> {
return createOutput(options) as ReturnType<typeof loader<Options>>;
// @ts-expect-error -- forced type cast
return createOutput(options);
}
function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
@@ -270,7 +273,7 @@ function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
};
const walker = indexPages(storages, getUrl, options.i18n);
const builder = createPageTreeBuilder();
const builder = createPageTreeBuilder(getUrl);
let pageTree: LoaderOutput<LoaderConfig>['pageTree'] | undefined;
return {
@@ -280,7 +283,6 @@ function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
pageTree ??= builder.buildI18n({
storages,
resolveIcon: options.icon,
getUrl,
i18n: options.i18n,
...options.pageTree,
}) as unknown as LoaderOutput<LoaderConfig>['pageTree'];
@@ -288,7 +290,6 @@ function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
pageTree ??= builder.build({
storage: storages[''],
resolveIcon: options.icon,
getUrl,
...options.pageTree,
});
}
+19 -14
View File
@@ -1,9 +1,9 @@
import type { ReactElement } from 'react';
import type { I18nConfig } from '@/i18n';
import type * as PageTree from '../server/page-tree';
import type { File, Folder, MetaFile, PageFile, Storage } from './file-system';
import type { Folder, MetaFile, PageFile, Storage } from './file-system';
import { joinPath } from '@/utils/path';
import { type UrlFn } from './types';
import type { MetaData, PageData, UrlFn } from './types';
interface PageTreeBuilderContext {
storage: Storage;
@@ -12,10 +12,14 @@ interface PageTreeBuilderContext {
localeStorage?: Storage;
builder: PageTreeBuilder;
options: Options;
options: BaseOptions;
getUrl: UrlFn;
}
interface Options {
export interface BaseOptions<
Page extends PageData = PageData,
Meta extends MetaData = MetaData,
> {
/**
* Remove references to the file path of original nodes (`$ref`)
*
@@ -23,23 +27,22 @@ interface Options {
*/
noRef?: boolean;
attachFile?: (node: PageTree.Item, file?: PageFile) => PageTree.Item;
attachFile?: (node: PageTree.Item, file?: PageFile<Page>) => PageTree.Item;
attachFolder?: (
node: PageTree.Folder,
folder: Folder,
meta?: MetaFile,
folder: Folder<Page, Meta>,
meta?: MetaFile<Meta>,
) => PageTree.Folder;
attachSeparator?: (node: PageTree.Separator) => PageTree.Separator;
getUrl: UrlFn;
resolveIcon?: (icon: string | undefined) => ReactElement | undefined;
}
export interface BuildPageTreeOptions extends Options {
export interface BuildPageTreeOptions extends BaseOptions {
storage: Storage;
}
export interface BuildPageTreeOptionsWithI18n extends Options {
export interface BuildPageTreeOptionsWithI18n extends BaseOptions {
storages: Record<string, Storage>;
i18n: I18nConfig;
}
@@ -63,7 +66,7 @@ const restReversed = 'z...a' as const;
const extractPrefix = '...';
const excludePrefix = '!';
function isPageFile(node: Folder | File): node is PageFile {
function isPageFile(node: Folder | PageFile | MetaFile): node is PageFile {
return 'data' in node && node.format === 'page';
}
@@ -74,7 +77,7 @@ function isPageFile(node: Folder | File): node is PageFile {
* @returns Nodes with specified locale in context (sorted)
*/
function buildAll(
nodes: (Folder | File)[],
nodes: (Folder | MetaFile | PageFile)[],
ctx: PageTreeBuilderContext,
skipIndex: boolean,
): PageTree.Node[] {
@@ -262,7 +265,7 @@ function buildFileNode(
name: file.data.data.title ?? pathToName(file.file.name),
description: file.data.data.description,
icon: ctx.options.resolveIcon?.(file.data.data.icon),
url: ctx.options.getUrl(file.data.slugs, ctx.locale),
url: ctx.getUrl(file.data.slugs, ctx.locale),
$ref: !ctx.options.noRef
? {
file: file.file.path,
@@ -284,19 +287,21 @@ function build(ctx: PageTreeBuilderContext): PageTree.Root {
};
}
export function createPageTreeBuilder(): PageTreeBuilder {
export function createPageTreeBuilder(getUrl: UrlFn): PageTreeBuilder {
return {
build(options) {
return build({
options,
builder: this,
storage: options.storage,
getUrl,
});
},
buildI18n({ i18n, ...options }) {
const entries = i18n.languages.map<[string, PageTree.Root]>((lang) => {
const tree = build({
options,
getUrl,
builder: this,
locale: lang,
storage: options.storages[i18n.defaultLanguage],
+1 -1
View File
@@ -20,7 +20,7 @@
".source/**",
"!.contentlayer/.cache/**",
".next/**",
"content/docs/openapi/(generated)",
"content/docs/openapi/\\(generated\\)",
"!.next/cache/**",
"dist/**",
"out",