mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 23:49:51 +00:00
Chore: Support custom base directory for content sources
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@fumadocs/local-md': patch
|
||||
'@fumadocs/sanity': patch
|
||||
'fumadocs-mdx': patch
|
||||
---
|
||||
|
||||
Support custom base directory for content sources
|
||||
@@ -40,18 +40,19 @@ export function splitPath(path: string): string[] {
|
||||
*/
|
||||
export function joinPath(...paths: string[]): string {
|
||||
const out = [];
|
||||
const parsed = paths.flatMap((path) => path.split('/'));
|
||||
|
||||
for (const seg of parsed) {
|
||||
switch (seg) {
|
||||
case '..':
|
||||
out.pop();
|
||||
break;
|
||||
case '':
|
||||
case '.':
|
||||
break;
|
||||
default:
|
||||
out.push(seg);
|
||||
for (const path of paths) {
|
||||
for (const seg of path.split('/')) {
|
||||
switch (seg) {
|
||||
case '..':
|
||||
out.pop();
|
||||
break;
|
||||
case '':
|
||||
case '.':
|
||||
break;
|
||||
default:
|
||||
out.push(seg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,3 +68,17 @@ export function slash(path: string): string {
|
||||
|
||||
return path.replaceAll('\\', '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert (relative) file path to virtual file path.
|
||||
*
|
||||
* @param path - Relative path
|
||||
* @returns Normalized path, with no trailing/leading slashes
|
||||
* @throws Throws error if path starts with `./` or `../`
|
||||
*/
|
||||
export function normalize(path: string): string {
|
||||
const segments = path.split(/\/|\\/).filter((v) => v.length > 0);
|
||||
if (segments[0] === '.' || segments[0] === '..')
|
||||
throw new Error("It must not start with './' or '../'");
|
||||
return segments.join('/');
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FileSystem } from '@/source/storage/file-system';
|
||||
import { slash, splitPath } from '@/source/path';
|
||||
import { normalize } from '@/source/path';
|
||||
import type { ResolvedLoaderConfig } from '../loader';
|
||||
import { isStaticSource, MetaData, PageData, type StaticSource } from '../source';
|
||||
|
||||
@@ -99,7 +99,7 @@ export function createContentStorageBuilder(loaderConfig: ResolvedLoaderConfig)
|
||||
file = {
|
||||
format: 'page',
|
||||
type,
|
||||
path: normalizePath(inputFile.path),
|
||||
path: normalize(inputFile.path),
|
||||
// will be generated by the slugs plugin if unspecified
|
||||
slugs: inputFile.slugs as string[],
|
||||
data: inputFile.data,
|
||||
@@ -109,7 +109,7 @@ export function createContentStorageBuilder(loaderConfig: ResolvedLoaderConfig)
|
||||
file = {
|
||||
format: 'meta',
|
||||
type,
|
||||
path: normalizePath(inputFile.path),
|
||||
path: normalize(inputFile.path),
|
||||
absolutePath: inputFile.absolutePath,
|
||||
data: inputFile.data,
|
||||
};
|
||||
@@ -171,18 +171,6 @@ export function createContentStorageBuilder(loaderConfig: ResolvedLoaderConfig)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path - Relative path
|
||||
* @returns Normalized path, with no trailing/leading slashes
|
||||
* @throws Throws error if path starts with `./` or `../`
|
||||
*/
|
||||
function normalizePath(path: string): string {
|
||||
const segments = splitPath(slash(path));
|
||||
if (segments[0] === '.' || segments[0] === '..')
|
||||
throw new Error("It must not start with './' or '../'");
|
||||
return segments.join('/');
|
||||
}
|
||||
|
||||
function pushMapList<K, V>(map: Map<K, V[]>, k: K, v: V) {
|
||||
let list = map.get(k);
|
||||
if (!list) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type {
|
||||
DynamicSource,
|
||||
MetaData,
|
||||
PageData,
|
||||
StaticSource,
|
||||
VirtualFile,
|
||||
import {
|
||||
type DynamicSource,
|
||||
type MetaData,
|
||||
type PageData,
|
||||
type StaticSource,
|
||||
type VirtualFile,
|
||||
} from 'fumadocs-core/source';
|
||||
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
||||
import path from 'node:path';
|
||||
@@ -45,13 +45,17 @@ export interface LocalMarkdown<
|
||||
> {
|
||||
/** connect to dev server, required for hot reload */
|
||||
devServer: (url?: string) => Promise<void>;
|
||||
staticSource: <ModuleExports = Record<string, unknown>>() => Promise<
|
||||
staticSource: <ModuleExports = Record<string, unknown>>(
|
||||
options?: SourceOptions,
|
||||
) => Promise<
|
||||
StaticSource<{
|
||||
pageData: LocalMarkdownPage<StandardSchemaV1.InferOutput<FrontmatterSchema>, ModuleExports>;
|
||||
metaData: StandardSchemaV1.InferOutput<MetaSchema> & MetaData;
|
||||
}>
|
||||
>;
|
||||
dynamicSource: <ModuleExports = Record<string, unknown>>() => DynamicSource<{
|
||||
dynamicSource: <ModuleExports = Record<string, unknown>>(
|
||||
options?: SourceOptions,
|
||||
) => DynamicSource<{
|
||||
pageData: LocalMarkdownPage<StandardSchemaV1.InferOutput<FrontmatterSchema>, ModuleExports>;
|
||||
metaData: StandardSchemaV1.InferOutput<MetaSchema> & MetaData;
|
||||
}>;
|
||||
@@ -72,6 +76,11 @@ export interface LocalMarkdownPage<
|
||||
load: () => Promise<MarkdownRenderer<ModuleExports>>;
|
||||
}
|
||||
|
||||
interface SourceOptions {
|
||||
/** base directory for virtual file paths */
|
||||
baseDir?: string;
|
||||
}
|
||||
|
||||
export function localMd<
|
||||
FrontmatterSchema extends StandardSchemaV1 = typeof defaultSchemas.pageSchema,
|
||||
MetaSchema extends StandardSchemaV1 = typeof defaultSchemas.metaSchema,
|
||||
@@ -89,8 +98,9 @@ export function localMd<
|
||||
}>
|
||||
> | null = null;
|
||||
|
||||
async function createFiles() {
|
||||
async function createFiles(options?: SourceOptions) {
|
||||
const { metas, pages } = await storage.getPages();
|
||||
const baseDir = options?.baseDir;
|
||||
const files: VirtualFile<{
|
||||
pageData: LocalMarkdownPage<StandardSchemaV1.InferOutput<FrontmatterSchema>, never>;
|
||||
metaData: StandardSchemaV1.InferOutput<MetaSchema> & MetaData;
|
||||
@@ -101,7 +111,7 @@ export function localMd<
|
||||
|
||||
files.push({
|
||||
type: 'page',
|
||||
path: page.path,
|
||||
path: baseDir ? path.join(baseDir, page.path) : page.path,
|
||||
absolutePath: page.absolutePath,
|
||||
data: {
|
||||
title: frontmatter.title ?? path.basename(page.path, path.extname(page.path)),
|
||||
@@ -145,7 +155,7 @@ export function localMd<
|
||||
for (const meta of metas) {
|
||||
files.push({
|
||||
type: 'meta',
|
||||
path: meta.path,
|
||||
path: baseDir ? path.join(baseDir, meta.path) : meta.path,
|
||||
absolutePath: meta.absolutePath,
|
||||
data: meta.data!,
|
||||
});
|
||||
@@ -183,16 +193,16 @@ export function localMd<
|
||||
}
|
||||
});
|
||||
},
|
||||
dynamicSource() {
|
||||
dynamicSource(opts) {
|
||||
return {
|
||||
files: createFiles,
|
||||
files: () => createFiles(opts),
|
||||
configure(loader) {
|
||||
registeredLoaders.add(loader);
|
||||
},
|
||||
};
|
||||
},
|
||||
staticSource() {
|
||||
return (cachedStaticSource ??= createFiles().then((files) => ({ files })));
|
||||
staticSource(opts) {
|
||||
return (cachedStaticSource ??= createFiles(opts).then((files) => ({ files })));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MetaData, PageData, Source, VirtualFile } from 'fumadocs-core/source';
|
||||
import { type MetaData, type PageData, type Source, type VirtualFile } from 'fumadocs-core/source';
|
||||
import * as path from 'node:path';
|
||||
import type { DocCollection, DocsCollection, MetaCollection } from '@/config';
|
||||
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
||||
@@ -13,6 +13,11 @@ export type DocCollectionEntry<
|
||||
TC extends InternalTypeConfig = InternalTypeConfig,
|
||||
> = DocData & DocMethods & Frontmatter & TC['DocData'][Name];
|
||||
|
||||
interface ToFumadocsSourceOptions {
|
||||
/** base directory for virtual file paths */
|
||||
baseDir?: string;
|
||||
}
|
||||
|
||||
export type AsyncDocCollectionEntry<
|
||||
Name extends string = string,
|
||||
Frontmatter = unknown,
|
||||
@@ -30,7 +35,7 @@ export interface DocsCollectionEntry<
|
||||
> {
|
||||
docs: DocCollectionEntry<Name, Frontmatter, TC>[];
|
||||
meta: MetaCollectionEntry<Meta>[];
|
||||
toFumadocsSource: () => Source<{
|
||||
toFumadocsSource: (options?: ToFumadocsSourceOptions) => Source<{
|
||||
pageData: DocCollectionEntry<Name, Frontmatter, TC>;
|
||||
metaData: MetaCollectionEntry<Meta>;
|
||||
}>;
|
||||
@@ -44,7 +49,7 @@ export interface AsyncDocsCollectionEntry<
|
||||
> {
|
||||
docs: AsyncDocCollectionEntry<Name, Frontmatter, TC>[];
|
||||
meta: MetaCollectionEntry<Meta>[];
|
||||
toFumadocsSource: () => Source<{
|
||||
toFumadocsSource: (options?: ToFumadocsSourceOptions) => Source<{
|
||||
pageData: AsyncDocCollectionEntry<Name, Frontmatter, TC>;
|
||||
metaData: MetaCollectionEntry<Meta>;
|
||||
}>;
|
||||
@@ -175,8 +180,8 @@ export function server<Config, TC extends InternalTypeConfig>(options: ServerOpt
|
||||
const entry = {
|
||||
docs: await this.doc(name, base, docGlob),
|
||||
meta: await this.meta(name, base, metaGlob),
|
||||
toFumadocsSource() {
|
||||
return toFumadocsSource(this.docs, this.meta);
|
||||
toFumadocsSource(options) {
|
||||
return toFumadocsSource(this.docs, this.meta, options);
|
||||
},
|
||||
} satisfies DocsCollectionEntry;
|
||||
|
||||
@@ -203,8 +208,8 @@ export function server<Config, TC extends InternalTypeConfig>(options: ServerOpt
|
||||
const entry = {
|
||||
docs: await this.docLazy(name, base, docHeadGlob, docBodyGlob),
|
||||
meta: await this.meta(name, base, metaGlob),
|
||||
toFumadocsSource() {
|
||||
return toFumadocsSource(this.docs, this.meta);
|
||||
toFumadocsSource(options) {
|
||||
return toFumadocsSource(this.docs, this.meta, options);
|
||||
},
|
||||
} satisfies AsyncDocsCollectionEntry;
|
||||
|
||||
@@ -230,10 +235,12 @@ export function toFumadocsSource<
|
||||
>(
|
||||
pages: Page[],
|
||||
metas: Meta[],
|
||||
options?: ToFumadocsSourceOptions,
|
||||
): Source<{
|
||||
pageData: Page;
|
||||
metaData: Meta;
|
||||
}> {
|
||||
const baseDir = options?.baseDir;
|
||||
const files: VirtualFile<{
|
||||
pageData: Page;
|
||||
metaData: Meta;
|
||||
@@ -242,7 +249,7 @@ export function toFumadocsSource<
|
||||
for (const entry of pages) {
|
||||
files.push({
|
||||
type: 'page',
|
||||
path: entry.info.path,
|
||||
path: baseDir ? path.join(baseDir, entry.info.path) : entry.info.path,
|
||||
absolutePath: entry.info.fullPath,
|
||||
data: entry,
|
||||
});
|
||||
@@ -251,7 +258,7 @@ export function toFumadocsSource<
|
||||
for (const entry of metas) {
|
||||
files.push({
|
||||
type: 'meta',
|
||||
path: entry.info.path,
|
||||
path: baseDir ? path.join(baseDir, entry.info.path) : entry.info.path,
|
||||
absolutePath: entry.info.fullPath,
|
||||
data: entry,
|
||||
});
|
||||
|
||||
@@ -15,7 +15,9 @@ export default defineConfig({
|
||||
},
|
||||
target: 'es2023',
|
||||
platform: 'neutral',
|
||||
exports: true,
|
||||
exports: {
|
||||
bin: false,
|
||||
},
|
||||
deps: {
|
||||
onlyBundle: ['@fumadocs/vite'],
|
||||
neverBundle: ['webpack', 'bun', /^node:/],
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { ReactNode } from 'react';
|
||||
import type { PortableTextBlock, SlugValue } from 'sanity';
|
||||
import type { QueryParams, SanityClient } from '@sanity/client';
|
||||
import { renderToc } from './client';
|
||||
import path from 'node:path';
|
||||
|
||||
export type SanityOptions<Doc extends BaseDoc = BaseDoc> =
|
||||
| GenericSanityOptions<Doc>
|
||||
@@ -17,6 +18,9 @@ interface BaseSanityOptions<Doc extends BaseDoc> {
|
||||
|
||||
/** generate [virtual file path](https://fumadocs.dev/docs/headless/source-api/source#static-source) from document */
|
||||
generatePath?: (doc: ShallowDoc<Doc>) => string;
|
||||
|
||||
/** base directory for the virutal file paths */
|
||||
baseDir?: string;
|
||||
}
|
||||
|
||||
export interface GenericSanityOptions<
|
||||
@@ -61,7 +65,7 @@ export function createSanitySource<Doc extends BaseDoc>(
|
||||
pageData: DocToPage<Doc>;
|
||||
metaData: MetaData;
|
||||
}> {
|
||||
const { docType, generatePath } = options;
|
||||
const { docType, baseDir, generatePath } = options;
|
||||
let sanityFetch: <R = unknown>(query: string, params?: QueryParams) => Promise<R>;
|
||||
|
||||
if ('sanityFetch' in options) {
|
||||
@@ -86,6 +90,12 @@ export function createSanitySource<Doc extends BaseDoc>(
|
||||
|
||||
return data.map((file) => {
|
||||
const slugs = file.slug?.current?.split('/').filter((v) => v.length > 0) ?? [];
|
||||
let filePath = generatePath
|
||||
? generatePath(file)
|
||||
: slugs.length === 0
|
||||
? 'index.mdx'
|
||||
: `${slugs.join('/')}.mdx`;
|
||||
if (baseDir) filePath = path.join(baseDir, filePath);
|
||||
|
||||
return {
|
||||
type: 'page',
|
||||
@@ -142,11 +152,7 @@ export function createSanitySource<Doc extends BaseDoc>(
|
||||
},
|
||||
},
|
||||
slugs,
|
||||
path: generatePath
|
||||
? generatePath(file)
|
||||
: slugs.length === 0
|
||||
? 'index.mdx'
|
||||
: `${slugs.join('/')}.mdx`,
|
||||
path: filePath,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user