Core: Type file system utilities

This commit is contained in:
fuma-nama
2024-08-01 19:14:57 +08:00
parent 4f46746906
commit 978342f664
10 changed files with 96 additions and 56 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-core': patch
---
Type file system utilities (Note: This is an internal module, you're not supposed to reference it)
@@ -0,0 +1,9 @@
import { type ReactNode } from 'react';
export function ApiIndicator(): ReactNode {
return (
<span className="ms-auto rounded-full bg-fd-primary px-2 py-0.5 text-xs font-medium text-fd-primary-foreground">
API
</span>
);
}
+1 -1
View File
@@ -1,7 +1,7 @@
import { cva } from 'class-variance-authority';
const buttonVariants = cva(
'ring-offset-background inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring disabled:pointer-events-none disabled:opacity-50',
'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-fd-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
+17 -4
View File
@@ -5,6 +5,12 @@ import { loader } from 'fumadocs-core/source';
import { icons } from 'lucide-react';
import { map } from '@/.map';
import { create } from '@/components/ui/icon';
import { ApiIndicator } from '@/components/ui/api-indicator';
const frontmatter = defaultSchemas.frontmatter.extend({
preview: z.string().optional(),
index: z.boolean().default(false),
});
export const utils = loader({
baseUrl: '/docs',
@@ -15,12 +21,19 @@ export const utils = loader({
},
source: createMDXSource(map, {
schema: {
frontmatter: defaultSchemas.frontmatter.extend({
preview: z.string().optional(),
index: z.boolean().default(false),
}),
frontmatter,
},
}),
pageTree: {
attachFile(node, file) {
if (!file) return node;
const data = file.data.data as z.output<typeof frontmatter>;
if (!data.full) return node;
node.name = [node.name, ApiIndicator()];
return node;
},
},
});
export const blog = loader({
+28 -10
View File
@@ -1,3 +1,4 @@
import type { MetaData, PageData } from '@/source/types';
import {
parseFilePath,
parseFolderPath,
@@ -5,12 +6,26 @@ import {
type FileInfo,
} from './path';
export interface File {
export interface MetaFile {
file: FileInfo;
format: 'meta' | 'page';
data: Record<string, unknown>;
format: 'meta';
data: {
// TODO: Merge it into data (major)
data: MetaData;
};
}
export interface PageFile {
file: FileInfo;
format: 'page';
data: {
slugs: string[];
data: PageData;
};
}
export type File = MetaFile | PageFile;
export interface Folder {
file: FileInfo;
children: (File | Folder)[];
@@ -37,8 +52,11 @@ export class Storage {
* @param path - flattened path
* @param format - file format
*/
read(path: string, format: string): File | undefined {
return this.files.get(`${path}.${format}`);
read<F extends File['format']>(
path: string,
format: F,
): Extract<File, { format: F }> | undefined {
return this.files.get(`${path}.${format}`) as Extract<File, { format: F }>;
}
readDir(path: string): Folder | undefined {
@@ -49,16 +67,16 @@ export class Storage {
return this.rootFolder;
}
write(
write<F extends File['format']>(
path: string,
format: 'meta' | 'page',
data: Record<string, unknown>,
format: F,
data: Extract<File, { format: F }>['data'],
): void {
const node: File = {
const node = {
format,
file: parseFilePath(path),
data,
};
} as File;
this.makeDir(node.file.dirname);
this.readDir(node.file.dirname)?.children.push(node);
+4 -4
View File
@@ -1,4 +1,4 @@
import { type FileData } from '@/source/types';
import { type MetaData, type PageData } from '@/source/types';
import { parseFilePath, type FileInfo, normalizePath } from './path';
import { Storage } from './file-system';
@@ -42,13 +42,13 @@ export function loadFiles(files: VirtualFile[], options: LoadOptions): Storage {
storage.write(relativePath, file.type, {
slugs,
data: file.data,
} as FileData['file']);
data: file.data as PageData,
});
}
if (file.type === 'meta') {
storage.write(relativePath, file.type, {
data: file.data,
data: file.data as MetaData,
});
}
}
+6 -8
View File
@@ -5,14 +5,14 @@ import {
type VirtualFile,
type Transformer,
} from './load-files';
import type { FileData, MetaData, PageData, UrlFn } from './types';
import type { MetaData, PageData, UrlFn } from './types';
import type {
BuildPageTreeOptions,
BuildPageTreeOptionsWithI18n,
} from './page-tree-builder';
import { createPageTreeBuilder } from './page-tree-builder';
import { type FileInfo } from './path';
import type { File, Storage } from './file-system';
import type { File, PageFile, Storage } from './file-system';
export interface LoaderConfig {
source: SourceConfig;
@@ -225,16 +225,14 @@ function createOutput({
}
function fileToPage<Data = PageData>(
file: File,
file: PageFile,
getUrl: UrlFn,
locale?: string,
): Page<Data> {
const data = file.data as FileData['file'];
return {
file: file.file,
url: getUrl(data.slugs, locale),
slugs: data.slugs,
data: data.data as Data,
url: getUrl(file.data.slugs, locale),
slugs: file.data.slugs,
data: file.data.data as Data,
};
}
+16 -13
View File
@@ -1,8 +1,8 @@
import type { ReactElement } from 'react';
import type * as PageTree from '../server/page-tree';
import type { File, Folder, Storage } from './file-system';
import type { File, Folder, MetaFile, PageFile, Storage } from './file-system';
import { resolvePath } from './path';
import { type FileData, type UrlFn } from './types';
import { type UrlFn } from './types';
interface PageTreeBuilderContext {
lang?: string;
@@ -20,11 +20,12 @@ export interface BuildPageTreeOptions {
*/
attachFolderIds?: boolean;
attachFile?: (node: PageTree.Item, file?: File) => PageTree.Item;
// TODO: Rename to `attachPage` (major)
attachFile?: (node: PageTree.Item, file?: PageFile) => PageTree.Item;
attachFolder?: (
node: PageTree.Folder,
folder: Folder,
meta?: File,
meta?: MetaFile,
) => PageTree.Folder;
attachSeparator?: (node: PageTree.Separator) => PageTree.Separator;
@@ -170,7 +171,7 @@ function buildFolderNode(
'page',
);
const metadata = meta?.data.data as FileData['meta']['data'] | undefined;
const metadata = meta?.data.data;
const index = indexFile ? buildFileNode(indexFile, ctx) : undefined;
let children: PageTree.Node[];
@@ -221,16 +222,18 @@ function buildFolderNode(
);
}
function buildFileNode(file: File, ctx: PageTreeBuilderContext): PageTree.Item {
function buildFileNode(
file: PageFile,
ctx: PageTreeBuilderContext,
): PageTree.Item {
const localized =
findLocalizedFile(file.file.flattenedPath, 'page', ctx) ?? file;
const data = localized.data as FileData['file'];
const item: PageTree.Item = {
type: 'page',
name: data.data.title,
icon: ctx.options.resolveIcon?.(data.data.icon),
url: ctx.options.getUrl(data.slugs, ctx.lang),
name: localized.data.data.title,
icon: ctx.options.resolveIcon?.(localized.data.data.icon),
url: ctx.options.getUrl(localized.data.slugs, ctx.lang),
};
return removeUndefined(ctx.options.attachFile?.(item, file) ?? item);
@@ -272,11 +275,11 @@ export function createPageTreeBuilder(): PageTreeBuilder {
};
}
function findLocalizedFile(
function findLocalizedFile<F extends File['format']>(
path: string,
format: 'meta' | 'page',
format: F,
ctx: PageTreeBuilderContext,
): File | undefined {
): Extract<File, { format: F }> | undefined {
if (!ctx.lang) return;
return ctx.storage.read(`${path}.${ctx.lang}`, format);
-13
View File
@@ -29,16 +29,3 @@ export type InferMetaType<Utils extends LoaderOutput<any>> =
* @internal
*/
export type UrlFn = (slugs: string[], locale?: string) => string;
/**
* @internal
*/
export interface FileData {
meta: {
data: MetaData;
};
file: {
slugs: string[];
data: PageData;
};
}
+10 -3
View File
@@ -7,8 +7,15 @@ describe('Virtual Storage', () => {
const storage = new Storage();
test('Writing', () => {
storage.write('test.mdx', 'page', {});
storage.write('meta.json', 'meta', {});
storage.write('test.mdx', 'page', {
slugs: [],
data: {
title: 'Hello',
},
});
storage.write('meta.json', 'meta', {
data: {},
});
expect(storage.list().length).toBe(2);
});
@@ -20,7 +27,7 @@ describe('Virtual Storage', () => {
test('Nested Directories', () => {
storage.write('dir1/dir2/meta.json', 'meta', {
data: 'Hello',
data: {},
});
expect(storage.readDir('dir1')).toBeDefined();