Core: Simplify Source API storage management

This commit is contained in:
Fuma Nama
2025-08-21 11:42:35 +08:00
parent e785f98aaf
commit e254c65903
10 changed files with 100 additions and 101 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-core': patch
---
Simplify Source API storage management
+25 -3
View File
@@ -8,8 +8,18 @@ export class FileSystem<File> {
files = new Map<string, File>();
folders = new Map<string, string[]>();
constructor() {
this.folders.set('', []);
constructor(inherit?: FileSystem<File>) {
if (inherit) {
for (const [k, v] of inherit.folders) {
this.folders.set(k, v);
}
for (const [k, v] of inherit.files) {
this.files.set(k, v);
}
} else {
this.folders.set('', []);
}
}
read(path: string): File | undefined {
@@ -24,13 +34,25 @@ export class FileSystem<File> {
}
write(path: string, file: File): void {
const dir = dirname(path);
if (this.files.has(path)) {
this.files.set(path, file);
return;
}
const dir = dirname(path);
this.makeDir(dir);
this.readDir(dir)?.push(path);
this.files.set(path, file);
}
delete(path: string) {
return this.files.delete(path);
}
deleteDir(path: string) {
return this.folders.delete(path);
}
getFiles(): string[] {
return Array.from(this.files.keys());
}
+10 -5
View File
@@ -90,14 +90,13 @@ export function loadFiles(
}),
);
for (const lang of i18n.languages) {
const storage: ContentStorage = new FileSystem();
function scan(lang: string, fallback?: ContentStorage): ContentStorage {
const storage: ContentStorage = new FileSystem(fallback);
for (const item of normalized) {
const [path, locale = i18n.defaultLanguage] = parser(item.path);
if (locale !== lang) continue;
storage.write(path, item);
if (locale === lang) storage.write(path, item);
}
for (const transformer of transformers) {
@@ -107,7 +106,13 @@ export function loadFiles(
});
}
storages[lang] = storage;
return storage;
}
storages[i18n.defaultLanguage] = scan(i18n.defaultLanguage);
for (const lang of i18n.languages) {
storages[lang] ??= scan(lang, storages[i18n.defaultLanguage]);
}
return storages;
+29 -56
View File
@@ -189,11 +189,7 @@ export interface LoaderOutput<Config extends LoaderConfig> {
) => (Record<TSlug, string[]> & Record<TLang, string>)[];
}
function indexPages(
storages: Record<string, ContentStorage>,
getUrl: UrlFn,
i18n?: I18nConfig,
) {
function indexPages(storages: Record<string, ContentStorage>, getUrl: UrlFn) {
const result = {
// (locale.slugs -> page)
pages: new Map<string, Page>(),
@@ -203,41 +199,19 @@ function indexPages(
pathToPage: new Map<string, Page>(),
};
const defaultLanguage = i18n?.defaultLanguage ?? '';
for (const [lang, storage] of Object.entries(storages)) {
for (const filePath of storage.getFiles()) {
const item = storage.read(filePath)!;
const path = `${lang}.${filePath}`;
for (const filePath of storages[defaultLanguage].getFiles()) {
const item = storages[defaultLanguage].read(filePath)!;
const path = `${defaultLanguage}.${filePath}`;
if (item.format === 'meta') {
result.pathToMeta.set(path, fileToMeta(item));
}
if (item.format === 'page') {
const page = fileToPage(item, getUrl, defaultLanguage);
result.pathToPage.set(path, page);
result.pages.set(`${defaultLanguage}.${page.slugs.join('/')}`, page);
if (!i18n) continue;
for (const lang of i18n.languages) {
if (lang === defaultLanguage) continue;
const localizedItem = storages[lang].read(filePath);
const localizedPage = fileToPage(
localizedItem?.format === 'page' ? localizedItem : item,
getUrl,
lang,
);
if (localizedItem) {
result.pathToPage.set(`${lang}.${filePath}`, localizedPage);
}
result.pages.set(
`${lang}.${localizedPage.slugs.join('/')}`,
localizedPage,
);
if (item.format === 'meta') {
result.pathToMeta.set(path, fileToMeta(item));
continue;
}
const page = fileToPage(item, getUrl, lang);
result.pathToPage.set(path, page);
result.pages.set(`${lang}.${page.slugs.join('/')}`, page);
}
}
@@ -362,32 +336,31 @@ function createOutput(options: LoaderOptions): LoaderOutput<LoaderConfig> {
},
);
const walker = indexPages(storages, getUrl, i18n);
const walker = indexPages(storages, getUrl);
const builder = createPageTreeBuilder(getUrl);
let pageTree: LoaderOutput<LoaderConfig>['pageTree'] | undefined;
let pageTree: Record<string, PageTree.Root> | undefined;
return {
_i18n: i18n,
get pageTree() {
if (i18n) {
pageTree ??= builder.buildI18n({
storages,
resolveIcon: options.icon,
i18n: i18n,
...options.pageTree,
}) as unknown as LoaderOutput<LoaderConfig>['pageTree'];
} else {
pageTree ??= builder.build({
storage: storages[''],
resolveIcon: options.icon,
...options.pageTree,
});
}
pageTree ??= builder.buildI18n({
storages,
resolveIcon: options.icon,
...options.pageTree,
});
return pageTree;
return i18n
? (pageTree as unknown as LoaderOutput<LoaderConfig>['pageTree'])
: pageTree[defaultLanguage];
},
set pageTree(v) {
pageTree = v;
if (i18n) {
pageTree = v as unknown as Record<string, PageTree.Root>;
} else {
pageTree = {
defaultLanguage: v,
};
}
},
getPageByHref(href, { dir = '', language } = {}) {
const [value, hash] = href.split('#', 2);
+17 -20
View File
@@ -1,5 +1,4 @@
import type { ReactElement } from 'react';
import type { I18nConfig } from '@/i18n';
import type * as PageTree from './definitions';
import { joinPath } from '@/utils/path';
import type { MetaData, PageData, UrlFn } from '../types';
@@ -24,10 +23,10 @@ export interface PageTreeBuilderContext<
builder: PageTreeBuilder<Page, Meta>;
storage: ContentStorage<Page, Meta>;
locale?: string;
localeStorage?: ContentStorage<Page, Meta>;
getUrl: UrlFn;
storages?: Record<string, ContentStorage<Page, Meta>>;
locale?: string;
}
export interface PageTreeTransformer<
@@ -99,7 +98,6 @@ export interface PageTreeBuilder<
options: BaseOptions<Page, Meta> & {
id?: string;
storages: Record<string, ContentStorage<Page, Meta>>;
i18n: I18nConfig;
},
) => Record<string, PageTree.Root>;
}
@@ -217,14 +215,14 @@ function buildFolderNode(
isGlobalRoot: boolean,
ctx: PageTreeBuilderContext,
): PageTree.Folder | undefined {
const { storage, localeStorage, options, resolveName, transformers } = ctx;
const { storage, options, resolveName, transformers } = ctx;
const files = storage.readDir(folderPath);
if (!files) return;
const metaPath = resolveName(joinPath(folderPath, 'meta'), 'meta');
const indexPath = resolveName(joinPath(folderPath, 'index'), 'page');
let meta = localeStorage?.read(metaPath) ?? storage.read(metaPath);
let meta = storage.read(metaPath);
if (meta?.format !== 'meta') {
meta = undefined;
}
@@ -305,8 +303,8 @@ function buildFileNode(
path: string,
ctx: PageTreeBuilderContext,
): PageTree.Item | undefined {
const { options, getUrl, storage, localeStorage, locale, transformers } = ctx;
const page = localeStorage?.read(path) ?? storage.read(path);
const { options, getUrl, storage, locale, transformers } = ctx;
const page = storage.read(path);
if (page?.format !== 'page') return;
const { title, description, icon } = page.data;
@@ -393,29 +391,28 @@ export function createPageTreeBuilder(getUrl: UrlFn): PageTreeBuilder {
},
});
},
buildI18n({ id, i18n, ...options }) {
const storage = options.storages[i18n.defaultLanguage];
const resolve = createFlattenPathResolver(storage);
buildI18n({ id, storages, ...options }) {
const transformers = getTransformers(options);
const out: Record<string, PageTree.Root> = {};
const entries = i18n.languages.map<[string, PageTree.Root]>((lang) => {
const tree = build(id ?? lang ?? 'root', {
for (const [locale, storage] of Object.entries(storages)) {
const resolve = createFlattenPathResolver(storage);
out[locale] = build(id ?? (locale.length === 0 ? 'root' : locale), {
transformers,
builder: this,
options,
getUrl,
locale: lang,
locale,
storage,
localeStorage: options.storages[lang],
storages,
resolveName(name, format) {
return resolve(name, format) ?? name;
},
});
}
return [lang, tree];
});
return Object.fromEntries(entries);
return out;
},
};
}
+3 -10
View File
@@ -37,9 +37,7 @@ export function legacyTransformer<Page extends PageData, Meta extends MetaData>(
return {
file(node, file) {
if (!transformer.attachFile) return node;
const content = file
? (this.localeStorage?.read(file) ?? this.storage.read(file))
: undefined;
const content = file ? this.storage.read(file) : undefined;
return transformer.attachFile(
node,
@@ -50,17 +48,12 @@ export function legacyTransformer<Page extends PageData, Meta extends MetaData>(
if (!transformer.attachFolder) return node;
const files = this.storage.readDir(folderPath) ?? [];
const meta = metaPath
? (this.localeStorage?.read(metaPath) ?? this.storage.read(metaPath))
: undefined;
const meta = metaPath ? this.storage.read(metaPath) : undefined;
return transformer.attachFolder(
node,
{
children: files.flatMap(
(file) =>
this.localeStorage?.read(file) ?? this.storage.read(file) ?? [],
),
children: files.flatMap((file) => this.storage.read(file) ?? []),
},
meta?.format === 'meta' ? meta : undefined,
);
@@ -12,10 +12,8 @@ export function transformerFallback(): PageTreeTransformer {
for (const file of this.storage.getFiles()) {
if (addedFiles.has(file)) continue;
const content =
this.localeStorage?.read(file) ?? this.storage.read(file);
if (!content) continue;
isolatedStorage.write(file, content);
const content = this.storage.read(file);
if (content) isolatedStorage.write(file, content);
}
if (isolatedStorage.getFiles().length === 0) return root;
+4 -1
View File
@@ -13,6 +13,9 @@
},
{
"$id": "nested",
"$ref": {
"metaFile": "nested/meta.json",
},
"children": [
{
"$id": "nested/test.mdx",
@@ -24,7 +27,7 @@
"url": "/cn/nested/test",
},
],
"name": "Nested",
"name": "Nested Chinese",
"type": "folder",
},
],
@@ -18,7 +18,7 @@
"url": "/cn/nested/test",
},
],
"name": "Nested",
"name": "Nested Chinese",
"type": "folder",
},
],
+4 -1
View File
@@ -13,6 +13,9 @@
},
{
"$id": "nested",
"$ref": {
"metaFile": "nested/meta.json",
},
"children": [
{
"$id": "nested/test.mdx",
@@ -24,7 +27,7 @@
"url": "/cn/nested/test",
},
],
"name": "Nested",
"name": "Nested Chinese",
"type": "folder",
},
],