---
title: "Local Markdown Runtime"
description: "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 ..."
last_updated: "2026-10-12T07:53:59.831064+00:00"
canonical_url: "https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/content-sources/local-markdown-runtime"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [packages/local-md/src/client.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/client.ts)
- [packages/local-md/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/index.ts)
- [packages/local-md/src/md/compiler.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/md/compiler.ts)
- [packages/mdx-remote/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx-remote/src/index.ts)
- [packages/local-md/src/md/renderer.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/md/renderer.ts)
- [packages/local-md/src/js/executor-native.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/js/executor-native.ts)
- [packages/local-md/src/js/executor-virtual.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/js/executor-virtual.ts)
- [packages/core/src/content/md.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/content/md.ts)
- [packages/mdx-remote/src/render.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx-remote/src/render.ts)
- [packages/mdx/src/runtime/dynamic.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/runtime/dynamic.ts)
- [packages/local-md/src/dev/react-client.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/dev/react-client.ts)
- [packages/local-md/package.json](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/package.json)
- [packages/preview/src/pages/...slugs.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/pages/%5B...slugs%5D.tsx)
- [packages/obsidian/src/mdx/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/obsidian/src/mdx/index.ts)
- [packages/mdx-remote/src/compile.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx-remote/src/compile.ts)
- [packages/preview/src/components/markdown.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/components/markdown.tsx)
- [packages/local-md/src/dev/node-server.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/dev/node-server.ts)
- [packages/typescript/src/markdown.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/typescript/src/markdown.ts)
- [packages/mdx/src/runtime/server.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/runtime/server.ts)
- [packages/local-md/src/eval-estree-expression/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/eval-estree-expression/index.ts)
- [packages/twoslash/src/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/twoslash/src/index.ts)
- [packages/local-md/src/bin.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/bin.ts)
- [packages/core/src/mdx-plugins/remark-llms.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/remark-llms.ts)
- [packages/core/src/mdx-plugins/remark-llms.runtime.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/mdx-plugins/remark-llms.runtime.ts)
- [packages/mdx/src/loaders/mdx/remark-include.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/loaders/mdx/remark-include.ts)
- [packages/mdx/src/runtime/browser.tsx](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/runtime/browser.tsx)
- [packages/preview/src/lib/md.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/preview/src/lib/md.ts)
- [packages/mdx/src/loaders/mdx/index.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/mdx/src/loaders/mdx/index.ts)
- [packages/local-md/src/dev/node-client.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/dev/node-client.ts)
- [packages/core/src/content/mdx/preset-runtime.ts](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/core/src/content/mdx/preset-runtime.ts)
</details>

## 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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/index.ts#L1-L64)

## 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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/index.ts#L24-L40), [packages/local-md/src/index.ts:84-89](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/index.ts#L84-L89)

### 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](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/core-architecture/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.

```mermaid
sequenceDiagram
    participant Source as staticSource / dynamicSource
    participant Storage as storage.getPages()
    participant Compiler as compiler.compile()
    participant Renderer as fromAst / fromJS

    Source->>Storage: Fetch raw pages and meta files
    Storage-->>Source: Return pages & metas arrays
    Source->>Source: Build VirtualFile list with baseDir paths
    Note over Source,Renderer: Page.load() invoked lazily
    Source->>Compiler: Compile markdown content & frontmatter
    Compiler-->>Source: Return CompileResult (ast or code)
    Source->>Renderer: Generate MarkdownRenderer (fromAst or fromJS)
    Renderer-->>Source: Return rendered result
```

