Core: Support duplicated names with meta and page files

This commit is contained in:
Fuma Nama
2024-02-22 20:05:09 +08:00
parent 2fd6d6b8c5
commit 909b0e353a
6 changed files with 186 additions and 111 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-core': minor
---
Support duplicated names with meta and page files
+9 -6
View File
@@ -1,9 +1,9 @@
import type * as PageTree from '@/server/page-tree';
import { load, type LoadOptions, type VirtualFile } from './load';
import type { MetaData, PageData, Transformer } from './types';
import type { FileInfo, MetaData, PageData, Transformer } from './types';
import type { CreatePageTreeBuilderOptions } from './page-tree-builder';
import { createPageTreeBuilder } from './page-tree-builder';
import { joinPaths } from './path';
import { joinPaths, splitPath } from './path';
import type { Node, Page } from './file-system';
interface LoaderConfig {
@@ -106,6 +106,12 @@ function createGetUrl(
};
}
function getSlugs(info: FileInfo): string[] {
const result = [...splitPath(info.dirname), info.name];
return result[result.length - 1] === 'index' ? result.slice(0, -1) : result;
}
type InferSourceConfig<T> = T extends Source<infer Config> ? Config : never;
export function loader<Options extends LoaderOptions>(
@@ -124,10 +130,7 @@ function createOutput({
rootDir = '',
transformers,
baseUrl = '/',
slugs = (info) => {
const result = [...info.dirname.split('/'), info.name].filter(Boolean);
return result[result.length - 1] === 'index' ? result.slice(0, -1) : result;
},
slugs = getSlugs,
url = createGetUrl(baseUrl),
}: LoaderOptions): LoaderOutput<LoaderConfig> {
const result = load({
+24 -5
View File
@@ -27,7 +27,7 @@ export interface Folder<
export type Node<
MD extends MetaData = MetaData,
PD extends PageData = PageData,
> = Meta<MD> | Page<PD> | Folder;
> = Meta<MD> | Page<PD> | Folder<MD, PD>;
/**
* A virtual file system that solves inconsistent behaviours
@@ -48,11 +48,30 @@ export class Storage {
}
/**
* Read a file, it doesn't need an extension
* @param path - flattened path
*/
read(path: string): Page | Meta | undefined {
return this.files.get(path);
private read(path: string, type: string): Page | Meta | undefined {
return this.files.get(`${path}.${type}`);
}
/**
* @param path - flattened path
*/
readPage(path: string): Page | undefined {
const result = this.read(path, 'page');
if (result?.type !== 'page') return;
return result;
}
/**
* @param path - flattened path
*/
readMeta(path: string): Meta | undefined {
const result = this.read(path, 'meta');
if (result?.type !== 'meta') return;
return result;
}
readDir(path: string): Folder | undefined {
@@ -71,7 +90,7 @@ export class Storage {
this.makeDir(node.file.dirname);
this.readDir(node.file.dirname)?.children.push(node);
this.files.set(node.file.flattenedPath, node);
this.files.set(`${node.file.flattenedPath}.${node.type}`, node);
}
list(): (Page | Meta)[] {
+74 -58
View File
@@ -7,6 +7,7 @@ interface PageTreeBuilderContext {
storage: Storage;
lang?: string;
builder: PageTreeBuilder;
resolveIcon: (icon: string | undefined) => ReactElement | undefined;
}
@@ -61,7 +62,7 @@ function buildAll(
}
if (node.type === 'folder') {
output.push(buildFolderNode(node, ctx));
output.push(buildFolderNode(node, false, ctx));
}
}
@@ -72,29 +73,70 @@ function getFolderMeta(
folder: Folder,
ctx: PageTreeBuilderContext,
): Meta | undefined {
let meta = ctx.storage.read(joinPaths([folder.file.path, 'meta']));
let meta = ctx.storage.readMeta(joinPaths([folder.file.path, 'meta']));
if (ctx.lang) {
meta =
ctx.storage.read(joinPaths([folder.file.path, `meta.${ctx.lang}`])) ??
ctx.storage.readMeta(joinPaths([folder.file.path, `meta.${ctx.lang}`])) ??
meta;
}
if (meta?.type === 'meta') return meta;
return meta;
}
function resolveFolderItem(
folder: Folder,
item: string,
ctx: PageTreeBuilderContext,
addedNodePaths: Set<string>,
): PageTree.Node[] | '...' {
if (item === rest) return '...';
const separateResult = separator.exec(item);
if (separateResult?.groups) {
return [
{
type: 'separator',
name: separateResult.groups.name,
},
];
}
const extractResult = extractor.exec(item);
const path = joinPaths([
folder.file.path,
extractResult?.groups?.name ?? item,
]);
const itemNode = ctx.storage.readDir(path) ?? ctx.storage.readPage(path);
if (!itemNode) return [];
addedNodePaths.add(itemNode.file.path);
if (itemNode.type === 'folder') {
const node = buildFolderNode(itemNode, false, ctx);
return extractResult?.groups ? node.children : [node];
}
return [buildFileNode(itemNode, ctx)];
}
function buildFolderNode(
folder: Folder,
defaultIsRoot: boolean,
ctx: PageTreeBuilderContext,
defaultIsRoot = false,
): PageTree.Folder {
const indexNode = folder.children.find(
(node) => node.type === 'page' && node.file.name === 'index',
) as Page | undefined;
const index = indexNode ? buildFileNode(indexNode, ctx) : undefined;
const indexFile = ctx.storage.readPage(
joinPaths([folder.file.flattenedPath, 'index']),
);
const index = indexFile ? buildFileNode(indexFile, ctx) : undefined;
const meta = getFolderMeta(folder, ctx)?.data;
let children: PageTree.Folder['children'];
let children: PageTree.Node[];
if (!meta) {
children = buildAll(folder.children, ctx, !defaultIsRoot);
@@ -103,39 +145,7 @@ function buildFolderNode(
const addedNodePaths = new Set<string>();
const resolved = meta.pages?.flatMap<PageTree.Node | '...'>((item) => {
if (item === rest) return '...';
const result = separator.exec(item);
if (result?.groups) {
return {
type: 'separator',
name: result.groups.name,
};
}
const extractResult = extractor.exec(item);
const extractName = extractResult?.groups?.name ?? item;
const itemNode =
ctx.storage.readDir(joinPaths([folder.file.path, extractName])) ??
ctx.storage.read(joinPaths([folder.file.path, extractName]));
if (!itemNode) return [];
addedNodePaths.add(itemNode.file.path);
if (itemNode.type === 'folder') {
const node = buildFolderNode(itemNode, ctx);
return extractResult?.groups ? node.children : node;
}
if (itemNode.type === 'page') {
return buildFileNode(itemNode, ctx);
}
return [];
return resolveFolderItem(folder, item, ctx, addedNodePaths);
});
const restNodes = buildAll(
@@ -169,9 +179,11 @@ function buildFolderNode(
function buildFileNode(page: Page, ctx: PageTreeBuilderContext): PageTree.Item {
let localePage = page;
if (ctx.lang) {
const result = ctx.storage.read(`${page.file.flattenedPath}.${ctx.lang}`);
const result = ctx.storage.readPage(
`${page.file.flattenedPath}.${ctx.lang}`,
);
if (result?.type === 'page') localePage = result;
if (result) localePage = result;
}
return {
@@ -184,7 +196,7 @@ function buildFileNode(page: Page, ctx: PageTreeBuilderContext): PageTree.Item {
function build(ctx: PageTreeBuilderContext): PageTree.Root {
const root = ctx.storage.root();
const folder = buildFolderNode(root, ctx, true);
const folder = buildFolderNode(root, true, ctx);
return {
name: folder.name,
@@ -196,24 +208,28 @@ export function createPageTreeBuilder({
storage,
resolveIcon = () => undefined,
}: CreatePageTreeBuilderOptions): PageTreeBuilder {
const context: PageTreeBuilderContext = {
storage,
resolveIcon(icon) {
if (!icon) return;
return resolveIcon(icon);
},
};
function getContext(
builder: PageTreeBuilder,
locale?: string,
): PageTreeBuilderContext {
return {
storage,
lang: locale,
resolveIcon(icon) {
if (!icon) return;
return resolveIcon(icon);
},
builder,
};
}
return {
build() {
return build(context);
return build(getContext(this));
},
buildI18n({ languages = [] } = {}) {
const entries = languages.map<[string, PageTree.Root]>((lang) => {
const tree = build({
...context,
lang,
});
const tree = build(getContext(this, lang));
return [lang, tree];
});
+71 -39
View File
@@ -1,6 +1,5 @@
import { describe, expect, test } from 'vitest';
import { loader } from '../src/source';
import type * as PageTree from '../src/server/page-tree';
describe('Page Tree Builder', () => {
test('Simple', () => {
@@ -25,10 +24,19 @@ describe('Page Tree Builder', () => {
},
});
expect(result.pageTree).toEqual({
name: '',
children: [{ type: 'page', name: 'Hello', url: '/test' }],
} satisfies PageTree.Root);
expect(result.pageTree).toMatchInlineSnapshot(`
{
"children": [
{
"icon": undefined,
"name": "Hello",
"type": "page",
"url": "/test",
},
],
"name": "",
}
`);
});
test('Nested Directories', async () => {
@@ -60,23 +68,35 @@ describe('Page Tree Builder', () => {
},
});
expect(result.pageTree).toEqual({
name: '',
children: [
{ type: 'page', name: 'Hello', url: '/test' },
{
type: 'folder',
name: 'Nested',
children: [
{
type: 'page',
name: 'Nested Page',
url: '/nested/test',
},
],
},
],
} satisfies PageTree.Root);
expect(result.pageTree).toMatchInlineSnapshot(`
{
"children": [
{
"icon": undefined,
"name": "Hello",
"type": "page",
"url": "/test",
},
{
"children": [
{
"icon": undefined,
"name": "Nested Page",
"type": "page",
"url": "/nested/test",
},
],
"defaultOpen": undefined,
"icon": undefined,
"index": undefined,
"name": "Nested",
"root": undefined,
"type": "folder",
},
],
"name": "",
}
`);
});
test('Internationalized Routing', () => {
@@ -138,22 +158,34 @@ describe('Page Tree Builder', () => {
},
});
expect(result.pageTree['cn']).toEqual({
name: 'Docs Chinese',
children: [
{ type: 'page', name: 'Hello Chinese', url: '/cn/test' },
{
type: 'folder',
name: 'Nested Chinese',
children: [
{
type: 'page',
name: 'Nested Page Chinese',
url: '/cn/nested/test',
},
],
},
],
} satisfies PageTree.Root);
expect(result.pageTree['cn']).toMatchInlineSnapshot(`
{
"children": [
{
"icon": undefined,
"name": "Hello Chinese",
"type": "page",
"url": "/cn/test",
},
{
"children": [
{
"icon": undefined,
"name": "Nested Page Chinese",
"type": "page",
"url": "/cn/nested/test",
},
],
"defaultOpen": undefined,
"icon": undefined,
"index": undefined,
"name": "Nested Chinese",
"root": undefined,
"type": "folder",
},
],
"name": "Docs Chinese",
}
`);
});
});
+3 -3
View File
@@ -19,8 +19,8 @@ describe('Virtual Storage', () => {
});
test('Reading', () => {
expect(storage.read('test')).toBeDefined();
expect(storage.read('meta')).toBeDefined();
expect(storage.readPage('test')).toBeDefined();
expect(storage.readMeta('meta')).toBeDefined();
});
test('Nested Directories', () => {
@@ -31,7 +31,7 @@ describe('Virtual Storage', () => {
expect(storage.readDir('dir1')).toBeDefined();
expect(storage.readDir('dir1/dir2')).toBeDefined();
expect(storage.read('dir1/dir2/meta')).toBeDefined();
expect(storage.readMeta('dir1/dir2/meta')).toBeDefined();
});
});