# How the help() Function Parses JSDoc to Provide Runtime Documentation in ego-lite

> Discover how ego-lite's help() function uses build-time JSDoc extraction for efficient runtime documentation. Learn how it deserializes data for on-demand insights.

- Repository: [CitroLabs/ego-lite](https://github.com/citrolabs/ego-lite)
- Tags: internals
- Published: 2026-08-04

---

**`help()` deserializes pre-computed JSDoc data that was extracted at build time—rather than parsing source files at runtime—to deliver on-demand documentation for ego-lite's helper functions.**

The `help()` function in **ego-lite** (a lightweight browser automation framework developed by citrolabs) gives agents instant access to formatted documentation for any public helper. This article breaks down exactly how JSDoc comments travel from source code to runtime, following the complete pipeline implemented in `citrolabs/ego-lite`.

## Build-Time JSDoc Extraction

The heavy lifting happens before your code ever runs. The build script `scripts/extract-help-docs.mjs` parses the bundled JavaScript output and transforms JSDoc blocks into structured JSON.

### AST Parsing with Acorn

The extraction script uses **Acorn** to walk the TypeScript-compiled bundle:

```javascript
// scripts/extract-help-docs.mjs
import { parse } from 'acorn';

const ast = parse(bundleCode, {
  ecmaVersion: 'latest',
  sourceType: 'module',
  onComment: (isBlock, text, start, end) => {
    // Collect all block comments for JSDoc processing
  }
});

```

Each function declaration is matched with its preceding block comment. The `parseJSDoc` function then splits these comments into structured fields: **description**, **@param** entries, **@returns** information, and modifiers for optional, rest, or default parameters.

### Output Structure

Every extracted helper becomes a plain object with this shape:

```typescript
interface HelperDoc {
  name: string;
  signature: string;
  description: string;
  params: Array<{
    name: string;
    type: string;
    description: string;
    optional?: boolean;
    rest?: boolean;
    defaultValue?: string;
  }>;
  returns?: { type: string; description: string };
  async?: boolean;
}

```

## Embedding Documentation into the Runtime

Once extracted, the documentation must survive the bundling process. This happens in `scripts/build.mjs` through a two-step serialization technique.

### Double Stringification Technique

The `embedHelpDocs` function serializes the docs array twice:

```javascript
// scripts/build.mjs
const docs = extractHelpDocs(bundlePath);
const embeddedString = JSON.stringify(JSON.stringify(docs));

```

This double-wrapped JSON is injected into [`src/help-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/help-runtime.ts) by replacing the placeholder `__EGO_EMBEDDED_HELP_DOCS__`. The result is a static string literal baked directly into the distributed code—no filesystem reads, no network requests at runtime.

## Runtime Deserialization and Caching

The runtime side in [`src/help-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/help-runtime.ts) is intentionally minimal. It only needs to parse the embedded string and serve the results.

### Safe Parsing with Fallback

```typescript
// src/help-runtime.ts
function parseEmbeddedDocs(): HelperDoc[] {
  try {
    const raw = '__EGO_EMBEDDED_HELP_DOCS__'; // replaced at build
    return JSON.parse(JSON.parse(raw));
  } catch {
    // Placeholder not replaced (e.g., raw TS import)
    return [];
  }
}

```

### Lazy-Loaded Map Cache

`getDocsMap()` builds a `Map<string, HelperDoc>` only on first access, then caches it for subsequent calls:

```typescript
let docsMap: Map<string, HelperDoc> | undefined;

export function getDocsMap(): Map<string, HelperDoc> {
  if (!docsMap) {
    docsMap = new Map(parseEmbeddedDocs().map(d => [d.name, d]));
  }
  return docsMap;
}

```

## The help() API: Three Call Patterns

The exported `help()` function supports flexible usage patterns depending on how many helper names you provide.

### List All Available Helpers

Without arguments, `help()` returns every documented helper that exists in your `helpers` object:

```typescript
import { help, formatHelp } from "ego-browser";

// List all available helpers
const allDocs = help(helpers) as HelperDoc[];
console.log(allDocs.map(d => d.name).join(", "));
// Output: click, type, nav, scroll, wait, ...

```

### Single Helper Lookup

Pass one name to get its full documentation object:

```typescript
const navDoc = help(helpers, "nav") as HelperDoc;
console.log(formatHelp(navDoc));

```

**Example output:**

```

Navigate to a URL.
@param url string — Destination URL
@param timeout? number — Max wait time (default: 30)
@returns void

nav(url, timeout?) → void

```

### Batch Lookup

Multiple names return an array, with unknown helpers replaced by placeholder objects:

```typescript
const docs = help(helpers, "click", "type", "unknown") as HelperDoc[];
docs.forEach(d => console.log(formatHelp(d)));
// "unknown" outputs: "Unknown helper: unknown"

```

## Formatting Human-Readable Output

The `formatHelp()` function assembles the final display string by concatenating:

- **Description** (first paragraph of JSDoc)
- **@param lines** with optional/rest/default annotations
- **@returns line** (if present)
- **Function signature** generated from parsed metadata

This formatting happens at runtime—cheap string operations on already-parsed data.

## Key Files in the Pipeline

| File | Purpose |
|------|---------|
| `scripts/extract-help-docs.mjs` | Parses bundled JS with Acorn, extracts JSDoc, builds `HelperDoc` objects |
| `scripts/build.mjs` | Calls `embedHelpDocs`, double-serializes docs, injects into [`help-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/help-runtime.ts) |
| [`src/help-runtime.ts`](https://github.com/citrolabs/ego-lite/blob/main/src/help-runtime.ts) | Defines `parseEmbeddedDocs`, `getDocsMap`, `help()`, and `formatHelp()` |

According to the citrolabs/ego-lite source code, this three-file architecture cleanly separates **build-time computation** from **runtime serving**, keeping the browser bundle small and `help()` calls instantaneous.

## Summary

- **No runtime parsing**: `help()` never touches filesystem or parses ASTs at runtime
- **Build-time extraction**: Acorn walks the bundled JS in `extract-help-docs.mjs` to harvest JSDoc
- **Double serialization**: `JSON.stringify(JSON.stringify(docs))` safely embeds data as a string literal
- **Lazy caching**: `getDocsMap()` constructs and caches the lookup map on first use
- **Three API modes**: zero args (all), one arg (single), multiple args (batch)
- **Human formatting**: `formatHelp()` renders structured docs into readable output

## Frequently Asked Questions

### Does help() parse JSDoc comments every time it's called?

No. All JSDoc parsing happens at build time in `scripts/extract-help-docs.mjs`. At runtime, `help()` simply calls `JSON.parse` on the pre-serialized data and looks up entries from a cached Map.

### What happens if I call help() in a development environment before building?

The placeholder `__EGO_EMBEDDED_HELP_DOCS__` remains unmodified, causing `parseEmbeddedDocs()` to catch the parse error and return an empty array. No documentation will be available until after a proper build runs.

### How does the system handle @param tags with default values or optional markers?

The `parseJSDoc` function recognizes `?` for optional parameters, `...` for rest parameters, and `= value` syntax for defaults. These are stored as `optional`, `rest`, and `defaultValue` properties on each param object, which `formatHelp()` uses to generate signatures like `fnName(required, optional?, ...args)`.

### Can I extend help() to document my own custom helpers?

Yes, provided your helpers follow JSDoc conventions and your build process runs the same extraction pipeline. The `extractHelpDocs` function processes any function declaration with a preceding block comment, regardless of whether it's a core ego-lite helper or your own extension.