Sources: [packages/local-md/src/index.ts:42-64](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/index.ts#L42-L64), [packages/local-md/src/index.ts:101-165](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/index.ts#L101-L165)

### 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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/index.ts#L90-L93), [packages/local-md/src/index.ts:125-133](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/index.ts#L125-L133), [packages/local-md/src/index.ts:168-195](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/index.ts#L168-L195)

## 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:
1. `localMd` invokes `createMarkdownCompiler(config)` to instantiate the compiler engine.
Sources: [packages/local-md/src/index.ts:91-91](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/index.ts#L91-L91)
2. `createMarkdownCompiler` sets up lazy helper functions and returns the compiler object containing the `compile(input)` method.
Sources: [packages/local-md/src/md/compiler.ts:62-161](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/md/compiler.ts#L62-L161)
3. When `.mdx` files are processed, `compile()` invokes `createMdxCompiler()` to construct and cache the MDX processor using `@mdx-js/mdx`.
Sources: [packages/local-md/src/md/compiler.ts:105-118](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/md/compiler.ts#L105-L118), [packages/local-md/src/md/compiler.ts:143-145](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/md/compiler.ts#L143-L145)

```mermaid
sequenceDiagram
    participant LocalMd as localMd
    participant Compiler as createMarkdownCompiler
    participant MdxCompiler as createMdxCompiler

    LocalMd->>Compiler: createMarkdownCompiler(config)
    Compiler-->>LocalMd: Return MarkdownCompiler
    LocalMd->>Compiler: compile(input)
    Compiler->>MdxCompiler: createMdxCompiler() [if .mdx]
    MdxCompiler-->>Compiler: Return Mdx Processor
    Compiler-->>LocalMd: Return CompileResult (js or ast)
```

Sources: [packages/local-md/src/md/compiler.ts:62-65](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/md/compiler.ts#L62-L65), [packages/local-md/src/md/compiler.ts:105-118](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/md/compiler.ts#L105-L118), [packages/local-md/src/md/compiler.ts:141-160](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/md/compiler.ts#L141-L160)

### 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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/md/compiler.ts#L66-L103), [packages/local-md/src/md/compiler.ts:105-137](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/md/compiler.ts#L105-L137)

## 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.

```typescript
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/md/renderer.ts#L11-L22)

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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/md/renderer.ts#L30-L57)

### Structured Data and AST-to-React Transformation

When rendering from an Abstract Syntax Tree using `fromAst`, the engine maps [table of contents](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/ui-layouts/table-of-contents) items generated by rehype plugins into React nodes while executing through `hast-util-to-jsx-runtime`. 

```typescript
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/md/renderer.ts#L59-L106)

> [!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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/client.ts#L1-L1), [packages/local-md/src/md/renderer.ts:188-191](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/md/renderer.ts#L188-L191)

## 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.

```typescript
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/js/executor-virtual.ts#L7-L20)

### 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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/eval-estree-expression/index.ts#L49-L80)

> [!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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/eval-estree-expression/index.ts#L49-L68)

> [!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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/eval-estree-expression/index.ts#L55-L57), [packages/local-md/src/eval-estree-expression/index.ts:82-86](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/eval-estree-expression/index.ts#L82-L86)

## 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.

```typescript
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/js/executor-native.ts#L6-L24)

> [!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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/js/executor-native.ts#L13-L15)

### 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.

```typescript
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/js/executor-native.ts#L44-L74)

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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/js/executor-native.ts#L98-L153)

> [!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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/js/executor-native.ts#L140-L148)

## 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.

```typescript
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/bin.ts#L14-L62)

| 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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/bin.ts#L18-L18), [packages/local-md/src/bin.ts:33-41](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/bin.ts#L33-L41), [packages/local-md/src/bin.ts:78-86](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/bin.ts#L78-L86)

> [!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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/bin.ts#L78-L86)

### 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`.

```typescript
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/dev/node-server.ts#L33-L56)

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.

```typescript
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/dev/node-server.ts#L101-L111)

> [!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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/dev/node-server.ts#L95-L98), [packages/local-md/src/dev/node-server.ts:150-162](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/dev/node-server.ts#L150-L162)

### 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.

```typescript
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/dev/react-client.ts#L6-L39)

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.

```typescript
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](https://github.com/blade47/fumadocs/blob/a9f988897e7dc4ef3d62b2865d80da2f5607701e/packages/local-md/src/dev/node-client.ts#L11-L39)

## Related

- [Source Loader](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/technical/core-architecture/source-loader)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/e8872a72-1909-479c-b585-dc74c6e26726/llms.txt) for all pages in this wiki.
