Local MD: implement content source

This commit is contained in:
Fuma Nama
2026-04-16 19:38:40 +08:00
parent 47d8369e64
commit 53cf235de0
8 changed files with 160 additions and 22 deletions
+14 -2
View File
@@ -14,6 +14,11 @@
"dist"
],
"type": "module",
"exports": {
".": "./dist/index.js",
"./dev": "./dist/dev/index.js",
"./package.json": "./package.json"
},
"publishConfig": {
"access": "public"
},
@@ -27,14 +32,21 @@
"dependencies": {
"chokidar": "^5.0.0",
"eval-estree-expression": "^3.0.1",
"ignore": "^7.0.5",
"tinyglobby": "^0.2.16"
"hast-util-to-jsx-runtime": "^2.3.6",
"remark": "^15.0.1",
"remark-rehype": "^11.1.2",
"tinyglobby": "^0.2.16",
"unified": "^11.0.5",
"vfile": "^6.0.3"
},
"devDependencies": {
"@types/hast": "^3.0.4",
"@types/mdx": "^2.0.13",
"@types/node": "25.6.0",
"@types/react": "^19.2.14",
"@types/unist": "^3.0.3",
"fumadocs-core": "workspace:*",
"ignore": "^7.0.5",
"react": "^19.2.5",
"tsconfig": "workspace:*",
"tsdown": "0.21.8"
+1
View File
@@ -0,0 +1 @@
export * from './watcher';
@@ -2,7 +2,7 @@ import { type ChokidarOptions, type Matcher, watch } from 'chokidar';
import ignore, { type Ignore } from 'ignore';
import fs from 'node:fs/promises';
import path from 'node:path';
import { LocalMarkdownConfig } from '.';
import type { LocalMarkdownConfig } from '..';
export async function startWatcher(config: LocalMarkdownConfig) {
// init .gitignore
+3
View File
@@ -24,3 +24,6 @@ export interface LocalMarkdownConfig {
export async function localMd(config: LocalMarkdownConfig) {
return source(await getPages(config));
}
export type { Meta, Page } from './storage';
export * from './md';
+98
View File
@@ -0,0 +1,98 @@
import { remark } from 'remark';
import remarkRehype, { type Options as RemarkRehypeOptions } from 'remark-rehype';
import { type Compatible, VFile } from 'vfile';
import {
type Components,
type Options as RenderOptions,
toJsxRuntime,
} from 'hast-util-to-jsx-runtime';
import type { Root } from 'hast';
import * as JsxRuntime from 'react/jsx-runtime';
import type { Pluggable, PluggableList, Plugin } from 'unified';
import type { ReactNode } from 'react';
// @ts-expect-error -- untyped
import { evaluate } from 'eval-estree-expression';
export interface MarkdownCompilerOptions {
remarkPlugins?: PluggableList;
rehypePlugins?: PluggableList;
remarkRehypeOptions?: RemarkRehypeOptions;
}
export interface CompileResult {
tree: Root;
file: VFile;
render: (components?: Components) => ReactNode;
}
export interface MarkdownCompiler {
compile: (input: Compatible) => Promise<CompileResult>;
render: (tree: Root, file: VFile, components?: Components) => ReactNode;
}
export function createMarkdownCompiler({
rehypePlugins = [],
remarkPlugins = [],
remarkRehypeOptions,
}: MarkdownCompilerOptions = {}): MarkdownCompiler {
const processor = remark()
.use(remarkPlugins)
.use(remarkRehype, remarkRehypeOptions)
.use(rehypePlugins);
function renderOptions(override?: Partial<RenderOptions>): RenderOptions {
return {
development: false,
createEvaluater() {
return {
evaluateProgram(program) {
return evaluate.sync(program, { ...override?.components }, { functions: true });
},
evaluateExpression(node) {
return evaluate.sync(node, { ...override?.components }, { functions: true });
},
};
},
...JsxRuntime,
...override,
};
}
return {
async compile(input) {
const file = new VFile(input);
const tree = await processor.run(processor.parse(file), file);
return {
tree,
file,
render(components) {
return toJsxRuntime(
tree,
renderOptions({
filePath: file.path,
components,
}),
);
},
};
},
render(tree, file, components) {
return toJsxRuntime(
tree,
renderOptions({
filePath: file.path,
components,
}),
);
},
};
}
export function plugin<
PluginParameters extends unknown[],
Input extends string | import('unist').Node | undefined,
Output,
>(plugin: Plugin<PluginParameters, Input, Output>, ...params: NoInfer<PluginParameters>) {
return [plugin, ...params] as Pluggable;
}
+16 -14
View File
@@ -5,20 +5,15 @@ import { metaSchema, pageSchema } from 'fumadocs-core/source/schema';
import { LocalMarkdownConfig } from '.';
import { frontmatter as parseFrontmatter } from 'fumadocs-core/content/md/frontmatter';
export interface RawPage {
export interface Page {
type: 'page';
path: string;
absolutePath: string;
data: {
title: string;
description?: string;
content: string;
frontmatter: Record<string, unknown>;
};
data: PageData;
}
export interface RawMeta {
export interface Meta {
type: 'meta';
path: string;
absolutePath: string;
@@ -29,10 +24,17 @@ export interface RawMeta {
} & Record<string, unknown>;
}
type BuildFileOutput = RawPage | RawMeta | undefined;
export interface PageData {
title: string;
description?: string;
content: string;
frontmatter: Record<string, unknown>;
}
type BuildFileOutput = Page | Meta | undefined;
const CHUNK_SIZE = 100;
export const filesCache = new Map<string, RawPage | RawMeta>();
export const filesCache = new Map<string, Page | Meta>();
async function buildFile(config: LocalMarkdownConfig, file: string): Promise<BuildFileOutput> {
const absolutePath = path.resolve(config.dir, file);
@@ -80,8 +82,8 @@ export async function getPages(config: LocalMarkdownConfig) {
chunks.push(Promise.all(promises));
}
const pages: RawPage[] = [];
const metas: RawMeta[] = [];
const pages: Page[] = [];
const metas: Meta[] = [];
for await (const chunk of chunks) {
for (const item of chunk) {
if (!item) continue;
@@ -92,7 +94,7 @@ export async function getPages(config: LocalMarkdownConfig) {
return { pages, metas };
}
async function md(absolutePath: string, file: string): Promise<RawPage> {
async function md(absolutePath: string, file: string): Promise<Page> {
const content = await fs.readFile(absolutePath, 'utf-8');
const parsed = parseFrontmatter(content);
@@ -112,7 +114,7 @@ async function md(absolutePath: string, file: string): Promise<RawPage> {
};
}
async function json(absolutePath: string, file: string): Promise<RawMeta | undefined> {
async function json(absolutePath: string, file: string): Promise<Meta | undefined> {
const content = await fs.readFile(absolutePath, 'utf-8');
const parsed = JSON.parse(content);
const result = metaSchema.loose().safeParse(parsed);
+3 -2
View File
@@ -4,10 +4,11 @@ export default defineConfig({
dts: true,
fixedExtension: false,
target: 'es2023',
entry: ['./src/index.ts', './src/client/index.ts'],
entry: ['./src/index.ts', './src/dev/index.ts'],
format: 'esm',
exports: true,
deps: {
onlyBundle: [],
onlyBundle: ['ignore'],
neverBundle: ['mdx/types'],
},
});
+24 -3
View File
@@ -2186,13 +2186,28 @@ importers:
eval-estree-expression:
specifier: ^3.0.1
version: 3.0.1
ignore:
specifier: ^7.0.5
version: 7.0.5
hast-util-to-jsx-runtime:
specifier: ^2.3.6
version: 2.3.6
remark:
specifier: ^15.0.1
version: 15.0.1
remark-rehype:
specifier: ^11.1.2
version: 11.1.2
tinyglobby:
specifier: ^0.2.16
version: 0.2.16
unified:
specifier: ^11.0.5
version: 11.0.5
vfile:
specifier: ^6.0.3
version: 6.0.3
devDependencies:
'@types/hast':
specifier: ^3.0.4
version: 3.0.4
'@types/mdx':
specifier: ^2.0.13
version: 2.0.13
@@ -2202,9 +2217,15 @@ importers:
'@types/react':
specifier: ^19.2.14
version: 19.2.14
'@types/unist':
specifier: ^3.0.3
version: 3.0.3
fumadocs-core:
specifier: workspace:*
version: link:../core
ignore:
specifier: ^7.0.5
version: 7.0.5
react:
specifier: ^19.2.5
version: 19.2.5