mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 23:49:51 +00:00
MDX: Support defineDocs without re-exporting docs and meta collections
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-mdx': minor
|
||||
---
|
||||
|
||||
Support `defineDocs` without re-exporting `docs` and `meta` collections
|
||||
@@ -1,8 +1,11 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'tailwindcss' source(none);
|
||||
@import 'fumadocs-ui/css/neutral.css';
|
||||
@import 'fumadocs-ui/css/preset.css';
|
||||
@import 'fumadocs-twoslash/twoslash.css';
|
||||
|
||||
@source ".";
|
||||
@source "../components";
|
||||
@source "../content";
|
||||
@source '../node_modules/fumadocs-ui/dist/**/*.js';
|
||||
@source '../node_modules/fumadocs-openapi/dist/**/*.js';
|
||||
@plugin 'tailwindcss-animate';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { loader } from 'fumadocs-core/source';
|
||||
import { icons } from 'lucide-react';
|
||||
import { attachFile, createOpenAPI } from 'fumadocs-openapi/server';
|
||||
import { createElement } from 'react';
|
||||
import { meta, docs, blog as blogPosts } from '@/.source';
|
||||
import { docs, blog as blogPosts } from '@/.source';
|
||||
|
||||
export const source = loader({
|
||||
baseUrl: '/docs',
|
||||
@@ -12,7 +12,7 @@ export const source = loader({
|
||||
if (icon && icon in icons)
|
||||
return createElement(icons[icon as keyof typeof icons]);
|
||||
},
|
||||
source: createMDXSource(docs, meta),
|
||||
source: docs.toFumadocsSource(),
|
||||
pageTree: {
|
||||
attachFile,
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ import rehypeKatex from 'rehype-katex';
|
||||
import { z } from 'zod';
|
||||
import { rehypeCodeDefaultOptions } from 'fumadocs-core/mdx-plugins';
|
||||
|
||||
export const { docs, meta } = defineDocs({
|
||||
export const docs = defineDocs({
|
||||
docs: {
|
||||
async: true,
|
||||
schema: frontmatterSchema.extend({
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { GlobalConfig } from '@/config/types';
|
||||
import type { ProcessorOptions } from '@mdx-js/mdx';
|
||||
import {
|
||||
InternalDocCollection,
|
||||
InternalMetaCollection,
|
||||
LoadedConfig,
|
||||
} from '@/utils/load-config';
|
||||
import type { LoadedConfig } from '@/utils/load-config';
|
||||
import type {
|
||||
DocsCollection,
|
||||
DocCollection,
|
||||
MetaCollection,
|
||||
} from '@/config/define';
|
||||
|
||||
export function buildConfig(
|
||||
config: Record<string, unknown>,
|
||||
@@ -17,12 +18,16 @@ export function buildConfig(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof v === 'object' && '_doc' in v && v._doc === 'collections') {
|
||||
collections.set(
|
||||
k,
|
||||
v as unknown as InternalMetaCollection | InternalDocCollection,
|
||||
);
|
||||
continue;
|
||||
if (typeof v === 'object' && 'type' in v) {
|
||||
if (v.type === 'docs') {
|
||||
collections.set(k, v as DocsCollection);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (v.type === 'doc' || v.type === 'meta') {
|
||||
collections.set(k, v as unknown as MetaCollection | DocCollection);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (k === 'default') {
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { type ProcessorOptions } from '@mdx-js/mdx';
|
||||
import { type MDXOptions } from '@/utils/build-mdx';
|
||||
import {
|
||||
type BaseCollectionEntry,
|
||||
type GlobalConfig,
|
||||
type MarkdownProps,
|
||||
} from '@/config/types';
|
||||
import { type GlobalConfig } from '@/config/types';
|
||||
import { frontmatterSchema, metaSchema } from '@/utils/schema';
|
||||
import { StandardSchemaV1 } from '@standard-schema/spec';
|
||||
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
||||
|
||||
export interface TransformContext {
|
||||
path: string;
|
||||
@@ -54,6 +50,12 @@ export interface DocCollection<
|
||||
async?: Async;
|
||||
}
|
||||
|
||||
export interface DocsCollection {
|
||||
type: 'docs';
|
||||
docs: DocCollection;
|
||||
meta: MetaCollection;
|
||||
}
|
||||
|
||||
export function defineCollections<
|
||||
T extends 'doc' | 'meta',
|
||||
Schema extends StandardSchemaV1 = StandardSchemaV1,
|
||||
@@ -63,28 +65,14 @@ export function defineCollections<
|
||||
? DocCollection<Schema, Async>
|
||||
: MetaCollection<Schema>),
|
||||
): {
|
||||
_doc: 'collections';
|
||||
type: T;
|
||||
|
||||
_type: {
|
||||
async: Async;
|
||||
|
||||
runtime: T extends 'doc'
|
||||
? Async extends true
|
||||
? StandardSchemaV1.InferOutput<Schema> &
|
||||
BaseCollectionEntry & {
|
||||
load: () => Promise<MarkdownProps>;
|
||||
}
|
||||
: Omit<MarkdownProps, keyof StandardSchemaV1.InferOutput<Schema>> &
|
||||
StandardSchemaV1.InferOutput<Schema> &
|
||||
BaseCollectionEntry
|
||||
: typeof options extends MetaCollection
|
||||
? StandardSchemaV1.InferOutput<Schema> & BaseCollectionEntry
|
||||
: never;
|
||||
schema: Schema;
|
||||
};
|
||||
} {
|
||||
return {
|
||||
_doc: 'collections',
|
||||
// @ts-expect-error -- internal type inferring
|
||||
_type: undefined,
|
||||
...options,
|
||||
@@ -103,15 +91,17 @@ export function defineDocs<
|
||||
*/
|
||||
dir?: string | string[];
|
||||
|
||||
docs?: Partial<DocCollection<DocData, DocAsync>>;
|
||||
meta?: Partial<MetaCollection<MetaData>>;
|
||||
docs?: Omit<DocCollection<DocData, DocAsync>, 'dir' | 'type'>;
|
||||
meta?: Omit<MetaCollection<MetaData>, 'dir' | 'type'>;
|
||||
}): {
|
||||
type: 'docs';
|
||||
docs: ReturnType<typeof defineCollections<'doc', DocData, DocAsync>>;
|
||||
meta: ReturnType<typeof defineCollections<'meta', MetaData, false>>;
|
||||
} {
|
||||
const dir = options?.dir ?? 'content/docs';
|
||||
|
||||
return {
|
||||
type: 'docs',
|
||||
docs: defineCollections({
|
||||
type: 'doc',
|
||||
dir,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { type AnyZodObject, type z } from 'zod';
|
||||
import type { MDXProps } from 'mdx/types';
|
||||
import type { StructuredData } from 'fumadocs-core/mdx-plugins';
|
||||
import type { TableOfContents } from 'fumadocs-core/server';
|
||||
import { type DefaultMDXOptions } from '@/utils/mdx-options';
|
||||
import type { FC } from 'react';
|
||||
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
||||
|
||||
export interface GlobalConfig {
|
||||
/**
|
||||
@@ -37,7 +37,9 @@ export type InferSchema<CollectionOut> = CollectionOut extends {
|
||||
: never;
|
||||
|
||||
export type InferSchemaType<C> =
|
||||
InferSchema<C> extends AnyZodObject ? z.output<InferSchema<C>> : never;
|
||||
InferSchema<C> extends StandardSchemaV1
|
||||
? StandardSchemaV1.InferOutput<InferSchema<C>>
|
||||
: never;
|
||||
|
||||
export interface FileInfo {
|
||||
path: string;
|
||||
@@ -56,25 +58,6 @@ export interface MarkdownProps {
|
||||
lastModified?: Date;
|
||||
}
|
||||
|
||||
export type CollectionEntry<
|
||||
CollectionOut extends {
|
||||
_type: {
|
||||
runtime: unknown;
|
||||
};
|
||||
},
|
||||
> = CollectionOut['_type']['runtime'];
|
||||
|
||||
export interface BaseCollectionEntry {
|
||||
_file: FileInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get output type of collections
|
||||
*/
|
||||
export type GetOutput<
|
||||
C extends {
|
||||
_type: {
|
||||
runtime: unknown;
|
||||
};
|
||||
},
|
||||
> = CollectionEntry<C>[];
|
||||
|
||||
@@ -61,6 +61,7 @@ export default async function loader(
|
||||
? config.collections.get(collectionId)
|
||||
: undefined;
|
||||
|
||||
if (collection && collection.type === 'docs') collection = collection.docs;
|
||||
if (collection && collection.type !== 'doc') {
|
||||
collection = undefined;
|
||||
}
|
||||
|
||||
@@ -2,26 +2,28 @@ import * as path from 'node:path';
|
||||
import fg from 'fast-glob';
|
||||
import { getTypeFromPath } from '@/utils/get-type-from-path';
|
||||
import type { FileInfo } from '@/config/types';
|
||||
import {
|
||||
type InternalDocCollection,
|
||||
type InternalMetaCollection,
|
||||
type LoadedConfig,
|
||||
} from '@/utils/load-config';
|
||||
import { type LoadedConfig } from '@/utils/load-config';
|
||||
import type { DocCollection, MetaCollection } from '@/config';
|
||||
|
||||
export async function generateJS(
|
||||
configPath: string,
|
||||
config: LoadedConfig,
|
||||
outputPath: string,
|
||||
configHash: string,
|
||||
getFrontmatter: (file: string) => Promise<unknown>,
|
||||
getFrontmatter: (file: string) => unknown | Promise<unknown>,
|
||||
): Promise<string> {
|
||||
const outDir = path.dirname(outputPath);
|
||||
const imports: ImportInfo[] = [
|
||||
{
|
||||
type: 'named',
|
||||
names: ['toRuntime'],
|
||||
names: ['_runtime'],
|
||||
specifier: 'fumadocs-mdx',
|
||||
},
|
||||
{
|
||||
type: 'namespace',
|
||||
specifier: toImportPath(configPath, outDir),
|
||||
name: '_source',
|
||||
},
|
||||
];
|
||||
let asyncInit = false;
|
||||
const lines: string[] = [];
|
||||
@@ -29,59 +31,73 @@ export async function generateJS(
|
||||
config._runtime.files.clear();
|
||||
const entries = Array.from(config.collections.entries());
|
||||
|
||||
const declares = entries.map(async ([k, collection]) => {
|
||||
const files = await getCollectionFiles(collection);
|
||||
|
||||
if (collection.type === 'doc' && collection.async) {
|
||||
if (!asyncInit) {
|
||||
imports.push(
|
||||
{
|
||||
type: 'namespace',
|
||||
specifier: toImportPath(configPath, outDir),
|
||||
name: '_source',
|
||||
},
|
||||
{
|
||||
type: 'named',
|
||||
specifier: 'fumadocs-mdx/runtime/async',
|
||||
names: ['asyncFiles', 'buildConfig'],
|
||||
},
|
||||
);
|
||||
|
||||
lines.unshift(
|
||||
'const [err, _sourceConfig] = buildConfig(_source)',
|
||||
'if (err) throw new Error(err)',
|
||||
);
|
||||
|
||||
asyncInit = true;
|
||||
}
|
||||
|
||||
const entries = files.map(async (file) => {
|
||||
const frontmatter = await getFrontmatter(file.absolutePath);
|
||||
|
||||
return JSON.stringify({
|
||||
frontmatter,
|
||||
file,
|
||||
});
|
||||
});
|
||||
|
||||
return `export const ${k} = asyncFiles([${(await Promise.all(entries)).join(', ')}], "${k}", _sourceConfig)`;
|
||||
}
|
||||
|
||||
async function getEntries(collectionName: string, files: FileInfo[]) {
|
||||
const items = files.map(async (file, i) => {
|
||||
config._runtime.files.set(file.absolutePath, k);
|
||||
const importId = `${k}_${i}`;
|
||||
config._runtime.files.set(file.absolutePath, collectionName);
|
||||
const importId = `${collectionName}_${i}`;
|
||||
|
||||
imports.push({
|
||||
type: 'namespace',
|
||||
name: importId,
|
||||
specifier: `${toImportPath(file.absolutePath, outDir)}?collection=${k}&hash=${configHash}`,
|
||||
specifier: `${toImportPath(file.absolutePath, outDir)}?collection=${collectionName}&hash=${configHash}`,
|
||||
});
|
||||
|
||||
return `toRuntime("${collection.type}", ${importId}, ${JSON.stringify(file)})`;
|
||||
return `{ info: ${JSON.stringify(file)}, data: ${importId} }`;
|
||||
});
|
||||
|
||||
const resolvedItems = await Promise.all(items);
|
||||
return `export const ${k} = [${resolvedItems.join(', ')}];`;
|
||||
return Promise.all(items);
|
||||
}
|
||||
|
||||
async function getAsyncEntries(files: FileInfo[]) {
|
||||
if (!asyncInit) {
|
||||
imports.push({
|
||||
type: 'named',
|
||||
specifier: 'fumadocs-mdx/runtime/async',
|
||||
names: ['_runtimeAsync', 'buildConfig'],
|
||||
});
|
||||
|
||||
lines.unshift(
|
||||
'const [err, _sourceConfig] = buildConfig(_source)',
|
||||
'if (err) throw new Error(err)',
|
||||
);
|
||||
|
||||
asyncInit = true;
|
||||
}
|
||||
|
||||
const entries = files.map(async (file) => {
|
||||
const frontmatter = await getFrontmatter(file.absolutePath);
|
||||
|
||||
return JSON.stringify({
|
||||
info: file,
|
||||
data: frontmatter,
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(entries);
|
||||
}
|
||||
|
||||
const declares = entries.map(async ([k, collection]) => {
|
||||
if (collection.type === 'docs') {
|
||||
const docs = await getCollectionFiles(collection.docs);
|
||||
const metas = await getCollectionFiles(collection.meta);
|
||||
|
||||
if (collection.docs.async) {
|
||||
const docsEntries = (await getAsyncEntries(docs)).join(', ');
|
||||
const metaEntries = (await getEntries(k, metas)).join(', ');
|
||||
|
||||
return `export const ${k} = _runtimeAsync.docs<typeof _source.${k}>([${docsEntries}], [${metaEntries}], "${k}", _sourceConfig)`;
|
||||
}
|
||||
|
||||
return `export const ${k} = _runtime.docs<typeof _source.${k}>([${(await getEntries(k, docs)).join(', ')}], [${(await getEntries(k, metas)).join(', ')}])`;
|
||||
}
|
||||
|
||||
const files = await getCollectionFiles(collection);
|
||||
|
||||
if (collection.type === 'doc' && collection.async) {
|
||||
return `export const ${k} = _runtimeAsync.doc<typeof _source.${k}>([${(await getAsyncEntries(files)).join(', ')}], "${k}", _sourceConfig)`;
|
||||
}
|
||||
|
||||
return `export const ${k} = _runtime.${collection.type}<typeof _source.${k}>([${(await getEntries(k, files)).join(', ')}]);`;
|
||||
});
|
||||
|
||||
const resolvedDeclares = await Promise.all(declares);
|
||||
@@ -92,7 +108,7 @@ export async function generateJS(
|
||||
}
|
||||
|
||||
async function getCollectionFiles(
|
||||
collection: InternalDocCollection | InternalMetaCollection,
|
||||
collection: DocCollection | MetaCollection,
|
||||
): Promise<FileInfo[]> {
|
||||
const files = new Map<string, FileInfo>();
|
||||
const dirs = Array.isArray(collection.dir)
|
||||
@@ -163,24 +179,3 @@ export function toImportPath(file: string, dir: string): string {
|
||||
|
||||
return importPath.replaceAll(path.sep, '/');
|
||||
}
|
||||
|
||||
export function generateTypes(
|
||||
configPath: string,
|
||||
config: LoadedConfig,
|
||||
outputPath: string,
|
||||
): string {
|
||||
const importPath = JSON.stringify(
|
||||
toImportPath(configPath, path.dirname(outputPath)),
|
||||
);
|
||||
const lines: string[] = [
|
||||
'import type { GetOutput } from "fumadocs-mdx/config"',
|
||||
];
|
||||
|
||||
for (const name of config.collections.keys()) {
|
||||
lines.push(
|
||||
`export declare const ${name}: GetOutput<typeof import(${importPath}).${name}>`,
|
||||
);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as path from 'node:path';
|
||||
import * as fs from 'node:fs';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import grayMatter from 'gray-matter';
|
||||
import { getConfigHash, loadConfigCached } from '@/utils/config-cache';
|
||||
import { generateJS, generateTypes } from '@/map/generate';
|
||||
import { generateJS } from '@/map/generate';
|
||||
import { readFrontmatter } from '@/utils/read-frontmatter';
|
||||
|
||||
/**
|
||||
* Start a MDX server that builds index and manifest files.
|
||||
@@ -17,64 +17,25 @@ export async function start(
|
||||
): Promise<void> {
|
||||
let configHash = await getConfigHash(configPath);
|
||||
let config = await loadConfigCached(configPath, configHash);
|
||||
const jsOut = path.resolve(outDir, `index.js`);
|
||||
const typeOut = path.resolve(outDir, `index.d.ts`);
|
||||
const jsOut = path.resolve(outDir, `index.ts`);
|
||||
|
||||
const frontmatterCache = new Map<string, unknown>();
|
||||
let hookUpdate = false;
|
||||
|
||||
/**
|
||||
* Read frontmatter via stream, it is faster for large Markdown/MDX files
|
||||
*/
|
||||
async function readFrontmatter(file: string): Promise<unknown> {
|
||||
hookUpdate = true;
|
||||
const cached = frontmatterCache.get(file);
|
||||
if (cached) return cached;
|
||||
|
||||
const readStream = fs.createReadStream(file, {
|
||||
highWaterMark: 250,
|
||||
});
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
let idx = 0;
|
||||
let str = '';
|
||||
|
||||
readStream.on('data', (_chunk) => {
|
||||
const chunk = _chunk.toString();
|
||||
if (idx === 0 && !chunk.startsWith('---')) {
|
||||
res({});
|
||||
readStream.close();
|
||||
return;
|
||||
}
|
||||
|
||||
str += chunk;
|
||||
idx++;
|
||||
|
||||
if (str.includes('\n---')) {
|
||||
res(
|
||||
grayMatter({
|
||||
content: str,
|
||||
}).data,
|
||||
);
|
||||
|
||||
readStream.close();
|
||||
}
|
||||
});
|
||||
|
||||
readStream.on('end', () => res({}));
|
||||
readStream.on('error', (e) => rej(e));
|
||||
}).then((res) => {
|
||||
frontmatterCache.set(file, res);
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
jsOut,
|
||||
await generateJS(configPath, config, jsOut, configHash, readFrontmatter),
|
||||
await generateJS(configPath, config, jsOut, configHash, (file) => {
|
||||
hookUpdate = true;
|
||||
const cached = frontmatterCache.get(file);
|
||||
if (cached) return cached;
|
||||
|
||||
return readFrontmatter(file).then((res) => {
|
||||
frontmatterCache.set(file, res);
|
||||
return res;
|
||||
});
|
||||
}),
|
||||
);
|
||||
fs.writeFileSync(typeOut, generateTypes(configPath, config, typeOut));
|
||||
console.log('[MDX] initialized map file');
|
||||
|
||||
if (dev) {
|
||||
@@ -94,8 +55,6 @@ export async function start(
|
||||
if (isConfigFile) {
|
||||
configHash = await getConfigHash(configPath);
|
||||
config = await loadConfigCached(configPath, configHash);
|
||||
await writeFile(typeOut, generateTypes(configPath, config, typeOut));
|
||||
console.log('[MDX] Updated map types');
|
||||
}
|
||||
|
||||
if (isConfigFile || event !== 'change' || hookUpdate) {
|
||||
|
||||
@@ -4,9 +4,17 @@ import { type LoadedConfig } from '@/utils/load-config';
|
||||
export function watcher(configPath: string, config: LoadedConfig): FSWatcher {
|
||||
const deps: string[] = [configPath];
|
||||
|
||||
function add(dir: string | string[]) {
|
||||
if (Array.isArray(dir)) deps.push(...dir);
|
||||
else deps.push(dir);
|
||||
}
|
||||
for (const collection of config.collections.values()) {
|
||||
if (Array.isArray(collection.dir)) deps.push(...collection.dir);
|
||||
else deps.push(collection.dir);
|
||||
if (collection.type === 'docs') {
|
||||
add(collection.docs.dir);
|
||||
add(collection.meta.dir);
|
||||
} else {
|
||||
add(collection.dir);
|
||||
}
|
||||
}
|
||||
|
||||
return watch(deps, {
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
import * as path from 'node:path';
|
||||
import * as fs from 'node:fs';
|
||||
import { findConfigFile, loadConfig } from '@/utils/load-config';
|
||||
import { generateTypes } from '@/map/generate';
|
||||
import { generateJS } from '@/map/generate';
|
||||
import { getConfigHash } from '@/utils/config-cache';
|
||||
import { readFrontmatter } from '@/utils/read-frontmatter';
|
||||
|
||||
export async function postInstall(
|
||||
configPath = findConfigFile(),
|
||||
): Promise<void> {
|
||||
const typeOut = path.resolve('.source/index.d.ts');
|
||||
const jsOut = path.resolve('.source/index.ts');
|
||||
const config = await loadConfig(configPath);
|
||||
const hash = await getConfigHash(configPath);
|
||||
|
||||
fs.mkdirSync(path.dirname(typeOut), { recursive: true });
|
||||
fs.writeFileSync(typeOut, generateTypes(configPath, config, typeOut));
|
||||
fs.mkdirSync(path.dirname(jsOut), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
jsOut,
|
||||
await generateJS(
|
||||
configPath,
|
||||
config,
|
||||
path.resolve('.source/index.ts'),
|
||||
hash,
|
||||
readFrontmatter,
|
||||
),
|
||||
);
|
||||
console.log('[MDX] types generated');
|
||||
}
|
||||
|
||||
@@ -1,78 +1,123 @@
|
||||
import { type CollectionEntry, type FileInfo } from '@/config/types';
|
||||
import { type BaseCollectionEntry, type MarkdownProps } from '@/config/types';
|
||||
import { createCompiler, type MDXOptions } from '@fumadocs/mdx-remote';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import type { z } from 'zod';
|
||||
import type { LoadedConfig } from '@/utils/load-config';
|
||||
import { remarkInclude } from '@/mdx-plugins/remark-include';
|
||||
import type { defineCollections } from '@/config/define';
|
||||
import type { frontmatterSchema } from '@/utils/schema';
|
||||
import {
|
||||
remarkStructure,
|
||||
type StructuredData,
|
||||
} from 'fumadocs-core/mdx-plugins';
|
||||
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
||||
import { _runtime, createMDXSource, type RuntimeFile } from '@/runtime/index';
|
||||
import type { MetaData, PageData, Source } from 'fumadocs-core/source';
|
||||
|
||||
type T = ReturnType<
|
||||
typeof defineCollections<'doc', typeof frontmatterSchema, true>
|
||||
>;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function asyncFiles(
|
||||
files: {
|
||||
file: FileInfo;
|
||||
frontmatter: Record<string, unknown>;
|
||||
}[],
|
||||
collection: string,
|
||||
config: LoadedConfig,
|
||||
): unknown[] {
|
||||
async function init() {
|
||||
const col = config.collections.get(collection);
|
||||
let mdxOptions: MDXOptions;
|
||||
|
||||
if (col?.type === 'doc' && col.mdxOptions) {
|
||||
mdxOptions = col.mdxOptions as MDXOptions;
|
||||
} else {
|
||||
const options =
|
||||
typeof config.global?.mdxOptions === 'function'
|
||||
? await config.global.mdxOptions()
|
||||
: config.global?.mdxOptions;
|
||||
const remarkPlugins = options?.remarkPlugins ?? [];
|
||||
|
||||
mdxOptions = {
|
||||
...options,
|
||||
remarkPlugins: (v) =>
|
||||
typeof remarkPlugins === 'function'
|
||||
? [remarkInclude, ...remarkPlugins(v), remarkStructure]
|
||||
: [remarkInclude, ...v, ...remarkPlugins, remarkStructure],
|
||||
};
|
||||
}
|
||||
|
||||
return createCompiler(mdxOptions);
|
||||
export interface RuntimeAsync {
|
||||
doc: <C>(
|
||||
files: RuntimeFile[],
|
||||
collection: string,
|
||||
config: LoadedConfig,
|
||||
) => C extends ReturnType<
|
||||
typeof defineCollections<'doc', infer Schema extends StandardSchemaV1, true>
|
||||
>
|
||||
? (StandardSchemaV1.InferOutput<Schema> &
|
||||
BaseCollectionEntry & {
|
||||
load: () => Promise<MarkdownProps>;
|
||||
})[]
|
||||
: never;
|
||||
docs: <Docs>(
|
||||
docs: RuntimeFile[],
|
||||
metas: RuntimeFile[],
|
||||
collection: string,
|
||||
config: LoadedConfig,
|
||||
) => Docs extends {
|
||||
type: 'docs';
|
||||
docs: unknown;
|
||||
meta: unknown;
|
||||
}
|
||||
|
||||
const initCompiler = init();
|
||||
|
||||
return files.map(({ file, frontmatter }) => {
|
||||
return {
|
||||
...(frontmatter as z.infer<typeof frontmatterSchema>),
|
||||
_file: file,
|
||||
async load() {
|
||||
const compiler = await initCompiler;
|
||||
const out = await compiler.compile({
|
||||
source: (await fs.readFile(file.absolutePath)).toString(),
|
||||
filePath: file.absolutePath,
|
||||
});
|
||||
|
||||
return {
|
||||
body: out.body,
|
||||
toc: out.toc,
|
||||
structuredData: out.vfile.data.structuredData as StructuredData,
|
||||
_exports: out.exports ?? {},
|
||||
};
|
||||
},
|
||||
} satisfies CollectionEntry<T>;
|
||||
});
|
||||
? {
|
||||
docs: ReturnType<typeof _runtimeAsync.doc<Docs['docs']>>;
|
||||
meta: ReturnType<typeof _runtime.meta<Docs['meta']>>;
|
||||
toFumadocsSource: () => Source<{
|
||||
pageData: ReturnType<
|
||||
typeof _runtimeAsync.doc<Docs['docs']>
|
||||
>[number] extends PageData & BaseCollectionEntry
|
||||
? ReturnType<typeof _runtimeAsync.doc<Docs['docs']>>[number]
|
||||
: never;
|
||||
metaData: ReturnType<
|
||||
typeof _runtime.meta<Docs['meta']>
|
||||
>[number] extends MetaData & BaseCollectionEntry
|
||||
? ReturnType<typeof _runtime.meta<Docs['meta']>>[number]
|
||||
: never;
|
||||
}>;
|
||||
}
|
||||
: never;
|
||||
}
|
||||
|
||||
async function initCompiler(config: LoadedConfig, collection: string) {
|
||||
let col = config.collections.get(collection);
|
||||
if (col?.type === 'docs') col = col.docs;
|
||||
|
||||
let mdxOptions: MDXOptions;
|
||||
|
||||
if (col?.type === 'doc' && col.mdxOptions) {
|
||||
mdxOptions = col.mdxOptions as MDXOptions;
|
||||
} else {
|
||||
const options =
|
||||
typeof config.global?.mdxOptions === 'function'
|
||||
? await config.global.mdxOptions()
|
||||
: config.global?.mdxOptions;
|
||||
const remarkPlugins = options?.remarkPlugins ?? [];
|
||||
|
||||
mdxOptions = {
|
||||
...options,
|
||||
remarkPlugins: (v) =>
|
||||
typeof remarkPlugins === 'function'
|
||||
? [remarkInclude, ...remarkPlugins(v), remarkStructure]
|
||||
: [remarkInclude, ...v, ...remarkPlugins, remarkStructure],
|
||||
};
|
||||
}
|
||||
|
||||
return createCompiler(mdxOptions);
|
||||
}
|
||||
|
||||
export const _runtimeAsync: RuntimeAsync = {
|
||||
doc(files, collection, config) {
|
||||
const init = initCompiler(config, collection);
|
||||
|
||||
return files.map(({ info: file, data: frontmatter }) => {
|
||||
return {
|
||||
...frontmatter,
|
||||
_file: file,
|
||||
async load() {
|
||||
const compiler = await init;
|
||||
const out = await compiler.compile({
|
||||
source: (await fs.readFile(file.absolutePath)).toString(),
|
||||
filePath: file.absolutePath,
|
||||
});
|
||||
|
||||
return {
|
||||
body: out.body,
|
||||
toc: out.toc,
|
||||
structuredData: out.vfile.data.structuredData as StructuredData,
|
||||
_exports: out.exports ?? {},
|
||||
};
|
||||
},
|
||||
};
|
||||
}) as any;
|
||||
},
|
||||
docs(docs, metas, collection, config) {
|
||||
const parsedDocs = this.doc(docs, collection, config);
|
||||
const parsedMetas = _runtime.meta(metas);
|
||||
|
||||
return {
|
||||
docs: parsedDocs,
|
||||
meta: parsedMetas,
|
||||
toFumadocsSource() {
|
||||
return createMDXSource(parsedDocs, parsedMetas);
|
||||
},
|
||||
} as any;
|
||||
},
|
||||
};
|
||||
|
||||
export { buildConfig } from '@/config/build';
|
||||
|
||||
@@ -3,31 +3,104 @@ import {
|
||||
type PageData,
|
||||
type Source,
|
||||
} from 'fumadocs-core/source';
|
||||
import { type BaseCollectionEntry, type FileInfo } from '@/config';
|
||||
import {
|
||||
type BaseCollectionEntry,
|
||||
type defineCollections,
|
||||
type DocsCollection,
|
||||
type FileInfo,
|
||||
type MarkdownProps,
|
||||
} from '@/config';
|
||||
import type { VirtualFile } from 'fumadocs-core/source';
|
||||
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
||||
|
||||
export function toRuntime(
|
||||
type: 'doc' | 'meta',
|
||||
file: Record<string, unknown>,
|
||||
info: FileInfo,
|
||||
): unknown {
|
||||
if (type === 'doc') {
|
||||
const { default: body, frontmatter, ...exports } = file;
|
||||
export interface RuntimeFile {
|
||||
info: FileInfo;
|
||||
data: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface Runtime {
|
||||
doc: <C>(
|
||||
files: RuntimeFile[],
|
||||
) => C extends ReturnType<
|
||||
typeof defineCollections<
|
||||
'doc',
|
||||
infer Schema extends StandardSchemaV1,
|
||||
false
|
||||
>
|
||||
>
|
||||
? (Omit<MarkdownProps, keyof StandardSchemaV1.InferOutput<Schema>> &
|
||||
StandardSchemaV1.InferOutput<Schema> &
|
||||
BaseCollectionEntry)[]
|
||||
: never;
|
||||
meta: <C>(
|
||||
files: RuntimeFile[],
|
||||
) => C extends ReturnType<
|
||||
typeof defineCollections<
|
||||
'meta',
|
||||
infer Schema extends StandardSchemaV1,
|
||||
false
|
||||
>
|
||||
>
|
||||
? (StandardSchemaV1.InferOutput<Schema> & BaseCollectionEntry)[]
|
||||
: never;
|
||||
docs: <Docs>(
|
||||
docs: RuntimeFile[],
|
||||
metas: RuntimeFile[],
|
||||
) => Docs extends DocsCollection
|
||||
? {
|
||||
docs: ReturnType<typeof _runtime.doc<Docs['docs']>>;
|
||||
meta: ReturnType<typeof _runtime.meta<Docs['meta']>>;
|
||||
toFumadocsSource: () => Source<{
|
||||
pageData: ReturnType<
|
||||
typeof _runtime.doc<Docs['docs']>
|
||||
>[number] extends PageData & BaseCollectionEntry
|
||||
? ReturnType<typeof _runtime.doc<Docs['docs']>>[number]
|
||||
: never;
|
||||
metaData: ReturnType<
|
||||
typeof _runtime.meta<Docs['meta']>
|
||||
>[number] extends MetaData & BaseCollectionEntry
|
||||
? ReturnType<typeof _runtime.meta<Docs['meta']>>[number]
|
||||
: never;
|
||||
}>;
|
||||
}
|
||||
: never;
|
||||
}
|
||||
|
||||
export const _runtime: Runtime = {
|
||||
doc(files) {
|
||||
return files.map((file) => {
|
||||
const { default: body, frontmatter, ...exports } = file.data;
|
||||
|
||||
return {
|
||||
body,
|
||||
...exports,
|
||||
...(frontmatter as object),
|
||||
_exports: file.data,
|
||||
_file: file.info,
|
||||
};
|
||||
}) as any;
|
||||
},
|
||||
meta(files) {
|
||||
return files.map((file) => {
|
||||
return {
|
||||
...(file.data.default as object),
|
||||
_file: file.info,
|
||||
};
|
||||
}) as any;
|
||||
},
|
||||
docs(docs, metas) {
|
||||
const parsedDocs = this.doc(docs);
|
||||
const parsedMetas = this.meta(metas);
|
||||
|
||||
return {
|
||||
body,
|
||||
...exports,
|
||||
...(frontmatter as object),
|
||||
_exports: file,
|
||||
_file: info,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...(file.default as object),
|
||||
_file: info,
|
||||
};
|
||||
}
|
||||
docs: parsedDocs,
|
||||
meta: parsedMetas,
|
||||
toFumadocsSource() {
|
||||
return createMDXSource(parsedDocs, parsedMetas);
|
||||
},
|
||||
} as any;
|
||||
},
|
||||
};
|
||||
|
||||
export function createMDXSource<
|
||||
Doc extends PageData & BaseCollectionEntry,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import * as path from 'node:path';
|
||||
import type { DocCollection, MetaCollection } from '@/config/define';
|
||||
import type {
|
||||
DocCollection,
|
||||
DocsCollection,
|
||||
MetaCollection,
|
||||
} from '@/config/define';
|
||||
import { type GlobalConfig } from '@/config/types';
|
||||
import type { ProcessorOptions } from '@mdx-js/mdx';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
@@ -10,7 +14,7 @@ export function findConfigFile(): string {
|
||||
}
|
||||
|
||||
export interface LoadedConfig {
|
||||
collections: Map<string, InternalDocCollection | InternalMetaCollection>;
|
||||
collections: Map<string, DocCollection | MetaCollection | DocsCollection>;
|
||||
getDefaultMDXOptions: () => Promise<ProcessorOptions>;
|
||||
global?: GlobalConfig;
|
||||
|
||||
@@ -22,9 +26,6 @@ export interface LoadedConfig {
|
||||
};
|
||||
}
|
||||
|
||||
export type InternalDocCollection = DocCollection;
|
||||
export type InternalMetaCollection = MetaCollection;
|
||||
|
||||
export async function loadConfig(configPath: string): Promise<LoadedConfig> {
|
||||
const { build } = await import('esbuild');
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import * as fs from 'node:fs';
|
||||
import grayMatter from 'gray-matter';
|
||||
|
||||
/**
|
||||
* Read frontmatter via stream, it is faster for large Markdown/MDX files
|
||||
*/
|
||||
export async function readFrontmatter(file: string): Promise<unknown> {
|
||||
const readStream = fs.createReadStream(file, {
|
||||
highWaterMark: 250,
|
||||
});
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
let idx = 0;
|
||||
let str = '';
|
||||
|
||||
readStream.on('data', (_chunk) => {
|
||||
const chunk = _chunk.toString();
|
||||
if (idx === 0 && !chunk.startsWith('---')) {
|
||||
res({});
|
||||
readStream.close();
|
||||
return;
|
||||
}
|
||||
|
||||
str += chunk;
|
||||
idx++;
|
||||
|
||||
if (str.includes('\n---')) {
|
||||
res(
|
||||
grayMatter({
|
||||
content: str,
|
||||
}).data,
|
||||
);
|
||||
|
||||
readStream.close();
|
||||
}
|
||||
});
|
||||
|
||||
readStream.on('end', () => res({}));
|
||||
readStream.on('error', (e) => rej(e));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user