Relevant source files
The following files were used as context for generating this wiki page:
- packages/local-md/src/client.ts
- packages/local-md/src/index.ts
- packages/local-md/src/md/compiler.ts
- packages/mdx-remote/src/index.ts
- packages/local-md/src/md/renderer.ts
- packages/local-md/src/js/executor-native.ts
- packages/local-md/src/js/executor-virtual.ts
- packages/core/src/content/md.ts
- packages/mdx-remote/src/render.ts
- packages/mdx/src/runtime/dynamic.ts
- packages/local-md/src/dev/react-client.ts
- packages/local-md/package.json
- packages/preview/src/pages/...slugs.tsx
- packages/obsidian/src/mdx/index.ts
- packages/mdx-remote/src/compile.ts
- packages/preview/src/components/markdown.tsx
- packages/local-md/src/dev/node-server.ts
- packages/typescript/src/markdown.ts
- packages/mdx/src/runtime/server.ts
- packages/local-md/src/eval-estree-expression/index.ts
- packages/twoslash/src/index.ts
- packages/local-md/src/bin.ts
- packages/core/src/mdx-plugins/remark-llms.ts
- packages/core/src/mdx-plugins/remark-llms.runtime.ts
- packages/mdx/src/loaders/mdx/remark-include.ts
- packages/mdx/src/runtime/browser.tsx
- packages/preview/src/lib/md.ts
- packages/mdx/src/loaders/mdx/index.ts
- packages/local-md/src/dev/node-client.ts
- packages/core/src/content/mdx/preset-runtime.ts
Overview
The @fumadocs/local-md package provides a content source for Fumadocs that scans, compiles, and renders local Markdown and MDX files on the fly. It integrates directly with Fumadocs source loaders by discovering content directories, mapping frontmatter schemas via Standard Schema validators, and exposing both static and dynamic source generators for documentation pages.
Sources: packages/local-md/src/index.ts:1-64
Local source integration
Configuration and factory options
The localMd factory function accepts a configuration object conforming to LocalMarkdownConfig, which extends markdown compiler options with directory paths, file glob patterns, validation schemas, and renderer options. The configuration maps strongly typed frontmatter and meta schemas using the @standard-schema/spec standard.
| Option | Type | Default | Purpose |
|---|---|---|---|
dir | string | — | Root directory for content files scanned by the storage layer. |
include | string[] | defaultInclude | List of glob patterns customizing content files to be scanned. |
rendererOptions | Pick<MarkdownRendererASTOptions, 'executor'> | — | Options passed directly to the markdown renderer, such as custom script executors. |
frontmatterSchema | FrontmatterSchema | defaultSchemas.pageSchema | Standard Schema validator for page frontmatter objects. |
metaSchema | MetaSchema | defaultSchemas.metaSchema | Standard Schema validator for meta files. |
Sources: packages/local-md/src/index.ts:24-40, packages/local-md/src/index.ts:84-89
File loading and virtual source pipeline
The localMd return object exposes methods for generating staticSource and dynamicSource collections, managing file invalidation, and connecting to development servers Source Loader. The internal createFiles helper function retrieves raw pages and meta files from the storage layer, constructs VirtualFile objects with computed base directories, and attaches lazy load() functions to each page.
Sources: packages/local-md/src/index.ts:42-64, packages/local-md/src/index.ts:101-165
Hot reload and cache invalidation
When a file changes, invalidateFile resolves the absolute file path, resets the cached static source promise to null, invalidates the underlying storage cache, and iterates over all registered dynamic loaders to trigger v.invalidate(). The devServer method attempts to establish a WebSocket connection using an environment URL or explicit parameter, sends a watch-dir event configured with config.dir and config.include (falling back to defaultInclude), and listens for change events to invoke invalidateFile.
Note
The compilerCache WeakMap stores compilation promises keyed by raw page references, ensuring that repeated calls to load() on the same page reuse ongoing or completed compilation tasks without redundant disk reads.
Sources: packages/local-md/src/index.ts:90-93, packages/local-md/src/index.ts:125-133, packages/local-md/src/index.ts:168-195
Markdown compilation pipeline
Compiler factory and execution trace
The compilation engine in packages/local-md/src/md/compiler.ts exports createMarkdownCompiler, which returns an object with a compile method supporting both standard markdown and MDX inputs. Depending on whether the file extension is .mdx, the compiler dispatches processing through specific pipeline builders.
The call-chain execution walkthrough for compiling local documents follows this exact sequence:
localMdinvokescreateMarkdownCompiler(config)to instantiate the compiler engine. Sources: packages/local-md/src/index.ts:91-91createMarkdownCompilersets up lazy helper functions and returns the compiler object containing thecompile(input)method. Sources: packages/local-md/src/md/compiler.ts:62-161- When
.mdxfiles are processed,compile()invokescreateMdxCompiler()to construct and cache the MDX processor using@mdx-js/mdx. Sources: packages/local-md/src/md/compiler.ts:105-118, packages/local-md/src/md/compiler.ts:143-145
Sources: packages/local-md/src/md/compiler.ts:62-65, packages/local-md/src/md/compiler.ts:105-118, packages/local-md/src/md/compiler.ts:141-160
Syntax plugin resolution and processor options
Both Markdown and MDX compilers integrate a standard suite of remark and rehype plugins. These include remarkGfm for GitHub-Flavored Markdown support, remarkHeading, remarkNpm, remarkCodeTab, remarkStructure, rehypeCode, and rehypeToc. Each plugin can be conditionally disabled or customized by passing explicit option flags in MarkdownCompilerOptions.
| Plugin Option Property | Default Value / Behavior | Target Plugin |
|---|---|---|
remarkHeadingOptions | { generateToc: false, ...remarkHeadingOptions } | remarkHeading |
remarkNpmOptions | Passed directly if not false | remarkNpm |
remarkCodeTabOptions | Passed directly if not false | remarkCodeTab |
remarkStructureOptions | Passed directly if not false | remarkStructure |
rehypeCodeOptions | Passed directly if not false | rehypeCode |
rehypeTocOptions | { exportToc: { as: 'data' }, ...rehypeTocOptions } (MDX uses { as: 'esm', name: 'toc' }) | rehypeToc |
Note
Setting any of the plugin options to explicit boolean false (e.g., remarkHeadingOptions: false) completely disables that specific syntax transformation during pipeline initialization.
Sources: packages/local-md/src/md/compiler.ts:66-103, packages/local-md/src/md/compiler.ts:105-137
AST and serialized rendering
MarkdownRenderer Interface and Options
The rendering layer defined in packages/local-md/src/md/renderer.ts provides unified interfaces for managing compiled outputs, whether executed via native JavaScript compilation or abstract syntax tree transformation. The core abstraction is the MarkdownRenderer interface, which normalizes structured data retrieval, synchronous and asynchronous rendering passes, and serialization options.
export interface MarkdownRenderer<M = Record<string, unknown>> {
structuredData: StructuredData;
render: (
components?: MDXComponents,
context?: Record<string, unknown>,
) => Promise<MarkdownRendererResult<M>>;
renderSync: (
components?: MDXComponents,
context?: Record<string, unknown>,
) => MarkdownRendererResult<M>;
serialize: () => MarkdownRendererSerializedOptions;
}Sources: packages/local-md/src/md/renderer.ts:11-22
The engine supports two primary serialization options via MarkdownRendererSerializedOptions, discriminating between JavaScript execution outputs and AST trees.
| Option Type Property | Configuration Interface | Purpose / Fields |
|---|---|---|
type: 'js' | MarkdownRendererJSOptions | Code string execution containing code, filePath, optional baseUrl, and structuredData. |
type: 'ast' | MarkdownRendererASTOptions | HAST syntax tree processing containing tree, filePath, optional structuredData, rehypeToc, and an optional executor. |
Note
The executor option in MarkdownRendererASTOptions is ignored for MDX files, as MDX always utilizes the native JavaScript runtime engine.
Sources: packages/local-md/src/md/renderer.ts:30-57
Structured Data and AST-to-React Transformation
When rendering from an Abstract Syntax Tree using fromAst, the engine maps table of contents items generated by rehype plugins into React nodes while executing through hast-util-to-jsx-runtime.
export function fromAst<M>(options: MarkdownRendererASTOptions): MarkdownRenderer<M> {
const {
executor: getExecutor = executorVirtual,
filePath,
structuredData = {
headings: [],
contents: [],
},
rehypeToc = [],
tree,
} = options;
function renderSync(components?: MDXComponents, userContext?: Record<string, unknown>) {
const context = { ...components, ...userContext };
const executor = getExecutor({
jsx: JsxRuntime,
filePath,
});
const evaluater = toEvaluater(executor, context);
function render(tree: Root): ReactNode {
return toJsxRuntime(tree, {
filePath,
components,
development: false,
createEvaluater() {
return evaluater;
},
...JsxRuntime,
});
}
const toc = rehypeToc.map(
(item): TOCItemType => ({
...item,
title: render({
type: 'root',
children: item.title.children,
}),
}),
);
return {
toc,
body: render(tree),
exports: executor.getExports() as M,
};
}Sources: packages/local-md/src/md/renderer.ts:59-106
Tip
The rendererFromSerialized helper exported from packages/local-md/src/client.ts proxies directly to fromSerialized, checking the payload type to instantiate either fromJS or fromAst dynamically.
Sources: packages/local-md/src/client.ts:1-1, packages/local-md/src/md/renderer.ts:188-191
Virtual expression evaluation
Sandbox Engine and Executor Configuration
The virtual expression execution layer provides a lightweight sandboxed JavaScript evaluation engine specifically designed to function within constrained environments like Cloudflare's workerd runtime. Defined in packages/local-md/src/js/executor-virtual.ts, the executorVirtual function accepts a JSExecutorConfig options object and instantiates an ExpressionSync class from packages/local-md/src/eval-estree-expression/index.ts. It returns a standardized JSExecutor interface supporting expression evaluation, program execution, and export retrieval.
export function executorVirtual(options: JSExecutorConfig): JSExecutor {
const sync = new ExpressionSync(options);
return {
expression(expression, context) {
return sync.evaluate(expression, context);
},
program(expression, context) {
return sync.evaluate(expression, context);
},
getExports() {
return sync.getExports();
},
};
}Sources: packages/local-md/src/js/executor-virtual.ts:7-20
Expression Tree Traversal and Security Boundaries
The ExpressionSync class manages export maps and handles AST node evaluation through a dynamic visitor pattern. To prevent prototype pollution and unauthorized access to constructor or prototype chains during property lookups, the engine maintains strict key validation checks.
| Security Constant / Helper | Target Values / Types | Purpose / Constraint |
|---|---|---|
UNSAFE_KEYS | 'constructor', 'prototype', '__proto__', '__defineGetter__' | Set of blocked property names prohibited during key resolution. |
isSafeKey | string | number | Validates that property keys are strings or numbers and do not belong to UNSAFE_KEYS. |
isReturnValue | Object where type === RETURN | Internal symbol marker used to handle return statement unwrapping across block scopes. |
EvaluateOptions | Optional object with jsx?: Pick<typeof JsxRuntime, 'Fragment' | 'jsx' | 'jsxs'> | Optional configuration mapping React JSX runtime methods for evaluation. |
Sources: packages/local-md/src/eval-estree-expression/index.ts:49-80
Caution
Property access checking via isSafeKey actively blocks lookups involving constructor, prototype, and __proto__. Attempting to evaluate expressions containing these keys will reject property resolution to secure the evaluation sandbox against prototype manipulation.
Sources: packages/local-md/src/eval-estree-expression/index.ts:49-68
Note
The ExpressionSync constructor accepts optional evaluation parameters (EvaluateOptions) which inject JSX runtime functions required when rendering JSX elements inside evaluated AST expressions.
Sources: packages/local-md/src/eval-estree-expression/index.ts:55-57, packages/local-md/src/eval-estree-expression/index.ts:82-86
Native JavaScript execution
ESTree JSX Lowering and Context Initialization
The native execution engine implemented in packages/local-md/src/js/executor-native.ts provides server-side execution of JavaScript and JSX statements using new Function(). The executorNative function accepts a JSExecutorConfig options object containing the file path and JSX runtime hooks. It initializes an exports dictionary alongside a globalContext map bound to internal runtime variables.
const INTERNAL_VAR_NAMES = {
jsx: '_jsx',
jsxs: '_jsxs',
fragment: '_Fragment',
exports: '__fumadocs_exports',
} as const;
/**
* Execute JavaScript with `new Function()`, this is unsafe.
*/
export function executorNative(options: JSExecutorConfig): JSExecutor {
const { filePath, jsx } = options;
const exports: Record<string, unknown> = {};
const globalContext: Record<string, unknown> = {
[INTERNAL_VAR_NAMES.exports]: exports,
[INTERNAL_VAR_NAMES.jsx]: jsx.jsx,
[INTERNAL_VAR_NAMES.jsxs]: jsx.jsxs,
[INTERNAL_VAR_NAMES.fragment]: jsx.Fragment,
};Sources: packages/local-md/src/js/executor-native.ts:6-24
Warning
Because executorNative evaluates code via new Function(), it is unsafe for untrusted user input and should only be utilized in trusted server environments where dynamic code evaluation is explicitly permitted.
Sources: packages/local-md/src/js/executor-native.ts:13-15
Pattern Matching and Export Transformation
Before converting AST programs to JavaScript strings via toJs, statements are processed using buildJsx with an automatic runtime configuration. The visitor pattern extracts binding names from complex variable declarations and handles various export declarations through explicit assignment generation.
function allNames(pattern: Pattern): string[] {
switch (pattern.type) {
case 'Identifier':
return [pattern.name];
case 'ArrayPattern': {
const out: string[] = [];
for (const element of pattern.elements) {
if (element) out.push(...allNames(element));
}
return out;
}
case 'AssignmentPattern':
return allNames(pattern.left);
case 'MemberExpression':
return [];
case 'ObjectPattern': {
const out: string[] = [];
for (const prop of pattern.properties) {
if (prop.type === 'Property') {
if ('value' in prop && typeof prop.value === 'string') out.push(prop.value);
else if ('name' in prop && typeof prop.name === 'string') out.push(prop.name);
} else {
out.push(...allNames(prop.argument));
}
}
return out;
}
case 'RestElement':
return allNames(pattern.argument);
}
}Sources: packages/local-md/src/js/executor-native.ts:44-74
The program handler inspects statement types to map exports and rewrite declarations into valid assignment expressions.
| Statement Type | Supported Action / Transformation | Error or Fallback Behavior |
|---|---|---|
ExportAllDeclaration | Throws an error since star-exports are unsupported. | Throws Error: ${statement.type} is not supported |
ExportDefaultDeclaration | Rewrites class and function declarations into expressions, assigning them to __fumadocs_exports.default. | Throws if initializer is missing. |
ExportNamedDeclaration | Appends assignment statements for declared classes, functions, or variable patterns. | Throws if initializer is missing. |
Sources: packages/local-md/src/js/executor-native.ts:98-153
Important
When processing ExportNamedDeclaration with VariableDeclaration nodes, the engine recursively traverses identifier patterns using allNames to generate individual export assignments for every destructured or bound identifier.
Sources: packages/local-md/src/js/executor-native.ts:140-148
Development server synchronization
CLI Launcher and Environment Configuration
The command-line interface provided by local-md exposes the dev command via commander to initialize the development server and spawn a user-defined child command, such as a local development server runner. It validates port inputs, injects the running WebSocket development server URL into the process environment using setDevServerUrlInEnv, and forwards system signals like SIGINT and SIGTERM to the active child process.
program
.command('dev')
.description('start the local-md dev server and run a command')
.argument('<command...>', 'command to run')
.option('-p, --port <number>', 'port for the dev server', parsePort, 8000)
.allowUnknownOption()
.passThroughOptions()
.action(async (command: string[], options: { port: number }) => {
const handle = await startDevServer({
port: options.port,
});
setDevServerUrlInEnv(handle.url);
const child = spawn(command[0], command.slice(1), {
stdio: 'inherit',
shell: true,
env: process.env,
});
const forwardSignal = (signal: NodeJS.Signals) => {
if (child.exitCode === null && child.signalCode === null) {
child.kill(signal);
}
};
process.on('SIGINT', forwardSignal);
process.on('SIGTERM', forwardSignal);
let exitCode = 1;
try {
exitCode = await new Promise<number>((resolve, reject) => {
child.once('error', reject);
child.once('exit', (code, signal) => {
if (signal) {
resolve(signalToExitCode(signal));
return;
}
resolve(code ?? 0);
});
});
} finally {
process.off('SIGINT', forwardSignal);
process.off('SIGTERM', forwardSignal);
await handle.close().catch(() => undefined);
}
process.exit(exitCode);
});Sources: packages/local-md/src/bin.ts:14-62
| Signal / Option | Action or Handled Behavior | Resulting Exit Code |
|---|---|---|
--port <number> | Parses and validates the numeric port option (default is 8000). | Throws if invalid. |
SIGINT | Forwards the signal to the spawned child process if it is still running. | Exits with 130. |
SIGTERM | Forwards the signal to the spawned child process if it is still running. | Exits with 143. |
| General exit | Resolves exit code or signal equivalent when the child process terminates. | Default exit 1 on error. |
Sources: packages/local-md/src/bin.ts:18-18, packages/local-md/src/bin.ts:33-41, packages/local-md/src/bin.ts:78-86
Note
The helper function signalToExitCode maps incoming POSIX process signals (SIGINT to 130 and SIGTERM to 143) to standard shell exit codes before terminating the CLI process.
Sources: packages/local-md/src/bin.ts:78-86
WebSocket File Watching and Node Server
The Node.js development server manages file system monitoring with chokidar and handles client synchronization via a ws WebSocket server. startDevServer sets up default watch parameters ignoring non-file stats and matching paths registered across active clients using picomatch.
export async function startDevServer(options: DevServerOptions): Promise<DevServerHandle> {
const { host = '127.0.0.1', port } = options;
const url = `ws://${host}:${port}${LOCAL_MD_DEV_PATH}`;
let watchOptions: ChokidarOptions = {
ignoreInitial: true,
followSymlinks: false,
ignored(file, _stats) {
if (!_stats || !_stats.isFile()) return false;
for (const client of clients) {
for (const [dir, { matcher }] of client.watching) {
if (matcher(path.relative(dir, file))) return false;
}
}
return true;
},
};
if (options.watchOptions) {
watchOptions = options.watchOptions(watchOptions);
}
const watcher = watch([], watchOptions);Sources: packages/local-md/src/dev/node-server.ts:33-56
File watcher events (add, addDir, change, unlink, and unlinkDir) captured by Chokidar are filtered through WATCH_EVENTS and broadcast to all connected clients as change notification payloads.
watcher.on('all', (event, filePath) => {
if (!WATCH_EVENTS.has(event as DevWatchEvent)) return;
console.log(`[@fumadocs/local-md] ${event} at "${filePath}"`);
broadcast({
type: 'change',
event: event as DevWatchEvent,
absolutePath: filePath,
timestamp: Date.now(),
});
});Sources: packages/local-md/src/dev/node-server.ts:101-111
Warning
When a WebSocket client connection closes, the server automatically cleans up associated watches by verifying whether any remaining active clients still reference the watched directory before calling watcher.unwatch(dir).
Sources: packages/local-md/src/dev/node-server.ts:95-98, packages/local-md/src/dev/node-server.ts:150-162
Client Live-Reload Communication
Client connections are managed in the browser and Node environments. The React client component DevClient reads the development server URL from the environment, establishes a WebSocket connection, and refreshes the Next.js router whenever a file change event is decoded.
export function DevClient(): ReactNode {
const router = useRouter();
const onUpdate = useEffectEvent((event: DevServerEvent) => {
if (event.type === 'change') {
console.log(`[@fumadocs/local-md] "${event.absolutePath}" updated`);
router.refresh();
}
});
useEffect(() => {
const url = getDevServerUrlFromEnv();
if (!url) return;
const ws = new WebSocket(url);
ws.onopen = () => {
console.log(`[@fumadocs/local-md] connected to dev server at ${url}`);
};
ws.onmessage = (event) => {
const decoded = decodeDevEvent(String(event.data));
if (decoded) onUpdate(decoded);
};
ws.onclose = () => {
console.log(`[@fumadocs/local-md] disconnected from dev server at ${url}`);
};
return () => {
ws.close();
};
}, []);
return null;Sources: packages/local-md/src/dev/react-client.ts:6-39
For server-side or non-React consumers, connectDevServer maintains a singleton map of DevServerConnection instances. It queues pending client messages if the socket is not yet open and dispatches incoming decoded events to registered subscribers.
export function connectDevServer(url: string): DevServerConnection {
let connection = store.get(url);
if (!connection) {
connection = new DevServerConnection(url);
store.set(url, connection);
}
return connection;
}
export class DevServerConnection {
private readonly listeners = new Set<(event: DevServerEvent) => void>();
private pendingEvents: DevClientEvent[] = [];
private socket?: WebSocket;
readonly url: string;
constructor(url: string) {
this.url = url;
this.connect();
}
subscribe(listener: (event: DevServerEvent) => void) {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}Sources: packages/local-md/src/dev/node-client.ts:11-39
Related
On This Page
OverviewLocal source integrationConfiguration and factory optionsFile loading and virtual source pipelineHot reload and cache invalidationMarkdown compilation pipelineCompiler factory and execution traceSyntax plugin resolution and processor optionsAST and serialized renderingMarkdownRenderer Interface and OptionsStructured Data and AST-to-React TransformationVirtual expression evaluationSandbox Engine and Executor ConfigurationExpression Tree Traversal and Security BoundariesNative JavaScript executionESTree JSX Lowering and Context InitializationPattern Matching and Export TransformationDevelopment server synchronizationCLI Launcher and Environment ConfigurationWebSocket File Watching and Node ServerClient Live-Reload CommunicationRelated